@hasna/loops 0.4.27 → 0.4.29
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/CHANGELOG.md +205 -1
- package/README.md +232 -49
- package/dist/api/index.d.ts +20 -19
- package/dist/api/index.js +7770 -1342
- package/dist/cli/index.js +2976 -966
- package/dist/cli/safe-error-context.d.ts +1 -0
- package/dist/daemon/daemon.d.ts +1 -0
- package/dist/daemon/index.js +1899 -499
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/generated/storage-kit/mode.d.ts +6 -12
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5319 -1096
- package/dist/lib/advancement.d.ts +52 -0
- package/dist/lib/agent-adapter.d.ts +20 -2
- package/dist/lib/auth/route-policy.d.ts +14 -0
- package/dist/lib/auth/tenant-auth.d.ts +38 -0
- package/dist/lib/cloud/mode.d.ts +2 -8
- package/dist/lib/cloud/storage.d.ts +1 -2
- package/dist/lib/cloud/transport.d.ts +4 -18
- package/dist/lib/errors.d.ts +43 -1
- package/dist/lib/executor.d.ts +9 -0
- package/dist/lib/format.d.ts +2 -2
- package/dist/lib/goal/runner.d.ts +36 -3
- package/dist/lib/health.d.ts +1 -1
- package/dist/lib/labels.d.ts +4 -0
- package/dist/lib/loop-status.d.ts +4 -0
- package/dist/lib/migration.d.ts +70 -17
- package/dist/lib/mode.d.ts +2 -5
- package/dist/lib/mode.js +28 -37
- package/dist/lib/route/fields.d.ts +8 -0
- package/dist/lib/route/index.d.ts +2 -2
- package/dist/lib/route/throttle.d.ts +7 -0
- package/dist/lib/route/types.d.ts +3 -0
- package/dist/lib/run-completion.d.ts +18 -0
- package/dist/lib/scheduler.d.ts +5 -11
- package/dist/lib/storage/contract.d.ts +15 -1
- package/dist/lib/storage/index.d.ts +1 -1
- package/dist/lib/storage/index.js +4083 -486
- package/dist/lib/storage/pg-executor.d.ts +10 -5
- package/dist/lib/storage/postgres-loop-storage.d.ts +52 -26
- package/dist/lib/storage/postgres-schema.d.ts +8 -0
- package/dist/lib/storage/postgres-schema.js +1326 -1
- package/dist/lib/storage/postgres.d.ts +3 -1
- package/dist/lib/storage/postgres.js +1356 -27
- package/dist/lib/storage/provider-credentials.d.ts +84 -0
- package/dist/lib/storage/shared-database-transfer.d.ts +136 -0
- package/dist/lib/storage/sqlite.d.ts +15 -2
- package/dist/lib/storage/sqlite.js +1379 -217
- package/dist/lib/storage/tenant-backfill-s3.d.ts +53 -0
- package/dist/lib/storage/tenant-backfill.d.ts +40 -0
- package/dist/lib/store/index.d.ts +14 -8
- package/dist/lib/store.d.ts +129 -7
- package/dist/lib/store.js +1352 -218
- package/dist/lib/templates.d.ts +11 -0
- package/dist/lib/workflow-events.d.ts +6 -0
- package/dist/lib/workflow-provenance.d.ts +8 -0
- package/dist/lib/workflow-runner.d.ts +52 -4
- package/dist/mcp/index.js +2074 -657
- package/dist/runner/index.d.ts +20 -2
- package/dist/runner/index.js +2146 -183
- package/dist/sdk/http.d.ts +364 -4
- package/dist/sdk/http.js +379 -1
- package/dist/sdk/index.d.ts +7 -2
- package/dist/sdk/index.js +2341 -742
- package/dist/serve/index.d.ts +14 -0
- package/dist/serve/index.js +13269 -1830
- package/dist/types.d.ts +75 -3
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +32 -32
- package/docs/CUTOVER-RUNBOOK.md +158 -31
- package/docs/DEPLOYMENT_MODES.md +84 -56
- package/docs/RUNTIME_BOUNDARY.md +26 -26
- package/docs/SHARED-DATABASE-TRANSFER.md +95 -0
- package/docs/SHARED_KIT_EXTRACTION_INVENTORY.md +631 -0
- package/docs/TRANSCRIPT_LOOP_PATTERNS.md +3 -3
- package/docs/UNIFIED_PRODUCT_CONTRACT.md +365 -0
- package/docs/USAGE.md +150 -56
- package/docs/workflows/transcript-feedback-to-loops.json +2 -2
- package/package.json +8 -4
- package/dist/lib/storage/pg-runner-claim.d.ts +0 -40
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// src/lib/storage/postgres-schema.ts
|
|
3
3
|
import { createHash } from "crypto";
|
|
4
4
|
var POSTGRES_MIGRATION_LEDGER_TABLE = "open_loops_schema_migrations";
|
|
5
|
+
var POSTGRES_MIGRATION_ADVISORY_LOCK_SQL = "SELECT pg_advisory_xact_lock(1330466384, 1280262987)";
|
|
5
6
|
function checksumStorageSql(sql) {
|
|
6
7
|
const normalized = sql.trim().replace(/\r\n/g, `
|
|
7
8
|
`);
|
|
@@ -10,6 +11,283 @@ function checksumStorageSql(sql) {
|
|
|
10
11
|
function migration(id, sql) {
|
|
11
12
|
return Object.freeze({ id, sql: sql.trim(), checksum: checksumStorageSql(sql) });
|
|
12
13
|
}
|
|
14
|
+
var POSTGRES_TENANT_BOOTSTRAP_ROLES_SQL = `
|
|
15
|
+
DO $bootstrap_roles$
|
|
16
|
+
DECLARE role_name TEXT;
|
|
17
|
+
BEGIN
|
|
18
|
+
FOREACH role_name IN ARRAY ARRAY[
|
|
19
|
+
'open_loops_owner', 'open_loops_migrator',
|
|
20
|
+
'open_loops_runtime', 'open_loops_authenticator'
|
|
21
|
+
] LOOP
|
|
22
|
+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname=role_name) THEN
|
|
23
|
+
EXECUTE format(
|
|
24
|
+
'CREATE ROLE %I INHERIT NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS',
|
|
25
|
+
role_name
|
|
26
|
+
);
|
|
27
|
+
ELSIF EXISTS (
|
|
28
|
+
SELECT 1 FROM pg_roles WHERE rolname=role_name AND rolcanlogin
|
|
29
|
+
) THEN
|
|
30
|
+
RAISE EXCEPTION 'reserved OpenLoops database role % is LOGIN; detach or replace that credential with provider authority before tenant enforcement',
|
|
31
|
+
role_name;
|
|
32
|
+
ELSIF EXISTS (
|
|
33
|
+
SELECT 1 FROM pg_roles
|
|
34
|
+
WHERE rolname=role_name
|
|
35
|
+
AND (NOT rolinherit OR rolsuper OR rolcreatedb OR
|
|
36
|
+
rolcreaterole OR rolreplication OR rolbypassrls)
|
|
37
|
+
) THEN
|
|
38
|
+
EXECUTE format(
|
|
39
|
+
'ALTER ROLE %I INHERIT NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS',
|
|
40
|
+
role_name
|
|
41
|
+
);
|
|
42
|
+
END IF;
|
|
43
|
+
END LOOP;
|
|
44
|
+
END
|
|
45
|
+
$bootstrap_roles$;
|
|
46
|
+
`.trim();
|
|
47
|
+
var POSTGRES_TENANT_CLUSTER_ROLE_EXCLUSIVITY_SQL = `
|
|
48
|
+
DO $cluster_role_exclusivity$
|
|
49
|
+
DECLARE cross_database_dependency RECORD;
|
|
50
|
+
BEGIN
|
|
51
|
+
SELECT role.rolname AS role_name,
|
|
52
|
+
COALESCE(database.datname, dependency.dbid::text) AS database_name
|
|
53
|
+
INTO cross_database_dependency
|
|
54
|
+
FROM pg_shdepend dependency
|
|
55
|
+
JOIN pg_roles role
|
|
56
|
+
ON role.oid=dependency.refobjid
|
|
57
|
+
AND dependency.refclassid='pg_authid'::regclass
|
|
58
|
+
LEFT JOIN pg_database database ON database.oid=dependency.dbid
|
|
59
|
+
WHERE role.rolname IN (
|
|
60
|
+
'open_loops_owner', 'open_loops_migrator',
|
|
61
|
+
'open_loops_runtime', 'open_loops_authenticator'
|
|
62
|
+
)
|
|
63
|
+
AND dependency.dbid NOT IN (
|
|
64
|
+
0,
|
|
65
|
+
(SELECT oid FROM pg_database WHERE datname=current_database())
|
|
66
|
+
)
|
|
67
|
+
LIMIT 1;
|
|
68
|
+
IF FOUND THEN
|
|
69
|
+
RAISE EXCEPTION 'reserved OpenLoops role % has a dependency in database %; use a dedicated cluster or remove the cross-database dependency before tenant enforcement',
|
|
70
|
+
cross_database_dependency.role_name,
|
|
71
|
+
cross_database_dependency.database_name;
|
|
72
|
+
END IF;
|
|
73
|
+
|
|
74
|
+
SELECT role.rolname AS role_name, database.datname AS database_name
|
|
75
|
+
INTO cross_database_dependency
|
|
76
|
+
FROM pg_database database
|
|
77
|
+
JOIN pg_roles role ON role.oid=database.datdba
|
|
78
|
+
WHERE role.rolname IN (
|
|
79
|
+
'open_loops_owner', 'open_loops_migrator',
|
|
80
|
+
'open_loops_runtime', 'open_loops_authenticator'
|
|
81
|
+
)
|
|
82
|
+
AND database.datname<>current_database()
|
|
83
|
+
LIMIT 1;
|
|
84
|
+
IF FOUND THEN
|
|
85
|
+
RAISE EXCEPTION 'reserved OpenLoops role % owns database %; role names must be exclusive to the dedicated OpenLoops cluster',
|
|
86
|
+
cross_database_dependency.role_name,
|
|
87
|
+
cross_database_dependency.database_name;
|
|
88
|
+
END IF;
|
|
89
|
+
END
|
|
90
|
+
$cluster_role_exclusivity$;
|
|
91
|
+
`.trim();
|
|
92
|
+
var POSTGRES_TENANT_BOOTSTRAP_MEMBERSHIPS_SQL = `
|
|
93
|
+
DO $bootstrap_memberships$
|
|
94
|
+
DECLARE bootstrap_name NAME := session_user;
|
|
95
|
+
DECLARE bootstrap_superuser BOOLEAN;
|
|
96
|
+
DECLARE relevant_membership_count BIGINT;
|
|
97
|
+
DECLARE safe_privileged_role_count BIGINT;
|
|
98
|
+
BEGIN
|
|
99
|
+
IF bootstrap_name IN (
|
|
100
|
+
'open_loops_owner', 'open_loops_migrator',
|
|
101
|
+
'open_loops_runtime', 'open_loops_authenticator'
|
|
102
|
+
) THEN
|
|
103
|
+
RAISE EXCEPTION 'tenant enforcement bootstrap login must be distinct from OpenLoops database roles';
|
|
104
|
+
END IF;
|
|
105
|
+
SELECT rolsuper INTO bootstrap_superuser FROM pg_roles WHERE rolname=bootstrap_name;
|
|
106
|
+
|
|
107
|
+
-- PostgreSQL 16 grants CREATEROLE creators membership in newly created roles,
|
|
108
|
+
-- but createrole_self_grant defaults to empty: ADMIN can be true while
|
|
109
|
+
-- INHERIT and SET are false. Only a superuser can revoke the implicit row,
|
|
110
|
+
-- whose grantor is PostgreSQL's bootstrap superuser. A non-superuser runner
|
|
111
|
+
-- must therefore receive exact memberships during provider bootstrap.
|
|
112
|
+
IF bootstrap_superuser THEN
|
|
113
|
+
EXECUTE format(
|
|
114
|
+
'REVOKE open_loops_owner, open_loops_migrator, open_loops_runtime, open_loops_authenticator FROM %I',
|
|
115
|
+
bootstrap_name
|
|
116
|
+
);
|
|
117
|
+
EXECUTE format(
|
|
118
|
+
'GRANT open_loops_owner, open_loops_migrator TO %I WITH ADMIN FALSE, INHERIT TRUE, SET TRUE',
|
|
119
|
+
bootstrap_name
|
|
120
|
+
);
|
|
121
|
+
END IF;
|
|
122
|
+
|
|
123
|
+
SELECT count(*),
|
|
124
|
+
count(DISTINCT granted.rolname) FILTER (
|
|
125
|
+
WHERE granted.rolname IN ('open_loops_owner', 'open_loops_migrator')
|
|
126
|
+
AND NOT membership.admin_option
|
|
127
|
+
AND membership.inherit_option
|
|
128
|
+
AND membership.set_option
|
|
129
|
+
)
|
|
130
|
+
INTO relevant_membership_count, safe_privileged_role_count
|
|
131
|
+
FROM pg_auth_members membership
|
|
132
|
+
JOIN pg_roles granted ON granted.oid=membership.roleid
|
|
133
|
+
JOIN pg_roles member ON member.oid=membership.member
|
|
134
|
+
WHERE member.rolname=bootstrap_name
|
|
135
|
+
AND granted.rolname IN (
|
|
136
|
+
'open_loops_owner', 'open_loops_migrator',
|
|
137
|
+
'open_loops_runtime', 'open_loops_authenticator'
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
IF NOT bootstrap_superuser AND (
|
|
141
|
+
pg_has_role(bootstrap_name, 'open_loops_runtime', 'MEMBER') OR
|
|
142
|
+
pg_has_role(bootstrap_name, 'open_loops_authenticator', 'MEMBER') OR
|
|
143
|
+
relevant_membership_count <> 2 OR safe_privileged_role_count <> 2
|
|
144
|
+
) THEN
|
|
145
|
+
RAISE EXCEPTION 'non-superuser tenant enforcement requires pre-provisioned owner/migrator memberships with ADMIN FALSE, INHERIT TRUE, SET TRUE and no runtime/authenticator membership';
|
|
146
|
+
END IF;
|
|
147
|
+
IF NOT bootstrap_superuser AND EXISTS (
|
|
148
|
+
SELECT 1
|
|
149
|
+
FROM pg_roles service_login
|
|
150
|
+
WHERE service_login.rolcanlogin
|
|
151
|
+
AND NOT service_login.rolsuper
|
|
152
|
+
AND service_login.rolname<>bootstrap_name
|
|
153
|
+
AND (
|
|
154
|
+
pg_has_role(service_login.oid, 'open_loops_runtime', 'MEMBER') OR
|
|
155
|
+
pg_has_role(service_login.oid, 'open_loops_authenticator', 'MEMBER')
|
|
156
|
+
)
|
|
157
|
+
) THEN
|
|
158
|
+
RAISE EXCEPTION 'non-superuser tenant enforcement must run before runtime/authenticator service login memberships are provisioned';
|
|
159
|
+
END IF;
|
|
160
|
+
IF bootstrap_superuser AND (
|
|
161
|
+
relevant_membership_count <> 2 OR safe_privileged_role_count <> 2
|
|
162
|
+
) THEN
|
|
163
|
+
RAISE EXCEPTION 'tenant enforcement did not normalize bootstrap role memberships';
|
|
164
|
+
END IF;
|
|
165
|
+
END
|
|
166
|
+
$bootstrap_memberships$;
|
|
167
|
+
`.trim();
|
|
168
|
+
var POSTGRES_TENANT_PRIVILEGED_MEMBERSHIPS_SQL = `
|
|
169
|
+
DO $privileged_role_membership_acl$
|
|
170
|
+
DECLARE unsafe_membership RECORD;
|
|
171
|
+
BEGIN
|
|
172
|
+
SELECT granted.rolname AS granted_role, member.rolname AS member_role
|
|
173
|
+
INTO unsafe_membership
|
|
174
|
+
FROM pg_auth_members membership
|
|
175
|
+
JOIN pg_roles granted ON granted.oid=membership.roleid
|
|
176
|
+
JOIN pg_roles member ON member.oid=membership.member
|
|
177
|
+
WHERE (granted.rolname IN ('open_loops_owner', 'open_loops_migrator')
|
|
178
|
+
OR member.rolname IN (
|
|
179
|
+
'open_loops_owner', 'open_loops_migrator',
|
|
180
|
+
'open_loops_runtime', 'open_loops_authenticator'
|
|
181
|
+
))
|
|
182
|
+
AND NOT (
|
|
183
|
+
granted.rolname IN ('open_loops_owner', 'open_loops_migrator')
|
|
184
|
+
AND member.rolname=session_user
|
|
185
|
+
AND NOT membership.admin_option
|
|
186
|
+
AND membership.inherit_option
|
|
187
|
+
AND membership.set_option
|
|
188
|
+
)
|
|
189
|
+
LIMIT 1;
|
|
190
|
+
IF FOUND THEN
|
|
191
|
+
RAISE EXCEPTION 'unsafe privileged role membership % to %; remove it with the original grantor or provider authority before tenant enforcement',
|
|
192
|
+
unsafe_membership.granted_role,
|
|
193
|
+
unsafe_membership.member_role;
|
|
194
|
+
END IF;
|
|
195
|
+
END
|
|
196
|
+
$privileged_role_membership_acl$;
|
|
197
|
+
`.trim();
|
|
198
|
+
var POSTGRES_TENANT_SERVICE_ROLE_MEMBERSHIPS_SQL = `
|
|
199
|
+
DO $service_role_memberships$
|
|
200
|
+
DECLARE membership RECORD;
|
|
201
|
+
BEGIN
|
|
202
|
+
FOR membership IN
|
|
203
|
+
SELECT granted.rolname AS granted_role, member.rolname AS member_role
|
|
204
|
+
FROM pg_auth_members relation
|
|
205
|
+
JOIN pg_roles granted ON granted.oid=relation.roleid
|
|
206
|
+
JOIN pg_roles member ON member.oid=relation.member
|
|
207
|
+
WHERE member.rolname IN ('open_loops_runtime', 'open_loops_authenticator')
|
|
208
|
+
LOOP
|
|
209
|
+
EXECUTE format('REVOKE %I FROM %I', membership.granted_role, membership.member_role);
|
|
210
|
+
END LOOP;
|
|
211
|
+
END
|
|
212
|
+
$service_role_memberships$;
|
|
213
|
+
`.trim();
|
|
214
|
+
var POSTGRES_TENANT_UNSAFE_SERVICE_MEMBERSHIPS_SQL = `
|
|
215
|
+
DO $unsafe_service_members$
|
|
216
|
+
DECLARE unsafe_member RECORD;
|
|
217
|
+
BEGIN
|
|
218
|
+
FOR unsafe_member IN
|
|
219
|
+
SELECT granted.rolname AS granted_role, member.rolname AS member_role,
|
|
220
|
+
member.rolcanlogin AS member_can_login
|
|
221
|
+
FROM pg_auth_members membership
|
|
222
|
+
JOIN pg_roles granted ON granted.oid = membership.roleid
|
|
223
|
+
JOIN pg_roles member ON member.oid = membership.member
|
|
224
|
+
WHERE granted.rolname IN ('open_loops_runtime', 'open_loops_authenticator')
|
|
225
|
+
AND (membership.admin_option OR NOT membership.inherit_option OR NOT membership.set_option OR
|
|
226
|
+
NOT member.rolcanlogin OR NOT member.rolinherit OR
|
|
227
|
+
member.rolsuper OR member.rolbypassrls OR member.rolcreatedb OR
|
|
228
|
+
member.rolcreaterole OR member.rolreplication OR
|
|
229
|
+
pg_has_role(member.oid, 'open_loops_owner', 'MEMBER') OR
|
|
230
|
+
pg_has_role(member.oid, 'open_loops_migrator', 'MEMBER') OR
|
|
231
|
+
EXISTS (
|
|
232
|
+
SELECT 1 FROM pg_auth_members other_membership
|
|
233
|
+
WHERE other_membership.member = member.oid
|
|
234
|
+
AND other_membership.roleid <> granted.oid
|
|
235
|
+
) OR
|
|
236
|
+
EXISTS (
|
|
237
|
+
SELECT 1 FROM pg_auth_members downstream_membership
|
|
238
|
+
WHERE downstream_membership.roleid = member.oid
|
|
239
|
+
) OR
|
|
240
|
+
(granted.rolname = 'open_loops_runtime' AND
|
|
241
|
+
pg_has_role(member.oid, 'open_loops_authenticator', 'MEMBER')) OR
|
|
242
|
+
(granted.rolname = 'open_loops_authenticator' AND
|
|
243
|
+
pg_has_role(member.oid, 'open_loops_runtime', 'MEMBER')))
|
|
244
|
+
LOOP
|
|
245
|
+
IF unsafe_member.member_can_login THEN
|
|
246
|
+
RAISE EXCEPTION 'unsafe service login membership for % in %; normalize it with provider authority before tenant enforcement',
|
|
247
|
+
unsafe_member.member_role,
|
|
248
|
+
unsafe_member.granted_role;
|
|
249
|
+
END IF;
|
|
250
|
+
EXECUTE format('REVOKE %I FROM %I', unsafe_member.granted_role, unsafe_member.member_role);
|
|
251
|
+
END LOOP;
|
|
252
|
+
END
|
|
253
|
+
$unsafe_service_members$;
|
|
254
|
+
`.trim();
|
|
255
|
+
var POSTGRES_TENANT_SERVICE_MEMBER_CLEANUP_SQL = `
|
|
256
|
+
DO $service_member_acl$
|
|
257
|
+
DECLARE service_member RECORD;
|
|
258
|
+
BEGIN
|
|
259
|
+
FOR service_member IN
|
|
260
|
+
SELECT DISTINCT member.oid AS member_oid, member.rolname AS member_role
|
|
261
|
+
FROM pg_auth_members membership
|
|
262
|
+
JOIN pg_roles granted ON granted.oid = membership.roleid
|
|
263
|
+
JOIN pg_roles member ON member.oid = membership.member
|
|
264
|
+
WHERE granted.rolname IN ('open_loops_runtime', 'open_loops_authenticator')
|
|
265
|
+
AND member.rolcanlogin
|
|
266
|
+
LOOP
|
|
267
|
+
IF service_member.member_oid IN (
|
|
268
|
+
(SELECT oid FROM pg_roles WHERE rolname=session_user),
|
|
269
|
+
(SELECT datdba FROM pg_database WHERE datname=current_database()),
|
|
270
|
+
(SELECT nspowner FROM pg_namespace WHERE nspname='public')
|
|
271
|
+
) THEN
|
|
272
|
+
RAISE EXCEPTION 'provider/bootstrap login % must never be processed as a service login',
|
|
273
|
+
service_member.member_role;
|
|
274
|
+
END IF;
|
|
275
|
+
IF EXISTS (
|
|
276
|
+
SELECT 1
|
|
277
|
+
FROM pg_shdepend dependency
|
|
278
|
+
WHERE dependency.refclassid = 'pg_authid'::regclass
|
|
279
|
+
AND dependency.refobjid = service_member.member_oid
|
|
280
|
+
AND dependency.deptype = 'o'
|
|
281
|
+
AND dependency.dbid IN (0, (SELECT oid FROM pg_database WHERE datname = current_database()))
|
|
282
|
+
) THEN
|
|
283
|
+
RAISE EXCEPTION 'service login % owns database objects; reassign or remove them before tenant enforcement',
|
|
284
|
+
service_member.member_role;
|
|
285
|
+
END IF;
|
|
286
|
+
EXECUTE format('DROP OWNED BY %I', service_member.member_role);
|
|
287
|
+
END LOOP;
|
|
288
|
+
END
|
|
289
|
+
$service_member_acl$;
|
|
290
|
+
`.trim();
|
|
13
291
|
var POSTGRES_STORAGE_MIGRATIONS = Object.freeze([
|
|
14
292
|
migration("0001_core_runtime", `
|
|
15
293
|
CREATE TABLE IF NOT EXISTS loops (
|
|
@@ -352,6 +630,1045 @@ CREATE INDEX IF NOT EXISTS idx_run_receipts_knowledge_ids ON run_receipts USING
|
|
|
352
630
|
migration("0006_work_item_machine_id", `
|
|
353
631
|
ALTER TABLE workflow_work_items ADD COLUMN IF NOT EXISTS machine_id TEXT;
|
|
354
632
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_machine ON workflow_work_items(machine_id, status);
|
|
633
|
+
`),
|
|
634
|
+
migration("0007_work_item_gate_deaths", `
|
|
635
|
+
ALTER TABLE workflow_work_items ADD COLUMN IF NOT EXISTS gate_deaths INTEGER NOT NULL DEFAULT 0;
|
|
636
|
+
`),
|
|
637
|
+
migration("0011_workflow_run_provenance", `
|
|
638
|
+
ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS workflow_definition_hash TEXT;
|
|
639
|
+
`),
|
|
640
|
+
migration("0012_loop_labels", `
|
|
641
|
+
ALTER TABLE loops ADD COLUMN IF NOT EXISTS labels_json JSONB NOT NULL DEFAULT '[]'::jsonb;
|
|
642
|
+
`),
|
|
643
|
+
migration("0008_tenant_prepare", `
|
|
644
|
+
CREATE TABLE tenants (
|
|
645
|
+
id TEXT PRIMARY KEY,
|
|
646
|
+
slug TEXT NOT NULL UNIQUE,
|
|
647
|
+
name TEXT NOT NULL,
|
|
648
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'suspended')),
|
|
649
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
650
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
CREATE TABLE principals (
|
|
654
|
+
id TEXT PRIMARY KEY,
|
|
655
|
+
kind TEXT NOT NULL CHECK (kind IN ('human', 'service', 'machine')),
|
|
656
|
+
display_name TEXT NOT NULL,
|
|
657
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'suspended')),
|
|
658
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
659
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
CREATE TABLE tenant_memberships (
|
|
663
|
+
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
664
|
+
principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE CASCADE,
|
|
665
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'suspended')),
|
|
666
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
667
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
668
|
+
PRIMARY KEY (tenant_id, principal_id)
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
CREATE TABLE tenant_roles (
|
|
672
|
+
name TEXT PRIMARY KEY CHECK (name IN ('admin', 'operator', 'member', 'readonly', 'service', 'worker'))
|
|
673
|
+
);
|
|
674
|
+
INSERT INTO tenant_roles(name) VALUES ('admin'), ('operator'), ('member'), ('readonly'), ('service'), ('worker');
|
|
675
|
+
|
|
676
|
+
CREATE TABLE tenant_membership_roles (
|
|
677
|
+
tenant_id TEXT NOT NULL,
|
|
678
|
+
principal_id TEXT NOT NULL,
|
|
679
|
+
role TEXT NOT NULL REFERENCES tenant_roles(name),
|
|
680
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
681
|
+
PRIMARY KEY (tenant_id, principal_id, role),
|
|
682
|
+
FOREIGN KEY (tenant_id, principal_id) REFERENCES tenant_memberships(tenant_id, principal_id) ON DELETE CASCADE
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
686
|
+
kid TEXT PRIMARY KEY,
|
|
687
|
+
app TEXT NOT NULL,
|
|
688
|
+
agent TEXT,
|
|
689
|
+
scopes JSONB NOT NULL,
|
|
690
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
691
|
+
issued_at TIMESTAMPTZ NOT NULL,
|
|
692
|
+
expires_at TIMESTAMPTZ,
|
|
693
|
+
revoked_at TIMESTAMPTZ,
|
|
694
|
+
revoked_reason TEXT,
|
|
695
|
+
last_used_at TIMESTAMPTZ,
|
|
696
|
+
created_by TEXT,
|
|
697
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
698
|
+
);
|
|
699
|
+
ALTER TABLE api_keys ADD COLUMN tenant_id TEXT;
|
|
700
|
+
ALTER TABLE api_keys ADD COLUMN principal_id TEXT;
|
|
701
|
+
ALTER TABLE api_keys ADD COLUMN token_kind TEXT;
|
|
702
|
+
ALTER TABLE api_keys ADD COLUMN disabled_at TIMESTAMPTZ;
|
|
703
|
+
|
|
704
|
+
ALTER TABLE loops ADD COLUMN tenant_id TEXT;
|
|
705
|
+
ALTER TABLE loop_runs ADD COLUMN tenant_id TEXT;
|
|
706
|
+
ALTER TABLE daemon_lease ADD COLUMN tenant_id TEXT;
|
|
707
|
+
ALTER TABLE workflow_specs ADD COLUMN tenant_id TEXT;
|
|
708
|
+
ALTER TABLE workflow_runs ADD COLUMN tenant_id TEXT;
|
|
709
|
+
ALTER TABLE workflow_invocations ADD COLUMN tenant_id TEXT;
|
|
710
|
+
ALTER TABLE workflow_work_items ADD COLUMN tenant_id TEXT;
|
|
711
|
+
ALTER TABLE workflow_step_runs ADD COLUMN tenant_id TEXT;
|
|
712
|
+
ALTER TABLE workflow_events ADD COLUMN tenant_id TEXT;
|
|
713
|
+
ALTER TABLE goals ADD COLUMN tenant_id TEXT;
|
|
714
|
+
ALTER TABLE goal_plan_nodes ADD COLUMN tenant_id TEXT;
|
|
715
|
+
ALTER TABLE goal_runs ADD COLUMN tenant_id TEXT;
|
|
716
|
+
ALTER TABLE runner_machines ADD COLUMN tenant_id TEXT;
|
|
717
|
+
ALTER TABLE runner_leases ADD COLUMN tenant_id TEXT;
|
|
718
|
+
ALTER TABLE audit_events ADD COLUMN tenant_id TEXT;
|
|
719
|
+
ALTER TABLE audit_events ADD COLUMN principal_id TEXT;
|
|
720
|
+
ALTER TABLE audit_events ADD COLUMN request_id TEXT;
|
|
721
|
+
ALTER TABLE audit_events ADD COLUMN operation_id TEXT;
|
|
722
|
+
ALTER TABLE audit_events ADD COLUMN decision TEXT;
|
|
723
|
+
ALTER TABLE audit_events ADD COLUMN deny_reason TEXT;
|
|
724
|
+
ALTER TABLE run_receipts ADD COLUMN tenant_id TEXT;
|
|
725
|
+
|
|
726
|
+
CREATE TABLE tenant_row_assignments (
|
|
727
|
+
table_name TEXT NOT NULL,
|
|
728
|
+
row_id TEXT NOT NULL,
|
|
729
|
+
tenant_id TEXT NOT NULL REFERENCES tenants(id),
|
|
730
|
+
PRIMARY KEY (table_name, row_id),
|
|
731
|
+
CHECK (table_name IN (
|
|
732
|
+
'loops', 'loop_runs', 'daemon_lease', 'workflow_specs', 'workflow_runs',
|
|
733
|
+
'workflow_invocations', 'workflow_work_items', 'workflow_step_runs', 'workflow_events',
|
|
734
|
+
'goals', 'goal_plan_nodes', 'goal_runs', 'runner_machines', 'runner_leases',
|
|
735
|
+
'audit_events', 'run_receipts'
|
|
736
|
+
))
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
CREATE TABLE api_key_tenant_bindings (
|
|
740
|
+
kid TEXT PRIMARY KEY REFERENCES api_keys(kid) ON DELETE CASCADE,
|
|
741
|
+
tenant_id TEXT NOT NULL,
|
|
742
|
+
principal_id TEXT NOT NULL,
|
|
743
|
+
token_kind TEXT NOT NULL CHECK (token_kind IN ('api_key', 'service', 'machine')),
|
|
744
|
+
FOREIGN KEY (tenant_id, principal_id) REFERENCES tenant_memberships(tenant_id, principal_id)
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
CREATE TABLE preauth_audit_events (
|
|
748
|
+
id TEXT PRIMARY KEY,
|
|
749
|
+
request_id TEXT NOT NULL,
|
|
750
|
+
operation_id TEXT NOT NULL,
|
|
751
|
+
deny_reason TEXT NOT NULL,
|
|
752
|
+
metadata_json JSONB NOT NULL,
|
|
753
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
754
|
+
);
|
|
755
|
+
`),
|
|
756
|
+
migration("0009_tenant_backfill", `
|
|
757
|
+
DO $tenant_backfill$
|
|
758
|
+
DECLARE
|
|
759
|
+
target_table TEXT;
|
|
760
|
+
id_column TEXT;
|
|
761
|
+
missing_count BIGINT;
|
|
762
|
+
mapping_count BIGINT;
|
|
763
|
+
BEGIN
|
|
764
|
+
FOR target_table, id_column IN
|
|
765
|
+
SELECT * FROM (VALUES
|
|
766
|
+
('loops', 'id'), ('loop_runs', 'id'), ('daemon_lease', 'id'),
|
|
767
|
+
('workflow_specs', 'id'), ('workflow_runs', 'id'), ('workflow_invocations', 'id'),
|
|
768
|
+
('workflow_work_items', 'id'), ('workflow_step_runs', 'id'), ('workflow_events', 'id'),
|
|
769
|
+
('goals', 'id'), ('goal_plan_nodes', 'id'), ('goal_runs', 'id'),
|
|
770
|
+
('runner_machines', 'id'), ('runner_leases', 'id'), ('audit_events', 'id'),
|
|
771
|
+
('run_receipts', 'run_id')
|
|
772
|
+
) AS targets(table_name, row_id_column)
|
|
773
|
+
LOOP
|
|
774
|
+
EXECUTE format(
|
|
775
|
+
'UPDATE %I target SET tenant_id = assignment.tenant_id FROM tenant_row_assignments assignment WHERE assignment.table_name = $1 AND assignment.row_id = target.%I',
|
|
776
|
+
target_table, id_column
|
|
777
|
+
) USING target_table;
|
|
778
|
+
EXECUTE format('SELECT count(*) FROM %I WHERE tenant_id IS NULL', target_table) INTO missing_count;
|
|
779
|
+
IF missing_count > 0 THEN
|
|
780
|
+
RAISE EXCEPTION 'tenant backfill incomplete for %: % rows have no explicit assignment', target_table, missing_count;
|
|
781
|
+
END IF;
|
|
782
|
+
EXECUTE format('SELECT count(*) FROM tenant_row_assignments WHERE table_name = $1') INTO mapping_count USING target_table;
|
|
783
|
+
EXECUTE format('SELECT count(*) FROM %I', target_table) INTO missing_count;
|
|
784
|
+
IF mapping_count <> missing_count THEN
|
|
785
|
+
RAISE EXCEPTION 'tenant assignment cardinality mismatch for %: mappings %, rows %', target_table, mapping_count, missing_count;
|
|
786
|
+
END IF;
|
|
787
|
+
END LOOP;
|
|
788
|
+
|
|
789
|
+
UPDATE api_keys key
|
|
790
|
+
SET tenant_id = binding.tenant_id,
|
|
791
|
+
principal_id = binding.principal_id,
|
|
792
|
+
token_kind = binding.token_kind
|
|
793
|
+
FROM api_key_tenant_bindings binding
|
|
794
|
+
WHERE binding.kid = key.kid;
|
|
795
|
+
SELECT count(*) INTO missing_count
|
|
796
|
+
FROM api_keys
|
|
797
|
+
WHERE tenant_id IS NULL OR principal_id IS NULL OR token_kind IS NULL
|
|
798
|
+
OR agent IS DISTINCT FROM principal_id;
|
|
799
|
+
IF missing_count > 0 THEN
|
|
800
|
+
RAISE EXCEPTION 'api key tenant backfill incomplete: % keys have no exact principal binding', missing_count;
|
|
801
|
+
END IF;
|
|
802
|
+
|
|
803
|
+
IF EXISTS (
|
|
804
|
+
SELECT 1 FROM loop_runs child JOIN loops parent ON parent.id = child.loop_id WHERE child.tenant_id <> parent.tenant_id
|
|
805
|
+
UNION ALL SELECT 1 FROM workflow_runs child JOIN workflow_specs parent ON parent.id = child.workflow_id WHERE child.tenant_id <> parent.tenant_id
|
|
806
|
+
UNION ALL SELECT 1 FROM workflow_work_items child JOIN workflow_invocations parent ON parent.id = child.invocation_id WHERE child.tenant_id <> parent.tenant_id
|
|
807
|
+
UNION ALL SELECT 1 FROM workflow_step_runs child JOIN workflow_runs parent ON parent.id = child.workflow_run_id WHERE child.tenant_id <> parent.tenant_id
|
|
808
|
+
UNION ALL SELECT 1 FROM workflow_events child JOIN workflow_runs parent ON parent.id = child.workflow_run_id WHERE child.tenant_id <> parent.tenant_id
|
|
809
|
+
UNION ALL SELECT 1 FROM goal_plan_nodes child JOIN goals parent ON parent.id = child.goal_id WHERE child.tenant_id <> parent.tenant_id
|
|
810
|
+
UNION ALL SELECT 1 FROM goal_runs child JOIN goals parent ON parent.id = child.goal_id WHERE child.tenant_id <> parent.tenant_id
|
|
811
|
+
UNION ALL SELECT 1 FROM runner_leases child JOIN runner_machines parent ON parent.id = child.runner_id WHERE child.tenant_id <> parent.tenant_id
|
|
812
|
+
) THEN
|
|
813
|
+
RAISE EXCEPTION 'tenant backfill contains cross-tenant parent/child relationships';
|
|
814
|
+
END IF;
|
|
815
|
+
END
|
|
816
|
+
$tenant_backfill$;
|
|
817
|
+
`),
|
|
818
|
+
migration("0010_tenant_enforce", `
|
|
819
|
+
${POSTGRES_TENANT_BOOTSTRAP_ROLES_SQL}
|
|
820
|
+
|
|
821
|
+
${POSTGRES_TENANT_CLUSTER_ROLE_EXCLUSIVITY_SQL}
|
|
822
|
+
|
|
823
|
+
${POSTGRES_TENANT_BOOTSTRAP_MEMBERSHIPS_SQL}
|
|
824
|
+
|
|
825
|
+
${POSTGRES_TENANT_SERVICE_ROLE_MEMBERSHIPS_SQL}
|
|
826
|
+
|
|
827
|
+
${POSTGRES_TENANT_PRIVILEGED_MEMBERSHIPS_SQL}
|
|
828
|
+
|
|
829
|
+
${POSTGRES_TENANT_UNSAFE_SERVICE_MEMBERSHIPS_SQL}
|
|
830
|
+
|
|
831
|
+
${POSTGRES_TENANT_SERVICE_MEMBER_CLEANUP_SQL}
|
|
832
|
+
|
|
833
|
+
DO $service_role_acl$
|
|
834
|
+
DECLARE namespace RECORD;
|
|
835
|
+
DECLARE database_grantee RECORD;
|
|
836
|
+
DECLARE object_grantee RECORD;
|
|
837
|
+
BEGIN
|
|
838
|
+
FOR database_grantee IN
|
|
839
|
+
SELECT DISTINCT grantee.rolname AS grantee_role
|
|
840
|
+
FROM pg_database database
|
|
841
|
+
CROSS JOIN LATERAL aclexplode(COALESCE(database.datacl, acldefault('d', database.datdba))) acl
|
|
842
|
+
JOIN pg_roles grantee ON grantee.oid=acl.grantee
|
|
843
|
+
WHERE database.datname=current_database()
|
|
844
|
+
AND acl.grantee<>database.datdba
|
|
845
|
+
LOOP
|
|
846
|
+
EXECUTE format('REVOKE ALL PRIVILEGES ON DATABASE %I FROM %I', current_database(), database_grantee.grantee_role);
|
|
847
|
+
END LOOP;
|
|
848
|
+
EXECUTE format(
|
|
849
|
+
'REVOKE ALL PRIVILEGES ON DATABASE %I FROM PUBLIC',
|
|
850
|
+
current_database()
|
|
851
|
+
);
|
|
852
|
+
FOR namespace IN
|
|
853
|
+
SELECT oid, nspname
|
|
854
|
+
FROM pg_namespace
|
|
855
|
+
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
|
|
856
|
+
AND nspname NOT LIKE 'pg_toast%'
|
|
857
|
+
AND nspname NOT LIKE 'pg_temp_%'
|
|
858
|
+
LOOP
|
|
859
|
+
FOR object_grantee IN
|
|
860
|
+
SELECT class.relkind, class.relname,
|
|
861
|
+
CASE WHEN acl.grantee = 0 THEN 'PUBLIC' ELSE format('%I', grantee.rolname) END AS grantee_sql
|
|
862
|
+
FROM pg_class class
|
|
863
|
+
CROSS JOIN LATERAL aclexplode(class.relacl) acl
|
|
864
|
+
LEFT JOIN pg_roles grantee ON grantee.oid = acl.grantee
|
|
865
|
+
WHERE class.relnamespace = namespace.oid
|
|
866
|
+
AND class.relkind IN ('r', 'p', 'v', 'm', 'f', 'S')
|
|
867
|
+
AND (acl.grantee = 0 OR grantee.oid IS NOT NULL)
|
|
868
|
+
LOOP
|
|
869
|
+
IF object_grantee.relkind = 'S' THEN
|
|
870
|
+
EXECUTE format(
|
|
871
|
+
'REVOKE ALL PRIVILEGES ON SEQUENCE %I.%I FROM %s',
|
|
872
|
+
namespace.nspname,
|
|
873
|
+
object_grantee.relname,
|
|
874
|
+
object_grantee.grantee_sql
|
|
875
|
+
);
|
|
876
|
+
ELSE
|
|
877
|
+
EXECUTE format(
|
|
878
|
+
'REVOKE ALL PRIVILEGES ON TABLE %I.%I FROM %s',
|
|
879
|
+
namespace.nspname,
|
|
880
|
+
object_grantee.relname,
|
|
881
|
+
object_grantee.grantee_sql
|
|
882
|
+
);
|
|
883
|
+
END IF;
|
|
884
|
+
END LOOP;
|
|
885
|
+
FOR object_grantee IN
|
|
886
|
+
SELECT proc.oid::regprocedure AS function_signature,
|
|
887
|
+
CASE WHEN acl.grantee = 0 THEN 'PUBLIC' ELSE format('%I', grantee.rolname) END AS grantee_sql
|
|
888
|
+
FROM pg_proc proc
|
|
889
|
+
CROSS JOIN LATERAL aclexplode(COALESCE(proc.proacl, acldefault('f', proc.proowner))) acl
|
|
890
|
+
LEFT JOIN pg_roles grantee ON grantee.oid = acl.grantee
|
|
891
|
+
WHERE proc.pronamespace = namespace.oid
|
|
892
|
+
AND (acl.grantee = 0 OR grantee.oid IS NOT NULL)
|
|
893
|
+
LOOP
|
|
894
|
+
EXECUTE format(
|
|
895
|
+
'REVOKE ALL PRIVILEGES ON FUNCTION %s FROM %s',
|
|
896
|
+
object_grantee.function_signature,
|
|
897
|
+
object_grantee.grantee_sql
|
|
898
|
+
);
|
|
899
|
+
END LOOP;
|
|
900
|
+
FOR object_grantee IN
|
|
901
|
+
SELECT CASE WHEN acl.grantee = 0 THEN 'PUBLIC' ELSE format('%I', grantee.rolname) END AS grantee_sql
|
|
902
|
+
FROM pg_namespace ns
|
|
903
|
+
CROSS JOIN LATERAL aclexplode(ns.nspacl) acl
|
|
904
|
+
LEFT JOIN pg_roles grantee ON grantee.oid = acl.grantee
|
|
905
|
+
WHERE ns.oid = namespace.oid
|
|
906
|
+
AND (acl.grantee = 0 OR grantee.oid IS NOT NULL)
|
|
907
|
+
LOOP
|
|
908
|
+
EXECUTE format('REVOKE ALL PRIVILEGES ON SCHEMA %I FROM %s', namespace.nspname, object_grantee.grantee_sql);
|
|
909
|
+
END LOOP;
|
|
910
|
+
END LOOP;
|
|
911
|
+
END
|
|
912
|
+
$service_role_acl$;
|
|
913
|
+
|
|
914
|
+
DO $service_connect$
|
|
915
|
+
DECLARE service_member RECORD;
|
|
916
|
+
BEGIN
|
|
917
|
+
FOR service_member IN
|
|
918
|
+
SELECT DISTINCT member.rolname AS member_role
|
|
919
|
+
FROM pg_auth_members membership
|
|
920
|
+
JOIN pg_roles granted ON granted.oid = membership.roleid
|
|
921
|
+
JOIN pg_roles member ON member.oid = membership.member
|
|
922
|
+
WHERE granted.rolname IN ('open_loops_runtime', 'open_loops_authenticator')
|
|
923
|
+
AND member.rolcanlogin
|
|
924
|
+
LOOP
|
|
925
|
+
EXECUTE format('GRANT CONNECT ON DATABASE %I TO %I', current_database(), service_member.member_role);
|
|
926
|
+
END LOOP;
|
|
927
|
+
END
|
|
928
|
+
$service_connect$;
|
|
929
|
+
|
|
930
|
+
DO $service_role_cross_memberships$
|
|
931
|
+
DECLARE membership RECORD;
|
|
932
|
+
BEGIN
|
|
933
|
+
FOR membership IN
|
|
934
|
+
SELECT granted.rolname AS granted_role, member.rolname AS member_role
|
|
935
|
+
FROM pg_auth_members relation
|
|
936
|
+
JOIN pg_roles granted ON granted.oid=relation.roleid
|
|
937
|
+
JOIN pg_roles member ON member.oid=relation.member
|
|
938
|
+
WHERE (member.rolname='open_loops_runtime' AND
|
|
939
|
+
granted.rolname IN ('open_loops_owner', 'open_loops_migrator', 'open_loops_authenticator'))
|
|
940
|
+
OR (member.rolname='open_loops_authenticator' AND
|
|
941
|
+
granted.rolname IN ('open_loops_owner', 'open_loops_migrator', 'open_loops_runtime'))
|
|
942
|
+
LOOP
|
|
943
|
+
EXECUTE format('REVOKE %I FROM %I', membership.granted_role, membership.member_role);
|
|
944
|
+
END LOOP;
|
|
945
|
+
END
|
|
946
|
+
$service_role_cross_memberships$;
|
|
947
|
+
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
|
|
948
|
+
REVOKE ALL ON SCHEMA public FROM open_loops_runtime, open_loops_authenticator;
|
|
949
|
+
GRANT USAGE ON SCHEMA public TO open_loops_runtime, open_loops_authenticator;
|
|
950
|
+
GRANT USAGE, CREATE ON SCHEMA public TO open_loops_owner, open_loops_migrator;
|
|
951
|
+
|
|
952
|
+
ALTER TABLE loop_runs DROP CONSTRAINT loop_runs_loop_id_fkey;
|
|
953
|
+
ALTER TABLE workflow_runs DROP CONSTRAINT workflow_runs_workflow_id_fkey;
|
|
954
|
+
ALTER TABLE workflow_runs DROP CONSTRAINT workflow_runs_loop_id_fkey;
|
|
955
|
+
ALTER TABLE workflow_runs DROP CONSTRAINT workflow_runs_loop_run_id_fkey;
|
|
956
|
+
ALTER TABLE workflow_work_items DROP CONSTRAINT workflow_work_items_invocation_id_fkey;
|
|
957
|
+
ALTER TABLE workflow_work_items DROP CONSTRAINT workflow_work_items_workflow_id_fkey;
|
|
958
|
+
ALTER TABLE workflow_work_items DROP CONSTRAINT workflow_work_items_loop_id_fkey;
|
|
959
|
+
ALTER TABLE workflow_work_items DROP CONSTRAINT workflow_work_items_workflow_run_id_fkey;
|
|
960
|
+
ALTER TABLE workflow_step_runs DROP CONSTRAINT workflow_step_runs_workflow_run_id_fkey;
|
|
961
|
+
ALTER TABLE workflow_events DROP CONSTRAINT workflow_events_workflow_run_id_fkey;
|
|
962
|
+
ALTER TABLE goal_plan_nodes DROP CONSTRAINT goal_plan_nodes_goal_id_fkey;
|
|
963
|
+
ALTER TABLE goal_runs DROP CONSTRAINT goal_runs_goal_id_fkey;
|
|
964
|
+
ALTER TABLE runner_leases DROP CONSTRAINT runner_leases_runner_id_fkey;
|
|
965
|
+
ALTER TABLE runner_leases DROP CONSTRAINT runner_leases_loop_run_id_fkey;
|
|
966
|
+
ALTER TABLE runner_leases DROP CONSTRAINT runner_leases_workflow_run_id_fkey;
|
|
967
|
+
ALTER TABLE runner_leases DROP CONSTRAINT runner_leases_check;
|
|
968
|
+
|
|
969
|
+
ALTER TABLE loops DROP CONSTRAINT loops_pkey;
|
|
970
|
+
ALTER TABLE loop_runs DROP CONSTRAINT loop_runs_pkey;
|
|
971
|
+
ALTER TABLE loop_runs DROP CONSTRAINT loop_runs_loop_id_scheduled_for_key;
|
|
972
|
+
ALTER TABLE daemon_lease DROP CONSTRAINT daemon_lease_pkey;
|
|
973
|
+
ALTER TABLE workflow_specs DROP CONSTRAINT workflow_specs_pkey;
|
|
974
|
+
ALTER TABLE workflow_runs DROP CONSTRAINT workflow_runs_pkey;
|
|
975
|
+
ALTER TABLE workflow_invocations DROP CONSTRAINT workflow_invocations_pkey;
|
|
976
|
+
ALTER TABLE workflow_work_items DROP CONSTRAINT workflow_work_items_pkey;
|
|
977
|
+
ALTER TABLE workflow_work_items DROP CONSTRAINT workflow_work_items_route_key_idempotency_key_key;
|
|
978
|
+
ALTER TABLE workflow_step_runs DROP CONSTRAINT workflow_step_runs_pkey;
|
|
979
|
+
ALTER TABLE workflow_step_runs DROP CONSTRAINT workflow_step_runs_workflow_run_id_step_id_key;
|
|
980
|
+
ALTER TABLE workflow_events DROP CONSTRAINT workflow_events_pkey;
|
|
981
|
+
ALTER TABLE workflow_events DROP CONSTRAINT workflow_events_workflow_run_id_sequence_key;
|
|
982
|
+
ALTER TABLE goals DROP CONSTRAINT goals_pkey;
|
|
983
|
+
ALTER TABLE goal_plan_nodes DROP CONSTRAINT goal_plan_nodes_pkey;
|
|
984
|
+
ALTER TABLE goal_plan_nodes DROP CONSTRAINT goal_plan_nodes_plan_id_key_key;
|
|
985
|
+
ALTER TABLE goal_runs DROP CONSTRAINT goal_runs_pkey;
|
|
986
|
+
ALTER TABLE runner_machines DROP CONSTRAINT runner_machines_pkey;
|
|
987
|
+
ALTER TABLE runner_leases DROP CONSTRAINT runner_leases_pkey;
|
|
988
|
+
ALTER TABLE run_receipts DROP CONSTRAINT run_receipts_pkey;
|
|
989
|
+
ALTER TABLE audit_events DROP CONSTRAINT audit_events_pkey;
|
|
990
|
+
|
|
991
|
+
DROP INDEX idx_workflows_name_active;
|
|
992
|
+
DROP INDEX idx_workflow_runs_idempotency;
|
|
993
|
+
DROP INDEX idx_workflow_invocations_dedupe;
|
|
994
|
+
DROP INDEX idx_runner_leases_active_loop_run;
|
|
995
|
+
DROP INDEX idx_runner_leases_active_workflow_run;
|
|
996
|
+
|
|
997
|
+
CREATE OR REPLACE FUNCTION public.open_loops_current_tenant_id() RETURNS TEXT
|
|
998
|
+
LANGUAGE sql STABLE PARALLEL SAFE SET search_path = pg_catalog
|
|
999
|
+
RETURN NULLIF(pg_catalog.current_setting('open_loops.tenant_id', true), '');
|
|
1000
|
+
ALTER FUNCTION public.open_loops_current_tenant_id() OWNER TO open_loops_owner;
|
|
1001
|
+
DO $tenant_function_acl$
|
|
1002
|
+
DECLARE grantee RECORD;
|
|
1003
|
+
BEGIN
|
|
1004
|
+
FOR grantee IN
|
|
1005
|
+
SELECT rolname
|
|
1006
|
+
FROM pg_roles
|
|
1007
|
+
WHERE rolname NOT IN ('open_loops_owner', 'open_loops_runtime')
|
|
1008
|
+
LOOP
|
|
1009
|
+
EXECUTE format(
|
|
1010
|
+
'REVOKE ALL ON FUNCTION public.open_loops_current_tenant_id() FROM %I',
|
|
1011
|
+
grantee.rolname
|
|
1012
|
+
);
|
|
1013
|
+
END LOOP;
|
|
1014
|
+
END
|
|
1015
|
+
$tenant_function_acl$;
|
|
1016
|
+
REVOKE ALL ON FUNCTION public.open_loops_current_tenant_id() FROM PUBLIC;
|
|
1017
|
+
GRANT EXECUTE ON FUNCTION public.open_loops_current_tenant_id() TO open_loops_owner;
|
|
1018
|
+
GRANT EXECUTE ON FUNCTION public.open_loops_current_tenant_id() TO open_loops_runtime;
|
|
1019
|
+
|
|
1020
|
+
DO $tenant_defaults$
|
|
1021
|
+
DECLARE table_name TEXT;
|
|
1022
|
+
BEGIN
|
|
1023
|
+
FOREACH table_name IN ARRAY ARRAY[
|
|
1024
|
+
'loops', 'loop_runs', 'daemon_lease', 'workflow_specs', 'workflow_runs',
|
|
1025
|
+
'workflow_invocations', 'workflow_work_items', 'workflow_step_runs', 'workflow_events',
|
|
1026
|
+
'goals', 'goal_plan_nodes', 'goal_runs', 'runner_machines', 'runner_leases', 'run_receipts'
|
|
1027
|
+
] LOOP
|
|
1028
|
+
EXECUTE format('ALTER TABLE %I ALTER COLUMN tenant_id SET DEFAULT public.open_loops_current_tenant_id()', table_name);
|
|
1029
|
+
END LOOP;
|
|
1030
|
+
END
|
|
1031
|
+
$tenant_defaults$;
|
|
1032
|
+
|
|
1033
|
+
ALTER TABLE loops ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id);
|
|
1034
|
+
ALTER TABLE loop_runs ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id), ADD UNIQUE (tenant_id, loop_id, scheduled_for);
|
|
1035
|
+
ALTER TABLE daemon_lease ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id);
|
|
1036
|
+
ALTER TABLE workflow_specs ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id);
|
|
1037
|
+
ALTER TABLE workflow_runs ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id);
|
|
1038
|
+
ALTER TABLE workflow_invocations ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id);
|
|
1039
|
+
ALTER TABLE workflow_work_items ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id), ADD UNIQUE (tenant_id, route_key, idempotency_key);
|
|
1040
|
+
ALTER TABLE workflow_step_runs ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id), ADD UNIQUE (tenant_id, workflow_run_id, step_id);
|
|
1041
|
+
ALTER TABLE workflow_events ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id), ADD UNIQUE (tenant_id, workflow_run_id, sequence);
|
|
1042
|
+
ALTER TABLE goals ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id);
|
|
1043
|
+
ALTER TABLE goal_plan_nodes ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id), ADD UNIQUE (tenant_id, plan_id, key);
|
|
1044
|
+
ALTER TABLE goal_runs ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id);
|
|
1045
|
+
ALTER TABLE runner_machines ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id);
|
|
1046
|
+
ALTER TABLE runner_leases ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id);
|
|
1047
|
+
ALTER TABLE run_receipts ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, run_id);
|
|
1048
|
+
ALTER TABLE audit_events ALTER COLUMN tenant_id SET NOT NULL, ADD PRIMARY KEY (tenant_id, id);
|
|
1049
|
+
ALTER TABLE api_keys
|
|
1050
|
+
ALTER COLUMN tenant_id SET NOT NULL,
|
|
1051
|
+
ALTER COLUMN principal_id SET NOT NULL,
|
|
1052
|
+
ALTER COLUMN token_kind SET NOT NULL,
|
|
1053
|
+
ADD FOREIGN KEY (tenant_id, principal_id) REFERENCES tenant_memberships(tenant_id, principal_id),
|
|
1054
|
+
ADD CHECK (app = 'loops'),
|
|
1055
|
+
ADD CHECK (token_kind IN ('api_key', 'service', 'machine')),
|
|
1056
|
+
ADD CHECK (jsonb_typeof(scopes) = 'array');
|
|
1057
|
+
|
|
1058
|
+
DO $tenant_foreign_keys$
|
|
1059
|
+
DECLARE table_name TEXT;
|
|
1060
|
+
BEGIN
|
|
1061
|
+
FOREACH table_name IN ARRAY ARRAY[
|
|
1062
|
+
'loops', 'loop_runs', 'daemon_lease', 'workflow_specs', 'workflow_runs',
|
|
1063
|
+
'workflow_invocations', 'workflow_work_items', 'workflow_step_runs', 'workflow_events',
|
|
1064
|
+
'goals', 'goal_plan_nodes', 'goal_runs', 'runner_machines', 'runner_leases',
|
|
1065
|
+
'audit_events', 'run_receipts'
|
|
1066
|
+
] LOOP
|
|
1067
|
+
EXECUTE format(
|
|
1068
|
+
'ALTER TABLE %I ADD CONSTRAINT %I FOREIGN KEY (tenant_id) REFERENCES tenants(id)',
|
|
1069
|
+
table_name,
|
|
1070
|
+
table_name || '_tenant_id_fkey'
|
|
1071
|
+
);
|
|
1072
|
+
END LOOP;
|
|
1073
|
+
END
|
|
1074
|
+
$tenant_foreign_keys$;
|
|
1075
|
+
|
|
1076
|
+
ALTER TABLE loop_runs ADD FOREIGN KEY (tenant_id, loop_id) REFERENCES loops(tenant_id, id) ON DELETE CASCADE;
|
|
1077
|
+
ALTER TABLE workflow_runs ADD FOREIGN KEY (tenant_id, workflow_id) REFERENCES workflow_specs(tenant_id, id) ON DELETE CASCADE;
|
|
1078
|
+
ALTER TABLE workflow_runs ADD FOREIGN KEY (tenant_id, loop_id) REFERENCES loops(tenant_id, id) ON DELETE SET NULL (loop_id);
|
|
1079
|
+
ALTER TABLE workflow_runs ADD FOREIGN KEY (tenant_id, loop_run_id) REFERENCES loop_runs(tenant_id, id) ON DELETE SET NULL (loop_run_id);
|
|
1080
|
+
ALTER TABLE workflow_invocations ADD FOREIGN KEY (tenant_id, workflow_id) REFERENCES workflow_specs(tenant_id, id) ON DELETE SET NULL (workflow_id);
|
|
1081
|
+
ALTER TABLE workflow_work_items ADD FOREIGN KEY (tenant_id, invocation_id) REFERENCES workflow_invocations(tenant_id, id) ON DELETE CASCADE;
|
|
1082
|
+
ALTER TABLE workflow_work_items ADD FOREIGN KEY (tenant_id, workflow_id) REFERENCES workflow_specs(tenant_id, id) ON DELETE SET NULL (workflow_id);
|
|
1083
|
+
ALTER TABLE workflow_work_items ADD FOREIGN KEY (tenant_id, loop_id) REFERENCES loops(tenant_id, id) ON DELETE SET NULL (loop_id);
|
|
1084
|
+
ALTER TABLE workflow_work_items ADD FOREIGN KEY (tenant_id, workflow_run_id) REFERENCES workflow_runs(tenant_id, id) ON DELETE SET NULL (workflow_run_id);
|
|
1085
|
+
ALTER TABLE workflow_step_runs ADD FOREIGN KEY (tenant_id, workflow_run_id) REFERENCES workflow_runs(tenant_id, id) ON DELETE CASCADE;
|
|
1086
|
+
ALTER TABLE workflow_events ADD FOREIGN KEY (tenant_id, workflow_run_id) REFERENCES workflow_runs(tenant_id, id) ON DELETE CASCADE;
|
|
1087
|
+
ALTER TABLE goal_plan_nodes ADD FOREIGN KEY (tenant_id, goal_id) REFERENCES goals(tenant_id, id) ON DELETE CASCADE;
|
|
1088
|
+
ALTER TABLE goal_runs ADD FOREIGN KEY (tenant_id, goal_id) REFERENCES goals(tenant_id, id) ON DELETE CASCADE;
|
|
1089
|
+
ALTER TABLE runner_leases ADD FOREIGN KEY (tenant_id, runner_id) REFERENCES runner_machines(tenant_id, id) ON DELETE CASCADE;
|
|
1090
|
+
ALTER TABLE runner_leases ADD FOREIGN KEY (tenant_id, loop_run_id) REFERENCES loop_runs(tenant_id, id) ON DELETE CASCADE;
|
|
1091
|
+
ALTER TABLE runner_leases ADD FOREIGN KEY (tenant_id, workflow_run_id) REFERENCES workflow_runs(tenant_id, id) ON DELETE CASCADE;
|
|
1092
|
+
ALTER TABLE runner_leases ADD CONSTRAINT runner_leases_exactly_one_run CHECK (num_nonnulls(loop_run_id, workflow_run_id) = 1);
|
|
1093
|
+
ALTER TABLE run_receipts ADD FOREIGN KEY (tenant_id, loop_id) REFERENCES loops(tenant_id, id);
|
|
1094
|
+
|
|
1095
|
+
ALTER TABLE loop_runs ADD FOREIGN KEY (tenant_id, goal_run_id) REFERENCES goal_runs(tenant_id, id) ON DELETE SET NULL (goal_run_id);
|
|
1096
|
+
ALTER TABLE workflow_runs ADD FOREIGN KEY (tenant_id, invocation_id) REFERENCES workflow_invocations(tenant_id, id) ON DELETE SET NULL (invocation_id);
|
|
1097
|
+
ALTER TABLE workflow_runs ADD FOREIGN KEY (tenant_id, work_item_id) REFERENCES workflow_work_items(tenant_id, id) ON DELETE SET NULL (work_item_id);
|
|
1098
|
+
ALTER TABLE workflow_runs ADD FOREIGN KEY (tenant_id, goal_run_id) REFERENCES goal_runs(tenant_id, id) ON DELETE SET NULL (goal_run_id);
|
|
1099
|
+
ALTER TABLE workflow_step_runs ADD FOREIGN KEY (tenant_id, goal_run_id) REFERENCES goal_runs(tenant_id, id) ON DELETE SET NULL (goal_run_id);
|
|
1100
|
+
ALTER TABLE goals ADD FOREIGN KEY (tenant_id, loop_id) REFERENCES loops(tenant_id, id) ON DELETE SET NULL (loop_id);
|
|
1101
|
+
ALTER TABLE goals ADD FOREIGN KEY (tenant_id, loop_run_id) REFERENCES loop_runs(tenant_id, id) ON DELETE SET NULL (loop_run_id);
|
|
1102
|
+
ALTER TABLE goals ADD FOREIGN KEY (tenant_id, workflow_id) REFERENCES workflow_specs(tenant_id, id) ON DELETE SET NULL (workflow_id);
|
|
1103
|
+
ALTER TABLE goals ADD FOREIGN KEY (tenant_id, workflow_run_id) REFERENCES workflow_runs(tenant_id, id) ON DELETE SET NULL (workflow_run_id);
|
|
1104
|
+
ALTER TABLE goal_runs ADD FOREIGN KEY (tenant_id, loop_id) REFERENCES loops(tenant_id, id) ON DELETE SET NULL (loop_id);
|
|
1105
|
+
ALTER TABLE goal_runs ADD FOREIGN KEY (tenant_id, loop_run_id) REFERENCES loop_runs(tenant_id, id) ON DELETE SET NULL (loop_run_id);
|
|
1106
|
+
ALTER TABLE goal_runs ADD FOREIGN KEY (tenant_id, workflow_id) REFERENCES workflow_specs(tenant_id, id) ON DELETE SET NULL (workflow_id);
|
|
1107
|
+
ALTER TABLE goal_runs ADD FOREIGN KEY (tenant_id, workflow_run_id) REFERENCES workflow_runs(tenant_id, id) ON DELETE SET NULL (workflow_run_id);
|
|
1108
|
+
|
|
1109
|
+
CREATE UNIQUE INDEX idx_workflows_name_active ON workflow_specs(tenant_id, name) WHERE status = 'active';
|
|
1110
|
+
CREATE UNIQUE INDEX idx_workflow_runs_idempotency ON workflow_runs(tenant_id, workflow_id, idempotency_key) WHERE idempotency_key IS NOT NULL;
|
|
1111
|
+
CREATE UNIQUE INDEX idx_workflow_invocations_dedupe ON workflow_invocations(tenant_id, source_kind, source_dedupe_key) WHERE source_dedupe_key IS NOT NULL;
|
|
1112
|
+
CREATE UNIQUE INDEX idx_runner_leases_active_loop_run ON runner_leases(tenant_id, loop_run_id) WHERE loop_run_id IS NOT NULL AND status = 'active';
|
|
1113
|
+
CREATE UNIQUE INDEX idx_runner_leases_active_workflow_run ON runner_leases(tenant_id, workflow_run_id) WHERE workflow_run_id IS NOT NULL AND status = 'active';
|
|
1114
|
+
|
|
1115
|
+
DO $rls_policy_reset$
|
|
1116
|
+
DECLARE policy_record RECORD;
|
|
1117
|
+
BEGIN
|
|
1118
|
+
FOR policy_record IN
|
|
1119
|
+
SELECT schemaname, tablename, policyname
|
|
1120
|
+
FROM pg_policies
|
|
1121
|
+
WHERE schemaname = 'public'
|
|
1122
|
+
AND tablename = ANY(ARRAY[
|
|
1123
|
+
'tenants', 'tenant_memberships', 'tenant_membership_roles', 'api_keys',
|
|
1124
|
+
'loops', 'loop_runs', 'daemon_lease', 'workflow_specs', 'workflow_runs',
|
|
1125
|
+
'workflow_invocations', 'workflow_work_items', 'workflow_step_runs', 'workflow_events',
|
|
1126
|
+
'goals', 'goal_plan_nodes', 'goal_runs', 'runner_machines', 'runner_leases',
|
|
1127
|
+
'audit_events', 'run_receipts'
|
|
1128
|
+
])
|
|
1129
|
+
LOOP
|
|
1130
|
+
EXECUTE format('DROP POLICY %I ON %I.%I', policy_record.policyname, policy_record.schemaname, policy_record.tablename);
|
|
1131
|
+
END LOOP;
|
|
1132
|
+
END
|
|
1133
|
+
$rls_policy_reset$;
|
|
1134
|
+
|
|
1135
|
+
DO $rls$
|
|
1136
|
+
DECLARE table_name TEXT;
|
|
1137
|
+
BEGIN
|
|
1138
|
+
FOREACH table_name IN ARRAY ARRAY[
|
|
1139
|
+
'tenant_memberships', 'tenant_membership_roles', 'api_keys',
|
|
1140
|
+
'loops', 'loop_runs', 'daemon_lease', 'workflow_specs', 'workflow_runs',
|
|
1141
|
+
'workflow_invocations', 'workflow_work_items', 'workflow_step_runs', 'workflow_events',
|
|
1142
|
+
'goals', 'goal_plan_nodes', 'goal_runs', 'runner_machines', 'runner_leases',
|
|
1143
|
+
'audit_events', 'run_receipts'
|
|
1144
|
+
] LOOP
|
|
1145
|
+
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', table_name);
|
|
1146
|
+
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', table_name);
|
|
1147
|
+
EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (tenant_id = public.open_loops_current_tenant_id()) WITH CHECK (tenant_id = public.open_loops_current_tenant_id())', table_name);
|
|
1148
|
+
END LOOP;
|
|
1149
|
+
END
|
|
1150
|
+
$rls$;
|
|
1151
|
+
ALTER TABLE tenants ENABLE ROW LEVEL SECURITY;
|
|
1152
|
+
ALTER TABLE tenants FORCE ROW LEVEL SECURITY;
|
|
1153
|
+
CREATE POLICY tenant_isolation ON tenants
|
|
1154
|
+
USING (id = public.open_loops_current_tenant_id())
|
|
1155
|
+
WITH CHECK (id = public.open_loops_current_tenant_id());
|
|
1156
|
+
CREATE POLICY auth_definer_tenant_lookup ON tenants TO open_loops_owner USING (true);
|
|
1157
|
+
CREATE POLICY auth_definer_membership_lookup ON tenant_memberships TO open_loops_owner USING (true);
|
|
1158
|
+
CREATE POLICY auth_definer_membership_roles_lookup ON tenant_membership_roles TO open_loops_owner USING (true);
|
|
1159
|
+
CREATE POLICY auth_definer_key_lookup ON api_keys TO open_loops_owner USING (true);
|
|
1160
|
+
CREATE POLICY auth_definer_audit_insert ON audit_events
|
|
1161
|
+
FOR INSERT TO open_loops_owner WITH CHECK (true);
|
|
1162
|
+
|
|
1163
|
+
DO $owners$
|
|
1164
|
+
DECLARE table_name TEXT;
|
|
1165
|
+
BEGIN
|
|
1166
|
+
FOREACH table_name IN ARRAY ARRAY[
|
|
1167
|
+
'tenants', 'principals', 'tenant_memberships', 'tenant_roles', 'tenant_membership_roles',
|
|
1168
|
+
'api_keys', 'loops', 'loop_runs', 'daemon_lease', 'workflow_specs', 'workflow_runs',
|
|
1169
|
+
'workflow_invocations', 'workflow_work_items', 'workflow_step_runs', 'workflow_events',
|
|
1170
|
+
'goals', 'goal_plan_nodes', 'goal_runs', 'runner_machines', 'runner_leases',
|
|
1171
|
+
'audit_events', 'run_receipts'
|
|
1172
|
+
] LOOP
|
|
1173
|
+
EXECUTE format('ALTER TABLE %I OWNER TO open_loops_owner', table_name);
|
|
1174
|
+
END LOOP;
|
|
1175
|
+
END
|
|
1176
|
+
$owners$;
|
|
1177
|
+
ALTER TABLE preauth_audit_events OWNER TO open_loops_owner;
|
|
1178
|
+
ALTER TABLE open_loops_schema_migrations OWNER TO open_loops_migrator;
|
|
1179
|
+
|
|
1180
|
+
GRANT SELECT, UPDATE, REFERENCES ON tenants TO open_loops_owner;
|
|
1181
|
+
SET ROLE open_loops_owner;
|
|
1182
|
+
DO $tenant_foreign_keys_owner$
|
|
1183
|
+
DECLARE table_name TEXT;
|
|
1184
|
+
BEGIN
|
|
1185
|
+
FOREACH table_name IN ARRAY ARRAY[
|
|
1186
|
+
'loops', 'loop_runs', 'daemon_lease', 'workflow_specs', 'workflow_runs',
|
|
1187
|
+
'workflow_invocations', 'workflow_work_items', 'workflow_step_runs', 'workflow_events',
|
|
1188
|
+
'goals', 'goal_plan_nodes', 'goal_runs', 'runner_machines', 'runner_leases',
|
|
1189
|
+
'audit_events', 'run_receipts'
|
|
1190
|
+
] LOOP
|
|
1191
|
+
EXECUTE format('ALTER TABLE %I DROP CONSTRAINT IF EXISTS %I', table_name, table_name || '_tenant_id_fkey');
|
|
1192
|
+
EXECUTE format(
|
|
1193
|
+
'ALTER TABLE %I ADD CONSTRAINT %I FOREIGN KEY (tenant_id) REFERENCES tenants(id)',
|
|
1194
|
+
table_name,
|
|
1195
|
+
table_name || '_tenant_id_fkey'
|
|
1196
|
+
);
|
|
1197
|
+
END LOOP;
|
|
1198
|
+
END
|
|
1199
|
+
$tenant_foreign_keys_owner$;
|
|
1200
|
+
RESET ROLE;
|
|
1201
|
+
|
|
1202
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON loops, loop_runs, daemon_lease, workflow_specs, workflow_runs, workflow_invocations,
|
|
1203
|
+
workflow_work_items, workflow_step_runs, workflow_events, goals, goal_plan_nodes, goal_runs,
|
|
1204
|
+
runner_machines, runner_leases, run_receipts TO open_loops_runtime;
|
|
1205
|
+
GRANT SELECT, UPDATE, REFERENCES ON tenants TO open_loops_runtime;
|
|
1206
|
+
GRANT SELECT ON open_loops_schema_migrations TO open_loops_runtime;
|
|
1207
|
+
|
|
1208
|
+
CREATE OR REPLACE FUNCTION public.open_loops_reject_runtime_tenant_update()
|
|
1209
|
+
RETURNS TRIGGER
|
|
1210
|
+
LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog
|
|
1211
|
+
AS $$
|
|
1212
|
+
BEGIN
|
|
1213
|
+
IF pg_has_role(current_user, 'open_loops_runtime', 'USAGE') THEN
|
|
1214
|
+
RAISE EXCEPTION 'runtime role cannot update tenants' USING ERRCODE = '42501';
|
|
1215
|
+
END IF;
|
|
1216
|
+
RETURN NEW;
|
|
1217
|
+
END;
|
|
1218
|
+
$$;
|
|
1219
|
+
ALTER FUNCTION public.open_loops_reject_runtime_tenant_update() OWNER TO open_loops_owner;
|
|
1220
|
+
REVOKE ALL ON FUNCTION public.open_loops_reject_runtime_tenant_update() FROM PUBLIC, open_loops_runtime, open_loops_authenticator;
|
|
1221
|
+
DROP TRIGGER IF EXISTS open_loops_reject_runtime_tenant_update ON tenants;
|
|
1222
|
+
CREATE TRIGGER open_loops_reject_runtime_tenant_update
|
|
1223
|
+
BEFORE UPDATE ON tenants
|
|
1224
|
+
FOR EACH ROW
|
|
1225
|
+
EXECUTE FUNCTION public.open_loops_reject_runtime_tenant_update();
|
|
1226
|
+
|
|
1227
|
+
ALTER TABLE audit_events
|
|
1228
|
+
ADD CHECK (decision IS NULL OR decision IN ('allow', 'deny')),
|
|
1229
|
+
ADD CHECK (operation_id IS NULL OR operation_id ~ '^[A-Za-z][A-Za-z0-9.]{1,127}$'),
|
|
1230
|
+
ADD CHECK (request_id IS NULL OR (length(request_id) BETWEEN 1 AND 128));
|
|
1231
|
+
|
|
1232
|
+
CREATE OR REPLACE FUNCTION public.open_loops_authenticate_key(p_kid TEXT, p_token_hash TEXT)
|
|
1233
|
+
RETURNS TABLE (
|
|
1234
|
+
kid TEXT, app TEXT, agent TEXT, scopes JSONB, token_hash TEXT, issued_at TIMESTAMPTZ,
|
|
1235
|
+
expires_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ, disabled_at TIMESTAMPTZ,
|
|
1236
|
+
tenant_id TEXT, tenant_status TEXT, principal_id TEXT, principal_status TEXT,
|
|
1237
|
+
membership_status TEXT, token_kind TEXT, roles TEXT[]
|
|
1238
|
+
)
|
|
1239
|
+
LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog
|
|
1240
|
+
AS $$
|
|
1241
|
+
SELECT key.kid, key.app, key.agent, key.scopes, key.token_hash, key.issued_at,
|
|
1242
|
+
key.expires_at, key.revoked_at, key.disabled_at,
|
|
1243
|
+
key.tenant_id, tenant.status, key.principal_id, principal.status,
|
|
1244
|
+
membership.status, key.token_kind,
|
|
1245
|
+
COALESCE(array_agg(membership_role.role ORDER BY membership_role.role)
|
|
1246
|
+
FILTER (WHERE membership_role.role IS NOT NULL), ARRAY[]::TEXT[])
|
|
1247
|
+
FROM public.api_keys key
|
|
1248
|
+
JOIN public.tenants tenant ON tenant.id = key.tenant_id
|
|
1249
|
+
JOIN public.principals principal ON principal.id = key.principal_id
|
|
1250
|
+
JOIN public.tenant_memberships membership
|
|
1251
|
+
ON membership.tenant_id = key.tenant_id AND membership.principal_id = key.principal_id
|
|
1252
|
+
LEFT JOIN public.tenant_membership_roles membership_role
|
|
1253
|
+
ON membership_role.tenant_id = membership.tenant_id AND membership_role.principal_id = membership.principal_id
|
|
1254
|
+
WHERE key.kid = p_kid AND key.token_hash = p_token_hash
|
|
1255
|
+
GROUP BY key.kid, key.app, key.agent, key.scopes, key.token_hash, key.issued_at,
|
|
1256
|
+
key.expires_at, key.revoked_at, key.disabled_at, key.tenant_id, tenant.status,
|
|
1257
|
+
key.principal_id, principal.status, membership.status, key.token_kind;
|
|
1258
|
+
$$;
|
|
1259
|
+
ALTER FUNCTION public.open_loops_authenticate_key(TEXT, TEXT) OWNER TO open_loops_owner;
|
|
1260
|
+
REVOKE ALL ON FUNCTION public.open_loops_authenticate_key(TEXT, TEXT)
|
|
1261
|
+
FROM PUBLIC, open_loops_runtime, open_loops_authenticator;
|
|
1262
|
+
GRANT EXECUTE ON FUNCTION public.open_loops_authenticate_key(TEXT, TEXT) TO open_loops_authenticator;
|
|
1263
|
+
|
|
1264
|
+
CREATE OR REPLACE FUNCTION public.open_loops_append_auth_audit(
|
|
1265
|
+
p_id TEXT, p_kid TEXT, p_token_hash TEXT, p_request_id TEXT,
|
|
1266
|
+
p_operation_id TEXT, p_decision TEXT, p_deny_reason TEXT, p_metadata JSONB
|
|
1267
|
+
) RETURNS VOID
|
|
1268
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog
|
|
1269
|
+
AS $$
|
|
1270
|
+
DECLARE
|
|
1271
|
+
bound_tenant_id TEXT;
|
|
1272
|
+
bound_principal_id TEXT;
|
|
1273
|
+
BEGIN
|
|
1274
|
+
IF p_decision NOT IN ('allow', 'deny') THEN
|
|
1275
|
+
RAISE EXCEPTION 'invalid authentication audit decision';
|
|
1276
|
+
END IF;
|
|
1277
|
+
IF p_operation_id !~ '^[A-Za-z][A-Za-z0-9.]{1,127}$' THEN
|
|
1278
|
+
RAISE EXCEPTION 'invalid authentication audit operation';
|
|
1279
|
+
END IF;
|
|
1280
|
+
IF p_request_id IS NULL OR length(p_request_id) NOT BETWEEN 1 AND 128 THEN
|
|
1281
|
+
RAISE EXCEPTION 'invalid authentication audit request id';
|
|
1282
|
+
END IF;
|
|
1283
|
+
IF p_metadata IS NULL OR jsonb_typeof(p_metadata) <> 'object' THEN
|
|
1284
|
+
RAISE EXCEPTION 'invalid authentication audit metadata';
|
|
1285
|
+
END IF;
|
|
1286
|
+
IF p_decision = 'allow' AND p_deny_reason IS NOT NULL THEN
|
|
1287
|
+
RAISE EXCEPTION 'allow audit cannot include a deny reason';
|
|
1288
|
+
END IF;
|
|
1289
|
+
|
|
1290
|
+
SELECT key.tenant_id, key.principal_id
|
|
1291
|
+
INTO bound_tenant_id, bound_principal_id
|
|
1292
|
+
FROM public.api_keys key
|
|
1293
|
+
WHERE key.kid = p_kid AND key.token_hash = p_token_hash;
|
|
1294
|
+
|
|
1295
|
+
IF bound_tenant_id IS NULL THEN
|
|
1296
|
+
INSERT INTO public.preauth_audit_events(id, request_id, operation_id, deny_reason, metadata_json)
|
|
1297
|
+
VALUES (p_id, p_request_id, p_operation_id, COALESCE(p_deny_reason, 'unauthorized'), p_metadata);
|
|
1298
|
+
ELSE
|
|
1299
|
+
INSERT INTO public.audit_events(
|
|
1300
|
+
tenant_id, id, actor, action, subject_type, subject_id, metadata_json, created_at,
|
|
1301
|
+
principal_id, request_id, operation_id, decision, deny_reason
|
|
1302
|
+
) VALUES (
|
|
1303
|
+
bound_tenant_id, p_id, bound_principal_id, 'auth.' || p_decision,
|
|
1304
|
+
'api_request', p_request_id, p_metadata, now(), bound_principal_id, p_request_id,
|
|
1305
|
+
p_operation_id, p_decision, p_deny_reason
|
|
1306
|
+
);
|
|
1307
|
+
END IF;
|
|
1308
|
+
END
|
|
1309
|
+
$$;
|
|
1310
|
+
ALTER FUNCTION public.open_loops_append_auth_audit(TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, JSONB) OWNER TO open_loops_owner;
|
|
1311
|
+
REVOKE ALL ON FUNCTION public.open_loops_append_auth_audit(TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, JSONB)
|
|
1312
|
+
FROM PUBLIC, open_loops_runtime, open_loops_authenticator;
|
|
1313
|
+
GRANT EXECUTE ON FUNCTION public.open_loops_append_auth_audit(TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, JSONB) TO open_loops_authenticator;
|
|
1314
|
+
|
|
1315
|
+
DO $auth_function_acl$
|
|
1316
|
+
DECLARE function_acl RECORD;
|
|
1317
|
+
BEGIN
|
|
1318
|
+
FOR function_acl IN
|
|
1319
|
+
SELECT proc.oid::regprocedure AS function_signature,
|
|
1320
|
+
CASE WHEN proc.oid = 'public.open_loops_current_tenant_id()'::regprocedure
|
|
1321
|
+
THEN (SELECT oid FROM pg_roles WHERE rolname = 'open_loops_runtime')
|
|
1322
|
+
ELSE (SELECT oid FROM pg_roles WHERE rolname = 'open_loops_authenticator')
|
|
1323
|
+
END AS allowed_grantee,
|
|
1324
|
+
acl.grantee,
|
|
1325
|
+
CASE WHEN acl.grantee = 0 THEN 'PUBLIC' ELSE format('%I', grantee.rolname) END AS grantee_sql
|
|
1326
|
+
FROM pg_proc proc
|
|
1327
|
+
CROSS JOIN LATERAL aclexplode(COALESCE(proc.proacl, acldefault('f', proc.proowner))) acl
|
|
1328
|
+
LEFT JOIN pg_roles grantee ON grantee.oid = acl.grantee
|
|
1329
|
+
WHERE proc.oid = ANY(ARRAY[
|
|
1330
|
+
'public.open_loops_current_tenant_id()'::regprocedure,
|
|
1331
|
+
'public.open_loops_authenticate_key(text,text)'::regprocedure,
|
|
1332
|
+
'public.open_loops_append_auth_audit(text,text,text,text,text,text,text,jsonb)'::regprocedure
|
|
1333
|
+
])
|
|
1334
|
+
AND (acl.grantee = 0 OR grantee.oid IS NOT NULL)
|
|
1335
|
+
AND acl.grantee <> CASE WHEN proc.oid = 'public.open_loops_current_tenant_id()'::regprocedure
|
|
1336
|
+
THEN (SELECT oid FROM pg_roles WHERE rolname = 'open_loops_runtime')
|
|
1337
|
+
ELSE (SELECT oid FROM pg_roles WHERE rolname = 'open_loops_authenticator')
|
|
1338
|
+
END
|
|
1339
|
+
LOOP
|
|
1340
|
+
EXECUTE format('REVOKE ALL PRIVILEGES ON FUNCTION %s FROM %s', function_acl.function_signature, function_acl.grantee_sql);
|
|
1341
|
+
END LOOP;
|
|
1342
|
+
END
|
|
1343
|
+
$auth_function_acl$;
|
|
1344
|
+
|
|
1345
|
+
REVOKE CREATE ON SCHEMA public FROM open_loops_owner, open_loops_migrator;
|
|
1346
|
+
GRANT USAGE ON SCHEMA public TO open_loops_owner, open_loops_migrator;
|
|
1347
|
+
|
|
1348
|
+
${POSTGRES_TENANT_PRIVILEGED_MEMBERSHIPS_SQL}
|
|
1349
|
+
|
|
1350
|
+
DO $tenant_enforcement_postconditions$
|
|
1351
|
+
DECLARE tenant_function REGPROCEDURE := 'public.open_loops_current_tenant_id()'::regprocedure;
|
|
1352
|
+
BEGIN
|
|
1353
|
+
IF EXISTS (
|
|
1354
|
+
SELECT 1 FROM pg_roles
|
|
1355
|
+
WHERE rolname IN ('open_loops_owner', 'open_loops_migrator', 'open_loops_runtime', 'open_loops_authenticator')
|
|
1356
|
+
AND (rolcanlogin OR NOT rolinherit OR rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls)
|
|
1357
|
+
) THEN
|
|
1358
|
+
RAISE EXCEPTION 'tenant enforcement did not normalize OpenLoops database roles';
|
|
1359
|
+
END IF;
|
|
1360
|
+
IF EXISTS (
|
|
1361
|
+
SELECT 1
|
|
1362
|
+
FROM pg_auth_members auth_membership
|
|
1363
|
+
JOIN pg_roles granted ON granted.oid = auth_membership.roleid
|
|
1364
|
+
JOIN pg_roles member ON member.oid = auth_membership.member
|
|
1365
|
+
WHERE (granted.rolname IN ('open_loops_owner', 'open_loops_migrator')
|
|
1366
|
+
OR member.rolname IN ('open_loops_owner', 'open_loops_migrator', 'open_loops_runtime', 'open_loops_authenticator'))
|
|
1367
|
+
AND NOT (
|
|
1368
|
+
granted.rolname IN ('open_loops_owner', 'open_loops_migrator')
|
|
1369
|
+
AND member.rolname = session_user
|
|
1370
|
+
AND NOT auth_membership.admin_option
|
|
1371
|
+
AND auth_membership.inherit_option
|
|
1372
|
+
AND auth_membership.set_option
|
|
1373
|
+
)
|
|
1374
|
+
) THEN
|
|
1375
|
+
RAISE EXCEPTION 'tenant enforcement left privileged role memberships unsafe';
|
|
1376
|
+
END IF;
|
|
1377
|
+
IF (SELECT pg_get_userbyid(proowner) FROM pg_proc WHERE oid = tenant_function) <> 'open_loops_owner' THEN
|
|
1378
|
+
RAISE EXCEPTION 'tenant enforcement did not secure the tenant context function owner';
|
|
1379
|
+
END IF;
|
|
1380
|
+
IF EXISTS (
|
|
1381
|
+
SELECT 1
|
|
1382
|
+
FROM pg_proc proc,
|
|
1383
|
+
LATERAL aclexplode(COALESCE(proc.proacl, acldefault('f', proc.proowner))) acl
|
|
1384
|
+
WHERE proc.oid = tenant_function
|
|
1385
|
+
AND acl.privilege_type = 'EXECUTE'
|
|
1386
|
+
AND acl.grantee NOT IN (
|
|
1387
|
+
(SELECT oid FROM pg_roles WHERE rolname='open_loops_owner'),
|
|
1388
|
+
(SELECT oid FROM pg_roles WHERE rolname='open_loops_runtime')
|
|
1389
|
+
)
|
|
1390
|
+
) THEN
|
|
1391
|
+
RAISE EXCEPTION 'tenant enforcement left an unexpected tenant context function grant';
|
|
1392
|
+
END IF;
|
|
1393
|
+
IF NOT has_function_privilege('open_loops_runtime', tenant_function, 'EXECUTE') OR
|
|
1394
|
+
has_function_privilege('open_loops_authenticator', tenant_function, 'EXECUTE') THEN
|
|
1395
|
+
RAISE EXCEPTION 'tenant enforcement tenant context function grants are incomplete';
|
|
1396
|
+
END IF;
|
|
1397
|
+
IF has_schema_privilege('open_loops_owner', 'public', 'CREATE') OR
|
|
1398
|
+
has_schema_privilege('open_loops_migrator', 'public', 'CREATE') THEN
|
|
1399
|
+
RAISE EXCEPTION 'tenant enforcement left bootstrap schema creation privileges enabled';
|
|
1400
|
+
END IF;
|
|
1401
|
+
IF EXISTS (
|
|
1402
|
+
SELECT 1
|
|
1403
|
+
FROM unnest(ARRAY[
|
|
1404
|
+
'tenants', 'tenant_memberships', 'tenant_membership_roles', 'api_keys',
|
|
1405
|
+
'loops', 'loop_runs', 'daemon_lease', 'workflow_specs', 'workflow_runs',
|
|
1406
|
+
'workflow_invocations', 'workflow_work_items', 'workflow_step_runs', 'workflow_events',
|
|
1407
|
+
'goals', 'goal_plan_nodes', 'goal_runs', 'runner_machines', 'runner_leases',
|
|
1408
|
+
'audit_events', 'run_receipts'
|
|
1409
|
+
]) AS protected_table(name)
|
|
1410
|
+
JOIN pg_class class ON class.oid = format('public.%I', protected_table.name)::regclass
|
|
1411
|
+
WHERE pg_get_userbyid(class.relowner) <> 'open_loops_owner'
|
|
1412
|
+
OR NOT class.relrowsecurity
|
|
1413
|
+
OR NOT class.relforcerowsecurity
|
|
1414
|
+
) THEN
|
|
1415
|
+
RAISE EXCEPTION 'tenant enforcement did not secure protected table ownership and RLS flags';
|
|
1416
|
+
END IF;
|
|
1417
|
+
IF EXISTS (
|
|
1418
|
+
WITH protected(table_name, discriminator) AS (
|
|
1419
|
+
VALUES
|
|
1420
|
+
('tenants', 'id'),
|
|
1421
|
+
('tenant_memberships', 'tenant_id'),
|
|
1422
|
+
('tenant_membership_roles', 'tenant_id'),
|
|
1423
|
+
('api_keys', 'tenant_id'),
|
|
1424
|
+
('loops', 'tenant_id'),
|
|
1425
|
+
('loop_runs', 'tenant_id'),
|
|
1426
|
+
('daemon_lease', 'tenant_id'),
|
|
1427
|
+
('workflow_specs', 'tenant_id'),
|
|
1428
|
+
('workflow_runs', 'tenant_id'),
|
|
1429
|
+
('workflow_invocations', 'tenant_id'),
|
|
1430
|
+
('workflow_work_items', 'tenant_id'),
|
|
1431
|
+
('workflow_step_runs', 'tenant_id'),
|
|
1432
|
+
('workflow_events', 'tenant_id'),
|
|
1433
|
+
('goals', 'tenant_id'),
|
|
1434
|
+
('goal_plan_nodes', 'tenant_id'),
|
|
1435
|
+
('goal_runs', 'tenant_id'),
|
|
1436
|
+
('runner_machines', 'tenant_id'),
|
|
1437
|
+
('runner_leases', 'tenant_id'),
|
|
1438
|
+
('audit_events', 'tenant_id'),
|
|
1439
|
+
('run_receipts', 'tenant_id')
|
|
1440
|
+
),
|
|
1441
|
+
expected(table_name, policy_name, command, roles, qualifier, check_expr) AS (
|
|
1442
|
+
SELECT table_name, 'tenant_isolation', '*', ARRAY['public'], discriminator, discriminator FROM protected
|
|
1443
|
+
UNION ALL VALUES
|
|
1444
|
+
('tenants', 'auth_definer_tenant_lookup', '*', ARRAY['open_loops_owner'], 'true', NULL),
|
|
1445
|
+
('tenant_memberships', 'auth_definer_membership_lookup', '*', ARRAY['open_loops_owner'], 'true', NULL),
|
|
1446
|
+
('tenant_membership_roles', 'auth_definer_membership_roles_lookup', '*', ARRAY['open_loops_owner'], 'true', NULL),
|
|
1447
|
+
('api_keys', 'auth_definer_key_lookup', '*', ARRAY['open_loops_owner'], 'true', NULL),
|
|
1448
|
+
('audit_events', 'auth_definer_audit_insert', 'a', ARRAY['open_loops_owner'], NULL, 'true')
|
|
1449
|
+
),
|
|
1450
|
+
actual AS (
|
|
1451
|
+
SELECT class.relname AS table_name,
|
|
1452
|
+
policy.polname AS policy_name,
|
|
1453
|
+
policy.polcmd::text AS command,
|
|
1454
|
+
policy.polpermissive AS permissive,
|
|
1455
|
+
ARRAY(
|
|
1456
|
+
SELECT CASE WHEN role_oid = 0 THEN 'public' ELSE role.rolname::text END
|
|
1457
|
+
FROM unnest(policy.polroles) role_oid
|
|
1458
|
+
LEFT JOIN pg_roles role ON role.oid = role_oid
|
|
1459
|
+
ORDER BY 1
|
|
1460
|
+
) AS roles,
|
|
1461
|
+
COALESCE(regexp_replace(pg_get_expr(policy.polqual, policy.polrelid), '\\s+', ' ', 'g'), '') AS qualifier,
|
|
1462
|
+
COALESCE(regexp_replace(pg_get_expr(policy.polwithcheck, policy.polrelid), '\\s+', ' ', 'g'), '') AS check_expr
|
|
1463
|
+
FROM pg_policy policy
|
|
1464
|
+
JOIN pg_class class ON class.oid = policy.polrelid
|
|
1465
|
+
JOIN pg_namespace namespace ON namespace.oid = class.relnamespace
|
|
1466
|
+
WHERE namespace.nspname = 'public'
|
|
1467
|
+
AND class.relname IN (SELECT table_name FROM protected)
|
|
1468
|
+
),
|
|
1469
|
+
missing_or_bad AS (
|
|
1470
|
+
SELECT expected.*
|
|
1471
|
+
FROM expected
|
|
1472
|
+
LEFT JOIN actual
|
|
1473
|
+
ON actual.table_name = expected.table_name
|
|
1474
|
+
AND actual.policy_name = expected.policy_name
|
|
1475
|
+
WHERE actual.policy_name IS NULL
|
|
1476
|
+
OR NOT actual.permissive
|
|
1477
|
+
OR actual.command <> expected.command
|
|
1478
|
+
OR actual.roles <> expected.roles
|
|
1479
|
+
OR NOT CASE expected.qualifier
|
|
1480
|
+
WHEN 'tenant_id' THEN actual.qualifier = ANY(ARRAY[
|
|
1481
|
+
'(tenant_id = open_loops_current_tenant_id())',
|
|
1482
|
+
'(tenant_id = public.open_loops_current_tenant_id())'
|
|
1483
|
+
])
|
|
1484
|
+
WHEN 'id' THEN actual.qualifier = ANY(ARRAY[
|
|
1485
|
+
'(id = open_loops_current_tenant_id())',
|
|
1486
|
+
'(id = public.open_loops_current_tenant_id())'
|
|
1487
|
+
])
|
|
1488
|
+
WHEN 'true' THEN actual.qualifier = 'true'
|
|
1489
|
+
ELSE actual.qualifier = ''
|
|
1490
|
+
END
|
|
1491
|
+
OR NOT CASE expected.check_expr
|
|
1492
|
+
WHEN 'tenant_id' THEN actual.check_expr = ANY(ARRAY[
|
|
1493
|
+
'(tenant_id = open_loops_current_tenant_id())',
|
|
1494
|
+
'(tenant_id = public.open_loops_current_tenant_id())'
|
|
1495
|
+
])
|
|
1496
|
+
WHEN 'id' THEN actual.check_expr = ANY(ARRAY[
|
|
1497
|
+
'(id = open_loops_current_tenant_id())',
|
|
1498
|
+
'(id = public.open_loops_current_tenant_id())'
|
|
1499
|
+
])
|
|
1500
|
+
WHEN 'true' THEN actual.check_expr = 'true'
|
|
1501
|
+
ELSE actual.check_expr = ''
|
|
1502
|
+
END
|
|
1503
|
+
),
|
|
1504
|
+
unexpected AS (
|
|
1505
|
+
SELECT actual.*
|
|
1506
|
+
FROM actual
|
|
1507
|
+
LEFT JOIN expected
|
|
1508
|
+
ON expected.table_name = actual.table_name
|
|
1509
|
+
AND expected.policy_name = actual.policy_name
|
|
1510
|
+
WHERE expected.policy_name IS NULL
|
|
1511
|
+
)
|
|
1512
|
+
SELECT 1 FROM missing_or_bad
|
|
1513
|
+
UNION ALL
|
|
1514
|
+
SELECT 1 FROM unexpected
|
|
1515
|
+
) THEN
|
|
1516
|
+
RAISE EXCEPTION 'tenant enforcement did not install the exact protected-table RLS policy inventory';
|
|
1517
|
+
END IF;
|
|
1518
|
+
IF EXISTS (
|
|
1519
|
+
WITH runtime_tables(table_name) AS (
|
|
1520
|
+
VALUES
|
|
1521
|
+
('loops'), ('loop_runs'), ('daemon_lease'), ('workflow_specs'), ('workflow_runs'),
|
|
1522
|
+
('workflow_invocations'), ('workflow_work_items'), ('workflow_step_runs'), ('workflow_events'),
|
|
1523
|
+
('goals'), ('goal_plan_nodes'), ('goal_runs'), ('runner_machines'), ('runner_leases'), ('run_receipts')
|
|
1524
|
+
),
|
|
1525
|
+
protected_tables(table_name) AS (
|
|
1526
|
+
VALUES
|
|
1527
|
+
('tenants'), ('tenant_memberships'), ('tenant_membership_roles'), ('api_keys'), ('audit_events'),
|
|
1528
|
+
('loops'), ('loop_runs'), ('daemon_lease'), ('workflow_specs'), ('workflow_runs'),
|
|
1529
|
+
('workflow_invocations'), ('workflow_work_items'), ('workflow_step_runs'), ('workflow_events'),
|
|
1530
|
+
('goals'), ('goal_plan_nodes'), ('goal_runs'), ('runner_machines'), ('runner_leases'), ('run_receipts')
|
|
1531
|
+
)
|
|
1532
|
+
SELECT 1
|
|
1533
|
+
FROM protected_tables protected
|
|
1534
|
+
JOIN pg_class class ON class.oid = format('public.%I', protected.table_name)::regclass
|
|
1535
|
+
CROSS JOIN LATERAL aclexplode(class.relacl) acl
|
|
1536
|
+
LEFT JOIN pg_roles grantee ON grantee.oid = acl.grantee
|
|
1537
|
+
WHERE acl.privilege_type IS NOT NULL
|
|
1538
|
+
AND NOT (
|
|
1539
|
+
protected.table_name IN (SELECT table_name FROM runtime_tables)
|
|
1540
|
+
AND COALESCE(grantee.rolname = 'open_loops_runtime', false)
|
|
1541
|
+
AND acl.privilege_type IN ('SELECT', 'INSERT', 'UPDATE', 'DELETE')
|
|
1542
|
+
AND NOT acl.is_grantable
|
|
1543
|
+
)
|
|
1544
|
+
AND NOT (
|
|
1545
|
+
protected.table_name = 'tenants'
|
|
1546
|
+
AND COALESCE(grantee.rolname = 'open_loops_runtime', false)
|
|
1547
|
+
AND acl.privilege_type IN ('SELECT', 'UPDATE', 'REFERENCES')
|
|
1548
|
+
AND NOT acl.is_grantable
|
|
1549
|
+
)
|
|
1550
|
+
AND NOT COALESCE(grantee.rolname = 'open_loops_owner', false)
|
|
1551
|
+
) THEN
|
|
1552
|
+
RAISE EXCEPTION 'tenant enforcement left unexpected protected table grants';
|
|
1553
|
+
END IF;
|
|
1554
|
+
IF NOT has_column_privilege('open_loops_runtime', 'public.tenants', 'id', 'UPDATE') THEN
|
|
1555
|
+
RAISE EXCEPTION 'tenant enforcement did not grant runtime tenant key lock privilege';
|
|
1556
|
+
END IF;
|
|
1557
|
+
IF NOT EXISTS (
|
|
1558
|
+
SELECT 1
|
|
1559
|
+
FROM pg_trigger trigger
|
|
1560
|
+
JOIN pg_proc proc ON proc.oid = trigger.tgfoid
|
|
1561
|
+
WHERE trigger.tgrelid = 'public.tenants'::regclass
|
|
1562
|
+
AND trigger.tgname = 'open_loops_reject_runtime_tenant_update'
|
|
1563
|
+
AND NOT trigger.tgisinternal
|
|
1564
|
+
AND trigger.tgenabled = 'O'
|
|
1565
|
+
AND proc.oid = 'public.open_loops_reject_runtime_tenant_update()'::regprocedure
|
|
1566
|
+
AND pg_get_userbyid(proc.proowner) = 'open_loops_owner'
|
|
1567
|
+
AND NOT proc.prosecdef
|
|
1568
|
+
AND COALESCE(proc.proconfig, ARRAY[]::text[]) @> ARRAY['search_path=pg_catalog']
|
|
1569
|
+
AND proc.prosrc ILIKE '%pg_has_role%open_loops_runtime%USAGE%'
|
|
1570
|
+
) THEN
|
|
1571
|
+
RAISE EXCEPTION 'tenant enforcement did not install runtime tenant update guard';
|
|
1572
|
+
END IF;
|
|
1573
|
+
IF EXISTS (
|
|
1574
|
+
SELECT 1
|
|
1575
|
+
FROM pg_class class
|
|
1576
|
+
JOIN pg_namespace namespace ON namespace.oid = class.relnamespace
|
|
1577
|
+
JOIN pg_attribute attribute ON attribute.attrelid = class.oid
|
|
1578
|
+
CROSS JOIN LATERAL aclexplode(attribute.attacl) acl
|
|
1579
|
+
LEFT JOIN pg_roles grantee ON grantee.oid = acl.grantee
|
|
1580
|
+
WHERE namespace.nspname = 'public'
|
|
1581
|
+
AND class.relname = ANY(ARRAY[
|
|
1582
|
+
'tenants', 'tenant_memberships', 'tenant_membership_roles', 'api_keys',
|
|
1583
|
+
'loops', 'loop_runs', 'daemon_lease', 'workflow_specs', 'workflow_runs',
|
|
1584
|
+
'workflow_invocations', 'workflow_work_items', 'workflow_step_runs', 'workflow_events',
|
|
1585
|
+
'goals', 'goal_plan_nodes', 'goal_runs', 'runner_machines', 'runner_leases',
|
|
1586
|
+
'audit_events', 'run_receipts'
|
|
1587
|
+
])
|
|
1588
|
+
AND attribute.attnum > 0
|
|
1589
|
+
AND NOT attribute.attisdropped
|
|
1590
|
+
AND acl.privilege_type IS NOT NULL
|
|
1591
|
+
) THEN
|
|
1592
|
+
RAISE EXCEPTION 'tenant enforcement left unexpected protected column grants';
|
|
1593
|
+
END IF;
|
|
1594
|
+
IF EXISTS (
|
|
1595
|
+
SELECT 1
|
|
1596
|
+
FROM pg_proc proc
|
|
1597
|
+
CROSS JOIN LATERAL aclexplode(COALESCE(proc.proacl, acldefault('f', proc.proowner))) acl
|
|
1598
|
+
LEFT JOIN pg_roles grantee ON grantee.oid = acl.grantee
|
|
1599
|
+
WHERE proc.oid = ANY(ARRAY[
|
|
1600
|
+
'public.open_loops_current_tenant_id()'::regprocedure,
|
|
1601
|
+
'public.open_loops_authenticate_key(text,text)'::regprocedure,
|
|
1602
|
+
'public.open_loops_append_auth_audit(text,text,text,text,text,text,text,jsonb)'::regprocedure
|
|
1603
|
+
])
|
|
1604
|
+
AND acl.privilege_type = 'EXECUTE'
|
|
1605
|
+
AND NOT (
|
|
1606
|
+
proc.oid = 'public.open_loops_current_tenant_id()'::regprocedure
|
|
1607
|
+
AND grantee.rolname = 'open_loops_runtime'
|
|
1608
|
+
AND NOT acl.is_grantable
|
|
1609
|
+
)
|
|
1610
|
+
AND NOT (
|
|
1611
|
+
proc.oid = ANY(ARRAY[
|
|
1612
|
+
'public.open_loops_authenticate_key(text,text)'::regprocedure,
|
|
1613
|
+
'public.open_loops_append_auth_audit(text,text,text,text,text,text,text,jsonb)'::regprocedure
|
|
1614
|
+
])
|
|
1615
|
+
AND grantee.rolname = 'open_loops_authenticator'
|
|
1616
|
+
AND NOT acl.is_grantable
|
|
1617
|
+
)
|
|
1618
|
+
) THEN
|
|
1619
|
+
RAISE EXCEPTION 'tenant enforcement left unexpected authentication function grants';
|
|
1620
|
+
END IF;
|
|
1621
|
+
IF EXISTS (
|
|
1622
|
+
SELECT 1
|
|
1623
|
+
FROM pg_proc function
|
|
1624
|
+
JOIN pg_namespace namespace ON namespace.oid=function.pronamespace
|
|
1625
|
+
CROSS JOIN LATERAL aclexplode(COALESCE(function.proacl, acldefault('f', function.proowner))) acl
|
|
1626
|
+
LEFT JOIN pg_roles grantee ON grantee.oid = acl.grantee
|
|
1627
|
+
WHERE namespace.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
1628
|
+
AND namespace.nspname NOT LIKE 'pg_toast%'
|
|
1629
|
+
AND namespace.nspname NOT LIKE 'pg_temp_%'
|
|
1630
|
+
AND acl.privilege_type = 'EXECUTE'
|
|
1631
|
+
AND NOT acl.is_grantable
|
|
1632
|
+
AND acl.grantee <> function.proowner
|
|
1633
|
+
AND NOT (
|
|
1634
|
+
namespace.nspname='public'
|
|
1635
|
+
AND function.oid = 'public.open_loops_current_tenant_id()'::regprocedure
|
|
1636
|
+
AND COALESCE(grantee.rolname IN ('open_loops_owner', 'open_loops_runtime'), false)
|
|
1637
|
+
)
|
|
1638
|
+
AND NOT (
|
|
1639
|
+
namespace.nspname='public'
|
|
1640
|
+
AND function.oid = ANY(ARRAY[
|
|
1641
|
+
'public.open_loops_authenticate_key(text,text)'::regprocedure,
|
|
1642
|
+
'public.open_loops_append_auth_audit(text,text,text,text,text,text,text,jsonb)'::regprocedure
|
|
1643
|
+
])
|
|
1644
|
+
AND COALESCE(grantee.rolname IN ('open_loops_owner', 'open_loops_authenticator'), false)
|
|
1645
|
+
)
|
|
1646
|
+
) THEN
|
|
1647
|
+
RAISE EXCEPTION 'tenant enforcement left unexpected function privileges outside the OpenLoops auth surface';
|
|
1648
|
+
END IF;
|
|
1649
|
+
IF EXISTS (
|
|
1650
|
+
SELECT 1
|
|
1651
|
+
FROM pg_auth_members membership
|
|
1652
|
+
JOIN pg_roles granted ON granted.oid = membership.roleid
|
|
1653
|
+
JOIN pg_roles member ON member.oid = membership.member
|
|
1654
|
+
WHERE granted.rolname IN ('open_loops_runtime', 'open_loops_authenticator')
|
|
1655
|
+
AND member.rolcanlogin
|
|
1656
|
+
AND NOT EXISTS (
|
|
1657
|
+
SELECT 1
|
|
1658
|
+
FROM pg_database database
|
|
1659
|
+
CROSS JOIN LATERAL aclexplode(COALESCE(database.datacl, acldefault('d', database.datdba))) acl
|
|
1660
|
+
WHERE database.datname = current_database()
|
|
1661
|
+
AND acl.grantee = member.oid
|
|
1662
|
+
AND acl.privilege_type = 'CONNECT'
|
|
1663
|
+
)
|
|
1664
|
+
) THEN
|
|
1665
|
+
RAISE EXCEPTION 'tenant enforcement did not grant direct database CONNECT to every service login';
|
|
1666
|
+
END IF;
|
|
1667
|
+
END
|
|
1668
|
+
$tenant_enforcement_postconditions$;
|
|
1669
|
+
|
|
1670
|
+
DROP TABLE api_key_tenant_bindings;
|
|
1671
|
+
DROP TABLE tenant_row_assignments;
|
|
355
1672
|
`)
|
|
356
1673
|
]);
|
|
357
1674
|
|
|
@@ -370,34 +1687,27 @@ class PostgresStorage {
|
|
|
370
1687
|
}
|
|
371
1688
|
async migrate(opts = {}) {
|
|
372
1689
|
const dryRun = opts.dryRun === true;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
1690
|
+
const throughIndex = opts.through === undefined ? this.migrations.length - 1 : this.migrations.findIndex((migration2) => migration2.id === opts.through);
|
|
1691
|
+
if (throughIndex < 0)
|
|
1692
|
+
throw new Error(`Unknown Postgres migration target ${opts.through}`);
|
|
1693
|
+
if (dryRun) {
|
|
1694
|
+
const applied = await this.tryReadAppliedMigrations();
|
|
1695
|
+
const plan2 = this.buildPlan(applied);
|
|
1696
|
+
return { backend: this.backend, dryRun, applied, plan: plan2 };
|
|
381
1697
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
if (dryRun)
|
|
394
|
-
return { backend: this.backend, dryRun, applied, plan };
|
|
395
|
-
const run = async () => {
|
|
396
|
-
for (const item of plan) {
|
|
1698
|
+
await this.ensureLedger();
|
|
1699
|
+
let plan = [];
|
|
1700
|
+
const run = async (executor = this.executor) => {
|
|
1701
|
+
await executor.query(POSTGRES_MIGRATION_ADVISORY_LOCK_SQL);
|
|
1702
|
+
const lockedApplied = await this.readAppliedMigrations(executor);
|
|
1703
|
+
plan = this.buildPlan(lockedApplied);
|
|
1704
|
+
for (const [index, item] of plan.entries()) {
|
|
1705
|
+
if (index > throughIndex)
|
|
1706
|
+
break;
|
|
397
1707
|
if (item.state === "already_applied")
|
|
398
1708
|
continue;
|
|
399
|
-
await
|
|
400
|
-
await
|
|
1709
|
+
await executor.execute(item.migration.sql);
|
|
1710
|
+
await executor.execute(`INSERT INTO ${POSTGRES_MIGRATION_LEDGER_TABLE} (id, checksum, applied_at) VALUES ($1, $2, NOW())`, [item.migration.id, item.migration.checksum]);
|
|
401
1711
|
}
|
|
402
1712
|
};
|
|
403
1713
|
if (this.executor.transaction)
|
|
@@ -423,8 +1733,8 @@ CREATE TABLE IF NOT EXISTS ${POSTGRES_MIGRATION_LEDGER_TABLE} (
|
|
|
423
1733
|
);
|
|
424
1734
|
`);
|
|
425
1735
|
}
|
|
426
|
-
async readAppliedMigrations() {
|
|
427
|
-
const rows = await
|
|
1736
|
+
async readAppliedMigrations(executor = this.executor) {
|
|
1737
|
+
const rows = await executor.query(`SELECT id, checksum, applied_at FROM ${POSTGRES_MIGRATION_LEDGER_TABLE} ORDER BY id ASC`);
|
|
428
1738
|
return rows.map((row) => ({
|
|
429
1739
|
id: row.id,
|
|
430
1740
|
checksum: row.checksum,
|
|
@@ -440,6 +1750,25 @@ CREATE TABLE IF NOT EXISTS ${POSTGRES_MIGRATION_LEDGER_TABLE} (
|
|
|
440
1750
|
throw error;
|
|
441
1751
|
}
|
|
442
1752
|
}
|
|
1753
|
+
buildPlan(applied) {
|
|
1754
|
+
const knownMigrationIds = new Set(this.migrations.map((migration2) => migration2.id));
|
|
1755
|
+
for (const row of applied) {
|
|
1756
|
+
if (!knownMigrationIds.has(row.id)) {
|
|
1757
|
+
throw new Error(`Postgres migration ${row.id} is not recognized by this binary`);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
const appliedById = new Map(applied.map((row) => [row.id, row]));
|
|
1761
|
+
for (const migration2 of this.migrations) {
|
|
1762
|
+
const existing = appliedById.get(migration2.id);
|
|
1763
|
+
if (existing && existing.checksum !== migration2.checksum) {
|
|
1764
|
+
throw new Error(`Postgres migration checksum mismatch for ${migration2.id}`);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
return this.migrations.map((migration2) => ({
|
|
1768
|
+
migration: migration2,
|
|
1769
|
+
state: appliedById.has(migration2.id) ? "already_applied" : "pending"
|
|
1770
|
+
}));
|
|
1771
|
+
}
|
|
443
1772
|
}
|
|
444
1773
|
function createPostgresStorage(executor) {
|
|
445
1774
|
return new PostgresStorage(executor);
|