@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
package/dist/auth-schema.d.ts
CHANGED
|
@@ -5,6 +5,125 @@ import type { Pool, PoolClient } from "pg";
|
|
|
5
5
|
* / Go ports (asserted by the parity-fence CI).
|
|
6
6
|
*/
|
|
7
7
|
export declare const AUTH_SCHEMA_SQL = "CREATE SCHEMA IF NOT EXISTS auth;\n\nCREATE OR REPLACE FUNCTION auth.set_user_id(p_user_id uuid) RETURNS void\nLANGUAGE sql AS $$ SELECT set_config('app.user_id', p_user_id::text, true) $$;\n\nCREATE OR REPLACE FUNCTION auth.set_tenant_id(p_tenant_id uuid) RETURNS void\nLANGUAGE sql AS $$ SELECT set_config('app.tenant_id', p_tenant_id::text, true) $$;\n\nCREATE OR REPLACE FUNCTION auth.init(p_user_id uuid, p_tenant_id uuid) RETURNS void\nLANGUAGE sql AS $$\n SELECT set_config('app.user_id', p_user_id::text, true);\n SELECT set_config('app.tenant_id', p_tenant_id::text, true);\n$$;\n\nCREATE OR REPLACE FUNCTION auth.init_tenant(p_tenant_id uuid) RETURNS void\nLANGUAGE sql AS $$ SELECT set_config('app.tenant_id', p_tenant_id::text, true) $$;\n\nCREATE OR REPLACE FUNCTION auth.get_user_id() RETURNS uuid\nLANGUAGE sql STABLE AS $$ SELECT nullif(current_setting('app.user_id', true), '')::uuid $$;\n\nCREATE OR REPLACE FUNCTION auth.get_tenant_id() RETURNS uuid\nLANGUAGE sql STABLE AS $$ SELECT nullif(current_setting('app.tenant_id', true), '')::uuid $$;\n\n-- Stub variant. Replaced in-place by migrate-gen's 0001_user_tenant_ids.sql\n-- once the service has employees_mirror + customers_mirror tables.\nCREATE OR REPLACE FUNCTION auth.user_tenant_ids(p_user_id uuid) RETURNS SETOF uuid\nLANGUAGE sql STABLE AS $$ SELECT NULL::uuid WHERE false $$;\n";
|
|
8
|
+
/**
|
|
9
|
+
* `auth.session_tenant_ids()` — THE authenticated-pool resolver.
|
|
10
|
+
*
|
|
11
|
+
* Returns AT MOST ONE tenant: the one bound to `app.tenant_id`, and only if the
|
|
12
|
+
* principal in `app.user_id` genuinely holds it per `auth.user_tenant_ids`.
|
|
13
|
+
* Edge-signed session tenant **INTERSECT** server-resolved membership.
|
|
14
|
+
*
|
|
15
|
+
* WHY THIS EXISTS. The `authenticated` predicate used to be
|
|
16
|
+
* `tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id()))` — the UNION
|
|
17
|
+
* of every tenant the user belongs to, with no tenant fence at all. A session
|
|
18
|
+
* opened for a user with memberships in tenants A and B could read AND write
|
|
19
|
+
* both. Reproduced end-to-end: a legitimate tenant-A session transitioned a
|
|
20
|
+
* tenant-B approval task, got HTTP 200, and released a 250,000 INR refund.
|
|
21
|
+
*
|
|
22
|
+
* WHY SUBSTITUTION RATHER THAN APPENDING A FENCE. The obvious repair is to
|
|
23
|
+
* append `AND tenant_id = auth.get_tenant_id()` at the top level of each
|
|
24
|
+
* policy. That breaks every policy whose shape is not a bare tenant equality,
|
|
25
|
+
* and the fleet is full of them:
|
|
26
|
+
*
|
|
27
|
+
* - tables with NO `tenant_id` column that reach it through a nested
|
|
28
|
+
* `EXISTS` or a sub-`SELECT` (`tenant_role_permissions`, `fx_rate_cache`)
|
|
29
|
+
* - platform-default escape hatches — `tenant_id IS NULL OR tenant_id IN (…)`
|
|
30
|
+
* (hr's statutory masters, `pricing_snapshots`)
|
|
31
|
+
* - additive ORs on a differently-named column — `is_public_listing = TRUE OR
|
|
32
|
+
* developer_tenant_id IN (…)` (`oauth_apps` marketplace listings)
|
|
33
|
+
* - policies fencing on a column that is not `tenant_id` at all (`tenants.id`,
|
|
34
|
+
* `legal_documents.scope`, `impersonation_sessions.target_tenant_id`)
|
|
35
|
+
* - TEXT-vs-uuid casts that need fixing in opposite directions per table
|
|
36
|
+
*
|
|
37
|
+
* Swapping the resolver INSIDE the subquery leaves the column, the cast and the
|
|
38
|
+
* surrounding OR untouched, so all of those keep working while the set the
|
|
39
|
+
* subquery yields narrows from "every tenant you belong to" to "the one you are
|
|
40
|
+
* logged into, if you really belong to it". One substitution, zero shape
|
|
41
|
+
* changes, and NO `AS RESTRICTIVE` (which would AND against the additive
|
|
42
|
+
* permissive master policies and kill every platform-default row).
|
|
43
|
+
*
|
|
44
|
+
* `user_tenant_ids` is deliberately LEFT INTACT. It still answers the honest
|
|
45
|
+
* question "which tenants does this user belong to", which a tenant-switcher or
|
|
46
|
+
* a login tenant-picker legitimately needs. Overloading it with session scope
|
|
47
|
+
* would have silently broken those callers with no error and no test coverage.
|
|
48
|
+
*
|
|
49
|
+
* SECURITY DEFINER is required, not decorative:
|
|
50
|
+
* - `authenticated` has no privilege on the membership mirror, so an INVOKER
|
|
51
|
+
* function raises `permission denied for table user_memberships`;
|
|
52
|
+
* - if the mirror is itself RLS-policied, an INVOKER function re-enters its
|
|
53
|
+
* own policy and dies with `stack depth limit exceeded` (54001).
|
|
54
|
+
*
|
|
55
|
+
* FAIL-CLOSED SPLIT, deliberate:
|
|
56
|
+
* - GUCs unset → RAISE 42501. Misconfiguration is LOUD. An unbound session
|
|
57
|
+
* silently yielding an empty grid reads to a human as "there is no data
|
|
58
|
+
* here", which is how a broken session survives a week of QA.
|
|
59
|
+
* - bound, but no membership → EMPTY SET, no error. A legitimate non-member
|
|
60
|
+
* seeing nothing is not an error condition, and platform-default rows
|
|
61
|
+
* reached through a sibling `OR` must still resolve.
|
|
62
|
+
*/
|
|
63
|
+
export declare const SESSION_TENANT_IDS_SQL = "CREATE OR REPLACE FUNCTION auth.session_tenant_ids() RETURNS SETOF uuid\nLANGUAGE plpgsql STABLE SECURITY DEFINER\nSET search_path = pg_catalog, public, pg_temp\nAS $session$\nDECLARE\n v_user uuid;\n v_tenant uuid;\nBEGIN\n v_user := nullif(current_setting('app.user_id', true), '')::uuid;\n v_tenant := nullif(current_setting('app.tenant_id', true), '')::uuid;\n IF v_user IS NULL THEN\n RAISE EXCEPTION 'db_rls_session_user_missing: app.user_id is not bound on this connection'\n USING ERRCODE = '42501',\n HINT = 'The authenticated pool is the user-session pool; a session with no principal cannot be membership-checked. Open it with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';\n END IF;\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'db_rls_session_tenant_missing: app.tenant_id is not bound on this connection'\n USING ERRCODE = '42501',\n HINT = 'Every authenticated-pool session must be EXPLICITLY tenant-scoped. Open it with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';\n END IF;\n RETURN QUERY SELECT v_tenant WHERE v_tenant IN (SELECT auth.user_tenant_ids(v_user));\nEND\n$session$;\n";
|
|
64
|
+
/**
|
|
65
|
+
* Marker embedded in the DEGRADED body so `pg_proc.prosrc` introspection can
|
|
66
|
+
* tell the two variants apart. {@link verifyTenantFence} reports any database
|
|
67
|
+
* running the degraded resolver — the point is that a service is never on it by
|
|
68
|
+
* accident.
|
|
69
|
+
*/
|
|
70
|
+
export declare const SESSION_TENANT_IDS_DEGRADED_MARKER = "DB_RLS_DEGRADED_NO_MEMBERSHIP_MIRROR";
|
|
71
|
+
/**
|
|
72
|
+
* DEGRADED variant of {@link SESSION_TENANT_IDS_SQL} — fence ONLY, no
|
|
73
|
+
* membership intersect.
|
|
74
|
+
*
|
|
75
|
+
* FOR SERVICES THAT SHIP NO MEMBERSHIP MIRROR, AND NOTHING ELSE. Such a service
|
|
76
|
+
* has `auth.user_tenant_ids` as the bootstrap stub (`SELECT NULL::uuid WHERE
|
|
77
|
+
* false`), so the strict resolver intersects to the empty set and the entire
|
|
78
|
+
* authenticated pool becomes silently deny-all — the P0 shape of drift
|
|
79
|
+
* 199feabc, where crm's 430 policies were dead and nobody noticed.
|
|
80
|
+
*
|
|
81
|
+
* THE LIST BELOW IS DERIVED FROM THE REPOS, NOT FROM MEMORY. An earlier
|
|
82
|
+
* revision of this comment asserted "nodii-crm-service alone", which was wrong
|
|
83
|
+
* by six services and wrong about crm's urgency. Several repos ship a phase-004
|
|
84
|
+
* or phase-005 `0002_create_rls_helpers.sql` whose real `user_tenant_ids` is
|
|
85
|
+
* FULLY COMMENTED OUT, so the phase-002 stub is still the winner:
|
|
86
|
+
*
|
|
87
|
+
* STUB + a LIVE authenticated pool → deny-all TODAY, needs this variant:
|
|
88
|
+
* - nodii-billing-service 77 call sites; saga + dsar migrations run at
|
|
89
|
+
* BOOT (`src/lib/bootstrap.ts`), so its
|
|
90
|
+
* `saga_outbox` / `lifecycle_requests` /
|
|
91
|
+
* `data_residency_holds` are library-owned. The
|
|
92
|
+
* money path. Highest risk in the fleet.
|
|
93
|
+
* - nodii-finance-service 14 call sites. Already self-mitigates via
|
|
94
|
+
* `010-lib-auth-pool-mode`, which rewrites the
|
|
95
|
+
* lib policies to `= auth.get_tenant_id()`.
|
|
96
|
+
* - nodii-notification-service 3 call sites. Its own policies use
|
|
97
|
+
* `= auth.get_tenant_id()` and keep working, so
|
|
98
|
+
* only the library-owned tables go dark — which
|
|
99
|
+
* is exactly the shape nobody notices.
|
|
100
|
+
*
|
|
101
|
+
* STUB + a pool that is built but has ZERO handler call sites → not broken
|
|
102
|
+
* today, breaks the moment a handler uses it:
|
|
103
|
+
* - nodii-crm-service, nodii-kyc-service, nodii-ticketing-service,
|
|
104
|
+
* nodii-product-service
|
|
105
|
+
*
|
|
106
|
+
* Does NOT need it: nodii-geo-service (no authenticated pool at all —
|
|
107
|
+
* 2-pool), nodii-edge (no `user_tenant_ids`, RLS mode `single`),
|
|
108
|
+
* nodii-customer-service and nodii-vendor-service (already GUC-bodied),
|
|
109
|
+
* nodii-auth / nodii-hr-service / nodii-task-tracking / nodii-tenant-service
|
|
110
|
+
* (REAL membership helpers).
|
|
111
|
+
*
|
|
112
|
+
* WHAT YOU GIVE UP. The database stops verifying the principal. Isolation rests
|
|
113
|
+
* entirely on the application binding `app.tenant_id` correctly — RLS degrades
|
|
114
|
+
* from an independent second layer into "the app said so". A forged or simply
|
|
115
|
+
* mistaken tenant binding becomes access. That is a real reduction in
|
|
116
|
+
* defence-in-depth and it is why this is a separate, named, reported constant
|
|
117
|
+
* rather than a boolean flag on the strict one.
|
|
118
|
+
*
|
|
119
|
+
* THE EXIT. Land a membership mirror, point `auth.user_tenant_ids` at it, then
|
|
120
|
+
* apply {@link SESSION_TENANT_IDS_SQL} over this. No policy changes — the
|
|
121
|
+
* predicate at every call site is identical for both variants.
|
|
122
|
+
*/
|
|
123
|
+
export declare const SESSION_TENANT_IDS_DEGRADED_SQL = "CREATE OR REPLACE FUNCTION auth.session_tenant_ids() RETURNS SETOF uuid\nLANGUAGE plpgsql STABLE SECURITY DEFINER\nSET search_path = pg_catalog, public, pg_temp\nAS $session$\nDECLARE\n v_tenant uuid;\nBEGIN\n -- DB_RLS_DEGRADED_NO_MEMBERSHIP_MIRROR: no membership mirror in this service,\n -- so the session tenant is accepted WITHOUT proving the principal holds it.\n -- Replace with the strict resolver as soon as a mirror exists.\n IF nullif(current_setting('app.user_id', true), '') IS NULL THEN\n RAISE EXCEPTION 'db_rls_session_user_missing: app.user_id is not bound on this connection'\n USING ERRCODE = '42501',\n HINT = 'Open the session with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';\n END IF;\n v_tenant := nullif(current_setting('app.tenant_id', true), '')::uuid;\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'db_rls_session_tenant_missing: app.tenant_id is not bound on this connection'\n USING ERRCODE = '42501',\n HINT = 'Every authenticated-pool session must be EXPLICITLY tenant-scoped. Open it with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';\n END IF;\n RETURN QUERY SELECT v_tenant;\nEND\n$session$;\n";
|
|
124
|
+
/** Grant for the resolver. Separate from {@link AUTH_SCHEMA_GRANTS_SQL} for the
|
|
125
|
+
* same reason: the roles must exist before the GRANT runs. */
|
|
126
|
+
export declare const SESSION_TENANT_IDS_GRANTS_SQL = "GRANT EXECUTE ON FUNCTION auth.session_tenant_ids() TO nodii_services, authenticated;\n";
|
|
8
127
|
/**
|
|
9
128
|
* Grants for the 7 helper functions. Emitted separately because they
|
|
10
129
|
* reference roles that must exist before the GRANT runs — applied last
|
|
@@ -33,6 +152,7 @@ type AnyExecutor = Pool | PoolClient | SqlExecutor;
|
|
|
33
152
|
*/
|
|
34
153
|
export declare function applyAuthSchemaMigrations(client: AnyExecutor, opts?: {
|
|
35
154
|
includeGrants?: boolean;
|
|
155
|
+
degradedSessionResolver?: boolean;
|
|
36
156
|
}): Promise<void>;
|
|
37
157
|
/**
|
|
38
158
|
* Composite full DDL — the schema + helpers + grants, joined.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-schema.d.ts","sourceRoot":"","sources":["../src/auth-schema.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,eAAe,s1CA2B3B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,8oBAQlC,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC;AAED,KAAK,WAAW,GAAG,IAAI,GAAG,UAAU,GAAG,WAAW,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,WAAW,EACnB,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,
|
|
1
|
+
{"version":3,"file":"auth-schema.d.ts","sourceRoot":"","sources":["../src/auth-schema.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,eAAe,s1CA2B3B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,eAAO,MAAM,sBAAsB,iqCAuBlC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kCAAkC,yCACP,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,eAAO,MAAM,+BAA+B,isCAwB3C,CAAC;AAEF;+DAC+D;AAC/D,eAAO,MAAM,6BAA6B,4FACzC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,8oBAQlC,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC;AAED,KAAK,WAAW,GAAG,IAAI,GAAG,UAAU,GAAG,WAAW,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,WAAW,EACnB,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAC;IAAC,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GACxE,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACrC,MAAM,CAKR"}
|
package/dist/auth-schema.js
CHANGED
|
@@ -52,6 +52,173 @@ LANGUAGE sql STABLE AS $$ SELECT nullif(current_setting('app.tenant_id', true),
|
|
|
52
52
|
CREATE OR REPLACE FUNCTION auth.user_tenant_ids(p_user_id uuid) RETURNS SETOF uuid
|
|
53
53
|
LANGUAGE sql STABLE AS $$ SELECT NULL::uuid WHERE false $$;
|
|
54
54
|
`;
|
|
55
|
+
/**
|
|
56
|
+
* `auth.session_tenant_ids()` — THE authenticated-pool resolver.
|
|
57
|
+
*
|
|
58
|
+
* Returns AT MOST ONE tenant: the one bound to `app.tenant_id`, and only if the
|
|
59
|
+
* principal in `app.user_id` genuinely holds it per `auth.user_tenant_ids`.
|
|
60
|
+
* Edge-signed session tenant **INTERSECT** server-resolved membership.
|
|
61
|
+
*
|
|
62
|
+
* WHY THIS EXISTS. The `authenticated` predicate used to be
|
|
63
|
+
* `tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id()))` — the UNION
|
|
64
|
+
* of every tenant the user belongs to, with no tenant fence at all. A session
|
|
65
|
+
* opened for a user with memberships in tenants A and B could read AND write
|
|
66
|
+
* both. Reproduced end-to-end: a legitimate tenant-A session transitioned a
|
|
67
|
+
* tenant-B approval task, got HTTP 200, and released a 250,000 INR refund.
|
|
68
|
+
*
|
|
69
|
+
* WHY SUBSTITUTION RATHER THAN APPENDING A FENCE. The obvious repair is to
|
|
70
|
+
* append `AND tenant_id = auth.get_tenant_id()` at the top level of each
|
|
71
|
+
* policy. That breaks every policy whose shape is not a bare tenant equality,
|
|
72
|
+
* and the fleet is full of them:
|
|
73
|
+
*
|
|
74
|
+
* - tables with NO `tenant_id` column that reach it through a nested
|
|
75
|
+
* `EXISTS` or a sub-`SELECT` (`tenant_role_permissions`, `fx_rate_cache`)
|
|
76
|
+
* - platform-default escape hatches — `tenant_id IS NULL OR tenant_id IN (…)`
|
|
77
|
+
* (hr's statutory masters, `pricing_snapshots`)
|
|
78
|
+
* - additive ORs on a differently-named column — `is_public_listing = TRUE OR
|
|
79
|
+
* developer_tenant_id IN (…)` (`oauth_apps` marketplace listings)
|
|
80
|
+
* - policies fencing on a column that is not `tenant_id` at all (`tenants.id`,
|
|
81
|
+
* `legal_documents.scope`, `impersonation_sessions.target_tenant_id`)
|
|
82
|
+
* - TEXT-vs-uuid casts that need fixing in opposite directions per table
|
|
83
|
+
*
|
|
84
|
+
* Swapping the resolver INSIDE the subquery leaves the column, the cast and the
|
|
85
|
+
* surrounding OR untouched, so all of those keep working while the set the
|
|
86
|
+
* subquery yields narrows from "every tenant you belong to" to "the one you are
|
|
87
|
+
* logged into, if you really belong to it". One substitution, zero shape
|
|
88
|
+
* changes, and NO `AS RESTRICTIVE` (which would AND against the additive
|
|
89
|
+
* permissive master policies and kill every platform-default row).
|
|
90
|
+
*
|
|
91
|
+
* `user_tenant_ids` is deliberately LEFT INTACT. It still answers the honest
|
|
92
|
+
* question "which tenants does this user belong to", which a tenant-switcher or
|
|
93
|
+
* a login tenant-picker legitimately needs. Overloading it with session scope
|
|
94
|
+
* would have silently broken those callers with no error and no test coverage.
|
|
95
|
+
*
|
|
96
|
+
* SECURITY DEFINER is required, not decorative:
|
|
97
|
+
* - `authenticated` has no privilege on the membership mirror, so an INVOKER
|
|
98
|
+
* function raises `permission denied for table user_memberships`;
|
|
99
|
+
* - if the mirror is itself RLS-policied, an INVOKER function re-enters its
|
|
100
|
+
* own policy and dies with `stack depth limit exceeded` (54001).
|
|
101
|
+
*
|
|
102
|
+
* FAIL-CLOSED SPLIT, deliberate:
|
|
103
|
+
* - GUCs unset → RAISE 42501. Misconfiguration is LOUD. An unbound session
|
|
104
|
+
* silently yielding an empty grid reads to a human as "there is no data
|
|
105
|
+
* here", which is how a broken session survives a week of QA.
|
|
106
|
+
* - bound, but no membership → EMPTY SET, no error. A legitimate non-member
|
|
107
|
+
* seeing nothing is not an error condition, and platform-default rows
|
|
108
|
+
* reached through a sibling `OR` must still resolve.
|
|
109
|
+
*/
|
|
110
|
+
export const SESSION_TENANT_IDS_SQL = `CREATE OR REPLACE FUNCTION auth.session_tenant_ids() RETURNS SETOF uuid
|
|
111
|
+
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
|
112
|
+
SET search_path = pg_catalog, public, pg_temp
|
|
113
|
+
AS $session$
|
|
114
|
+
DECLARE
|
|
115
|
+
v_user uuid;
|
|
116
|
+
v_tenant uuid;
|
|
117
|
+
BEGIN
|
|
118
|
+
v_user := nullif(current_setting('app.user_id', true), '')::uuid;
|
|
119
|
+
v_tenant := nullif(current_setting('app.tenant_id', true), '')::uuid;
|
|
120
|
+
IF v_user IS NULL THEN
|
|
121
|
+
RAISE EXCEPTION 'db_rls_session_user_missing: app.user_id is not bound on this connection'
|
|
122
|
+
USING ERRCODE = '42501',
|
|
123
|
+
HINT = 'The authenticated pool is the user-session pool; a session with no principal cannot be membership-checked. Open it with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';
|
|
124
|
+
END IF;
|
|
125
|
+
IF v_tenant IS NULL THEN
|
|
126
|
+
RAISE EXCEPTION 'db_rls_session_tenant_missing: app.tenant_id is not bound on this connection'
|
|
127
|
+
USING ERRCODE = '42501',
|
|
128
|
+
HINT = 'Every authenticated-pool session must be EXPLICITLY tenant-scoped. Open it with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';
|
|
129
|
+
END IF;
|
|
130
|
+
RETURN QUERY SELECT v_tenant WHERE v_tenant IN (SELECT auth.user_tenant_ids(v_user));
|
|
131
|
+
END
|
|
132
|
+
$session$;
|
|
133
|
+
`;
|
|
134
|
+
/**
|
|
135
|
+
* Marker embedded in the DEGRADED body so `pg_proc.prosrc` introspection can
|
|
136
|
+
* tell the two variants apart. {@link verifyTenantFence} reports any database
|
|
137
|
+
* running the degraded resolver — the point is that a service is never on it by
|
|
138
|
+
* accident.
|
|
139
|
+
*/
|
|
140
|
+
export const SESSION_TENANT_IDS_DEGRADED_MARKER = "DB_RLS_DEGRADED_NO_MEMBERSHIP_MIRROR";
|
|
141
|
+
/**
|
|
142
|
+
* DEGRADED variant of {@link SESSION_TENANT_IDS_SQL} — fence ONLY, no
|
|
143
|
+
* membership intersect.
|
|
144
|
+
*
|
|
145
|
+
* FOR SERVICES THAT SHIP NO MEMBERSHIP MIRROR, AND NOTHING ELSE. Such a service
|
|
146
|
+
* has `auth.user_tenant_ids` as the bootstrap stub (`SELECT NULL::uuid WHERE
|
|
147
|
+
* false`), so the strict resolver intersects to the empty set and the entire
|
|
148
|
+
* authenticated pool becomes silently deny-all — the P0 shape of drift
|
|
149
|
+
* 199feabc, where crm's 430 policies were dead and nobody noticed.
|
|
150
|
+
*
|
|
151
|
+
* THE LIST BELOW IS DERIVED FROM THE REPOS, NOT FROM MEMORY. An earlier
|
|
152
|
+
* revision of this comment asserted "nodii-crm-service alone", which was wrong
|
|
153
|
+
* by six services and wrong about crm's urgency. Several repos ship a phase-004
|
|
154
|
+
* or phase-005 `0002_create_rls_helpers.sql` whose real `user_tenant_ids` is
|
|
155
|
+
* FULLY COMMENTED OUT, so the phase-002 stub is still the winner:
|
|
156
|
+
*
|
|
157
|
+
* STUB + a LIVE authenticated pool → deny-all TODAY, needs this variant:
|
|
158
|
+
* - nodii-billing-service 77 call sites; saga + dsar migrations run at
|
|
159
|
+
* BOOT (`src/lib/bootstrap.ts`), so its
|
|
160
|
+
* `saga_outbox` / `lifecycle_requests` /
|
|
161
|
+
* `data_residency_holds` are library-owned. The
|
|
162
|
+
* money path. Highest risk in the fleet.
|
|
163
|
+
* - nodii-finance-service 14 call sites. Already self-mitigates via
|
|
164
|
+
* `010-lib-auth-pool-mode`, which rewrites the
|
|
165
|
+
* lib policies to `= auth.get_tenant_id()`.
|
|
166
|
+
* - nodii-notification-service 3 call sites. Its own policies use
|
|
167
|
+
* `= auth.get_tenant_id()` and keep working, so
|
|
168
|
+
* only the library-owned tables go dark — which
|
|
169
|
+
* is exactly the shape nobody notices.
|
|
170
|
+
*
|
|
171
|
+
* STUB + a pool that is built but has ZERO handler call sites → not broken
|
|
172
|
+
* today, breaks the moment a handler uses it:
|
|
173
|
+
* - nodii-crm-service, nodii-kyc-service, nodii-ticketing-service,
|
|
174
|
+
* nodii-product-service
|
|
175
|
+
*
|
|
176
|
+
* Does NOT need it: nodii-geo-service (no authenticated pool at all —
|
|
177
|
+
* 2-pool), nodii-edge (no `user_tenant_ids`, RLS mode `single`),
|
|
178
|
+
* nodii-customer-service and nodii-vendor-service (already GUC-bodied),
|
|
179
|
+
* nodii-auth / nodii-hr-service / nodii-task-tracking / nodii-tenant-service
|
|
180
|
+
* (REAL membership helpers).
|
|
181
|
+
*
|
|
182
|
+
* WHAT YOU GIVE UP. The database stops verifying the principal. Isolation rests
|
|
183
|
+
* entirely on the application binding `app.tenant_id` correctly — RLS degrades
|
|
184
|
+
* from an independent second layer into "the app said so". A forged or simply
|
|
185
|
+
* mistaken tenant binding becomes access. That is a real reduction in
|
|
186
|
+
* defence-in-depth and it is why this is a separate, named, reported constant
|
|
187
|
+
* rather than a boolean flag on the strict one.
|
|
188
|
+
*
|
|
189
|
+
* THE EXIT. Land a membership mirror, point `auth.user_tenant_ids` at it, then
|
|
190
|
+
* apply {@link SESSION_TENANT_IDS_SQL} over this. No policy changes — the
|
|
191
|
+
* predicate at every call site is identical for both variants.
|
|
192
|
+
*/
|
|
193
|
+
export const SESSION_TENANT_IDS_DEGRADED_SQL = `CREATE OR REPLACE FUNCTION auth.session_tenant_ids() RETURNS SETOF uuid
|
|
194
|
+
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
|
195
|
+
SET search_path = pg_catalog, public, pg_temp
|
|
196
|
+
AS $session$
|
|
197
|
+
DECLARE
|
|
198
|
+
v_tenant uuid;
|
|
199
|
+
BEGIN
|
|
200
|
+
-- ${SESSION_TENANT_IDS_DEGRADED_MARKER}: no membership mirror in this service,
|
|
201
|
+
-- so the session tenant is accepted WITHOUT proving the principal holds it.
|
|
202
|
+
-- Replace with the strict resolver as soon as a mirror exists.
|
|
203
|
+
IF nullif(current_setting('app.user_id', true), '') IS NULL THEN
|
|
204
|
+
RAISE EXCEPTION 'db_rls_session_user_missing: app.user_id is not bound on this connection'
|
|
205
|
+
USING ERRCODE = '42501',
|
|
206
|
+
HINT = 'Open the session with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';
|
|
207
|
+
END IF;
|
|
208
|
+
v_tenant := nullif(current_setting('app.tenant_id', true), '')::uuid;
|
|
209
|
+
IF v_tenant IS NULL THEN
|
|
210
|
+
RAISE EXCEPTION 'db_rls_session_tenant_missing: app.tenant_id is not bound on this connection'
|
|
211
|
+
USING ERRCODE = '42501',
|
|
212
|
+
HINT = 'Every authenticated-pool session must be EXPLICITLY tenant-scoped. Open it with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';
|
|
213
|
+
END IF;
|
|
214
|
+
RETURN QUERY SELECT v_tenant;
|
|
215
|
+
END
|
|
216
|
+
$session$;
|
|
217
|
+
`;
|
|
218
|
+
/** Grant for the resolver. Separate from {@link AUTH_SCHEMA_GRANTS_SQL} for the
|
|
219
|
+
* same reason: the roles must exist before the GRANT runs. */
|
|
220
|
+
export const SESSION_TENANT_IDS_GRANTS_SQL = `GRANT EXECUTE ON FUNCTION auth.session_tenant_ids() TO nodii_services, authenticated;
|
|
221
|
+
`;
|
|
55
222
|
/**
|
|
56
223
|
* Grants for the 7 helper functions. Emitted separately because they
|
|
57
224
|
* reference roles that must exist before the GRANT runs — applied last
|
|
@@ -84,8 +251,18 @@ export async function applyAuthSchemaMigrations(client, opts = {}) {
|
|
|
84
251
|
// SqlExecutor`; cast to the minimal shape.
|
|
85
252
|
const exec = client;
|
|
86
253
|
await exec.query(AUTH_SCHEMA_SQL);
|
|
254
|
+
// The resolver is applied unconditionally alongside the seven. A database
|
|
255
|
+
// where the policies reference `auth.session_tenant_ids()` but the function is
|
|
256
|
+
// absent does not merely misbehave — Postgres validates the reference at
|
|
257
|
+
// CREATE POLICY time, so the migration itself fails with `function
|
|
258
|
+
// auth.session_tenant_ids() does not exist`. Installing both together makes
|
|
259
|
+
// that unreachable.
|
|
260
|
+
await exec.query(opts.degradedSessionResolver
|
|
261
|
+
? SESSION_TENANT_IDS_DEGRADED_SQL
|
|
262
|
+
: SESSION_TENANT_IDS_SQL);
|
|
87
263
|
if (includeGrants) {
|
|
88
264
|
await exec.query(AUTH_SCHEMA_GRANTS_SQL);
|
|
265
|
+
await exec.query(SESSION_TENANT_IDS_GRANTS_SQL);
|
|
89
266
|
}
|
|
90
267
|
}
|
|
91
268
|
/**
|
package/dist/auth-schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-schema.js","sourceRoot":"","sources":["../src/auth-schema.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,EAAE;AACF,oEAAoE;AACpE,qEAAqE;AACrE,2BAA2B;AAC3B,EAAE;AACF,mEAAmE;AACnE,qBAAqB;AACrB,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,qEAAqE;AACrE,4FAA4F;AAC5F,8DAA8D;AAC9D,8DAA8D;AAC9D,0EAA0E;AAC1E,EAAE;AACF,yEAAyE;AACzE,uEAAuE;AACvE,0EAA0E;AAC1E,oEAAoE;AAIpE;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B9B,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;;;;;;CAQrC,CAAC;AAYF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,MAAmB,EACnB,
|
|
1
|
+
{"version":3,"file":"auth-schema.js","sourceRoot":"","sources":["../src/auth-schema.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,EAAE;AACF,oEAAoE;AACpE,qEAAqE;AACrE,2BAA2B;AAC3B,EAAE;AACF,mEAAmE;AACnE,qBAAqB;AACrB,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,qEAAqE;AACrE,4FAA4F;AAC5F,8DAA8D;AAC9D,8DAA8D;AAC9D,0EAA0E;AAC1E,EAAE;AACF,yEAAyE;AACzE,uEAAuE;AACvE,0EAA0E;AAC1E,oEAAoE;AAIpE;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B9B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;CAuBrC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kCAAkC,GAC7C,sCAAsC,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG;;;;;;;OAOxC,kCAAkC;;;;;;;;;;;;;;;;;CAiBxC,CAAC;AAEF;+DAC+D;AAC/D,MAAM,CAAC,MAAM,6BAA6B,GAAG;CAC5C,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;;;;;;CAQrC,CAAC;AAYF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,MAAmB,EACnB,OAAuE,EAAE;IAEzE,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC;IACjD,wEAAwE;IACxE,2CAA2C;IAC3C,MAAM,IAAI,GAAG,MAAqB,CAAC;IACnC,MAAM,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAClC,0EAA0E;IAC1E,+EAA+E;IAC/E,yEAAyE;IACzE,mEAAmE;IACnE,4EAA4E;IAC5E,oBAAoB;IACpB,MAAM,IAAI,CAAC,KAAK,CACd,IAAI,CAAC,uBAAuB;QAC1B,CAAC,CAAC,+BAA+B;QACjC,CAAC,CAAC,sBAAsB,CAC3B,CAAC;IACF,IAAI,aAAa,EAAE,CAAC;QAClB,MAAM,IAAI,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;QACzC,MAAM,IAAI,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;IAClD,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAoC,EAAE;IAEtC,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC;IACjD,OAAO,aAAa;QAClB,CAAC,CAAC,GAAG,eAAe,KAAK,sBAAsB,EAAE;QACjD,CAAC,CAAC,eAAe,CAAC;AACtB,CAAC"}
|
package/dist/context.d.ts
CHANGED
|
@@ -11,15 +11,33 @@ import { type AutoDispatchOpts, type PgTx, type PlatformOpsContextOpts, type Sys
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function withUserContext<T>(opts: UserContextOpts, fn: (tx: PgTx) => Promise<T>): Promise<T>;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* @deprecated REMOVED — always throws {@link CrossTenantSessionRefused}.
|
|
15
|
+
*
|
|
16
|
+
* This helper opened an `authenticated`-pool transaction binding ONLY
|
|
17
|
+
* `app.user_id`, deliberately leaving `app.tenant_id` unset so that the old
|
|
18
|
+
* union predicate `tenant_id IN (SELECT auth.user_tenant_ids(...))` would
|
|
19
|
+
* resolve across EVERY tenant the principal was an active member of. Its own
|
|
20
|
+
* docblock named task-tracking as the use case.
|
|
21
|
+
*
|
|
22
|
+
* That is not a policy bug that the predicate fix cleans up incidentally — it
|
|
23
|
+
* is the MECHANISM of the incident, sitting in the library. Reproduced
|
|
24
|
+
* end-to-end: a legitimate tenant-A session transitioned a tenant-B approval
|
|
25
|
+
* task, got HTTP 200, and released a 250,000 INR refund. Rewriting predicates
|
|
26
|
+
* while leaving this callable would have changed nothing for any caller of it.
|
|
27
|
+
*
|
|
28
|
+
* Migrate to `withUserContext({ userId, tenantId }, fn)` — the same pool, with
|
|
29
|
+
* the tenant the request actually resolved to. If a caller genuinely needs to
|
|
30
|
+
* know which tenants a user belongs to (a tenant switcher, a login
|
|
31
|
+
* tenant-picker), query `auth.user_tenant_ids` directly from a services-pool
|
|
32
|
+
* context; that function is deliberately left intact and still answers exactly
|
|
33
|
+
* that question. What is no longer available is opening a DATA session that
|
|
34
|
+
* spans them.
|
|
35
|
+
*
|
|
36
|
+
* The symbol is retained rather than deleted so a JavaScript consumer (no
|
|
37
|
+
* typechecker) gets this explanation at runtime instead of
|
|
38
|
+
* `undefined is not a function`.
|
|
21
39
|
*/
|
|
22
|
-
export declare function withUserMembershipContext<T>(
|
|
40
|
+
export declare function withUserMembershipContext<T>(_opts: UserMembershipContextOpts, _fn: (tx: PgTx) => Promise<T>): Promise<T>;
|
|
23
41
|
/**
|
|
24
42
|
* withSystemContext — opens a tx on the `services` (nodii_services) pool,
|
|
25
43
|
* sets `app.tenant_id` via tx-scoped `set_config`, yields the tx, commits
|
package/dist/context.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,KAAK,gBAAgB,
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,KAAK,gBAAgB,EAGrB,KAAK,IAAI,EACT,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EAEtB,KAAK,eAAe,EACpB,KAAK,yBAAyB,EAC/B,MAAM,YAAY,CAAC;AAEpB;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,IAAI,EAAE,eAAe,EACrB,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAyBZ;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAC/C,KAAK,EAAE,yBAAyB,EAChC,GAAG,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5B,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,IAAI,EAAE,iBAAiB,EACvB,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,QAAQ,EAAE,gBAAgB,GAAG,CAAC,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,EACvD,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GACjC,OAAO,CAAC,CAAC,CAAC,CAiBZ;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,IAAI,EAAE,sBAAsB,EAC5B,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAYZ;AAMD,4DAA4D;AAC5D,wBAAgB,8CAA8C,IAAI,IAAI,CAErE;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,IAAI,EAAE,sBAAsB,EAC5B,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAWZ;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,IAAI,EAAE,gBAAgB,EACtB,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAeZ"}
|
package/dist/context.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { requirePool } from "./init.js";
|
|
8
8
|
import { recordOwnerContextInvocation } from "./metrics.js";
|
|
9
9
|
import { assertMigrateAllowed, } from "./migrate-guard.js";
|
|
10
|
-
import { MissingTenantContext, TransactionRolledBack, } from "./types.js";
|
|
10
|
+
import { CrossTenantSessionRefused, MissingTenantContext, TransactionRolledBack, } from "./types.js";
|
|
11
11
|
/**
|
|
12
12
|
* withUserContext — opens a tx on the `authenticated` pool, sets
|
|
13
13
|
* `app.user_id` + `app.tenant_id` (+ optional `app.source_ip`) via
|
|
@@ -18,6 +18,13 @@ import { MissingTenantContext, TransactionRolledBack, } from "./types.js";
|
|
|
18
18
|
* Wraps any error thrown by `fn` in `TransactionRolledBack`.
|
|
19
19
|
*/
|
|
20
20
|
export async function withUserContext(opts, fn) {
|
|
21
|
+
// Reject a blank principal or tenant HERE rather than binding an empty GUC
|
|
22
|
+
// and letting `auth.session_tenant_ids()` raise 42501 on the first query.
|
|
23
|
+
// Both fail closed, but this one names the caller instead of a policy.
|
|
24
|
+
if (!opts?.userId)
|
|
25
|
+
throw new MissingTenantContext();
|
|
26
|
+
if (!opts?.tenantId)
|
|
27
|
+
throw new MissingTenantContext();
|
|
21
28
|
const pool = requirePool("authenticated");
|
|
22
29
|
return pool.withTransaction(async (tx) => {
|
|
23
30
|
// Canonical GUC namespace is `app.*` (08-rls §2/§6): the generated
|
|
@@ -40,30 +47,34 @@ export async function withUserContext(opts, fn) {
|
|
|
40
47
|
});
|
|
41
48
|
}
|
|
42
49
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
+
* @deprecated REMOVED — always throws {@link CrossTenantSessionRefused}.
|
|
51
|
+
*
|
|
52
|
+
* This helper opened an `authenticated`-pool transaction binding ONLY
|
|
53
|
+
* `app.user_id`, deliberately leaving `app.tenant_id` unset so that the old
|
|
54
|
+
* union predicate `tenant_id IN (SELECT auth.user_tenant_ids(...))` would
|
|
55
|
+
* resolve across EVERY tenant the principal was an active member of. Its own
|
|
56
|
+
* docblock named task-tracking as the use case.
|
|
57
|
+
*
|
|
58
|
+
* That is not a policy bug that the predicate fix cleans up incidentally — it
|
|
59
|
+
* is the MECHANISM of the incident, sitting in the library. Reproduced
|
|
60
|
+
* end-to-end: a legitimate tenant-A session transitioned a tenant-B approval
|
|
61
|
+
* task, got HTTP 200, and released a 250,000 INR refund. Rewriting predicates
|
|
62
|
+
* while leaving this callable would have changed nothing for any caller of it.
|
|
63
|
+
*
|
|
64
|
+
* Migrate to `withUserContext({ userId, tenantId }, fn)` — the same pool, with
|
|
65
|
+
* the tenant the request actually resolved to. If a caller genuinely needs to
|
|
66
|
+
* know which tenants a user belongs to (a tenant switcher, a login
|
|
67
|
+
* tenant-picker), query `auth.user_tenant_ids` directly from a services-pool
|
|
68
|
+
* context; that function is deliberately left intact and still answers exactly
|
|
69
|
+
* that question. What is no longer available is opening a DATA session that
|
|
70
|
+
* spans them.
|
|
71
|
+
*
|
|
72
|
+
* The symbol is retained rather than deleted so a JavaScript consumer (no
|
|
73
|
+
* typechecker) gets this explanation at runtime instead of
|
|
74
|
+
* `undefined is not a function`.
|
|
50
75
|
*/
|
|
51
|
-
export async function withUserMembershipContext(
|
|
52
|
-
|
|
53
|
-
return pool.withTransaction(async (tx) => {
|
|
54
|
-
// ONLY app.user_id — leaving app.tenant_id unset is intentional: the §4
|
|
55
|
-
// predicate keys on user_tenant_ids(get_user_id()), not a pinned tenant.
|
|
56
|
-
await tx.setConfig("app.user_id", opts.userId);
|
|
57
|
-
if (opts.sourceIp !== undefined) {
|
|
58
|
-
await tx.setConfig("app.source_ip", opts.sourceIp);
|
|
59
|
-
}
|
|
60
|
-
try {
|
|
61
|
-
return await fn(tx);
|
|
62
|
-
}
|
|
63
|
-
catch (cause) {
|
|
64
|
-
throw new TransactionRolledBack(cause);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
76
|
+
export async function withUserMembershipContext(_opts, _fn) {
|
|
77
|
+
throw new CrossTenantSessionRefused("withUserMembershipContext");
|
|
67
78
|
}
|
|
68
79
|
/**
|
|
69
80
|
* withSystemContext — opens a tx on the `services` (nodii_services) pool,
|
package/dist/context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,EAAE;AACF,uEAAuE;AACvE,0EAA0E;AAC1E,kEAAkE;AAClE,YAAY;AAEZ,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,4BAA4B,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,oBAAoB,GAErB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAEL,oBAAoB,EAIpB,qBAAqB,GAGtB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAqB,EACrB,EAA4B;IAE5B,MAAM,IAAI,GAAG,WAAW,CAAC,eAAe,CAAC,CAAC;IAC1C,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACvC,mEAAmE;QACnE,yEAAyE;QACzE,6EAA6E;QAC7E,8EAA8E;QAC9E,8EAA8E;QAC9E,4EAA4E;QAC5E,MAAM,EAAE,CAAC,SAAS,CAAC,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAC/C,MAAM,EAAE,CAAC,SAAS,CAAC,eAAe,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnD,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAChC,MAAM,EAAE,CAAC,SAAS,CAAC,eAAe,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,qBAAqB,CAAC,KAAK,CAAC,CAAC;QACzC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
|
package/dist/drizzle-client.d.ts
CHANGED
|
@@ -10,12 +10,24 @@ type DrizzleClient<TSchema extends Record<string, unknown> = Record<string, unkn
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function withUserContextDb<T, TSchema extends Record<string, unknown> = Record<string, unknown>>(opts: UserContextOpts, fn: (db: DrizzleClient<TSchema>) => Promise<T>): Promise<T>;
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* @deprecated REMOVED — always throws {@link CrossTenantSessionRefused}.
|
|
14
|
+
*
|
|
15
|
+
* Opened an `authenticated`-pool transaction binding ONLY `app.user_id`,
|
|
16
|
+
* leaving `app.tenant_id` deliberately unset so the old union predicate
|
|
17
|
+
* resolved across EVERY tenant the principal belonged to. That is the
|
|
18
|
+
* mechanism of the cross-tenant approval incident, not a side effect of it:
|
|
19
|
+
* a legitimate tenant-A session transitioned a tenant-B approval task, got
|
|
20
|
+
* HTTP 200, and released a 250,000 INR refund.
|
|
21
|
+
*
|
|
22
|
+
* Migrate to `withUserContextDb({ userId, tenantId }, fn)`.
|
|
23
|
+
* To LIST a user's tenants (switcher / login picker), query
|
|
24
|
+
* `auth.user_tenant_ids` from a services-pool context — that function is
|
|
25
|
+
* intact; what is gone is opening a DATA session that spans them.
|
|
26
|
+
*
|
|
27
|
+
* Retained as a throwing stub rather than deleted so a JavaScript consumer
|
|
28
|
+
* gets this explanation instead of `undefined is not a function`.
|
|
17
29
|
*/
|
|
18
|
-
export declare function withUserMembershipContextDb<T, TSchema extends Record<string, unknown> = Record<string, unknown>>(
|
|
30
|
+
export declare function withUserMembershipContextDb<T, TSchema extends Record<string, unknown> = Record<string, unknown>>(_opts: UserMembershipContextOpts, _fn: (db: DrizzleClient<TSchema>) => Promise<T>): Promise<T>;
|
|
19
31
|
/**
|
|
20
32
|
* withSystemContext (Drizzle variant) — opens a tx on the `services` pool
|
|
21
33
|
* with `app.tenant_id` GUC bound; yields a Drizzle client.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drizzle-client.d.ts","sourceRoot":"","sources":["../src/drizzle-client.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAKzE,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EACf,yBAAyB,EAC1B,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"drizzle-client.d.ts","sourceRoot":"","sources":["../src/drizzle-client.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAKzE,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EACf,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAWpB,KAAK,aAAa,CAChB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAC/D,cAAc,CAAC,OAAO,CAAC,CAAC;AA2D5B;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,CAAC,EACD,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjE,IAAI,EAAE,eAAe,EACrB,EAAE,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC7C,OAAO,CAAC,CAAC,CAAC,CAoBZ;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,2BAA2B,CAC/C,CAAC,EACD,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjE,KAAK,EAAE,yBAAyB,EAChC,GAAG,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC9C,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,CAAC,EACD,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjE,IAAI,EAAE,iBAAiB,EACvB,EAAE,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC7C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,CAAC,EACD,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjE,QAAQ,EAAE,gBAAgB,GAAG,CAAC,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,EACzE,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GACnD,OAAO,CAAC,CAAC,CAAC,CAeZ;AAoBD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/drizzle-client.js
CHANGED
|
@@ -21,7 +21,7 @@ import { requirePool } from "./init.js";
|
|
|
21
21
|
import { recordOwnerContextInvocation, withTxSpan } from "./metrics.js";
|
|
22
22
|
import { assertMigrateAllowed, } from "./migrate-guard.js";
|
|
23
23
|
import { PgClientPool } from "./pg-pool-adapter.js";
|
|
24
|
-
import { MissingTenantContext, TransactionRolledBack } from "./types.js";
|
|
24
|
+
import { CrossTenantSessionRefused, MissingTenantContext, TransactionRolledBack, } from "./types.js";
|
|
25
25
|
function clientFor(_pool) {
|
|
26
26
|
// Only PgClientPool exposes a real pg PoolClient. In-memory pools don't.
|
|
27
27
|
// The caller is responsible for using *Db helpers only against real pools.
|
|
@@ -97,24 +97,25 @@ export async function withUserContextDb(opts, fn) {
|
|
|
97
97
|
}, (db) => fn(db));
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
100
|
+
* @deprecated REMOVED — always throws {@link CrossTenantSessionRefused}.
|
|
101
|
+
*
|
|
102
|
+
* Opened an `authenticated`-pool transaction binding ONLY `app.user_id`,
|
|
103
|
+
* leaving `app.tenant_id` deliberately unset so the old union predicate
|
|
104
|
+
* resolved across EVERY tenant the principal belonged to. That is the
|
|
105
|
+
* mechanism of the cross-tenant approval incident, not a side effect of it:
|
|
106
|
+
* a legitimate tenant-A session transitioned a tenant-B approval task, got
|
|
107
|
+
* HTTP 200, and released a 250,000 INR refund.
|
|
108
|
+
*
|
|
109
|
+
* Migrate to `withUserContextDb({ userId, tenantId }, fn)`.
|
|
110
|
+
* To LIST a user's tenants (switcher / login picker), query
|
|
111
|
+
* `auth.user_tenant_ids` from a services-pool context — that function is
|
|
112
|
+
* intact; what is gone is opening a DATA session that spans them.
|
|
113
|
+
*
|
|
114
|
+
* Retained as a throwing stub rather than deleted so a JavaScript consumer
|
|
115
|
+
* gets this explanation instead of `undefined is not a function`.
|
|
104
116
|
*/
|
|
105
|
-
export async function withUserMembershipContextDb(
|
|
106
|
-
|
|
107
|
-
throw new MissingTenantContext();
|
|
108
|
-
const pool = requireRealPool("authenticated");
|
|
109
|
-
return withRealTx(pool, async (client) => {
|
|
110
|
-
// ONLY app.user_id — set_user_id, not init, so no tenant is pinned.
|
|
111
|
-
await client.query("SELECT auth.set_user_id($1::uuid)", [opts.userId]);
|
|
112
|
-
if (opts.sourceIp !== undefined) {
|
|
113
|
-
await client.query("SELECT set_config('app.source_ip', $1, true)", [
|
|
114
|
-
opts.sourceIp,
|
|
115
|
-
]);
|
|
116
|
-
}
|
|
117
|
-
}, (db) => fn(db));
|
|
117
|
+
export async function withUserMembershipContextDb(_opts, _fn) {
|
|
118
|
+
throw new CrossTenantSessionRefused("withUserMembershipContextDb");
|
|
118
119
|
}
|
|
119
120
|
/**
|
|
120
121
|
* withSystemContext (Drizzle variant) — opens a tx on the `services` pool
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drizzle-client.js","sourceRoot":"","sources":["../src/drizzle-client.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,yEAAyE;AACzE,wEAAwE;AACxE,iEAAiE;AACjE,uEAAuE;AACvE,2DAA2D;AAC3D,EAAE;AACF,iBAAiB;AACjB,0EAA0E;AAC1E,sEAAsE;AACtE,0BAA0B;AAC1B,sEAAsE;AACtE,wCAAwC;AACxC,8DAA8D;AAC9D,sEAAsE;AACtE,qEAAqE;AACrE,6CAA6C;AAE7C,OAAO,EAAE,OAAO,EAAuB,MAAM,2BAA2B,CAAC;AAGzE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,4BAA4B,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EACL,oBAAoB,GAErB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAMpD,OAAO,
|
|
1
|
+
{"version":3,"file":"drizzle-client.js","sourceRoot":"","sources":["../src/drizzle-client.ts"],"names":[],"mappings":"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"}
|