feat(spec,security): nav landing exclusivity + field-permission predicate guard (anti filter-oracle)#2630
Merged
Merged
Conversation
…cate guard Two guards completing objectui#2251 / objectui ADR-0055: spec — NavigationItemSchema rejects object items combining filters with recordId or viewName (superRefine on the union member; base schema stays extendable). Runtime precedence would silently ignore the extras — a stale recordId hijacking a configured filters slice — so the ambiguous shape is now unwritable (ADR-0053 correct-by-construction). The legacy recordId+viewName combination stays tolerated (documented). 4 new schema tests; api-surface unchanged. plugin-security — anti filter-oracle predicate guard. FieldMasker only masks RESULTS; filtering/sorting/grouping/aggregating by a hidden field still leaked its values through row presence. The middleware now rejects (403, reason: field_predicate_denied) caller queries whose where/orderBy/groupBy/having/aggregations/windowFunctions reference a non-readable field — evaluated against the caller's AST BEFORE RLS injection so RLS policies may keep referencing hidden fields. Rejection over silent dropping: removing an $and branch widens results and re-opens the oracle (Salesforce FLS errors the same way). 10 new unit tests; plugin-security suite 183/183. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018m3GX7EMKNPDZuee152EKK
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 96 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…ion layer table Layer 6 (field-level security) now states the predicate guard: caller queries filtering/sorting/grouping/aggregating by a non-readable field are rejected (403 field_predicate_denied) rather than value-leaking through row presence; RLS-injected predicates are exempt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018m3GX7EMKNPDZuee152EKK
os-zhuang
marked this pull request as ready for review
July 5, 2026 08:50
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Completes the framework half of objectstack-ai/objectui#2251 (objectui ADR-0055) — the two guards that make AI/author mistakes either unwritable or harmless.
1. spec —
ObjectNavItemlanding exclusivity (correct-by-construction)NavigationItemSchemanow rejects an object nav item combiningfilterswithrecordIdorviewName. Runtime precedence (recordId→filters→viewName) would silently ignore the extras — the failure mode being a stalerecordIdhijacking a configuredfiltersslice. Following ADR-0053's philosophy, the ambiguous state is now unrepresentable rather than precedence-resolved.superRefineon the union member (the baseObjectNavItemSchemastays.extend()-able); custom issue anchored atfilterswith the fix in the message.recordId+viewNamecombination stays tolerated (documented: viewName is ignored when recordId is set) — zero released-metadata impact;filtersitself ships in the same unreleased minor (feat(spec): ObjectNavItem.filters — declarative slices on the bare data surface #2626).api-surface.jsonunchanged.2. plugin-security — field-level predicate guard (anti filter-oracle)
The hole:
FieldMaskerstrips non-readable fields from results, but nothing sanitized predicates. Filteringsalary >= 100000(or sorting/grouping/aggregating bysalary) changes which rows return even though the column is masked — row presence is the oracle. objectui's/datasurface (URL-drivenfilter[...], objectui#2255) makes arbitrary predicates first-class, so the engine must close this independent of the client.The fix: the security middleware rejects (403
PermissionDeniedError,details.reason = 'field_predicate_denied', offending fields listed) any caller query whosewhere/orderBy/groupBy/having/aggregations(field + FILTER) /windowFunctions(field + partitionBy + over.orderBy) reference a field the caller's field permissions mark non-readable.Design decisions:
$orbranch narrows results, dropping an$andbranch widens them and re-opens the oracle. Salesforce FLS errors the same way; the message carries the prescription.owner_id) and must not be rejected. The guard's position in the middleware defines the caller/system boundary.fields) is deliberately NOT guarded — selecting a hidden field is harmless becauseFieldMaskerstrips it; only predicates leak.predicate-guard.ts(exported:assertReadableQueryFields,collectQueryFields,collectConditionFields), wired as step 2.9 of the existing middleware.Verification
@objectstack/spec: full suite 6694/6694 (62 in app.test.ts incl. 4 new); build +gen:api-surfaceclean (no surface diff)@objectstack/plugin-security: 183/183 (10 new predicate-guard cases) afterturbo buildof the dependency graphminorfor both packages, with the FROM → TO migration note for the exclusivity tighteningFollow-up (noted, not included)
A
packages/verifyfilter-oracle invariant (probe a hidden field via predicate as a low-privilege member, assert 403/no-influence — therls.ts#1994 pattern) would give this end-to-end regression coverage per app; kept out to keep this PR focused on the enforcement itself.🤖 Generated with Claude Code
https://claude.ai/code/session_018m3GX7EMKNPDZuee152EKK
Generated by Claude Code