Skip to content

feat(lint): warn when a master-detail child has no object-level CRUD grant (#2700)#2721

Merged
os-zhuang merged 1 commit into
mainfrom
feat/lint-master-detail-ungranted-crud
Jul 10, 2026
Merged

feat(lint): warn when a master-detail child has no object-level CRUD grant (#2700)#2721
os-zhuang merged 1 commit into
mainfrom
feat/lint-master-detail-ungranted-crud

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #2700.

Problem

A downstream app (os-tianshun-mtc) hit a silent DX trap: a master-detail child object was missing object-level CRUD in its permission sets, so role-bound non-admin users couldn't read/write the subtable — surfacing only as "can't fill in / can't submit the subtable" with no clear error. (downstream: steedos-labs/os-tianshun-mtc#43)

The root cause is two independent permission gates:

  1. Gate ① object-level CRUD (checkObjectPermission) — a permission set that doesn't list the object returns falsePermissionDeniedError (403).
  2. Gate ② record-level RLS / controlled_by_parent — decides which records, only after gate ① passes.

ADR-0055's controlled_by_parent lives entirely in gate ② (read injects masterFK IN (accessible masters), write requires master edit) and never derives gate ①. So "child access derives from the master" is a record-level guarantee only; object-level CRUD must still be granted per-object. Missing it, the cbp derivation never even runs — gate ① blocks first. This is deliberate secure-by-default (Salesforce parity); CRUD auto-inheritance is intentionally not adopted.

This "likely misconfiguration" is statically detectable but was flagged nowhere at compile time.

Change

New rule security-master-detail-ungranted (advisory warning, does not gate the build) in the ADR-0090 D7 security-posture linter (validateSecurityPosture).

Why the D7 linter, not data-model-rules.ts (the issue's first suggestion)

The rule cross-references objects × permissions. lintDataModel(objects) only receives objects (and feeds the metadata-eval scorer), whereas validateSecurityPosture(stack) already receives the full stack and is wired into both os lint and os compile — where only error gates, so a warning prints without blocking. This is the issue's stated alternative.

Verification

  • @objectstack/lint: 173 passed (13 new cases: incident shape, all 6 grant bits, '*' wildcard, array field form, system exemption, zero-permission-sets, path/message assertions).
  • @objectstack/cli (direct consumer): 334 passed — no snapshot/count drift.
  • tsc --noEmit: clean.
  • Ran against the built dist with an incident-shaped stack → 1 warning; granting the child → silent.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 10, 2026 3:40am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Jul 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

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

  • content/docs/permissions/authorization.mdx (via @objectstack/lint)

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.

…grant (#2700)

New ADR-0090 D7 security-posture rule `security-master-detail-ungranted`
(advisory warning; does not gate the build). A master-detail detail object
derives its RECORD-level access from the master (ADR-0055 controlled_by_parent,
gate 2), but object-level CRUD is a SEPARATE gate 1 (checkObjectPermission)
that is never derived — a permission set that grants the parent but forgets the
child denies role-bound non-admin users a 403 before the parent-derived access
is ever consulted, the silent "can't submit the subtable" trap (framework#2700,
downstream os-tianshun-mtc#43).

Flags a non-system detail (has a master_detail field) that NO authored
permission set grants (explicit entry or '*' wildcard). Stays silent when the
package authors no permission sets, when a package-declared '*' wildcard grant
covers every object, or for sys_*/isSystem objects — keeping false positives
near zero. The per-set gap and CRUD auto-inheritance are deliberately out of
scope (secure-by-default, Salesforce parity).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@os-zhuang
os-zhuang force-pushed the feat/lint-master-detail-ungranted-crud branch from 7a18d14 to feba25e Compare July 10, 2026 03:31
@os-zhuang
os-zhuang merged commit 2f3581f into main Jul 10, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the feat/lint-master-detail-ungranted-crud branch July 10, 2026 03:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[平台/DX] 编译期 lint:master-detail 子对象在所有权限集中都无对象级 CRUD 授权时告警

1 participant