Skip to content

Commit 3c2fa8f

Browse files
committed
wip(plugin-security): walled-posture accepted defaults as host configuration
1 parent 34b740f commit 3c2fa8f

2 files changed

Lines changed: 268 additions & 43 deletions

File tree

‎packages/plugins/plugin-security/src/security-plugin.ts‎

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ import {
6565
confirmAudienceBindingSuggestion,
6666
dismissAudienceBindingSuggestion,
6767
readAcceptedEveryoneSets,
68+
readAcceptedDefaultPermissionSetsOption,
69+
refuseAcceptedDefaultsWithoutWall,
70+
type DeploymentAcceptance,
6871
type SuggestionDeps,
6972
type SuggestionListFilter,
7073
} from './suggested-audience-bindings.js';
@@ -907,6 +910,34 @@ export interface SecurityPluginOptions {
907910
* @default 'member_default'
908911
*/
909912
fallbackPermissionSet?: string | null;
913+
/**
914+
* [ADR-0131 D5] The package defaults this deployment accepts for the
915+
* `everyone` anchor, declared as host configuration — read ONLY under a
916+
* walled posture (`group` / `isolated`, `postureEnforcesWall`).
917+
*
918+
* Under a wall, platform standing is configuration, and no reserved
919+
* organization owns deployment-level rows (ADR-0131 §1.5). So a walled host
920+
* names here the `isDefault: true` permission sets every signed-in member
921+
* holds beside the baseline. Declared, this list IS the deployment-level
922+
* acceptance: the `everyone` definition derives from the baseline plus these
923+
* sets, the `sys_audience_binding_suggestion` record is neither read nor
924+
* written, and its list / accept / dismiss doors answer 409
925+
* `SUGGESTION_STATE` naming this option. `[]` declares that nothing beyond
926+
* the baseline is accepted.
927+
*
928+
* Each name passes the same checks an accepted suggestion does, on every
929+
* derivation: the security catalog serves it under some package, it declares
930+
* `isDefault: true`, and it carries no bit the `everyone` anchor forbids
931+
* (ADR-0090 D7). A name failing one is not granted and is reported — at
932+
* `error` for a forbidden bit.
933+
*
934+
* Omitted (the default), the acceptance is the record, owned by the Default
935+
* Organization, as before. Declared under the `single` posture it is refused
936+
* at `start()`: there the record is the acceptance (ADR-0131 D5's
937+
* Default-Organization row), and a declaration nothing reads would only
938+
* look enforced.
939+
*/
940+
acceptedDefaultPermissionSets?: readonly string[];
910941
}
911942

912943
/**
@@ -1166,6 +1197,8 @@ export class SecurityPlugin implements Plugin {
11661197
private fieldMasker = new FieldMasker();
11671198
private readonly bootstrapPermissionSets: PermissionSet[];
11681199
private readonly fallbackPermissionSet: string | null;
1200+
/** [ADR-0131 D5] {@link SecurityPluginOptions.acceptedDefaultPermissionSets}, frozen. */
1201+
private readonly acceptedDefaultPermissionSets: readonly string[] | undefined;
11691202
/**
11701203
* [#7555, ADR-0090 D5] The HUMAN baseline, as the list of set names it is:
11711204
* {@link fallbackPermissionSet} COMPOSED WITH the platform baseline
@@ -1479,6 +1512,15 @@ export class SecurityPlugin implements Plugin {
14791512
// [#7555] …and the baseline that actually resolves is that name COMPOSED
14801513
// with the platform's own, never one displacing the other (ADR-0090 D5).
14811514
this.baselinePermissionSets = composeHumanBaselinePermissionSets(this.fallbackPermissionSet);
1515+
this.acceptedDefaultPermissionSets = readAcceptedDefaultPermissionSetsOption(options.acceptedDefaultPermissionSets);
1516+
}
1517+
1518+
/**
1519+
* [ADR-0131 D5] Where the deployment-level acceptance lives: the posture this
1520+
* plugin resolved, and the host's declaration (read only under a wall).
1521+
*/
1522+
private deploymentAcceptance(): DeploymentAcceptance {
1523+
return { posture: this.tenancyPosture, acceptedDefaultPermissionSets: this.acceptedDefaultPermissionSets };
14821524
}
14831525

14841526
async init(ctx: PluginContext): Promise<void> {
@@ -1895,6 +1937,10 @@ export class SecurityPlugin implements Plugin {
18951937
} catch {
18961938
/* engine absent — nothing to widen */
18971939
}
1940+
// [ADR-0131 D5] The host's accepted package defaults are read only under a
1941+
// wall; declared under `single` they are refused here, the first moment the
1942+
// posture is known, rather than left looking enforced.
1943+
refuseAcceptedDefaultsWithoutWall(this.tenancyPosture, this.acceptedDefaultPermissionSets);
18981944
// [ADR-0105 D11] Optional app/plugin membership resolver for the §7.3.1
18991945
// `IN (current_user.<key>)` sets. Absent is the norm — probe once.
19001946
try {
@@ -2012,6 +2058,7 @@ export class SecurityPlugin implements Plugin {
20122058
metadata,
20132059
posture: () => this.tenancyPosture,
20142060
rederiveEveryone: () => this.declareEveryone(ctx.logger),
2061+
acceptedDefaultPermissionSets: this.acceptedDefaultPermissionSets,
20152062
logger: ctx.logger,
20162063
};
20172064
// Typed against the published contract so the registered surface cannot
@@ -4375,7 +4422,7 @@ export class SecurityPlugin implements Plugin {
43754422
if (evt?.type !== 'permission' || evt.state === 'draft') return;
43764423
void (async () => {
43774424
try {
4378-
await reconcileAudienceBindingSuggestions(ql, ctx.logger);
4425+
await reconcileAudienceBindingSuggestions(ql, ctx.logger, this.deploymentAcceptance());
43794426
} catch (e) {
43804427
ctx.logger.warn('[security] audience-binding suggestion sync failed (non-fatal)', { error: (e as Error).message });
43814428
}
@@ -4471,7 +4518,7 @@ export class SecurityPlugin implements Plugin {
44714518
// `manage_metadata` holder's decision — never auto-bound. ONE
44724519
// deployment-level pass, owned by the Default Organization (#22801 → A).
44734520
try {
4474-
await reconcileAudienceBindingSuggestions(ql, ctx.logger);
4521+
await reconcileAudienceBindingSuggestions(ql, ctx.logger, this.deploymentAcceptance());
44754522
} catch (e) {
44764523
ctx.logger.warn('[security] audience-binding suggestion sync failed (non-fatal)', { error: (e as Error).message });
44774524
}
@@ -4670,13 +4717,16 @@ export class SecurityPlugin implements Plugin {
46704717
logger.warn('[security] the everyone anchor does not name a baseline set — high-privilege bits', { set, offending });
46714718
});
46724719
try {
4673-
const derived = await readAcceptedEveryoneSets(ql, this.metadata);
4720+
const derived = await readAcceptedEveryoneSets(ql, this.metadata, this.deploymentAcceptance());
46744721
for (const name of derived.accepted) if (!sets.includes(name)) sets.push(name);
46754722
for (const w of derived.withheld) {
4676-
const line =
4677-
`[security] the everyone anchor does not carry the accepted set '${w.set}' (package '${w.packageId}'): ` +
4678-
`${w.reason}. Its acceptance stays recorded and grants nothing until the set is served again as ` +
4679-
'accepted; a manage_metadata holder can revoke it in Setup.';
4723+
const line = w.source === 'host-configuration'
4724+
? `[security] the everyone anchor does not carry the set '${w.set}' the security plugin's ` +
4725+
`\`acceptedDefaultPermissionSets\` option declares: ${w.reason}. It grants nothing while that holds; ` +
4726+
'remove the name from the option, or serve the set again as an isDefault set some package declares.'
4727+
: `[security] the everyone anchor does not carry the accepted set '${w.set}' (package '${w.packageId}'): ` +
4728+
`${w.reason}. Its acceptance stays recorded and grants nothing until the set is served again as ` +
4729+
'accepted; a manage_metadata holder can revoke it in Setup.';
46804730
if (w.forbidden) logger.error(line, undefined, { set: w.set, package: w.packageId });
46814731
else logger.warn(line, { set: w.set, package: w.packageId });
46824732
}

0 commit comments

Comments
 (0)