Repository navigation
Expand file tree
/
Copy pathadmin-standing-surface.ts
More file actions
293 lines (281 loc) · 14.7 KB
/
Copy pathadmin-standing-surface.ts
File metadata and controls
293 lines (281 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
/**
* ADMIN_STANDING_SURFACE — what `resolveAuthzContext` READS when it decides
* who is an administrator, declared beside the resolver that reads it.
*
* ## Why this file exists (commit f8eb73601)
*
* `plugin-auth`'s break-glass guard (`last-admin-guard.ts`, ADR-0135 D5.2)
* decides whether a pending write can empty the administrator population by
* testing the payload against three standing-key lists — `MEMBER_STANDING_KEYS`,
* `GRANT_STANDING_KEYS`, `PERMISSION_SET_STANDING_KEYS`. Those lists are not an
* independent design artifact: they are a CACHE of the columns this resolver
* consumes. A payload touching none of them is skipped without any reads, so a
* column this resolver starts reading and the guard's list omits is a write
* class the guard silently stops judging — the one write class that can lock an
* installation out of its own administration, with no in-product recovery.
*
* Nothing bound the two together. The correspondence was carried by a comment,
* and it had already gone false once: #6084 wrote, beside the list, that
* everything a permission-set write touches other than `name` — naming `active`
* explicitly — is invisible to "who is an administrator". That was true when
* written. #8613 made `active` a resolution-time predicate (a DEACTIVATED
* `admin_full_access` set confers nothing, §6b below), and the sentence became
* false. It was caught by one agent reading the comment closely enough to
* notice it contradicted the code being written. Nothing mechanical would have
* caught it: the guard's own tests stay green, because the guard is simply never
* consulted for that write.
*
* ## What this file is, and what it is NOT
*
* It is a MEASUREMENT, not a wish. Its column lists are asserted equal to what
* the resolver actually reads at runtime, by
* `admin-standing-surface.test.ts`, which drives the real
* `resolveAuthzContext` over a recording engine and collects every property
* access and every `where` key per table. That is deliberate: a hand-written
* list of "columns the derivation reads" is the same artifact as the comment
* that went stale, one indirection along. Observation is also the only reading
* that survives the derivation moving INTO a helper — `active` is read by
* `isRowActive(ps)` and the window bounds by `isGrantActive(row, now)`, neither
* of which names a column at the resolver's own call site.
*
* It is NOT a projection the resolver consumes. `ql.find` here returns whole
* rows and the reads are ordinary property accesses on untyped rows, so nothing
* in this file can FORCE the resolver to read only what it declares. The force
* comes from the observation test: add a read, and this declaration is red
* until it is updated; update this declaration, and `plugin-auth`'s
* correspondence test is red until every new column is either in a standing-key
* list or explicitly excluded with a reason.
*
* ## Reading the entries
*
* Every table this resolution path reads is listed — including the ones that
* CANNOT confer administrator standing, each with the reason it cannot. That is
* the table-level half of the same guarantee: a resolver that starts deriving
* administrator standing from a new table would otherwise be invisible to a
* column-set comparison, because the new table appears in neither side's list.
*
* ## ⚠️ Tables are no longer the whole surface (#11663 L2)
*
* Since the platform-admin re-anchor's core leg, one input to the administrator
* derivation is NOT a table at all: the deployment's declared administrator
* list, read from the environment on every resolution
* (`security/platform-admin.ts`). A file that listed only tables would go on
* being perfectly accurate about the tables while silently claiming the
* derivation reads nothing else — the same shape as the stale comment this file
* replaced, one level up. {@link ADMIN_STANDING_NON_TABLE_INPUTS} is the place
* that says so, and it is deliberately a SEPARATE export rather than a
* pseudo-row in the table map: the map is compared for equality against
* observed table reads, and a pseudo-row would have to be excluded from that
* comparison by name, which is exactly the kind of special case that rots.
*/
/** How a table this resolver reads relates to "who is an administrator". */
export interface AdminStandingTable {
/**
* `derives` — a write to this table can change the administrator population,
* so `last-admin-guard.ts` must carry a standing-key list for it.
* `reads-only` — this resolver reads the table for something else entirely.
*/
readonly role: 'derives' | 'reads-only';
/** Why the row above is the right classification. Prose, but pinned to a measured table. */
readonly reason: string;
/**
* Every column this resolver reads on the table — property accesses and
* `where` keys alike, in every spelling it actually touches. Declared for
* `derives` tables only; asserted equal to the observed set.
*/
readonly columns?: readonly string[];
}
/**
* The measured read surface of the administrator derivation.
*
* Scope, stated so the gate cannot be read as claiming more than it measures:
* this is the SESSION/user-id resolution path — `resolveAuthzContext` with a
* principal, and therefore all of `resolveUserAuthzGrants`. The API-key
* ADMISSION path (`resolveApiKeyAdmission`) is outside it on purpose: it
* authenticates a principal and seeds `permissions` with the key's scopes, and
* confers no administrator standing of its own — `hasPlatformAdminGrant` is set
* from the catalog's `admin_full_access` set reached through the anchoring
* `sys_user_permission_set` grant (§6b — the Default Organization's or an
* organization-less one, on a NON-WALLED posture only since #11663 L5) or from the deployment config matched against the caller's own
* STORED `sys_user` row (§6b-config, on every posture), never from a scope
* string and never from the caller-seedable `grants.email`.
*/
export const ADMIN_STANDING_SURFACE: Readonly<Record<string, AdminStandingTable>> = {
sys_metadata_activation: {
role: 'derives',
reason:
'[ADR-0131 D3, ADR-0126 §4] The activation ledger the resolver reads deactivation from '
+ '(`readDisabledCatalogNames`, §6a): a `permission` row of `admin_full_access` whose `active` '
+ 'is false un-makes every GRANT-derived platform administrator at once (§6b), with no identity '
+ 'table touched; a `position` row switches that position off. Deployment-wide (no tenant '
+ 'column). No catalog row is read — the row objects are retired (ADR-0131 D3, C3 stage 8): the '
+ 'set itself — its existence and its body — is the catalog definition (see the '
+ '`security-catalog` non-table input). ⚠️ It does NOT un-make a CONFIG-derived administrator '
+ '(§6b-config), which never reads a set.',
columns: [
'metadata_type',
'name',
'active',
],
},
sys_user_permission_set: {
role: 'derives',
reason:
'The grant that makes a user a platform admin UNDER A NON-WALLED POSTURE: an in-window '
+ '(ADR-0091) grant of `admin_full_access` owned by the Default Organization (ADR-0131 D5, the '
+ 'row the first-user promotion writes) or by no organization (the row it wrote before) — §6, '
+ '`grantAnchorsPlatformStanding`. Re-pointing it, scoping it to another organization or moving '
+ 'it out of its window revokes the standing while leaving the row in place. ⚠️ Under a '
+ 'WALLED posture (`group`/`isolated`) it confers NOTHING: the walled half '
+ 'of this legacy anchor is retired, so standing there comes from `OS_PLATFORM_OWNER_EMAIL` '
+ '(§6b-config) and from nothing else, and this row is read only for the permission set it '
+ 'names. Under `single` — the default — the row is still the anchor that rig\'s '
+ 'zero-config first-user promotion mints, owned by the Default Organization (see '
+ '`sys_organization`). [ADR-0131 D4] The grant holds its set BY '
+ 'NAME (`permission_set`): a grant that names nothing confers nothing, and its '
+ '`permission_set_id` is not read here.',
columns: [
'user_id',
'permission_set',
'organization_id',
'organizationId',
'valid_from',
'validFrom',
'valid_until',
'validUntil',
],
},
sys_organization: {
role: 'derives',
reason:
'[ADR-0131 D5] Names the Default Organization — `slug = \'default\'`, else the only '
+ 'organization (`readDefaultOrganizationId`) — whose `admin_full_access` grant anchors '
+ 'platform standing under `single` (§6). Read only for a principal holding an '
+ 'organization-owned grant of that set, and never under a wall. Re-slugging the Default '
+ 'Organization while several organizations exist, or deleting it, moves the anchor off its '
+ 'grant and revokes that standing with no grant row touched.',
columns: [
'id',
'slug',
],
},
sys_member: {
role: 'derives',
reason:
'Organization owner/admin standing (§3). The graded `role` is projected into `positions` '
+ 'here and separately drives the `organization_admin` capability grant, which is what the '
+ 'posture ladder reads; the break-glass guard counts the same rows one step earlier, by '
+ 'grade (ADR-0108). Either way a downgrade of the last graded membership is a write that '
+ 'can empty the administrator population.',
columns: [
'user_id',
'userId',
'organization_id',
'organizationId',
'role',
'valid_from',
'validFrom',
'valid_until',
'validUntil',
],
},
sys_user: {
role: 'derives',
reason:
'RECLASSIFIED from `reads-only` when the config anchor landed. This table used to be read only for the '
+ '`current_user.email` RLS fallback and the cloud ADR-0024 `ai_seat` synthesis (§7), and the '
+ 'note here said so: "Neither confers administrator standing." That sentence is now FALSE. '
+ 'The config anchor (§6b-config) matches the row\'s own `email` against the deployment\'s '
+ 'declared administrator list and requires `email_verified` to read verified, so a write '
+ 'that changes either column takes platform-admin standing away from a config-derived '
+ 'administrator — an address change and an email_verified reset are both ordinary, '
+ 'reachable writes, and neither touches a grant table. `banned` stays absent from the '
+ 'column list because the resolver still never reads it; the guard watches the ban/delete '
+ 'WRITE SHAPES on this table for its own reasons, which is a different question from what '
+ 'this resolver consumes.',
columns: [
'id',
'email',
'email_verified',
'ai_access',
],
},
sys_user_position: {
role: 'reads-only',
reason:
'ADR-0057 D4 platform-RBAC position assignments (§4). A position can carry permission sets '
+ '(its catalog definition\'s `permissionSets`, ADR-0131 D3/D4) but never platform-admin '
+ 'standing — §6b requires the set to be reached through an unscoped USER grant, so a '
+ 'position-bound `admin_full_access` resolves the set name into `permissions` and leaves '
+ '`hasPlatformAdminGrant` false.',
},
};
/** A derivation input that is not a table — see {@link ADMIN_STANDING_NON_TABLE_INPUTS}. */
export interface AdminStandingNonTableInput {
/**
* How the value reaches the resolver: `env` for a process environment
* variable, `security-catalog` for a definition the resolver reads from the
* catalog bound to its engine (ADR-0131 D3/D4).
*/
readonly kind: 'env' | 'security-catalog';
/** The exact spelling an operator sets — quotable verbatim in a refusal message. */
readonly name: string;
/** What it decides, and what a break-glass guard can and cannot do about it. */
readonly reason: string;
}
/**
* [#11663 L2] Inputs to the administrator derivation that no table write can
* reach — declared here so this file's silence about them cannot be read as
* "the derivation reads only tables".
*
* The practical consequence is the one worth writing down: a break-glass guard
* simulates a pending WRITE, and there is no write to simulate for any of
* these. Standing that rests on one of them is taken away by changing the
* deployment's configuration and rolling the process, which is deliberately
* outside every in-product path — including every path an agent could be talked
* into calling. That is the whole point of the config anchor, and it is also
* the reason a guard cannot promise to prevent this class of lockout: it can
* only refuse the writes it can see.
*/
export const ADMIN_STANDING_NON_TABLE_INPUTS: readonly AdminStandingNonTableInput[] = [
{
kind: 'env',
name: 'OS_PLATFORM_OWNER_EMAIL',
reason:
'The deployment\'s declared platform administrator(s) — one address or a comma-separated '
+ 'list, matched case-insensitively against `sys_user.email` and conferring standing only '
+ 'when that row\'s `email_verified` reads verified (§6b-config). Read live on every '
+ 'derivation with a per-process memo keyed on the raw string, so a rolled process picks up '
+ 'a change with no special path. Unset, blank, or carrying any unparseable entry means '
+ 'ZERO config-derived administrators, fail closed. No runtime write reaches it, so no '
+ 'break-glass guard can simulate a change to it: revocation is a configuration change plus '
+ 'a process roll, by design.',
},
{
kind: 'security-catalog',
name: 'admin_full_access',
reason:
'The permission-set definition an unscoped `admin_full_access` user grant names, read from '
+ 'the security catalog by name (ADR-0131 D3/D4) — the platform\'s own declaration, shipped '
+ 'on the security plugin\'s manifest. A name the catalog does not hold confers nothing, so '
+ 'every grant-derived platform administrator rests on it. It is code-declared and refused '
+ 'an overlay at the metadata door, so no runtime write reaches it either: removing it is '
+ 'a code change plus a process roll.',
},
];
/** The tables a write to which can change who is an administrator. */
export function adminStandingTables(): string[] {
return Object.entries(ADMIN_STANDING_SURFACE)
.filter(([, t]) => t.role === 'derives')
.map(([name]) => name)
.sort();
}
/**
* The columns this resolver reads on `table`, or `undefined` when the table is
* not part of the administrator derivation.
*/
export function adminStandingColumns(table: string): readonly string[] | undefined {
const entry = ADMIN_STANDING_SURFACE[table];
return entry?.role === 'derives' ? entry.columns : undefined;
}