@automagik/omni 2.260723.1 → 2.260727.1
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/db/drizzle/0040_multitenancy_control_plane.sql +435 -0
- package/db/drizzle/0041_tenant_ownership_columns.sql +3423 -0
- package/db/drizzle/meta/_journal.json +14 -0
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/keys.d.ts.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/index.js +1132 -32
- package/dist/lib/credential-status.d.ts +28 -0
- package/dist/lib/credential-status.d.ts.map +1 -0
- package/dist/lib/role-cutover.d.ts +18 -0
- package/dist/lib/role-cutover.d.ts.map +1 -1
- package/dist/runtime-env.d.ts.map +1 -1
- package/dist/sdk/client.d.ts +27 -0
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/server/index.js +3877 -773
- package/package.json +1 -1
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
-- Multitenancy control plane — additive foundation (wish: omni-full-multitenancy, Group G1).
|
|
2
|
+
--
|
|
3
|
+
-- Establishes first-class tenant identity, principals/memberships, a bounded
|
|
4
|
+
-- fixed-role registry, an ISOLATED authentication credential index, separate
|
|
5
|
+
-- platform credentials, tenant key lineage/delegation foundations, and split
|
|
6
|
+
-- tenant/platform audit stores. Frozen G0 contract: ADR-0001/0003/0005/0006/0010
|
|
7
|
+
-- and OWNERSHIP_MATRIX "New ownership/control tables".
|
|
8
|
+
--
|
|
9
|
+
-- This migration is purely additive:
|
|
10
|
+
-- * It does NOT touch legacy `api_keys` or any existing business table.
|
|
11
|
+
-- * It adds NO tenant_id to existing tables (that is Group G2).
|
|
12
|
+
-- * It adds NO RLS policies or runtime roles (that is Group G3).
|
|
13
|
+
--
|
|
14
|
+
-- No hard tenant delete: every foreign key referencing `tenants` uses
|
|
15
|
+
-- ON DELETE RESTRICT (or NO ACTION for deferred/self references) so nothing can
|
|
16
|
+
-- cascade-delete a tenant or erase security/audit lineage. Tenant lifecycle
|
|
17
|
+
-- ends at `archived`.
|
|
18
|
+
--
|
|
19
|
+
-- Hand-written per the established convention (see 0032_genie_hosts.sql):
|
|
20
|
+
-- CREATE TABLE IF NOT EXISTS + explicit constraints, journal entry only, no
|
|
21
|
+
-- drizzle snapshot. Idempotent so a partial apply can be safely re-run.
|
|
22
|
+
|
|
23
|
+
-- ---------------------------------------------------------------------------
|
|
24
|
+
-- tenants — platform control plane
|
|
25
|
+
-- ---------------------------------------------------------------------------
|
|
26
|
+
CREATE TABLE IF NOT EXISTS "tenants" (
|
|
27
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
28
|
+
"slug" varchar(63) NOT NULL,
|
|
29
|
+
"display_name" varchar(255) NOT NULL,
|
|
30
|
+
"status" varchar(20) DEFAULT 'active' NOT NULL,
|
|
31
|
+
"policy_version" integer DEFAULT 1 NOT NULL,
|
|
32
|
+
"revocation_epoch" integer DEFAULT 0 NOT NULL,
|
|
33
|
+
"max_key_ttl_seconds" integer NOT NULL,
|
|
34
|
+
"max_key_rate_limit" integer NOT NULL,
|
|
35
|
+
"max_key_budget" integer NOT NULL,
|
|
36
|
+
"created_by_principal_id" uuid,
|
|
37
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
38
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
39
|
+
"suspended_at" timestamp with time zone,
|
|
40
|
+
"archived_at" timestamp with time zone,
|
|
41
|
+
CONSTRAINT "tenants_status_check" CHECK ("status" IN ('active', 'suspended', 'archived')),
|
|
42
|
+
CONSTRAINT "tenants_slug_format_check" CHECK ("slug" ~ '^[a-z0-9][a-z0-9-]*$'),
|
|
43
|
+
CONSTRAINT "tenants_epochs_check" CHECK ("policy_version" >= 1 AND "revocation_epoch" >= 0),
|
|
44
|
+
CONSTRAINT "tenants_key_policy_check" CHECK ("max_key_ttl_seconds" > 0 AND "max_key_rate_limit" > 0 AND "max_key_budget" > 0)
|
|
45
|
+
);
|
|
46
|
+
--> statement-breakpoint
|
|
47
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "tenants_slug_uq" ON "tenants" ("slug");
|
|
48
|
+
--> statement-breakpoint
|
|
49
|
+
CREATE INDEX IF NOT EXISTS "tenants_status_idx" ON "tenants" ("status");
|
|
50
|
+
--> statement-breakpoint
|
|
51
|
+
|
|
52
|
+
-- Hard deletion is forbidden even for an otherwise-unreferenced tenant. The
|
|
53
|
+
-- lifecycle terminates at archived; future purge workflows require a separate,
|
|
54
|
+
-- explicitly approved model rather than DELETE.
|
|
55
|
+
CREATE OR REPLACE FUNCTION "reject_tenant_hard_delete"()
|
|
56
|
+
RETURNS trigger
|
|
57
|
+
LANGUAGE plpgsql
|
|
58
|
+
AS $$
|
|
59
|
+
BEGIN
|
|
60
|
+
RAISE EXCEPTION 'tenant hard delete is forbidden';
|
|
61
|
+
END;
|
|
62
|
+
$$;
|
|
63
|
+
--> statement-breakpoint
|
|
64
|
+
DROP TRIGGER IF EXISTS "tenants_no_hard_delete" ON "tenants";
|
|
65
|
+
--> statement-breakpoint
|
|
66
|
+
CREATE TRIGGER "tenants_no_hard_delete"
|
|
67
|
+
BEFORE DELETE ON "tenants"
|
|
68
|
+
FOR EACH ROW EXECUTE FUNCTION "reject_tenant_hard_delete"();
|
|
69
|
+
--> statement-breakpoint
|
|
70
|
+
|
|
71
|
+
-- ---------------------------------------------------------------------------
|
|
72
|
+
-- principals — platform identity plane
|
|
73
|
+
-- ---------------------------------------------------------------------------
|
|
74
|
+
CREATE TABLE IF NOT EXISTS "principals" (
|
|
75
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
76
|
+
"type" varchar(20) NOT NULL,
|
|
77
|
+
"subject" varchar(255) NOT NULL,
|
|
78
|
+
"display_name" varchar(255),
|
|
79
|
+
"status" varchar(20) DEFAULT 'active' NOT NULL,
|
|
80
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
81
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
82
|
+
"disabled_at" timestamp with time zone,
|
|
83
|
+
CONSTRAINT "principals_type_check" CHECK ("type" IN ('human', 'service')),
|
|
84
|
+
CONSTRAINT "principals_status_check" CHECK ("status" IN ('active', 'disabled'))
|
|
85
|
+
);
|
|
86
|
+
--> statement-breakpoint
|
|
87
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "principals_subject_uq" ON "principals" ("subject");
|
|
88
|
+
--> statement-breakpoint
|
|
89
|
+
CREATE INDEX IF NOT EXISTS "principals_status_idx" ON "principals" ("status");
|
|
90
|
+
--> statement-breakpoint
|
|
91
|
+
|
|
92
|
+
-- Deferred FK: tenants.created_by_principal_id -> principals.id (RESTRICT).
|
|
93
|
+
DO $$ BEGIN
|
|
94
|
+
ALTER TABLE "tenants" ADD CONSTRAINT "tenants_created_by_principal_id_fk"
|
|
95
|
+
FOREIGN KEY ("created_by_principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT;
|
|
96
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
97
|
+
END $$;
|
|
98
|
+
--> statement-breakpoint
|
|
99
|
+
|
|
100
|
+
-- ---------------------------------------------------------------------------
|
|
101
|
+
-- tenant_memberships — principal <-> tenant role relation
|
|
102
|
+
-- ---------------------------------------------------------------------------
|
|
103
|
+
CREATE TABLE IF NOT EXISTS "tenant_memberships" (
|
|
104
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
105
|
+
"tenant_id" uuid NOT NULL,
|
|
106
|
+
"principal_id" uuid NOT NULL,
|
|
107
|
+
"role" varchar(32) NOT NULL,
|
|
108
|
+
"status" varchar(20) DEFAULT 'active' NOT NULL,
|
|
109
|
+
"invited_by_principal_id" uuid,
|
|
110
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
111
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
112
|
+
"disabled_at" timestamp with time zone,
|
|
113
|
+
CONSTRAINT "tenant_memberships_role_check" CHECK ("role" IN ('tenant-owner', 'tenant-admin', 'tenant-operator', 'tenant-viewer')),
|
|
114
|
+
CONSTRAINT "tenant_memberships_status_check" CHECK ("status" IN ('active', 'disabled')),
|
|
115
|
+
CONSTRAINT "tenant_memberships_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT,
|
|
116
|
+
CONSTRAINT "tenant_memberships_principal_id_fk" FOREIGN KEY ("principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT,
|
|
117
|
+
CONSTRAINT "tenant_memberships_invited_by_principal_id_fk" FOREIGN KEY ("invited_by_principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT
|
|
118
|
+
);
|
|
119
|
+
--> statement-breakpoint
|
|
120
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "tenant_memberships_tenant_principal_uq" ON "tenant_memberships" ("tenant_id", "principal_id");
|
|
121
|
+
--> statement-breakpoint
|
|
122
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "tenant_memberships_tenant_id_uq" ON "tenant_memberships" ("tenant_id", "id");
|
|
123
|
+
--> statement-breakpoint
|
|
124
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "tenant_memberships_tenant_principal_id_uq" ON "tenant_memberships" ("tenant_id", "principal_id", "id");
|
|
125
|
+
--> statement-breakpoint
|
|
126
|
+
CREATE INDEX IF NOT EXISTS "tenant_memberships_tenant_idx" ON "tenant_memberships" ("tenant_id");
|
|
127
|
+
--> statement-breakpoint
|
|
128
|
+
CREATE INDEX IF NOT EXISTS "tenant_memberships_principal_idx" ON "tenant_memberships" ("principal_id");
|
|
129
|
+
--> statement-breakpoint
|
|
130
|
+
|
|
131
|
+
-- ---------------------------------------------------------------------------
|
|
132
|
+
-- tenant_role_policies — fixed, bounded role ceiling registry (seed data)
|
|
133
|
+
-- ---------------------------------------------------------------------------
|
|
134
|
+
CREATE TABLE IF NOT EXISTS "tenant_role_policies" (
|
|
135
|
+
"role" varchar(32) PRIMARY KEY NOT NULL,
|
|
136
|
+
"description" text NOT NULL,
|
|
137
|
+
"max_scopes" text[] NOT NULL,
|
|
138
|
+
"can_manage_memberships" boolean NOT NULL,
|
|
139
|
+
"can_delegate_keys" boolean NOT NULL,
|
|
140
|
+
"max_delegation_depth" integer DEFAULT 1 NOT NULL,
|
|
141
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
142
|
+
CONSTRAINT "tenant_role_policies_role_check" CHECK ("role" IN ('tenant-owner', 'tenant-admin', 'tenant-operator', 'tenant-viewer')),
|
|
143
|
+
CONSTRAINT "tenant_role_policies_depth_check" CHECK ("max_delegation_depth" >= 0),
|
|
144
|
+
CONSTRAINT "tenant_role_policies_no_platform_authority_check" CHECK (
|
|
145
|
+
cardinality("max_scopes") > 0
|
|
146
|
+
AND array_position("max_scopes", NULL) IS NULL
|
|
147
|
+
AND NOT ('*' = ANY("max_scopes"))
|
|
148
|
+
AND array_to_string("max_scopes", ',') !~ '(^|,)platform:'
|
|
149
|
+
),
|
|
150
|
+
CONSTRAINT "tenant_role_policies_fixed_ceiling_check" CHECK (
|
|
151
|
+
("role" = 'tenant-owner' AND "max_scopes" = ARRAY['tenant:*', 'keys:delegate']::text[] AND "can_manage_memberships" AND "can_delegate_keys" AND "max_delegation_depth" = 1)
|
|
152
|
+
OR ("role" = 'tenant-admin' AND "max_scopes" = ARRAY['tenant:*', 'keys:delegate']::text[] AND "can_manage_memberships" AND "can_delegate_keys" AND "max_delegation_depth" = 1)
|
|
153
|
+
OR ("role" = 'tenant-operator' AND "max_scopes" = ARRAY['tenant:read', 'tenant:write']::text[] AND NOT "can_manage_memberships" AND NOT "can_delegate_keys" AND "max_delegation_depth" = 0)
|
|
154
|
+
OR ("role" = 'tenant-viewer' AND "max_scopes" = ARRAY['tenant:read']::text[] AND NOT "can_manage_memberships" AND NOT "can_delegate_keys" AND "max_delegation_depth" = 0)
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
--> statement-breakpoint
|
|
158
|
+
|
|
159
|
+
-- Seed the four fixed roles with bounded ceilings. No role carries '*'.
|
|
160
|
+
INSERT INTO "tenant_role_policies" ("role", "description", "max_scopes", "can_manage_memberships", "can_delegate_keys", "max_delegation_depth")
|
|
161
|
+
VALUES
|
|
162
|
+
('tenant-owner', 'Membership/lifecycle authority inside the tenant; cannot create platform authority.', ARRAY['tenant:*', 'keys:delegate']::text[], true, true, 1),
|
|
163
|
+
('tenant-admin', 'Full tenant resource administration and bounded delegation.', ARRAY['tenant:*', 'keys:delegate']::text[], true, true, 1),
|
|
164
|
+
('tenant-operator', 'Operational write access without membership/key-policy administration.', ARRAY['tenant:read', 'tenant:write']::text[], false, false, 0),
|
|
165
|
+
('tenant-viewer', 'Read-only tenant access.', ARRAY['tenant:read']::text[], false, false, 0)
|
|
166
|
+
ON CONFLICT ("role") DO UPDATE SET
|
|
167
|
+
"description" = EXCLUDED."description",
|
|
168
|
+
"max_scopes" = EXCLUDED."max_scopes",
|
|
169
|
+
"can_manage_memberships" = EXCLUDED."can_manage_memberships",
|
|
170
|
+
"can_delegate_keys" = EXCLUDED."can_delegate_keys",
|
|
171
|
+
"max_delegation_depth" = EXCLUDED."max_delegation_depth";
|
|
172
|
+
--> statement-breakpoint
|
|
173
|
+
|
|
174
|
+
-- ---------------------------------------------------------------------------
|
|
175
|
+
-- platform_api_keys — platform credential class (break-glass / automation)
|
|
176
|
+
-- ---------------------------------------------------------------------------
|
|
177
|
+
CREATE TABLE IF NOT EXISTS "platform_api_keys" (
|
|
178
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
179
|
+
"name" varchar(255) NOT NULL,
|
|
180
|
+
"description" text,
|
|
181
|
+
"key_prefix" varchar(12) NOT NULL,
|
|
182
|
+
"key_hash" varchar(64) NOT NULL,
|
|
183
|
+
"scopes" text[] NOT NULL,
|
|
184
|
+
"status" varchar(20) DEFAULT 'active' NOT NULL,
|
|
185
|
+
"principal_id" uuid NOT NULL,
|
|
186
|
+
"created_by_principal_id" uuid,
|
|
187
|
+
"expires_at" timestamp with time zone,
|
|
188
|
+
"last_used_at" timestamp with time zone,
|
|
189
|
+
"revoked_at" timestamp with time zone,
|
|
190
|
+
"revoked_by" varchar(255),
|
|
191
|
+
"revoke_reason" text,
|
|
192
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
193
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
194
|
+
CONSTRAINT "platform_api_keys_status_check" CHECK ("status" IN ('active', 'revoked')),
|
|
195
|
+
CONSTRAINT "platform_api_keys_principal_id_fk" FOREIGN KEY ("principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT,
|
|
196
|
+
CONSTRAINT "platform_api_keys_created_by_principal_id_fk" FOREIGN KEY ("created_by_principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT
|
|
197
|
+
);
|
|
198
|
+
--> statement-breakpoint
|
|
199
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "platform_api_keys_name_uq" ON "platform_api_keys" ("name");
|
|
200
|
+
--> statement-breakpoint
|
|
201
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "platform_api_keys_key_hash_uq" ON "platform_api_keys" ("key_hash");
|
|
202
|
+
--> statement-breakpoint
|
|
203
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "platform_api_keys_id_principal_uq" ON "platform_api_keys" ("id", "principal_id");
|
|
204
|
+
--> statement-breakpoint
|
|
205
|
+
CREATE INDEX IF NOT EXISTS "platform_api_keys_key_prefix_idx" ON "platform_api_keys" ("key_prefix");
|
|
206
|
+
--> statement-breakpoint
|
|
207
|
+
CREATE INDEX IF NOT EXISTS "platform_api_keys_status_idx" ON "platform_api_keys" ("status");
|
|
208
|
+
--> statement-breakpoint
|
|
209
|
+
|
|
210
|
+
-- ---------------------------------------------------------------------------
|
|
211
|
+
-- tenant_key_lineage — tenant-visible key metadata + delegation lineage
|
|
212
|
+
-- ---------------------------------------------------------------------------
|
|
213
|
+
CREATE TABLE IF NOT EXISTS "tenant_key_lineage" (
|
|
214
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
215
|
+
"tenant_id" uuid NOT NULL,
|
|
216
|
+
"principal_id" uuid NOT NULL,
|
|
217
|
+
"membership_id" uuid NOT NULL,
|
|
218
|
+
"actor_role" varchar(32) NOT NULL,
|
|
219
|
+
"name" varchar(255) NOT NULL,
|
|
220
|
+
"key_prefix" varchar(12) NOT NULL,
|
|
221
|
+
"scopes" text[] NOT NULL,
|
|
222
|
+
"resource_constraints" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
223
|
+
"status" varchar(20) DEFAULT 'active' NOT NULL,
|
|
224
|
+
"parent_key_id" uuid,
|
|
225
|
+
"root_key_id" uuid NOT NULL,
|
|
226
|
+
"depth" integer DEFAULT 0 NOT NULL,
|
|
227
|
+
"created_by_principal_id" uuid,
|
|
228
|
+
"expires_at" timestamp with time zone,
|
|
229
|
+
"rate_limit" integer,
|
|
230
|
+
"budget" integer,
|
|
231
|
+
"revoked_at" timestamp with time zone,
|
|
232
|
+
"revoke_reason" text,
|
|
233
|
+
"revocation_epoch" integer DEFAULT 0 NOT NULL,
|
|
234
|
+
"ancestor_revoked" boolean DEFAULT false NOT NULL,
|
|
235
|
+
"ceiling_snapshot" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
236
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
237
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
238
|
+
CONSTRAINT "tenant_key_lineage_role_check" CHECK ("actor_role" IN ('tenant-owner', 'tenant-admin', 'tenant-operator', 'tenant-viewer')),
|
|
239
|
+
CONSTRAINT "tenant_key_lineage_status_check" CHECK ("status" IN ('active', 'revoked', 'expired')),
|
|
240
|
+
CONSTRAINT "tenant_key_lineage_depth_check" CHECK ("depth" >= 0),
|
|
241
|
+
CONSTRAINT "tenant_key_lineage_positive_limits_check" CHECK (("rate_limit" IS NULL OR "rate_limit" > 0) AND ("budget" IS NULL OR "budget" > 0)),
|
|
242
|
+
CONSTRAINT "tenant_key_lineage_principal_membership_pair_check" CHECK (("principal_id" IS NULL AND "membership_id" IS NULL) OR ("principal_id" IS NOT NULL AND "membership_id" IS NOT NULL)),
|
|
243
|
+
CONSTRAINT "tenant_key_lineage_depth_shape_check" CHECK (("depth" = 0 AND "parent_key_id" IS NULL AND "root_key_id" = "id") OR ("depth" = 1 AND "parent_key_id" IS NOT NULL)),
|
|
244
|
+
CONSTRAINT "tenant_key_lineage_no_platform_authority_check" CHECK (
|
|
245
|
+
cardinality("scopes") > 0
|
|
246
|
+
AND array_position("scopes", NULL) IS NULL
|
|
247
|
+
AND NOT ('*' = ANY("scopes"))
|
|
248
|
+
AND array_to_string("scopes", ',') !~ '(^|,)platform:'
|
|
249
|
+
),
|
|
250
|
+
CONSTRAINT "tenant_key_lineage_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT,
|
|
251
|
+
CONSTRAINT "tenant_key_lineage_principal_id_fk" FOREIGN KEY ("principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT,
|
|
252
|
+
CONSTRAINT "tenant_key_lineage_membership_id_fk" FOREIGN KEY ("membership_id") REFERENCES "tenant_memberships" ("id") ON DELETE RESTRICT,
|
|
253
|
+
CONSTRAINT "tenant_key_lineage_membership_principal_fk" FOREIGN KEY ("tenant_id", "principal_id", "membership_id") REFERENCES "tenant_memberships" ("tenant_id", "principal_id", "id") ON DELETE RESTRICT,
|
|
254
|
+
CONSTRAINT "tenant_key_lineage_parent_key_id_fk" FOREIGN KEY ("parent_key_id") REFERENCES "tenant_key_lineage" ("id") ON DELETE RESTRICT,
|
|
255
|
+
CONSTRAINT "tenant_key_lineage_created_by_principal_id_fk" FOREIGN KEY ("created_by_principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT
|
|
256
|
+
);
|
|
257
|
+
--> statement-breakpoint
|
|
258
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "tenant_key_lineage_tenant_id_uq" ON "tenant_key_lineage" ("tenant_id", "id");
|
|
259
|
+
--> statement-breakpoint
|
|
260
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "tenant_key_lineage_auth_binding_uq" ON "tenant_key_lineage" ("tenant_id", "id", "principal_id", "membership_id", "actor_role");
|
|
261
|
+
--> statement-breakpoint
|
|
262
|
+
CREATE INDEX IF NOT EXISTS "tenant_key_lineage_tenant_idx" ON "tenant_key_lineage" ("tenant_id");
|
|
263
|
+
--> statement-breakpoint
|
|
264
|
+
CREATE INDEX IF NOT EXISTS "tenant_key_lineage_parent_idx" ON "tenant_key_lineage" ("parent_key_id");
|
|
265
|
+
--> statement-breakpoint
|
|
266
|
+
CREATE INDEX IF NOT EXISTS "tenant_key_lineage_root_idx" ON "tenant_key_lineage" ("root_key_id");
|
|
267
|
+
--> statement-breakpoint
|
|
268
|
+
CREATE INDEX IF NOT EXISTS "tenant_key_lineage_status_idx" ON "tenant_key_lineage" ("status");
|
|
269
|
+
--> statement-breakpoint
|
|
270
|
+
|
|
271
|
+
DO $$ BEGIN
|
|
272
|
+
ALTER TABLE "tenant_key_lineage" ADD CONSTRAINT "tenant_key_lineage_parent_tenant_fk"
|
|
273
|
+
FOREIGN KEY ("tenant_id", "parent_key_id") REFERENCES "tenant_key_lineage" ("tenant_id", "id") ON DELETE RESTRICT;
|
|
274
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
275
|
+
END $$;
|
|
276
|
+
--> statement-breakpoint
|
|
277
|
+
DO $$ BEGIN
|
|
278
|
+
ALTER TABLE "tenant_key_lineage" ADD CONSTRAINT "tenant_key_lineage_root_tenant_fk"
|
|
279
|
+
FOREIGN KEY ("tenant_id", "root_key_id") REFERENCES "tenant_key_lineage" ("tenant_id", "id") ON DELETE RESTRICT;
|
|
280
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
281
|
+
END $$;
|
|
282
|
+
--> statement-breakpoint
|
|
283
|
+
|
|
284
|
+
-- ---------------------------------------------------------------------------
|
|
285
|
+
-- auth_credentials — ISOLATED authentication index (platform-owned)
|
|
286
|
+
-- ---------------------------------------------------------------------------
|
|
287
|
+
CREATE TABLE IF NOT EXISTS "auth_credentials" (
|
|
288
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
289
|
+
"credential_class" varchar(20) NOT NULL,
|
|
290
|
+
"key_hash" varchar(64) NOT NULL,
|
|
291
|
+
"key_prefix" varchar(12) NOT NULL,
|
|
292
|
+
"tenant_id" uuid,
|
|
293
|
+
"principal_id" uuid,
|
|
294
|
+
"membership_id" uuid,
|
|
295
|
+
"actor_role" varchar(32),
|
|
296
|
+
"scopes" text[] NOT NULL,
|
|
297
|
+
"status" varchar(20) DEFAULT 'active' NOT NULL,
|
|
298
|
+
"tenant_key_lineage_id" uuid,
|
|
299
|
+
"platform_api_key_id" uuid,
|
|
300
|
+
"policy_snapshot_version" integer DEFAULT 1 NOT NULL,
|
|
301
|
+
"revocation_epoch_snapshot" integer DEFAULT 0 NOT NULL,
|
|
302
|
+
"expires_at" timestamp with time zone,
|
|
303
|
+
"revoked_at" timestamp with time zone,
|
|
304
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
305
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
306
|
+
CONSTRAINT "auth_credentials_class_check" CHECK ("credential_class" IN ('tenant', 'platform')),
|
|
307
|
+
CONSTRAINT "auth_credentials_status_check" CHECK ("status" IN ('active', 'revoked', 'expired')),
|
|
308
|
+
CONSTRAINT "auth_credentials_class_separation_check" CHECK (
|
|
309
|
+
(
|
|
310
|
+
"credential_class" = 'tenant'
|
|
311
|
+
AND "tenant_id" IS NOT NULL
|
|
312
|
+
AND "tenant_key_lineage_id" IS NOT NULL
|
|
313
|
+
AND "actor_role" IS NOT NULL
|
|
314
|
+
AND "principal_id" IS NOT NULL
|
|
315
|
+
AND "membership_id" IS NOT NULL
|
|
316
|
+
AND "platform_api_key_id" IS NULL
|
|
317
|
+
) OR (
|
|
318
|
+
"credential_class" = 'platform'
|
|
319
|
+
AND "tenant_id" IS NULL
|
|
320
|
+
AND "platform_api_key_id" IS NOT NULL
|
|
321
|
+
AND "principal_id" IS NOT NULL
|
|
322
|
+
AND "membership_id" IS NULL
|
|
323
|
+
AND "tenant_key_lineage_id" IS NULL
|
|
324
|
+
AND "actor_role" IS NULL
|
|
325
|
+
)
|
|
326
|
+
),
|
|
327
|
+
CONSTRAINT "auth_credentials_principal_membership_pair_check" CHECK ("credential_class" <> 'tenant' OR (("principal_id" IS NULL AND "membership_id" IS NULL) OR ("principal_id" IS NOT NULL AND "membership_id" IS NOT NULL))),
|
|
328
|
+
CONSTRAINT "auth_credentials_tenant_no_wildcard_check" CHECK (
|
|
329
|
+
"credential_class" <> 'tenant' OR (
|
|
330
|
+
cardinality("scopes") > 0
|
|
331
|
+
AND array_position("scopes", NULL) IS NULL
|
|
332
|
+
AND NOT ('*' = ANY("scopes"))
|
|
333
|
+
AND array_to_string("scopes", ',') !~ '(^|,)platform:'
|
|
334
|
+
)
|
|
335
|
+
),
|
|
336
|
+
CONSTRAINT "auth_credentials_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT,
|
|
337
|
+
CONSTRAINT "auth_credentials_principal_id_fk" FOREIGN KEY ("principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT,
|
|
338
|
+
CONSTRAINT "auth_credentials_membership_id_fk" FOREIGN KEY ("membership_id") REFERENCES "tenant_memberships" ("id") ON DELETE RESTRICT,
|
|
339
|
+
CONSTRAINT "auth_credentials_tenant_key_lineage_id_fk" FOREIGN KEY ("tenant_key_lineage_id") REFERENCES "tenant_key_lineage" ("id") ON DELETE RESTRICT,
|
|
340
|
+
CONSTRAINT "auth_credentials_platform_api_key_id_fk" FOREIGN KEY ("platform_api_key_id") REFERENCES "platform_api_keys" ("id") ON DELETE RESTRICT,
|
|
341
|
+
CONSTRAINT "auth_credentials_tenant_lineage_fk" FOREIGN KEY ("tenant_id", "tenant_key_lineage_id") REFERENCES "tenant_key_lineage" ("tenant_id", "id") ON DELETE RESTRICT,
|
|
342
|
+
CONSTRAINT "auth_credentials_tenant_lineage_binding_fk" FOREIGN KEY ("tenant_id", "tenant_key_lineage_id", "principal_id", "membership_id", "actor_role") REFERENCES "tenant_key_lineage" ("tenant_id", "id", "principal_id", "membership_id", "actor_role") ON DELETE RESTRICT,
|
|
343
|
+
CONSTRAINT "auth_credentials_membership_principal_fk" FOREIGN KEY ("tenant_id", "principal_id", "membership_id") REFERENCES "tenant_memberships" ("tenant_id", "principal_id", "id") ON DELETE RESTRICT,
|
|
344
|
+
CONSTRAINT "auth_credentials_platform_source_principal_fk" FOREIGN KEY ("platform_api_key_id", "principal_id") REFERENCES "platform_api_keys" ("id", "principal_id") ON DELETE RESTRICT
|
|
345
|
+
);
|
|
346
|
+
--> statement-breakpoint
|
|
347
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "auth_credentials_key_hash_uq" ON "auth_credentials" ("key_hash");
|
|
348
|
+
--> statement-breakpoint
|
|
349
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "auth_credentials_tenant_lineage_uq" ON "auth_credentials" ("tenant_key_lineage_id");
|
|
350
|
+
--> statement-breakpoint
|
|
351
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "auth_credentials_platform_source_uq" ON "auth_credentials" ("platform_api_key_id");
|
|
352
|
+
--> statement-breakpoint
|
|
353
|
+
CREATE INDEX IF NOT EXISTS "auth_credentials_class_tenant_idx" ON "auth_credentials" ("credential_class", "tenant_id");
|
|
354
|
+
--> statement-breakpoint
|
|
355
|
+
|
|
356
|
+
-- ---------------------------------------------------------------------------
|
|
357
|
+
-- tenant_audit_logs — append-only, tenant-scoped audit store
|
|
358
|
+
-- ---------------------------------------------------------------------------
|
|
359
|
+
CREATE TABLE IF NOT EXISTS "tenant_audit_logs" (
|
|
360
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
361
|
+
"tenant_id" uuid NOT NULL,
|
|
362
|
+
"actor_principal_id" uuid,
|
|
363
|
+
"actor_credential_id" uuid NOT NULL,
|
|
364
|
+
"action" varchar(100) NOT NULL,
|
|
365
|
+
"target_type" varchar(100),
|
|
366
|
+
"target_id" varchar(255),
|
|
367
|
+
"request_id" varchar(100) NOT NULL,
|
|
368
|
+
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
369
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
370
|
+
CONSTRAINT "tenant_audit_logs_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT,
|
|
371
|
+
CONSTRAINT "tenant_audit_logs_actor_principal_id_fk" FOREIGN KEY ("actor_principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT
|
|
372
|
+
);
|
|
373
|
+
--> statement-breakpoint
|
|
374
|
+
CREATE INDEX IF NOT EXISTS "tenant_audit_logs_tenant_idx" ON "tenant_audit_logs" ("tenant_id");
|
|
375
|
+
--> statement-breakpoint
|
|
376
|
+
CREATE INDEX IF NOT EXISTS "tenant_audit_logs_created_at_idx" ON "tenant_audit_logs" ("created_at");
|
|
377
|
+
--> statement-breakpoint
|
|
378
|
+
|
|
379
|
+
-- ---------------------------------------------------------------------------
|
|
380
|
+
-- platform_audit_logs — append-only platform-admin audit store
|
|
381
|
+
-- ---------------------------------------------------------------------------
|
|
382
|
+
CREATE TABLE IF NOT EXISTS "platform_audit_logs" (
|
|
383
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
384
|
+
"actor_principal_id" uuid,
|
|
385
|
+
"actor_credential_id" uuid NOT NULL,
|
|
386
|
+
"action" varchar(100) NOT NULL,
|
|
387
|
+
"target_tenant_id" uuid,
|
|
388
|
+
"reason" text NOT NULL,
|
|
389
|
+
"request_id" varchar(100) NOT NULL,
|
|
390
|
+
"before_metadata" jsonb,
|
|
391
|
+
"after_metadata" jsonb,
|
|
392
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
393
|
+
CONSTRAINT "platform_audit_logs_target_shape_check" CHECK ("target_tenant_id" IS NOT NULL OR "action" = 'tenant.list'),
|
|
394
|
+
CONSTRAINT "platform_audit_logs_actor_principal_id_fk" FOREIGN KEY ("actor_principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT,
|
|
395
|
+
CONSTRAINT "platform_audit_logs_target_tenant_id_fk" FOREIGN KEY ("target_tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT
|
|
396
|
+
);
|
|
397
|
+
--> statement-breakpoint
|
|
398
|
+
CREATE INDEX IF NOT EXISTS "platform_audit_logs_target_tenant_idx" ON "platform_audit_logs" ("target_tenant_id");
|
|
399
|
+
--> statement-breakpoint
|
|
400
|
+
CREATE INDEX IF NOT EXISTS "platform_audit_logs_created_at_idx" ON "platform_audit_logs" ("created_at");
|
|
401
|
+
--> statement-breakpoint
|
|
402
|
+
|
|
403
|
+
-- Enforce append-only audit semantics in the database, independent of application code.
|
|
404
|
+
CREATE OR REPLACE FUNCTION "reject_audit_log_mutation"()
|
|
405
|
+
RETURNS trigger
|
|
406
|
+
LANGUAGE plpgsql
|
|
407
|
+
AS $$
|
|
408
|
+
BEGIN
|
|
409
|
+
RAISE EXCEPTION 'multitenancy audit logs are append-only';
|
|
410
|
+
END;
|
|
411
|
+
$$;
|
|
412
|
+
--> statement-breakpoint
|
|
413
|
+
DROP TRIGGER IF EXISTS "tenant_audit_logs_append_only" ON "tenant_audit_logs";
|
|
414
|
+
--> statement-breakpoint
|
|
415
|
+
CREATE TRIGGER "tenant_audit_logs_append_only"
|
|
416
|
+
BEFORE UPDATE OR DELETE ON "tenant_audit_logs"
|
|
417
|
+
FOR EACH ROW EXECUTE FUNCTION "reject_audit_log_mutation"();
|
|
418
|
+
--> statement-breakpoint
|
|
419
|
+
DROP TRIGGER IF EXISTS "tenant_audit_logs_no_truncate" ON "tenant_audit_logs";
|
|
420
|
+
--> statement-breakpoint
|
|
421
|
+
CREATE TRIGGER "tenant_audit_logs_no_truncate"
|
|
422
|
+
BEFORE TRUNCATE ON "tenant_audit_logs"
|
|
423
|
+
FOR EACH STATEMENT EXECUTE FUNCTION "reject_audit_log_mutation"();
|
|
424
|
+
--> statement-breakpoint
|
|
425
|
+
DROP TRIGGER IF EXISTS "platform_audit_logs_append_only" ON "platform_audit_logs";
|
|
426
|
+
--> statement-breakpoint
|
|
427
|
+
CREATE TRIGGER "platform_audit_logs_append_only"
|
|
428
|
+
BEFORE UPDATE OR DELETE ON "platform_audit_logs"
|
|
429
|
+
FOR EACH ROW EXECUTE FUNCTION "reject_audit_log_mutation"();
|
|
430
|
+
--> statement-breakpoint
|
|
431
|
+
DROP TRIGGER IF EXISTS "platform_audit_logs_no_truncate" ON "platform_audit_logs";
|
|
432
|
+
--> statement-breakpoint
|
|
433
|
+
CREATE TRIGGER "platform_audit_logs_no_truncate"
|
|
434
|
+
BEFORE TRUNCATE ON "platform_audit_logs"
|
|
435
|
+
FOR EACH STATEMENT EXECUTE FUNCTION "reject_audit_log_mutation"();
|