@stratum-hq/lib 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -20
- package/dist/crypto.d.ts +9 -6
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +26 -18
- package/dist/crypto.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/migrate-schemas.js +1 -1
- package/dist/migrate-schemas.js.map +1 -1
- package/dist/migrations/001_init.sql +3 -3
- package/dist/migrations/031_subtree_read_scope.sql +307 -0
- package/dist/scoped-job.d.ts +9 -1
- package/dist/scoped-job.d.ts.map +1 -1
- package/dist/scoped-job.js +3 -2
- package/dist/scoped-job.js.map +1 -1
- package/dist/services/abac-service.js +1 -1
- package/dist/services/abac-service.js.map +1 -1
- package/dist/services/api-key-service.d.ts +1 -1
- package/dist/services/api-key-service.d.ts.map +1 -1
- package/dist/services/config-service.js +5 -5
- package/dist/services/config-service.js.map +1 -1
- package/dist/services/event-service.js +2 -2
- package/dist/services/event-service.js.map +1 -1
- package/dist/services/key-rotation-service.d.ts +18 -4
- package/dist/services/key-rotation-service.d.ts.map +1 -1
- package/dist/services/key-rotation-service.js +30 -10
- package/dist/services/key-rotation-service.js.map +1 -1
- package/dist/services/permission-service.js +1 -1
- package/dist/services/permission-service.js.map +1 -1
- package/dist/services/rate-limit-service.d.ts +3 -3
- package/dist/services/region-service.js +1 -1
- package/dist/services/region-service.js.map +1 -1
- package/dist/services/retention-service.d.ts +2 -2
- package/dist/services/retention-service.js +2 -2
- package/dist/services/role-service.d.ts +1 -1
- package/dist/services/role-service.js +1 -1
- package/dist/services/tenant-service.d.ts +2 -2
- package/dist/services/tenant-service.js +4 -4
- package/dist/services/tenant-service.js.map +1 -1
- package/dist/stratum.d.ts +5 -5
- package/dist/stratum.d.ts.map +1 -1
- package/dist/stratum.js +9 -9
- package/dist/stratum.js.map +1 -1
- package/dist/telemetry.d.ts +2 -2
- package/dist/telemetry.js +4 -4
- package/dist/telemetry.js.map +1 -1
- package/dist/url-redaction.js +1 -1
- package/dist/url-redaction.js.map +1 -1
- package/package.json +20 -12
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @stratum-hq/lib
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Multi-tenancy for Node.js and PostgreSQL: tenant hierarchy, config inheritance, row-level security, ABAC, audit log, and GDPR tooling. This is the framework-agnostic core of [Stratum](https://github.com/stratum-hq/Stratum). Embed it directly in your Node.js app. It talks straight to PostgreSQL with no HTTP server in between.
|
|
4
|
+
|
|
5
|
+
Read the documentation at [docs.stratum-hq.org/packages/lib](https://docs.stratum-hq.org/packages/lib/).
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -26,7 +28,7 @@ const customer = await stratum.createTenant({
|
|
|
26
28
|
isolation_strategy: "SHARED_RLS",
|
|
27
29
|
});
|
|
28
30
|
|
|
29
|
-
// Config flows root → leaf
|
|
31
|
+
// Config flows root → leaf; children inherit automatically
|
|
30
32
|
await stratum.setConfig(msp.id, "max_seats", { value: 500, locked: true });
|
|
31
33
|
const config = await stratum.resolveConfig(customer.id);
|
|
32
34
|
// → { max_seats: { value: 500, inherited: true, locked: true } }
|
|
@@ -34,19 +36,19 @@ const config = await stratum.resolveConfig(customer.id);
|
|
|
34
36
|
const permissions = await stratum.resolvePermissions(customer.id);
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
The `pool` is **borrowed, not owned
|
|
39
|
+
The `pool` is **borrowed, not owned**: Stratum never creates or closes it. With `autoMigrate: true`, `initialize()` runs the schema migrations on first start; leave it off and manage migrations yourself via `migrate`.
|
|
38
40
|
|
|
39
41
|
## API Summary
|
|
40
42
|
|
|
41
43
|
The `Stratum` instance covers the full tenant lifecycle:
|
|
42
44
|
|
|
43
|
-
- **Tenants
|
|
44
|
-
- **Config
|
|
45
|
-
- **Permissions & ABAC
|
|
46
|
-
- **API keys & roles
|
|
47
|
-
- **Webhooks & audit
|
|
48
|
-
- **GDPR & regions
|
|
49
|
-
- **Usage metering
|
|
45
|
+
- **Tenants**: `createTenant`, `getTenant`, `listTenants`, `updateTenant`, `moveTenant`, `getAncestors`, `getDescendants`, `batchCreateTenants`
|
|
46
|
+
- **Config**: `resolveConfig`, `setConfig`, `deleteConfig`, `batchSetConfig`, `diffConfig`
|
|
47
|
+
- **Permissions & ABAC**: `resolvePermissions`, `createPermission`, `createAbacPolicy`, `evaluateAbac`
|
|
48
|
+
- **API keys & roles**: `createApiKey`, `validateApiKey`, `rotateApiKey`, `createRole`, `assignRoleToKey`
|
|
49
|
+
- **Webhooks & audit**: `createWebhook`, `testWebhook`, `queryAuditLogs`, `listFailedDeliveries`
|
|
50
|
+
- **GDPR & regions**: `exportTenantData`, `purgeTenant`, `grantConsent`, `createRegion`, `migrateRegion`
|
|
51
|
+
- **Usage metering**: `recordUsage`, `aggregateUsage` (see [docs/usage-metering.md](../../docs/usage-metering.md))
|
|
50
52
|
|
|
51
53
|
Low-level pool helpers are also exported:
|
|
52
54
|
|
|
@@ -64,11 +66,11 @@ await withTransaction(pool, async (client) => {
|
|
|
64
66
|
establishes the tenant context in two layers for the whole duration of the job
|
|
65
67
|
and tears both down when the job settles:
|
|
66
68
|
|
|
67
|
-
- **AsyncLocalStorage
|
|
69
|
+
- **AsyncLocalStorage**: the tenant is placed in the SDK's ALS store, so code
|
|
68
70
|
inside the job reads it through `Stratum.currentTenantId()` /
|
|
69
71
|
`Stratum.currentTenantContext()` exactly as a request handler would. Each job
|
|
70
72
|
gets its own store, so concurrent jobs cannot observe each other's tenant.
|
|
71
|
-
- **Postgres row-level security
|
|
73
|
+
- **Postgres row-level security**: the job runs through the data-plane
|
|
72
74
|
`withTenantContext` (`@stratum-hq/db-adapters`), which opens a transaction and
|
|
73
75
|
issues `SET LOCAL app.current_tenant_id`. Every query the job makes on the
|
|
74
76
|
provided client is confined to that tenant by RLS, so it cannot read or write
|
|
@@ -82,7 +84,7 @@ import { runScopedJob } from "@stratum-hq/lib";
|
|
|
82
84
|
// `pool` must connect as a NON-superuser, NON-BYPASSRLS role, or RLS is a no-op.
|
|
83
85
|
await runScopedJob(pool, tenantId, async (client) => {
|
|
84
86
|
Stratum.currentTenantId(); // === tenantId
|
|
85
|
-
// Confined to `tenantId` by RLS
|
|
87
|
+
// Confined to `tenantId` by RLS; no app-layer WHERE filter needed.
|
|
86
88
|
await client.query("SELECT * FROM invoices WHERE status = 'pending'");
|
|
87
89
|
});
|
|
88
90
|
```
|
|
@@ -96,7 +98,7 @@ ADR 0001 (`docs/adr/0001-postgres-rls-defense-in-depth.md`).
|
|
|
96
98
|
|
|
97
99
|
## Rate Limiting
|
|
98
100
|
|
|
99
|
-
`RateLimiter` is a standalone, storage-agnostic per-tenant rate-limiting primitive. It is not part of the `Stratum` facade (which is bound to a single `pg.Pool`)
|
|
101
|
+
`RateLimiter` is a standalone, storage-agnostic per-tenant rate-limiting primitive. It is not part of the `Stratum` facade (which is bound to a single `pg.Pool`). The point is that its storage is pluggable. It ships with a process-local in-memory store and a documented `RateLimitStore` contract you can implement over Redis, Postgres, or anything else.
|
|
100
102
|
|
|
101
103
|
> This is distinct from the HTTP-layer rate limiting in `@stratum-hq/control-plane`. Use this to embed per-tenant limits directly in an application.
|
|
102
104
|
|
|
@@ -115,7 +117,7 @@ const limiter = new RateLimiter({
|
|
|
115
117
|
// one tenant's usage never affects another's.
|
|
116
118
|
const res = await limiter.checkLimit(tenantId, "api");
|
|
117
119
|
if (!res.allowed) {
|
|
118
|
-
throw new Error(`rate limited
|
|
120
|
+
throw new Error(`rate limited, retry in ${res.retryAfter}s`);
|
|
119
121
|
}
|
|
120
122
|
// res: { allowed, limit, remaining, resetAt, retryAfter }
|
|
121
123
|
```
|
|
@@ -130,7 +132,7 @@ For each tenant the limit is resolved in order:
|
|
|
130
132
|
2. the static `limits[tenantId]` override,
|
|
131
133
|
3. `defaultLimit`.
|
|
132
134
|
|
|
133
|
-
`resolveLimit` is the seam for **config inheritance
|
|
135
|
+
`resolveLimit` is the seam for **config inheritance**: back it with `stratum.resolveConfig` to drive limits from the tenant config tree:
|
|
134
136
|
|
|
135
137
|
```typescript
|
|
136
138
|
const limiter = new RateLimiter({
|
|
@@ -162,9 +164,9 @@ interface RateLimitStore {
|
|
|
162
164
|
|
|
163
165
|
Implementation notes:
|
|
164
166
|
|
|
165
|
-
- **Redis
|
|
166
|
-
- **Postgres
|
|
167
|
-
- **In-memory
|
|
167
|
+
- **Redis**: `INCR key`, then on a reply of `1`, `PEXPIRE key windowMs`; derive `resetAt` from `now + PTTL`. A short Lua script keeps it atomic and returns count + TTL in one round trip.
|
|
168
|
+
- **Postgres**: `INSERT ... ON CONFLICT (key) DO UPDATE` returning the new count, resetting the row when `reset_at` has passed.
|
|
169
|
+
- **In-memory**: `MemoryRateLimitStore`; single-process only, holds one entry per distinct `tenantId:key` pair. For high key cardinality or multi-process deployments, use a store with native TTL eviction.
|
|
168
170
|
|
|
169
171
|
## Error Handling
|
|
170
172
|
|
|
@@ -177,7 +179,7 @@ try {
|
|
|
177
179
|
await stratum.setConfig(childId, "max_seats", { value: 999 });
|
|
178
180
|
} catch (err) {
|
|
179
181
|
if (err instanceof ConfigLockedError) {
|
|
180
|
-
// A parent locked this key
|
|
182
|
+
// A parent locked this key, so the child cannot override
|
|
181
183
|
}
|
|
182
184
|
}
|
|
183
185
|
```
|
package/dist/crypto.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/** Encrypts a value. Returns versioned format: v1:iv:authTag:ciphertext (all hex) */
|
|
2
2
|
export declare function encrypt(plaintext: string): string;
|
|
3
3
|
/** Decrypts a versioned encrypted value. Supports v1 format and legacy (no version prefix).
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* If the current key and salt fail, it tries the previous pair: STRATUM_ENCRYPTION_KEY_PREVIOUS
|
|
5
|
+
* and STRATUM_HKDF_SALT_PREVIOUS. A previous variable that is unset uses the current value.
|
|
6
|
+
* This keeps values readable during a rolling deployment and until a rotation finishes. */
|
|
6
7
|
export declare function decrypt(encrypted: string): string;
|
|
7
|
-
/** Encrypts a value with the key derived from `keyMaterial`.
|
|
8
|
+
/** Encrypts a value with the key derived from `keyMaterial` and `saltHex`.
|
|
9
|
+
* If `saltHex` is not given, the configured salt applies.
|
|
8
10
|
* Key rotation uses it because it must not read or change process.env. */
|
|
9
|
-
export declare function encryptWithKeyMaterial(plaintext: string, keyMaterial: string): string;
|
|
10
|
-
/** Returns the plaintext, or null when the value does not decrypt with the key derived from `keyMaterial`.
|
|
11
|
+
export declare function encryptWithKeyMaterial(plaintext: string, keyMaterial: string, saltHex?: string): string;
|
|
12
|
+
/** Returns the plaintext, or null when the value does not decrypt with the key derived from `keyMaterial` and `saltHex`.
|
|
13
|
+
* If `saltHex` is not given, the configured salt applies.
|
|
11
14
|
* Key rotation uses the null result to try the next key instead of failing. */
|
|
12
|
-
export declare function decryptWithKeyMaterial(encrypted: string, keyMaterial: string): string | null;
|
|
15
|
+
export declare function decryptWithKeyMaterial(encrypted: string, keyMaterial: string, saltHex?: string): string | null;
|
|
13
16
|
//# sourceMappingURL=crypto.d.ts.map
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAyFA,qFAAqF;AACrF,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;2FAG2F;AAC3F,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAejD;AAED;;0EAE0E;AAC1E,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvG;AAED;;+EAE+E;AAC/E,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM9G"}
|
package/dist/crypto.js
CHANGED
|
@@ -35,23 +35,27 @@ const HKDF_SALT = (() => {
|
|
|
35
35
|
}
|
|
36
36
|
return Buffer.from(NON_PRODUCTION_DEFAULT_SALT, "utf8");
|
|
37
37
|
})();
|
|
38
|
-
function hkdfDeriveKey(keyMaterial, info = "stratum-aes-key") {
|
|
39
|
-
return Buffer.from(node_crypto_1.default.hkdfSync("sha256", Buffer.from(keyMaterial, "utf8"),
|
|
38
|
+
function hkdfDeriveKey(keyMaterial, salt = HKDF_SALT, info = "stratum-aes-key") {
|
|
39
|
+
return Buffer.from(node_crypto_1.default.hkdfSync("sha256", Buffer.from(keyMaterial, "utf8"), salt, info, 32));
|
|
40
40
|
}
|
|
41
|
-
function
|
|
41
|
+
function getEncryptionKeyMaterial() {
|
|
42
42
|
const envKey = process.env.STRATUM_ENCRYPTION_KEY ?? process.env.WEBHOOK_ENCRYPTION_KEY;
|
|
43
43
|
if (envKey) {
|
|
44
|
-
return
|
|
44
|
+
return envKey;
|
|
45
45
|
}
|
|
46
46
|
const strictEnv = nodeEnvRequiringKeyMaterial();
|
|
47
47
|
if (strictEnv) {
|
|
48
48
|
throw new Error(`STRATUM_ENCRYPTION_KEY must be set in ${strictEnv}`);
|
|
49
49
|
}
|
|
50
50
|
// Development and test only fallback
|
|
51
|
-
return
|
|
51
|
+
return "stratum-dev-key";
|
|
52
|
+
}
|
|
53
|
+
function getEncryptionKey() {
|
|
54
|
+
return hkdfDeriveKey(getEncryptionKeyMaterial());
|
|
52
55
|
}
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
// A hex salt argument follows the STRATUM_HKDF_SALT format. No argument means the configured salt.
|
|
57
|
+
function deriveKey(keyMaterial, saltHex) {
|
|
58
|
+
return hkdfDeriveKey(keyMaterial, saltHex ? Buffer.from(saltHex, "hex") : HKDF_SALT);
|
|
55
59
|
}
|
|
56
60
|
function encryptWithKey(plaintext, key) {
|
|
57
61
|
const iv = node_crypto_1.default.randomBytes(IV_LENGTH);
|
|
@@ -86,35 +90,39 @@ function encrypt(plaintext) {
|
|
|
86
90
|
return encryptWithKey(plaintext, getEncryptionKey());
|
|
87
91
|
}
|
|
88
92
|
/** Decrypts a versioned encrypted value. Supports v1 format and legacy (no version prefix).
|
|
89
|
-
*
|
|
90
|
-
*
|
|
93
|
+
* If the current key and salt fail, it tries the previous pair: STRATUM_ENCRYPTION_KEY_PREVIOUS
|
|
94
|
+
* and STRATUM_HKDF_SALT_PREVIOUS. A previous variable that is unset uses the current value.
|
|
95
|
+
* This keeps values readable during a rolling deployment and until a rotation finishes. */
|
|
91
96
|
function decrypt(encrypted) {
|
|
92
97
|
try {
|
|
93
98
|
return decryptWithKey(encrypted, getEncryptionKey());
|
|
94
99
|
}
|
|
95
100
|
catch (err) {
|
|
96
101
|
const previousKey = process.env.STRATUM_ENCRYPTION_KEY_PREVIOUS;
|
|
97
|
-
|
|
102
|
+
const previousSalt = process.env.STRATUM_HKDF_SALT_PREVIOUS;
|
|
103
|
+
if (previousKey || previousSalt) {
|
|
98
104
|
try {
|
|
99
|
-
return decryptWithKey(encrypted,
|
|
105
|
+
return decryptWithKey(encrypted, deriveKey(previousKey || getEncryptionKeyMaterial(), previousSalt));
|
|
100
106
|
}
|
|
101
107
|
catch {
|
|
102
|
-
// Both
|
|
108
|
+
// Both pairs failed: throw the original error
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
111
|
throw err;
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
|
-
/** Encrypts a value with the key derived from `keyMaterial`.
|
|
114
|
+
/** Encrypts a value with the key derived from `keyMaterial` and `saltHex`.
|
|
115
|
+
* If `saltHex` is not given, the configured salt applies.
|
|
109
116
|
* Key rotation uses it because it must not read or change process.env. */
|
|
110
|
-
function encryptWithKeyMaterial(plaintext, keyMaterial) {
|
|
111
|
-
return encryptWithKey(plaintext, deriveKey(keyMaterial));
|
|
117
|
+
function encryptWithKeyMaterial(plaintext, keyMaterial, saltHex) {
|
|
118
|
+
return encryptWithKey(plaintext, deriveKey(keyMaterial, saltHex));
|
|
112
119
|
}
|
|
113
|
-
/** Returns the plaintext, or null when the value does not decrypt with the key derived from `keyMaterial`.
|
|
120
|
+
/** Returns the plaintext, or null when the value does not decrypt with the key derived from `keyMaterial` and `saltHex`.
|
|
121
|
+
* If `saltHex` is not given, the configured salt applies.
|
|
114
122
|
* Key rotation uses the null result to try the next key instead of failing. */
|
|
115
|
-
function decryptWithKeyMaterial(encrypted, keyMaterial) {
|
|
123
|
+
function decryptWithKeyMaterial(encrypted, keyMaterial, saltHex) {
|
|
116
124
|
try {
|
|
117
|
-
return decryptWithKey(encrypted, deriveKey(keyMaterial));
|
|
125
|
+
return decryptWithKey(encrypted, deriveKey(keyMaterial, saltHex));
|
|
118
126
|
}
|
|
119
127
|
catch {
|
|
120
128
|
return null;
|
package/dist/crypto.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { RateLimiter, MemoryRateLimitStore } from "./services/rate-limit-service
|
|
|
12
12
|
export type { RateLimit, RateLimitResult, RateLimitState, RateLimitStore, RateLimitResolver, RateLimiterOptions, } from "./services/rate-limit-service.js";
|
|
13
13
|
export type { ApiKeyRecord, CreatedApiKey, ValidatedApiKey, CreateApiKeyOptions } from "./services/api-key-service.js";
|
|
14
14
|
export type { BatchCreateResult } from "./services/tenant-service.js";
|
|
15
|
-
export type { KeyRotationResult, KeyRotationUnreadableRow } from "./services/key-rotation-service.js";
|
|
15
|
+
export type { KeyRotationResult, KeyRotationSalts, KeyRotationUnreadableRow } from "./services/key-rotation-service.js";
|
|
16
16
|
export type { DeliveryStats } from "./services/event-service.js";
|
|
17
17
|
export type { Role, CreateRoleInput, UpdateRoleInput } from "./services/role-service.js";
|
|
18
18
|
export type { AuditContext, AuditEntry, AuditLogQuery, RecordAuditEventInput } from "@stratum-hq/core";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,iBAAiB,EACjB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EACL,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,iCAAiC,EACjC,KAAK,oBAAoB,GAC1B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAKrE,OAAO,EACL,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,wBAAwB,EACxB,sBAAsB,EACtB,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,uBAAuB,EACvB,iCAAiC,EACjC,qBAAqB,EACrB,uBAAuB,EACvB,+BAA+B,EAC/B,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,yBAAyB,EACzB,mBAAmB,EACnB,uBAAuB,EACvB,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EACV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,eAAe,EACf,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACrF,YAAY,EACV,SAAS,EACT,eAAe,EACf,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACvH,YAAY,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACtE,YAAY,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,iBAAiB,EACjB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EACL,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,iCAAiC,EACjC,KAAK,oBAAoB,GAC1B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAKrE,OAAO,EACL,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,wBAAwB,EACxB,sBAAsB,EACtB,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,uBAAuB,EACvB,iCAAiC,EACjC,qBAAqB,EACrB,uBAAuB,EACvB,+BAA+B,EAC/B,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,yBAAyB,EACzB,mBAAmB,EACnB,uBAAuB,EACvB,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EACV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,eAAe,EACf,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACrF,YAAY,EACV,SAAS,EACT,eAAe,EACf,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACvH,YAAY,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACtE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AACxH,YAAY,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,YAAY,EAAE,IAAI,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAGzF,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAGvG,YAAY,EACV,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/migrate-schemas.js
CHANGED
|
@@ -126,7 +126,7 @@ function hashSchemaLock(schema) {
|
|
|
126
126
|
return (hash ^ 0x5354524d) | 0; // XOR with "STRM"
|
|
127
127
|
}
|
|
128
128
|
function quoteIdent(name) {
|
|
129
|
-
// Simple identifier quoting
|
|
129
|
+
// Simple identifier quoting: disallow anything that could break out
|
|
130
130
|
if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
|
|
131
131
|
throw new Error(`Invalid schema name: ${name}`);
|
|
132
132
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrate-schemas.js","sourceRoot":"","sources":["../src/migrate-schemas.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"migrate-schemas.js","sourceRoot":"","sources":["../src/migrate-schemas.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
DO $$ BEGIN
|
|
3
3
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
4
4
|
EXCEPTION WHEN insufficient_privilege THEN
|
|
5
|
-
RAISE WARNING 'Cannot create uuid-ossp extension
|
|
5
|
+
RAISE WARNING 'Cannot create uuid-ossp extension: insufficient privileges. Ask a database administrator to run: CREATE EXTENSION IF NOT EXISTS "uuid-ossp";';
|
|
6
6
|
END $$;
|
|
7
7
|
|
|
8
8
|
DO $$ BEGIN
|
|
9
9
|
CREATE EXTENSION IF NOT EXISTS "ltree";
|
|
10
10
|
EXCEPTION WHEN insufficient_privilege THEN
|
|
11
|
-
RAISE WARNING 'Cannot create ltree extension
|
|
11
|
+
RAISE WARNING 'Cannot create ltree extension: insufficient privileges. Ask a database administrator to run: CREATE EXTENSION IF NOT EXISTS "ltree";';
|
|
12
12
|
END $$;
|
|
13
13
|
|
|
14
14
|
-- Warn if application role has BYPASSRLS privilege.
|
|
@@ -127,7 +127,7 @@ CREATE TRIGGER update_config_entries_updated_at
|
|
|
127
127
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
128
128
|
|
|
129
129
|
-- Config entries uses ON DELETE CASCADE (unlike permission_policies which uses RESTRICT)
|
|
130
|
-
-- because config values are safe to delete with a tenant
|
|
130
|
+
-- because config values are safe to delete with a tenant; they carry no semantic guarantees
|
|
131
131
|
-- like PERMANENT revocation mode. If soft-delete is used (v1 default), CASCADE never fires.
|
|
132
132
|
|
|
133
133
|
-- API keys table (for control plane auth)
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
-- Migration 031: Opt-in subtree read scope for row-level security.
|
|
2
|
+
--
|
|
3
|
+
-- A session scoped to tenant T can opt in to read the rows of T and of every
|
|
4
|
+
-- descendant of T. The default stays exact-tenant isolation (019, 020).
|
|
5
|
+
--
|
|
6
|
+
-- Context model, in addition to 019:
|
|
7
|
+
-- * Subtree scope: SET LOCAL app.tenant_scope = 'subtree' (transaction
|
|
8
|
+
-- scoped). Only the exact value 'subtree' widens reads. The library sets it
|
|
9
|
+
-- only through its context helpers (db-adapters setTenantContext and
|
|
10
|
+
-- withTenantContext with { scope: 'subtree' }).
|
|
11
|
+
--
|
|
12
|
+
-- Reads only. Each table below gets a second permissive policy, tenant_subtree_read,
|
|
13
|
+
-- FOR SELECT. PostgreSQL ORs it with tenant_isolation for reads. INSERT,
|
|
14
|
+
-- UPDATE and DELETE must also pass the policies for their own command, and
|
|
15
|
+
-- tenant_isolation is the only one. Writes therefore stay limited to the exact
|
|
16
|
+
-- tenant: an insert for a descendant fails WITH CHECK, and UPDATE and DELETE
|
|
17
|
+
-- do not reach a descendant's rows. SELECT ... FOR UPDATE and FOR SHARE also
|
|
18
|
+
-- need the UPDATE policy, so in subtree scope they return the exact tenant's
|
|
19
|
+
-- rows only. The 019 and 020 policies do not change.
|
|
20
|
+
--
|
|
21
|
+
-- Tables with a subtree read policy:
|
|
22
|
+
-- tenants (on id), config_entries (rows with sensitive = false only),
|
|
23
|
+
-- permission_policies, audit_logs, webhook_events, consent_records,
|
|
24
|
+
-- abac_policies, roles, usage_events, and, through their parent row,
|
|
25
|
+
-- webhook_deliveries (via webhook_events) and principal_roles (via roles).
|
|
26
|
+
--
|
|
27
|
+
-- Credential-bearing rows stay exact-tenant. api_keys and webhooks get no
|
|
28
|
+
-- subtree policy, and the config_entries policy admits no row with
|
|
29
|
+
-- sensitive = true. A key row holds a key hash, a webhook row holds a signing
|
|
30
|
+
-- secret hash and a URL that can carry credentials, and a sensitive config
|
|
31
|
+
-- row holds a secret value. Their 019 policies stay the only ones that admit
|
|
32
|
+
-- a tenant session, so only the exact tenant reads them. webhook_deliveries
|
|
33
|
+
-- is scoped through webhook_events, not webhooks, so it gives no subtree path
|
|
34
|
+
-- to a webhook row.
|
|
35
|
+
--
|
|
36
|
+
-- Tenant status does not affect the scope. Suspended, archived and pending
|
|
37
|
+
-- descendants are in the subtree, as exact-tenant RLS ignores status too.
|
|
38
|
+
-- Applications filter by status when they need to.
|
|
39
|
+
--
|
|
40
|
+
-- The bypass (app.bypass_rls = 'on') does not change: it sees every row.
|
|
41
|
+
--
|
|
42
|
+
-- The scope follows the tree as it is when the statement runs: a move through
|
|
43
|
+
-- moveTenant changes the subtree at once. The tree columns of tenants change
|
|
44
|
+
-- only under the bypass (see guard_tenant_tree_columns below), so a tenant
|
|
45
|
+
-- session cannot change its own position in the tree.
|
|
46
|
+
--
|
|
47
|
+
-- Cost: the function runs once per policy reference in a statement, and its
|
|
48
|
+
-- result grows with the size of the subtree. Each table then matches tenant_id
|
|
49
|
+
-- against that array, so it needs an index on tenant_id.
|
|
50
|
+
|
|
51
|
+
-- ---------------------------------------------------------------------------
|
|
52
|
+
-- stratum_subtree_tenant_ids(): the current tenant and all its descendants.
|
|
53
|
+
--
|
|
54
|
+
-- The subtree is selected by the ID-based ancestry_path, not by the
|
|
55
|
+
-- slug-derived ancestry_ltree, for the reason getDescendants gives in
|
|
56
|
+
-- tenant-service.ts: an isolation boundary must not follow slugs. A
|
|
57
|
+
-- descendant's ancestry_path is the subtree path itself or starts with it
|
|
58
|
+
-- plus '/'. The ~>=~ / ~<~ range lets idx_tenant_ancestry_path_prefix (028)
|
|
59
|
+
-- serve the lookup with a value known only at run time ('0' is the character
|
|
60
|
+
-- after '/'). The LIKE then drops what the range admits but is not a path
|
|
61
|
+
-- segment boundary.
|
|
62
|
+
--
|
|
63
|
+
-- The policies call the function in a scalar subquery, so PostgreSQL runs it
|
|
64
|
+
-- once per policy reference in a statement as an InitPlan and then matches
|
|
65
|
+
-- tenant_id against the array with the table's own index on tenant_id.
|
|
66
|
+
--
|
|
67
|
+
-- The function must read the whole subtree while the caller sees only its own
|
|
68
|
+
-- tenants row (019). It does what 029 does: SECURITY DEFINER would not help,
|
|
69
|
+
-- because FORCE ROW LEVEL SECURITY applies to the owner too, so a SET clause
|
|
70
|
+
-- turns on the 019 bypass for the function's own duration. A SET clause is
|
|
71
|
+
-- restored when the function exits, on error too. The second SET clause clears
|
|
72
|
+
-- app.tenant_scope inside the function, so the tenant_subtree_read policy on
|
|
73
|
+
-- tenants cannot call the function again while it runs.
|
|
74
|
+
--
|
|
75
|
+
-- The function returns only ids in the subtree of app.current_tenant_id. A
|
|
76
|
+
-- session that can call it can already set both settings, so it gives no new
|
|
77
|
+
-- access. With no current tenant it returns an empty array.
|
|
78
|
+
--
|
|
79
|
+
-- search_path is pinned below to pg_catalog, the schema this migration runs
|
|
80
|
+
-- in (where 001 created tenants), and pg_temp last, so that a table a session
|
|
81
|
+
-- creates cannot stand in for tenants.
|
|
82
|
+
-- ---------------------------------------------------------------------------
|
|
83
|
+
CREATE OR REPLACE FUNCTION stratum_subtree_tenant_ids()
|
|
84
|
+
RETURNS uuid[]
|
|
85
|
+
LANGUAGE sql
|
|
86
|
+
STABLE
|
|
87
|
+
SET app.bypass_rls = 'on'
|
|
88
|
+
SET app.tenant_scope = ''
|
|
89
|
+
AS $$
|
|
90
|
+
WITH cur AS (
|
|
91
|
+
SELECT t.id, rtrim(t.ancestry_path, '/') || '/' || t.id::text AS sub
|
|
92
|
+
FROM tenants t
|
|
93
|
+
WHERE t.id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
94
|
+
)
|
|
95
|
+
SELECT coalesce(array_agg(s.id), '{}'::uuid[])
|
|
96
|
+
FROM (
|
|
97
|
+
SELECT cur.id FROM cur
|
|
98
|
+
UNION ALL
|
|
99
|
+
SELECT d.id
|
|
100
|
+
FROM cur
|
|
101
|
+
JOIN tenants d
|
|
102
|
+
ON d.ancestry_path ~>=~ cur.sub
|
|
103
|
+
AND d.ancestry_path ~<~ (cur.sub || '0')
|
|
104
|
+
AND (d.ancestry_path = cur.sub OR d.ancestry_path LIKE cur.sub || '/%')
|
|
105
|
+
) s
|
|
106
|
+
$$;
|
|
107
|
+
|
|
108
|
+
DO $pin$ BEGIN
|
|
109
|
+
EXECUTE format(
|
|
110
|
+
'ALTER FUNCTION stratum_subtree_tenant_ids() SET search_path = pg_catalog, %I, pg_temp',
|
|
111
|
+
current_schema()
|
|
112
|
+
);
|
|
113
|
+
END $pin$;
|
|
114
|
+
|
|
115
|
+
-- Reusable predicate (inlined per policy below, as in 019):
|
|
116
|
+
-- current_setting('app.tenant_scope', true) = 'subtree'
|
|
117
|
+
-- AND tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])
|
|
118
|
+
-- The scope check comes first. With any other scope the AND stops there, so
|
|
119
|
+
-- the default path never runs the function.
|
|
120
|
+
|
|
121
|
+
-- ---------------------------------------------------------------------------
|
|
122
|
+
-- Direct tenant_id tables.
|
|
123
|
+
-- ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
-- config_entries: non-sensitive rows only. A sensitive row holds a secret
|
|
126
|
+
-- value, so only the exact tenant reads it (019).
|
|
127
|
+
DROP POLICY IF EXISTS tenant_subtree_read ON config_entries;
|
|
128
|
+
CREATE POLICY tenant_subtree_read ON config_entries FOR SELECT
|
|
129
|
+
USING (
|
|
130
|
+
current_setting('app.tenant_scope', true) = 'subtree'
|
|
131
|
+
AND tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])
|
|
132
|
+
AND NOT sensitive
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
DROP POLICY IF EXISTS tenant_subtree_read ON permission_policies;
|
|
136
|
+
CREATE POLICY tenant_subtree_read ON permission_policies FOR SELECT
|
|
137
|
+
USING (
|
|
138
|
+
current_setting('app.tenant_scope', true) = 'subtree'
|
|
139
|
+
AND tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
DROP POLICY IF EXISTS tenant_subtree_read ON audit_logs;
|
|
143
|
+
CREATE POLICY tenant_subtree_read ON audit_logs FOR SELECT
|
|
144
|
+
USING (
|
|
145
|
+
current_setting('app.tenant_scope', true) = 'subtree'
|
|
146
|
+
AND tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
DROP POLICY IF EXISTS tenant_subtree_read ON webhook_events;
|
|
150
|
+
CREATE POLICY tenant_subtree_read ON webhook_events FOR SELECT
|
|
151
|
+
USING (
|
|
152
|
+
current_setting('app.tenant_scope', true) = 'subtree'
|
|
153
|
+
AND tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
-- webhooks: no subtree policy. A webhook row holds credential material, so
|
|
157
|
+
-- only the exact tenant reads it (019).
|
|
158
|
+
DROP POLICY IF EXISTS tenant_subtree_read ON webhooks;
|
|
159
|
+
|
|
160
|
+
DROP POLICY IF EXISTS tenant_subtree_read ON consent_records;
|
|
161
|
+
CREATE POLICY tenant_subtree_read ON consent_records FOR SELECT
|
|
162
|
+
USING (
|
|
163
|
+
current_setting('app.tenant_scope', true) = 'subtree'
|
|
164
|
+
AND tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
DROP POLICY IF EXISTS tenant_subtree_read ON abac_policies;
|
|
168
|
+
CREATE POLICY tenant_subtree_read ON abac_policies FOR SELECT
|
|
169
|
+
USING (
|
|
170
|
+
current_setting('app.tenant_scope', true) = 'subtree'
|
|
171
|
+
AND tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
DROP POLICY IF EXISTS tenant_subtree_read ON roles;
|
|
175
|
+
CREATE POLICY tenant_subtree_read ON roles FOR SELECT
|
|
176
|
+
USING (
|
|
177
|
+
current_setting('app.tenant_scope', true) = 'subtree'
|
|
178
|
+
AND tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
DROP POLICY IF EXISTS tenant_subtree_read ON usage_events;
|
|
182
|
+
CREATE POLICY tenant_subtree_read ON usage_events FOR SELECT
|
|
183
|
+
USING (
|
|
184
|
+
current_setting('app.tenant_scope', true) = 'subtree'
|
|
185
|
+
AND tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
-- ---------------------------------------------------------------------------
|
|
189
|
+
-- tenants registry: the subtree's own registry rows, on id.
|
|
190
|
+
-- ---------------------------------------------------------------------------
|
|
191
|
+
DROP POLICY IF EXISTS tenant_subtree_read ON tenants;
|
|
192
|
+
CREATE POLICY tenant_subtree_read ON tenants FOR SELECT
|
|
193
|
+
USING (
|
|
194
|
+
current_setting('app.tenant_scope', true) = 'subtree'
|
|
195
|
+
AND id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
-- ---------------------------------------------------------------------------
|
|
199
|
+
-- Indirect tables: scoped through their tenant-bearing parent, as in 019.
|
|
200
|
+
-- ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
DROP POLICY IF EXISTS tenant_subtree_read ON webhook_deliveries;
|
|
203
|
+
CREATE POLICY tenant_subtree_read ON webhook_deliveries FOR SELECT
|
|
204
|
+
USING (
|
|
205
|
+
current_setting('app.tenant_scope', true) = 'subtree'
|
|
206
|
+
AND EXISTS (
|
|
207
|
+
SELECT 1 FROM webhook_events we
|
|
208
|
+
WHERE we.id = webhook_deliveries.event_id
|
|
209
|
+
AND we.tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
DROP POLICY IF EXISTS tenant_subtree_read ON principal_roles;
|
|
214
|
+
CREATE POLICY tenant_subtree_read ON principal_roles FOR SELECT
|
|
215
|
+
USING (
|
|
216
|
+
current_setting('app.tenant_scope', true) = 'subtree'
|
|
217
|
+
AND EXISTS (
|
|
218
|
+
SELECT 1 FROM roles r
|
|
219
|
+
WHERE r.id = principal_roles.role_id
|
|
220
|
+
AND r.tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
-- ---------------------------------------------------------------------------
|
|
225
|
+
-- Tree columns change only under the bypass.
|
|
226
|
+
--
|
|
227
|
+
-- A tenant session may update its own tenants row (019), and the subtree read
|
|
228
|
+
-- above follows parent_id and ancestry_path. This BEFORE trigger refuses any
|
|
229
|
+
-- change to parent_id, ancestry_path, depth or ancestry_ltree unless
|
|
230
|
+
-- app.bypass_rls = 'on'. The library changes these columns only in
|
|
231
|
+
-- createTenant, moveTenant and the other tree operations, which all run under
|
|
232
|
+
-- the bypass (pool-helpers.ts), and so do the migrations. A role that skips
|
|
233
|
+
-- RLS must also set the bypass to change them.
|
|
234
|
+
--
|
|
235
|
+
-- A slug rename stays allowed: the trigger is named to run before
|
|
236
|
+
-- maintain_tenant_ancestry_ltree (BEFORE triggers run in name order), so it
|
|
237
|
+
-- compares the ancestry_ltree the statement wrote, not the one that trigger
|
|
238
|
+
-- recomputes from the new slug.
|
|
239
|
+
-- ---------------------------------------------------------------------------
|
|
240
|
+
CREATE OR REPLACE FUNCTION refuse_tenant_tree_column_change()
|
|
241
|
+
RETURNS TRIGGER
|
|
242
|
+
AS $$
|
|
243
|
+
BEGIN
|
|
244
|
+
IF current_setting('app.bypass_rls', true) IS DISTINCT FROM 'on' AND (
|
|
245
|
+
NEW.parent_id IS DISTINCT FROM OLD.parent_id
|
|
246
|
+
OR NEW.ancestry_path IS DISTINCT FROM OLD.ancestry_path
|
|
247
|
+
OR NEW.depth IS DISTINCT FROM OLD.depth
|
|
248
|
+
OR NEW.ancestry_ltree IS DISTINCT FROM OLD.ancestry_ltree
|
|
249
|
+
) THEN
|
|
250
|
+
RAISE EXCEPTION 'tenant % tree columns (parent_id, ancestry_path, depth, ancestry_ltree) change only under app.bypass_rls', OLD.id
|
|
251
|
+
USING ERRCODE = 'insufficient_privilege';
|
|
252
|
+
END IF;
|
|
253
|
+
RETURN NEW;
|
|
254
|
+
END;
|
|
255
|
+
$$ language 'plpgsql';
|
|
256
|
+
|
|
257
|
+
DO $pin$ BEGIN
|
|
258
|
+
EXECUTE format(
|
|
259
|
+
'ALTER FUNCTION refuse_tenant_tree_column_change() SET search_path = pg_catalog, %I, pg_temp',
|
|
260
|
+
current_schema()
|
|
261
|
+
);
|
|
262
|
+
END $pin$;
|
|
263
|
+
|
|
264
|
+
DROP TRIGGER IF EXISTS guard_tenant_tree_columns ON tenants;
|
|
265
|
+
CREATE TRIGGER guard_tenant_tree_columns
|
|
266
|
+
BEFORE UPDATE OF parent_id, ancestry_path, depth, ancestry_ltree ON tenants
|
|
267
|
+
FOR EACH ROW
|
|
268
|
+
EXECUTE FUNCTION refuse_tenant_tree_column_change();
|
|
269
|
+
|
|
270
|
+
-- ---------------------------------------------------------------------------
|
|
271
|
+
-- refuse_tenant_parent_cycle() (029), with pg_temp last on its search_path.
|
|
272
|
+
--
|
|
273
|
+
-- The body is the one 029 created, unchanged. 029 pinned search_path to
|
|
274
|
+
-- pg_catalog and the migration schema; this adds pg_temp at the end, so the
|
|
275
|
+
-- cycle walk always reads the tenants table of that schema.
|
|
276
|
+
-- ---------------------------------------------------------------------------
|
|
277
|
+
CREATE OR REPLACE FUNCTION refuse_tenant_parent_cycle()
|
|
278
|
+
RETURNS TRIGGER
|
|
279
|
+
SET app.bypass_rls = 'on'
|
|
280
|
+
AS $$
|
|
281
|
+
BEGIN
|
|
282
|
+
IF NEW.parent_id = NEW.id OR EXISTS (
|
|
283
|
+
WITH RECURSIVE up(id, parent_id, seen) AS (
|
|
284
|
+
SELECT t.id, t.parent_id, ARRAY[t.id]
|
|
285
|
+
FROM tenants t
|
|
286
|
+
WHERE t.id = NEW.parent_id
|
|
287
|
+
UNION ALL
|
|
288
|
+
SELECT t.id, t.parent_id, up.seen || t.id
|
|
289
|
+
FROM tenants t
|
|
290
|
+
JOIN up ON t.id = up.parent_id
|
|
291
|
+
WHERE t.id <> ALL (up.seen)
|
|
292
|
+
)
|
|
293
|
+
SELECT 1 FROM up WHERE up.id = NEW.id
|
|
294
|
+
) THEN
|
|
295
|
+
RAISE EXCEPTION 'tenant % cannot be its own ancestor (parent_id cycle)', NEW.id
|
|
296
|
+
USING ERRCODE = 'check_violation';
|
|
297
|
+
END IF;
|
|
298
|
+
RETURN NEW;
|
|
299
|
+
END;
|
|
300
|
+
$$ language 'plpgsql';
|
|
301
|
+
|
|
302
|
+
DO $pin$ BEGIN
|
|
303
|
+
EXECUTE format(
|
|
304
|
+
'ALTER FUNCTION refuse_tenant_parent_cycle() SET search_path = pg_catalog, %I, pg_temp',
|
|
305
|
+
current_schema()
|
|
306
|
+
);
|
|
307
|
+
END $pin$;
|