@caelo-cms/provisioning 0.11.11 → 0.11.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/admin-domain.d.ts +24 -0
  2. package/dist/admin-domain.d.ts.map +1 -1
  3. package/dist/admin-domain.js +43 -0
  4. package/dist/admin-domain.js.map +1 -1
  5. package/dist/agent-db.js +171 -15
  6. package/dist/converge-job-gcp.d.ts +7 -0
  7. package/dist/converge-job-gcp.d.ts.map +1 -1
  8. package/dist/converge-job-gcp.js +33 -1
  9. package/dist/converge-job-gcp.js.map +1 -1
  10. package/dist/converge-job-preflight.d.ts +34 -2
  11. package/dist/converge-job-preflight.d.ts.map +1 -1
  12. package/dist/converge-job-preflight.js +68 -1
  13. package/dist/converge-job-preflight.js.map +1 -1
  14. package/dist/converge-job-setup.d.ts.map +1 -1
  15. package/dist/converge-job-setup.js +4 -1
  16. package/dist/converge-job-setup.js.map +1 -1
  17. package/dist/converge-job.d.ts +27 -3
  18. package/dist/converge-job.d.ts.map +1 -1
  19. package/dist/converge-job.js +93 -11
  20. package/dist/converge-job.js.map +1 -1
  21. package/dist/infra-contract.d.ts +1 -1
  22. package/dist/infra-contract.d.ts.map +1 -1
  23. package/dist/infra-contract.js +5 -1
  24. package/dist/infra-contract.js.map +1 -1
  25. package/dist/install-job.d.ts +6 -0
  26. package/dist/install-job.d.ts.map +1 -1
  27. package/dist/install-job.js +6 -0
  28. package/dist/install-job.js.map +1 -1
  29. package/dist/lifecycle.d.ts.map +1 -1
  30. package/dist/lifecycle.js +14 -4
  31. package/dist/lifecycle.js.map +1 -1
  32. package/dist/mcp-auth-proxy-setup.d.ts +3 -1
  33. package/dist/mcp-auth-proxy-setup.d.ts.map +1 -1
  34. package/dist/mcp-auth-proxy-setup.js +9 -4
  35. package/dist/mcp-auth-proxy-setup.js.map +1 -1
  36. package/dist/scheduler-setup.d.ts +5 -0
  37. package/dist/scheduler-setup.d.ts.map +1 -1
  38. package/dist/scheduler-setup.js +7 -0
  39. package/dist/scheduler-setup.js.map +1 -1
  40. package/dist/upgrader-iam-preflight.d.ts +8 -6
  41. package/dist/upgrader-iam-preflight.d.ts.map +1 -1
  42. package/dist/upgrader-iam-preflight.js +5 -1
  43. package/dist/upgrader-iam-preflight.js.map +1 -1
  44. package/dist/wizards/gcp-cost.js +1 -1
  45. package/dist/wizards/gcp-cost.js.map +1 -1
  46. package/package.json +2 -2
  47. package/src/admin-domain.test.ts +65 -0
  48. package/src/admin-domain.ts +53 -0
  49. package/src/converge-job-gcp.ts +33 -1
  50. package/src/converge-job-preflight.test.ts +101 -0
  51. package/src/converge-job-preflight.ts +89 -3
  52. package/src/converge-job-setup.ts +4 -1
  53. package/src/converge-job.test.ts +143 -0
  54. package/src/converge-job.ts +143 -19
  55. package/src/infra-contract.test.ts +4 -3
  56. package/src/infra-contract.ts +5 -1
  57. package/src/install-job.ts +6 -0
  58. package/src/lifecycle.ts +16 -6
  59. package/src/mcp-auth-proxy-setup.test.ts +33 -1
  60. package/src/mcp-auth-proxy-setup.ts +11 -4
  61. package/src/scheduler-setup.test.ts +6 -0
  62. package/src/scheduler-setup.ts +7 -0
  63. package/src/upgrader-iam-preflight.ts +15 -10
  64. package/src/wizards/gcp-cost.ts +1 -1
@@ -127,7 +127,7 @@ export function estimateGcpCost(inputs) {
127
127
  {
128
128
  name: "Quality checks (Cloud Run audit job)",
129
129
  monthlyUsd: 2,
130
- notes: "one container per Lighthouse run, all in parallel (up to 5 pages x 3 runs, ~90 s at 2 vCPU each): ~$0.05 per audited Stage, partly in the free tier; results bucket ~$0",
130
+ notes: "one container per Lighthouse run, all in parallel (up to 5 pages x 3 runs, ~90 s at 2 vCPU each; a page whose runs disagree gets 2 more): ~$0.05 per audited Stage, at most ~$0.08, partly in the free tier; results bucket ~$0",
131
131
  },
132
132
  {
133
133
  name: "Screenshots (Cloud Run capture job)",
@@ -1 +1 @@
1
- {"version":3,"file":"gcp-cost.js","sourceRoot":"","sources":["../../src/wizards/gcp-cost.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gcp-cost.js","sourceRoot":"","sources":["../../src/wizards/gcp-cost.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caelo-cms/provisioning",
3
- "version": "0.11.11",
3
+ "version": "0.11.13",
4
4
  "private": false,
5
5
  "license": "MPL-2.0",
6
6
  "description": "Pulumi-driven provisioning for Caelo CMS — `bunx @caelo-cms/provisioning --provider <self-hosted|gcp|aws|azure>` brings up a TLS-served install with Postgres + storage + secrets manager + the admin + the API gateway.",
@@ -40,7 +40,7 @@
40
40
  "prepublishOnly": "tsc -b --force && bun build ../platform-updater/src/agent-db.ts --target=bun --outfile=dist/agent-db.js && tsc --project stacks/gcp/tsconfig.json && tsc --project stacks/gcp-firebase/tsconfig.json"
41
41
  },
42
42
  "dependencies": {
43
- "@caelo-cms/shared": "^0.11.11",
43
+ "@caelo-cms/shared": "^0.11.13",
44
44
  "@clack/prompts": "1.8.1",
45
45
  "@pulumi/command": "^1",
46
46
  "@pulumi/gcp": "^10",
@@ -340,3 +340,68 @@ describe("setAdminStagingHostEnv (#1058)", () => {
340
340
  expect(b.status === "failed" && b.error).toContain("set CAELO_STAGING_HOST on the admin");
341
341
  });
342
342
  });
343
+
344
+ describe("stagingDomainState", () => {
345
+ const target = { projectId: "p", region: "europe-west1", stagingHost: "staging.example.com" };
346
+
347
+ it("reports an existing mapping as mapped", async () => {
348
+ const { run, calls } = fakeGcloud({
349
+ "beta run domain-mappings describe": [ok("staging.example.com\n")],
350
+ });
351
+ const { stagingDomainState } = await import("./admin-domain.js");
352
+ expect(await stagingDomainState({ ...target, run })).toEqual({ status: "mapped" });
353
+ expect(calls).toContainEqual([
354
+ "beta",
355
+ "run",
356
+ "domain-mappings",
357
+ "describe",
358
+ "--domain=staging.example.com",
359
+ "--region=europe-west1",
360
+ "--project=p",
361
+ "--format=value(metadata.name)",
362
+ ]);
363
+ });
364
+
365
+ it("reports gcloud's NOT_FOUND as unmapped", async () => {
366
+ const { run } = fakeGcloud({
367
+ "beta run domain-mappings describe": [
368
+ fail(
369
+ "ERROR: (gcloud.beta.run.domain-mappings.describe) NOT_FOUND: Resource 'staging.example.com' of kind 'DOMAIN_MAPPING' in region 'europe-west1' in project 'p' does not exist.",
370
+ ),
371
+ ],
372
+ });
373
+ const { stagingDomainState } = await import("./admin-domain.js");
374
+ expect(await stagingDomainState({ ...target, run })).toEqual({ status: "unmapped" });
375
+ });
376
+
377
+ it("does not guess on another error", async () => {
378
+ const { run } = fakeGcloud({
379
+ "beta run domain-mappings describe": [fail("PERMISSION_DENIED: run.domainmappings.get")],
380
+ });
381
+ const { stagingDomainState } = await import("./admin-domain.js");
382
+ expect(await stagingDomainState({ ...target, run })).toEqual({
383
+ status: "unknown",
384
+ reason: "PERMISSION_DENIED: run.domainmappings.get",
385
+ });
386
+ });
387
+
388
+ it("says why when gcloud cannot list its components", async () => {
389
+ const { run } = fakeGcloud({ "components list": [fail("component manager crashed")] });
390
+ const { stagingDomainState } = await import("./admin-domain.js");
391
+ expect(await stagingDomainState({ ...target, run })).toEqual({
392
+ status: "unknown",
393
+ reason: 'check the gcloud "beta" component: component manager crashed',
394
+ });
395
+ });
396
+
397
+ it("never installs the beta component to answer", async () => {
398
+ const { run, calls } = fakeGcloud({ "components list": [ok("")] });
399
+ const { stagingDomainState } = await import("./admin-domain.js");
400
+ expect(await stagingDomainState({ ...target, run })).toEqual({
401
+ status: "unknown",
402
+ reason: 'the gcloud "beta" component is not installed',
403
+ });
404
+ expect(calls.some((c) => c.join(" ").startsWith("components install"))).toBe(false);
405
+ expect(calls.some((c) => c[0] === "beta")).toBe(false);
406
+ });
407
+ });
@@ -183,6 +183,59 @@ export async function enableStagingDomain(
183
183
  return mapDomain(run, logOf(opts), scope, verifyDomain, opts.stagingHost, found.service);
184
184
  }
185
185
 
186
+ /** What {@link stagingDomainState} found. */
187
+ export type StagingDomainState =
188
+ | { readonly status: "mapped" }
189
+ | { readonly status: "unmapped" }
190
+ | { readonly status: "unknown"; readonly reason: string };
191
+
192
+ /**
193
+ * Whether the install's staging host already has its Cloud Run domain
194
+ * mapping, read-only. The upgrade asks before it tells the operator to run
195
+ * `staging-domain enable`, so an install that serves its staging host is
196
+ * not sent to set it up again. It never installs the gcloud `beta`
197
+ * component (an upgrade should not pause for a download to print a hint):
198
+ * without it, or on any error but gcloud's NOT_FOUND, the state is unknown
199
+ * and says why.
200
+ */
201
+ export async function stagingDomainState(opts: {
202
+ projectId: string;
203
+ region: string;
204
+ stagingHost: string;
205
+ run?: Run;
206
+ }): Promise<StagingDomainState> {
207
+ const run = opts.run ?? defaultGcloud;
208
+ const listed = await run([
209
+ "components",
210
+ "list",
211
+ "--only-local-state",
212
+ "--filter=id=beta",
213
+ "--format=value(id)",
214
+ ]);
215
+ if (!listed.ok) {
216
+ return {
217
+ status: "unknown",
218
+ reason: `check the gcloud "beta" component: ${listed.stderr.trim() || `exit ${listed.exitCode}`}`,
219
+ };
220
+ }
221
+ if (!listed.stdout.split("\n").some((s) => s.trim() === "beta")) {
222
+ return { status: "unknown", reason: 'the gcloud "beta" component is not installed' };
223
+ }
224
+ const described = await run([
225
+ "beta",
226
+ "run",
227
+ "domain-mappings",
228
+ "describe",
229
+ `--domain=${opts.stagingHost}`,
230
+ `--region=${opts.region}`,
231
+ `--project=${opts.projectId}`,
232
+ "--format=value(metadata.name)",
233
+ ]);
234
+ if (described.ok) return { status: "mapped" };
235
+ if (/\bNOT_FOUND\b/.test(described.stderr)) return { status: "unmapped" };
236
+ return { status: "unknown", reason: described.stderr.trim() };
237
+ }
238
+
186
239
  /** What {@link setAdminStagingHostEnv} did. */
187
240
  export type AdminStagingEnvResult =
188
241
  | { readonly status: "set"; readonly service: string; readonly previous: string | null }
@@ -22,9 +22,10 @@ import { join } from "node:path";
22
22
  import { CAELO_VERSION } from "@caelo-cms/shared";
23
23
  import { readConvergeCheckpoint, writeConvergeCheckpoint } from "./converge-checkpoint.js";
24
24
  import type { ConvergeJobDeps, ConvergeQueue, ConvergeUpgradeOutcome } from "./converge-job.js";
25
+ import { isSelfGrantableRole } from "./converge-job-preflight.js";
25
26
  import { CONVERGE_TASK_TIMEOUT_MINUTES } from "./converge-job-setup.js";
26
27
  import { gcloud } from "./gcloud.js";
27
- import { type GcloudRunner, realSleep } from "./gcloud-retry.js";
28
+ import { type GcloudRunner, realSleep, runWithRetry } from "./gcloud-retry.js";
28
29
  import {
29
30
  convergeJobName,
30
31
  GCP_STACK_ENV,
@@ -42,6 +43,7 @@ import {
42
43
  } from "./install-state.js";
43
44
  import { runMigrationsViaCloudRunJob } from "./migration-runner.js";
44
45
  import { installIapAllowlist } from "./operator-access.js";
46
+ import { provisionerServiceAccountEmail } from "./provisioner-identity.js";
45
47
  import { fetchVerifiedReleaseManifest, manifestServiceImages } from "./release-manifest.js";
46
48
  import type { HttpFetch } from "./runtime-secrets.js";
47
49
  import { publicSiteUrl, STAGING_HOST_ENV } from "./stack-contract.js";
@@ -203,6 +205,26 @@ export function startedExecutionName(stdout: string, stderr: string): string | n
203
205
  return /^[a-z0-9-]+$/.test(name) ? name : null;
204
206
  }
205
207
 
208
+ /**
209
+ * The `gcloud` argv that grants the install's provisioner — this job's
210
+ * own identity, never another principal — `role` on the project, or null
211
+ * when `role` is not on the self-grant allowlist (converge-job-preflight.ts).
212
+ * Exported for the tests that pin the boundary.
213
+ */
214
+ export function selfGrantArgs(projectId: string, role: string): string[] | null {
215
+ if (!isSelfGrantableRole(role)) return null;
216
+ return [
217
+ "projects",
218
+ "add-iam-policy-binding",
219
+ projectId,
220
+ `--member=serviceAccount:${provisionerServiceAccountEmail(projectId)}`,
221
+ `--role=${role}`,
222
+ "--condition=None",
223
+ "--quiet",
224
+ "--format=none",
225
+ ];
226
+ }
227
+
206
228
  /**
207
229
  * Every dependency of {@link runConvergeJob} except the database (the queue
208
230
  * and how it is secured, #1373) and the log.
@@ -366,6 +388,16 @@ export function gcpConvergeDeps(
366
388
  projectId,
367
389
  region,
368
390
  iam: gcloudIamTester(run, http),
391
+ grantOwnRole: async (role) => {
392
+ const args = selfGrantArgs(projectId, role);
393
+ if (!args)
394
+ return { ok: false, error: `${role} is not a role the converge job may grant itself` };
395
+ const r = await runWithRetry(run, realSleep, args);
396
+ return r.ok
397
+ ? { ok: true }
398
+ : { ok: false, error: r.stderr.trim() || `gcloud exited ${r.exitCode}` };
399
+ },
400
+ sleep: realSleep,
369
401
  execution: placement.execution,
370
402
  ownVersion: CAELO_VERSION,
371
403
  // The default deps: the fixed registry and signer, never overridden.
@@ -7,10 +7,15 @@
7
7
  */
8
8
 
9
9
  import { describe, expect, it } from "bun:test";
10
+ import { selfGrantArgs } from "./converge-job-gcp.js";
10
11
  import {
11
12
  CONVERGE_PREFLIGHT_VOICE,
13
+ CONVERGE_SELF_GRANTABLE_ROLES,
12
14
  convergeAccessChecks,
13
15
  INSTALL_ROLE_USES,
16
+ isSelfGrantableRole,
17
+ SELF_GRANT_FORBIDDEN_ROLES,
18
+ selfGrantPlan,
14
19
  } from "./converge-job-preflight.js";
15
20
  import { PROVISIONER_ROLE_LIST } from "./gcloud.js";
16
21
  import { INSTALL_JOB_ROLES } from "./install-job.js";
@@ -79,6 +84,7 @@ describe("convergeAccessChecks", () => {
79
84
  for (const role of INSTALL_JOB_ROLES) {
80
85
  expect(checks).toContainEqual({
81
86
  resource: { kind: "project", projectId: "acme" },
87
+ role,
82
88
  ...INSTALL_ROLE_USES[role],
83
89
  });
84
90
  }
@@ -111,3 +117,98 @@ describe("convergeAccessChecks", () => {
111
117
  expect(reason).toContain("Cloud Shell or the computer you installed from");
112
118
  });
113
119
  });
120
+
121
+ describe("the converge job's self-grant allowlist (2026-11-03)", () => {
122
+ it("is exactly the roles converge-job-setup.ts grants the provisioner", () => {
123
+ expect([...CONVERGE_SELF_GRANTABLE_ROLES]).toEqual([...INSTALL_JOB_ROLES]);
124
+ expect(CONVERGE_SELF_GRANTABLE_ROLES).toContain("roles/workflows.editor");
125
+ });
126
+
127
+ it("names no ownership or IAM-policy role", () => {
128
+ for (const role of CONVERGE_SELF_GRANTABLE_ROLES) {
129
+ for (const f of SELF_GRANT_FORBIDDEN_ROLES) {
130
+ expect(role === f || (f.endsWith(".") && role.startsWith(f))).toBe(false);
131
+ }
132
+ }
133
+ });
134
+
135
+ it("refuses every role outside the list, escalations first", () => {
136
+ for (const role of [
137
+ "roles/owner",
138
+ "roles/editor",
139
+ "roles/resourcemanager.projectIamAdmin",
140
+ "roles/iam.securityAdmin",
141
+ "roles/iam.serviceAccountKeyAdmin",
142
+ "roles/run.admin",
143
+ "roles/workflows.admin",
144
+ "roles/workflows.editor ",
145
+ "",
146
+ ]) {
147
+ expect(isSelfGrantableRole(role)).toBe(false);
148
+ }
149
+ for (const role of INSTALL_JOB_ROLES) expect(isSelfGrantableRole(role)).toBe(true);
150
+ });
151
+
152
+ it("the grant names the project and the provisioner only, and refuses other roles", () => {
153
+ expect(selfGrantArgs("acme", "roles/workflows.editor")).toEqual([
154
+ "projects",
155
+ "add-iam-policy-binding",
156
+ "acme",
157
+ "--member=serviceAccount:caelo-provisioner@acme.iam.gserviceaccount.com",
158
+ "--role=roles/workflows.editor",
159
+ "--condition=None",
160
+ "--quiet",
161
+ "--format=none",
162
+ ]);
163
+ for (const role of [
164
+ "roles/owner",
165
+ "roles/resourcemanager.projectIamAdmin",
166
+ "roles/run.admin",
167
+ ]) {
168
+ expect(selfGrantArgs("acme", role)).toBeNull();
169
+ }
170
+ });
171
+ });
172
+
173
+ describe("selfGrantPlan", () => {
174
+ it("an install from before 0.11.11 (no workflows.editor) needs one grant and nothing blocks", async () => {
175
+ const r = await runIamPreflight(
176
+ convergeAccessChecks(TARGET),
177
+ provisioner([
178
+ ...PROVISIONER_ROLE_LIST,
179
+ ...INSTALL_JOB_ROLES.filter((role) => role !== "roles/workflows.editor"),
180
+ ]),
181
+ );
182
+ expect(r.kind).toBe("missing");
183
+ if (r.kind !== "missing") return;
184
+ expect(selfGrantPlan(r.missing)).toEqual({ grant: ["roles/workflows.editor"], blocking: [] });
185
+ });
186
+
187
+ it("every install-job role missing: all of them, in list order", async () => {
188
+ const r = await runIamPreflight(
189
+ convergeAccessChecks(TARGET),
190
+ provisioner(PROVISIONER_ROLE_LIST),
191
+ );
192
+ if (r.kind !== "missing") throw new Error("expected missing");
193
+ const plan = selfGrantPlan([...r.missing].reverse());
194
+ expect(plan.grant).toEqual([...INSTALL_JOB_ROLES]);
195
+ expect(plan.blocking).toEqual([]);
196
+ });
197
+
198
+ it("a check without a role, or with a role off the list, blocks", () => {
199
+ const actAs = convergeAccessChecks(TARGET).find((c) =>
200
+ c.permissions.includes("iam.serviceAccounts.actAs"),
201
+ );
202
+ if (!actAs) throw new Error("no actAs check");
203
+ const forged = {
204
+ ...actAs,
205
+ role: "roles/owner" as unknown as (typeof INSTALL_JOB_ROLES)[number],
206
+ };
207
+ const plan = selfGrantPlan([
208
+ { check: actAs, missing: ["iam.serviceAccounts.actAs"] },
209
+ { check: forged, missing: ["iam.serviceAccounts.actAs"] },
210
+ ]);
211
+ expect(plan.grant).toEqual([]);
212
+ expect(plan.blocking).toHaveLength(2);
213
+ });
214
+ });
@@ -26,6 +26,23 @@
26
26
  * - the Cloud SQL IAM login `ensureConvergeJob` creates (member of
27
27
  * {@link UPGRADER_DB_ROLE}): the request row and the timeline.
28
28
  *
29
+ * Self-grant (maintainer decision 2026-11-03). A release may add a role to
30
+ * {@link INSTALL_JOB_ROLES} (0.11.11 added roles/workflows.editor for the
31
+ * scheduler tick, #1140); an install set up before it lacks that role until
32
+ * a converge's `ensureConvergeJob` grants it, and the converge job used to
33
+ * stop here and send the Owner to Cloud Shell for exactly that grant. Now a
34
+ * missing role of {@link CONVERGE_SELF_GRANTABLE_ROLES} — this release's own
35
+ * compiled-in list, nothing a request, a record or the environment names —
36
+ * is not a reason to stop: stage 1 leaves it to stage 2, and stage 2 (the
37
+ * target release's verified ops image, under the state lock) grants it to
38
+ * the provisioner at project level, records each grant on the timeline and
39
+ * asks IAM again (converge-job.ts). Not an escalation: the provisioner holds
40
+ * roles/resourcemanager.projectIamAdmin since install, so it could grant
41
+ * itself any role already; the allowlist only narrows what the job does
42
+ * with that. {@link SELF_GRANT_FORBIDDEN_ROLES} pins that the list never
43
+ * names an ownership or IAM-policy role, and the upgrader job — which holds
44
+ * no IAM rights at all — has no grant step.
45
+ *
29
46
  * Not checked: the permissions `pulumi up` needs for the stack. They are the
30
47
  * provisioner's PROVISIONER_ROLE_LIST (gcloud.ts), granted at install by the
31
48
  * Owner's own account; a converge cannot repair them and Pulumi names what
@@ -37,7 +54,7 @@ import { convergeJobName } from "./gcp-names.js";
37
54
  import { INSTALL_JOB_ROLES, type InstallJobRole } from "./install-job.js";
38
55
  import { provisionerServiceAccountEmail } from "./provisioner-identity.js";
39
56
  import { OPS_INSTALL_PREFIX, opsBucketName } from "./state-backend.js";
40
- import type { AccessCheck, PreflightVoice } from "./upgrader-iam-preflight.js";
57
+ import type { AccessCheck, MissingAccess, PreflightVoice } from "./upgrader-iam-preflight.js";
41
58
  import { UPGRADER_DB_ROLE } from "./upgrader-setup.js";
42
59
 
43
60
  /**
@@ -83,17 +100,86 @@ export const CONVERGE_PREFLIGHT_VOICE: PreflightVoice = {
83
100
  "Run the update once from Cloud Shell or the computer you installed from (Settings → Updates shows the command); it repairs the converge job's access.",
84
101
  };
85
102
 
103
+ /**
104
+ * The roles the converge job may grant its own account when the self-check
105
+ * finds them missing: exactly the roles `ensureConvergeJob` grants it, as
106
+ * compiled into this release (see the file comment).
107
+ */
108
+ export const CONVERGE_SELF_GRANTABLE_ROLES: readonly InstallJobRole[] = INSTALL_JOB_ROLES;
109
+
110
+ /**
111
+ * Roles the converge job must never grant itself, whatever a release's list
112
+ * says — ownership and IAM-policy roles (prefix matches). The tests assert
113
+ * {@link CONVERGE_SELF_GRANTABLE_ROLES} names none of them, and the grant
114
+ * step refuses them again at run time.
115
+ */
116
+ export const SELF_GRANT_FORBIDDEN_ROLES = [
117
+ "roles/owner",
118
+ "roles/editor",
119
+ "roles/resourcemanager.",
120
+ "roles/iam.securityAdmin",
121
+ "roles/iam.organizationRoleAdmin",
122
+ "roles/iam.serviceAccountKeyAdmin",
123
+ "roles/iam.workloadIdentityPoolAdmin",
124
+ "roles/orgpolicy.",
125
+ "roles/billing.",
126
+ ] as const;
127
+
128
+ /** Whether the converge job may grant `role` to its own account (see the file comment). */
129
+ export function isSelfGrantableRole(role: string): role is InstallJobRole {
130
+ return (
131
+ (CONVERGE_SELF_GRANTABLE_ROLES as readonly string[]).includes(role) &&
132
+ !SELF_GRANT_FORBIDDEN_ROLES.some((f) => role === f || (f.endsWith(".") && role.startsWith(f)))
133
+ );
134
+ }
135
+
136
+ /** A converge self-check; `role` marks a check that granting that role repairs. */
137
+ export interface ConvergeAccessCheck extends AccessCheck {
138
+ readonly role?: InstallJobRole;
139
+ }
140
+
141
+ /** What the converge job does about the self-check's missing permissions. */
142
+ export interface SelfGrantPlan {
143
+ /** Allowlisted roles to grant the provisioner, in list order. */
144
+ readonly grant: readonly InstallJobRole[];
145
+ /** What no grant of the job repairs: the run stops on any of these. */
146
+ readonly blocking: readonly MissingAccess<ConvergeAccessCheck>[];
147
+ }
148
+
149
+ /**
150
+ * Split the self-check's missing permissions into roles the job grants
151
+ * itself and the rest. A check counts as repairable only when its whole
152
+ * shortfall is permissions of an allowlisted role it is marked with.
153
+ */
154
+ export function selfGrantPlan(
155
+ missing: readonly MissingAccess<ConvergeAccessCheck>[],
156
+ ): SelfGrantPlan {
157
+ const grant: InstallJobRole[] = [];
158
+ const blocking: MissingAccess<ConvergeAccessCheck>[] = [];
159
+ for (const m of missing) {
160
+ const role = m.check.role;
161
+ if (role && isSelfGrantableRole(role) && !(m.unchecked && m.unchecked.length > 0)) {
162
+ if (!grant.includes(role)) grant.push(role);
163
+ } else {
164
+ blocking.push(m);
165
+ }
166
+ }
167
+ const order = CONVERGE_SELF_GRANTABLE_ROLES as readonly string[];
168
+ grant.sort((a, b) => order.indexOf(a) - order.indexOf(b));
169
+ return { grant, blocking };
170
+ }
171
+
86
172
  /** Every permission a converge run relies on after its decision (see the file comment). */
87
173
  export function convergeAccessChecks(target: {
88
174
  readonly projectId: string;
89
175
  readonly region: string;
90
176
  readonly env: string;
91
- }): AccessCheck[] {
177
+ }): ConvergeAccessCheck[] {
92
178
  const { projectId, region, env } = target;
93
179
  const project = { kind: "project", projectId } as const;
94
180
  const bucket = opsBucketName(projectId);
95
181
  return [
96
- ...INSTALL_JOB_ROLES.map((role) => ({ resource: project, ...INSTALL_ROLE_USES[role] })),
182
+ ...INSTALL_JOB_ROLES.map((role) => ({ resource: project, role, ...INSTALL_ROLE_USES[role] })),
97
183
  {
98
184
  need: `read and write the install's records and state lock (${OPS_INSTALL_PREFIX}/) and the update's checkpoint (${CONVERGE_STATE_PREFIX}/) in the ops bucket`,
99
185
  resource: { kind: "bucket", bucket },
@@ -41,7 +41,10 @@
41
41
  * - {@link INSTALL_JOB_ROLES} (the operator-access step of a converge
42
42
  * creates a custom role and a Cloud Scheduler job, and the job's lines
43
43
  * must reach Cloud Logging) — what an install-job install already
44
- * grants; not an escalation, the provisioner holds projectIamAdmin;
44
+ * grants; not an escalation, the provisioner holds projectIamAdmin. The
45
+ * converge job of a release that adds a role here grants it itself when
46
+ * it finds it missing (converge-job-preflight.ts, the self-grant), so
47
+ * the role reaches an older install without a terminal run;
45
48
  * - a Cloud SQL IAM database login that is a member of `platform_updater`
46
49
  * only (migration 0267): it reads its request and writes the timeline,
47
50
  * nothing else in the database.