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
51 lines
2.1 KiB
TypeScript
51 lines
2.1 KiB
TypeScript
import { afterEach, describe, expect, test } from 'bun:test';
|
|
import { createRole } from 'officerdb';
|
|
import { db } from 'officerdb/db';
|
|
import { roles } from 'officerdb/schema';
|
|
import { like } from 'drizzle-orm';
|
|
import { roleDevGrants } from './os-user-postgres';
|
|
|
|
/*
|
|
`roleDevGrants` is the replacement for the hardcoded `role === 'Developer'` gate — see the comment above
|
|
it. What matters here is that it reads the ROW, not the name: a role called something else entirely gets
|
|
dev tooling if and only if its `grantsPostgres`/`grantsDocker` columns say so, and a role that does not
|
|
exist reads as neither rather than throwing.
|
|
*/
|
|
|
|
const PREFIX = 'zz-test-devgrants-';
|
|
const name = (suffix: string) => `${PREFIX}${suffix}`;
|
|
|
|
afterEach(async () => {
|
|
await db.delete(roles).where(like(roles.name, `${PREFIX}%`));
|
|
});
|
|
|
|
describe('roleDevGrants', () => {
|
|
test('reads both flags off a role that is not named Developer', async () => {
|
|
const created = await createRole({ name: name('custom-dev'), grantsPostgres: true, grantsDocker: true });
|
|
expect(created.ok).toBe(true);
|
|
|
|
expect(await roleDevGrants(name('custom-dev'))).toEqual({ grantsPostgres: true, grantsDocker: true });
|
|
});
|
|
|
|
test('a role with grantsDocker false does not get docker, even with grantsPostgres true', async () => {
|
|
const created = await createRole({ name: name('pg-only'), grantsPostgres: true, grantsDocker: false });
|
|
expect(created.ok).toBe(true);
|
|
|
|
expect(await roleDevGrants(name('pg-only'))).toEqual({ grantsPostgres: true, grantsDocker: false });
|
|
});
|
|
|
|
test('a role granted neither gets neither', async () => {
|
|
const created = await createRole({ name: name('plain') });
|
|
expect(created.ok).toBe(true);
|
|
|
|
expect(await roleDevGrants(name('plain'))).toEqual({ grantsPostgres: false, grantsDocker: false });
|
|
});
|
|
|
|
test('a role that does not exist reads as neither, matching "absence denies" rather than throwing', async () => {
|
|
await expect(roleDevGrants(name('never-created'))).resolves.toEqual({
|
|
grantsPostgres: false,
|
|
grantsDocker: false,
|
|
});
|
|
});
|
|
});
|