Skip to content

access: changing an object's sharing model to public_read_write after it was first published leaves members unable to edit records they can read — the #8023 write-floor exemption does not follow the change (detail withheld pending maintainer) #26596

Closed

Description

@objectstack-fleet

Filing gate: ① product defect, reproduced twice, landing site named (maintainer direct dispatch, authorizing words quoted in the audit comment).

Symptom (customer-visible)

An admin changes an existing object's org-wide default to Public read/write (Studio › Access › org-wide defaults, or a metadata save) and publishes. A member whose permission set grants Read + Edit on that object can now read another user's record, but editing it is refused with the record-level sentence "You do not have access to this record…".

The same object declared public_read_write from its first publish lets the same member edit (2xx). The only difference between the two objects is whether the OWD was changed after the first publish.

This contradicts the ruling #8023 landed in PR #8072: a public_read_write OWD opens row-level updates, not just the creator's.

  • Fail-closed: this is over-refusal, and nothing is exposed.
  • The step-by-step reproduction is withheld from this card, per the maintainer's instruction for authorization defects. It goes to the implementer privately.

Measured

  • Repo 3fcafcd3, rerun on origin/main 898703f9. No commit to plugin-security or plugin-sharing lies in between.
  • 2026-10-27: two independent reproductions on a fresh probe package, plus one control object declared public_read_write from birth (2xx). One reproduction used an object the member had never touched before the change.
  • The Studio QA Access card reproduced it independently on its own server.

Landing site (suspected — the implementer verifies)

  • packages/plugins/plugin-security/src/security-plugin.ts: the per-object posture (owdOpensRowWrites, written by owdDeclaresOpenRowWrites) appears not to be re-derived when a published object's sharingModel changes.
  • Reads (the sharing service) do pick the change up, so it is the security plugin's posture/collection path, not the OWD itself.
  • The existing pin packages/qa/dogfood/test/owd-public-read-write-write-floor.dogfood.test.ts only boots objects with their final OWD, so it cannot see a change at runtime.

Acceptance

Filing facts

Activity

  1. added
    bugSomething isn't working
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Oct 27, 2026
  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (maintainer direct dispatch)
    Session: session_01J8VVAkZns5rNnK2rtszzbQ
    Account: huangyiirene
    Branch: claude/issue-26596-owd-change-write-floor
    Worktree: objectstack-issue-26596
    Domain: domain:server
    Seat: domain:server#1 (direct-dispatch session, not a lane seat)
    File surface: packages/plugins/plugin-security/src/**, packages/qa/dogfood/test/** (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: xhigh (security)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: plugin-security's per-object posture keeps the creator-only update floor after a published OWD change | none — #8072's exemption covers only an OWD declared from the first publish | any member of an org whose admin opens an existing object to public_read_write; fail-closed (over-refusal)
    Thread-read: none
    Premise: holds (reproduced twice 2026-10-27 on a server built from 3fcafcd; no plugin-security/plugin-sharing commit between 3fcafcd and origin/main 898703f)
    Serial constraints cleared: open PR #26337 (ADR-0142 phase 2, ownership queues) may touch plugin-security — rebase over it if it lands first; no other in-flight claim on this surface
    Direct-dispatch authority — maintainer, this session, 2026-10-27, verbatim: 「3 个 AUTH 类问题(复现细节都不公开);你可以直接派发处理」
    Reproduction steps stay off GitHub per that instruction; they go to the implementer in the private dispatch prompt.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Fold: this card is now the chain head of a folded family dispatch with #26698 (same root cause; that card's details are withheld pending maintainer).

    • The branch stays claude/issue-26596-owd-change-write-floor.
    • One PR carries Fixes #26596 and Fixes #26698, one commit per card.
    • File surface is extended to packages/metadata/src/**, the subscription seam only.
    • Session: session_01J8VVAkZns5rNnK2rtszzbQ

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 26596,
    "folded_with": [
    26698
    ],
    "status": "done",
    "branch": "claude/issue-26596-owd-change-write-floor",
    "pr": "#26878",
    "session": "session_01J8VVAkZns5rNnK2rtszzbQ",
    "premise_still_valid": true,
    "summary": "plugin-security's metadata-derived caches (objectSecurityMetaCache, fieldNamesCache and its datetime/number siblings, tenancyDisabledCache, cbpRelCache, plus the epoch.bump('metadata') memo retirement) now clear on the protocol's post-persistence onMetadataMutation (non-draft; every authoring door; cluster peers replay into the same listeners after convergence) and on the metadata:reloaded kernel hook, via the new metadata-change-invalidation.ts; the dead md.watch('') probe is gone and the clearing body moved verbatim into invalidateMetadataDerivedCaches(). Landing site is plugin-security only: measurement showed the metadata service (NodeMetadataManager) is not the producer for runtime authoring, so packages/metadata/src was not touched. HTTP dogfood pin covers five runtime declaration changes; red 5/6 on the pre-fix build, 6/6 after. One commit per card: 9864bb9 (#26596) and 8dcb3e2 (#26698). PR body and this comment carry no symptom detail (maintainer disclosure rule); the private detail is in the final report to the PM only.",
    "hypotheses": {
    "H1": "CONFIRMED: owdOpensRowWrites is read from objectSecurityMetaCache, populated at first use and never cleared; the pin case for an OWD change goes green once the subscription fires, and red again when only the protocol listener is ablated.",
    "H2": "CONFIRMED and WIDER: notifyWatchersLocal keys by exact type, so subscribe('
    ') never fires; but measured, even exact-type subscribe('object') / watchService('object') fire 0 times for two runtime PUT /meta/object saves, while protocol onMetadataMutation fired 2. Runtime authoring goes protocol saveMetaItem then applyRegistryWriteThrough, and ObjectQL mirrors the object into the metadata service with registerInMemory / notify:false. The PM-suggested route (subscribe through MetadataManager's API) would not have fixed either card.",
    "H3": "onMetadataMutation receives cluster replays: applyRemoteMetadataMutation calls notifyMutationListenersLocal after the peer's own registry convergence; already pinned by packages/metadata-protocol/src/protocol.cluster-mutation-fanout.test.ts ('the peer's onMetadataMutation listeners receive the remote event, once, after convergence'). Not re-measured; stated in the PR body.",
    "H4": "plugin-security is the ONLY probe of a metadata-service watch in packages/** (git grep for '.watch(' / md?.watch / watch === 'function'). Other hits are live and unrelated: MetadataRepository.watch in metadata-core / metadata-fs / metadata-protocol / MetadataManager's repo loop, chokidar in cli dev and metadata-fs, the Redis client in service-cluster-redis. Nothing else fixed."
    },
    "tests": "Pin packages/qa/dogfood/test/security-verdict-follows-runtime-metadata-change.dogfood.test.ts: pre-fix (base 4373103, closure built from it) 'Tests 5 failed | 1 passed (6)' VERDICT command-exit 1; post-fix 'Tests 6 passed (6)'; post-merge with origin/main 3c2187f (closure rebuilt, 67/67 tasks) pin + owd-public-read-write-write-floor 'Test Files 2 passed (2) Tests 16 passed (16)'. Ablation (protocol listener disabled, metadata:reloaded hook kept): ablation-replace anchor x1 to x0, blob 677b3111 to df2e7be4; plugin-security build exit 0; ablation-dist-preflight marker present in dist/index.js + dist/index.mjs; pin 'Tests 5 failed | 1 passed (6)'; restore blob == HEAD 677b3111, git diff HEAD empty; rebuild exit 0; preflight --absent 'marker absent from all 6 built files'; pin 'Tests 6 passed (6)'. Final head 8dcb3e2 (tree f87a2d44, identical to the tested merge head 8b4f335): plugin-security typecheck green (check:test-typecheck OK, 0 errors), dogfood typecheck green, 'pnpm --filter @objectstack/plugin-security test' -> 'Test Files 232 passed (232) Tests 5040 passed | 45 skipped (5085)'. Earlier full runs: one red on everyone-rederivation-on-permission-mutation.test.ts (it counted every protocol listener; now reads the bootstrap's own) and one load run with 2 x 5000 ms timeouts (delegated-organization-membership, rls-stored-list-ordering-fails-closed) that passed in isolation and in the final full run. Seam probe (throwaway file, deleted): 'NodeMetadataManager has watch: undefined', seen {subscribeObject:0, subscribeStar:0, watchServiceObject:0, watchServiceStar:0, protocolMutation:2}.",
    "gates": "Local, on head 8dcb3e2: dispatch-gates --commands --repo objectstack-ai/objectstack (no paths; change set from merge base 3c2187f, 9 paths, +720/-62) derived 66 commands; all 66 run, each exit captured before any pipe; 63 exit 0 first time; 3 exit 3 (PREREQUISITE NOT MET) re-run to a reading: check-plugin-teardown-shape --self-test (fetched pinned commit 621a487 at depth 1) exit 0, 48 cases; check:dual-build-cjs-loads (built 7 packages lacking dist, turbo exit 0) exit 0; check:type-check-debt under the verify lock exit 0 '0 raw tsc error(s)'. 'dispatch-gates --ran': '66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN' exit 0. eslint --no-inline-config on the 8 changed TS files exit 0. CI on PR #26878 at report time: 21 check-runs, 2 skipped (18.x upgrade harness, Packed-tarball smoke: opt-in), 19 in_progress including Type Check (workspace / source / consumer / debt ledger), Lint (ESLint / repo & tooling / CI, release & engine / docs & code / PM dispatch-gates / scoped x3), Check Changeset, PR-body guards, Governed Surface Queue Guard, Spec property liveness, Check PR Size; Test Core / Build Core not yet listed. Not waited on (CI convergence is the PM's).",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm (Clause-②: no)",
    "mcp_calls": "0",
    "api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create -> relay POST /repos/objectstack-ai/objectstack/pulls (#26878, draft) + POST /repos//issues/26878/assignees [huangyiirene], read-back 10414/10414 bytes identical; (2) comment -> relay POST /repos//issues/26596/comments (this report). git pushes (not REST): empty-branch probe, WIP pushes, one --force-with-lease=claude/issue-26596-owd-change-write-floor:8b4f335d re-lay.",
    "files_changed": [
    ".changeset/26596-security-cache-invalidation.md",
    "packages/plugins/plugin-security/src/metadata-change-invalidation.ts",
    "packages/plugins/plugin-security/src/metadata-change-invalidation.test.ts",
    "packages/plugins/plugin-security/src/security-plugin.ts",
    "packages/plugins/plugin-security/src/security-plugin-cbp-relation.ts",
    "packages/plugins/plugin-security/src/security-plugin.test.ts",
    "packages/plugins/plugin-security/src/controlled-by-parent-reference-alias.test.ts",
    "packages/plugins/plugin-security/src/everyone-rederivation-on-permission-mutation.test.ts",
    "packages/qa/dogfood/test/security-verdict-follows-runtime-metadata-change.dogfood.test.ts"
    ],
    "line_budget": "9 files, +720 / -62 = 782 changed lines vs merge base 3c2187f (human-merge threshold 3000: under); no skills/** or governed surface touched.",
    "deviations": [
    "dispatch-gates was run WITHOUT path arguments (the role file's rule: derive the change set from the merge base, never a hand-built list); the dispatch said 'YOUR_CHANGED_PATHS'. Conflict noted, role file followed.",
    "File surface: dispatch named packages/metadata/src/** for the subscription seam; nothing there was changed (measurement put the seam in the protocol). Touched only plugin-security/src, packages/qa/dogfood/test and .changeset.",
    "History re-laid once: 5 WIP commits + a merge of origin/main were replaced by two card commits on 3c2187f (9864bb9 #26596, 8dcb3e2 #26698), tree-identical to the tested head (f87a2d44), pushed with --force-with-lease=claude/issue-26596-owd-change-write-floor:8b4f335d before the PR existed (all five AGENTS.md conditions held).",
    "PR footer uses the AGENTS.md session-URL form, not the harness reminder form (CLAUDE.md -> AGENTS.md outranks the reminder).",
    "The GitHub copy of this report omits private_detail (maintainer disclosure rule); the final message to the PM carries it.",
    "A pid file was written to /post-merge.pid (filesystem root) by a shell-variable scoping slip; removal was refused by the sandbox safety check. It holds only the text 18958. Please delete it by hand.",
    "Fetched commit 621a487 at depth 1 into the shared object store so check-plugin-teardown-shape --self-test could read its pinned fixture.",
    "test:repo (vitest repo project) not run locally: the diff touches none of its tests; CI Test Core owns it. pnpm lint not run (CI-owned); eslint on the changed files run instead.",
    "New dogfood file: 24.9 s file wall (tests 6.65 s) on a shared box, over the 10 s file line like every stack-booting dogfood file; no test-budget class fits, none declared."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed — IMPORTANT for the spec lane: IMetadataService.watch? (packages/spec/src/contracts/metadata-service.ts) is declared, implemented by no shipped metadata service, and after this PR consumed by nobody; it is exactly the optional member whose silent absence produced this defect. Declared surface with zero pull: PR acceptance note only (not a class a/b/c filing: optional, no contract violated, not authorable metadata). Dedupe words: IMetadataService watch optional member unimplemented, metadata service watch retire, MetadataManager watchService.",
    "carrier: none · noted, not filed — metadata-service watcher events (dev FileSystemRepository watch and its metadata.changed cluster replay) are not subscribed: no runtime object registration announces there (all notify:false), and a clear on those events would land before ObjectQL's async registry refresh for the same event. Not reproduced. Dedupe words: security cache FileSystemRepository watch, metadata.changed replay security invalidation.",
    "carrier: none · noted, not filed — getObjectFieldNames awaits loadObjectFieldNames before fieldNamesCache.set, a microtask window in which a mutation's clear could be overtaken by a stale set. Theoretical, not exercised, pre-existing."
    ]
    }


    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

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions