Skip to content

Commit 44bb1e1

Browse files
committed
dogfood: the admin_full_access case pins the last-admin guard's ruled pair; row-active.ts names its remaining readers
Claude-Session: https://claude.ai/code/session_015LutkhFn8wzgD5J86PtpYN Co-authored-by: Claude <noreply@anthropic.com>
1 parent faf0c20 commit 44bb1e1

2 files changed

Lines changed: 54 additions & 14 deletions

File tree

‎packages/core/src/security/row-active.ts‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,12 @@
2020
* predicate. The Deactivate dialog's promise is kept by that ledger row, not by
2121
* this column.
2222
*
23-
* What still reads the column through this predicate, on `main`: the
24-
* last-administrator guard's enumeration (`plugin-auth` `last-admin-guard.ts`),
25-
* the explain engine's "deactivated" label (`plugin-security`
26-
* `explain-engine.ts`), the security plugin's set loader (`plugin-security`
27-
* `security-plugin.ts`) and the sharing-rule position check (`plugin-sharing`
28-
* `sharing-rule-service.ts`). The reasoning below is about reading the column
29-
* at all, and holds for each of them.
23+
* What still reads the column through this predicate, on `main`: the explain
24+
* engine's "deactivated" label (`plugin-security` `explain-engine.ts`) and the
25+
* security plugin's set loader (`plugin-security` `security-plugin.ts`). The
26+
* last-administrator guard and the sharing-rule position check read the ledger
27+
* instead. The reasoning below is about reading the column at all, and holds
28+
* for each remaining reader.
3029
*
3130
* ## Why the predicate is "explicitly deactivated", not "explicitly active"
3231
*

‎packages/qa/dogfood/test/catalog-activation-door.dogfood.test.ts‎

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
// - the name is checked against the catalog the security plugin really binds
1313
// (a declared position, a declared permission set, a name nobody declares);
1414
// - the write goes through the engine, so the last-admin guard's ledger hook
15-
// really refuses switching `admin_full_access` off;
15+
// really judges switching `admin_full_access` off: refused when no
16+
// administrator would be left, landing when one stands;
1617
// - the resolver over the booted engine stops granting through a position
1718
// the door switched off, for a holder of that position.
1819

@@ -36,6 +37,7 @@ const HOLDER_EMAIL = 'exec.holder@example.com';
3637
interface Engine {
3738
find(object: string, options?: unknown): Promise<Array<Record<string, unknown>>>;
3839
insert(object: string, data: Record<string, unknown>, options?: unknown): Promise<unknown>;
40+
update(object: string, data: Record<string, unknown>, options?: unknown): Promise<unknown>;
3941
}
4042

4143
describe('the catalog activation door, on a booted showcase', () => {
@@ -113,13 +115,52 @@ describe('the catalog activation door, on a booted showcase', () => {
113115
expect(rows[0].active === true || rows[0].active === 1).toBe(true);
114116
});
115117

116-
it('switching `admin_full_access` off is refused 403 by the last-admin guard, and no row lands', async () => {
117-
const res = await flip('permission', 'admin_full_access', { enabled: false });
118-
const body = await res.json() as { error?: { code?: string } };
118+
// [ADR-0135 D5.2] The last-administrator guard refuses a ledger switch-off
119+
// of `admin_full_access` only when it would leave nobody who can sign in as
120+
// an administrator. Standing here is the seeded admin's unscoped
121+
// `admin_full_access` grant (the `single` posture declares no owner email)
122+
// plus the default organization's owner membership, which the same admin
123+
// holds. So the pair: with the owner membership demoted the grant is the
124+
// last standing and the switch-off is refused; with it standing the
125+
// switch-off lands. Every change is undone in `finally`, so a failed
126+
// assertion leaves the shared boot as it found it.
127+
it('switching `admin_full_access` off with no administrator left standing is refused 403, and no row lands', async () => {
128+
const owners = await ql.find('sys_member', { where: { role: 'owner' }, context: SYSTEM_CTX });
129+
// Anti-vacuity: the boot really has an owner to demote.
130+
expect(owners.length, 'no owner membership on this boot').toBeGreaterThan(0);
131+
try {
132+
for (const m of owners) await ql.update('sys_member', { id: m.id, role: 'member' }, { context: SYSTEM_CTX });
133+
134+
const res = await flip('permission', 'admin_full_access', { enabled: false });
135+
const body = await res.json() as { error?: { code?: string } };
136+
137+
expect(res.status).toBe(403);
138+
expect(body.error?.code).toBe('PERMISSION_DENIED');
139+
expect(await rowsOf('permission', 'admin_full_access')).toEqual([]);
140+
} finally {
141+
for (const m of owners) await ql.update('sys_member', { id: m.id, role: 'owner' }, { context: SYSTEM_CTX });
142+
}
143+
});
119144

120-
expect(res.status).toBe(403);
121-
expect(body.error?.code).toBe('PERMISSION_DENIED');
122-
expect(await rowsOf('permission', 'admin_full_access')).toEqual([]);
145+
it('CONTROL: switching `admin_full_access` off with the organization owner still standing lands 200', async () => {
146+
try {
147+
const res = await flip('permission', 'admin_full_access', { enabled: false });
148+
const text = await res.clone().text();
149+
150+
expect(res.status, text).toBe(200);
151+
const rows = await rowsOf('permission', 'admin_full_access');
152+
expect(rows).toHaveLength(1);
153+
expect(rows[0].active === false || rows[0].active === 0).toBe(true);
154+
} finally {
155+
// Restored through the engine, not the door: with the set off the
156+
// caller may no longer hold `manage_metadata`. Switching it back on
157+
// is never refused by the guard.
158+
for (const row of await rowsOf('permission', 'admin_full_access')) {
159+
await ql.update(LEDGER, { id: row.id, active: true }, { context: SYSTEM_CTX });
160+
}
161+
}
162+
const restored = await rowsOf('permission', 'admin_full_access');
163+
expect(restored.every((r) => r.active === true || r.active === 1)).toBe(true);
123164
});
124165

125166
it('a name nobody declares is 404 and writes nothing', async () => {

0 commit comments

Comments
 (0)