@opengeni/db 1.0.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-LTG3S6ZY.js → chunk-EULAN2NP.js} +2 -2
- package/dist/{chunk-CUPM3FGB.js → chunk-NJVZPWXY.js} +3 -3
- package/dist/{chunk-4SR4HKKA.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-6ZYKCRFO.js → chunk-PR2WMLOQ.js} +7580 -4417
- package/dist/chunk-PR2WMLOQ.js.map +1 -0
- package/dist/{chunk-ZWREYEWA.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.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 +1595 -81
- package/dist/index.js +21571 -8126
- 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 +22 -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 +41 -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 +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/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/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-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 +11651 -1548
- 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 +985 -0
- package/src/pr-review.ts +890 -0
- package/src/preference-registry.ts +31 -1
- package/src/provision-roles.ts +877 -6
- package/src/runtime-posture-cli.ts +2 -0
- package/src/runtime-posture.ts +1291 -76
- package/src/schema.ts +3303 -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 +437 -84
- package/src/session-realtime-context.ts +3 -3
- package/src/session-realtime-ledger.ts +22 -11
- 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-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-LTG3S6ZY.js.map → chunk-EULAN2NP.js.map} +0 -0
- /package/dist/{chunk-CUPM3FGB.js.map → chunk-NJVZPWXY.js.map} +0 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Tenancy backfill receipt and unresolved-row ledger (organization-tenancy
|
|
3
|
+
-- backfill phase D).
|
|
4
|
+
--
|
|
5
|
+
-- `docs/organization-tenancy.md` phase D requires that backfill "record
|
|
6
|
+
-- backfill receipts and unresolved rows without widening access". This
|
|
7
|
+
-- migration owns only that ledger. It classifies nothing, reads no resource
|
|
8
|
+
-- table, and grants no authority: a receipt is content-free bookkeeping about
|
|
9
|
+
-- a backfill run, and an unresolved row records only that one exact resource
|
|
10
|
+
-- id could not be deterministically classified, plus a fixed reason code.
|
|
11
|
+
--
|
|
12
|
+
-- Two properties are load-bearing and deliberately enforced in the schema
|
|
13
|
+
-- rather than in application code:
|
|
14
|
+
--
|
|
15
|
+
-- 1. The ledger can never widen access. It stores resource ids and fixed
|
|
16
|
+
-- reason codes only - never resource content, names, subjects, owners, or
|
|
17
|
+
-- any proposed authority. An unresolved row is an explicit refusal to
|
|
18
|
+
-- infer, so it must not carry the inference it declined to make.
|
|
19
|
+
-- 2. `opengeni_app` holds no direct DML on either table. Both are FORCE RLS
|
|
20
|
+
-- and gated by the existing `opengeni.organization_tenancy_lifecycle`
|
|
21
|
+
-- GUC (0263's pattern) under the new `tenancy_backfill_ledger` operation
|
|
22
|
+
-- name, so the only write path is the SECURITY DEFINER seam below.
|
|
23
|
+
--
|
|
24
|
+
-- Unresolved rows are append-only: a row is a durable record that a human or a
|
|
25
|
+
-- later deterministic rule must resolve it, and silently mutating or deleting
|
|
26
|
+
-- one would erase the obligation. Receipts advance only forward through their
|
|
27
|
+
-- own lifecycle function.
|
|
28
|
+
|
|
29
|
+
SET LOCAL lock_timeout = '5s';
|
|
30
|
+
SET LOCAL statement_timeout = '10min';
|
|
31
|
+
|
|
32
|
+
CREATE TABLE "tenancy_backfill_receipts" (
|
|
33
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
34
|
+
"account_id" uuid NOT NULL REFERENCES "managed_accounts" ("id") ON DELETE CASCADE,
|
|
35
|
+
"resource_family" text NOT NULL,
|
|
36
|
+
"run_key" text NOT NULL,
|
|
37
|
+
"status" text NOT NULL DEFAULT 'open',
|
|
38
|
+
"classified_count" bigint NOT NULL DEFAULT 0,
|
|
39
|
+
"skipped_count" bigint NOT NULL DEFAULT 0,
|
|
40
|
+
"unresolved_count" bigint NOT NULL DEFAULT 0,
|
|
41
|
+
"started_at" timestamptz NOT NULL DEFAULT now(),
|
|
42
|
+
"completed_at" timestamptz,
|
|
43
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
44
|
+
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
45
|
+
CONSTRAINT "tenancy_backfill_receipt_family_chk" CHECK (
|
|
46
|
+
"resource_family" IN (
|
|
47
|
+
'organization_memberships',
|
|
48
|
+
'personal_workspaces',
|
|
49
|
+
'sessions',
|
|
50
|
+
'variable_sets',
|
|
51
|
+
'rigs',
|
|
52
|
+
'machines',
|
|
53
|
+
'connections',
|
|
54
|
+
'documents',
|
|
55
|
+
'codex_credentials'
|
|
56
|
+
)
|
|
57
|
+
),
|
|
58
|
+
CONSTRAINT "tenancy_backfill_receipt_status_chk" CHECK (
|
|
59
|
+
"status" IN ('open', 'completed', 'failed')
|
|
60
|
+
),
|
|
61
|
+
CONSTRAINT "tenancy_backfill_receipt_counts_chk" CHECK (
|
|
62
|
+
"classified_count" >= 0 AND "skipped_count" >= 0 AND "unresolved_count" >= 0
|
|
63
|
+
),
|
|
64
|
+
CONSTRAINT "tenancy_backfill_receipt_run_key_chk" CHECK (
|
|
65
|
+
length("run_key") BETWEEN 1 AND 200
|
|
66
|
+
),
|
|
67
|
+
-- A terminal receipt must be stamped; an open one must not be.
|
|
68
|
+
CONSTRAINT "tenancy_backfill_receipt_completion_chk" CHECK (
|
|
69
|
+
("status" = 'open' AND "completed_at" IS NULL)
|
|
70
|
+
OR ("status" <> 'open' AND "completed_at" IS NOT NULL)
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
-- One receipt per (organization, resource family, run). This is the
|
|
75
|
+
-- idempotency anchor: a re-run with the same key adopts the existing receipt
|
|
76
|
+
-- rather than creating a second, so a retried or resumed sweep cannot
|
|
77
|
+
-- double-count.
|
|
78
|
+
CREATE UNIQUE INDEX "tenancy_backfill_receipt_run_uq"
|
|
79
|
+
ON "tenancy_backfill_receipts" ("account_id", "resource_family", "run_key");
|
|
80
|
+
CREATE INDEX "tenancy_backfill_receipt_account_time_idx"
|
|
81
|
+
ON "tenancy_backfill_receipts" ("account_id", "started_at", "id");
|
|
82
|
+
|
|
83
|
+
CREATE TABLE "tenancy_backfill_unresolved_rows" (
|
|
84
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
85
|
+
"receipt_id" uuid NOT NULL
|
|
86
|
+
REFERENCES "tenancy_backfill_receipts" ("id") ON DELETE CASCADE,
|
|
87
|
+
"account_id" uuid NOT NULL REFERENCES "managed_accounts" ("id") ON DELETE CASCADE,
|
|
88
|
+
"resource_family" text NOT NULL,
|
|
89
|
+
"resource_id" uuid NOT NULL,
|
|
90
|
+
"reason_code" text NOT NULL,
|
|
91
|
+
"observed_at" timestamptz NOT NULL DEFAULT now(),
|
|
92
|
+
CONSTRAINT "tenancy_backfill_unresolved_family_chk" CHECK (
|
|
93
|
+
"resource_family" IN (
|
|
94
|
+
'organization_memberships',
|
|
95
|
+
'personal_workspaces',
|
|
96
|
+
'sessions',
|
|
97
|
+
'variable_sets',
|
|
98
|
+
'rigs',
|
|
99
|
+
'machines',
|
|
100
|
+
'connections',
|
|
101
|
+
'documents',
|
|
102
|
+
'codex_credentials'
|
|
103
|
+
)
|
|
104
|
+
),
|
|
105
|
+
-- Fixed, content-free vocabulary. Every code states why classification was
|
|
106
|
+
-- refused; none of them encodes a guess about what the answer might be.
|
|
107
|
+
CONSTRAINT "tenancy_backfill_unresolved_reason_chk" CHECK (
|
|
108
|
+
"reason_code" IN (
|
|
109
|
+
'no_deterministic_evidence',
|
|
110
|
+
'ambiguous_candidate_authority',
|
|
111
|
+
'missing_organization_membership',
|
|
112
|
+
'conflicting_authority_rows',
|
|
113
|
+
'external_lane_owns_row',
|
|
114
|
+
'legacy_shape_unrecognized'
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
-- One unresolved record per resource per receipt, so a resumed sweep that
|
|
120
|
+
-- re-observes the same row does not inflate the obligation count.
|
|
121
|
+
CREATE UNIQUE INDEX "tenancy_backfill_unresolved_row_uq"
|
|
122
|
+
ON "tenancy_backfill_unresolved_rows" ("receipt_id", "resource_family", "resource_id");
|
|
123
|
+
CREATE INDEX "tenancy_backfill_unresolved_account_family_idx"
|
|
124
|
+
ON "tenancy_backfill_unresolved_rows" ("account_id", "resource_family", "observed_at");
|
|
125
|
+
|
|
126
|
+
ALTER TABLE "tenancy_backfill_receipts" ENABLE ROW LEVEL SECURITY;
|
|
127
|
+
ALTER TABLE "tenancy_backfill_receipts" FORCE ROW LEVEL SECURITY;
|
|
128
|
+
ALTER TABLE "tenancy_backfill_unresolved_rows" ENABLE ROW LEVEL SECURITY;
|
|
129
|
+
ALTER TABLE "tenancy_backfill_unresolved_rows" FORCE ROW LEVEL SECURITY;
|
|
130
|
+
|
|
131
|
+
-- Writes are reachable only from the lifecycle seam below, which is the sole
|
|
132
|
+
-- setter of this GUC value. Reads additionally stay organization-scoped.
|
|
133
|
+
DROP POLICY IF EXISTS organization_tenancy_lifecycle ON "tenancy_backfill_receipts";
|
|
134
|
+
CREATE POLICY organization_tenancy_lifecycle ON "tenancy_backfill_receipts"
|
|
135
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
136
|
+
IN ('tenancy_backfill_ledger'))
|
|
137
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
138
|
+
IN ('tenancy_backfill_ledger'));
|
|
139
|
+
|
|
140
|
+
DROP POLICY IF EXISTS organization_tenancy_lifecycle ON "tenancy_backfill_unresolved_rows";
|
|
141
|
+
CREATE POLICY organization_tenancy_lifecycle ON "tenancy_backfill_unresolved_rows"
|
|
142
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
143
|
+
IN ('tenancy_backfill_ledger'))
|
|
144
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
145
|
+
IN ('tenancy_backfill_ledger'));
|
|
146
|
+
|
|
147
|
+
-- Unresolved rows are append-only evidence. Cascade cleanup from a deleted
|
|
148
|
+
-- organization or receipt is permitted; a direct rewrite is not.
|
|
149
|
+
--
|
|
150
|
+
-- The permitted-cascade test is deliberately NOT "is the parent row gone".
|
|
151
|
+
-- This function is SECURITY DEFINER, so its reads run as the table owner, and
|
|
152
|
+
-- under FORCE RLS with no lifecycle GUC set the owner sees zero rows in
|
|
153
|
+
-- `tenancy_backfill_receipts`. A parent-absence probe therefore reports
|
|
154
|
+
-- "parent gone" while the receipt is very much alive, and any nested delete
|
|
155
|
+
-- (`pg_trigger_depth() > 1`) would slip through - the guard would be weakest
|
|
156
|
+
-- exactly where the caller is most privileged. The test harness cannot catch
|
|
157
|
+
-- that, because it migrates as a superuser, for whom FORCE RLS never engages.
|
|
158
|
+
--
|
|
159
|
+
-- Instead the exemption is proven from referential provenance: PostgreSQL runs
|
|
160
|
+
-- FK `ON DELETE CASCADE` as an internal RI trigger, which is the only context
|
|
161
|
+
-- this guard may yield to. We claim the lifecycle GUC before probing so the
|
|
162
|
+
-- owner's own reads are policy-visible, and restore whatever the caller had.
|
|
163
|
+
CREATE OR REPLACE FUNCTION guard_tenancy_backfill_unresolved_row()
|
|
164
|
+
RETURNS trigger
|
|
165
|
+
LANGUAGE plpgsql
|
|
166
|
+
SECURITY DEFINER
|
|
167
|
+
SET search_path FROM CURRENT
|
|
168
|
+
AS $$
|
|
169
|
+
DECLARE
|
|
170
|
+
previous_lifecycle_marker text := pg_catalog.current_setting(
|
|
171
|
+
'opengeni.organization_tenancy_lifecycle', true
|
|
172
|
+
);
|
|
173
|
+
parent_alive boolean;
|
|
174
|
+
BEGIN
|
|
175
|
+
IF TG_OP = 'DELETE' THEN
|
|
176
|
+
IF pg_trigger_depth() > 1 THEN
|
|
177
|
+
PERFORM pg_catalog.set_config(
|
|
178
|
+
'opengeni.organization_tenancy_lifecycle', 'tenancy_backfill_ledger', true
|
|
179
|
+
);
|
|
180
|
+
SELECT EXISTS (SELECT 1 FROM managed_accounts WHERE id = OLD.account_id)
|
|
181
|
+
AND EXISTS (SELECT 1 FROM tenancy_backfill_receipts WHERE id = OLD.receipt_id)
|
|
182
|
+
INTO parent_alive;
|
|
183
|
+
PERFORM pg_catalog.set_config(
|
|
184
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
185
|
+
coalesce(previous_lifecycle_marker, ''), true
|
|
186
|
+
);
|
|
187
|
+
IF NOT parent_alive THEN
|
|
188
|
+
RETURN OLD;
|
|
189
|
+
END IF;
|
|
190
|
+
END IF;
|
|
191
|
+
RAISE EXCEPTION 'tenancy backfill unresolved rows are append-only'
|
|
192
|
+
USING ERRCODE = '42501';
|
|
193
|
+
END IF;
|
|
194
|
+
RAISE EXCEPTION 'tenancy backfill unresolved rows are immutable'
|
|
195
|
+
USING ERRCODE = '42501';
|
|
196
|
+
END;
|
|
197
|
+
$$;
|
|
198
|
+
|
|
199
|
+
CREATE TRIGGER guard_tenancy_backfill_unresolved_row
|
|
200
|
+
BEFORE UPDATE OR DELETE ON "tenancy_backfill_unresolved_rows"
|
|
201
|
+
FOR EACH ROW EXECUTE FUNCTION guard_tenancy_backfill_unresolved_row();
|
|
202
|
+
|
|
203
|
+
-- Opens or adopts the receipt for one exact (organization, family, run).
|
|
204
|
+
-- Idempotent: a resumed sweep re-adopts its own open receipt and never opens a
|
|
205
|
+
-- second one. Re-opening a settled receipt is refused rather than silently
|
|
206
|
+
-- reviving it, because its counts are already evidence.
|
|
207
|
+
--
|
|
208
|
+
-- Idempotency is CONCURRENT, not merely serial. `SELECT ... FOR UPDATE` locks
|
|
209
|
+
-- nothing when no row exists yet, so two sweeps opening the same run key would
|
|
210
|
+
-- both fall through to the INSERT and one would surface a raw `23505` instead
|
|
211
|
+
-- of adopting its peer's receipt. The insert therefore yields to the unique
|
|
212
|
+
-- index (`ON CONFLICT DO NOTHING`) and re-selects the winner's row under the
|
|
213
|
+
-- same lock. Under READ COMMITTED, PostgreSQL's speculative insertion has
|
|
214
|
+
-- already waited for the conflicting transaction, so the re-select either sees
|
|
215
|
+
-- the committed winner or (if that peer rolled back) the retried insert
|
|
216
|
+
-- succeeded and there is nothing to re-read.
|
|
217
|
+
CREATE OR REPLACE FUNCTION open_tenancy_backfill_receipt(
|
|
218
|
+
p_account_id uuid,
|
|
219
|
+
p_resource_family text,
|
|
220
|
+
p_run_key text
|
|
221
|
+
) RETURNS uuid
|
|
222
|
+
LANGUAGE plpgsql
|
|
223
|
+
SECURITY DEFINER
|
|
224
|
+
SET search_path FROM CURRENT
|
|
225
|
+
AS $$
|
|
226
|
+
DECLARE
|
|
227
|
+
previous_lifecycle_marker text := pg_catalog.current_setting(
|
|
228
|
+
'opengeni.organization_tenancy_lifecycle', true
|
|
229
|
+
);
|
|
230
|
+
receipt_id uuid;
|
|
231
|
+
receipt_status text;
|
|
232
|
+
BEGIN
|
|
233
|
+
IF p_account_id IS NULL OR p_resource_family IS NULL OR p_run_key IS NULL THEN
|
|
234
|
+
RAISE EXCEPTION 'tenancy backfill receipt requires organization, family, and run key'
|
|
235
|
+
USING ERRCODE = '22004';
|
|
236
|
+
END IF;
|
|
237
|
+
|
|
238
|
+
IF p_account_id IS DISTINCT FROM nullif(
|
|
239
|
+
pg_catalog.current_setting('opengeni.account_id', true), ''
|
|
240
|
+
)::uuid
|
|
241
|
+
THEN
|
|
242
|
+
RAISE EXCEPTION 'tenancy backfill ledger scope mismatch'
|
|
243
|
+
USING ERRCODE = '42501';
|
|
244
|
+
END IF;
|
|
245
|
+
|
|
246
|
+
PERFORM pg_catalog.set_config(
|
|
247
|
+
'opengeni.organization_tenancy_lifecycle', 'tenancy_backfill_ledger', true
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
SELECT existing.id, existing.status INTO receipt_id, receipt_status
|
|
251
|
+
FROM tenancy_backfill_receipts existing
|
|
252
|
+
WHERE existing.account_id = p_account_id
|
|
253
|
+
AND existing.resource_family = p_resource_family
|
|
254
|
+
AND existing.run_key = p_run_key
|
|
255
|
+
FOR UPDATE;
|
|
256
|
+
|
|
257
|
+
IF receipt_id IS NULL THEN
|
|
258
|
+
INSERT INTO tenancy_backfill_receipts (account_id, resource_family, run_key)
|
|
259
|
+
VALUES (p_account_id, p_resource_family, p_run_key)
|
|
260
|
+
ON CONFLICT ("account_id", "resource_family", "run_key") DO NOTHING
|
|
261
|
+
RETURNING id, status INTO receipt_id, receipt_status;
|
|
262
|
+
|
|
263
|
+
IF receipt_id IS NULL THEN
|
|
264
|
+
-- A concurrent opener committed the same run key first: adopt its row.
|
|
265
|
+
SELECT existing.id, existing.status INTO receipt_id, receipt_status
|
|
266
|
+
FROM tenancy_backfill_receipts existing
|
|
267
|
+
WHERE existing.account_id = p_account_id
|
|
268
|
+
AND existing.resource_family = p_resource_family
|
|
269
|
+
AND existing.run_key = p_run_key
|
|
270
|
+
FOR UPDATE;
|
|
271
|
+
|
|
272
|
+
IF receipt_id IS NULL THEN
|
|
273
|
+
RAISE EXCEPTION 'tenancy backfill receipt for run key % could not be opened or adopted',
|
|
274
|
+
p_run_key
|
|
275
|
+
USING ERRCODE = '40001';
|
|
276
|
+
END IF;
|
|
277
|
+
END IF;
|
|
278
|
+
END IF;
|
|
279
|
+
|
|
280
|
+
IF receipt_status <> 'open' THEN
|
|
281
|
+
RAISE EXCEPTION 'tenancy backfill receipt % is already settled', receipt_id
|
|
282
|
+
USING ERRCODE = '55000';
|
|
283
|
+
END IF;
|
|
284
|
+
|
|
285
|
+
PERFORM pg_catalog.set_config(
|
|
286
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
287
|
+
coalesce(previous_lifecycle_marker, ''), true
|
|
288
|
+
);
|
|
289
|
+
RETURN receipt_id;
|
|
290
|
+
END;
|
|
291
|
+
$$;
|
|
292
|
+
|
|
293
|
+
-- Records that one exact resource could not be deterministically classified.
|
|
294
|
+
-- Deliberately takes no authority/owner/subject argument: this seam exists to
|
|
295
|
+
-- record a refusal to infer, so it must be impossible to smuggle a guess
|
|
296
|
+
-- through it. Idempotent per (receipt, family, resource).
|
|
297
|
+
CREATE OR REPLACE FUNCTION record_tenancy_backfill_unresolved(
|
|
298
|
+
p_receipt_id uuid,
|
|
299
|
+
p_resource_id uuid,
|
|
300
|
+
p_reason_code text
|
|
301
|
+
) RETURNS void
|
|
302
|
+
LANGUAGE plpgsql
|
|
303
|
+
SECURITY DEFINER
|
|
304
|
+
SET search_path FROM CURRENT
|
|
305
|
+
AS $$
|
|
306
|
+
DECLARE
|
|
307
|
+
previous_lifecycle_marker text := pg_catalog.current_setting(
|
|
308
|
+
'opengeni.organization_tenancy_lifecycle', true
|
|
309
|
+
);
|
|
310
|
+
receipt_row record;
|
|
311
|
+
inserted_count integer;
|
|
312
|
+
BEGIN
|
|
313
|
+
IF p_receipt_id IS NULL OR p_resource_id IS NULL OR p_reason_code IS NULL THEN
|
|
314
|
+
RAISE EXCEPTION 'tenancy backfill unresolved row requires receipt, resource, and reason'
|
|
315
|
+
USING ERRCODE = '22004';
|
|
316
|
+
END IF;
|
|
317
|
+
|
|
318
|
+
PERFORM pg_catalog.set_config(
|
|
319
|
+
'opengeni.organization_tenancy_lifecycle', 'tenancy_backfill_ledger', true
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
SELECT receipt.id, receipt.account_id, receipt.resource_family, receipt.status
|
|
323
|
+
INTO receipt_row
|
|
324
|
+
FROM tenancy_backfill_receipts receipt
|
|
325
|
+
WHERE receipt.id = p_receipt_id
|
|
326
|
+
FOR UPDATE;
|
|
327
|
+
|
|
328
|
+
IF receipt_row.id IS NULL THEN
|
|
329
|
+
RAISE EXCEPTION 'tenancy backfill receipt % does not exist', p_receipt_id
|
|
330
|
+
USING ERRCODE = '23503';
|
|
331
|
+
END IF;
|
|
332
|
+
IF receipt_row.account_id IS DISTINCT FROM nullif(
|
|
333
|
+
pg_catalog.current_setting('opengeni.account_id', true), ''
|
|
334
|
+
)::uuid
|
|
335
|
+
THEN
|
|
336
|
+
RAISE EXCEPTION 'tenancy backfill ledger scope mismatch'
|
|
337
|
+
USING ERRCODE = '42501';
|
|
338
|
+
END IF;
|
|
339
|
+
IF receipt_row.status <> 'open' THEN
|
|
340
|
+
RAISE EXCEPTION 'tenancy backfill receipt % is already settled', p_receipt_id
|
|
341
|
+
USING ERRCODE = '55000';
|
|
342
|
+
END IF;
|
|
343
|
+
|
|
344
|
+
INSERT INTO tenancy_backfill_unresolved_rows (
|
|
345
|
+
receipt_id, account_id, resource_family, resource_id, reason_code
|
|
346
|
+
)
|
|
347
|
+
VALUES (
|
|
348
|
+
receipt_row.id, receipt_row.account_id, receipt_row.resource_family,
|
|
349
|
+
p_resource_id, p_reason_code
|
|
350
|
+
)
|
|
351
|
+
ON CONFLICT ("receipt_id", "resource_family", "resource_id") DO NOTHING;
|
|
352
|
+
|
|
353
|
+
GET DIAGNOSTICS inserted_count = ROW_COUNT;
|
|
354
|
+
IF inserted_count > 0 THEN
|
|
355
|
+
UPDATE tenancy_backfill_receipts
|
|
356
|
+
SET unresolved_count = unresolved_count + 1, updated_at = now()
|
|
357
|
+
WHERE id = receipt_row.id;
|
|
358
|
+
END IF;
|
|
359
|
+
|
|
360
|
+
PERFORM pg_catalog.set_config(
|
|
361
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
362
|
+
coalesce(previous_lifecycle_marker, ''), true
|
|
363
|
+
);
|
|
364
|
+
END;
|
|
365
|
+
$$;
|
|
366
|
+
|
|
367
|
+
-- Settles a receipt. The unresolved count is owned by the append path above
|
|
368
|
+
-- and is never supplied by the caller, so a caller cannot understate its own
|
|
369
|
+
-- outstanding obligations.
|
|
370
|
+
CREATE OR REPLACE FUNCTION complete_tenancy_backfill_receipt(
|
|
371
|
+
p_receipt_id uuid,
|
|
372
|
+
p_classified_count bigint,
|
|
373
|
+
p_skipped_count bigint,
|
|
374
|
+
p_status text DEFAULT 'completed'
|
|
375
|
+
) RETURNS void
|
|
376
|
+
LANGUAGE plpgsql
|
|
377
|
+
SECURITY DEFINER
|
|
378
|
+
SET search_path FROM CURRENT
|
|
379
|
+
AS $$
|
|
380
|
+
DECLARE
|
|
381
|
+
previous_lifecycle_marker text := pg_catalog.current_setting(
|
|
382
|
+
'opengeni.organization_tenancy_lifecycle', true
|
|
383
|
+
);
|
|
384
|
+
receipt_status text;
|
|
385
|
+
receipt_account_id uuid;
|
|
386
|
+
BEGIN
|
|
387
|
+
IF p_receipt_id IS NULL OR p_classified_count IS NULL OR p_skipped_count IS NULL THEN
|
|
388
|
+
RAISE EXCEPTION 'tenancy backfill completion requires receipt and counts'
|
|
389
|
+
USING ERRCODE = '22004';
|
|
390
|
+
END IF;
|
|
391
|
+
IF p_status NOT IN ('completed', 'failed') THEN
|
|
392
|
+
RAISE EXCEPTION 'tenancy backfill completion status must be completed or failed'
|
|
393
|
+
USING ERRCODE = '22023';
|
|
394
|
+
END IF;
|
|
395
|
+
IF p_classified_count < 0 OR p_skipped_count < 0 THEN
|
|
396
|
+
RAISE EXCEPTION 'tenancy backfill counts must be non-negative'
|
|
397
|
+
USING ERRCODE = '22023';
|
|
398
|
+
END IF;
|
|
399
|
+
|
|
400
|
+
PERFORM pg_catalog.set_config(
|
|
401
|
+
'opengeni.organization_tenancy_lifecycle', 'tenancy_backfill_ledger', true
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
SELECT receipt.status, receipt.account_id INTO receipt_status, receipt_account_id
|
|
405
|
+
FROM tenancy_backfill_receipts receipt
|
|
406
|
+
WHERE receipt.id = p_receipt_id
|
|
407
|
+
FOR UPDATE;
|
|
408
|
+
|
|
409
|
+
IF receipt_status IS NULL THEN
|
|
410
|
+
RAISE EXCEPTION 'tenancy backfill receipt % does not exist', p_receipt_id
|
|
411
|
+
USING ERRCODE = '23503';
|
|
412
|
+
END IF;
|
|
413
|
+
IF receipt_account_id IS DISTINCT FROM nullif(
|
|
414
|
+
pg_catalog.current_setting('opengeni.account_id', true), ''
|
|
415
|
+
)::uuid
|
|
416
|
+
THEN
|
|
417
|
+
RAISE EXCEPTION 'tenancy backfill ledger scope mismatch'
|
|
418
|
+
USING ERRCODE = '42501';
|
|
419
|
+
END IF;
|
|
420
|
+
IF receipt_status <> 'open' THEN
|
|
421
|
+
RAISE EXCEPTION 'tenancy backfill receipt % is already settled', p_receipt_id
|
|
422
|
+
USING ERRCODE = '55000';
|
|
423
|
+
END IF;
|
|
424
|
+
|
|
425
|
+
UPDATE tenancy_backfill_receipts
|
|
426
|
+
SET status = p_status,
|
|
427
|
+
classified_count = p_classified_count,
|
|
428
|
+
skipped_count = p_skipped_count,
|
|
429
|
+
completed_at = now(),
|
|
430
|
+
updated_at = now()
|
|
431
|
+
WHERE id = p_receipt_id;
|
|
432
|
+
|
|
433
|
+
PERFORM pg_catalog.set_config(
|
|
434
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
435
|
+
coalesce(previous_lifecycle_marker, ''), true
|
|
436
|
+
);
|
|
437
|
+
END;
|
|
438
|
+
$$;
|
|
439
|
+
|
|
440
|
+
REVOKE ALL ON TABLE "tenancy_backfill_receipts" FROM PUBLIC;
|
|
441
|
+
REVOKE ALL ON TABLE "tenancy_backfill_unresolved_rows" FROM PUBLIC;
|
|
442
|
+
REVOKE ALL ON FUNCTION guard_tenancy_backfill_unresolved_row() FROM PUBLIC;
|
|
443
|
+
REVOKE ALL ON FUNCTION open_tenancy_backfill_receipt(uuid,text,text) FROM PUBLIC;
|
|
444
|
+
REVOKE ALL ON FUNCTION record_tenancy_backfill_unresolved(uuid,uuid,text) FROM PUBLIC;
|
|
445
|
+
REVOKE ALL ON FUNCTION complete_tenancy_backfill_receipt(uuid,bigint,bigint,text) FROM PUBLIC;
|
|
446
|
+
|
|
447
|
+
DO $tenancy_backfill_ledger_grants$
|
|
448
|
+
BEGIN
|
|
449
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
450
|
+
-- No direct DML: the lifecycle seam is the only write path, and reads go
|
|
451
|
+
-- through it or through an operator command running as the owner.
|
|
452
|
+
REVOKE ALL ON TABLE "tenancy_backfill_receipts" FROM opengeni_app;
|
|
453
|
+
REVOKE ALL ON TABLE "tenancy_backfill_unresolved_rows" FROM opengeni_app;
|
|
454
|
+
GRANT EXECUTE ON FUNCTION open_tenancy_backfill_receipt(uuid,text,text) TO opengeni_app;
|
|
455
|
+
GRANT EXECUTE ON FUNCTION record_tenancy_backfill_unresolved(uuid,uuid,text) TO opengeni_app;
|
|
456
|
+
GRANT EXECUTE ON FUNCTION complete_tenancy_backfill_receipt(uuid,bigint,bigint,text)
|
|
457
|
+
TO opengeni_app;
|
|
458
|
+
END IF;
|
|
459
|
+
END $tenancy_backfill_ledger_grants$;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Visibility-isolate the two session references that migration 0225's
|
|
3
|
+
-- foreign-key loop could not reach correctly.
|
|
4
|
+
--
|
|
5
|
+
-- 1. `session_list_snapshots.ordinary_session_ids` is a bare `uuid[]` with no
|
|
6
|
+
-- foreign key to `sessions`, so 0225's loop (which enumerates relations that
|
|
7
|
+
-- carry an FK to `sessions.id`) never installed `session_visibility_isolation`
|
|
8
|
+
-- on it. A cached list page captured while a session was `workspace_shared`
|
|
9
|
+
-- therefore keeps holding that session id for the snapshot's whole TTL after
|
|
10
|
+
-- the session transitions to `user_private` — the exact "cache stripping"
|
|
11
|
+
-- prerequisite `docs/organization-tenancy.md` phase F names.
|
|
12
|
+
--
|
|
13
|
+
-- An explicit visibility predicate over the array was rejected: a snapshot
|
|
14
|
+
-- holds up to 5,000 ids and a subject may hold up to 32 live snapshots, so a
|
|
15
|
+
-- RESTRICTIVE policy would have to evaluate up to 160,000 per-element
|
|
16
|
+
-- `session_reference_visible()` calls on the hot first-page read, it would
|
|
17
|
+
-- turn a partially stale cache into a hard 410 for the whole row, and (like
|
|
18
|
+
-- the pin defect below) it would make the row permanently undeletable. The
|
|
19
|
+
-- identity is instead stripped once, at the transition, where the cost is
|
|
20
|
+
-- bounded by the snapshots that actually name the session.
|
|
21
|
+
--
|
|
22
|
+
-- The stripped slot is REPLACED by the reserved all-zero UUID rather than
|
|
23
|
+
-- removed, so snapshot cardinality and every in-flight cursor offset stay
|
|
24
|
+
-- byte-stable. `gen_random_uuid()` never produces the nil UUID, so the slot
|
|
25
|
+
-- matches no session and the read path drops it exactly as it already drops
|
|
26
|
+
-- a session that RLS hid between pages. `array_remove` would shift every
|
|
27
|
+
-- later offset and silently skip one still-visible session.
|
|
28
|
+
--
|
|
29
|
+
-- 2. `session_pins` IS covered by 0225's loop, with a `FOR ALL` RESTRICTIVE
|
|
30
|
+
-- policy. That makes a non-owner's stale pin both invisible and permanently
|
|
31
|
+
-- undeletable by the member who created it: PostgreSQL applies SELECT
|
|
32
|
+
-- policies to a DELETE that reads any column, so a command-scoped
|
|
33
|
+
-- `FOR DELETE` exemption cannot work. The USING side therefore gains an
|
|
34
|
+
-- explicit owner escape. It discloses nothing new: the pre-existing
|
|
35
|
+
-- permissive `workspace_isolation` policy already limits every visible pin to
|
|
36
|
+
-- `subject_id = current_subject_id()`, and a pin row's only session-derived
|
|
37
|
+
-- field is an id that same subject supplied. The WITH CHECK side keeps the
|
|
38
|
+
-- strict predicate, so a member still cannot pin — or flip `pinned` on — a
|
|
39
|
+
-- session they cannot see, and INSERT cannot be used as a session-existence
|
|
40
|
+
-- oracle through the foreign key.
|
|
41
|
+
|
|
42
|
+
SET LOCAL lock_timeout = '5s';
|
|
43
|
+
SET LOCAL statement_timeout = '10min';
|
|
44
|
+
|
|
45
|
+
DO $session_visibility_cache_stripping$
|
|
46
|
+
DECLARE
|
|
47
|
+
data_schema text := current_schema();
|
|
48
|
+
capability_predicate text;
|
|
49
|
+
BEGIN
|
|
50
|
+
-- The lifecycle needs to strip OTHER subjects' cached pages, which the
|
|
51
|
+
-- subject-scoped `workspace_isolation` policy forbids. Reuse 0225's
|
|
52
|
+
-- transaction-local capability instead of a forgeable GUC marker: the
|
|
53
|
+
-- capability row lives in `session_visibility_write_capabilities`, which is
|
|
54
|
+
-- FORCE RLS, owner-only, and fully revoked from the runtime role, so the
|
|
55
|
+
-- runtime role cannot mint one. `pg_current_xact_id_if_assigned()` keeps this
|
|
56
|
+
-- predicate from assigning a transaction id on read-only traffic.
|
|
57
|
+
EXECUTE format($ddl$
|
|
58
|
+
CREATE OR REPLACE FUNCTION %1$I.session_visibility_lifecycle_capability_held()
|
|
59
|
+
RETURNS boolean
|
|
60
|
+
LANGUAGE sql
|
|
61
|
+
STABLE
|
|
62
|
+
SECURITY DEFINER
|
|
63
|
+
SET search_path = %1$I, pg_catalog
|
|
64
|
+
AS $body$
|
|
65
|
+
SELECT EXISTS (
|
|
66
|
+
SELECT 1
|
|
67
|
+
FROM session_visibility_write_capabilities capability
|
|
68
|
+
WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
|
|
69
|
+
AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
70
|
+
AND capability.capability_id = nullif(
|
|
71
|
+
pg_catalog.current_setting(
|
|
72
|
+
'opengeni.session_visibility_write_capability', true
|
|
73
|
+
),
|
|
74
|
+
''
|
|
75
|
+
)::uuid
|
|
76
|
+
)
|
|
77
|
+
$body$;
|
|
78
|
+
$ddl$, data_schema);
|
|
79
|
+
EXECUTE format(
|
|
80
|
+
'REVOKE ALL ON FUNCTION %I.session_visibility_lifecycle_capability_held() FROM PUBLIC',
|
|
81
|
+
data_schema
|
|
82
|
+
);
|
|
83
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
84
|
+
EXECUTE format(
|
|
85
|
+
'GRANT EXECUTE ON FUNCTION %I.session_visibility_lifecycle_capability_held() TO opengeni_app',
|
|
86
|
+
data_schema
|
|
87
|
+
);
|
|
88
|
+
END IF;
|
|
89
|
+
|
|
90
|
+
-- The cheap GUC test short-circuits ordinary traffic before the capability
|
|
91
|
+
-- lookup; the GUC alone is never authority.
|
|
92
|
+
capability_predicate := format(
|
|
93
|
+
'nullif(pg_catalog.current_setting('
|
|
94
|
+
|| '''opengeni.session_visibility_write_capability'', true), '''') IS NOT NULL '
|
|
95
|
+
|| 'AND %I.session_visibility_lifecycle_capability_held()',
|
|
96
|
+
data_schema
|
|
97
|
+
);
|
|
98
|
+
EXECUTE format(
|
|
99
|
+
'DROP POLICY IF EXISTS session_visibility_cache_stripping ON %I.session_list_snapshots',
|
|
100
|
+
data_schema
|
|
101
|
+
);
|
|
102
|
+
EXECUTE format(
|
|
103
|
+
'CREATE POLICY session_visibility_cache_stripping ON %I.session_list_snapshots '
|
|
104
|
+
|| 'FOR ALL USING (%s) WITH CHECK (%s)',
|
|
105
|
+
data_schema,
|
|
106
|
+
capability_predicate,
|
|
107
|
+
capability_predicate
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
EXECUTE format($ddl$
|
|
111
|
+
CREATE OR REPLACE FUNCTION %1$I.strip_private_session_list_snapshots()
|
|
112
|
+
RETURNS trigger
|
|
113
|
+
LANGUAGE plpgsql
|
|
114
|
+
SECURITY DEFINER
|
|
115
|
+
SET search_path = %1$I, pg_catalog
|
|
116
|
+
AS $body$
|
|
117
|
+
BEGIN
|
|
118
|
+
-- Every authorized visibility transition already holds the 0225 write
|
|
119
|
+
-- capability (its own `sessions` UPDATE is impossible without it), so the
|
|
120
|
+
-- strip rides on the exact same authority in the exact same transaction.
|
|
121
|
+
-- The owner's own cached pages stay intact: the session remains visible to
|
|
122
|
+
-- them, so their snapshot is not stale.
|
|
123
|
+
UPDATE session_list_snapshots snapshot
|
|
124
|
+
SET ordinary_session_ids = coalesce((
|
|
125
|
+
SELECT pg_catalog.array_agg(
|
|
126
|
+
CASE WHEN entry.element = NEW.id
|
|
127
|
+
THEN '00000000-0000-0000-0000-000000000000'::uuid
|
|
128
|
+
ELSE entry.element
|
|
129
|
+
END
|
|
130
|
+
ORDER BY entry.ordinality
|
|
131
|
+
)
|
|
132
|
+
FROM pg_catalog.unnest(snapshot.ordinary_session_ids)
|
|
133
|
+
WITH ORDINALITY AS entry(element, ordinality)
|
|
134
|
+
), '{}'::uuid[])
|
|
135
|
+
WHERE snapshot.account_id = NEW.account_id
|
|
136
|
+
AND snapshot.workspace_id = NEW.workspace_id
|
|
137
|
+
AND snapshot.subject_id IS DISTINCT FROM NEW.owner_subject_id
|
|
138
|
+
AND NEW.id = ANY(snapshot.ordinary_session_ids);
|
|
139
|
+
RETURN NULL;
|
|
140
|
+
END;
|
|
141
|
+
$body$;
|
|
142
|
+
$ddl$, data_schema);
|
|
143
|
+
EXECUTE format(
|
|
144
|
+
'DROP TRIGGER IF EXISTS sessions_strip_private_list_snapshots ON %I.sessions',
|
|
145
|
+
data_schema
|
|
146
|
+
);
|
|
147
|
+
EXECUTE format(
|
|
148
|
+
'CREATE TRIGGER sessions_strip_private_list_snapshots '
|
|
149
|
+
|| 'AFTER UPDATE OF visibility ON %1$I.sessions '
|
|
150
|
+
|| 'FOR EACH ROW '
|
|
151
|
+
|| 'WHEN (NEW.visibility = ''user_private'' '
|
|
152
|
+
|| 'AND OLD.visibility IS DISTINCT FROM NEW.visibility) '
|
|
153
|
+
|| 'EXECUTE FUNCTION %1$I.strip_private_session_list_snapshots()',
|
|
154
|
+
data_schema
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
EXECUTE format(
|
|
158
|
+
'DROP POLICY IF EXISTS session_visibility_isolation ON %I.session_pins',
|
|
159
|
+
data_schema
|
|
160
|
+
);
|
|
161
|
+
EXECUTE format(
|
|
162
|
+
'CREATE POLICY session_visibility_isolation ON %1$I.session_pins AS RESTRICTIVE '
|
|
163
|
+
|| 'FOR ALL USING ('
|
|
164
|
+
|| '%1$I.session_reference_visible(account_id, workspace_id, session_id) '
|
|
165
|
+
|| 'OR subject_id = opengeni_private.current_subject_id()'
|
|
166
|
+
|| ') WITH CHECK ('
|
|
167
|
+
|| '%1$I.session_reference_visible(account_id, workspace_id, session_id)'
|
|
168
|
+
|| ')',
|
|
169
|
+
data_schema
|
|
170
|
+
);
|
|
171
|
+
END
|
|
172
|
+
$session_visibility_cache_stripping$;
|
|
173
|
+
|
|
174
|
+
COMMENT ON COLUMN "session_list_snapshots"."ordinary_session_ids" IS
|
|
175
|
+
'Ordered slots of one short-lived cached list page. A slot holding the reserved all-zero UUID is a redacted session identity stripped by a visibility transition; it preserves cursor offsets and resolves to no session.';
|
|
176
|
+
|
|
177
|
+
COMMENT ON FUNCTION strip_private_session_list_snapshots() IS
|
|
178
|
+
'Strips a session identity from every other subject''s cached list page when that session becomes user_private, under the 0225 visibility write capability.';
|
|
179
|
+
|
|
180
|
+
COMMENT ON FUNCTION session_visibility_lifecycle_capability_held() IS
|
|
181
|
+
'True only inside a transaction holding an owner-minted session-visibility write capability; the runtime role cannot mint one.';
|