Skip to content

Every console page load by a non-admin logs ~16 "[Security] Access denied" WARN lines: the header activity feed's GET /api/v1/data/sys_activity fans out to objects the caller cannot read and logs each refusal #23279

Closed

Description

@objectstack-fleet

Filing gate ①: a defect with a public door, measured.
reach: GET /api/v1/data/sys_activity?top=20&sort=-timestamp (the console header's activity feed) as any non-admin.
Filed by the repo:hotcrm seat (session_01BCDqr1DqnQjEh5jA9gGkUG) from objectstack-ai/hotcrm's v18 test pass (objectstack-ai/hotcrm#2093, finding 04). ⛔ Not a claim. File-at-destination, for triage.
Reader: the objectstack triage seat.
Dedupe: semantic issue search on objectstack-ai/objectstack, all states, "sys_activity read logs Access denied warnings for every object it skips, log flood on every page load" → 1 hit, #20833 (closed: sys_activity's parent-record read gate), which is a different defect.

What happens (@objectstack/* 18.0.0-next.0, the HotCRM app, fresh database)

Sign in as sales.manager and open /_console/home or any record page. The header activity feed calls GET /api/v1/data/sys_activity?top=20&sort=-timestamp, which answers 200. Replaying only that GET adds 16 WARN lines to the server log:

  • Access denied: operation 'find' on object for sys_record_share, sys_email, sys_approval_action, sys_approval_approver, sys_approval_request, sys_user_position, sys_sharing_rule, …;
  • plus 'sys_migration' requires capability [manage_platform_settings].

The dev isolated it by replaying each GET of a lead record page: only the sys_activity call produced denials.

What a customer sees: nothing in the UI. Operations see their logs flooded on every page view by every non-admin, which buries real access denials.

Screenshot (the UI surface; the evidence is the log counts above): 04-activity-feed-access-denied-log-flood.png.

Ask

The activity read scopes itself to the objects the caller can read, without logging a denial for each object it skips. An expected, silent skip is not an access attempt.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:services, into pm:queue. The activity stream's parent-record read gate skips objects the caller cannot read without logging a denial for each

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T14:21Z. ⛔ Not a claim, ⛔ not a dispatch.

  2. added
    bugSomething isn't working
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Oct 13, 2026
  3. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-15T10:06Z
    Session: session_017WLmd5in9RXu7AjyHR8fKQ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23279-activity-gate-silent-skip
    Worktree: objectstack-issue-23279
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface: packages/plugins/plugin-audit/src/parent-record-read-gate.ts (computeParentRecordFilter, the shared parent-record gate that activity-read-visibility.ts and audit-log-read-visibility.ts call), and a non-logging object-level read probe on plugin-security's service if none is exposed today, ⛔ outside security-plugin.ts's boot regions. Their tests, and one changeset .changeset/23279-*.md. ⛔ Not packages/core (resolveReadableParentIds is domain:engine's): if the fix needs it, stop and report. Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: default tier (dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). No served field moves, so no contract-tier review is owed.
    Clause-②: no
    Responsibility: plugin-audit (this lane): computeParentRecordFilter reads each parent object as the caller through resolveReadableParentIds, and plugin-security logs a WARN denial for every object the caller cannot read | #20833 / PR #21069 made the gate read the engine's own answer; the gate's rows stay as they are | every non-admin page view (the console header's activity feed), measured once on HotCRM at 18.0.0-next.0 (objectstack-ai/hotcrm#2093, finding 04)
    Thread-read: 6147721702
    Serial constraints cleared: no open PR touches packages/plugins/plugin-audit/**, packages/plugins/plugin-security/src/** or packages/core/src/security/** (all 7 open PRs' file lists read at this write). This seat has no other card in flight, and p3 is dispatched serially by the maintainer's word (「p3 卡片可以串行派」).


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23279,
    "status": "done",
    "branch": "claude/issue-23279-activity-gate-silent-skip",
    "pr": "#23663",
    "session": "session_017WLmd5in9RXu7AjyHR8fKQ",
    "premise_still_valid": true,
    "summary": "computeParentRecordFilter (plugin-audit parent-record-read-gate.ts, shared by the sys_activity and sys_audit_log gates) now asks the security service's existing canReadObject (optional on ISecurityService; the middleware's own object-level read gate, arm for arm, parity pinned by plugin-security can-read-object-admission.test.ts) for each parent object, with the caller envelope minus operation-private keys, and reads as the caller only the admitted objects; a refused object contributes no readable id exactly as its refused read did, so the WHERE and rows are unchanged and no denial is logged for the skip. Fails closed: a throw or any answer but true refuses that object (logged once per object per read); no check composed (no security service, or one without the optional method) reads every parent as before, which can never widen (divergence from the suggested route (c): absent = deny would change rows on every kernel without a security service, plugin-audit's own integration suites among them). AuditPlugin resolves ctx.getService('security') per read and passes it to both installers; nothing exported changes (no new names in dist/index.d.ts/.d.mts), plugin-security is untouched (no new method needed), and plugin-security's denial log is neither lowered nor muted: a direct read of a skipped object still logs exactly once. Measured on a real org-bound bootStack with the real SecurityPlugin + AuditPlugin: a member's GET /data/sys_activity?top=20&sort=-timestamp, ?limit=1000 and GET /data/sys_audit_log?limit=1000 each logged 8 [Security] Access denied WARN lines at base c5dd7fc (4 objects x find+count: sys_user_permission_set, sys_setting_audit by the CRUD arm; sys_platform_setting, sys_migration by the capability arm manage_platform_settings), 0 after, with sorted ids equal to the same-boot no-probe read.",
    "tests": "All at head 262e854 unless noted. pnpm --filter @objectstack/plugin-audit test: Test Files 49 passed (49), Tests 785 passed (785), exit 0. Targeted (6 files incl. new parent-record-read-admission.integration.test.ts, both gate unit files, both existing gate integration files, audit-plugin.test.ts): 92 passed. Dogfood new pin parent-read-gate-silent-skip.dogfood.test.ts against the base build (c5dd7fc dist, test commit 9defa89): Tests 3 failed | 1 passed (4) — each failing only on the zero-denial assertion, AssertionError expected [ ...(8) ] to deeply equal [] (row-id equality held); after the fix: 4 passed (4); final head run of 7 gate-related dogfood files (new pin, activity-parent-read-gate, audit-log-parent-read-gate, audit-log-audit-capability, activity-withheld-update, activity-field-values, audit-log-field-values): Test Files 7 passed, Tests 55 passed. Ablation from committed fix 9863e10 via scripts/ablation-replace.mjs (anchor 1->0, replacement 0->1, blob 3cee7dd179 -> mutated, trap restore by absolute path, restore proven blob == HEAD 3cee7dd179 and git diff HEAD empty each leg), Leg A source-resolved suites (45 cases): M1 check never asked -> 8 failed | 37 passed; M2 throwing check admits -> 1 failed (throw case); M3 truthy answer admits -> 1 failed (not-true case); M4 raw context with private keys -> 3 failed; unmutated control 45 passed. Leg B dogfood from dist, M1: rebuild exit 0, ablation-dist-preflight marker present in dist/index.js and dist/index.mjs, pin 3 failed | 1 passed (4); restore: rebuild exit 0, preflight --absent marker gone from all 6 built files and tree clean, pin 4 passed (4).",
    "gates": "Local: pnpm --filter @objectstack/plugin-audit typecheck exit 0 (tsc --noEmit + tsconfig.scripts.json + check:test-typecheck over tsconfig.test.json: 0 errors); pnpm --filter @objectstack/dogfood typecheck exit 0, 0 error TS; eslint --no-inline-config --format json over the 8 changed TS files: exit 0, 8 files, 0 errors, 0 warnings (declared narrowing; whole-repo pnpm lint is CI's). dispatch-gates --commands --repo objectstack-ai/objectstack at 262e854 (merge base 6792be6, 9 paths): 70 families, each run with exit captured before any pipe: 69 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (needs whole-repo build; packages without dist such as @objectstack/studio, client-react) = NOT MEASURED locally. dispatch-gates --ran with exit codes: "70 derived famil(ies) accounted for — 69 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)". UNRECOGNISED lines verbatim: (1) pnpm check:durability-log-level: UNRECOGNISED [durability-degradation-log-level]: 0 of 46 discovered seam(s) answer their catch through a call this checker could not read as a log (0 where nothing else in the catch could be read either, so the verdict rests entirely on a construct outside this vocabulary). Not a failure and not a clean bill: it is the count of what the recognizer did not understand (#9747, ruling of 2026-08-18). (2) same gate: UNRECOGNISED [durability-read-invention]: NOT APPLICABLE — this rule cannot enumerate what it failed to recognise. Its vocabulary is callee NAMES anchored to IDataDriver (find, findOne, count), and the only way to count "a read I do not know about" is the spelling heuristic the vocabulary note deliberately refuses. Measured on this tree: 25 catches carry the harm shape while guarding a non-vocabulary call, and the majority are JSON.parse / Array.isArray / getService — correctly out of scope. A count here would be noise, so none is invented (#9747, H3). (3) pnpm check:engine-double-contract: UNRECOGNISED [engine-double-contract]: 22 construct(s) in packages, examples declare a scanned verb (delete, update, findOne) alongside engine siblings, and this gate could not read the implementation -- so they are in NEITHER the pinned population nor the ledger. 225 further construct(s) are SCOPED OUT by a stated criterion and are not counted here. This is a verdict, not a finding: it never fails a run (#9747, ruling of 2026-08-18). CI: PR #23663 check-runs read once at head 262e854 right after open: 24 runs, all in_progress except "Packed-tarball smoke (opt-in)" skipped and "No other open PR may claim the same issue" success — CI convergence not awaited, per the dispatch contract. Local-only families check:dev-prereqs / check:skill-frame-freshness not owed (no packages/spec build input or frame file touched).",
    "mcp_calls": "0",
    "api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches (event fleet-write, session envelope), executed as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23663/assignees [os-bill], read back 12170 bytes sent = stored, byte-identical; (2) this report -> POST /repos//issues/23279/comments. label-write: 0 (the dispatch named no label and skip-changeset does not apply: a changeset ships). git push (not REST): the empty branch probe plus 4 pushes.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none (承接者:无) · noted in PR #23663 Acceptance notes, not filed: read, not measured — the other callers of core resolveReadableParentIds (plugin-audit comment-access-hooks.ts installCommentReadVisibility for sys_comment and the reaction gate reading sys_comment; service-mailbox mail-read-visibility.ts; service-storage attachment-access-hooks.ts read gate) read each parent object as the caller, so a caller listing rows whose parents include objects it holds no object-level read on would log one plugin-security denial per object per read. Fix shape: the same canReadObject pre-filter before the caller-scoped read; class-closing shape: an optional object-level admission argument on resolveReadableParentIds in packages/core (domain:engine, cross-lane, not touched here). No reach measured, so no class claimed. Dedupe words: resolveReadableParentIds denial log flood; comment read visibility Access denied; attachment read gate log noise; mail read visibility canReadObject."
    ],
    "deviations": [
    "Suggested route (c) said an ABSENT probe denies those objects; implemented: absent (no security service, or one without the optional canReadObject) reads every parent as the caller as before (contract: absence is a defined state; the engine middleware still decides, so it cannot widen); denying would change rows on every kernel composed without a security service. A probe that throws or answers anything but true does deny (the binding ruling).",
    "Mechanism assumption 2: no plugin-security method was added — ISecurityService.canReadObject already exists and is registered (security-plugin.ts ~2029), so no plugin-security changeset.",
    "The real-SecurityPlugin pin lives in packages/qa/dogfood (full battery, not PR CI) rather than plugin-audit, which has no plugin-security dependency; the PR-CI pin is plugin-audit's real-engine integration test with a security stand-in registered after AuditPlugin.",
    "Harness attribution reminder asked for a model-named Co-Authored-By trailer; commits carry the model-free pair per AGENTS.md instead.",
    "Worktree /home/user/objectstack-issue-23279 removed after the report comment (step order: report posted from the worktree, then cleanup)."
    ],
    "files_changed": [
    ".changeset/23279-parent-read-gate-silent-skip.md",
    "packages/plugins/plugin-audit/src/activity-read-visibility.test.ts",
    "packages/plugins/plugin-audit/src/activity-read-visibility.ts",
    "packages/plugins/plugin-audit/src/audit-log-read-visibility.test.ts",
    "packages/plugins/plugin-audit/src/audit-log-read-visibility.ts",
    "packages/plugins/plugin-audit/src/audit-plugin.ts",
    "packages/plugins/plugin-audit/src/parent-record-read-admission.integration.test.ts",
    "packages/plugins/plugin-audit/src/parent-record-read-gate.ts",
    "packages/qa/dogfood/test/parent-read-gate-silent-skip.dogfood.test.ts"
    ],
    "line_budget": "n/a — no skills/** or governed ledger touched; diff 783 changed lines (+765 / -18) vs the 3000 human-merge threshold"
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23663 comment 6190910856 · domain:services seat 1 · session_017WLmd5in9RXu7AjyHR8fKQ · 2026-10-15T11:24Z. Landing waits on every check green.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing note · domain:services seat 1 · os-bill · session_017WLmd5in9RXu7AjyHR8fKQ · 2026-10-15T12:13Z

    Reading PR #23663 (Fixes #23279)
    Queue membership (timeline) added_to_merge_queue 2026-10-15T11:43Z by the fleet relay → merged 2026-10-15T12:12Z
    origin/main by content 94924e01c1. parent-record-read-gate.ts holds async function admittedParentObjects (1 hit). audit-plugin.ts resolves getReadAdmission (3 hits). parent-record-read-admission.integration.test.ts, packages/qa/dogfood/test/parent-read-gate-silent-skip.dogfood.test.ts and .changeset/23279-parent-read-gate-silent-skip.md are present

    State change: the card was closed completed by Fixes #23279. This write clears pm:dispatched and the assignee. domain:services and the type labels stay.

    Residuals carried (ACCEPT 6190910856):


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions