@opengeni/db 1.0.1 → 3.2.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attached-browser-devices.d.ts +7 -0
- package/dist/automations.d.ts +154 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-integrations.d.ts +13 -0
- package/dist/child-lifecycle-notices.d.ts +68 -0
- package/dist/{chunk-4SR4HKKA.js → chunk-2EH4WRHL.js} +19 -11
- package/dist/chunk-2EH4WRHL.js.map +1 -0
- package/dist/{chunk-CUPM3FGB.js → chunk-2PRS4J5Q.js} +3 -3
- package/dist/chunk-6HMM2R5T.js +390 -0
- package/dist/chunk-6HMM2R5T.js.map +1 -0
- package/dist/{chunk-ZWREYEWA.js → chunk-AZHW7R5V.js} +56 -11
- package/dist/chunk-AZHW7R5V.js.map +1 -0
- package/dist/{chunk-LTG3S6ZY.js → chunk-E6CLMKEE.js} +2 -2
- package/dist/{chunk-6ZYKCRFO.js → chunk-KRLPEMLH.js} +7591 -4417
- package/dist/chunk-KRLPEMLH.js.map +1 -0
- package/dist/chunk-NAB6FKFN.js +516 -0
- package/dist/chunk-NAB6FKFN.js.map +1 -0
- package/dist/{chunk-SK7YYHBI.js → chunk-SIBYBA2D.js} +46 -1
- package/dist/chunk-SIBYBA2D.js.map +1 -0
- package/dist/chunk-UZNXG44K.js +4508 -0
- package/dist/chunk-UZNXG44K.js.map +1 -0
- package/dist/codemode-operations.d.ts +1 -6
- package/dist/company-brain-context-selection-schema.d.ts +705 -0
- package/dist/company-brain-context-selection.d.ts +94 -0
- package/dist/company-brain-governed-writes.d.ts +24 -0
- package/dist/company-brain.d.ts +55 -0
- package/dist/company-profile-agent-admin.d.ts +15 -0
- package/dist/company-profile-schema.d.ts +619 -2
- package/dist/company-profile.d.ts +9 -4
- package/dist/connection-authority.d.ts +95 -0
- package/dist/connection-scopes.d.ts +8 -0
- package/dist/connection-tenancy-backfill.d.ts +85 -0
- package/dist/connection-token-resolver.d.ts +30 -1
- package/dist/database.d.ts +23 -4
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.d.ts +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/governed-learning-activation-schema.d.ts +984 -0
- package/dist/governed-learning-activation.d.ts +98 -0
- package/dist/governed-learning-evaluator-schema.d.ts +633 -0
- package/dist/governed-learning-evaluator.d.ts +28 -0
- package/dist/index.d.ts +1606 -81
- package/dist/index.js +21898 -8147
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +18 -0
- package/dist/integration-bindings.d.ts +1 -0
- package/dist/integration-facets.d.ts +30 -0
- package/dist/interaction-placement-loss.d.ts +22 -0
- package/dist/managed-human-provisioning.d.ts +25 -0
- package/dist/memory-domain.d.ts +3 -3
- package/dist/migrate.d.ts +11 -1
- package/dist/migrate.js +1 -1
- package/dist/new-session-drafts.d.ts +43 -5
- package/dist/organization-membership-backfill.d.ts +174 -0
- package/dist/organization-membership-lifecycle.d.ts +162 -0
- package/dist/pr-review.d.ts +107 -0
- package/dist/preference-registry-schema.d.ts +2 -0
- package/dist/preference-registry.d.ts +2 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +29 -8
- package/dist/schema.d.ts +24914 -13703
- package/dist/schema.js +99 -1
- package/dist/scoped-knowledge-schema.d.ts +52 -1
- package/dist/session-background-commands.d.ts +125 -0
- package/dist/session-background-commands.js +34 -0
- package/dist/session-background-commands.js.map +1 -0
- package/dist/session-control.d.ts +156 -3
- package/dist/session-goal-pacing.d.ts +68 -0
- package/dist/session-queue-commands.d.ts +43 -4
- package/dist/session-realtime-ledger.d.ts +2 -0
- package/dist/session-tenancy.d.ts +71 -1
- package/dist/session-tenancy.js +25 -3
- package/dist/session-tool-call-settlement.d.ts +9 -4
- package/dist/slack-routing-personal-workspace.d.ts +23 -0
- package/dist/slack-routing.d.ts +268 -0
- package/dist/slack-task-policy-schema.d.ts +34 -0
- package/dist/slack-task-policy.d.ts +8 -0
- package/dist/slack-user-link-access.d.ts +28 -0
- package/dist/task-notes-schema.d.ts +594 -0
- package/dist/task-notes.d.ts +39 -0
- package/dist/turn-initiator.d.ts +2 -0
- package/dist/user-resource-authority.d.ts +109 -0
- package/dist/video-generation.js +3 -3
- package/dist/workspace-authority.d.ts +81 -0
- package/dist/workspace-instruction-policies-schema.d.ts +279 -0
- package/dist/workspace-instruction-policies.d.ts +62 -0
- package/dist/workspace-learning-policy.d.ts +1 -0
- package/dist/workspace-membership-permissions.d.ts +2 -0
- package/drizzle/0240_enrollment_connection_authority.sql +39 -0
- package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
- package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
- package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
- package/drizzle/0245_model_context_contribution_facts.sql +88 -0
- package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
- package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
- package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
- package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
- package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
- package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
- package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
- package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
- package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
- package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
- package/drizzle/0256_connection_authority_delegation.sql +703 -0
- package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
- package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
- package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
- package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
- package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
- package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
- package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
- package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
- package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
- package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
- package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
- package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
- package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
- package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
- package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
- package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
- package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
- package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
- package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
- package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
- package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
- package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
- package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
- package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
- package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
- package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
- package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
- package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
- package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
- package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
- package/drizzle/0289_session_composer_policy_authority.sql +597 -0
- package/drizzle/0290_organization_membership_backfill.sql +268 -0
- package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
- package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
- package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
- package/drizzle/0294_preference_activation_authority.sql +237 -0
- package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
- package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
- package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
- package/drizzle/0298_organization_tenancy_parity.sql +609 -0
- package/drizzle/0299_organization_membership_lock_order.sql +291 -0
- package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
- package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
- package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
- package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
- package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
- package/drizzle/0305_personal_resource_grant_management.sql +584 -0
- package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
- package/drizzle/0307_session_attention_state.sql +51 -0
- package/drizzle/0308_session_archives.sql +35 -0
- package/drizzle/0309_channel_project_pins.sql +6 -0
- package/drizzle/0310_channel_project_order.sql +27 -0
- package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
- package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
- package/drizzle/0313_private_child_session_authority.sql +365 -0
- package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
- package/drizzle/0315_personal_github_repository_selection.sql +624 -0
- package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
- package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
- package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
- package/drizzle/0319_event_triggered_automations.sql +254 -0
- package/drizzle/0320_pr_review_pack.sql +113 -0
- package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
- package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
- package/drizzle/0323_organization_private_session_enablement.sql +489 -0
- package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
- package/drizzle/0325_child_lifecycle_notices.sql +44 -0
- package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
- package/drizzle/0327_slack_first_task_hint.sql +29 -0
- package/drizzle/0328_session_background_commands.sql +590 -0
- package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
- package/drizzle/0330_api_key_descriptions.sql +20 -0
- package/drizzle/0331_managed_organization_creation.sql +163 -0
- package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
- package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
- package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
- package/drizzle/0335_slack_workspace_routing.sql +460 -0
- package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
- package/drizzle/0337_slack_routed_action_handles.sql +189 -0
- package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
- package/drizzle/0339_document_authority_reclassification.sql +1153 -0
- package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
- package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -0
- package/drizzle/0342_slack_route_prompt_single_pending.sql +32 -0
- package/drizzle/0343_personal_document_force_rls_lock_repair.sql +146 -0
- package/drizzle/0344_private_session_visibility_transition_gate.sql +85 -0
- package/drizzle/0345_tenant_scoped_session_tenancy_fence.sql +2839 -0
- package/drizzle/0346_document_migration_audit_surface.sql +454 -0
- package/drizzle/0347_connection_authority_convergence_evidence.sql +396 -0
- package/package.json +10 -6
- package/src/attached-browser-devices.ts +233 -5
- package/src/automations.ts +911 -0
- package/src/browser-sessions.ts +10 -0
- package/src/capability-integrations.ts +175 -1
- package/src/child-lifecycle-notices.ts +241 -0
- package/src/codemode-operations.ts +1 -35
- package/src/company-brain-context-selection-schema.ts +100 -0
- package/src/company-brain-context-selection.ts +359 -0
- package/src/company-brain-governed-writes.ts +1170 -0
- package/src/company-brain.ts +309 -0
- package/src/company-profile-agent-admin.ts +225 -0
- package/src/company-profile-schema.ts +121 -1
- package/src/company-profile.ts +21 -0
- package/src/computer-sessions.ts +10 -1
- package/src/connection-authority.ts +290 -0
- package/src/connection-scopes.ts +20 -0
- package/src/connection-tenancy-backfill.ts +167 -0
- package/src/connection-token-resolver.ts +263 -31
- package/src/database.ts +87 -2
- package/src/editable-artifacts-schema.ts +10 -5
- package/src/editable-artifacts.ts +21 -11
- package/src/governed-learning-activation-schema.ts +121 -0
- package/src/governed-learning-activation.ts +588 -0
- package/src/governed-learning-evaluator-schema.ts +72 -0
- package/src/governed-learning-evaluator.ts +217 -0
- package/src/index.ts +11676 -1550
- package/src/insights.ts +107 -0
- package/src/integration-bindings.ts +88 -6
- package/src/integration-facets.ts +287 -84
- package/src/interaction-placement-loss.ts +234 -0
- package/src/managed-human-provisioning.ts +116 -0
- package/src/memory-domain.ts +27 -32
- package/src/migrate.ts +77 -1
- package/src/new-session-drafts.ts +229 -35
- package/src/organization-membership-backfill.ts +735 -0
- package/src/organization-membership-lifecycle.ts +995 -0
- package/src/pr-review.ts +890 -0
- package/src/preference-registry.ts +31 -1
- package/src/provision-roles.ts +905 -6
- package/src/runtime-posture-cli.ts +2 -0
- package/src/runtime-posture.ts +1317 -72
- package/src/schema.ts +3316 -428
- package/src/scoped-knowledge-schema.ts +16 -8
- package/src/scoped-knowledge.ts +1 -1
- package/src/session-background-commands.ts +759 -0
- package/src/session-control.ts +640 -74
- package/src/session-goal-pacing.ts +126 -0
- package/src/session-queue-commands.ts +441 -85
- package/src/session-realtime-context.ts +4 -6
- package/src/session-realtime-ledger.ts +28 -14
- package/src/session-realtime-mirror.ts +1 -1
- package/src/session-tenancy.ts +410 -49
- package/src/session-tool-call-settlement.ts +38 -29
- package/src/slack-routing-personal-workspace.ts +67 -0
- package/src/slack-routing.ts +867 -0
- package/src/slack-task-policy-schema.ts +5 -0
- package/src/slack-task-policy.ts +18 -0
- package/src/slack-user-link-access.ts +138 -11
- package/src/task-notes-schema.ts +74 -0
- package/src/task-notes.ts +124 -2
- package/src/turn-initiator.ts +8 -0
- package/src/user-resource-authority.ts +377 -0
- package/src/workspace-authority.ts +160 -0
- package/src/workspace-instruction-policies-schema.ts +53 -1
- package/src/workspace-instruction-policies.ts +528 -77
- package/src/workspace-learning-policy.ts +21 -5
- package/src/workspace-membership-permissions.ts +9 -0
- package/dist/chunk-4SR4HKKA.js.map +0 -1
- package/dist/chunk-6ZYKCRFO.js.map +0 -1
- package/dist/chunk-NXAFLCQX.js +0 -2468
- package/dist/chunk-NXAFLCQX.js.map +0 -1
- package/dist/chunk-SK7YYHBI.js.map +0 -1
- package/dist/chunk-WD5ROSLL.js +0 -117
- package/dist/chunk-WD5ROSLL.js.map +0 -1
- package/dist/chunk-ZWREYEWA.js.map +0 -1
- /package/dist/{chunk-CUPM3FGB.js.map → chunk-2PRS4J5Q.js.map} +0 -0
- /package/dist/{chunk-LTG3S6ZY.js.map → chunk-E6CLMKEE.js.map} +0 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Migration 0297: session ownership classification plus the one deterministic
|
|
3
|
+
-- backfill (organization-tenancy phase D, slice 6).
|
|
4
|
+
--
|
|
5
|
+
-- THE PREMISE THIS SLICE WAS SCOPED ON WAS WRONG
|
|
6
|
+
-- ----------------------------------------------
|
|
7
|
+
-- Slice 6 was planned as "sessions are 100% owner-NULL; nothing writes session
|
|
8
|
+
-- ownership". That is false. Migration 0225's `guard_session_authority_write`
|
|
9
|
+
-- trigger DERIVES session ownership on every INSERT, and 0225's
|
|
10
|
+
-- `session_visibility_isolation` policies are live on `sessions` and on every
|
|
11
|
+
-- table that references it. A session created today through the ordinary
|
|
12
|
+
-- `createSession` path by a managed human who holds a `workspace_memberships`
|
|
13
|
+
-- row comes out with `owner_organization_membership_id` and `owner_subject_id`
|
|
14
|
+
-- populated. There is therefore no undifferentiated ownerless backlog to drain
|
|
15
|
+
-- - there is a live derivation rule, and a precisely shaped set of rows it
|
|
16
|
+
-- cannot reach.
|
|
17
|
+
--
|
|
18
|
+
-- THE ACTUAL POPULATION (measured against the live trigger, not assumed)
|
|
19
|
+
-- ---------------------------------------------------------------------
|
|
20
|
+
-- 0225's derivation has exactly two branches, both INSERT-only and both taken
|
|
21
|
+
-- only when BOTH owner columns arrive NULL:
|
|
22
|
+
--
|
|
23
|
+
-- 1. `parent_session_id IS NOT NULL` - inherit the parent's owner pair
|
|
24
|
+
-- verbatim from the same (account, workspace). The parent branch WINS
|
|
25
|
+
-- even when it resolves NULL, so a child of an ownerless parent is
|
|
26
|
+
-- ownerless no matter who created it.
|
|
27
|
+
-- 2. otherwise, `created_by_kind = 'subject'` - the active
|
|
28
|
+
-- `organization_memberships` row for `created_by_subject_id`, but ONLY
|
|
29
|
+
-- when that subject also holds a `workspace_memberships` row in the
|
|
30
|
+
-- session's workspace.
|
|
31
|
+
--
|
|
32
|
+
-- Everything else silently keeps a NULL owner pair. Enumerated:
|
|
33
|
+
--
|
|
34
|
+
-- * `created_by_kind = 'service'` - the scheduler, site-auth maintenance,
|
|
35
|
+
-- delegated embedding-host service initiators, and the pre-0096
|
|
36
|
+
-- `unattributed-legacy` default. No human is named at all.
|
|
37
|
+
-- * `created_by_kind = 'subject'` with a non-`user:` subject - `api_key:*`
|
|
38
|
+
-- and `configured:*`. Both 0219 (`p_subject_id NOT LIKE 'user:%'` rejects
|
|
39
|
+
-- provisioning) and 0263 (invitations, role backfill, self-membership
|
|
40
|
+
-- listing) are gated to `user:%`, so these subjects can NEVER acquire an
|
|
41
|
+
-- organization anchor. These rows are permanently unrepairable, and API
|
|
42
|
+
-- keys keep minting new ones, so this is a GROWING population and not a
|
|
43
|
+
-- drainable backlog.
|
|
44
|
+
-- * a `user:` subject with no active organization membership.
|
|
45
|
+
-- * A MANAGED HUMAN IN THEIR OWN PERSONAL WORKSPACE. Slice B provisions the
|
|
46
|
+
-- personal workspace as a pointer on the membership row WITHOUT a durable
|
|
47
|
+
-- `workspace_memberships` row (docs/organization-tenancy.md). Branch 2's
|
|
48
|
+
-- `EXISTS (workspace_memberships ...)` guard therefore fails, and the
|
|
49
|
+
-- human's own personal-workspace sessions come out ownerless. This is the
|
|
50
|
+
-- one population where the organization's own durable lifecycle data
|
|
51
|
+
-- already names exactly one subject.
|
|
52
|
+
--
|
|
53
|
+
-- WHAT IS REPAIRABLE, AND WHAT IS NOT
|
|
54
|
+
-- -----------------------------------
|
|
55
|
+
-- REPAIRABLE (this migration's backfill, and nothing else):
|
|
56
|
+
--
|
|
57
|
+
-- A. Personal-workspace convergence. `sessions.workspace_id` is exactly the
|
|
58
|
+
-- `personal_workspace_id` of one ACTIVE `organization_memberships` row -
|
|
59
|
+
-- a 1:1 lifecycle anchor enforced by 0218's unique
|
|
60
|
+
-- `organization_memberships_personal_workspace_idx` on
|
|
61
|
+
-- (account_id, personal_workspace_id) - AND the session's
|
|
62
|
+
-- `created_by_subject_id` is that same membership's `subject_id`. Two
|
|
63
|
+
-- independent durable facts converge on one subject, and the workspace
|
|
64
|
+
-- anchor alone already identifies the owner; `created_by` is used only as
|
|
65
|
+
-- a veto, never as the evidence. This is the same convergence the
|
|
66
|
+
-- activated 0264 runtime exception uses to derive an owner-only
|
|
67
|
+
-- personal-workspace grant. When the two facts DISAGREE the row is
|
|
68
|
+
-- recorded `ambiguous_candidate_authority` and left alone.
|
|
69
|
+
-- B. Parent inheritance closure. An ownerless session whose same-workspace
|
|
70
|
+
-- parent now HAS an owner pair. This is branch 1 of the live trigger
|
|
71
|
+
-- replayed against durable parent data - not a new rule - and it is what
|
|
72
|
+
-- makes (A) leave the tree in exactly the state 0225 would have produced
|
|
73
|
+
-- had the root been attributable in the first place. It is also what
|
|
74
|
+
-- makes the driver resumable across a crash between the root write and
|
|
75
|
+
-- the closure.
|
|
76
|
+
--
|
|
77
|
+
-- NOT REPAIRABLE, deliberately:
|
|
78
|
+
--
|
|
79
|
+
-- * A managed human's session in an ORDINARY SHARED workspace that predates
|
|
80
|
+
-- 0225 (or that was created while the human lacked a workspace-membership
|
|
81
|
+
-- row). Branch 2 could be replayed, but its `workspace_memberships`
|
|
82
|
+
-- predicate is evaluated against TODAY's grants, and phase D says in as
|
|
83
|
+
-- many words: never infer user authority from `created_by`, connection
|
|
84
|
+
-- attribution, a default workspace, a resource name, or CURRENT ACCESS. A
|
|
85
|
+
-- workspace membership granted after the session was created, or revoked
|
|
86
|
+
-- and re-granted since, would silently manufacture authority. Recorded
|
|
87
|
+
-- `no_deterministic_evidence`.
|
|
88
|
+
-- * Every `service`-created session (`legacy_shape_unrecognized`) and every
|
|
89
|
+
-- non-`user:` subject session (`external_lane_owns_row`). No amount of
|
|
90
|
+
-- later lifecycle work can attribute these; they are permanently
|
|
91
|
+
-- ownerless by construction.
|
|
92
|
+
--
|
|
93
|
+
-- WHY A SECURITY DEFINER SEAM AND NOT A MIGRATION-TIME `UPDATE`
|
|
94
|
+
-- -------------------------------------------------------------
|
|
95
|
+
-- `sessions` is FORCE ROW LEVEL SECURITY and its ordinary policy is
|
|
96
|
+
-- workspace-scoped, which is false while the `opengeni.workspace_id` GUC is
|
|
97
|
+
-- unset - as it is during migration. FORCE RLS applies to the table owner, and
|
|
98
|
+
-- OpenGeni's documented deployment posture is a NON-superuser migration
|
|
99
|
+
-- principal without BYPASSRLS. A bare `UPDATE sessions ... WHERE ...` in a
|
|
100
|
+
-- migration body therefore matches ZERO rows and reports success on such a
|
|
101
|
+
-- deployment; it only appears to work in a harness that migrates as superuser.
|
|
102
|
+
-- Both routines below consequently run behind a transaction-scoped
|
|
103
|
+
-- capability-claiming policy, exactly like 0254, 0262, 0285, and the sibling
|
|
104
|
+
-- classification seam.
|
|
105
|
+
--
|
|
106
|
+
-- WRITE FENCES THIS SEAM HONOURS
|
|
107
|
+
-- ------------------------------
|
|
108
|
+
-- * 0225 block C: any UPDATE touching the owner pair needs a matching
|
|
109
|
+
-- `session_visibility_write_capabilities` row plus the
|
|
110
|
+
-- `opengeni.session_visibility_write_capability` GUC. The backfill mints
|
|
111
|
+
-- one for the duration of its own write and deletes it afterwards, on both
|
|
112
|
+
-- the success and the exception path.
|
|
113
|
+
-- * 0225 block B: the owner pair must move together, and the backfill never
|
|
114
|
+
-- touches `visibility` or `authority_epoch`. Every candidate row is
|
|
115
|
+
-- necessarily `workspace_shared` (0218's `sessions_private_owner_check`
|
|
116
|
+
-- forbids `user_private` without an owner), and
|
|
117
|
+
-- `session_visibility_isolation` short-circuits on `workspace_shared`, so
|
|
118
|
+
-- attributing an owner changes NO read for anybody. It is classification,
|
|
119
|
+
-- not a grant.
|
|
120
|
+
-- * 0214's commit gate fires only `BEFORE INSERT OR UPDATE OF updated_at,
|
|
121
|
+
-- activity_revision, activity_revision_pending_xid`. The backfill writes
|
|
122
|
+
-- only the two owner columns, so it neither needs nor forges a session
|
|
123
|
+
-- activity gate, and it does not bump `updated_at` - an operator
|
|
124
|
+
-- classification pass is not session activity.
|
|
125
|
+
-- * No `session_events` row is appended. The timeline is exact human/audit
|
|
126
|
+
-- truth for accepted payloads; the evidence for this run is the tenancy
|
|
127
|
+
-- backfill ledger receipt, which is what phase D actually asks for.
|
|
128
|
+
--
|
|
129
|
+
-- Receipt recording is guarded by `to_regprocedure` so this migration is
|
|
130
|
+
-- order-independent with respect to the ledger migration and reports
|
|
131
|
+
-- `ledgerAvailable` honestly rather than failing or silently skipping.
|
|
132
|
+
|
|
133
|
+
SET LOCAL lock_timeout = '5s';
|
|
134
|
+
SET LOCAL statement_timeout = '5min';
|
|
135
|
+
|
|
136
|
+
-- Freeze the active data schema into the SECURITY DEFINER routines below.
|
|
137
|
+
-- Standalone installs resolve to public; embedded installs resolve to their
|
|
138
|
+
-- dedicated schema. pg_temp remains explicitly last.
|
|
139
|
+
SELECT pg_catalog.set_config(
|
|
140
|
+
'search_path',
|
|
141
|
+
pg_catalog.format('%I, pg_catalog, pg_temp', pg_catalog.current_schema()),
|
|
142
|
+
true
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
-- A transaction-scoped, migration-owner-only capability. Deliberately NOT a
|
|
146
|
+
-- reuse of 0285's inventory capability or the sibling classification one: each
|
|
147
|
+
-- of those policies pins the literal `current_user` observed while its own
|
|
148
|
+
-- migration ran, and neither grants the UPDATE this seam needs on `sessions`.
|
|
149
|
+
CREATE TABLE opengeni_private.session_ownership_capabilities (
|
|
150
|
+
"backend_pid" integer NOT NULL,
|
|
151
|
+
"transaction_id" xid8 NOT NULL,
|
|
152
|
+
CONSTRAINT "session_ownership_capabilities_pk" PRIMARY KEY (
|
|
153
|
+
"backend_pid", "transaction_id"
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
REVOKE ALL ON TABLE opengeni_private.session_ownership_capabilities FROM PUBLIC;
|
|
157
|
+
|
|
158
|
+
CREATE OR REPLACE FUNCTION opengeni_private.session_ownership_capability_active()
|
|
159
|
+
RETURNS boolean
|
|
160
|
+
LANGUAGE sql
|
|
161
|
+
STABLE
|
|
162
|
+
SECURITY DEFINER
|
|
163
|
+
SET search_path = pg_catalog
|
|
164
|
+
AS $capability_active$
|
|
165
|
+
SELECT EXISTS (
|
|
166
|
+
SELECT 1
|
|
167
|
+
FROM opengeni_private.session_ownership_capabilities capability
|
|
168
|
+
WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
|
|
169
|
+
AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
170
|
+
)
|
|
171
|
+
$capability_active$;
|
|
172
|
+
REVOKE ALL ON FUNCTION
|
|
173
|
+
opengeni_private.session_ownership_capability_active() FROM PUBLIC;
|
|
174
|
+
|
|
175
|
+
DO $session_ownership_capability_policies$
|
|
176
|
+
DECLARE
|
|
177
|
+
data_schema text := current_schema();
|
|
178
|
+
migration_owner text := current_user;
|
|
179
|
+
BEGIN
|
|
180
|
+
-- `sessions` needs FOR ALL because the backfill both reads and writes it;
|
|
181
|
+
-- the WITH CHECK half is the same predicate, so the capability can never
|
|
182
|
+
-- move a row out of the organization it was claimed for. The membership
|
|
183
|
+
-- table stays read-only here - this seam never mints or mutates authority.
|
|
184
|
+
EXECUTE format(
|
|
185
|
+
'CREATE POLICY session_ownership_capability_write ON %I.sessions '
|
|
186
|
+
|| 'FOR ALL USING (current_user = %L AND '
|
|
187
|
+
|| 'opengeni_private.session_ownership_capability_active()) '
|
|
188
|
+
|| 'WITH CHECK (current_user = %L AND '
|
|
189
|
+
|| 'opengeni_private.session_ownership_capability_active())',
|
|
190
|
+
data_schema,
|
|
191
|
+
migration_owner,
|
|
192
|
+
migration_owner
|
|
193
|
+
);
|
|
194
|
+
EXECUTE format(
|
|
195
|
+
'CREATE POLICY session_ownership_capability_read ON %I.organization_memberships '
|
|
196
|
+
|| 'FOR SELECT USING (current_user = %L AND '
|
|
197
|
+
|| 'opengeni_private.session_ownership_capability_active())',
|
|
198
|
+
data_schema,
|
|
199
|
+
migration_owner
|
|
200
|
+
);
|
|
201
|
+
END
|
|
202
|
+
$session_ownership_capability_policies$;
|
|
203
|
+
|
|
204
|
+
-- Shared preconditions for both routines. The subject check is load-bearing:
|
|
205
|
+
-- `session_visibility_isolation` is RESTRICTIVE and short-circuits only when
|
|
206
|
+
-- no `opengeni.subject_id` GUC is set. Running this seam inside an actor scope
|
|
207
|
+
-- would silently hide another owner's `user_private` sessions and UNDERCOUNT
|
|
208
|
+
-- rather than fail, so it is refused outright.
|
|
209
|
+
CREATE OR REPLACE FUNCTION opengeni_private.assert_session_ownership_scope(
|
|
210
|
+
p_organization_id uuid,
|
|
211
|
+
p_run_key text
|
|
212
|
+
) RETURNS void
|
|
213
|
+
LANGUAGE plpgsql
|
|
214
|
+
STABLE
|
|
215
|
+
SET search_path FROM CURRENT
|
|
216
|
+
AS $$
|
|
217
|
+
BEGIN
|
|
218
|
+
IF p_organization_id IS NULL THEN
|
|
219
|
+
RAISE EXCEPTION 'session ownership classification requires an organization id'
|
|
220
|
+
USING ERRCODE = '22004';
|
|
221
|
+
END IF;
|
|
222
|
+
IF p_organization_id IS DISTINCT FROM nullif(
|
|
223
|
+
pg_catalog.current_setting('opengeni.account_id', true), ''
|
|
224
|
+
)::uuid
|
|
225
|
+
THEN
|
|
226
|
+
RAISE EXCEPTION 'session ownership classification scope mismatch'
|
|
227
|
+
USING ERRCODE = '42501';
|
|
228
|
+
END IF;
|
|
229
|
+
IF nullif(pg_catalog.current_setting('opengeni.subject_id', true), '') IS NOT NULL THEN
|
|
230
|
+
RAISE EXCEPTION 'session ownership classification must run outside an actor subject scope'
|
|
231
|
+
USING ERRCODE = '42501';
|
|
232
|
+
END IF;
|
|
233
|
+
IF p_run_key IS NOT NULL AND length(btrim(p_run_key)) = 0 THEN
|
|
234
|
+
RAISE EXCEPTION 'session ownership run key must not be blank'
|
|
235
|
+
USING ERRCODE = '22023';
|
|
236
|
+
END IF;
|
|
237
|
+
END;
|
|
238
|
+
$$;
|
|
239
|
+
REVOKE ALL ON FUNCTION
|
|
240
|
+
opengeni_private.assert_session_ownership_scope(uuid, text) FROM PUBLIC;
|
|
241
|
+
|
|
242
|
+
CREATE OR REPLACE FUNCTION opengeni_private.tenancy_backfill_ledger_available()
|
|
243
|
+
RETURNS boolean
|
|
244
|
+
LANGUAGE sql
|
|
245
|
+
STABLE
|
|
246
|
+
SET search_path FROM CURRENT
|
|
247
|
+
AS $$
|
|
248
|
+
SELECT to_regprocedure('open_tenancy_backfill_receipt(uuid, text, text)') IS NOT NULL
|
|
249
|
+
AND to_regprocedure('record_tenancy_backfill_unresolved(uuid, uuid, text)') IS NOT NULL
|
|
250
|
+
AND to_regprocedure(
|
|
251
|
+
'complete_tenancy_backfill_receipt(uuid, bigint, bigint, text)'
|
|
252
|
+
) IS NOT NULL
|
|
253
|
+
$$;
|
|
254
|
+
REVOKE ALL ON FUNCTION
|
|
255
|
+
opengeni_private.tenancy_backfill_ledger_available() FROM PUBLIC;
|
|
256
|
+
|
|
257
|
+
-- One verdict per session in one organization.
|
|
258
|
+
--
|
|
259
|
+
-- `p_run_key` is the durability switch. Omitted (NULL) the call is a pure
|
|
260
|
+
-- read. Supplied, the same verdicts are additionally recorded through the
|
|
261
|
+
-- tenancy backfill ledger as one `sessions` receipt plus one append-only
|
|
262
|
+
-- unresolved row per session that could not be attributed - which is also why
|
|
263
|
+
-- the run key must be fresh per run: the ledger refuses to re-open a settled
|
|
264
|
+
-- receipt.
|
|
265
|
+
--
|
|
266
|
+
-- This routine NEVER writes a session row. `rewroteSessionRows` is reported
|
|
267
|
+
-- literally false so a reader cannot mistake it for the backfill.
|
|
268
|
+
CREATE OR REPLACE FUNCTION classify_organization_session_ownership(
|
|
269
|
+
p_organization_id uuid,
|
|
270
|
+
p_run_key text DEFAULT NULL
|
|
271
|
+
) RETURNS jsonb
|
|
272
|
+
LANGUAGE plpgsql
|
|
273
|
+
SECURITY DEFINER
|
|
274
|
+
SET search_path FROM CURRENT
|
|
275
|
+
SET statement_timeout = '5min'
|
|
276
|
+
AS $$
|
|
277
|
+
DECLARE
|
|
278
|
+
claimed_rows integer := 0;
|
|
279
|
+
claimed boolean := false;
|
|
280
|
+
ledger_available boolean;
|
|
281
|
+
summary jsonb;
|
|
282
|
+
receipt_id uuid;
|
|
283
|
+
-- One verdict per session row. Re-executed for the ledger statement rather
|
|
284
|
+
-- than staged in a temp table: a temp relation outlives the call inside the
|
|
285
|
+
-- caller's transaction and would collide on a second call in that same
|
|
286
|
+
-- transaction. $1 is the organization id.
|
|
287
|
+
classification_sql constant text := $classification$
|
|
288
|
+
SELECT session_row.id AS session_id,
|
|
289
|
+
CASE
|
|
290
|
+
-- Already attributed. The two 0225/0218 FKs prove the membership id
|
|
291
|
+
-- and the (account, subject) pair each EXIST, but nothing proves they
|
|
292
|
+
-- are the SAME row, that it is live, or that the pair is internally
|
|
293
|
+
-- consistent. Those are the only unenforced parts of an attributed
|
|
294
|
+
-- session, so they are exactly what this branch proves.
|
|
295
|
+
WHEN session_row.owner_organization_membership_id IS NOT NULL
|
|
296
|
+
OR session_row.owner_subject_id IS NOT NULL THEN
|
|
297
|
+
CASE
|
|
298
|
+
WHEN session_row.owner_organization_membership_id IS NULL
|
|
299
|
+
OR session_row.owner_subject_id IS NULL
|
|
300
|
+
OR owner_membership.id IS NULL
|
|
301
|
+
OR owner_membership.subject_id IS DISTINCT FROM session_row.owner_subject_id
|
|
302
|
+
THEN 'conflicting_authority_rows'
|
|
303
|
+
WHEN owner_membership.status <> 'active'
|
|
304
|
+
OR owner_membership.revoked_at IS NOT NULL
|
|
305
|
+
THEN 'missing_organization_membership'
|
|
306
|
+
ELSE 'owner_attributed'
|
|
307
|
+
END
|
|
308
|
+
-- Ownerless from here down. Parent inheritance first, mirroring the
|
|
309
|
+
-- live trigger's own branch order.
|
|
310
|
+
WHEN owned_parent.id IS NOT NULL THEN 'repairable_parent_inheritance'
|
|
311
|
+
WHEN session_row.created_by_kind <> 'subject' THEN 'legacy_shape_unrecognized'
|
|
312
|
+
WHEN session_row.created_by_subject_id NOT LIKE 'user:%'
|
|
313
|
+
THEN 'external_lane_owns_row'
|
|
314
|
+
WHEN personal_anchor.id IS NOT NULL
|
|
315
|
+
AND personal_anchor.subject_id = session_row.created_by_subject_id
|
|
316
|
+
THEN 'repairable_personal_workspace'
|
|
317
|
+
-- The workspace anchor names a DIFFERENT subject than the creator.
|
|
318
|
+
-- Two durable facts disagree; refusing is the only correct answer.
|
|
319
|
+
WHEN personal_anchor.id IS NOT NULL THEN 'ambiguous_candidate_authority'
|
|
320
|
+
WHEN creator_membership.id IS NULL THEN 'missing_organization_membership'
|
|
321
|
+
-- An active managed human in an ordinary shared workspace. Replaying
|
|
322
|
+
-- the trigger's second branch here would evaluate TODAY's workspace
|
|
323
|
+
-- grants against a historical session; phase D forbids exactly that.
|
|
324
|
+
ELSE 'no_deterministic_evidence'
|
|
325
|
+
END AS verdict
|
|
326
|
+
FROM sessions session_row
|
|
327
|
+
LEFT JOIN organization_memberships owner_membership
|
|
328
|
+
ON owner_membership.id = session_row.owner_organization_membership_id
|
|
329
|
+
AND owner_membership.account_id = session_row.account_id
|
|
330
|
+
LEFT JOIN sessions owned_parent
|
|
331
|
+
ON owned_parent.id = session_row.parent_session_id
|
|
332
|
+
AND owned_parent.account_id = session_row.account_id
|
|
333
|
+
AND owned_parent.workspace_id = session_row.workspace_id
|
|
334
|
+
AND owned_parent.owner_organization_membership_id IS NOT NULL
|
|
335
|
+
AND owned_parent.owner_subject_id IS NOT NULL
|
|
336
|
+
LEFT JOIN organization_memberships personal_anchor
|
|
337
|
+
ON personal_anchor.account_id = session_row.account_id
|
|
338
|
+
AND personal_anchor.personal_workspace_id = session_row.workspace_id
|
|
339
|
+
AND personal_anchor.status = 'active'
|
|
340
|
+
AND personal_anchor.revoked_at IS NULL
|
|
341
|
+
LEFT JOIN organization_memberships creator_membership
|
|
342
|
+
ON creator_membership.account_id = session_row.account_id
|
|
343
|
+
AND creator_membership.subject_id = session_row.created_by_subject_id
|
|
344
|
+
AND creator_membership.status = 'active'
|
|
345
|
+
AND creator_membership.revoked_at IS NULL
|
|
346
|
+
WHERE session_row.account_id = $1
|
|
347
|
+
$classification$;
|
|
348
|
+
-- The three terminal/repairable verdicts. Everything else is an explicit
|
|
349
|
+
-- refusal carrying a fixed ledger reason code and never a proposed owner.
|
|
350
|
+
resolved_verdicts constant text :=
|
|
351
|
+
'(''owner_attributed'', ''repairable_personal_workspace'',
|
|
352
|
+
''repairable_parent_inheritance'')';
|
|
353
|
+
BEGIN
|
|
354
|
+
PERFORM opengeni_private.assert_session_ownership_scope(p_organization_id, p_run_key);
|
|
355
|
+
ledger_available := p_run_key IS NOT NULL
|
|
356
|
+
AND opengeni_private.tenancy_backfill_ledger_available();
|
|
357
|
+
|
|
358
|
+
INSERT INTO opengeni_private.session_ownership_capabilities (
|
|
359
|
+
backend_pid, transaction_id
|
|
360
|
+
) VALUES (pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id())
|
|
361
|
+
ON CONFLICT DO NOTHING;
|
|
362
|
+
-- Only release what this call claimed, so a nested or enclosing holder of
|
|
363
|
+
-- the same transaction-scoped capability is never revoked underneath.
|
|
364
|
+
GET DIAGNOSTICS claimed_rows = ROW_COUNT;
|
|
365
|
+
claimed := claimed_rows > 0;
|
|
366
|
+
|
|
367
|
+
EXECUTE format($summary$
|
|
368
|
+
WITH classified AS (%1$s)
|
|
369
|
+
SELECT pg_catalog.jsonb_build_object(
|
|
370
|
+
'total', count(*)::int,
|
|
371
|
+
'ownerAttributed', count(*) FILTER (WHERE verdict = 'owner_attributed')::int,
|
|
372
|
+
'repairablePersonalWorkspace', count(*) FILTER (
|
|
373
|
+
WHERE verdict = 'repairable_personal_workspace'
|
|
374
|
+
)::int,
|
|
375
|
+
'repairableParentInheritance', count(*) FILTER (
|
|
376
|
+
WHERE verdict = 'repairable_parent_inheritance'
|
|
377
|
+
)::int,
|
|
378
|
+
'unresolved', count(*) FILTER (WHERE verdict NOT IN %2$s)::int,
|
|
379
|
+
'unresolvedByReason', coalesce((
|
|
380
|
+
SELECT pg_catalog.jsonb_object_agg(reason, reason_count)
|
|
381
|
+
FROM (
|
|
382
|
+
SELECT verdict AS reason, count(*)::int AS reason_count
|
|
383
|
+
FROM classified
|
|
384
|
+
WHERE verdict NOT IN %2$s
|
|
385
|
+
GROUP BY verdict
|
|
386
|
+
) grouped
|
|
387
|
+
), '{}'::jsonb),
|
|
388
|
+
'unresolvedRows', coalesce((
|
|
389
|
+
SELECT pg_catalog.jsonb_agg(
|
|
390
|
+
pg_catalog.jsonb_build_object(
|
|
391
|
+
'sessionId', bounded.session_id, 'reasonCode', bounded.verdict
|
|
392
|
+
) ORDER BY bounded.session_id
|
|
393
|
+
)
|
|
394
|
+
FROM (
|
|
395
|
+
SELECT session_id, verdict FROM classified
|
|
396
|
+
WHERE verdict NOT IN %2$s
|
|
397
|
+
ORDER BY session_id LIMIT 1000
|
|
398
|
+
) bounded
|
|
399
|
+
), '[]'::jsonb),
|
|
400
|
+
'unresolvedTruncated', count(*) FILTER (WHERE verdict NOT IN %2$s) > 1000
|
|
401
|
+
)
|
|
402
|
+
FROM classified
|
|
403
|
+
$summary$, classification_sql, resolved_verdicts)
|
|
404
|
+
USING p_organization_id
|
|
405
|
+
INTO summary;
|
|
406
|
+
|
|
407
|
+
IF ledger_available THEN
|
|
408
|
+
EXECUTE 'SELECT open_tenancy_backfill_receipt($1, $2, $3)'
|
|
409
|
+
USING p_organization_id, 'sessions', p_run_key
|
|
410
|
+
INTO receipt_id;
|
|
411
|
+
-- One set-based statement, not one round trip per unresolved session. The
|
|
412
|
+
-- recorded set is identical; only the plpgsql loop is gone. `receipt_id` is
|
|
413
|
+
-- inlined as a literal because `classification_sql` already owns `$1`.
|
|
414
|
+
--
|
|
415
|
+
-- The ledger is recorded in full on purpose: this routine summarizes an
|
|
416
|
+
-- entire organization and the receipt is its evidence, so truncating it
|
|
417
|
+
-- would make the receipt a sample. The cost is therefore proportional to
|
|
418
|
+
-- the unresolved population, and the classification query is evaluated
|
|
419
|
+
-- once for the summary and once here - the deliberate trade against
|
|
420
|
+
-- materializing an unbounded row set in memory. `--classify` is opt-in and
|
|
421
|
+
-- writes no session row.
|
|
422
|
+
EXECUTE format(
|
|
423
|
+
'SELECT record_tenancy_backfill_unresolved(%3$L::uuid, unresolved.session_id, '
|
|
424
|
+
|| 'unresolved.verdict) FROM ('
|
|
425
|
+
|| 'SELECT session_id, verdict FROM (%1$s) classified '
|
|
426
|
+
|| 'WHERE verdict NOT IN %2$s ORDER BY session_id) unresolved',
|
|
427
|
+
classification_sql,
|
|
428
|
+
resolved_verdicts,
|
|
429
|
+
receipt_id
|
|
430
|
+
) USING p_organization_id;
|
|
431
|
+
-- `classified_count` is the population this run PROVED already carries a
|
|
432
|
+
-- live, internally consistent owner; `skipped_count` is the deterministically
|
|
433
|
+
-- repairable population this classification run deliberately did NOT touch.
|
|
434
|
+
-- Neither reflects a rewrite: this routine changes no session row.
|
|
435
|
+
EXECUTE 'SELECT complete_tenancy_backfill_receipt($1, $2, $3, $4)'
|
|
436
|
+
USING receipt_id,
|
|
437
|
+
(summary ->> 'ownerAttributed')::bigint,
|
|
438
|
+
(summary ->> 'repairablePersonalWorkspace')::bigint
|
|
439
|
+
+ (summary ->> 'repairableParentInheritance')::bigint,
|
|
440
|
+
'completed';
|
|
441
|
+
summary := summary || pg_catalog.jsonb_build_object('receiptId', receipt_id);
|
|
442
|
+
END IF;
|
|
443
|
+
|
|
444
|
+
IF claimed THEN
|
|
445
|
+
DELETE FROM opengeni_private.session_ownership_capabilities
|
|
446
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
447
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
|
|
448
|
+
END IF;
|
|
449
|
+
|
|
450
|
+
RETURN pg_catalog.jsonb_build_object(
|
|
451
|
+
'schemaVersion', 1,
|
|
452
|
+
'organizationId', p_organization_id,
|
|
453
|
+
'runKey', p_run_key,
|
|
454
|
+
'ledgerAvailable', ledger_available,
|
|
455
|
+
'rewroteSessionRows', false,
|
|
456
|
+
'sessions', summary
|
|
457
|
+
);
|
|
458
|
+
EXCEPTION WHEN OTHERS THEN
|
|
459
|
+
IF claimed THEN
|
|
460
|
+
DELETE FROM opengeni_private.session_ownership_capabilities
|
|
461
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
462
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
|
|
463
|
+
END IF;
|
|
464
|
+
RAISE;
|
|
465
|
+
END
|
|
466
|
+
$$;
|
|
467
|
+
REVOKE ALL ON FUNCTION
|
|
468
|
+
classify_organization_session_ownership(uuid, text) FROM PUBLIC;
|
|
469
|
+
|
|
470
|
+
-- One bounded, resumable, idempotent batch of the ONLY deterministic repair.
|
|
471
|
+
--
|
|
472
|
+
-- `p_dry_run` defaults TRUE: an operator who forgets the flag gets a report,
|
|
473
|
+
-- not a write. `p_limit` bounds the root candidates claimed with
|
|
474
|
+
-- `FOR UPDATE ... SKIP LOCKED`, so concurrent drivers never contend and a
|
|
475
|
+
-- killed driver leaves no half-claimed state. Re-running is a no-op once a row
|
|
476
|
+
-- is attributed, because an attributed row no longer matches either candidate
|
|
477
|
+
-- predicate.
|
|
478
|
+
CREATE OR REPLACE FUNCTION backfill_organization_session_ownership(
|
|
479
|
+
p_organization_id uuid,
|
|
480
|
+
p_limit integer DEFAULT 500,
|
|
481
|
+
p_dry_run boolean DEFAULT true,
|
|
482
|
+
p_run_key text DEFAULT NULL
|
|
483
|
+
) RETURNS jsonb
|
|
484
|
+
LANGUAGE plpgsql
|
|
485
|
+
SECURITY DEFINER
|
|
486
|
+
SET search_path FROM CURRENT
|
|
487
|
+
SET statement_timeout = '5min'
|
|
488
|
+
AS $$
|
|
489
|
+
DECLARE
|
|
490
|
+
claimed_rows integer := 0;
|
|
491
|
+
claimed boolean := false;
|
|
492
|
+
ledger_available boolean;
|
|
493
|
+
visibility_capability_id uuid := pg_catalog.gen_random_uuid();
|
|
494
|
+
previous_visibility_capability text;
|
|
495
|
+
personal_candidates integer := 0;
|
|
496
|
+
inherited_candidates integer := 0;
|
|
497
|
+
personal_attributed integer := 0;
|
|
498
|
+
inherited_attributed integer := 0;
|
|
499
|
+
inheritance_passes integer := 0;
|
|
500
|
+
closure_incomplete boolean := false;
|
|
501
|
+
pass_rows integer;
|
|
502
|
+
receipt_id uuid;
|
|
503
|
+
BEGIN
|
|
504
|
+
PERFORM opengeni_private.assert_session_ownership_scope(p_organization_id, p_run_key);
|
|
505
|
+
IF p_limit IS NULL OR p_limit < 1 OR p_limit > 5000 THEN
|
|
506
|
+
RAISE EXCEPTION 'session ownership backfill limit must be between 1 and 5000'
|
|
507
|
+
USING ERRCODE = '22023';
|
|
508
|
+
END IF;
|
|
509
|
+
IF p_dry_run IS NULL THEN
|
|
510
|
+
RAISE EXCEPTION 'session ownership backfill requires an explicit dry-run flag'
|
|
511
|
+
USING ERRCODE = '22004';
|
|
512
|
+
END IF;
|
|
513
|
+
ledger_available := p_run_key IS NOT NULL
|
|
514
|
+
AND opengeni_private.tenancy_backfill_ledger_available();
|
|
515
|
+
|
|
516
|
+
INSERT INTO opengeni_private.session_ownership_capabilities (
|
|
517
|
+
backend_pid, transaction_id
|
|
518
|
+
) VALUES (pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id())
|
|
519
|
+
ON CONFLICT DO NOTHING;
|
|
520
|
+
GET DIAGNOSTICS claimed_rows = ROW_COUNT;
|
|
521
|
+
claimed := claimed_rows > 0;
|
|
522
|
+
|
|
523
|
+
IF p_dry_run THEN
|
|
524
|
+
-- No locking clause on a dry run: a report must not block a live writer.
|
|
525
|
+
SELECT count(*)::integer INTO personal_candidates
|
|
526
|
+
FROM sessions session_row
|
|
527
|
+
JOIN organization_memberships anchor
|
|
528
|
+
ON anchor.account_id = session_row.account_id
|
|
529
|
+
AND anchor.personal_workspace_id = session_row.workspace_id
|
|
530
|
+
AND anchor.status = 'active'
|
|
531
|
+
AND anchor.revoked_at IS NULL
|
|
532
|
+
AND anchor.subject_id = session_row.created_by_subject_id
|
|
533
|
+
WHERE session_row.account_id = p_organization_id
|
|
534
|
+
AND session_row.owner_organization_membership_id IS NULL
|
|
535
|
+
AND session_row.owner_subject_id IS NULL
|
|
536
|
+
AND session_row.created_by_kind = 'subject'
|
|
537
|
+
-- Same fence the classifier applies before it ever reaches
|
|
538
|
+
-- 'repairable_personal_workspace': a non-`user:` creator is
|
|
539
|
+
-- 'external_lane_owns_row', which this migration calls permanently
|
|
540
|
+
-- unrepairable. Without this line the write path would be more
|
|
541
|
+
-- permissive than the classifier that authorizes it, and a dry run
|
|
542
|
+
-- would promise an attribution the classifier refuses.
|
|
543
|
+
AND session_row.created_by_subject_id LIKE 'user:%';
|
|
544
|
+
|
|
545
|
+
-- Only the inheritance backlog that exists RIGHT NOW. It deliberately
|
|
546
|
+
-- does not model the closure a real run's root writes would create, so a
|
|
547
|
+
-- dry run never over-promises.
|
|
548
|
+
SELECT count(*)::integer INTO inherited_candidates
|
|
549
|
+
FROM sessions session_row
|
|
550
|
+
JOIN sessions parent_row
|
|
551
|
+
ON parent_row.id = session_row.parent_session_id
|
|
552
|
+
AND parent_row.account_id = session_row.account_id
|
|
553
|
+
AND parent_row.workspace_id = session_row.workspace_id
|
|
554
|
+
AND parent_row.owner_organization_membership_id IS NOT NULL
|
|
555
|
+
AND parent_row.owner_subject_id IS NOT NULL
|
|
556
|
+
WHERE session_row.account_id = p_organization_id
|
|
557
|
+
AND session_row.owner_organization_membership_id IS NULL
|
|
558
|
+
AND session_row.owner_subject_id IS NULL;
|
|
559
|
+
ELSE
|
|
560
|
+
-- 0225 block C fences every owner-pair UPDATE behind this capability.
|
|
561
|
+
previous_visibility_capability := pg_catalog.current_setting(
|
|
562
|
+
'opengeni.session_visibility_write_capability', true
|
|
563
|
+
);
|
|
564
|
+
INSERT INTO session_visibility_write_capabilities (
|
|
565
|
+
backend_pid, transaction_id, capability_id
|
|
566
|
+
) VALUES (
|
|
567
|
+
pg_catalog.pg_backend_pid(),
|
|
568
|
+
pg_catalog.pg_current_xact_id(),
|
|
569
|
+
visibility_capability_id
|
|
570
|
+
);
|
|
571
|
+
PERFORM pg_catalog.set_config(
|
|
572
|
+
'opengeni.session_visibility_write_capability',
|
|
573
|
+
visibility_capability_id::text,
|
|
574
|
+
true
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
WITH candidates AS (
|
|
578
|
+
SELECT session_row.id AS session_id,
|
|
579
|
+
anchor.id AS owner_membership_id,
|
|
580
|
+
anchor.subject_id AS owner_subject_id
|
|
581
|
+
FROM sessions session_row
|
|
582
|
+
JOIN organization_memberships anchor
|
|
583
|
+
ON anchor.account_id = session_row.account_id
|
|
584
|
+
AND anchor.personal_workspace_id = session_row.workspace_id
|
|
585
|
+
AND anchor.status = 'active'
|
|
586
|
+
AND anchor.revoked_at IS NULL
|
|
587
|
+
AND anchor.subject_id = session_row.created_by_subject_id
|
|
588
|
+
WHERE session_row.account_id = p_organization_id
|
|
589
|
+
AND session_row.owner_organization_membership_id IS NULL
|
|
590
|
+
AND session_row.owner_subject_id IS NULL
|
|
591
|
+
AND session_row.created_by_kind = 'subject'
|
|
592
|
+
-- See the dry-run branch: `external_lane_owns_row` is unrepairable by
|
|
593
|
+
-- construction here, not merely by the absence of a matching anchor.
|
|
594
|
+
AND session_row.created_by_subject_id LIKE 'user:%'
|
|
595
|
+
ORDER BY session_row.id
|
|
596
|
+
LIMIT p_limit
|
|
597
|
+
FOR UPDATE OF session_row SKIP LOCKED
|
|
598
|
+
)
|
|
599
|
+
UPDATE sessions target
|
|
600
|
+
SET owner_organization_membership_id = candidates.owner_membership_id,
|
|
601
|
+
owner_subject_id = candidates.owner_subject_id
|
|
602
|
+
FROM candidates
|
|
603
|
+
WHERE target.id = candidates.session_id
|
|
604
|
+
AND target.account_id = p_organization_id;
|
|
605
|
+
GET DIAGNOSTICS personal_attributed = ROW_COUNT;
|
|
606
|
+
personal_candidates := personal_attributed;
|
|
607
|
+
inherited_candidates := 0;
|
|
608
|
+
|
|
609
|
+
-- Parent-inheritance closure: branch 1 of the live trigger replayed
|
|
610
|
+
-- against durable parent data. Bounded by tree depth and terminated by a
|
|
611
|
+
-- pass that changes nothing, so it is finite even on a malformed graph.
|
|
612
|
+
LOOP
|
|
613
|
+
inheritance_passes := inheritance_passes + 1;
|
|
614
|
+
IF inheritance_passes > 64 THEN
|
|
615
|
+
-- A tree deeper than 64 (or wider than p_limit at some level) is left
|
|
616
|
+
-- for the next batch rather than run unbounded; `moreLikely` says so.
|
|
617
|
+
closure_incomplete := true;
|
|
618
|
+
EXIT;
|
|
619
|
+
END IF;
|
|
620
|
+
WITH candidates AS (
|
|
621
|
+
SELECT session_row.id AS session_id,
|
|
622
|
+
parent_row.owner_organization_membership_id AS owner_membership_id,
|
|
623
|
+
parent_row.owner_subject_id AS owner_subject_id
|
|
624
|
+
FROM sessions session_row
|
|
625
|
+
JOIN sessions parent_row
|
|
626
|
+
ON parent_row.id = session_row.parent_session_id
|
|
627
|
+
AND parent_row.account_id = session_row.account_id
|
|
628
|
+
AND parent_row.workspace_id = session_row.workspace_id
|
|
629
|
+
AND parent_row.owner_organization_membership_id IS NOT NULL
|
|
630
|
+
AND parent_row.owner_subject_id IS NOT NULL
|
|
631
|
+
WHERE session_row.account_id = p_organization_id
|
|
632
|
+
AND session_row.owner_organization_membership_id IS NULL
|
|
633
|
+
AND session_row.owner_subject_id IS NULL
|
|
634
|
+
ORDER BY session_row.id
|
|
635
|
+
LIMIT p_limit
|
|
636
|
+
FOR UPDATE OF session_row SKIP LOCKED
|
|
637
|
+
)
|
|
638
|
+
UPDATE sessions target
|
|
639
|
+
SET owner_organization_membership_id = candidates.owner_membership_id,
|
|
640
|
+
owner_subject_id = candidates.owner_subject_id
|
|
641
|
+
FROM candidates
|
|
642
|
+
WHERE target.id = candidates.session_id
|
|
643
|
+
AND target.account_id = p_organization_id;
|
|
644
|
+
GET DIAGNOSTICS pass_rows = ROW_COUNT;
|
|
645
|
+
inherited_attributed := inherited_attributed + pass_rows;
|
|
646
|
+
IF pass_rows >= p_limit THEN
|
|
647
|
+
closure_incomplete := true;
|
|
648
|
+
END IF;
|
|
649
|
+
EXIT WHEN pass_rows = 0;
|
|
650
|
+
END LOOP;
|
|
651
|
+
|
|
652
|
+
DELETE FROM session_visibility_write_capabilities
|
|
653
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
654
|
+
AND transaction_id = pg_catalog.pg_current_xact_id()
|
|
655
|
+
AND capability_id = visibility_capability_id;
|
|
656
|
+
PERFORM pg_catalog.set_config(
|
|
657
|
+
'opengeni.session_visibility_write_capability',
|
|
658
|
+
coalesce(previous_visibility_capability, ''),
|
|
659
|
+
true
|
|
660
|
+
);
|
|
661
|
+
END IF;
|
|
662
|
+
|
|
663
|
+
IF ledger_available THEN
|
|
664
|
+
EXECUTE 'SELECT open_tenancy_backfill_receipt($1, $2, $3)'
|
|
665
|
+
USING p_organization_id, 'sessions', p_run_key
|
|
666
|
+
INTO receipt_id;
|
|
667
|
+
-- `classified_count` is what this batch actually attributed (0 on a dry
|
|
668
|
+
-- run); `skipped_count` is what it left for the next batch or for another
|
|
669
|
+
-- concurrent driver. The unresolved obligations belong to the classifier,
|
|
670
|
+
-- which is the routine that can name a reason for every row.
|
|
671
|
+
EXECUTE 'SELECT complete_tenancy_backfill_receipt($1, $2, $3, $4)'
|
|
672
|
+
USING receipt_id,
|
|
673
|
+
(personal_attributed + inherited_attributed)::bigint,
|
|
674
|
+
CASE WHEN p_dry_run
|
|
675
|
+
THEN (personal_candidates + inherited_candidates)::bigint
|
|
676
|
+
ELSE 0::bigint
|
|
677
|
+
END,
|
|
678
|
+
'completed';
|
|
679
|
+
END IF;
|
|
680
|
+
|
|
681
|
+
IF claimed THEN
|
|
682
|
+
DELETE FROM opengeni_private.session_ownership_capabilities
|
|
683
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
684
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
|
|
685
|
+
END IF;
|
|
686
|
+
|
|
687
|
+
RETURN pg_catalog.jsonb_build_object(
|
|
688
|
+
'schemaVersion', 1,
|
|
689
|
+
'organizationId', p_organization_id,
|
|
690
|
+
'runKey', p_run_key,
|
|
691
|
+
'ledgerAvailable', ledger_available,
|
|
692
|
+
'dryRun', p_dry_run,
|
|
693
|
+
'limit', p_limit,
|
|
694
|
+
'receiptId', receipt_id,
|
|
695
|
+
'personalWorkspaceCandidates', personal_candidates,
|
|
696
|
+
'parentInheritanceCandidates', inherited_candidates,
|
|
697
|
+
'personalWorkspaceAttributed', personal_attributed,
|
|
698
|
+
'parentInheritanceAttributed', inherited_attributed,
|
|
699
|
+
'attributed', personal_attributed + inherited_attributed,
|
|
700
|
+
-- The driver keeps calling while this is true. It is deliberately derived
|
|
701
|
+
-- from THIS batch's own result rather than from a fresh count, so a row
|
|
702
|
+
-- another driver holds cannot spin this loop forever.
|
|
703
|
+
'moreLikely',
|
|
704
|
+
NOT p_dry_run AND (personal_attributed >= p_limit OR closure_incomplete)
|
|
705
|
+
);
|
|
706
|
+
EXCEPTION WHEN OTHERS THEN
|
|
707
|
+
IF NOT p_dry_run THEN
|
|
708
|
+
DELETE FROM session_visibility_write_capabilities
|
|
709
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
710
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
711
|
+
AND capability_id = visibility_capability_id;
|
|
712
|
+
PERFORM pg_catalog.set_config(
|
|
713
|
+
'opengeni.session_visibility_write_capability',
|
|
714
|
+
coalesce(previous_visibility_capability, ''),
|
|
715
|
+
true
|
|
716
|
+
);
|
|
717
|
+
END IF;
|
|
718
|
+
IF claimed THEN
|
|
719
|
+
DELETE FROM opengeni_private.session_ownership_capabilities
|
|
720
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
721
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
|
|
722
|
+
END IF;
|
|
723
|
+
RAISE;
|
|
724
|
+
END
|
|
725
|
+
$$;
|
|
726
|
+
|
|
727
|
+
REVOKE ALL ON FUNCTION
|
|
728
|
+
backfill_organization_session_ownership(uuid, integer, boolean, text) FROM PUBLIC;
|
|
729
|
+
|
|
730
|
+
DO $session_ownership_grants$
|
|
731
|
+
BEGIN
|
|
732
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
733
|
+
GRANT EXECUTE ON FUNCTION
|
|
734
|
+
classify_organization_session_ownership(uuid, text) TO opengeni_app;
|
|
735
|
+
GRANT EXECUTE ON FUNCTION
|
|
736
|
+
backfill_organization_session_ownership(uuid, integer, boolean, text) TO opengeni_app;
|
|
737
|
+
-- The predicate must be independently EXECUTE-granted (0254's, 0285's, and
|
|
738
|
+
-- the sibling classification seam's exact lesson): an ordinary opengeni_app
|
|
739
|
+
-- read of ANY table this capability protects evaluates every PERMISSIVE
|
|
740
|
+
-- policy on it, so without this grant an unrelated read of `sessions` or
|
|
741
|
+
-- `organization_memberships` fails closed with a permission error instead
|
|
742
|
+
-- of the policy branch simply evaluating false.
|
|
743
|
+
GRANT EXECUTE ON FUNCTION
|
|
744
|
+
opengeni_private.session_ownership_capability_active() TO opengeni_app;
|
|
745
|
+
END IF;
|
|
746
|
+
END
|
|
747
|
+
$session_ownership_grants$;
|
|
748
|
+
|
|
749
|
+
COMMENT ON FUNCTION classify_organization_session_ownership(uuid, text) IS
|
|
750
|
+
'Organization-tenancy phase D session ownership classification. Read-only over sessions: it proves every attributed session points at one live, internally consistent organization membership and names a fixed reason for every session it refuses to attribute, recording those refusals through the tenancy backfill ledger. It never writes a session row and never infers user authority from created_by, a default workspace, or current workspace access.';
|
|
751
|
+
COMMENT ON FUNCTION backfill_organization_session_ownership(uuid, integer, boolean, text) IS
|
|
752
|
+
'Organization-tenancy phase D session ownership backfill. Attributes ONLY the two deterministic populations: a session in exactly one active membership''s personal workspace whose creator subject is that same membership, and the parent-inheritance closure that migration 0225''s own trigger would have produced. Bounded, resumable (FOR UPDATE SKIP LOCKED), idempotent, dry-run by default; it never changes visibility, authority epoch, or any read.';
|