Skip to content

feat(spec,security): nav landing exclusivity + field-permission predicate guard (anti filter-oracle) - #2630

Merged
os-zhuang merged 2 commits into
mainfrom
claude/spec-filters-exclusivity
Jul 5, 2026
Merged

feat(spec,security): nav landing exclusivity + field-permission predicate guard (anti filter-oracle)#2630
os-zhuang merged 2 commits into
mainfrom
claude/spec-filters-exclusivity

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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 — ObjectNavItem landing exclusivity (correct-by-construction)

NavigationItemSchema now rejects an object nav item combining filters with recordId or viewName. Runtime precedence (recordIdfiltersviewName) would silently ignore the extras — the failure mode being a stale recordId hijacking a configured filters slice. Following ADR-0053's philosophy, the ambiguous state is now unrepresentable rather than precedence-resolved.

  • Implemented as superRefine on the union member (the base ObjectNavItemSchema stays .extend()-able); custom issue anchored at filters with the fix in the message.
  • The legacy recordId + viewName combination stays tolerated (documented: viewName is ignored when recordId is set) — zero released-metadata impact; filters itself ships in the same unreleased minor (feat(spec): ObjectNavItem.filters — declarative slices on the bare data surface #2626).
  • 4 new schema tests (accept filters-only, reject filters+viewName, reject filters+recordId, tolerate legacy combo). api-surface.json unchanged.

2. plugin-security — field-level predicate guard (anti filter-oracle)

The hole: FieldMasker strips non-readable fields from results, but nothing sanitized predicates. Filtering salary >= 100000 (or sorting/grouping/aggregating by salary) changes which rows return even though the column is masked — row presence is the oracle. objectui's /data surface (URL-driven filter[...], 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 whose where / orderBy / groupBy / having / aggregations (field + FILTER) / windowFunctions (field + partitionBy + over.orderBy) reference a field the caller's field permissions mark non-readable.

Design decisions:

  • Reject, never silently rewrite — dropping an $or branch narrows results, dropping an $and branch widens them and re-opens the oracle. Salesforce FLS errors the same way; the message carries the prescription.
  • Runs against the caller's AST, before RLS injection — RLS policies legitimately reference fields the caller can't read (e.g. owner_id) and must not be rejected. The guard's position in the middleware defines the caller/system boundary.
  • Projection (fields) is deliberately NOT guarded — selecting a hidden field is harmless because FieldMasker strips it; only predicates leak.
  • Pure logic in 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-surface clean (no surface diff)
  • @objectstack/plugin-security: 183/183 (10 new predicate-guard cases) after turbo build of the dependency graph
  • Changeset: minor for both packages, with the FROM → TO migration note for the exclusivity tightening

Follow-up (noted, not included)

A packages/verify filter-oracle invariant (probe a hidden field via predicate as a low-privilege member, assert 403/no-influence — the rls.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

…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
@vercel

vercelBot commented Jul 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 5, 2026 8:40am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests protocol:ui tooling size/m labels Jul 5, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-security, @objectstack/spec.

96 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cloud-artifact-api.mdx(via packages/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/getting-started/cli.mdx(via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/permissions/access-recipes.mdx(via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx(via packages/plugins/plugin-security, packages/spec)
  • content/docs/permissions/permission-sets.mdx(via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/profiles.mdx(via @objectstack/spec)
  • content/docs/permissions/roles.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/role-based-interfaces.mdx(via packages/plugins/plugin-security)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…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
os-zhuang marked this pull request as ready for review July 5, 2026 08:50
@os-zhuang
os-zhuang merged commit 3962023 into mainJul 5, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/spec-filters-exclusivity branch July 5, 2026 08:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:uisize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude