diff --git a/.changeset/20751-services-strings-stage5-state-the-decision.md b/.changeset/20751-services-strings-stage5-state-the-decision.md new file mode 100644 index 00000000000..1f52bb33350 --- /dev/null +++ b/.changeset/20751-services-strings-stage5-state-the-decision.md @@ -0,0 +1,19 @@ +--- +'@objectstack/plugin-security': patch +--- + +Security refusals, explain details, field help and log lines no longer cite tracker numbers; each one states the decision behind it in words + +Clause-②: no + +Some strings the security plugin shows to administrators, authors and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + +- The curated capability-name refusal says a curated name is refused at authoring so that no admin-authored row can collide with the row the platform seeds for it. +- The two delegation anchor refusals say the business-unit anchor roots the delegate's business-unit visibility, so a delegation may only narrow it. +- The `managed_by` field help on `sys_permission_set` and `sys_position`, in every shipped locale, says capabilities, permission sets and positions all share one platform / package / admin vocabulary. +- The explain details for an unresolvable security posture and for the View/Modify All Data bypass drop their citations; those sentences already said that access fails closed and that the write path consults the same bypass. +- The derived-capability boot warning says the derivation refreshes a row's label and description only when it can prove the row is the platform's own, and that the seeder neither adopts a row it cannot prove is its own nor backfills provenance on the operator's behalf. +- The fail-closed log lines say what each denial protects: a `controlled_by_parent` child is readable and writable only where its master is, and a chain the derivation cannot resolve admits no child; only a resolved sharing allow (Modify All Data or an edit-level share) may replace the platform ownership floor; an authored-policy verdict that cannot be resolved never lifts the sharing refusal; a path that bypasses the engine middleware never runs without the owner and share scope a direct read applies; a delegated read is never scoped wider than its delegator's own; an unreadable posture never defaults to public or uncontracted. +- The public-form line says an anonymous submission cannot set ownership, tenancy or audit columns; the uninstall line says a package's permission rows are removed by `package_id`, so no grant outlives the package; the platform-owner wall-bypass line says only the declared platform owner's reads cross the wall and writes stay walled for everyone. The org-scoping entitlement, masking-rule, permission-set resolution, vocabulary-normalization and service-registration lines drop their citations, and the log lines that carried a tracker number in their `[security/…]` prefix now open with `[security]`. + +Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix or prefix) needs the new spelling. diff --git a/packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts b/packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts index d0159ca82f2..937db60276e 100644 --- a/packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts +++ b/packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts @@ -746,14 +746,17 @@ export async function bootstrapSystemCapabilities( 'at all), so it most likely arrived as app seed data replayed per organization, or a legacy ' + 'import. Fix it AT ITS SOURCE: Setup cannot, because ADR-0066 asset ownership refuses every ' + 'admin-door edit and delete on a platform-stamped row. Note the platform bucket stays empty ' + - "either way — that is the #8552 posture for an occupied name, not a consequence of the stamp." + 'either way, and not because of the stamp: when a row the seeder cannot prove is its own ' + + 'already holds the name, the seeder declines rather than adopting that row or backfilling a stamp.' : " The organization's row is a supported extension (ADR-0066 D1 — admins EXTEND the " + 'registry), so there is nothing for an operator to remove; the platform bucket is left empty ' + - 'deliberately, and adopting or backfilling it was rejected in #8552.'; + 'deliberately: the seeder does not adopt a row it cannot prove is its own, and does not ' + + "backfill provenance on the operator's behalf."; options.logger?.warn?.( `[security] derived capability "${def.name}" has no platform placeholder and none was seeded. ` + `The row this pass found for the name is ${provenance} ${locality}, and its label and ` + - 'description were left as their author wrote them (#5876 — unchanged). In the platform ' + + 'description were left as their author wrote them (the derivation refreshes them only on a row ' + + "it can prove is the platform's own). In the platform " + `(NULL-organization) bucket, where the declared unique key admits one row per name: ${bucket}. ` + "The platform's own derived placeholder is therefore missing from sys_capability " + 'installation-wide. Grants and requiredPermissions referencing the name are unaffected — ' + diff --git a/packages/plugins/plugin-security/src/cleanup-package-permissions.ts b/packages/plugins/plugin-security/src/cleanup-package-permissions.ts index 1ea95a9bb02..aec1a55b523 100644 --- a/packages/plugins/plugin-security/src/cleanup-package-permissions.ts +++ b/packages/plugins/plugin-security/src/cleanup-package-permissions.ts @@ -192,7 +192,7 @@ export async function cleanupPackagePermissions( ); } if (out.sets + out.positionBindings + out.userGrants + out.suggestions > 0) { - logger?.info?.('[security] package permission rows revoked on uninstall (#2747)', { + logger?.info?.('[security] package permission rows revoked on uninstall — removed by package_id, so no grant outlives the package (ADR-0090 D5)', { packageId, ...out, }); } diff --git a/packages/plugins/plugin-security/src/default-report-sink.test.ts b/packages/plugins/plugin-security/src/default-report-sink.test.ts index 01e71531fc6..11cd651754e 100644 --- a/packages/plugins/plugin-security/src/default-report-sink.test.ts +++ b/packages/plugins/plugin-security/src/default-report-sink.test.ts @@ -77,7 +77,7 @@ describe('[#10556 (a)] SecurityPlugin default report sink', () => { expect(filter).toBeDefined(); expect(seen).toHaveLength(1); - expect(String(seen[0]?.[0])).toContain('denying (fail-closed, #2852)'); + expect(String(seen[0]?.[0])).toContain("denying (fail-closed: a delegated read is never scoped wider than its delegator's own)"); }); it('guarantees a `warn` channel on the default sink, and routes it to the console', () => { diff --git a/packages/plugins/plugin-security/src/delegated-admin-gate.ts b/packages/plugins/plugin-security/src/delegated-admin-gate.ts index 44f891d3293..8fb102aff56 100644 --- a/packages/plugins/plugin-security/src/delegated-admin-gate.ts +++ b/packages/plugins/plugin-security/src/delegated-admin-gate.ts @@ -621,8 +621,8 @@ export class DelegatedAdminGate { if (!allowed.has(rowAnchor)) { deny( allowed.size === 0 - ? `business unit anchor '${rowAnchor}' cannot be validated against your own '${positionName}' anchor — an anchor that cannot be proven within your own range is refused (cloud#830: anchoring only narrows)` - : `business unit anchor '${rowAnchor}' is outside your own effective anchor for '${positionName}' — a delegation may only narrow visibility, never widen it (cloud#830: anchoring only narrows)`, + ? `business unit anchor '${rowAnchor}' cannot be validated against your own '${positionName}' anchor — an anchor that cannot be proven within your own range is refused, because the anchor roots the delegate's business-unit visibility and may only narrow yours` + : `business unit anchor '${rowAnchor}' is outside your own effective anchor for '${positionName}' — a delegation may only narrow visibility, never widen it, because the anchor roots the delegate's business-unit visibility`, { position: positionName, businessUnitId: rowAnchor }, ); } diff --git a/packages/plugins/plugin-security/src/deployment-platform-global-exemption.test.ts b/packages/plugins/plugin-security/src/deployment-platform-global-exemption.test.ts index 50afb1681a0..6f09f0bacb1 100644 --- a/packages/plugins/plugin-security/src/deployment-platform-global-exemption.test.ts +++ b/packages/plugins/plugin-security/src/deployment-platform-global-exemption.test.ts @@ -160,7 +160,7 @@ describe('[#12699] platformGlobalObjects — the deployment carve-out', () => { }); // And nothing to refuse means nothing to warn about. const warned = logger.warn.mock.calls.map((c) => String(c[0])); - expect(warned.filter((m) => m.includes('#12699'))).toEqual([]); + expect(warned.filter((m) => m.includes('org-scoping entitlement key'))).toEqual([]); }); it('composes with, never replaces, the object-level channel: `tenancy.enabled:false` stays exempt with no deployment declaration', async () => { diff --git a/packages/plugins/plugin-security/src/explain-engine.ts b/packages/plugins/plugin-security/src/explain-engine.ts index 0690d812bac..872b2d32d09 100644 --- a/packages/plugins/plugin-security/src/explain-engine.ts +++ b/packages/plugins/plugin-security/src/explain-engine.ts @@ -1406,7 +1406,7 @@ async function applyRecordAttribution( rowFilter: null, rules: [], detail: `View/Modify All Data via [${vamaSets.join(', ')}] admits this record regardless of ownership — ` + - 'the same bypass the write path consults (#4647).', + 'the same bypass the write path consults.', } : { outcome: 'not_evaluated', rules: [], detail: 'No View/Modify All Data bypass applies to this record.' }; } @@ -1865,13 +1865,13 @@ export async function explainAccess(deps: ExplainEngineDeps, input: ExplainInput (delegatorVama ? ` AND by the delegator [${delegatorVama.join(', ')}]` : '') + ` — ownership and sharing checks are skipped` + (vamaBit === 'modify' - ? ` (Modify All Data: the write path consults this SAME bypass, #4647).` + ? ` (Modify All Data: the write path consults this SAME bypass).` : `.`) : agentVama.length > 0 && delegatorVama !== null && delegatorVama.length === 0 ? `Agent holds View/Modify All Data via [${agentVama.join(', ')}] but the DELEGATOR does not — D10 intersection strips the bypass.` : viewOnlySets.length > 0 ? `View All Data held via [${viewOnlySets.join(', ')}] does NOT bypass ownership for ${operation} — ` + - `a write bypass requires Modify All Data (modifyAllRecords), so ownership and sharing still decide (#4647).` + `a write bypass requires Modify All Data (modifyAllRecords), so ownership and sharing still decide.` : 'No View/Modify All Data bypass.', contributors: vamaEffective ? vamaSets.map((n) => ({ kind: 'permission_set' as const, name: n, via: viaOf(n) })) : [], }); diff --git a/packages/plugins/plugin-security/src/metadata-outage-unresolved-cause.test.ts b/packages/plugins/plugin-security/src/metadata-outage-unresolved-cause.test.ts index ef0ec747f97..d7680447cbd 100644 --- a/packages/plugins/plugin-security/src/metadata-outage-unresolved-cause.test.ts +++ b/packages/plugins/plugin-security/src/metadata-outage-unresolved-cause.test.ts @@ -347,7 +347,7 @@ describe('[#10424] the wording module states three distinct things', () => { expect(remedy).toContain('Do NOT change the declaration'); expect(remedy).toContain('NOT a permissions problem'); expect(unresolvedPostureExplainDetail('task', 'metadata_unavailable')).toContain('OUTAGE'); - expect(unresolvedPostureExplainDetail('task', 'metadata_unavailable')).toContain('#3545'); + expect(unresolvedPostureExplainDetail('task', 'metadata_unavailable')).toContain('rather than defaulting to public/uncontracted'); }); it('the outage denial keeps the pinned opening clause verbatim', () => { @@ -362,7 +362,7 @@ describe('[#10424] the wording module states three distinct things', () => { for (const c of causes) { expect(unresolvedPostureDenialMessage('task', 'find', c)).toContain('[Security] Access denied:'); expect(unresolvedPostureExplainDetail('task', c)).toContain('fails CLOSED'); - expect(unresolvedPostureLogLine('task', 'find', 'u1', c)).toContain('fail-closed, #3545'); + expect(unresolvedPostureLogLine('task', 'find', 'u1', c)).toContain('fail-closed: an unreadable posture never defaults to public'); } }); }); diff --git a/packages/plugins/plugin-security/src/normalize-managed-by.ts b/packages/plugins/plugin-security/src/normalize-managed-by.ts index 9a6baa7a2d7..e3f38ae0526 100644 --- a/packages/plugins/plugin-security/src/normalize-managed-by.ts +++ b/packages/plugins/plugin-security/src/normalize-managed-by.ts @@ -187,7 +187,7 @@ export async function normalizeManagedByVocab( const permissionSets = await normalizeObject(ql, 'sys_permission_set', PERMISSION_SET_MAP, options.logger); const total = positions + permissionSets; if (total > 0) { - options.logger?.info?.('[security] managed_by vocab normalized to platform/package/admin (A4 #2920)', { + options.logger?.info?.('[security] managed_by vocab normalized to platform/package/admin, the one vocabulary every RBAC catalog shares', { positions, permissionSets, }); diff --git a/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts b/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts index 55c7e41edc2..569eefef3a3 100644 --- a/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts @@ -333,7 +333,8 @@ export const SysPermissionSet = ObjectSchema.create({ readonly: true, defaultValue: 'admin', description: - "Record provenance (unified tri-state, A4 #2920): 'platform' = shipped by the " + + "Record provenance, on the one platform / package / admin vocabulary that capabilities, " + + "permission sets and positions all share: 'platform' = shipped by the " + "platform; 'package' = versioned package metadata (re-seeded on upgrade, read-mostly " + "for admins); 'admin' = created/owned in this environment by an administrator " + "(live-edited, never touched by package seeding). Legacy rows may carry 'user' (== admin).", diff --git a/packages/plugins/plugin-security/src/objects/sys-position.object.ts b/packages/plugins/plugin-security/src/objects/sys-position.object.ts index 97a478c4c8e..a81b163ba8a 100644 --- a/packages/plugins/plugin-security/src/objects/sys-position.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-position.object.ts @@ -285,7 +285,8 @@ export const SysPosition = ObjectSchema.create({ readonly: true, defaultValue: 'admin', description: - 'Record provenance (unified tri-state, A4 #2920): platform = framework built-in ' + + 'Record provenance, on the one platform / package / admin vocabulary that capabilities, ' + + 'permission sets and positions all share: platform = framework built-in ' + '(read-only) / package = stack/package-declared / admin = tenant-created. Legacy rows ' + 'may carry system (== platform) / config (== package) / user (== admin).', options: [ diff --git a/packages/plugins/plugin-security/src/permission-evaluator.ts b/packages/plugins/plugin-security/src/permission-evaluator.ts index 44c0697a2cb..1db23dbe989 100644 --- a/packages/plugins/plugin-security/src/permission-evaluator.ts +++ b/packages/plugins/plugin-security/src/permission-evaluator.ts @@ -508,7 +508,7 @@ export class PermissionEvaluator { } catch (e) { allPermSets = []; options.logger?.warn?.( - '[security] permission-set metadata list() failed — falling back to bootstrap/db sources (#2565)', + '[security] permission-set metadata list() failed — falling back to bootstrap/db sources', { requested: identifiers, error: (e as Error)?.message }, ); } @@ -555,7 +555,7 @@ export class PermissionEvaluator { // DB error silently drops custom permission sets and the // resulting 403s point nowhere near the cause (#2565). options.logger?.warn?.( - '[security] sys_permission_set db lookup failed — unresolved sets grant nothing this request (#2565)', + '[security] sys_permission_set db lookup failed — unresolved sets grant nothing this request', { unresolved, error: (e as Error)?.message }, ); } diff --git a/packages/plugins/plugin-security/src/security-plugin.ts b/packages/plugins/plugin-security/src/security-plugin.ts index b58073a52d9..4758c98b7fd 100644 --- a/packages/plugins/plugin-security/src/security-plugin.ts +++ b/packages/plugins/plugin-security/src/security-plugin.ts @@ -1034,7 +1034,7 @@ export class SecurityPlugin implements Plugin { if (this.warnedEntitlementRefusals.has(refusal.problem)) continue; this.warnedEntitlementRefusals.add(refusal.problem); this.logger?.warn?.( - `[security/#12699] org-scoping entitlement key '${refusal.key}' REFUSED — ${refusal.problem}`, + `[security] org-scoping entitlement key '${refusal.key}' REFUSED — ${refusal.problem}`, { key: refusal.key, declared: refusal.value }, ); } @@ -1638,14 +1638,14 @@ export class SecurityPlugin implements Plugin { const entitlement = this.deploymentOrgScopingEntitlement(); if (entitlement.platformGlobalObjects.size > 0) { ctx.logger.info( - `[security/#12699] deployment declares ${entitlement.platformGlobalObjects.size} platform-global ` + + `[security] deployment declares ${entitlement.platformGlobalObjects.size} platform-global ` + `object(s) — Layer 0 does not wall them on THIS deployment`, { objects: [...entitlement.platformGlobalObjects].sort() }, ); } if (entitlement.suppressUnboundedOrgAdminGrant) { ctx.logger.info( - '[security/#12699] deployment suppresses the unbounded organization_admin auto-grant — ' + + '[security] deployment suppresses the unbounded organization_admin auto-grant — ' + 'membership-driven grants hand out organization_admin_no_bypass under this walled posture', ); } @@ -2101,7 +2101,7 @@ export class SecurityPlugin implements Plugin { discardPermissionSetOverlay(overlayDiscardDeps, callerContext, id), }); ctx.registerService('security', registeredSecurityService); - ctx.logger.info('[security] registered "security" service (getReadFilter, canReadObject, getReadableFields, getWritableFields, getQueryableFields, getMetadataReadableFields, canExport, checkAuthoredRowWrite, resolvePermissionSetNames, resolvePermissionSetsForContext, explain, audience-binding suggestions, discardPermissionSetOverlay) — ADR-0021 D-C / ADR-0090 D5/D6/D9 / ADR-0094 / ADR-0106 D7 / #3544 / #3547 / #5493 / #7616'); + ctx.logger.info('[security] registered "security" service (getReadFilter, canReadObject, getReadableFields, getWritableFields, getQueryableFields, getMetadataReadableFields, canExport, checkAuthoredRowWrite, resolvePermissionSetNames, resolvePermissionSetsForContext, explain, audience-binding suggestions, discardPermissionSetOverlay) — ADR-0021 D-C / ADR-0090 D5/D6/D9 / ADR-0094 / ADR-0106 D7'); } catch (e) { ctx.logger.warn?.('[security] failed to register "security" service', { error: (e as Error).message, @@ -2213,7 +2213,8 @@ export class SecurityPlugin implements Plugin { if (stripped.size > 0) { ctx.logger.warn( `[security] public-form insert on '${grantObject}' supplied server-managed ` + - `field(s) [${[...stripped].join(', ')}] — stripped (#3022)`, + `field(s) [${[...stripped].join(', ')}] — stripped: an anonymous form submission cannot set ` + + `ownership, tenancy or audit columns`, ); } } @@ -5144,7 +5145,8 @@ export class SecurityPlugin implements Plugin { this.logger.error?.( `[security] controlled_by_parent write gate could not resolve the sharing (OWD) edit ` + `check for '${object}' record '${recordId}' (user ${context?.userId ?? 'unknown'}) — ` + - `denying (fail-closed, #5386)`, + `denying (fail-closed: a child is writable only where its master is, so a master check that ` + + `cannot be resolved refuses)`, e instanceof Error ? e : new Error(String(e)), ); return false; @@ -5243,7 +5245,8 @@ export class SecurityPlugin implements Plugin { this.logger.error?.( `[security] the row-level write gate could not resolve the sharing (${method}) verdict ` + `for '${object}' record '${recordId}' (user ${context?.userId ?? 'unknown'}) — keeping ` + - `the platform ownership floor (fail-closed, #5492)`, + `the platform ownership floor (fail-closed: only a resolved sharing allow, from Modify All Data ` + + `or an edit-level share, may replace that floor)`, e instanceof Error ? e : new Error(String(e)), ); return 'deny'; @@ -5442,7 +5445,7 @@ export class SecurityPlugin implements Plugin { this.logger.warn?.( `[security] checkAuthoredRowWrite could not resolve an authored-policy verdict for ` + `'${object}' record '${recordId}' (${operation}, user ${context?.userId ?? 'unknown'}) — ` + - `abstaining (fail-closed, #5493)`, + `abstaining (fail-closed: a verdict that cannot be resolved never lifts the sharing refusal)`, e instanceof Error ? e : new Error(String(e)), ); return 'abstain'; @@ -5500,7 +5503,8 @@ export class SecurityPlugin implements Plugin { } catch (e) { this.logger.error?.( `[security] getReadFilter could not resolve the sharing (OWD) read scope for object ` + - `'${object}' (user ${context?.userId ?? 'unknown'}) — denying (fail-closed, #4467)`, + `'${object}' (user ${context?.userId ?? 'unknown'}) — denying (fail-closed: a path that bypasses ` + + `the engine middleware never runs without the owner and share scope a direct read applies)`, e instanceof Error ? e : new Error(String(e)), ); return { ...RLS_DENY_FILTER }; @@ -5537,7 +5541,8 @@ export class SecurityPlugin implements Plugin { `[security] getReadFilter received an on-behalf-of context for object ` + `'${object}' (agent ${context?.userId ?? 'unknown'} on behalf of ` + `${context.onBehalfOf.userId}) — the D10 delegator intersection is not ` + - `implemented on the read-scope path; denying (fail-closed, #2852)`, + `implemented on the read-scope path; denying (fail-closed: a delegated read is never scoped ` + + `wider than its delegator's own)`, ); return { ...RLS_DENY_FILTER }; } @@ -7048,7 +7053,9 @@ export class SecurityPlugin implements Plugin { `[Security] Access denied: '${name}' is a platform-curated capability name — a sys_capability ` + `row cannot be created with it or renamed to it through the admin door. The platform defines ` + `this capability and seeds its own row for it; grants and requiredPermissions already resolve ` + - `the name. Choose a different capability name (ADR-0066 asset ownership, #8552).`, + `the name. A curated name is refused at authoring so that no admin-authored row can collide ` + + `with the row the platform seeds for it. Choose a different capability name (ADR-0066 asset ` + + `ownership).`, { operation: op, object: opCtx.object, name, curated: true }, ); }; @@ -7661,8 +7668,9 @@ export class SecurityPlugin implements Plugin { // `sys_audit_log` ledger is deliberately not the sink here. Named after // the cloud precedent (`cross_org_admin_read`). this.logger.warn?.( - `[security/#12974] ${PLATFORM_OWNER_WALL_BYPASS_EVENT}: verified platform owner crossed ` + - 'the Layer 0 organization wall — the org filter below was NOT appended', + `[security] ${PLATFORM_OWNER_WALL_BYPASS_EVENT}: verified platform owner crossed ` + + 'the Layer 0 organization wall on a read — the org filter below was NOT appended (only the ' + + "declared platform owner's reads cross it; writes stay walled for everyone)", { event: PLATFORM_OWNER_WALL_BYPASS_EVENT, object, @@ -8241,7 +8249,8 @@ export class SecurityPlugin implements Plugin { if (ancestors.includes(object)) { this.logger.error?.( `[security] controlled_by_parent derivation found a CYCLE resolving '${object}' ` + - `(chain: ${[...ancestors, object].join(' -> ')}) — denying (fail-closed, #11082)`, + `(chain: ${[...ancestors, object].join(' -> ')}) — denying (fail-closed: a chain the derivation ` + + `cannot resolve admits no child rather than leaving it unrestricted)`, ); return { [rel.fk]: { $in: [] } }; } @@ -8249,7 +8258,8 @@ export class SecurityPlugin implements Plugin { this.logger.error?.( `[security] controlled_by_parent derivation exceeded the chain depth bound ` + `(${CBP_MAX_CHAIN_DEPTH}) resolving '${object}' ` + - `(chain: ${[...ancestors, object].join(' -> ')}) — denying (fail-closed, #11082)`, + `(chain: ${[...ancestors, object].join(' -> ')}) — denying (fail-closed: a chain the derivation ` + + `cannot resolve admits no child rather than leaving it unrestricted)`, ); return { [rel.fk]: { $in: [] } }; } @@ -8266,7 +8276,8 @@ export class SecurityPlugin implements Plugin { this.logger.error?.( `[security] controlled_by_parent derivation could not resolve the sharing (OWD) read ` + `scope of master '${rel.master}' for '${object}' (user ${context?.userId ?? 'unknown'}) ` + - `— denying (fail-closed, #5386)`, + `— denying (fail-closed: a child is readable only where its master is, so a master scope that ` + + `cannot be resolved admits no child)`, e instanceof Error ? e : new Error(String(e)), ); return { [rel.fk]: { $in: [] } }; @@ -8901,7 +8912,7 @@ export class SecurityPlugin implements Plugin { // FULL mask, never to the unmasked value. (The spec parse rejects such // declarations at authoring; this covers rows that arrived around it.) this.logger?.warn?.( - `[security/#8993] field '${object}.${fname}' declares an invalid maskingRule — ` + + `[security] field '${object}.${fname}' declares an invalid maskingRule — ` + `applying a full mask (fail-closed). Fix the declaration to a preset ` + `('phone' | 'id_card' | 'bank_account' | 'email' | 'name') or {keepHead, keepTail}.`, { object, field: fname }, diff --git a/packages/plugins/plugin-security/src/translations/en.objects.generated.ts b/packages/plugins/plugin-security/src/translations/en.objects.generated.ts index 0d7ac44750f..80a11376e64 100644 --- a/packages/plugins/plugin-security/src/translations/en.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/en.objects.generated.ts @@ -43,7 +43,7 @@ export const enObjects: NonNullable = { }, managed_by: { label: "Managed By", - help: "Record provenance (unified tri-state, A4 #2920): platform = framework built-in (read-only) / package = stack/package-declared / admin = tenant-created. Legacy rows may carry system (== platform) / config (== package) / user (== admin).", + help: "Record provenance, on the one platform / package / admin vocabulary that capabilities, permission sets and positions all share: platform = framework built-in (read-only) / package = stack/package-declared / admin = tenant-created. Legacy rows may carry system (== platform) / config (== package) / user (== admin).", options: { platform: "Platform", package: "Package", @@ -230,7 +230,7 @@ export const enObjects: NonNullable = { }, managed_by: { label: "Managed By", - help: "Record provenance (unified tri-state, A4 #2920): 'platform' = shipped by the platform; 'package' = versioned package metadata (re-seeded on upgrade, read-mostly for admins); 'admin' = created/owned in this environment by an administrator (live-edited, never touched by package seeding). Legacy rows may carry 'user' (== admin).", + help: "Record provenance, on the one platform / package / admin vocabulary that capabilities, permission sets and positions all share: 'platform' = shipped by the platform; 'package' = versioned package metadata (re-seeded on upgrade, read-mostly for admins); 'admin' = created/owned in this environment by an administrator (live-edited, never touched by package seeding). Legacy rows may carry 'user' (== admin).", options: { platform: "Platform", package: "Package", diff --git a/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts b/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts index c3d180f3dd7..1eb61479a38 100644 --- a/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts @@ -43,7 +43,7 @@ export const esESObjects: NonNullable = { }, managed_by: { label: "Gestionado por", - help: "Procedencia del registro: platform (integrado) / package (declarado) / admin (creado por el inquilino).", + help: "Procedencia del registro, en el único vocabulario platform / package / admin que comparten capacidades, conjuntos de permisos y puestos: platform (integrado) / package (declarado) / admin (creado por el inquilino).", options: { platform: "Plataforma", package: "Paquete", @@ -230,7 +230,7 @@ export const esESObjects: NonNullable = { }, managed_by: { label: "Gestionado por", - help: "Procedencia del registro: platform (distribuido) / package (empaquetado) / admin (creado en el entorno).", + help: "Procedencia del registro, en el único vocabulario platform / package / admin que comparten capacidades, conjuntos de permisos y puestos: platform (distribuido) / package (empaquetado) / admin (creado en el entorno).", options: { platform: "Plataforma", package: "Paquete", diff --git a/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts b/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts index efc9e96d117..50c49f10117 100644 --- a/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts @@ -43,7 +43,7 @@ export const jaJPObjects: NonNullable = { }, managed_by: { label: "管理元", - help: "レコードの出所:platform(組み込み)/ package(宣言済み)/ admin(テナント作成)。", + help: "レコードの出所(ケイパビリティ・権限セット・ポジションで共通の platform / package / admin の語彙):platform(組み込み)/ package(宣言済み)/ admin(テナント作成)。", options: { platform: "プラットフォーム", package: "パッケージ", @@ -230,7 +230,7 @@ export const jaJPObjects: NonNullable = { }, managed_by: { label: "管理元", - help: "レコードの出所:platform(配布)/ package(パッケージ)/ admin(環境作成)。", + help: "レコードの出所(ケイパビリティ・権限セット・ポジションで共通の platform / package / admin の語彙):platform(配布)/ package(パッケージ)/ admin(環境作成)。", options: { platform: "プラットフォーム", package: "パッケージ", diff --git a/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts b/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts index 13f3fc0b13b..439c35421c9 100644 --- a/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts @@ -43,7 +43,7 @@ export const zhCNObjects: NonNullable = { }, managed_by: { label: "管理来源", - help: "记录来源:platform(平台内置)/ package(应用包声明)/ admin(租户创建)。", + help: "记录来源(能力、权限集与岗位共用同一套 platform / package / admin 取值):platform(平台内置)/ package(应用包声明)/ admin(租户创建)。", options: { platform: "平台", package: "应用包", @@ -230,7 +230,7 @@ export const zhCNObjects: NonNullable = { }, managed_by: { label: "管理来源", - help: "记录来源:platform(平台发布)/ package(应用包发布)/ admin(环境自建)。", + help: "记录来源(能力、权限集与岗位共用同一套 platform / package / admin 取值):platform(平台发布)/ package(应用包发布)/ admin(环境自建)。", options: { platform: "平台", package: "应用包", diff --git a/packages/plugins/plugin-security/src/unresolved-posture.ts b/packages/plugins/plugin-security/src/unresolved-posture.ts index c168612a3d9..e96859b9f88 100644 --- a/packages/plugins/plugin-security/src/unresolved-posture.ts +++ b/packages/plugins/plugin-security/src/unresolved-posture.ts @@ -194,17 +194,17 @@ export function unresolvedPostureExplainDetail( const unknown = `The security posture of '${object}' could not be resolved (neither the live schema nor the ` + `metadata service returned it) — its 'private' flag and required-capability contract are ` - + `unknown, so access fails CLOSED rather than defaulting to public/uncontracted (#3545). ${remedy}`; + + `unknown, so access fails CLOSED rather than defaulting to public/uncontracted. ${remedy}`; switch (cause) { case 'unpublished_draft': return `'${object}' is not published — a draft declaration exists but no published one, so its 'private' ` + `flag and required-capability contract are unknown and access fails CLOSED rather than ` - + `defaulting to public/uncontracted (#3545). ${remedy}`; + + `defaulting to public/uncontracted. ${remedy}`; case 'metadata_unavailable': return `The security posture of '${object}' could not be resolved: the metadata service reported its ` + `own read as DEGRADED, so this is a metadata-store OUTAGE and not necessarily an absent ` + `declaration. Its 'private' flag and required-capability contract are unknown, so access fails ` - + `CLOSED rather than defaulting to public/uncontracted (#3545). ${remedy}`; + + `CLOSED rather than defaulting to public/uncontracted. ${remedy}`; case 'unknown': return unknown; default: @@ -226,11 +226,11 @@ export function unresolvedPostureLogLine( ): string { const unknown = `[security] object security posture unresolvable for operation '${operation}' on ` - + `object '${object}' (user ${userId}) — denying request (fail-closed, #3545)`; + + `object '${object}' (user ${userId}) — denying request (fail-closed: an unreadable posture never defaults to public or uncontracted)`; switch (cause) { case 'unpublished_draft': return `[security] object '${object}' has a DRAFT declaration and no published one — denying operation ` - + `'${operation}' (user ${userId}) with the unpublished-object refusal (fail-closed, #3545/#10401)`; + + `'${operation}' (user ${userId}) with the unpublished-object refusal (fail-closed: an unreadable posture never defaults to public or uncontracted)`; // [#10424] The operator-facing half of the split, and the reason the log // line is worth changing at all: a metadata-store outage is an INCIDENT and // a query against a missing object is routine, and they were the same line. @@ -239,7 +239,7 @@ export function unresolvedPostureLogLine( return `[security] object security posture unresolvable for operation '${operation}' on ` + `object '${object}' (user ${userId}) — the metadata service reported a DEGRADED read, i.e. a ` + `metadata-store OUTAGE rather than an absent declaration — denying request ` - + `(fail-closed, #3545/#10424)`; + + `(fail-closed: an unreadable posture never defaults to public or uncontracted)`; case 'unknown': return unknown; default: diff --git a/scripts/doc-authoring-prose-id.baseline.json b/scripts/doc-authoring-prose-id.baseline.json index 8776c095746..1d4adefea34 100644 --- a/scripts/doc-authoring-prose-id.baseline.json +++ b/scripts/doc-authoring-prose-id.baseline.json @@ -2,55 +2,6 @@ "packages/plugins/plugin-audit/src/audit-writers.ts": { "#5226": 1 }, - "packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts": { - "#5876": 1, - "#8552": 2 - }, - "packages/plugins/plugin-security/src/cleanup-package-permissions.ts": { - "#2747": 1 - }, - "packages/plugins/plugin-security/src/delegated-admin-gate.ts": { - "#830": 2 - }, - "packages/plugins/plugin-security/src/explain-engine.ts": { - "#4647": 3 - }, - "packages/plugins/plugin-security/src/normalize-managed-by.ts": { - "#2920": 1 - }, - "packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts": { - "#2920": 1 - }, - "packages/plugins/plugin-security/src/objects/sys-position.object.ts": { - "#2920": 1 - }, - "packages/plugins/plugin-security/src/permission-evaluator.ts": { - "#2565": 2 - }, - "packages/plugins/plugin-security/src/security-plugin.ts": { - "#11082": 2, - "#12699": 3, - "#12974": 1, - "#2852": 1, - "#3022": 1, - "#3544": 1, - "#3547": 1, - "#4467": 1, - "#5386": 2, - "#5492": 1, - "#5493": 2, - "#7616": 1, - "#8552": 1, - "#8993": 1 - }, - "packages/plugins/plugin-security/src/translations/en.objects.generated.ts": { - "#2920": 2 - }, - "packages/plugins/plugin-security/src/unresolved-posture.ts": { - "#10401": 1, - "#10424": 1, - "#3545": 6 - }, "packages/plugins/plugin-sharing/src/share-link-service.ts": { "#5190": 1 },