@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.
- package/dist/canonical-human-identities.d.ts +20 -0
- package/dist/canonical-human-identities.js +7 -3
- package/dist/{chunk-ZWRIPCTR.js → chunk-2NT476HD.js} +2 -2
- package/dist/{chunk-PYMY3AJW.js → chunk-744DFC65.js} +202 -7
- package/dist/chunk-744DFC65.js.map +1 -0
- package/dist/{chunk-O6FTELKH.js → chunk-CDMMSUWJ.js} +3 -3
- package/dist/chunk-G3732M3T.js +299 -0
- package/dist/chunk-G3732M3T.js.map +1 -0
- package/dist/{chunk-TRBOWUG3.js → chunk-IW6O6G7R.js} +2 -2
- package/dist/{chunk-HY3F5Q4E.js → chunk-Q5YUGQVK.js} +3 -3
- package/dist/{chunk-V3TRUXFJ.js → chunk-UGDVSQ6U.js} +72 -19
- package/dist/chunk-UGDVSQ6U.js.map +1 -0
- package/dist/{chunk-C2AFP7SB.js → chunk-UJ2EW3QI.js} +6664 -6051
- package/dist/chunk-UJ2EW3QI.js.map +1 -0
- package/dist/{chunk-H7MXYG7R.js → chunk-WW7AJ5IC.js} +2 -2
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/index.d.ts +44 -6
- package/dist/index.js +2914 -1535
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-set-schema.d.ts +1399 -0
- package/dist/managed-auth-session-sets.d.ts +145 -0
- package/dist/managed-auth-session-sets.js +50 -0
- package/dist/managed-auth-session-sets.js.map +1 -0
- package/dist/organization-recovery-schema.d.ts +2144 -0
- package/dist/organization-recovery.d.ts +186 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +7 -4
- package/dist/schema.d.ts +9 -6
- package/dist/schema.js +43 -1
- package/dist/session-background-commands.js +3 -3
- package/dist/session-control.d.ts +5 -3
- package/dist/session-queue-commands.d.ts +2 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/video-generation.js +3 -3
- package/dist/work-claims-schema.d.ts +975 -0
- package/dist/work-claims.d.ts +83 -0
- package/drizzle/0362_managed_auth_session_sets.sql +2347 -0
- package/drizzle/0363_organization_recovery_custody.sql +2418 -0
- package/drizzle/0364_workspace_learning_policy_snapshot_lock_order.sql +221 -0
- package/drizzle/0365_permission_scoped_work_claims.sql +1306 -0
- package/drizzle/0366_session_discovery_title_search_index.sql +5 -0
- package/drizzle/0367_session_discovery_goal_search_index.sql +5 -0
- package/drizzle/0368_session_discovery_claim_search_index.sql +6 -0
- package/package.json +10 -6
- package/src/canonical-human-identities.ts +105 -27
- package/src/index.ts +1471 -433
- package/src/managed-auth-session-set-schema.ts +176 -0
- package/src/managed-auth-session-sets.ts +430 -0
- package/src/organization-recovery-schema.ts +277 -0
- package/src/organization-recovery.ts +424 -0
- package/src/provision-roles.ts +67 -0
- package/src/runtime-posture.ts +144 -6
- package/src/schema.ts +3 -0
- package/src/session-control.ts +255 -16
- package/src/session-queue-commands.ts +10 -1
- package/src/session-tool-call-settlement.ts +6 -1
- package/src/work-claims-schema.ts +148 -0
- package/src/work-claims.ts +200 -0
- package/dist/chunk-C2AFP7SB.js.map +0 -1
- package/dist/chunk-PYMY3AJW.js.map +0 -1
- package/dist/chunk-V3TRUXFJ.js.map +0 -1
- /package/dist/{chunk-ZWRIPCTR.js.map → chunk-2NT476HD.js.map} +0 -0
- /package/dist/{chunk-O6FTELKH.js.map → chunk-CDMMSUWJ.js.map} +0 -0
- /package/dist/{chunk-TRBOWUG3.js.map → chunk-IW6O6G7R.js.map} +0 -0
- /package/dist/{chunk-HY3F5Q4E.js.map → chunk-Q5YUGQVK.js.map} +0 -0
- /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.';
|