Skip to content

[security] An RLS policy authored in Studio's package door is silently discarded — Save succeeds, no row filter is ever persisted #4302

Description

@huangyiirene

Impact

An author fills in a row-level-security policy (name, object, USING, CHECK), clicks a fully-enabled Save, gets no error, and is told the permission set saved. No row filter is ever persisted.

Anyone reading the Studio surface afterwards sees a permission set that looks configured. The records the policy was meant to scope are unprotected, and the failure mode is silence — which is worse than a refusal, because the admin now has positive (false) evidence that a control is in place.

rowLevelSecurity is not the only casualty: tab visibility and delegated-admin scope are reverted the same way.

Reproduction (observed on the wire)

  1. Studio → Access pillar for a writable package → a permission set → Row-Level Security → Add policy.
  2. Fill Policy name, Object, USING (and CHECK).
  3. Save.

The PUT body is:

{"name":"qa_st2_rls","label":"QA ST2 RLS","objects":{},"fields":{}}

— no rowLevelSecurity key at all. The server answers 200, and a GET /api/v1/meta/permission/<n> after publish carries no policy.

Control that isolates the door: the environment door (the routed editor at /apps/setup/metadata/permission/<n>, no packageId) does send rowLevelSecurity in the body. So this is specific to the package path, not to the editor's data collection — the editor gathers the policy correctly and the package save path drops it.

Root cause (located)

PermissionMatrixEditor.doSave() routes through mergePermissionSlice() whenever packageId is set, and permission-slice.ts:117-124 returns:

{...base, name, label, isDefault, objects, fields}

It carries only those five keys from edited and takes every advanced facet from base — a fresh server read. So rowLevelSecurity, tab visibility and delegated-admin scope are silently reverted to whatever the server already had.

The slice is a whitelist that has drifted behind the facets the editor can author. That is the general shape worth fixing, not just the one missing key: any facet added to the editor in future is silently dropped on this door until someone remembers to widen the whitelist.

Acceptance criteria

  • A policy authored through the package door is persisted — asserted by reading GET /api/v1/meta/permission/<n> back after publish and finding the policy, not by asserting the save returned 200 (a 200 is exactly what this defect already produces).
  • The same assertion covers the other facets the whitelist drops: tab visibility and delegated-admin scope.
  • The environment door's behaviour is unchanged — assert it as a guard so the fix does not regress the path that already works.
  • Consider a structural guard rather than five more keys: if the slice must stay a whitelist, something should fail loudly when the editor can author a facet the slice does not carry. A silent drop is what made this expensive.

Companion defect, filed separately

The console also seeds a new policy with the retired priority key (PermissionAdvancedFacets.tsx:368), which the environment door rejects with a located 422 (rowLevelSecurity[].priority was removed in @objectstack/spec 17.0.0, #3896 security audit). That one is being filed from the same QA run as the public companion.

Taken together: an RLS policy cannot currently be saved from the console on either door — the package door drops it silently (this card), and the environment door refuses it loudly over the retired key (the companion). This card is the dangerous half; a loud refusal at least tells the truth.

Provenance & disclosure

Held back from the public run card objectstack-ai/objectstack#7695 pending disclosure handling; the private write-up is docs/qa/platform-checklist/FOLLOW-UPS.md §1a entry D13 (objectstack branch claude/platform-test-checklist-ocwugl @ 1ce13eb). Filed publicly under the maintainer's standing instruction of 2026-08-11 (verbatim): 「有问题就立卡片」.

Source

QA run objectstack-ai/objectstack#7695 (studio-authoring.expression-editors, finding B; framework 92f26f75, console bundle 09987b680), private write-up FOLLOW-UPS.md §1a D13.

Activity

  1. self-assigned this
    on Aug 13, 2026
  2. yinlianghui commented on Aug 13, 2026

    @yinlianghui
    Collaborator

    CLAIM — session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4302-package-door-slice. Dispatching a dev agent now.

    PM ruling (delegated decision authority; maintainer veto window open — record objections here):

    1. Fix shape: invert the drift, don't append to it. mergePermissionSlice() carries every facet the editor can author from edited; base supplies only what the editor cannot author. Before widening, measure two things: (a) WHY the slice exists (blame / ADR-0086 package-draft semantics) — if the hold-back was deliberate for a reason that still stands, stop and report; (b) the server's package-door acceptance of the dropped facets (objectstack protocol, READ-ONLY sibling checkout) — if the server rejects rowLevelSecurity on the package door, the premise inverts into a server card: stop and report rather than shipping a client change that converts a silent drop into a 422.
    2. Structural guard is a test, not UI copy: enumerate the editor's authorable facet keys against the slice's carried keys and fail on drift, so the next facet added to the editor cannot silently drop on this door. No runtime message, no i18n rows (keeps this card disjoint from Field inspector: Save is not gated on CEL errors — a parse-fault formula saves and publishes as the live field definition #4306's possible i18n touch).
    3. The companion seed defect (retired priority at PermissionAdvancedFacets.tsx:368): fold the minimal seed fix ONLY if measured necessary for the acceptance criterion (package-door persistence read-back fails over the seeded key); otherwise leave it to its companion card. Search for the companion card first and reference it either way.
    4. Tests pin the wire, not the toast: the PUT body carries the facets (the card's own evidence was the body missing the key), plus the environment door unchanged as a guard pin.

    Mutual exclusion / sequencing: gate worktree creation on PR #4525 (issue #4518) being visible in origin/main — it touches PermissionMatrixEditor.tsx and is armed on green CI, so the wait is minutes. Do not modify #4525's artifact-tier code beyond what doSave needs. Disjoint from #4306 (inspectors/ — read-only for this card), #4490 (plugin-grid), #4040 (plugin-dashboard).


    Generated by Claude Code


    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

bugSomething isn't workingpm:queue

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions