roles phase 3: dev tools are a row, not a hardcoded name
rolePermitsDevTools() was `role === 'Developer'` — one line gating two real grants, a Postgres login role with CREATEDB and a rootless Docker daemon. A role the owner created at runtime could never have either, which defeats the point of roles being data. Two boolean columns on `roles` now carry it, and roleDevGrants() reads the row. A role that no longer exists reads as neither grant, matching "absence denies" everywhere else. NOT the `provisioningScript` D8 asked for, and that column is removed rather than left dead. A shell script stored on a role row and run at provisioning time is arbitrary root-level code in a database column, and D8's own warning says why that is worse than it looks: `usermod -aG docker` is root by a longer route. So the row CHOOSES from a vocabulary the platform already knows how to carry out — the same shape as `multiarch` on a plugin's osDependencies, for the same reason. The seed re-asserts both flags on every run for system roles, and only for them. Without that, existing rows take the column default of false the moment it lands and Developer silently loses its tools on the next deploy — every host already has these four rows, so an insert-only seed would have shipped that. `description` stays insert-only, because an edit made on one server must survive a deploy. `db:push` could not apply this and said nothing. Dropping a column while adding another makes drizzle-kit ask whether it is a rename; with no terminal it bails and the wrapper still exits 0, leaving the seed to fail against an unchanged schema. Recorded in src/databases/CLAUDE.md with the SQL, because it will hit every host on the next deploy and no agent driving a rollout has a terminal. Verified here: SQL applied guarded, Developer keeps both grants and no other role gained any, second push emits zero DDL and no prompt, 1423 pass / 0 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
+31
-17
@@ -7,31 +7,45 @@
|
||||
*
|
||||
* ── Why these four are `isSystem` ──
|
||||
*
|
||||
* Each is named by name somewhere in the code: `rolePermitsDevTools()` tests for `'Developer'`,
|
||||
* `bootstrap.ts` writes `'Super Admin'`, `users.role` defaults to `'Member'`, and `'Admin'` is the one
|
||||
* `rolePermitsDevTools` deliberately excludes. Renaming or deleting one of those is not a data change, it
|
||||
* is a code change that has not happened — so the flag exists for the delete and rename paths to refuse on,
|
||||
* rather than for the failure to turn up later as a missing database role or an account that cannot log in.
|
||||
* Each is named by name somewhere in the code: this file matches `'Developer'`, `'Admin'` and `'Member'`
|
||||
* by name on every run, and `bootstrap.ts` writes `'Super Admin'`. Renaming or deleting one of those is not
|
||||
* a data change, it is a code change that has not happened — so the flag exists for the delete and rename
|
||||
* paths to refuse on, rather than for the failure to turn up later as a missing database role or an
|
||||
* account that cannot log in.
|
||||
*
|
||||
* ── `description` versus `grantsPostgres`/`grantsDocker` ──
|
||||
*
|
||||
* `seedRole` treats these two kinds of column differently, and it matters here specifically because this
|
||||
* is the one caller that seeds `isSystem: true` rows. `description` is written once, on insert, and never
|
||||
* touched again — an edit made on one server must not be reverted by the next deploy. `grantsPostgres` and
|
||||
* `grantsDocker` ARE re-asserted every run for a system role, because they are not the operator's data,
|
||||
* they are what "Developer" MEANS — the same standing as the name itself. `rolePermitsDevTools()` used to
|
||||
* be that declaration, hardcoded as `role === 'Developer'`; this table is where it lives now, and this
|
||||
* SEED array is the only place either flag is set for these four roles. See `seedRole`'s own comment in
|
||||
* `officer_db/src/roles/queries.ts` for the mechanics.
|
||||
*
|
||||
* ── What this deliberately does NOT do ──
|
||||
*
|
||||
* No foreign key from `users.role`. That change cannot go through `db:push`: push creates the table empty
|
||||
* and adds the constraint in the same run, so every existing account violates it — verified against a
|
||||
* scratch database on 2026-08-23, where push reported the violation, created the table, skipped the
|
||||
* constraint, and exited 0. The FK is a separate release, once every host holds these rows.
|
||||
*
|
||||
* So today `users.role` is still a plain text column and nothing enforces that its value is a real role.
|
||||
* That is the intended state of this phase: additive, unable to fail, and safe to run on five hosts
|
||||
* unattended.
|
||||
*
|
||||
* Never overwrites. A description edited on one server must not be reverted by the next deploy.
|
||||
* Never overwrite `description`, as above. And never touch a CUSTOM role's grants — those are set through
|
||||
* `PATCH /api/roles/:name/grants` (`setRoleGrants`), which itself refuses to touch a system role for the
|
||||
* mirror-image reason: writing there would look like it worked and then silently revert on the next run of
|
||||
* this script.
|
||||
*/
|
||||
import { seedRole, waitForDatabase } from '../src/databases/officer_db/src/index';
|
||||
|
||||
const SEED: { name: string; description: string }[] = [
|
||||
type SeedRoleEntry = { name: string; description: string; grantsPostgres?: boolean; grantsDocker?: boolean };
|
||||
|
||||
// Match today's behaviour exactly: only Developer passed `rolePermitsDevTools` before this table existed,
|
||||
// so Developer is the only role seeded with either flag true.
|
||||
const SEED: SeedRoleEntry[] = [
|
||||
{ name: 'Super Admin', description: 'Full authority over the platform, and an OS account with sudo' },
|
||||
{ name: 'Admin', description: 'Administers the platform. No operating-system privilege' },
|
||||
{ name: 'Developer', description: 'A member who is also trusted with developer tooling on the machine' },
|
||||
{
|
||||
name: 'Developer',
|
||||
description: 'A member who is also trusted with developer tooling on the machine',
|
||||
grantsPostgres: true,
|
||||
grantsDocker: true,
|
||||
},
|
||||
{ name: 'Member', description: 'The default. Reaches only what their role has been granted' },
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user