@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,165 @@
|
|
|
1
|
+
import { DbRlsError } from "./types.js";
|
|
2
|
+
import type { RlsQuerier } from "./verify-rls.js";
|
|
3
|
+
/** Why one policy (or helper) failed the fence check. */
|
|
4
|
+
export type TenantFenceReason =
|
|
5
|
+
/** THE HOLE. The policy consults `auth.user_tenant_ids` directly — the
|
|
6
|
+
* membership union — instead of resolving through the session resolver, so
|
|
7
|
+
* it spans every tenant the principal belongs to. This is the shape that
|
|
8
|
+
* released a refund. */
|
|
9
|
+
"cross_tenant_union"
|
|
10
|
+
/** Permissive, granted to `authenticated`, with no tenant reference at all —
|
|
11
|
+
* `USING (true)` or equivalent. Every tenant, every row. */
|
|
12
|
+
| "unfenced_permissive"
|
|
13
|
+
/** The positive rule failing in the general case: there IS a predicate, but
|
|
14
|
+
* nothing in it resolves through `auth.session_tenant_ids()`. Covers the
|
|
15
|
+
* mangled-by-a-sweep case that an absence check would wave through. */
|
|
16
|
+
| "not_resolved_through_session"
|
|
17
|
+
/** `auth.session_tenant_ids()` is not installed, so every policy referencing
|
|
18
|
+
* it is broken. (In practice this surfaces at CREATE POLICY time, since
|
|
19
|
+
* Postgres validates the reference then — but a function dropped AFTER the
|
|
20
|
+
* policies were created lands here.) */
|
|
21
|
+
| "resolver_missing"
|
|
22
|
+
/** The resolver exists but does not RAISE on an unbound session — someone
|
|
23
|
+
* replaced it with a NULL-returning getter, restoring silent failure. */
|
|
24
|
+
| "resolver_not_loud"
|
|
25
|
+
/** The resolver exists but is not SECURITY DEFINER, so it cannot read the
|
|
26
|
+
* membership mirror and will fail with `permission denied` (or recurse into
|
|
27
|
+
* the mirror's own policy and die at stack depth). */
|
|
28
|
+
| "resolver_not_security_definer"
|
|
29
|
+
/** The DEGRADED, fence-only resolver is installed: the session tenant is
|
|
30
|
+
* accepted WITHOUT proving the principal holds it. Legitimate for a service
|
|
31
|
+
* with no membership mirror, but never something to be on by accident.
|
|
32
|
+
* Reported unless `allowDegradedResolver` is set. */
|
|
33
|
+
| "resolver_degraded"
|
|
34
|
+
/** `auth.user_tenant_ids` is still the bootstrap stub returning the empty
|
|
35
|
+
* set, so the STRICT resolver intersects to nothing and the whole
|
|
36
|
+
* authenticated pool is silently deny-all. (P0 drift 199feabc — crm's 430
|
|
37
|
+
* dead policies — turned into a loud failure.) */
|
|
38
|
+
| "membership_helper_is_stub"
|
|
39
|
+
/** THE HOLE A POLICY-LEVEL CHECK CANNOT SEE. The table has a `tenant_id`
|
|
40
|
+
* column but `relrowsecurity = false`, so no policy is ever consulted. Since
|
|
41
|
+
* the canonical grants are deliberately blanket —
|
|
42
|
+
* `GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO
|
|
43
|
+
* authenticated`, plus an `ALTER DEFAULT PRIVILEGES` that arms every FUTURE
|
|
44
|
+
* table the same way — a table that simply never enables RLS is readable and
|
|
45
|
+
* writable across every tenant. It is STRICTLY MORE OPEN than the union
|
|
46
|
+
* predicate this release removes, and it passes any check that inspects
|
|
47
|
+
* `pg_policies`, because it has no rows there to inspect. */
|
|
48
|
+
| "rls_disabled_on_tenant_table"
|
|
49
|
+
/** A UNIQUE index on a tenant-scoped table that does NOT include the tenant
|
|
50
|
+
* column. RLS filters which rows you can SEE; it does not partition the
|
|
51
|
+
* uniqueness namespace. A caller-supplied key unique on its own lets one
|
|
52
|
+
* tenant collide with another's row — `INSERT … ON CONFLICT` then either
|
|
53
|
+
* raises 23505 (leaking existence) or takes an UPDATE path against a row the
|
|
54
|
+
* session cannot see. An idempotent-mint `SELECT` with no tenant predicate
|
|
55
|
+
* on such a key returns the other tenant's row outright. */
|
|
56
|
+
| "tenant_unscoped_unique_index"
|
|
57
|
+
/** A VIEW, MATERIALIZED VIEW, or FOREIGN TABLE exposing a tenant column to
|
|
58
|
+
* `authenticated`. A matview cannot have RLS AT ALL; a foreign table's
|
|
59
|
+
* policies are not enforced remotely; and a plain view without
|
|
60
|
+
* `security_invoker = true` executes with the VIEW OWNER's privileges, so
|
|
61
|
+
* the underlying table's RLS is bypassed entirely. Each is a routine
|
|
62
|
+
* migration artifact, and the justification for flagging an unprotected
|
|
63
|
+
* table applies verbatim: it is strictly more open than the union this
|
|
64
|
+
* release removes. */
|
|
65
|
+
| "unprotected_relation";
|
|
66
|
+
/** One policy (or helper) that failed the fence check. */
|
|
67
|
+
export interface TenantFenceOffender {
|
|
68
|
+
/** `schema.table`, or `auth.<fn>()` for a helper-level offender. */
|
|
69
|
+
object: string;
|
|
70
|
+
/** Policy name, or the helper function name. */
|
|
71
|
+
policy: string;
|
|
72
|
+
/** `SELECT` / `INSERT` / `UPDATE` / `DELETE` / `ALL`, or `-` for helpers. */
|
|
73
|
+
command: string;
|
|
74
|
+
reason: TenantFenceReason;
|
|
75
|
+
/** The deparsed expression that failed, straight from the catalog. */
|
|
76
|
+
expression: string | null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Thrown by {@link verifyTenantFence}. Carries the structured offender list so
|
|
80
|
+
* a `db:setup` gate can render a precise failure — and so a caller can tell
|
|
81
|
+
* "this is a stale service that needs regenerating" apart from "someone
|
|
82
|
+
* hand-wrote a cross-tenant policy in prod".
|
|
83
|
+
*/
|
|
84
|
+
export declare class TenantFenceError extends DbRlsError {
|
|
85
|
+
readonly offenders: readonly TenantFenceOffender[];
|
|
86
|
+
constructor(offenders: readonly TenantFenceOffender[]);
|
|
87
|
+
}
|
|
88
|
+
export interface VerifyTenantFenceOpts {
|
|
89
|
+
/** Schema(s) to check. Defaults to `["public"]`. */
|
|
90
|
+
schema?: string | readonly string[];
|
|
91
|
+
/** Restrict to these table names (bare or `schema.table`). Omit to check
|
|
92
|
+
* every table in `schema` that has an `authenticated` policy. */
|
|
93
|
+
tables?: readonly string[];
|
|
94
|
+
/**
|
|
95
|
+
* Policies exempt from the POSITIVE rule, as `table.policy_name` or just
|
|
96
|
+
* `policy_name`. Use sparingly, and argue it at the call site — the whole
|
|
97
|
+
* value of a positive assertion is that the exemptions are enumerated in one
|
|
98
|
+
* reviewable place instead of being implicit in a regex.
|
|
99
|
+
*
|
|
100
|
+
* The legitimate case is a policy that is genuinely not tenant-scoped: a
|
|
101
|
+
* public-listing row, a platform-global reference table exposed to
|
|
102
|
+
* `authenticated` (08-rls § 7.6), or a self-row policy keyed on
|
|
103
|
+
* `auth.get_user_id()` with no tenant dimension.
|
|
104
|
+
*/
|
|
105
|
+
exemptPolicies?: readonly string[];
|
|
106
|
+
/** Accept the degraded fence-only resolver. Defaults to `false`. */
|
|
107
|
+
allowDegradedResolver?: boolean;
|
|
108
|
+
/** Also verify the `auth.*` helper shapes. Defaults to `true`. */
|
|
109
|
+
checkHelpers?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Also assert that every table carrying a tenant column has RLS ENABLED.
|
|
112
|
+
* Defaults to `true`. This is the check that catches the table a migration
|
|
113
|
+
* simply never protected — see `rls_disabled_on_tenant_table`.
|
|
114
|
+
*/
|
|
115
|
+
checkTableCoverage?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Also flag UNIQUE indexes on tenant-scoped tables that omit the tenant
|
|
118
|
+
* column. Defaults to `true`.
|
|
119
|
+
*/
|
|
120
|
+
checkUniqueIndexes?: boolean;
|
|
121
|
+
/** An ADDITIONAL tenant column name to recognise, on top of
|
|
122
|
+
* {@link tenantColumns}. Kept for source compatibility. */
|
|
123
|
+
tenantColumn?: string;
|
|
124
|
+
/** Column names that mark a relation as tenant-scoped. Defaults to
|
|
125
|
+
* `["tenant_id", "org_id", "organization_id"]`. Keying coverage on a SINGLE
|
|
126
|
+
* configured name exempted every table using one of the others — including
|
|
127
|
+
* `org_id`, which `withRls` explicitly supports and ships a test for. */
|
|
128
|
+
tenantColumns?: readonly string[];
|
|
129
|
+
/**
|
|
130
|
+
* Tables exempt from the table-level checks, bare or `schema.table`.
|
|
131
|
+
*
|
|
132
|
+
* The honest case is a tenant-scoped table deliberately left RLS-free
|
|
133
|
+
* because it is owner-only and no non-owner role is granted on it. Argue it
|
|
134
|
+
* at the call site — this list is the record of every place the fleet has
|
|
135
|
+
* decided a tenant column does not imply a tenant boundary.
|
|
136
|
+
*/
|
|
137
|
+
exemptTables?: readonly string[];
|
|
138
|
+
}
|
|
139
|
+
/** Does this expression resolve through the session resolver? */
|
|
140
|
+
export declare function resolvesThroughSession(expr: string | null): boolean;
|
|
141
|
+
/** Does this expression consult the raw membership union? */
|
|
142
|
+
export declare function consultsMembershipUnion(expr: string | null): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Classify ONE authenticated policy. Exported so the test-suite can assert the
|
|
145
|
+
* classification directly without standing up every offending shape in the
|
|
146
|
+
* catalog, and so a caller can reuse the rule in a bespoke gate.
|
|
147
|
+
*
|
|
148
|
+
* The order matters: the two SPECIFIC diagnoses come first because they tell a
|
|
149
|
+
* reader what to do, and the general positive-rule failure is the fallback.
|
|
150
|
+
*/
|
|
151
|
+
export declare function classifyAuthPolicy(expr: string | null): TenantFenceReason | null;
|
|
152
|
+
/**
|
|
153
|
+
* Asserts that EVERY policy granted to `authenticated` resolves through
|
|
154
|
+
* `auth.session_tenant_ids()`, and that the resolver itself is installed, loud,
|
|
155
|
+
* and SECURITY DEFINER.
|
|
156
|
+
*
|
|
157
|
+
* @throws {TenantFenceError} listing every offender, with the deparsed
|
|
158
|
+
* expression straight from the catalog.
|
|
159
|
+
*/
|
|
160
|
+
export declare function verifyTenantFence(querier: RlsQuerier, opts?: VerifyTenantFenceOpts): Promise<void>;
|
|
161
|
+
/** The bootstrap stub is `SELECT NULL::uuid WHERE false` — any body that can
|
|
162
|
+
* never return a row. Matched structurally rather than by exact string so a
|
|
163
|
+
* reformatted copy is still caught. */
|
|
164
|
+
export declare function isStubMembershipHelper(prosrc: string): boolean;
|
|
165
|
+
//# sourceMappingURL=tenant-fence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-fence.d.ts","sourceRoot":"","sources":["../src/tenant-fence.ts"],"names":[],"mappings":"AAsDA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,yDAAyD;AACzD,MAAM,MAAM,iBAAiB;AAC3B;;;yBAGyB;AACvB,oBAAoB;AACtB;6DAC6D;GAC3D,qBAAqB;AACvB;;wEAEwE;GACtE,8BAA8B;AAChC;;;yCAGyC;GACvC,kBAAkB;AACpB;0EAC0E;GACxE,mBAAmB;AACrB;;uDAEuD;GACrD,+BAA+B;AACjC;;;sDAGsD;GACpD,mBAAmB;AACrB;;;mDAGmD;GACjD,2BAA2B;AAC7B;;;;;;;;8DAQ8D;GAC5D,8BAA8B;AAChC;;;;;;6DAM6D;GAC3D,8BAA8B;AAChC;;;;;;;uBAOuB;GACrB,sBAAsB,CAAC;AAE3B,0DAA0D;AAC1D,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,sEAAsE;IACtE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,UAAU;IAC9C,QAAQ,CAAC,SAAS,EAAE,SAAS,mBAAmB,EAAE,CAAC;gBACvC,SAAS,EAAE,SAAS,mBAAmB,EAAE;CAKtD;AAqCD,MAAM,WAAW,qBAAqB;IACpC,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IACpC;sEACkE;IAClE,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,oEAAoE;IACpE,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,kEAAkE;IAClE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;gEAC4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;8EAG0E;IAC1E,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAmCD,iEAAiE;AACjE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAEnE;AAED,6DAA6D;AAC7D,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAEpE;AAiBD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,GAAG,IAAI,GAClB,iBAAiB,GAAG,IAAI,CAO1B;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,UAAU,EACnB,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,IAAI,CAAC,CAkHf;AAqVD;;wCAEwC;AACxC,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE9D"}
|