@stratum-hq/db-adapters 1.2.0 → 1.3.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 +10 -0
- package/dist/rls/manager.d.ts.map +1 -1
- package/dist/rls/manager.js +23 -6
- package/dist/rls/manager.js.map +1 -1
- package/dist/rls/policy-check.d.ts +43 -0
- package/dist/rls/policy-check.d.ts.map +1 -0
- package/dist/rls/policy-check.js +193 -0
- package/dist/rls/policy-check.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,6 +56,16 @@ try {
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
`createPolicy` checks every row-level security policy already on the table (the table the name resolves to through the `search_path`) before it adds the `tenant_isolation` policy or keeps an existing one, and throws without changing anything if any check fails:
|
|
60
|
+
|
|
61
|
+
- PostgreSQL ORs permissive policies together, so every permissive policy, whatever its name or roles, must compare `tenant_id` with the current tenant setting `app.current_tenant_id` for the commands it covers (`USING` for reads, updates and deletes; `WITH CHECK` for inserts, and for updates when set). A role-specific permissive policy counts too, because `createPolicy` cannot know which role your application connects as.
|
|
62
|
+
- Restrictive policies can only narrow access, so they may check anything.
|
|
63
|
+
- An existing `tenant_isolation` policy must be permissive, apply to all commands, and apply to `PUBLIC` (no `TO` clause), like the one `createPolicy` generates and every policy Stratum ships.
|
|
64
|
+
|
|
65
|
+
The check recognizes the form Stratum generates, with the operands in either order, with casts, ANDed with other conditions, or ORed with Stratum's `app.bypass_rls` bypass. A policy that isolates correctly but is written in another form is also refused; replace it with the generated form. This is a breaking change for callers that relied on the old skip, shipped in a minor release.
|
|
66
|
+
|
|
67
|
+
`isRLSEnabled` reports on the table that the name resolves to through the `search_path`, not on a table with the same name in another schema.
|
|
68
|
+
|
|
59
69
|
Also available: `disableRLS`, `dropPolicy`, `isRLSEnabled`, `addTenantColumn`, `createIsolationPolicy`, and low-level session helpers `setTenantContext` / `resetTenantContext` / `getCurrentTenantId`. Schema-per-tenant and database-per-tenant variants (`SchemaRawAdapter`, `DatabasePoolManager`, …) are exported too.
|
|
60
70
|
|
|
61
71
|
## Schema-per-tenant search_path
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAapB,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAWlB"}
|
package/dist/rls/manager.js
CHANGED
|
@@ -5,6 +5,8 @@ exports.dropPolicy = dropPolicy;
|
|
|
5
5
|
exports.enableRLS = enableRLS;
|
|
6
6
|
exports.disableRLS = disableRLS;
|
|
7
7
|
exports.isRLSEnabled = isRLSEnabled;
|
|
8
|
+
const policy_check_js_1 = require("./policy-check.js");
|
|
9
|
+
const TENANT_FILTER = "tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid";
|
|
8
10
|
// Validate table name to prevent SQL injection (only allows alphanumeric + underscores)
|
|
9
11
|
function validateTableName(tableName) {
|
|
10
12
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
@@ -14,11 +16,24 @@ function validateTableName(tableName) {
|
|
|
14
16
|
}
|
|
15
17
|
async function createPolicy(client, tableName) {
|
|
16
18
|
const safe = validateTableName(tableName);
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
// Read every policy on the table that the name resolves to, in whichever
|
|
20
|
+
// schema that is. PostgreSQL ORs permissive policies together, so each one
|
|
21
|
+
// must filter by tenant, and a policy's name proves nothing.
|
|
22
|
+
const existing = await client.query(`SELECT p.policyname, p.permissive, p.cmd, p.qual, p.with_check, p.roles::text[] AS roles
|
|
23
|
+
FROM pg_policies p
|
|
24
|
+
JOIN pg_class c ON c.relname = p.tablename
|
|
25
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = p.schemaname
|
|
26
|
+
WHERE c.oid = to_regclass($1)`, [safe]);
|
|
27
|
+
const issues = (0, policy_check_js_1.tablePolicyIssues)(existing.rows);
|
|
28
|
+
if (issues.length > 0) {
|
|
29
|
+
throw new Error(`[stratum] Table ${safe} has row-level security policies that do not isolate it by tenant: ` +
|
|
30
|
+
`${issues.join("; ")} (expected ${TENANT_FILTER}). ` +
|
|
31
|
+
`Drop or correct those policies, then call createPolicy again.`);
|
|
32
|
+
}
|
|
33
|
+
if (!existing.rows.some((p) => p.policyname === "tenant_isolation")) {
|
|
34
|
+
// Cannot use parameterized queries inside DO blocks or for DDL identifiers.
|
|
35
|
+
// Table name is validated via allowlist regex above.
|
|
36
|
+
await client.query(`CREATE POLICY tenant_isolation ON ${safe} USING (${TENANT_FILTER})`);
|
|
22
37
|
}
|
|
23
38
|
}
|
|
24
39
|
async function dropPolicy(client, tableName) {
|
|
@@ -36,7 +51,9 @@ async function disableRLS(client, tableName) {
|
|
|
36
51
|
await client.query(`ALTER TABLE ${safe} DISABLE ROW LEVEL SECURITY`);
|
|
37
52
|
}
|
|
38
53
|
async function isRLSEnabled(client, tableName) {
|
|
39
|
-
const
|
|
54
|
+
const safe = validateTableName(tableName);
|
|
55
|
+
// The table the name resolves to, in whichever schema that is.
|
|
56
|
+
const res = await client.query(`SELECT relrowsecurity FROM pg_class WHERE oid = to_regclass($1)`, [safe]);
|
|
40
57
|
if (res.rows.length === 0) {
|
|
41
58
|
return false;
|
|
42
59
|
}
|
package/dist/rls/manager.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":";;AAaA,oCA6BC;AAED,gCAMC;AAED,8BAOC;AAED,gCAOC;AAED,oCAcC;AAnFD,uDAAsE;AAEtE,MAAM,aAAa,GAAG,8EAA8E,CAAC;AAErG,wFAAwF;AACxF,SAAS,iBAAiB,CAAC,SAAiB;IAC1C,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,uBAAuB,SAAS,EAAE,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,yEAAyE;IACzE,2EAA2E;IAC3E,6DAA6D;IAC7D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CACjC;;;;oCAIgC,EAChC,CAAC,IAAI,CAAC,CACP,CAAC;IACF,MAAM,MAAM,GAAG,IAAA,mCAAiB,EAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,mBAAmB,IAAI,qEAAqE;YAC1F,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,aAAa,KAAK;YACpD,+DAA+D,CAClE,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,kBAAkB,CAAC,EAAE,CAAC;QACpE,4EAA4E;QAC5E,qDAAqD;QACrD,MAAM,MAAM,CAAC,KAAK,CAAC,qCAAqC,IAAI,WAAW,aAAa,GAAG,CAAC,CAAC;IAC3F,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,UAAU,CAC9B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,6CAA6C,IAAI,EAAE,CAAC,CAAC;AAC1E,CAAC;AAEM,KAAK,UAAU,SAAS,CAC7B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,4BAA4B,CAAC,CAAC;IACpE,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,2BAA2B,CAAC,CAAC;AACrE,CAAC;AAEM,KAAK,UAAU,UAAU,CAC9B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,8BAA8B,CAAC,CAAC;IACtE,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,6BAA6B,CAAC,CAAC;AACvE,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,+DAA+D;IAC/D,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,iEAAiE,EACjE,CAAC,IAAI,CAAC,CACP,CAAC;IACF,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether the row-level security policies on a table restrict rows to
|
|
3
|
+
* the current tenant, from the expressions PostgreSQL stores in pg_policies.
|
|
4
|
+
*
|
|
5
|
+
* `createPolicy` uses this before it keeps an existing tenant_isolation policy
|
|
6
|
+
* or adds one, because a policy's name proves nothing about what it checks, and
|
|
7
|
+
* PostgreSQL ORs permissive policies together: one permissive policy that does
|
|
8
|
+
* not filter by tenant opens the table whatever tenant_isolation says.
|
|
9
|
+
*
|
|
10
|
+
* Rules:
|
|
11
|
+
* - Every permissive policy must filter by tenant for the commands it covers:
|
|
12
|
+
* USING for SELECT, UPDATE, DELETE and ALL, WITH CHECK for INSERT, and WITH
|
|
13
|
+
* CHECK for UPDATE and ALL when it is set. This applies whatever roles the
|
|
14
|
+
* policy names, because this package cannot know which role the
|
|
15
|
+
* application connects as.
|
|
16
|
+
* - Restrictive policies can only narrow access, so they may check anything.
|
|
17
|
+
* - The tenant_isolation policy itself must be permissive, apply to ALL
|
|
18
|
+
* commands, and apply to PUBLIC, like the one `createPolicy` generates and
|
|
19
|
+
* every policy Stratum ships.
|
|
20
|
+
*
|
|
21
|
+
* The check recognises the form this package generates, with the operands in
|
|
22
|
+
* either order, with casts, inside an AND with other conditions, and ORed with
|
|
23
|
+
* Stratum's administrative bypass. Anything else is reported as not filtering
|
|
24
|
+
* by tenant, so an unusual but correct policy is refused, never accepted.
|
|
25
|
+
*/
|
|
26
|
+
/** One row of pg_policies. */
|
|
27
|
+
export interface PolicyRow {
|
|
28
|
+
policyname: string;
|
|
29
|
+
/** "PERMISSIVE" or "RESTRICTIVE". */
|
|
30
|
+
permissive: string;
|
|
31
|
+
/** "ALL", "SELECT", "INSERT", "UPDATE" or "DELETE". */
|
|
32
|
+
cmd: string;
|
|
33
|
+
qual: string | null;
|
|
34
|
+
with_check: string | null;
|
|
35
|
+
/** The roles the policy applies to; ["public"] for PUBLIC. */
|
|
36
|
+
roles: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Every reason the table's policies do not isolate it by tenant. Empty when
|
|
40
|
+
* they do, or when the table has no policies.
|
|
41
|
+
*/
|
|
42
|
+
export declare function tablePolicyIssues(policies: PolicyRow[]): string[];
|
|
43
|
+
//# sourceMappingURL=policy-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy-check.d.ts","sourceRoot":"","sources":["../../src/rls/policy-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,8BAA8B;AAC9B,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,8DAA8D;IAC9D,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AA0JD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAgBjE"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Decides whether the row-level security policies on a table restrict rows to
|
|
4
|
+
* the current tenant, from the expressions PostgreSQL stores in pg_policies.
|
|
5
|
+
*
|
|
6
|
+
* `createPolicy` uses this before it keeps an existing tenant_isolation policy
|
|
7
|
+
* or adds one, because a policy's name proves nothing about what it checks, and
|
|
8
|
+
* PostgreSQL ORs permissive policies together: one permissive policy that does
|
|
9
|
+
* not filter by tenant opens the table whatever tenant_isolation says.
|
|
10
|
+
*
|
|
11
|
+
* Rules:
|
|
12
|
+
* - Every permissive policy must filter by tenant for the commands it covers:
|
|
13
|
+
* USING for SELECT, UPDATE, DELETE and ALL, WITH CHECK for INSERT, and WITH
|
|
14
|
+
* CHECK for UPDATE and ALL when it is set. This applies whatever roles the
|
|
15
|
+
* policy names, because this package cannot know which role the
|
|
16
|
+
* application connects as.
|
|
17
|
+
* - Restrictive policies can only narrow access, so they may check anything.
|
|
18
|
+
* - The tenant_isolation policy itself must be permissive, apply to ALL
|
|
19
|
+
* commands, and apply to PUBLIC, like the one `createPolicy` generates and
|
|
20
|
+
* every policy Stratum ships.
|
|
21
|
+
*
|
|
22
|
+
* The check recognises the form this package generates, with the operands in
|
|
23
|
+
* either order, with casts, inside an AND with other conditions, and ORed with
|
|
24
|
+
* Stratum's administrative bypass. Anything else is reported as not filtering
|
|
25
|
+
* by tenant, so an unusual but correct policy is refused, never accepted.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.tablePolicyIssues = tablePolicyIssues;
|
|
29
|
+
/** The session setting that holds the current tenant (see rls/session.ts). */
|
|
30
|
+
const TENANT_SETTING = "app.current_tenant_id";
|
|
31
|
+
/** The setting Stratum's own policies admit for administrative access. */
|
|
32
|
+
const BYPASS_SETTING = "app.bypass_rls";
|
|
33
|
+
/** Drops whitespace and casts, lower-cases, and keeps string literals intact. */
|
|
34
|
+
function normalize(expr) {
|
|
35
|
+
let out = "";
|
|
36
|
+
for (let i = 0; i < expr.length; i++) {
|
|
37
|
+
const ch = expr[i];
|
|
38
|
+
if (ch === "'") {
|
|
39
|
+
const end = expr.indexOf("'", i + 1);
|
|
40
|
+
const stop = end === -1 ? expr.length : end + 1;
|
|
41
|
+
out += expr.slice(i, stop);
|
|
42
|
+
i = stop - 1;
|
|
43
|
+
}
|
|
44
|
+
else if (!/\s/.test(ch)) {
|
|
45
|
+
out += ch.toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Casts such as ::text, ::uuid and ::character varying (spaces already gone).
|
|
49
|
+
return out.replace(/::[a-z_]+(?:\[\])?/g, "");
|
|
50
|
+
}
|
|
51
|
+
/** Index of the parenthesis that closes the one at `open`. */
|
|
52
|
+
function closingParen(expr, open) {
|
|
53
|
+
let depth = 0;
|
|
54
|
+
for (let i = open; i < expr.length; i++) {
|
|
55
|
+
const ch = expr[i];
|
|
56
|
+
if (ch === "'") {
|
|
57
|
+
const end = expr.indexOf("'", i + 1);
|
|
58
|
+
if (end === -1)
|
|
59
|
+
return -1;
|
|
60
|
+
i = end;
|
|
61
|
+
}
|
|
62
|
+
else if (ch === "(") {
|
|
63
|
+
depth++;
|
|
64
|
+
}
|
|
65
|
+
else if (ch === ")") {
|
|
66
|
+
depth--;
|
|
67
|
+
if (depth === 0)
|
|
68
|
+
return i;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return -1;
|
|
72
|
+
}
|
|
73
|
+
/** Removes parentheses that wrap the whole expression. */
|
|
74
|
+
function stripOuterParens(expr) {
|
|
75
|
+
let e = expr;
|
|
76
|
+
while (e.startsWith("(") && e.endsWith(")") && closingParen(e, 0) === e.length - 1) {
|
|
77
|
+
e = e.slice(1, -1);
|
|
78
|
+
}
|
|
79
|
+
return e;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Splits a normalized expression on a top-level boolean operator. Whitespace
|
|
83
|
+
* is gone, so the operator is found between a closing parenthesis and an
|
|
84
|
+
* opening one, which is how PostgreSQL prints every operand of AND / OR.
|
|
85
|
+
*/
|
|
86
|
+
function splitTopLevel(expr, op) {
|
|
87
|
+
const parts = [];
|
|
88
|
+
let depth = 0;
|
|
89
|
+
let start = 0;
|
|
90
|
+
for (let i = 0; i < expr.length; i++) {
|
|
91
|
+
const ch = expr[i];
|
|
92
|
+
if (ch === "'") {
|
|
93
|
+
const end = expr.indexOf("'", i + 1);
|
|
94
|
+
if (end === -1)
|
|
95
|
+
break;
|
|
96
|
+
i = end;
|
|
97
|
+
}
|
|
98
|
+
else if (ch === "(") {
|
|
99
|
+
depth++;
|
|
100
|
+
}
|
|
101
|
+
else if (ch === ")") {
|
|
102
|
+
depth--;
|
|
103
|
+
if (depth === 0 && expr.startsWith(`${op}(`, i + 1)) {
|
|
104
|
+
parts.push(expr.slice(start, i + 1));
|
|
105
|
+
start = i + 1 + op.length;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
parts.push(expr.slice(start));
|
|
110
|
+
return parts.map(stripOuterParens);
|
|
111
|
+
}
|
|
112
|
+
/** Removes every parenthesis; only safe on an operand with no AND / OR in it. */
|
|
113
|
+
function flatten(expr) {
|
|
114
|
+
return expr.replace(/[()]/g, "");
|
|
115
|
+
}
|
|
116
|
+
const SETTING_READ = `current_setting'${TENANT_SETTING.replace(".", "\\.")}'(?:,(?:true|false))?`;
|
|
117
|
+
/** The current tenant, read directly or through NULLIF(..., ''). */
|
|
118
|
+
const CURRENT_TENANT = `(?:nullif${SETTING_READ},''|${SETTING_READ})`;
|
|
119
|
+
const TENANT_MATCH = new RegExp(`^(?:tenant_id=${CURRENT_TENANT}|${CURRENT_TENANT}=tenant_id)$`);
|
|
120
|
+
const BYPASS_MATCH = new RegExp(`^current_setting'${BYPASS_SETTING.replace(".", "\\.")}'(?:,(?:true|false))?='on'$`);
|
|
121
|
+
/** True when the normalized expression only admits rows of the current tenant. */
|
|
122
|
+
function filtersByTenant(expr) {
|
|
123
|
+
const e = stripOuterParens(expr);
|
|
124
|
+
const disjuncts = splitTopLevel(e, "or");
|
|
125
|
+
if (disjuncts.length > 1) {
|
|
126
|
+
// Every branch must be safe; Stratum's administrative bypass is one of them.
|
|
127
|
+
return (disjuncts.every((d) => BYPASS_MATCH.test(flatten(d)) || filtersByTenant(d)) &&
|
|
128
|
+
disjuncts.some((d) => !BYPASS_MATCH.test(flatten(d))));
|
|
129
|
+
}
|
|
130
|
+
const conjuncts = splitTopLevel(e, "and");
|
|
131
|
+
if (conjuncts.length > 1) {
|
|
132
|
+
return conjuncts.some((c) => filtersByTenant(c));
|
|
133
|
+
}
|
|
134
|
+
return TENANT_MATCH.test(flatten(e));
|
|
135
|
+
}
|
|
136
|
+
/** An expression on one line, for messages. */
|
|
137
|
+
function oneLine(expr) {
|
|
138
|
+
return expr === null ? "none" : expr.replace(/\s+/g, " ");
|
|
139
|
+
}
|
|
140
|
+
/** Why one permissive policy lets rows of other tenants through, or null. */
|
|
141
|
+
function permissiveIssue(p) {
|
|
142
|
+
const name = `policy "${oneLine(p.policyname)}" (${p.cmd})`;
|
|
143
|
+
// INSERT policies have only WITH CHECK; SELECT and DELETE only USING. For
|
|
144
|
+
// ALL and UPDATE a missing WITH CHECK means PostgreSQL reuses USING.
|
|
145
|
+
if (p.cmd !== "INSERT") {
|
|
146
|
+
if (p.qual === null || !filtersByTenant(normalize(p.qual))) {
|
|
147
|
+
return `${name} USING (${oneLine(p.qual)}) does not filter by tenant`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (p.cmd === "INSERT" || p.with_check !== null) {
|
|
151
|
+
if (p.with_check === null || !filtersByTenant(normalize(p.with_check))) {
|
|
152
|
+
return `${name} WITH CHECK (${oneLine(p.with_check)}) does not filter by tenant`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
/** Why the tenant_isolation policy is not the shape createPolicy generates, or null. */
|
|
158
|
+
function tenantIsolationShapeIssue(p) {
|
|
159
|
+
const name = `policy "${oneLine(p.policyname)}"`;
|
|
160
|
+
if (p.permissive !== "PERMISSIVE") {
|
|
161
|
+
return `${name} is ${p.permissive.toLowerCase()}, not permissive`;
|
|
162
|
+
}
|
|
163
|
+
if (p.cmd !== "ALL") {
|
|
164
|
+
return `${name} applies to ${p.cmd} only, not ALL commands`;
|
|
165
|
+
}
|
|
166
|
+
if (p.roles.length !== 1 || p.roles[0] !== "public") {
|
|
167
|
+
return `${name} applies to roles ${p.roles.join(", ")}, not PUBLIC`;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Every reason the table's policies do not isolate it by tenant. Empty when
|
|
173
|
+
* they do, or when the table has no policies.
|
|
174
|
+
*/
|
|
175
|
+
function tablePolicyIssues(policies) {
|
|
176
|
+
const issues = [];
|
|
177
|
+
for (const p of policies) {
|
|
178
|
+
if (p.policyname === "tenant_isolation") {
|
|
179
|
+
const shape = tenantIsolationShapeIssue(p);
|
|
180
|
+
if (shape !== null) {
|
|
181
|
+
issues.push(shape);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (p.permissive === "PERMISSIVE") {
|
|
186
|
+
const issue = permissiveIssue(p);
|
|
187
|
+
if (issue !== null)
|
|
188
|
+
issues.push(issue);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return issues;
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=policy-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy-check.js","sourceRoot":"","sources":["../../src/rls/policy-check.ts"],"names":[],"mappings":"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"}
|