@@ -76,8 +76,24 @@ function wildcardGrantsSuperRead(objects: Record<string, any>): boolean {
7676 * the client is told must be derived from the server's actual effective
7777 * enforcement, never from an independent reading of the declarations.
7878 *
79- * The super-user grant covers private/managed objects on the server, so folding
80- * it here is exactly as broad as real enforcement — never broader.
79+ * The super-user grant covers private/managed objects on the server, so the
80+ * read and write bypass reach every entry here.
81+ *
82+ * [#20134] This reads the MERGED wildcard, so it knows only the two bypass
83+ * bits, and only four entry bits. The per-set half,
84+ * {@link foldSuperUserWildcardGrants}, carries what that reading cannot: the
85+ * `transfer` the write bypass grants, and a super-user wildcard's own plain
86+ * bits (`'*': { viewAllRecords, allowEdit }` edits). Two cells here are known
87+ * to be BROADER than enforcement, and are left exactly as they are for the
88+ * over-grant card of this family (#20136):
89+ * - the merged bypass is folded into an entry the super-user set itself names
90+ * narrower — `resolveObjectPermission` answers that set with its explicit
91+ * entry, so the walled `organization_admin` is granted edit on
92+ * `sys_position` here and refused it by the server;
93+ * - `allowCreate` is pulled on the write bypass, which the spec's
94+ * `objectPermissionGrants` deliberately gives no create cell: a
95+ * `'*': { modifyAllRecords: true }` without `allowCreate` is granted create
96+ * here and refused it by the server.
8197 */
8298export function foldWildcardSuperUser ( objects : Record < string , any > ) : void {
8399 const wild = objects ?. [ '*' ] ;
@@ -96,6 +112,64 @@ export function foldWildcardSuperUser(objects: Record<string, any>): void {
96112 }
97113}
98114
115+ /**
116+ * [#20134] The per-set half of the super-user fold: each set's SUPER-USER
117+ * `'*'` puts every bit it grants on each entry that set does not name, mutating
118+ * the map in place. Runs after the seed (so every registered object has an
119+ * entry) and BEFORE the managed-write clamp.
120+ *
121+ * Without it the map held only the four bits {@link foldWildcardSuperUser}
122+ * pulls, so `current_user.can(object, 'transfer')` answered `false` for
123+ * `admin_full_access` and the walled `organization_admin` on every object
124+ * where `PermissionEvaluator.checkObjectPermission('transfer', …)` answers
125+ * `true` through `modifyAllRecords`, and a super-read wildcard lost its own
126+ * plain bits (`'*': { viewAllRecords, allowEdit }` read only).
127+ *
128+ * The bits are DERIVED, not listed: for each grant bit, the spec's
129+ * `objectPermissionGrants` — the one fold `checkObjectPermission` itself asks
130+ * — is read on the set's wildcard. For a super-user wildcard its read cell is
131+ * always `true`, so its export cell is exactly the wildcard's own
132+ * `allowExport`, the grant half the evaluator's export conjunction reads.
133+ *
134+ * Exactly as broad as `resolveObjectPermission`, never broader:
135+ * - a set that names the object contributes nothing here: for that set the
136+ * explicit entry is the whole answer, and the merge already carries it;
137+ * - a super-user wildcard covers a private object as much as a public one,
138+ * so no posture is read;
139+ * - only `true` bits are set — a bit the wildcard does not grant is left
140+ * as the other passes left it.
141+ *
142+ * A plain wildcard is {@link materializePlainWildcardCoverage}'s, and a map
143+ * holding no super-user wildcard leaves this pass without a single write.
144+ */
145+ function foldSuperUserWildcardGrants (
146+ objects : Record < string , any > ,
147+ sets : ReadonlyArray < EffectiveObjectPermissionsInputSet | null | undefined > ,
148+ ) : void {
149+ const superWildcards : Array < { named : Record < string , unknown > ; bits : readonly GrantBit [ ] } > = [ ] ;
150+ for ( const ps of sets ) {
151+ const named = ps ?. objects as Record < string , unknown > | null | undefined ;
152+ const wild = named ?. [ '*' ] as Record < string , unknown > | null | undefined ;
153+ if ( ! named || ! wild || typeof wild !== 'object' ) continue ;
154+ // The file's one reading of "is this a super-user wildcard?", asked of the set.
155+ if ( ! wildcardGrantsSuperRead ( named ) ) continue ;
156+ const bits = GRANT_BITS . filter ( ( bit ) => objectPermissionGrants ( wild as EffectiveObjectPermission , bit ) ) ;
157+ superWildcards . push ( { named, bits } ) ;
158+ }
159+ if ( superWildcards . length === 0 ) return ;
160+ for ( const [ obj , acc ] of Object . entries ( objects ) as Array < [ string , any ] > ) {
161+ if ( obj === '*' || ! acc ) continue ;
162+ for ( const { named, bits } of superWildcards ) {
163+ // `resolveObjectPermission`'s own test: a set's explicit entry, when it
164+ // has one, is that set's whole answer for the object.
165+ if ( named [ obj ] ) continue ;
166+ for ( const bit of bits ) {
167+ if ( acc [ bit ] !== true ) acc [ bit ] = true ;
168+ }
169+ }
170+ }
171+ }
172+
99173/** Minimal schema shape the managed-write clamp needs. */
100174export interface ManagedSchemaLike {
101175 managedBy ?: string ;
@@ -200,6 +274,7 @@ function isPrivatePosture(schema: ObjectAccessPostureLike | null | undefined): b
200274
201275/** The `allow*` bits {@link objectPermissionGrants} reads, i.e. every bit a `can()` verb resolves to. */
202276const GRANT_BITS = [ 'allowRead' , 'allowCreate' , 'allowEdit' , 'allowDelete' , 'allowTransfer' , 'allowExport' ] as const ;
277+ type GrantBit = ( typeof GRANT_BITS ) [ number ] ;
203278const GRANT_BIT_SET : ReadonlySet < string > = new Set ( GRANT_BITS ) ;
204279
205280/** Does the entry grant any verb on its own? (`objectPermissionGrants` over every verb target.) */
@@ -285,14 +360,14 @@ function materializePlainWildcardCoverage(
285360
286361/**
287362 * [#3391] Seed false-initialized per-object entries for a wildcard SUPER-USER,
288- * for every registered object whose `apiMethods` whitelist tightens exposure .
363+ * for every registered object the merged map does not already carry .
289364 *
290365 * A super-user's grant is usually the `'*'` wildcard, not explicit per-object
291- * entries — so restricting objects never appear in the merged `objects` map and
292- * would miss their `apiOperations` annotation. Seeding a `{allow*: false}` entry
293- * lets {@link foldWildcardSuperUser } pull it true (super-user reads/writes
294- * everything) and lets {@link annotateEffectiveApiOperations} attach the effective
295- * set . Runs BEFORE fold .
366+ * entries — so the objects it reaches never appear in the merged `objects` map.
367+ * Seeding a `{allow*: false}` entry lets { @link foldWildcardSuperUser} and
368+ * {@link foldSuperUserWildcardGrants } pull its grants true and lets
369+ * {@link annotateEffectiveApiOperations} attach the effective operation set
370+ * where the object narrows it . Runs BEFORE both folds .
296371 *
297372 * [#18990] Admitted by {@link wildcardGrantsSuperRead} — the READ bypass, so
298373 * BOTH super-user classes are seeded, and a plain wildcard grant carrying
@@ -311,33 +386,37 @@ function materializePlainWildcardCoverage(
311386 * (`PermissionEvaluator.checkObjectPermission`), so the entry is exactly as
312387 * broad as real enforcement, never broader.
313388 *
314- * [#18931] A schema is skipped only when it needs NO annotation, which is the
315- * predicate {@link annotateEffectiveApiOperations} itself applies: unrestricted
316- * AND the export axis leaves `export` in place. Resolving WITHOUT the export
317- * slot made this pass disagree with annotate's — an unrestricted object whose
318- * `export` the axis withholds got no entry, annotate (which iterates existing
319- * entries only) never saw it, and `/me/permissions` stayed silent for exactly
320- * the population #8681 created: a wildcard-only admin holding no `allowExport`.
321- * The client's `apiOperations` is then `undefined`, its default-allow path
322- * renders an Export button, and the click is refused `403 EXPORT_NOT_PERMITTED`.
389+ * [#18931] An unrestricted object whose `export` the export axis withholds is
390+ * seeded too: annotate iterates existing entries only, so without an entry
391+ * `/me/permissions` stayed silent for exactly the population #8681 created — a
392+ * wildcard-only admin holding no `allowExport` — the client's `apiOperations`
393+ * was `undefined`, its default-allow path rendered an Export button, and the
394+ * click was refused `403 EXPORT_NOT_PERMITTED`.
395+ *
396+ * [#20134] And so is EVERY other registered object absent from the map — the
397+ * name keeps its #3391 origin, the scope does not. The pass used to skip an
398+ * unrestricted object whose `export` stays allowed, the one object that needs
399+ * no `apiOperations` annotation. But the entry is not only the annotation's
400+ * carrier: `current_user.can()` reads the map entry by entry and an absent one
401+ * as "no grant", so a super-user wildcard that also carries `allowExport` left
402+ * every such object reading `false` for every verb the server lets that
403+ * principal perform. The entry is the truth the #18990 ruling asks for — every
404+ * object a principal can reach gets one — and annotate keeps its own skip:
405+ * an entry seeded for an unrestricted, export-allowed object carries no
406+ * `apiOperations`, so the client's default-allow path for the operation set is
407+ * exactly as it was.
323408 */
324409export function seedSuperUserRestrictedObjects (
325410 objects : Record < string , any > ,
326411 allSchemas : readonly ApiExposureSchemaLike [ ] ,
327412) : void {
328413 if ( ! wildcardGrantsSuperRead ( objects ) ) return ;
329- // [#18931] The export slot annotate will read for an entry seeded here. A
330- // seeded entry carries no `allowExport` of its own and `foldWildcardSuperUser`
331- // does not add one, so annotate's `acc.allowExport ?? wildExport` resolves to
332- // exactly this wildcard bit — the two passes cannot diverge again.
333- const userExportAllowed = objects [ '*' ] ?. allowExport === true ;
414+ // A super-user wildcard covers every registered object, private ones
415+ // included (`PermissionEvaluator`'s `resolveObjectPermission`), so every
416+ // registered object the merge left absent gets an entry.
334417 for ( const schema of allSchemas ) {
335418 const name = schema ?. name ;
336419 if ( ! name || name === '*' || objects [ name ] ) continue ;
337- const eff = resolveEffectiveApiMethods ( schema . enable ?? undefined , { userExportAllowed } ) ;
338- // Same skip predicate as annotate: there is nothing to say about an
339- // unrestricted object that keeps its full operation closure.
340- if ( eff . mode === 'unrestricted' && userExportAllowed ) continue ;
341420 objects [ name ] = { allowCreate : false , allowRead : false , allowEdit : false , allowDelete : false } ;
342421 }
343422}
@@ -432,7 +511,9 @@ export interface EffectiveObjectPermissionsInputSet {
432511 * 3. {@link materializePlainWildcardCoverage} — [#20083] each set's plain
433512 * `'*'` onto the registered objects it covers for that set, so the map is
434513 * as broad as `checkObjectPermission` there; guarded like (2);
435- * 4. {@link foldWildcardSuperUser};
514+ * 4. {@link foldWildcardSuperUser}, then {@link foldSuperUserWildcardGrants} —
515+ * [#20134] each set's super-user `'*'` onto the entries it covers for that
516+ * set, every bit it grants;
436517 * 5. {@link clampManagedObjectWrites};
437518 * 6. {@link annotateEffectiveApiOperations} — guarded like (2).
438519 *
@@ -466,9 +547,10 @@ export function buildEffectiveObjectPermissions(
466547 try { return source . allSchemas ?.( ) ?? [ ] ; } catch { return [ ] as ApiExposureSchemaLike [ ] ; }
467548 } ) ( ) ;
468549 // [#3391] For a wildcard super-user — [#18990] either bypass bit, not
469- // modify-all alone — seed restricting objects absent from the merged map so
470- // fold pulls what it pulls and annotate can attach their effective
471- // apiOperations. Guarded — a failure here must never drop the whole map.
550+ // modify-all alone — seed [#20134] every registered object absent from the
551+ // merged map, so the folds pull what they pull and annotate can attach the
552+ // effective apiOperations where the object narrows them. Guarded — a failure
553+ // here must never drop the whole map.
472554 try {
473555 seedSuperUserRestrictedObjects ( objects , allSchemas ) ;
474556 } catch ( e : any ) {
@@ -486,9 +568,12 @@ export function buildEffectiveObjectPermissions(
486568 // Make the per-object map reflect the server's ACTUAL effective enforcement
487569 // = permission-set grant ∩ identity write guard (ADR-0057 D10, cited as an
488570 // attribution, #9628): (1) fold the `'*'` super-user grant into every object
489- // so an admin's wildcard is not shadowed by another set's explicit deny;
571+ // so an admin's wildcard is not shadowed by another set's explicit deny —
572+ // the merged bypass bits, then [#20134] every bit each set's super-user
573+ // wildcard grants, `transfer` and its own plain bits included;
490574 // (2) re-clamp guarded managed objects by their write affordance.
491575 foldWildcardSuperUser ( objects ) ;
576+ foldSuperUserWildcardGrants ( objects , sets ) ;
492577 clampManagedObjectWrites ( objects , schemaOf ) ;
493578 // [#3391] Annotate the per-object effective API operation set. Guarded: on
494579 // failure `apiOperations` is simply omitted and a client falls back to its
0 commit comments