fix(auth): reject reserved role-permission names as database names + complete cluster_user handling#1913
fix(auth): reject reserved role-permission names as database names + complete cluster_user handling#1913kylebernhardy wants to merge 4 commits into
Conversation
A role's `permission` object keys per-database permission blocks by database name alongside named flags (super_user, cluster_user, structure_user, operations, _expandedOperations). A database named after one of those flags collides with it — the role parser cannot tell a `super_user` flag from a `super_user` database's permissions — producing undefined behavior, including a privilege-escalation path: a component declaring `@table(database: "super_user")` plus a roles.yaml granting permissions on that "database" makes `permission.super_user` a truthy object, which the super-user check reads as granted. Reject the reserved names as database identifiers on both creation surfaces: - the operations API (`create_schema` / `create_table`), via DB_NAME_CONSTRAINTS - schema authoring (`schema.graphql` `@table(database:)` / programmatic `table()`), which registers databases through resources/databases.ts and bypasses the operations validation The reserved set is defined once in hdbTerms as RESERVED_DATABASE_NAMES and also replaces the incomplete `USERS_NOT_DBS` list in roles.ts (which was missing cluster_user / operations, so a roles.yaml using those flags mis-parsed them as database names). Fixes harper#1016. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
There was a problem hiding this comment.
Code Review
This pull request introduces validation to prevent the use of reserved database names (such as super_user, cluster_user, structure_user, operations, and _expandedOperations) that collide with role permission flags. This validation is applied to both the operations API and the schema-authoring path. A new test file is added to verify these constraints. The review feedback points out a style guide violation in the test file where node:assert/strict is imported instead of the plain node:assert module, which is lint-rejected in this repository.
|
Reviewed; no blockers found. |
Follow-on to the reserved-name fix, closing three related pre-existing gaps in how `cluster_user` is treated as a role permission flag (surfaced in review): - Role validation derived its boolean-flag list from ROLE_TYPES_ENUM, which held only `super_user`. A non-boolean `cluster_user` therefore skipped the boolean check and fell through to database validation, reporting a misleading "database 'cluster_user' does not exist" instead of the boolean-type error the SU_CU_ROLE_BOOLEAN_ERROR name already implies. Added CLUSTER_USER to ROLE_TYPES_ENUM. - translateRolePermissions preserved super_user / structure_user / operations but dropped `cluster_user`; now preserved alongside them for consistency (cluster_user is otherwise unread at runtime today, so no behavior change — just a faithful translation). - Removed the dead SU_CU_ROLE_COMBINED_ERROR from the test-error helper: it has no production counterpart and no test uses it (the su+cu rejection is covered by SU_CU_ROLE_NO_PERMS_ALLOWED). Refs harper#1016. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
|
@kriszyp — a release-targeting question on this one. The core fix rejects reserved role-permission names ( We're planning to land this on main / v5.2. Given the escalation angle, do you think it also warrants a v5.1 patch (via the (This PR also folds in three related pre-existing Comment generated by kAIle (Claude Opus 4.8) |
Comment generated by kAIle (Claude Opus 4.8)
… is authoring-only Deep-review follow-up (comments only): document that `access` is intentionally outside RESERVED_DATABASE_NAMES (stripped pre-persistence) and that the reserved-name check lives on the authoring path — not database()/makeTable — so a pre-existing reserved-name database still loads and can be dropped to remediate. Comment generated by kAIle (Claude Opus 4.8)
Summary
A role's
permissionobject mixes named flags —super_user,cluster_user,structure_user,operations,_expandedOperations— with per-database permission blocks keyed by database name. A database named after one of those flags collides with it: the role parser cannot tell asuper_userflag from asuper_userdatabase's permissions.Purpose
Fixes #1016. Beyond "undefined behavior," there's a concrete privilege-escalation path (surfaced in review): a component shipping
@table(database: "super_user")plus aroles.yamlgranting permissions on that "database" makespermission.super_usera truthy object, and the super-user check (!!permission.super_user) then reads as granted.The fix
Reject the reserved names as database identifiers on both creation surfaces, from one shared source of truth (
RESERVED_DATABASE_NAMESinhdbTerms.ts):create_schema/create_table) —.invalid(...)onDB_NAME_CONSTRAINTSindataLayer/schema.ts.schema.graphql@table(database:)/ programmatictable()) — a check inresources/databases.ts::table(), which registers databases and bypasses the operations validation. This is the path the escalation uses.It also folds the incomplete
USERS_NOT_DBSinresources/roles.ts(was['super_user', 'structure_user']— missingcluster_user/operations) into the same shared constant, fixing a latent bug where aroles.yamlusing those flags mis-parsed them as database names.Where to look
table()throws aClientError(matching the existing schema-validation throws ingraphql.ts), so it's load-safe: an invalid schema fails to load rather than bricking the instance._expandedOperationsis included (an internal,_-prefixed key) becauseschemaRegexpermits leading underscores and a same-named database would overwrite the cached operationsSetat translation time (aTypeErrorcrash). Confirm the set matchesUserRoleNamedPermissionsinsecurity/user.ts..invalid()matches exact strings; database names are currently case-sensitive (case-insensitive handling is separate issue Case-insensitive schema and table name handling #1028), so exact match is correct.Tests
New
unitTests/dataLayer/reservedSchemaNames.test.js— each reserved name × three surfaces (create_schema,create_table,table()), plus a non-reserved control. 15 of 16 fail onorigin/main(verified by reverting the source files). Existingresources/databases,query,crud,defineTable-registration,permissions,permissionsTranslatorsuites pass unchanged.Also fixed (related
cluster_usergaps, second commit)Three pre-existing gaps in how
cluster_useris handled as a role flag, surfaced in review:role_validation.tsmisleading error — its boolean-flag list came fromROLE_TYPES_ENUM(onlysuper_user), so a non-booleancluster_userskipped the boolean check and fell through to database validation, reportingdatabase 'cluster_user' does not existinstead of the boolean-type error theSU_CU_ROLE_BOOLEAN_ERRORname already implies. Fixed by addingCLUSTER_USERtoROLE_TYPES_ENUM. Guarded by two newrole_validationtests (both fail onorigin/main).permissionsTranslator.jsdroppedcluster_user— preservedsuper_user/structure_user/operationsbut notcluster_user. Now preserved alongside them. Note:cluster_useris read nowhere at runtime today (only a type def + an impersonation-clear write), so this is a consistency/hygiene fix with no behavioral effect — flagging that explicitly.SU_CU_ROLE_COMBINED_ERRORremoved from the test-error helper — no production counterpart, no test uses it (the su+cu rejection is covered bySU_CU_ROLE_NO_PERMS_ALLOWED).Upgrade / migration note
Deep-review (auth + data-integrity domain passes) confirmed the fix is startup-safe: a reserved-name database that already exists on disk still loads (schema replay goes through
makeTable, not the guardedtable()), so its data stays accessible, and it can be dropped to remediate (the drop path goes throughdatabase(), also unguarded — which is why the check is deliberately on the authoring path only).But such a database's component will fail to load after upgrade — the
@table(database:)authoring call now throws, so that Resource becomes anErrorResource(logged, isolated to that component; not fatal to the instance). The highest-risk name isoperations, which only became a permission flag recently (95aa06fd1), so pre-existingoperationsdatabases are more plausible thansuper_userones. Release notes should tell operators to rename any pre-existing database named after a reserved flag before upgrading.Note
Security-adjacent (the escalation path). Milestone is v5.2; could be a v5.1 patch given the security angle — flagging for a decision rather than labeling.
Generated by an LLM (Claude Opus 4.8).