@nodii/db-rls 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth-schema.d.ts +120 -0
- package/dist/auth-schema.d.ts.map +1 -1
- package/dist/auth-schema.js +177 -0
- package/dist/auth-schema.js.map +1 -1
- package/dist/context.d.ts +26 -8
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +35 -24
- package/dist/context.js.map +1 -1
- package/dist/drizzle-client.d.ts +17 -5
- package/dist/drizzle-client.d.ts.map +1 -1
- package/dist/drizzle-client.js +19 -18
- package/dist/drizzle-client.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -7
- package/dist/index.js.map +1 -1
- package/dist/postgres-js-client.d.ts +17 -10
- package/dist/postgres-js-client.d.ts.map +1 -1
- package/dist/postgres-js-client.js +19 -21
- package/dist/postgres-js-client.js.map +1 -1
- package/dist/rls-policy-gen.d.ts +43 -27
- package/dist/rls-policy-gen.d.ts.map +1 -1
- package/dist/rls-policy-gen.js +40 -15
- package/dist/rls-policy-gen.js.map +1 -1
- package/dist/rls-policy.d.ts +11 -1
- package/dist/rls-policy.d.ts.map +1 -1
- package/dist/rls-policy.js +31 -4
- package/dist/rls-policy.js.map +1 -1
- package/dist/tenant-fence.d.ts +165 -0
- package/dist/tenant-fence.d.ts.map +1 -0
- package/dist/tenant-fence.js +542 -0
- package/dist/tenant-fence.js.map +1 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -1
- package/dist/verify-rls-cli.d.ts.map +1 -1
- package/dist/verify-rls-cli.js +86 -10
- package/dist/verify-rls-cli.js.map +1 -1
- package/dist/verify-rls.d.ts +26 -5
- package/dist/verify-rls.d.ts.map +1 -1
- package/dist/verify-rls.js +73 -5
- package/dist/verify-rls.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
// verifyTenantFence — the catalog-time half of the guard.
|
|
2
|
+
//
|
|
3
|
+
// Fixing the generator fixes what the generator emits. The operator's
|
|
4
|
+
// requirement is stronger: *"enforce that even if this shape regresses we DON'T
|
|
5
|
+
// ALLOW ANYTHING CROSS-TENANT EVER"*. Three mechanisms cover three different
|
|
6
|
+
// ways a regression arrives, and they are deliberately NOT the same mechanism:
|
|
7
|
+
//
|
|
8
|
+
// 1. GENERATION TIME — `authenticatedPredicate` is the single source of truth
|
|
9
|
+
// for every emitter (both string generators, the Drizzle builder, and the
|
|
10
|
+
// three libraries that inline a copy). One function to change, and the
|
|
11
|
+
// byte-equality tests in saga / dsar / audit-chain fail the moment a copy
|
|
12
|
+
// drifts from it.
|
|
13
|
+
//
|
|
14
|
+
// 2. SESSION TIME — `withUserMembershipContext` throws, and
|
|
15
|
+
// `auth.session_tenant_ids()` raises 42501 on an unbound session, so a
|
|
16
|
+
// tenant-less user session cannot quietly become either a union or a
|
|
17
|
+
// silently-empty grid.
|
|
18
|
+
//
|
|
19
|
+
// 3. CATALOG TIME — THIS MODULE. It reads what POSTGRES ACTUALLY STORED
|
|
20
|
+
// (`pg_policies.qual` / `.with_check`, already deparsed by the server) and
|
|
21
|
+
// refuses to let a deployment proceed unless every `authenticated` policy
|
|
22
|
+
// resolves through `auth.session_tenant_ids()`.
|
|
23
|
+
//
|
|
24
|
+
// (3) is the one a static source lint cannot reach, and that is the whole reason
|
|
25
|
+
// it exists. A source rule sees the repository. It does not see:
|
|
26
|
+
// - a `CREATE POLICY` run by hand against prod during an incident;
|
|
27
|
+
// - a migration that landed from a branch the lint never ran on;
|
|
28
|
+
// - a policy that predates the lint and was never regenerated;
|
|
29
|
+
// - a hand-edited `0010_rls_policies.sql` whose header says "GENERATED";
|
|
30
|
+
// - a service still on an old @nodii/db-rls that emits the union;
|
|
31
|
+
// - a boot-time re-emitter (saga / dsar / audit-chain DROP+CREATE on EVERY
|
|
32
|
+
// boot) that reinstates its own copy after a correct migration ran.
|
|
33
|
+
// The catalog sees all six, because the catalog is the thing that actually
|
|
34
|
+
// decides who reads which row. A source lint and this verifier are complements:
|
|
35
|
+
// the lint fails the PR, this fails the deploy.
|
|
36
|
+
//
|
|
37
|
+
// WHY THE ASSERTION IS POSITIVE.
|
|
38
|
+
//
|
|
39
|
+
// The tempting implementation is "find policies containing
|
|
40
|
+
// `user_tenant_ids` without a fence, and fail if any remain" — an ABSENCE
|
|
41
|
+
// assertion. It passes in a case it must not: a sweep (or a hand-edit, or a
|
|
42
|
+
// regexp_replace repair) that mangles a policy into something which no longer
|
|
43
|
+
// mentions `user_tenant_ids` AND is not correctly fenced either. Absence of the
|
|
44
|
+
// bad string is not presence of the good one. So the rule here is inverted:
|
|
45
|
+
// every policy granted to `authenticated` MUST resolve through
|
|
46
|
+
// `auth.session_tenant_ids()`, or appear on an explicit, argued exemption list.
|
|
47
|
+
// Anything else is an offender, including shapes nobody has thought of yet.
|
|
48
|
+
//
|
|
49
|
+
// Wire it into `db:setup` / `db:verify` next to `verifyRls`:
|
|
50
|
+
//
|
|
51
|
+
// await verifyRls(pool, { requiredTables });
|
|
52
|
+
// await verifyTenantFence(pool); // throws TenantFenceError, loud
|
|
53
|
+
import { SESSION_TENANT_IDS_DEGRADED_MARKER } from "./auth-schema.js";
|
|
54
|
+
import { DbRlsError } from "./types.js";
|
|
55
|
+
/**
|
|
56
|
+
* Thrown by {@link verifyTenantFence}. Carries the structured offender list so
|
|
57
|
+
* a `db:setup` gate can render a precise failure — and so a caller can tell
|
|
58
|
+
* "this is a stale service that needs regenerating" apart from "someone
|
|
59
|
+
* hand-wrote a cross-tenant policy in prod".
|
|
60
|
+
*/
|
|
61
|
+
export class TenantFenceError extends DbRlsError {
|
|
62
|
+
offenders;
|
|
63
|
+
constructor(offenders) {
|
|
64
|
+
super(formatFenceMessage(offenders));
|
|
65
|
+
this.name = "TenantFenceError";
|
|
66
|
+
this.offenders = offenders;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const REASON_TEXT = {
|
|
70
|
+
cross_tenant_union: "consults auth.user_tenant_ids directly instead of auth.session_tenant_ids() — resolves across EVERY tenant the principal belongs to",
|
|
71
|
+
unfenced_permissive: "is permissive with no tenant reference at all — every tenant, every row",
|
|
72
|
+
not_resolved_through_session: "does not resolve through auth.session_tenant_ids() — it may be fenced some other way, but nothing here proves it, and an unrecognized shape is not a safe shape",
|
|
73
|
+
resolver_missing: "auth.session_tenant_ids() is not installed — apply SESSION_TENANT_IDS_SQL (applyAuthSchemaMigrations ships it)",
|
|
74
|
+
resolver_not_loud: "auth.session_tenant_ids() does not RAISE on an unbound session — it was replaced with a NULL-returning getter, which restores silent failure",
|
|
75
|
+
resolver_not_security_definer: "auth.session_tenant_ids() is not SECURITY DEFINER — it cannot read the membership mirror (permission denied), or will recurse into the mirror's own policy (54001)",
|
|
76
|
+
resolver_degraded: "the DEGRADED fence-only resolver is installed — the session tenant is trusted WITHOUT proving membership. Valid only for a service with no membership mirror; pass allowDegradedResolver to accept it deliberately",
|
|
77
|
+
membership_helper_is_stub: "auth.user_tenant_ids is still the bootstrap stub returning the empty set — the strict resolver intersects to nothing, so the authenticated pool is silently deny-all",
|
|
78
|
+
rls_disabled_on_tenant_table: "has a tenant_id column but ROW LEVEL SECURITY is NOT ENABLED — with the canonical blanket grants this table is readable and writable across EVERY tenant, and no policy is ever consulted. Fix: ALTER TABLE … ENABLE ROW LEVEL SECURITY and apply withRls()",
|
|
79
|
+
tenant_unscoped_unique_index: "is a UNIQUE index on a tenant-scoped table that omits the tenant column from its KEY — RLS filters visibility but does NOT partition uniqueness, so one tenant can collide with (or resolve onto) another tenant's row. Fix: make it UNIQUE (tenant_id, …)",
|
|
80
|
+
unprotected_relation: "exposes a tenant column but cannot enforce RLS — a materialized/foreign relation has no row policies at all, and a view without `security_invoker = true` runs as its OWNER and bypasses the underlying table's policies. Fix: CREATE VIEW … WITH (security_invoker = true), or revoke it from authenticated",
|
|
81
|
+
};
|
|
82
|
+
function formatFenceMessage(offenders) {
|
|
83
|
+
const lines = offenders.map((o) => ` - ${o.object} [${o.policy}${o.command === "-" ? "" : ` ${o.command}`}] ${REASON_TEXT[o.reason]}\n ${o.expression ?? "(no expression)"}`);
|
|
84
|
+
return `tenant_fence: ${offenders.length} authenticated-pool policy/helper defect(s). A user session must reach exactly ONE tenant — the one the edge resolved from the request — and nothing else, ever.\n${lines.join("\n")}\nFix: regenerate policies with @nodii/db-rls withRls() / generateRlsPolicy(), which emit \`<col> IN (SELECT auth.session_tenant_ids())\`, and apply the resolver via applyAuthSchemaMigrations. Nodii-staff cross-tenant access is a different path entirely: the platform_ops pool + withPlatformOpsContext.`;
|
|
85
|
+
}
|
|
86
|
+
// Matching notes — the `auth.` prefix is OPTIONAL on purpose.
|
|
87
|
+
//
|
|
88
|
+
// `pg_policies.qual` is `pg_get_expr(...)`, and pg_get_expr omits a schema
|
|
89
|
+
// qualifier for any function already reachable through the CALLER'S
|
|
90
|
+
// search_path. So the identical policy deparses as
|
|
91
|
+
// `(tenant_id IN (SELECT auth.session_tenant_ids()))` with search_path=public
|
|
92
|
+
// `(tenant_id IN (SELECT session_tenant_ids()))` with `auth` on the path
|
|
93
|
+
// A verifier that insisted on `auth.` would silently PASS every policy on every
|
|
94
|
+
// service whose migration runner puts `auth` in its search_path — which is most
|
|
95
|
+
// of them. A security check that quietly finds nothing is worse than no check,
|
|
96
|
+
// because it is reported as a green.
|
|
97
|
+
const RESOLVER_RE = /\b(?:auth\s*\.\s*)?session_tenant_ids\s*\(/i;
|
|
98
|
+
const UNION_RE = /\b(?:auth\s*\.\s*)?user_tenant_ids\s*\(/i;
|
|
99
|
+
/** Does this expression resolve through the session resolver? */
|
|
100
|
+
export function resolvesThroughSession(expr) {
|
|
101
|
+
return expr !== null && RESOLVER_RE.test(expr);
|
|
102
|
+
}
|
|
103
|
+
/** Does this expression consult the raw membership union? */
|
|
104
|
+
export function consultsMembershipUnion(expr) {
|
|
105
|
+
return expr !== null && UNION_RE.test(expr);
|
|
106
|
+
}
|
|
107
|
+
// (Role matching used to live here as a string compare over
|
|
108
|
+
// `pg_policies.roles`. It is now resolved by the catalog in SQL — see the note
|
|
109
|
+
// on the policy query in `verifyTenantFence`. A `{public}` policy applies to
|
|
110
|
+
// authenticated without containing the word, and role membership is
|
|
111
|
+
// transitive; neither fact is visible to a substring test.)
|
|
112
|
+
function isExempt(row, exempt) {
|
|
113
|
+
if (!exempt || exempt.length === 0)
|
|
114
|
+
return false;
|
|
115
|
+
const qualified = `${row.tablename}.${row.policyname}`;
|
|
116
|
+
return exempt.includes(row.policyname) || exempt.includes(qualified);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Classify ONE authenticated policy. Exported so the test-suite can assert the
|
|
120
|
+
* classification directly without standing up every offending shape in the
|
|
121
|
+
* catalog, and so a caller can reuse the rule in a bespoke gate.
|
|
122
|
+
*
|
|
123
|
+
* The order matters: the two SPECIFIC diagnoses come first because they tell a
|
|
124
|
+
* reader what to do, and the general positive-rule failure is the fallback.
|
|
125
|
+
*/
|
|
126
|
+
export function classifyAuthPolicy(expr) {
|
|
127
|
+
if (resolvesThroughSession(expr))
|
|
128
|
+
return null;
|
|
129
|
+
if (consultsMembershipUnion(expr))
|
|
130
|
+
return "cross_tenant_union";
|
|
131
|
+
if (expr === null || expr.trim() === "" || /^\(?\s*true\s*\)?$/i.test(expr)) {
|
|
132
|
+
return "unfenced_permissive";
|
|
133
|
+
}
|
|
134
|
+
return "not_resolved_through_session";
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Asserts that EVERY policy granted to `authenticated` resolves through
|
|
138
|
+
* `auth.session_tenant_ids()`, and that the resolver itself is installed, loud,
|
|
139
|
+
* and SECURITY DEFINER.
|
|
140
|
+
*
|
|
141
|
+
* @throws {TenantFenceError} listing every offender, with the deparsed
|
|
142
|
+
* expression straight from the catalog.
|
|
143
|
+
*/
|
|
144
|
+
export async function verifyTenantFence(querier, opts = {}) {
|
|
145
|
+
const schemas = opts.schema === undefined
|
|
146
|
+
? ["public"]
|
|
147
|
+
: typeof opts.schema === "string"
|
|
148
|
+
? [opts.schema]
|
|
149
|
+
: [...opts.schema];
|
|
150
|
+
const checkHelpers = opts.checkHelpers ?? true;
|
|
151
|
+
const offenders = [];
|
|
152
|
+
// ROLE RESOLUTION IS DONE IN SQL, AND IT IS NOT A STRING COMPARE.
|
|
153
|
+
//
|
|
154
|
+
// The first version of this read `pg_policies.roles::text` and skipped any
|
|
155
|
+
// policy whose text did not literally contain "authenticated". Three ways
|
|
156
|
+
// through that, each proven to yield a live cross-tenant read AND write while
|
|
157
|
+
// the verifier reported zero offenders:
|
|
158
|
+
//
|
|
159
|
+
// 1. `CREATE POLICY … FOR ALL USING (true)` with NO `TO` clause. Postgres
|
|
160
|
+
// records `polroles = {0}` (PUBLIC), rendered `{public}`. It APPLIES to
|
|
161
|
+
// authenticated and ORs with the fenced policy — permissive policies are
|
|
162
|
+
// unioned — but the literal string is absent, so it was skipped. This is
|
|
163
|
+
// item 1 on this module's own threat list: the hand-written hotfix.
|
|
164
|
+
// 2. Explicit `TO PUBLIC` — same thing, said out loud.
|
|
165
|
+
// 3. `TO app_reader`, where `authenticated` has been GRANTed app_reader.
|
|
166
|
+
// Role membership is transitive; a string compare does not know that.
|
|
167
|
+
//
|
|
168
|
+
// `pg_has_role(role, role, 'USAGE')` is the authority Postgres itself uses,
|
|
169
|
+
// and OID 0 in `polroles` is the canonical spelling of PUBLIC. Both are
|
|
170
|
+
// resolved here rather than in TypeScript, because the catalog is the only
|
|
171
|
+
// thing that knows the role graph.
|
|
172
|
+
const { rows } = await querier.query(`SELECT n.nspname AS schemaname,
|
|
173
|
+
c.relname AS tablename,
|
|
174
|
+
pol.polname AS policyname,
|
|
175
|
+
CASE pol.polcmd WHEN 'r' THEN 'SELECT' WHEN 'a' THEN 'INSERT'
|
|
176
|
+
WHEN 'w' THEN 'UPDATE' WHEN 'd' THEN 'DELETE'
|
|
177
|
+
ELSE 'ALL' END AS cmd,
|
|
178
|
+
pol.polpermissive AS permissive,
|
|
179
|
+
pg_get_expr(pol.polqual, pol.polrelid) AS qual,
|
|
180
|
+
pg_get_expr(pol.polwithcheck, pol.polrelid) AS with_check,
|
|
181
|
+
(
|
|
182
|
+
-- PUBLIC (OID 0) applies to everyone. Otherwise ask Postgres
|
|
183
|
+
-- whether 'authenticated' IS or INHERITS the policy's role.
|
|
184
|
+
EXISTS (SELECT 1 FROM unnest(pol.polroles) r WHERE r = 0)
|
|
185
|
+
OR (
|
|
186
|
+
EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated')
|
|
187
|
+
AND EXISTS (
|
|
188
|
+
SELECT 1 FROM unnest(pol.polroles) r
|
|
189
|
+
WHERE pg_has_role('authenticated', r, 'USAGE')
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
) AS applies_to_authenticated
|
|
193
|
+
FROM pg_policy pol
|
|
194
|
+
JOIN pg_class c ON c.oid = pol.polrelid
|
|
195
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
196
|
+
WHERE n.nspname = ANY($1::text[])
|
|
197
|
+
ORDER BY c.relname, pol.polname`, [schemas]);
|
|
198
|
+
const wanted = opts.tables?.map((t) => t.split(".").pop() ?? t);
|
|
199
|
+
for (const row of rows) {
|
|
200
|
+
if (!row.applies_to_authenticated)
|
|
201
|
+
continue;
|
|
202
|
+
// A RESTRICTIVE policy can only ever narrow access — it is ANDed, never
|
|
203
|
+
// ORed — so it cannot be the source of a cross-tenant grant and is not
|
|
204
|
+
// required to carry the fence itself.
|
|
205
|
+
if (row.permissive === false)
|
|
206
|
+
continue;
|
|
207
|
+
if (wanted && !wanted.includes(row.tablename))
|
|
208
|
+
continue;
|
|
209
|
+
if (isExempt(row, opts.exemptPolicies))
|
|
210
|
+
continue;
|
|
211
|
+
// A policy can carry USING, WITH CHECK, or both. EVERY clause present must
|
|
212
|
+
// resolve — a fenced read with an unfenced WITH CHECK is a cross-tenant
|
|
213
|
+
// WRITE, which is exactly what the refund incident exercised.
|
|
214
|
+
for (const expr of [row.qual, row.with_check]) {
|
|
215
|
+
if (expr === null)
|
|
216
|
+
continue;
|
|
217
|
+
const reason = classifyAuthPolicy(expr);
|
|
218
|
+
if (reason !== null) {
|
|
219
|
+
offenders.push({
|
|
220
|
+
object: `${row.schemaname}.${row.tablename}`,
|
|
221
|
+
policy: row.policyname,
|
|
222
|
+
command: row.cmd,
|
|
223
|
+
reason,
|
|
224
|
+
expression: expr,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// A policy with NEITHER clause is permissive-everything.
|
|
229
|
+
if (row.qual === null && row.with_check === null) {
|
|
230
|
+
offenders.push({
|
|
231
|
+
object: `${row.schemaname}.${row.tablename}`,
|
|
232
|
+
policy: row.policyname,
|
|
233
|
+
command: row.cmd,
|
|
234
|
+
reason: "unfenced_permissive",
|
|
235
|
+
expression: null,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (opts.checkTableCoverage ?? true) {
|
|
240
|
+
offenders.push(...(await checkTenantTableCoverage(querier, schemas, opts)));
|
|
241
|
+
}
|
|
242
|
+
if (opts.checkUniqueIndexes ?? true) {
|
|
243
|
+
offenders.push(...(await checkTenantUniqueIndexes(querier, schemas, opts)));
|
|
244
|
+
}
|
|
245
|
+
if (checkHelpers) {
|
|
246
|
+
offenders.push(...(await checkResolverHelpers(querier, opts.allowDegradedResolver)));
|
|
247
|
+
}
|
|
248
|
+
if (offenders.length > 0)
|
|
249
|
+
throw new TenantFenceError(offenders);
|
|
250
|
+
}
|
|
251
|
+
const RELKIND_LABEL = {
|
|
252
|
+
v: "VIEW",
|
|
253
|
+
m: "MATERIALIZED VIEW",
|
|
254
|
+
f: "FOREIGN TABLE",
|
|
255
|
+
p: "PARTITIONED TABLE",
|
|
256
|
+
r: "TABLE",
|
|
257
|
+
};
|
|
258
|
+
/** Default column names that mark a relation as tenant-scoped. `org_id` is in
|
|
259
|
+
* the default set because `withRls`/`generateRlsPolicy` accept an arbitrary
|
|
260
|
+
* `tenantColumn` and ship a test for `org_id` specifically — coverage keyed on
|
|
261
|
+
* a single configured name exempted exactly the tables the library encourages
|
|
262
|
+
* people to create. */
|
|
263
|
+
const DEFAULT_TENANT_COLUMNS = ["tenant_id", "org_id", "organization_id"];
|
|
264
|
+
/** Normalise a Postgres `text[]` that may arrive parsed or as `{a,b}`. */
|
|
265
|
+
function toStringArray(v) {
|
|
266
|
+
if (Array.isArray(v))
|
|
267
|
+
return v;
|
|
268
|
+
if (typeof v !== "string")
|
|
269
|
+
return [];
|
|
270
|
+
return v
|
|
271
|
+
.replace(/^\{|\}$/g, "")
|
|
272
|
+
.split(",")
|
|
273
|
+
.map((x) => x.replace(/^"|"$/g, "").trim())
|
|
274
|
+
.filter(Boolean);
|
|
275
|
+
}
|
|
276
|
+
function tenantColumnNames(opts) {
|
|
277
|
+
const extra = opts.tenantColumn ? [opts.tenantColumn] : [];
|
|
278
|
+
const all = [...extra, ...(opts.tenantColumns ?? DEFAULT_TENANT_COLUMNS)];
|
|
279
|
+
return [...new Set(all)];
|
|
280
|
+
}
|
|
281
|
+
function tableExempt(row, exempt) {
|
|
282
|
+
if (!exempt || exempt.length === 0)
|
|
283
|
+
return false;
|
|
284
|
+
return (exempt.includes(row.tablename) ||
|
|
285
|
+
exempt.includes(`${row.schemaname}.${row.tablename}`));
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* THE TABLE-LEVEL POSITIVE ASSERTION.
|
|
289
|
+
*
|
|
290
|
+
* Discovers tenant-scoped tables FROM THE CATALOG — every table carrying a
|
|
291
|
+
* `tenant_id` column — rather than from a caller-supplied list, and requires
|
|
292
|
+
* each to have `relrowsecurity = true`.
|
|
293
|
+
*
|
|
294
|
+
* The discovery is the entire point, and it is why this does not simply reuse
|
|
295
|
+
* `verifyRls`. `verifyRls` takes `requiredTables`: the caller enumerates the
|
|
296
|
+
* tables that need RLS. But the failure being defended against is a migration
|
|
297
|
+
* that adds a tenant-scoped table and forgets to protect it — and an author who
|
|
298
|
+
* forgets `ENABLE ROW LEVEL SECURITY` in that migration is the same author who
|
|
299
|
+
* forgets to add the table to the list, in the same change. A hand-maintained
|
|
300
|
+
* list is blind to precisely the tables at risk, which is why `verifyRls` can
|
|
301
|
+
* sit in a repo, wired and green, while twelve unprotected tables accumulate
|
|
302
|
+
* behind it. Asking the catalog "which tables have a tenant column?" cannot be
|
|
303
|
+
* forgotten, because nobody has to remember anything.
|
|
304
|
+
*
|
|
305
|
+
* Tables WITHOUT a tenant column are deliberately not considered: a
|
|
306
|
+
* platform-global reference table is GRANT-controlled with no RLS by design
|
|
307
|
+
* (08-rls § 7.6), and flagging those would train everyone to pass
|
|
308
|
+
* `exemptTables` until the check means nothing.
|
|
309
|
+
*/
|
|
310
|
+
async function checkTenantTableCoverage(querier, schemas, opts) {
|
|
311
|
+
const tenantColumns = tenantColumnNames(opts);
|
|
312
|
+
// relkind: r = table, p = partitioned table, v = view, m = matview,
|
|
313
|
+
// f = foreign table. The original version filtered `relkind = 'r'`, which
|
|
314
|
+
// silently exempted the four kinds that CANNOT protect themselves — and a
|
|
315
|
+
// view over a tenant table is an ordinary migration artifact, not an exotic
|
|
316
|
+
// case. Partitioned parents were excluded too, despite being where the
|
|
317
|
+
// policies for an entire partition set are declared.
|
|
318
|
+
const { rows } = await querier.query(`SELECT n.nspname AS schemaname,
|
|
319
|
+
c.relname AS tablename,
|
|
320
|
+
c.relrowsecurity,
|
|
321
|
+
c.relkind::text AS relkind,
|
|
322
|
+
COALESCE(
|
|
323
|
+
(SELECT true FROM unnest(COALESCE(c.reloptions, '{}')) o
|
|
324
|
+
WHERE lower(replace(o, ' ', '')) = 'security_invoker=true'),
|
|
325
|
+
false) AS security_invoker,
|
|
326
|
+
(SELECT a.attname FROM pg_attribute a
|
|
327
|
+
WHERE a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped
|
|
328
|
+
AND a.attname = ANY($2::text[]) LIMIT 1) AS tenant_column
|
|
329
|
+
FROM pg_class c
|
|
330
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
331
|
+
WHERE c.relkind = ANY('{r,p,v,m,f}')
|
|
332
|
+
AND n.nspname = ANY($1::text[])
|
|
333
|
+
AND EXISTS (
|
|
334
|
+
SELECT 1 FROM pg_attribute a
|
|
335
|
+
WHERE a.attrelid = c.oid
|
|
336
|
+
AND a.attname = ANY($2::text[])
|
|
337
|
+
AND a.attnum > 0
|
|
338
|
+
AND NOT a.attisdropped
|
|
339
|
+
)
|
|
340
|
+
ORDER BY n.nspname, c.relname`, [schemas, tenantColumns]);
|
|
341
|
+
const wanted = opts.tables?.map((t) => t.split(".").pop() ?? t);
|
|
342
|
+
const out = [];
|
|
343
|
+
for (const row of rows) {
|
|
344
|
+
if (wanted && !wanted.includes(row.tablename))
|
|
345
|
+
continue;
|
|
346
|
+
if (tableExempt(row, opts.exemptTables))
|
|
347
|
+
continue;
|
|
348
|
+
const col = row.tenant_column ?? tenantColumns[0];
|
|
349
|
+
if (row.relkind === "v" || row.relkind === "m" || row.relkind === "f") {
|
|
350
|
+
// A view with security_invoker=true executes as the CALLER, so the
|
|
351
|
+
// underlying table's RLS applies and the view is genuinely safe.
|
|
352
|
+
if (row.relkind === "v" && row.security_invoker)
|
|
353
|
+
continue;
|
|
354
|
+
out.push({
|
|
355
|
+
object: `${row.schemaname}.${row.tablename}`,
|
|
356
|
+
policy: `(${RELKIND_LABEL[row.relkind] ?? row.relkind})`,
|
|
357
|
+
command: "-",
|
|
358
|
+
reason: "unprotected_relation",
|
|
359
|
+
expression: `${row.tablename} exposes ${col}; relkind=${row.relkind}${row.relkind === "v" ? ", security_invoker=false" : ", RLS is not supported on this relkind"}`,
|
|
360
|
+
});
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (row.relrowsecurity)
|
|
364
|
+
continue;
|
|
365
|
+
out.push({
|
|
366
|
+
object: `${row.schemaname}.${row.tablename}`,
|
|
367
|
+
policy: "(no policies — RLS is OFF)",
|
|
368
|
+
command: "-",
|
|
369
|
+
reason: "rls_disabled_on_tenant_table",
|
|
370
|
+
expression: `${row.tablename} has a ${col} column; relrowsecurity = false`,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Flags UNIQUE indexes on tenant-scoped tables whose KEY omits the tenant
|
|
377
|
+
* column.
|
|
378
|
+
*
|
|
379
|
+
* RLS decides which rows a session may SEE. It does not partition the
|
|
380
|
+
* uniqueness namespace, and the two are independent in a way that is easy to
|
|
381
|
+
* miss: a table can be flawlessly fenced and still let one tenant reach
|
|
382
|
+
* another's row through a unique key.
|
|
383
|
+
*
|
|
384
|
+
* The live exploit that motivated this: a queue table with a caller-supplied
|
|
385
|
+
* `dedupe_key` carrying `UNIQUE (dedupe_key)` rather than
|
|
386
|
+
* `UNIQUE (tenant_id, dedupe_key)`. Tenant A submits the key tenant B already
|
|
387
|
+
* used; the idempotent-mint lookup — a `SELECT` with no tenant predicate —
|
|
388
|
+
* finds B's row and the endpoint returns it. Even with RLS correctly enabled,
|
|
389
|
+
* the collision still surfaces as a 23505 that leaks the row's existence, and
|
|
390
|
+
* an `ON CONFLICT DO UPDATE` still targets a row the session cannot see.
|
|
391
|
+
*
|
|
392
|
+
* KEY COLUMNS COME FROM THE CATALOG, NOT FROM `indexdef`. The first version
|
|
393
|
+
* sliced the DDL text from the first `(` to the LAST `)`, which swallowed
|
|
394
|
+
* everything a unique index can legally carry after its key list, and then
|
|
395
|
+
* substring-matched the tenant column inside it. Both of these were therefore
|
|
396
|
+
* SKIPPED while remaining fully exploitable:
|
|
397
|
+
*
|
|
398
|
+
* UNIQUE (dedupe_key) WHERE (tenant_id IS NOT NULL) -- partial predicate
|
|
399
|
+
* UNIQUE (dedupe_key) INCLUDE (tenant_id) -- non-key payload
|
|
400
|
+
*
|
|
401
|
+
* A partial `WHERE` narrows which rows participate, not which tenants share the
|
|
402
|
+
* namespace; an `INCLUDE` column is payload and is not part of the uniqueness
|
|
403
|
+
* key at all. `pg_index.indnkeyatts` knows the difference and text-slicing
|
|
404
|
+
* cannot.
|
|
405
|
+
*/
|
|
406
|
+
async function checkTenantUniqueIndexes(querier, schemas, opts) {
|
|
407
|
+
const tenantColumns = tenantColumnNames(opts);
|
|
408
|
+
const { rows } = await querier.query(`SELECT n.nspname AS schemaname,
|
|
409
|
+
t.relname AS tablename,
|
|
410
|
+
ic.relname AS indexname,
|
|
411
|
+
pg_get_indexdef(i.indexrelid) AS indexdef,
|
|
412
|
+
i.indisprimary AS is_primary,
|
|
413
|
+
ARRAY(
|
|
414
|
+
SELECT a.attname
|
|
415
|
+
FROM generate_subscripts(i.indkey, 1) AS k
|
|
416
|
+
JOIN pg_attribute a
|
|
417
|
+
ON a.attrelid = i.indrelid AND a.attnum = i.indkey[k]
|
|
418
|
+
WHERE k < i.indnkeyatts
|
|
419
|
+
) AS key_columns,
|
|
420
|
+
NOT EXISTS (
|
|
421
|
+
SELECT 1
|
|
422
|
+
FROM generate_subscripts(i.indkey, 1) AS k
|
|
423
|
+
JOIN pg_attribute a
|
|
424
|
+
ON a.attrelid = i.indrelid AND a.attnum = i.indkey[k]
|
|
425
|
+
WHERE k < i.indnkeyatts
|
|
426
|
+
AND a.atthasdef IS NOT TRUE
|
|
427
|
+
AND a.attidentity = ''
|
|
428
|
+
) AS all_keys_generated
|
|
429
|
+
FROM pg_index i
|
|
430
|
+
JOIN pg_class t ON t.oid = i.indrelid
|
|
431
|
+
JOIN pg_class ic ON ic.oid = i.indexrelid
|
|
432
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
433
|
+
WHERE i.indisunique
|
|
434
|
+
AND n.nspname = ANY($1::text[])
|
|
435
|
+
AND EXISTS (
|
|
436
|
+
SELECT 1 FROM pg_attribute a
|
|
437
|
+
WHERE a.attrelid = t.oid
|
|
438
|
+
AND a.attname = ANY($2::text[])
|
|
439
|
+
AND a.attnum > 0
|
|
440
|
+
AND NOT a.attisdropped
|
|
441
|
+
)
|
|
442
|
+
ORDER BY t.relname, ic.relname`, [schemas, tenantColumns]);
|
|
443
|
+
const wanted = opts.tables?.map((t) => t.split(".").pop() ?? t);
|
|
444
|
+
const out = [];
|
|
445
|
+
for (const row of rows) {
|
|
446
|
+
if (wanted && !wanted.includes(row.tablename))
|
|
447
|
+
continue;
|
|
448
|
+
if (tableExempt(row, opts.exemptTables))
|
|
449
|
+
continue;
|
|
450
|
+
// `RlsQuerier` is driver-agnostic: node-postgres hands back a parsed
|
|
451
|
+
// string[] for text[], postgres.js and a raw wire read can hand back the
|
|
452
|
+
// literal `{a,b}`. Normalise rather than assume — a silently-empty key list
|
|
453
|
+
// would make every index look unsafe (or, with the test inverted, safe).
|
|
454
|
+
const keyCols = toStringArray(row.key_columns);
|
|
455
|
+
// The tenant column is in the KEY — the namespace is partitioned. Safe.
|
|
456
|
+
if (keyCols.some((c) => tenantColumns.includes(c)))
|
|
457
|
+
continue;
|
|
458
|
+
// A SURROGATE primary key is safe, and only a surrogate one. The original
|
|
459
|
+
// exclusion dropped every `contype = 'p'` index on the rationale that "no
|
|
460
|
+
// tenant chooses its value" — true of `id uuid DEFAULT gen_random_uuid()`,
|
|
461
|
+
// false of `dedupe_key text PRIMARY KEY`, which is a caller-supplied
|
|
462
|
+
// natural key and is exactly the shape that was exploited. The honest test
|
|
463
|
+
// is not "is it the primary key" but "did the SERVER generate every key
|
|
464
|
+
// column", which `atthasdef` / `attidentity` answer directly.
|
|
465
|
+
if (row.is_primary && row.all_keys_generated)
|
|
466
|
+
continue;
|
|
467
|
+
out.push({
|
|
468
|
+
object: `${row.schemaname}.${row.tablename}`,
|
|
469
|
+
policy: row.indexname,
|
|
470
|
+
command: "-",
|
|
471
|
+
reason: "tenant_unscoped_unique_index",
|
|
472
|
+
expression: `${row.indexdef} [key columns: ${keyCols.join(", ")}]`,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
async function checkResolverHelpers(querier, allowDegraded) {
|
|
478
|
+
const out = [];
|
|
479
|
+
const { rows: procs } = await querier.query(`SELECT p.proname, p.prosrc, p.prosecdef
|
|
480
|
+
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
481
|
+
WHERE n.nspname = 'auth'
|
|
482
|
+
AND p.proname IN ('session_tenant_ids','user_tenant_ids')`, []);
|
|
483
|
+
const resolver = procs.find((p) => p.proname === "session_tenant_ids");
|
|
484
|
+
const membership = procs.find((p) => p.proname === "user_tenant_ids");
|
|
485
|
+
if (!resolver) {
|
|
486
|
+
out.push({
|
|
487
|
+
object: "auth.session_tenant_ids()",
|
|
488
|
+
policy: "session_tenant_ids",
|
|
489
|
+
command: "-",
|
|
490
|
+
reason: "resolver_missing",
|
|
491
|
+
expression: null,
|
|
492
|
+
});
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
if (!/RAISE\s+EXCEPTION/i.test(resolver.prosrc)) {
|
|
496
|
+
out.push({
|
|
497
|
+
object: "auth.session_tenant_ids()",
|
|
498
|
+
policy: "session_tenant_ids",
|
|
499
|
+
command: "-",
|
|
500
|
+
reason: "resolver_not_loud",
|
|
501
|
+
expression: resolver.prosrc.slice(0, 400),
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (!resolver.prosecdef) {
|
|
505
|
+
out.push({
|
|
506
|
+
object: "auth.session_tenant_ids()",
|
|
507
|
+
policy: "session_tenant_ids",
|
|
508
|
+
command: "-",
|
|
509
|
+
reason: "resolver_not_security_definer",
|
|
510
|
+
expression: resolver.prosrc.slice(0, 400),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
const degraded = resolver.prosrc.includes(SESSION_TENANT_IDS_DEGRADED_MARKER);
|
|
514
|
+
if (degraded && !allowDegraded) {
|
|
515
|
+
out.push({
|
|
516
|
+
object: "auth.session_tenant_ids()",
|
|
517
|
+
policy: "session_tenant_ids",
|
|
518
|
+
command: "-",
|
|
519
|
+
reason: "resolver_degraded",
|
|
520
|
+
expression: resolver.prosrc.slice(0, 400),
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
// The stub check only matters for the STRICT resolver: the degraded one never
|
|
524
|
+
// consults `user_tenant_ids`, so a stub there is expected, not a defect.
|
|
525
|
+
if (!degraded && membership && isStubMembershipHelper(membership.prosrc)) {
|
|
526
|
+
out.push({
|
|
527
|
+
object: "auth.user_tenant_ids(uuid)",
|
|
528
|
+
policy: "user_tenant_ids",
|
|
529
|
+
command: "-",
|
|
530
|
+
reason: "membership_helper_is_stub",
|
|
531
|
+
expression: membership.prosrc.trim().slice(0, 400),
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
/** The bootstrap stub is `SELECT NULL::uuid WHERE false` — any body that can
|
|
537
|
+
* never return a row. Matched structurally rather than by exact string so a
|
|
538
|
+
* reformatted copy is still caught. */
|
|
539
|
+
export function isStubMembershipHelper(prosrc) {
|
|
540
|
+
return /where\s+false/i.test(prosrc) && !/from\s+\w/i.test(prosrc);
|
|
541
|
+
}
|
|
542
|
+
//# sourceMappingURL=tenant-fence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-fence.js","sourceRoot":"","sources":["../src/tenant-fence.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -118,6 +118,24 @@ export declare class PoolUnavailable extends DbRlsError {
|
|
|
118
118
|
export declare class MissingTenantContext extends DbRlsError {
|
|
119
119
|
constructor();
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Thrown when something asks for a database session that would let a tenant
|
|
123
|
+
* user reach more than one tenant's rows.
|
|
124
|
+
*
|
|
125
|
+
* The only caller that triggers this today is `withUserMembershipContext`,
|
|
126
|
+
* which is retained as a throwing stub so a JavaScript consumer gets a sentence
|
|
127
|
+
* rather than `undefined is not a function`. It is a runtime throw and not
|
|
128
|
+
* merely a removed export because the call sites live in services that do not
|
|
129
|
+
* all typecheck against this package at build time.
|
|
130
|
+
*
|
|
131
|
+
* Nodii STAFF cross-tenant access is a different path entirely and is
|
|
132
|
+
* unaffected: the separate, RBAC-gated, audit-stamped `platform_ops` pool
|
|
133
|
+
* (`withPlatformOpsContext`).
|
|
134
|
+
*/
|
|
135
|
+
export declare class CrossTenantSessionRefused extends DbRlsError {
|
|
136
|
+
readonly helper: string;
|
|
137
|
+
constructor(helper: string);
|
|
138
|
+
}
|
|
121
139
|
export declare class TransactionRolledBack extends DbRlsError {
|
|
122
140
|
readonly cause: unknown;
|
|
123
141
|
constructor(cause: unknown);
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED
|
@@ -45,6 +45,28 @@ export class MissingTenantContext extends DbRlsError {
|
|
|
45
45
|
this.name = "MissingTenantContext";
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Thrown when something asks for a database session that would let a tenant
|
|
50
|
+
* user reach more than one tenant's rows.
|
|
51
|
+
*
|
|
52
|
+
* The only caller that triggers this today is `withUserMembershipContext`,
|
|
53
|
+
* which is retained as a throwing stub so a JavaScript consumer gets a sentence
|
|
54
|
+
* rather than `undefined is not a function`. It is a runtime throw and not
|
|
55
|
+
* merely a removed export because the call sites live in services that do not
|
|
56
|
+
* all typecheck against this package at build time.
|
|
57
|
+
*
|
|
58
|
+
* Nodii STAFF cross-tenant access is a different path entirely and is
|
|
59
|
+
* unaffected: the separate, RBAC-gated, audit-stamped `platform_ops` pool
|
|
60
|
+
* (`withPlatformOpsContext`).
|
|
61
|
+
*/
|
|
62
|
+
export class CrossTenantSessionRefused extends DbRlsError {
|
|
63
|
+
helper;
|
|
64
|
+
constructor(helper) {
|
|
65
|
+
super(`cross_tenant_session_refused: ${helper}() opened an authenticated-pool session with NO tenant bound, so the RLS predicate resolved across EVERY tenant the principal belonged to. That is the mechanism of the cross-tenant approval incident, and it is no longer available. Use withUserContext({ userId, tenantId }, fn) with the tenant the request resolved to. To LIST a user's tenants (switcher / login picker), query auth.user_tenant_ids from a services-pool context — that function is intact; what is gone is opening a data session that spans them. Nodii staff cross-tenant access is unchanged: withPlatformOpsContext.`);
|
|
66
|
+
this.name = "CrossTenantSessionRefused";
|
|
67
|
+
this.helper = helper;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
48
70
|
export class TransactionRolledBack extends DbRlsError {
|
|
49
71
|
cause;
|
|
50
72
|
constructor(cause) {
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,EAAE;AACF,oEAAoE;AACpE,uEAAuE;AACvE,EAAE;AACF,gBAAgB;AAChB,qDAAqD;AACrD,uEAAuE;AACvE,0EAA0E;AAC1E,oDAAoD;AACpD,0EAA0E;AAC1E,wBAAwB;AACxB,+DAA+D;AAC/D,iDAAiD;AACjD,EAAE;AACF,uCAAuC;AACvC,0CAA0C;AAC1C,kDAAkD;AAClD,0EAA0E;AAwH1E,+EAA+E;AAC/E,yBAAyB;AAEzB,MAAM,OAAO,UAAW,SAAQ,KAAK;IACnC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,OAAO,mBAAoB,SAAQ,UAAU;IACjD;QACE,KAAK,CACH,uHAAuH,CACxH,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,MAAM,OAAO,eAAgB,SAAQ,UAAU;IACpC,IAAI,CAAW;IACxB,YAAY,IAAc;QACxB,KAAK,CAAC,yBAAyB,IAAI,sCAAsC,CAAC,CAAC;QAC3E,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,OAAO,oBAAqB,SAAQ,UAAU;IAClD;QACE,KAAK,CACH,+EAA+E,CAChF,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AAED,MAAM,OAAO,qBAAsB,SAAQ,UAAU;IAC1C,KAAK,CAAU;IACxB,YAAY,KAAc;QACxB,KAAK,CACH,4BACE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACvD,EAAE,CACH,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;QACpC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,0BAA2B,SAAQ,UAAU;IAC/C,QAAQ,CAAS;IAC1B,YAAY,QAAgB;QAC1B,KAAK,CACH,4CAA4C,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,gOAAgO,CACrS,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAC;QACzC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,gBAAiB,SAAQ,UAAU;IAC9C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF"}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,EAAE;AACF,oEAAoE;AACpE,uEAAuE;AACvE,EAAE;AACF,gBAAgB;AAChB,qDAAqD;AACrD,uEAAuE;AACvE,0EAA0E;AAC1E,oDAAoD;AACpD,0EAA0E;AAC1E,wBAAwB;AACxB,+DAA+D;AAC/D,iDAAiD;AACjD,EAAE;AACF,uCAAuC;AACvC,0CAA0C;AAC1C,kDAAkD;AAClD,0EAA0E;AAwH1E,+EAA+E;AAC/E,yBAAyB;AAEzB,MAAM,OAAO,UAAW,SAAQ,KAAK;IACnC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,OAAO,mBAAoB,SAAQ,UAAU;IACjD;QACE,KAAK,CACH,uHAAuH,CACxH,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,MAAM,OAAO,eAAgB,SAAQ,UAAU;IACpC,IAAI,CAAW;IACxB,YAAY,IAAc;QACxB,KAAK,CAAC,yBAAyB,IAAI,sCAAsC,CAAC,CAAC;QAC3E,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,OAAO,oBAAqB,SAAQ,UAAU;IAClD;QACE,KAAK,CACH,+EAA+E,CAChF,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,yBAA0B,SAAQ,UAAU;IAC9C,MAAM,CAAS;IACxB,YAAY,MAAc;QACxB,KAAK,CACH,iCAAiC,MAAM,okBAAokB,CAC5mB,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;QACxC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED,MAAM,OAAO,qBAAsB,SAAQ,UAAU;IAC1C,KAAK,CAAU;IACxB,YAAY,KAAc;QACxB,KAAK,CACH,4BACE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACvD,EAAE,CACH,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;QACpC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,0BAA2B,SAAQ,UAAU;IAC/C,QAAQ,CAAS;IAC1B,YAAY,QAAgB;QAC1B,KAAK,CACH,4CAA4C,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,gOAAgO,CACrS,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAC;QACzC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,gBAAiB,SAAQ,UAAU;IAC9C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-rls-cli.d.ts","sourceRoot":"","sources":["../src/verify-rls-cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"verify-rls-cli.d.ts","sourceRoot":"","sources":["../src/verify-rls-cli.ts"],"names":[],"mappings":"AAmFA,MAAM,WAAW,qBAAqB;IACpC,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;CACjB;AAqBD;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,OAAO,CAAC,qBAAqB,CAAC,CAsEhC"}
|