@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,268 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Migration 0290: read-only organization-membership backfill enumeration seam.
|
|
3
|
+
--
|
|
4
|
+
-- Phase D of docs/organization-tenancy.md requires provisioning personal
|
|
5
|
+
-- workspaces for active memberships "through an idempotent lifecycle
|
|
6
|
+
-- operation". That lifecycle operation already exists - 0219's
|
|
7
|
+
-- ensure_managed_human_personal_workspace(uuid, text, uuid) - and 0263's
|
|
8
|
+
-- membership lifecycle owns invitation-driven provisioning. What is missing is
|
|
9
|
+
-- a way for an operator to FIND the humans that need it: the population the
|
|
10
|
+
-- 0285 inventory counts as workspaceMemberSubjectsWithoutMembershipAnchor
|
|
11
|
+
-- (humans who held workspace access before 0219 and never re-authenticated
|
|
12
|
+
-- afterwards, so nothing ever ran the provisioning hook for them) plus any
|
|
13
|
+
-- membership that carries no personal workspace.
|
|
14
|
+
--
|
|
15
|
+
-- This migration therefore adds NO tables, NO write path, and NO new authority.
|
|
16
|
+
-- It adds exactly two read-only SECURITY DEFINER enumerations over
|
|
17
|
+
-- organization_memberships, which is FORCE ROW LEVEL SECURITY with zero direct
|
|
18
|
+
-- application-role privileges. Every other table the backfill driver needs
|
|
19
|
+
-- (workspaces, workspace_memberships, managed_accounts, auth_users) is read by
|
|
20
|
+
-- the ordinary application role under its normal account RLS, exactly like the
|
|
21
|
+
-- existing managed-human provisioning hook - so this seam deliberately does not
|
|
22
|
+
-- need 0285's capability-row pattern, and cannot silently mis-read under a
|
|
23
|
+
-- non-BYPASSRLS function owner.
|
|
24
|
+
--
|
|
25
|
+
-- Visibility is granted through the established transaction-local lifecycle
|
|
26
|
+
-- marker rather than a widened policy, and the new marker is added to the
|
|
27
|
+
-- organization_memberships policy's USING clause ONLY. It is structurally
|
|
28
|
+
-- incapable of authorizing a write: WITH CHECK keeps exactly the three
|
|
29
|
+
-- pre-existing write markers.
|
|
30
|
+
|
|
31
|
+
SET LOCAL lock_timeout = '5s';
|
|
32
|
+
SET LOCAL statement_timeout = '5min';
|
|
33
|
+
|
|
34
|
+
-- Read-only marker. USING gains 'organization_membership_backfill'; WITH CHECK
|
|
35
|
+
-- deliberately does not, so the backfill enumeration can never write here.
|
|
36
|
+
DROP POLICY IF EXISTS organization_tenancy_lifecycle ON "organization_memberships";
|
|
37
|
+
CREATE POLICY organization_tenancy_lifecycle ON "organization_memberships"
|
|
38
|
+
USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
39
|
+
IN (
|
|
40
|
+
'managed_human_provisioning',
|
|
41
|
+
'session_visibility_activation',
|
|
42
|
+
'organization_membership_lifecycle',
|
|
43
|
+
'organization_membership_backfill'
|
|
44
|
+
))
|
|
45
|
+
WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
|
|
46
|
+
IN (
|
|
47
|
+
'managed_human_provisioning',
|
|
48
|
+
'session_visibility_activation',
|
|
49
|
+
'organization_membership_lifecycle'
|
|
50
|
+
));
|
|
51
|
+
|
|
52
|
+
-- Anchor state for an explicit, bounded set of subjects. The driver already
|
|
53
|
+
-- knows which subjects hold workspace access (an ordinary application-role
|
|
54
|
+
-- read); this answers only "does an organization membership anchor exist, and
|
|
55
|
+
-- what does it point at". It never enumerates subjects on its own, so it
|
|
56
|
+
-- cannot become a cross-organization membership discovery path.
|
|
57
|
+
CREATE OR REPLACE FUNCTION list_organization_membership_backfill_anchors(
|
|
58
|
+
p_organization_id uuid,
|
|
59
|
+
p_subject_ids text[]
|
|
60
|
+
) RETURNS jsonb
|
|
61
|
+
LANGUAGE plpgsql
|
|
62
|
+
SECURITY DEFINER
|
|
63
|
+
SET search_path FROM CURRENT
|
|
64
|
+
SET statement_timeout = '30s'
|
|
65
|
+
AS $body$
|
|
66
|
+
DECLARE
|
|
67
|
+
result jsonb;
|
|
68
|
+
subject_id_value text;
|
|
69
|
+
previous_marker text := pg_catalog.current_setting(
|
|
70
|
+
'opengeni.organization_tenancy_lifecycle', true
|
|
71
|
+
);
|
|
72
|
+
BEGIN
|
|
73
|
+
IF p_organization_id IS NULL THEN
|
|
74
|
+
RAISE EXCEPTION 'membership backfill requires an organization id'
|
|
75
|
+
USING ERRCODE = '22004';
|
|
76
|
+
END IF;
|
|
77
|
+
IF p_organization_id IS DISTINCT FROM nullif(
|
|
78
|
+
pg_catalog.current_setting('opengeni.account_id', true), ''
|
|
79
|
+
)::uuid
|
|
80
|
+
THEN
|
|
81
|
+
RAISE EXCEPTION 'membership backfill scope mismatch' USING ERRCODE = '42501';
|
|
82
|
+
END IF;
|
|
83
|
+
IF p_subject_ids IS NULL
|
|
84
|
+
OR pg_catalog.array_length(p_subject_ids, 1) IS NULL
|
|
85
|
+
OR pg_catalog.array_length(p_subject_ids, 1) > 500
|
|
86
|
+
THEN
|
|
87
|
+
RAISE EXCEPTION 'membership backfill requires 1..500 subjects'
|
|
88
|
+
USING ERRCODE = '22023';
|
|
89
|
+
END IF;
|
|
90
|
+
FOREACH subject_id_value IN ARRAY p_subject_ids LOOP
|
|
91
|
+
IF subject_id_value IS NULL
|
|
92
|
+
OR subject_id_value <> pg_catalog.btrim(subject_id_value)
|
|
93
|
+
OR pg_catalog.length(subject_id_value) NOT BETWEEN 1 AND 1024
|
|
94
|
+
OR subject_id_value NOT LIKE 'user:%'
|
|
95
|
+
THEN
|
|
96
|
+
RAISE EXCEPTION 'membership backfill subject is invalid'
|
|
97
|
+
USING ERRCODE = '22023';
|
|
98
|
+
END IF;
|
|
99
|
+
END LOOP;
|
|
100
|
+
|
|
101
|
+
PERFORM pg_catalog.set_config(
|
|
102
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
103
|
+
'organization_membership_backfill',
|
|
104
|
+
true
|
|
105
|
+
);
|
|
106
|
+
SELECT COALESCE(pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object(
|
|
107
|
+
'subjectId', anchor.subject_id,
|
|
108
|
+
'membershipId', anchor.id,
|
|
109
|
+
'status', anchor.status,
|
|
110
|
+
'personalWorkspaceId', anchor.personal_workspace_id
|
|
111
|
+
) ORDER BY anchor.subject_id), '[]'::jsonb)
|
|
112
|
+
INTO result
|
|
113
|
+
FROM organization_memberships anchor
|
|
114
|
+
WHERE anchor.account_id = p_organization_id
|
|
115
|
+
AND anchor.subject_id = ANY (p_subject_ids);
|
|
116
|
+
|
|
117
|
+
PERFORM pg_catalog.set_config(
|
|
118
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
119
|
+
COALESCE(previous_marker, ''),
|
|
120
|
+
true
|
|
121
|
+
);
|
|
122
|
+
RETURN result;
|
|
123
|
+
EXCEPTION WHEN OTHERS THEN
|
|
124
|
+
PERFORM pg_catalog.set_config(
|
|
125
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
126
|
+
COALESCE(previous_marker, ''),
|
|
127
|
+
true
|
|
128
|
+
);
|
|
129
|
+
RAISE;
|
|
130
|
+
END
|
|
131
|
+
$body$;
|
|
132
|
+
|
|
133
|
+
-- Memberships that exist but carry no personal workspace. 0285 counts these as
|
|
134
|
+
-- organizationMemberships.activeWithoutPersonalWorkspace; this returns the
|
|
135
|
+
-- exact bounded, deterministically ordered subjects behind that count so the
|
|
136
|
+
-- driver can converge them through the same lifecycle seam.
|
|
137
|
+
--
|
|
138
|
+
-- p_after_subject_id is the resumable keyset cursor. Without it a bounded pass
|
|
139
|
+
-- would return the same first p_limit subjects forever, so an organization
|
|
140
|
+
-- whose first p_limit rows cannot be resolved from deterministic evidence
|
|
141
|
+
-- (they stay in this population permanently) would starve every subject behind
|
|
142
|
+
-- them. The cursor is strictly exclusive and the ordering is the same
|
|
143
|
+
-- subject_id ordering the driver merges on, so repeated passes walk the whole
|
|
144
|
+
-- population exactly once instead of re-reading one window.
|
|
145
|
+
CREATE OR REPLACE FUNCTION list_organization_memberships_without_personal_workspace(
|
|
146
|
+
p_organization_id uuid,
|
|
147
|
+
p_limit integer DEFAULT 25,
|
|
148
|
+
p_after_subject_id text DEFAULT NULL
|
|
149
|
+
) RETURNS jsonb
|
|
150
|
+
LANGUAGE plpgsql
|
|
151
|
+
SECURITY DEFINER
|
|
152
|
+
SET search_path FROM CURRENT
|
|
153
|
+
SET statement_timeout = '30s'
|
|
154
|
+
AS $body$
|
|
155
|
+
DECLARE
|
|
156
|
+
result jsonb;
|
|
157
|
+
previous_marker text := pg_catalog.current_setting(
|
|
158
|
+
'opengeni.organization_tenancy_lifecycle', true
|
|
159
|
+
);
|
|
160
|
+
BEGIN
|
|
161
|
+
IF p_organization_id IS NULL THEN
|
|
162
|
+
RAISE EXCEPTION 'membership backfill requires an organization id'
|
|
163
|
+
USING ERRCODE = '22004';
|
|
164
|
+
END IF;
|
|
165
|
+
IF p_organization_id IS DISTINCT FROM nullif(
|
|
166
|
+
pg_catalog.current_setting('opengeni.account_id', true), ''
|
|
167
|
+
)::uuid
|
|
168
|
+
THEN
|
|
169
|
+
RAISE EXCEPTION 'membership backfill scope mismatch' USING ERRCODE = '42501';
|
|
170
|
+
END IF;
|
|
171
|
+
IF p_limit IS NULL OR p_limit NOT BETWEEN 1 AND 100 THEN
|
|
172
|
+
RAISE EXCEPTION 'membership backfill limit must be 1..100'
|
|
173
|
+
USING ERRCODE = '22023';
|
|
174
|
+
END IF;
|
|
175
|
+
IF p_after_subject_id IS NOT NULL
|
|
176
|
+
AND (
|
|
177
|
+
p_after_subject_id <> pg_catalog.btrim(p_after_subject_id)
|
|
178
|
+
OR pg_catalog.length(p_after_subject_id) NOT BETWEEN 1 AND 1024
|
|
179
|
+
OR p_after_subject_id NOT LIKE 'user:%'
|
|
180
|
+
)
|
|
181
|
+
THEN
|
|
182
|
+
RAISE EXCEPTION 'membership backfill cursor is invalid'
|
|
183
|
+
USING ERRCODE = '22023';
|
|
184
|
+
END IF;
|
|
185
|
+
|
|
186
|
+
PERFORM pg_catalog.set_config(
|
|
187
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
188
|
+
'organization_membership_backfill',
|
|
189
|
+
true
|
|
190
|
+
);
|
|
191
|
+
SELECT COALESCE(pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object(
|
|
192
|
+
'subjectId', pending.subject_id,
|
|
193
|
+
'membershipId', pending.id,
|
|
194
|
+
'status', pending.status,
|
|
195
|
+
'personalWorkspaceId', NULL
|
|
196
|
+
) ORDER BY pending.subject_id COLLATE "C"), '[]'::jsonb)
|
|
197
|
+
INTO result
|
|
198
|
+
FROM (
|
|
199
|
+
-- `COLLATE "C"` is load-bearing, not cosmetic. The driver merges this page
|
|
200
|
+
-- with its own workspace-access page in JavaScript and re-sorts the union
|
|
201
|
+
-- with the default comparator, which is code-unit (byte) order. The
|
|
202
|
+
-- database's default collation is locale-aware (`en_US.utf8` in practice),
|
|
203
|
+
-- where `_` is ignorable and case is secondary, so `user:Alpha_two` sorts
|
|
204
|
+
-- BEFORE `user:a_b` in JavaScript and AFTER it in the database. A keyset
|
|
205
|
+
-- cursor taken from the JavaScript order would then skip every subject the
|
|
206
|
+
-- database considers already behind it - a silent, permanent drop that
|
|
207
|
+
-- still reports `drained: true`. Pinning both sides to `C` makes the two
|
|
208
|
+
-- orders identical.
|
|
209
|
+
SELECT candidate.id, candidate.subject_id, candidate.status
|
|
210
|
+
FROM organization_memberships candidate
|
|
211
|
+
WHERE candidate.account_id = p_organization_id
|
|
212
|
+
AND candidate.personal_workspace_id IS NULL
|
|
213
|
+
AND candidate.status IN ('active', 'provisioning')
|
|
214
|
+
AND (
|
|
215
|
+
p_after_subject_id IS NULL
|
|
216
|
+
OR candidate.subject_id COLLATE "C" > p_after_subject_id COLLATE "C"
|
|
217
|
+
)
|
|
218
|
+
ORDER BY candidate.subject_id COLLATE "C"
|
|
219
|
+
LIMIT p_limit
|
|
220
|
+
) pending;
|
|
221
|
+
|
|
222
|
+
PERFORM pg_catalog.set_config(
|
|
223
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
224
|
+
COALESCE(previous_marker, ''),
|
|
225
|
+
true
|
|
226
|
+
);
|
|
227
|
+
RETURN result;
|
|
228
|
+
EXCEPTION WHEN OTHERS THEN
|
|
229
|
+
PERFORM pg_catalog.set_config(
|
|
230
|
+
'opengeni.organization_tenancy_lifecycle',
|
|
231
|
+
COALESCE(previous_marker, ''),
|
|
232
|
+
true
|
|
233
|
+
);
|
|
234
|
+
RAISE;
|
|
235
|
+
END
|
|
236
|
+
$body$;
|
|
237
|
+
|
|
238
|
+
DO $membership_backfill_search_path$
|
|
239
|
+
DECLARE
|
|
240
|
+
data_schema text := current_schema();
|
|
241
|
+
BEGIN
|
|
242
|
+
EXECUTE format(
|
|
243
|
+
'ALTER FUNCTION %I.list_organization_membership_backfill_anchors(uuid,text[]) '
|
|
244
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
245
|
+
data_schema, data_schema
|
|
246
|
+
);
|
|
247
|
+
EXECUTE format(
|
|
248
|
+
'ALTER FUNCTION %I.list_organization_memberships_without_personal_workspace(uuid,integer,text) '
|
|
249
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
250
|
+
data_schema, data_schema
|
|
251
|
+
);
|
|
252
|
+
END
|
|
253
|
+
$membership_backfill_search_path$;
|
|
254
|
+
|
|
255
|
+
REVOKE ALL ON FUNCTION
|
|
256
|
+
list_organization_membership_backfill_anchors(uuid, text[]) FROM PUBLIC;
|
|
257
|
+
REVOKE ALL ON FUNCTION
|
|
258
|
+
list_organization_memberships_without_personal_workspace(uuid, integer, text) FROM PUBLIC;
|
|
259
|
+
DO $membership_backfill_grant$
|
|
260
|
+
BEGIN
|
|
261
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
262
|
+
GRANT EXECUTE ON FUNCTION
|
|
263
|
+
list_organization_membership_backfill_anchors(uuid, text[]) TO opengeni_app;
|
|
264
|
+
GRANT EXECUTE ON FUNCTION
|
|
265
|
+
list_organization_memberships_without_personal_workspace(uuid, integer, text) TO opengeni_app;
|
|
266
|
+
END IF;
|
|
267
|
+
END
|
|
268
|
+
$membership_backfill_grant$;
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Migration 0291: explicit resource-classification assertion and receipt seam
|
|
3
|
+
-- for Variable Sets, Rigs, and Connected Machines (organization-tenancy phase D, slices 4-5).
|
|
4
|
+
--
|
|
5
|
+
-- WHAT PHASE D ASKS FOR, AND WHAT THE SCHEMA ALREADY GUARANTEES
|
|
6
|
+
-- ------------------------------------------------------------
|
|
7
|
+
-- `docs/organization-tenancy.md` phase D requires that every existing resource
|
|
8
|
+
-- be classified explicitly as workspace-owned unless there is reviewed,
|
|
9
|
+
-- deterministic evidence for user ownership, and that the run "record backfill
|
|
10
|
+
-- receipts and unresolved rows without widening access".
|
|
11
|
+
--
|
|
12
|
+
-- For these three families the classification is ALREADY terminal for every
|
|
13
|
+
-- row, and no data rewrite is possible without inventing a signal phase D
|
|
14
|
+
-- forbids:
|
|
15
|
+
--
|
|
16
|
+
-- * `authority_scope` is `text NOT NULL DEFAULT 'workspace'` on all three
|
|
17
|
+
-- tables (0230 for variable sets and rigs, 0262 for enrollments), so every
|
|
18
|
+
-- pre-existing row carries an explicit workspace classification already.
|
|
19
|
+
-- * Each `*_authority_shape_check` REQUIRES `authority_id IS NULL` and
|
|
20
|
+
-- `owner_organization_membership_id IS NULL` for organization/workspace
|
|
21
|
+
-- scope, and all three were `VALIDATE`d at creation. PostgreSQL has
|
|
22
|
+
-- therefore already proven the shape of every row.
|
|
23
|
+
-- * A legacy unmigrated row and a deliberately workspace-scoped row are
|
|
24
|
+
-- consequently BYTE-IDENTICAL. There is no discriminator, and 0262's own
|
|
25
|
+
-- header records the reviewed decision: "Existing rows remain
|
|
26
|
+
-- workspace-owned."
|
|
27
|
+
-- * `origin_workspace_id` does not rescue that: it is provenance, not
|
|
28
|
+
-- classification. No shape check constrains it for these three families
|
|
29
|
+
-- (unlike `connections`, whose 0256 workspace branch requires
|
|
30
|
+
-- `origin_workspace_id = workspace_id`), every read of it is gated behind
|
|
31
|
+
-- `authority_scope = 'user'` (0241/0252/0253), and the user lane is always
|
|
32
|
+
-- populated by the lifecycle functions. Its polarity is also inconsistent
|
|
33
|
+
-- across the three families, so it carries no usable signal either way.
|
|
34
|
+
--
|
|
35
|
+
-- Migration 0256 is the one sibling family with a genuine discriminator -
|
|
36
|
+
-- `connections.subject_id` plus an active `organization_memberships` row - so
|
|
37
|
+
-- it could deterministically mint user authority. None of these three tables
|
|
38
|
+
-- has a `subject_id`, and phase D explicitly forbids substituting `created_by`,
|
|
39
|
+
-- connection attribution, a default workspace, a resource name, or current
|
|
40
|
+
-- access. This migration therefore rewrites NOTHING and asserts instead.
|
|
41
|
+
--
|
|
42
|
+
-- WHY THIS IS A SECURITY DEFINER SEAM AND NOT A MIGRATION-TIME `UPDATE`
|
|
43
|
+
-- --------------------------------------------------------------------
|
|
44
|
+
-- All three tables are FORCE ROW LEVEL SECURITY and their ordinary policy is
|
|
45
|
+
-- `workspace_rls_visible(account_id, workspace_id)`, which is false whenever
|
|
46
|
+
-- the `opengeni.workspace_id` GUC is unset - as it is during migration. FORCE
|
|
47
|
+
-- RLS applies to the table owner, and OpenGeni's documented deployment posture
|
|
48
|
+
-- (docs/deployment.md) is a NON-superuser migration principal without
|
|
49
|
+
-- BYPASSRLS. A bare `UPDATE ... WHERE ...` in a migration body therefore
|
|
50
|
+
-- matches zero rows and reports success on such a deployment; it only appears
|
|
51
|
+
-- to work in the test harness, which migrates as a superuser for whom FORCE
|
|
52
|
+
-- RLS never engages. Any classification work for these families has to run
|
|
53
|
+
-- behind a capability-claiming definer seam, which is exactly why 0254, 0262,
|
|
54
|
+
-- and 0285 are all shaped this way.
|
|
55
|
+
--
|
|
56
|
+
-- WHAT THIS SEAM ADDS OVER 0285's INVENTORY
|
|
57
|
+
-- -----------------------------------------
|
|
58
|
+
-- 0285 counts rows by `authority_scope`. It cannot say whether a row that
|
|
59
|
+
-- CLAIMS user ownership actually holds it, because no constraint ties a
|
|
60
|
+
-- resource's `authority_id` to an authority row of the matching
|
|
61
|
+
-- `resource_kind`/`resource_id`, nor to a live authority or a live
|
|
62
|
+
-- organization membership. Those are the only genuinely unenforced parts of
|
|
63
|
+
-- the classification, and they are what this seam proves per row. Anything it
|
|
64
|
+
-- cannot prove is recorded as an unresolved obligation through migration
|
|
65
|
+
-- 0286's ledger - never guessed, never rewritten.
|
|
66
|
+
--
|
|
67
|
+
-- The seam is read-only over every resource table: its sole writes are its own
|
|
68
|
+
-- transaction-scoped capability row and, when a run key is supplied, the 0286
|
|
69
|
+
-- ledger. Receipt recording is guarded by `to_regprocedure` so this migration
|
|
70
|
+
-- is order-independent with respect to the ledger migration and reports
|
|
71
|
+
-- `ledgerAvailable` honestly rather than failing or silently skipping.
|
|
72
|
+
|
|
73
|
+
SET LOCAL lock_timeout = '5s';
|
|
74
|
+
SET LOCAL statement_timeout = '5min';
|
|
75
|
+
|
|
76
|
+
-- Freeze the active data schema into the SECURITY DEFINER routines below.
|
|
77
|
+
-- Standalone installs resolve to public; embedded installs resolve to their
|
|
78
|
+
-- dedicated schema. pg_temp remains explicitly last.
|
|
79
|
+
SELECT pg_catalog.set_config(
|
|
80
|
+
'search_path',
|
|
81
|
+
pg_catalog.format('%I, pg_catalog, pg_temp', pg_catalog.current_schema()),
|
|
82
|
+
true
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
-- A transaction-scoped, migration-owner-only capability. Deliberately NOT a
|
|
86
|
+
-- reuse of 0285's inventory capability: that policy pins the literal
|
|
87
|
+
-- `current_user` observed while 0285 ran, so reusing it would silently fail on
|
|
88
|
+
-- any deployment whose migration principal changed between the two migrations,
|
|
89
|
+
-- and it carries no policy for `organization_user_resource_authorities`, which
|
|
90
|
+
-- this seam must join.
|
|
91
|
+
CREATE TABLE opengeni_private.resource_classification_capabilities (
|
|
92
|
+
"backend_pid" integer NOT NULL,
|
|
93
|
+
"transaction_id" xid8 NOT NULL,
|
|
94
|
+
CONSTRAINT "resource_classification_capabilities_pk" PRIMARY KEY (
|
|
95
|
+
"backend_pid", "transaction_id"
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
REVOKE ALL ON TABLE opengeni_private.resource_classification_capabilities FROM PUBLIC;
|
|
99
|
+
|
|
100
|
+
CREATE OR REPLACE FUNCTION opengeni_private.resource_classification_capability_active()
|
|
101
|
+
RETURNS boolean
|
|
102
|
+
LANGUAGE sql
|
|
103
|
+
STABLE
|
|
104
|
+
SECURITY DEFINER
|
|
105
|
+
SET search_path = pg_catalog
|
|
106
|
+
AS $capability_active$
|
|
107
|
+
SELECT EXISTS (
|
|
108
|
+
SELECT 1
|
|
109
|
+
FROM opengeni_private.resource_classification_capabilities capability
|
|
110
|
+
WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
|
|
111
|
+
AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
112
|
+
)
|
|
113
|
+
$capability_active$;
|
|
114
|
+
REVOKE ALL ON FUNCTION
|
|
115
|
+
opengeni_private.resource_classification_capability_active() FROM PUBLIC;
|
|
116
|
+
|
|
117
|
+
DO $resource_classification_capability_policies$
|
|
118
|
+
DECLARE
|
|
119
|
+
data_schema text := current_schema();
|
|
120
|
+
migration_owner text := current_user;
|
|
121
|
+
table_name text;
|
|
122
|
+
BEGIN
|
|
123
|
+
FOREACH table_name IN ARRAY ARRAY[
|
|
124
|
+
'workspace_variable_sets',
|
|
125
|
+
'rigs',
|
|
126
|
+
'enrollments',
|
|
127
|
+
'organization_user_resource_authorities',
|
|
128
|
+
'organization_memberships'
|
|
129
|
+
] LOOP
|
|
130
|
+
EXECUTE format(
|
|
131
|
+
'CREATE POLICY resource_classification_capability_read ON %I.%I '
|
|
132
|
+
|| 'FOR SELECT USING (current_user = %L AND '
|
|
133
|
+
|| 'opengeni_private.resource_classification_capability_active())',
|
|
134
|
+
data_schema,
|
|
135
|
+
table_name,
|
|
136
|
+
migration_owner
|
|
137
|
+
);
|
|
138
|
+
END LOOP;
|
|
139
|
+
END
|
|
140
|
+
$resource_classification_capability_policies$;
|
|
141
|
+
|
|
142
|
+
-- Asserts the explicit authority classification of every Variable Set, Rig,
|
|
143
|
+
-- and Connected Machine in one organization.
|
|
144
|
+
--
|
|
145
|
+
-- `p_run_key` is the durability switch. Omitted (NULL) the call is a pure
|
|
146
|
+
-- read. Supplied, the same verdicts are additionally recorded through the 0286
|
|
147
|
+
-- ledger as one receipt per family plus one append-only unresolved row per
|
|
148
|
+
-- resource that could not be proven - which is also why the run key must be
|
|
149
|
+
-- fresh per run: the ledger refuses to re-open a settled receipt.
|
|
150
|
+
CREATE OR REPLACE FUNCTION verify_organization_resource_classification(
|
|
151
|
+
p_organization_id uuid,
|
|
152
|
+
p_run_key text DEFAULT NULL
|
|
153
|
+
) RETURNS jsonb
|
|
154
|
+
LANGUAGE plpgsql
|
|
155
|
+
SECURITY DEFINER
|
|
156
|
+
SET search_path FROM CURRENT
|
|
157
|
+
SET statement_timeout = '5min'
|
|
158
|
+
AS $$
|
|
159
|
+
DECLARE
|
|
160
|
+
-- family label (0286 vocabulary), resource table, authority resource_kind
|
|
161
|
+
families constant text[][] := ARRAY[
|
|
162
|
+
ARRAY['variable_sets', 'workspace_variable_sets', 'variable_set'],
|
|
163
|
+
ARRAY['rigs', 'rigs', 'rig'],
|
|
164
|
+
ARRAY['machines', 'enrollments', 'connected_machine']
|
|
165
|
+
];
|
|
166
|
+
family text[];
|
|
167
|
+
claimed_rows integer := 0;
|
|
168
|
+
claimed boolean := false;
|
|
169
|
+
ledger_available boolean;
|
|
170
|
+
classification_sql text;
|
|
171
|
+
family_result jsonb;
|
|
172
|
+
families_result jsonb := '{}'::jsonb;
|
|
173
|
+
receipt_id uuid;
|
|
174
|
+
unresolved_row record;
|
|
175
|
+
BEGIN
|
|
176
|
+
IF p_organization_id IS NULL THEN
|
|
177
|
+
RAISE EXCEPTION 'resource classification requires an organization id'
|
|
178
|
+
USING ERRCODE = '22004';
|
|
179
|
+
END IF;
|
|
180
|
+
IF p_organization_id IS DISTINCT FROM nullif(
|
|
181
|
+
pg_catalog.current_setting('opengeni.account_id', true), ''
|
|
182
|
+
)::uuid
|
|
183
|
+
THEN
|
|
184
|
+
RAISE EXCEPTION 'resource classification scope mismatch'
|
|
185
|
+
USING ERRCODE = '42501';
|
|
186
|
+
END IF;
|
|
187
|
+
IF p_run_key IS NOT NULL AND length(btrim(p_run_key)) = 0 THEN
|
|
188
|
+
RAISE EXCEPTION 'resource classification run key must not be blank'
|
|
189
|
+
USING ERRCODE = '22023';
|
|
190
|
+
END IF;
|
|
191
|
+
|
|
192
|
+
-- The ledger lives in its own migration. Probe rather than assume, so this
|
|
193
|
+
-- seam neither hard-fails nor silently drops the obligation when the two
|
|
194
|
+
-- land in either order.
|
|
195
|
+
ledger_available := p_run_key IS NOT NULL
|
|
196
|
+
AND to_regprocedure('open_tenancy_backfill_receipt(uuid, text, text)') IS NOT NULL
|
|
197
|
+
AND to_regprocedure('record_tenancy_backfill_unresolved(uuid, uuid, text)') IS NOT NULL
|
|
198
|
+
AND to_regprocedure(
|
|
199
|
+
'complete_tenancy_backfill_receipt(uuid, bigint, bigint, text)'
|
|
200
|
+
) IS NOT NULL;
|
|
201
|
+
|
|
202
|
+
INSERT INTO opengeni_private.resource_classification_capabilities (
|
|
203
|
+
backend_pid, transaction_id
|
|
204
|
+
) VALUES (pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id())
|
|
205
|
+
ON CONFLICT DO NOTHING;
|
|
206
|
+
-- Only release what this call claimed, so a nested or enclosing holder of
|
|
207
|
+
-- the same transaction-scoped capability is never revoked underneath.
|
|
208
|
+
GET DIAGNOSTICS claimed_rows = ROW_COUNT;
|
|
209
|
+
claimed := claimed_rows > 0;
|
|
210
|
+
|
|
211
|
+
FOREACH family SLICE 1 IN ARRAY families LOOP
|
|
212
|
+
-- One verdict per row. The first two branches are the phase D terminal
|
|
213
|
+
-- states; every other branch is an explicit refusal to classify, carrying
|
|
214
|
+
-- a fixed 0286 reason code and never a proposed owner.
|
|
215
|
+
classification_sql := format($classification$
|
|
216
|
+
SELECT resource.id AS resource_id,
|
|
217
|
+
CASE
|
|
218
|
+
WHEN resource.authority_scope = 'workspace'
|
|
219
|
+
AND resource.authority_id IS NULL
|
|
220
|
+
AND resource.owner_organization_membership_id IS NULL
|
|
221
|
+
THEN 'workspace_owned'
|
|
222
|
+
-- Organization scope is a deliberate lifecycle choice (0254/0262),
|
|
223
|
+
-- never a legacy default, so it is reported separately rather than
|
|
224
|
+
-- folded into the workspace population it is not part of.
|
|
225
|
+
WHEN resource.authority_scope = 'organization'
|
|
226
|
+
AND resource.authority_id IS NULL
|
|
227
|
+
AND resource.owner_organization_membership_id IS NULL
|
|
228
|
+
THEN 'organization_owned'
|
|
229
|
+
WHEN resource.authority_scope <> 'user'
|
|
230
|
+
OR resource.authority_id IS NULL
|
|
231
|
+
OR resource.owner_organization_membership_id IS NULL
|
|
232
|
+
THEN 'legacy_shape_unrecognized'
|
|
233
|
+
WHEN authority.id IS NULL
|
|
234
|
+
OR authority.resource_kind <> %2$L
|
|
235
|
+
OR authority.resource_id <> resource.id
|
|
236
|
+
OR authority.organization_membership_id
|
|
237
|
+
<> resource.owner_organization_membership_id
|
|
238
|
+
THEN 'conflicting_authority_rows'
|
|
239
|
+
WHEN membership.id IS NULL
|
|
240
|
+
OR membership.status <> 'active'
|
|
241
|
+
OR membership.revoked_at IS NOT NULL
|
|
242
|
+
THEN 'missing_organization_membership'
|
|
243
|
+
WHEN authority.status <> 'active' OR authority.revoked_at IS NOT NULL
|
|
244
|
+
THEN 'ambiguous_candidate_authority'
|
|
245
|
+
WHEN resource.origin_workspace_id IS NULL
|
|
246
|
+
THEN 'no_deterministic_evidence'
|
|
247
|
+
ELSE 'user_owned'
|
|
248
|
+
END AS verdict
|
|
249
|
+
FROM %1$I resource
|
|
250
|
+
LEFT JOIN organization_user_resource_authorities authority
|
|
251
|
+
ON authority.id = resource.authority_id
|
|
252
|
+
AND authority.account_id = resource.account_id
|
|
253
|
+
LEFT JOIN organization_memberships membership
|
|
254
|
+
ON membership.id = resource.owner_organization_membership_id
|
|
255
|
+
AND membership.account_id = resource.account_id
|
|
256
|
+
WHERE resource.account_id = $1
|
|
257
|
+
$classification$, family[2], family[3]);
|
|
258
|
+
|
|
259
|
+
EXECUTE format($summary$
|
|
260
|
+
WITH classified AS (%1$s)
|
|
261
|
+
SELECT pg_catalog.jsonb_build_object(
|
|
262
|
+
'total', count(*)::int,
|
|
263
|
+
'workspaceOwned', count(*) FILTER (WHERE verdict = 'workspace_owned')::int,
|
|
264
|
+
'organizationOwned', count(*) FILTER (
|
|
265
|
+
WHERE verdict = 'organization_owned'
|
|
266
|
+
)::int,
|
|
267
|
+
'userOwned', count(*) FILTER (WHERE verdict = 'user_owned')::int,
|
|
268
|
+
'unresolved', count(*) FILTER (
|
|
269
|
+
WHERE verdict NOT IN ('workspace_owned', 'organization_owned', 'user_owned')
|
|
270
|
+
)::int,
|
|
271
|
+
'unresolvedRows', coalesce((
|
|
272
|
+
SELECT pg_catalog.jsonb_agg(
|
|
273
|
+
pg_catalog.jsonb_build_object(
|
|
274
|
+
'resourceId', bounded.resource_id, 'reasonCode', bounded.verdict
|
|
275
|
+
) ORDER BY bounded.resource_id
|
|
276
|
+
)
|
|
277
|
+
FROM (
|
|
278
|
+
SELECT resource_id, verdict FROM classified
|
|
279
|
+
WHERE verdict NOT IN (
|
|
280
|
+
'workspace_owned', 'organization_owned', 'user_owned'
|
|
281
|
+
)
|
|
282
|
+
ORDER BY resource_id LIMIT 1000
|
|
283
|
+
) bounded
|
|
284
|
+
), '[]'::jsonb),
|
|
285
|
+
'unresolvedTruncated', count(*) FILTER (
|
|
286
|
+
WHERE verdict NOT IN ('workspace_owned', 'organization_owned', 'user_owned')
|
|
287
|
+
) > 1000
|
|
288
|
+
)
|
|
289
|
+
FROM classified
|
|
290
|
+
$summary$, classification_sql)
|
|
291
|
+
USING p_organization_id
|
|
292
|
+
INTO family_result;
|
|
293
|
+
|
|
294
|
+
IF ledger_available THEN
|
|
295
|
+
EXECUTE 'SELECT open_tenancy_backfill_receipt($1, $2, $3)'
|
|
296
|
+
USING p_organization_id, family[1], p_run_key
|
|
297
|
+
INTO receipt_id;
|
|
298
|
+
FOR unresolved_row IN EXECUTE format(
|
|
299
|
+
'SELECT resource_id, verdict FROM (%1$s) classified '
|
|
300
|
+
|| 'WHERE verdict NOT IN '
|
|
301
|
+
|| '(''workspace_owned'', ''organization_owned'', ''user_owned'') '
|
|
302
|
+
|| 'ORDER BY resource_id',
|
|
303
|
+
classification_sql
|
|
304
|
+
) USING p_organization_id LOOP
|
|
305
|
+
EXECUTE 'SELECT record_tenancy_backfill_unresolved($1, $2, $3)'
|
|
306
|
+
USING receipt_id, unresolved_row.resource_id, unresolved_row.verdict;
|
|
307
|
+
END LOOP;
|
|
308
|
+
-- `classified_count` is the population this run PROVED already carries a
|
|
309
|
+
-- terminal non-user classification; `skipped_count` is the reviewed
|
|
310
|
+
-- user-owned population the backfill must not touch. Neither reflects a
|
|
311
|
+
-- rewrite: this run changes no resource row.
|
|
312
|
+
EXECUTE 'SELECT complete_tenancy_backfill_receipt($1, $2, $3, $4)'
|
|
313
|
+
USING receipt_id,
|
|
314
|
+
(family_result ->> 'workspaceOwned')::bigint
|
|
315
|
+
+ (family_result ->> 'organizationOwned')::bigint,
|
|
316
|
+
(family_result ->> 'userOwned')::bigint,
|
|
317
|
+
'completed';
|
|
318
|
+
family_result := family_result
|
|
319
|
+
|| pg_catalog.jsonb_build_object('receiptId', receipt_id);
|
|
320
|
+
END IF;
|
|
321
|
+
|
|
322
|
+
families_result := families_result
|
|
323
|
+
|| pg_catalog.jsonb_build_object(family[1], family_result);
|
|
324
|
+
END LOOP;
|
|
325
|
+
|
|
326
|
+
IF claimed THEN
|
|
327
|
+
DELETE FROM opengeni_private.resource_classification_capabilities
|
|
328
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
329
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
|
|
330
|
+
END IF;
|
|
331
|
+
|
|
332
|
+
RETURN pg_catalog.jsonb_build_object(
|
|
333
|
+
'schemaVersion', 1,
|
|
334
|
+
'organizationId', p_organization_id,
|
|
335
|
+
'runKey', p_run_key,
|
|
336
|
+
'ledgerAvailable', ledger_available,
|
|
337
|
+
'rewroteResourceRows', false,
|
|
338
|
+
'families', families_result
|
|
339
|
+
);
|
|
340
|
+
EXCEPTION WHEN OTHERS THEN
|
|
341
|
+
IF claimed THEN
|
|
342
|
+
DELETE FROM opengeni_private.resource_classification_capabilities
|
|
343
|
+
WHERE backend_pid = pg_catalog.pg_backend_pid()
|
|
344
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
|
|
345
|
+
END IF;
|
|
346
|
+
RAISE;
|
|
347
|
+
END
|
|
348
|
+
$$;
|
|
349
|
+
|
|
350
|
+
REVOKE ALL ON FUNCTION
|
|
351
|
+
verify_organization_resource_classification(uuid, text) FROM PUBLIC;
|
|
352
|
+
|
|
353
|
+
DO $resource_classification_grants$
|
|
354
|
+
BEGIN
|
|
355
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
356
|
+
GRANT EXECUTE ON FUNCTION
|
|
357
|
+
verify_organization_resource_classification(uuid, text) TO opengeni_app;
|
|
358
|
+
-- The predicate must be independently EXECUTE-granted (0254's and 0285's
|
|
359
|
+
-- exact lesson): an ordinary opengeni_app read of ANY table this
|
|
360
|
+
-- capability protects evaluates every PERMISSIVE policy on it, so without
|
|
361
|
+
-- this grant an unrelated read of `rigs`, `enrollments`, or
|
|
362
|
+
-- `workspace_variable_sets` fails closed with a permission error instead
|
|
363
|
+
-- of the policy branch simply evaluating false.
|
|
364
|
+
GRANT EXECUTE ON FUNCTION
|
|
365
|
+
opengeni_private.resource_classification_capability_active() TO opengeni_app;
|
|
366
|
+
END IF;
|
|
367
|
+
END
|
|
368
|
+
$resource_classification_grants$;
|
|
369
|
+
|
|
370
|
+
COMMENT ON FUNCTION verify_organization_resource_classification(uuid, text) IS
|
|
371
|
+
'Organization-tenancy phase D assertion seam for Variable Sets, Rigs, and Connected Machines. Read-only over every resource table: it proves each row already carries an explicit terminal authority classification and records what it cannot prove through the 0286 ledger. It never rewrites a resource row and never infers user ownership.';
|