@opengeni/db 1.0.1 → 3.2.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-4SR4HKKA.js → chunk-2EH4WRHL.js} +19 -11
- package/dist/chunk-2EH4WRHL.js.map +1 -0
- package/dist/{chunk-CUPM3FGB.js → chunk-2PRS4J5Q.js} +3 -3
- package/dist/chunk-6HMM2R5T.js +390 -0
- package/dist/chunk-6HMM2R5T.js.map +1 -0
- package/dist/{chunk-ZWREYEWA.js → chunk-AZHW7R5V.js} +56 -11
- package/dist/chunk-AZHW7R5V.js.map +1 -0
- package/dist/{chunk-LTG3S6ZY.js → chunk-E6CLMKEE.js} +2 -2
- package/dist/{chunk-6ZYKCRFO.js → chunk-KRLPEMLH.js} +7591 -4417
- package/dist/chunk-KRLPEMLH.js.map +1 -0
- package/dist/chunk-NAB6FKFN.js +516 -0
- package/dist/chunk-NAB6FKFN.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-UZNXG44K.js +4508 -0
- package/dist/chunk-UZNXG44K.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 +85 -0
- package/dist/connection-token-resolver.d.ts +30 -1
- package/dist/database.d.ts +23 -4
- package/dist/editable-artifact-durable-export.js +2 -2
- 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 +1606 -81
- package/dist/index.js +21898 -8147
- 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/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 +29 -8
- package/dist/schema.d.ts +24914 -13703
- package/dist/schema.js +99 -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 +43 -4
- package/dist/session-realtime-ledger.d.ts +2 -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 +268 -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 +28 -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/0241_atomic_personal_resource_delegation.sql +1000 -0
- package/drizzle/0241_enrollment_agent_runtime.sql +70 -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/drizzle/0342_slack_route_prompt_single_pending.sql +32 -0
- package/drizzle/0343_personal_document_force_rls_lock_repair.sql +146 -0
- package/drizzle/0344_private_session_visibility_transition_gate.sql +85 -0
- package/drizzle/0345_tenant_scoped_session_tenancy_fence.sql +2839 -0
- package/drizzle/0346_document_migration_audit_surface.sql +454 -0
- package/drizzle/0347_connection_authority_convergence_evidence.sql +396 -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 +167 -0
- package/src/connection-token-resolver.ts +263 -31
- package/src/database.ts +87 -2
- 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 +11676 -1550
- 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/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 +995 -0
- package/src/pr-review.ts +890 -0
- package/src/preference-registry.ts +31 -1
- package/src/provision-roles.ts +905 -6
- package/src/runtime-posture-cli.ts +2 -0
- package/src/runtime-posture.ts +1317 -72
- package/src/schema.ts +3316 -428
- 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 +441 -85
- package/src/session-realtime-context.ts +4 -6
- package/src/session-realtime-ledger.ts +28 -14
- package/src/session-realtime-mirror.ts +1 -1
- package/src/session-tenancy.ts +410 -49
- package/src/session-tool-call-settlement.ts +38 -29
- package/src/slack-routing-personal-workspace.ts +67 -0
- package/src/slack-routing.ts +867 -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 +138 -11
- 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-4SR4HKKA.js.map +0 -1
- package/dist/chunk-6ZYKCRFO.js.map +0 -1
- package/dist/chunk-NXAFLCQX.js +0 -2468
- package/dist/chunk-NXAFLCQX.js.map +0 -1
- package/dist/chunk-SK7YYHBI.js.map +0 -1
- package/dist/chunk-WD5ROSLL.js +0 -117
- package/dist/chunk-WD5ROSLL.js.map +0 -1
- package/dist/chunk-ZWREYEWA.js.map +0 -1
- /package/dist/{chunk-CUPM3FGB.js.map → chunk-2PRS4J5Q.js.map} +0 -0
- /package/dist/{chunk-LTG3S6ZY.js.map → chunk-E6CLMKEE.js.map} +0 -0
|
@@ -0,0 +1,1153 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Explicit, replay-safe Document authority reclassification and resumable
|
|
3
|
+
-- Default collection backfill. Existing authority is never rewritten by this
|
|
4
|
+
-- migration: legacy workspace rows stay workspace-scoped and legacy personal
|
|
5
|
+
-- rows stay anchored to their original workspace until this lifecycle is
|
|
6
|
+
-- called with an exact expected tuple.
|
|
7
|
+
|
|
8
|
+
SET LOCAL lock_timeout = '5s';
|
|
9
|
+
SET LOCAL statement_timeout = '10min';
|
|
10
|
+
|
|
11
|
+
CREATE TABLE document_authority_reclassifications (
|
|
12
|
+
operation_id uuid PRIMARY KEY,
|
|
13
|
+
input_hash text NOT NULL,
|
|
14
|
+
account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE RESTRICT,
|
|
15
|
+
request_workspace_id uuid NOT NULL,
|
|
16
|
+
document_id uuid NOT NULL,
|
|
17
|
+
actor_subject_id text NOT NULL,
|
|
18
|
+
previous_authority_kind text NOT NULL,
|
|
19
|
+
previous_authority_workspace_id uuid,
|
|
20
|
+
previous_authority_subject_id text,
|
|
21
|
+
previous_authority_id uuid,
|
|
22
|
+
previous_owner_organization_membership_id uuid,
|
|
23
|
+
resulting_authority_kind text NOT NULL,
|
|
24
|
+
resulting_authority_workspace_id uuid,
|
|
25
|
+
resulting_authority_subject_id text,
|
|
26
|
+
resulting_authority_id uuid,
|
|
27
|
+
resulting_owner_organization_membership_id uuid,
|
|
28
|
+
result jsonb NOT NULL,
|
|
29
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
30
|
+
-- The receipt is workspace-owned evidence, so it dies with its workspace.
|
|
31
|
+
-- RESTRICT here would make any workspace in which a single Document was ever
|
|
32
|
+
-- reclassified permanently undeletable: `deleteWorkspaceIfQuiescent` relies on
|
|
33
|
+
-- cascades and has no quiescence branch for these tables, so the FK would
|
|
34
|
+
-- surface as an untyped 500 on `DELETE /v1/workspaces/:workspaceId` rather
|
|
35
|
+
-- than a typed status. The row-level immutability trigger below still refuses
|
|
36
|
+
-- every direct rewrite; only the referential cascade may remove it.
|
|
37
|
+
CONSTRAINT document_authority_reclassifications_workspace_fk
|
|
38
|
+
FOREIGN KEY (request_workspace_id, account_id)
|
|
39
|
+
REFERENCES workspaces(id, account_id) ON DELETE CASCADE,
|
|
40
|
+
CONSTRAINT document_authority_reclassifications_hash_chk CHECK (
|
|
41
|
+
input_hash ~ '^[0-9a-f]{64}$'
|
|
42
|
+
),
|
|
43
|
+
CONSTRAINT document_authority_reclassifications_actor_chk CHECK (
|
|
44
|
+
actor_subject_id = btrim(actor_subject_id)
|
|
45
|
+
AND octet_length(convert_to(actor_subject_id, 'UTF8')) BETWEEN 1 AND 1024
|
|
46
|
+
),
|
|
47
|
+
CONSTRAINT document_authority_reclassifications_kind_chk CHECK (
|
|
48
|
+
previous_authority_kind IN ('organization', 'workspace', 'personal')
|
|
49
|
+
AND resulting_authority_kind IN ('organization', 'workspace', 'personal')
|
|
50
|
+
),
|
|
51
|
+
CONSTRAINT document_authority_reclassifications_result_chk CHECK (
|
|
52
|
+
jsonb_typeof(result) = 'object'
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
CREATE INDEX document_authority_reclassifications_document_idx
|
|
56
|
+
ON document_authority_reclassifications (
|
|
57
|
+
account_id, request_workspace_id, document_id, created_at, operation_id
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE document_default_collection_backfill_runs (
|
|
61
|
+
run_id uuid PRIMARY KEY,
|
|
62
|
+
account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE RESTRICT,
|
|
63
|
+
actor_subject_id text NOT NULL,
|
|
64
|
+
status text NOT NULL DEFAULT 'running',
|
|
65
|
+
last_workspace_id uuid,
|
|
66
|
+
processed_count integer NOT NULL DEFAULT 0,
|
|
67
|
+
created_count integer NOT NULL DEFAULT 0,
|
|
68
|
+
adopted_count integer NOT NULL DEFAULT 0,
|
|
69
|
+
started_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
70
|
+
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
71
|
+
completed_at timestamptz,
|
|
72
|
+
CONSTRAINT document_default_collection_backfill_runs_actor_chk CHECK (
|
|
73
|
+
actor_subject_id = btrim(actor_subject_id)
|
|
74
|
+
AND octet_length(convert_to(actor_subject_id, 'UTF8')) BETWEEN 1 AND 1024
|
|
75
|
+
),
|
|
76
|
+
CONSTRAINT document_default_collection_backfill_runs_status_chk CHECK (
|
|
77
|
+
(status = 'running' AND completed_at IS NULL)
|
|
78
|
+
OR (status = 'completed' AND completed_at IS NOT NULL)
|
|
79
|
+
),
|
|
80
|
+
CONSTRAINT document_default_collection_backfill_runs_count_chk CHECK (
|
|
81
|
+
processed_count >= 0 AND created_count >= 0 AND adopted_count >= 0
|
|
82
|
+
AND processed_count = created_count + adopted_count
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE TABLE document_default_collection_backfill_operations (
|
|
87
|
+
operation_id uuid PRIMARY KEY,
|
|
88
|
+
run_id uuid NOT NULL REFERENCES document_default_collection_backfill_runs(run_id)
|
|
89
|
+
ON DELETE RESTRICT,
|
|
90
|
+
account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE RESTRICT,
|
|
91
|
+
input_hash text NOT NULL,
|
|
92
|
+
result jsonb NOT NULL,
|
|
93
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
94
|
+
CONSTRAINT document_default_collection_backfill_operations_hash_chk CHECK (
|
|
95
|
+
input_hash ~ '^[0-9a-f]{64}$'
|
|
96
|
+
),
|
|
97
|
+
CONSTRAINT document_default_collection_backfill_operations_result_chk CHECK (
|
|
98
|
+
jsonb_typeof(result) = 'object'
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE document_default_collection_backfill_receipts (
|
|
103
|
+
run_id uuid NOT NULL REFERENCES document_default_collection_backfill_runs(run_id)
|
|
104
|
+
ON DELETE RESTRICT,
|
|
105
|
+
account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE RESTRICT,
|
|
106
|
+
workspace_id uuid NOT NULL,
|
|
107
|
+
-- CASCADE on both parents. One admin-triggered backfill writes a receipt for
|
|
108
|
+
-- EVERY workspace in the account, so RESTRICT would pin the whole account as
|
|
109
|
+
-- undeletable after a single run - and `document_bases.workspace_id` already
|
|
110
|
+
-- cascades, so a RESTRICT on `base_id` would block that cascade too. The
|
|
111
|
+
-- receipt describes a collection inside the workspace; once the workspace,
|
|
112
|
+
-- its bases, and its documents are gone the receipt is evidence about
|
|
113
|
+
-- nothing. Direct rewrites stay refused by the immutability trigger below.
|
|
114
|
+
base_id uuid NOT NULL REFERENCES document_bases(id) ON DELETE CASCADE,
|
|
115
|
+
outcome text NOT NULL,
|
|
116
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
117
|
+
PRIMARY KEY (run_id, workspace_id),
|
|
118
|
+
CONSTRAINT document_default_collection_backfill_receipts_workspace_fk
|
|
119
|
+
FOREIGN KEY (workspace_id, account_id)
|
|
120
|
+
REFERENCES workspaces(id, account_id) ON DELETE CASCADE,
|
|
121
|
+
CONSTRAINT document_default_collection_backfill_receipts_outcome_chk CHECK (
|
|
122
|
+
outcome IN ('created', 'adopted')
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
CREATE TABLE opengeni_private.document_migration_capabilities (
|
|
127
|
+
backend_pid integer NOT NULL,
|
|
128
|
+
transaction_id xid8 NOT NULL,
|
|
129
|
+
capability_kind text NOT NULL,
|
|
130
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
131
|
+
PRIMARY KEY (backend_pid, transaction_id, capability_kind),
|
|
132
|
+
CONSTRAINT document_migration_capabilities_kind_chk CHECK (
|
|
133
|
+
capability_kind IN ('reclassify', 'default_backfill')
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
REVOKE ALL ON TABLE opengeni_private.document_migration_capabilities FROM PUBLIC;
|
|
137
|
+
|
|
138
|
+
CREATE OR REPLACE FUNCTION opengeni_private.document_migration_capability_active(
|
|
139
|
+
p_capability_kind text
|
|
140
|
+
) RETURNS boolean
|
|
141
|
+
LANGUAGE sql
|
|
142
|
+
STABLE
|
|
143
|
+
SECURITY DEFINER
|
|
144
|
+
SET search_path = pg_catalog
|
|
145
|
+
AS $body$
|
|
146
|
+
SELECT EXISTS (
|
|
147
|
+
SELECT 1
|
|
148
|
+
FROM opengeni_private.document_migration_capabilities capability
|
|
149
|
+
WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
|
|
150
|
+
AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
151
|
+
AND capability.capability_kind = p_capability_kind
|
|
152
|
+
)
|
|
153
|
+
$body$;
|
|
154
|
+
REVOKE ALL ON FUNCTION
|
|
155
|
+
opengeni_private.document_migration_capability_active(text) FROM PUBLIC;
|
|
156
|
+
|
|
157
|
+
ALTER TABLE document_authority_reclassifications ENABLE ROW LEVEL SECURITY;
|
|
158
|
+
ALTER TABLE document_authority_reclassifications FORCE ROW LEVEL SECURITY;
|
|
159
|
+
ALTER TABLE document_default_collection_backfill_runs ENABLE ROW LEVEL SECURITY;
|
|
160
|
+
ALTER TABLE document_default_collection_backfill_runs FORCE ROW LEVEL SECURITY;
|
|
161
|
+
ALTER TABLE document_default_collection_backfill_operations ENABLE ROW LEVEL SECURITY;
|
|
162
|
+
ALTER TABLE document_default_collection_backfill_operations FORCE ROW LEVEL SECURITY;
|
|
163
|
+
ALTER TABLE document_default_collection_backfill_receipts ENABLE ROW LEVEL SECURITY;
|
|
164
|
+
ALTER TABLE document_default_collection_backfill_receipts FORCE ROW LEVEL SECURITY;
|
|
165
|
+
|
|
166
|
+
DO $document_migration_policies$
|
|
167
|
+
DECLARE
|
|
168
|
+
data_schema text := current_schema();
|
|
169
|
+
migration_owner text := current_user;
|
|
170
|
+
table_name text;
|
|
171
|
+
BEGIN
|
|
172
|
+
EXECUTE format(
|
|
173
|
+
'CREATE POLICY document_reclassification_actor_read ON %I.document_authority_reclassifications '
|
|
174
|
+
|| 'FOR SELECT USING (account_id = nullif(current_setting(''opengeni.account_id'', true), '''')::uuid '
|
|
175
|
+
|| 'AND request_workspace_id = nullif(current_setting(''opengeni.workspace_id'', true), '''')::uuid '
|
|
176
|
+
|| 'AND actor_subject_id = nullif(current_setting(''opengeni.subject_id'', true), ''''))',
|
|
177
|
+
data_schema
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
FOREACH table_name IN ARRAY ARRAY[
|
|
181
|
+
'document_authority_reclassifications',
|
|
182
|
+
'document_default_collection_backfill_runs',
|
|
183
|
+
'document_default_collection_backfill_operations',
|
|
184
|
+
'document_default_collection_backfill_receipts'
|
|
185
|
+
] LOOP
|
|
186
|
+
EXECUTE format(
|
|
187
|
+
'CREATE POLICY document_migration_lifecycle ON %I.%I '
|
|
188
|
+
|| 'USING (current_user = %L AND opengeni_private.document_migration_capability_active('
|
|
189
|
+
|| 'CASE WHEN %L = ''document_authority_reclassifications'' THEN ''reclassify'' '
|
|
190
|
+
|| 'ELSE ''default_backfill'' END)) '
|
|
191
|
+
|| 'WITH CHECK (current_user = %L AND opengeni_private.document_migration_capability_active('
|
|
192
|
+
|| 'CASE WHEN %L = ''document_authority_reclassifications'' THEN ''reclassify'' '
|
|
193
|
+
|| 'ELSE ''default_backfill'' END))',
|
|
194
|
+
data_schema, table_name, migration_owner, table_name, migration_owner, table_name
|
|
195
|
+
);
|
|
196
|
+
END LOOP;
|
|
197
|
+
|
|
198
|
+
FOREACH table_name IN ARRAY ARRAY['documents', 'document_chunks'] LOOP
|
|
199
|
+
EXECUTE format(
|
|
200
|
+
'CREATE POLICY document_reclassification_lifecycle ON %I.%I '
|
|
201
|
+
|| 'USING (current_user = %L AND '
|
|
202
|
+
|| 'opengeni_private.document_migration_capability_active(''reclassify'')) '
|
|
203
|
+
|| 'WITH CHECK (current_user = %L AND '
|
|
204
|
+
|| 'opengeni_private.document_migration_capability_active(''reclassify''))',
|
|
205
|
+
data_schema, table_name, migration_owner, migration_owner
|
|
206
|
+
);
|
|
207
|
+
END LOOP;
|
|
208
|
+
|
|
209
|
+
FOREACH table_name IN ARRAY ARRAY['workspaces', 'document_bases'] LOOP
|
|
210
|
+
EXECUTE format(
|
|
211
|
+
'CREATE POLICY document_default_backfill_lifecycle ON %I.%I '
|
|
212
|
+
|| 'USING (current_user = %L AND '
|
|
213
|
+
|| 'opengeni_private.document_migration_capability_active(''default_backfill'')) '
|
|
214
|
+
|| 'WITH CHECK (current_user = %L AND '
|
|
215
|
+
|| 'opengeni_private.document_migration_capability_active(''default_backfill''))',
|
|
216
|
+
data_schema, table_name, migration_owner, migration_owner
|
|
217
|
+
);
|
|
218
|
+
END LOOP;
|
|
219
|
+
|
|
220
|
+
FOREACH table_name IN ARRAY ARRAY[
|
|
221
|
+
'organization_user_resource_authorities',
|
|
222
|
+
'organization_user_resource_grants'
|
|
223
|
+
] LOOP
|
|
224
|
+
EXECUTE format(
|
|
225
|
+
'CREATE POLICY document_reclassification_lifecycle ON %I.%I '
|
|
226
|
+
|| 'USING (current_user = %L AND '
|
|
227
|
+
|| 'opengeni_private.document_migration_capability_active(''reclassify'')) '
|
|
228
|
+
|| 'WITH CHECK (current_user = %L AND '
|
|
229
|
+
|| 'opengeni_private.document_migration_capability_active(''reclassify''))',
|
|
230
|
+
data_schema, table_name, migration_owner, migration_owner
|
|
231
|
+
);
|
|
232
|
+
END LOOP;
|
|
233
|
+
END
|
|
234
|
+
$document_migration_policies$;
|
|
235
|
+
|
|
236
|
+
CREATE FUNCTION opengeni_private.reject_document_migration_receipt_mutation()
|
|
237
|
+
RETURNS trigger
|
|
238
|
+
LANGUAGE plpgsql
|
|
239
|
+
AS $body$
|
|
240
|
+
BEGIN
|
|
241
|
+
-- A receipt is never rewritten and is never removed on its own. The single
|
|
242
|
+
-- exception is the referential cascade of an owning parent (its workspace, or
|
|
243
|
+
-- the Default base that workspace cascade already removes), which is what
|
|
244
|
+
-- keeps `DELETE /v1/workspaces/:workspaceId` working after this migration's
|
|
245
|
+
-- lifecycle has run. `pg_trigger_depth()` is the exact, RLS-immune
|
|
246
|
+
-- discriminator: a direct statement enters this trigger at depth 1, while a
|
|
247
|
+
-- referential action is itself executed from the parent's constraint trigger
|
|
248
|
+
-- and therefore enters at depth 2 or deeper. Checking whether the parent row
|
|
249
|
+
-- still exists would NOT work here - this trigger runs as the invoking role,
|
|
250
|
+
-- and `workspaces` looks empty to a FORCE-RLS-bound owner with no tenant GUC.
|
|
251
|
+
IF TG_OP = 'DELETE' AND pg_trigger_depth() > 1 THEN
|
|
252
|
+
RETURN OLD;
|
|
253
|
+
END IF;
|
|
254
|
+
RAISE EXCEPTION 'document migration receipts are immutable' USING ERRCODE = '55000';
|
|
255
|
+
END
|
|
256
|
+
$body$;
|
|
257
|
+
-- House pattern for an `opengeni_private` trigger function: it is invoked by the
|
|
258
|
+
-- trigger, never called directly, so leaving it PUBLIC-executable widens the
|
|
259
|
+
-- grant surface for no reason. `0333_session_turn_prompt_routing.sql` revokes
|
|
260
|
+
-- its equivalent the same way, and the dedicated editable-artifact materializer
|
|
261
|
+
-- role's grant enumeration pins exactly this.
|
|
262
|
+
REVOKE ALL ON FUNCTION opengeni_private.reject_document_migration_receipt_mutation() FROM PUBLIC;
|
|
263
|
+
|
|
264
|
+
CREATE TRIGGER document_authority_reclassifications_immutable
|
|
265
|
+
BEFORE UPDATE OR DELETE ON document_authority_reclassifications
|
|
266
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.reject_document_migration_receipt_mutation();
|
|
267
|
+
CREATE TRIGGER document_default_collection_backfill_operations_immutable
|
|
268
|
+
BEFORE UPDATE OR DELETE ON document_default_collection_backfill_operations
|
|
269
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.reject_document_migration_receipt_mutation();
|
|
270
|
+
CREATE TRIGGER document_default_collection_backfill_receipts_immutable
|
|
271
|
+
BEFORE UPDATE OR DELETE ON document_default_collection_backfill_receipts
|
|
272
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.reject_document_migration_receipt_mutation();
|
|
273
|
+
|
|
274
|
+
CREATE OR REPLACE FUNCTION opengeni_private.apply_document_authority()
|
|
275
|
+
RETURNS trigger
|
|
276
|
+
LANGUAGE plpgsql
|
|
277
|
+
SET search_path FROM CURRENT
|
|
278
|
+
AS $body$
|
|
279
|
+
DECLARE
|
|
280
|
+
reclassification_allowed boolean := false;
|
|
281
|
+
BEGIN
|
|
282
|
+
IF TG_OP = 'UPDATE' AND (
|
|
283
|
+
NEW.authority_kind IS DISTINCT FROM OLD.authority_kind
|
|
284
|
+
OR NEW.authority_workspace_id IS DISTINCT FROM OLD.authority_workspace_id
|
|
285
|
+
OR NEW.authority_subject_id IS DISTINCT FROM OLD.authority_subject_id
|
|
286
|
+
OR NEW.authority_id IS DISTINCT FROM OLD.authority_id
|
|
287
|
+
OR NEW.owner_organization_membership_id
|
|
288
|
+
IS DISTINCT FROM OLD.owner_organization_membership_id
|
|
289
|
+
OR NEW.origin_workspace_id IS DISTINCT FROM OLD.origin_workspace_id
|
|
290
|
+
OR NEW.created_by IS DISTINCT FROM OLD.created_by
|
|
291
|
+
OR NEW.visibility IS DISTINCT FROM OLD.visibility
|
|
292
|
+
) THEN
|
|
293
|
+
IF opengeni_private.document_migration_capability_active('reclassify') THEN
|
|
294
|
+
SELECT EXISTS (
|
|
295
|
+
SELECT 1
|
|
296
|
+
FROM document_authority_reclassifications receipt
|
|
297
|
+
WHERE receipt.document_id = OLD.id
|
|
298
|
+
AND receipt.account_id = OLD.account_id
|
|
299
|
+
AND receipt.previous_authority_kind = OLD.authority_kind
|
|
300
|
+
AND receipt.previous_authority_workspace_id
|
|
301
|
+
IS NOT DISTINCT FROM OLD.authority_workspace_id
|
|
302
|
+
AND receipt.previous_authority_subject_id
|
|
303
|
+
IS NOT DISTINCT FROM OLD.authority_subject_id
|
|
304
|
+
AND receipt.previous_authority_id IS NOT DISTINCT FROM OLD.authority_id
|
|
305
|
+
AND receipt.previous_owner_organization_membership_id
|
|
306
|
+
IS NOT DISTINCT FROM OLD.owner_organization_membership_id
|
|
307
|
+
AND receipt.resulting_authority_kind = NEW.authority_kind
|
|
308
|
+
AND receipt.resulting_authority_workspace_id
|
|
309
|
+
IS NOT DISTINCT FROM NEW.authority_workspace_id
|
|
310
|
+
AND receipt.resulting_authority_subject_id
|
|
311
|
+
IS NOT DISTINCT FROM NEW.authority_subject_id
|
|
312
|
+
AND receipt.resulting_authority_id IS NOT DISTINCT FROM NEW.authority_id
|
|
313
|
+
AND receipt.resulting_owner_organization_membership_id
|
|
314
|
+
IS NOT DISTINCT FROM NEW.owner_organization_membership_id
|
|
315
|
+
) INTO reclassification_allowed;
|
|
316
|
+
END IF;
|
|
317
|
+
IF NOT reclassification_allowed
|
|
318
|
+
OR NEW.origin_workspace_id IS DISTINCT FROM OLD.origin_workspace_id
|
|
319
|
+
OR NEW.created_by IS DISTINCT FROM OLD.created_by
|
|
320
|
+
THEN
|
|
321
|
+
RAISE EXCEPTION 'document authority is immutable';
|
|
322
|
+
END IF;
|
|
323
|
+
END IF;
|
|
324
|
+
IF NEW.authority_kind IS NULL OR (
|
|
325
|
+
NEW.authority_kind = 'workspace'
|
|
326
|
+
AND NEW.authority_workspace_id IS NULL
|
|
327
|
+
AND NEW.visibility = 'private'
|
|
328
|
+
) THEN
|
|
329
|
+
NEW.authority_kind := CASE
|
|
330
|
+
WHEN NEW.visibility = 'private' THEN 'personal'
|
|
331
|
+
ELSE 'workspace'
|
|
332
|
+
END;
|
|
333
|
+
END IF;
|
|
334
|
+
NEW.origin_workspace_id := NEW.workspace_id;
|
|
335
|
+
CASE NEW.authority_kind
|
|
336
|
+
WHEN 'organization' THEN
|
|
337
|
+
NEW.authority_workspace_id := NULL;
|
|
338
|
+
NEW.authority_subject_id := NULL;
|
|
339
|
+
NEW.authority_id := NULL;
|
|
340
|
+
NEW.owner_organization_membership_id := NULL;
|
|
341
|
+
NEW.visibility := 'workspace';
|
|
342
|
+
WHEN 'workspace' THEN
|
|
343
|
+
NEW.authority_workspace_id := NEW.workspace_id;
|
|
344
|
+
NEW.authority_subject_id := NULL;
|
|
345
|
+
NEW.authority_id := NULL;
|
|
346
|
+
NEW.owner_organization_membership_id := NULL;
|
|
347
|
+
NEW.visibility := 'workspace';
|
|
348
|
+
WHEN 'personal' THEN
|
|
349
|
+
NEW.authority_subject_id := coalesce(
|
|
350
|
+
nullif(btrim(NEW.authority_subject_id), ''),
|
|
351
|
+
nullif(btrim(NEW.created_by), '')
|
|
352
|
+
);
|
|
353
|
+
IF NEW.authority_id IS NULL THEN
|
|
354
|
+
NEW.authority_workspace_id := NEW.workspace_id;
|
|
355
|
+
NEW.owner_organization_membership_id := NULL;
|
|
356
|
+
ELSE
|
|
357
|
+
NEW.authority_workspace_id := NULL;
|
|
358
|
+
END IF;
|
|
359
|
+
NEW.visibility := 'private';
|
|
360
|
+
ELSE
|
|
361
|
+
RAISE EXCEPTION 'invalid document authority kind: %', NEW.authority_kind;
|
|
362
|
+
END CASE;
|
|
363
|
+
RETURN NEW;
|
|
364
|
+
END
|
|
365
|
+
$body$;
|
|
366
|
+
|
|
367
|
+
CREATE OR REPLACE FUNCTION opengeni_private.apply_document_chunk_authority()
|
|
368
|
+
RETURNS trigger
|
|
369
|
+
LANGUAGE plpgsql
|
|
370
|
+
SET search_path FROM CURRENT
|
|
371
|
+
AS $body$
|
|
372
|
+
DECLARE
|
|
373
|
+
parent documents%ROWTYPE;
|
|
374
|
+
reclassification_allowed boolean := false;
|
|
375
|
+
BEGIN
|
|
376
|
+
IF TG_OP = 'UPDATE' AND (
|
|
377
|
+
NEW.authority_kind IS DISTINCT FROM OLD.authority_kind
|
|
378
|
+
OR NEW.authority_workspace_id IS DISTINCT FROM OLD.authority_workspace_id
|
|
379
|
+
OR NEW.authority_subject_id IS DISTINCT FROM OLD.authority_subject_id
|
|
380
|
+
OR NEW.authority_id IS DISTINCT FROM OLD.authority_id
|
|
381
|
+
OR NEW.owner_organization_membership_id
|
|
382
|
+
IS DISTINCT FROM OLD.owner_organization_membership_id
|
|
383
|
+
OR NEW.document_id IS DISTINCT FROM OLD.document_id
|
|
384
|
+
) THEN
|
|
385
|
+
IF opengeni_private.document_migration_capability_active('reclassify') THEN
|
|
386
|
+
SELECT EXISTS (
|
|
387
|
+
SELECT 1
|
|
388
|
+
FROM document_authority_reclassifications receipt
|
|
389
|
+
WHERE receipt.document_id = OLD.document_id
|
|
390
|
+
AND receipt.account_id = OLD.account_id
|
|
391
|
+
AND receipt.previous_authority_kind = OLD.authority_kind
|
|
392
|
+
AND receipt.previous_authority_workspace_id
|
|
393
|
+
IS NOT DISTINCT FROM OLD.authority_workspace_id
|
|
394
|
+
AND receipt.previous_authority_subject_id
|
|
395
|
+
IS NOT DISTINCT FROM OLD.authority_subject_id
|
|
396
|
+
AND receipt.previous_authority_id IS NOT DISTINCT FROM OLD.authority_id
|
|
397
|
+
AND receipt.previous_owner_organization_membership_id
|
|
398
|
+
IS NOT DISTINCT FROM OLD.owner_organization_membership_id
|
|
399
|
+
AND receipt.resulting_authority_kind = NEW.authority_kind
|
|
400
|
+
AND receipt.resulting_authority_workspace_id
|
|
401
|
+
IS NOT DISTINCT FROM NEW.authority_workspace_id
|
|
402
|
+
AND receipt.resulting_authority_subject_id
|
|
403
|
+
IS NOT DISTINCT FROM NEW.authority_subject_id
|
|
404
|
+
AND receipt.resulting_authority_id IS NOT DISTINCT FROM NEW.authority_id
|
|
405
|
+
AND receipt.resulting_owner_organization_membership_id
|
|
406
|
+
IS NOT DISTINCT FROM NEW.owner_organization_membership_id
|
|
407
|
+
) INTO reclassification_allowed;
|
|
408
|
+
END IF;
|
|
409
|
+
IF NOT reclassification_allowed OR NEW.document_id IS DISTINCT FROM OLD.document_id THEN
|
|
410
|
+
RAISE EXCEPTION 'document chunk authority is immutable';
|
|
411
|
+
END IF;
|
|
412
|
+
END IF;
|
|
413
|
+
SELECT * INTO parent FROM documents WHERE id = NEW.document_id;
|
|
414
|
+
IF NOT FOUND
|
|
415
|
+
OR parent.account_id IS DISTINCT FROM NEW.account_id
|
|
416
|
+
OR parent.workspace_id IS DISTINCT FROM NEW.workspace_id
|
|
417
|
+
OR parent.base_id IS DISTINCT FROM NEW.base_id
|
|
418
|
+
OR parent.file_id IS DISTINCT FROM NEW.file_id
|
|
419
|
+
THEN
|
|
420
|
+
RAISE EXCEPTION 'document chunk parent identity mismatch';
|
|
421
|
+
END IF;
|
|
422
|
+
NEW.authority_kind := parent.authority_kind;
|
|
423
|
+
NEW.authority_workspace_id := parent.authority_workspace_id;
|
|
424
|
+
NEW.authority_subject_id := parent.authority_subject_id;
|
|
425
|
+
NEW.authority_id := parent.authority_id;
|
|
426
|
+
NEW.owner_organization_membership_id := parent.owner_organization_membership_id;
|
|
427
|
+
RETURN NEW;
|
|
428
|
+
END
|
|
429
|
+
$body$;
|
|
430
|
+
|
|
431
|
+
CREATE FUNCTION reclassify_document_authority(p_command jsonb)
|
|
432
|
+
RETURNS jsonb
|
|
433
|
+
LANGUAGE plpgsql
|
|
434
|
+
SECURITY DEFINER
|
|
435
|
+
SET search_path FROM CURRENT
|
|
436
|
+
AS $body$
|
|
437
|
+
DECLARE
|
|
438
|
+
account_id_value uuid := nullif(p_command ->> 'accountId', '')::uuid;
|
|
439
|
+
request_workspace_id_value uuid := nullif(p_command ->> 'workspaceId', '')::uuid;
|
|
440
|
+
document_id_value uuid := nullif(p_command ->> 'documentId', '')::uuid;
|
|
441
|
+
operation_id_value uuid := nullif(p_command ->> 'operationId', '')::uuid;
|
|
442
|
+
actor_subject text := nullif(p_command ->> 'actorSubjectId', '');
|
|
443
|
+
expected_kind text := p_command #>> '{expectedAuthority,kind}';
|
|
444
|
+
expected_workspace_id uuid := nullif(
|
|
445
|
+
p_command #>> '{expectedAuthority,workspaceId}', ''
|
|
446
|
+
)::uuid;
|
|
447
|
+
expected_subject_id text := nullif(
|
|
448
|
+
p_command #>> '{expectedAuthority,subjectId}', ''
|
|
449
|
+
);
|
|
450
|
+
expected_authority_id uuid := nullif(
|
|
451
|
+
p_command #>> '{expectedAuthority,authorityId}', ''
|
|
452
|
+
)::uuid;
|
|
453
|
+
target_kind text := p_command ->> 'targetAuthorityKind';
|
|
454
|
+
account_admin_authorization jsonb := p_command -> 'accountAdminAuthorization';
|
|
455
|
+
account_admin_authorized boolean := false;
|
|
456
|
+
input_hash_value text;
|
|
457
|
+
existing_receipt document_authority_reclassifications%ROWTYPE;
|
|
458
|
+
document_row documents%ROWTYPE;
|
|
459
|
+
owner_member organization_memberships%ROWTYPE;
|
|
460
|
+
target_workspace_id uuid;
|
|
461
|
+
target_subject_id text;
|
|
462
|
+
target_authority_id uuid;
|
|
463
|
+
target_owner_membership_id uuid;
|
|
464
|
+
old_authority_id uuid;
|
|
465
|
+
receipt_created_at timestamptz;
|
|
466
|
+
result_value jsonb;
|
|
467
|
+
BEGIN
|
|
468
|
+
IF p_command IS NULL THEN
|
|
469
|
+
RAISE EXCEPTION 'document reclassification command is required' USING ERRCODE = '22023';
|
|
470
|
+
ELSIF account_id_value IS NULL THEN
|
|
471
|
+
RAISE EXCEPTION 'document reclassification account id is required' USING ERRCODE = '22023';
|
|
472
|
+
ELSIF request_workspace_id_value IS NULL THEN
|
|
473
|
+
RAISE EXCEPTION 'document reclassification workspace id is required' USING ERRCODE = '22023';
|
|
474
|
+
ELSIF document_id_value IS NULL THEN
|
|
475
|
+
RAISE EXCEPTION 'document reclassification document id is required' USING ERRCODE = '22023';
|
|
476
|
+
ELSIF operation_id_value IS NULL THEN
|
|
477
|
+
RAISE EXCEPTION 'document reclassification operation id is required' USING ERRCODE = '22023';
|
|
478
|
+
ELSIF actor_subject IS NULL THEN
|
|
479
|
+
RAISE EXCEPTION 'document reclassification actor is required' USING ERRCODE = '22023';
|
|
480
|
+
END IF;
|
|
481
|
+
IF actor_subject IS DISTINCT FROM nullif(current_setting('opengeni.subject_id', true), '') THEN
|
|
482
|
+
RAISE EXCEPTION 'document reclassification actor authority is invalid'
|
|
483
|
+
USING ERRCODE = '42501';
|
|
484
|
+
END IF;
|
|
485
|
+
IF account_id_value IS DISTINCT FROM nullif(
|
|
486
|
+
current_setting('opengeni.account_id', true), ''
|
|
487
|
+
)::uuid
|
|
488
|
+
THEN
|
|
489
|
+
RAISE EXCEPTION 'document reclassification account authority is invalid'
|
|
490
|
+
USING ERRCODE = '42501';
|
|
491
|
+
END IF;
|
|
492
|
+
IF request_workspace_id_value IS DISTINCT FROM nullif(
|
|
493
|
+
current_setting('opengeni.workspace_id', true), ''
|
|
494
|
+
)::uuid
|
|
495
|
+
THEN
|
|
496
|
+
RAISE EXCEPTION 'document reclassification workspace authority is invalid'
|
|
497
|
+
USING ERRCODE = '42501';
|
|
498
|
+
END IF;
|
|
499
|
+
-- `NULL NOT IN (...)` is NULL, not TRUE, so an ABSENT kind would slip past
|
|
500
|
+
-- this guard and only surface much later as the compare-and-set 40001 the API
|
|
501
|
+
-- renders as a 409 conflict. A missing kind is malformed input: reject it here
|
|
502
|
+
-- with 22023 so the caller gets a 400.
|
|
503
|
+
IF expected_kind IS NULL
|
|
504
|
+
OR target_kind IS NULL
|
|
505
|
+
OR expected_kind NOT IN ('organization', 'workspace', 'personal')
|
|
506
|
+
OR target_kind NOT IN ('organization', 'workspace', 'personal')
|
|
507
|
+
OR octet_length(convert_to(actor_subject, 'UTF8')) NOT BETWEEN 1 AND 1024
|
|
508
|
+
THEN
|
|
509
|
+
RAISE EXCEPTION 'document reclassification input is invalid'
|
|
510
|
+
USING ERRCODE = '22023';
|
|
511
|
+
END IF;
|
|
512
|
+
|
|
513
|
+
IF account_admin_authorization IS NOT NULL
|
|
514
|
+
AND account_admin_authorization <> 'null'::jsonb
|
|
515
|
+
THEN
|
|
516
|
+
account_admin_authorized :=
|
|
517
|
+
jsonb_typeof(account_admin_authorization) = 'object'
|
|
518
|
+
AND coalesce(account_admin_authorization ->> 'permission', '') = 'account:admin'
|
|
519
|
+
AND nullif(account_admin_authorization ->> 'accountId', '')::uuid
|
|
520
|
+
IS NOT DISTINCT FROM account_id_value
|
|
521
|
+
AND nullif(account_admin_authorization ->> 'actorSubjectId', '')
|
|
522
|
+
IS NOT DISTINCT FROM actor_subject
|
|
523
|
+
AND coalesce(account_admin_authorization ->> 'authorizationId', '')
|
|
524
|
+
~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
|
|
525
|
+
END IF;
|
|
526
|
+
|
|
527
|
+
-- The authorization stamp is fresh request provenance, not logical input.
|
|
528
|
+
-- Excluding it lets a later exactly-authorized HTTP retry converge on the
|
|
529
|
+
-- immutable operation receipt while changed business input still conflicts.
|
|
530
|
+
input_hash_value := encode(
|
|
531
|
+
sha256(convert_to((p_command - 'accountAdminAuthorization')::text, 'UTF8')),
|
|
532
|
+
'hex'
|
|
533
|
+
);
|
|
534
|
+
PERFORM pg_advisory_xact_lock(
|
|
535
|
+
hashtextextended('document-authority-reclassification:' || operation_id_value::text, 0)
|
|
536
|
+
);
|
|
537
|
+
INSERT INTO opengeni_private.document_migration_capabilities (
|
|
538
|
+
backend_pid, transaction_id, capability_kind
|
|
539
|
+
) VALUES (pg_backend_pid(), pg_current_xact_id(), 'reclassify')
|
|
540
|
+
ON CONFLICT DO NOTHING;
|
|
541
|
+
SELECT * INTO existing_receipt
|
|
542
|
+
FROM document_authority_reclassifications receipt
|
|
543
|
+
WHERE receipt.operation_id = operation_id_value
|
|
544
|
+
FOR UPDATE;
|
|
545
|
+
IF FOUND THEN
|
|
546
|
+
IF existing_receipt.account_id IS DISTINCT FROM account_id_value
|
|
547
|
+
OR existing_receipt.input_hash IS DISTINCT FROM input_hash_value
|
|
548
|
+
THEN
|
|
549
|
+
RAISE EXCEPTION 'document reclassification operation id was reused with different input'
|
|
550
|
+
USING ERRCODE = '23505';
|
|
551
|
+
END IF;
|
|
552
|
+
IF (
|
|
553
|
+
existing_receipt.previous_authority_kind = 'organization'
|
|
554
|
+
OR existing_receipt.resulting_authority_kind = 'organization'
|
|
555
|
+
) AND NOT account_admin_authorized
|
|
556
|
+
THEN
|
|
557
|
+
RAISE EXCEPTION 'organization document reclassification requires exact account authority'
|
|
558
|
+
USING ERRCODE = '42501';
|
|
559
|
+
END IF;
|
|
560
|
+
DELETE FROM opengeni_private.document_migration_capabilities
|
|
561
|
+
WHERE backend_pid = pg_backend_pid()
|
|
562
|
+
AND transaction_id = pg_current_xact_id_if_assigned()
|
|
563
|
+
AND capability_kind = 'reclassify';
|
|
564
|
+
RETURN existing_receipt.result;
|
|
565
|
+
END IF;
|
|
566
|
+
DELETE FROM opengeni_private.document_migration_capabilities
|
|
567
|
+
WHERE backend_pid = pg_backend_pid()
|
|
568
|
+
AND transaction_id = pg_current_xact_id_if_assigned()
|
|
569
|
+
AND capability_kind = 'reclassify';
|
|
570
|
+
|
|
571
|
+
SELECT * INTO document_row
|
|
572
|
+
FROM documents document_value
|
|
573
|
+
WHERE document_value.id = document_id_value
|
|
574
|
+
AND document_value.account_id = account_id_value
|
|
575
|
+
FOR UPDATE;
|
|
576
|
+
IF NOT FOUND THEN
|
|
577
|
+
RAISE EXCEPTION 'document not found' USING ERRCODE = 'P0002';
|
|
578
|
+
END IF;
|
|
579
|
+
IF document_row.authority_kind IS DISTINCT FROM expected_kind
|
|
580
|
+
OR document_row.authority_workspace_id IS DISTINCT FROM expected_workspace_id
|
|
581
|
+
OR document_row.authority_subject_id IS DISTINCT FROM expected_subject_id
|
|
582
|
+
OR document_row.authority_id IS DISTINCT FROM expected_authority_id
|
|
583
|
+
THEN
|
|
584
|
+
RAISE EXCEPTION 'document authority changed before reclassification'
|
|
585
|
+
USING ERRCODE = '40001';
|
|
586
|
+
END IF;
|
|
587
|
+
IF (document_row.authority_kind = 'organization' OR target_kind = 'organization')
|
|
588
|
+
AND NOT account_admin_authorized
|
|
589
|
+
THEN
|
|
590
|
+
RAISE EXCEPTION 'organization document reclassification requires exact account authority'
|
|
591
|
+
USING ERRCODE = '42501';
|
|
592
|
+
END IF;
|
|
593
|
+
IF target_kind = 'workspace'
|
|
594
|
+
AND request_workspace_id_value IS DISTINCT FROM document_row.workspace_id
|
|
595
|
+
THEN
|
|
596
|
+
RAISE EXCEPTION 'workspace document target requires the immutable origin workspace route'
|
|
597
|
+
USING ERRCODE = '42501';
|
|
598
|
+
END IF;
|
|
599
|
+
IF target_kind = 'personal'
|
|
600
|
+
AND document_row.created_by IS DISTINCT FROM actor_subject
|
|
601
|
+
THEN
|
|
602
|
+
RAISE EXCEPTION 'personal document reclassification requires the original creating subject'
|
|
603
|
+
USING ERRCODE = '42501';
|
|
604
|
+
END IF;
|
|
605
|
+
|
|
606
|
+
old_authority_id := document_row.authority_id;
|
|
607
|
+
target_subject_id := CASE WHEN target_kind = 'personal' THEN actor_subject ELSE NULL END;
|
|
608
|
+
target_authority_id := CASE WHEN target_kind = 'personal' THEN document_row.authority_id ELSE NULL END;
|
|
609
|
+
target_owner_membership_id := CASE
|
|
610
|
+
WHEN target_kind = 'personal' THEN document_row.owner_organization_membership_id
|
|
611
|
+
ELSE NULL
|
|
612
|
+
END;
|
|
613
|
+
|
|
614
|
+
IF target_kind = 'personal' AND target_authority_id IS NULL THEN
|
|
615
|
+
-- The capability window MUST be open before the membership read, not after
|
|
616
|
+
-- it. `organization_memberships` is FORCE ROW LEVEL SECURITY and carries no
|
|
617
|
+
-- GUC-only read policy: every branch is capability- or lifecycle-gated. This
|
|
618
|
+
-- routine is SECURITY DEFINER, so it runs as the schema OWNER, and `FORCE`
|
|
619
|
+
-- binds the owner too on the documented deployment posture (a NOSUPERUSER,
|
|
620
|
+
-- NOBYPASSRLS migration login). Reading first would match zero rows, leave
|
|
621
|
+
-- `IF FOUND` false, and silently skip this entire portable-authority
|
|
622
|
+
-- activation - see `docs/force-rls-migration-backfills.md`.
|
|
623
|
+
INSERT INTO opengeni_private.personal_document_authority_capabilities (
|
|
624
|
+
backend_pid, transaction_id, capability_kind
|
|
625
|
+
) VALUES (pg_backend_pid(), pg_current_xact_id(), 'write')
|
|
626
|
+
ON CONFLICT DO NOTHING;
|
|
627
|
+
-- Fail loudly rather than fall through to the legacy lane if the window is
|
|
628
|
+
-- ever not established: a blinded read here is indistinguishable from
|
|
629
|
+
-- "this subject holds no organization membership", and the difference is
|
|
630
|
+
-- whether a personal Document is portable or permanently workspace-pinned.
|
|
631
|
+
IF NOT opengeni_private.personal_document_authority_capability_active('write') THEN
|
|
632
|
+
RAISE EXCEPTION 'document reclassification personal authority window is unavailable'
|
|
633
|
+
USING ERRCODE = '55000';
|
|
634
|
+
END IF;
|
|
635
|
+
-- Deliberately NOT `FOR SHARE`. PostgreSQL applies a relation's UPDATE
|
|
636
|
+
-- policies to any SELECT that carries a locking clause, and every read
|
|
637
|
+
-- policy on `organization_memberships` is `FOR SELECT` only, so `FOR SHARE`
|
|
638
|
+
-- / `FOR KEY SHARE` / `FOR UPDATE` all return ZERO ROWS here without
|
|
639
|
+
-- raising - the same silent-blinding class as the FORCE-RLS backfill no-op.
|
|
640
|
+
-- The membership still cannot be deleted or re-keyed under us, by one of
|
|
641
|
+
-- two mechanisms depending on which arm the write below takes. On the
|
|
642
|
+
-- fresh-insert arm the RI check for
|
|
643
|
+
-- `organization_user_resource_authorities_membership_fk` bypasses row
|
|
644
|
+
-- security and takes `FOR KEY SHARE` on the exact membership row. On the
|
|
645
|
+
-- ON CONFLICT DO UPDATE arm no row is inserted and the FK columns are
|
|
646
|
+
-- unchanged, so PostgreSQL skips the RI check entirely - but the row it is
|
|
647
|
+
-- reactivating already references that membership under ON DELETE RESTRICT,
|
|
648
|
+
-- so the DELETE is refused by that reference instead. Either way a
|
|
649
|
+
-- concurrent non-key `UPDATE` of the membership is NOT blocked; that
|
|
650
|
+
-- narrower write-skew window is documented as a known residual.
|
|
651
|
+
SELECT membership.* INTO owner_member
|
|
652
|
+
FROM organization_memberships membership
|
|
653
|
+
WHERE membership.account_id = account_id_value
|
|
654
|
+
AND membership.subject_id = actor_subject
|
|
655
|
+
AND membership.status = 'active'
|
|
656
|
+
AND membership.revoked_at IS NULL;
|
|
657
|
+
IF FOUND THEN
|
|
658
|
+
-- Reactivate rather than insert blind. The rollback direction below only
|
|
659
|
+
-- marks the prior authority `revoked`; it never deletes the row, and
|
|
660
|
+
-- `organization_user_resource_authorities_resource_identity_idx` is
|
|
661
|
+
-- UNIQUE (account_id, organization_membership_id, resource_kind,
|
|
662
|
+
-- resource_id) with NO status predicate. A plain INSERT therefore made
|
|
663
|
+
-- the documented `personal -> workspace -> personal` round trip fail
|
|
664
|
+
-- permanently with 23505, which the route surfaces as an untyped 500.
|
|
665
|
+
-- `origin_workspace_id` is deliberately preserved on reactivation: it is
|
|
666
|
+
-- privacy-safe provenance of where the authority first originated, never
|
|
667
|
+
-- an ownership or authorization anchor, so it must not be rewritten to
|
|
668
|
+
-- wherever the Document happens to sit now.
|
|
669
|
+
INSERT INTO organization_user_resource_authorities (
|
|
670
|
+
id, account_id, organization_membership_id, resource_kind,
|
|
671
|
+
resource_id, origin_workspace_id, generation, status
|
|
672
|
+
) VALUES (
|
|
673
|
+
gen_random_uuid(), account_id_value, owner_member.id, 'document',
|
|
674
|
+
document_row.id, document_row.workspace_id, 1, 'active'
|
|
675
|
+
)
|
|
676
|
+
ON CONFLICT (account_id, organization_membership_id, resource_kind, resource_id)
|
|
677
|
+
DO UPDATE SET
|
|
678
|
+
status = 'active',
|
|
679
|
+
generation = organization_user_resource_authorities.generation + 1,
|
|
680
|
+
revoked_at = NULL,
|
|
681
|
+
updated_at = clock_timestamp()
|
|
682
|
+
RETURNING id, organization_membership_id
|
|
683
|
+
INTO target_authority_id, target_owner_membership_id;
|
|
684
|
+
ELSE
|
|
685
|
+
-- Configured/local and rolling-legacy subjects have no organization
|
|
686
|
+
-- membership and keep the pre-organization workspace-anchored personal
|
|
687
|
+
-- lane. Close the window immediately so nothing after this point runs
|
|
688
|
+
-- with a capability it did not earn.
|
|
689
|
+
DELETE FROM opengeni_private.personal_document_authority_capabilities
|
|
690
|
+
WHERE backend_pid = pg_backend_pid()
|
|
691
|
+
AND transaction_id = pg_current_xact_id_if_assigned()
|
|
692
|
+
AND capability_kind = 'write';
|
|
693
|
+
END IF;
|
|
694
|
+
END IF;
|
|
695
|
+
|
|
696
|
+
-- Derived AFTER the authority decision, and never before it. The receipt and
|
|
697
|
+
-- the API response must state the tuple `apply_document_authority` actually
|
|
698
|
+
-- stores, and that trigger normalizes `authority_workspace_id` to NULL for
|
|
699
|
+
-- every personal row with a non-null `authority_id` (a portable personal
|
|
700
|
+
-- Document is anchored by its authority row, never by a workspace). Computing
|
|
701
|
+
-- this up front made a `personal(portable) -> personal` receipt claim an
|
|
702
|
+
-- anchoring that does not exist, and the trigger's receipt match compares the
|
|
703
|
+
-- PRE-normalization NEW row, so nothing caught it.
|
|
704
|
+
target_workspace_id := CASE
|
|
705
|
+
WHEN target_kind = 'workspace' THEN document_row.workspace_id
|
|
706
|
+
WHEN target_kind = 'personal' AND target_authority_id IS NULL
|
|
707
|
+
THEN document_row.workspace_id
|
|
708
|
+
ELSE NULL
|
|
709
|
+
END;
|
|
710
|
+
IF target_authority_id IS NULL THEN
|
|
711
|
+
target_owner_membership_id := NULL;
|
|
712
|
+
END IF;
|
|
713
|
+
|
|
714
|
+
-- The receipt column and the receipt's own `createdAt` must be the SAME
|
|
715
|
+
-- instant. Two `clock_timestamp()` calls drift by microseconds, which makes
|
|
716
|
+
-- the `created_at = cursor` tie-break in
|
|
717
|
+
-- `list_document_authority_reclassifications` unreachable and can silently
|
|
718
|
+
-- skip a receipt whose stored `created_at` sorts before the cursor the client
|
|
719
|
+
-- echoed back from `result.createdAt`.
|
|
720
|
+
receipt_created_at := clock_timestamp();
|
|
721
|
+
result_value := jsonb_build_object(
|
|
722
|
+
'operationId', operation_id_value,
|
|
723
|
+
'documentId', document_row.id,
|
|
724
|
+
'previousAuthority', jsonb_build_object(
|
|
725
|
+
'kind', document_row.authority_kind,
|
|
726
|
+
'workspaceId', document_row.authority_workspace_id,
|
|
727
|
+
'subjectId', document_row.authority_subject_id,
|
|
728
|
+
'authorityId', document_row.authority_id
|
|
729
|
+
),
|
|
730
|
+
'authority', jsonb_build_object(
|
|
731
|
+
'kind', target_kind,
|
|
732
|
+
'workspaceId', target_workspace_id,
|
|
733
|
+
'subjectId', target_subject_id,
|
|
734
|
+
'authorityId', target_authority_id
|
|
735
|
+
),
|
|
736
|
+
'createdAt', receipt_created_at
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
INSERT INTO opengeni_private.document_migration_capabilities (
|
|
740
|
+
backend_pid, transaction_id, capability_kind
|
|
741
|
+
) VALUES (pg_backend_pid(), pg_current_xact_id(), 'reclassify')
|
|
742
|
+
ON CONFLICT DO NOTHING;
|
|
743
|
+
INSERT INTO document_authority_reclassifications (
|
|
744
|
+
operation_id, input_hash, account_id, request_workspace_id, document_id,
|
|
745
|
+
actor_subject_id,
|
|
746
|
+
previous_authority_kind, previous_authority_workspace_id,
|
|
747
|
+
previous_authority_subject_id, previous_authority_id,
|
|
748
|
+
previous_owner_organization_membership_id,
|
|
749
|
+
resulting_authority_kind, resulting_authority_workspace_id,
|
|
750
|
+
resulting_authority_subject_id, resulting_authority_id,
|
|
751
|
+
resulting_owner_organization_membership_id, result, created_at
|
|
752
|
+
) VALUES (
|
|
753
|
+
operation_id_value, input_hash_value, account_id_value,
|
|
754
|
+
request_workspace_id_value, document_row.id, actor_subject,
|
|
755
|
+
document_row.authority_kind, document_row.authority_workspace_id,
|
|
756
|
+
document_row.authority_subject_id, document_row.authority_id,
|
|
757
|
+
document_row.owner_organization_membership_id,
|
|
758
|
+
target_kind, target_workspace_id, target_subject_id, target_authority_id,
|
|
759
|
+
target_owner_membership_id, result_value, receipt_created_at
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
UPDATE documents SET
|
|
763
|
+
authority_kind = target_kind,
|
|
764
|
+
authority_workspace_id = target_workspace_id,
|
|
765
|
+
authority_subject_id = target_subject_id,
|
|
766
|
+
authority_id = target_authority_id,
|
|
767
|
+
owner_organization_membership_id = target_owner_membership_id,
|
|
768
|
+
visibility = CASE WHEN target_kind = 'personal' THEN 'private' ELSE 'workspace' END,
|
|
769
|
+
updated_at = clock_timestamp()
|
|
770
|
+
WHERE id = document_row.id;
|
|
771
|
+
UPDATE document_chunks SET
|
|
772
|
+
authority_kind = target_kind,
|
|
773
|
+
authority_workspace_id = target_workspace_id,
|
|
774
|
+
authority_subject_id = target_subject_id,
|
|
775
|
+
authority_id = target_authority_id,
|
|
776
|
+
owner_organization_membership_id = target_owner_membership_id
|
|
777
|
+
WHERE document_id = document_row.id;
|
|
778
|
+
|
|
779
|
+
IF old_authority_id IS NOT NULL AND old_authority_id IS DISTINCT FROM target_authority_id THEN
|
|
780
|
+
UPDATE organization_user_resource_grants SET
|
|
781
|
+
status = 'revoked', generation = generation + 1,
|
|
782
|
+
revoked_at = coalesce(revoked_at, clock_timestamp()),
|
|
783
|
+
updated_at = clock_timestamp()
|
|
784
|
+
WHERE authority_id = old_authority_id
|
|
785
|
+
AND account_id = account_id_value
|
|
786
|
+
AND status IN ('active', 'consumed');
|
|
787
|
+
UPDATE organization_user_resource_authorities SET
|
|
788
|
+
status = 'revoked', generation = generation + 1,
|
|
789
|
+
revoked_at = coalesce(revoked_at, clock_timestamp()),
|
|
790
|
+
updated_at = clock_timestamp()
|
|
791
|
+
WHERE id = old_authority_id
|
|
792
|
+
AND account_id = account_id_value
|
|
793
|
+
AND status <> 'revoked';
|
|
794
|
+
END IF;
|
|
795
|
+
DELETE FROM opengeni_private.personal_document_authority_capabilities
|
|
796
|
+
WHERE backend_pid = pg_backend_pid()
|
|
797
|
+
AND transaction_id = pg_current_xact_id_if_assigned()
|
|
798
|
+
AND capability_kind = 'write';
|
|
799
|
+
DELETE FROM opengeni_private.document_migration_capabilities
|
|
800
|
+
WHERE backend_pid = pg_backend_pid()
|
|
801
|
+
AND transaction_id = pg_current_xact_id_if_assigned()
|
|
802
|
+
AND capability_kind = 'reclassify';
|
|
803
|
+
RETURN result_value;
|
|
804
|
+
EXCEPTION WHEN OTHERS THEN
|
|
805
|
+
DELETE FROM opengeni_private.personal_document_authority_capabilities
|
|
806
|
+
WHERE backend_pid = pg_backend_pid()
|
|
807
|
+
AND transaction_id = pg_current_xact_id_if_assigned()
|
|
808
|
+
AND capability_kind = 'write';
|
|
809
|
+
DELETE FROM opengeni_private.document_migration_capabilities
|
|
810
|
+
WHERE backend_pid = pg_backend_pid()
|
|
811
|
+
AND transaction_id = pg_current_xact_id_if_assigned()
|
|
812
|
+
AND capability_kind = 'reclassify';
|
|
813
|
+
RAISE;
|
|
814
|
+
END
|
|
815
|
+
$body$;
|
|
816
|
+
|
|
817
|
+
CREATE FUNCTION list_document_authority_reclassifications(
|
|
818
|
+
p_account_id uuid,
|
|
819
|
+
p_workspace_id uuid,
|
|
820
|
+
p_actor_subject_id text,
|
|
821
|
+
p_document_id uuid,
|
|
822
|
+
p_limit integer,
|
|
823
|
+
p_before_created_at timestamptz,
|
|
824
|
+
p_before_operation_id uuid
|
|
825
|
+
) RETURNS SETOF jsonb
|
|
826
|
+
LANGUAGE sql
|
|
827
|
+
STABLE
|
|
828
|
+
SECURITY DEFINER
|
|
829
|
+
SET search_path FROM CURRENT
|
|
830
|
+
AS $body$
|
|
831
|
+
SELECT receipt.result
|
|
832
|
+
FROM document_authority_reclassifications receipt
|
|
833
|
+
WHERE receipt.account_id = p_account_id
|
|
834
|
+
AND receipt.request_workspace_id = p_workspace_id
|
|
835
|
+
AND receipt.actor_subject_id = p_actor_subject_id
|
|
836
|
+
AND receipt.document_id = p_document_id
|
|
837
|
+
AND p_account_id = nullif(current_setting('opengeni.account_id', true), '')::uuid
|
|
838
|
+
AND p_workspace_id = nullif(current_setting('opengeni.workspace_id', true), '')::uuid
|
|
839
|
+
AND p_actor_subject_id = nullif(current_setting('opengeni.subject_id', true), '')
|
|
840
|
+
AND (
|
|
841
|
+
p_before_created_at IS NULL
|
|
842
|
+
OR receipt.created_at < p_before_created_at
|
|
843
|
+
OR (
|
|
844
|
+
receipt.created_at = p_before_created_at
|
|
845
|
+
AND receipt.operation_id < p_before_operation_id
|
|
846
|
+
)
|
|
847
|
+
)
|
|
848
|
+
ORDER BY receipt.created_at DESC, receipt.operation_id DESC
|
|
849
|
+
LIMIT least(greatest(coalesce(p_limit, 1), 1), 101)
|
|
850
|
+
$body$;
|
|
851
|
+
|
|
852
|
+
CREATE FUNCTION run_document_default_collection_backfill(p_command jsonb)
|
|
853
|
+
RETURNS jsonb
|
|
854
|
+
LANGUAGE plpgsql
|
|
855
|
+
SECURITY DEFINER
|
|
856
|
+
SET search_path FROM CURRENT
|
|
857
|
+
AS $body$
|
|
858
|
+
DECLARE
|
|
859
|
+
account_id_value uuid := nullif(p_command ->> 'accountId', '')::uuid;
|
|
860
|
+
request_workspace_id_value uuid := nullif(p_command ->> 'workspaceId', '')::uuid;
|
|
861
|
+
actor_subject text := nullif(p_command ->> 'actorSubjectId', '');
|
|
862
|
+
run_id_value uuid := nullif(p_command ->> 'runId', '')::uuid;
|
|
863
|
+
operation_id_value uuid := nullif(p_command ->> 'operationId', '')::uuid;
|
|
864
|
+
batch_size integer := coalesce((p_command ->> 'batchSize')::integer, 50);
|
|
865
|
+
account_admin_authorization jsonb := p_command -> 'accountAdminAuthorization';
|
|
866
|
+
account_admin_authorized boolean := false;
|
|
867
|
+
input_hash_value text;
|
|
868
|
+
existing_operation document_default_collection_backfill_operations%ROWTYPE;
|
|
869
|
+
run_row document_default_collection_backfill_runs%ROWTYPE;
|
|
870
|
+
workspace_row workspaces%ROWTYPE;
|
|
871
|
+
base_row document_bases%ROWTYPE;
|
|
872
|
+
outcome_value text;
|
|
873
|
+
batch_processed integer := 0;
|
|
874
|
+
batch_created integer := 0;
|
|
875
|
+
batch_adopted integer := 0;
|
|
876
|
+
has_more boolean;
|
|
877
|
+
result_value jsonb;
|
|
878
|
+
BEGIN
|
|
879
|
+
IF p_command IS NULL THEN
|
|
880
|
+
RAISE EXCEPTION 'document Default collection backfill command is required'
|
|
881
|
+
USING ERRCODE = '22023';
|
|
882
|
+
ELSIF account_id_value IS NULL THEN
|
|
883
|
+
RAISE EXCEPTION 'document Default collection backfill account id is required'
|
|
884
|
+
USING ERRCODE = '22023';
|
|
885
|
+
ELSIF request_workspace_id_value IS NULL THEN
|
|
886
|
+
RAISE EXCEPTION 'document Default collection backfill workspace id is required'
|
|
887
|
+
USING ERRCODE = '22023';
|
|
888
|
+
ELSIF actor_subject IS NULL THEN
|
|
889
|
+
RAISE EXCEPTION 'document Default collection backfill actor is required'
|
|
890
|
+
USING ERRCODE = '22023';
|
|
891
|
+
ELSIF run_id_value IS NULL THEN
|
|
892
|
+
RAISE EXCEPTION 'document Default collection backfill run id is required'
|
|
893
|
+
USING ERRCODE = '22023';
|
|
894
|
+
ELSIF operation_id_value IS NULL THEN
|
|
895
|
+
RAISE EXCEPTION 'document Default collection backfill operation id is required'
|
|
896
|
+
USING ERRCODE = '22023';
|
|
897
|
+
ELSIF batch_size NOT BETWEEN 1 AND 100 THEN
|
|
898
|
+
RAISE EXCEPTION 'document Default collection backfill batch size is invalid'
|
|
899
|
+
USING ERRCODE = '22023';
|
|
900
|
+
END IF;
|
|
901
|
+
IF account_id_value IS DISTINCT FROM nullif(
|
|
902
|
+
current_setting('opengeni.account_id', true), ''
|
|
903
|
+
)::uuid
|
|
904
|
+
THEN
|
|
905
|
+
RAISE EXCEPTION 'document Default collection backfill account authority is invalid'
|
|
906
|
+
USING ERRCODE = '42501';
|
|
907
|
+
END IF;
|
|
908
|
+
IF request_workspace_id_value IS DISTINCT FROM nullif(
|
|
909
|
+
current_setting('opengeni.workspace_id', true), ''
|
|
910
|
+
)::uuid
|
|
911
|
+
THEN
|
|
912
|
+
RAISE EXCEPTION 'document Default collection backfill workspace authority is invalid'
|
|
913
|
+
USING ERRCODE = '42501';
|
|
914
|
+
END IF;
|
|
915
|
+
IF actor_subject IS DISTINCT FROM nullif(
|
|
916
|
+
current_setting('opengeni.subject_id', true), ''
|
|
917
|
+
)
|
|
918
|
+
THEN
|
|
919
|
+
RAISE EXCEPTION 'document Default collection backfill actor authority is invalid'
|
|
920
|
+
USING ERRCODE = '42501';
|
|
921
|
+
END IF;
|
|
922
|
+
IF account_admin_authorization IS NOT NULL
|
|
923
|
+
AND account_admin_authorization <> 'null'::jsonb
|
|
924
|
+
THEN
|
|
925
|
+
account_admin_authorized :=
|
|
926
|
+
jsonb_typeof(account_admin_authorization) = 'object'
|
|
927
|
+
AND coalesce(account_admin_authorization ->> 'permission', '') = 'account:admin'
|
|
928
|
+
AND nullif(account_admin_authorization ->> 'accountId', '')::uuid
|
|
929
|
+
IS NOT DISTINCT FROM account_id_value
|
|
930
|
+
AND nullif(account_admin_authorization ->> 'actorSubjectId', '')
|
|
931
|
+
IS NOT DISTINCT FROM actor_subject
|
|
932
|
+
AND coalesce(account_admin_authorization ->> 'authorizationId', '')
|
|
933
|
+
~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
|
|
934
|
+
END IF;
|
|
935
|
+
IF NOT account_admin_authorized THEN
|
|
936
|
+
RAISE EXCEPTION 'document Default collection backfill requires organization administration'
|
|
937
|
+
USING ERRCODE = '42501';
|
|
938
|
+
END IF;
|
|
939
|
+
|
|
940
|
+
input_hash_value := encode(
|
|
941
|
+
sha256(convert_to((p_command - 'accountAdminAuthorization')::text, 'UTF8')),
|
|
942
|
+
'hex'
|
|
943
|
+
);
|
|
944
|
+
PERFORM pg_advisory_xact_lock(
|
|
945
|
+
hashtextextended('document-default-collection-backfill:' || run_id_value::text, 0)
|
|
946
|
+
);
|
|
947
|
+
INSERT INTO opengeni_private.document_migration_capabilities (
|
|
948
|
+
backend_pid, transaction_id, capability_kind
|
|
949
|
+
) VALUES (pg_backend_pid(), pg_current_xact_id(), 'default_backfill')
|
|
950
|
+
ON CONFLICT DO NOTHING;
|
|
951
|
+
|
|
952
|
+
SELECT * INTO existing_operation
|
|
953
|
+
FROM document_default_collection_backfill_operations operation
|
|
954
|
+
WHERE operation.operation_id = operation_id_value
|
|
955
|
+
FOR UPDATE;
|
|
956
|
+
IF FOUND THEN
|
|
957
|
+
IF existing_operation.account_id IS DISTINCT FROM account_id_value
|
|
958
|
+
OR existing_operation.run_id IS DISTINCT FROM run_id_value
|
|
959
|
+
OR existing_operation.input_hash IS DISTINCT FROM input_hash_value
|
|
960
|
+
THEN
|
|
961
|
+
RAISE EXCEPTION 'document Default backfill operation id was reused with different input'
|
|
962
|
+
USING ERRCODE = '23505';
|
|
963
|
+
END IF;
|
|
964
|
+
DELETE FROM opengeni_private.document_migration_capabilities
|
|
965
|
+
WHERE backend_pid = pg_backend_pid()
|
|
966
|
+
AND transaction_id = pg_current_xact_id_if_assigned()
|
|
967
|
+
AND capability_kind = 'default_backfill';
|
|
968
|
+
RETURN existing_operation.result;
|
|
969
|
+
END IF;
|
|
970
|
+
|
|
971
|
+
SELECT * INTO run_row
|
|
972
|
+
FROM document_default_collection_backfill_runs run
|
|
973
|
+
WHERE run.run_id = run_id_value
|
|
974
|
+
FOR UPDATE;
|
|
975
|
+
IF NOT FOUND THEN
|
|
976
|
+
INSERT INTO document_default_collection_backfill_runs (
|
|
977
|
+
run_id, account_id, actor_subject_id
|
|
978
|
+
) VALUES (run_id_value, account_id_value, actor_subject)
|
|
979
|
+
RETURNING * INTO run_row;
|
|
980
|
+
ELSIF run_row.account_id IS DISTINCT FROM account_id_value
|
|
981
|
+
OR run_row.actor_subject_id IS DISTINCT FROM actor_subject
|
|
982
|
+
THEN
|
|
983
|
+
RAISE EXCEPTION 'document Default backfill run identity mismatch'
|
|
984
|
+
USING ERRCODE = '23505';
|
|
985
|
+
END IF;
|
|
986
|
+
|
|
987
|
+
IF run_row.status = 'running' THEN
|
|
988
|
+
FOR workspace_row IN
|
|
989
|
+
SELECT workspace.* FROM workspaces workspace
|
|
990
|
+
WHERE workspace.account_id = account_id_value
|
|
991
|
+
AND (run_row.last_workspace_id IS NULL OR workspace.id > run_row.last_workspace_id)
|
|
992
|
+
ORDER BY workspace.id
|
|
993
|
+
LIMIT batch_size
|
|
994
|
+
FOR UPDATE
|
|
995
|
+
LOOP
|
|
996
|
+
SELECT * INTO base_row
|
|
997
|
+
FROM document_bases base
|
|
998
|
+
WHERE base.workspace_id = workspace_row.id
|
|
999
|
+
AND lower(btrim(base.name)) = 'default'
|
|
1000
|
+
ORDER BY base.id
|
|
1001
|
+
LIMIT 1
|
|
1002
|
+
FOR UPDATE;
|
|
1003
|
+
IF FOUND THEN
|
|
1004
|
+
outcome_value := 'adopted';
|
|
1005
|
+
batch_adopted := batch_adopted + 1;
|
|
1006
|
+
ELSE
|
|
1007
|
+
INSERT INTO document_bases (
|
|
1008
|
+
account_id, workspace_id, name, description
|
|
1009
|
+
) VALUES (
|
|
1010
|
+
account_id_value, workspace_row.id, 'Default',
|
|
1011
|
+
'Default base for dropped files and notes.'
|
|
1012
|
+
)
|
|
1013
|
+
ON CONFLICT DO NOTHING
|
|
1014
|
+
RETURNING * INTO base_row;
|
|
1015
|
+
IF FOUND THEN
|
|
1016
|
+
outcome_value := 'created';
|
|
1017
|
+
batch_created := batch_created + 1;
|
|
1018
|
+
ELSE
|
|
1019
|
+
SELECT * INTO STRICT base_row FROM document_bases base
|
|
1020
|
+
WHERE base.workspace_id = workspace_row.id
|
|
1021
|
+
AND lower(btrim(base.name)) = 'default'
|
|
1022
|
+
ORDER BY base.id LIMIT 1;
|
|
1023
|
+
outcome_value := 'adopted';
|
|
1024
|
+
batch_adopted := batch_adopted + 1;
|
|
1025
|
+
END IF;
|
|
1026
|
+
END IF;
|
|
1027
|
+
INSERT INTO document_default_collection_backfill_receipts (
|
|
1028
|
+
run_id, account_id, workspace_id, base_id, outcome
|
|
1029
|
+
) VALUES (
|
|
1030
|
+
run_id_value, account_id_value, workspace_row.id, base_row.id, outcome_value
|
|
1031
|
+
) ON CONFLICT (run_id, workspace_id) DO NOTHING;
|
|
1032
|
+
run_row.last_workspace_id := workspace_row.id;
|
|
1033
|
+
batch_processed := batch_processed + 1;
|
|
1034
|
+
END LOOP;
|
|
1035
|
+
|
|
1036
|
+
SELECT EXISTS (
|
|
1037
|
+
SELECT 1 FROM workspaces workspace
|
|
1038
|
+
WHERE workspace.account_id = account_id_value
|
|
1039
|
+
AND (run_row.last_workspace_id IS NULL OR workspace.id > run_row.last_workspace_id)
|
|
1040
|
+
) INTO has_more;
|
|
1041
|
+
UPDATE document_default_collection_backfill_runs SET
|
|
1042
|
+
last_workspace_id = run_row.last_workspace_id,
|
|
1043
|
+
processed_count = processed_count + batch_processed,
|
|
1044
|
+
created_count = created_count + batch_created,
|
|
1045
|
+
adopted_count = adopted_count + batch_adopted,
|
|
1046
|
+
status = CASE WHEN has_more THEN 'running' ELSE 'completed' END,
|
|
1047
|
+
updated_at = clock_timestamp(),
|
|
1048
|
+
completed_at = CASE WHEN has_more THEN NULL ELSE clock_timestamp() END
|
|
1049
|
+
WHERE run_id = run_id_value
|
|
1050
|
+
RETURNING * INTO run_row;
|
|
1051
|
+
END IF;
|
|
1052
|
+
|
|
1053
|
+
result_value := jsonb_build_object(
|
|
1054
|
+
'runId', run_row.run_id,
|
|
1055
|
+
'operationId', operation_id_value,
|
|
1056
|
+
'status', run_row.status,
|
|
1057
|
+
'lastWorkspaceId', run_row.last_workspace_id,
|
|
1058
|
+
'processedCount', run_row.processed_count,
|
|
1059
|
+
'createdCount', run_row.created_count,
|
|
1060
|
+
'adoptedCount', run_row.adopted_count,
|
|
1061
|
+
'completedAt', run_row.completed_at
|
|
1062
|
+
);
|
|
1063
|
+
INSERT INTO document_default_collection_backfill_operations (
|
|
1064
|
+
operation_id, run_id, account_id, input_hash, result
|
|
1065
|
+
) VALUES (
|
|
1066
|
+
operation_id_value, run_id_value, account_id_value, input_hash_value, result_value
|
|
1067
|
+
);
|
|
1068
|
+
DELETE FROM opengeni_private.document_migration_capabilities
|
|
1069
|
+
WHERE backend_pid = pg_backend_pid()
|
|
1070
|
+
AND transaction_id = pg_current_xact_id_if_assigned()
|
|
1071
|
+
AND capability_kind = 'default_backfill';
|
|
1072
|
+
RETURN result_value;
|
|
1073
|
+
EXCEPTION WHEN OTHERS THEN
|
|
1074
|
+
DELETE FROM opengeni_private.document_migration_capabilities
|
|
1075
|
+
WHERE backend_pid = pg_backend_pid()
|
|
1076
|
+
AND transaction_id = pg_current_xact_id_if_assigned()
|
|
1077
|
+
AND capability_kind = 'default_backfill';
|
|
1078
|
+
RAISE;
|
|
1079
|
+
END
|
|
1080
|
+
$body$;
|
|
1081
|
+
|
|
1082
|
+
DO $document_migration_search_path$
|
|
1083
|
+
DECLARE
|
|
1084
|
+
data_schema text := current_schema();
|
|
1085
|
+
BEGIN
|
|
1086
|
+
EXECUTE format(
|
|
1087
|
+
'ALTER FUNCTION opengeni_private.apply_document_authority() '
|
|
1088
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
1089
|
+
data_schema
|
|
1090
|
+
);
|
|
1091
|
+
EXECUTE format(
|
|
1092
|
+
'ALTER FUNCTION opengeni_private.apply_document_chunk_authority() '
|
|
1093
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
1094
|
+
data_schema
|
|
1095
|
+
);
|
|
1096
|
+
EXECUTE format(
|
|
1097
|
+
'ALTER FUNCTION %I.reclassify_document_authority(jsonb) '
|
|
1098
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
1099
|
+
data_schema, data_schema
|
|
1100
|
+
);
|
|
1101
|
+
EXECUTE format(
|
|
1102
|
+
'ALTER FUNCTION %I.list_document_authority_reclassifications('
|
|
1103
|
+
|| 'uuid,uuid,text,uuid,integer,timestamptz,uuid) '
|
|
1104
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
1105
|
+
data_schema, data_schema
|
|
1106
|
+
);
|
|
1107
|
+
EXECUTE format(
|
|
1108
|
+
'ALTER FUNCTION %I.run_document_default_collection_backfill(jsonb) '
|
|
1109
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
1110
|
+
data_schema, data_schema
|
|
1111
|
+
);
|
|
1112
|
+
END
|
|
1113
|
+
$document_migration_search_path$;
|
|
1114
|
+
|
|
1115
|
+
REVOKE ALL ON TABLE document_authority_reclassifications FROM PUBLIC;
|
|
1116
|
+
REVOKE ALL ON TABLE document_default_collection_backfill_runs FROM PUBLIC;
|
|
1117
|
+
REVOKE ALL ON TABLE document_default_collection_backfill_operations FROM PUBLIC;
|
|
1118
|
+
REVOKE ALL ON TABLE document_default_collection_backfill_receipts FROM PUBLIC;
|
|
1119
|
+
REVOKE ALL ON FUNCTION reclassify_document_authority(jsonb) FROM PUBLIC;
|
|
1120
|
+
REVOKE ALL ON FUNCTION
|
|
1121
|
+
list_document_authority_reclassifications(
|
|
1122
|
+
uuid, uuid, text, uuid, integer, timestamptz, uuid
|
|
1123
|
+
) FROM PUBLIC;
|
|
1124
|
+
REVOKE ALL ON FUNCTION run_document_default_collection_backfill(jsonb) FROM PUBLIC;
|
|
1125
|
+
|
|
1126
|
+
DO $document_migration_runtime_grants$
|
|
1127
|
+
BEGIN
|
|
1128
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
1129
|
+
GRANT SELECT ON TABLE document_authority_reclassifications TO opengeni_app;
|
|
1130
|
+
REVOKE INSERT, UPDATE, DELETE ON TABLE document_authority_reclassifications
|
|
1131
|
+
FROM opengeni_app;
|
|
1132
|
+
REVOKE ALL ON TABLE document_default_collection_backfill_runs FROM opengeni_app;
|
|
1133
|
+
REVOKE ALL ON TABLE document_default_collection_backfill_operations FROM opengeni_app;
|
|
1134
|
+
REVOKE ALL ON TABLE document_default_collection_backfill_receipts FROM opengeni_app;
|
|
1135
|
+
GRANT EXECUTE ON FUNCTION reclassify_document_authority(jsonb) TO opengeni_app;
|
|
1136
|
+
GRANT EXECUTE ON FUNCTION
|
|
1137
|
+
list_document_authority_reclassifications(
|
|
1138
|
+
uuid, uuid, text, uuid, integer, timestamptz, uuid
|
|
1139
|
+
) TO opengeni_app;
|
|
1140
|
+
GRANT EXECUTE ON FUNCTION run_document_default_collection_backfill(jsonb) TO opengeni_app;
|
|
1141
|
+
GRANT EXECUTE ON FUNCTION
|
|
1142
|
+
opengeni_private.document_migration_capability_active(text) TO opengeni_app;
|
|
1143
|
+
REVOKE ALL ON TABLE opengeni_private.document_migration_capabilities FROM opengeni_app;
|
|
1144
|
+
END IF;
|
|
1145
|
+
END
|
|
1146
|
+
$document_migration_runtime_grants$;
|
|
1147
|
+
|
|
1148
|
+
COMMENT ON TABLE document_authority_reclassifications IS
|
|
1149
|
+
'Immutable same-transaction before/after receipts for explicit Document authority changes.';
|
|
1150
|
+
COMMENT ON TABLE document_default_collection_backfill_runs IS
|
|
1151
|
+
'Resumable account-bounded creation/adoption progress for the internal Default collection.';
|
|
1152
|
+
COMMENT ON TABLE document_default_collection_backfill_receipts IS
|
|
1153
|
+
'Immutable per-workspace evidence for each Default collection backfill run.';
|