@opengeni/db 4.2.2 → 4.3.2-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/canonical-human-identities.js +3 -3
- package/dist/{chunk-S4Y42VSX.js → chunk-AI4NC6XW.js} +212 -7
- package/dist/chunk-AI4NC6XW.js.map +1 -0
- package/dist/{chunk-SNLIKELL.js → chunk-DVSLOUEI.js} +2 -2
- package/dist/{chunk-6GAPAYZR.js → chunk-F4BIHJ7J.js} +562 -86
- package/dist/chunk-F4BIHJ7J.js.map +1 -0
- package/dist/{chunk-B2KR3TEM.js → chunk-I4ITNMX2.js} +2 -2
- package/dist/{chunk-MGVOYVPS.js → chunk-PFBUHOI5.js} +3 -3
- package/dist/{chunk-6Y7ZDVJ5.js → chunk-UUPY6IGP.js} +2 -2
- package/dist/{chunk-FPTI4NP4.js → chunk-UVBPOGA7.js} +2 -2
- package/dist/{chunk-7WX3R3YL.js → chunk-XBXANJQS.js} +7 -2
- package/dist/chunk-XBXANJQS.js.map +1 -0
- package/dist/{chunk-6IQ4X35Y.js → chunk-YVYBAWBU.js} +46 -1
- package/dist/chunk-YVYBAWBU.js.map +1 -0
- package/dist/connect-attempts.d.ts +60 -0
- package/dist/connection-setup-authority.d.ts +10 -0
- package/dist/connection-token-resolver.d.ts +11 -2
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/external-identities.d.ts +16 -0
- package/dist/external-identity-links.d.ts +74 -0
- package/dist/external-link-work.d.ts +51 -0
- package/dist/host-mcp-binding-match.d.ts +5 -0
- package/dist/host-mcp-bindings.d.ts +73 -0
- package/dist/host-mcp-task-authority.d.ts +47 -0
- package/dist/host-selection-identity.d.ts +6 -0
- package/dist/index.d.ts +60 -35
- package/dist/index.js +15247 -13793
- package/dist/index.js.map +1 -1
- package/dist/live-session-attempt.d.ts +275 -0
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/memory-domain.d.ts +3 -3
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.js +2 -2
- package/dist/runtime-posture.d.ts +6 -6
- package/dist/scheduled-task-revision-authority.d.ts +11 -0
- package/dist/schema.d.ts +1279 -9
- package/dist/schema.js +11 -1
- package/dist/session-background-commands.js +4 -4
- package/dist/session-command-output.js +4 -4
- package/dist/session-event-slices.js +2 -2
- package/dist/session-queue-commands.d.ts +7 -0
- package/dist/session-tenancy.d.ts +2 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/video-generation.js +3 -3
- package/dist/workspace-authority.d.ts +4 -4
- package/dist/workspace-tool-defaults.js +4 -4
- package/drizzle/0437_organization_scoped_external_workspaces.sql +71 -0
- package/drizzle/0438_durable_connect_attempts.sql +95 -0
- package/drizzle/0439_external_identity_provisioning.sql +128 -0
- package/drizzle/0440_external_workspace_member_removal.sql +59 -0
- package/drizzle/0441_external_identity_membership_lifecycle.sql +163 -0
- package/drizzle/0442_external_owning_user_authority.sql +81 -0
- package/drizzle/0443_host_mcp_binding_registry.sql +93 -0
- package/drizzle/0444_host_mcp_delegations.sql +129 -0
- package/drizzle/0445_host_mcp_turn_authorities.sql +145 -0
- package/drizzle/0446_host_mcp_causal_continuation.sql +126 -0
- package/drizzle/0447_host_mcp_task_authorities.sql +264 -0
- package/drizzle/0448_host_mcp_child_authority.sql +115 -0
- package/drizzle/0449_external_identity_link_lifecycle.sql +107 -0
- package/drizzle/0450_external_identity_link_work.sql +179 -0
- package/drizzle/0451_external_link_preview_and_permission_ceiling.sql +82 -0
- package/drizzle/0452_external_link_scheduled_origin.sql +34 -0
- package/drizzle/0453_host_mcp_native_owner.sql +35 -0
- package/drizzle/0454_connect_origin_authority.sql +42 -0
- package/drizzle/0455_external_link_inventory_labels.sql +44 -0
- package/drizzle/0456_social_connection_versions.sql +14 -0
- package/drizzle/0457_canonical_session_scope_subject.sql +46 -0
- package/drizzle/0458_skill_review_wire_compatibility.sql +36 -0
- package/package.json +6 -6
- package/src/connect-attempts.ts +317 -0
- package/src/connection-authority.ts +9 -0
- package/src/connection-setup-authority.ts +35 -0
- package/src/connection-token-resolver.ts +177 -48
- package/src/external-identities.ts +70 -0
- package/src/external-identity-links.ts +364 -0
- package/src/external-link-work.ts +228 -0
- package/src/host-mcp-binding-match.ts +33 -0
- package/src/host-mcp-bindings.ts +741 -0
- package/src/host-mcp-task-authority.ts +383 -0
- package/src/host-selection-identity.ts +29 -0
- package/src/index.ts +481 -151
- package/src/live-session-attempt.ts +85 -0
- package/src/managed-human-provisioning.ts +6 -1
- package/src/memory-domain.ts +6 -11
- package/src/provision-roles.ts +3 -0
- package/src/runtime-posture.ts +47 -0
- package/src/scheduled-task-revision-authority.ts +30 -0
- package/src/schema.ts +233 -7
- package/src/session-control.ts +14 -0
- package/src/session-queue-commands.ts +11 -0
- package/src/session-tenancy.ts +7 -0
- package/src/workspace-artifacts.ts +10 -4
- package/src/workspace-authority.ts +5 -5
- package/dist/chunk-6GAPAYZR.js.map +0 -1
- package/dist/chunk-6IQ4X35Y.js.map +0 -1
- package/dist/chunk-7WX3R3YL.js.map +0 -1
- package/dist/chunk-S4Y42VSX.js.map +0 -1
- /package/dist/{chunk-SNLIKELL.js.map → chunk-DVSLOUEI.js.map} +0 -0
- /package/dist/{chunk-B2KR3TEM.js.map → chunk-I4ITNMX2.js.map} +0 -0
- /package/dist/{chunk-MGVOYVPS.js.map → chunk-PFBUHOI5.js.map} +0 -0
- /package/dist/{chunk-6Y7ZDVJ5.js.map → chunk-UUPY6IGP.js.map} +0 -0
- /package/dist/{chunk-FPTI4NP4.js.map → chunk-UVBPOGA7.js.map} +0 -0
package/dist/schema.js
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
composerDrafts,
|
|
56
56
|
computerSessionAssociations,
|
|
57
57
|
computerSessions,
|
|
58
|
+
connectAttempts,
|
|
58
59
|
connectionDisconnectOperations,
|
|
59
60
|
connectionUseAuditFacts,
|
|
60
61
|
connectionUseOnceConsumptionReceipts,
|
|
@@ -87,6 +88,7 @@ import {
|
|
|
87
88
|
enrollmentOsValues,
|
|
88
89
|
enrollmentStatusValues,
|
|
89
90
|
enrollments,
|
|
91
|
+
externalIdentities,
|
|
90
92
|
feedbackSubmissions,
|
|
91
93
|
fileUploads,
|
|
92
94
|
files,
|
|
@@ -104,6 +106,9 @@ import {
|
|
|
104
106
|
hostExportCursorState,
|
|
105
107
|
hostExportDeadLetters,
|
|
106
108
|
hostExportOutbox,
|
|
109
|
+
hostMcpBindings,
|
|
110
|
+
hostMcpDelegations,
|
|
111
|
+
hostMcpTurnAuthorities,
|
|
107
112
|
imageGenerationOperations,
|
|
108
113
|
importBatches,
|
|
109
114
|
integrationFacetBindingOwners,
|
|
@@ -354,7 +359,7 @@ import {
|
|
|
354
359
|
xaiRotationSettings,
|
|
355
360
|
xaiSessionAccountPins,
|
|
356
361
|
xaiSubscriptionCredentials
|
|
357
|
-
} from "./chunk-
|
|
362
|
+
} from "./chunk-AI4NC6XW.js";
|
|
358
363
|
import "./chunk-PZ5AY32C.js";
|
|
359
364
|
export {
|
|
360
365
|
agentRunStates,
|
|
@@ -413,6 +418,7 @@ export {
|
|
|
413
418
|
composerDrafts,
|
|
414
419
|
computerSessionAssociations,
|
|
415
420
|
computerSessions,
|
|
421
|
+
connectAttempts,
|
|
416
422
|
connectionDisconnectOperations,
|
|
417
423
|
connectionUseAuditFacts,
|
|
418
424
|
connectionUseOnceConsumptionReceipts,
|
|
@@ -445,6 +451,7 @@ export {
|
|
|
445
451
|
enrollmentOsValues,
|
|
446
452
|
enrollmentStatusValues,
|
|
447
453
|
enrollments,
|
|
454
|
+
externalIdentities,
|
|
448
455
|
feedbackSubmissions,
|
|
449
456
|
fileUploads,
|
|
450
457
|
files,
|
|
@@ -462,6 +469,9 @@ export {
|
|
|
462
469
|
hostExportCursorState,
|
|
463
470
|
hostExportDeadLetters,
|
|
464
471
|
hostExportOutbox,
|
|
472
|
+
hostMcpBindings,
|
|
473
|
+
hostMcpDelegations,
|
|
474
|
+
hostMcpTurnAuthorities,
|
|
465
475
|
imageGenerationOperations,
|
|
466
476
|
importBatches,
|
|
467
477
|
integrationFacetBindingOwners,
|
|
@@ -22,10 +22,10 @@ import {
|
|
|
22
22
|
settleClaimedConnectedMachineBackgroundCommandWithMutation,
|
|
23
23
|
settleConnectedMachineSessionBackgroundCommandWithMutation,
|
|
24
24
|
settleSessionBackgroundCommandForRetainedProcessInTransaction
|
|
25
|
-
} from "./chunk-
|
|
26
|
-
import "./chunk-
|
|
27
|
-
import "./chunk-
|
|
28
|
-
import "./chunk-
|
|
25
|
+
} from "./chunk-F4BIHJ7J.js";
|
|
26
|
+
import "./chunk-PFBUHOI5.js";
|
|
27
|
+
import "./chunk-I4ITNMX2.js";
|
|
28
|
+
import "./chunk-AI4NC6XW.js";
|
|
29
29
|
import "./chunk-PZ5AY32C.js";
|
|
30
30
|
export {
|
|
31
31
|
COMMAND_OUTPUT_DEFAULT_BYTES,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
lockSessionEventWriteRows
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-F4BIHJ7J.js";
|
|
4
|
+
import "./chunk-PFBUHOI5.js";
|
|
5
5
|
import {
|
|
6
6
|
withSessionActivityRlsContext
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-I4ITNMX2.js";
|
|
8
8
|
import {
|
|
9
9
|
fromPostgresLosslessJson,
|
|
10
10
|
sandboxRetainedProcesses,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
sessionEvents,
|
|
13
13
|
sessions,
|
|
14
14
|
withLosslessContentWriteVersion
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-AI4NC6XW.js";
|
|
16
16
|
import "./chunk-PZ5AY32C.js";
|
|
17
17
|
|
|
18
18
|
// src/session-command-output.ts
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
rawRows,
|
|
3
3
|
withWorkspaceRls
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-I4ITNMX2.js";
|
|
5
5
|
import {
|
|
6
6
|
LOSSLESS_JSON_STRING_PREFIX,
|
|
7
7
|
fromPostgresLosslessJson,
|
|
8
8
|
sessionEvents,
|
|
9
9
|
sessionTurns
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-AI4NC6XW.js";
|
|
11
11
|
import "./chunk-PZ5AY32C.js";
|
|
12
12
|
|
|
13
13
|
// src/session-event-slices.ts
|
|
@@ -230,6 +230,13 @@ export declare function submitHumanPromptInTransaction(db: SessionActivityDataba
|
|
|
230
230
|
/** Trusted database-only admission seam. It runs only after a completed
|
|
231
231
|
* operation replay has been ruled out; throwing rolls the prompt back. */
|
|
232
232
|
beforeFreshPromptCommit?: (tx: Database) => Promise<void>;
|
|
233
|
+
/** Backend-only authority capture, atomic with the newly accepted turn. */
|
|
234
|
+
captureTurnAuthority?: (tx: Database, turnId: string) => Promise<void>;
|
|
235
|
+
selectedHostMcpDelegations?: Array<{
|
|
236
|
+
serverId: string;
|
|
237
|
+
delegationId: string;
|
|
238
|
+
generation: number;
|
|
239
|
+
}>;
|
|
233
240
|
/** Request-scoped callers bound the control prefix wait; lifecycle callers omit it. */
|
|
234
241
|
controlLockTimeoutMs?: number;
|
|
235
242
|
}): Promise<SubmitHumanPromptResult>;
|
|
@@ -33,6 +33,7 @@ export type TransitionSessionVisibilityInput = {
|
|
|
33
33
|
targetVisibility: SessionTenancyVisibility;
|
|
34
34
|
expectedAuthorityEpoch: number;
|
|
35
35
|
operationKey: string;
|
|
36
|
+
beforeCommit?: (tx: Database) => Promise<void>;
|
|
36
37
|
};
|
|
37
38
|
export type TransitionSessionVisibilityResult = {
|
|
38
39
|
operationId: string;
|
|
@@ -60,6 +61,7 @@ export type ForkSessionContentInput = {
|
|
|
60
61
|
operationKey: string;
|
|
61
62
|
runtimeRequest?: ForkSessionRuntimeRequest;
|
|
62
63
|
runtimeConfiguration?: ForkSessionRuntimeConfiguration;
|
|
64
|
+
beforeCommit?: (tx: Database) => Promise<void>;
|
|
63
65
|
};
|
|
64
66
|
export type ForkSessionRuntimeRequest = {
|
|
65
67
|
variableSetIds: string[];
|
package/dist/session-tenancy.js
CHANGED
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
replayAppliedSessionFork,
|
|
15
15
|
sessionTenancyProductActivated,
|
|
16
16
|
transitionSessionVisibility
|
|
17
|
-
} from "./chunk-
|
|
18
|
-
import "./chunk-
|
|
19
|
-
import "./chunk-
|
|
17
|
+
} from "./chunk-XBXANJQS.js";
|
|
18
|
+
import "./chunk-I4ITNMX2.js";
|
|
19
|
+
import "./chunk-AI4NC6XW.js";
|
|
20
20
|
import "./chunk-PZ5AY32C.js";
|
|
21
21
|
export {
|
|
22
22
|
SessionTenancyAccessError,
|
package/dist/video-generation.js
CHANGED
|
@@ -29,9 +29,9 @@ import {
|
|
|
29
29
|
settleVideoGenerationFailure,
|
|
30
30
|
settleVideoGenerationReady,
|
|
31
31
|
updateWorkspaceVideoGenerationPolicy
|
|
32
|
-
} from "./chunk-
|
|
33
|
-
import "./chunk-
|
|
34
|
-
import "./chunk-
|
|
32
|
+
} from "./chunk-PFBUHOI5.js";
|
|
33
|
+
import "./chunk-I4ITNMX2.js";
|
|
34
|
+
import "./chunk-AI4NC6XW.js";
|
|
35
35
|
import "./chunk-PZ5AY32C.js";
|
|
36
36
|
export {
|
|
37
37
|
ACTIVE_VIDEO_GENERATION_STATUSES,
|
|
@@ -51,10 +51,10 @@ import { type Database } from "./database.js";
|
|
|
51
51
|
* adding a caller, that stamp is the part you have to get right; this function
|
|
52
52
|
* will happily answer `true` for any subject you scope a transaction to.
|
|
53
53
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
54
|
+
* Persisted `external_user:` subjects also have an organization membership and
|
|
55
|
+
* an exact Personal pointer. They use the same consistency check only after the
|
|
56
|
+
* API's dedicated verified external proof, never a native-cookie stamp. Other
|
|
57
|
+
* service/configured/local subjects retain the plain membership answer.
|
|
58
58
|
*/
|
|
59
59
|
export declare function subjectHasLiveWorkspaceAuthorityInScope(scopedDb: Database, input: {
|
|
60
60
|
accountId: string;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
lockWorkspaceInferenceControl
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-F4BIHJ7J.js";
|
|
4
|
+
import "./chunk-PFBUHOI5.js";
|
|
5
5
|
import {
|
|
6
6
|
withWorkspaceRls
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-I4ITNMX2.js";
|
|
8
|
+
import "./chunk-AI4NC6XW.js";
|
|
9
9
|
import "./chunk-PZ5AY32C.js";
|
|
10
10
|
|
|
11
11
|
// src/workspace-tool-defaults.ts
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
-- Stop every old API and worker before activation. Old binaries use a global
|
|
3
|
+
-- conflict target and must not restart after this organization-scoped cutover.
|
|
4
|
+
SET LOCAL lock_timeout = '5s';
|
|
5
|
+
SET LOCAL statement_timeout = '5min';
|
|
6
|
+
|
|
7
|
+
DO $drain$
|
|
8
|
+
DECLARE
|
|
9
|
+
roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
|
|
10
|
+
BEGIN
|
|
11
|
+
IF roles IS NULL OR jsonb_typeof(roles) <> 'array' THEN
|
|
12
|
+
RAISE EXCEPTION '0437 requires an explicit application database role list' USING ERRCODE = '55000';
|
|
13
|
+
END IF;
|
|
14
|
+
IF jsonb_array_length(roles) NOT BETWEEN 1 AND 16 OR EXISTS (
|
|
15
|
+
SELECT 1 FROM jsonb_array_elements(roles) item
|
|
16
|
+
WHERE jsonb_typeof(item) <> 'string' OR btrim(item #>> '{}') = ''
|
|
17
|
+
OR item #>> '{}' <> btrim(item #>> '{}') OR octet_length(item #>> '{}') > 63
|
|
18
|
+
) THEN
|
|
19
|
+
RAISE EXCEPTION '0437 received invalid application database roles' USING ERRCODE = '55000';
|
|
20
|
+
END IF;
|
|
21
|
+
IF EXISTS (
|
|
22
|
+
SELECT 1 FROM pg_stat_activity activity
|
|
23
|
+
JOIN jsonb_array_elements_text(roles) role_name ON role_name = activity.usename
|
|
24
|
+
WHERE activity.datname = current_database() AND activity.pid <> pg_backend_pid()
|
|
25
|
+
) THEN
|
|
26
|
+
RAISE EXCEPTION '0437 requires all application database sessions stopped' USING ERRCODE = '55000';
|
|
27
|
+
END IF;
|
|
28
|
+
END
|
|
29
|
+
$drain$;
|
|
30
|
+
|
|
31
|
+
LOCK TABLE workspaces IN ACCESS EXCLUSIVE MODE;
|
|
32
|
+
|
|
33
|
+
DO $cutover$
|
|
34
|
+
DECLARE
|
|
35
|
+
definition text;
|
|
36
|
+
old_target constant text := 'ON CONFLICT (external_source, external_id) DO UPDATE';
|
|
37
|
+
new_target constant text := 'ON CONFLICT (account_id, external_source, external_id) DO UPDATE';
|
|
38
|
+
old_personal_guard constant text := 'WHERE conflicting_workspace.external_source = ''opengeni:organization-membership''';
|
|
39
|
+
new_personal_guard constant text := 'WHERE conflicting_workspace.account_id = p_account_id AND conflicting_workspace.external_source = ''opengeni:organization-membership''';
|
|
40
|
+
BEGIN
|
|
41
|
+
IF EXISTS (
|
|
42
|
+
SELECT 1 FROM pg_stat_activity activity
|
|
43
|
+
JOIN jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) role_name
|
|
44
|
+
ON role_name = activity.usename
|
|
45
|
+
WHERE activity.datname = current_database() AND activity.pid <> pg_backend_pid()
|
|
46
|
+
) THEN
|
|
47
|
+
RAISE EXCEPTION '0437 requires all application database sessions stopped' USING ERRCODE = '55000';
|
|
48
|
+
END IF;
|
|
49
|
+
-- Preserve the deployed lifecycle function, including its security attributes
|
|
50
|
+
-- and all intervening fixes. Fail closed if its one reviewed target drifted.
|
|
51
|
+
SELECT pg_get_functiondef('organization_membership_command(jsonb)'::regprocedure) INTO definition;
|
|
52
|
+
IF strpos(definition, 'RETURN organization_membership_command_0263(p_command);') = 0 THEN
|
|
53
|
+
RAISE EXCEPTION '0437 organization membership wrapper drifted' USING ERRCODE = '55000';
|
|
54
|
+
END IF;
|
|
55
|
+
-- The live wrapper retains the newer offboarding/session-tenancy fences;
|
|
56
|
+
-- invitation acceptance delegates to this separately named implementation.
|
|
57
|
+
SELECT pg_get_functiondef('organization_membership_command_0263(jsonb)'::regprocedure) INTO definition;
|
|
58
|
+
IF (length(definition) - length(replace(definition, old_target, ''))) / length(old_target) <> 1 THEN
|
|
59
|
+
RAISE EXCEPTION '0437 organization membership conflict target drifted' USING ERRCODE = '55000';
|
|
60
|
+
END IF;
|
|
61
|
+
EXECUTE replace(definition, old_target, new_target);
|
|
62
|
+
SELECT pg_get_functiondef('ensure_managed_human_personal_workspace(uuid,text,uuid)'::regprocedure) INTO definition;
|
|
63
|
+
IF (length(definition) - length(replace(definition, old_personal_guard, ''))) / length(old_personal_guard) <> 1 THEN
|
|
64
|
+
RAISE EXCEPTION '0437 personal workspace conflict guard drifted' USING ERRCODE = '55000';
|
|
65
|
+
END IF;
|
|
66
|
+
EXECUTE replace(definition, old_personal_guard, new_personal_guard);
|
|
67
|
+
END
|
|
68
|
+
$cutover$;
|
|
69
|
+
|
|
70
|
+
DROP INDEX workspaces_external_idx;
|
|
71
|
+
CREATE UNIQUE INDEX workspaces_external_idx ON workspaces (account_id, external_source, external_id);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
-- The exact runtime table/RLS/grant contract changes. Drain all old API and
|
|
3
|
+
-- worker logins and provision only the matching binary's role after migration.
|
|
4
|
+
SET LOCAL lock_timeout = '5s';
|
|
5
|
+
SET LOCAL statement_timeout = '5min';
|
|
6
|
+
|
|
7
|
+
DO $drain$
|
|
8
|
+
DECLARE
|
|
9
|
+
roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
|
|
10
|
+
BEGIN
|
|
11
|
+
IF roles IS NULL OR jsonb_typeof(roles) <> 'array' THEN
|
|
12
|
+
RAISE EXCEPTION '0438 requires an explicit application database role list' USING ERRCODE = '55000';
|
|
13
|
+
END IF;
|
|
14
|
+
IF jsonb_array_length(roles) NOT BETWEEN 1 AND 16 OR EXISTS (
|
|
15
|
+
SELECT 1 FROM jsonb_array_elements(roles) item
|
|
16
|
+
WHERE jsonb_typeof(item) <> 'string' OR btrim(item #>> '{}') = ''
|
|
17
|
+
OR item #>> '{}' <> btrim(item #>> '{}') OR octet_length(item #>> '{}') > 63
|
|
18
|
+
) THEN
|
|
19
|
+
RAISE EXCEPTION '0438 received invalid application database roles' USING ERRCODE = '55000';
|
|
20
|
+
END IF;
|
|
21
|
+
IF EXISTS (
|
|
22
|
+
SELECT 1 FROM pg_stat_activity activity
|
|
23
|
+
JOIN jsonb_array_elements_text(roles) role_name ON role_name = activity.usename
|
|
24
|
+
WHERE activity.datname = current_database() AND activity.pid <> pg_backend_pid()
|
|
25
|
+
) THEN
|
|
26
|
+
RAISE EXCEPTION '0438 requires all application database sessions stopped' USING ERRCODE = '55000';
|
|
27
|
+
END IF;
|
|
28
|
+
END
|
|
29
|
+
$drain$;
|
|
30
|
+
|
|
31
|
+
CREATE TABLE connect_attempts (
|
|
32
|
+
id uuid PRIMARY KEY,
|
|
33
|
+
account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
|
|
34
|
+
workspace_id uuid NOT NULL,
|
|
35
|
+
subject_id text NOT NULL CHECK (octet_length(subject_id) BETWEEN 1 AND 1024),
|
|
36
|
+
idempotency_key_hash text NOT NULL CHECK (idempotency_key_hash ~ '^[0-9a-f]{64}$'),
|
|
37
|
+
request_digest text NOT NULL CHECK (request_digest ~ '^[0-9a-f]{64}$'),
|
|
38
|
+
return_url text NOT NULL CHECK (octet_length(return_url) BETWEEN 1 AND 4096),
|
|
39
|
+
projection jsonb NOT NULL CHECK (jsonb_typeof(projection) = 'object' AND octet_length(projection::text) <= 1048576),
|
|
40
|
+
operation_id text CHECK (octet_length(operation_id) BETWEEN 1 AND 512),
|
|
41
|
+
operation_digest text CHECK (operation_digest ~ '^[0-9a-f]{64}$'),
|
|
42
|
+
receipts jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(receipts) = 'object' AND octet_length(receipts::text) <= 4194304),
|
|
43
|
+
expires_at timestamptz NOT NULL,
|
|
44
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
45
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
46
|
+
CONSTRAINT connect_attempts_workspace_account_fk FOREIGN KEY (workspace_id, account_id)
|
|
47
|
+
REFERENCES workspaces(id, account_id) ON DELETE CASCADE,
|
|
48
|
+
CONSTRAINT connect_attempts_operation_pair_check CHECK ((operation_id IS NULL) = (operation_digest IS NULL)),
|
|
49
|
+
CONSTRAINT connect_attempts_projection_identity_check CHECK (
|
|
50
|
+
projection ?& ARRAY['id', 'workspaceId', 'revision', 'state', 'expiresAt']
|
|
51
|
+
AND projection->>'id' = id::text AND projection->>'workspaceId' = workspace_id::text
|
|
52
|
+
AND (projection->>'revision')::bigint > 0
|
|
53
|
+
AND (projection->>'expiresAt')::timestamptz = expires_at
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
CREATE UNIQUE INDEX connect_attempts_actor_idempotency_idx ON connect_attempts
|
|
57
|
+
(workspace_id, subject_id, idempotency_key_hash);
|
|
58
|
+
CREATE INDEX connect_attempts_actor_pending_idx ON connect_attempts
|
|
59
|
+
(workspace_id, subject_id, created_at DESC, id DESC)
|
|
60
|
+
WHERE projection->>'state' NOT IN ('complete','cancelled','expired');
|
|
61
|
+
CREATE INDEX connect_attempts_expiry_idx ON connect_attempts (expires_at, id);
|
|
62
|
+
|
|
63
|
+
ALTER TABLE connect_attempts ENABLE ROW LEVEL SECURITY;
|
|
64
|
+
ALTER TABLE connect_attempts FORCE ROW LEVEL SECURITY;
|
|
65
|
+
CREATE POLICY connect_attempts_actor_scope ON connect_attempts
|
|
66
|
+
USING (account_id = opengeni_private.current_account_id()
|
|
67
|
+
AND workspace_id = opengeni_private.current_workspace_id()
|
|
68
|
+
AND subject_id = opengeni_private.current_subject_id())
|
|
69
|
+
WITH CHECK (account_id = opengeni_private.current_account_id()
|
|
70
|
+
AND workspace_id = opengeni_private.current_workspace_id()
|
|
71
|
+
AND subject_id = opengeni_private.current_subject_id());
|
|
72
|
+
|
|
73
|
+
-- New-table defaults must not leave privileges for old or unrelated logins.
|
|
74
|
+
REVOKE ALL ON TABLE connect_attempts FROM PUBLIC;
|
|
75
|
+
DO $acl$
|
|
76
|
+
DECLARE target_role record;
|
|
77
|
+
BEGIN
|
|
78
|
+
FOR target_role IN
|
|
79
|
+
SELECT DISTINCT role.rolname FROM pg_class relation
|
|
80
|
+
CROSS JOIN LATERAL aclexplode(coalesce(relation.relacl, acldefault('r', relation.relowner))) privilege
|
|
81
|
+
JOIN pg_roles role ON role.oid = privilege.grantee
|
|
82
|
+
WHERE relation.oid = 'connect_attempts'::regclass AND privilege.grantee <> relation.relowner
|
|
83
|
+
LOOP
|
|
84
|
+
EXECUTE format('REVOKE ALL ON TABLE connect_attempts FROM %I', target_role.rolname);
|
|
85
|
+
END LOOP;
|
|
86
|
+
IF EXISTS (
|
|
87
|
+
SELECT 1 FROM pg_stat_activity activity
|
|
88
|
+
JOIN jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) role_name
|
|
89
|
+
ON role_name = activity.usename
|
|
90
|
+
WHERE activity.datname = current_database() AND activity.pid <> pg_backend_pid()
|
|
91
|
+
) THEN
|
|
92
|
+
RAISE EXCEPTION '0438 requires all application database sessions stopped' USING ERRCODE = '55000';
|
|
93
|
+
END IF;
|
|
94
|
+
END
|
|
95
|
+
$acl$;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
-- External identity storage/provisioning only. Does not grant workspace access,
|
|
3
|
+
-- activate private sessions, link native users, or change authentication.
|
|
4
|
+
SET LOCAL lock_timeout = '5s';
|
|
5
|
+
SET LOCAL statement_timeout = '5min';
|
|
6
|
+
DO $drain$
|
|
7
|
+
DECLARE roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
|
|
8
|
+
BEGIN
|
|
9
|
+
IF roles IS NULL OR jsonb_typeof(roles) <> 'array' THEN
|
|
10
|
+
RAISE EXCEPTION '0439 requires application database roles' USING ERRCODE = '55000';
|
|
11
|
+
END IF;
|
|
12
|
+
IF jsonb_array_length(roles) NOT BETWEEN 1 AND 16 OR EXISTS (
|
|
13
|
+
SELECT 1 FROM jsonb_array_elements(roles) item WHERE jsonb_typeof(item) <> 'string'
|
|
14
|
+
OR btrim(item #>> '{}') = '' OR item #>> '{}' <> btrim(item #>> '{}')
|
|
15
|
+
OR octet_length(item #>> '{}') > 63
|
|
16
|
+
) THEN RAISE EXCEPTION '0439 invalid application roles' USING ERRCODE = '55000'; END IF;
|
|
17
|
+
IF EXISTS (SELECT 1 FROM pg_stat_activity a JOIN jsonb_array_elements_text(roles) r ON r = a.usename
|
|
18
|
+
WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
|
|
19
|
+
RAISE EXCEPTION '0439 requires stopped application sessions' USING ERRCODE = '55000';
|
|
20
|
+
END IF;
|
|
21
|
+
END
|
|
22
|
+
$drain$;
|
|
23
|
+
|
|
24
|
+
CREATE TABLE external_identities (
|
|
25
|
+
id uuid PRIMARY KEY,
|
|
26
|
+
account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
|
|
27
|
+
source text NOT NULL CHECK (char_length(source) BETWEEN 1 AND 200),
|
|
28
|
+
external_id text NOT NULL CHECK (char_length(external_id) BETWEEN 1 AND 1024),
|
|
29
|
+
subject_id text NOT NULL CHECK (subject_id = 'external_user:' || id::text),
|
|
30
|
+
organization_membership_id uuid NOT NULL,
|
|
31
|
+
personal_workspace_id uuid NOT NULL,
|
|
32
|
+
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','disabled','revoked')),
|
|
33
|
+
authorization_revision bigint NOT NULL DEFAULT 1 CHECK (authorization_revision > 0),
|
|
34
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
35
|
+
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
36
|
+
UNIQUE (account_id, source, external_id),
|
|
37
|
+
UNIQUE (account_id, subject_id),
|
|
38
|
+
FOREIGN KEY (organization_membership_id, account_id) REFERENCES organization_memberships(id, account_id),
|
|
39
|
+
FOREIGN KEY (personal_workspace_id, account_id) REFERENCES workspaces(id, account_id)
|
|
40
|
+
);
|
|
41
|
+
ALTER TABLE external_identities ENABLE ROW LEVEL SECURITY;
|
|
42
|
+
ALTER TABLE external_identities FORCE ROW LEVEL SECURITY;
|
|
43
|
+
CREATE POLICY external_identity_lifecycle ON external_identities
|
|
44
|
+
USING (account_id = opengeni_private.current_account_id()
|
|
45
|
+
AND current_setting('opengeni.organization_tenancy_lifecycle', true) = 'external_identity_provisioning')
|
|
46
|
+
WITH CHECK (account_id = opengeni_private.current_account_id()
|
|
47
|
+
AND current_setting('opengeni.organization_tenancy_lifecycle', true) = 'external_identity_provisioning');
|
|
48
|
+
CREATE POLICY external_identity_provisioning ON organization_memberships
|
|
49
|
+
USING (account_id = opengeni_private.current_account_id()
|
|
50
|
+
AND current_setting('opengeni.organization_tenancy_lifecycle', true) = 'external_identity_provisioning')
|
|
51
|
+
WITH CHECK (account_id = opengeni_private.current_account_id()
|
|
52
|
+
AND current_setting('opengeni.organization_tenancy_lifecycle', true) = 'external_identity_provisioning');
|
|
53
|
+
REVOKE ALL ON external_identities FROM PUBLIC;
|
|
54
|
+
|
|
55
|
+
-- Account scope is established by the authenticated API before this call.
|
|
56
|
+
-- Like other lifecycle functions, this is not itself an HTTP authenticator.
|
|
57
|
+
CREATE FUNCTION ensure_external_identity(p_account_id uuid, p_source text, p_external_id text)
|
|
58
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT
|
|
59
|
+
AS $body$
|
|
60
|
+
DECLARE
|
|
61
|
+
identity_row external_identities%ROWTYPE;
|
|
62
|
+
membership_row organization_memberships%ROWTYPE;
|
|
63
|
+
identity_id uuid;
|
|
64
|
+
personal_id uuid;
|
|
65
|
+
membership_id uuid;
|
|
66
|
+
subject text;
|
|
67
|
+
previous_marker text := current_setting('opengeni.organization_tenancy_lifecycle', true);
|
|
68
|
+
previous_workspace text := current_setting('opengeni.workspace_id', true);
|
|
69
|
+
BEGIN
|
|
70
|
+
IF p_account_id IS NULL OR p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
|
|
71
|
+
OR p_source IS NULL OR char_length(p_source) NOT BETWEEN 1 AND 200
|
|
72
|
+
OR p_external_id IS NULL OR char_length(p_external_id) NOT BETWEEN 1 AND 1024
|
|
73
|
+
THEN RAISE EXCEPTION 'invalid external identity scope' USING ERRCODE = '42501'; END IF;
|
|
74
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', 'external_identity_provisioning', true);
|
|
75
|
+
-- Serialize a tuple without relying on delimiters in opaque host identifiers.
|
|
76
|
+
PERFORM pg_advisory_xact_lock(hashtextextended(jsonb_build_array(p_account_id,p_source,p_external_id)::text, 0));
|
|
77
|
+
SELECT * INTO identity_row FROM external_identities
|
|
78
|
+
WHERE account_id = p_account_id AND source = p_source AND external_id = p_external_id FOR UPDATE;
|
|
79
|
+
IF NOT FOUND THEN
|
|
80
|
+
identity_id := gen_random_uuid();
|
|
81
|
+
subject := 'external_user:' || identity_id::text;
|
|
82
|
+
PERFORM set_config('opengeni.workspace_id', '', true);
|
|
83
|
+
INSERT INTO workspaces (account_id, name, external_source, external_id)
|
|
84
|
+
VALUES (p_account_id, 'Personal workspace', 'opengeni:organization-membership', p_account_id::text || ':' || subject)
|
|
85
|
+
RETURNING id INTO personal_id;
|
|
86
|
+
PERFORM set_config('opengeni.workspace_id', personal_id::text, true);
|
|
87
|
+
INSERT INTO workspace_inference_controls (workspace_id, account_id) VALUES (personal_id, p_account_id);
|
|
88
|
+
INSERT INTO organization_memberships (account_id, subject_id, role, status, personal_workspace_id)
|
|
89
|
+
VALUES (p_account_id, subject, 'member', 'active', personal_id) RETURNING id INTO membership_id;
|
|
90
|
+
INSERT INTO external_identities (id, account_id, source, external_id, subject_id, organization_membership_id, personal_workspace_id)
|
|
91
|
+
VALUES (identity_id, p_account_id, p_source, p_external_id, subject, membership_id, personal_id)
|
|
92
|
+
RETURNING * INTO identity_row;
|
|
93
|
+
END IF;
|
|
94
|
+
SELECT * INTO membership_row FROM organization_memberships
|
|
95
|
+
WHERE id = identity_row.organization_membership_id AND account_id = p_account_id FOR UPDATE;
|
|
96
|
+
IF NOT FOUND OR identity_row.status <> 'active' OR membership_row.status <> 'active'
|
|
97
|
+
OR membership_row.subject_id IS DISTINCT FROM identity_row.subject_id
|
|
98
|
+
OR membership_row.personal_workspace_id IS DISTINCT FROM identity_row.personal_workspace_id
|
|
99
|
+
THEN RAISE EXCEPTION 'external identity is unavailable' USING ERRCODE = '42501'; END IF;
|
|
100
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', coalesce(previous_marker,''), true);
|
|
101
|
+
PERFORM set_config('opengeni.workspace_id', coalesce(previous_workspace,''), true);
|
|
102
|
+
RETURN jsonb_build_object('id', identity_row.id, 'accountId', identity_row.account_id,
|
|
103
|
+
'source', identity_row.source, 'externalId', identity_row.external_id,
|
|
104
|
+
'subjectId', identity_row.subject_id, 'organizationMembershipId', identity_row.organization_membership_id,
|
|
105
|
+
'personalWorkspaceId', identity_row.personal_workspace_id, 'status', identity_row.status,
|
|
106
|
+
'authorizationRevision', identity_row.authorization_revision);
|
|
107
|
+
EXCEPTION WHEN OTHERS THEN
|
|
108
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', coalesce(previous_marker,''), true);
|
|
109
|
+
PERFORM set_config('opengeni.workspace_id', coalesce(previous_workspace,''), true);
|
|
110
|
+
RAISE;
|
|
111
|
+
END
|
|
112
|
+
$body$;
|
|
113
|
+
REVOKE ALL ON FUNCTION ensure_external_identity(uuid,text,text) FROM PUBLIC;
|
|
114
|
+
DO $acl$
|
|
115
|
+
DECLARE target_role record;
|
|
116
|
+
BEGIN
|
|
117
|
+
FOR target_role IN SELECT DISTINCT role.rolname FROM pg_class relation
|
|
118
|
+
CROSS JOIN LATERAL aclexplode(coalesce(relation.relacl, acldefault('r', relation.relowner))) privilege
|
|
119
|
+
JOIN pg_roles role ON role.oid = privilege.grantee
|
|
120
|
+
WHERE relation.oid = 'external_identities'::regclass AND privilege.grantee <> relation.relowner
|
|
121
|
+
LOOP EXECUTE format('REVOKE ALL ON external_identities FROM %I', target_role.rolname); END LOOP;
|
|
122
|
+
IF EXISTS (SELECT 1 FROM pg_stat_activity a
|
|
123
|
+
JOIN jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) r ON r = a.usename
|
|
124
|
+
WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
|
|
125
|
+
RAISE EXCEPTION '0439 requires stopped application sessions' USING ERRCODE = '55000';
|
|
126
|
+
END IF;
|
|
127
|
+
END
|
|
128
|
+
$acl$;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Extend the existing fenced teardown, never replace its settlement or lock
|
|
3
|
+
-- protocol. Service authority is limited to external members in shared workspaces.
|
|
4
|
+
SET LOCAL lock_timeout = '5s';
|
|
5
|
+
SET LOCAL statement_timeout = '5min';
|
|
6
|
+
|
|
7
|
+
DO $patch$
|
|
8
|
+
DECLARE
|
|
9
|
+
definition text;
|
|
10
|
+
anchor text;
|
|
11
|
+
replacement text;
|
|
12
|
+
BEGIN
|
|
13
|
+
definition := pg_get_functiondef(
|
|
14
|
+
'opengeni_private.assert_workspace_membership_removal_actor(uuid,uuid,text,text)'::regprocedure);
|
|
15
|
+
anchor := ' actor_can_administer boolean;';
|
|
16
|
+
IF (length(definition) - length(replace(definition, anchor, ''))) / length(anchor) <> 1 THEN
|
|
17
|
+
RAISE EXCEPTION '0440 removal declaration drift' USING ERRCODE = '55000';
|
|
18
|
+
END IF;
|
|
19
|
+
definition := replace(definition, anchor, anchor || E'\n service_permissions jsonb;\n service_can_administer boolean := false;');
|
|
20
|
+
anchor := ' SELECT actor_is_organization_administrator OR EXISTS (';
|
|
21
|
+
replacement := $service$
|
|
22
|
+
IF p_actor_subject LIKE 'api_key:%' THEN
|
|
23
|
+
-- The API authenticates the key before supplying this subject. This is a
|
|
24
|
+
-- live consistency fence, not authentication from an arbitrary UUID.
|
|
25
|
+
SELECT credential.permissions INTO service_permissions
|
|
26
|
+
FROM api_keys credential
|
|
27
|
+
WHERE credential.account_id = p_account_id
|
|
28
|
+
AND 'api_key:' || credential.id::text = p_actor_subject
|
|
29
|
+
AND credential.workspace_id IS NULL
|
|
30
|
+
AND credential.credential_kind = 'organization'
|
|
31
|
+
AND credential.revoked_at IS NULL
|
|
32
|
+
AND (credential.expires_at IS NULL OR credential.expires_at > clock_timestamp())
|
|
33
|
+
FOR SHARE;
|
|
34
|
+
service_can_administer := coalesce(
|
|
35
|
+
service_permissions ?| ARRAY['workspace:admin', 'members:manage'], false
|
|
36
|
+
) AND EXISTS (
|
|
37
|
+
SELECT 1 FROM organization_memberships target
|
|
38
|
+
WHERE target.account_id = p_account_id
|
|
39
|
+
AND target.subject_id = p_target_subject
|
|
40
|
+
AND target.subject_id ~ '^external_user:[0-9a-f-]{36}$'
|
|
41
|
+
);
|
|
42
|
+
END IF;
|
|
43
|
+
SELECT actor_is_organization_administrator OR service_can_administer OR EXISTS (
|
|
44
|
+
$service$;
|
|
45
|
+
IF (length(definition) - length(replace(definition, anchor, ''))) / length(anchor) <> 1 THEN
|
|
46
|
+
RAISE EXCEPTION '0440 removal authority drift' USING ERRCODE = '55000';
|
|
47
|
+
END IF;
|
|
48
|
+
EXECUTE replace(definition, anchor, replacement);
|
|
49
|
+
|
|
50
|
+
definition := pg_get_functiondef('workspace_membership_removal_command(jsonb)'::regprocedure);
|
|
51
|
+
anchor := '''human'', actor_subject, ''workspace.membership.remove''';
|
|
52
|
+
IF (length(definition) - length(replace(definition, anchor, ''))) / length(anchor) <> 1
|
|
53
|
+
OR strpos(definition, 'acquire_session_tenancy_fence') = 0 THEN
|
|
54
|
+
RAISE EXCEPTION '0440 removal receipt or fence drift' USING ERRCODE = '55000';
|
|
55
|
+
END IF;
|
|
56
|
+
EXECUTE replace(definition, anchor,
|
|
57
|
+
'CASE WHEN actor_subject LIKE ''api_key:%'' THEN ''service'' ELSE ''human'' END, actor_subject, ''workspace.membership.remove''');
|
|
58
|
+
END
|
|
59
|
+
$patch$;
|