@opengeni/db 3.4.0-canary.2 → 3.6.0

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 (67) hide show
  1. package/dist/canonical-human-identities.d.ts +20 -0
  2. package/dist/canonical-human-identities.js +7 -3
  3. package/dist/{chunk-ZWRIPCTR.js → chunk-2NT476HD.js} +2 -2
  4. package/dist/{chunk-PYMY3AJW.js → chunk-744DFC65.js} +202 -7
  5. package/dist/chunk-744DFC65.js.map +1 -0
  6. package/dist/{chunk-O6FTELKH.js → chunk-CDMMSUWJ.js} +3 -3
  7. package/dist/chunk-G3732M3T.js +299 -0
  8. package/dist/chunk-G3732M3T.js.map +1 -0
  9. package/dist/{chunk-TRBOWUG3.js → chunk-IW6O6G7R.js} +2 -2
  10. package/dist/{chunk-HY3F5Q4E.js → chunk-Q5YUGQVK.js} +3 -3
  11. package/dist/{chunk-V3TRUXFJ.js → chunk-UGDVSQ6U.js} +72 -19
  12. package/dist/chunk-UGDVSQ6U.js.map +1 -0
  13. package/dist/{chunk-C2AFP7SB.js → chunk-UJ2EW3QI.js} +6664 -6051
  14. package/dist/chunk-UJ2EW3QI.js.map +1 -0
  15. package/dist/{chunk-H7MXYG7R.js → chunk-WW7AJ5IC.js} +2 -2
  16. package/dist/editable-artifact-durable-export.js +2 -2
  17. package/dist/editable-artifacts.js +3 -3
  18. package/dist/index.d.ts +44 -6
  19. package/dist/index.js +2914 -1535
  20. package/dist/index.js.map +1 -1
  21. package/dist/managed-auth-session-set-schema.d.ts +1399 -0
  22. package/dist/managed-auth-session-sets.d.ts +145 -0
  23. package/dist/managed-auth-session-sets.js +50 -0
  24. package/dist/managed-auth-session-sets.js.map +1 -0
  25. package/dist/organization-recovery-schema.d.ts +2144 -0
  26. package/dist/organization-recovery.d.ts +186 -0
  27. package/dist/provision-roles.js +1 -1
  28. package/dist/runtime-posture.d.ts +7 -4
  29. package/dist/schema.d.ts +9 -6
  30. package/dist/schema.js +43 -1
  31. package/dist/session-background-commands.js +3 -3
  32. package/dist/session-control.d.ts +5 -3
  33. package/dist/session-queue-commands.d.ts +2 -0
  34. package/dist/session-tenancy.js +3 -3
  35. package/dist/video-generation.js +3 -3
  36. package/dist/work-claims-schema.d.ts +975 -0
  37. package/dist/work-claims.d.ts +83 -0
  38. package/drizzle/0362_managed_auth_session_sets.sql +2347 -0
  39. package/drizzle/0363_organization_recovery_custody.sql +2418 -0
  40. package/drizzle/0364_workspace_learning_policy_snapshot_lock_order.sql +221 -0
  41. package/drizzle/0365_permission_scoped_work_claims.sql +1306 -0
  42. package/drizzle/0366_session_discovery_title_search_index.sql +5 -0
  43. package/drizzle/0367_session_discovery_goal_search_index.sql +5 -0
  44. package/drizzle/0368_session_discovery_claim_search_index.sql +6 -0
  45. package/package.json +10 -6
  46. package/src/canonical-human-identities.ts +105 -27
  47. package/src/index.ts +1471 -433
  48. package/src/managed-auth-session-set-schema.ts +176 -0
  49. package/src/managed-auth-session-sets.ts +430 -0
  50. package/src/organization-recovery-schema.ts +277 -0
  51. package/src/organization-recovery.ts +424 -0
  52. package/src/provision-roles.ts +67 -0
  53. package/src/runtime-posture.ts +144 -6
  54. package/src/schema.ts +3 -0
  55. package/src/session-control.ts +255 -16
  56. package/src/session-queue-commands.ts +10 -1
  57. package/src/session-tool-call-settlement.ts +6 -1
  58. package/src/work-claims-schema.ts +148 -0
  59. package/src/work-claims.ts +200 -0
  60. package/dist/chunk-C2AFP7SB.js.map +0 -1
  61. package/dist/chunk-PYMY3AJW.js.map +0 -1
  62. package/dist/chunk-V3TRUXFJ.js.map +0 -1
  63. /package/dist/{chunk-ZWRIPCTR.js.map → chunk-2NT476HD.js.map} +0 -0
  64. /package/dist/{chunk-O6FTELKH.js.map → chunk-CDMMSUWJ.js.map} +0 -0
  65. /package/dist/{chunk-TRBOWUG3.js.map → chunk-IW6O6G7R.js.map} +0 -0
  66. /package/dist/{chunk-HY3F5Q4E.js.map → chunk-Q5YUGQVK.js.map} +0 -0
  67. /package/dist/{chunk-H7MXYG7R.js.map → chunk-WW7AJ5IC.js.map} +0 -0
@@ -0,0 +1,2418 @@
1
+ -- deployment-mode: rolling
2
+ -- Revisioned organization recovery custody and immutable workspace ownership.
3
+ -- Recovery can only promote one existing active human membership to co-owner.
4
+ -- It cannot transfer workspaces, Personal resources, data, or billing authority.
5
+
6
+ SET LOCAL lock_timeout = '5s';
7
+ SET LOCAL statement_timeout = '10min';
8
+
9
+ CREATE TABLE "organization_recovery_policies" (
10
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
11
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
12
+ "revision" bigint NOT NULL,
13
+ "configured_by_membership_id" uuid NOT NULL,
14
+ "configured_by_identity_id" uuid NOT NULL
15
+ REFERENCES "canonical_human_identities"("id") ON DELETE RESTRICT,
16
+ "configured_at" timestamptz NOT NULL DEFAULT now(),
17
+ CONSTRAINT "organization_recovery_policies_membership_fk"
18
+ FOREIGN KEY ("configured_by_membership_id", "account_id")
19
+ REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
20
+ CONSTRAINT "organization_recovery_policies_revision_check" CHECK ("revision" > 0),
21
+ CONSTRAINT "organization_recovery_policies_id_account_unique" UNIQUE ("id", "account_id"),
22
+ CONSTRAINT "organization_recovery_policies_account_revision_unique"
23
+ UNIQUE ("account_id", "revision")
24
+ );
25
+
26
+ CREATE TABLE "organization_recovery_policy_heads" (
27
+ "account_id" uuid PRIMARY KEY REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
28
+ "current_policy_id" uuid NOT NULL UNIQUE,
29
+ "revision" bigint NOT NULL,
30
+ "enabled" boolean NOT NULL DEFAULT true,
31
+ "activated_at" timestamptz,
32
+ "created_at" timestamptz NOT NULL DEFAULT now(),
33
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
34
+ CONSTRAINT "organization_recovery_policy_heads_policy_fk"
35
+ FOREIGN KEY ("current_policy_id", "account_id")
36
+ REFERENCES "organization_recovery_policies"("id", "account_id") ON DELETE RESTRICT,
37
+ CONSTRAINT "organization_recovery_policy_heads_revision_check" CHECK ("revision" > 0)
38
+ );
39
+
40
+ CREATE TABLE "organization_recovery_custodians" (
41
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
42
+ "account_id" uuid NOT NULL,
43
+ "policy_id" uuid NOT NULL,
44
+ "ordinal" integer NOT NULL,
45
+ "membership_id" uuid NOT NULL,
46
+ "canonical_identity_id" uuid NOT NULL
47
+ REFERENCES "canonical_human_identities"("id") ON DELETE RESTRICT,
48
+ "enrolled_at" timestamptz NOT NULL DEFAULT now(),
49
+ CONSTRAINT "organization_recovery_custodians_policy_fk"
50
+ FOREIGN KEY ("policy_id", "account_id")
51
+ REFERENCES "organization_recovery_policies"("id", "account_id") ON DELETE RESTRICT,
52
+ CONSTRAINT "organization_recovery_custodians_membership_fk"
53
+ FOREIGN KEY ("membership_id", "account_id")
54
+ REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
55
+ CONSTRAINT "organization_recovery_custodians_ordinal_check" CHECK ("ordinal" BETWEEN 1 AND 3),
56
+ CONSTRAINT "organization_recovery_custodians_policy_ordinal_unique"
57
+ UNIQUE ("policy_id", "ordinal"),
58
+ CONSTRAINT "organization_recovery_custodians_policy_membership_unique"
59
+ UNIQUE ("policy_id", "membership_id"),
60
+ CONSTRAINT "organization_recovery_custodians_policy_identity_unique"
61
+ UNIQUE ("policy_id", "canonical_identity_id"),
62
+ CONSTRAINT "organization_recovery_custodians_id_policy_unique" UNIQUE ("id", "policy_id")
63
+ );
64
+ CREATE INDEX "organization_recovery_custodians_account_policy_idx"
65
+ ON "organization_recovery_custodians" ("account_id", "policy_id", "ordinal");
66
+
67
+ CREATE TABLE "organization_recovery_custodian_acceptances" (
68
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
69
+ "account_id" uuid NOT NULL,
70
+ "policy_id" uuid NOT NULL,
71
+ "custodian_id" uuid NOT NULL,
72
+ "membership_id" uuid NOT NULL,
73
+ "canonical_identity_id" uuid NOT NULL,
74
+ "auth_user_id" text NOT NULL REFERENCES "auth_users"("id") ON DELETE RESTRICT,
75
+ "login_binding_id" uuid NOT NULL
76
+ REFERENCES "canonical_human_login_bindings"("id") ON DELETE RESTRICT,
77
+ "membership_authorization_revision" bigint NOT NULL,
78
+ "identity_revision" bigint NOT NULL,
79
+ "auth_revision" bigint NOT NULL,
80
+ "subject_revision" bigint NOT NULL,
81
+ "login_binding_revision" bigint NOT NULL,
82
+ "reauth_operation_id" uuid NOT NULL
83
+ REFERENCES "managed_auth_session_set_operations"("operation_id") ON DELETE RESTRICT,
84
+ "accepted_at" timestamptz NOT NULL DEFAULT now(),
85
+ CONSTRAINT "organization_recovery_acceptances_policy_fk"
86
+ FOREIGN KEY ("policy_id", "account_id")
87
+ REFERENCES "organization_recovery_policies"("id", "account_id") ON DELETE RESTRICT,
88
+ CONSTRAINT "organization_recovery_acceptances_custodian_fk"
89
+ FOREIGN KEY ("custodian_id", "policy_id")
90
+ REFERENCES "organization_recovery_custodians"("id", "policy_id") ON DELETE RESTRICT,
91
+ CONSTRAINT "organization_recovery_acceptances_membership_fk"
92
+ FOREIGN KEY ("membership_id", "account_id")
93
+ REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
94
+ CONSTRAINT "organization_recovery_acceptances_revision_check" CHECK (
95
+ "membership_authorization_revision" > 0 AND "identity_revision" > 0
96
+ AND "auth_revision" > 0 AND "subject_revision" > 0
97
+ AND "login_binding_revision" > 0
98
+ )
99
+ );
100
+ CREATE INDEX "organization_recovery_acceptances_current_idx"
101
+ ON "organization_recovery_custodian_acceptances"
102
+ ("policy_id", "canonical_identity_id", "accepted_at" DESC, "id" DESC);
103
+
104
+ CREATE TABLE "organization_recovery_operations" (
105
+ "id" uuid PRIMARY KEY,
106
+ "account_id" uuid NOT NULL,
107
+ "policy_id" uuid NOT NULL,
108
+ "policy_revision" bigint NOT NULL,
109
+ "revision" bigint NOT NULL DEFAULT 1,
110
+ "state" text NOT NULL DEFAULT 'collecting',
111
+ "target_membership_id" uuid NOT NULL,
112
+ "target_identity_id" uuid NOT NULL
113
+ REFERENCES "canonical_human_identities"("id") ON DELETE RESTRICT,
114
+ "target_auth_user_id" text NOT NULL REFERENCES "auth_users"("id") ON DELETE RESTRICT,
115
+ "target_membership_authorization_revision" bigint NOT NULL,
116
+ "target_identity_revision" bigint NOT NULL,
117
+ "target_auth_revision" bigint NOT NULL,
118
+ "target_subject_revision" bigint NOT NULL,
119
+ "started_by_custodian_id" uuid NOT NULL,
120
+ "started_by_identity_id" uuid NOT NULL
121
+ REFERENCES "canonical_human_identities"("id") ON DELETE RESTRICT,
122
+ "quorum_at" timestamptz,
123
+ "executable_at" timestamptz,
124
+ "expires_at" timestamptz NOT NULL,
125
+ "notification_batch_id" uuid,
126
+ "notification_count" integer,
127
+ "notification_digest" text,
128
+ "executed_at" timestamptz,
129
+ "cancelled_at" timestamptz,
130
+ "superseded_at" timestamptz,
131
+ "created_at" timestamptz NOT NULL DEFAULT now(),
132
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
133
+ CONSTRAINT "organization_recovery_operations_policy_fk"
134
+ FOREIGN KEY ("policy_id", "account_id")
135
+ REFERENCES "organization_recovery_policies"("id", "account_id") ON DELETE RESTRICT,
136
+ CONSTRAINT "organization_recovery_operations_target_fk"
137
+ FOREIGN KEY ("target_membership_id", "account_id")
138
+ REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
139
+ CONSTRAINT "organization_recovery_operations_starter_fk"
140
+ FOREIGN KEY ("started_by_custodian_id", "policy_id")
141
+ REFERENCES "organization_recovery_custodians"("id", "policy_id") ON DELETE RESTRICT,
142
+ CONSTRAINT "organization_recovery_operations_revision_check" CHECK (
143
+ "policy_revision" > 0 AND "revision" > 0
144
+ AND "target_membership_authorization_revision" > 0
145
+ AND "target_identity_revision" > 0 AND "target_auth_revision" > 0
146
+ AND "target_subject_revision" > 0
147
+ ),
148
+ CONSTRAINT "organization_recovery_operations_state_check" CHECK (
149
+ "state" IN ('collecting', 'cooling', 'executed', 'cancelled', 'expired', 'superseded')
150
+ ),
151
+ CONSTRAINT "organization_recovery_operations_time_check" CHECK (
152
+ "expires_at" = "created_at" + interval '30 days'
153
+ AND (
154
+ ("quorum_at" IS NULL AND "executable_at" IS NULL
155
+ AND "notification_batch_id" IS NULL AND "notification_count" IS NULL
156
+ AND "notification_digest" IS NULL)
157
+ OR
158
+ ("quorum_at" IS NOT NULL AND "executable_at" = "quorum_at" + interval '7 days'
159
+ AND "notification_batch_id" IS NOT NULL AND "notification_count" > 0
160
+ AND "notification_digest" ~ '^[0-9a-f]{64}$')
161
+ )
162
+ ),
163
+ CONSTRAINT "organization_recovery_operations_terminal_check" CHECK (
164
+ ("state" = 'executed' AND "executed_at" IS NOT NULL
165
+ AND "cancelled_at" IS NULL AND "superseded_at" IS NULL)
166
+ OR ("state" = 'cancelled' AND "cancelled_at" IS NOT NULL
167
+ AND "executed_at" IS NULL AND "superseded_at" IS NULL)
168
+ OR ("state" = 'superseded' AND "superseded_at" IS NOT NULL
169
+ AND "executed_at" IS NULL AND "cancelled_at" IS NULL)
170
+ OR ("state" IN ('collecting', 'cooling', 'expired')
171
+ AND "executed_at" IS NULL AND "cancelled_at" IS NULL AND "superseded_at" IS NULL)
172
+ ),
173
+ CONSTRAINT "organization_recovery_operations_id_account_unique" UNIQUE ("id", "account_id")
174
+ );
175
+ CREATE UNIQUE INDEX "organization_recovery_operations_one_live_idx"
176
+ ON "organization_recovery_operations" ("account_id")
177
+ WHERE "state" IN ('collecting', 'cooling');
178
+ CREATE INDEX "organization_recovery_operations_account_created_idx"
179
+ ON "organization_recovery_operations" ("account_id", "created_at" DESC, "id" DESC);
180
+
181
+ CREATE TABLE "organization_recovery_approvals" (
182
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
183
+ "account_id" uuid NOT NULL,
184
+ "operation_id" uuid NOT NULL,
185
+ "operation_revision" bigint NOT NULL,
186
+ "policy_id" uuid NOT NULL,
187
+ "custodian_id" uuid NOT NULL,
188
+ "membership_id" uuid NOT NULL,
189
+ "canonical_identity_id" uuid NOT NULL,
190
+ "auth_user_id" text NOT NULL REFERENCES "auth_users"("id") ON DELETE RESTRICT,
191
+ "login_binding_id" uuid NOT NULL
192
+ REFERENCES "canonical_human_login_bindings"("id") ON DELETE RESTRICT,
193
+ "membership_authorization_revision" bigint NOT NULL,
194
+ "identity_revision" bigint NOT NULL,
195
+ "auth_revision" bigint NOT NULL,
196
+ "subject_revision" bigint NOT NULL,
197
+ "login_binding_revision" bigint NOT NULL,
198
+ "custodian_acceptance_id" uuid NOT NULL
199
+ REFERENCES "organization_recovery_custodian_acceptances"("id") ON DELETE RESTRICT,
200
+ "reauth_operation_id" uuid NOT NULL
201
+ REFERENCES "managed_auth_session_set_operations"("operation_id") ON DELETE RESTRICT,
202
+ "approved_at" timestamptz NOT NULL DEFAULT now(),
203
+ CONSTRAINT "organization_recovery_approvals_operation_fk"
204
+ FOREIGN KEY ("operation_id", "account_id")
205
+ REFERENCES "organization_recovery_operations"("id", "account_id") ON DELETE RESTRICT,
206
+ CONSTRAINT "organization_recovery_approvals_custodian_fk"
207
+ FOREIGN KEY ("custodian_id", "policy_id")
208
+ REFERENCES "organization_recovery_custodians"("id", "policy_id") ON DELETE RESTRICT,
209
+ CONSTRAINT "organization_recovery_approvals_membership_fk"
210
+ FOREIGN KEY ("membership_id", "account_id")
211
+ REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
212
+ CONSTRAINT "organization_recovery_approvals_revision_check" CHECK (
213
+ "operation_revision" > 0 AND "membership_authorization_revision" > 0
214
+ AND "identity_revision" > 0 AND "auth_revision" > 0
215
+ AND "subject_revision" > 0 AND "login_binding_revision" > 0
216
+ ),
217
+ CONSTRAINT "organization_recovery_approvals_evidence_unique" UNIQUE (
218
+ "operation_id", "canonical_identity_id", "membership_authorization_revision",
219
+ "identity_revision", "auth_revision", "subject_revision", "login_binding_revision"
220
+ )
221
+ );
222
+ CREATE INDEX "organization_recovery_approvals_current_idx"
223
+ ON "organization_recovery_approvals"
224
+ ("operation_id", "canonical_identity_id", "approved_at" DESC, "id" DESC);
225
+
226
+ CREATE TABLE "organization_recovery_command_receipts" (
227
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
228
+ "operation_id" uuid NOT NULL,
229
+ "action" text NOT NULL,
230
+ "actor_membership_id" uuid NOT NULL,
231
+ "actor_identity_id" uuid NOT NULL,
232
+ "input_hash" text NOT NULL,
233
+ "result" jsonb NOT NULL,
234
+ "created_at" timestamptz NOT NULL DEFAULT now(),
235
+ PRIMARY KEY ("account_id", "operation_id"),
236
+ CONSTRAINT "organization_recovery_receipts_membership_fk"
237
+ FOREIGN KEY ("actor_membership_id", "account_id")
238
+ REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
239
+ CONSTRAINT "organization_recovery_receipts_identity_fk"
240
+ FOREIGN KEY ("actor_identity_id")
241
+ REFERENCES "canonical_human_identities"("id") ON DELETE RESTRICT,
242
+ CONSTRAINT "organization_recovery_receipts_action_check" CHECK (
243
+ "action" IN ('configure_policy', 'accept_custody', 'disable_policy',
244
+ 'start_operation', 'approve_operation', 'cancel_operation', 'execute_operation')
245
+ ),
246
+ CONSTRAINT "organization_recovery_receipts_hash_check" CHECK ("input_hash" ~ '^[0-9a-f]{64}$'),
247
+ CONSTRAINT "organization_recovery_receipts_result_check" CHECK (
248
+ pg_catalog.jsonb_typeof("result") = 'object' AND octet_length("result"::text) <= 65536
249
+ )
250
+ );
251
+
252
+ CREATE TABLE "organization_recovery_events" (
253
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
254
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
255
+ "policy_id" uuid,
256
+ "operation_id" uuid,
257
+ "command_operation_id" uuid,
258
+ "event_type" text NOT NULL,
259
+ "actor_membership_id" uuid,
260
+ "actor_identity_id" uuid,
261
+ "event_revision" bigint NOT NULL,
262
+ "evidence" jsonb NOT NULL DEFAULT '{}'::jsonb,
263
+ "created_at" timestamptz NOT NULL DEFAULT now(),
264
+ CONSTRAINT "organization_recovery_events_policy_fk"
265
+ FOREIGN KEY ("policy_id", "account_id")
266
+ REFERENCES "organization_recovery_policies"("id", "account_id") ON DELETE RESTRICT,
267
+ CONSTRAINT "organization_recovery_events_operation_fk"
268
+ FOREIGN KEY ("operation_id", "account_id")
269
+ REFERENCES "organization_recovery_operations"("id", "account_id") ON DELETE RESTRICT,
270
+ CONSTRAINT "organization_recovery_events_membership_fk"
271
+ FOREIGN KEY ("actor_membership_id", "account_id")
272
+ REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
273
+ CONSTRAINT "organization_recovery_events_revision_check" CHECK ("event_revision" > 0),
274
+ CONSTRAINT "organization_recovery_events_type_check" CHECK (
275
+ "event_type" IN ('policy_configured', 'custody_accepted', 'policy_activated',
276
+ 'policy_disabled', 'operation_started', 'operation_approved', 'quorum_started',
277
+ 'operation_cancelled', 'operation_executed', 'operation_expired',
278
+ 'operation_superseded')
279
+ ),
280
+ CONSTRAINT "organization_recovery_events_evidence_check" CHECK (
281
+ pg_catalog.jsonb_typeof("evidence") = 'object' AND octet_length("evidence"::text) <= 8192
282
+ )
283
+ );
284
+ CREATE UNIQUE INDEX "organization_recovery_events_command_type_unique"
285
+ ON "organization_recovery_events" ("account_id", "command_operation_id", "event_type")
286
+ WHERE "command_operation_id" IS NOT NULL;
287
+
288
+ CREATE TABLE "organization_recovery_notification_outbox" (
289
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
290
+ "account_id" uuid NOT NULL,
291
+ "policy_id" uuid NOT NULL,
292
+ "operation_id" uuid NOT NULL,
293
+ "batch_id" uuid NOT NULL,
294
+ "recipient_membership_id" uuid NOT NULL,
295
+ "recipient_identity_id" uuid NOT NULL,
296
+ "audience" text NOT NULL,
297
+ "notification_type" text NOT NULL,
298
+ "idempotency_key" text NOT NULL UNIQUE,
299
+ "payload_digest" text NOT NULL,
300
+ "payload" jsonb NOT NULL,
301
+ "created_at" timestamptz NOT NULL DEFAULT now(),
302
+ CONSTRAINT "organization_recovery_outbox_policy_fk"
303
+ FOREIGN KEY ("policy_id", "account_id")
304
+ REFERENCES "organization_recovery_policies"("id", "account_id") ON DELETE RESTRICT,
305
+ CONSTRAINT "organization_recovery_outbox_operation_fk"
306
+ FOREIGN KEY ("operation_id", "account_id")
307
+ REFERENCES "organization_recovery_operations"("id", "account_id") ON DELETE RESTRICT,
308
+ CONSTRAINT "organization_recovery_outbox_recipient_fk"
309
+ FOREIGN KEY ("recipient_membership_id", "account_id")
310
+ REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
311
+ CONSTRAINT "organization_recovery_outbox_type_check" CHECK (
312
+ "notification_type" = 'recovery_quorum_started'
313
+ ),
314
+ CONSTRAINT "organization_recovery_outbox_digest_check" CHECK (
315
+ "payload_digest" ~ '^[0-9a-f]{64}$'
316
+ ),
317
+ CONSTRAINT "organization_recovery_outbox_payload_check" CHECK (
318
+ pg_catalog.jsonb_typeof("payload") = 'object' AND octet_length("payload"::text) <= 8192
319
+ )
320
+ );
321
+ CREATE INDEX "organization_recovery_outbox_batch_idx"
322
+ ON "organization_recovery_notification_outbox" ("operation_id", "batch_id", "id");
323
+
324
+ CREATE TABLE "organization_recovery_notification_attempts" (
325
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
326
+ "outbox_id" uuid NOT NULL
327
+ REFERENCES "organization_recovery_notification_outbox"("id") ON DELETE RESTRICT,
328
+ "delivery_id" uuid NOT NULL,
329
+ "provider" text NOT NULL,
330
+ "claim_owner" text NOT NULL,
331
+ "attempt_number" integer NOT NULL,
332
+ "lease_expires_at" timestamptz NOT NULL,
333
+ "phase" text NOT NULL,
334
+ "provider_message_id" text,
335
+ "error_class" text,
336
+ "created_at" timestamptz NOT NULL DEFAULT now(),
337
+ CONSTRAINT "organization_recovery_attempts_provider_check" CHECK (
338
+ "provider" = lower(btrim("provider")) AND length("provider") BETWEEN 1 AND 64
339
+ ),
340
+ CONSTRAINT "organization_recovery_attempts_claim_check" CHECK (
341
+ length(btrim("claim_owner")) BETWEEN 1 AND 256 AND "attempt_number" BETWEEN 1 AND 5
342
+ ),
343
+ CONSTRAINT "organization_recovery_attempts_phase_check" CHECK (
344
+ "phase" IN ('provider_started', 'sent', 'failed', 'outcome_unknown',
345
+ 'claim_expired', 'reconciled_sent', 'reconciled_retry')
346
+ ),
347
+ CONSTRAINT "organization_recovery_attempts_error_check" CHECK (
348
+ ("phase" = 'failed' AND "error_class" IS NOT NULL)
349
+ OR ("phase" <> 'failed' AND "error_class" IS NULL)
350
+ ),
351
+ CONSTRAINT "organization_recovery_attempts_delivery_phase_unique"
352
+ UNIQUE ("outbox_id", "delivery_id", "phase")
353
+ );
354
+
355
+ -- Even the migration owner cannot reparent a workspace by issuing direct SQL.
356
+ CREATE OR REPLACE FUNCTION reject_workspace_account_id_change()
357
+ RETURNS trigger LANGUAGE plpgsql AS $body$
358
+ BEGIN
359
+ IF NEW.account_id IS DISTINCT FROM OLD.account_id THEN
360
+ RAISE EXCEPTION 'workspace organization ownership is immutable; transfer is unsupported'
361
+ USING ERRCODE = '0A000';
362
+ END IF;
363
+ RETURN NEW;
364
+ END
365
+ $body$;
366
+
367
+ -- Owner-only dispatcher seam. It journals provider_started before returning;
368
+ -- the provider call happens outside the transaction and settle appends the
369
+ -- terminal phase. Claims are leased and bounded to five attempts. An expired
370
+ -- claim is explicitly journaled before another dispatcher may receive the same
371
+ -- stable idempotency key. Failed delivery uses bounded exponential pacing;
372
+ -- outcome_unknown stays blocked until explicit reconciliation.
373
+ CREATE OR REPLACE FUNCTION prepare_organization_recovery_notifications(
374
+ p_provider text, p_claim_owner text, p_limit integer, p_lease_seconds integer
375
+ ) RETURNS jsonb
376
+ LANGUAGE plpgsql SECURITY DEFINER AS $body$
377
+ DECLARE
378
+ result jsonb;
379
+ now_value timestamptz := pg_catalog.clock_timestamp();
380
+ previous_marker text := pg_catalog.current_setting(
381
+ 'opengeni.organization_recovery_lifecycle', true
382
+ );
383
+ BEGIN
384
+ IF p_provider IS NULL OR p_provider <> lower(btrim(p_provider))
385
+ OR length(p_provider) NOT BETWEEN 1 AND 64
386
+ OR p_claim_owner IS NULL OR length(btrim(p_claim_owner)) NOT BETWEEN 1 AND 256
387
+ OR p_limit NOT BETWEEN 1 AND 100
388
+ OR p_lease_seconds NOT BETWEEN 15 AND 300
389
+ THEN
390
+ RAISE EXCEPTION 'organization recovery notification claim is invalid'
391
+ USING ERRCODE = '22023';
392
+ END IF;
393
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle', 'active', true);
394
+
395
+ -- A provider_started lease is not silently forgotten. Its expiry is immutable
396
+ -- evidence that the payload was handed to a dispatcher but was not recorded
397
+ -- as called; the stable provider idempotency key remains unchanged on reclaim.
398
+ WITH stale AS (
399
+ SELECT outbox.id, started.delivery_id, started.provider, started.claim_owner,
400
+ started.attempt_number, started.lease_expires_at
401
+ FROM organization_recovery_notification_outbox outbox
402
+ INNER JOIN LATERAL (
403
+ SELECT attempt.*
404
+ FROM organization_recovery_notification_attempts attempt
405
+ WHERE attempt.outbox_id = outbox.id AND attempt.phase = 'provider_started'
406
+ ORDER BY attempt.attempt_number DESC, attempt.created_at DESC, attempt.id DESC
407
+ LIMIT 1
408
+ ) started ON true
409
+ WHERE started.lease_expires_at <= now_value
410
+ AND NOT EXISTS (
411
+ SELECT 1 FROM organization_recovery_notification_attempts terminal
412
+ WHERE terminal.outbox_id = outbox.id
413
+ AND terminal.delivery_id = started.delivery_id
414
+ AND terminal.phase IN ('sent', 'failed', 'outcome_unknown', 'claim_expired')
415
+ )
416
+ ORDER BY outbox.created_at, outbox.id
417
+ LIMIT p_limit
418
+ FOR UPDATE OF outbox SKIP LOCKED
419
+ )
420
+ INSERT INTO organization_recovery_notification_attempts (
421
+ outbox_id, delivery_id, provider, claim_owner, attempt_number,
422
+ lease_expires_at, phase, created_at
423
+ ) SELECT
424
+ stale.id, stale.delivery_id, stale.provider, stale.claim_owner,
425
+ stale.attempt_number, stale.lease_expires_at, 'claim_expired', now_value
426
+ FROM stale
427
+ ON CONFLICT (outbox_id, delivery_id, phase) DO NOTHING;
428
+
429
+ WITH candidates AS (
430
+ SELECT outbox.id
431
+ FROM organization_recovery_notification_outbox outbox
432
+ LEFT JOIN LATERAL (
433
+ SELECT attempt.phase, attempt.attempt_number, attempt.created_at
434
+ FROM organization_recovery_notification_attempts attempt
435
+ WHERE attempt.outbox_id = outbox.id
436
+ ORDER BY attempt.created_at DESC, attempt.id DESC
437
+ LIMIT 1
438
+ ) latest ON true
439
+ WHERE NOT EXISTS (
440
+ SELECT 1 FROM organization_recovery_notification_attempts attempt
441
+ WHERE attempt.outbox_id = outbox.id
442
+ AND attempt.phase IN ('sent', 'reconciled_sent')
443
+ )
444
+ AND NOT EXISTS (
445
+ SELECT 1 FROM organization_recovery_notification_attempts ambiguous
446
+ WHERE ambiguous.outbox_id = outbox.id AND ambiguous.phase = 'outcome_unknown'
447
+ AND NOT EXISTS (
448
+ SELECT 1 FROM organization_recovery_notification_attempts reconciliation
449
+ WHERE reconciliation.outbox_id = ambiguous.outbox_id
450
+ AND reconciliation.delivery_id = ambiguous.delivery_id
451
+ AND reconciliation.phase IN ('reconciled_sent', 'reconciled_retry')
452
+ )
453
+ )
454
+ AND NOT EXISTS (
455
+ SELECT 1 FROM organization_recovery_notification_attempts started
456
+ WHERE started.outbox_id = outbox.id AND started.phase = 'provider_started'
457
+ AND NOT EXISTS (
458
+ SELECT 1 FROM organization_recovery_notification_attempts terminal
459
+ WHERE terminal.outbox_id = started.outbox_id
460
+ AND terminal.delivery_id = started.delivery_id
461
+ AND terminal.phase IN ('sent', 'failed', 'outcome_unknown', 'claim_expired')
462
+ )
463
+ )
464
+ AND (
465
+ SELECT pg_catalog.count(*)
466
+ FROM organization_recovery_notification_attempts started
467
+ WHERE started.outbox_id = outbox.id AND started.phase = 'provider_started'
468
+ ) < 5
469
+ AND (
470
+ latest.phase IS NULL
471
+ OR latest.phase IN ('claim_expired', 'reconciled_retry')
472
+ OR (
473
+ latest.phase = 'failed'
474
+ AND latest.created_at + pg_catalog.make_interval(
475
+ secs => least(900, 60 * (2 ^ greatest(0, latest.attempt_number - 1))::integer)
476
+ ) <= now_value
477
+ )
478
+ )
479
+ ORDER BY outbox.created_at, outbox.id
480
+ LIMIT p_limit FOR UPDATE OF outbox SKIP LOCKED
481
+ ), started AS (
482
+ INSERT INTO organization_recovery_notification_attempts (
483
+ outbox_id, delivery_id, provider, claim_owner, attempt_number,
484
+ lease_expires_at, phase, created_at
485
+ )
486
+ SELECT candidate.id, gen_random_uuid(), p_provider, p_claim_owner,
487
+ 1 + (
488
+ SELECT pg_catalog.count(*)::integer
489
+ FROM organization_recovery_notification_attempts prior
490
+ WHERE prior.outbox_id = candidate.id AND prior.phase = 'provider_started'
491
+ ), now_value + pg_catalog.make_interval(secs => p_lease_seconds),
492
+ 'provider_started', now_value
493
+ FROM candidates candidate
494
+ RETURNING id, outbox_id, delivery_id, provider, claim_owner,
495
+ attempt_number, lease_expires_at, created_at
496
+ )
497
+ SELECT coalesce(pg_catalog.jsonb_agg(
498
+ pg_catalog.jsonb_build_object(
499
+ 'attemptId', started.id::text,
500
+ 'outboxId', outbox.id::text,
501
+ 'deliveryId', started.delivery_id::text,
502
+ 'provider', started.provider,
503
+ 'claimOwner', started.claim_owner,
504
+ 'attemptNumber', started.attempt_number,
505
+ 'leaseExpiresAt', started.lease_expires_at,
506
+ 'idempotencyKey', outbox.idempotency_key,
507
+ 'recipientCanonicalIdentityId', outbox.recipient_identity_id::text,
508
+ 'notificationType', outbox.notification_type,
509
+ 'payloadDigest', outbox.payload_digest,
510
+ 'payload', outbox.payload
511
+ ) ORDER BY outbox.created_at, outbox.id
512
+ ), '[]'::jsonb) INTO result
513
+ FROM started
514
+ INNER JOIN organization_recovery_notification_outbox outbox
515
+ ON outbox.id = started.outbox_id;
516
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
517
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END, true);
518
+ RETURN result;
519
+ EXCEPTION WHEN OTHERS THEN
520
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
521
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END, true);
522
+ RAISE;
523
+ END
524
+ $body$;
525
+
526
+ CREATE OR REPLACE FUNCTION settle_organization_recovery_notification(
527
+ p_outbox_id uuid, p_delivery_id uuid, p_claim_owner text, p_phase text,
528
+ p_provider_message_id text, p_error_class text
529
+ ) RETURNS jsonb
530
+ LANGUAGE plpgsql SECURITY DEFINER AS $body$
531
+ DECLARE
532
+ prior organization_recovery_notification_attempts%ROWTYPE;
533
+ existing organization_recovery_notification_attempts%ROWTYPE;
534
+ result jsonb;
535
+ previous_marker text := pg_catalog.current_setting(
536
+ 'opengeni.organization_recovery_lifecycle', true
537
+ );
538
+ BEGIN
539
+ IF p_outbox_id IS NULL OR p_delivery_id IS NULL
540
+ OR p_claim_owner IS NULL OR length(btrim(p_claim_owner)) NOT BETWEEN 1 AND 256
541
+ OR p_phase NOT IN ('sent', 'failed', 'outcome_unknown')
542
+ OR (p_phase = 'failed' AND nullif(btrim(p_error_class), '') IS NULL)
543
+ OR (p_phase <> 'failed' AND p_error_class IS NOT NULL)
544
+ OR (p_provider_message_id IS NOT NULL AND octet_length(p_provider_message_id) > 1024)
545
+ OR (p_error_class IS NOT NULL AND octet_length(p_error_class) > 256)
546
+ THEN
547
+ RAISE EXCEPTION 'organization recovery notification settlement is invalid'
548
+ USING ERRCODE = '22023';
549
+ END IF;
550
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle', 'active', true);
551
+ PERFORM 1 FROM organization_recovery_notification_outbox outbox
552
+ WHERE outbox.id = p_outbox_id FOR UPDATE;
553
+ IF NOT FOUND THEN
554
+ RAISE EXCEPTION 'organization recovery notification delivery is unavailable'
555
+ USING ERRCODE = '42501';
556
+ END IF;
557
+ SELECT * INTO prior FROM organization_recovery_notification_attempts attempt
558
+ WHERE attempt.outbox_id = p_outbox_id AND attempt.delivery_id = p_delivery_id
559
+ AND attempt.phase = 'provider_started' AND attempt.claim_owner = p_claim_owner;
560
+ IF NOT FOUND THEN
561
+ RAISE EXCEPTION 'organization recovery notification delivery is unavailable'
562
+ USING ERRCODE = '42501';
563
+ END IF;
564
+ IF EXISTS (
565
+ SELECT 1 FROM organization_recovery_notification_attempts attempt
566
+ WHERE attempt.outbox_id = p_outbox_id AND attempt.delivery_id = p_delivery_id
567
+ AND attempt.phase = 'claim_expired'
568
+ ) THEN
569
+ RAISE EXCEPTION 'organization recovery notification claim is stale'
570
+ USING ERRCODE = '40001';
571
+ END IF;
572
+ SELECT * INTO existing FROM organization_recovery_notification_attempts attempt
573
+ WHERE attempt.outbox_id = p_outbox_id AND attempt.delivery_id = p_delivery_id
574
+ AND attempt.phase IN ('sent', 'failed', 'outcome_unknown')
575
+ ORDER BY attempt.created_at DESC, attempt.id DESC LIMIT 1;
576
+ IF FOUND THEN
577
+ IF existing.phase = p_phase
578
+ AND existing.provider_message_id IS NOT DISTINCT FROM p_provider_message_id
579
+ AND existing.error_class IS NOT DISTINCT FROM p_error_class
580
+ THEN
581
+ result := pg_catalog.jsonb_build_object(
582
+ 'outboxId', p_outbox_id::text, 'deliveryId', p_delivery_id::text,
583
+ 'claimOwner', prior.claim_owner, 'attemptNumber', prior.attempt_number,
584
+ 'phase', existing.phase, 'providerMessageId', existing.provider_message_id,
585
+ 'errorClass', existing.error_class, 'settledAt', existing.created_at,
586
+ 'replay', true
587
+ );
588
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
589
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END, true);
590
+ RETURN result;
591
+ END IF;
592
+ RAISE EXCEPTION 'organization recovery notification settlement conflicts'
593
+ USING ERRCODE = '23505';
594
+ END IF;
595
+ INSERT INTO organization_recovery_notification_attempts (
596
+ outbox_id, delivery_id, provider, claim_owner, attempt_number,
597
+ lease_expires_at, phase, provider_message_id, error_class
598
+ ) VALUES (
599
+ p_outbox_id, p_delivery_id, prior.provider, prior.claim_owner,
600
+ prior.attempt_number, prior.lease_expires_at, p_phase,
601
+ p_provider_message_id, p_error_class
602
+ ) RETURNING pg_catalog.jsonb_build_object(
603
+ 'outboxId', outbox_id::text, 'deliveryId', delivery_id::text,
604
+ 'claimOwner', claim_owner, 'attemptNumber', attempt_number,
605
+ 'phase', phase, 'providerMessageId', provider_message_id,
606
+ 'errorClass', error_class, 'settledAt', created_at, 'replay', false
607
+ ) INTO result;
608
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
609
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END, true);
610
+ RETURN result;
611
+ EXCEPTION WHEN OTHERS THEN
612
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
613
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END, true);
614
+ RAISE;
615
+ END
616
+ $body$;
617
+
618
+ CREATE OR REPLACE FUNCTION reconcile_organization_recovery_notification(
619
+ p_outbox_id uuid, p_delivery_id uuid, p_reconciliation_owner text,
620
+ p_resolution text, p_provider_message_id text
621
+ ) RETURNS jsonb
622
+ LANGUAGE plpgsql SECURITY DEFINER AS $body$
623
+ DECLARE
624
+ ambiguous organization_recovery_notification_attempts%ROWTYPE;
625
+ existing organization_recovery_notification_attempts%ROWTYPE;
626
+ resolved_phase text;
627
+ result jsonb;
628
+ previous_marker text := pg_catalog.current_setting(
629
+ 'opengeni.organization_recovery_lifecycle', true
630
+ );
631
+ BEGIN
632
+ IF p_outbox_id IS NULL OR p_delivery_id IS NULL
633
+ OR p_reconciliation_owner IS NULL
634
+ OR length(btrim(p_reconciliation_owner)) NOT BETWEEN 1 AND 256
635
+ OR p_resolution NOT IN ('sent', 'retry')
636
+ OR (p_resolution = 'retry' AND p_provider_message_id IS NOT NULL)
637
+ OR (p_provider_message_id IS NOT NULL AND octet_length(p_provider_message_id) > 1024)
638
+ THEN
639
+ RAISE EXCEPTION 'organization recovery notification reconciliation is invalid'
640
+ USING ERRCODE = '22023';
641
+ END IF;
642
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle', 'active', true);
643
+ PERFORM 1 FROM organization_recovery_notification_outbox outbox
644
+ WHERE outbox.id = p_outbox_id FOR UPDATE;
645
+ IF NOT FOUND THEN
646
+ RAISE EXCEPTION 'organization recovery notification delivery is unavailable'
647
+ USING ERRCODE = '42501';
648
+ END IF;
649
+ SELECT * INTO ambiguous FROM organization_recovery_notification_attempts attempt
650
+ WHERE attempt.outbox_id = p_outbox_id AND attempt.delivery_id = p_delivery_id
651
+ AND attempt.phase = 'outcome_unknown';
652
+ IF NOT FOUND THEN
653
+ RAISE EXCEPTION 'only an ambiguous notification delivery may be reconciled'
654
+ USING ERRCODE = '42501';
655
+ END IF;
656
+ resolved_phase := CASE WHEN p_resolution = 'sent'
657
+ THEN 'reconciled_sent' ELSE 'reconciled_retry' END;
658
+ SELECT * INTO existing FROM organization_recovery_notification_attempts attempt
659
+ WHERE attempt.outbox_id = p_outbox_id AND attempt.delivery_id = p_delivery_id
660
+ AND attempt.phase IN ('reconciled_sent', 'reconciled_retry')
661
+ ORDER BY attempt.created_at DESC, attempt.id DESC LIMIT 1;
662
+ IF FOUND THEN
663
+ IF existing.phase <> resolved_phase
664
+ OR existing.provider_message_id IS DISTINCT FROM p_provider_message_id
665
+ THEN
666
+ RAISE EXCEPTION 'organization recovery notification reconciliation conflicts'
667
+ USING ERRCODE = '23505';
668
+ END IF;
669
+ result := pg_catalog.jsonb_build_object(
670
+ 'outboxId', p_outbox_id::text, 'deliveryId', p_delivery_id::text,
671
+ 'reconciliationOwner', existing.claim_owner,
672
+ 'attemptNumber', existing.attempt_number,
673
+ 'resolution', p_resolution, 'providerMessageId', existing.provider_message_id,
674
+ 'reconciledAt', existing.created_at, 'replay', true
675
+ );
676
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
677
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END, true);
678
+ RETURN result;
679
+ END IF;
680
+ INSERT INTO organization_recovery_notification_attempts (
681
+ outbox_id, delivery_id, provider, claim_owner, attempt_number,
682
+ lease_expires_at, phase, provider_message_id
683
+ ) VALUES (
684
+ p_outbox_id, p_delivery_id, ambiguous.provider, p_reconciliation_owner,
685
+ ambiguous.attempt_number, ambiguous.lease_expires_at,
686
+ resolved_phase, p_provider_message_id
687
+ ) RETURNING pg_catalog.jsonb_build_object(
688
+ 'outboxId', outbox_id::text, 'deliveryId', delivery_id::text,
689
+ 'reconciliationOwner', claim_owner, 'attemptNumber', attempt_number,
690
+ 'resolution', p_resolution, 'providerMessageId', provider_message_id,
691
+ 'reconciledAt', created_at, 'replay', false
692
+ ) INTO result;
693
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
694
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END, true);
695
+ RETURN result;
696
+ EXCEPTION WHEN OTHERS THEN
697
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
698
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END, true);
699
+ RAISE;
700
+ END
701
+ $body$;
702
+
703
+ -- Mutation decisions must be based on one stable custody-evidence snapshot. Canonical
704
+ -- identity operations serialize revision and binding changes through the identity row;
705
+ -- lock every policy custodian in deterministic order before reading acceptances or
706
+ -- approvals so a concurrent authority change either commits first and is observed or
707
+ -- waits until this recovery command commits.
708
+ CREATE OR REPLACE FUNCTION organization_recovery_lock_policy_evidence(
709
+ p_policy_id uuid, p_operation_id uuid DEFAULT NULL
710
+ ) RETURNS void
711
+ LANGUAGE plpgsql SECURITY DEFINER AS $body$
712
+ BEGIN
713
+ IF p_policy_id IS NULL THEN
714
+ RAISE EXCEPTION 'organization recovery policy is unavailable' USING ERRCODE = '55000';
715
+ END IF;
716
+
717
+ PERFORM 1
718
+ FROM organization_memberships membership
719
+ INNER JOIN organization_recovery_custodians custodian
720
+ ON custodian.membership_id = membership.id
721
+ AND custodian.account_id = membership.account_id
722
+ WHERE custodian.policy_id = p_policy_id
723
+ ORDER BY membership.id
724
+ FOR UPDATE OF membership;
725
+
726
+ PERFORM 1
727
+ FROM canonical_human_identity_subjects subject_row
728
+ INNER JOIN organization_memberships membership
729
+ ON membership.subject_id = 'user:' || subject_row.auth_user_id
730
+ INNER JOIN organization_recovery_custodians custodian
731
+ ON custodian.membership_id = membership.id
732
+ AND custodian.account_id = membership.account_id
733
+ AND custodian.canonical_identity_id = subject_row.identity_id
734
+ WHERE custodian.policy_id = p_policy_id
735
+ ORDER BY subject_row.auth_user_id
736
+ FOR UPDATE OF subject_row;
737
+
738
+ PERFORM 1
739
+ FROM canonical_human_identities identity_row
740
+ INNER JOIN organization_recovery_custodians custodian
741
+ ON custodian.canonical_identity_id = identity_row.id
742
+ WHERE custodian.policy_id = p_policy_id
743
+ ORDER BY identity_row.id
744
+ FOR UPDATE OF identity_row;
745
+
746
+ PERFORM 1
747
+ FROM canonical_human_login_bindings binding
748
+ WHERE binding.id IN (
749
+ SELECT acceptance.login_binding_id
750
+ FROM organization_recovery_custodian_acceptances acceptance
751
+ WHERE acceptance.policy_id = p_policy_id
752
+ UNION
753
+ SELECT approval.login_binding_id
754
+ FROM organization_recovery_approvals approval
755
+ WHERE p_operation_id IS NOT NULL AND approval.operation_id = p_operation_id
756
+ )
757
+ ORDER BY binding.id
758
+ FOR UPDATE OF binding;
759
+ END
760
+ $body$;
761
+
762
+ CREATE OR REPLACE FUNCTION organization_recovery_command(p_command jsonb)
763
+ RETURNS jsonb
764
+ LANGUAGE plpgsql SECURITY DEFINER AS $body$
765
+ DECLARE
766
+ action_name text := p_command ->> 'action';
767
+ account_id_value uuid := nullif(p_command ->> 'organizationId', '')::uuid;
768
+ actor_subject text := p_command ->> 'actorSubjectId';
769
+ actor_auth_user_id text := p_command ->> 'actorAuthUserId';
770
+ actor_auth_session_id text := p_command ->> 'actorAuthSessionId';
771
+ command_operation_id uuid := nullif(p_command ->> 'operationId', '')::uuid;
772
+ recovery_operation_id uuid := nullif(p_command ->> 'recoveryOperationId', '')::uuid;
773
+ expected_policy_revision bigint := nullif(p_command ->> 'expectedPolicyRevision', '')::bigint;
774
+ expected_operation_revision bigint := nullif(p_command ->> 'expectedOperationRevision', '')::bigint;
775
+ fence jsonb := p_command -> 'actorFence';
776
+ input_hash_value text;
777
+ now_value timestamptz := pg_catalog.clock_timestamp();
778
+ actor organization_memberships%ROWTYPE;
779
+ actor_identity canonical_human_identities%ROWTYPE;
780
+ actor_subject_row canonical_human_identity_subjects%ROWTYPE;
781
+ receipt organization_recovery_command_receipts%ROWTYPE;
782
+ head_row organization_recovery_policy_heads%ROWTYPE;
783
+ policy_row organization_recovery_policies%ROWTYPE;
784
+ operation_row organization_recovery_operations%ROWTYPE;
785
+ target organization_memberships%ROWTYPE;
786
+ target_identity canonical_human_identities%ROWTYPE;
787
+ target_subject_row canonical_human_identity_subjects%ROWTYPE;
788
+ custodian_row organization_recovery_custodians%ROWTYPE;
789
+ current_acceptance_id uuid;
790
+ recent_proof jsonb;
791
+ custodian_ids uuid[];
792
+ custodian_count integer;
793
+ valid_acceptance_count integer;
794
+ approvals_before integer;
795
+ approvals_after integer;
796
+ new_policy_id uuid;
797
+ new_policy_revision bigint;
798
+ target_membership_id uuid;
799
+ batch_id_value uuid;
800
+ journal_count integer;
801
+ journal_digest text;
802
+ result jsonb;
803
+ item record;
804
+ candidate_identity_id uuid;
805
+ candidate_auth_user_id text;
806
+ candidate_membership organization_memberships%ROWTYPE;
807
+ previous_recovery_marker text := pg_catalog.current_setting(
808
+ 'opengeni.organization_recovery_lifecycle', true
809
+ );
810
+ previous_organization_marker text := pg_catalog.current_setting(
811
+ 'opengeni.organization_tenancy_lifecycle', true
812
+ );
813
+ previous_canonical_marker text := pg_catalog.current_setting(
814
+ 'opengeni.canonical_human_identity_lifecycle', true
815
+ );
816
+ BEGIN
817
+ IF p_command IS NULL OR pg_catalog.jsonb_typeof(p_command) <> 'object'
818
+ OR action_name NOT IN ('configure_policy', 'accept_custody', 'disable_policy',
819
+ 'start_operation', 'approve_operation', 'cancel_operation', 'execute_operation')
820
+ OR account_id_value IS NULL OR actor_subject IS NULL OR actor_auth_user_id IS NULL
821
+ OR actor_auth_session_id IS NULL
822
+ OR actor_subject <> 'user:' || actor_auth_user_id OR command_operation_id IS NULL
823
+ OR actor_subject IS DISTINCT FROM opengeni_private.current_subject_id()
824
+ OR account_id_value IS DISTINCT FROM opengeni_private.current_account_id()
825
+ OR fence IS NULL OR pg_catalog.jsonb_typeof(fence) <> 'object'
826
+ THEN
827
+ RAISE EXCEPTION 'organization recovery command authority is invalid'
828
+ USING ERRCODE = '42501';
829
+ END IF;
830
+
831
+ -- First lock is always the selected browser actor's exact lease. The request id is
832
+ -- deliberately excluded from the semantic receipt digest because a retry is
833
+ -- served by a fresh request lease.
834
+ PERFORM managed_auth_actor_mutation_fence(
835
+ fence ->> 'authorityHash', nullif(fence ->> 'actorEpoch', '')::bigint,
836
+ nullif(fence ->> 'requestId', '')::uuid
837
+ );
838
+ PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
839
+ 'organization-membership:' || account_id_value::text, 0
840
+ ));
841
+ PERFORM 1 FROM managed_accounts account
842
+ WHERE account.id = account_id_value FOR KEY SHARE;
843
+ IF NOT FOUND THEN
844
+ RAISE EXCEPTION 'organization recovery authority is invalid' USING ERRCODE = '42501';
845
+ END IF;
846
+
847
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle', 'active', true);
848
+ PERFORM pg_catalog.set_config(
849
+ 'opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true
850
+ );
851
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle', 'active', true);
852
+ SELECT * INTO actor FROM organization_memberships membership
853
+ WHERE membership.account_id = account_id_value AND membership.subject_id = actor_subject
854
+ FOR UPDATE;
855
+ IF NOT FOUND OR actor.status <> 'active' OR actor_subject NOT LIKE 'user:%' THEN
856
+ RAISE EXCEPTION 'organization recovery authority is invalid' USING ERRCODE = '42501';
857
+ END IF;
858
+ SELECT * INTO actor_subject_row FROM canonical_human_identity_subjects subject_row
859
+ WHERE subject_row.auth_user_id = actor_auth_user_id AND subject_row.status = 'active'
860
+ FOR UPDATE;
861
+ IF NOT FOUND THEN
862
+ RAISE EXCEPTION 'organization recovery authority is invalid' USING ERRCODE = '42501';
863
+ END IF;
864
+ SELECT * INTO actor_identity FROM canonical_human_identities identity_row
865
+ WHERE identity_row.id = actor_subject_row.identity_id FOR UPDATE;
866
+ IF NOT FOUND OR actor_identity.status <> 'active' OR actor_identity.recovery_state <> 'ready' THEN
867
+ RAISE EXCEPTION 'organization recovery authority is invalid' USING ERRCODE = '42501';
868
+ END IF;
869
+
870
+ input_hash_value := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
871
+ (p_command - 'actorFence' - 'actorAuthSessionId')::text, 'UTF8'
872
+ )), 'hex');
873
+ SELECT * INTO receipt FROM organization_recovery_command_receipts candidate
874
+ WHERE candidate.account_id = account_id_value
875
+ AND candidate.operation_id = command_operation_id
876
+ FOR UPDATE;
877
+ IF FOUND THEN
878
+ IF receipt.action IS DISTINCT FROM action_name
879
+ OR receipt.input_hash IS DISTINCT FROM input_hash_value
880
+ THEN
881
+ RAISE EXCEPTION 'organization recovery operation id was reused with different input'
882
+ USING ERRCODE = '23505';
883
+ END IF;
884
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
885
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
886
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
887
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
888
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
889
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
890
+ RETURN pg_catalog.jsonb_build_object('replay', true, 'overview', receipt.result);
891
+ END IF;
892
+
893
+ recent_proof := organization_recovery_assert_recent_reauth(
894
+ fence, actor_subject, actor_auth_session_id, actor_auth_user_id
895
+ );
896
+ IF (recent_proof ->> 'canonicalIdentityId')::uuid <> actor_identity.id
897
+ OR (recent_proof ->> 'identityRevision')::bigint <> actor_identity.identity_revision
898
+ OR (recent_proof ->> 'authRevision')::bigint <> actor_identity.auth_revision
899
+ OR (recent_proof ->> 'subjectRevision')::bigint <> actor_subject_row.revision
900
+ THEN
901
+ RAISE EXCEPTION 'recent reauthentication actor is stale' USING ERRCODE = '42501';
902
+ END IF;
903
+
904
+ SELECT * INTO head_row FROM organization_recovery_policy_heads head
905
+ WHERE head.account_id = account_id_value FOR UPDATE;
906
+
907
+ IF action_name = 'configure_policy' THEN
908
+ IF actor.role <> 'owner' THEN
909
+ RAISE EXCEPTION 'active organization owner required' USING ERRCODE = '42501';
910
+ END IF;
911
+ IF (head_row.account_id IS NULL
912
+ AND coalesce(expected_policy_revision, 0::bigint) <> 0::bigint)
913
+ OR (head_row.account_id IS NOT NULL AND head_row.revision <> expected_policy_revision)
914
+ THEN
915
+ RAISE EXCEPTION 'organization recovery policy revision is stale' USING ERRCODE = '40001';
916
+ END IF;
917
+ IF pg_catalog.jsonb_typeof(p_command -> 'custodianMembershipIds') <> 'array' THEN
918
+ RAISE EXCEPTION 'exactly three custodians are required' USING ERRCODE = '22023';
919
+ END IF;
920
+ SELECT pg_catalog.array_agg(value::uuid ORDER BY ordinality),
921
+ pg_catalog.count(*)::integer
922
+ INTO custodian_ids, custodian_count
923
+ FROM pg_catalog.jsonb_array_elements_text(p_command -> 'custodianMembershipIds')
924
+ WITH ORDINALITY candidate(value, ordinality);
925
+ IF custodian_count <> 3
926
+ OR (SELECT pg_catalog.count(DISTINCT id) FROM pg_catalog.unnest(custodian_ids) id) <> 3
927
+ OR actor.id = ANY(custodian_ids)
928
+ THEN
929
+ RAISE EXCEPTION 'exactly three distinct non-owner custodians are required'
930
+ USING ERRCODE = '22023';
931
+ END IF;
932
+ PERFORM 1 FROM organization_memberships membership
933
+ WHERE membership.account_id = account_id_value AND membership.id = ANY(custodian_ids)
934
+ ORDER BY membership.id FOR UPDATE;
935
+ SELECT pg_catalog.count(*)::integer, pg_catalog.count(DISTINCT subject_row.identity_id)::integer
936
+ INTO custodian_count, valid_acceptance_count
937
+ FROM organization_memberships membership
938
+ INNER JOIN canonical_human_identity_subjects subject_row
939
+ ON membership.subject_id = 'user:' || subject_row.auth_user_id
940
+ AND subject_row.status = 'active'
941
+ INNER JOIN canonical_human_identities identity_row
942
+ ON identity_row.id = subject_row.identity_id
943
+ AND identity_row.status = 'active' AND identity_row.recovery_state = 'ready'
944
+ WHERE membership.account_id = account_id_value AND membership.id = ANY(custodian_ids)
945
+ AND membership.status = 'active' AND membership.role <> 'owner';
946
+ IF custodian_count <> 3 OR valid_acceptance_count <> 3 THEN
947
+ RAISE EXCEPTION 'custodians must be distinct active non-owner canonical humans'
948
+ USING ERRCODE = '42501';
949
+ END IF;
950
+ PERFORM 1 FROM canonical_human_identities identity_row
951
+ WHERE identity_row.id IN (
952
+ SELECT subject_row.identity_id
953
+ FROM organization_memberships membership
954
+ INNER JOIN canonical_human_identity_subjects subject_row
955
+ ON membership.subject_id = 'user:' || subject_row.auth_user_id
956
+ WHERE membership.account_id = account_id_value AND membership.id = ANY(custodian_ids)
957
+ ) ORDER BY identity_row.id FOR UPDATE;
958
+
959
+ new_policy_id := gen_random_uuid();
960
+ new_policy_revision := coalesce(head_row.revision, 0::bigint) + 1::bigint;
961
+ INSERT INTO organization_recovery_policies (
962
+ id, account_id, revision, configured_by_membership_id, configured_by_identity_id
963
+ ) VALUES (
964
+ new_policy_id, account_id_value, new_policy_revision, actor.id, actor_identity.id
965
+ );
966
+ FOR item IN
967
+ SELECT id, ordinality::integer AS ordinal
968
+ FROM pg_catalog.unnest(custodian_ids) WITH ORDINALITY candidate(id, ordinality)
969
+ ORDER BY ordinality
970
+ LOOP
971
+ SELECT * INTO candidate_membership
972
+ FROM organization_memberships membership
973
+ WHERE membership.account_id = account_id_value AND membership.id = item.id;
974
+ SELECT subject_row.identity_id, subject_row.auth_user_id
975
+ INTO candidate_identity_id, candidate_auth_user_id
976
+ FROM canonical_human_identity_subjects subject_row
977
+ WHERE candidate_membership.subject_id = 'user:' || subject_row.auth_user_id
978
+ AND subject_row.status = 'active';
979
+ INSERT INTO organization_recovery_custodians (
980
+ account_id, policy_id, ordinal, membership_id, canonical_identity_id
981
+ ) VALUES (
982
+ account_id_value, new_policy_id, item.ordinal,
983
+ candidate_membership.id, candidate_identity_id
984
+ );
985
+ END LOOP;
986
+ WITH superseded AS (
987
+ UPDATE organization_recovery_operations SET
988
+ state = 'superseded', superseded_at = now_value,
989
+ revision = revision + 1, updated_at = now_value
990
+ WHERE account_id = account_id_value AND state IN ('collecting', 'cooling')
991
+ RETURNING account_id, policy_id, id, revision
992
+ )
993
+ INSERT INTO organization_recovery_events (
994
+ account_id, policy_id, operation_id, command_operation_id, event_type,
995
+ actor_membership_id, actor_identity_id, event_revision, evidence
996
+ ) SELECT
997
+ superseded.account_id, superseded.policy_id, superseded.id, command_operation_id,
998
+ 'operation_superseded', actor.id, actor_identity.id, superseded.revision,
999
+ pg_catalog.jsonb_build_object('reason', 'policy_rotated')
1000
+ FROM superseded;
1001
+ INSERT INTO organization_recovery_policy_heads (
1002
+ account_id, current_policy_id, revision, enabled, activated_at, updated_at
1003
+ ) VALUES (
1004
+ account_id_value, new_policy_id, new_policy_revision, true, NULL, now_value
1005
+ ) ON CONFLICT (account_id) DO UPDATE SET
1006
+ current_policy_id = EXCLUDED.current_policy_id,
1007
+ revision = EXCLUDED.revision, enabled = true,
1008
+ activated_at = NULL, updated_at = EXCLUDED.updated_at;
1009
+ INSERT INTO organization_recovery_events (
1010
+ account_id, policy_id, command_operation_id, event_type,
1011
+ actor_membership_id, actor_identity_id, event_revision,
1012
+ evidence
1013
+ ) VALUES (
1014
+ account_id_value, new_policy_id, command_operation_id, 'policy_configured',
1015
+ actor.id, actor_identity.id, new_policy_revision,
1016
+ pg_catalog.jsonb_build_object('custodianCount', 3, 'priorPolicyId', head_row.current_policy_id)
1017
+ );
1018
+
1019
+ ELSIF action_name = 'accept_custody' THEN
1020
+ IF head_row.account_id IS NULL OR NOT head_row.enabled THEN
1021
+ RAISE EXCEPTION 'organization recovery is unavailable' USING ERRCODE = '55000';
1022
+ END IF;
1023
+ SELECT * INTO policy_row FROM organization_recovery_policies policy
1024
+ WHERE policy.id = head_row.current_policy_id AND policy.account_id = account_id_value;
1025
+ IF policy_row.revision <> expected_policy_revision THEN
1026
+ RAISE EXCEPTION 'organization recovery policy revision is stale' USING ERRCODE = '40001';
1027
+ END IF;
1028
+ PERFORM organization_recovery_lock_policy_evidence(policy_row.id, NULL);
1029
+ SELECT * INTO custodian_row FROM organization_recovery_custodians custodian
1030
+ WHERE custodian.policy_id = policy_row.id
1031
+ AND custodian.membership_id = actor.id
1032
+ AND custodian.canonical_identity_id = actor_identity.id
1033
+ FOR UPDATE;
1034
+ IF NOT FOUND OR actor.role = 'owner' THEN
1035
+ RAISE EXCEPTION 'organization recovery is unavailable' USING ERRCODE = '42501';
1036
+ END IF;
1037
+ current_acceptance_id := organization_recovery_valid_acceptance_id(
1038
+ policy_row.id, actor_identity.id
1039
+ );
1040
+ IF current_acceptance_id IS NULL THEN
1041
+ INSERT INTO organization_recovery_custodian_acceptances (
1042
+ account_id, policy_id, custodian_id, membership_id, canonical_identity_id,
1043
+ auth_user_id, login_binding_id, membership_authorization_revision,
1044
+ identity_revision, auth_revision, subject_revision, login_binding_revision,
1045
+ reauth_operation_id, accepted_at
1046
+ ) VALUES (
1047
+ account_id_value, policy_row.id, custodian_row.id, actor.id, actor_identity.id,
1048
+ actor_auth_user_id, (recent_proof ->> 'loginBindingId')::uuid,
1049
+ actor.authorization_revision, actor_identity.identity_revision,
1050
+ actor_identity.auth_revision, actor_subject_row.revision,
1051
+ (recent_proof ->> 'loginBindingRevision')::bigint,
1052
+ (recent_proof ->> 'reauthOperationId')::uuid, now_value
1053
+ ) RETURNING id INTO current_acceptance_id;
1054
+ INSERT INTO organization_recovery_events (
1055
+ account_id, policy_id, command_operation_id, event_type,
1056
+ actor_membership_id, actor_identity_id, event_revision
1057
+ ) VALUES (
1058
+ account_id_value, policy_row.id, command_operation_id, 'custody_accepted',
1059
+ actor.id, actor_identity.id, policy_row.revision
1060
+ );
1061
+ END IF;
1062
+ SELECT pg_catalog.count(*)::integer INTO valid_acceptance_count
1063
+ FROM organization_recovery_custodians custodian
1064
+ WHERE custodian.policy_id = policy_row.id
1065
+ AND organization_recovery_valid_acceptance_id(
1066
+ custodian.policy_id, custodian.canonical_identity_id
1067
+ ) IS NOT NULL;
1068
+ IF valid_acceptance_count = 3 AND head_row.activated_at IS NULL THEN
1069
+ UPDATE organization_recovery_policy_heads SET
1070
+ activated_at = now_value, updated_at = now_value
1071
+ WHERE account_id = account_id_value AND current_policy_id = policy_row.id;
1072
+ INSERT INTO organization_recovery_events (
1073
+ account_id, policy_id, command_operation_id, event_type,
1074
+ actor_membership_id, actor_identity_id, event_revision
1075
+ ) VALUES (
1076
+ account_id_value, policy_row.id, command_operation_id, 'policy_activated',
1077
+ actor.id, actor_identity.id, policy_row.revision
1078
+ );
1079
+ END IF;
1080
+
1081
+ ELSIF action_name = 'disable_policy' THEN
1082
+ IF actor.role <> 'owner' OR head_row.account_id IS NULL OR NOT head_row.enabled THEN
1083
+ RAISE EXCEPTION 'active organization owner required' USING ERRCODE = '42501';
1084
+ END IF;
1085
+ SELECT * INTO policy_row FROM organization_recovery_policies policy
1086
+ WHERE policy.id = head_row.current_policy_id AND policy.account_id = account_id_value;
1087
+ IF head_row.revision <> expected_policy_revision THEN
1088
+ RAISE EXCEPTION 'organization recovery policy revision is stale' USING ERRCODE = '40001';
1089
+ END IF;
1090
+ UPDATE organization_recovery_policy_heads SET
1091
+ enabled = false, revision = revision + 1, updated_at = now_value
1092
+ WHERE account_id = account_id_value;
1093
+ WITH superseded AS (
1094
+ UPDATE organization_recovery_operations SET
1095
+ state = 'superseded', superseded_at = now_value,
1096
+ revision = revision + 1, updated_at = now_value
1097
+ WHERE account_id = account_id_value AND state IN ('collecting', 'cooling')
1098
+ RETURNING account_id, policy_id, id, revision
1099
+ )
1100
+ INSERT INTO organization_recovery_events (
1101
+ account_id, policy_id, operation_id, command_operation_id, event_type,
1102
+ actor_membership_id, actor_identity_id, event_revision, evidence
1103
+ ) SELECT
1104
+ superseded.account_id, superseded.policy_id, superseded.id, command_operation_id,
1105
+ 'operation_superseded', actor.id, actor_identity.id, superseded.revision,
1106
+ pg_catalog.jsonb_build_object('reason', 'policy_disabled')
1107
+ FROM superseded;
1108
+ INSERT INTO organization_recovery_events (
1109
+ account_id, policy_id, command_operation_id, event_type,
1110
+ actor_membership_id, actor_identity_id, event_revision
1111
+ ) VALUES (
1112
+ account_id_value, policy_row.id, command_operation_id, 'policy_disabled',
1113
+ actor.id, actor_identity.id, policy_row.revision
1114
+ );
1115
+
1116
+ ELSIF action_name = 'start_operation' THEN
1117
+ IF head_row.account_id IS NULL OR NOT head_row.enabled THEN
1118
+ RAISE EXCEPTION 'organization recovery is unavailable' USING ERRCODE = '55000';
1119
+ END IF;
1120
+ recovery_operation_id := coalesce(recovery_operation_id, gen_random_uuid());
1121
+ SELECT * INTO policy_row FROM organization_recovery_policies policy
1122
+ WHERE policy.id = head_row.current_policy_id AND policy.account_id = account_id_value;
1123
+ IF policy_row.revision <> expected_policy_revision THEN
1124
+ RAISE EXCEPTION 'organization recovery policy revision is stale' USING ERRCODE = '40001';
1125
+ END IF;
1126
+ PERFORM organization_recovery_lock_policy_evidence(policy_row.id, NULL);
1127
+ SELECT pg_catalog.count(*)::integer INTO valid_acceptance_count
1128
+ FROM organization_recovery_custodians custodian
1129
+ WHERE custodian.policy_id = policy_row.id
1130
+ AND organization_recovery_valid_acceptance_id(
1131
+ custodian.policy_id, custodian.canonical_identity_id
1132
+ ) IS NOT NULL;
1133
+ SELECT * INTO custodian_row FROM organization_recovery_custodians custodian
1134
+ WHERE custodian.policy_id = policy_row.id
1135
+ AND custodian.membership_id = actor.id
1136
+ AND custodian.canonical_identity_id = actor_identity.id
1137
+ FOR UPDATE;
1138
+ current_acceptance_id := organization_recovery_valid_acceptance_id(
1139
+ policy_row.id, actor_identity.id
1140
+ );
1141
+ IF valid_acceptance_count <> 3 OR custodian_row.id IS NULL OR current_acceptance_id IS NULL THEN
1142
+ RAISE EXCEPTION 'organization recovery is unavailable' USING ERRCODE = '55000';
1143
+ END IF;
1144
+ WITH expired AS (
1145
+ UPDATE organization_recovery_operations SET
1146
+ state = 'expired', revision = revision + 1, updated_at = now_value
1147
+ WHERE account_id = account_id_value AND state IN ('collecting', 'cooling')
1148
+ AND expires_at <= now_value
1149
+ RETURNING account_id, policy_id, id, revision
1150
+ )
1151
+ INSERT INTO organization_recovery_events (
1152
+ account_id, policy_id, operation_id, command_operation_id, event_type,
1153
+ actor_membership_id, actor_identity_id, event_revision
1154
+ ) SELECT
1155
+ expired.account_id, expired.policy_id, expired.id, command_operation_id,
1156
+ 'operation_expired', actor.id, actor_identity.id, expired.revision
1157
+ FROM expired;
1158
+ IF EXISTS (
1159
+ SELECT 1 FROM organization_recovery_operations operation
1160
+ WHERE operation.account_id = account_id_value
1161
+ AND operation.state IN ('collecting', 'cooling')
1162
+ ) THEN
1163
+ RAISE EXCEPTION 'an organization recovery operation is already active'
1164
+ USING ERRCODE = '55000';
1165
+ END IF;
1166
+ target_membership_id := nullif(p_command ->> 'targetMembershipId', '')::uuid;
1167
+ SELECT * INTO target FROM organization_memberships membership
1168
+ WHERE membership.account_id = account_id_value AND membership.id = target_membership_id
1169
+ FOR UPDATE;
1170
+ IF NOT FOUND OR target.status <> 'active' OR target.role = 'owner'
1171
+ OR target.subject_id NOT LIKE 'user:%'
1172
+ THEN
1173
+ RAISE EXCEPTION 'organization recovery target is unavailable' USING ERRCODE = '42501';
1174
+ END IF;
1175
+ SELECT * INTO target_subject_row FROM canonical_human_identity_subjects subject_row
1176
+ WHERE subject_row.auth_user_id = pg_catalog.substr(target.subject_id, 6)
1177
+ AND subject_row.status = 'active' FOR UPDATE;
1178
+ SELECT * INTO target_identity FROM canonical_human_identities identity_row
1179
+ WHERE identity_row.id = target_subject_row.identity_id FOR UPDATE;
1180
+ IF target_subject_row.identity_id IS NULL OR target_identity.status <> 'active'
1181
+ OR target_identity.recovery_state <> 'ready'
1182
+ THEN
1183
+ RAISE EXCEPTION 'organization recovery target is unavailable' USING ERRCODE = '42501';
1184
+ END IF;
1185
+ INSERT INTO organization_recovery_operations (
1186
+ id, account_id, policy_id, policy_revision, target_membership_id,
1187
+ target_identity_id, target_auth_user_id, target_membership_authorization_revision,
1188
+ target_identity_revision, target_auth_revision, target_subject_revision,
1189
+ started_by_custodian_id, started_by_identity_id, expires_at, created_at, updated_at
1190
+ ) VALUES (
1191
+ recovery_operation_id, account_id_value, policy_row.id, policy_row.revision,
1192
+ target.id, target_identity.id, target_subject_row.auth_user_id,
1193
+ target.authorization_revision, target_identity.identity_revision,
1194
+ target_identity.auth_revision, target_subject_row.revision,
1195
+ custodian_row.id, actor_identity.id, now_value + interval '30 days', now_value, now_value
1196
+ ) RETURNING * INTO operation_row;
1197
+ INSERT INTO organization_recovery_events (
1198
+ account_id, policy_id, operation_id, command_operation_id, event_type,
1199
+ actor_membership_id, actor_identity_id, event_revision,
1200
+ evidence
1201
+ ) VALUES (
1202
+ account_id_value, policy_row.id, operation_row.id, command_operation_id,
1203
+ 'operation_started', actor.id, actor_identity.id, operation_row.revision,
1204
+ pg_catalog.jsonb_build_object('targetMembershipId', target.id::text)
1205
+ );
1206
+
1207
+ ELSIF action_name IN ('approve_operation', 'cancel_operation', 'execute_operation') THEN
1208
+ IF recovery_operation_id IS NULL THEN
1209
+ RAISE EXCEPTION 'organization recovery operation is unavailable' USING ERRCODE = '42501';
1210
+ END IF;
1211
+ SELECT * INTO operation_row FROM organization_recovery_operations operation
1212
+ WHERE operation.id = recovery_operation_id AND operation.account_id = account_id_value
1213
+ FOR UPDATE;
1214
+ IF NOT FOUND OR operation_row.revision <> expected_operation_revision THEN
1215
+ RAISE EXCEPTION 'organization recovery operation revision is stale' USING ERRCODE = '40001';
1216
+ END IF;
1217
+ IF operation_row.state NOT IN ('collecting', 'cooling')
1218
+ OR operation_row.expires_at <= now_value
1219
+ THEN
1220
+ RAISE EXCEPTION 'organization recovery operation is unavailable' USING ERRCODE = '55000';
1221
+ END IF;
1222
+ IF head_row.account_id IS NULL OR NOT head_row.enabled
1223
+ OR head_row.current_policy_id <> operation_row.policy_id
1224
+ THEN
1225
+ RAISE EXCEPTION 'organization recovery is unavailable' USING ERRCODE = '55000';
1226
+ END IF;
1227
+ SELECT * INTO policy_row FROM organization_recovery_policies policy
1228
+ WHERE policy.id = operation_row.policy_id AND policy.account_id = account_id_value;
1229
+
1230
+ IF action_name = 'cancel_operation' THEN
1231
+ IF actor.role <> 'owner' THEN
1232
+ RAISE EXCEPTION 'active organization owner required' USING ERRCODE = '42501';
1233
+ END IF;
1234
+ UPDATE organization_recovery_operations SET
1235
+ state = 'cancelled', cancelled_at = now_value,
1236
+ revision = revision + 1, updated_at = now_value
1237
+ WHERE id = operation_row.id RETURNING * INTO operation_row;
1238
+ INSERT INTO organization_recovery_events (
1239
+ account_id, policy_id, operation_id, command_operation_id, event_type,
1240
+ actor_membership_id, actor_identity_id, event_revision
1241
+ ) VALUES (
1242
+ account_id_value, policy_row.id, operation_row.id, command_operation_id,
1243
+ 'operation_cancelled', actor.id, actor_identity.id, operation_row.revision
1244
+ );
1245
+
1246
+ ELSE
1247
+ PERFORM organization_recovery_lock_policy_evidence(
1248
+ policy_row.id, operation_row.id
1249
+ );
1250
+ SELECT pg_catalog.count(*)::integer INTO valid_acceptance_count
1251
+ FROM organization_recovery_custodians custodian
1252
+ WHERE custodian.policy_id = policy_row.id
1253
+ AND organization_recovery_valid_acceptance_id(
1254
+ custodian.policy_id, custodian.canonical_identity_id
1255
+ ) IS NOT NULL;
1256
+ IF valid_acceptance_count <> 3 THEN
1257
+ RAISE EXCEPTION 'organization recovery is unavailable' USING ERRCODE = '55000';
1258
+ END IF;
1259
+ SELECT * INTO custodian_row FROM organization_recovery_custodians custodian
1260
+ WHERE custodian.policy_id = policy_row.id
1261
+ AND custodian.membership_id = actor.id
1262
+ AND custodian.canonical_identity_id = actor_identity.id
1263
+ FOR UPDATE;
1264
+ current_acceptance_id := organization_recovery_valid_acceptance_id(
1265
+ policy_row.id, actor_identity.id
1266
+ );
1267
+ IF custodian_row.id IS NULL OR current_acceptance_id IS NULL
1268
+ OR operation_row.target_identity_id = actor_identity.id OR actor.role = 'owner'
1269
+ THEN
1270
+ RAISE EXCEPTION 'organization recovery approval authority is invalid'
1271
+ USING ERRCODE = '42501';
1272
+ END IF;
1273
+
1274
+ IF action_name = 'approve_operation' THEN
1275
+ approvals_before := organization_recovery_valid_approval_count(operation_row.id);
1276
+ IF NOT EXISTS (
1277
+ SELECT 1 FROM organization_recovery_approvals approval
1278
+ WHERE approval.operation_id = operation_row.id
1279
+ AND approval.canonical_identity_id = actor_identity.id
1280
+ AND approval.membership_authorization_revision = actor.authorization_revision
1281
+ AND approval.identity_revision = actor_identity.identity_revision
1282
+ AND approval.auth_revision = actor_identity.auth_revision
1283
+ AND approval.subject_revision = actor_subject_row.revision
1284
+ AND approval.custodian_acceptance_id = current_acceptance_id
1285
+ ) THEN
1286
+ INSERT INTO organization_recovery_approvals (
1287
+ account_id, operation_id, operation_revision, policy_id, custodian_id,
1288
+ membership_id, canonical_identity_id, auth_user_id, login_binding_id,
1289
+ membership_authorization_revision, identity_revision, auth_revision,
1290
+ subject_revision, login_binding_revision, custodian_acceptance_id,
1291
+ reauth_operation_id, approved_at
1292
+ ) VALUES (
1293
+ account_id_value, operation_row.id, operation_row.revision,
1294
+ policy_row.id, custodian_row.id, actor.id, actor_identity.id,
1295
+ actor_auth_user_id, (recent_proof ->> 'loginBindingId')::uuid,
1296
+ actor.authorization_revision, actor_identity.identity_revision,
1297
+ actor_identity.auth_revision, actor_subject_row.revision,
1298
+ (recent_proof ->> 'loginBindingRevision')::bigint,
1299
+ current_acceptance_id, (recent_proof ->> 'reauthOperationId')::uuid, now_value
1300
+ );
1301
+ approvals_after := organization_recovery_valid_approval_count(operation_row.id);
1302
+ UPDATE organization_recovery_operations SET
1303
+ revision = revision + 1, updated_at = now_value
1304
+ WHERE id = operation_row.id RETURNING * INTO operation_row;
1305
+ INSERT INTO organization_recovery_events (
1306
+ account_id, policy_id, operation_id, command_operation_id, event_type,
1307
+ actor_membership_id, actor_identity_id, event_revision
1308
+ ) VALUES (
1309
+ account_id_value, policy_row.id, operation_row.id, command_operation_id,
1310
+ 'operation_approved', actor.id, actor_identity.id, operation_row.revision
1311
+ );
1312
+
1313
+ IF approvals_before < 2 AND approvals_after >= 2 THEN
1314
+ batch_id_value := gen_random_uuid();
1315
+ WITH raw_recipients AS (
1316
+ SELECT membership.id AS membership_id, subject_row.identity_id, 'owner'::text AS audience
1317
+ FROM organization_memberships membership
1318
+ INNER JOIN canonical_human_identity_subjects subject_row
1319
+ ON membership.subject_id = 'user:' || subject_row.auth_user_id
1320
+ AND subject_row.status = 'active'
1321
+ INNER JOIN canonical_human_identities identity_row
1322
+ ON identity_row.id = subject_row.identity_id
1323
+ AND identity_row.status = 'active' AND identity_row.recovery_state = 'ready'
1324
+ WHERE membership.account_id = account_id_value
1325
+ AND membership.status = 'active' AND membership.role = 'owner'
1326
+ UNION ALL
1327
+ SELECT custodian.membership_id, custodian.canonical_identity_id, 'custodian'::text
1328
+ FROM organization_recovery_custodians custodian
1329
+ WHERE custodian.policy_id = policy_row.id
1330
+ UNION ALL
1331
+ SELECT operation_row.target_membership_id, operation_row.target_identity_id,
1332
+ 'target'::text
1333
+ ), recipients AS (
1334
+ SELECT pg_catalog.min(membership_id::text)::uuid AS membership_id,
1335
+ identity_id,
1336
+ pg_catalog.string_agg(DISTINCT audience, ',' ORDER BY audience) AS audience
1337
+ FROM raw_recipients GROUP BY identity_id
1338
+ ), payloads AS (
1339
+ SELECT recipient.*,
1340
+ pg_catalog.jsonb_build_object(
1341
+ 'organizationId', account_id_value::text,
1342
+ 'operationId', operation_row.id::text,
1343
+ 'targetMembershipId', operation_row.target_membership_id::text,
1344
+ 'quorumAt', now_value,
1345
+ 'cooldownEndsAt', now_value + interval '7 days',
1346
+ 'authorityGained', 'organization_owner',
1347
+ 'existingOwnersPreserved', true,
1348
+ 'personalContentTransferred', false,
1349
+ 'workspaceTransferred', false,
1350
+ 'billingTransferred', false
1351
+ ) AS payload
1352
+ FROM recipients recipient
1353
+ )
1354
+ INSERT INTO organization_recovery_notification_outbox (
1355
+ account_id, policy_id, operation_id, batch_id,
1356
+ recipient_membership_id, recipient_identity_id, audience,
1357
+ notification_type, idempotency_key, payload_digest, payload, created_at
1358
+ ) SELECT
1359
+ account_id_value, policy_row.id, operation_row.id, batch_id_value,
1360
+ payload.membership_id, payload.identity_id, payload.audience,
1361
+ 'recovery_quorum_started',
1362
+ 'organization-recovery:' || operation_row.id::text || ':'
1363
+ || batch_id_value::text || ':' || payload.identity_id::text,
1364
+ pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
1365
+ payload.payload::text, 'UTF8'
1366
+ )), 'hex'), payload.payload, now_value
1367
+ FROM payloads payload;
1368
+ SELECT pg_catalog.count(*)::integer,
1369
+ pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
1370
+ pg_catalog.string_agg(
1371
+ outbox.id::text || ':' || outbox.payload_digest, ',' ORDER BY outbox.id
1372
+ ), 'UTF8'
1373
+ )), 'hex')
1374
+ INTO journal_count, journal_digest
1375
+ FROM organization_recovery_notification_outbox outbox
1376
+ WHERE outbox.operation_id = operation_row.id AND outbox.batch_id = batch_id_value;
1377
+ IF journal_count < 1 OR journal_digest IS NULL THEN
1378
+ RAISE EXCEPTION 'organization recovery notification journal is unavailable'
1379
+ USING ERRCODE = '55000';
1380
+ END IF;
1381
+ UPDATE organization_recovery_operations SET
1382
+ state = 'cooling', quorum_at = now_value,
1383
+ executable_at = now_value + interval '7 days',
1384
+ notification_batch_id = batch_id_value,
1385
+ notification_count = journal_count,
1386
+ notification_digest = journal_digest,
1387
+ revision = revision + 1, updated_at = now_value
1388
+ WHERE id = operation_row.id RETURNING * INTO operation_row;
1389
+ INSERT INTO organization_recovery_events (
1390
+ account_id, policy_id, operation_id, command_operation_id, event_type,
1391
+ actor_membership_id, actor_identity_id, event_revision,
1392
+ evidence
1393
+ ) VALUES (
1394
+ account_id_value, policy_row.id, operation_row.id, command_operation_id,
1395
+ 'quorum_started', actor.id, actor_identity.id, operation_row.revision,
1396
+ pg_catalog.jsonb_build_object(
1397
+ 'notificationBatchId', batch_id_value::text,
1398
+ 'notificationCount', journal_count,
1399
+ 'notificationDigest', journal_digest,
1400
+ 'cooldownDays', 7
1401
+ )
1402
+ );
1403
+ END IF;
1404
+ END IF;
1405
+
1406
+ ELSE
1407
+ -- Execute revalidates the complete policy, target, quorum, cooldown and
1408
+ -- exact immutable notification journal before the sole role update.
1409
+ approvals_after := organization_recovery_valid_approval_count(operation_row.id);
1410
+ IF valid_acceptance_count <> 3 OR approvals_after < 2
1411
+ OR operation_row.quorum_at IS NULL OR operation_row.executable_at > now_value
1412
+ OR NOT EXISTS (
1413
+ SELECT 1 FROM organization_recovery_approvals approval
1414
+ WHERE approval.operation_id = operation_row.id
1415
+ AND approval.canonical_identity_id = actor_identity.id
1416
+ AND approval.custodian_acceptance_id = current_acceptance_id
1417
+ AND approval.membership_authorization_revision = actor.authorization_revision
1418
+ AND approval.identity_revision = actor_identity.identity_revision
1419
+ AND approval.auth_revision = actor_identity.auth_revision
1420
+ AND approval.subject_revision = actor_subject_row.revision
1421
+ )
1422
+ THEN
1423
+ RAISE EXCEPTION 'organization recovery execution is unavailable'
1424
+ USING ERRCODE = '55000';
1425
+ END IF;
1426
+ SELECT * INTO target FROM organization_memberships membership
1427
+ WHERE membership.account_id = account_id_value
1428
+ AND membership.id = operation_row.target_membership_id
1429
+ FOR UPDATE;
1430
+ SELECT * INTO target_subject_row FROM canonical_human_identity_subjects subject_row
1431
+ WHERE subject_row.auth_user_id = operation_row.target_auth_user_id FOR UPDATE;
1432
+ SELECT * INTO target_identity FROM canonical_human_identities identity_row
1433
+ WHERE identity_row.id = operation_row.target_identity_id FOR UPDATE;
1434
+ IF target.id IS NULL OR target.status <> 'active' OR target.role = 'owner'
1435
+ OR target.authorization_revision <> operation_row.target_membership_authorization_revision
1436
+ OR target_subject_row.identity_id <> operation_row.target_identity_id
1437
+ OR target_subject_row.status <> 'active'
1438
+ OR target_subject_row.revision <> operation_row.target_subject_revision
1439
+ OR target_identity.status <> 'active' OR target_identity.recovery_state <> 'ready'
1440
+ OR target_identity.identity_revision <> operation_row.target_identity_revision
1441
+ OR target_identity.auth_revision <> operation_row.target_auth_revision
1442
+ THEN
1443
+ RAISE EXCEPTION 'organization recovery target is unavailable' USING ERRCODE = '55000';
1444
+ END IF;
1445
+ SELECT pg_catalog.count(*)::integer,
1446
+ pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
1447
+ pg_catalog.string_agg(
1448
+ outbox.id::text || ':' || outbox.payload_digest, ',' ORDER BY outbox.id
1449
+ ), 'UTF8'
1450
+ )), 'hex')
1451
+ INTO journal_count, journal_digest
1452
+ FROM organization_recovery_notification_outbox outbox
1453
+ WHERE outbox.operation_id = operation_row.id
1454
+ AND outbox.batch_id = operation_row.notification_batch_id;
1455
+ IF journal_count IS DISTINCT FROM operation_row.notification_count
1456
+ OR journal_digest IS DISTINCT FROM operation_row.notification_digest
1457
+ THEN
1458
+ RAISE EXCEPTION 'organization recovery notification journal is invalid'
1459
+ USING ERRCODE = '55000';
1460
+ END IF;
1461
+ UPDATE organization_memberships SET
1462
+ role = 'owner', authorization_revision = authorization_revision + 1,
1463
+ updated_at = now_value
1464
+ WHERE id = target.id AND account_id = account_id_value
1465
+ AND authorization_revision = operation_row.target_membership_authorization_revision;
1466
+ IF NOT FOUND THEN
1467
+ RAISE EXCEPTION 'organization recovery target revision is stale' USING ERRCODE = '40001';
1468
+ END IF;
1469
+ UPDATE organization_recovery_operations SET
1470
+ state = 'executed', executed_at = now_value,
1471
+ revision = revision + 1, updated_at = now_value
1472
+ WHERE id = operation_row.id AND state = 'cooling'
1473
+ RETURNING * INTO operation_row;
1474
+ IF NOT FOUND THEN
1475
+ RAISE EXCEPTION 'organization recovery execution is unavailable'
1476
+ USING ERRCODE = '55000';
1477
+ END IF;
1478
+ INSERT INTO organization_recovery_events (
1479
+ account_id, policy_id, operation_id, command_operation_id, event_type,
1480
+ actor_membership_id, actor_identity_id, event_revision,
1481
+ evidence
1482
+ ) VALUES (
1483
+ account_id_value, policy_row.id, operation_row.id, command_operation_id,
1484
+ 'operation_executed', actor.id, actor_identity.id, operation_row.revision,
1485
+ pg_catalog.jsonb_build_object(
1486
+ 'promotedMembershipId', target.id::text,
1487
+ 'authorityGained', 'organization_owner',
1488
+ 'existingOwnersPreserved', true,
1489
+ 'personalContentTransferred', false,
1490
+ 'workspaceTransferred', false,
1491
+ 'billingTransferred', false
1492
+ )
1493
+ );
1494
+ END IF;
1495
+ END IF;
1496
+ END IF;
1497
+
1498
+ result := organization_recovery_overview_json(
1499
+ account_id_value, actor_subject,
1500
+ (recent_proof ->> 'reauthenticatedAt')::timestamptz
1501
+ );
1502
+ INSERT INTO organization_recovery_command_receipts (
1503
+ account_id, operation_id, action, actor_membership_id,
1504
+ actor_identity_id, input_hash, result, created_at
1505
+ ) VALUES (
1506
+ account_id_value, command_operation_id, action_name, actor.id,
1507
+ actor_identity.id, input_hash_value, result, now_value
1508
+ );
1509
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
1510
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
1511
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
1512
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
1513
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
1514
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
1515
+ RETURN pg_catalog.jsonb_build_object('replay', false, 'overview', result);
1516
+ EXCEPTION WHEN OTHERS THEN
1517
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
1518
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
1519
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
1520
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
1521
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
1522
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
1523
+ RAISE;
1524
+ END
1525
+ $body$;
1526
+
1527
+ CREATE TRIGGER reject_workspace_account_id_change
1528
+ BEFORE UPDATE OF account_id ON "workspaces"
1529
+ FOR EACH ROW EXECUTE FUNCTION reject_workspace_account_id_change();
1530
+
1531
+ CREATE OR REPLACE FUNCTION organization_recovery_append_only()
1532
+ RETURNS trigger LANGUAGE plpgsql AS $body$
1533
+ BEGIN
1534
+ RAISE EXCEPTION 'organization recovery evidence is append-only' USING ERRCODE = '42501';
1535
+ END
1536
+ $body$;
1537
+
1538
+ CREATE TRIGGER organization_recovery_policies_append_only
1539
+ BEFORE UPDATE OR DELETE ON "organization_recovery_policies"
1540
+ FOR EACH ROW EXECUTE FUNCTION organization_recovery_append_only();
1541
+ CREATE TRIGGER organization_recovery_custodians_append_only
1542
+ BEFORE UPDATE OR DELETE ON "organization_recovery_custodians"
1543
+ FOR EACH ROW EXECUTE FUNCTION organization_recovery_append_only();
1544
+ CREATE TRIGGER organization_recovery_acceptances_append_only
1545
+ BEFORE UPDATE OR DELETE ON "organization_recovery_custodian_acceptances"
1546
+ FOR EACH ROW EXECUTE FUNCTION organization_recovery_append_only();
1547
+ CREATE TRIGGER organization_recovery_approvals_append_only
1548
+ BEFORE UPDATE OR DELETE ON "organization_recovery_approvals"
1549
+ FOR EACH ROW EXECUTE FUNCTION organization_recovery_append_only();
1550
+ CREATE TRIGGER organization_recovery_receipts_append_only
1551
+ BEFORE UPDATE OR DELETE ON "organization_recovery_command_receipts"
1552
+ FOR EACH ROW EXECUTE FUNCTION organization_recovery_append_only();
1553
+ CREATE TRIGGER organization_recovery_events_append_only
1554
+ BEFORE UPDATE OR DELETE ON "organization_recovery_events"
1555
+ FOR EACH ROW EXECUTE FUNCTION organization_recovery_append_only();
1556
+ CREATE TRIGGER organization_recovery_outbox_append_only
1557
+ BEFORE UPDATE OR DELETE ON "organization_recovery_notification_outbox"
1558
+ FOR EACH ROW EXECUTE FUNCTION organization_recovery_append_only();
1559
+ CREATE TRIGGER organization_recovery_attempts_append_only
1560
+ BEFORE UPDATE OR DELETE ON "organization_recovery_notification_attempts"
1561
+ FOR EACH ROW EXECUTE FUNCTION organization_recovery_append_only();
1562
+
1563
+ CREATE OR REPLACE FUNCTION organization_recovery_exactly_three_custodians()
1564
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER AS $body$
1565
+ DECLARE
1566
+ policy_id_value uuid := CASE WHEN TG_OP = 'DELETE' THEN OLD.id ELSE NEW.id END;
1567
+ previous_marker text := pg_catalog.current_setting('opengeni.organization_recovery_lifecycle', true);
1568
+ BEGIN
1569
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle', 'active', true);
1570
+ IF (SELECT pg_catalog.count(*) FROM organization_recovery_custodians custodian
1571
+ WHERE custodian.policy_id = policy_id_value) <> 3 THEN
1572
+ RAISE EXCEPTION 'an organization recovery policy must contain exactly three custodians'
1573
+ USING ERRCODE = '23514';
1574
+ END IF;
1575
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
1576
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END, true);
1577
+ RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
1578
+ EXCEPTION WHEN OTHERS THEN
1579
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
1580
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END, true);
1581
+ RAISE;
1582
+ END
1583
+ $body$;
1584
+ CREATE CONSTRAINT TRIGGER organization_recovery_exactly_three_custodians
1585
+ AFTER INSERT OR UPDATE ON "organization_recovery_policies"
1586
+ DEFERRABLE INITIALLY DEFERRED
1587
+ FOR EACH ROW EXECUTE FUNCTION organization_recovery_exactly_three_custodians();
1588
+
1589
+ DO $organization_recovery_rls$
1590
+ DECLARE table_name text;
1591
+ BEGIN
1592
+ FOREACH table_name IN ARRAY ARRAY[
1593
+ 'organization_recovery_policies', 'organization_recovery_policy_heads',
1594
+ 'organization_recovery_custodians', 'organization_recovery_custodian_acceptances',
1595
+ 'organization_recovery_operations', 'organization_recovery_approvals',
1596
+ 'organization_recovery_command_receipts', 'organization_recovery_events',
1597
+ 'organization_recovery_notification_outbox', 'organization_recovery_notification_attempts'
1598
+ ] LOOP
1599
+ EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', table_name);
1600
+ EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', table_name);
1601
+ EXECUTE format(
1602
+ 'CREATE POLICY organization_recovery_lifecycle ON %I '
1603
+ || 'USING (current_setting(''opengeni.organization_recovery_lifecycle'', true) = ''active'') '
1604
+ || 'WITH CHECK (current_setting(''opengeni.organization_recovery_lifecycle'', true) = ''active'')',
1605
+ table_name
1606
+ );
1607
+ EXECUTE format('REVOKE ALL ON TABLE %I FROM PUBLIC', table_name);
1608
+ END LOOP;
1609
+ END
1610
+ $organization_recovery_rls$;
1611
+
1612
+ -- The latest acceptance is valid only while every membership, canonical-human,
1613
+ -- subject, login-binding, and auth revision still exactly matches its evidence.
1614
+ CREATE OR REPLACE FUNCTION organization_recovery_valid_acceptance_id(
1615
+ p_policy_id uuid, p_identity_id uuid
1616
+ ) RETURNS uuid
1617
+ LANGUAGE plpgsql SECURITY DEFINER AS $body$
1618
+ DECLARE
1619
+ acceptance_id_value uuid;
1620
+ previous_recovery_marker text := pg_catalog.current_setting(
1621
+ 'opengeni.organization_recovery_lifecycle', true
1622
+ );
1623
+ previous_canonical_marker text := pg_catalog.current_setting(
1624
+ 'opengeni.canonical_human_identity_lifecycle', true
1625
+ );
1626
+ previous_organization_marker text := pg_catalog.current_setting(
1627
+ 'opengeni.organization_tenancy_lifecycle', true
1628
+ );
1629
+ BEGIN
1630
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle', 'active', true);
1631
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle', 'active', true);
1632
+ PERFORM pg_catalog.set_config(
1633
+ 'opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true
1634
+ );
1635
+ SELECT acceptance.id INTO acceptance_id_value
1636
+ FROM organization_recovery_custodian_acceptances acceptance
1637
+ INNER JOIN organization_recovery_custodians custodian
1638
+ ON custodian.id = acceptance.custodian_id
1639
+ AND custodian.policy_id = acceptance.policy_id
1640
+ AND custodian.membership_id = acceptance.membership_id
1641
+ AND custodian.canonical_identity_id = acceptance.canonical_identity_id
1642
+ INNER JOIN organization_memberships membership
1643
+ ON membership.id = acceptance.membership_id
1644
+ AND membership.account_id = acceptance.account_id
1645
+ INNER JOIN canonical_human_identity_subjects subject_row
1646
+ ON subject_row.auth_user_id = acceptance.auth_user_id
1647
+ AND subject_row.identity_id = acceptance.canonical_identity_id
1648
+ INNER JOIN canonical_human_identities identity_row
1649
+ ON identity_row.id = acceptance.canonical_identity_id
1650
+ INNER JOIN canonical_human_login_bindings binding
1651
+ ON binding.id = acceptance.login_binding_id
1652
+ AND binding.identity_id = acceptance.canonical_identity_id
1653
+ WHERE acceptance.policy_id = p_policy_id
1654
+ AND acceptance.canonical_identity_id = p_identity_id
1655
+ AND membership.subject_id = 'user:' || acceptance.auth_user_id
1656
+ AND membership.status = 'active' AND membership.role <> 'owner'
1657
+ AND membership.authorization_revision = acceptance.membership_authorization_revision
1658
+ AND subject_row.status = 'active' AND subject_row.revision = acceptance.subject_revision
1659
+ AND identity_row.status = 'active' AND identity_row.recovery_state = 'ready'
1660
+ AND identity_row.identity_revision = acceptance.identity_revision
1661
+ AND identity_row.auth_revision = acceptance.auth_revision
1662
+ AND binding.status = 'active' AND binding.revision = acceptance.login_binding_revision
1663
+ ORDER BY acceptance.accepted_at DESC, acceptance.id DESC
1664
+ LIMIT 1;
1665
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
1666
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
1667
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
1668
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
1669
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
1670
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
1671
+ RETURN acceptance_id_value;
1672
+ EXCEPTION WHEN OTHERS THEN
1673
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
1674
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
1675
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
1676
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
1677
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
1678
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
1679
+ RAISE;
1680
+ END
1681
+ $body$;
1682
+
1683
+ CREATE OR REPLACE FUNCTION organization_recovery_valid_approval_count(
1684
+ p_operation_id uuid
1685
+ ) RETURNS integer
1686
+ LANGUAGE plpgsql SECURITY DEFINER AS $body$
1687
+ DECLARE
1688
+ approval_count integer;
1689
+ previous_recovery_marker text := pg_catalog.current_setting(
1690
+ 'opengeni.organization_recovery_lifecycle', true
1691
+ );
1692
+ previous_canonical_marker text := pg_catalog.current_setting(
1693
+ 'opengeni.canonical_human_identity_lifecycle', true
1694
+ );
1695
+ previous_organization_marker text := pg_catalog.current_setting(
1696
+ 'opengeni.organization_tenancy_lifecycle', true
1697
+ );
1698
+ BEGIN
1699
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle', 'active', true);
1700
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle', 'active', true);
1701
+ PERFORM pg_catalog.set_config(
1702
+ 'opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true
1703
+ );
1704
+ SELECT pg_catalog.count(DISTINCT approval.canonical_identity_id)::integer
1705
+ INTO approval_count
1706
+ FROM organization_recovery_approvals approval
1707
+ INNER JOIN organization_recovery_operations operation
1708
+ ON operation.id = approval.operation_id
1709
+ AND operation.account_id = approval.account_id
1710
+ AND operation.policy_id = approval.policy_id
1711
+ INNER JOIN organization_recovery_custodians custodian
1712
+ ON custodian.id = approval.custodian_id
1713
+ AND custodian.policy_id = approval.policy_id
1714
+ AND custodian.membership_id = approval.membership_id
1715
+ AND custodian.canonical_identity_id = approval.canonical_identity_id
1716
+ INNER JOIN organization_memberships membership
1717
+ ON membership.id = approval.membership_id
1718
+ AND membership.account_id = approval.account_id
1719
+ INNER JOIN canonical_human_identity_subjects subject_row
1720
+ ON subject_row.auth_user_id = approval.auth_user_id
1721
+ AND subject_row.identity_id = approval.canonical_identity_id
1722
+ INNER JOIN canonical_human_identities identity_row
1723
+ ON identity_row.id = approval.canonical_identity_id
1724
+ INNER JOIN canonical_human_login_bindings binding
1725
+ ON binding.id = approval.login_binding_id
1726
+ AND binding.identity_id = approval.canonical_identity_id
1727
+ WHERE approval.operation_id = p_operation_id
1728
+ AND approval.canonical_identity_id <> operation.target_identity_id
1729
+ AND membership.subject_id = 'user:' || approval.auth_user_id
1730
+ AND membership.status = 'active' AND membership.role <> 'owner'
1731
+ AND membership.authorization_revision = approval.membership_authorization_revision
1732
+ AND subject_row.status = 'active' AND subject_row.revision = approval.subject_revision
1733
+ AND identity_row.status = 'active' AND identity_row.recovery_state = 'ready'
1734
+ AND identity_row.identity_revision = approval.identity_revision
1735
+ AND identity_row.auth_revision = approval.auth_revision
1736
+ AND binding.status = 'active' AND binding.revision = approval.login_binding_revision
1737
+ AND organization_recovery_valid_acceptance_id(
1738
+ approval.policy_id, approval.canonical_identity_id
1739
+ ) = approval.custodian_acceptance_id;
1740
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
1741
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
1742
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
1743
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
1744
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
1745
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
1746
+ RETURN coalesce(approval_count, 0);
1747
+ EXCEPTION WHEN OTHERS THEN
1748
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
1749
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
1750
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
1751
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
1752
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
1753
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
1754
+ RAISE;
1755
+ END
1756
+ $body$;
1757
+
1758
+ -- A proof is recent only when it is the current selected slot's exact
1759
+ -- complete_reauth operation and every stamped identity authority is still live.
1760
+ CREATE OR REPLACE FUNCTION organization_recovery_resolve_recent_reauth(
1761
+ p_fence jsonb, p_actor_subject text, p_auth_session_id text, p_auth_user_id text
1762
+ ) RETURNS jsonb
1763
+ LANGUAGE plpgsql SECURITY DEFINER AS $body$
1764
+ DECLARE
1765
+ authority_hash_value text := p_fence ->> 'authorityHash';
1766
+ actor_epoch_value bigint := nullif(p_fence ->> 'actorEpoch', '')::bigint;
1767
+ result jsonb;
1768
+ previous_managed_marker text := pg_catalog.current_setting(
1769
+ 'opengeni.managed_auth_session_set_lifecycle', true
1770
+ );
1771
+ previous_canonical_marker text := pg_catalog.current_setting(
1772
+ 'opengeni.canonical_human_identity_lifecycle', true
1773
+ );
1774
+ BEGIN
1775
+ IF p_fence IS NULL OR pg_catalog.jsonb_typeof(p_fence) <> 'object'
1776
+ OR authority_hash_value !~ '^[0-9a-f]{64}$' OR actor_epoch_value < 1
1777
+ OR p_auth_session_id IS NULL OR p_auth_user_id IS NULL
1778
+ OR p_actor_subject <> 'user:' || p_auth_user_id
1779
+ THEN
1780
+ RETURN NULL;
1781
+ END IF;
1782
+ PERFORM pg_catalog.set_config('opengeni.managed_auth_session_set_lifecycle', 'active', true);
1783
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle', 'active', true);
1784
+ SELECT pg_catalog.jsonb_build_object(
1785
+ 'sessionSetId', session_set.id::text,
1786
+ 'generation', session_set.generation::text,
1787
+ 'actorEpoch', session_set.actor_epoch::text,
1788
+ 'slotId', slot.id::text,
1789
+ 'authSessionId', auth_session.id,
1790
+ 'authUserId', auth_session.user_id,
1791
+ 'canonicalIdentityId', identity_row.id::text,
1792
+ 'identityRevision', identity_row.identity_revision::text,
1793
+ 'authRevision', identity_row.auth_revision::text,
1794
+ 'subjectRevision', subject_row.revision::text,
1795
+ 'loginBindingId', binding.id::text,
1796
+ 'loginBindingRevision', binding.revision::text,
1797
+ 'reauthOperationId', operation.operation_id::text,
1798
+ 'reauthenticatedAt', operation.created_at
1799
+ ) INTO result
1800
+ FROM managed_auth_session_sets session_set
1801
+ INNER JOIN managed_auth_browser_installations installation
1802
+ ON installation.id = session_set.installation_id
1803
+ INNER JOIN managed_auth_login_slots slot
1804
+ ON slot.id = session_set.selected_slot_id
1805
+ AND slot.session_set_id = session_set.id
1806
+ INNER JOIN auth_sessions auth_session
1807
+ ON auth_session.id = slot.auth_session_id
1808
+ INNER JOIN canonical_human_identity_subjects subject_row
1809
+ ON subject_row.auth_user_id = auth_session.user_id
1810
+ AND subject_row.identity_id = slot.identity_id
1811
+ INNER JOIN canonical_human_identities identity_row
1812
+ ON identity_row.id = slot.identity_id
1813
+ INNER JOIN canonical_human_login_bindings binding
1814
+ ON binding.id = slot.login_binding_id
1815
+ AND binding.identity_id = slot.identity_id
1816
+ INNER JOIN managed_auth_session_set_operations operation
1817
+ ON operation.session_set_id = session_set.id
1818
+ WHERE session_set.authority_hash = authority_hash_value
1819
+ AND session_set.actor_epoch = actor_epoch_value
1820
+ AND session_set.state = 'ready' AND session_set.revoked_at IS NULL
1821
+ AND session_set.idle_expires_at > pg_catalog.clock_timestamp()
1822
+ AND session_set.absolute_expires_at > pg_catalog.clock_timestamp()
1823
+ AND installation.revoked_at IS NULL
1824
+ AND installation.idle_expires_at > pg_catalog.clock_timestamp()
1825
+ AND installation.absolute_expires_at > pg_catalog.clock_timestamp()
1826
+ AND slot.status = 'active' AND slot.auth_session_id = p_auth_session_id
1827
+ AND slot.auth_user_id = p_auth_user_id
1828
+ AND auth_session.user_id = p_auth_user_id
1829
+ AND auth_session.expires_at > pg_catalog.clock_timestamp()
1830
+ AND auth_session.identity_id = slot.identity_id
1831
+ AND auth_session.identity_revision = slot.identity_revision
1832
+ AND auth_session.auth_revision = slot.auth_revision
1833
+ AND auth_session.login_binding_id = slot.login_binding_id
1834
+ AND auth_session.login_binding_revision = slot.login_binding_revision
1835
+ AND subject_row.status = 'active'
1836
+ AND identity_row.status = 'active' AND identity_row.recovery_state = 'ready'
1837
+ AND identity_row.identity_revision = slot.identity_revision
1838
+ AND identity_row.auth_revision = slot.auth_revision
1839
+ AND binding.status = 'active' AND binding.revision = slot.login_binding_revision
1840
+ AND operation.operation_type = 'complete_reauth'
1841
+ AND operation.target_slot_id = session_set.selected_slot_id
1842
+ AND operation.result_generation = session_set.generation
1843
+ AND operation.result_actor_epoch = session_set.actor_epoch
1844
+ AND operation.outcome IN ('applied', 'converged')
1845
+ AND operation.created_at >= pg_catalog.clock_timestamp() - interval '10 minutes'
1846
+ AND operation.created_at <= pg_catalog.clock_timestamp()
1847
+ ORDER BY operation.created_at DESC, operation.operation_id DESC
1848
+ LIMIT 1;
1849
+ PERFORM pg_catalog.set_config('opengeni.managed_auth_session_set_lifecycle',
1850
+ CASE WHEN previous_managed_marker IS NULL THEN '' ELSE previous_managed_marker END, true);
1851
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
1852
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
1853
+ RETURN result;
1854
+ EXCEPTION WHEN OTHERS THEN
1855
+ PERFORM pg_catalog.set_config('opengeni.managed_auth_session_set_lifecycle',
1856
+ CASE WHEN previous_managed_marker IS NULL THEN '' ELSE previous_managed_marker END, true);
1857
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
1858
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
1859
+ RAISE;
1860
+ END
1861
+ $body$;
1862
+
1863
+ CREATE OR REPLACE FUNCTION organization_recovery_assert_recent_reauth(
1864
+ p_fence jsonb, p_actor_subject text, p_auth_session_id text, p_auth_user_id text
1865
+ ) RETURNS jsonb
1866
+ LANGUAGE plpgsql SECURITY DEFINER AS $body$
1867
+ DECLARE
1868
+ result jsonb;
1869
+ BEGIN
1870
+ result := organization_recovery_resolve_recent_reauth(
1871
+ p_fence, p_actor_subject, p_auth_session_id, p_auth_user_id
1872
+ );
1873
+ IF result IS NULL THEN
1874
+ RAISE EXCEPTION 'recent reauthentication is required' USING ERRCODE = '42501';
1875
+ END IF;
1876
+ RETURN result;
1877
+ END
1878
+ $body$;
1879
+
1880
+ CREATE OR REPLACE FUNCTION organization_recovery_valid_approvals_json(
1881
+ p_operation_id uuid
1882
+ ) RETURNS jsonb
1883
+ LANGUAGE plpgsql SECURITY DEFINER AS $body$
1884
+ DECLARE
1885
+ result jsonb;
1886
+ previous_recovery_marker text := pg_catalog.current_setting(
1887
+ 'opengeni.organization_recovery_lifecycle', true
1888
+ );
1889
+ previous_canonical_marker text := pg_catalog.current_setting(
1890
+ 'opengeni.canonical_human_identity_lifecycle', true
1891
+ );
1892
+ previous_organization_marker text := pg_catalog.current_setting(
1893
+ 'opengeni.organization_tenancy_lifecycle', true
1894
+ );
1895
+ BEGIN
1896
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle', 'active', true);
1897
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle', 'active', true);
1898
+ PERFORM pg_catalog.set_config(
1899
+ 'opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true
1900
+ );
1901
+ WITH current_approvals AS (
1902
+ SELECT DISTINCT ON (approval.canonical_identity_id)
1903
+ approval.membership_id, approval.canonical_identity_id, approval.approved_at, approval.id,
1904
+ auth_user.name, auth_user.email
1905
+ FROM organization_recovery_approvals approval
1906
+ INNER JOIN organization_recovery_operations operation
1907
+ ON operation.id = approval.operation_id
1908
+ AND operation.account_id = approval.account_id
1909
+ AND operation.policy_id = approval.policy_id
1910
+ INNER JOIN organization_recovery_custodians custodian
1911
+ ON custodian.id = approval.custodian_id
1912
+ AND custodian.policy_id = approval.policy_id
1913
+ AND custodian.membership_id = approval.membership_id
1914
+ AND custodian.canonical_identity_id = approval.canonical_identity_id
1915
+ INNER JOIN organization_memberships membership
1916
+ ON membership.id = approval.membership_id
1917
+ AND membership.account_id = approval.account_id
1918
+ INNER JOIN canonical_human_identity_subjects subject_row
1919
+ ON subject_row.auth_user_id = approval.auth_user_id
1920
+ AND subject_row.identity_id = approval.canonical_identity_id
1921
+ INNER JOIN canonical_human_identities identity_row
1922
+ ON identity_row.id = approval.canonical_identity_id
1923
+ INNER JOIN canonical_human_login_bindings binding
1924
+ ON binding.id = approval.login_binding_id
1925
+ AND binding.identity_id = approval.canonical_identity_id
1926
+ INNER JOIN auth_users auth_user ON auth_user.id = approval.auth_user_id
1927
+ WHERE approval.operation_id = p_operation_id
1928
+ AND approval.canonical_identity_id <> operation.target_identity_id
1929
+ AND membership.subject_id = 'user:' || approval.auth_user_id
1930
+ AND membership.status = 'active' AND membership.role <> 'owner'
1931
+ AND membership.authorization_revision = approval.membership_authorization_revision
1932
+ AND subject_row.status = 'active' AND subject_row.revision = approval.subject_revision
1933
+ AND identity_row.status = 'active' AND identity_row.recovery_state = 'ready'
1934
+ AND identity_row.identity_revision = approval.identity_revision
1935
+ AND identity_row.auth_revision = approval.auth_revision
1936
+ AND binding.status = 'active' AND binding.revision = approval.login_binding_revision
1937
+ AND organization_recovery_valid_acceptance_id(
1938
+ approval.policy_id, approval.canonical_identity_id
1939
+ ) = approval.custodian_acceptance_id
1940
+ ORDER BY approval.canonical_identity_id, approval.approved_at DESC, approval.id DESC
1941
+ )
1942
+ SELECT coalesce(pg_catalog.jsonb_agg(
1943
+ pg_catalog.jsonb_build_object(
1944
+ 'membershipId', approval.membership_id::text,
1945
+ 'name', approval.name,
1946
+ 'email', approval.email,
1947
+ 'approvedAt', approval.approved_at
1948
+ ) ORDER BY approval.approved_at, approval.id
1949
+ ), '[]'::jsonb) INTO result
1950
+ FROM current_approvals approval;
1951
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
1952
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
1953
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
1954
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
1955
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
1956
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
1957
+ RETURN result;
1958
+ EXCEPTION WHEN OTHERS THEN
1959
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
1960
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
1961
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
1962
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
1963
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
1964
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
1965
+ RAISE;
1966
+ END
1967
+ $body$;
1968
+
1969
+ CREATE OR REPLACE FUNCTION organization_recovery_overview_json(
1970
+ p_account_id uuid, p_actor_subject text, p_recent_reauthentication_at timestamptz
1971
+ ) RETURNS jsonb
1972
+ LANGUAGE plpgsql SECURITY DEFINER AS $body$
1973
+ DECLARE
1974
+ head_row organization_recovery_policy_heads%ROWTYPE;
1975
+ policy_row organization_recovery_policies%ROWTYPE;
1976
+ operation_row organization_recovery_operations%ROWTYPE;
1977
+ custodian_rows jsonb := '[]'::jsonb;
1978
+ eligible_member_rows jsonb := '[]'::jsonb;
1979
+ eligible_member_count integer := 0;
1980
+ valid_acceptances integer := 0;
1981
+ valid_approvals integer := 0;
1982
+ policy_state text;
1983
+ operation_state text;
1984
+ target_current boolean := false;
1985
+ target_name text;
1986
+ target_email text;
1987
+ approvals_rows jsonb := '[]'::jsonb;
1988
+ actor_membership organization_memberships%ROWTYPE;
1989
+ actor_subject_row canonical_human_identity_subjects%ROWTYPE;
1990
+ actor_identity canonical_human_identities%ROWTYPE;
1991
+ actor_is_custodian boolean := false;
1992
+ actor_has_acceptance boolean := false;
1993
+ actor_has_approval boolean := false;
1994
+ notification_journaled boolean := false;
1995
+ actual_notification_count integer := 0;
1996
+ actual_notification_digest text;
1997
+ capability_configure boolean := false;
1998
+ capability_accept boolean := false;
1999
+ capability_disable boolean := false;
2000
+ capability_start boolean := false;
2001
+ capability_approve boolean := false;
2002
+ capability_cancel boolean := false;
2003
+ capability_execute boolean := false;
2004
+ has_recent_reauthentication boolean := p_recent_reauthentication_at IS NOT NULL;
2005
+ now_value timestamptz := pg_catalog.clock_timestamp();
2006
+ previous_recovery_marker text := pg_catalog.current_setting(
2007
+ 'opengeni.organization_recovery_lifecycle', true
2008
+ );
2009
+ previous_canonical_marker text := pg_catalog.current_setting(
2010
+ 'opengeni.canonical_human_identity_lifecycle', true
2011
+ );
2012
+ previous_organization_marker text := pg_catalog.current_setting(
2013
+ 'opengeni.organization_tenancy_lifecycle', true
2014
+ );
2015
+ BEGIN
2016
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle', 'active', true);
2017
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle', 'active', true);
2018
+ PERFORM pg_catalog.set_config(
2019
+ 'opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true
2020
+ );
2021
+ SELECT * INTO actor_membership FROM organization_memberships membership
2022
+ WHERE membership.account_id = p_account_id AND membership.subject_id = p_actor_subject
2023
+ AND membership.status = 'active' FOR KEY SHARE;
2024
+ IF actor_membership.id IS NULL OR p_actor_subject NOT LIKE 'user:%' THEN
2025
+ RAISE EXCEPTION 'organization recovery authority is invalid' USING ERRCODE = '42501';
2026
+ END IF;
2027
+ IF p_actor_subject LIKE 'user:%' THEN
2028
+ SELECT * INTO actor_subject_row FROM canonical_human_identity_subjects subject_row
2029
+ WHERE subject_row.auth_user_id = pg_catalog.substr(p_actor_subject, 6)
2030
+ AND subject_row.status = 'active' FOR KEY SHARE;
2031
+ IF actor_subject_row.identity_id IS NOT NULL THEN
2032
+ SELECT * INTO actor_identity FROM canonical_human_identities identity_row
2033
+ WHERE identity_row.id = actor_subject_row.identity_id
2034
+ AND identity_row.status = 'active' AND identity_row.recovery_state = 'ready'
2035
+ FOR KEY SHARE;
2036
+ END IF;
2037
+ END IF;
2038
+ capability_configure := has_recent_reauthentication
2039
+ AND actor_membership.role = 'owner' AND actor_identity.id IS NOT NULL;
2040
+ SELECT pg_catalog.count(*)::integer,
2041
+ coalesce(pg_catalog.jsonb_agg(
2042
+ pg_catalog.jsonb_build_object(
2043
+ 'membershipId', membership.id::text,
2044
+ 'name', auth_user.name,
2045
+ 'email', auth_user.email
2046
+ ) ORDER BY lower(coalesce(auth_user.name, auth_user.email, membership.subject_id)),
2047
+ membership.id
2048
+ ), '[]'::jsonb)
2049
+ INTO eligible_member_count, eligible_member_rows
2050
+ FROM organization_memberships membership
2051
+ INNER JOIN canonical_human_identity_subjects subject_row
2052
+ ON membership.subject_id = 'user:' || subject_row.auth_user_id
2053
+ AND subject_row.status = 'active'
2054
+ INNER JOIN canonical_human_identities identity_row
2055
+ ON identity_row.id = subject_row.identity_id
2056
+ AND identity_row.status = 'active' AND identity_row.recovery_state = 'ready'
2057
+ INNER JOIN auth_users auth_user ON auth_user.id = subject_row.auth_user_id
2058
+ WHERE membership.account_id = p_account_id
2059
+ AND membership.status = 'active' AND membership.role <> 'owner';
2060
+ IF eligible_member_count > 1000 THEN
2061
+ RAISE EXCEPTION 'organization recovery target inventory exceeds the bounded projection'
2062
+ USING ERRCODE = '54000';
2063
+ END IF;
2064
+ SELECT * INTO head_row FROM organization_recovery_policy_heads head
2065
+ WHERE head.account_id = p_account_id;
2066
+ IF NOT FOUND THEN
2067
+ IF actor_membership.role <> 'owner' THEN
2068
+ RAISE EXCEPTION 'organization recovery authority is invalid' USING ERRCODE = '42501';
2069
+ END IF;
2070
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
2071
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
2072
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
2073
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
2074
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
2075
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
2076
+ RETURN pg_catalog.jsonb_build_object(
2077
+ 'organizationId', p_account_id::text, 'policy', NULL, 'operation', NULL,
2078
+ 'availability', 'recovery_unavailable', 'unavailableReason', 'no_policy',
2079
+ 'recentReauthenticationAt', p_recent_reauthentication_at,
2080
+ 'eligibleMembers', eligible_member_rows,
2081
+ 'capabilities', pg_catalog.jsonb_build_object(
2082
+ 'configure', capability_configure, 'accept', false, 'disable', false,
2083
+ 'start', false, 'approve', false, 'cancel', false, 'execute', false
2084
+ )
2085
+ );
2086
+ END IF;
2087
+ SELECT * INTO policy_row FROM organization_recovery_policies policy
2088
+ WHERE policy.id = head_row.current_policy_id AND policy.account_id = p_account_id;
2089
+ SELECT pg_catalog.count(*) FILTER (WHERE current_acceptance.id IS NOT NULL)::integer,
2090
+ coalesce(pg_catalog.jsonb_agg(
2091
+ pg_catalog.jsonb_build_object(
2092
+ 'ordinal', custodian.ordinal,
2093
+ 'membershipId', custodian.membership_id::text,
2094
+ 'name', auth_user.name,
2095
+ 'email', auth_user.email,
2096
+ 'enrollmentState', CASE
2097
+ WHEN current_acceptance.id IS NOT NULL THEN 'accepted'
2098
+ WHEN current_eligibility.eligible THEN 'pending_acceptance'
2099
+ ELSE 'ineligible'
2100
+ END,
2101
+ 'acceptedAt', current_acceptance.accepted_at
2102
+ ) ORDER BY custodian.ordinal
2103
+ ), '[]'::jsonb)
2104
+ INTO valid_acceptances, custodian_rows
2105
+ FROM organization_recovery_custodians custodian
2106
+ INNER JOIN organization_memberships membership
2107
+ ON membership.id = custodian.membership_id AND membership.account_id = custodian.account_id
2108
+ LEFT JOIN auth_users auth_user ON membership.subject_id = 'user:' || auth_user.id
2109
+ LEFT JOIN LATERAL (
2110
+ SELECT acceptance.id, acceptance.accepted_at
2111
+ FROM organization_recovery_custodian_acceptances acceptance
2112
+ WHERE acceptance.id = organization_recovery_valid_acceptance_id(
2113
+ custodian.policy_id, custodian.canonical_identity_id
2114
+ )
2115
+ ) current_acceptance ON true
2116
+ LEFT JOIN LATERAL (
2117
+ SELECT EXISTS (
2118
+ SELECT 1
2119
+ FROM canonical_human_identity_subjects subject_row
2120
+ INNER JOIN canonical_human_identities identity_row
2121
+ ON identity_row.id = subject_row.identity_id
2122
+ WHERE membership.subject_id = 'user:' || subject_row.auth_user_id
2123
+ AND subject_row.identity_id = custodian.canonical_identity_id
2124
+ AND subject_row.status = 'active'
2125
+ AND identity_row.status = 'active' AND identity_row.recovery_state = 'ready'
2126
+ AND membership.status = 'active' AND membership.role <> 'owner'
2127
+ ) AS eligible
2128
+ ) current_eligibility ON true
2129
+ WHERE custodian.policy_id = policy_row.id;
2130
+ policy_state := CASE
2131
+ WHEN NOT head_row.enabled THEN 'disabled'
2132
+ WHEN valid_acceptances = 3 THEN 'active'
2133
+ WHEN head_row.activated_at IS NULL THEN 'pending_acceptance'
2134
+ ELSE 'degraded'
2135
+ END;
2136
+ SELECT * INTO operation_row FROM organization_recovery_operations operation
2137
+ WHERE operation.account_id = p_account_id
2138
+ ORDER BY operation.created_at DESC, operation.id DESC LIMIT 1;
2139
+ IF FOUND THEN
2140
+ valid_approvals := organization_recovery_valid_approval_count(operation_row.id);
2141
+ approvals_rows := organization_recovery_valid_approvals_json(operation_row.id);
2142
+ SELECT auth_user.name, auth_user.email INTO target_name, target_email
2143
+ FROM auth_users auth_user WHERE auth_user.id = operation_row.target_auth_user_id;
2144
+ SELECT EXISTS (
2145
+ SELECT 1
2146
+ FROM organization_memberships membership
2147
+ INNER JOIN canonical_human_identity_subjects subject_row
2148
+ ON membership.subject_id = 'user:' || subject_row.auth_user_id
2149
+ INNER JOIN canonical_human_identities identity_row
2150
+ ON identity_row.id = subject_row.identity_id
2151
+ WHERE membership.id = operation_row.target_membership_id
2152
+ AND membership.account_id = operation_row.account_id
2153
+ AND membership.status = 'active' AND membership.role <> 'owner'
2154
+ AND membership.authorization_revision = operation_row.target_membership_authorization_revision
2155
+ AND subject_row.auth_user_id = operation_row.target_auth_user_id
2156
+ AND subject_row.identity_id = operation_row.target_identity_id
2157
+ AND subject_row.status = 'active'
2158
+ AND subject_row.revision = operation_row.target_subject_revision
2159
+ AND identity_row.status = 'active' AND identity_row.recovery_state = 'ready'
2160
+ AND identity_row.identity_revision = operation_row.target_identity_revision
2161
+ AND identity_row.auth_revision = operation_row.target_auth_revision
2162
+ ) INTO target_current;
2163
+ operation_state := CASE
2164
+ WHEN operation_row.state IN ('executed', 'cancelled', 'superseded') THEN operation_row.state
2165
+ WHEN operation_row.expires_at <= now_value THEN 'expired'
2166
+ WHEN operation_row.policy_id <> head_row.current_policy_id OR NOT head_row.enabled
2167
+ THEN 'superseded'
2168
+ WHEN policy_state <> 'active' OR valid_approvals < 2 OR NOT target_current THEN 'collecting'
2169
+ ELSE 'cooling'
2170
+ END;
2171
+ IF operation_row.notification_batch_id IS NOT NULL THEN
2172
+ SELECT pg_catalog.count(*)::integer,
2173
+ pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
2174
+ pg_catalog.string_agg(
2175
+ outbox.id::text || ':' || outbox.payload_digest, ',' ORDER BY outbox.id
2176
+ ), 'UTF8'
2177
+ )), 'hex')
2178
+ INTO actual_notification_count, actual_notification_digest
2179
+ FROM organization_recovery_notification_outbox outbox
2180
+ WHERE outbox.operation_id = operation_row.id
2181
+ AND outbox.batch_id = operation_row.notification_batch_id;
2182
+ notification_journaled := actual_notification_count > 0
2183
+ AND actual_notification_count IS NOT DISTINCT FROM operation_row.notification_count
2184
+ AND actual_notification_digest IS NOT DISTINCT FROM operation_row.notification_digest;
2185
+ END IF;
2186
+ END IF;
2187
+ IF actor_identity.id IS NOT NULL THEN
2188
+ actor_is_custodian := EXISTS (
2189
+ SELECT 1 FROM organization_recovery_custodians custodian
2190
+ WHERE custodian.policy_id = policy_row.id
2191
+ AND custodian.membership_id = actor_membership.id
2192
+ AND custodian.canonical_identity_id = actor_identity.id
2193
+ );
2194
+ actor_has_acceptance := actor_is_custodian AND
2195
+ organization_recovery_valid_acceptance_id(policy_row.id, actor_identity.id) IS NOT NULL;
2196
+ actor_has_approval := operation_row.id IS NOT NULL AND EXISTS (
2197
+ SELECT 1 FROM pg_catalog.jsonb_array_elements(approvals_rows) approval
2198
+ WHERE approval ->> 'membershipId' = actor_membership.id::text
2199
+ );
2200
+ END IF;
2201
+ IF actor_membership.role <> 'owner' AND NOT actor_is_custodian THEN
2202
+ RAISE EXCEPTION 'organization recovery authority is invalid' USING ERRCODE = '42501';
2203
+ END IF;
2204
+
2205
+ capability_accept := has_recent_reauthentication AND head_row.enabled AND actor_is_custodian
2206
+ AND actor_membership.role <> 'owner' AND actor_identity.id IS NOT NULL
2207
+ AND NOT actor_has_acceptance;
2208
+ capability_disable := has_recent_reauthentication
2209
+ AND actor_membership.role = 'owner' AND actor_identity.id IS NOT NULL
2210
+ AND head_row.enabled;
2211
+ capability_start := has_recent_reauthentication AND head_row.enabled AND policy_state = 'active'
2212
+ AND actor_is_custodian AND actor_has_acceptance
2213
+ AND actor_membership.role <> 'owner' AND actor_identity.id IS NOT NULL
2214
+ AND NOT EXISTS (
2215
+ SELECT 1 FROM organization_recovery_operations live_operation
2216
+ WHERE live_operation.account_id = p_account_id
2217
+ AND live_operation.state IN ('collecting', 'cooling')
2218
+ AND live_operation.expires_at > now_value
2219
+ );
2220
+ capability_approve := has_recent_reauthentication AND operation_row.id IS NOT NULL
2221
+ AND operation_row.state IN ('collecting', 'cooling')
2222
+ AND operation_row.expires_at > now_value
2223
+ AND head_row.enabled AND policy_state = 'active' AND valid_acceptances = 3
2224
+ AND operation_row.policy_id = head_row.current_policy_id
2225
+ AND actor_is_custodian AND actor_has_acceptance
2226
+ AND actor_membership.role <> 'owner' AND actor_identity.id IS NOT NULL
2227
+ AND operation_row.target_identity_id <> actor_identity.id
2228
+ AND NOT actor_has_approval;
2229
+ capability_cancel := has_recent_reauthentication AND operation_row.id IS NOT NULL
2230
+ AND operation_row.state IN ('collecting', 'cooling')
2231
+ AND operation_row.expires_at > now_value
2232
+ AND head_row.enabled AND operation_row.policy_id = head_row.current_policy_id
2233
+ AND actor_membership.role = 'owner' AND actor_identity.id IS NOT NULL;
2234
+ capability_execute := has_recent_reauthentication AND operation_row.id IS NOT NULL
2235
+ AND operation_row.state = 'cooling' AND operation_state = 'cooling'
2236
+ AND operation_row.executable_at <= now_value AND operation_row.expires_at > now_value
2237
+ AND policy_state = 'active' AND valid_acceptances = 3 AND valid_approvals >= 2
2238
+ AND target_current AND notification_journaled
2239
+ AND actor_is_custodian AND actor_has_acceptance AND actor_has_approval
2240
+ AND actor_membership.role <> 'owner' AND actor_identity.id IS NOT NULL
2241
+ AND operation_row.target_identity_id <> actor_identity.id;
2242
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
2243
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
2244
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
2245
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
2246
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
2247
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
2248
+ RETURN pg_catalog.jsonb_build_object(
2249
+ 'organizationId', p_account_id::text,
2250
+ 'availability', CASE
2251
+ WHEN policy_state = 'active' AND actor_identity.id IS NOT NULL
2252
+ THEN 'available' ELSE 'recovery_unavailable' END,
2253
+ 'unavailableReason', CASE
2254
+ WHEN actor_identity.id IS NULL THEN 'identity_unavailable'
2255
+ WHEN policy_state = 'pending_acceptance' THEN 'pending_acceptance'
2256
+ WHEN policy_state = 'degraded' THEN 'degraded'
2257
+ WHEN policy_state = 'disabled' THEN 'disabled'
2258
+ ELSE NULL END,
2259
+ 'recentReauthenticationAt', p_recent_reauthentication_at,
2260
+ 'eligibleMembers', eligible_member_rows,
2261
+ 'policy', pg_catalog.jsonb_build_object(
2262
+ 'id', policy_row.id::text, 'organizationId', p_account_id::text,
2263
+ 'revision', head_row.revision, 'state', policy_state,
2264
+ 'custodians', custodian_rows
2265
+ , 'createdAt', policy_row.configured_at, 'updatedAt', head_row.updated_at
2266
+ ),
2267
+ 'operation', CASE WHEN operation_row.id IS NULL THEN NULL ELSE pg_catalog.jsonb_build_object(
2268
+ 'id', operation_row.id::text, 'organizationId', p_account_id::text,
2269
+ 'policyId', operation_row.policy_id::text,
2270
+ 'policyRevision', operation_row.policy_revision,
2271
+ 'revision', operation_row.revision,
2272
+ 'state', operation_state,
2273
+ 'target', pg_catalog.jsonb_build_object(
2274
+ 'membershipId', operation_row.target_membership_id::text,
2275
+ 'name', target_name, 'email', target_email
2276
+ ),
2277
+ 'approvalCount', valid_approvals,
2278
+ 'approvals', approvals_rows,
2279
+ 'quorumAt', operation_row.quorum_at,
2280
+ 'executableAt', operation_row.executable_at,
2281
+ 'expiresAt', operation_row.expires_at,
2282
+ 'notificationJournaled', notification_journaled,
2283
+ 'executedAt', operation_row.executed_at,
2284
+ 'cancelledAt', operation_row.cancelled_at,
2285
+ 'createdAt', operation_row.created_at,
2286
+ 'updatedAt', operation_row.updated_at
2287
+ ) END,
2288
+ 'capabilities', pg_catalog.jsonb_build_object(
2289
+ 'configure', capability_configure, 'accept', capability_accept,
2290
+ 'disable', capability_disable, 'start', capability_start,
2291
+ 'approve', capability_approve, 'cancel', capability_cancel,
2292
+ 'execute', capability_execute
2293
+ )
2294
+ );
2295
+ EXCEPTION WHEN OTHERS THEN
2296
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
2297
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
2298
+ PERFORM pg_catalog.set_config('opengeni.canonical_human_identity_lifecycle',
2299
+ CASE WHEN previous_canonical_marker IS NULL THEN '' ELSE previous_canonical_marker END, true);
2300
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
2301
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
2302
+ RAISE;
2303
+ END
2304
+ $body$;
2305
+
2306
+ CREATE OR REPLACE FUNCTION get_organization_recovery_overview(
2307
+ p_account_id uuid, p_actor_subject text, p_fence jsonb,
2308
+ p_auth_session_id text, p_auth_user_id text
2309
+ ) RETURNS jsonb
2310
+ LANGUAGE plpgsql SECURITY DEFINER AS $body$
2311
+ DECLARE
2312
+ recent_proof jsonb;
2313
+ previous_recovery_marker text := pg_catalog.current_setting(
2314
+ 'opengeni.organization_recovery_lifecycle', true
2315
+ );
2316
+ previous_organization_marker text := pg_catalog.current_setting(
2317
+ 'opengeni.organization_tenancy_lifecycle', true
2318
+ );
2319
+ BEGIN
2320
+ IF p_account_id IS NULL OR p_actor_subject IS NULL
2321
+ OR p_actor_subject IS DISTINCT FROM opengeni_private.current_subject_id()
2322
+ OR p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
2323
+ THEN
2324
+ RAISE EXCEPTION 'organization recovery authority is invalid' USING ERRCODE = '42501';
2325
+ END IF;
2326
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle', 'active', true);
2327
+ PERFORM pg_catalog.set_config(
2328
+ 'opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true
2329
+ );
2330
+ IF NOT EXISTS (
2331
+ SELECT 1 FROM organization_memberships membership
2332
+ WHERE membership.account_id = p_account_id AND membership.subject_id = p_actor_subject
2333
+ AND membership.status = 'active'
2334
+ ) THEN
2335
+ RAISE EXCEPTION 'organization recovery authority is invalid' USING ERRCODE = '42501';
2336
+ END IF;
2337
+ recent_proof := organization_recovery_resolve_recent_reauth(
2338
+ p_fence, p_actor_subject, p_auth_session_id, p_auth_user_id
2339
+ );
2340
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
2341
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
2342
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
2343
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
2344
+ RETURN organization_recovery_overview_json(
2345
+ p_account_id, p_actor_subject,
2346
+ CASE WHEN recent_proof IS NULL THEN NULL
2347
+ ELSE (recent_proof ->> 'reauthenticatedAt')::timestamptz END
2348
+ );
2349
+ EXCEPTION WHEN OTHERS THEN
2350
+ PERFORM pg_catalog.set_config('opengeni.organization_recovery_lifecycle',
2351
+ CASE WHEN previous_recovery_marker IS NULL THEN '' ELSE previous_recovery_marker END, true);
2352
+ PERFORM pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
2353
+ CASE WHEN previous_organization_marker IS NULL THEN '' ELSE previous_organization_marker END, true);
2354
+ RAISE;
2355
+ END
2356
+ $body$;
2357
+
2358
+ DO $organization_recovery_security$
2359
+ DECLARE
2360
+ data_schema text := current_schema();
2361
+ signature text;
2362
+ BEGIN
2363
+ FOREACH signature IN ARRAY ARRAY[
2364
+ 'organization_recovery_command(jsonb)',
2365
+ 'organization_recovery_lock_policy_evidence(uuid,uuid)',
2366
+ 'get_organization_recovery_overview(uuid,text,jsonb,text,text)',
2367
+ 'organization_recovery_overview_json(uuid,text,timestamptz)',
2368
+ 'organization_recovery_valid_acceptance_id(uuid,uuid)',
2369
+ 'organization_recovery_valid_approval_count(uuid)',
2370
+ 'organization_recovery_valid_approvals_json(uuid)',
2371
+ 'organization_recovery_resolve_recent_reauth(jsonb,text,text,text)',
2372
+ 'organization_recovery_assert_recent_reauth(jsonb,text,text,text)',
2373
+ 'organization_recovery_exactly_three_custodians()',
2374
+ 'prepare_organization_recovery_notifications(text,text,integer,integer)',
2375
+ 'settle_organization_recovery_notification(uuid,uuid,text,text,text,text)',
2376
+ 'reconcile_organization_recovery_notification(uuid,uuid,text,text,text)'
2377
+ ] LOOP
2378
+ EXECUTE format(
2379
+ 'ALTER FUNCTION %I.%s SET search_path = pg_catalog, %I, pg_temp',
2380
+ data_schema, signature, data_schema
2381
+ );
2382
+ EXECUTE format('REVOKE ALL ON FUNCTION %I.%s FROM PUBLIC', data_schema, signature);
2383
+ END LOOP;
2384
+ FOREACH signature IN ARRAY ARRAY[
2385
+ 'reject_workspace_account_id_change()',
2386
+ 'organization_recovery_append_only()'
2387
+ ] LOOP
2388
+ EXECUTE format(
2389
+ 'ALTER FUNCTION %I.%s SET search_path = pg_catalog, %I, pg_temp',
2390
+ data_schema, signature, data_schema
2391
+ );
2392
+ EXECUTE format('REVOKE ALL ON FUNCTION %I.%s FROM PUBLIC', data_schema, signature);
2393
+ END LOOP;
2394
+ IF pg_catalog.to_regrole('opengeni_app') IS NOT NULL THEN
2395
+ EXECUTE format(
2396
+ 'GRANT EXECUTE ON FUNCTION %I.organization_recovery_command(jsonb) TO opengeni_app',
2397
+ data_schema
2398
+ );
2399
+ EXECUTE format(
2400
+ 'GRANT EXECUTE ON FUNCTION %I.get_organization_recovery_overview(uuid,text,jsonb,text,text) TO opengeni_app',
2401
+ data_schema
2402
+ );
2403
+ END IF;
2404
+ END
2405
+ $organization_recovery_security$;
2406
+
2407
+ COMMENT ON TABLE "organization_recovery_policies" IS
2408
+ 'Immutable revisioned three-custodian organization recovery policy evidence.';
2409
+ COMMENT ON TABLE "organization_recovery_operations" IS
2410
+ 'CAS-governed quorum recovery operations with fixed seven-day cooldown and promotion-only execution.';
2411
+ COMMENT ON TABLE "organization_recovery_command_receipts" IS
2412
+ 'Append-only exact-command recovery receipts; request actor-lease ids are excluded from semantic digests.';
2413
+ COMMENT ON TABLE "organization_recovery_notification_outbox" IS
2414
+ 'Immutable provider-neutral notification work; external I/O occurs only after provider_started is journaled.';
2415
+ COMMENT ON TABLE "organization_recovery_notification_attempts" IS
2416
+ 'Append-only bounded delivery claims, terminal outcomes, stale-claim evidence, and explicit ambiguous-outcome reconciliation.';
2417
+ COMMENT ON FUNCTION reject_workspace_account_id_change() IS
2418
+ 'Rejects every distinct workspace organization-owner mutation, including direct migration-owner SQL.';