Skip to content

data: a lookup accepts an id that does not exist in the referenced object — including the RBAC permission-set link tables #4441

Description

@baozhoutao

Found while verifying the 17.0.0-rc.1 checklist on #3909 (section A, the export-axis anchor gate). Verified on main @ 1ee48bc60, showcase under os serve --dev, admin session.

A lookup field that declares reference: '<object>' accepts an id that does not exist in the referenced object, on both platform and application objects:

# RBAC link table — permission_set_id is required + reference: 'sys_permission_set'
POST /api/v1/data/sys_position_permission_set
     {"position_id":"position_msa65wsxuij6auhn","permission_set_id":"ps_does_not_exist_at_all"}
→ 200, row created

# ordinary application object — project is required + reference: 'showcase_project'
POST /api/v1/data/showcase_task
     {"title":"…","project":"proj_does_not_exist","status":"backlog"}
→ 200, row created

The field metadata is unambiguous about the constraint:

"permission_set_id": {"type":"lookup","required":true,"reference":"sys_permission_set","deleteBehavior":"set_null", }

deleteBehavior: 'set_null' shows the platform does reason about this edge on the delete side; the insert side never checks.

Why this one is worth more than the usual referential-integrity nit

The RBAC link tables are the sharp case. sys_position_permission_set is how a position is granted a permission set, and a dangling row there is a security-surface record that resolves to nothing. It reads back fine (GET 200), it appears in listings, and an admin auditing "what does the everyone anchor carry?" sees a binding whose target cannot be inspected. Worse, the audience-anchor gate (ADR-0090 D5/D9) has to resolve the set to judge its privilege — a binding whose set does not resolve is a gate input that cannot be evaluated. I hit this by accident: a shell variable expanded to the literal string FAIL, and the platform stored permission_set_id: "FAIL" against the everyone position without complaint.

This also sits directly against the posture this train shipped everywhere else: #4240/#4303/#4315 refuse a field name that does not exist, #4209 refuses a filter the server cannot apply, #4200 refuses a non-AST $filter, ADR-0113 (#4031) made required a real write contract. A reference that names an object is the same kind of declared constraint — and it is the one that still passes anything.

(Related but distinct: seeds already count and report dropped reference fields, #3932 — so the seed path takes reference resolution seriously while the REST write path does not.)

Expected

An insert/update writing a lookup value that resolves to no row in the referenced object is refused with a 400 in the declared fields[] envelope naming the field and the unresolvable id — the same shape required violations already produce. If a deployment genuinely needs dangling references (import staging, eventual consistency), that should be the opt-in, not the default.

Part of the #3909 rc.0/rc.1 verification (section A).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions