@opengeni/db 0.36.1 → 3.1.0-canary.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/attached-browser-devices.d.ts +7 -0
- package/dist/automations.d.ts +154 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-integrations.d.ts +13 -0
- package/dist/child-lifecycle-notices.d.ts +68 -0
- package/dist/{chunk-GYO7ED2Q.js → chunk-EULAN2NP.js} +2 -2
- package/dist/{chunk-VTPRIXKG.js → chunk-NJVZPWXY.js} +3 -3
- package/dist/{chunk-HSOWSYOX.js → chunk-NMUBV7Z5.js} +19 -11
- package/dist/chunk-NMUBV7Z5.js.map +1 -0
- package/dist/chunk-OF5OBB76.js +4457 -0
- package/dist/chunk-OF5OBB76.js.map +1 -0
- package/dist/{chunk-5KTIEDX7.js → chunk-PR2WMLOQ.js} +7714 -4443
- package/dist/chunk-PR2WMLOQ.js.map +1 -0
- package/dist/{chunk-Z7VYMNPZ.js → chunk-SCPUJS53.js} +41 -4
- package/dist/chunk-SCPUJS53.js.map +1 -0
- package/dist/{chunk-SK7YYHBI.js → chunk-SIBYBA2D.js} +46 -1
- package/dist/chunk-SIBYBA2D.js.map +1 -0
- package/dist/chunk-TIKF3KTI.js +383 -0
- package/dist/chunk-TIKF3KTI.js.map +1 -0
- package/dist/chunk-ZZ5LJEFX.js +516 -0
- package/dist/chunk-ZZ5LJEFX.js.map +1 -0
- package/dist/codemode-operations.d.ts +1 -6
- package/dist/company-brain-context-selection-schema.d.ts +705 -0
- package/dist/company-brain-context-selection.d.ts +94 -0
- package/dist/company-brain-governed-writes.d.ts +24 -0
- package/dist/company-brain.d.ts +55 -0
- package/dist/company-profile-agent-admin.d.ts +15 -0
- package/dist/company-profile-schema.d.ts +619 -2
- package/dist/company-profile.d.ts +9 -4
- package/dist/connection-authority.d.ts +95 -0
- package/dist/connection-scopes.d.ts +8 -0
- package/dist/connection-tenancy-backfill.d.ts +41 -0
- package/dist/connection-token-resolver.d.ts +30 -1
- package/dist/database.d.ts +19 -0
- package/dist/editable-artifact-durable-export.d.ts +10 -0
- package/dist/editable-artifact-durable-export.js +18 -3
- package/dist/editable-artifact-durable-export.js.map +1 -1
- package/dist/editable-artifacts.d.ts +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/governed-learning-activation-schema.d.ts +984 -0
- package/dist/governed-learning-activation.d.ts +98 -0
- package/dist/governed-learning-evaluator-schema.d.ts +633 -0
- package/dist/governed-learning-evaluator.d.ts +28 -0
- package/dist/index.d.ts +1633 -87
- package/dist/index.js +22004 -8109
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +18 -0
- package/dist/integration-bindings.d.ts +1 -0
- package/dist/integration-facets.d.ts +30 -0
- package/dist/interaction-placement-loss.d.ts +22 -0
- package/dist/knowledge-source-sync-schema.d.ts +730 -0
- package/dist/knowledge-source-sync.d.ts +60 -0
- package/dist/managed-human-provisioning.d.ts +25 -0
- package/dist/memory-domain.d.ts +3 -3
- package/dist/migrate.d.ts +11 -1
- package/dist/migrate.js +1 -1
- package/dist/new-session-drafts.d.ts +43 -5
- package/dist/organization-membership-backfill.d.ts +174 -0
- package/dist/organization-membership-lifecycle.d.ts +162 -0
- package/dist/pr-review.d.ts +107 -0
- package/dist/preference-registry-schema.d.ts +2 -0
- package/dist/preference-registry.d.ts +2 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +23 -9
- package/dist/schema.d.ts +24925 -13697
- package/dist/schema.js +103 -1
- package/dist/scoped-knowledge-schema.d.ts +52 -1
- package/dist/session-background-commands.d.ts +125 -0
- package/dist/session-background-commands.js +34 -0
- package/dist/session-background-commands.js.map +1 -0
- package/dist/session-control.d.ts +156 -3
- package/dist/session-goal-pacing.d.ts +68 -0
- package/dist/session-queue-commands.d.ts +42 -5
- package/dist/session-realtime-ledger.d.ts +3 -0
- package/dist/session-tenancy.d.ts +71 -1
- package/dist/session-tenancy.js +25 -3
- package/dist/session-tool-call-settlement.d.ts +9 -4
- package/dist/slack-routing-personal-workspace.d.ts +23 -0
- package/dist/slack-routing.d.ts +153 -0
- package/dist/slack-task-policy-schema.d.ts +34 -0
- package/dist/slack-task-policy.d.ts +8 -0
- package/dist/slack-user-link-access.d.ts +1 -0
- package/dist/task-notes-schema.d.ts +594 -0
- package/dist/task-notes.d.ts +39 -0
- package/dist/turn-initiator.d.ts +2 -0
- package/dist/user-resource-authority.d.ts +109 -0
- package/dist/video-generation.js +3 -3
- package/dist/workspace-authority.d.ts +81 -0
- package/dist/workspace-instruction-policies-schema.d.ts +279 -0
- package/dist/workspace-instruction-policies.d.ts +62 -0
- package/dist/workspace-learning-policy.d.ts +1 -0
- package/dist/workspace-membership-permissions.d.ts +2 -0
- package/drizzle/0240_enrollment_connection_authority.sql +39 -0
- package/drizzle/0240_model_context_user_messages.sql +204 -0
- package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
- package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
- package/drizzle/0243_google_drive_object_acl_authority.sql +554 -0
- package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
- package/drizzle/0245_model_context_contribution_facts.sql +88 -0
- package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
- package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
- package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
- package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
- package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
- package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
- package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
- package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
- package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
- package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
- package/drizzle/0256_connection_authority_delegation.sql +703 -0
- package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
- package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
- package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
- package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
- package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
- package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
- package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
- package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
- package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
- package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
- package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
- package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
- package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
- package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
- package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
- package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
- package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
- package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
- package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
- package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
- package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
- package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
- package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
- package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
- package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
- package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
- package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
- package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
- package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
- package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
- package/drizzle/0289_session_composer_policy_authority.sql +597 -0
- package/drizzle/0290_organization_membership_backfill.sql +268 -0
- package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
- package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
- package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
- package/drizzle/0294_preference_activation_authority.sql +237 -0
- package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
- package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
- package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
- package/drizzle/0298_organization_tenancy_parity.sql +609 -0
- package/drizzle/0299_organization_membership_lock_order.sql +291 -0
- package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
- package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
- package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
- package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
- package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
- package/drizzle/0305_personal_resource_grant_management.sql +584 -0
- package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
- package/drizzle/0307_session_attention_state.sql +51 -0
- package/drizzle/0308_session_archives.sql +35 -0
- package/drizzle/0309_channel_project_pins.sql +6 -0
- package/drizzle/0310_channel_project_order.sql +27 -0
- package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
- package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
- package/drizzle/0313_private_child_session_authority.sql +365 -0
- package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
- package/drizzle/0315_personal_github_repository_selection.sql +624 -0
- package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
- package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
- package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
- package/drizzle/0319_event_triggered_automations.sql +254 -0
- package/drizzle/0320_pr_review_pack.sql +113 -0
- package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
- package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
- package/drizzle/0323_organization_private_session_enablement.sql +489 -0
- package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
- package/drizzle/0325_child_lifecycle_notices.sql +44 -0
- package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
- package/drizzle/0327_slack_first_task_hint.sql +29 -0
- package/drizzle/0328_session_background_commands.sql +590 -0
- package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
- package/drizzle/0330_api_key_descriptions.sql +20 -0
- package/drizzle/0331_managed_organization_creation.sql +163 -0
- package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
- package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
- package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
- package/drizzle/0335_slack_workspace_routing.sql +460 -0
- package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
- package/drizzle/0337_slack_routed_action_handles.sql +189 -0
- package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
- package/drizzle/0339_document_authority_reclassification.sql +1153 -0
- package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
- package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -0
- package/package.json +10 -6
- package/src/attached-browser-devices.ts +233 -5
- package/src/automations.ts +911 -0
- package/src/browser-sessions.ts +10 -0
- package/src/capability-integrations.ts +175 -1
- package/src/child-lifecycle-notices.ts +241 -0
- package/src/codemode-operations.ts +1 -35
- package/src/company-brain-context-selection-schema.ts +100 -0
- package/src/company-brain-context-selection.ts +359 -0
- package/src/company-brain-governed-writes.ts +1170 -0
- package/src/company-brain.ts +309 -0
- package/src/company-profile-agent-admin.ts +225 -0
- package/src/company-profile-schema.ts +121 -1
- package/src/company-profile.ts +21 -0
- package/src/computer-sessions.ts +10 -1
- package/src/connection-authority.ts +290 -0
- package/src/connection-scopes.ts +20 -0
- package/src/connection-tenancy-backfill.ts +81 -0
- package/src/connection-token-resolver.ts +263 -31
- package/src/database.ts +75 -2
- package/src/editable-artifact-durable-export.ts +27 -0
- package/src/editable-artifacts-schema.ts +10 -5
- package/src/editable-artifacts.ts +21 -11
- package/src/governed-learning-activation-schema.ts +121 -0
- package/src/governed-learning-activation.ts +588 -0
- package/src/governed-learning-evaluator-schema.ts +72 -0
- package/src/governed-learning-evaluator.ts +217 -0
- package/src/index.ts +12001 -1699
- package/src/insights.ts +107 -0
- package/src/integration-bindings.ts +88 -6
- package/src/integration-facets.ts +287 -84
- package/src/interaction-placement-loss.ts +234 -0
- package/src/knowledge-source-sync-schema.ts +83 -0
- package/src/knowledge-source-sync.ts +530 -0
- package/src/managed-human-provisioning.ts +116 -0
- package/src/memory-domain.ts +27 -32
- package/src/migrate.ts +77 -1
- package/src/new-session-drafts.ts +229 -35
- package/src/organization-membership-backfill.ts +735 -0
- package/src/organization-membership-lifecycle.ts +985 -0
- package/src/pr-review.ts +890 -0
- package/src/preference-registry.ts +31 -1
- package/src/provision-roles.ts +931 -24
- package/src/runtime-posture-cli.ts +2 -0
- package/src/runtime-posture.ts +1315 -52
- package/src/schema.ts +3336 -438
- package/src/scoped-knowledge-schema.ts +16 -8
- package/src/scoped-knowledge.ts +1 -1
- package/src/session-background-commands.ts +759 -0
- package/src/session-control.ts +640 -74
- package/src/session-goal-pacing.ts +126 -0
- package/src/session-queue-commands.ts +450 -98
- package/src/session-realtime-context.ts +19 -3
- package/src/session-realtime-ledger.ts +75 -12
- package/src/session-realtime-mirror.ts +1 -1
- package/src/session-tenancy.ts +403 -49
- package/src/session-tool-call-settlement.ts +38 -29
- package/src/slack-routing-personal-workspace.ts +67 -0
- package/src/slack-routing.ts +469 -0
- package/src/slack-task-policy-schema.ts +5 -0
- package/src/slack-task-policy.ts +18 -0
- package/src/slack-user-link-access.ts +17 -1
- package/src/task-notes-schema.ts +74 -0
- package/src/task-notes.ts +124 -2
- package/src/turn-initiator.ts +8 -0
- package/src/user-resource-authority.ts +377 -0
- package/src/workspace-authority.ts +160 -0
- package/src/workspace-instruction-policies-schema.ts +53 -1
- package/src/workspace-instruction-policies.ts +528 -77
- package/src/workspace-learning-policy.ts +21 -5
- package/src/workspace-membership-permissions.ts +9 -0
- package/dist/chunk-5KTIEDX7.js.map +0 -1
- package/dist/chunk-FPVAD4CO.js +0 -117
- package/dist/chunk-FPVAD4CO.js.map +0 -1
- package/dist/chunk-HSOWSYOX.js.map +0 -1
- package/dist/chunk-SK7YYHBI.js.map +0 -1
- package/dist/chunk-VKLB2ZGI.js +0 -2389
- package/dist/chunk-VKLB2ZGI.js.map +0 -1
- package/dist/chunk-Z7VYMNPZ.js.map +0 -1
- /package/dist/{chunk-GYO7ED2Q.js.map → chunk-EULAN2NP.js.map} +0 -0
- /package/dist/{chunk-VTPRIXKG.js.map → chunk-NJVZPWXY.js.map} +0 -0
|
@@ -0,0 +1,3284 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Organization invitation, role, suspension, reactivation, offboarding and
|
|
3
|
+
-- retention-policy authority and the operator-driven destructive retention
|
|
4
|
+
-- lifecycle. Provider email delivery remains outside this database lifecycle.
|
|
5
|
+
|
|
6
|
+
SET LOCAL lock_timeout = '5s';
|
|
7
|
+
SET LOCAL statement_timeout = '10min';
|
|
8
|
+
|
|
9
|
+
ALTER TABLE "session_realtime_modes"
|
|
10
|
+
DROP CONSTRAINT "session_realtime_modes_end_reason_check";
|
|
11
|
+
ALTER TABLE "session_realtime_modes"
|
|
12
|
+
ADD CONSTRAINT "session_realtime_modes_end_reason_check"
|
|
13
|
+
CHECK (
|
|
14
|
+
"end_reason" IS NULL
|
|
15
|
+
OR "end_reason" IN ('user_stop', 'browser_unload', 'lease_expired', 'authority_revoked')
|
|
16
|
+
) NOT VALID;
|
|
17
|
+
ALTER TABLE "session_realtime_modes"
|
|
18
|
+
VALIDATE CONSTRAINT "session_realtime_modes_end_reason_check";
|
|
19
|
+
|
|
20
|
+
ALTER TABLE "session_attempt_interruptions"
|
|
21
|
+
DROP CONSTRAINT "session_attempt_interruptions_kind_check";
|
|
22
|
+
ALTER TABLE "session_attempt_interruptions"
|
|
23
|
+
ADD CONSTRAINT "session_attempt_interruptions_kind_check"
|
|
24
|
+
CHECK ("kind" IN (
|
|
25
|
+
'session_pause', 'workspace_pause', 'steer', 'maintenance',
|
|
26
|
+
'authority_change', 'organization_membership_revoked'
|
|
27
|
+
)) NOT VALID;
|
|
28
|
+
ALTER TABLE "session_attempt_interruptions"
|
|
29
|
+
VALIDATE CONSTRAINT "session_attempt_interruptions_kind_check";
|
|
30
|
+
|
|
31
|
+
ALTER TABLE "organization_memberships"
|
|
32
|
+
ADD COLUMN "role" text NOT NULL DEFAULT 'member';
|
|
33
|
+
|
|
34
|
+
UPDATE "organization_memberships" membership
|
|
35
|
+
SET role = 'owner'
|
|
36
|
+
FROM "managed_accounts" account
|
|
37
|
+
WHERE account.id = membership.account_id
|
|
38
|
+
AND account.external_source = 'better-auth:user'
|
|
39
|
+
AND membership.subject_id = 'user:' || account.external_id;
|
|
40
|
+
|
|
41
|
+
ALTER TABLE "organization_memberships"
|
|
42
|
+
ADD CONSTRAINT "organization_memberships_role_check"
|
|
43
|
+
CHECK ("role" IN ('owner', 'admin', 'member')) NOT VALID;
|
|
44
|
+
ALTER TABLE "organization_memberships"
|
|
45
|
+
VALIDATE CONSTRAINT "organization_memberships_role_check";
|
|
46
|
+
|
|
47
|
+
ALTER TABLE "organization_user_retention_policies"
|
|
48
|
+
DROP CONSTRAINT "organization_user_retention_policies_duration_check";
|
|
49
|
+
ALTER TABLE "organization_user_retention_policies"
|
|
50
|
+
ADD CONSTRAINT "organization_user_retention_policies_duration_check" CHECK (
|
|
51
|
+
("mode" = 'retain' AND "retention_days" IS NULL)
|
|
52
|
+
OR (
|
|
53
|
+
"mode" = 'delete_after'
|
|
54
|
+
AND "retention_days" IS NOT NULL
|
|
55
|
+
AND "retention_days" BETWEEN 30 AND 90
|
|
56
|
+
)
|
|
57
|
+
) NOT VALID;
|
|
58
|
+
ALTER TABLE "organization_user_retention_policies"
|
|
59
|
+
VALIDATE CONSTRAINT "organization_user_retention_policies_duration_check";
|
|
60
|
+
|
|
61
|
+
-- The pre-existing managed-human provisioner creates the one organization
|
|
62
|
+
-- whose external identity is the same human but predates roles. Preserve that
|
|
63
|
+
-- seamless bootstrap without teaching the general invitation path to infer
|
|
64
|
+
-- authority from provenance: only this exact self-organization INSERT is
|
|
65
|
+
-- promoted, and later role changes remain explicit lifecycle commands.
|
|
66
|
+
CREATE OR REPLACE FUNCTION opengeni_private.assign_managed_self_organization_owner()
|
|
67
|
+
RETURNS trigger
|
|
68
|
+
LANGUAGE plpgsql
|
|
69
|
+
SECURITY DEFINER
|
|
70
|
+
SET search_path FROM CURRENT
|
|
71
|
+
AS $body$
|
|
72
|
+
BEGIN
|
|
73
|
+
IF EXISTS (
|
|
74
|
+
SELECT 1 FROM managed_accounts account
|
|
75
|
+
WHERE account.id = NEW.account_id
|
|
76
|
+
AND account.external_source = 'better-auth:user'
|
|
77
|
+
AND NEW.subject_id = 'user:' || account.external_id
|
|
78
|
+
) THEN
|
|
79
|
+
NEW.role := 'owner';
|
|
80
|
+
END IF;
|
|
81
|
+
RETURN NEW;
|
|
82
|
+
END
|
|
83
|
+
$body$;
|
|
84
|
+
CREATE TRIGGER organization_memberships_assign_managed_self_owner
|
|
85
|
+
BEFORE INSERT ON organization_memberships
|
|
86
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.assign_managed_self_organization_owner();
|
|
87
|
+
|
|
88
|
+
CREATE TABLE "organization_membership_invitations" (
|
|
89
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
90
|
+
"account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
|
|
91
|
+
"target_subject_id" text NOT NULL,
|
|
92
|
+
"target_email" text NOT NULL,
|
|
93
|
+
"role" text NOT NULL DEFAULT 'member',
|
|
94
|
+
"status" text NOT NULL DEFAULT 'pending',
|
|
95
|
+
"revision" bigint NOT NULL DEFAULT 1,
|
|
96
|
+
"created_by_membership_id" uuid NOT NULL,
|
|
97
|
+
"accepted_membership_id" uuid,
|
|
98
|
+
"expires_at" timestamptz NOT NULL,
|
|
99
|
+
"created_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
100
|
+
"updated_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
101
|
+
CONSTRAINT "organization_membership_invitations_creator_fk"
|
|
102
|
+
FOREIGN KEY ("created_by_membership_id", "account_id")
|
|
103
|
+
REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
|
|
104
|
+
CONSTRAINT "organization_membership_invitations_accepted_membership_fk"
|
|
105
|
+
FOREIGN KEY ("accepted_membership_id", "account_id")
|
|
106
|
+
REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
|
|
107
|
+
CONSTRAINT "organization_membership_invitations_subject_check" CHECK (
|
|
108
|
+
"target_subject_id" = btrim("target_subject_id")
|
|
109
|
+
AND "target_subject_id" LIKE 'user:%'
|
|
110
|
+
AND octet_length(convert_to("target_subject_id", 'UTF8')) BETWEEN 6 AND 1024
|
|
111
|
+
),
|
|
112
|
+
CONSTRAINT "organization_membership_invitations_email_check" CHECK (
|
|
113
|
+
"target_email" = lower(btrim("target_email"))
|
|
114
|
+
AND octet_length(convert_to("target_email", 'UTF8')) BETWEEN 3 AND 320
|
|
115
|
+
),
|
|
116
|
+
CONSTRAINT "organization_membership_invitations_role_check"
|
|
117
|
+
CHECK ("role" IN ('owner', 'admin', 'member')),
|
|
118
|
+
CONSTRAINT "organization_membership_invitations_status_check"
|
|
119
|
+
CHECK ("status" IN ('pending', 'accepted', 'revoked', 'expired')),
|
|
120
|
+
CONSTRAINT "organization_membership_invitations_revision_check" CHECK ("revision" > 0),
|
|
121
|
+
CONSTRAINT "organization_membership_invitations_acceptance_check" CHECK (
|
|
122
|
+
("status" = 'accepted' AND "accepted_membership_id" IS NOT NULL)
|
|
123
|
+
OR ("status" <> 'accepted' AND "accepted_membership_id" IS NULL)
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
CREATE UNIQUE INDEX "organization_membership_invitations_id_account_idx"
|
|
127
|
+
ON "organization_membership_invitations" ("id", "account_id");
|
|
128
|
+
CREATE UNIQUE INDEX "organization_membership_invitations_pending_target_uq"
|
|
129
|
+
ON "organization_membership_invitations" ("account_id", "target_subject_id")
|
|
130
|
+
WHERE "status" = 'pending';
|
|
131
|
+
CREATE INDEX "organization_membership_invitations_account_created_idx"
|
|
132
|
+
ON "organization_membership_invitations" ("account_id", "created_at" DESC, "id" DESC);
|
|
133
|
+
CREATE INDEX "organization_membership_invitations_subject_created_idx"
|
|
134
|
+
ON "organization_membership_invitations" ("target_subject_id", "created_at" DESC, "id" DESC);
|
|
135
|
+
|
|
136
|
+
CREATE TABLE "organization_membership_operation_receipts" (
|
|
137
|
+
"account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
|
|
138
|
+
"operation_id" uuid NOT NULL,
|
|
139
|
+
"action" text NOT NULL,
|
|
140
|
+
"input_hash" text NOT NULL,
|
|
141
|
+
"result" jsonb NOT NULL,
|
|
142
|
+
"created_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
143
|
+
CONSTRAINT "organization_membership_operation_receipts_pk"
|
|
144
|
+
PRIMARY KEY ("account_id", "operation_id"),
|
|
145
|
+
CONSTRAINT "organization_membership_operation_receipts_action_check" CHECK (
|
|
146
|
+
"action" IN ('invite', 'accept', 'revoke_invitation', 'change_role', 'suspend', 'reactivate', 'offboard', 'retention')
|
|
147
|
+
),
|
|
148
|
+
CONSTRAINT "organization_membership_operation_receipts_input_hash_check"
|
|
149
|
+
CHECK ("input_hash" ~ '^[0-9a-f]{64}$')
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
CREATE TABLE "organization_membership_lifecycle_events" (
|
|
153
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
154
|
+
"account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
|
|
155
|
+
"operation_id" uuid NOT NULL,
|
|
156
|
+
"actor_membership_id" uuid NOT NULL,
|
|
157
|
+
"target_membership_id" uuid,
|
|
158
|
+
"kind" text NOT NULL,
|
|
159
|
+
"prior_authorization_revision" bigint,
|
|
160
|
+
"resulting_authorization_revision" bigint,
|
|
161
|
+
"reason" text,
|
|
162
|
+
"created_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
163
|
+
CONSTRAINT "organization_membership_lifecycle_events_actor_fk"
|
|
164
|
+
FOREIGN KEY ("actor_membership_id", "account_id")
|
|
165
|
+
REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
|
|
166
|
+
CONSTRAINT "organization_membership_lifecycle_events_target_fk"
|
|
167
|
+
FOREIGN KEY ("target_membership_id", "account_id")
|
|
168
|
+
REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
|
|
169
|
+
CONSTRAINT "organization_membership_lifecycle_events_kind_check" CHECK (
|
|
170
|
+
"kind" IN ('invite', 'accept', 'revoke_invitation', 'change_role', 'suspend', 'reactivate', 'offboard', 'retention')
|
|
171
|
+
),
|
|
172
|
+
CONSTRAINT "organization_membership_lifecycle_events_reason_check" CHECK (
|
|
173
|
+
"reason" IS NULL OR octet_length(convert_to("reason", 'UTF8')) <= 512
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
CREATE UNIQUE INDEX "organization_membership_lifecycle_events_operation_uq"
|
|
177
|
+
ON "organization_membership_lifecycle_events" ("account_id", "operation_id");
|
|
178
|
+
|
|
179
|
+
CREATE TABLE "organization_user_retention_deletions" (
|
|
180
|
+
"account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
|
|
181
|
+
"membership_id" uuid NOT NULL,
|
|
182
|
+
"retention_until" timestamptz NOT NULL,
|
|
183
|
+
"state" text NOT NULL DEFAULT 'claimed',
|
|
184
|
+
"claim_operation_id" uuid NOT NULL,
|
|
185
|
+
"claim_expires_at" timestamptz NOT NULL,
|
|
186
|
+
"attempt_count" integer NOT NULL DEFAULT 1,
|
|
187
|
+
"database_result" jsonb,
|
|
188
|
+
"database_finalized_at" timestamptz,
|
|
189
|
+
"result" jsonb,
|
|
190
|
+
"completed_at" timestamptz,
|
|
191
|
+
"updated_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
192
|
+
CONSTRAINT "organization_user_retention_deletions_pk"
|
|
193
|
+
PRIMARY KEY ("account_id", "membership_id"),
|
|
194
|
+
CONSTRAINT "organization_user_retention_deletions_membership_fk"
|
|
195
|
+
FOREIGN KEY ("membership_id", "account_id")
|
|
196
|
+
REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
|
|
197
|
+
CONSTRAINT "organization_user_retention_deletions_state_check"
|
|
198
|
+
CHECK ("state" IN ('claimed', 'failed', 'completed')),
|
|
199
|
+
CONSTRAINT "organization_user_retention_deletions_attempt_check"
|
|
200
|
+
CHECK ("attempt_count" > 0),
|
|
201
|
+
CONSTRAINT "organization_user_retention_deletions_completion_check" CHECK (
|
|
202
|
+
("state" = 'completed' AND "completed_at" IS NOT NULL AND "result" IS NOT NULL)
|
|
203
|
+
OR ("state" <> 'completed' AND "completed_at" IS NULL AND "result" IS NULL)
|
|
204
|
+
),
|
|
205
|
+
CONSTRAINT "organization_retention_deletions_db_finalized_chk" CHECK (
|
|
206
|
+
("database_finalized_at" IS NULL AND "database_result" IS NULL)
|
|
207
|
+
OR ("database_finalized_at" IS NOT NULL AND "database_result" IS NOT NULL)
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
CREATE UNIQUE INDEX "organization_user_retention_deletions_operation_uq"
|
|
211
|
+
ON "organization_user_retention_deletions" ("claim_operation_id");
|
|
212
|
+
CREATE INDEX "organization_user_retention_deletions_claim_idx"
|
|
213
|
+
ON "organization_user_retention_deletions" ("account_id", "state", "claim_expires_at");
|
|
214
|
+
CREATE INDEX "organization_memberships_retention_due_idx"
|
|
215
|
+
ON "organization_memberships" ("account_id", "personal_retention_until", "id")
|
|
216
|
+
WHERE "status" = 'revoked' AND "personal_retention_until" IS NOT NULL;
|
|
217
|
+
|
|
218
|
+
CREATE TABLE "organization_user_retention_object_obligations" (
|
|
219
|
+
"account_id" uuid NOT NULL,
|
|
220
|
+
"membership_id" uuid NOT NULL,
|
|
221
|
+
"object_kind" text NOT NULL,
|
|
222
|
+
"source_id" text NOT NULL,
|
|
223
|
+
"object_bucket" text NOT NULL,
|
|
224
|
+
"object_key" text NOT NULL,
|
|
225
|
+
"prepared_operation_id" uuid NOT NULL,
|
|
226
|
+
"object_key_hash" text NOT NULL,
|
|
227
|
+
"prepared_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
228
|
+
CONSTRAINT "organization_user_retention_object_obligations_pk"
|
|
229
|
+
PRIMARY KEY ("account_id", "membership_id", "object_kind", "source_id", "object_bucket"),
|
|
230
|
+
CONSTRAINT "organization_retention_object_obligations_deletion_fk"
|
|
231
|
+
FOREIGN KEY ("account_id", "membership_id")
|
|
232
|
+
REFERENCES "organization_user_retention_deletions"("account_id", "membership_id")
|
|
233
|
+
ON DELETE RESTRICT,
|
|
234
|
+
CONSTRAINT "organization_user_retention_object_obligations_hash_check"
|
|
235
|
+
CHECK ("object_key_hash" ~ '^[0-9a-f]{64}$'),
|
|
236
|
+
CONSTRAINT "organization_user_retention_object_obligations_shape_check" CHECK (
|
|
237
|
+
"object_kind" IN (
|
|
238
|
+
'file', 'session_recording', 'browser_state_artifact', 'browser_state_upload',
|
|
239
|
+
'transcription_recording_object', 'video_staging_reference',
|
|
240
|
+
'workspace_artifact_version', 'editable_artifact_blob',
|
|
241
|
+
'workspace_capture_manifest', 'workspace_capture_tree_index',
|
|
242
|
+
'workspace_capture_blob'
|
|
243
|
+
)
|
|
244
|
+
AND octet_length("source_id") BETWEEN 1 AND 2048
|
|
245
|
+
AND octet_length(convert_to("object_bucket", 'UTF8')) BETWEEN 1 AND 1024
|
|
246
|
+
AND octet_length(convert_to("object_key", 'UTF8')) BETWEEN 1 AND 4096
|
|
247
|
+
)
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
CREATE TABLE "organization_user_retention_object_deletion_receipts" (
|
|
251
|
+
"account_id" uuid NOT NULL,
|
|
252
|
+
"membership_id" uuid NOT NULL,
|
|
253
|
+
"object_kind" text NOT NULL,
|
|
254
|
+
"source_id" text NOT NULL,
|
|
255
|
+
"object_bucket" text NOT NULL,
|
|
256
|
+
"operation_id" uuid NOT NULL,
|
|
257
|
+
"deleted_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
258
|
+
CONSTRAINT "organization_user_retention_object_deletion_receipts_pk"
|
|
259
|
+
PRIMARY KEY ("account_id", "membership_id", "object_kind", "source_id", "object_bucket"),
|
|
260
|
+
CONSTRAINT "organization_retention_object_deletions_obligation_fk"
|
|
261
|
+
FOREIGN KEY ("account_id", "membership_id", "object_kind", "source_id", "object_bucket")
|
|
262
|
+
REFERENCES "organization_user_retention_object_obligations"(
|
|
263
|
+
"account_id", "membership_id", "object_kind", "source_id", "object_bucket"
|
|
264
|
+
) ON DELETE RESTRICT
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
CREATE TABLE "organization_user_retention_deletion_events" (
|
|
268
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
269
|
+
"account_id" uuid NOT NULL,
|
|
270
|
+
"membership_id" uuid NOT NULL,
|
|
271
|
+
"operation_id" uuid NOT NULL,
|
|
272
|
+
"kind" text NOT NULL,
|
|
273
|
+
"reason_code" text,
|
|
274
|
+
"result" jsonb NOT NULL,
|
|
275
|
+
"created_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
276
|
+
CONSTRAINT "organization_user_retention_deletion_events_membership_fk"
|
|
277
|
+
FOREIGN KEY ("membership_id", "account_id")
|
|
278
|
+
REFERENCES "organization_memberships"("id", "account_id") ON DELETE RESTRICT,
|
|
279
|
+
CONSTRAINT "organization_user_retention_deletion_events_kind_check"
|
|
280
|
+
CHECK ("kind" IN ('claimed', 'failed', 'completed')),
|
|
281
|
+
CONSTRAINT "organization_user_retention_deletion_events_reason_check"
|
|
282
|
+
CHECK (
|
|
283
|
+
("kind" = 'failed' AND "reason_code" IS NOT NULL
|
|
284
|
+
AND "reason_code" ~ '^[a-z0-9_]{1,64}$')
|
|
285
|
+
OR ("kind" IN ('claimed', 'completed') AND "reason_code" IS NULL)
|
|
286
|
+
)
|
|
287
|
+
);
|
|
288
|
+
CREATE UNIQUE INDEX "organization_user_retention_deletion_events_operation_kind_uq"
|
|
289
|
+
ON "organization_user_retention_deletion_events" ("account_id", "operation_id", "kind");
|
|
290
|
+
CREATE INDEX "organization_retention_events_member_created_idx"
|
|
291
|
+
ON "organization_user_retention_deletion_events" ("account_id", "membership_id", "created_at", "id");
|
|
292
|
+
|
|
293
|
+
ALTER TABLE "organization_membership_invitations" ENABLE ROW LEVEL SECURITY;
|
|
294
|
+
ALTER TABLE "organization_membership_invitations" FORCE ROW LEVEL SECURITY;
|
|
295
|
+
ALTER TABLE "organization_membership_operation_receipts" ENABLE ROW LEVEL SECURITY;
|
|
296
|
+
ALTER TABLE "organization_membership_operation_receipts" FORCE ROW LEVEL SECURITY;
|
|
297
|
+
ALTER TABLE "organization_membership_lifecycle_events" ENABLE ROW LEVEL SECURITY;
|
|
298
|
+
ALTER TABLE "organization_membership_lifecycle_events" FORCE ROW LEVEL SECURITY;
|
|
299
|
+
ALTER TABLE "organization_user_retention_deletions" ENABLE ROW LEVEL SECURITY;
|
|
300
|
+
ALTER TABLE "organization_user_retention_deletions" FORCE ROW LEVEL SECURITY;
|
|
301
|
+
ALTER TABLE "organization_user_retention_object_obligations" ENABLE ROW LEVEL SECURITY;
|
|
302
|
+
ALTER TABLE "organization_user_retention_object_obligations" FORCE ROW LEVEL SECURITY;
|
|
303
|
+
ALTER TABLE "organization_user_retention_object_deletion_receipts" ENABLE ROW LEVEL SECURITY;
|
|
304
|
+
ALTER TABLE "organization_user_retention_object_deletion_receipts" FORCE ROW LEVEL SECURITY;
|
|
305
|
+
ALTER TABLE "organization_user_retention_deletion_events" ENABLE ROW LEVEL SECURITY;
|
|
306
|
+
ALTER TABLE "organization_user_retention_deletion_events" FORCE ROW LEVEL SECURITY;
|
|
307
|
+
|
|
308
|
+
DROP POLICY IF EXISTS organization_tenancy_lifecycle ON "organization_memberships";
|
|
309
|
+
CREATE POLICY organization_tenancy_lifecycle ON "organization_memberships"
|
|
310
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
311
|
+
IN (
|
|
312
|
+
'managed_human_provisioning',
|
|
313
|
+
'session_visibility_activation',
|
|
314
|
+
'organization_membership_lifecycle'
|
|
315
|
+
))
|
|
316
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
317
|
+
IN (
|
|
318
|
+
'managed_human_provisioning',
|
|
319
|
+
'session_visibility_activation',
|
|
320
|
+
'organization_membership_lifecycle'
|
|
321
|
+
));
|
|
322
|
+
DROP POLICY IF EXISTS organization_tenancy_lifecycle ON "organization_user_retention_policies";
|
|
323
|
+
CREATE POLICY organization_tenancy_lifecycle ON "organization_user_retention_policies"
|
|
324
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
325
|
+
IN ('managed_human_provisioning', 'organization_membership_lifecycle'))
|
|
326
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
327
|
+
IN ('managed_human_provisioning', 'organization_membership_lifecycle'));
|
|
328
|
+
DROP POLICY IF EXISTS organization_tenancy_lifecycle ON "organization_user_resource_authorities";
|
|
329
|
+
CREATE POLICY organization_tenancy_lifecycle ON "organization_user_resource_authorities"
|
|
330
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
331
|
+
IN ('managed_human_provisioning', 'organization_membership_lifecycle'))
|
|
332
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
333
|
+
IN ('managed_human_provisioning', 'organization_membership_lifecycle'));
|
|
334
|
+
DROP POLICY IF EXISTS organization_tenancy_lifecycle ON "organization_user_resource_grants";
|
|
335
|
+
CREATE POLICY organization_tenancy_lifecycle ON "organization_user_resource_grants"
|
|
336
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
337
|
+
IN (
|
|
338
|
+
'managed_human_provisioning',
|
|
339
|
+
'session_visibility_activation',
|
|
340
|
+
'organization_membership_lifecycle'
|
|
341
|
+
))
|
|
342
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
343
|
+
IN (
|
|
344
|
+
'managed_human_provisioning',
|
|
345
|
+
'session_visibility_activation',
|
|
346
|
+
'organization_membership_lifecycle'
|
|
347
|
+
));
|
|
348
|
+
|
|
349
|
+
CREATE POLICY organization_tenancy_lifecycle ON "organization_membership_invitations"
|
|
350
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
351
|
+
= 'organization_membership_lifecycle')
|
|
352
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
353
|
+
= 'organization_membership_lifecycle');
|
|
354
|
+
CREATE POLICY organization_tenancy_lifecycle ON "organization_membership_operation_receipts"
|
|
355
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
356
|
+
= 'organization_membership_lifecycle')
|
|
357
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
358
|
+
= 'organization_membership_lifecycle');
|
|
359
|
+
CREATE POLICY organization_tenancy_lifecycle ON "organization_membership_lifecycle_events"
|
|
360
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
361
|
+
= 'organization_membership_lifecycle')
|
|
362
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
363
|
+
= 'organization_membership_lifecycle');
|
|
364
|
+
CREATE POLICY organization_tenancy_lifecycle ON "organization_user_retention_deletions"
|
|
365
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
366
|
+
= 'organization_membership_lifecycle')
|
|
367
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
368
|
+
= 'organization_membership_lifecycle');
|
|
369
|
+
CREATE POLICY organization_tenancy_lifecycle ON "organization_user_retention_object_obligations"
|
|
370
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
371
|
+
= 'organization_membership_lifecycle')
|
|
372
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
373
|
+
= 'organization_membership_lifecycle');
|
|
374
|
+
CREATE POLICY organization_tenancy_lifecycle ON "organization_user_retention_object_deletion_receipts"
|
|
375
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
376
|
+
= 'organization_membership_lifecycle')
|
|
377
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
378
|
+
= 'organization_membership_lifecycle');
|
|
379
|
+
CREATE POLICY organization_tenancy_lifecycle ON "organization_user_retention_deletion_events"
|
|
380
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
381
|
+
= 'organization_membership_lifecycle')
|
|
382
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
383
|
+
= 'organization_membership_lifecycle');
|
|
384
|
+
|
|
385
|
+
CREATE OR REPLACE FUNCTION opengeni_private.organization_membership_row_json(
|
|
386
|
+
p_membership organization_memberships
|
|
387
|
+
) RETURNS jsonb
|
|
388
|
+
LANGUAGE sql IMMUTABLE
|
|
389
|
+
SET search_path = pg_catalog
|
|
390
|
+
AS $body$
|
|
391
|
+
SELECT pg_catalog.jsonb_build_object(
|
|
392
|
+
'id', p_membership.id,
|
|
393
|
+
'organizationId', p_membership.account_id,
|
|
394
|
+
'subjectId', p_membership.subject_id,
|
|
395
|
+
'role', p_membership.role,
|
|
396
|
+
'status', p_membership.status,
|
|
397
|
+
'authorizationRevision', p_membership.authorization_revision,
|
|
398
|
+
'personalWorkspaceId', p_membership.personal_workspace_id,
|
|
399
|
+
'revokedAt', p_membership.revoked_at,
|
|
400
|
+
'personalRetentionUntil', p_membership.personal_retention_until,
|
|
401
|
+
'createdAt', p_membership.created_at,
|
|
402
|
+
'updatedAt', p_membership.updated_at
|
|
403
|
+
)
|
|
404
|
+
$body$;
|
|
405
|
+
|
|
406
|
+
CREATE OR REPLACE FUNCTION opengeni_private.organization_invitation_row_json(
|
|
407
|
+
p_invitation organization_membership_invitations
|
|
408
|
+
) RETURNS jsonb
|
|
409
|
+
LANGUAGE sql STABLE
|
|
410
|
+
SET search_path = pg_catalog
|
|
411
|
+
AS $body$
|
|
412
|
+
SELECT pg_catalog.jsonb_build_object(
|
|
413
|
+
'id', p_invitation.id,
|
|
414
|
+
'organizationId', p_invitation.account_id,
|
|
415
|
+
'targetEmail', p_invitation.target_email,
|
|
416
|
+
'role', p_invitation.role,
|
|
417
|
+
'status', CASE
|
|
418
|
+
WHEN p_invitation.status = 'pending'
|
|
419
|
+
AND p_invitation.expires_at <= pg_catalog.clock_timestamp()
|
|
420
|
+
THEN 'expired'
|
|
421
|
+
ELSE p_invitation.status
|
|
422
|
+
END,
|
|
423
|
+
'revision', p_invitation.revision,
|
|
424
|
+
'expiresAt', p_invitation.expires_at,
|
|
425
|
+
'acceptedMembershipId', p_invitation.accepted_membership_id,
|
|
426
|
+
'createdAt', p_invitation.created_at,
|
|
427
|
+
'updatedAt', p_invitation.updated_at
|
|
428
|
+
)
|
|
429
|
+
$body$;
|
|
430
|
+
|
|
431
|
+
CREATE OR REPLACE FUNCTION list_self_organization_memberships(p_subject_id text)
|
|
432
|
+
RETURNS jsonb
|
|
433
|
+
LANGUAGE plpgsql
|
|
434
|
+
SECURITY DEFINER
|
|
435
|
+
SET search_path FROM CURRENT
|
|
436
|
+
AS $body$
|
|
437
|
+
DECLARE result jsonb;
|
|
438
|
+
BEGIN
|
|
439
|
+
IF p_subject_id IS NULL
|
|
440
|
+
OR p_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
441
|
+
OR p_subject_id NOT LIKE 'user:%'
|
|
442
|
+
THEN
|
|
443
|
+
RAISE EXCEPTION 'managed human subject authority required' USING ERRCODE = '42501';
|
|
444
|
+
END IF;
|
|
445
|
+
PERFORM pg_catalog.set_config(
|
|
446
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
447
|
+
'organization_membership_lifecycle', true
|
|
448
|
+
);
|
|
449
|
+
SELECT coalesce(
|
|
450
|
+
pg_catalog.jsonb_agg(
|
|
451
|
+
opengeni_private.organization_membership_row_json(membership)
|
|
452
|
+
ORDER BY membership.created_at, membership.id
|
|
453
|
+
), '[]'::jsonb
|
|
454
|
+
) INTO result
|
|
455
|
+
FROM organization_memberships membership
|
|
456
|
+
WHERE membership.subject_id = p_subject_id;
|
|
457
|
+
RETURN result;
|
|
458
|
+
END
|
|
459
|
+
$body$;
|
|
460
|
+
|
|
461
|
+
CREATE OR REPLACE FUNCTION list_self_organization_invitations(
|
|
462
|
+
p_subject_id text,
|
|
463
|
+
p_cursor_id uuid,
|
|
464
|
+
p_limit integer
|
|
465
|
+
)
|
|
466
|
+
RETURNS jsonb
|
|
467
|
+
LANGUAGE plpgsql
|
|
468
|
+
SECURITY DEFINER
|
|
469
|
+
SET search_path FROM CURRENT
|
|
470
|
+
AS $body$
|
|
471
|
+
DECLARE
|
|
472
|
+
cursor_created_at timestamptz;
|
|
473
|
+
result jsonb;
|
|
474
|
+
BEGIN
|
|
475
|
+
IF p_subject_id IS NULL
|
|
476
|
+
OR p_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
477
|
+
OR p_subject_id NOT LIKE 'user:%'
|
|
478
|
+
OR p_limit IS NULL OR p_limit < 1 OR p_limit > 100
|
|
479
|
+
THEN
|
|
480
|
+
RAISE EXCEPTION 'managed human subject authority required' USING ERRCODE = '42501';
|
|
481
|
+
END IF;
|
|
482
|
+
PERFORM pg_catalog.set_config(
|
|
483
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
484
|
+
'organization_membership_lifecycle', true
|
|
485
|
+
);
|
|
486
|
+
IF p_cursor_id IS NOT NULL THEN
|
|
487
|
+
SELECT invitation.created_at INTO cursor_created_at
|
|
488
|
+
FROM organization_membership_invitations invitation
|
|
489
|
+
WHERE invitation.target_subject_id = p_subject_id
|
|
490
|
+
AND invitation.id = p_cursor_id;
|
|
491
|
+
IF NOT FOUND THEN
|
|
492
|
+
RAISE EXCEPTION 'organization invitation cursor not found' USING ERRCODE = 'P0002';
|
|
493
|
+
END IF;
|
|
494
|
+
END IF;
|
|
495
|
+
SELECT coalesce(
|
|
496
|
+
pg_catalog.jsonb_agg(
|
|
497
|
+
opengeni_private.organization_invitation_row_json(candidate)
|
|
498
|
+
ORDER BY candidate.created_at DESC, candidate.id DESC
|
|
499
|
+
), '[]'::jsonb
|
|
500
|
+
) INTO result
|
|
501
|
+
FROM (
|
|
502
|
+
SELECT invitation.*
|
|
503
|
+
FROM organization_membership_invitations invitation
|
|
504
|
+
WHERE invitation.target_subject_id = p_subject_id
|
|
505
|
+
AND (
|
|
506
|
+
p_cursor_id IS NULL
|
|
507
|
+
OR (invitation.created_at, invitation.id) < (cursor_created_at, p_cursor_id)
|
|
508
|
+
)
|
|
509
|
+
ORDER BY invitation.created_at DESC, invitation.id DESC
|
|
510
|
+
LIMIT p_limit + 1
|
|
511
|
+
) candidate;
|
|
512
|
+
RETURN result;
|
|
513
|
+
END
|
|
514
|
+
$body$;
|
|
515
|
+
|
|
516
|
+
-- Rolling compatibility for callers that predate keyset pagination. The
|
|
517
|
+
-- legacy signature is deliberately capped rather than returning unbounded
|
|
518
|
+
-- invitation history.
|
|
519
|
+
CREATE OR REPLACE FUNCTION list_self_organization_invitations(p_subject_id text)
|
|
520
|
+
RETURNS jsonb
|
|
521
|
+
LANGUAGE sql
|
|
522
|
+
SECURITY DEFINER
|
|
523
|
+
SET search_path FROM CURRENT
|
|
524
|
+
AS $body$
|
|
525
|
+
SELECT list_self_organization_invitations(p_subject_id, NULL::uuid, 100)
|
|
526
|
+
$body$;
|
|
527
|
+
|
|
528
|
+
CREATE OR REPLACE FUNCTION get_self_organization_invitation(
|
|
529
|
+
p_subject_id text,
|
|
530
|
+
p_invitation_id uuid
|
|
531
|
+
)
|
|
532
|
+
RETURNS jsonb
|
|
533
|
+
LANGUAGE plpgsql
|
|
534
|
+
SECURITY DEFINER
|
|
535
|
+
SET search_path FROM CURRENT
|
|
536
|
+
AS $body$
|
|
537
|
+
DECLARE invitation organization_membership_invitations%ROWTYPE;
|
|
538
|
+
BEGIN
|
|
539
|
+
IF p_subject_id IS NULL
|
|
540
|
+
OR p_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
541
|
+
OR p_subject_id NOT LIKE 'user:%'
|
|
542
|
+
OR p_invitation_id IS NULL
|
|
543
|
+
THEN
|
|
544
|
+
RAISE EXCEPTION 'managed human subject authority required' USING ERRCODE = '42501';
|
|
545
|
+
END IF;
|
|
546
|
+
PERFORM pg_catalog.set_config(
|
|
547
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
548
|
+
'organization_membership_lifecycle', true
|
|
549
|
+
);
|
|
550
|
+
SELECT candidate.* INTO invitation
|
|
551
|
+
FROM organization_membership_invitations candidate
|
|
552
|
+
WHERE candidate.id = p_invitation_id
|
|
553
|
+
AND candidate.target_subject_id = p_subject_id;
|
|
554
|
+
IF NOT FOUND THEN
|
|
555
|
+
RAISE EXCEPTION 'organization invitation not found' USING ERRCODE = 'P0002';
|
|
556
|
+
END IF;
|
|
557
|
+
RETURN opengeni_private.organization_invitation_row_json(invitation);
|
|
558
|
+
END
|
|
559
|
+
$body$;
|
|
560
|
+
|
|
561
|
+
CREATE OR REPLACE FUNCTION list_organization_members(
|
|
562
|
+
p_account_id uuid,
|
|
563
|
+
p_actor_subject_id text
|
|
564
|
+
) RETURNS jsonb
|
|
565
|
+
LANGUAGE plpgsql
|
|
566
|
+
SECURITY DEFINER
|
|
567
|
+
SET search_path FROM CURRENT
|
|
568
|
+
AS $body$
|
|
569
|
+
DECLARE actor organization_memberships%ROWTYPE; result jsonb;
|
|
570
|
+
BEGIN
|
|
571
|
+
IF p_account_id IS NULL
|
|
572
|
+
OR p_actor_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
573
|
+
OR p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
|
|
574
|
+
THEN
|
|
575
|
+
RAISE EXCEPTION 'organization member authority required' USING ERRCODE = '42501';
|
|
576
|
+
END IF;
|
|
577
|
+
PERFORM pg_catalog.set_config(
|
|
578
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
579
|
+
'organization_membership_lifecycle', true
|
|
580
|
+
);
|
|
581
|
+
SELECT * INTO actor FROM organization_memberships membership
|
|
582
|
+
WHERE membership.account_id = p_account_id
|
|
583
|
+
AND membership.subject_id = p_actor_subject_id;
|
|
584
|
+
IF NOT FOUND OR actor.status <> 'active' OR actor.role NOT IN ('owner', 'admin') THEN
|
|
585
|
+
RAISE EXCEPTION 'organization administration required' USING ERRCODE = '42501';
|
|
586
|
+
END IF;
|
|
587
|
+
SELECT coalesce(
|
|
588
|
+
pg_catalog.jsonb_agg(
|
|
589
|
+
opengeni_private.organization_membership_row_json(membership)
|
|
590
|
+
ORDER BY membership.created_at, membership.id
|
|
591
|
+
), '[]'::jsonb
|
|
592
|
+
) INTO result
|
|
593
|
+
FROM organization_memberships membership
|
|
594
|
+
WHERE membership.account_id = p_account_id;
|
|
595
|
+
RETURN result;
|
|
596
|
+
END
|
|
597
|
+
$body$;
|
|
598
|
+
|
|
599
|
+
CREATE OR REPLACE FUNCTION list_organization_invitations(
|
|
600
|
+
p_account_id uuid,
|
|
601
|
+
p_actor_subject_id text,
|
|
602
|
+
p_cursor_id uuid,
|
|
603
|
+
p_limit integer
|
|
604
|
+
) RETURNS jsonb
|
|
605
|
+
LANGUAGE plpgsql
|
|
606
|
+
SECURITY DEFINER
|
|
607
|
+
SET search_path FROM CURRENT
|
|
608
|
+
AS $body$
|
|
609
|
+
DECLARE
|
|
610
|
+
actor organization_memberships%ROWTYPE;
|
|
611
|
+
cursor_created_at timestamptz;
|
|
612
|
+
result jsonb;
|
|
613
|
+
BEGIN
|
|
614
|
+
IF p_account_id IS NULL
|
|
615
|
+
OR p_actor_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
616
|
+
OR p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
|
|
617
|
+
OR p_limit IS NULL OR p_limit < 1 OR p_limit > 100
|
|
618
|
+
THEN
|
|
619
|
+
RAISE EXCEPTION 'organization invitation page authority is invalid'
|
|
620
|
+
USING ERRCODE = '42501';
|
|
621
|
+
END IF;
|
|
622
|
+
PERFORM pg_catalog.set_config(
|
|
623
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
624
|
+
'organization_membership_lifecycle', true
|
|
625
|
+
);
|
|
626
|
+
SELECT * INTO actor FROM organization_memberships membership
|
|
627
|
+
WHERE membership.account_id = p_account_id
|
|
628
|
+
AND membership.subject_id = p_actor_subject_id;
|
|
629
|
+
IF NOT FOUND OR actor.status <> 'active' OR actor.role NOT IN ('owner', 'admin') THEN
|
|
630
|
+
RAISE EXCEPTION 'organization administration required' USING ERRCODE = '42501';
|
|
631
|
+
END IF;
|
|
632
|
+
IF p_cursor_id IS NOT NULL THEN
|
|
633
|
+
SELECT invitation.created_at INTO cursor_created_at
|
|
634
|
+
FROM organization_membership_invitations invitation
|
|
635
|
+
WHERE invitation.account_id = p_account_id AND invitation.id = p_cursor_id;
|
|
636
|
+
IF NOT FOUND THEN
|
|
637
|
+
RAISE EXCEPTION 'organization invitation cursor not found' USING ERRCODE = 'P0002';
|
|
638
|
+
END IF;
|
|
639
|
+
END IF;
|
|
640
|
+
SELECT coalesce(
|
|
641
|
+
pg_catalog.jsonb_agg(
|
|
642
|
+
opengeni_private.organization_invitation_row_json(candidate)
|
|
643
|
+
ORDER BY candidate.created_at DESC, candidate.id DESC
|
|
644
|
+
), '[]'::jsonb
|
|
645
|
+
) INTO result
|
|
646
|
+
FROM (
|
|
647
|
+
SELECT invitation.*
|
|
648
|
+
FROM organization_membership_invitations invitation
|
|
649
|
+
WHERE invitation.account_id = p_account_id
|
|
650
|
+
AND (
|
|
651
|
+
p_cursor_id IS NULL
|
|
652
|
+
OR (invitation.created_at, invitation.id) < (cursor_created_at, p_cursor_id)
|
|
653
|
+
)
|
|
654
|
+
ORDER BY invitation.created_at DESC, invitation.id DESC
|
|
655
|
+
LIMIT p_limit + 1
|
|
656
|
+
) candidate;
|
|
657
|
+
RETURN result;
|
|
658
|
+
END
|
|
659
|
+
$body$;
|
|
660
|
+
|
|
661
|
+
CREATE OR REPLACE FUNCTION prepare_organization_membership_protocol_settlements(
|
|
662
|
+
p_command jsonb
|
|
663
|
+
) RETURNS jsonb
|
|
664
|
+
LANGUAGE plpgsql
|
|
665
|
+
SECURITY DEFINER
|
|
666
|
+
SET search_path FROM CURRENT
|
|
667
|
+
AS $body$
|
|
668
|
+
DECLARE
|
|
669
|
+
action_name text := p_command ->> 'action';
|
|
670
|
+
account_id_value uuid := nullif(p_command ->> 'organizationId', '')::uuid;
|
|
671
|
+
actor_subject text := p_command ->> 'actorSubjectId';
|
|
672
|
+
operation_id_value uuid := nullif(p_command ->> 'operationId', '')::uuid;
|
|
673
|
+
target_id uuid := nullif(p_command ->> 'membershipId', '')::uuid;
|
|
674
|
+
expected_revision bigint := nullif(
|
|
675
|
+
p_command ->> 'expectedAuthorizationRevision', ''
|
|
676
|
+
)::bigint;
|
|
677
|
+
input_hash_value text;
|
|
678
|
+
receipt_row organization_membership_operation_receipts%ROWTYPE;
|
|
679
|
+
actor organization_memberships%ROWTYPE;
|
|
680
|
+
target organization_memberships%ROWTYPE;
|
|
681
|
+
workspace_id_value uuid;
|
|
682
|
+
result jsonb := '[]'::jsonb;
|
|
683
|
+
previous_workspace text := pg_catalog.current_setting('opengeni.workspace_id', true);
|
|
684
|
+
BEGIN
|
|
685
|
+
IF p_command IS NULL
|
|
686
|
+
OR action_name NOT IN ('suspend', 'offboard')
|
|
687
|
+
OR account_id_value IS NULL
|
|
688
|
+
OR operation_id_value IS NULL
|
|
689
|
+
OR target_id IS NULL
|
|
690
|
+
OR expected_revision IS NULL
|
|
691
|
+
OR actor_subject IS NULL
|
|
692
|
+
OR actor_subject IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
693
|
+
OR account_id_value IS DISTINCT FROM opengeni_private.current_account_id()
|
|
694
|
+
THEN
|
|
695
|
+
RAISE EXCEPTION 'organization membership protocol preparation authority is invalid'
|
|
696
|
+
USING ERRCODE = '42501';
|
|
697
|
+
END IF;
|
|
698
|
+
input_hash_value := pg_catalog.encode(
|
|
699
|
+
pg_catalog.sha256(pg_catalog.convert_to(p_command::text, 'UTF8')), 'hex'
|
|
700
|
+
);
|
|
701
|
+
PERFORM 1 FROM managed_accounts account
|
|
702
|
+
WHERE account.id = account_id_value FOR UPDATE;
|
|
703
|
+
IF NOT FOUND THEN
|
|
704
|
+
RAISE EXCEPTION 'organization not found' USING ERRCODE = 'P0002';
|
|
705
|
+
END IF;
|
|
706
|
+
PERFORM pg_catalog.set_config(
|
|
707
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
708
|
+
'organization_membership_lifecycle', true
|
|
709
|
+
);
|
|
710
|
+
SELECT * INTO receipt_row
|
|
711
|
+
FROM organization_membership_operation_receipts receipt
|
|
712
|
+
WHERE receipt.account_id = account_id_value
|
|
713
|
+
AND receipt.operation_id = operation_id_value
|
|
714
|
+
FOR UPDATE;
|
|
715
|
+
IF FOUND THEN
|
|
716
|
+
IF receipt_row.action IS DISTINCT FROM action_name
|
|
717
|
+
OR receipt_row.input_hash IS DISTINCT FROM input_hash_value
|
|
718
|
+
THEN
|
|
719
|
+
RAISE EXCEPTION 'organization operation id was reused with different input'
|
|
720
|
+
USING ERRCODE = '23505';
|
|
721
|
+
END IF;
|
|
722
|
+
RETURN result;
|
|
723
|
+
END IF;
|
|
724
|
+
FOR workspace_id_value IN
|
|
725
|
+
SELECT workspace.id FROM workspaces workspace
|
|
726
|
+
WHERE workspace.account_id = account_id_value ORDER BY workspace.id
|
|
727
|
+
LOOP
|
|
728
|
+
PERFORM pg_catalog.set_config(
|
|
729
|
+
'opengeni.workspace_id', workspace_id_value::text, true
|
|
730
|
+
);
|
|
731
|
+
PERFORM 1 FROM workspace_inference_controls control
|
|
732
|
+
WHERE control.account_id = account_id_value
|
|
733
|
+
AND control.workspace_id = workspace_id_value
|
|
734
|
+
FOR SHARE;
|
|
735
|
+
PERFORM 1 FROM workspaces workspace
|
|
736
|
+
WHERE workspace.account_id = account_id_value
|
|
737
|
+
AND workspace.id = workspace_id_value
|
|
738
|
+
FOR KEY SHARE;
|
|
739
|
+
END LOOP;
|
|
740
|
+
PERFORM 1 FROM organization_memberships membership
|
|
741
|
+
WHERE membership.account_id = account_id_value
|
|
742
|
+
AND membership.id IN (
|
|
743
|
+
target_id,
|
|
744
|
+
(
|
|
745
|
+
SELECT candidate.id FROM organization_memberships candidate
|
|
746
|
+
WHERE candidate.account_id = account_id_value
|
|
747
|
+
AND candidate.subject_id = actor_subject
|
|
748
|
+
)
|
|
749
|
+
)
|
|
750
|
+
ORDER BY membership.id
|
|
751
|
+
FOR UPDATE;
|
|
752
|
+
SELECT * INTO actor FROM organization_memberships membership
|
|
753
|
+
WHERE membership.account_id = account_id_value
|
|
754
|
+
AND membership.subject_id = actor_subject;
|
|
755
|
+
SELECT * INTO target FROM organization_memberships membership
|
|
756
|
+
WHERE membership.account_id = account_id_value AND membership.id = target_id;
|
|
757
|
+
IF actor.id IS NULL OR actor.status <> 'active' OR actor.role NOT IN ('owner', 'admin') THEN
|
|
758
|
+
RAISE EXCEPTION 'organization administration required' USING ERRCODE = '42501';
|
|
759
|
+
END IF;
|
|
760
|
+
IF target.id IS NULL THEN
|
|
761
|
+
RAISE EXCEPTION 'organization member not found' USING ERRCODE = 'P0002';
|
|
762
|
+
END IF;
|
|
763
|
+
IF target.authorization_revision IS DISTINCT FROM expected_revision THEN
|
|
764
|
+
RAISE EXCEPTION 'organization membership revision is stale' USING ERRCODE = '40001';
|
|
765
|
+
END IF;
|
|
766
|
+
IF actor.role = 'admin' AND target.role <> 'member' THEN
|
|
767
|
+
RAISE EXCEPTION 'administrators may manage members only' USING ERRCODE = '42501';
|
|
768
|
+
END IF;
|
|
769
|
+
IF action_name = 'suspend' AND target.status <> 'active' THEN
|
|
770
|
+
RAISE EXCEPTION 'only an active member can be suspended' USING ERRCODE = '55000';
|
|
771
|
+
ELSIF action_name = 'offboard' AND target.status NOT IN ('active', 'suspended') THEN
|
|
772
|
+
RAISE EXCEPTION 'only an active or suspended member can be offboarded'
|
|
773
|
+
USING ERRCODE = '55000';
|
|
774
|
+
END IF;
|
|
775
|
+
IF target.role = 'owner' AND NOT EXISTS (
|
|
776
|
+
SELECT 1 FROM organization_memberships other
|
|
777
|
+
WHERE other.account_id = account_id_value AND other.id <> target.id
|
|
778
|
+
AND other.status = 'active' AND other.role = 'owner'
|
|
779
|
+
) THEN
|
|
780
|
+
RAISE EXCEPTION 'cannot remove the last active organization owner'
|
|
781
|
+
USING ERRCODE = '55000';
|
|
782
|
+
END IF;
|
|
783
|
+
FOR workspace_id_value IN
|
|
784
|
+
SELECT workspace.id FROM workspaces workspace
|
|
785
|
+
WHERE workspace.account_id = account_id_value ORDER BY workspace.id
|
|
786
|
+
LOOP
|
|
787
|
+
PERFORM pg_catalog.set_config(
|
|
788
|
+
'opengeni.workspace_id', workspace_id_value::text, true
|
|
789
|
+
);
|
|
790
|
+
PERFORM 1 FROM sessions affected
|
|
791
|
+
WHERE affected.account_id = account_id_value
|
|
792
|
+
AND affected.workspace_id = workspace_id_value
|
|
793
|
+
AND (
|
|
794
|
+
affected.owner_organization_membership_id = target.id
|
|
795
|
+
OR EXISTS (
|
|
796
|
+
SELECT 1 FROM session_turns initiated
|
|
797
|
+
WHERE initiated.session_id = affected.id
|
|
798
|
+
AND initiated.initiating_human_subject_id = target.subject_id
|
|
799
|
+
)
|
|
800
|
+
)
|
|
801
|
+
ORDER BY affected.id
|
|
802
|
+
FOR NO KEY UPDATE;
|
|
803
|
+
PERFORM 1 FROM session_turns turn_row
|
|
804
|
+
WHERE turn_row.account_id = account_id_value
|
|
805
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
806
|
+
AND EXISTS (
|
|
807
|
+
SELECT 1 FROM sessions owned
|
|
808
|
+
WHERE owned.id = turn_row.session_id
|
|
809
|
+
AND (
|
|
810
|
+
owned.owner_organization_membership_id = target.id
|
|
811
|
+
OR turn_row.initiating_human_subject_id = target.subject_id
|
|
812
|
+
)
|
|
813
|
+
)
|
|
814
|
+
ORDER BY turn_row.id
|
|
815
|
+
FOR UPDATE;
|
|
816
|
+
PERFORM 1 FROM session_turn_attempts attempt
|
|
817
|
+
WHERE attempt.account_id = account_id_value
|
|
818
|
+
AND attempt.workspace_id = workspace_id_value
|
|
819
|
+
AND EXISTS (
|
|
820
|
+
SELECT 1 FROM sessions owned
|
|
821
|
+
WHERE owned.id = attempt.session_id
|
|
822
|
+
AND (
|
|
823
|
+
owned.owner_organization_membership_id = target.id
|
|
824
|
+
OR EXISTS (
|
|
825
|
+
SELECT 1 FROM session_turns initiated
|
|
826
|
+
WHERE initiated.id = attempt.turn_id
|
|
827
|
+
AND initiated.initiating_human_subject_id = target.subject_id
|
|
828
|
+
)
|
|
829
|
+
)
|
|
830
|
+
)
|
|
831
|
+
ORDER BY attempt.id
|
|
832
|
+
FOR UPDATE;
|
|
833
|
+
SELECT result || COALESCE(
|
|
834
|
+
pg_catalog.jsonb_agg(
|
|
835
|
+
pg_catalog.jsonb_build_object(
|
|
836
|
+
'accountId', turn_row.account_id,
|
|
837
|
+
'workspaceId', turn_row.workspace_id,
|
|
838
|
+
'sessionId', turn_row.session_id,
|
|
839
|
+
'turnId', turn_row.id,
|
|
840
|
+
'executionGeneration', turn_row.execution_generation,
|
|
841
|
+
'turnAssociation', CASE WHEN owned.active_turn_id = turn_row.id
|
|
842
|
+
THEN 'current' ELSE NULL END
|
|
843
|
+
) ORDER BY turn_row.session_id, turn_row.id
|
|
844
|
+
), '[]'::jsonb
|
|
845
|
+
) INTO result
|
|
846
|
+
FROM session_turns turn_row
|
|
847
|
+
JOIN sessions owned ON owned.id = turn_row.session_id
|
|
848
|
+
WHERE turn_row.account_id = account_id_value
|
|
849
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
850
|
+
AND turn_row.status IN (
|
|
851
|
+
'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
|
|
852
|
+
)
|
|
853
|
+
AND (
|
|
854
|
+
owned.owner_organization_membership_id = target.id
|
|
855
|
+
OR turn_row.initiating_human_subject_id = target.subject_id
|
|
856
|
+
)
|
|
857
|
+
AND EXISTS (
|
|
858
|
+
SELECT 1 FROM session_pending_tool_calls pending
|
|
859
|
+
WHERE pending.account_id = turn_row.account_id
|
|
860
|
+
AND pending.workspace_id = turn_row.workspace_id
|
|
861
|
+
AND pending.session_id = turn_row.session_id
|
|
862
|
+
AND pending.turn_id = turn_row.id
|
|
863
|
+
)
|
|
864
|
+
AND NOT EXISTS (
|
|
865
|
+
SELECT 1 FROM session_turn_attempts live_attempt
|
|
866
|
+
WHERE live_attempt.account_id = turn_row.account_id
|
|
867
|
+
AND live_attempt.workspace_id = turn_row.workspace_id
|
|
868
|
+
AND live_attempt.session_id = turn_row.session_id
|
|
869
|
+
AND live_attempt.turn_id = turn_row.id
|
|
870
|
+
AND live_attempt.id = turn_row.active_attempt_id
|
|
871
|
+
AND live_attempt.state IN ('claimed', 'running')
|
|
872
|
+
);
|
|
873
|
+
END LOOP;
|
|
874
|
+
PERFORM pg_catalog.set_config(
|
|
875
|
+
'opengeni.workspace_id', COALESCE(previous_workspace, ''), true
|
|
876
|
+
);
|
|
877
|
+
RETURN result;
|
|
878
|
+
EXCEPTION WHEN OTHERS THEN
|
|
879
|
+
PERFORM pg_catalog.set_config(
|
|
880
|
+
'opengeni.workspace_id', COALESCE(previous_workspace, ''), true
|
|
881
|
+
);
|
|
882
|
+
RAISE;
|
|
883
|
+
END
|
|
884
|
+
$body$;
|
|
885
|
+
|
|
886
|
+
CREATE OR REPLACE FUNCTION organization_membership_command(p_command jsonb)
|
|
887
|
+
RETURNS jsonb
|
|
888
|
+
LANGUAGE plpgsql
|
|
889
|
+
SECURITY DEFINER
|
|
890
|
+
SET search_path FROM CURRENT
|
|
891
|
+
AS $body$
|
|
892
|
+
DECLARE
|
|
893
|
+
action_name text := p_command ->> 'action';
|
|
894
|
+
account_id_value uuid := nullif(p_command ->> 'organizationId', '')::uuid;
|
|
895
|
+
actor_subject text := p_command ->> 'actorSubjectId';
|
|
896
|
+
operation_id_value uuid := nullif(p_command ->> 'operationId', '')::uuid;
|
|
897
|
+
input_hash_value text;
|
|
898
|
+
receipt_row organization_membership_operation_receipts%ROWTYPE;
|
|
899
|
+
actor organization_memberships%ROWTYPE;
|
|
900
|
+
target organization_memberships%ROWTYPE;
|
|
901
|
+
invitation organization_membership_invitations%ROWTYPE;
|
|
902
|
+
policy organization_user_retention_policies%ROWTYPE;
|
|
903
|
+
result jsonb;
|
|
904
|
+
now_value timestamptz := pg_catalog.clock_timestamp();
|
|
905
|
+
target_subject text;
|
|
906
|
+
target_email text;
|
|
907
|
+
requested_role text;
|
|
908
|
+
target_id uuid;
|
|
909
|
+
expected_revision bigint;
|
|
910
|
+
workspace_id_value uuid;
|
|
911
|
+
reason_value text;
|
|
912
|
+
retention_days_value integer;
|
|
913
|
+
previous_workspace text := pg_catalog.current_setting('opengeni.workspace_id', true);
|
|
914
|
+
previous_visibility_capability text := pg_catalog.current_setting(
|
|
915
|
+
'opengeni.session_visibility_write_capability', true
|
|
916
|
+
);
|
|
917
|
+
previous_activity_gate_state text := pg_catalog.current_setting(
|
|
918
|
+
'opengeni.session_activity_gate_state', true
|
|
919
|
+
);
|
|
920
|
+
previous_activity_gate_workspace text := pg_catalog.current_setting(
|
|
921
|
+
'opengeni.session_activity_gate_workspace_id', true
|
|
922
|
+
);
|
|
923
|
+
visibility_capability_id uuid;
|
|
924
|
+
interruption_operation_id uuid;
|
|
925
|
+
affected_session_ids uuid[];
|
|
926
|
+
affected_realtime_ids uuid[];
|
|
927
|
+
cancelled_turn_ids uuid[];
|
|
928
|
+
cancelled_turn record;
|
|
929
|
+
cancelled_request record;
|
|
930
|
+
cancelled_sequence bigint;
|
|
931
|
+
cancelled_association text;
|
|
932
|
+
settled_update_ids uuid[];
|
|
933
|
+
settled_history_item_id uuid;
|
|
934
|
+
activity_revision_value bigint;
|
|
935
|
+
BEGIN
|
|
936
|
+
IF p_command IS NULL
|
|
937
|
+
OR action_name NOT IN ('invite', 'accept', 'revoke_invitation', 'change_role', 'suspend', 'reactivate', 'offboard', 'retention')
|
|
938
|
+
OR account_id_value IS NULL
|
|
939
|
+
OR operation_id_value IS NULL
|
|
940
|
+
OR actor_subject IS NULL
|
|
941
|
+
OR actor_subject IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
942
|
+
OR account_id_value IS DISTINCT FROM opengeni_private.current_account_id()
|
|
943
|
+
THEN
|
|
944
|
+
RAISE EXCEPTION 'organization membership command authority is invalid'
|
|
945
|
+
USING ERRCODE = '42501';
|
|
946
|
+
END IF;
|
|
947
|
+
input_hash_value := pg_catalog.encode(
|
|
948
|
+
pg_catalog.sha256(pg_catalog.convert_to(p_command::text, 'UTF8')), 'hex'
|
|
949
|
+
);
|
|
950
|
+
PERFORM 1 FROM managed_accounts account WHERE account.id = account_id_value FOR UPDATE;
|
|
951
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'organization not found' USING ERRCODE = 'P0002'; END IF;
|
|
952
|
+
PERFORM pg_catalog.set_config(
|
|
953
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
954
|
+
'organization_membership_lifecycle', true
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
SELECT * INTO receipt_row
|
|
958
|
+
FROM organization_membership_operation_receipts receipt
|
|
959
|
+
WHERE receipt.account_id = account_id_value
|
|
960
|
+
AND receipt.operation_id = operation_id_value
|
|
961
|
+
FOR UPDATE;
|
|
962
|
+
IF FOUND THEN
|
|
963
|
+
IF receipt_row.action IS DISTINCT FROM action_name
|
|
964
|
+
OR receipt_row.input_hash IS DISTINCT FROM input_hash_value
|
|
965
|
+
THEN
|
|
966
|
+
RAISE EXCEPTION 'organization operation id was reused with different input'
|
|
967
|
+
USING ERRCODE = '23505';
|
|
968
|
+
END IF;
|
|
969
|
+
RETURN receipt_row.result;
|
|
970
|
+
END IF;
|
|
971
|
+
|
|
972
|
+
IF action_name = 'accept' THEN
|
|
973
|
+
target_id := nullif(p_command ->> 'invitationId', '')::uuid;
|
|
974
|
+
expected_revision := nullif(p_command ->> 'expectedRevision', '')::bigint;
|
|
975
|
+
SELECT * INTO invitation
|
|
976
|
+
FROM organization_membership_invitations candidate
|
|
977
|
+
WHERE candidate.id = target_id AND candidate.account_id = account_id_value
|
|
978
|
+
FOR UPDATE;
|
|
979
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'invitation not found' USING ERRCODE = 'P0002'; END IF;
|
|
980
|
+
IF invitation.target_subject_id IS DISTINCT FROM actor_subject THEN
|
|
981
|
+
RAISE EXCEPTION 'invitation belongs to another subject' USING ERRCODE = '42501';
|
|
982
|
+
END IF;
|
|
983
|
+
IF invitation.status <> 'pending' OR invitation.expires_at <= now_value THEN
|
|
984
|
+
RAISE EXCEPTION 'invitation is not active' USING ERRCODE = '55000';
|
|
985
|
+
END IF;
|
|
986
|
+
IF invitation.revision IS DISTINCT FROM expected_revision THEN
|
|
987
|
+
RAISE EXCEPTION 'invitation revision is stale' USING ERRCODE = '40001';
|
|
988
|
+
END IF;
|
|
989
|
+
SELECT * INTO target FROM organization_memberships membership
|
|
990
|
+
WHERE membership.account_id = account_id_value
|
|
991
|
+
AND membership.subject_id = actor_subject
|
|
992
|
+
FOR UPDATE;
|
|
993
|
+
IF NOT FOUND THEN
|
|
994
|
+
workspace_id_value := gen_random_uuid();
|
|
995
|
+
PERFORM pg_catalog.set_config('opengeni.workspace_id', workspace_id_value::text, true);
|
|
996
|
+
INSERT INTO workspaces (
|
|
997
|
+
id, account_id, name, slug, external_source, external_id
|
|
998
|
+
) VALUES (
|
|
999
|
+
workspace_id_value, account_id_value, 'Personal workspace', NULL,
|
|
1000
|
+
'opengeni:organization-membership', account_id_value::text || ':' || actor_subject
|
|
1001
|
+
) ON CONFLICT (external_source, external_id) DO UPDATE
|
|
1002
|
+
SET updated_at = workspaces.updated_at
|
|
1003
|
+
RETURNING id INTO workspace_id_value;
|
|
1004
|
+
INSERT INTO workspace_inference_controls (workspace_id, account_id)
|
|
1005
|
+
VALUES (workspace_id_value, account_id_value) ON CONFLICT DO NOTHING;
|
|
1006
|
+
INSERT INTO organization_memberships (
|
|
1007
|
+
account_id, subject_id, role, status, personal_workspace_id
|
|
1008
|
+
) VALUES (
|
|
1009
|
+
account_id_value, actor_subject, invitation.role, 'active', workspace_id_value
|
|
1010
|
+
) RETURNING * INTO target;
|
|
1011
|
+
ELSE
|
|
1012
|
+
IF target.status = 'active' THEN
|
|
1013
|
+
RAISE EXCEPTION 'organization membership is already active' USING ERRCODE = '55000';
|
|
1014
|
+
END IF;
|
|
1015
|
+
RAISE EXCEPTION 'inactive organization membership requires explicit owner lifecycle action'
|
|
1016
|
+
USING ERRCODE = '55000';
|
|
1017
|
+
END IF;
|
|
1018
|
+
UPDATE organization_membership_invitations SET
|
|
1019
|
+
status = 'accepted', revision = revision + 1,
|
|
1020
|
+
accepted_membership_id = target.id, updated_at = now_value
|
|
1021
|
+
WHERE id = invitation.id RETURNING * INTO invitation;
|
|
1022
|
+
result := pg_catalog.jsonb_build_object(
|
|
1023
|
+
'invitation', opengeni_private.organization_invitation_row_json(invitation),
|
|
1024
|
+
'membership', opengeni_private.organization_membership_row_json(target)
|
|
1025
|
+
);
|
|
1026
|
+
actor := target;
|
|
1027
|
+
ELSE
|
|
1028
|
+
-- Existing session-visibility activation serializes workspace -> member ->
|
|
1029
|
+
-- session. Offboarding spans every owned workspace, so pre-lock that exact
|
|
1030
|
+
-- workspace prefix in UUID order before locking either actor or target
|
|
1031
|
+
-- membership. This prevents member/workspace lock inversion while the
|
|
1032
|
+
-- later per-workspace pass takes the canonical session/turn/attempt suffix.
|
|
1033
|
+
IF action_name IN ('suspend', 'offboard') THEN
|
|
1034
|
+
target_id := nullif(p_command ->> 'membershipId', '')::uuid;
|
|
1035
|
+
expected_revision := nullif(
|
|
1036
|
+
p_command ->> 'expectedAuthorizationRevision', ''
|
|
1037
|
+
)::bigint;
|
|
1038
|
+
FOR workspace_id_value IN
|
|
1039
|
+
SELECT workspace.id
|
|
1040
|
+
FROM workspaces workspace
|
|
1041
|
+
WHERE workspace.account_id = account_id_value
|
|
1042
|
+
ORDER BY workspace.id
|
|
1043
|
+
LOOP
|
|
1044
|
+
PERFORM pg_catalog.set_config(
|
|
1045
|
+
'opengeni.workspace_id', workspace_id_value::text, true
|
|
1046
|
+
);
|
|
1047
|
+
PERFORM 1 FROM workspace_inference_controls control
|
|
1048
|
+
WHERE control.account_id = account_id_value
|
|
1049
|
+
AND control.workspace_id = workspace_id_value
|
|
1050
|
+
FOR SHARE;
|
|
1051
|
+
PERFORM 1 FROM workspaces workspace
|
|
1052
|
+
WHERE workspace.account_id = account_id_value
|
|
1053
|
+
AND workspace.id = workspace_id_value
|
|
1054
|
+
FOR KEY SHARE;
|
|
1055
|
+
END LOOP;
|
|
1056
|
+
PERFORM 1 FROM organization_memberships membership
|
|
1057
|
+
WHERE membership.account_id = account_id_value
|
|
1058
|
+
AND membership.id IN (
|
|
1059
|
+
target_id,
|
|
1060
|
+
(
|
|
1061
|
+
SELECT candidate.id FROM organization_memberships candidate
|
|
1062
|
+
WHERE candidate.account_id = account_id_value
|
|
1063
|
+
AND candidate.subject_id = actor_subject
|
|
1064
|
+
)
|
|
1065
|
+
)
|
|
1066
|
+
ORDER BY membership.id
|
|
1067
|
+
FOR UPDATE;
|
|
1068
|
+
SELECT * INTO actor FROM organization_memberships membership
|
|
1069
|
+
WHERE membership.account_id = account_id_value
|
|
1070
|
+
AND membership.subject_id = actor_subject;
|
|
1071
|
+
ELSE
|
|
1072
|
+
SELECT * INTO actor FROM organization_memberships membership
|
|
1073
|
+
WHERE membership.account_id = account_id_value
|
|
1074
|
+
AND membership.subject_id = actor_subject
|
|
1075
|
+
FOR UPDATE;
|
|
1076
|
+
END IF;
|
|
1077
|
+
IF NOT FOUND OR actor.status <> 'active' THEN
|
|
1078
|
+
RAISE EXCEPTION 'active organization membership required' USING ERRCODE = '42501';
|
|
1079
|
+
END IF;
|
|
1080
|
+
|
|
1081
|
+
IF action_name = 'invite' THEN
|
|
1082
|
+
IF actor.role NOT IN ('owner', 'admin') THEN
|
|
1083
|
+
RAISE EXCEPTION 'organization administration required' USING ERRCODE = '42501';
|
|
1084
|
+
END IF;
|
|
1085
|
+
target_subject := p_command ->> 'targetSubjectId';
|
|
1086
|
+
target_email := lower(btrim(p_command ->> 'targetEmail'));
|
|
1087
|
+
requested_role := p_command ->> 'role';
|
|
1088
|
+
IF target_subject IS NULL OR target_subject NOT LIKE 'user:%'
|
|
1089
|
+
OR requested_role NOT IN ('owner', 'admin', 'member')
|
|
1090
|
+
OR (actor.role = 'admin' AND requested_role <> 'member')
|
|
1091
|
+
OR nullif(p_command ->> 'expiresAt', '')::timestamptz <= now_value
|
|
1092
|
+
OR nullif(p_command ->> 'expiresAt', '')::timestamptz > now_value + interval '30 days'
|
|
1093
|
+
THEN
|
|
1094
|
+
RAISE EXCEPTION 'invitation input is invalid' USING ERRCODE = '22023';
|
|
1095
|
+
END IF;
|
|
1096
|
+
IF EXISTS (
|
|
1097
|
+
SELECT 1 FROM organization_memberships membership
|
|
1098
|
+
WHERE membership.account_id = account_id_value
|
|
1099
|
+
AND membership.subject_id = target_subject
|
|
1100
|
+
AND membership.status = 'active'
|
|
1101
|
+
) THEN
|
|
1102
|
+
RAISE EXCEPTION 'subject is already an active organization member' USING ERRCODE = '55000';
|
|
1103
|
+
END IF;
|
|
1104
|
+
IF EXISTS (
|
|
1105
|
+
SELECT 1 FROM organization_memberships membership
|
|
1106
|
+
WHERE membership.account_id = account_id_value
|
|
1107
|
+
AND membership.subject_id = target_subject
|
|
1108
|
+
AND membership.status IN ('suspended', 'revoked')
|
|
1109
|
+
) THEN
|
|
1110
|
+
RAISE EXCEPTION 'inactive organization membership cannot be re-invited'
|
|
1111
|
+
USING ERRCODE = '55000';
|
|
1112
|
+
END IF;
|
|
1113
|
+
UPDATE organization_membership_invitations SET
|
|
1114
|
+
status = 'revoked', revision = revision + 1, updated_at = now_value
|
|
1115
|
+
WHERE account_id = account_id_value
|
|
1116
|
+
AND target_subject_id = target_subject AND status = 'pending';
|
|
1117
|
+
INSERT INTO organization_membership_invitations (
|
|
1118
|
+
account_id, target_subject_id, target_email, role,
|
|
1119
|
+
created_by_membership_id, expires_at
|
|
1120
|
+
) VALUES (
|
|
1121
|
+
account_id_value, target_subject, target_email, requested_role,
|
|
1122
|
+
actor.id, nullif(p_command ->> 'expiresAt', '')::timestamptz
|
|
1123
|
+
) RETURNING * INTO invitation;
|
|
1124
|
+
result := opengeni_private.organization_invitation_row_json(invitation);
|
|
1125
|
+
target := NULL;
|
|
1126
|
+
ELSIF action_name = 'revoke_invitation' THEN
|
|
1127
|
+
IF actor.role NOT IN ('owner', 'admin') THEN
|
|
1128
|
+
RAISE EXCEPTION 'organization administration required' USING ERRCODE = '42501';
|
|
1129
|
+
END IF;
|
|
1130
|
+
target_id := nullif(p_command ->> 'invitationId', '')::uuid;
|
|
1131
|
+
expected_revision := nullif(p_command ->> 'expectedRevision', '')::bigint;
|
|
1132
|
+
SELECT * INTO invitation
|
|
1133
|
+
FROM organization_membership_invitations candidate
|
|
1134
|
+
WHERE candidate.id = target_id AND candidate.account_id = account_id_value
|
|
1135
|
+
FOR UPDATE;
|
|
1136
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'invitation not found' USING ERRCODE = 'P0002'; END IF;
|
|
1137
|
+
IF invitation.revision IS DISTINCT FROM expected_revision THEN
|
|
1138
|
+
RAISE EXCEPTION 'invitation revision is stale' USING ERRCODE = '40001';
|
|
1139
|
+
END IF;
|
|
1140
|
+
IF invitation.status <> 'pending' THEN
|
|
1141
|
+
RAISE EXCEPTION 'only a pending invitation can be revoked' USING ERRCODE = '55000';
|
|
1142
|
+
END IF;
|
|
1143
|
+
IF actor.role = 'admin' AND invitation.role <> 'member' THEN
|
|
1144
|
+
RAISE EXCEPTION 'administrators may revoke member invitations only' USING ERRCODE = '42501';
|
|
1145
|
+
END IF;
|
|
1146
|
+
UPDATE organization_membership_invitations SET
|
|
1147
|
+
status = 'revoked', revision = revision + 1, updated_at = now_value
|
|
1148
|
+
WHERE id = invitation.id RETURNING * INTO invitation;
|
|
1149
|
+
result := opengeni_private.organization_invitation_row_json(invitation);
|
|
1150
|
+
target := NULL;
|
|
1151
|
+
ELSIF action_name = 'retention' THEN
|
|
1152
|
+
IF actor.role <> 'owner' THEN
|
|
1153
|
+
RAISE EXCEPTION 'organization owner required' USING ERRCODE = '42501';
|
|
1154
|
+
END IF;
|
|
1155
|
+
expected_revision := nullif(p_command ->> 'expectedVersion', '')::bigint;
|
|
1156
|
+
requested_role := p_command ->> 'mode';
|
|
1157
|
+
retention_days_value := nullif(p_command ->> 'retentionDays', '')::integer;
|
|
1158
|
+
IF requested_role NOT IN ('retain', 'delete_after')
|
|
1159
|
+
OR (requested_role = 'retain' AND retention_days_value IS NOT NULL)
|
|
1160
|
+
OR (requested_role = 'delete_after' AND retention_days_value NOT BETWEEN 30 AND 90)
|
|
1161
|
+
THEN
|
|
1162
|
+
RAISE EXCEPTION 'retention policy input is invalid' USING ERRCODE = '22023';
|
|
1163
|
+
END IF;
|
|
1164
|
+
SELECT * INTO policy FROM organization_user_retention_policies candidate
|
|
1165
|
+
WHERE candidate.account_id = account_id_value FOR UPDATE;
|
|
1166
|
+
IF NOT FOUND THEN
|
|
1167
|
+
INSERT INTO organization_user_retention_policies (
|
|
1168
|
+
account_id, mode, retention_days, version, updated_by_membership_id
|
|
1169
|
+
) VALUES (account_id_value, 'retain', NULL, 1, actor.id)
|
|
1170
|
+
RETURNING * INTO policy;
|
|
1171
|
+
END IF;
|
|
1172
|
+
IF policy.version IS DISTINCT FROM expected_revision THEN
|
|
1173
|
+
RAISE EXCEPTION 'retention policy version is stale' USING ERRCODE = '40001';
|
|
1174
|
+
END IF;
|
|
1175
|
+
UPDATE organization_user_retention_policies SET
|
|
1176
|
+
mode = requested_role, retention_days = retention_days_value,
|
|
1177
|
+
version = version + 1, updated_by_membership_id = actor.id,
|
|
1178
|
+
updated_at = now_value
|
|
1179
|
+
WHERE account_id = account_id_value RETURNING * INTO policy;
|
|
1180
|
+
result := pg_catalog.jsonb_build_object(
|
|
1181
|
+
'organizationId', policy.account_id, 'mode', policy.mode,
|
|
1182
|
+
'retentionDays', policy.retention_days, 'version', policy.version,
|
|
1183
|
+
'updatedAt', policy.updated_at
|
|
1184
|
+
);
|
|
1185
|
+
target := NULL;
|
|
1186
|
+
ELSE
|
|
1187
|
+
IF actor.role NOT IN ('owner', 'admin') THEN
|
|
1188
|
+
RAISE EXCEPTION 'organization administration required' USING ERRCODE = '42501';
|
|
1189
|
+
END IF;
|
|
1190
|
+
target_id := nullif(p_command ->> 'membershipId', '')::uuid;
|
|
1191
|
+
expected_revision := nullif(p_command ->> 'expectedAuthorizationRevision', '')::bigint;
|
|
1192
|
+
requested_role := p_command ->> 'role';
|
|
1193
|
+
reason_value := nullif(btrim(p_command ->> 'reason'), '');
|
|
1194
|
+
IF reason_value IS NOT NULL
|
|
1195
|
+
AND octet_length(convert_to(reason_value, 'UTF8')) > 512
|
|
1196
|
+
THEN RAISE EXCEPTION 'reason is too large' USING ERRCODE = '22023'; END IF;
|
|
1197
|
+
PERFORM 1 FROM organization_memberships membership
|
|
1198
|
+
WHERE membership.account_id = account_id_value
|
|
1199
|
+
AND membership.id IN (actor.id, target_id)
|
|
1200
|
+
ORDER BY membership.id FOR UPDATE;
|
|
1201
|
+
SELECT * INTO target FROM organization_memberships membership
|
|
1202
|
+
WHERE membership.account_id = account_id_value AND membership.id = target_id;
|
|
1203
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'organization member not found' USING ERRCODE = 'P0002'; END IF;
|
|
1204
|
+
IF target.authorization_revision IS DISTINCT FROM expected_revision THEN
|
|
1205
|
+
RAISE EXCEPTION 'organization membership revision is stale' USING ERRCODE = '40001';
|
|
1206
|
+
END IF;
|
|
1207
|
+
IF actor.role = 'admin' AND target.role <> 'member' THEN
|
|
1208
|
+
RAISE EXCEPTION 'administrators may manage members only' USING ERRCODE = '42501';
|
|
1209
|
+
END IF;
|
|
1210
|
+
IF target.role = 'owner' AND (
|
|
1211
|
+
action_name IN ('suspend', 'offboard')
|
|
1212
|
+
OR (action_name = 'change_role' AND requested_role <> 'owner')
|
|
1213
|
+
) AND NOT EXISTS (
|
|
1214
|
+
SELECT 1 FROM organization_memberships other
|
|
1215
|
+
WHERE other.account_id = account_id_value AND other.id <> target.id
|
|
1216
|
+
AND other.status = 'active' AND other.role = 'owner'
|
|
1217
|
+
) THEN
|
|
1218
|
+
RAISE EXCEPTION 'cannot remove the last active organization owner'
|
|
1219
|
+
USING ERRCODE = '55000';
|
|
1220
|
+
END IF;
|
|
1221
|
+
IF action_name = 'change_role' THEN
|
|
1222
|
+
IF target.status <> 'active' OR requested_role NOT IN ('owner', 'admin', 'member')
|
|
1223
|
+
OR (actor.role = 'admin' AND requested_role <> 'member')
|
|
1224
|
+
THEN RAISE EXCEPTION 'role transition is invalid' USING ERRCODE = '55000'; END IF;
|
|
1225
|
+
UPDATE organization_memberships SET role = requested_role,
|
|
1226
|
+
authorization_revision = authorization_revision + 1, updated_at = now_value
|
|
1227
|
+
WHERE id = target.id RETURNING * INTO target;
|
|
1228
|
+
ELSIF action_name = 'reactivate' THEN
|
|
1229
|
+
IF target.status <> 'suspended' THEN
|
|
1230
|
+
RAISE EXCEPTION 'only a suspended member can be reactivated' USING ERRCODE = '55000';
|
|
1231
|
+
END IF;
|
|
1232
|
+
UPDATE organization_memberships SET status = 'active',
|
|
1233
|
+
authorization_revision = authorization_revision + 1, updated_at = now_value
|
|
1234
|
+
WHERE id = target.id RETURNING * INTO target;
|
|
1235
|
+
ELSIF action_name IN ('suspend', 'offboard') THEN
|
|
1236
|
+
IF action_name = 'suspend' AND target.status <> 'active' THEN
|
|
1237
|
+
RAISE EXCEPTION 'only an active member can be suspended' USING ERRCODE = '55000';
|
|
1238
|
+
ELSIF action_name = 'offboard' AND target.status NOT IN ('active', 'suspended') THEN
|
|
1239
|
+
RAISE EXCEPTION 'only an active or suspended member can be offboarded'
|
|
1240
|
+
USING ERRCODE = '55000';
|
|
1241
|
+
END IF;
|
|
1242
|
+
SELECT * INTO policy FROM organization_user_retention_policies candidate
|
|
1243
|
+
WHERE candidate.account_id = account_id_value;
|
|
1244
|
+
UPDATE organization_user_resource_grants SET
|
|
1245
|
+
status = 'revoked', generation = generation + 1,
|
|
1246
|
+
revoked_at = now_value, updated_at = now_value
|
|
1247
|
+
WHERE account_id = account_id_value
|
|
1248
|
+
AND owner_organization_membership_id = target.id AND status = 'active';
|
|
1249
|
+
UPDATE organization_user_resource_authorities SET
|
|
1250
|
+
status = 'retained', generation = generation + 1, updated_at = now_value
|
|
1251
|
+
WHERE account_id = account_id_value
|
|
1252
|
+
AND organization_membership_id = target.id AND status = 'active'
|
|
1253
|
+
AND action_name = 'offboard';
|
|
1254
|
+
DELETE FROM workspace_memberships
|
|
1255
|
+
WHERE account_id = account_id_value AND subject_id = target.subject_id;
|
|
1256
|
+
-- Private-session attempts carry the exact authority epoch. Advance it
|
|
1257
|
+
-- under each workspace's FORCE-RLS context without touching updated_at
|
|
1258
|
+
-- (the session activity commit gate owns that separate concern).
|
|
1259
|
+
visibility_capability_id := pg_catalog.gen_random_uuid();
|
|
1260
|
+
INSERT INTO session_visibility_write_capabilities (
|
|
1261
|
+
backend_pid, transaction_id, capability_id
|
|
1262
|
+
) VALUES (
|
|
1263
|
+
pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id(),
|
|
1264
|
+
visibility_capability_id
|
|
1265
|
+
);
|
|
1266
|
+
PERFORM pg_catalog.set_config(
|
|
1267
|
+
'opengeni.session_visibility_write_capability',
|
|
1268
|
+
visibility_capability_id::text, true
|
|
1269
|
+
);
|
|
1270
|
+
FOR workspace_id_value IN
|
|
1271
|
+
SELECT workspace.id
|
|
1272
|
+
FROM workspaces workspace
|
|
1273
|
+
WHERE workspace.account_id = account_id_value
|
|
1274
|
+
ORDER BY workspace.id
|
|
1275
|
+
LOOP
|
|
1276
|
+
PERFORM pg_catalog.set_config(
|
|
1277
|
+
'opengeni.workspace_id', workspace_id_value::text, true
|
|
1278
|
+
);
|
|
1279
|
+
PERFORM pg_catalog.set_config(
|
|
1280
|
+
'opengeni.session_activity_gate_state', 'open', true
|
|
1281
|
+
);
|
|
1282
|
+
PERFORM pg_catalog.set_config(
|
|
1283
|
+
'opengeni.session_activity_gate_workspace_id', workspace_id_value::text, true
|
|
1284
|
+
);
|
|
1285
|
+
PERFORM 1 FROM workspace_inference_controls control
|
|
1286
|
+
WHERE control.account_id = account_id_value
|
|
1287
|
+
AND control.workspace_id = workspace_id_value
|
|
1288
|
+
FOR SHARE;
|
|
1289
|
+
PERFORM 1 FROM workspaces workspace
|
|
1290
|
+
WHERE workspace.account_id = account_id_value
|
|
1291
|
+
AND workspace.id = workspace_id_value
|
|
1292
|
+
FOR KEY SHARE;
|
|
1293
|
+
PERFORM 1 FROM sessions affected
|
|
1294
|
+
WHERE affected.account_id = account_id_value
|
|
1295
|
+
AND affected.workspace_id = workspace_id_value
|
|
1296
|
+
AND (
|
|
1297
|
+
affected.owner_organization_membership_id = target.id
|
|
1298
|
+
OR EXISTS (
|
|
1299
|
+
SELECT 1 FROM session_turns initiated
|
|
1300
|
+
WHERE initiated.session_id = affected.id
|
|
1301
|
+
AND initiated.initiating_human_subject_id = target.subject_id
|
|
1302
|
+
)
|
|
1303
|
+
)
|
|
1304
|
+
ORDER BY affected.id
|
|
1305
|
+
FOR NO KEY UPDATE;
|
|
1306
|
+
PERFORM 1 FROM session_turns turn_row
|
|
1307
|
+
WHERE turn_row.account_id = account_id_value
|
|
1308
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
1309
|
+
AND EXISTS (
|
|
1310
|
+
SELECT 1 FROM sessions owned
|
|
1311
|
+
WHERE owned.id = turn_row.session_id
|
|
1312
|
+
AND (
|
|
1313
|
+
owned.owner_organization_membership_id = target.id
|
|
1314
|
+
OR turn_row.initiating_human_subject_id = target.subject_id
|
|
1315
|
+
)
|
|
1316
|
+
)
|
|
1317
|
+
ORDER BY turn_row.id
|
|
1318
|
+
FOR UPDATE;
|
|
1319
|
+
PERFORM 1 FROM session_turn_attempts attempt
|
|
1320
|
+
WHERE attempt.account_id = account_id_value
|
|
1321
|
+
AND attempt.workspace_id = workspace_id_value
|
|
1322
|
+
AND EXISTS (
|
|
1323
|
+
SELECT 1 FROM sessions owned
|
|
1324
|
+
WHERE owned.id = attempt.session_id
|
|
1325
|
+
AND (
|
|
1326
|
+
owned.owner_organization_membership_id = target.id
|
|
1327
|
+
OR EXISTS (
|
|
1328
|
+
SELECT 1 FROM session_turns initiated
|
|
1329
|
+
WHERE initiated.id = attempt.turn_id
|
|
1330
|
+
AND initiated.initiating_human_subject_id = target.subject_id
|
|
1331
|
+
)
|
|
1332
|
+
)
|
|
1333
|
+
)
|
|
1334
|
+
ORDER BY attempt.id
|
|
1335
|
+
FOR UPDATE;
|
|
1336
|
+
PERFORM 1 FROM session_realtime_modes realtime
|
|
1337
|
+
WHERE realtime.account_id = account_id_value
|
|
1338
|
+
AND realtime.workspace_id = workspace_id_value
|
|
1339
|
+
AND realtime.owner_subject_id = target.subject_id
|
|
1340
|
+
AND realtime.state = 'active'
|
|
1341
|
+
ORDER BY realtime.id
|
|
1342
|
+
FOR UPDATE;
|
|
1343
|
+
SELECT COALESCE(pg_catalog.array_agg(realtime.id ORDER BY realtime.id), ARRAY[]::uuid[])
|
|
1344
|
+
INTO affected_realtime_ids
|
|
1345
|
+
FROM session_realtime_modes realtime
|
|
1346
|
+
WHERE realtime.account_id = account_id_value
|
|
1347
|
+
AND realtime.workspace_id = workspace_id_value
|
|
1348
|
+
AND realtime.owner_subject_id = target.subject_id
|
|
1349
|
+
AND realtime.state = 'active';
|
|
1350
|
+
PERFORM 1 FROM session_realtime_connections connection
|
|
1351
|
+
WHERE connection.account_id = account_id_value
|
|
1352
|
+
AND connection.workspace_id = workspace_id_value
|
|
1353
|
+
AND connection.realtime_id = ANY(affected_realtime_ids)
|
|
1354
|
+
AND connection.state IN ('negotiating', 'ready', 'active')
|
|
1355
|
+
ORDER BY connection.id
|
|
1356
|
+
FOR UPDATE;
|
|
1357
|
+
IF EXISTS (
|
|
1358
|
+
SELECT 1
|
|
1359
|
+
FROM session_turns turn_row
|
|
1360
|
+
JOIN sessions owned ON owned.id = turn_row.session_id
|
|
1361
|
+
WHERE turn_row.account_id = account_id_value
|
|
1362
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
1363
|
+
AND turn_row.status IN (
|
|
1364
|
+
'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
|
|
1365
|
+
)
|
|
1366
|
+
AND (
|
|
1367
|
+
owned.owner_organization_membership_id = target.id
|
|
1368
|
+
OR turn_row.initiating_human_subject_id = target.subject_id
|
|
1369
|
+
)
|
|
1370
|
+
AND EXISTS (
|
|
1371
|
+
SELECT 1 FROM session_pending_tool_calls pending
|
|
1372
|
+
WHERE pending.account_id = turn_row.account_id
|
|
1373
|
+
AND pending.workspace_id = turn_row.workspace_id
|
|
1374
|
+
AND pending.session_id = turn_row.session_id
|
|
1375
|
+
AND pending.turn_id = turn_row.id
|
|
1376
|
+
)
|
|
1377
|
+
AND NOT EXISTS (
|
|
1378
|
+
SELECT 1 FROM session_turn_attempts live_attempt
|
|
1379
|
+
WHERE live_attempt.account_id = turn_row.account_id
|
|
1380
|
+
AND live_attempt.workspace_id = turn_row.workspace_id
|
|
1381
|
+
AND live_attempt.session_id = turn_row.session_id
|
|
1382
|
+
AND live_attempt.turn_id = turn_row.id
|
|
1383
|
+
AND live_attempt.id = turn_row.active_attempt_id
|
|
1384
|
+
AND live_attempt.state IN ('claimed', 'running')
|
|
1385
|
+
)
|
|
1386
|
+
) THEN
|
|
1387
|
+
RAISE EXCEPTION 'pending tool calls require canonical protocol settlement'
|
|
1388
|
+
USING ERRCODE = '55000';
|
|
1389
|
+
END IF;
|
|
1390
|
+
SELECT COALESCE(pg_catalog.array_agg(DISTINCT turn_row.session_id), ARRAY[]::uuid[])
|
|
1391
|
+
INTO affected_session_ids
|
|
1392
|
+
FROM session_turns turn_row
|
|
1393
|
+
JOIN sessions owned
|
|
1394
|
+
ON owned.account_id = turn_row.account_id
|
|
1395
|
+
AND owned.workspace_id = turn_row.workspace_id
|
|
1396
|
+
AND owned.id = turn_row.session_id
|
|
1397
|
+
WHERE turn_row.account_id = account_id_value
|
|
1398
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
1399
|
+
AND turn_row.status IN (
|
|
1400
|
+
'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
|
|
1401
|
+
)
|
|
1402
|
+
AND (
|
|
1403
|
+
owned.owner_organization_membership_id = target.id
|
|
1404
|
+
OR turn_row.initiating_human_subject_id = target.subject_id
|
|
1405
|
+
);
|
|
1406
|
+
affected_session_ids := ARRAY(
|
|
1407
|
+
SELECT DISTINCT candidate.session_id
|
|
1408
|
+
FROM (
|
|
1409
|
+
SELECT unnest(affected_session_ids) AS session_id
|
|
1410
|
+
UNION ALL
|
|
1411
|
+
SELECT realtime.session_id
|
|
1412
|
+
FROM session_realtime_modes realtime
|
|
1413
|
+
WHERE realtime.id = ANY(affected_realtime_ids)
|
|
1414
|
+
) candidate
|
|
1415
|
+
ORDER BY candidate.session_id
|
|
1416
|
+
);
|
|
1417
|
+
interruption_operation_id := pg_catalog.gen_random_uuid();
|
|
1418
|
+
INSERT INTO session_command_receipts (
|
|
1419
|
+
id, account_id, workspace_id, actor_type, actor_subject_id,
|
|
1420
|
+
action, operation_key, canonical_request_hash, result
|
|
1421
|
+
) VALUES (
|
|
1422
|
+
interruption_operation_id, account_id_value, workspace_id_value,
|
|
1423
|
+
'human', actor_subject, 'organization.membership.' || action_name,
|
|
1424
|
+
'organization-membership:' || operation_id_value::text,
|
|
1425
|
+
input_hash_value,
|
|
1426
|
+
pg_catalog.jsonb_build_object(
|
|
1427
|
+
'organizationMembershipOperationId', operation_id_value,
|
|
1428
|
+
'organizationMembershipId', target.id
|
|
1429
|
+
)
|
|
1430
|
+
);
|
|
1431
|
+
INSERT INTO session_attempt_interruptions (
|
|
1432
|
+
account_id, workspace_id, session_id, operation_id, attempt_id,
|
|
1433
|
+
kind, control_revision
|
|
1434
|
+
)
|
|
1435
|
+
SELECT attempt.account_id, attempt.workspace_id, attempt.session_id,
|
|
1436
|
+
interruption_operation_id, attempt.id, 'organization_membership_revoked',
|
|
1437
|
+
CASE WHEN owned.owner_organization_membership_id = target.id
|
|
1438
|
+
THEN owned.authority_epoch + 1 ELSE owned.authority_epoch END
|
|
1439
|
+
FROM session_turn_attempts attempt
|
|
1440
|
+
JOIN sessions owned ON owned.id = attempt.session_id
|
|
1441
|
+
JOIN session_turns initiated ON initiated.id = attempt.turn_id
|
|
1442
|
+
WHERE attempt.account_id = account_id_value
|
|
1443
|
+
AND attempt.workspace_id = workspace_id_value
|
|
1444
|
+
AND (
|
|
1445
|
+
owned.owner_organization_membership_id = target.id
|
|
1446
|
+
OR initiated.initiating_human_subject_id = target.subject_id
|
|
1447
|
+
)
|
|
1448
|
+
AND attempt.state IN ('claimed', 'running')
|
|
1449
|
+
ON CONFLICT ON CONSTRAINT session_attempt_interruptions_operation_attempt_uq
|
|
1450
|
+
DO NOTHING;
|
|
1451
|
+
WITH cancelled AS (
|
|
1452
|
+
UPDATE session_turns turn_row
|
|
1453
|
+
SET status = 'cancelled', active_attempt_id = NULL,
|
|
1454
|
+
cancelled_by = actor_subject, cancel_reason = 'authority_changed',
|
|
1455
|
+
finished_at = now_value, updated_at = now_value,
|
|
1456
|
+
version = turn_row.version + 1
|
|
1457
|
+
WHERE turn_row.account_id = account_id_value
|
|
1458
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
1459
|
+
AND turn_row.status IN (
|
|
1460
|
+
'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
|
|
1461
|
+
)
|
|
1462
|
+
AND EXISTS (
|
|
1463
|
+
SELECT 1 FROM sessions owned
|
|
1464
|
+
WHERE owned.account_id = turn_row.account_id
|
|
1465
|
+
AND owned.workspace_id = turn_row.workspace_id
|
|
1466
|
+
AND owned.id = turn_row.session_id
|
|
1467
|
+
AND (
|
|
1468
|
+
owned.owner_organization_membership_id = target.id
|
|
1469
|
+
OR turn_row.initiating_human_subject_id = target.subject_id
|
|
1470
|
+
)
|
|
1471
|
+
)
|
|
1472
|
+
AND NOT EXISTS (
|
|
1473
|
+
SELECT 1 FROM session_turn_attempts live_attempt
|
|
1474
|
+
WHERE live_attempt.account_id = turn_row.account_id
|
|
1475
|
+
AND live_attempt.workspace_id = turn_row.workspace_id
|
|
1476
|
+
AND live_attempt.session_id = turn_row.session_id
|
|
1477
|
+
AND live_attempt.turn_id = turn_row.id
|
|
1478
|
+
AND live_attempt.id = turn_row.active_attempt_id
|
|
1479
|
+
AND live_attempt.state IN ('claimed', 'running')
|
|
1480
|
+
)
|
|
1481
|
+
RETURNING turn_row.id
|
|
1482
|
+
)
|
|
1483
|
+
SELECT COALESCE(pg_catalog.array_agg(cancelled.id ORDER BY cancelled.id), ARRAY[]::uuid[])
|
|
1484
|
+
INTO cancelled_turn_ids
|
|
1485
|
+
FROM cancelled;
|
|
1486
|
+
FOR cancelled_turn IN
|
|
1487
|
+
SELECT turn_row.id, turn_row.session_id, turn_row.execution_generation
|
|
1488
|
+
FROM session_turns turn_row
|
|
1489
|
+
WHERE turn_row.account_id = account_id_value
|
|
1490
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
1491
|
+
AND turn_row.id = ANY(cancelled_turn_ids)
|
|
1492
|
+
ORDER BY turn_row.session_id, turn_row.id
|
|
1493
|
+
LOOP
|
|
1494
|
+
SELECT affected.last_sequence,
|
|
1495
|
+
CASE WHEN affected.active_turn_id = cancelled_turn.id
|
|
1496
|
+
THEN 'current' ELSE NULL END
|
|
1497
|
+
INTO cancelled_sequence, cancelled_association
|
|
1498
|
+
FROM sessions affected
|
|
1499
|
+
WHERE affected.account_id = account_id_value
|
|
1500
|
+
AND affected.workspace_id = workspace_id_value
|
|
1501
|
+
AND affected.id = cancelled_turn.session_id;
|
|
1502
|
+
FOR cancelled_request IN
|
|
1503
|
+
UPDATE session_human_input_requests request_row
|
|
1504
|
+
SET status = 'cancelled', response = '{"outcome":"cancelled"}'::jsonb,
|
|
1505
|
+
responded_by = actor_subject, responded_at = now_value,
|
|
1506
|
+
updated_at = now_value
|
|
1507
|
+
WHERE request_row.account_id = account_id_value
|
|
1508
|
+
AND request_row.workspace_id = workspace_id_value
|
|
1509
|
+
AND request_row.session_id = cancelled_turn.session_id
|
|
1510
|
+
AND request_row.turn_id = cancelled_turn.id
|
|
1511
|
+
AND request_row.status = 'pending'
|
|
1512
|
+
RETURNING request_row.id, request_row.creation_attempt_id
|
|
1513
|
+
LOOP
|
|
1514
|
+
cancelled_sequence := cancelled_sequence + 1;
|
|
1515
|
+
INSERT INTO session_events (
|
|
1516
|
+
account_id, workspace_id, session_id, sequence, type, payload,
|
|
1517
|
+
payload_codec_version, turn_id, turn_generation,
|
|
1518
|
+
turn_attempt_id, turn_association, occurred_at
|
|
1519
|
+
) VALUES (
|
|
1520
|
+
account_id_value, workspace_id_value, cancelled_turn.session_id,
|
|
1521
|
+
cancelled_sequence, 'user.humanInputResponse',
|
|
1522
|
+
pg_catalog.jsonb_build_object(
|
|
1523
|
+
'requestId', cancelled_request.id,
|
|
1524
|
+
'response', pg_catalog.jsonb_build_object('outcome', 'cancelled')
|
|
1525
|
+
), 1, cancelled_turn.id, cancelled_turn.execution_generation,
|
|
1526
|
+
cancelled_request.creation_attempt_id, cancelled_association, now_value
|
|
1527
|
+
);
|
|
1528
|
+
END LOOP;
|
|
1529
|
+
SELECT
|
|
1530
|
+
pg_catalog.array_agg(
|
|
1531
|
+
update_row.id ORDER BY update_row.created_at, update_row.id
|
|
1532
|
+
),
|
|
1533
|
+
(pg_catalog.array_agg(
|
|
1534
|
+
update_row.delivered_history_item_id
|
|
1535
|
+
ORDER BY update_row.created_at, update_row.id
|
|
1536
|
+
))[1]
|
|
1537
|
+
INTO settled_update_ids, settled_history_item_id
|
|
1538
|
+
FROM session_system_updates update_row
|
|
1539
|
+
WHERE update_row.account_id = account_id_value
|
|
1540
|
+
AND update_row.workspace_id = workspace_id_value
|
|
1541
|
+
AND update_row.session_id = cancelled_turn.session_id
|
|
1542
|
+
AND update_row.delivered_turn_id = cancelled_turn.id
|
|
1543
|
+
AND update_row.state = 'delivered';
|
|
1544
|
+
IF COALESCE(pg_catalog.array_length(settled_update_ids, 1), 0) > 0
|
|
1545
|
+
AND NOT EXISTS (
|
|
1546
|
+
SELECT 1 FROM session_events prior_settlement
|
|
1547
|
+
WHERE prior_settlement.account_id = account_id_value
|
|
1548
|
+
AND prior_settlement.workspace_id = workspace_id_value
|
|
1549
|
+
AND prior_settlement.session_id = cancelled_turn.session_id
|
|
1550
|
+
AND prior_settlement.turn_id = cancelled_turn.id
|
|
1551
|
+
AND prior_settlement.type = 'system.update.settled'
|
|
1552
|
+
)
|
|
1553
|
+
THEN
|
|
1554
|
+
cancelled_sequence := cancelled_sequence + 1;
|
|
1555
|
+
INSERT INTO session_events (
|
|
1556
|
+
account_id, workspace_id, session_id, sequence, type, payload,
|
|
1557
|
+
payload_codec_version, turn_id, turn_generation,
|
|
1558
|
+
turn_association, occurred_at
|
|
1559
|
+
) VALUES (
|
|
1560
|
+
account_id_value, workspace_id_value, cancelled_turn.session_id,
|
|
1561
|
+
cancelled_sequence, 'system.update.settled',
|
|
1562
|
+
pg_catalog.jsonb_build_object(
|
|
1563
|
+
'updateIds', pg_catalog.to_jsonb(settled_update_ids),
|
|
1564
|
+
'count', pg_catalog.array_length(settled_update_ids, 1),
|
|
1565
|
+
'historyItemId', settled_history_item_id,
|
|
1566
|
+
'outcome', 'cancelled'
|
|
1567
|
+
), 1, cancelled_turn.id, cancelled_turn.execution_generation,
|
|
1568
|
+
cancelled_association, now_value
|
|
1569
|
+
);
|
|
1570
|
+
END IF;
|
|
1571
|
+
IF NOT EXISTS (
|
|
1572
|
+
SELECT 1 FROM session_events prior_cancel
|
|
1573
|
+
WHERE prior_cancel.account_id = account_id_value
|
|
1574
|
+
AND prior_cancel.workspace_id = workspace_id_value
|
|
1575
|
+
AND prior_cancel.session_id = cancelled_turn.session_id
|
|
1576
|
+
AND prior_cancel.turn_id = cancelled_turn.id
|
|
1577
|
+
AND prior_cancel.type = 'turn.cancelled'
|
|
1578
|
+
) THEN
|
|
1579
|
+
cancelled_sequence := cancelled_sequence + 1;
|
|
1580
|
+
INSERT INTO session_events (
|
|
1581
|
+
account_id, workspace_id, session_id, sequence, type, payload,
|
|
1582
|
+
payload_codec_version, turn_id, turn_generation,
|
|
1583
|
+
turn_association, occurred_at
|
|
1584
|
+
) VALUES (
|
|
1585
|
+
account_id_value, workspace_id_value, cancelled_turn.session_id,
|
|
1586
|
+
cancelled_sequence, 'turn.cancelled',
|
|
1587
|
+
pg_catalog.jsonb_build_object('reason', 'authority_changed'),
|
|
1588
|
+
1, cancelled_turn.id, cancelled_turn.execution_generation,
|
|
1589
|
+
cancelled_association, now_value
|
|
1590
|
+
);
|
|
1591
|
+
END IF;
|
|
1592
|
+
UPDATE sessions affected
|
|
1593
|
+
SET last_sequence = cancelled_sequence, updated_at = now_value
|
|
1594
|
+
WHERE affected.account_id = account_id_value
|
|
1595
|
+
AND affected.workspace_id = workspace_id_value
|
|
1596
|
+
AND affected.id = cancelled_turn.session_id;
|
|
1597
|
+
END LOOP;
|
|
1598
|
+
UPDATE interaction_interventions intervention
|
|
1599
|
+
SET status = 'cancelled', response_actor_subject_id = actor_subject,
|
|
1600
|
+
version = intervention.version + 1, settled_at = now_value,
|
|
1601
|
+
updated_at = now_value
|
|
1602
|
+
WHERE intervention.account_id = account_id_value
|
|
1603
|
+
AND intervention.workspace_id = workspace_id_value
|
|
1604
|
+
AND intervention.originating_turn_id = ANY(cancelled_turn_ids)
|
|
1605
|
+
AND intervention.status = 'open';
|
|
1606
|
+
DELETE FROM agent_run_states run_state
|
|
1607
|
+
WHERE run_state.account_id = account_id_value
|
|
1608
|
+
AND run_state.workspace_id = workspace_id_value
|
|
1609
|
+
AND run_state.turn_id = ANY(cancelled_turn_ids);
|
|
1610
|
+
UPDATE codex_capacity_waiters waiter
|
|
1611
|
+
SET status = 'superseded', observed_wake_revision = waiter.wake_revision,
|
|
1612
|
+
last_wake_reason = 'organization_membership_' || action_name,
|
|
1613
|
+
updated_at = now_value
|
|
1614
|
+
WHERE waiter.account_id = account_id_value
|
|
1615
|
+
AND waiter.workspace_id = workspace_id_value
|
|
1616
|
+
AND waiter.blocked_turn_id = ANY(cancelled_turn_ids)
|
|
1617
|
+
AND waiter.status = 'waiting';
|
|
1618
|
+
UPDATE xai_capacity_waiters waiter
|
|
1619
|
+
SET status = 'superseded', observed_wake_revision = waiter.wake_revision,
|
|
1620
|
+
last_wake_reason = 'organization_membership_' || action_name,
|
|
1621
|
+
updated_at = now_value
|
|
1622
|
+
WHERE waiter.account_id = account_id_value
|
|
1623
|
+
AND waiter.workspace_id = workspace_id_value
|
|
1624
|
+
AND waiter.blocked_turn_id = ANY(cancelled_turn_ids)
|
|
1625
|
+
AND waiter.status = 'waiting';
|
|
1626
|
+
UPDATE session_realtime_connections connection
|
|
1627
|
+
SET state = 'closed', closed_at = now_value, updated_at = now_value
|
|
1628
|
+
WHERE connection.account_id = account_id_value
|
|
1629
|
+
AND connection.workspace_id = workspace_id_value
|
|
1630
|
+
AND connection.realtime_id = ANY(affected_realtime_ids)
|
|
1631
|
+
AND connection.state IN ('negotiating', 'ready', 'active');
|
|
1632
|
+
WITH ended AS (
|
|
1633
|
+
UPDATE session_realtime_modes realtime
|
|
1634
|
+
SET state = 'ended', version = realtime.version + 1,
|
|
1635
|
+
ended_at = now_value, end_reason = 'authority_revoked',
|
|
1636
|
+
updated_at = now_value
|
|
1637
|
+
WHERE realtime.account_id = account_id_value
|
|
1638
|
+
AND realtime.workspace_id = workspace_id_value
|
|
1639
|
+
AND realtime.id = ANY(affected_realtime_ids)
|
|
1640
|
+
AND realtime.state = 'active'
|
|
1641
|
+
RETURNING realtime.id, realtime.session_id, realtime.operation_id,
|
|
1642
|
+
realtime.version, realtime.connection_epoch
|
|
1643
|
+
), advanced AS (
|
|
1644
|
+
UPDATE sessions affected
|
|
1645
|
+
SET last_sequence = affected.last_sequence + 1, updated_at = now_value
|
|
1646
|
+
FROM ended
|
|
1647
|
+
WHERE affected.account_id = account_id_value
|
|
1648
|
+
AND affected.workspace_id = workspace_id_value
|
|
1649
|
+
AND affected.id = ended.session_id
|
|
1650
|
+
RETURNING affected.id AS session_id, affected.last_sequence,
|
|
1651
|
+
ended.id AS realtime_id, ended.operation_id,
|
|
1652
|
+
ended.version, ended.connection_epoch
|
|
1653
|
+
)
|
|
1654
|
+
INSERT INTO session_events (
|
|
1655
|
+
account_id, workspace_id, session_id, sequence, type, payload, occurred_at
|
|
1656
|
+
)
|
|
1657
|
+
SELECT account_id_value, workspace_id_value, advanced.session_id,
|
|
1658
|
+
advanced.last_sequence, 'session.realtime.ended',
|
|
1659
|
+
pg_catalog.jsonb_build_object(
|
|
1660
|
+
'realtimeId', advanced.realtime_id,
|
|
1661
|
+
'operationId', advanced.operation_id,
|
|
1662
|
+
'reason', 'authority_revoked',
|
|
1663
|
+
'version', advanced.version,
|
|
1664
|
+
'connectionEpoch', advanced.connection_epoch
|
|
1665
|
+
), now_value
|
|
1666
|
+
FROM advanced;
|
|
1667
|
+
UPDATE sessions affected
|
|
1668
|
+
SET active_turn_id = CASE
|
|
1669
|
+
WHEN affected.active_turn_id = ANY(cancelled_turn_ids) THEN NULL
|
|
1670
|
+
ELSE affected.active_turn_id
|
|
1671
|
+
END,
|
|
1672
|
+
status = CASE
|
|
1673
|
+
WHEN affected.active_turn_id = ANY(cancelled_turn_ids) THEN
|
|
1674
|
+
CASE WHEN EXISTS (
|
|
1675
|
+
SELECT 1 FROM session_turns queued
|
|
1676
|
+
WHERE queued.account_id = affected.account_id
|
|
1677
|
+
AND queued.workspace_id = affected.workspace_id
|
|
1678
|
+
AND queued.session_id = affected.id
|
|
1679
|
+
AND queued.status = 'queued'
|
|
1680
|
+
) THEN 'queued' ELSE 'idle' END
|
|
1681
|
+
ELSE affected.status
|
|
1682
|
+
END,
|
|
1683
|
+
queue_head_position = COALESCE((
|
|
1684
|
+
SELECT min(queued.position)::bigint FROM session_turns queued
|
|
1685
|
+
WHERE queued.account_id = affected.account_id
|
|
1686
|
+
AND queued.workspace_id = affected.workspace_id
|
|
1687
|
+
AND queued.session_id = affected.id
|
|
1688
|
+
AND queued.status = 'queued'
|
|
1689
|
+
), 0),
|
|
1690
|
+
queue_tail_position = COALESCE((
|
|
1691
|
+
SELECT max(queued.position)::bigint FROM session_turns queued
|
|
1692
|
+
WHERE queued.account_id = affected.account_id
|
|
1693
|
+
AND queued.workspace_id = affected.workspace_id
|
|
1694
|
+
AND queued.session_id = affected.id
|
|
1695
|
+
AND queued.status = 'queued'
|
|
1696
|
+
), 0),
|
|
1697
|
+
queue_version = affected.queue_version + 1,
|
|
1698
|
+
updated_at = now_value
|
|
1699
|
+
WHERE affected.account_id = account_id_value
|
|
1700
|
+
AND affected.workspace_id = workspace_id_value
|
|
1701
|
+
AND EXISTS (
|
|
1702
|
+
SELECT 1 FROM session_turns cancelled
|
|
1703
|
+
WHERE cancelled.account_id = affected.account_id
|
|
1704
|
+
AND cancelled.workspace_id = affected.workspace_id
|
|
1705
|
+
AND cancelled.session_id = affected.id
|
|
1706
|
+
AND cancelled.id = ANY(cancelled_turn_ids)
|
|
1707
|
+
);
|
|
1708
|
+
UPDATE session_system_updates update_row SET state = 'cancelled'
|
|
1709
|
+
WHERE update_row.account_id = account_id_value
|
|
1710
|
+
AND update_row.workspace_id = workspace_id_value
|
|
1711
|
+
AND update_row.state = 'pending'
|
|
1712
|
+
AND EXISTS (
|
|
1713
|
+
SELECT 1 FROM sessions owned
|
|
1714
|
+
WHERE owned.id = update_row.session_id
|
|
1715
|
+
AND owned.owner_organization_membership_id = target.id
|
|
1716
|
+
);
|
|
1717
|
+
UPDATE session_system_update_outbox outbox
|
|
1718
|
+
SET status = 'failed', last_error = 'organization_membership_' || action_name,
|
|
1719
|
+
updated_at = now_value
|
|
1720
|
+
WHERE outbox.account_id = account_id_value
|
|
1721
|
+
AND outbox.workspace_id = workspace_id_value
|
|
1722
|
+
AND outbox.status = 'pending'
|
|
1723
|
+
AND EXISTS (
|
|
1724
|
+
SELECT 1 FROM sessions source_session
|
|
1725
|
+
WHERE source_session.id = outbox.source_session_id
|
|
1726
|
+
AND source_session.owner_organization_membership_id = target.id
|
|
1727
|
+
);
|
|
1728
|
+
UPDATE session_goals goal_row
|
|
1729
|
+
SET status = 'paused', paused_reason = 'api',
|
|
1730
|
+
rationale = CASE WHEN action_name = 'suspend'
|
|
1731
|
+
THEN 'Organization membership was suspended; explicit reauthorization is required.'
|
|
1732
|
+
ELSE 'Organization membership was revoked; explicit reauthorization is required.' END,
|
|
1733
|
+
version = goal_row.version + 1, updated_at = now_value
|
|
1734
|
+
WHERE goal_row.account_id = account_id_value
|
|
1735
|
+
AND goal_row.workspace_id = workspace_id_value
|
|
1736
|
+
AND goal_row.status = 'active'
|
|
1737
|
+
AND EXISTS (
|
|
1738
|
+
SELECT 1 FROM sessions owned
|
|
1739
|
+
WHERE owned.id = goal_row.session_id
|
|
1740
|
+
AND owned.owner_organization_membership_id = target.id
|
|
1741
|
+
);
|
|
1742
|
+
UPDATE scheduled_tasks task
|
|
1743
|
+
SET status = 'paused', personal_connection_delegations = '[]'::jsonb,
|
|
1744
|
+
authority_revision = task.authority_revision + 1,
|
|
1745
|
+
updated_at = now_value
|
|
1746
|
+
WHERE task.account_id = account_id_value
|
|
1747
|
+
AND task.workspace_id = workspace_id_value
|
|
1748
|
+
AND task.status = 'active'
|
|
1749
|
+
AND task.created_by_kind = 'subject'
|
|
1750
|
+
AND task.created_by_subject_id = target.subject_id;
|
|
1751
|
+
UPDATE organization_user_resource_grants grant_row
|
|
1752
|
+
SET status = 'revoked', revoked_at = now_value,
|
|
1753
|
+
generation = grant_row.generation + 1, updated_at = now_value
|
|
1754
|
+
WHERE grant_row.account_id = account_id_value
|
|
1755
|
+
AND grant_row.workspace_id = workspace_id_value
|
|
1756
|
+
AND grant_row.status = 'active'
|
|
1757
|
+
AND EXISTS (
|
|
1758
|
+
SELECT 1 FROM sessions owned
|
|
1759
|
+
WHERE owned.id = grant_row.session_id
|
|
1760
|
+
AND owned.owner_organization_membership_id = target.id
|
|
1761
|
+
AND owned.authority_epoch = grant_row.authority_epoch
|
|
1762
|
+
);
|
|
1763
|
+
WITH advanced AS (
|
|
1764
|
+
UPDATE sessions owned SET
|
|
1765
|
+
authority_epoch = owned.authority_epoch + 1,
|
|
1766
|
+
initial_personal_connection_delegations = '[]'::jsonb,
|
|
1767
|
+
last_sequence = owned.last_sequence + 1
|
|
1768
|
+
WHERE owned.account_id = account_id_value
|
|
1769
|
+
AND owned.workspace_id = workspace_id_value
|
|
1770
|
+
AND owned.owner_organization_membership_id = target.id
|
|
1771
|
+
RETURNING owned.id, owned.workspace_id, owned.last_sequence,
|
|
1772
|
+
owned.authority_epoch
|
|
1773
|
+
)
|
|
1774
|
+
INSERT INTO session_events (
|
|
1775
|
+
account_id, workspace_id, session_id, sequence, type, payload, occurred_at
|
|
1776
|
+
)
|
|
1777
|
+
SELECT account_id_value, advanced.workspace_id, advanced.id,
|
|
1778
|
+
advanced.last_sequence,
|
|
1779
|
+
CASE WHEN action_name = 'suspend'
|
|
1780
|
+
THEN 'session.authority.suspended' ELSE 'session.authority.revoked' END,
|
|
1781
|
+
pg_catalog.jsonb_build_object(
|
|
1782
|
+
'operationId', operation_id_value,
|
|
1783
|
+
'organizationMembershipId', target.id,
|
|
1784
|
+
'previousAuthorityEpoch', advanced.authority_epoch - 1,
|
|
1785
|
+
'authorityEpoch', advanced.authority_epoch
|
|
1786
|
+
), now_value
|
|
1787
|
+
FROM advanced;
|
|
1788
|
+
INSERT INTO session_workflow_wake_outbox (
|
|
1789
|
+
session_id, account_id, workspace_id, temporal_workflow_id,
|
|
1790
|
+
reason, control_revision
|
|
1791
|
+
)
|
|
1792
|
+
SELECT affected.id, affected.account_id, affected.workspace_id,
|
|
1793
|
+
COALESCE(affected.temporal_workflow_id, 'session-' || affected.id::text),
|
|
1794
|
+
'organization_membership_' || action_name, 1
|
|
1795
|
+
FROM sessions affected
|
|
1796
|
+
WHERE affected.account_id = account_id_value
|
|
1797
|
+
AND affected.workspace_id = workspace_id_value
|
|
1798
|
+
AND affected.id = ANY(affected_session_ids)
|
|
1799
|
+
ON CONFLICT (session_id) DO UPDATE SET
|
|
1800
|
+
wake_revision = session_workflow_wake_outbox.wake_revision + 1,
|
|
1801
|
+
control_revision = session_workflow_wake_outbox.wake_revision + 1,
|
|
1802
|
+
temporal_workflow_id = EXCLUDED.temporal_workflow_id,
|
|
1803
|
+
reason = EXCLUDED.reason, attempts = 0,
|
|
1804
|
+
next_attempt_at = now_value, last_error = NULL,
|
|
1805
|
+
updated_at = now_value;
|
|
1806
|
+
PERFORM pg_catalog.set_config(
|
|
1807
|
+
'opengeni.session_activity_gate_state', 'preparing', true
|
|
1808
|
+
);
|
|
1809
|
+
SET CONSTRAINTS ALL IMMEDIATE;
|
|
1810
|
+
SET CONSTRAINTS sessions_activity_insert_commit_guard,
|
|
1811
|
+
sessions_activity_update_commit_guard DEFERRED;
|
|
1812
|
+
PERFORM pg_catalog.set_config(
|
|
1813
|
+
'opengeni.session_activity_gate_state', 'finalizing', true
|
|
1814
|
+
);
|
|
1815
|
+
UPDATE workspace_session_activity_revisions counter
|
|
1816
|
+
SET revision = counter.revision + 1
|
|
1817
|
+
WHERE counter.workspace_id = workspace_id_value
|
|
1818
|
+
RETURNING counter.revision INTO activity_revision_value;
|
|
1819
|
+
IF activity_revision_value IS NULL THEN
|
|
1820
|
+
RAISE EXCEPTION 'organization membership session activity counter is unavailable'
|
|
1821
|
+
USING ERRCODE = '55000';
|
|
1822
|
+
END IF;
|
|
1823
|
+
UPDATE sessions affected
|
|
1824
|
+
SET activity_revision = activity_revision_value,
|
|
1825
|
+
activity_revision_pending_xid = NULL
|
|
1826
|
+
WHERE affected.account_id = account_id_value
|
|
1827
|
+
AND affected.workspace_id = workspace_id_value
|
|
1828
|
+
AND affected.activity_revision_pending_xid
|
|
1829
|
+
= pg_catalog.pg_current_xact_id()::text::bigint;
|
|
1830
|
+
SET CONSTRAINTS sessions_activity_insert_commit_guard,
|
|
1831
|
+
sessions_activity_update_commit_guard IMMEDIATE;
|
|
1832
|
+
SET CONSTRAINTS sessions_activity_insert_commit_guard,
|
|
1833
|
+
sessions_activity_update_commit_guard DEFERRED;
|
|
1834
|
+
PERFORM pg_catalog.set_config(
|
|
1835
|
+
'opengeni.session_activity_gate_state',
|
|
1836
|
+
COALESCE(previous_activity_gate_state, ''), true
|
|
1837
|
+
);
|
|
1838
|
+
PERFORM pg_catalog.set_config(
|
|
1839
|
+
'opengeni.session_activity_gate_workspace_id',
|
|
1840
|
+
COALESCE(previous_activity_gate_workspace, ''), true
|
|
1841
|
+
);
|
|
1842
|
+
END LOOP;
|
|
1843
|
+
DELETE FROM session_visibility_write_capabilities capability
|
|
1844
|
+
WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
|
|
1845
|
+
AND capability.transaction_id = pg_catalog.pg_current_xact_id()
|
|
1846
|
+
AND capability.capability_id = visibility_capability_id;
|
|
1847
|
+
PERFORM pg_catalog.set_config(
|
|
1848
|
+
'opengeni.session_visibility_write_capability',
|
|
1849
|
+
coalesce(previous_visibility_capability, ''), true
|
|
1850
|
+
);
|
|
1851
|
+
UPDATE organization_memberships SET
|
|
1852
|
+
status = CASE WHEN action_name = 'suspend' THEN 'suspended' ELSE 'revoked' END,
|
|
1853
|
+
authorization_revision = authorization_revision + 1,
|
|
1854
|
+
revoked_at = CASE WHEN action_name = 'offboard' THEN now_value ELSE NULL END,
|
|
1855
|
+
personal_retention_until = CASE
|
|
1856
|
+
WHEN action_name = 'offboard' AND policy.mode = 'delete_after'
|
|
1857
|
+
THEN now_value + pg_catalog.make_interval(days => policy.retention_days)
|
|
1858
|
+
ELSE NULL
|
|
1859
|
+
END,
|
|
1860
|
+
updated_at = now_value
|
|
1861
|
+
WHERE id = target.id RETURNING * INTO target;
|
|
1862
|
+
END IF;
|
|
1863
|
+
result := opengeni_private.organization_membership_row_json(target);
|
|
1864
|
+
END IF;
|
|
1865
|
+
END IF;
|
|
1866
|
+
|
|
1867
|
+
INSERT INTO organization_membership_operation_receipts (
|
|
1868
|
+
account_id, operation_id, action, input_hash, result
|
|
1869
|
+
) VALUES (
|
|
1870
|
+
account_id_value, operation_id_value, action_name, input_hash_value, result
|
|
1871
|
+
);
|
|
1872
|
+
INSERT INTO organization_membership_lifecycle_events (
|
|
1873
|
+
account_id, operation_id, actor_membership_id, target_membership_id, kind,
|
|
1874
|
+
prior_authorization_revision, resulting_authorization_revision, reason
|
|
1875
|
+
) VALUES (
|
|
1876
|
+
account_id_value, operation_id_value, actor.id,
|
|
1877
|
+
CASE WHEN target.id IS NULL THEN NULL ELSE target.id END,
|
|
1878
|
+
action_name, expected_revision,
|
|
1879
|
+
CASE WHEN target.id IS NULL THEN NULL ELSE target.authorization_revision END,
|
|
1880
|
+
reason_value
|
|
1881
|
+
);
|
|
1882
|
+
PERFORM pg_catalog.set_config(
|
|
1883
|
+
'opengeni.workspace_id', coalesce(previous_workspace, ''), true
|
|
1884
|
+
);
|
|
1885
|
+
RETURN result;
|
|
1886
|
+
EXCEPTION WHEN OTHERS THEN
|
|
1887
|
+
PERFORM pg_catalog.set_config(
|
|
1888
|
+
'opengeni.workspace_id', coalesce(previous_workspace, ''), true
|
|
1889
|
+
);
|
|
1890
|
+
PERFORM pg_catalog.set_config(
|
|
1891
|
+
'opengeni.session_visibility_write_capability',
|
|
1892
|
+
coalesce(previous_visibility_capability, ''), true
|
|
1893
|
+
);
|
|
1894
|
+
PERFORM pg_catalog.set_config(
|
|
1895
|
+
'opengeni.session_activity_gate_state',
|
|
1896
|
+
coalesce(previous_activity_gate_state, ''), true
|
|
1897
|
+
);
|
|
1898
|
+
PERFORM pg_catalog.set_config(
|
|
1899
|
+
'opengeni.session_activity_gate_workspace_id',
|
|
1900
|
+
coalesce(previous_activity_gate_workspace, ''), true
|
|
1901
|
+
);
|
|
1902
|
+
RAISE;
|
|
1903
|
+
END
|
|
1904
|
+
$body$;
|
|
1905
|
+
|
|
1906
|
+
CREATE OR REPLACE FUNCTION get_organization_retention_policy(
|
|
1907
|
+
p_account_id uuid,
|
|
1908
|
+
p_actor_subject_id text
|
|
1909
|
+
) RETURNS jsonb
|
|
1910
|
+
LANGUAGE plpgsql
|
|
1911
|
+
SECURITY DEFINER
|
|
1912
|
+
SET search_path FROM CURRENT
|
|
1913
|
+
AS $body$
|
|
1914
|
+
DECLARE actor organization_memberships%ROWTYPE; policy organization_user_retention_policies%ROWTYPE;
|
|
1915
|
+
BEGIN
|
|
1916
|
+
IF p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
|
|
1917
|
+
OR p_actor_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
1918
|
+
THEN RAISE EXCEPTION 'organization authority required' USING ERRCODE = '42501'; END IF;
|
|
1919
|
+
PERFORM pg_catalog.set_config(
|
|
1920
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
1921
|
+
'organization_membership_lifecycle', true
|
|
1922
|
+
);
|
|
1923
|
+
SELECT * INTO actor FROM organization_memberships membership
|
|
1924
|
+
WHERE membership.account_id = p_account_id AND membership.subject_id = p_actor_subject_id;
|
|
1925
|
+
IF NOT FOUND OR actor.status <> 'active' OR actor.role NOT IN ('owner', 'admin') THEN
|
|
1926
|
+
RAISE EXCEPTION 'organization administration required' USING ERRCODE = '42501';
|
|
1927
|
+
END IF;
|
|
1928
|
+
SELECT * INTO policy FROM organization_user_retention_policies candidate
|
|
1929
|
+
WHERE candidate.account_id = p_account_id;
|
|
1930
|
+
IF NOT FOUND THEN
|
|
1931
|
+
RETURN pg_catalog.jsonb_build_object(
|
|
1932
|
+
'organizationId', p_account_id, 'mode', 'retain',
|
|
1933
|
+
'retentionDays', NULL, 'version', 1, 'updatedAt', actor.created_at
|
|
1934
|
+
);
|
|
1935
|
+
END IF;
|
|
1936
|
+
RETURN pg_catalog.jsonb_build_object(
|
|
1937
|
+
'organizationId', policy.account_id, 'mode', policy.mode,
|
|
1938
|
+
'retentionDays', policy.retention_days, 'version', policy.version,
|
|
1939
|
+
'updatedAt', policy.updated_at
|
|
1940
|
+
);
|
|
1941
|
+
END
|
|
1942
|
+
$body$;
|
|
1943
|
+
|
|
1944
|
+
CREATE OR REPLACE FUNCTION opengeni_private.assert_organization_retention_account(
|
|
1945
|
+
p_account_id uuid
|
|
1946
|
+
) RETURNS void
|
|
1947
|
+
LANGUAGE plpgsql
|
|
1948
|
+
SET search_path FROM CURRENT
|
|
1949
|
+
AS $body$
|
|
1950
|
+
BEGIN
|
|
1951
|
+
IF p_account_id IS NULL
|
|
1952
|
+
OR p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
|
|
1953
|
+
THEN
|
|
1954
|
+
RAISE EXCEPTION 'retention organization context mismatch' USING ERRCODE = '42501';
|
|
1955
|
+
END IF;
|
|
1956
|
+
END
|
|
1957
|
+
$body$;
|
|
1958
|
+
|
|
1959
|
+
CREATE OR REPLACE FUNCTION assert_active_managed_human_organization_membership(
|
|
1960
|
+
p_account_id uuid,
|
|
1961
|
+
p_subject_id text
|
|
1962
|
+
) RETURNS bigint
|
|
1963
|
+
LANGUAGE plpgsql
|
|
1964
|
+
SECURITY DEFINER
|
|
1965
|
+
SET search_path FROM CURRENT
|
|
1966
|
+
AS $body$
|
|
1967
|
+
DECLARE
|
|
1968
|
+
membership organization_memberships%ROWTYPE;
|
|
1969
|
+
previous_lifecycle_marker text := pg_catalog.current_setting(
|
|
1970
|
+
'opengeni.organization_tenancy_lifecycle', true
|
|
1971
|
+
);
|
|
1972
|
+
BEGIN
|
|
1973
|
+
IF p_account_id IS NULL
|
|
1974
|
+
OR p_subject_id IS NULL
|
|
1975
|
+
OR p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
|
|
1976
|
+
OR p_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
1977
|
+
THEN
|
|
1978
|
+
RAISE EXCEPTION 'managed-human mutation authority is invalid'
|
|
1979
|
+
USING ERRCODE = '42501';
|
|
1980
|
+
END IF;
|
|
1981
|
+
PERFORM pg_catalog.set_config(
|
|
1982
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
1983
|
+
'organization_membership_lifecycle', true
|
|
1984
|
+
);
|
|
1985
|
+
SELECT * INTO membership
|
|
1986
|
+
FROM organization_memberships candidate
|
|
1987
|
+
WHERE candidate.account_id = p_account_id
|
|
1988
|
+
AND candidate.subject_id = p_subject_id
|
|
1989
|
+
FOR KEY SHARE;
|
|
1990
|
+
IF NOT FOUND THEN
|
|
1991
|
+
PERFORM pg_catalog.set_config(
|
|
1992
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
1993
|
+
COALESCE(previous_lifecycle_marker, ''), true
|
|
1994
|
+
);
|
|
1995
|
+
RETURN NULL;
|
|
1996
|
+
END IF;
|
|
1997
|
+
IF membership.status <> 'active' THEN
|
|
1998
|
+
RAISE EXCEPTION 'active organization membership required for managed-human mutation'
|
|
1999
|
+
USING ERRCODE = '42501';
|
|
2000
|
+
END IF;
|
|
2001
|
+
PERFORM pg_catalog.set_config(
|
|
2002
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
2003
|
+
COALESCE(previous_lifecycle_marker, ''), true
|
|
2004
|
+
);
|
|
2005
|
+
RETURN membership.authorization_revision;
|
|
2006
|
+
EXCEPTION WHEN OTHERS THEN
|
|
2007
|
+
PERFORM pg_catalog.set_config(
|
|
2008
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
2009
|
+
COALESCE(previous_lifecycle_marker, ''), true
|
|
2010
|
+
);
|
|
2011
|
+
RAISE;
|
|
2012
|
+
END
|
|
2013
|
+
$body$;
|
|
2014
|
+
|
|
2015
|
+
CREATE OR REPLACE FUNCTION opengeni_private.organization_retention_file_candidates(
|
|
2016
|
+
p_account_id uuid,
|
|
2017
|
+
p_membership_id uuid
|
|
2018
|
+
) RETURNS TABLE(file_id uuid, object_bucket text, object_key text)
|
|
2019
|
+
LANGUAGE sql
|
|
2020
|
+
SECURITY DEFINER
|
|
2021
|
+
SET search_path FROM CURRENT
|
|
2022
|
+
AS $body$
|
|
2023
|
+
WITH authority_context AS MATERIALIZED (
|
|
2024
|
+
SELECT opengeni_private.assert_organization_retention_account(p_account_id)
|
|
2025
|
+
), member AS (
|
|
2026
|
+
SELECT membership.personal_workspace_id
|
|
2027
|
+
FROM organization_memberships membership
|
|
2028
|
+
CROSS JOIN authority_context
|
|
2029
|
+
WHERE membership.account_id = p_account_id
|
|
2030
|
+
AND membership.id = p_membership_id
|
|
2031
|
+
), owned_document_files AS (
|
|
2032
|
+
SELECT document.file_id
|
|
2033
|
+
FROM documents document
|
|
2034
|
+
JOIN organization_user_resource_authorities authority
|
|
2035
|
+
ON authority.id = document.authority_id
|
|
2036
|
+
AND authority.account_id = document.account_id
|
|
2037
|
+
AND authority.organization_membership_id = document.owner_organization_membership_id
|
|
2038
|
+
AND authority.resource_kind = 'document'
|
|
2039
|
+
AND authority.resource_id = document.id
|
|
2040
|
+
WHERE authority.account_id = p_account_id
|
|
2041
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2042
|
+
)
|
|
2043
|
+
SELECT file.id, file.bucket, file.object_key
|
|
2044
|
+
FROM files file
|
|
2045
|
+
CROSS JOIN member
|
|
2046
|
+
WHERE file.account_id = p_account_id
|
|
2047
|
+
AND (
|
|
2048
|
+
file.workspace_id = member.personal_workspace_id
|
|
2049
|
+
OR file.id IN (SELECT owned.file_id FROM owned_document_files owned)
|
|
2050
|
+
)
|
|
2051
|
+
AND NOT EXISTS (
|
|
2052
|
+
SELECT 1
|
|
2053
|
+
FROM documents other
|
|
2054
|
+
LEFT JOIN organization_user_resource_authorities other_authority
|
|
2055
|
+
ON other_authority.id = other.authority_id
|
|
2056
|
+
AND other_authority.account_id = other.account_id
|
|
2057
|
+
AND other_authority.organization_membership_id = other.owner_organization_membership_id
|
|
2058
|
+
AND other_authority.resource_kind = 'document'
|
|
2059
|
+
AND other_authority.resource_id = other.id
|
|
2060
|
+
WHERE other.file_id = file.id
|
|
2061
|
+
AND (
|
|
2062
|
+
other_authority.id IS NULL
|
|
2063
|
+
OR other_authority.account_id <> p_account_id
|
|
2064
|
+
OR other_authority.organization_membership_id <> p_membership_id
|
|
2065
|
+
)
|
|
2066
|
+
)
|
|
2067
|
+
ORDER BY file.id
|
|
2068
|
+
$body$;
|
|
2069
|
+
|
|
2070
|
+
-- Closed inventory of every @opengeni/storage key whose owning metadata is
|
|
2071
|
+
-- removed with a personal workspace. Provider-native sandbox checkpoint
|
|
2072
|
+
-- artifacts deliberately do not appear here: their global GC rows survive
|
|
2073
|
+
-- workspace deletion and remain the sole provider-bound deletion authority.
|
|
2074
|
+
CREATE OR REPLACE FUNCTION opengeni_private.organization_retention_object_candidates(
|
|
2075
|
+
p_account_id uuid,
|
|
2076
|
+
p_membership_id uuid
|
|
2077
|
+
) RETURNS TABLE(object_kind text, source_id text, object_bucket text, object_key text)
|
|
2078
|
+
LANGUAGE sql
|
|
2079
|
+
SECURITY DEFINER
|
|
2080
|
+
SET search_path FROM CURRENT
|
|
2081
|
+
AS $body$
|
|
2082
|
+
WITH authority_context AS MATERIALIZED (
|
|
2083
|
+
SELECT opengeni_private.assert_organization_retention_account(p_account_id)
|
|
2084
|
+
), member AS (
|
|
2085
|
+
SELECT membership.personal_workspace_id
|
|
2086
|
+
FROM organization_memberships membership
|
|
2087
|
+
CROSS JOIN authority_context
|
|
2088
|
+
WHERE membership.account_id = p_account_id
|
|
2089
|
+
AND membership.id = p_membership_id
|
|
2090
|
+
)
|
|
2091
|
+
SELECT 'file'::text, candidate.file_id::text, candidate.object_bucket, candidate.object_key
|
|
2092
|
+
FROM opengeni_private.organization_retention_file_candidates(
|
|
2093
|
+
p_account_id, p_membership_id
|
|
2094
|
+
) candidate
|
|
2095
|
+
UNION ALL
|
|
2096
|
+
SELECT 'session_recording', recording.id::text, NULL::text, recording.storage_key
|
|
2097
|
+
FROM session_recordings recording CROSS JOIN member
|
|
2098
|
+
WHERE recording.account_id = p_account_id
|
|
2099
|
+
AND recording.workspace_id = member.personal_workspace_id
|
|
2100
|
+
AND recording.storage_key IS NOT NULL
|
|
2101
|
+
UNION ALL
|
|
2102
|
+
SELECT 'browser_state_artifact', artifact.id::text, NULL::text, artifact.object_key
|
|
2103
|
+
FROM browser_state_artifacts artifact CROSS JOIN member
|
|
2104
|
+
WHERE artifact.account_id = p_account_id
|
|
2105
|
+
AND artifact.workspace_id = member.personal_workspace_id
|
|
2106
|
+
AND artifact.state <> 'deleted'
|
|
2107
|
+
UNION ALL
|
|
2108
|
+
SELECT 'browser_state_upload', upload.id::text, NULL::text, upload.object_key
|
|
2109
|
+
FROM browser_state_uploads upload CROSS JOIN member
|
|
2110
|
+
WHERE upload.account_id = p_account_id
|
|
2111
|
+
AND upload.workspace_id = member.personal_workspace_id
|
|
2112
|
+
AND upload.state <> 'deleted'
|
|
2113
|
+
AND (
|
|
2114
|
+
upload.state <> 'committed'
|
|
2115
|
+
OR NOT EXISTS (
|
|
2116
|
+
SELECT 1
|
|
2117
|
+
FROM browser_state_artifacts committed
|
|
2118
|
+
WHERE committed.account_id = upload.account_id
|
|
2119
|
+
AND committed.workspace_id = upload.workspace_id
|
|
2120
|
+
AND committed.id = upload.committed_artifact_id
|
|
2121
|
+
AND committed.object_key = upload.object_key
|
|
2122
|
+
AND committed.state <> 'deleted'
|
|
2123
|
+
)
|
|
2124
|
+
)
|
|
2125
|
+
UNION ALL
|
|
2126
|
+
SELECT 'transcription_recording_object',
|
|
2127
|
+
recording_object.recording_id::text || ':' || encode(
|
|
2128
|
+
digest(recording_object.object_key, 'sha256'), 'hex'
|
|
2129
|
+
),
|
|
2130
|
+
NULL::text,
|
|
2131
|
+
recording_object.object_key
|
|
2132
|
+
FROM transcription_recording_objects recording_object CROSS JOIN member
|
|
2133
|
+
WHERE recording_object.account_id = p_account_id
|
|
2134
|
+
AND recording_object.workspace_id = member.personal_workspace_id
|
|
2135
|
+
AND recording_object.cleaned_at IS NULL
|
|
2136
|
+
UNION ALL
|
|
2137
|
+
SELECT 'video_staging_reference',
|
|
2138
|
+
reference.operation_id::text || ':' || reference.ordinal::text,
|
|
2139
|
+
NULL::text,
|
|
2140
|
+
reference.staging_object_key
|
|
2141
|
+
FROM video_generation_references reference CROSS JOIN member
|
|
2142
|
+
WHERE reference.account_id = p_account_id
|
|
2143
|
+
AND reference.workspace_id = member.personal_workspace_id
|
|
2144
|
+
AND reference.staging_object_key IS NOT NULL
|
|
2145
|
+
AND reference.cleaned_at IS NULL
|
|
2146
|
+
UNION ALL
|
|
2147
|
+
SELECT 'workspace_artifact_version', version.id::text, NULL::text, version.content_key
|
|
2148
|
+
FROM workspace_artifact_versions version CROSS JOIN member
|
|
2149
|
+
WHERE version.account_id = p_account_id
|
|
2150
|
+
AND version.workspace_id = member.personal_workspace_id
|
|
2151
|
+
UNION ALL
|
|
2152
|
+
SELECT 'editable_artifact_blob',
|
|
2153
|
+
blob.artifact_id || ':' || blob.id,
|
|
2154
|
+
NULL::text,
|
|
2155
|
+
blob.object_reference
|
|
2156
|
+
FROM editable_artifact_blob_refs blob CROSS JOIN member
|
|
2157
|
+
WHERE blob.account_id = p_account_id
|
|
2158
|
+
AND blob.workspace_id = member.personal_workspace_id
|
|
2159
|
+
UNION ALL
|
|
2160
|
+
SELECT 'workspace_capture_manifest', capture.id::text, NULL::text, capture.manifest_key
|
|
2161
|
+
FROM workspace_captures capture CROSS JOIN member
|
|
2162
|
+
WHERE capture.account_id = p_account_id
|
|
2163
|
+
AND capture.workspace_id = member.personal_workspace_id
|
|
2164
|
+
AND capture.manifest_key IS NOT NULL
|
|
2165
|
+
UNION ALL
|
|
2166
|
+
SELECT 'workspace_capture_tree_index', capture.id::text, NULL::text, capture.tree_index_key
|
|
2167
|
+
FROM workspace_captures capture CROSS JOIN member
|
|
2168
|
+
WHERE capture.account_id = p_account_id
|
|
2169
|
+
AND capture.workspace_id = member.personal_workspace_id
|
|
2170
|
+
AND capture.tree_index_key IS NOT NULL
|
|
2171
|
+
UNION ALL
|
|
2172
|
+
SELECT 'workspace_capture_blob',
|
|
2173
|
+
capture.id::text || ':' || blob.ordinality::text,
|
|
2174
|
+
NULL::text,
|
|
2175
|
+
blob.value #>> '{}'
|
|
2176
|
+
FROM workspace_captures capture CROSS JOIN member
|
|
2177
|
+
CROSS JOIN LATERAL jsonb_array_elements(
|
|
2178
|
+
CASE WHEN jsonb_typeof(capture.blob_keys) = 'array'
|
|
2179
|
+
THEN capture.blob_keys ELSE '[]'::jsonb END
|
|
2180
|
+
) WITH ORDINALITY AS blob(value, ordinality)
|
|
2181
|
+
WHERE capture.account_id = p_account_id
|
|
2182
|
+
AND capture.workspace_id = member.personal_workspace_id
|
|
2183
|
+
AND jsonb_typeof(blob.value) = 'string'
|
|
2184
|
+
ORDER BY 1, 2
|
|
2185
|
+
$body$;
|
|
2186
|
+
|
|
2187
|
+
CREATE OR REPLACE FUNCTION preview_organization_retention_deletions(
|
|
2188
|
+
p_account_id uuid,
|
|
2189
|
+
p_limit integer DEFAULT 25
|
|
2190
|
+
) RETURNS jsonb
|
|
2191
|
+
LANGUAGE plpgsql
|
|
2192
|
+
SECURITY DEFINER
|
|
2193
|
+
SET search_path FROM CURRENT
|
|
2194
|
+
AS $body$
|
|
2195
|
+
DECLARE result jsonb;
|
|
2196
|
+
BEGIN
|
|
2197
|
+
IF p_account_id IS NULL OR p_limit IS NULL OR p_limit NOT BETWEEN 1 AND 100 THEN
|
|
2198
|
+
RAISE EXCEPTION 'invalid retention preview input' USING ERRCODE = '22023';
|
|
2199
|
+
END IF;
|
|
2200
|
+
PERFORM opengeni_private.assert_organization_retention_account(p_account_id);
|
|
2201
|
+
PERFORM pg_catalog.set_config(
|
|
2202
|
+
'opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true
|
|
2203
|
+
);
|
|
2204
|
+
SELECT COALESCE(pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object(
|
|
2205
|
+
'membershipId', candidate.id,
|
|
2206
|
+
'retentionUntil', candidate.personal_retention_until,
|
|
2207
|
+
'personalWorkspaceId', candidate.personal_workspace_id,
|
|
2208
|
+
'resourceCount', candidate.resource_count,
|
|
2209
|
+
'objectCount', candidate.object_count
|
|
2210
|
+
) ORDER BY candidate.personal_retention_until, candidate.id), '[]'::jsonb)
|
|
2211
|
+
INTO result
|
|
2212
|
+
FROM (
|
|
2213
|
+
SELECT membership.id, membership.personal_retention_until,
|
|
2214
|
+
membership.personal_workspace_id,
|
|
2215
|
+
(SELECT count(*)::integer FROM organization_user_resource_authorities authority
|
|
2216
|
+
WHERE authority.account_id = p_account_id
|
|
2217
|
+
AND authority.organization_membership_id = membership.id) AS resource_count,
|
|
2218
|
+
CASE WHEN deletion.database_finalized_at IS NOT NULL THEN
|
|
2219
|
+
(SELECT count(*)::integer
|
|
2220
|
+
FROM organization_user_retention_object_obligations receipt
|
|
2221
|
+
WHERE receipt.account_id = p_account_id
|
|
2222
|
+
AND receipt.membership_id = membership.id
|
|
2223
|
+
AND NOT EXISTS (
|
|
2224
|
+
SELECT 1
|
|
2225
|
+
FROM organization_user_retention_object_deletion_receipts deleted
|
|
2226
|
+
WHERE deleted.account_id = receipt.account_id
|
|
2227
|
+
AND deleted.membership_id = receipt.membership_id
|
|
2228
|
+
AND deleted.object_kind = receipt.object_kind
|
|
2229
|
+
AND deleted.source_id = receipt.source_id
|
|
2230
|
+
AND deleted.object_bucket = receipt.object_bucket
|
|
2231
|
+
))
|
|
2232
|
+
ELSE
|
|
2233
|
+
(SELECT count(*)::integer
|
|
2234
|
+
FROM opengeni_private.organization_retention_object_candidates(
|
|
2235
|
+
p_account_id, membership.id
|
|
2236
|
+
))
|
|
2237
|
+
END AS object_count
|
|
2238
|
+
FROM organization_memberships membership
|
|
2239
|
+
LEFT JOIN organization_user_retention_deletions deletion
|
|
2240
|
+
ON deletion.account_id = membership.account_id
|
|
2241
|
+
AND deletion.membership_id = membership.id
|
|
2242
|
+
WHERE membership.account_id = p_account_id
|
|
2243
|
+
AND membership.status = 'revoked'
|
|
2244
|
+
AND membership.personal_retention_until IS NOT NULL
|
|
2245
|
+
AND membership.personal_retention_until <= clock_timestamp()
|
|
2246
|
+
AND (deletion.state IS NULL OR deletion.state <> 'completed')
|
|
2247
|
+
ORDER BY membership.personal_retention_until, membership.id
|
|
2248
|
+
LIMIT p_limit
|
|
2249
|
+
) candidate;
|
|
2250
|
+
RETURN result;
|
|
2251
|
+
END
|
|
2252
|
+
$body$;
|
|
2253
|
+
|
|
2254
|
+
CREATE OR REPLACE FUNCTION claim_organization_retention_deletion(
|
|
2255
|
+
p_account_id uuid,
|
|
2256
|
+
p_operation_id uuid,
|
|
2257
|
+
p_excluded_membership_ids uuid[] DEFAULT ARRAY[]::uuid[]
|
|
2258
|
+
) RETURNS jsonb
|
|
2259
|
+
LANGUAGE plpgsql
|
|
2260
|
+
SECURITY DEFINER
|
|
2261
|
+
SET search_path FROM CURRENT
|
|
2262
|
+
AS $body$
|
|
2263
|
+
DECLARE
|
|
2264
|
+
member organization_memberships%ROWTYPE;
|
|
2265
|
+
deletion organization_user_retention_deletions%ROWTYPE;
|
|
2266
|
+
prior_event organization_user_retention_deletion_events%ROWTYPE;
|
|
2267
|
+
result_value jsonb;
|
|
2268
|
+
object_count integer;
|
|
2269
|
+
deleted_object_count integer;
|
|
2270
|
+
BEGIN
|
|
2271
|
+
IF p_account_id IS NULL OR p_operation_id IS NULL
|
|
2272
|
+
OR p_excluded_membership_ids IS NULL
|
|
2273
|
+
THEN
|
|
2274
|
+
RAISE EXCEPTION 'invalid retention claim input' USING ERRCODE = '22023';
|
|
2275
|
+
END IF;
|
|
2276
|
+
IF coalesce(pg_catalog.array_length(p_excluded_membership_ids, 1), 0) > 100 THEN
|
|
2277
|
+
RAISE EXCEPTION 'too many excluded retention memberships' USING ERRCODE = '22023';
|
|
2278
|
+
END IF;
|
|
2279
|
+
PERFORM opengeni_private.assert_organization_retention_account(p_account_id);
|
|
2280
|
+
PERFORM pg_catalog.set_config(
|
|
2281
|
+
'opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true
|
|
2282
|
+
);
|
|
2283
|
+
PERFORM pg_catalog.pg_advisory_xact_lock(
|
|
2284
|
+
pg_catalog.hashtextextended('organization-retention:' || p_operation_id::text, 0)
|
|
2285
|
+
);
|
|
2286
|
+
SELECT * INTO prior_event
|
|
2287
|
+
FROM organization_user_retention_deletion_events event
|
|
2288
|
+
WHERE event.account_id = p_account_id AND event.operation_id = p_operation_id
|
|
2289
|
+
ORDER BY CASE event.kind WHEN 'failed' THEN 0 WHEN 'claimed' THEN 1 ELSE 2 END
|
|
2290
|
+
LIMIT 1;
|
|
2291
|
+
IF FOUND THEN
|
|
2292
|
+
IF prior_event.membership_id = ANY(p_excluded_membership_ids) THEN
|
|
2293
|
+
RAISE EXCEPTION 'retention operation replay conflicts with excluded membership'
|
|
2294
|
+
USING ERRCODE = '23505';
|
|
2295
|
+
END IF;
|
|
2296
|
+
IF prior_event.kind = 'failed' THEN
|
|
2297
|
+
RAISE EXCEPTION 'retention operation previously failed: %', prior_event.reason_code
|
|
2298
|
+
USING ERRCODE = '55000';
|
|
2299
|
+
END IF;
|
|
2300
|
+
RETURN prior_event.result;
|
|
2301
|
+
END IF;
|
|
2302
|
+
IF EXISTS (
|
|
2303
|
+
SELECT 1 FROM organization_user_retention_deletion_events event
|
|
2304
|
+
WHERE event.operation_id = p_operation_id AND event.account_id <> p_account_id
|
|
2305
|
+
) THEN
|
|
2306
|
+
RAISE EXCEPTION 'retention operation belongs to another organization'
|
|
2307
|
+
USING ERRCODE = '42501';
|
|
2308
|
+
END IF;
|
|
2309
|
+
|
|
2310
|
+
SELECT membership.* INTO member
|
|
2311
|
+
FROM organization_memberships membership
|
|
2312
|
+
LEFT JOIN organization_user_retention_deletions current_deletion
|
|
2313
|
+
ON current_deletion.account_id = membership.account_id
|
|
2314
|
+
AND current_deletion.membership_id = membership.id
|
|
2315
|
+
WHERE membership.account_id = p_account_id
|
|
2316
|
+
AND NOT (membership.id = ANY(p_excluded_membership_ids))
|
|
2317
|
+
AND membership.status = 'revoked'
|
|
2318
|
+
AND membership.personal_retention_until IS NOT NULL
|
|
2319
|
+
AND membership.personal_retention_until <= clock_timestamp()
|
|
2320
|
+
AND (
|
|
2321
|
+
current_deletion.state IS NULL
|
|
2322
|
+
OR current_deletion.state = 'failed'
|
|
2323
|
+
OR (current_deletion.state = 'claimed'
|
|
2324
|
+
AND current_deletion.claim_expires_at <= clock_timestamp())
|
|
2325
|
+
)
|
|
2326
|
+
ORDER BY membership.personal_retention_until, membership.id
|
|
2327
|
+
FOR UPDATE OF membership SKIP LOCKED
|
|
2328
|
+
LIMIT 1;
|
|
2329
|
+
IF NOT FOUND THEN RETURN NULL; END IF;
|
|
2330
|
+
|
|
2331
|
+
INSERT INTO organization_user_retention_deletions (
|
|
2332
|
+
account_id, membership_id, retention_until, state, claim_operation_id,
|
|
2333
|
+
claim_expires_at, attempt_count, updated_at
|
|
2334
|
+
) VALUES (
|
|
2335
|
+
p_account_id, member.id, member.personal_retention_until, 'claimed',
|
|
2336
|
+
p_operation_id, clock_timestamp() + interval '15 minutes', 1, clock_timestamp()
|
|
2337
|
+
)
|
|
2338
|
+
ON CONFLICT (account_id, membership_id) DO UPDATE SET
|
|
2339
|
+
retention_until = EXCLUDED.retention_until,
|
|
2340
|
+
state = 'claimed',
|
|
2341
|
+
claim_operation_id = EXCLUDED.claim_operation_id,
|
|
2342
|
+
claim_expires_at = EXCLUDED.claim_expires_at,
|
|
2343
|
+
attempt_count = organization_user_retention_deletions.attempt_count + 1,
|
|
2344
|
+
result = NULL,
|
|
2345
|
+
completed_at = NULL,
|
|
2346
|
+
updated_at = clock_timestamp()
|
|
2347
|
+
RETURNING * INTO deletion;
|
|
2348
|
+
|
|
2349
|
+
IF deletion.database_finalized_at IS NULL THEN
|
|
2350
|
+
SELECT count(*)::integer INTO object_count
|
|
2351
|
+
FROM opengeni_private.organization_retention_object_candidates(p_account_id, member.id);
|
|
2352
|
+
ELSE
|
|
2353
|
+
SELECT count(*)::integer INTO object_count
|
|
2354
|
+
FROM organization_user_retention_object_obligations receipt
|
|
2355
|
+
WHERE receipt.account_id = p_account_id AND receipt.membership_id = member.id;
|
|
2356
|
+
END IF;
|
|
2357
|
+
SELECT count(*)::integer INTO deleted_object_count
|
|
2358
|
+
FROM organization_user_retention_object_deletion_receipts receipt
|
|
2359
|
+
WHERE receipt.account_id = p_account_id AND receipt.membership_id = member.id;
|
|
2360
|
+
result_value := pg_catalog.jsonb_build_object(
|
|
2361
|
+
'organizationId', p_account_id,
|
|
2362
|
+
'membershipId', member.id,
|
|
2363
|
+
'operationId', p_operation_id,
|
|
2364
|
+
'retentionUntil', member.personal_retention_until,
|
|
2365
|
+
'claimExpiresAt', deletion.claim_expires_at,
|
|
2366
|
+
'personalWorkspaceId', member.personal_workspace_id,
|
|
2367
|
+
'objectCount', object_count,
|
|
2368
|
+
'deletedObjectCount', deleted_object_count
|
|
2369
|
+
);
|
|
2370
|
+
INSERT INTO organization_user_retention_deletion_events (
|
|
2371
|
+
account_id, membership_id, operation_id, kind, result
|
|
2372
|
+
) VALUES (p_account_id, member.id, p_operation_id, 'claimed', result_value);
|
|
2373
|
+
RETURN result_value;
|
|
2374
|
+
END
|
|
2375
|
+
$body$;
|
|
2376
|
+
|
|
2377
|
+
CREATE OR REPLACE FUNCTION list_organization_retention_deletion_objects(
|
|
2378
|
+
p_account_id uuid,
|
|
2379
|
+
p_membership_id uuid,
|
|
2380
|
+
p_operation_id uuid,
|
|
2381
|
+
p_object_bucket text,
|
|
2382
|
+
p_limit integer DEFAULT 100
|
|
2383
|
+
) RETURNS jsonb
|
|
2384
|
+
LANGUAGE plpgsql
|
|
2385
|
+
SECURITY DEFINER
|
|
2386
|
+
SET search_path FROM CURRENT
|
|
2387
|
+
AS $body$
|
|
2388
|
+
DECLARE result jsonb;
|
|
2389
|
+
BEGIN
|
|
2390
|
+
IF nullif(p_object_bucket, '') IS NULL
|
|
2391
|
+
OR octet_length(convert_to(p_object_bucket, 'UTF8')) > 1024
|
|
2392
|
+
THEN
|
|
2393
|
+
RAISE EXCEPTION 'invalid retention object bucket' USING ERRCODE = '22023';
|
|
2394
|
+
END IF;
|
|
2395
|
+
IF p_limit IS NULL OR p_limit NOT BETWEEN 1 AND 100 THEN
|
|
2396
|
+
RAISE EXCEPTION 'invalid retention object page size' USING ERRCODE = '22023';
|
|
2397
|
+
END IF;
|
|
2398
|
+
PERFORM opengeni_private.assert_organization_retention_account(p_account_id);
|
|
2399
|
+
PERFORM pg_catalog.set_config(
|
|
2400
|
+
'opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true
|
|
2401
|
+
);
|
|
2402
|
+
UPDATE organization_user_retention_deletions deletion
|
|
2403
|
+
SET claim_expires_at = clock_timestamp() + interval '15 minutes', updated_at = clock_timestamp()
|
|
2404
|
+
WHERE deletion.account_id = p_account_id
|
|
2405
|
+
AND deletion.membership_id = p_membership_id
|
|
2406
|
+
AND deletion.claim_operation_id = p_operation_id
|
|
2407
|
+
AND deletion.state = 'claimed'
|
|
2408
|
+
AND deletion.claim_expires_at > clock_timestamp()
|
|
2409
|
+
AND deletion.database_finalized_at IS NOT NULL
|
|
2410
|
+
AND deletion.database_result ->> 'objectBucket' = p_object_bucket;
|
|
2411
|
+
IF NOT FOUND THEN
|
|
2412
|
+
RAISE EXCEPTION 'retention deletion claim is stale' USING ERRCODE = '40001';
|
|
2413
|
+
END IF;
|
|
2414
|
+
SELECT COALESCE(pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object(
|
|
2415
|
+
'objectKind', candidate.object_kind,
|
|
2416
|
+
'sourceId', candidate.source_id,
|
|
2417
|
+
'objectBucket', candidate.object_bucket,
|
|
2418
|
+
'objectKey', candidate.object_key
|
|
2419
|
+
) ORDER BY candidate.object_kind, candidate.source_id), '[]'::jsonb)
|
|
2420
|
+
INTO result
|
|
2421
|
+
FROM (
|
|
2422
|
+
SELECT receipt.object_kind, receipt.source_id, receipt.object_bucket, receipt.object_key
|
|
2423
|
+
FROM organization_user_retention_object_obligations receipt
|
|
2424
|
+
WHERE receipt.account_id = p_account_id
|
|
2425
|
+
AND receipt.membership_id = p_membership_id
|
|
2426
|
+
AND NOT EXISTS (
|
|
2427
|
+
SELECT 1
|
|
2428
|
+
FROM organization_user_retention_object_deletion_receipts deleted
|
|
2429
|
+
WHERE deleted.account_id = receipt.account_id
|
|
2430
|
+
AND deleted.membership_id = receipt.membership_id
|
|
2431
|
+
AND deleted.object_kind = receipt.object_kind
|
|
2432
|
+
AND deleted.source_id = receipt.source_id
|
|
2433
|
+
AND deleted.object_bucket = receipt.object_bucket
|
|
2434
|
+
)
|
|
2435
|
+
ORDER BY receipt.object_kind, receipt.source_id
|
|
2436
|
+
LIMIT p_limit
|
|
2437
|
+
) candidate;
|
|
2438
|
+
RETURN result;
|
|
2439
|
+
END
|
|
2440
|
+
$body$;
|
|
2441
|
+
|
|
2442
|
+
CREATE OR REPLACE FUNCTION record_organization_retention_object_deleted(
|
|
2443
|
+
p_account_id uuid,
|
|
2444
|
+
p_membership_id uuid,
|
|
2445
|
+
p_operation_id uuid,
|
|
2446
|
+
p_object_kind text,
|
|
2447
|
+
p_source_id text,
|
|
2448
|
+
p_object_bucket text,
|
|
2449
|
+
p_object_key text
|
|
2450
|
+
) RETURNS boolean
|
|
2451
|
+
LANGUAGE plpgsql
|
|
2452
|
+
SECURITY DEFINER
|
|
2453
|
+
SET search_path FROM CURRENT
|
|
2454
|
+
AS $body$
|
|
2455
|
+
DECLARE obligation organization_user_retention_object_obligations%ROWTYPE;
|
|
2456
|
+
BEGIN
|
|
2457
|
+
IF p_object_kind NOT IN (
|
|
2458
|
+
'file', 'session_recording', 'browser_state_artifact', 'browser_state_upload',
|
|
2459
|
+
'transcription_recording_object', 'video_staging_reference',
|
|
2460
|
+
'workspace_artifact_version', 'editable_artifact_blob',
|
|
2461
|
+
'workspace_capture_manifest', 'workspace_capture_tree_index',
|
|
2462
|
+
'workspace_capture_blob'
|
|
2463
|
+
)
|
|
2464
|
+
OR nullif(p_source_id, '') IS NULL
|
|
2465
|
+
OR octet_length(convert_to(p_source_id, 'UTF8')) > 2048
|
|
2466
|
+
OR nullif(p_object_bucket, '') IS NULL
|
|
2467
|
+
OR octet_length(convert_to(p_object_bucket, 'UTF8')) > 1024
|
|
2468
|
+
OR nullif(p_object_key, '') IS NULL
|
|
2469
|
+
OR octet_length(convert_to(p_object_key, 'UTF8')) > 4096
|
|
2470
|
+
THEN RAISE EXCEPTION 'invalid retention object key' USING ERRCODE = '22023'; END IF;
|
|
2471
|
+
PERFORM opengeni_private.assert_organization_retention_account(p_account_id);
|
|
2472
|
+
PERFORM pg_catalog.set_config(
|
|
2473
|
+
'opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true
|
|
2474
|
+
);
|
|
2475
|
+
UPDATE organization_user_retention_deletions deletion
|
|
2476
|
+
SET claim_expires_at = clock_timestamp() + interval '15 minutes', updated_at = clock_timestamp()
|
|
2477
|
+
WHERE deletion.account_id = p_account_id
|
|
2478
|
+
AND deletion.membership_id = p_membership_id
|
|
2479
|
+
AND deletion.claim_operation_id = p_operation_id
|
|
2480
|
+
AND deletion.state = 'claimed'
|
|
2481
|
+
AND deletion.claim_expires_at > clock_timestamp()
|
|
2482
|
+
AND deletion.database_finalized_at IS NOT NULL
|
|
2483
|
+
AND deletion.database_result ->> 'objectBucket' = p_object_bucket;
|
|
2484
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'retention deletion claim is stale' USING ERRCODE = '40001'; END IF;
|
|
2485
|
+
SELECT * INTO obligation
|
|
2486
|
+
FROM organization_user_retention_object_obligations receipt
|
|
2487
|
+
WHERE receipt.account_id = p_account_id
|
|
2488
|
+
AND receipt.membership_id = p_membership_id
|
|
2489
|
+
AND receipt.object_kind = p_object_kind
|
|
2490
|
+
AND receipt.source_id = p_source_id
|
|
2491
|
+
AND receipt.object_bucket = p_object_bucket
|
|
2492
|
+
FOR UPDATE;
|
|
2493
|
+
IF NOT FOUND OR obligation.object_key IS DISTINCT FROM p_object_key
|
|
2494
|
+
OR obligation.object_key_hash <> encode(digest(p_object_key, 'sha256'), 'hex')
|
|
2495
|
+
THEN
|
|
2496
|
+
RAISE EXCEPTION 'retention object is outside the exact claim' USING ERRCODE = '42501';
|
|
2497
|
+
END IF;
|
|
2498
|
+
INSERT INTO organization_user_retention_object_deletion_receipts (
|
|
2499
|
+
account_id, membership_id, object_kind, source_id, object_bucket, operation_id
|
|
2500
|
+
) VALUES (
|
|
2501
|
+
p_account_id, p_membership_id, p_object_kind, p_source_id, p_object_bucket, p_operation_id
|
|
2502
|
+
) ON CONFLICT (account_id, membership_id, object_kind, source_id, object_bucket) DO NOTHING;
|
|
2503
|
+
RETURN FOUND;
|
|
2504
|
+
END
|
|
2505
|
+
$body$;
|
|
2506
|
+
|
|
2507
|
+
CREATE OR REPLACE FUNCTION fail_organization_retention_deletion(
|
|
2508
|
+
p_account_id uuid,
|
|
2509
|
+
p_membership_id uuid,
|
|
2510
|
+
p_operation_id uuid,
|
|
2511
|
+
p_reason_code text
|
|
2512
|
+
) RETURNS boolean
|
|
2513
|
+
LANGUAGE plpgsql
|
|
2514
|
+
SECURITY DEFINER
|
|
2515
|
+
SET search_path FROM CURRENT
|
|
2516
|
+
AS $body$
|
|
2517
|
+
DECLARE
|
|
2518
|
+
prior_event organization_user_retention_deletion_events%ROWTYPE;
|
|
2519
|
+
event_result jsonb;
|
|
2520
|
+
BEGIN
|
|
2521
|
+
IF p_reason_code IS NULL OR p_reason_code !~ '^[a-z0-9_]{1,64}$' THEN
|
|
2522
|
+
RAISE EXCEPTION 'invalid retention failure code' USING ERRCODE = '22023';
|
|
2523
|
+
END IF;
|
|
2524
|
+
PERFORM opengeni_private.assert_organization_retention_account(p_account_id);
|
|
2525
|
+
PERFORM pg_catalog.set_config(
|
|
2526
|
+
'opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true
|
|
2527
|
+
);
|
|
2528
|
+
PERFORM pg_catalog.pg_advisory_xact_lock(
|
|
2529
|
+
pg_catalog.hashtextextended('organization-retention:' || p_operation_id::text, 0)
|
|
2530
|
+
);
|
|
2531
|
+
SELECT * INTO prior_event
|
|
2532
|
+
FROM organization_user_retention_deletion_events event
|
|
2533
|
+
WHERE event.account_id = p_account_id
|
|
2534
|
+
AND event.operation_id = p_operation_id
|
|
2535
|
+
AND event.kind = 'failed';
|
|
2536
|
+
IF FOUND THEN
|
|
2537
|
+
IF prior_event.membership_id IS DISTINCT FROM p_membership_id
|
|
2538
|
+
OR prior_event.reason_code IS DISTINCT FROM p_reason_code
|
|
2539
|
+
THEN
|
|
2540
|
+
RAISE EXCEPTION 'retention failure operation id was reused with different input'
|
|
2541
|
+
USING ERRCODE = '23505';
|
|
2542
|
+
END IF;
|
|
2543
|
+
RETURN true;
|
|
2544
|
+
END IF;
|
|
2545
|
+
UPDATE organization_user_retention_deletions deletion
|
|
2546
|
+
SET state = 'failed', claim_expires_at = clock_timestamp(), updated_at = clock_timestamp()
|
|
2547
|
+
WHERE deletion.account_id = p_account_id
|
|
2548
|
+
AND deletion.membership_id = p_membership_id
|
|
2549
|
+
AND deletion.claim_operation_id = p_operation_id
|
|
2550
|
+
AND deletion.state = 'claimed';
|
|
2551
|
+
IF NOT FOUND THEN
|
|
2552
|
+
RETURN false;
|
|
2553
|
+
END IF;
|
|
2554
|
+
event_result := pg_catalog.jsonb_build_object(
|
|
2555
|
+
'organizationId', p_account_id, 'membershipId', p_membership_id,
|
|
2556
|
+
'operationId', p_operation_id, 'outcome', 'failed', 'reasonCode', p_reason_code
|
|
2557
|
+
);
|
|
2558
|
+
INSERT INTO organization_user_retention_deletion_events (
|
|
2559
|
+
account_id, membership_id, operation_id, kind, reason_code, result
|
|
2560
|
+
) VALUES (
|
|
2561
|
+
p_account_id, p_membership_id, p_operation_id, 'failed', p_reason_code, event_result
|
|
2562
|
+
) ON CONFLICT (account_id, operation_id, kind) DO NOTHING;
|
|
2563
|
+
SELECT * INTO prior_event
|
|
2564
|
+
FROM organization_user_retention_deletion_events event
|
|
2565
|
+
WHERE event.account_id = p_account_id
|
|
2566
|
+
AND event.operation_id = p_operation_id
|
|
2567
|
+
AND event.kind = 'failed';
|
|
2568
|
+
IF NOT FOUND
|
|
2569
|
+
OR prior_event.membership_id IS DISTINCT FROM p_membership_id
|
|
2570
|
+
OR prior_event.reason_code IS DISTINCT FROM p_reason_code
|
|
2571
|
+
THEN
|
|
2572
|
+
RAISE EXCEPTION 'retention failure operation id was reused with different input'
|
|
2573
|
+
USING ERRCODE = '23505';
|
|
2574
|
+
END IF;
|
|
2575
|
+
RETURN true;
|
|
2576
|
+
END
|
|
2577
|
+
$body$;
|
|
2578
|
+
|
|
2579
|
+
CREATE OR REPLACE FUNCTION finalize_organization_retention_deletion(
|
|
2580
|
+
p_account_id uuid,
|
|
2581
|
+
p_membership_id uuid,
|
|
2582
|
+
p_operation_id uuid,
|
|
2583
|
+
p_object_bucket text
|
|
2584
|
+
) RETURNS jsonb
|
|
2585
|
+
LANGUAGE plpgsql
|
|
2586
|
+
SECURITY DEFINER
|
|
2587
|
+
SET search_path FROM CURRENT
|
|
2588
|
+
AS $body$
|
|
2589
|
+
DECLARE
|
|
2590
|
+
member organization_memberships%ROWTYPE;
|
|
2591
|
+
deletion organization_user_retention_deletions%ROWTYPE;
|
|
2592
|
+
prior_event organization_user_retention_deletion_events%ROWTYPE;
|
|
2593
|
+
unsupported_kind text;
|
|
2594
|
+
candidate_file_ids uuid[];
|
|
2595
|
+
deleted_documents integer := 0;
|
|
2596
|
+
deleted_files integer := 0;
|
|
2597
|
+
deleted_variable_sets integer := 0;
|
|
2598
|
+
deleted_rigs integer := 0;
|
|
2599
|
+
deleted_connections integer := 0;
|
|
2600
|
+
deleted_codex integer := 0;
|
|
2601
|
+
deleted_xai integer := 0;
|
|
2602
|
+
tombstoned_machines integer := 0;
|
|
2603
|
+
deleted_workspace integer := 0;
|
|
2604
|
+
object_count integer := 0;
|
|
2605
|
+
locked_personal_workspace_id uuid;
|
|
2606
|
+
result_value jsonb;
|
|
2607
|
+
BEGIN
|
|
2608
|
+
IF nullif(p_object_bucket, '') IS NULL
|
|
2609
|
+
OR octet_length(convert_to(p_object_bucket, 'UTF8')) > 1024
|
|
2610
|
+
THEN
|
|
2611
|
+
RAISE EXCEPTION 'invalid retention object bucket' USING ERRCODE = '22023';
|
|
2612
|
+
END IF;
|
|
2613
|
+
PERFORM opengeni_private.assert_organization_retention_account(p_account_id);
|
|
2614
|
+
PERFORM pg_catalog.set_config(
|
|
2615
|
+
'opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true
|
|
2616
|
+
);
|
|
2617
|
+
PERFORM pg_catalog.pg_advisory_xact_lock(
|
|
2618
|
+
pg_catalog.hashtextextended('organization-retention:' || p_operation_id::text, 0)
|
|
2619
|
+
);
|
|
2620
|
+
SELECT * INTO prior_event FROM organization_user_retention_deletion_events event
|
|
2621
|
+
WHERE event.account_id = p_account_id AND event.operation_id = p_operation_id
|
|
2622
|
+
AND event.kind = 'completed';
|
|
2623
|
+
IF FOUND THEN
|
|
2624
|
+
IF prior_event.membership_id IS DISTINCT FROM p_membership_id THEN
|
|
2625
|
+
RAISE EXCEPTION 'retention completion operation id belongs to another membership'
|
|
2626
|
+
USING ERRCODE = '23505';
|
|
2627
|
+
END IF;
|
|
2628
|
+
SELECT candidate.database_result INTO result_value
|
|
2629
|
+
FROM organization_user_retention_deletions candidate
|
|
2630
|
+
WHERE candidate.account_id = p_account_id
|
|
2631
|
+
AND candidate.membership_id = p_membership_id;
|
|
2632
|
+
IF NOT FOUND
|
|
2633
|
+
OR result_value ->> 'objectBucket' IS DISTINCT FROM p_object_bucket
|
|
2634
|
+
THEN
|
|
2635
|
+
RAISE EXCEPTION 'retention completion operation id has different frozen input'
|
|
2636
|
+
USING ERRCODE = '23505';
|
|
2637
|
+
END IF;
|
|
2638
|
+
RETURN result_value;
|
|
2639
|
+
END IF;
|
|
2640
|
+
SELECT * INTO member FROM organization_memberships membership
|
|
2641
|
+
WHERE membership.account_id = p_account_id AND membership.id = p_membership_id;
|
|
2642
|
+
IF NOT FOUND OR member.status <> 'revoked'
|
|
2643
|
+
OR member.personal_retention_until IS NULL
|
|
2644
|
+
OR member.personal_retention_until > clock_timestamp()
|
|
2645
|
+
THEN RAISE EXCEPTION 'retention membership is not eligible' USING ERRCODE = '42501'; END IF;
|
|
2646
|
+
locked_personal_workspace_id := member.personal_workspace_id;
|
|
2647
|
+
IF locked_personal_workspace_id IS NOT NULL THEN
|
|
2648
|
+
PERFORM 1 FROM workspaces workspace
|
|
2649
|
+
WHERE workspace.account_id = p_account_id
|
|
2650
|
+
AND workspace.id = locked_personal_workspace_id
|
|
2651
|
+
FOR UPDATE;
|
|
2652
|
+
IF NOT FOUND THEN
|
|
2653
|
+
RAISE EXCEPTION 'retention personal workspace is missing' USING ERRCODE = '40001';
|
|
2654
|
+
END IF;
|
|
2655
|
+
END IF;
|
|
2656
|
+
SELECT * INTO member FROM organization_memberships membership
|
|
2657
|
+
WHERE membership.account_id = p_account_id AND membership.id = p_membership_id
|
|
2658
|
+
FOR UPDATE;
|
|
2659
|
+
IF NOT FOUND OR member.status <> 'revoked'
|
|
2660
|
+
OR member.personal_retention_until IS NULL
|
|
2661
|
+
OR member.personal_retention_until > clock_timestamp()
|
|
2662
|
+
OR member.personal_workspace_id IS DISTINCT FROM locked_personal_workspace_id
|
|
2663
|
+
THEN RAISE EXCEPTION 'retention membership changed during finalization'
|
|
2664
|
+
USING ERRCODE = '40001'; END IF;
|
|
2665
|
+
SELECT * INTO deletion FROM organization_user_retention_deletions candidate
|
|
2666
|
+
WHERE candidate.account_id = p_account_id AND candidate.membership_id = p_membership_id
|
|
2667
|
+
FOR UPDATE;
|
|
2668
|
+
IF NOT FOUND OR deletion.state <> 'claimed'
|
|
2669
|
+
OR deletion.claim_operation_id <> p_operation_id
|
|
2670
|
+
OR deletion.claim_expires_at <= clock_timestamp()
|
|
2671
|
+
THEN RAISE EXCEPTION 'retention deletion claim is stale' USING ERRCODE = '40001'; END IF;
|
|
2672
|
+
IF deletion.retention_until IS DISTINCT FROM member.personal_retention_until THEN
|
|
2673
|
+
RAISE EXCEPTION 'retention deadline changed after claim' USING ERRCODE = '40001';
|
|
2674
|
+
END IF;
|
|
2675
|
+
IF deletion.database_finalized_at IS NOT NULL THEN
|
|
2676
|
+
IF deletion.database_result ->> 'objectBucket' IS DISTINCT FROM p_object_bucket
|
|
2677
|
+
OR EXISTS (
|
|
2678
|
+
SELECT 1 FROM organization_user_retention_object_obligations obligation
|
|
2679
|
+
WHERE obligation.account_id = p_account_id
|
|
2680
|
+
AND obligation.membership_id = p_membership_id
|
|
2681
|
+
AND obligation.object_bucket <> p_object_bucket
|
|
2682
|
+
)
|
|
2683
|
+
THEN
|
|
2684
|
+
RAISE EXCEPTION 'retention object bucket changed after database finalization'
|
|
2685
|
+
USING ERRCODE = '40001';
|
|
2686
|
+
END IF;
|
|
2687
|
+
RETURN pg_catalog.jsonb_set(
|
|
2688
|
+
deletion.database_result,
|
|
2689
|
+
'{operationId}',
|
|
2690
|
+
pg_catalog.to_jsonb(p_operation_id::text),
|
|
2691
|
+
false
|
|
2692
|
+
);
|
|
2693
|
+
END IF;
|
|
2694
|
+
IF member.personal_workspace_id IS NOT NULL AND EXISTS (
|
|
2695
|
+
SELECT 1
|
|
2696
|
+
FROM documents document
|
|
2697
|
+
LEFT JOIN organization_user_resource_authorities authority
|
|
2698
|
+
ON authority.id = document.authority_id
|
|
2699
|
+
AND authority.account_id = document.account_id
|
|
2700
|
+
AND authority.organization_membership_id = document.owner_organization_membership_id
|
|
2701
|
+
AND authority.resource_kind = 'document'
|
|
2702
|
+
AND authority.resource_id = document.id
|
|
2703
|
+
WHERE document.account_id = p_account_id
|
|
2704
|
+
AND document.workspace_id = member.personal_workspace_id
|
|
2705
|
+
AND (
|
|
2706
|
+
authority.id IS NULL
|
|
2707
|
+
OR authority.organization_membership_id <> p_membership_id
|
|
2708
|
+
)
|
|
2709
|
+
) THEN
|
|
2710
|
+
RAISE EXCEPTION 'personal workspace contains retained external document authority'
|
|
2711
|
+
USING ERRCODE = '55000';
|
|
2712
|
+
END IF;
|
|
2713
|
+
IF member.personal_workspace_id IS NOT NULL AND EXISTS (
|
|
2714
|
+
SELECT 1
|
|
2715
|
+
FROM workspace_captures capture
|
|
2716
|
+
WHERE capture.account_id = p_account_id
|
|
2717
|
+
AND capture.workspace_id = member.personal_workspace_id
|
|
2718
|
+
AND (
|
|
2719
|
+
pg_catalog.jsonb_typeof(capture.blob_keys) <> 'array'
|
|
2720
|
+
OR CASE WHEN pg_catalog.jsonb_typeof(capture.blob_keys) = 'array' THEN EXISTS (
|
|
2721
|
+
SELECT 1
|
|
2722
|
+
FROM pg_catalog.jsonb_array_elements(capture.blob_keys) blob(value)
|
|
2723
|
+
WHERE pg_catalog.jsonb_typeof(blob.value) <> 'string'
|
|
2724
|
+
OR octet_length(convert_to(blob.value #>> '{}', 'UTF8')) NOT BETWEEN 1 AND 4096
|
|
2725
|
+
) ELSE false END
|
|
2726
|
+
)
|
|
2727
|
+
) THEN
|
|
2728
|
+
RAISE EXCEPTION 'personal workspace contains malformed external object inventory'
|
|
2729
|
+
USING ERRCODE = '55000';
|
|
2730
|
+
END IF;
|
|
2731
|
+
SELECT authority.resource_kind INTO unsupported_kind
|
|
2732
|
+
FROM organization_user_resource_authorities authority
|
|
2733
|
+
WHERE authority.account_id = p_account_id
|
|
2734
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2735
|
+
AND authority.resource_kind NOT IN (
|
|
2736
|
+
'codex_subscription', 'connected_machine', 'connection', 'document',
|
|
2737
|
+
'rig', 'variable_set', 'xai_subscription'
|
|
2738
|
+
)
|
|
2739
|
+
ORDER BY authority.resource_kind LIMIT 1;
|
|
2740
|
+
IF FOUND THEN
|
|
2741
|
+
RAISE EXCEPTION 'unsupported retained resource kind: %', unsupported_kind
|
|
2742
|
+
USING ERRCODE = '55000';
|
|
2743
|
+
END IF;
|
|
2744
|
+
PERFORM 1 FROM organization_user_resource_authorities authority
|
|
2745
|
+
WHERE authority.account_id = p_account_id
|
|
2746
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2747
|
+
ORDER BY authority.id FOR UPDATE;
|
|
2748
|
+
SELECT COALESCE(array_agg(candidate.file_id ORDER BY candidate.file_id), ARRAY[]::uuid[])
|
|
2749
|
+
INTO candidate_file_ids
|
|
2750
|
+
FROM opengeni_private.organization_retention_file_candidates(
|
|
2751
|
+
p_account_id, p_membership_id
|
|
2752
|
+
) candidate;
|
|
2753
|
+
PERFORM 1 FROM files file
|
|
2754
|
+
WHERE file.account_id = p_account_id AND file.id = ANY(candidate_file_ids)
|
|
2755
|
+
ORDER BY file.id FOR UPDATE;
|
|
2756
|
+
IF EXISTS (
|
|
2757
|
+
SELECT 1
|
|
2758
|
+
FROM opengeni_private.organization_retention_file_candidates(
|
|
2759
|
+
p_account_id, p_membership_id
|
|
2760
|
+
) candidate
|
|
2761
|
+
WHERE candidate.object_bucket IS DISTINCT FROM p_object_bucket
|
|
2762
|
+
) THEN
|
|
2763
|
+
RAISE EXCEPTION 'retention file bucket differs from configured object storage'
|
|
2764
|
+
USING ERRCODE = '55000';
|
|
2765
|
+
END IF;
|
|
2766
|
+
-- Freeze every external-key source before materializing obligations. The
|
|
2767
|
+
-- workspace row above excludes new FK children; these row locks exclude key
|
|
2768
|
+
-- rotation while exact obligations are copied and the workspace is erased.
|
|
2769
|
+
PERFORM 1 FROM session_recordings recording
|
|
2770
|
+
WHERE recording.account_id = p_account_id
|
|
2771
|
+
AND recording.workspace_id = member.personal_workspace_id
|
|
2772
|
+
ORDER BY recording.id FOR UPDATE;
|
|
2773
|
+
PERFORM 1 FROM browser_state_artifacts artifact
|
|
2774
|
+
WHERE artifact.account_id = p_account_id
|
|
2775
|
+
AND artifact.workspace_id = member.personal_workspace_id
|
|
2776
|
+
ORDER BY artifact.id FOR UPDATE;
|
|
2777
|
+
PERFORM 1 FROM browser_state_uploads upload
|
|
2778
|
+
WHERE upload.account_id = p_account_id
|
|
2779
|
+
AND upload.workspace_id = member.personal_workspace_id
|
|
2780
|
+
ORDER BY upload.id FOR UPDATE;
|
|
2781
|
+
PERFORM 1 FROM transcription_recording_objects recording_object
|
|
2782
|
+
WHERE recording_object.account_id = p_account_id
|
|
2783
|
+
AND recording_object.workspace_id = member.personal_workspace_id
|
|
2784
|
+
ORDER BY recording_object.object_key FOR UPDATE;
|
|
2785
|
+
PERFORM 1 FROM video_generation_references reference
|
|
2786
|
+
WHERE reference.account_id = p_account_id
|
|
2787
|
+
AND reference.workspace_id = member.personal_workspace_id
|
|
2788
|
+
ORDER BY reference.operation_id, reference.ordinal FOR UPDATE;
|
|
2789
|
+
PERFORM 1 FROM workspace_artifact_versions version
|
|
2790
|
+
WHERE version.account_id = p_account_id
|
|
2791
|
+
AND version.workspace_id = member.personal_workspace_id
|
|
2792
|
+
ORDER BY version.id FOR UPDATE;
|
|
2793
|
+
PERFORM 1 FROM editable_artifact_blob_refs blob
|
|
2794
|
+
WHERE blob.account_id = p_account_id
|
|
2795
|
+
AND blob.workspace_id = member.personal_workspace_id
|
|
2796
|
+
ORDER BY blob.artifact_id, blob.id FOR UPDATE;
|
|
2797
|
+
PERFORM 1 FROM workspace_captures capture
|
|
2798
|
+
WHERE capture.account_id = p_account_id
|
|
2799
|
+
AND capture.workspace_id = member.personal_workspace_id
|
|
2800
|
+
ORDER BY capture.id FOR UPDATE;
|
|
2801
|
+
INSERT INTO organization_user_retention_object_obligations (
|
|
2802
|
+
account_id, membership_id, object_kind, source_id, object_bucket, object_key,
|
|
2803
|
+
prepared_operation_id, object_key_hash
|
|
2804
|
+
)
|
|
2805
|
+
SELECT p_account_id, p_membership_id, candidate.object_kind,
|
|
2806
|
+
candidate.source_id, p_object_bucket, candidate.object_key, p_operation_id,
|
|
2807
|
+
encode(digest(candidate.object_key, 'sha256'), 'hex')
|
|
2808
|
+
FROM opengeni_private.organization_retention_object_candidates(
|
|
2809
|
+
p_account_id, p_membership_id
|
|
2810
|
+
) candidate;
|
|
2811
|
+
GET DIAGNOSTICS object_count = ROW_COUNT;
|
|
2812
|
+
|
|
2813
|
+
DELETE FROM documents document
|
|
2814
|
+
USING organization_user_resource_authorities authority
|
|
2815
|
+
WHERE authority.account_id = p_account_id
|
|
2816
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2817
|
+
AND authority.resource_kind = 'document'
|
|
2818
|
+
AND authority.resource_id = document.id
|
|
2819
|
+
AND document.account_id = authority.account_id
|
|
2820
|
+
AND document.authority_id = authority.id
|
|
2821
|
+
AND document.owner_organization_membership_id = authority.organization_membership_id;
|
|
2822
|
+
GET DIAGNOSTICS deleted_documents = ROW_COUNT;
|
|
2823
|
+
DELETE FROM files file
|
|
2824
|
+
WHERE file.account_id = p_account_id AND file.id = ANY(candidate_file_ids)
|
|
2825
|
+
AND NOT EXISTS (SELECT 1 FROM documents document WHERE document.file_id = file.id);
|
|
2826
|
+
GET DIAGNOSTICS deleted_files = ROW_COUNT;
|
|
2827
|
+
|
|
2828
|
+
UPDATE scheduled_tasks task SET status = 'paused', variable_set_id = NULL,
|
|
2829
|
+
authority_revision = authority_revision + 1, updated_at = clock_timestamp()
|
|
2830
|
+
WHERE task.account_id = p_account_id AND task.variable_set_id IN (
|
|
2831
|
+
SELECT authority.resource_id FROM organization_user_resource_authorities authority
|
|
2832
|
+
WHERE authority.account_id = p_account_id
|
|
2833
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2834
|
+
AND authority.resource_kind = 'variable_set'
|
|
2835
|
+
);
|
|
2836
|
+
DELETE FROM workspace_variable_sets resource
|
|
2837
|
+
USING organization_user_resource_authorities authority
|
|
2838
|
+
WHERE authority.account_id = p_account_id
|
|
2839
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2840
|
+
AND authority.resource_kind = 'variable_set'
|
|
2841
|
+
AND authority.resource_id = resource.id
|
|
2842
|
+
AND resource.account_id = authority.account_id
|
|
2843
|
+
AND resource.authority_id = authority.id
|
|
2844
|
+
AND resource.owner_organization_membership_id = authority.organization_membership_id;
|
|
2845
|
+
GET DIAGNOSTICS deleted_variable_sets = ROW_COUNT;
|
|
2846
|
+
|
|
2847
|
+
UPDATE capability_facet_installations binding SET connection_id = NULL,
|
|
2848
|
+
status = 'needs_attention',
|
|
2849
|
+
attention_code = 'owner_retention_deleted',
|
|
2850
|
+
version = version + 1, updated_at = clock_timestamp()
|
|
2851
|
+
WHERE binding.account_id = p_account_id AND binding.connection_id IN (
|
|
2852
|
+
SELECT authority.resource_id FROM organization_user_resource_authorities authority
|
|
2853
|
+
WHERE authority.account_id = p_account_id
|
|
2854
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2855
|
+
AND authority.resource_kind = 'connection'
|
|
2856
|
+
);
|
|
2857
|
+
UPDATE integration_facet_bindings binding SET connection_id = NULL,
|
|
2858
|
+
status = 'needs_attention',
|
|
2859
|
+
last_error_code = 'owner_retention_deleted',
|
|
2860
|
+
version = version + 1, updated_at = clock_timestamp()
|
|
2861
|
+
WHERE binding.account_id = p_account_id AND binding.connection_id IN (
|
|
2862
|
+
SELECT authority.resource_id FROM organization_user_resource_authorities authority
|
|
2863
|
+
WHERE authority.account_id = p_account_id
|
|
2864
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2865
|
+
AND authority.resource_kind = 'connection'
|
|
2866
|
+
);
|
|
2867
|
+
DELETE FROM connections resource
|
|
2868
|
+
USING organization_user_resource_authorities authority
|
|
2869
|
+
WHERE authority.account_id = p_account_id
|
|
2870
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2871
|
+
AND authority.resource_kind = 'connection'
|
|
2872
|
+
AND authority.resource_id = resource.id
|
|
2873
|
+
AND resource.account_id = authority.account_id
|
|
2874
|
+
AND resource.authority_id = authority.id
|
|
2875
|
+
AND resource.owner_organization_membership_id = authority.organization_membership_id;
|
|
2876
|
+
GET DIAGNOSTICS deleted_connections = ROW_COUNT;
|
|
2877
|
+
|
|
2878
|
+
DELETE FROM codex_subscription_credentials resource
|
|
2879
|
+
USING organization_user_resource_authorities authority
|
|
2880
|
+
WHERE authority.account_id = p_account_id
|
|
2881
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2882
|
+
AND authority.resource_kind = 'codex_subscription'
|
|
2883
|
+
AND authority.resource_id = resource.id
|
|
2884
|
+
AND resource.account_id = authority.account_id
|
|
2885
|
+
AND resource.organization_user_resource_authority_id = authority.id
|
|
2886
|
+
AND resource.owner_organization_membership_id = authority.organization_membership_id;
|
|
2887
|
+
GET DIAGNOSTICS deleted_codex = ROW_COUNT;
|
|
2888
|
+
DELETE FROM xai_subscription_credentials resource
|
|
2889
|
+
USING organization_user_resource_authorities authority
|
|
2890
|
+
WHERE authority.account_id = p_account_id
|
|
2891
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2892
|
+
AND authority.resource_kind = 'xai_subscription'
|
|
2893
|
+
AND authority.resource_id = resource.id
|
|
2894
|
+
AND resource.account_id = authority.account_id
|
|
2895
|
+
AND resource.organization_user_resource_authority_id = authority.id
|
|
2896
|
+
AND resource.owner_organization_membership_id = authority.organization_membership_id;
|
|
2897
|
+
GET DIAGNOSTICS deleted_xai = ROW_COUNT;
|
|
2898
|
+
|
|
2899
|
+
DELETE FROM rigs resource
|
|
2900
|
+
USING organization_user_resource_authorities authority
|
|
2901
|
+
WHERE authority.account_id = p_account_id
|
|
2902
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2903
|
+
AND authority.resource_kind = 'rig'
|
|
2904
|
+
AND authority.resource_id = resource.id
|
|
2905
|
+
AND resource.account_id = authority.account_id
|
|
2906
|
+
AND resource.authority_id = authority.id
|
|
2907
|
+
AND resource.owner_organization_membership_id = authority.organization_membership_id;
|
|
2908
|
+
GET DIAGNOSTICS deleted_rigs = ROW_COUNT;
|
|
2909
|
+
|
|
2910
|
+
UPDATE enrollments resource SET status = 'revoked',
|
|
2911
|
+
revoked_at = COALESCE(resource.revoked_at, clock_timestamp()),
|
|
2912
|
+
generation = resource.generation + 1, connection_instance_id = NULL,
|
|
2913
|
+
connection_lease_expires_at = NULL, desktop_unavailable_reason = NULL,
|
|
2914
|
+
updated_at = clock_timestamp()
|
|
2915
|
+
FROM organization_user_resource_authorities authority
|
|
2916
|
+
WHERE authority.account_id = p_account_id
|
|
2917
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2918
|
+
AND authority.resource_kind = 'connected_machine'
|
|
2919
|
+
AND authority.resource_id = resource.id
|
|
2920
|
+
AND resource.account_id = authority.account_id
|
|
2921
|
+
AND resource.authority_id = authority.id
|
|
2922
|
+
AND resource.owner_organization_membership_id = authority.organization_membership_id;
|
|
2923
|
+
GET DIAGNOSTICS tombstoned_machines = ROW_COUNT;
|
|
2924
|
+
|
|
2925
|
+
UPDATE organization_user_resource_authorities authority
|
|
2926
|
+
SET status = 'revoked', revoked_at = COALESCE(authority.revoked_at, clock_timestamp()),
|
|
2927
|
+
generation = authority.generation + 1, updated_at = clock_timestamp()
|
|
2928
|
+
WHERE authority.account_id = p_account_id
|
|
2929
|
+
AND authority.organization_membership_id = p_membership_id
|
|
2930
|
+
AND authority.status <> 'revoked';
|
|
2931
|
+
UPDATE organization_memberships membership
|
|
2932
|
+
SET personal_workspace_id = NULL, updated_at = clock_timestamp()
|
|
2933
|
+
WHERE membership.account_id = p_account_id AND membership.id = p_membership_id;
|
|
2934
|
+
IF member.personal_workspace_id IS NOT NULL THEN
|
|
2935
|
+
DELETE FROM workspaces workspace
|
|
2936
|
+
WHERE workspace.account_id = p_account_id AND workspace.id = member.personal_workspace_id;
|
|
2937
|
+
GET DIAGNOSTICS deleted_workspace = ROW_COUNT;
|
|
2938
|
+
END IF;
|
|
2939
|
+
result_value := pg_catalog.jsonb_build_object(
|
|
2940
|
+
'organizationId', p_account_id,
|
|
2941
|
+
'membershipId', p_membership_id,
|
|
2942
|
+
'operationId', p_operation_id,
|
|
2943
|
+
'outcome', 'cleanup_pending',
|
|
2944
|
+
'objectBucket', p_object_bucket,
|
|
2945
|
+
'objectCount', object_count,
|
|
2946
|
+
'deletedResources', pg_catalog.jsonb_build_object(
|
|
2947
|
+
'documents', deleted_documents, 'files', deleted_files,
|
|
2948
|
+
'variableSets', deleted_variable_sets, 'rigs', deleted_rigs,
|
|
2949
|
+
'connections', deleted_connections, 'codexSubscriptions', deleted_codex,
|
|
2950
|
+
'xaiSubscriptions', deleted_xai, 'connectedMachinesTombstoned', tombstoned_machines,
|
|
2951
|
+
'personalWorkspaces', deleted_workspace
|
|
2952
|
+
),
|
|
2953
|
+
'databaseFinalizedAt', clock_timestamp()
|
|
2954
|
+
);
|
|
2955
|
+
UPDATE organization_user_retention_deletions current_deletion
|
|
2956
|
+
SET database_result = result_value, database_finalized_at = clock_timestamp(),
|
|
2957
|
+
claim_expires_at = clock_timestamp() + interval '15 minutes',
|
|
2958
|
+
updated_at = clock_timestamp()
|
|
2959
|
+
WHERE current_deletion.account_id = p_account_id
|
|
2960
|
+
AND current_deletion.membership_id = p_membership_id
|
|
2961
|
+
AND current_deletion.claim_operation_id = p_operation_id;
|
|
2962
|
+
RETURN result_value;
|
|
2963
|
+
END
|
|
2964
|
+
$body$;
|
|
2965
|
+
|
|
2966
|
+
CREATE OR REPLACE FUNCTION complete_organization_retention_deletion(
|
|
2967
|
+
p_account_id uuid,
|
|
2968
|
+
p_membership_id uuid,
|
|
2969
|
+
p_operation_id uuid,
|
|
2970
|
+
p_object_bucket text
|
|
2971
|
+
) RETURNS jsonb
|
|
2972
|
+
LANGUAGE plpgsql
|
|
2973
|
+
SECURITY DEFINER
|
|
2974
|
+
SET search_path FROM CURRENT
|
|
2975
|
+
AS $body$
|
|
2976
|
+
DECLARE
|
|
2977
|
+
deletion organization_user_retention_deletions%ROWTYPE;
|
|
2978
|
+
prior_event organization_user_retention_deletion_events%ROWTYPE;
|
|
2979
|
+
result_value jsonb;
|
|
2980
|
+
object_count integer;
|
|
2981
|
+
BEGIN
|
|
2982
|
+
IF nullif(p_object_bucket, '') IS NULL
|
|
2983
|
+
OR octet_length(convert_to(p_object_bucket, 'UTF8')) > 1024
|
|
2984
|
+
THEN
|
|
2985
|
+
RAISE EXCEPTION 'invalid retention object bucket' USING ERRCODE = '22023';
|
|
2986
|
+
END IF;
|
|
2987
|
+
PERFORM opengeni_private.assert_organization_retention_account(p_account_id);
|
|
2988
|
+
PERFORM pg_catalog.set_config(
|
|
2989
|
+
'opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true
|
|
2990
|
+
);
|
|
2991
|
+
SELECT * INTO prior_event
|
|
2992
|
+
FROM organization_user_retention_deletion_events event
|
|
2993
|
+
WHERE event.account_id = p_account_id
|
|
2994
|
+
AND event.operation_id = p_operation_id
|
|
2995
|
+
AND event.kind = 'completed';
|
|
2996
|
+
IF FOUND THEN
|
|
2997
|
+
IF prior_event.membership_id IS DISTINCT FROM p_membership_id THEN
|
|
2998
|
+
RAISE EXCEPTION 'retention completion operation id belongs to another membership'
|
|
2999
|
+
USING ERRCODE = '23505';
|
|
3000
|
+
END IF;
|
|
3001
|
+
SELECT * INTO deletion
|
|
3002
|
+
FROM organization_user_retention_deletions candidate
|
|
3003
|
+
WHERE candidate.account_id = p_account_id
|
|
3004
|
+
AND candidate.membership_id = p_membership_id;
|
|
3005
|
+
IF NOT FOUND
|
|
3006
|
+
OR deletion.database_result ->> 'objectBucket' IS DISTINCT FROM p_object_bucket
|
|
3007
|
+
THEN
|
|
3008
|
+
RAISE EXCEPTION 'retention completion operation id has different frozen input'
|
|
3009
|
+
USING ERRCODE = '23505';
|
|
3010
|
+
END IF;
|
|
3011
|
+
RETURN prior_event.result;
|
|
3012
|
+
END IF;
|
|
3013
|
+
SELECT * INTO deletion
|
|
3014
|
+
FROM organization_user_retention_deletions candidate
|
|
3015
|
+
WHERE candidate.account_id = p_account_id
|
|
3016
|
+
AND candidate.membership_id = p_membership_id
|
|
3017
|
+
FOR UPDATE;
|
|
3018
|
+
IF NOT FOUND THEN
|
|
3019
|
+
RAISE EXCEPTION 'retention deletion claim is stale' USING ERRCODE = '40001';
|
|
3020
|
+
END IF;
|
|
3021
|
+
-- A concurrent exact retry can miss the completed event before waiting on
|
|
3022
|
+
-- the deletion-row lock. Recheck under the lock so the loser returns the
|
|
3023
|
+
-- winner's immutable result instead of misclassifying completion as stale.
|
|
3024
|
+
SELECT * INTO prior_event
|
|
3025
|
+
FROM organization_user_retention_deletion_events event
|
|
3026
|
+
WHERE event.account_id = p_account_id
|
|
3027
|
+
AND event.operation_id = p_operation_id
|
|
3028
|
+
AND event.kind = 'completed';
|
|
3029
|
+
IF FOUND THEN
|
|
3030
|
+
IF prior_event.membership_id IS DISTINCT FROM p_membership_id
|
|
3031
|
+
OR deletion.database_result ->> 'objectBucket' IS DISTINCT FROM p_object_bucket
|
|
3032
|
+
THEN
|
|
3033
|
+
RAISE EXCEPTION 'retention completion operation id has different frozen input'
|
|
3034
|
+
USING ERRCODE = '23505';
|
|
3035
|
+
END IF;
|
|
3036
|
+
RETURN prior_event.result;
|
|
3037
|
+
END IF;
|
|
3038
|
+
IF deletion.state <> 'claimed'
|
|
3039
|
+
OR deletion.claim_operation_id <> p_operation_id
|
|
3040
|
+
OR deletion.claim_expires_at <= clock_timestamp()
|
|
3041
|
+
OR deletion.database_finalized_at IS NULL
|
|
3042
|
+
OR deletion.database_result ->> 'objectBucket' IS DISTINCT FROM p_object_bucket
|
|
3043
|
+
THEN
|
|
3044
|
+
RAISE EXCEPTION 'retention deletion claim is stale' USING ERRCODE = '40001';
|
|
3045
|
+
END IF;
|
|
3046
|
+
IF EXISTS (
|
|
3047
|
+
SELECT 1 FROM organization_user_retention_object_obligations receipt
|
|
3048
|
+
WHERE receipt.account_id = p_account_id
|
|
3049
|
+
AND receipt.membership_id = p_membership_id
|
|
3050
|
+
AND NOT EXISTS (
|
|
3051
|
+
SELECT 1
|
|
3052
|
+
FROM organization_user_retention_object_deletion_receipts deleted
|
|
3053
|
+
WHERE deleted.account_id = receipt.account_id
|
|
3054
|
+
AND deleted.membership_id = receipt.membership_id
|
|
3055
|
+
AND deleted.object_kind = receipt.object_kind
|
|
3056
|
+
AND deleted.source_id = receipt.source_id
|
|
3057
|
+
AND deleted.object_bucket = receipt.object_bucket
|
|
3058
|
+
)
|
|
3059
|
+
) THEN
|
|
3060
|
+
RAISE EXCEPTION 'retention object cleanup remains incomplete' USING ERRCODE = '55000';
|
|
3061
|
+
END IF;
|
|
3062
|
+
SELECT count(*)::integer INTO object_count
|
|
3063
|
+
FROM organization_user_retention_object_obligations receipt
|
|
3064
|
+
WHERE receipt.account_id = p_account_id
|
|
3065
|
+
AND receipt.membership_id = p_membership_id;
|
|
3066
|
+
result_value := pg_catalog.jsonb_build_object(
|
|
3067
|
+
'organizationId', p_account_id,
|
|
3068
|
+
'membershipId', p_membership_id,
|
|
3069
|
+
'operationId', p_operation_id,
|
|
3070
|
+
'outcome', 'completed',
|
|
3071
|
+
'deletedResources', deletion.database_result -> 'deletedResources'
|
|
3072
|
+
|| pg_catalog.jsonb_build_object('externalObjects', object_count),
|
|
3073
|
+
'completedAt', clock_timestamp()
|
|
3074
|
+
);
|
|
3075
|
+
UPDATE organization_user_retention_deletions current_deletion
|
|
3076
|
+
SET state = 'completed', result = result_value, completed_at = clock_timestamp(),
|
|
3077
|
+
claim_expires_at = clock_timestamp(), updated_at = clock_timestamp()
|
|
3078
|
+
WHERE current_deletion.account_id = p_account_id
|
|
3079
|
+
AND current_deletion.membership_id = p_membership_id
|
|
3080
|
+
AND current_deletion.claim_operation_id = p_operation_id;
|
|
3081
|
+
INSERT INTO organization_user_retention_deletion_events (
|
|
3082
|
+
account_id, membership_id, operation_id, kind, result
|
|
3083
|
+
) VALUES (p_account_id, p_membership_id, p_operation_id, 'completed', result_value);
|
|
3084
|
+
RETURN result_value;
|
|
3085
|
+
END
|
|
3086
|
+
$body$;
|
|
3087
|
+
|
|
3088
|
+
-- Pin every SECURITY DEFINER routine after creation. FROM CURRENT is used only
|
|
3089
|
+
-- while parsing the target-schema migration; the durable posture is closed.
|
|
3090
|
+
DO $pin_and_grant$
|
|
3091
|
+
DECLARE data_schema text := current_schema();
|
|
3092
|
+
BEGIN
|
|
3093
|
+
EXECUTE format(
|
|
3094
|
+
'ALTER FUNCTION opengeni_private.assign_managed_self_organization_owner() SET search_path = pg_catalog, %I, pg_temp',
|
|
3095
|
+
data_schema
|
|
3096
|
+
);
|
|
3097
|
+
EXECUTE format(
|
|
3098
|
+
'ALTER FUNCTION %I.list_self_organization_memberships(text) SET search_path = pg_catalog, %I, pg_temp',
|
|
3099
|
+
data_schema, data_schema
|
|
3100
|
+
);
|
|
3101
|
+
EXECUTE format(
|
|
3102
|
+
'ALTER FUNCTION %I.list_self_organization_invitations(text) SET search_path = pg_catalog, %I, pg_temp',
|
|
3103
|
+
data_schema, data_schema
|
|
3104
|
+
);
|
|
3105
|
+
EXECUTE format(
|
|
3106
|
+
'ALTER FUNCTION %I.list_self_organization_invitations(text,uuid,integer) SET search_path = pg_catalog, %I, pg_temp',
|
|
3107
|
+
data_schema, data_schema
|
|
3108
|
+
);
|
|
3109
|
+
EXECUTE format(
|
|
3110
|
+
'ALTER FUNCTION %I.get_self_organization_invitation(text,uuid) SET search_path = pg_catalog, %I, pg_temp',
|
|
3111
|
+
data_schema, data_schema
|
|
3112
|
+
);
|
|
3113
|
+
EXECUTE format(
|
|
3114
|
+
'ALTER FUNCTION %I.list_organization_members(uuid,text) SET search_path = pg_catalog, %I, pg_temp',
|
|
3115
|
+
data_schema, data_schema
|
|
3116
|
+
);
|
|
3117
|
+
EXECUTE format(
|
|
3118
|
+
'ALTER FUNCTION %I.list_organization_invitations(uuid,text,uuid,integer) SET search_path = pg_catalog, %I, pg_temp',
|
|
3119
|
+
data_schema, data_schema
|
|
3120
|
+
);
|
|
3121
|
+
EXECUTE format(
|
|
3122
|
+
'ALTER FUNCTION %I.organization_membership_command(jsonb) SET search_path = pg_catalog, %I, pg_temp',
|
|
3123
|
+
data_schema, data_schema
|
|
3124
|
+
);
|
|
3125
|
+
EXECUTE format(
|
|
3126
|
+
'ALTER FUNCTION %I.prepare_organization_membership_protocol_settlements(jsonb) SET search_path = pg_catalog, %I, pg_temp',
|
|
3127
|
+
data_schema, data_schema
|
|
3128
|
+
);
|
|
3129
|
+
EXECUTE format(
|
|
3130
|
+
'ALTER FUNCTION %I.get_organization_retention_policy(uuid,text) SET search_path = pg_catalog, %I, pg_temp',
|
|
3131
|
+
data_schema, data_schema
|
|
3132
|
+
);
|
|
3133
|
+
EXECUTE format(
|
|
3134
|
+
'ALTER FUNCTION opengeni_private.assert_organization_retention_account(uuid) SET search_path = pg_catalog, %I, pg_temp',
|
|
3135
|
+
data_schema
|
|
3136
|
+
);
|
|
3137
|
+
EXECUTE format(
|
|
3138
|
+
'ALTER FUNCTION %I.assert_active_managed_human_organization_membership(uuid,text) SET search_path = pg_catalog, %I, pg_temp',
|
|
3139
|
+
data_schema, data_schema
|
|
3140
|
+
);
|
|
3141
|
+
EXECUTE format(
|
|
3142
|
+
'ALTER FUNCTION opengeni_private.organization_retention_file_candidates(uuid,uuid) SET search_path = pg_catalog, %I, pg_temp',
|
|
3143
|
+
data_schema
|
|
3144
|
+
);
|
|
3145
|
+
EXECUTE format(
|
|
3146
|
+
'ALTER FUNCTION opengeni_private.organization_retention_object_candidates(uuid,uuid) SET search_path = pg_catalog, %I, pg_temp',
|
|
3147
|
+
data_schema
|
|
3148
|
+
);
|
|
3149
|
+
EXECUTE format(
|
|
3150
|
+
'ALTER FUNCTION %I.preview_organization_retention_deletions(uuid,integer) SET search_path = pg_catalog, %I, pg_temp',
|
|
3151
|
+
data_schema, data_schema
|
|
3152
|
+
);
|
|
3153
|
+
EXECUTE format(
|
|
3154
|
+
'ALTER FUNCTION %I.claim_organization_retention_deletion(uuid,uuid,uuid[]) SET search_path = pg_catalog, %I, pg_temp',
|
|
3155
|
+
data_schema, data_schema
|
|
3156
|
+
);
|
|
3157
|
+
EXECUTE format(
|
|
3158
|
+
'ALTER FUNCTION %I.list_organization_retention_deletion_objects(uuid,uuid,uuid,text,integer) SET search_path = pg_catalog, %I, pg_temp',
|
|
3159
|
+
data_schema, data_schema
|
|
3160
|
+
);
|
|
3161
|
+
EXECUTE format(
|
|
3162
|
+
'ALTER FUNCTION %I.record_organization_retention_object_deleted(uuid,uuid,uuid,text,text,text,text) SET search_path = pg_catalog, %I, pg_temp',
|
|
3163
|
+
data_schema, data_schema
|
|
3164
|
+
);
|
|
3165
|
+
EXECUTE format(
|
|
3166
|
+
'ALTER FUNCTION %I.fail_organization_retention_deletion(uuid,uuid,uuid,text) SET search_path = pg_catalog, %I, pg_temp',
|
|
3167
|
+
data_schema, data_schema
|
|
3168
|
+
);
|
|
3169
|
+
EXECUTE format(
|
|
3170
|
+
'ALTER FUNCTION %I.finalize_organization_retention_deletion(uuid,uuid,uuid,text) SET search_path = pg_catalog, %I, pg_temp',
|
|
3171
|
+
data_schema, data_schema
|
|
3172
|
+
);
|
|
3173
|
+
EXECUTE format(
|
|
3174
|
+
'ALTER FUNCTION %I.complete_organization_retention_deletion(uuid,uuid,uuid,text) SET search_path = pg_catalog, %I, pg_temp',
|
|
3175
|
+
data_schema, data_schema
|
|
3176
|
+
);
|
|
3177
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
3178
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.assign_managed_self_organization_owner() TO opengeni_app;
|
|
3179
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.organization_membership_row_json(organization_memberships) TO opengeni_app;
|
|
3180
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.organization_invitation_row_json(organization_membership_invitations) TO opengeni_app;
|
|
3181
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.list_self_organization_memberships(text) TO opengeni_app', data_schema);
|
|
3182
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.list_self_organization_invitations(text) TO opengeni_app', data_schema);
|
|
3183
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.list_self_organization_invitations(text,uuid,integer) TO opengeni_app', data_schema);
|
|
3184
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.get_self_organization_invitation(text,uuid) TO opengeni_app', data_schema);
|
|
3185
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.list_organization_members(uuid,text) TO opengeni_app', data_schema);
|
|
3186
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.list_organization_invitations(uuid,text,uuid,integer) TO opengeni_app', data_schema);
|
|
3187
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.organization_membership_command(jsonb) TO opengeni_app', data_schema);
|
|
3188
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.prepare_organization_membership_protocol_settlements(jsonb) TO opengeni_app', data_schema);
|
|
3189
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.get_organization_retention_policy(uuid,text) TO opengeni_app', data_schema);
|
|
3190
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.assert_active_managed_human_organization_membership(uuid,text) TO opengeni_app', data_schema);
|
|
3191
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.preview_organization_retention_deletions(uuid,integer) TO opengeni_app', data_schema);
|
|
3192
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.claim_organization_retention_deletion(uuid,uuid,uuid[]) TO opengeni_app', data_schema);
|
|
3193
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.list_organization_retention_deletion_objects(uuid,uuid,uuid,text,integer) TO opengeni_app', data_schema);
|
|
3194
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.record_organization_retention_object_deleted(uuid,uuid,uuid,text,text,text,text) TO opengeni_app', data_schema);
|
|
3195
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.fail_organization_retention_deletion(uuid,uuid,uuid,text) TO opengeni_app', data_schema);
|
|
3196
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.finalize_organization_retention_deletion(uuid,uuid,uuid,text) TO opengeni_app', data_schema);
|
|
3197
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.complete_organization_retention_deletion(uuid,uuid,uuid,text) TO opengeni_app', data_schema);
|
|
3198
|
+
END IF;
|
|
3199
|
+
END
|
|
3200
|
+
$pin_and_grant$;
|
|
3201
|
+
|
|
3202
|
+
REVOKE ALL ON FUNCTION opengeni_private.organization_membership_row_json(organization_memberships) FROM PUBLIC;
|
|
3203
|
+
REVOKE ALL ON FUNCTION opengeni_private.organization_invitation_row_json(organization_membership_invitations) FROM PUBLIC;
|
|
3204
|
+
REVOKE ALL ON FUNCTION opengeni_private.assign_managed_self_organization_owner() FROM PUBLIC;
|
|
3205
|
+
REVOKE ALL ON FUNCTION list_self_organization_memberships(text) FROM PUBLIC;
|
|
3206
|
+
REVOKE ALL ON FUNCTION list_self_organization_invitations(text) FROM PUBLIC;
|
|
3207
|
+
REVOKE ALL ON FUNCTION list_self_organization_invitations(text,uuid,integer) FROM PUBLIC;
|
|
3208
|
+
REVOKE ALL ON FUNCTION get_self_organization_invitation(text,uuid) FROM PUBLIC;
|
|
3209
|
+
REVOKE ALL ON FUNCTION list_organization_members(uuid,text) FROM PUBLIC;
|
|
3210
|
+
REVOKE ALL ON FUNCTION list_organization_invitations(uuid,text,uuid,integer) FROM PUBLIC;
|
|
3211
|
+
REVOKE ALL ON FUNCTION organization_membership_command(jsonb) FROM PUBLIC;
|
|
3212
|
+
REVOKE ALL ON FUNCTION prepare_organization_membership_protocol_settlements(jsonb) FROM PUBLIC;
|
|
3213
|
+
REVOKE ALL ON FUNCTION get_organization_retention_policy(uuid,text) FROM PUBLIC;
|
|
3214
|
+
REVOKE ALL ON FUNCTION assert_active_managed_human_organization_membership(uuid,text) FROM PUBLIC;
|
|
3215
|
+
REVOKE ALL ON FUNCTION opengeni_private.assert_organization_retention_account(uuid) FROM PUBLIC;
|
|
3216
|
+
REVOKE ALL ON FUNCTION opengeni_private.organization_retention_file_candidates(uuid,uuid) FROM PUBLIC;
|
|
3217
|
+
REVOKE ALL ON FUNCTION opengeni_private.organization_retention_object_candidates(uuid,uuid) FROM PUBLIC;
|
|
3218
|
+
REVOKE ALL ON FUNCTION preview_organization_retention_deletions(uuid,integer) FROM PUBLIC;
|
|
3219
|
+
REVOKE ALL ON FUNCTION claim_organization_retention_deletion(uuid,uuid,uuid[]) FROM PUBLIC;
|
|
3220
|
+
REVOKE ALL ON FUNCTION list_organization_retention_deletion_objects(uuid,uuid,uuid,text,integer) FROM PUBLIC;
|
|
3221
|
+
REVOKE ALL ON FUNCTION record_organization_retention_object_deleted(uuid,uuid,uuid,text,text,text,text) FROM PUBLIC;
|
|
3222
|
+
REVOKE ALL ON FUNCTION fail_organization_retention_deletion(uuid,uuid,uuid,text) FROM PUBLIC;
|
|
3223
|
+
REVOKE ALL ON FUNCTION finalize_organization_retention_deletion(uuid,uuid,uuid,text) FROM PUBLIC;
|
|
3224
|
+
REVOKE ALL ON FUNCTION complete_organization_retention_deletion(uuid,uuid,uuid,text) FROM PUBLIC;
|
|
3225
|
+
|
|
3226
|
+
REVOKE ALL ON TABLE organization_memberships FROM PUBLIC;
|
|
3227
|
+
REVOKE ALL ON TABLE organization_user_retention_policies FROM PUBLIC;
|
|
3228
|
+
REVOKE ALL ON TABLE organization_membership_invitations FROM PUBLIC;
|
|
3229
|
+
REVOKE ALL ON TABLE organization_membership_operation_receipts FROM PUBLIC;
|
|
3230
|
+
REVOKE ALL ON TABLE organization_user_retention_deletions FROM PUBLIC;
|
|
3231
|
+
REVOKE ALL ON TABLE organization_user_retention_object_obligations FROM PUBLIC;
|
|
3232
|
+
REVOKE ALL ON TABLE organization_user_retention_object_deletion_receipts FROM PUBLIC;
|
|
3233
|
+
REVOKE ALL ON TABLE organization_user_retention_deletion_events FROM PUBLIC;
|
|
3234
|
+
DO $revoke_app_dml$
|
|
3235
|
+
BEGIN
|
|
3236
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
3237
|
+
REVOKE ALL ON TABLE organization_memberships FROM opengeni_app;
|
|
3238
|
+
REVOKE ALL ON TABLE organization_user_retention_policies FROM opengeni_app;
|
|
3239
|
+
REVOKE ALL ON TABLE organization_membership_invitations FROM opengeni_app;
|
|
3240
|
+
REVOKE ALL ON TABLE organization_membership_operation_receipts FROM opengeni_app;
|
|
3241
|
+
REVOKE ALL ON TABLE organization_membership_lifecycle_events FROM opengeni_app;
|
|
3242
|
+
REVOKE ALL ON TABLE organization_user_retention_deletions FROM opengeni_app;
|
|
3243
|
+
REVOKE ALL ON TABLE organization_user_retention_object_obligations FROM opengeni_app;
|
|
3244
|
+
REVOKE ALL ON TABLE organization_user_retention_object_deletion_receipts FROM opengeni_app;
|
|
3245
|
+
REVOKE ALL ON TABLE organization_user_retention_deletion_events FROM opengeni_app;
|
|
3246
|
+
END IF;
|
|
3247
|
+
END
|
|
3248
|
+
$revoke_app_dml$;
|
|
3249
|
+
|
|
3250
|
+
CREATE OR REPLACE FUNCTION opengeni_private.organization_membership_history_immutable()
|
|
3251
|
+
RETURNS trigger LANGUAGE plpgsql SET search_path = pg_catalog AS $body$
|
|
3252
|
+
BEGIN RAISE EXCEPTION 'organization membership history is immutable' USING ERRCODE = '55000'; END
|
|
3253
|
+
$body$;
|
|
3254
|
+
REVOKE ALL ON FUNCTION opengeni_private.organization_membership_history_immutable() FROM PUBLIC;
|
|
3255
|
+
CREATE TRIGGER organization_membership_operation_receipts_immutable
|
|
3256
|
+
BEFORE UPDATE OR DELETE ON organization_membership_operation_receipts
|
|
3257
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.organization_membership_history_immutable();
|
|
3258
|
+
CREATE TRIGGER organization_membership_lifecycle_events_immutable
|
|
3259
|
+
BEFORE UPDATE OR DELETE ON organization_membership_lifecycle_events
|
|
3260
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.organization_membership_history_immutable();
|
|
3261
|
+
CREATE TRIGGER organization_user_retention_object_obligations_immutable
|
|
3262
|
+
BEFORE UPDATE OR DELETE ON organization_user_retention_object_obligations
|
|
3263
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.organization_membership_history_immutable();
|
|
3264
|
+
CREATE TRIGGER organization_user_retention_object_deletion_receipts_immutable
|
|
3265
|
+
BEFORE UPDATE OR DELETE ON organization_user_retention_object_deletion_receipts
|
|
3266
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.organization_membership_history_immutable();
|
|
3267
|
+
CREATE TRIGGER organization_user_retention_deletion_events_immutable
|
|
3268
|
+
BEFORE UPDATE OR DELETE ON organization_user_retention_deletion_events
|
|
3269
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.organization_membership_history_immutable();
|
|
3270
|
+
|
|
3271
|
+
COMMENT ON TABLE organization_membership_invitations IS
|
|
3272
|
+
'Registered-human organization invitations; acceptance is exact-subject and provider delivery is external.';
|
|
3273
|
+
COMMENT ON TABLE organization_membership_operation_receipts IS
|
|
3274
|
+
'Immutable input-bound idempotency receipts for organization membership lifecycle commands.';
|
|
3275
|
+
COMMENT ON TABLE organization_membership_lifecycle_events IS
|
|
3276
|
+
'Immutable value-bounded organization membership lifecycle audit evidence.';
|
|
3277
|
+
COMMENT ON TABLE organization_user_retention_deletions IS
|
|
3278
|
+
'Mutable, claim-fenced state for bounded destructive retention of one expired offboarded member.';
|
|
3279
|
+
COMMENT ON TABLE organization_user_retention_object_obligations IS
|
|
3280
|
+
'Immutable exact-bucket/key cleanup obligations prepared by database-first retention finalization; protected lifecycle capability only.';
|
|
3281
|
+
COMMENT ON TABLE organization_user_retention_object_deletion_receipts IS
|
|
3282
|
+
'Immutable content-free proof that one exact-bucket/key prepared retention cleanup obligation was deleted from external storage.';
|
|
3283
|
+
COMMENT ON TABLE organization_user_retention_deletion_events IS
|
|
3284
|
+
'Immutable content-free claim, failure and completion evidence for membership retention deletion.';
|