Skip to content

Commit 915e940

Browse files
feat(plugin-security): under a walled posture, raise the access upgrade report in-app in the organization OS_PLATFORM_OWNER_ORGANIZATION names (#25489)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuUSKmaWjm1sW7kkQuwKewbx)_ Fixes #25413 Clause-②: yes (widening) Metadata-protocol: none Claim 6367880115 (`domain:services` seat 1, #6021). The branch is the harness-pinned `claude/project-thread-mfvys8`. It names no issue, so the claim is the identity record for it. #24447 (claim 6367925751) follows on this branch after this PR merges, because its report calls the helper added here. ## What changed Under `group` and `isolated`, the boot access upgrade report reached the operator only as one `error` line in the server log (option A, ruling 6353787407). The maintainer chose to configure the owner's organization the way the owner's email is configured. - **`@objectstack/types`:** `PLATFORM_OWNER_ORGANIZATION_ENV` (`OS_PLATFORM_OWNER_ORGANIZATION`) and `resolvePlatformOwnerOrganization()`, beside the `OS_PLATFORM_OWNER_EMAIL` pair. They use the same trim and blank rule. - **`plugin-security`, new `upgrade-report-delivery.ts`:** the delivery of a boot upgrade report, moved out of `access-upgrade-report.ts` so #24447 can call it (Wanted 6). - **`single`:** unchanged and byte-identical. The notification is stamped with the Default Organization and goes to its `admin_full_access` holders plus the operators. The four "reached no administrator" causes are kept. - **Walled, setting unset:** the operator line only, worded as before (Wanted 5). - **Walled, setting set:** the value is resolved in `sys_organization` by id, then by slug. The same `security.access_upgrade` notification is emitted, with the same `dedupKey`, stamped with that organization and addressed to the configured operators. The operator line stays beside it and says a copy went in-app (Wanted 2). - **Set, naming no organization:** one `error` naming the value and the fix, then the operator line. Nothing is raised in-app and there is no fallback (Wanted 3). An unreadable `sys_organization`, a missing messaging service, no configured operator, or a refused emit each get the same shape with their own cause. - **Visibility warnings (Wanted 3 and 4):** - A configured operator who is not a verified member of that organization gets a `warn`, with a state of `no-account`, `unverified` or `not-member`. - An administrator of it who is not a configured operator gets a `warn`. An administrator here is an `owner`/`admin` membership grade, or an `admin_full_access` / `organization_admin` / `organization_admin_no_bypass` grant scoped to it. - The check costs two `find`s (`sys_member` and grants, by organization) plus the platform-admin standing read the bootstrap already uses. - A read failure gets one `warn`, and the copy still goes. - **`access-upgrade-report.ts`:** `reportAccessUpgrade` hands its finding set to the helper. The comparison, the per-category log lines and the payload are unchanged. `AccessUpgradeNotifier` and `AccessUpgradeReportLogger` stay exported as aliases, so `security-plugin.ts` is not touched (the #25223 split series edits it). - **`auto-org-admin-grant.ts`:** `isAdminRole` is exported. It is the existing owner/admin grade test, reused by the warning instead of copied. - **Docs:** a row in `content/docs/deployment/environment-variables.mdx` beside `OS_PLATFORM_OWNER_EMAIL`. It says the organization must hold the operators only, and why. Not touched: `security-plugin.ts`, `packages/core`, `packages/spec`, service-messaging, the ADR. ## Tests - **`upgrade-report-delivery.test.ts` (new, 17 cases), each with a CONTROL:** - resolved by id, by slug, and with stray spaces - unset (the exact unchanged line) and blank - an unknown value, with its CONTROL once the organization exists - an unreadable `sys_organization`, no messaging, a refused emit, no operator - a non-member operator; no-account and unverified operators - an administrator by grade and by grant, with the CONTROL that a plain member and the tenant's own administrator are not flagged - an unreadable `sys_member` - `single` with the setting set and unset - **`access-upgrade-report.wiring.test.ts`:** a real ObjectQL + SqlDriver + `SecurityPlugin`. It covers `isolated` and `group` with the setting naming the operator's dedicated organization (one emit, stamped with it, audience the operator, operator line beside it, and the same `dedupKey` on the next boot), and `isolated` with an unknown value. The existing `single` / walled / CONTROL cases pass unchanged. - **`packages/types/src/env.test.ts`:** spelling, unset and blank, and a trimmed id or slug. ## Verification - **Package suites:** - plugin-security `pnpm test`: exit 0, 215 files, 4485 passed and 45 skipped. - types `env.test.ts`: 58 passed. - `pnpm typecheck` for plugin-security (tsc, scripts and test-typecheck) and for types: exit 0. - eslint on the touched files: exit 0. - **Dispatch gates:** all 100 commands `dispatch-gates --commands` derived exit 0. Two were re-run after a fix: - `check-plugin-teardown-shape --self-test` exited 3 in a shallow clone. After `git fetch --unshallow` it exits 0. - `check:where-matcher` flagged the new test double. The double now refuses a combinator, and the gate exits 0. - `check-governed-merges --branch`: not governed, 1,100 changed lines. - **Ablations** (committed source, mutated with `scripts/ablation-replace.mjs`, restored to HEAD, `git diff HEAD` empty): 1. Walled branch always takes the unset path: 16 cases go red (13 helper, 3 wiring). 2. Membership warning off: 2 go red. 3. Admin grade not read: 1 goes red. 4. Slug lookup off: 4 go red (2 helper, and the isolated and group wiring cases). - **R1:** `OS_LOG_LEVEL=info pnpm dev -- --fresh` on the showcase logs `[security] access upgrade report: every activation ledger row … matches the registry at this boot {"absent":[]}`, with no ERROR line. - **R4:** production `os serve` of the showcase on a persistent SQLite file, under `isolated` and `group`. Rig details: - environment: `OS_PLATFORM_OWNER_EMAIL=op@r4.example` and `OS_AUTH_MEMBERSHIP_POLICY=invite-only` - rows written directly: the operator as a verified `sys_user`, `owner` of a dedicated organization `r4-platform-ops`, a tenant organization beside it, and the customer's switch-off of `showcase_auditor` (`com.example.showcase`, `active=0`) - the vendor rename was a temporary source edit to `showcase_audit_reader`, with the `auditor` position binding it, reverted afterwards | boot | isolated | group | |:--|:--|:--| | clean | nothing reported | nothing reported | | CONTROL (name still shipped, setting set) | nothing reported, no `sys_notification` | same | | rename, setting `r4-platform-ops` | re-enablement `ERROR`, then the operator line with "A copy is raised in-app …" and `organizationId: org_r4_platform`. One `sys_notification` row (`organization_id` `org_r4_platform`, dedup key `security.access_upgrade:a9b3…`), one inbox delivery and one `sys_inbox_message` row for `usr_r4_operator` in that organization. No row in any other organization, no refused write, no "reached no administrator". | same | | rename again | `[messaging] emit: dedupKey … already emitted; skipping`, still one row | same | | setting `r4-platform-opz` (unknown) | one `ERROR … "r4-platform-opz" names no organization by id or by slug … Fix: …`, then the unset-form operator line | not run | | setting unset | the unset-form operator line only, no line naming the setting | not run | | an `admin` member added to the operator's organization | `WARN … 1 administrator(s) … are not configured operators …` | not run | | setting pointing at the tenant organization | `WARN … 1 configured operator(s) are not verified members … state not-member` | not run | The only other ERROR line on every rig boot is `[auth] no_sign_in_account_at_boot`. The rig caused it by inserting `sys_user` directly, and it is present on the CONTROL boot too. The WARN lines match the CONTROL boot's. --- _Generated by [Claude Code](https://claude.ai/code/session_01XHebWJmL3ZgbVpjM6JERNA)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01XHebWJmL3ZgbVpjM6JERNA Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4c8f40a commit 915e940

9 files changed

Lines changed: 936 additions & 164 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/types': patch
3+
'@objectstack/plugin-security': patch
4+
---
5+
6+
feat: under a walled posture, `OS_PLATFORM_OWNER_ORGANIZATION` raises the access upgrade report in-app in the platform operators' own organization, beside the operator log line
7+
8+
Clause-②: yes (widening: a new optional environment variable, `PLATFORM_OWNER_ORGANIZATION_ENV` and `resolvePlatformOwnerOrganization()` exported from `@objectstack/types`; unset, every posture behaves as before)
9+
Metadata-protocol: none
10+
11+
Under `group` and `isolated` the boot access upgrade report reached the operator only as one `error` line in the server log. The notification family is organization-owned, a walled system write that names no organization is refused (ADR-0131 D9), and the operator belongs to no organization by default.
12+
13+
- **New setting:** `OS_PLATFORM_OWNER_ORGANIZATION`, the id or slug of the operators' own organization, beside `OS_PLATFORM_OWNER_EMAIL`. It is read only under a walled posture.
14+
- **What it does:** with it set, the report emits the same `security.access_upgrade` notification, with the same `dedupKey` per finding set, stamped with that organization and addressed to the `OS_PLATFORM_OWNER_EMAIL` operators. It lands in that organization's Notification Events and the operators' inbox, and in no other organization. The operator line stays beside it.
15+
- **Use a dedicated organization that holds the operators only.** Every administrator of it can read the notification in Setup, and the findings can name other organizations' ids and rule names. Boot logs a `warn` for a configured operator who is not a verified member of it. It logs another `warn` for an administrator of it who is not a configured operator: an `owner` or `admin` member, or an `admin_full_access` or organization-admin grant scoped to it.
16+
- **A value naming no organization** logs one `error` naming the value and the fix, and raises nothing in-app. It never falls back to another organization.
17+
- **Unchanged:** unset, the operator line is the only channel, worded as before. Under `single` the setting is ignored, and the Default Organization's administrators are told as before.
18+
19+
The walled and `single` delivery now lives in one helper, `upgrade-report-delivery.ts`, so the non-access upgrade report (#24447) can raise its findings the same way.

‎content/docs/deployment/environment-variables.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Read by `@objectstack/service-print` when `PrintServicePlugin` is composed; see
102102
| `OS_MULTI_ORG_ENABLED` | boolean | `false` | Superseded by `OS_TENANCY_POSTURE`, and still honoured: `true` selects the `isolated` posture. When `true`, organization creation/switching UI is exposed. |
103103
| `OS_AUTOMATION_SCHEDULED_WORK_ENABLED` | boolean | `false` | Whether this deployment runs **package-authored scheduled work**: time-triggered flows (a `type: 'schedule'` flow with a `config.schedule` cadence, and the `timeRelative` sweep) and packaged `defineJob` cron jobs. **OFF by default in every posture and every kernel** — a clock-driven workload's cost is a fact about the deployment, not about the flow, so an author cannot decide it and no metadata key asks them to. Accepts `true`/`1`/`on`/`yes` (case-insensitive); anything else, including unset, is off. While it is off nothing arms, and every such flow is listed by `getTriggerBindingAudit()` and the CLI startup summary as **disabled by deployment policy** — never as a binding failure, and nothing about the flow needs fixing. While it is on, what a time-triggered flow owes depends on the posture: under `isolated` it must declare `config.organization` on its start node or it is not armed; under `group` the declaration is optional and an undeclared sweep reads group-wide with each run acting as its own swept record's organization (a record-less cron there carries none, and its tenant-scoped writes are refused — declare one if it writes); under `single` it needs no declaration and its runs carry no organization. ⛔ Platform-internal scheduled work — approvals escalation, the lifecycle Reaper, the messaging dispatch loop, membership backfill — is **not** gated by this: the boundary is *authored by a package*, not *runs on the job service*. `os doctor` prints the effective value. |
104104
| `OS_PLATFORM_OWNER_EMAIL` | csv | — | The deployment's platform administrators: one email address, or a comma-separated list. A caller resolves `PLATFORM_ADMIN` when their own stored `sys_user` row carries a declared address **and** reads email-verified. Comparison is trimmed and case-insensitive; duplicates collapse and blank entries are dropped. One **unparseable** entry refuses the **whole** variable rather than just that entry — the deployment then has zero configured administrators, loudly — because a silently narrower administrator set is the worse failure. **Required under the walled postures** (`group` / `isolated`): unset or blank there **refuses to boot**, since the first self-registrant is not promoted and no grant row is written. Unset under `single` is normal — that posture still promotes the first human account. Read live per resolution, so revocation is a config change plus a process reload; there is no runtime endpoint that changes it. See [First boot: create the admin](/docs/deployment/self-hosting#first-boot-create-the-admin). |
105+
| `OS_PLATFORM_OWNER_ORGANIZATION` | string | — | Walled postures only (`group` / `isolated`): the id or slug of the platform operators' own organization. The boot upgrade reports raise deployment-level findings, such as the access upgrade report, as one `error` line addressed to the operator. With this set they are also raised in-app: one notification stamped with that organization and addressed to the `OS_PLATFORM_OWNER_EMAIL` operators, deduplicated per finding set across boots. No other organization is told. **Use a dedicated organization that holds the operators only**: every administrator of it can read the notification in Setup, Notification Events, and the findings can name other organizations' ids and rule names. Boot logs a `warn` for a configured operator who is not a verified member of it, and for an administrator of it who is not a configured operator. A value that names no organization logs one `error` naming the value and raises nothing in-app; it never falls back to another organization. Unset, the log line is the only channel. Ignored under `single`, where the Default Organization's administrators are told. |
105106
| `OS_OIDC_PROVIDER_ENABLED` | boolean | tracks MCP | When `true`, expose this instance as an OIDC identity provider. When unset it follows the MCP server surface (`OS_MCP_SERVER_ENABLED`, on by default) — the MCP human-client track is OAuth 2.1, so every MCP-enabled deployment is its own authorization server. |
106107
| `OS_COOKIE_DOMAIN` | string | — | Cookie domain for cross-subdomain session sharing (e.g. `.example.com`). |
107108
| `OS_TRUSTED_ORIGINS` | csv | — | Comma-separated list of origins permitted for auth callbacks / CSRF. |

‎packages/plugins/plugin-security/src/access-upgrade-report.ts‎

Lines changed: 51 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -77,23 +77,21 @@
7777
* ## Who is told
7878
*
7979
* The log, always, once per category. And the people who can act on the
80-
* choice:
80+
* choice, through `upgrade-report-delivery.ts`:
8181
*
8282
* - under `single`, through the existing notification ingress
83-
* (`messaging.emit()`, ADR-0030: a `sys_notification` event in Setup's
84-
* Notification Events, and the in-app inbox), to the Default
85-
* Organization's `admin_full_access` holders and any
86-
* `OS_PLATFORM_OWNER_EMAIL` address. The notification's `dedupKey` is the
87-
* finding set's digest, so an unchanged set notifies once, not once per
88-
* boot. No messaging service, nobody to tell, or a refused emit: one log
89-
* line says so, with the fix for that cause;
90-
* - under a wall (`group`, `isolated`), the operator alone, by one `error`
91-
* line addressed to the operator. The ledger and the catalog are
92-
* deployment-level and only the operator may switch or clone there
93-
* (ADR-0126 §5, ADR-0131 D3); the notification family is
94-
* organization-scoped, so a walled write with no organization is refused
95-
* (ADR-0131 D9), and stamping an organization would tell a tenant. No
96-
* notification is attempted.
83+
* (`messaging.emit()`, ADR-0030), to the Default Organization's
84+
* `admin_full_access` holders and any `OS_PLATFORM_OWNER_EMAIL` address.
85+
* The notification's `dedupKey` is the finding set's digest, so an
86+
* unchanged set notifies once, not once per boot;
87+
* - under a wall (`group`, `isolated`), the operator alone. The ledger and
88+
* the catalog are deployment-level and only the operator may switch or
89+
* clone there (ADR-0126 §5, ADR-0131 D3). One `error` line addressed to the
90+
* operator is written. When `OS_PLATFORM_OWNER_ORGANIZATION` names an
91+
* organization, the same notification is also raised in-app there,
92+
* addressed to the operators (#25413). Otherwise no notification is
93+
* attempted: a walled write with no organization is refused (ADR-0131 D9),
94+
* and stamping a tenant's organization would tell that tenant.
9795
*
9896
* ## When it runs
9997
*
@@ -104,14 +102,16 @@
104102
import { createHash } from 'node:crypto';
105103
import {
106104
METADATA_ACTIVATION_TABLE,
107-
readDefaultOrganizationId,
108-
resolvePlatformAdminEmails,
109105
type SecurityCatalogEntry,
110106
type SecurityCatalogReader,
111107
} from '@objectstack/core';
112108
import { isTenantAuthored } from '@objectstack/metadata-core';
113-
import { ADMIN_FULL_ACCESS } from '@objectstack/spec/identity';
114-
import { GRANT_OBJECT, GRANT_SET_NAME_FIELD } from './grant-permission-set-name.js';
109+
import {
110+
deliverUpgradeReport,
111+
type UpgradeReportDelivery,
112+
type UpgradeReportLogger,
113+
type UpgradeReportNotifier,
114+
} from './upgrade-report-delivery.js';
115115

116116
/** The notification topic the report is raised on. */
117117
export const ACCESS_UPGRADE_TOPIC = 'security.access_upgrade';
@@ -549,63 +549,19 @@ export function hasAccessUpgradeFindings(report: AccessUpgradeReport): boolean {
549549
// ─────────────────────────────────────────────────────────────────────────────
550550

551551
/** The kernel logger, as this module uses it. */
552-
export interface AccessUpgradeReportLogger {
553-
info?: (message: string, meta?: Record<string, any>) => void;
554-
warn: (message: string, meta?: Record<string, any>) => void;
555-
error?: (message: string, cause?: Error, meta?: Record<string, any>) => void;
556-
}
552+
export type AccessUpgradeReportLogger = UpgradeReportLogger;
557553

558554
/** The notification ingress, as this module uses it — service-messaging's `emit`. */
559-
export interface AccessUpgradeNotifier {
560-
emit(input: {
561-
topic: string;
562-
audience: readonly string[];
563-
severity: 'warning';
564-
dedupKey: string;
565-
organizationId?: string;
566-
payload: Record<string, unknown>;
567-
}): Promise<unknown>;
568-
}
555+
export type AccessUpgradeNotifier = UpgradeReportNotifier;
569556

570557
export interface AccessUpgradeBootDeps extends AccessUpgradeReportDeps {
571558
logger?: AccessUpgradeReportLogger;
572559
/** The `messaging` service; absent when the composition has none. */
573560
messaging?: AccessUpgradeNotifier;
574-
/** `true` under `group` / `isolated`: the operator alone is told, in the log. */
561+
/** `true` under `group` / `isolated`: the operator alone is told (see `upgrade-report-delivery.ts`). */
575562
walled: boolean;
576563
}
577564

578-
/** Who the report is delivered to, and the organization a `single` delivery is stamped with. */
579-
export interface AccessUpgradeAudience {
580-
recipients: string[];
581-
organizationId?: string;
582-
}
583-
584-
/**
585-
* The people who can act on an access choice under `single`: any configured
586-
* platform operator and every holder of the Default Organization's
587-
* `admin_full_access` grant. A read that throws propagates. (Under a wall no
588-
* audience is resolved: see {@link reportAccessUpgrade}.)
589-
*/
590-
export async function resolveAccessUpgradeAudience(
591-
engine: AccessUpgradeReportEngine,
592-
): Promise<AccessUpgradeAudience> {
593-
const recipients = [...resolvePlatformAdminEmails().emails];
594-
const organizationId = await readDefaultOrganizationId(engine);
595-
if (organizationId === undefined || !engine.getObject(GRANT_OBJECT)) return { recipients };
596-
const grants = await engine.find(GRANT_OBJECT, {
597-
where: { [GRANT_SET_NAME_FIELD]: ADMIN_FULL_ACCESS, organization_id: organizationId },
598-
fields: ['user_id'],
599-
limit: 10000,
600-
context: SYSTEM_CTX,
601-
});
602-
for (const row of Array.isArray(grants) ? grants : []) {
603-
const userId = nameOf((row as Record<string, unknown>)?.user_id);
604-
if (userId !== undefined && !recipients.includes(userId)) recipients.push(userId);
605-
}
606-
return { recipients, organizationId };
607-
}
608-
609565
function listed<T>(items: readonly T[]): { items: T[]; more?: number } {
610566
return items.length > LOGGED_LIMIT
611567
? { items: items.slice(0, LOGGED_LIMIT), more: items.length - LOGGED_LIMIT }
@@ -706,29 +662,22 @@ function logReport(report: AccessUpgradeReport, logger?: AccessUpgradeReportLogg
706662
}
707663
}
708664

709-
/** What the boot report did with its findings. */
710-
export interface AccessUpgradeDelivery {
711-
/**
712-
* `notified`: handed to the ingress (`single`). `operator-log`: walled, so
713-
* the operator's line in this log is the channel. `undelivered`: `single`
714-
* findings that reached no administrator, said in the log with the cause.
715-
*/
716-
outcome: 'clean' | 'stopped' | 'notified' | 'operator-log' | 'undelivered';
717-
recipients: string[];
665+
/** What the boot report did with its findings: `clean` or `stopped`, else the delivery's own answer. */
666+
export interface AccessUpgradeDelivery extends Omit<UpgradeReportDelivery, 'outcome' | 'findingSet'> {
667+
outcome: 'clean' | 'stopped' | UpgradeReportDelivery['outcome'];
718668
/** The finding set's digest — the notification's `dedupKey`. */
719669
findingSet?: string;
720-
reason?: string;
721670
}
722671

672+
/** Why only the operator can act on access findings under a wall: the delivery's operator line. */
673+
const ACCESS_OPERATOR_REASON =
674+
'the activation ledger and the security catalog are shared by every organization — so only the operator can act ' +
675+
'on them, through the activation and clone doors.';
676+
723677
/**
724678
* The boot entry: collect, log, and raise the findings to the people who can
725-
* act on them. Never throws for a read or a delivery that did not happen.
726-
*
727-
* Under a wall the notification is never attempted: the findings are
728-
* deployment-level, the operator alone can act on them, and the notification
729-
* family is organization-scoped, so a walled system write with no organization
730-
* is refused (ADR-0131 D9) and stamping one would hand the finding to a tenant.
731-
* One `error` line addressed to the operator is the channel there.
679+
* act on them, through `upgrade-report-delivery.ts`. Never throws for a read
680+
* or a delivery that did not happen.
732681
*/
733682
export async function reportAccessUpgrade(
734683
engine: AccessUpgradeReportEngine,
@@ -739,88 +688,30 @@ export async function reportAccessUpgrade(
739688
if (report.stopped) return { report, delivery: { outcome: 'stopped', recipients: [] } };
740689
if (!hasAccessUpgradeFindings(report)) return { report, delivery: { outcome: 'clean', recipients: [] } };
741690

742-
const findingSet = accessUpgradeDedupKey(report);
743-
if (deps.walled) {
744-
const operators = [...resolvePlatformAdminEmails().emails];
745-
logError(
746-
deps.logger,
747-
'[security] platform operator: the access upgrade findings above are deployment-level — the activation ledger ' +
748-
'and the security catalog are shared by every organization — so only the operator can act on them, through ' +
749-
'the activation and clone doors. This tenancy posture has no in-app channel for a deployment-level ' +
750-
'notification, so this log is where they are raised; no organization is told. ' +
751-
(operators.length > 0
752-
? 'The configured operator(s) are named in this line.'
753-
: 'No operator is configured: set OS_PLATFORM_OWNER_EMAIL to name the platform operator.'),
754-
{ findingSet, operators },
755-
);
756-
return { report, delivery: { outcome: 'operator-log', recipients: operators, findingSet } };
757-
}
758-
759-
const undelivered = (
760-
reason: string,
761-
fix: string,
762-
recipients: string[] = [],
763-
): { report: AccessUpgradeReport; delivery: AccessUpgradeDelivery } => {
764-
logError(
765-
deps.logger,
766-
`[security] the access upgrade findings above reached no administrator: ${reason}. They are in this log only. ` +
767-
`Fix: ${fix}`,
768-
{ findingSet },
769-
);
770-
return { report, delivery: { outcome: 'undelivered', recipients, findingSet, reason } };
771-
};
772-
if (!deps.messaging || typeof deps.messaging.emit !== 'function') {
773-
return undelivered('this composition has no messaging service', 'compose the messaging service.');
774-
}
775-
776-
let audience: AccessUpgradeAudience;
777-
try {
778-
audience = await resolveAccessUpgradeAudience(engine);
779-
} catch (e) {
780-
return undelivered(
781-
`the administrators could not be read (${errorText(e)})`,
782-
'restore the store the grants are read from; the next boot reads them again.',
783-
);
784-
}
785-
if (audience.recipients.length === 0) {
786-
return undelivered(
787-
'no administrator is configured or granted',
788-
'grant admin_full_access in the Default Organization, or set OS_PLATFORM_OWNER_EMAIL.',
789-
);
790-
}
791691
const counts = {
792692
reEnablements: report.reEnablements.length,
793693
orphans: report.orphans.length,
794694
collisions: report.collisions.length,
795695
brokenReferences: report.brokenReferences.length,
796696
uncoveredDenies: report.uncoveredDenies.length,
797697
};
798-
try {
799-
await deps.messaging.emit({
800-
topic: ACCESS_UPGRADE_TOPIC,
801-
audience: audience.recipients,
802-
severity: 'warning',
803-
dedupKey: findingSet,
804-
...(audience.organizationId !== undefined ? { organizationId: audience.organizationId } : {}),
805-
payload: {
806-
title: 'An upgrade changed what your access settings cover',
807-
body:
808-
'A package upgrade renamed, removed or collided with a sharing rule, permission set, position or ' +
809-
'capability your settings name. ' +
810-
(counts.reEnablements > 0
811-
? `${counts.reEnablements} item(s) you switched off may be active again under a new name. `
812-
: '') +
813-
'Nothing was changed. The server log line "[security] access upgrade" lists each finding with its fix.',
814-
counts,
815-
findings: accessUpgradeSummary(report),
816-
},
817-
});
818-
} catch (e) {
819-
return undelivered(
820-
`the notification ingress refused it (${errorText(e)})`,
821-
'repair the cause the ingress names; the next boot raises the same findings again.',
822-
audience.recipients,
823-
);
824-
}
825-
return { report, delivery: { outcome: 'notified', recipients: audience.recipients, findingSet } };
698+
const delivery = await deliverUpgradeReport(engine, deps, {
699+
topic: ACCESS_UPGRADE_TOPIC,
700+
findingSet: accessUpgradeDedupKey(report),
701+
subject: 'the access upgrade findings above',
702+
operatorReason: ACCESS_OPERATOR_REASON,
703+
payload: {
704+
title: 'An upgrade changed what your access settings cover',
705+
body:
706+
'A package upgrade renamed, removed or collided with a sharing rule, permission set, position or ' +
707+
'capability your settings name. ' +
708+
(counts.reEnablements > 0
709+
? `${counts.reEnablements} item(s) you switched off may be active again under a new name. `
710+
: '') +
711+
'Nothing was changed. The server log line "[security] access upgrade" lists each finding with its fix.',
712+
counts,
713+
findings: accessUpgradeSummary(report),
714+
},
715+
});
716+
return { report, delivery };
826717
}

0 commit comments

Comments
 (0)