@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,1347 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Complete the organization-tenancy evidence chain, converge the final
|
|
3
|
+
-- deterministic connection compatibility population, and make activation
|
|
4
|
+
-- consume one fenced evidence snapshot. This migration activates no tenant.
|
|
5
|
+
-- Existing zero-receipt activations stay replayable; new activations require
|
|
6
|
+
-- six exact receipts, including connection convergence. Five was never a
|
|
7
|
+
-- shipped cardinality, so the constraint does not tolerate it.
|
|
8
|
+
|
|
9
|
+
SET LOCAL lock_timeout = '5s';
|
|
10
|
+
SET LOCAL statement_timeout = '10min';
|
|
11
|
+
|
|
12
|
+
ALTER TABLE tenancy_backfill_unresolved_rows
|
|
13
|
+
DROP CONSTRAINT tenancy_backfill_unresolved_reason_chk,
|
|
14
|
+
ADD CONSTRAINT tenancy_backfill_unresolved_reason_chk CHECK (
|
|
15
|
+
reason_code IN (
|
|
16
|
+
'no_deterministic_evidence', 'ambiguous_candidate_authority',
|
|
17
|
+
'missing_organization_membership', 'conflicting_authority_rows',
|
|
18
|
+
'external_lane_owns_row', 'legacy_shape_unrecognized',
|
|
19
|
+
'missing_login_identity', 'organization_identity_mismatch',
|
|
20
|
+
'missing_owner_workspace_membership', 'membership_terminal_status',
|
|
21
|
+
'deterministic_repair_pending'
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
ALTER TABLE session_tenancy_activations
|
|
26
|
+
ADD COLUMN backfill_receipt_ids uuid[] NOT NULL DEFAULT ARRAY[]::uuid[],
|
|
27
|
+
ADD CONSTRAINT session_tenancy_activation_backfill_receipts_check CHECK (
|
|
28
|
+
cardinality(backfill_receipt_ids) IN (0, 6)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
-- An application-controlled GUC is not a write capability. The exact
|
|
32
|
+
-- backend+transaction+organization receipt allows only the reviewed upgrader
|
|
33
|
+
-- to cross the otherwise immutable connection-authority boundary.
|
|
34
|
+
CREATE TABLE opengeni_private.connection_tenancy_backfill_capabilities (
|
|
35
|
+
backend_pid integer NOT NULL,
|
|
36
|
+
transaction_id xid8 NOT NULL,
|
|
37
|
+
account_id uuid NOT NULL,
|
|
38
|
+
CONSTRAINT connection_tenancy_backfill_capabilities_pk PRIMARY KEY (
|
|
39
|
+
backend_pid, transaction_id, account_id
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
REVOKE ALL ON TABLE opengeni_private.connection_tenancy_backfill_capabilities FROM PUBLIC;
|
|
43
|
+
|
|
44
|
+
CREATE FUNCTION opengeni_private.connection_tenancy_backfill_capability_active(
|
|
45
|
+
p_account_id uuid
|
|
46
|
+
) RETURNS boolean
|
|
47
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
48
|
+
SET search_path = pg_catalog, opengeni_private
|
|
49
|
+
AS $capability$
|
|
50
|
+
SELECT EXISTS (
|
|
51
|
+
SELECT 1 FROM opengeni_private.connection_tenancy_backfill_capabilities capability
|
|
52
|
+
WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
|
|
53
|
+
AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
54
|
+
AND capability.account_id = p_account_id
|
|
55
|
+
)
|
|
56
|
+
$capability$;
|
|
57
|
+
REVOKE ALL ON FUNCTION
|
|
58
|
+
opengeni_private.connection_tenancy_backfill_capability_active(uuid) FROM PUBLIC;
|
|
59
|
+
|
|
60
|
+
-- FORCE-RLS visibility exists only while the SECURITY DEFINER classifier or
|
|
61
|
+
-- upgrader holds its unforgeable transaction capability, and only for the
|
|
62
|
+
-- migration owner the reviewed seams run as (0285/0298's conjunct). Direct
|
|
63
|
+
-- runtime DML cannot create the capability row.
|
|
64
|
+
DO $connection_tenancy_backfill_capability_policies$
|
|
65
|
+
DECLARE
|
|
66
|
+
data_schema text := pg_catalog.current_schema();
|
|
67
|
+
migration_owner text := current_user;
|
|
68
|
+
BEGIN
|
|
69
|
+
EXECUTE pg_catalog.format(
|
|
70
|
+
'CREATE POLICY connection_tenancy_backfill_read ON %I.organization_memberships '
|
|
71
|
+
|| 'FOR SELECT USING (current_user = %L AND '
|
|
72
|
+
|| 'opengeni_private.connection_tenancy_backfill_capability_active(account_id))',
|
|
73
|
+
data_schema, migration_owner
|
|
74
|
+
);
|
|
75
|
+
EXECUTE pg_catalog.format(
|
|
76
|
+
'CREATE POLICY connection_tenancy_backfill_read ON %I.connections '
|
|
77
|
+
|| 'FOR SELECT USING (current_user = %L AND '
|
|
78
|
+
|| 'opengeni_private.connection_tenancy_backfill_capability_active(account_id))',
|
|
79
|
+
data_schema, migration_owner
|
|
80
|
+
);
|
|
81
|
+
EXECUTE pg_catalog.format(
|
|
82
|
+
'CREATE POLICY connection_tenancy_backfill_update ON %I.connections '
|
|
83
|
+
|| 'FOR UPDATE USING (current_user = %L AND '
|
|
84
|
+
|| 'opengeni_private.connection_tenancy_backfill_capability_active(account_id)) '
|
|
85
|
+
|| 'WITH CHECK (current_user = %L AND '
|
|
86
|
+
|| 'opengeni_private.connection_tenancy_backfill_capability_active(account_id))',
|
|
87
|
+
data_schema, migration_owner, migration_owner
|
|
88
|
+
);
|
|
89
|
+
EXECUTE pg_catalog.format(
|
|
90
|
+
'CREATE POLICY connection_tenancy_backfill_read '
|
|
91
|
+
|| 'ON %I.organization_user_resource_authorities '
|
|
92
|
+
|| 'FOR SELECT USING (resource_kind = ''connection'' AND current_user = %L AND '
|
|
93
|
+
|| 'opengeni_private.connection_tenancy_backfill_capability_active(account_id))',
|
|
94
|
+
data_schema, migration_owner
|
|
95
|
+
);
|
|
96
|
+
EXECUTE pg_catalog.format(
|
|
97
|
+
'CREATE POLICY connection_tenancy_backfill_insert '
|
|
98
|
+
|| 'ON %I.organization_user_resource_authorities '
|
|
99
|
+
|| 'FOR INSERT WITH CHECK (resource_kind = ''connection'' AND current_user = %L AND '
|
|
100
|
+
|| 'opengeni_private.connection_tenancy_backfill_capability_active(account_id))',
|
|
101
|
+
data_schema, migration_owner
|
|
102
|
+
);
|
|
103
|
+
END
|
|
104
|
+
$connection_tenancy_backfill_capability_policies$;
|
|
105
|
+
|
|
106
|
+
-- Owner-only marker policies for the connection owner-authority binding below.
|
|
107
|
+
--
|
|
108
|
+
-- `organization_memberships` and `organization_user_resource_authorities` are
|
|
109
|
+
-- FORCE ROW LEVEL SECURITY with ZERO runtime table privileges, so the marker is
|
|
110
|
+
-- unreachable as an application write vector - it is exactly the gate the
|
|
111
|
+
-- existing `organization_tenancy_lifecycle` policies on both tables already
|
|
112
|
+
-- use - and the `current_user` conjunct keeps every branch owner-only besides.
|
|
113
|
+
-- They are separate narrow policies rather than new markers on the shared
|
|
114
|
+
-- lifecycle policy so a later migration restating that policy's list cannot
|
|
115
|
+
-- silently drop connection binding (0305 dropped 0290's marker exactly that
|
|
116
|
+
-- way).
|
|
117
|
+
--
|
|
118
|
+
-- The `FOR UPDATE` policy on `organization_memberships` is not a write grant to
|
|
119
|
+
-- the binding: PostgreSQL evaluates the UPDATE/ALL policy USING clause for
|
|
120
|
+
-- `SELECT ... FOR SHARE` as well as the SELECT one, so a row-locking lookup is
|
|
121
|
+
-- blind without it.
|
|
122
|
+
DO $connection_authority_binding_policies$
|
|
123
|
+
DECLARE
|
|
124
|
+
data_schema text := pg_catalog.current_schema();
|
|
125
|
+
migration_owner text := current_user;
|
|
126
|
+
-- Tenant-fenced in the policy itself, not only in the seam: the window can
|
|
127
|
+
-- never widen into an organization-wide membership or authority read.
|
|
128
|
+
marker constant text :=
|
|
129
|
+
'current_setting(''opengeni.organization_tenancy_lifecycle'', true) '
|
|
130
|
+
|| '= ''connection_authority_binding'' AND account_id = nullif('
|
|
131
|
+
|| 'current_setting(''opengeni.account_id'', true), '''')::uuid';
|
|
132
|
+
BEGIN
|
|
133
|
+
EXECUTE pg_catalog.format(
|
|
134
|
+
'CREATE POLICY connection_authority_binding_read ON %I.organization_memberships '
|
|
135
|
+
|| 'FOR SELECT USING (current_user = %L AND %s)',
|
|
136
|
+
data_schema, migration_owner, marker
|
|
137
|
+
);
|
|
138
|
+
EXECUTE pg_catalog.format(
|
|
139
|
+
'CREATE POLICY connection_authority_binding_lock ON %I.organization_memberships '
|
|
140
|
+
|| 'FOR UPDATE USING (current_user = %L AND %s)',
|
|
141
|
+
data_schema, migration_owner, marker
|
|
142
|
+
);
|
|
143
|
+
EXECUTE pg_catalog.format(
|
|
144
|
+
'CREATE POLICY connection_authority_binding_insert '
|
|
145
|
+
|| 'ON %I.organization_user_resource_authorities '
|
|
146
|
+
|| 'FOR INSERT WITH CHECK (resource_kind = ''connection'' AND current_user = %L AND %s)',
|
|
147
|
+
data_schema, migration_owner, marker
|
|
148
|
+
);
|
|
149
|
+
END
|
|
150
|
+
$connection_authority_binding_policies$;
|
|
151
|
+
|
|
152
|
+
-- Migration 0290 gave its two read-only membership seams the
|
|
153
|
+
-- `organization_membership_backfill` marker on the shared
|
|
154
|
+
-- `organization_tenancy_lifecycle` policy. Migration 0305 then restated that
|
|
155
|
+
-- policy's marker list to add `personal_resource_grant_management` and dropped
|
|
156
|
+
-- 0290's entry, so `list_organization_membership_backfill_anchors` and
|
|
157
|
+
-- `list_organization_memberships_without_personal_workspace` have returned `[]`
|
|
158
|
+
-- ever since for a NON-superuser migration owner - measured, not inferred, on
|
|
159
|
+
-- `acquireOwnerMigratedTestDatabase`. Both are SECURITY DEFINER owned by that
|
|
160
|
+
-- role, so even a superuser CALLER gets the owner's RLS; only a
|
|
161
|
+
-- superuser-migrated database (every existing test harness) hid it.
|
|
162
|
+
--
|
|
163
|
+
-- The consequence is phase D's membership half: an already-anchored subject is
|
|
164
|
+
-- misread as provisionable, and the memberships that carry no personal
|
|
165
|
+
-- workspace - the actual backfill target population - are invisible, so the
|
|
166
|
+
-- walk can never converge them and its `organization_memberships` receipt
|
|
167
|
+
-- counts are wrong.
|
|
168
|
+
--
|
|
169
|
+
-- Restored as its own narrow read-only policy rather than as a fourth entry in
|
|
170
|
+
-- the shared list, so the next migration to restate that list cannot silently
|
|
171
|
+
-- delete it again. Both seams are plain `SELECT`s, so `FOR SELECT` is exactly
|
|
172
|
+
-- the command they issue.
|
|
173
|
+
DO $organization_membership_backfill_read_policy$
|
|
174
|
+
DECLARE
|
|
175
|
+
data_schema text := pg_catalog.current_schema();
|
|
176
|
+
migration_owner text := current_user;
|
|
177
|
+
BEGIN
|
|
178
|
+
EXECUTE pg_catalog.format(
|
|
179
|
+
'CREATE POLICY organization_membership_backfill_read '
|
|
180
|
+
|| 'ON %I.organization_memberships FOR SELECT USING (current_user = %L AND '
|
|
181
|
+
|| 'current_setting(''opengeni.organization_tenancy_lifecycle'', true) '
|
|
182
|
+
|| '= ''organization_membership_backfill'' AND account_id = nullif('
|
|
183
|
+
|| 'current_setting(''opengeni.account_id'', true), '''')::uuid)',
|
|
184
|
+
data_schema, migration_owner
|
|
185
|
+
);
|
|
186
|
+
END
|
|
187
|
+
$organization_membership_backfill_read_policy$;
|
|
188
|
+
|
|
189
|
+
-- The single owner-authority seam both connection-binding branches use.
|
|
190
|
+
-- `organization_memberships` and `organization_user_resource_authorities` are
|
|
191
|
+
-- FORCE ROW LEVEL SECURITY, and OpenGeni migrates and runs its SECURITY DEFINER
|
|
192
|
+
-- routines as a NON-superuser owner without BYPASSRLS, so the owner is
|
|
193
|
+
-- policy-bound too. 0256's inline `SELECT ... FOR SHARE` plus authority INSERT
|
|
194
|
+
-- therefore could not work on any production deployment: the mint path silently
|
|
195
|
+
-- degraded every personal connection to `legacy_user`, and 0340's backfill
|
|
196
|
+
-- verification would have raised `42501` on every deterministic candidate.
|
|
197
|
+
-- Opening the reviewed marker window for exactly this binding is 0263's
|
|
198
|
+
-- `assert_active_managed_human_organization_membership` pattern, restored on
|
|
199
|
+
-- every exit. The row lock is kept: it serializes the mint and the bounded
|
|
200
|
+
-- upgrade against a concurrent membership revocation.
|
|
201
|
+
--
|
|
202
|
+
-- Tenant-fenced, so it never becomes a cross-organization membership oracle.
|
|
203
|
+
-- It is a resolver, never an authorization: the trigger still owns the decision
|
|
204
|
+
-- that the caller IS this subject. `provision-roles` blanket-grants EXECUTE on
|
|
205
|
+
-- every `opengeni_private` routine to the runtime role, and the four authority
|
|
206
|
+
-- tables must keep ZERO direct application DML, so the seam additionally
|
|
207
|
+
-- refuses to do anything outside a trigger: a direct call by the application
|
|
208
|
+
-- role can neither read a membership nor mint an authority row.
|
|
209
|
+
CREATE FUNCTION opengeni_private.bind_connection_owner_authority(
|
|
210
|
+
p_account_id uuid,
|
|
211
|
+
p_subject_id text,
|
|
212
|
+
p_connection_id uuid DEFAULT NULL,
|
|
213
|
+
p_workspace_id uuid DEFAULT NULL,
|
|
214
|
+
OUT membership_id uuid,
|
|
215
|
+
OUT authority_id uuid
|
|
216
|
+
) RETURNS record
|
|
217
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
218
|
+
SET search_path FROM CURRENT
|
|
219
|
+
AS $owner_authority$
|
|
220
|
+
DECLARE
|
|
221
|
+
previous_lifecycle text := pg_catalog.current_setting(
|
|
222
|
+
'opengeni.organization_tenancy_lifecycle', true
|
|
223
|
+
);
|
|
224
|
+
BEGIN
|
|
225
|
+
membership_id := NULL;
|
|
226
|
+
authority_id := NULL;
|
|
227
|
+
IF pg_catalog.pg_trigger_depth() = 0
|
|
228
|
+
OR p_account_id IS NULL OR p_subject_id IS NULL
|
|
229
|
+
OR p_account_id IS DISTINCT FROM nullif(
|
|
230
|
+
pg_catalog.current_setting('opengeni.account_id', true), ''
|
|
231
|
+
)::uuid
|
|
232
|
+
THEN
|
|
233
|
+
RETURN;
|
|
234
|
+
END IF;
|
|
235
|
+
PERFORM pg_catalog.set_config(
|
|
236
|
+
'opengeni.organization_tenancy_lifecycle', 'connection_authority_binding', true
|
|
237
|
+
);
|
|
238
|
+
SELECT membership.id INTO membership_id
|
|
239
|
+
FROM organization_memberships membership
|
|
240
|
+
WHERE membership.account_id = p_account_id
|
|
241
|
+
AND membership.subject_id = p_subject_id
|
|
242
|
+
AND membership.status = 'active' AND membership.revoked_at IS NULL
|
|
243
|
+
FOR SHARE;
|
|
244
|
+
IF membership_id IS NOT NULL AND p_connection_id IS NOT NULL
|
|
245
|
+
AND p_workspace_id IS NOT NULL
|
|
246
|
+
THEN
|
|
247
|
+
authority_id := pg_catalog.gen_random_uuid();
|
|
248
|
+
INSERT INTO organization_user_resource_authorities (
|
|
249
|
+
id, account_id, organization_membership_id, resource_kind, resource_id,
|
|
250
|
+
origin_workspace_id, generation, status
|
|
251
|
+
) VALUES (
|
|
252
|
+
authority_id, p_account_id, membership_id, 'connection', p_connection_id,
|
|
253
|
+
p_workspace_id, 1, 'active'
|
|
254
|
+
);
|
|
255
|
+
END IF;
|
|
256
|
+
PERFORM pg_catalog.set_config(
|
|
257
|
+
'opengeni.organization_tenancy_lifecycle', coalesce(previous_lifecycle, ''), true
|
|
258
|
+
);
|
|
259
|
+
RETURN;
|
|
260
|
+
EXCEPTION WHEN OTHERS THEN
|
|
261
|
+
PERFORM pg_catalog.set_config(
|
|
262
|
+
'opengeni.organization_tenancy_lifecycle', coalesce(previous_lifecycle, ''), true
|
|
263
|
+
);
|
|
264
|
+
RAISE;
|
|
265
|
+
END
|
|
266
|
+
$owner_authority$;
|
|
267
|
+
REVOKE ALL ON FUNCTION
|
|
268
|
+
opengeni_private.bind_connection_owner_authority(uuid, text, uuid, uuid) FROM PUBLIC;
|
|
269
|
+
|
|
270
|
+
-- Exact-scoped activation lookup used by the connection/writer triggers. The
|
|
271
|
+
-- account must already be the transaction tenant: this keeps the helper safe
|
|
272
|
+
-- under the provisioner's reviewed private-helper EXECUTE posture.
|
|
273
|
+
CREATE FUNCTION opengeni_private.session_tenancy_account_activated(p_account_id uuid)
|
|
274
|
+
RETURNS boolean
|
|
275
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
276
|
+
SET search_path FROM CURRENT
|
|
277
|
+
AS $activated$
|
|
278
|
+
SELECT p_account_id IS NOT DISTINCT FROM nullif(
|
|
279
|
+
pg_catalog.current_setting('opengeni.account_id', true), ''
|
|
280
|
+
)::uuid
|
|
281
|
+
AND EXISTS (
|
|
282
|
+
SELECT 1 FROM session_tenancy_activations activation
|
|
283
|
+
WHERE activation.account_id = p_account_id AND activation.activation_version = 1
|
|
284
|
+
)
|
|
285
|
+
$activated$;
|
|
286
|
+
REVOKE ALL ON FUNCTION opengeni_private.session_tenancy_account_activated(uuid) FROM PUBLIC;
|
|
287
|
+
|
|
288
|
+
-- Preserve 0256 byte-for-byte behavior before activation. Afterwards a new
|
|
289
|
+
-- personal connection must have a live membership and may not reopen the
|
|
290
|
+
-- legacy_user lane. The only immutable-authority transition is the exact
|
|
291
|
+
-- capability-held legacy_user -> user backfill below.
|
|
292
|
+
CREATE OR REPLACE FUNCTION opengeni_private.bind_connection_authority()
|
|
293
|
+
RETURNS trigger
|
|
294
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
295
|
+
SET search_path FROM CURRENT
|
|
296
|
+
AS $binding$
|
|
297
|
+
DECLARE
|
|
298
|
+
caller_subject text := nullif(pg_catalog.current_setting('opengeni.subject_id', true), '');
|
|
299
|
+
membership_id uuid;
|
|
300
|
+
authority_value uuid;
|
|
301
|
+
identity_changed boolean;
|
|
302
|
+
backfill_transition boolean;
|
|
303
|
+
BEGIN
|
|
304
|
+
IF TG_OP = 'INSERT' THEN
|
|
305
|
+
NEW.origin_workspace_id := NEW.workspace_id;
|
|
306
|
+
NEW.authority_generation := 1;
|
|
307
|
+
IF NEW.subject_id IS NULL THEN
|
|
308
|
+
NEW.authority_scope := 'workspace';
|
|
309
|
+
NEW.authority_id := NULL;
|
|
310
|
+
NEW.owner_organization_membership_id := NULL;
|
|
311
|
+
RETURN NEW;
|
|
312
|
+
END IF;
|
|
313
|
+
IF caller_subject IS DISTINCT FROM NEW.subject_id THEN
|
|
314
|
+
RAISE EXCEPTION 'personal connection owner must be the authenticated subject'
|
|
315
|
+
USING ERRCODE = '42501';
|
|
316
|
+
END IF;
|
|
317
|
+
SELECT binding.membership_id, binding.authority_id
|
|
318
|
+
INTO membership_id, authority_value
|
|
319
|
+
FROM opengeni_private.bind_connection_owner_authority(
|
|
320
|
+
NEW.account_id, caller_subject, NEW.id, NEW.workspace_id
|
|
321
|
+
) binding;
|
|
322
|
+
IF membership_id IS NULL THEN
|
|
323
|
+
IF opengeni_private.session_tenancy_account_activated(NEW.account_id) THEN
|
|
324
|
+
RAISE EXCEPTION 'activated organization requires connection membership authority'
|
|
325
|
+
USING ERRCODE = '42501';
|
|
326
|
+
END IF;
|
|
327
|
+
NEW.authority_scope := 'legacy_user';
|
|
328
|
+
NEW.authority_id := NULL;
|
|
329
|
+
NEW.owner_organization_membership_id := NULL;
|
|
330
|
+
RETURN NEW;
|
|
331
|
+
END IF;
|
|
332
|
+
NEW.authority_scope := 'user';
|
|
333
|
+
NEW.authority_id := authority_value;
|
|
334
|
+
NEW.owner_organization_membership_id := membership_id;
|
|
335
|
+
RETURN NEW;
|
|
336
|
+
END IF;
|
|
337
|
+
|
|
338
|
+
backfill_transition :=
|
|
339
|
+
opengeni_private.connection_tenancy_backfill_capability_active(NEW.account_id)
|
|
340
|
+
AND OLD.authority_scope = 'legacy_user'
|
|
341
|
+
AND OLD.subject_id IS NOT NULL
|
|
342
|
+
AND OLD.authority_id IS NULL
|
|
343
|
+
AND OLD.owner_organization_membership_id IS NULL
|
|
344
|
+
AND NEW.account_id IS NOT DISTINCT FROM OLD.account_id
|
|
345
|
+
AND NEW.workspace_id IS NOT DISTINCT FROM OLD.workspace_id
|
|
346
|
+
AND NEW.subject_id IS NOT DISTINCT FROM OLD.subject_id
|
|
347
|
+
AND NEW.authority_scope = 'user'
|
|
348
|
+
AND NEW.authority_id IS NOT NULL
|
|
349
|
+
AND NEW.owner_organization_membership_id IS NOT NULL
|
|
350
|
+
AND NEW.origin_workspace_id IS NOT DISTINCT FROM OLD.origin_workspace_id;
|
|
351
|
+
|
|
352
|
+
IF NEW.account_id IS DISTINCT FROM OLD.account_id
|
|
353
|
+
OR NEW.workspace_id IS DISTINCT FROM OLD.workspace_id
|
|
354
|
+
OR NEW.subject_id IS DISTINCT FROM OLD.subject_id
|
|
355
|
+
OR NEW.authority_scope IS DISTINCT FROM OLD.authority_scope
|
|
356
|
+
OR NEW.authority_id IS DISTINCT FROM OLD.authority_id
|
|
357
|
+
OR NEW.owner_organization_membership_id IS DISTINCT FROM OLD.owner_organization_membership_id
|
|
358
|
+
OR NEW.origin_workspace_id IS DISTINCT FROM OLD.origin_workspace_id
|
|
359
|
+
THEN
|
|
360
|
+
IF NOT backfill_transition THEN
|
|
361
|
+
RAISE EXCEPTION 'connection owner authority is immutable' USING ERRCODE = '23514';
|
|
362
|
+
END IF;
|
|
363
|
+
SELECT binding.membership_id INTO membership_id
|
|
364
|
+
FROM opengeni_private.bind_connection_owner_authority(
|
|
365
|
+
NEW.account_id, NEW.subject_id
|
|
366
|
+
) binding;
|
|
367
|
+
IF membership_id IS DISTINCT FROM NEW.owner_organization_membership_id THEN
|
|
368
|
+
RAISE EXCEPTION 'connection backfill membership authority is unavailable'
|
|
369
|
+
USING ERRCODE = '42501';
|
|
370
|
+
END IF;
|
|
371
|
+
PERFORM 1 FROM organization_user_resource_authorities authority
|
|
372
|
+
WHERE authority.id = NEW.authority_id
|
|
373
|
+
AND authority.account_id = NEW.account_id
|
|
374
|
+
AND authority.organization_membership_id = NEW.owner_organization_membership_id
|
|
375
|
+
AND authority.resource_kind = 'connection' AND authority.resource_id = NEW.id
|
|
376
|
+
AND authority.origin_workspace_id = NEW.origin_workspace_id
|
|
377
|
+
AND authority.generation = 1
|
|
378
|
+
AND authority.status = 'active' AND authority.revoked_at IS NULL;
|
|
379
|
+
IF NOT FOUND THEN
|
|
380
|
+
RAISE EXCEPTION 'connection backfill common authority is unavailable'
|
|
381
|
+
USING ERRCODE = '42501';
|
|
382
|
+
END IF;
|
|
383
|
+
NEW.authority_generation := OLD.authority_generation + 1;
|
|
384
|
+
RETURN NEW;
|
|
385
|
+
END IF;
|
|
386
|
+
|
|
387
|
+
identity_changed := NEW.provider_domain IS DISTINCT FROM OLD.provider_domain
|
|
388
|
+
OR NEW.kind IS DISTINCT FROM OLD.kind OR NEW.status IS DISTINCT FROM OLD.status;
|
|
389
|
+
IF identity_changed THEN
|
|
390
|
+
NEW.authority_generation := OLD.authority_generation + 1;
|
|
391
|
+
ELSIF NEW.authority_generation NOT IN (
|
|
392
|
+
OLD.authority_generation, OLD.authority_generation + 1
|
|
393
|
+
) THEN
|
|
394
|
+
RAISE EXCEPTION 'connection authority generation must be stable or advance once'
|
|
395
|
+
USING ERRCODE = '23514';
|
|
396
|
+
END IF;
|
|
397
|
+
RETURN NEW;
|
|
398
|
+
END
|
|
399
|
+
$binding$;
|
|
400
|
+
REVOKE ALL ON FUNCTION opengeni_private.bind_connection_authority() FROM PUBLIC;
|
|
401
|
+
|
|
402
|
+
-- Full-population classifier. A legacy row with an exact live same-account
|
|
403
|
+
-- membership is a deterministic repair obligation; without one it is
|
|
404
|
+
-- unresolved. Origin workspace/current access are never ownership evidence.
|
|
405
|
+
CREATE FUNCTION classify_organization_connection_authority(
|
|
406
|
+
p_account_id uuid, p_run_key text DEFAULT NULL
|
|
407
|
+
) RETURNS jsonb
|
|
408
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
409
|
+
SET search_path FROM CURRENT
|
|
410
|
+
SET statement_timeout = '5min'
|
|
411
|
+
AS $classification$
|
|
412
|
+
DECLARE
|
|
413
|
+
summary jsonb;
|
|
414
|
+
receipt_id uuid;
|
|
415
|
+
unresolved_row record;
|
|
416
|
+
BEGIN
|
|
417
|
+
IF p_account_id IS NULL THEN
|
|
418
|
+
RAISE EXCEPTION 'connection authority classification requires an organization id'
|
|
419
|
+
USING ERRCODE = '22004';
|
|
420
|
+
END IF;
|
|
421
|
+
IF p_account_id IS DISTINCT FROM nullif(
|
|
422
|
+
pg_catalog.current_setting('opengeni.account_id', true), ''
|
|
423
|
+
)::uuid
|
|
424
|
+
THEN
|
|
425
|
+
RAISE EXCEPTION 'connection authority classification scope mismatch'
|
|
426
|
+
USING ERRCODE = '42501';
|
|
427
|
+
END IF;
|
|
428
|
+
IF p_run_key IS NOT NULL AND length(btrim(p_run_key)) = 0 THEN
|
|
429
|
+
RAISE EXCEPTION 'connection authority classification run key must not be blank'
|
|
430
|
+
USING ERRCODE = '22023';
|
|
431
|
+
END IF;
|
|
432
|
+
INSERT INTO opengeni_private.connection_tenancy_backfill_capabilities (
|
|
433
|
+
backend_pid, transaction_id, account_id
|
|
434
|
+
) VALUES (pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id(), p_account_id)
|
|
435
|
+
ON CONFLICT DO NOTHING;
|
|
436
|
+
|
|
437
|
+
WITH classified AS (
|
|
438
|
+
SELECT connection_row.id AS resource_id,
|
|
439
|
+
CASE
|
|
440
|
+
WHEN connection_row.authority_scope = 'workspace'
|
|
441
|
+
AND connection_row.subject_id IS NULL AND connection_row.authority_id IS NULL
|
|
442
|
+
AND connection_row.owner_organization_membership_id IS NULL
|
|
443
|
+
AND connection_row.origin_workspace_id = connection_row.workspace_id
|
|
444
|
+
THEN 'workspace_owned'
|
|
445
|
+
WHEN connection_row.authority_scope = 'user'
|
|
446
|
+
AND connection_row.subject_id IS NOT NULL
|
|
447
|
+
AND authority.id = connection_row.authority_id
|
|
448
|
+
AND authority.resource_kind = 'connection'
|
|
449
|
+
AND authority.resource_id = connection_row.id
|
|
450
|
+
AND authority.organization_membership_id = connection_row.owner_organization_membership_id
|
|
451
|
+
AND authority.origin_workspace_id = connection_row.origin_workspace_id
|
|
452
|
+
AND authority.status = 'active' AND authority.revoked_at IS NULL
|
|
453
|
+
AND membership.id = connection_row.owner_organization_membership_id
|
|
454
|
+
AND membership.subject_id = connection_row.subject_id
|
|
455
|
+
AND membership.status = 'active' AND membership.revoked_at IS NULL
|
|
456
|
+
THEN 'user_owned'
|
|
457
|
+
WHEN connection_row.authority_scope = 'legacy_user'
|
|
458
|
+
AND connection_row.subject_id IS NOT NULL AND connection_row.authority_id IS NULL
|
|
459
|
+
AND connection_row.owner_organization_membership_id IS NULL
|
|
460
|
+
AND membership.id IS NOT NULL
|
|
461
|
+
AND membership.status = 'active' AND membership.revoked_at IS NULL
|
|
462
|
+
THEN 'deterministic_repair_pending'
|
|
463
|
+
WHEN connection_row.authority_scope = 'legacy_user'
|
|
464
|
+
THEN 'missing_organization_membership'
|
|
465
|
+
WHEN connection_row.authority_scope = 'user'
|
|
466
|
+
AND (membership.id IS NULL OR membership.status <> 'active'
|
|
467
|
+
OR membership.revoked_at IS NOT NULL)
|
|
468
|
+
THEN 'missing_organization_membership'
|
|
469
|
+
WHEN connection_row.authority_scope = 'user' THEN 'conflicting_authority_rows'
|
|
470
|
+
ELSE 'legacy_shape_unrecognized'
|
|
471
|
+
END AS verdict
|
|
472
|
+
FROM connections connection_row
|
|
473
|
+
LEFT JOIN organization_memberships membership
|
|
474
|
+
ON membership.account_id = connection_row.account_id
|
|
475
|
+
AND membership.subject_id = connection_row.subject_id
|
|
476
|
+
AND membership.status = 'active' AND membership.revoked_at IS NULL
|
|
477
|
+
LEFT JOIN organization_user_resource_authorities authority
|
|
478
|
+
ON authority.id = connection_row.authority_id
|
|
479
|
+
AND authority.account_id = connection_row.account_id
|
|
480
|
+
WHERE connection_row.account_id = p_account_id
|
|
481
|
+
)
|
|
482
|
+
SELECT pg_catalog.jsonb_build_object(
|
|
483
|
+
'schemaVersion', 1, 'organizationId', p_account_id, 'runKey', p_run_key,
|
|
484
|
+
'rewroteConnectionRows', false,
|
|
485
|
+
'connections', pg_catalog.jsonb_build_object(
|
|
486
|
+
'total', count(*)::int,
|
|
487
|
+
'workspaceOwned', count(*) FILTER (WHERE verdict = 'workspace_owned')::int,
|
|
488
|
+
'userOwned', count(*) FILTER (WHERE verdict = 'user_owned')::int,
|
|
489
|
+
'deterministicRepairPending', count(*) FILTER (
|
|
490
|
+
WHERE verdict = 'deterministic_repair_pending'
|
|
491
|
+
)::int,
|
|
492
|
+
'unresolved', count(*) FILTER (
|
|
493
|
+
WHERE verdict NOT IN ('workspace_owned', 'user_owned')
|
|
494
|
+
)::int
|
|
495
|
+
)
|
|
496
|
+
) INTO summary FROM classified;
|
|
497
|
+
|
|
498
|
+
IF p_run_key IS NOT NULL THEN
|
|
499
|
+
SELECT open_tenancy_backfill_receipt(p_account_id, 'connections', p_run_key)
|
|
500
|
+
INTO receipt_id;
|
|
501
|
+
FOR unresolved_row IN
|
|
502
|
+
WITH classified AS (
|
|
503
|
+
SELECT connection_row.id AS resource_id,
|
|
504
|
+
CASE
|
|
505
|
+
WHEN connection_row.authority_scope = 'workspace'
|
|
506
|
+
AND connection_row.subject_id IS NULL AND connection_row.authority_id IS NULL
|
|
507
|
+
AND connection_row.owner_organization_membership_id IS NULL
|
|
508
|
+
AND connection_row.origin_workspace_id = connection_row.workspace_id
|
|
509
|
+
THEN 'terminal'
|
|
510
|
+
WHEN connection_row.authority_scope = 'user'
|
|
511
|
+
AND connection_row.subject_id IS NOT NULL
|
|
512
|
+
AND authority.id = connection_row.authority_id
|
|
513
|
+
AND authority.resource_kind = 'connection'
|
|
514
|
+
AND authority.resource_id = connection_row.id
|
|
515
|
+
AND authority.organization_membership_id
|
|
516
|
+
= connection_row.owner_organization_membership_id
|
|
517
|
+
AND authority.origin_workspace_id = connection_row.origin_workspace_id
|
|
518
|
+
AND authority.status = 'active' AND authority.revoked_at IS NULL
|
|
519
|
+
AND membership.id = connection_row.owner_organization_membership_id
|
|
520
|
+
AND membership.subject_id = connection_row.subject_id
|
|
521
|
+
AND membership.status = 'active' AND membership.revoked_at IS NULL
|
|
522
|
+
THEN 'terminal'
|
|
523
|
+
WHEN connection_row.authority_scope = 'legacy_user'
|
|
524
|
+
AND membership.id IS NOT NULL
|
|
525
|
+
AND membership.status = 'active' AND membership.revoked_at IS NULL
|
|
526
|
+
THEN 'deterministic_repair_pending'
|
|
527
|
+
WHEN connection_row.authority_scope = 'legacy_user'
|
|
528
|
+
OR (connection_row.authority_scope = 'user'
|
|
529
|
+
AND (membership.id IS NULL OR membership.status <> 'active'
|
|
530
|
+
OR membership.revoked_at IS NOT NULL))
|
|
531
|
+
THEN 'missing_organization_membership'
|
|
532
|
+
WHEN connection_row.authority_scope = 'user' THEN 'conflicting_authority_rows'
|
|
533
|
+
ELSE 'legacy_shape_unrecognized'
|
|
534
|
+
END AS reason_code
|
|
535
|
+
FROM connections connection_row
|
|
536
|
+
LEFT JOIN organization_memberships membership
|
|
537
|
+
ON membership.account_id = connection_row.account_id
|
|
538
|
+
AND membership.subject_id = connection_row.subject_id
|
|
539
|
+
AND membership.status = 'active' AND membership.revoked_at IS NULL
|
|
540
|
+
LEFT JOIN organization_user_resource_authorities authority
|
|
541
|
+
ON authority.id = connection_row.authority_id
|
|
542
|
+
AND authority.account_id = connection_row.account_id
|
|
543
|
+
WHERE connection_row.account_id = p_account_id
|
|
544
|
+
)
|
|
545
|
+
SELECT resource_id, reason_code FROM classified
|
|
546
|
+
WHERE reason_code <> 'terminal' ORDER BY resource_id
|
|
547
|
+
LOOP
|
|
548
|
+
PERFORM record_tenancy_backfill_unresolved(
|
|
549
|
+
receipt_id, unresolved_row.resource_id, unresolved_row.reason_code
|
|
550
|
+
);
|
|
551
|
+
END LOOP;
|
|
552
|
+
PERFORM complete_tenancy_backfill_receipt(
|
|
553
|
+
receipt_id,
|
|
554
|
+
(summary #>> '{connections,workspaceOwned}')::bigint
|
|
555
|
+
+ (summary #>> '{connections,userOwned}')::bigint,
|
|
556
|
+
0, 'completed'
|
|
557
|
+
);
|
|
558
|
+
summary := summary || pg_catalog.jsonb_build_object('receiptId', receipt_id);
|
|
559
|
+
END IF;
|
|
560
|
+
|
|
561
|
+
DELETE FROM opengeni_private.connection_tenancy_backfill_capabilities
|
|
562
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
563
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
564
|
+
AND account_id = p_account_id;
|
|
565
|
+
RETURN summary;
|
|
566
|
+
EXCEPTION WHEN OTHERS THEN
|
|
567
|
+
DELETE FROM opengeni_private.connection_tenancy_backfill_capabilities
|
|
568
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
569
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
570
|
+
AND account_id = p_account_id;
|
|
571
|
+
RAISE;
|
|
572
|
+
END
|
|
573
|
+
$classification$;
|
|
574
|
+
REVOKE ALL ON FUNCTION classify_organization_connection_authority(uuid, text) FROM PUBLIC;
|
|
575
|
+
|
|
576
|
+
-- Bounded, resumable, SKIP-LOCKED upgrader. It changes only legacy_user rows
|
|
577
|
+
-- whose exact subject has one live same-organization membership.
|
|
578
|
+
CREATE FUNCTION backfill_organization_connection_authority(
|
|
579
|
+
p_account_id uuid, p_limit integer DEFAULT 500, p_dry_run boolean DEFAULT true
|
|
580
|
+
) RETURNS jsonb
|
|
581
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
582
|
+
SET search_path FROM CURRENT
|
|
583
|
+
SET statement_timeout = '5min'
|
|
584
|
+
AS $backfill$
|
|
585
|
+
DECLARE
|
|
586
|
+
candidate_count integer := 0;
|
|
587
|
+
upgraded_count integer := 0;
|
|
588
|
+
BEGIN
|
|
589
|
+
IF p_account_id IS NULL THEN
|
|
590
|
+
RAISE EXCEPTION 'connection authority backfill requires an organization id'
|
|
591
|
+
USING ERRCODE = '22004';
|
|
592
|
+
END IF;
|
|
593
|
+
IF p_account_id IS DISTINCT FROM nullif(
|
|
594
|
+
pg_catalog.current_setting('opengeni.account_id', true), ''
|
|
595
|
+
)::uuid
|
|
596
|
+
THEN
|
|
597
|
+
RAISE EXCEPTION 'connection authority backfill scope mismatch' USING ERRCODE = '42501';
|
|
598
|
+
END IF;
|
|
599
|
+
IF p_limit IS NULL OR p_limit NOT BETWEEN 1 AND 5000 OR p_dry_run IS NULL THEN
|
|
600
|
+
RAISE EXCEPTION 'connection authority backfill request is invalid' USING ERRCODE = '22023';
|
|
601
|
+
END IF;
|
|
602
|
+
INSERT INTO opengeni_private.connection_tenancy_backfill_capabilities (
|
|
603
|
+
backend_pid, transaction_id, account_id
|
|
604
|
+
) VALUES (pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id(), p_account_id)
|
|
605
|
+
ON CONFLICT DO NOTHING;
|
|
606
|
+
|
|
607
|
+
IF p_dry_run THEN
|
|
608
|
+
SELECT count(*)::int INTO candidate_count FROM (
|
|
609
|
+
SELECT connection_row.id
|
|
610
|
+
FROM connections connection_row
|
|
611
|
+
JOIN organization_memberships membership
|
|
612
|
+
ON membership.account_id = connection_row.account_id
|
|
613
|
+
AND membership.subject_id = connection_row.subject_id
|
|
614
|
+
AND membership.status = 'active' AND membership.revoked_at IS NULL
|
|
615
|
+
WHERE connection_row.account_id = p_account_id
|
|
616
|
+
AND connection_row.authority_scope = 'legacy_user'
|
|
617
|
+
ORDER BY connection_row.id LIMIT p_limit
|
|
618
|
+
) candidate;
|
|
619
|
+
ELSE
|
|
620
|
+
WITH candidates AS MATERIALIZED (
|
|
621
|
+
SELECT connection_row.id, connection_row.account_id,
|
|
622
|
+
connection_row.origin_workspace_id, membership.id AS membership_id
|
|
623
|
+
FROM connections connection_row
|
|
624
|
+
JOIN organization_memberships membership
|
|
625
|
+
ON membership.account_id = connection_row.account_id
|
|
626
|
+
AND membership.subject_id = connection_row.subject_id
|
|
627
|
+
AND membership.status = 'active' AND membership.revoked_at IS NULL
|
|
628
|
+
WHERE connection_row.account_id = p_account_id
|
|
629
|
+
AND connection_row.authority_scope = 'legacy_user'
|
|
630
|
+
ORDER BY connection_row.id LIMIT p_limit
|
|
631
|
+
FOR UPDATE OF connection_row SKIP LOCKED
|
|
632
|
+
), authorities AS (
|
|
633
|
+
INSERT INTO organization_user_resource_authorities (
|
|
634
|
+
id, account_id, organization_membership_id, resource_kind, resource_id,
|
|
635
|
+
origin_workspace_id, generation, status
|
|
636
|
+
)
|
|
637
|
+
SELECT pg_catalog.gen_random_uuid(), candidate.account_id, candidate.membership_id,
|
|
638
|
+
'connection', candidate.id, candidate.origin_workspace_id, 1, 'active'
|
|
639
|
+
FROM candidates candidate
|
|
640
|
+
RETURNING id, resource_id, organization_membership_id
|
|
641
|
+
), upgraded AS (
|
|
642
|
+
UPDATE connections connection_row SET authority_scope = 'user',
|
|
643
|
+
authority_id = authority.id,
|
|
644
|
+
owner_organization_membership_id = authority.organization_membership_id
|
|
645
|
+
FROM authorities authority
|
|
646
|
+
WHERE connection_row.id = authority.resource_id
|
|
647
|
+
AND connection_row.account_id = p_account_id
|
|
648
|
+
AND connection_row.authority_scope = 'legacy_user'
|
|
649
|
+
RETURNING connection_row.id
|
|
650
|
+
)
|
|
651
|
+
SELECT (SELECT count(*)::int FROM candidates),
|
|
652
|
+
(SELECT count(*)::int FROM upgraded)
|
|
653
|
+
INTO candidate_count, upgraded_count;
|
|
654
|
+
END IF;
|
|
655
|
+
|
|
656
|
+
DELETE FROM opengeni_private.connection_tenancy_backfill_capabilities
|
|
657
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
658
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
659
|
+
AND account_id = p_account_id;
|
|
660
|
+
RETURN pg_catalog.jsonb_build_object(
|
|
661
|
+
'schemaVersion', 1, 'organizationId', p_account_id,
|
|
662
|
+
'dryRun', p_dry_run, 'limit', p_limit,
|
|
663
|
+
'candidates', candidate_count, 'upgraded', upgraded_count,
|
|
664
|
+
'moreLikely', NOT p_dry_run AND candidate_count >= p_limit
|
|
665
|
+
);
|
|
666
|
+
EXCEPTION WHEN OTHERS THEN
|
|
667
|
+
DELETE FROM opengeni_private.connection_tenancy_backfill_capabilities
|
|
668
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
669
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
670
|
+
AND account_id = p_account_id;
|
|
671
|
+
RAISE;
|
|
672
|
+
END
|
|
673
|
+
$backfill$;
|
|
674
|
+
REVOKE ALL ON FUNCTION backfill_organization_connection_authority(uuid, integer, boolean)
|
|
675
|
+
FROM PUBLIC;
|
|
676
|
+
|
|
677
|
+
-- Once activated, surviving legacy_user rows are invisible to runtime reads.
|
|
678
|
+
-- This also fences an old worker that bypasses accepted-use resolution. The
|
|
679
|
+
-- audited inventory/parity/backfill capabilities retain recovery visibility.
|
|
680
|
+
--
|
|
681
|
+
-- Two constraints shape how this policy is created:
|
|
682
|
+
--
|
|
683
|
+
-- * `connections.authority_scope` arrives in 0256, and a partial-history
|
|
684
|
+
-- replay can legitimately defer that migration past this point (0249's
|
|
685
|
+
-- upgrade fixture does exactly that). Creating a policy over an absent
|
|
686
|
+
-- column aborts the whole chain there, so the creation is existence-guarded
|
|
687
|
+
-- exactly like 0298's parity capability policies. Such a database simply
|
|
688
|
+
-- cannot execute the connection lane, which is already true of every other
|
|
689
|
+
-- reader of that column.
|
|
690
|
+
-- * Every predicate a `connections` policy calls must live in
|
|
691
|
+
-- `opengeni_private` (`workspace_rls_visible` is the established example).
|
|
692
|
+
-- EXECUTE on the data-schema `session_tenancy_product_activated` is revoked
|
|
693
|
+
-- from PUBLIC and granted only to the runtime role, so calling it here
|
|
694
|
+
-- would make an unrelated SECURITY DEFINER owner of `connections` fail with
|
|
695
|
+
-- `42501` on an ordinary read. `opengeni_private.session_tenancy_account_activated`
|
|
696
|
+
-- is the byte-identical private predicate (same account-GUC fence, version 1).
|
|
697
|
+
DO $connection_tenancy_legacy_retirement$
|
|
698
|
+
DECLARE
|
|
699
|
+
data_schema text := pg_catalog.current_schema();
|
|
700
|
+
BEGIN
|
|
701
|
+
IF EXISTS (
|
|
702
|
+
SELECT 1 FROM pg_catalog.pg_attribute attribute
|
|
703
|
+
WHERE attribute.attrelid = pg_catalog.to_regclass(
|
|
704
|
+
pg_catalog.format('%I.%I', data_schema, 'connections')
|
|
705
|
+
)
|
|
706
|
+
AND attribute.attname = 'authority_scope'
|
|
707
|
+
AND attribute.attnum > 0
|
|
708
|
+
AND NOT attribute.attisdropped
|
|
709
|
+
) THEN
|
|
710
|
+
EXECUTE pg_catalog.format(
|
|
711
|
+
'CREATE POLICY connection_tenancy_legacy_retirement ON %I.connections '
|
|
712
|
+
|| 'AS RESTRICTIVE FOR SELECT USING ('
|
|
713
|
+
|| 'authority_scope <> ''legacy_user'''
|
|
714
|
+
|| ' OR NOT opengeni_private.session_tenancy_account_activated(account_id)'
|
|
715
|
+
|| ' OR opengeni_private.connection_tenancy_backfill_capability_active(account_id)'
|
|
716
|
+
|| ' OR opengeni_private.organization_tenancy_inventory_capability_active()'
|
|
717
|
+
|| ' OR opengeni_private.organization_tenancy_parity_capability_active())',
|
|
718
|
+
data_schema
|
|
719
|
+
);
|
|
720
|
+
END IF;
|
|
721
|
+
END
|
|
722
|
+
$connection_tenancy_legacy_retirement$;
|
|
723
|
+
|
|
724
|
+
-- Pre-0277 binaries/turns can omit attribution and land on the explicit
|
|
725
|
+
-- legacy default before activation. Afterwards a new legacy admission or
|
|
726
|
+
-- retained process is rejected instead of reopening the observation lane.
|
|
727
|
+
CREATE FUNCTION opengeni_private.guard_activated_tenancy_writer()
|
|
728
|
+
RETURNS trigger
|
|
729
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
730
|
+
SET search_path FROM CURRENT
|
|
731
|
+
AS $writer_fence$
|
|
732
|
+
BEGIN
|
|
733
|
+
IF NEW.initiator_kind = 'legacy_unattributed'
|
|
734
|
+
AND opengeni_private.session_tenancy_account_activated(NEW.account_id)
|
|
735
|
+
THEN
|
|
736
|
+
RAISE EXCEPTION 'activated organization refuses unattributed workspace writers'
|
|
737
|
+
USING ERRCODE = '42501';
|
|
738
|
+
END IF;
|
|
739
|
+
RETURN NEW;
|
|
740
|
+
END
|
|
741
|
+
$writer_fence$;
|
|
742
|
+
REVOKE ALL ON FUNCTION opengeni_private.guard_activated_tenancy_writer() FROM PUBLIC;
|
|
743
|
+
|
|
744
|
+
CREATE TRIGGER sandbox_workspace_mutation_admissions_tenancy_writer_fence
|
|
745
|
+
BEFORE INSERT ON sandbox_workspace_mutation_admissions
|
|
746
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_activated_tenancy_writer();
|
|
747
|
+
CREATE TRIGGER sandbox_retained_processes_tenancy_writer_fence
|
|
748
|
+
BEFORE INSERT ON sandbox_retained_processes
|
|
749
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_activated_tenancy_writer();
|
|
750
|
+
|
|
751
|
+
-- Preserve the reviewed 0298 report and replace only its known undercount:
|
|
752
|
+
-- stated session authority is an ordinary workspace-membership row OR the
|
|
753
|
+
-- active membership's own personal_workspace_id pointer (0302).
|
|
754
|
+
ALTER FUNCTION check_organization_tenancy_parity(uuid, integer, integer)
|
|
755
|
+
RENAME TO check_organization_tenancy_parity_pre_0340;
|
|
756
|
+
ALTER FUNCTION check_organization_tenancy_parity_pre_0340(uuid, integer, integer)
|
|
757
|
+
SET SCHEMA opengeni_private;
|
|
758
|
+
REVOKE ALL ON FUNCTION
|
|
759
|
+
opengeni_private.check_organization_tenancy_parity_pre_0340(uuid, integer, integer)
|
|
760
|
+
FROM PUBLIC;
|
|
761
|
+
|
|
762
|
+
CREATE FUNCTION check_organization_tenancy_parity(
|
|
763
|
+
p_organization_id uuid,
|
|
764
|
+
p_evidence_limit integer DEFAULT 10,
|
|
765
|
+
p_observation_window_days integer DEFAULT 30
|
|
766
|
+
) RETURNS jsonb
|
|
767
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
768
|
+
SET search_path FROM CURRENT
|
|
769
|
+
SET statement_timeout = '60s'
|
|
770
|
+
AS $parity$
|
|
771
|
+
DECLARE
|
|
772
|
+
result jsonb;
|
|
773
|
+
attributable_count integer;
|
|
774
|
+
BEGIN
|
|
775
|
+
result := opengeni_private.check_organization_tenancy_parity_pre_0340(
|
|
776
|
+
p_organization_id, p_evidence_limit, p_observation_window_days
|
|
777
|
+
);
|
|
778
|
+
INSERT INTO opengeni_private.organization_tenancy_parity_capabilities (
|
|
779
|
+
backend_pid, transaction_id
|
|
780
|
+
) VALUES (pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id())
|
|
781
|
+
ON CONFLICT DO NOTHING;
|
|
782
|
+
SELECT count(*)::int INTO attributable_count
|
|
783
|
+
FROM sessions session_row
|
|
784
|
+
WHERE session_row.account_id = p_organization_id
|
|
785
|
+
AND session_row.owner_organization_membership_id IS NULL
|
|
786
|
+
AND session_row.created_by_kind = 'subject'
|
|
787
|
+
AND session_row.created_by_subject_id IS NOT NULL
|
|
788
|
+
AND EXISTS (
|
|
789
|
+
SELECT 1 FROM organization_memberships membership
|
|
790
|
+
WHERE membership.account_id = p_organization_id
|
|
791
|
+
AND membership.subject_id = session_row.created_by_subject_id
|
|
792
|
+
AND membership.status = 'active' AND membership.revoked_at IS NULL
|
|
793
|
+
AND (
|
|
794
|
+
membership.personal_workspace_id = session_row.workspace_id
|
|
795
|
+
OR EXISTS (
|
|
796
|
+
SELECT 1 FROM workspace_memberships workspace_membership
|
|
797
|
+
WHERE workspace_membership.account_id = p_organization_id
|
|
798
|
+
AND workspace_membership.workspace_id = session_row.workspace_id
|
|
799
|
+
AND workspace_membership.subject_id = session_row.created_by_subject_id
|
|
800
|
+
)
|
|
801
|
+
)
|
|
802
|
+
);
|
|
803
|
+
DELETE FROM opengeni_private.organization_tenancy_parity_capabilities
|
|
804
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
805
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
|
|
806
|
+
RETURN pg_catalog.jsonb_set(
|
|
807
|
+
result, '{lanes,sessionsAttributableButUnattributed}',
|
|
808
|
+
pg_catalog.to_jsonb(attributable_count), false
|
|
809
|
+
);
|
|
810
|
+
EXCEPTION WHEN OTHERS THEN
|
|
811
|
+
DELETE FROM opengeni_private.organization_tenancy_parity_capabilities
|
|
812
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
813
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
|
|
814
|
+
RAISE;
|
|
815
|
+
END
|
|
816
|
+
$parity$;
|
|
817
|
+
REVOKE ALL ON FUNCTION check_organization_tenancy_parity(uuid, integer, integer)
|
|
818
|
+
FROM PUBLIC;
|
|
819
|
+
|
|
820
|
+
-- Same sorted UTF-8/C-locale canonical JSON algorithm as the operator command.
|
|
821
|
+
CREATE FUNCTION opengeni_private.tenancy_activation_canonical_json(value jsonb)
|
|
822
|
+
RETURNS text
|
|
823
|
+
LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE
|
|
824
|
+
SET search_path = pg_catalog, opengeni_private
|
|
825
|
+
AS $canonical_json$
|
|
826
|
+
DECLARE
|
|
827
|
+
rendered text;
|
|
828
|
+
BEGIN
|
|
829
|
+
CASE pg_catalog.jsonb_typeof(value)
|
|
830
|
+
WHEN 'object' THEN
|
|
831
|
+
SELECT coalesce(
|
|
832
|
+
'{' || pg_catalog.string_agg(
|
|
833
|
+
pg_catalog.to_jsonb(entry.key)::text || ':' ||
|
|
834
|
+
opengeni_private.tenancy_activation_canonical_json(entry.value),
|
|
835
|
+
',' ORDER BY entry.key COLLATE "C"
|
|
836
|
+
) || '}', '{}'
|
|
837
|
+
) INTO rendered
|
|
838
|
+
FROM pg_catalog.jsonb_each(value) AS entry(key, value);
|
|
839
|
+
RETURN rendered;
|
|
840
|
+
WHEN 'array' THEN
|
|
841
|
+
SELECT coalesce(
|
|
842
|
+
'[' || pg_catalog.string_agg(
|
|
843
|
+
opengeni_private.tenancy_activation_canonical_json(entry.value),
|
|
844
|
+
',' ORDER BY entry.ordinality
|
|
845
|
+
) || ']', '[]'
|
|
846
|
+
) INTO rendered
|
|
847
|
+
FROM pg_catalog.jsonb_array_elements(value)
|
|
848
|
+
WITH ORDINALITY AS entry(value, ordinality);
|
|
849
|
+
RETURN rendered;
|
|
850
|
+
ELSE
|
|
851
|
+
RETURN value::text;
|
|
852
|
+
END CASE;
|
|
853
|
+
END
|
|
854
|
+
$canonical_json$;
|
|
855
|
+
REVOKE ALL ON FUNCTION opengeni_private.tenancy_activation_canonical_json(jsonb) FROM PUBLIC;
|
|
856
|
+
|
|
857
|
+
-- The newest receipt per family is authoritative. Full-population classifiers
|
|
858
|
+
-- must settle; connection/resource families must carry zero unresolved rows.
|
|
859
|
+
CREATE OR REPLACE FUNCTION check_tenancy_backfill_activation_evidence(
|
|
860
|
+
p_account_id uuid
|
|
861
|
+
) RETURNS jsonb
|
|
862
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
863
|
+
SET search_path FROM CURRENT
|
|
864
|
+
SET statement_timeout = '5min'
|
|
865
|
+
AS $evidence$
|
|
866
|
+
DECLARE
|
|
867
|
+
previous_lifecycle text := pg_catalog.current_setting(
|
|
868
|
+
'opengeni.organization_tenancy_lifecycle', true
|
|
869
|
+
);
|
|
870
|
+
resource_report jsonb;
|
|
871
|
+
session_report jsonb;
|
|
872
|
+
connection_report jsonb;
|
|
873
|
+
result jsonb;
|
|
874
|
+
BEGIN
|
|
875
|
+
IF p_account_id IS NULL THEN
|
|
876
|
+
RAISE EXCEPTION 'tenancy backfill activation evidence requires an organization id'
|
|
877
|
+
USING ERRCODE = '22004';
|
|
878
|
+
END IF;
|
|
879
|
+
IF p_account_id IS DISTINCT FROM nullif(
|
|
880
|
+
pg_catalog.current_setting('opengeni.account_id', true), ''
|
|
881
|
+
)::uuid
|
|
882
|
+
THEN
|
|
883
|
+
RAISE EXCEPTION 'tenancy backfill activation evidence scope mismatch'
|
|
884
|
+
USING ERRCODE = '42501';
|
|
885
|
+
END IF;
|
|
886
|
+
resource_report := verify_organization_resource_classification(p_account_id, NULL);
|
|
887
|
+
session_report := classify_organization_session_ownership(p_account_id, NULL);
|
|
888
|
+
connection_report := classify_organization_connection_authority(p_account_id, NULL);
|
|
889
|
+
PERFORM pg_catalog.set_config(
|
|
890
|
+
'opengeni.organization_tenancy_lifecycle', 'tenancy_backfill_ledger', true
|
|
891
|
+
);
|
|
892
|
+
|
|
893
|
+
WITH required(resource_family, expected_total, unresolved_must_be_zero, family_order) AS (
|
|
894
|
+
VALUES
|
|
895
|
+
('organization_memberships'::text, NULL::bigint, false, 1),
|
|
896
|
+
('sessions'::text, (session_report #>> '{sessions,total}')::bigint, false, 2),
|
|
897
|
+
('variable_sets'::text,
|
|
898
|
+
(resource_report #>> '{families,variable_sets,total}')::bigint, true, 3),
|
|
899
|
+
('rigs'::text, (resource_report #>> '{families,rigs,total}')::bigint, true, 4),
|
|
900
|
+
('machines'::text, (resource_report #>> '{families,machines,total}')::bigint, true, 5),
|
|
901
|
+
('connections'::text,
|
|
902
|
+
(connection_report #>> '{connections,total}')::bigint, true, 6)
|
|
903
|
+
), latest_receipt AS (
|
|
904
|
+
SELECT DISTINCT ON (receipt.resource_family)
|
|
905
|
+
receipt.resource_family, receipt.id, receipt.run_key, receipt.status,
|
|
906
|
+
receipt.classified_count, receipt.skipped_count, receipt.unresolved_count,
|
|
907
|
+
receipt.started_at
|
|
908
|
+
FROM tenancy_backfill_receipts receipt
|
|
909
|
+
WHERE receipt.account_id = p_account_id
|
|
910
|
+
AND receipt.resource_family IN (SELECT resource_family FROM required)
|
|
911
|
+
ORDER BY receipt.resource_family, receipt.started_at DESC, receipt.id DESC
|
|
912
|
+
), evaluated AS (
|
|
913
|
+
SELECT required.*,
|
|
914
|
+
latest_receipt.id AS receipt_id, latest_receipt.run_key,
|
|
915
|
+
latest_receipt.status, latest_receipt.classified_count,
|
|
916
|
+
latest_receipt.skipped_count, latest_receipt.unresolved_count,
|
|
917
|
+
CASE
|
|
918
|
+
WHEN latest_receipt.id IS NULL THEN 'missing_receipt'
|
|
919
|
+
WHEN latest_receipt.status <> 'completed' THEN 'receipt_not_completed'
|
|
920
|
+
WHEN required.expected_total IS NOT NULL
|
|
921
|
+
AND latest_receipt.classified_count + latest_receipt.skipped_count
|
|
922
|
+
+ latest_receipt.unresolved_count <> required.expected_total
|
|
923
|
+
THEN 'population_mismatch'
|
|
924
|
+
WHEN required.unresolved_must_be_zero AND latest_receipt.unresolved_count <> 0
|
|
925
|
+
THEN 'unresolved_rows'
|
|
926
|
+
ELSE NULL
|
|
927
|
+
END AS blocker
|
|
928
|
+
FROM required LEFT JOIN latest_receipt USING (resource_family)
|
|
929
|
+
)
|
|
930
|
+
SELECT pg_catalog.jsonb_build_object(
|
|
931
|
+
'schemaVersion', 1, 'organizationId', p_account_id,
|
|
932
|
+
'ready', pg_catalog.bool_and(blocker IS NULL),
|
|
933
|
+
'receiptIds', coalesce(
|
|
934
|
+
pg_catalog.jsonb_agg(receipt_id ORDER BY family_order)
|
|
935
|
+
FILTER (WHERE receipt_id IS NOT NULL), '[]'::jsonb
|
|
936
|
+
),
|
|
937
|
+
'blockers', coalesce(
|
|
938
|
+
pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object(
|
|
939
|
+
'resourceFamily', resource_family, 'code', blocker
|
|
940
|
+
) ORDER BY family_order) FILTER (WHERE blocker IS NOT NULL), '[]'::jsonb
|
|
941
|
+
),
|
|
942
|
+
'families', pg_catalog.jsonb_object_agg(
|
|
943
|
+
resource_family, pg_catalog.jsonb_build_object(
|
|
944
|
+
'receiptId', receipt_id, 'runKey', run_key, 'status', status,
|
|
945
|
+
'classifiedCount', classified_count, 'skippedCount', skipped_count,
|
|
946
|
+
'unresolvedCount', unresolved_count, 'expectedTotal', expected_total,
|
|
947
|
+
'blocker', blocker
|
|
948
|
+
) ORDER BY family_order
|
|
949
|
+
)
|
|
950
|
+
) INTO result FROM evaluated;
|
|
951
|
+
|
|
952
|
+
PERFORM pg_catalog.set_config(
|
|
953
|
+
'opengeni.organization_tenancy_lifecycle', coalesce(previous_lifecycle, ''), true
|
|
954
|
+
);
|
|
955
|
+
RETURN result;
|
|
956
|
+
EXCEPTION WHEN OTHERS THEN
|
|
957
|
+
PERFORM pg_catalog.set_config(
|
|
958
|
+
'opengeni.organization_tenancy_lifecycle', coalesce(previous_lifecycle, ''), true
|
|
959
|
+
);
|
|
960
|
+
RAISE;
|
|
961
|
+
END
|
|
962
|
+
$evidence$;
|
|
963
|
+
REVOKE ALL ON FUNCTION check_tenancy_backfill_activation_evidence(uuid) FROM PUBLIC;
|
|
964
|
+
|
|
965
|
+
-- This deployment-wide transaction fence is the serialization boundary between
|
|
966
|
+
-- the first irreversible operator activation and later greenfield organization
|
|
967
|
+
-- provisioning. A provisioning transaction must acquire it before observing
|
|
968
|
+
-- session_tenancy_any_product_activation(): if it wins, it commits before the
|
|
969
|
+
-- boundary; if activation wins, it observes the committed boundary and can
|
|
970
|
+
-- create its organization-scoped receipt atomically. Keep the key and signature
|
|
971
|
+
-- stable for that forward lane. The function is owner-only so an application
|
|
972
|
+
-- session cannot hold the deployment boundary open.
|
|
973
|
+
CREATE FUNCTION lock_session_tenancy_activation_boundary()
|
|
974
|
+
RETURNS void
|
|
975
|
+
LANGUAGE sql VOLATILE
|
|
976
|
+
SET search_path FROM CURRENT
|
|
977
|
+
AS $boundary$
|
|
978
|
+
SELECT pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
979
|
+
'session-tenancy-canonical-boundary:v1', 0
|
|
980
|
+
))
|
|
981
|
+
$boundary$;
|
|
982
|
+
REVOKE ALL ON FUNCTION lock_session_tenancy_activation_boundary() FROM PUBLIC;
|
|
983
|
+
COMMENT ON FUNCTION lock_session_tenancy_activation_boundary() IS
|
|
984
|
+
'Owner-only deployment boundary fence shared by operator activation and atomic greenfield activation.';
|
|
985
|
+
|
|
986
|
+
-- Migration 0303 gave `session_tenancy_activations` FORCE ROW LEVEL SECURITY
|
|
987
|
+
-- and a `FOR SELECT`-only policy, and no INSERT policy at all. Under the
|
|
988
|
+
-- documented production posture - a NON-superuser owner without BYPASSRLS -
|
|
989
|
+
-- the activation's own receipt write was therefore denied `42501` AFTER every
|
|
990
|
+
-- gate had already passed, which made the whole cutover unexecutable. The
|
|
991
|
+
-- runtime role holds only `GRANT SELECT` on this table and is not the owner, so
|
|
992
|
+
-- an owner-only marker policy re-opens exactly the one command
|
|
993
|
+
-- `activate_session_tenancy_product` issues and nothing else. INSERT is the
|
|
994
|
+
-- complete write set: the table is append-only, has no UPDATE or DELETE writer
|
|
995
|
+
-- anywhere in the tree, and activation is one-way with no rollback path.
|
|
996
|
+
DO $session_tenancy_activation_receipt_policy$
|
|
997
|
+
DECLARE
|
|
998
|
+
data_schema text := pg_catalog.current_schema();
|
|
999
|
+
migration_owner text := current_user;
|
|
1000
|
+
BEGIN
|
|
1001
|
+
EXECUTE pg_catalog.format(
|
|
1002
|
+
'CREATE POLICY session_tenancy_activation_receipt_insert '
|
|
1003
|
+
|| 'ON %I.session_tenancy_activations FOR INSERT WITH CHECK ('
|
|
1004
|
+
|| 'current_user = %L AND '
|
|
1005
|
+
|| 'current_setting(''opengeni.organization_tenancy_lifecycle'', true) '
|
|
1006
|
+
|| '= ''session_tenancy_activation'' AND account_id = nullif('
|
|
1007
|
+
|| 'current_setting(''opengeni.account_id'', true), '''')::uuid)',
|
|
1008
|
+
data_schema, migration_owner
|
|
1009
|
+
);
|
|
1010
|
+
END
|
|
1011
|
+
$session_tenancy_activation_receipt_policy$;
|
|
1012
|
+
|
|
1013
|
+
-- Existing receipts replay before new evidence requirements. A new activation
|
|
1014
|
+
-- takes the organization lifecycle advisory prefix, verifies the app drain,
|
|
1015
|
+
-- write-locks every report source, and only then takes the deployment boundary.
|
|
1016
|
+
-- Under those locks it recomputes all reports, validates the exact parity
|
|
1017
|
+
-- catalogs, compares canonical digests, and stores six receipt ids.
|
|
1018
|
+
CREATE OR REPLACE FUNCTION activate_session_tenancy_product(
|
|
1019
|
+
p_account_id uuid,
|
|
1020
|
+
p_inventory_digest text,
|
|
1021
|
+
p_parity_digest text,
|
|
1022
|
+
p_activated_by text,
|
|
1023
|
+
p_application_roles text[]
|
|
1024
|
+
) RETURNS TABLE (
|
|
1025
|
+
account_id uuid, activation_version integer,
|
|
1026
|
+
activated_at timestamptz, replay boolean
|
|
1027
|
+
)
|
|
1028
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
1029
|
+
SET search_path FROM CURRENT
|
|
1030
|
+
AS $activation$
|
|
1031
|
+
DECLARE
|
|
1032
|
+
existing session_tenancy_activations%ROWTYPE;
|
|
1033
|
+
inserted session_tenancy_activations%ROWTYPE;
|
|
1034
|
+
previous_lifecycle text := pg_catalog.current_setting(
|
|
1035
|
+
'opengeni.organization_tenancy_lifecycle', true
|
|
1036
|
+
);
|
|
1037
|
+
inventory_report jsonb;
|
|
1038
|
+
parity_report jsonb;
|
|
1039
|
+
backfill_evidence jsonb;
|
|
1040
|
+
computed_inventory_digest text;
|
|
1041
|
+
computed_parity_digest text;
|
|
1042
|
+
evidence_receipt_ids uuid[];
|
|
1043
|
+
required_gate text;
|
|
1044
|
+
required_lane text;
|
|
1045
|
+
required_gates constant text[] := ARRAY[
|
|
1046
|
+
'membership_personal_workspace_pointer',
|
|
1047
|
+
'membership_personal_workspace_exclusive',
|
|
1048
|
+
'membership_personal_workspace_same_organization',
|
|
1049
|
+
'personal_workspace_has_no_membership_row',
|
|
1050
|
+
'authority_resource_single_owner',
|
|
1051
|
+
'grant_delegation_fence_complete',
|
|
1052
|
+
'grant_owner_membership_active',
|
|
1053
|
+
'grant_authority_live',
|
|
1054
|
+
'grant_session_fence_not_ahead',
|
|
1055
|
+
'session_owner_provenance_paired',
|
|
1056
|
+
'session_owner_subject_matches_membership',
|
|
1057
|
+
'session_owner_membership_same_organization',
|
|
1058
|
+
'login_binding_dispute_propagated',
|
|
1059
|
+
'identity_active_binding_owned',
|
|
1060
|
+
'user_scoped_resource_live_anchor'
|
|
1061
|
+
];
|
|
1062
|
+
required_lanes constant text[] := ARRAY[
|
|
1063
|
+
'connectionsLegacyUser',
|
|
1064
|
+
'workspaceWriterAdmissionsLegacyUnattributedInWindow',
|
|
1065
|
+
'workspaceWriterProcessesLegacyUnattributedInWindow',
|
|
1066
|
+
'documentsLegacyPersonalNullAuthority',
|
|
1067
|
+
'codexCredentialsUnattributedConnector',
|
|
1068
|
+
'workspaceMemberSubjectsWithoutMembershipAnchor',
|
|
1069
|
+
'sessionsAttributableButUnattributed',
|
|
1070
|
+
'connectionUseLegacyResolutionsInWindow'
|
|
1071
|
+
];
|
|
1072
|
+
BEGIN
|
|
1073
|
+
IF p_account_id IS NULL
|
|
1074
|
+
OR p_inventory_digest !~ '^[0-9a-f]{64}$'
|
|
1075
|
+
OR p_parity_digest !~ '^[0-9a-f]{64}$'
|
|
1076
|
+
OR pg_catalog.octet_length(pg_catalog.btrim(p_activated_by)) NOT BETWEEN 1 AND 256
|
|
1077
|
+
OR p_application_roles IS NULL
|
|
1078
|
+
OR pg_catalog.cardinality(p_application_roles) NOT BETWEEN 1 AND 16
|
|
1079
|
+
OR EXISTS (
|
|
1080
|
+
SELECT 1 FROM pg_catalog.unnest(p_application_roles) role_name
|
|
1081
|
+
WHERE role_name IS NULL OR role_name <> pg_catalog.btrim(role_name)
|
|
1082
|
+
OR pg_catalog.octet_length(role_name) NOT BETWEEN 1 AND 63
|
|
1083
|
+
)
|
|
1084
|
+
OR pg_catalog.cardinality(ARRAY(
|
|
1085
|
+
SELECT DISTINCT role_name FROM pg_catalog.unnest(p_application_roles) role_name
|
|
1086
|
+
)) <> pg_catalog.cardinality(p_application_roles)
|
|
1087
|
+
THEN
|
|
1088
|
+
RAISE EXCEPTION 'session tenancy activation request is invalid'
|
|
1089
|
+
USING ERRCODE = '22023';
|
|
1090
|
+
END IF;
|
|
1091
|
+
IF EXISTS (
|
|
1092
|
+
SELECT 1 FROM pg_catalog.pg_stat_activity activity
|
|
1093
|
+
WHERE activity.datname = pg_catalog.current_database()
|
|
1094
|
+
AND activity.usename = ANY(p_application_roles)
|
|
1095
|
+
AND activity.pid <> pg_catalog.pg_backend_pid()
|
|
1096
|
+
) THEN
|
|
1097
|
+
RAISE EXCEPTION 'session tenancy activation requires every application role session to be stopped'
|
|
1098
|
+
USING ERRCODE = '55000';
|
|
1099
|
+
END IF;
|
|
1100
|
+
|
|
1101
|
+
PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
1102
|
+
'organization-membership:' || p_account_id::text, 0
|
|
1103
|
+
));
|
|
1104
|
+
LOCK TABLE
|
|
1105
|
+
workspaces, workspace_memberships, organization_memberships,
|
|
1106
|
+
canonical_human_identities, canonical_human_identity_subjects,
|
|
1107
|
+
canonical_human_login_bindings, sessions, session_turns,
|
|
1108
|
+
session_turn_attempts, session_attempt_interruptions,
|
|
1109
|
+
session_system_updates, session_human_input_requests,
|
|
1110
|
+
session_pending_tool_calls, agent_run_states, session_goals,
|
|
1111
|
+
codex_capacity_waiters, xai_capacity_waiters, session_realtime_modes,
|
|
1112
|
+
session_realtime_connections, scheduled_tasks,
|
|
1113
|
+
workspace_variable_sets, rigs, enrollments, connections, documents,
|
|
1114
|
+
codex_subscription_credentials, sandbox_workspace_mutation_admissions,
|
|
1115
|
+
sandbox_retained_processes, sandbox_lease_holders,
|
|
1116
|
+
organization_user_resource_authorities, organization_user_resource_grants,
|
|
1117
|
+
connection_use_audit_facts, tenancy_backfill_receipts,
|
|
1118
|
+
tenancy_backfill_unresolved_rows, session_tenancy_activations
|
|
1119
|
+
IN ACCESS EXCLUSIVE MODE;
|
|
1120
|
+
|
|
1121
|
+
IF EXISTS (
|
|
1122
|
+
SELECT 1 FROM pg_catalog.pg_stat_activity activity
|
|
1123
|
+
WHERE activity.datname = pg_catalog.current_database()
|
|
1124
|
+
AND activity.usename = ANY(p_application_roles)
|
|
1125
|
+
AND activity.pid <> pg_catalog.pg_backend_pid()
|
|
1126
|
+
) THEN
|
|
1127
|
+
RAISE EXCEPTION 'session tenancy activation requires every application role session to be stopped'
|
|
1128
|
+
USING ERRCODE = '55000';
|
|
1129
|
+
END IF;
|
|
1130
|
+
|
|
1131
|
+
-- Lock every provisioning source before the shared deployment fence. The
|
|
1132
|
+
-- greenfield path writes its graph first and takes the fence last; reversing
|
|
1133
|
+
-- this order would deadlock RowExclusive -> boundary against boundary ->
|
|
1134
|
+
-- AccessExclusive during the first cutover.
|
|
1135
|
+
PERFORM lock_session_tenancy_activation_boundary();
|
|
1136
|
+
PERFORM pg_catalog.set_config('opengeni.account_id', p_account_id::text, true);
|
|
1137
|
+
SELECT * INTO existing FROM session_tenancy_activations activation
|
|
1138
|
+
WHERE activation.account_id = p_account_id;
|
|
1139
|
+
IF FOUND THEN
|
|
1140
|
+
IF existing.inventory_digest <> p_inventory_digest
|
|
1141
|
+
OR existing.parity_digest <> p_parity_digest
|
|
1142
|
+
THEN
|
|
1143
|
+
RAISE EXCEPTION 'session tenancy activation evidence conflicts with the durable receipt'
|
|
1144
|
+
USING ERRCODE = '23505';
|
|
1145
|
+
END IF;
|
|
1146
|
+
account_id := existing.account_id;
|
|
1147
|
+
activation_version := existing.activation_version;
|
|
1148
|
+
activated_at := existing.activated_at;
|
|
1149
|
+
replay := true;
|
|
1150
|
+
RETURN NEXT;
|
|
1151
|
+
RETURN;
|
|
1152
|
+
END IF;
|
|
1153
|
+
|
|
1154
|
+
inventory_report := inventory_organization_tenancy(p_account_id);
|
|
1155
|
+
parity_report := check_organization_tenancy_parity(p_account_id, 10, 30);
|
|
1156
|
+
backfill_evidence := check_tenancy_backfill_activation_evidence(p_account_id);
|
|
1157
|
+
IF inventory_report #>> '{schemaVersion}' <> '2'
|
|
1158
|
+
OR inventory_report ->> 'organizationId' <> p_account_id::text
|
|
1159
|
+
OR parity_report #>> '{schemaVersion}' <> '1'
|
|
1160
|
+
OR parity_report ->> 'organizationId' <> p_account_id::text
|
|
1161
|
+
OR pg_catalog.jsonb_typeof(parity_report -> 'gates') <> 'object'
|
|
1162
|
+
OR pg_catalog.jsonb_typeof(parity_report -> 'lanes') <> 'object'
|
|
1163
|
+
THEN
|
|
1164
|
+
RAISE EXCEPTION 'session tenancy activation evidence is structurally invalid'
|
|
1165
|
+
USING ERRCODE = '55000';
|
|
1166
|
+
END IF;
|
|
1167
|
+
|
|
1168
|
+
FOREACH required_gate IN ARRAY required_gates LOOP
|
|
1169
|
+
IF NOT (parity_report -> 'gates' ? required_gate)
|
|
1170
|
+
OR pg_catalog.jsonb_typeof(parity_report -> 'gates' -> required_gate -> 'violations')
|
|
1171
|
+
<> 'number'
|
|
1172
|
+
OR (parity_report #>> ARRAY['gates', required_gate, 'violations'])::bigint <> 0
|
|
1173
|
+
THEN
|
|
1174
|
+
RAISE EXCEPTION 'session tenancy activation parity gate is not clean: %', required_gate
|
|
1175
|
+
USING ERRCODE = '55000';
|
|
1176
|
+
END IF;
|
|
1177
|
+
END LOOP;
|
|
1178
|
+
IF EXISTS (
|
|
1179
|
+
SELECT 1 FROM pg_catalog.jsonb_each(parity_report -> 'gates') gate
|
|
1180
|
+
WHERE pg_catalog.jsonb_typeof(gate.value -> 'violations') <> 'number'
|
|
1181
|
+
OR (gate.value ->> 'violations')::bigint <> 0
|
|
1182
|
+
) THEN
|
|
1183
|
+
RAISE EXCEPTION 'session tenancy activation parity has a non-zero gate'
|
|
1184
|
+
USING ERRCODE = '55000';
|
|
1185
|
+
END IF;
|
|
1186
|
+
FOREACH required_lane IN ARRAY required_lanes LOOP
|
|
1187
|
+
IF NOT (parity_report -> 'lanes' ? required_lane)
|
|
1188
|
+
OR pg_catalog.jsonb_typeof(parity_report -> 'lanes' -> required_lane) <> 'number'
|
|
1189
|
+
OR (parity_report #>> ARRAY['lanes', required_lane])::bigint <> 0
|
|
1190
|
+
THEN
|
|
1191
|
+
RAISE EXCEPTION 'session tenancy activation compatibility lane is not drained: %',
|
|
1192
|
+
required_lane USING ERRCODE = '55000';
|
|
1193
|
+
END IF;
|
|
1194
|
+
END LOOP;
|
|
1195
|
+
IF EXISTS (
|
|
1196
|
+
SELECT 1 FROM pg_catalog.jsonb_each(parity_report -> 'lanes') lane
|
|
1197
|
+
WHERE pg_catalog.jsonb_typeof(lane.value) <> 'number'
|
|
1198
|
+
OR (lane.value #>> '{}')::bigint <> 0
|
|
1199
|
+
) THEN
|
|
1200
|
+
RAISE EXCEPTION 'session tenancy activation has an undrained compatibility lane'
|
|
1201
|
+
USING ERRCODE = '55000';
|
|
1202
|
+
END IF;
|
|
1203
|
+
IF coalesce((backfill_evidence ->> 'ready')::boolean, false) IS NOT TRUE THEN
|
|
1204
|
+
RAISE EXCEPTION 'session tenancy activation requires settled backfill evidence'
|
|
1205
|
+
USING ERRCODE = '55000', DETAIL = (backfill_evidence -> 'blockers')::text;
|
|
1206
|
+
END IF;
|
|
1207
|
+
|
|
1208
|
+
computed_inventory_digest := pg_catalog.encode(public.digest(pg_catalog.convert_to(
|
|
1209
|
+
opengeni_private.tenancy_activation_canonical_json(inventory_report), 'UTF8'
|
|
1210
|
+
), 'sha256'), 'hex');
|
|
1211
|
+
computed_parity_digest := pg_catalog.encode(public.digest(pg_catalog.convert_to(
|
|
1212
|
+
opengeni_private.tenancy_activation_canonical_json(parity_report), 'UTF8'
|
|
1213
|
+
), 'sha256'), 'hex');
|
|
1214
|
+
IF computed_inventory_digest <> p_inventory_digest
|
|
1215
|
+
OR computed_parity_digest <> p_parity_digest
|
|
1216
|
+
THEN
|
|
1217
|
+
RAISE EXCEPTION 'session tenancy activation evidence changed before the cutover lock'
|
|
1218
|
+
USING ERRCODE = '40001';
|
|
1219
|
+
END IF;
|
|
1220
|
+
|
|
1221
|
+
SELECT pg_catalog.array_agg(receipt_id::uuid ORDER BY ordinal)
|
|
1222
|
+
INTO evidence_receipt_ids
|
|
1223
|
+
FROM pg_catalog.jsonb_array_elements_text(backfill_evidence -> 'receiptIds')
|
|
1224
|
+
WITH ORDINALITY AS evidence(receipt_id, ordinal);
|
|
1225
|
+
IF pg_catalog.cardinality(evidence_receipt_ids) <> 6 THEN
|
|
1226
|
+
RAISE EXCEPTION 'session tenancy activation backfill evidence is structurally invalid'
|
|
1227
|
+
USING ERRCODE = '55000';
|
|
1228
|
+
END IF;
|
|
1229
|
+
-- The narrow marker window for the one append this function performs. The
|
|
1230
|
+
-- `RETURNING` read is covered by 0303's existing organization-isolation
|
|
1231
|
+
-- SELECT policy, whose account GUC this function already set above.
|
|
1232
|
+
BEGIN
|
|
1233
|
+
PERFORM pg_catalog.set_config(
|
|
1234
|
+
'opengeni.organization_tenancy_lifecycle', 'session_tenancy_activation', true
|
|
1235
|
+
);
|
|
1236
|
+
INSERT INTO session_tenancy_activations (
|
|
1237
|
+
account_id, activation_version, inventory_digest, parity_digest,
|
|
1238
|
+
activated_by, backfill_receipt_ids
|
|
1239
|
+
) VALUES (
|
|
1240
|
+
p_account_id, 1, p_inventory_digest, p_parity_digest,
|
|
1241
|
+
pg_catalog.btrim(p_activated_by), evidence_receipt_ids
|
|
1242
|
+
) RETURNING * INTO inserted;
|
|
1243
|
+
PERFORM pg_catalog.set_config(
|
|
1244
|
+
'opengeni.organization_tenancy_lifecycle', coalesce(previous_lifecycle, ''), true
|
|
1245
|
+
);
|
|
1246
|
+
EXCEPTION WHEN OTHERS THEN
|
|
1247
|
+
PERFORM pg_catalog.set_config(
|
|
1248
|
+
'opengeni.organization_tenancy_lifecycle', coalesce(previous_lifecycle, ''), true
|
|
1249
|
+
);
|
|
1250
|
+
RAISE;
|
|
1251
|
+
END;
|
|
1252
|
+
account_id := inserted.account_id;
|
|
1253
|
+
activation_version := inserted.activation_version;
|
|
1254
|
+
activated_at := inserted.activated_at;
|
|
1255
|
+
replay := false;
|
|
1256
|
+
RETURN NEXT;
|
|
1257
|
+
END
|
|
1258
|
+
$activation$;
|
|
1259
|
+
REVOKE ALL ON FUNCTION
|
|
1260
|
+
activate_session_tenancy_product(uuid, text, text, text, text[]) FROM PUBLIC;
|
|
1261
|
+
|
|
1262
|
+
DO $posture$
|
|
1263
|
+
DECLARE
|
|
1264
|
+
data_schema text := pg_catalog.current_schema();
|
|
1265
|
+
BEGIN
|
|
1266
|
+
EXECUTE pg_catalog.format(
|
|
1267
|
+
'ALTER FUNCTION opengeni_private.session_tenancy_account_activated(uuid) '
|
|
1268
|
+
|| 'SET search_path = pg_catalog, %I, opengeni_private, pg_temp', data_schema
|
|
1269
|
+
);
|
|
1270
|
+
EXECUTE pg_catalog.format(
|
|
1271
|
+
'ALTER FUNCTION opengeni_private.bind_connection_owner_authority(uuid,text,uuid,uuid) '
|
|
1272
|
+
|| 'SET search_path = pg_catalog, %I, opengeni_private, pg_temp', data_schema
|
|
1273
|
+
);
|
|
1274
|
+
EXECUTE pg_catalog.format(
|
|
1275
|
+
'ALTER FUNCTION opengeni_private.bind_connection_authority() '
|
|
1276
|
+
|| 'SET search_path = pg_catalog, %I, opengeni_private, pg_temp', data_schema
|
|
1277
|
+
);
|
|
1278
|
+
EXECUTE pg_catalog.format(
|
|
1279
|
+
'ALTER FUNCTION %I.classify_organization_connection_authority(uuid,text) '
|
|
1280
|
+
|| 'SET search_path = pg_catalog, %I, opengeni_private, pg_temp',
|
|
1281
|
+
data_schema, data_schema
|
|
1282
|
+
);
|
|
1283
|
+
EXECUTE pg_catalog.format(
|
|
1284
|
+
'ALTER FUNCTION %I.backfill_organization_connection_authority(uuid,integer,boolean) '
|
|
1285
|
+
|| 'SET search_path = pg_catalog, %I, opengeni_private, pg_temp',
|
|
1286
|
+
data_schema, data_schema
|
|
1287
|
+
);
|
|
1288
|
+
EXECUTE pg_catalog.format(
|
|
1289
|
+
'ALTER FUNCTION opengeni_private.guard_activated_tenancy_writer() '
|
|
1290
|
+
|| 'SET search_path = pg_catalog, %I, opengeni_private, pg_temp', data_schema
|
|
1291
|
+
);
|
|
1292
|
+
EXECUTE pg_catalog.format(
|
|
1293
|
+
'ALTER FUNCTION %I.check_organization_tenancy_parity(uuid,integer,integer) '
|
|
1294
|
+
|| 'SET search_path = pg_catalog, %I, opengeni_private, pg_temp',
|
|
1295
|
+
data_schema, data_schema
|
|
1296
|
+
);
|
|
1297
|
+
EXECUTE pg_catalog.format(
|
|
1298
|
+
'ALTER FUNCTION %I.check_tenancy_backfill_activation_evidence(uuid) '
|
|
1299
|
+
|| 'SET search_path = pg_catalog, %I, opengeni_private, pg_temp',
|
|
1300
|
+
data_schema, data_schema
|
|
1301
|
+
);
|
|
1302
|
+
EXECUTE pg_catalog.format(
|
|
1303
|
+
'ALTER FUNCTION %I.lock_session_tenancy_activation_boundary() '
|
|
1304
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
1305
|
+
data_schema, data_schema
|
|
1306
|
+
);
|
|
1307
|
+
EXECUTE pg_catalog.format(
|
|
1308
|
+
'ALTER FUNCTION %I.activate_session_tenancy_product(uuid,text,text,text,text[]) '
|
|
1309
|
+
|| 'SET search_path = pg_catalog, %I, opengeni_private, public, pg_temp',
|
|
1310
|
+
data_schema, data_schema
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
IF EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
1314
|
+
GRANT EXECUTE ON FUNCTION
|
|
1315
|
+
opengeni_private.connection_tenancy_backfill_capability_active(uuid)
|
|
1316
|
+
TO opengeni_app;
|
|
1317
|
+
-- Referenced by the RESTRICTIVE `connections` retirement policy, so the
|
|
1318
|
+
-- runtime role must be able to evaluate it on every ordinary read even in
|
|
1319
|
+
-- the migrate-without-reprovision order.
|
|
1320
|
+
GRANT EXECUTE ON FUNCTION
|
|
1321
|
+
opengeni_private.session_tenancy_account_activated(uuid) TO opengeni_app;
|
|
1322
|
+
GRANT EXECUTE ON FUNCTION
|
|
1323
|
+
opengeni_private.bind_connection_owner_authority(uuid, text, uuid, uuid)
|
|
1324
|
+
TO opengeni_app;
|
|
1325
|
+
EXECUTE pg_catalog.format(
|
|
1326
|
+
'GRANT EXECUTE ON FUNCTION %I.classify_organization_connection_authority(uuid,text) TO opengeni_app',
|
|
1327
|
+
data_schema
|
|
1328
|
+
);
|
|
1329
|
+
EXECUTE pg_catalog.format(
|
|
1330
|
+
'GRANT EXECUTE ON FUNCTION %I.backfill_organization_connection_authority(uuid,integer,boolean) TO opengeni_app',
|
|
1331
|
+
data_schema
|
|
1332
|
+
);
|
|
1333
|
+
EXECUTE pg_catalog.format(
|
|
1334
|
+
'GRANT EXECUTE ON FUNCTION %I.check_organization_tenancy_parity(uuid,integer,integer) TO opengeni_app',
|
|
1335
|
+
data_schema
|
|
1336
|
+
);
|
|
1337
|
+
REVOKE ALL ON FUNCTION
|
|
1338
|
+
opengeni_private.check_organization_tenancy_parity_pre_0340(uuid,integer,integer)
|
|
1339
|
+
FROM opengeni_app;
|
|
1340
|
+
END IF;
|
|
1341
|
+
END
|
|
1342
|
+
$posture$;
|
|
1343
|
+
|
|
1344
|
+
COMMENT ON FUNCTION classify_organization_connection_authority(uuid, text) IS
|
|
1345
|
+
'Full-population connection authority classifier and migration-0300 receipt writer. Deterministic legacy_user repairs remain unresolved until the bounded upgrader converges them.';
|
|
1346
|
+
COMMENT ON FUNCTION backfill_organization_connection_authority(uuid, integer, boolean) IS
|
|
1347
|
+
'Bounded SKIP-LOCKED legacy_user upgrader using only the connection subject and one exact live same-organization membership; origin workspace and current access are never ownership evidence.';
|