@nodii/db-rls 0.11.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.
Files changed (44) hide show
  1. package/dist/auth-schema.d.ts +120 -0
  2. package/dist/auth-schema.d.ts.map +1 -1
  3. package/dist/auth-schema.js +177 -0
  4. package/dist/auth-schema.js.map +1 -1
  5. package/dist/context.d.ts +26 -8
  6. package/dist/context.d.ts.map +1 -1
  7. package/dist/context.js +35 -24
  8. package/dist/context.js.map +1 -1
  9. package/dist/drizzle-client.d.ts +17 -5
  10. package/dist/drizzle-client.d.ts.map +1 -1
  11. package/dist/drizzle-client.js +19 -18
  12. package/dist/drizzle-client.js.map +1 -1
  13. package/dist/index.d.ts +6 -4
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +20 -7
  16. package/dist/index.js.map +1 -1
  17. package/dist/postgres-js-client.d.ts +17 -10
  18. package/dist/postgres-js-client.d.ts.map +1 -1
  19. package/dist/postgres-js-client.js +19 -21
  20. package/dist/postgres-js-client.js.map +1 -1
  21. package/dist/rls-policy-gen.d.ts +43 -27
  22. package/dist/rls-policy-gen.d.ts.map +1 -1
  23. package/dist/rls-policy-gen.js +40 -15
  24. package/dist/rls-policy-gen.js.map +1 -1
  25. package/dist/rls-policy.d.ts +11 -1
  26. package/dist/rls-policy.d.ts.map +1 -1
  27. package/dist/rls-policy.js +31 -4
  28. package/dist/rls-policy.js.map +1 -1
  29. package/dist/tenant-fence.d.ts +165 -0
  30. package/dist/tenant-fence.d.ts.map +1 -0
  31. package/dist/tenant-fence.js +542 -0
  32. package/dist/tenant-fence.js.map +1 -0
  33. package/dist/types.d.ts +18 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js +22 -0
  36. package/dist/types.js.map +1 -1
  37. package/dist/verify-rls-cli.d.ts.map +1 -1
  38. package/dist/verify-rls-cli.js +86 -10
  39. package/dist/verify-rls-cli.js.map +1 -1
  40. package/dist/verify-rls.d.ts +26 -5
  41. package/dist/verify-rls.d.ts.map +1 -1
  42. package/dist/verify-rls.js +73 -5
  43. package/dist/verify-rls.js.map +1 -1
  44. 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"}