@stratum-hq/db-adapters 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -2
- package/dist/database/manager.d.ts +1 -1
- package/dist/database/manager.js +2 -2
- package/dist/database/manager.js.map +1 -1
- package/dist/database/pool-manager.d.ts +1 -1
- package/dist/database/pool-manager.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/migration-helpers.d.ts +2 -1
- package/dist/migration-helpers.d.ts.map +1 -1
- package/dist/migration-helpers.js +8 -1
- package/dist/migration-helpers.js.map +1 -1
- package/dist/pglite/index.d.ts +60 -0
- package/dist/pglite/index.d.ts.map +1 -0
- package/dist/pglite/index.js +196 -0
- package/dist/pglite/index.js.map +1 -0
- package/dist/rls/manager.d.ts +21 -1
- package/dist/rls/manager.d.ts.map +1 -1
- package/dist/rls/manager.js +56 -7
- package/dist/rls/manager.js.map +1 -1
- package/dist/rls/policy-check.d.ts +54 -0
- package/dist/rls/policy-check.d.ts.map +1 -0
- package/dist/rls/policy-check.js +241 -0
- package/dist/rls/policy-check.js.map +1 -0
- package/dist/rls/session.d.ts +29 -2
- package/dist/rls/session.d.ts.map +1 -1
- package/dist/rls/session.js +28 -5
- package/dist/rls/session.js.map +1 -1
- package/package.json +24 -6
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
PostgreSQL adapters for [Stratum](https://github.com/stratum-hq/Stratum) that automatically scope queries to the current tenant using Row-Level Security. Supports raw `pg`, Prisma, Drizzle, and Sequelize, plus helpers for enabling RLS on your tables.
|
|
4
4
|
|
|
5
|
+
Read the documentation at [docs.stratum-hq.org/packages/db-adapters](https://docs.stratum-hq.org/packages/db-adapters/).
|
|
6
|
+
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -35,7 +37,7 @@ import { prismaWithTenant } from "@stratum-hq/db-adapters"; // Prisma
|
|
|
35
37
|
import { drizzleWithTenant } from "@stratum-hq/db-adapters"; // Drizzle
|
|
36
38
|
import { SequelizeAdapter, sequelizeWithTenantScope } from "@stratum-hq/db-adapters"; // Sequelize
|
|
37
39
|
|
|
38
|
-
// Prisma
|
|
40
|
+
// Prisma: all queries scoped to the current tenant
|
|
39
41
|
const tenantPrisma = prismaWithTenant(prisma, () => getTenantContext().tenant_id, pool);
|
|
40
42
|
const orders = await tenantPrisma.order.findMany();
|
|
41
43
|
```
|
|
@@ -56,6 +58,16 @@ try {
|
|
|
56
58
|
}
|
|
57
59
|
```
|
|
58
60
|
|
|
61
|
+
`createPolicy` checks every row-level security policy already on the table (the table the name resolves to through the `search_path`) before it adds the `tenant_isolation` policy or keeps an existing one, and throws without changing anything if any check fails:
|
|
62
|
+
|
|
63
|
+
- PostgreSQL ORs permissive policies together, so every permissive policy, whatever its name or roles, must compare `tenant_id` with the current tenant setting `app.current_tenant_id` for the commands it covers (`USING` for reads, updates and deletes; `WITH CHECK` for inserts, and for updates when set). A role-specific permissive policy counts too, because `createPolicy` cannot know which role your application connects as.
|
|
64
|
+
- Restrictive policies can only narrow access, so they may check anything.
|
|
65
|
+
- An existing `tenant_isolation` policy must be permissive, apply to all commands, and apply to `PUBLIC` (no `TO` clause), like the one `createPolicy` generates and every policy Stratum ships.
|
|
66
|
+
|
|
67
|
+
The check recognizes the form Stratum generates, with the operands in either order, with casts, ANDed with other conditions, or ORed with Stratum's `app.bypass_rls` bypass. A policy that isolates correctly but is written in another form is also refused; replace it with the generated form. This is a breaking change for callers that relied on the old skip, shipped in a minor release.
|
|
68
|
+
|
|
69
|
+
`isRLSEnabled` reports on the table that the name resolves to through the `search_path`, not on a table with the same name in another schema.
|
|
70
|
+
|
|
59
71
|
Also available: `disableRLS`, `dropPolicy`, `isRLSEnabled`, `addTenantColumn`, `createIsolationPolicy`, and low-level session helpers `setTenantContext` / `resetTenantContext` / `getCurrentTenantId`. Schema-per-tenant and database-per-tenant variants (`SchemaRawAdapter`, `DatabasePoolManager`, …) are exported too.
|
|
60
72
|
|
|
61
73
|
## Schema-per-tenant search_path
|
|
@@ -93,10 +105,55 @@ When the pool count reaches `maxPools`, the manager ends the least recently used
|
|
|
93
105
|
|
|
94
106
|
Concurrent first requests for one tenant share one pool.
|
|
95
107
|
|
|
108
|
+
## PGlite
|
|
109
|
+
|
|
110
|
+
`@stratum-hq/db-adapters/pglite` runs `@stratum-hq/lib` on [PGlite](https://pglite.dev), which is PostgreSQL compiled to WebAssembly. It works in Node and in the browser, and it needs no database server. Use it for fast local tests and for demos.
|
|
111
|
+
|
|
112
|
+
Install PGlite. It is an optional peer dependency, so npm does not install it for you.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm install @electric-sql/pglite
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { Stratum } from "@stratum-hq/lib";
|
|
120
|
+
import { withTenantContext } from "@stratum-hq/db-adapters";
|
|
121
|
+
import { createPglitePool, createRestrictedPool } from "@stratum-hq/db-adapters/pglite";
|
|
122
|
+
|
|
123
|
+
// In-memory database. Pass PGlite options, such as { dataDir: "idb://my-db" }, to keep data.
|
|
124
|
+
const pool = await createPglitePool();
|
|
125
|
+
const stratum = new Stratum({ pool, autoMigrate: true });
|
|
126
|
+
await stratum.initialize();
|
|
127
|
+
|
|
128
|
+
const acme = await stratum.createTenant({ name: "Acme", slug: "acme" });
|
|
129
|
+
|
|
130
|
+
// Row-level security applies only to a role that is not a superuser.
|
|
131
|
+
const appPool = await createRestrictedPool(pool);
|
|
132
|
+
await withTenantContext(appPool, acme.id, (client) => client.query("SELECT * FROM config_entries"));
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`createPglitePool(source?)` returns a `pg.Pool`-compatible object:
|
|
136
|
+
|
|
137
|
+
- `source` is a PGlite instance or PGlite options. When you pass options, the pool creates the instance and loads the `ltree` and `uuid_ossp` extensions. `pool.end()` then closes the instance.
|
|
138
|
+
- When you pass your own instance, load `ltree` and `uuid_ossp` yourself. `pool.end()` does not close your instance.
|
|
139
|
+
- `pool.pglite` is the PGlite instance.
|
|
140
|
+
|
|
141
|
+
`createRestrictedPool(pool, { role? })` creates the role `stratum_app` (or the name you give) with `NOSUPERUSER NOBYPASSRLS`. It grants the role read and write access to every table and sequence in the `public` schema. Default privileges extend the grants to tables that the superuser creates later. The returned pool runs every query as that role.
|
|
142
|
+
|
|
143
|
+
The restricted role is a test and demo convenience, not a security boundary: any query can leave it with `RESET ROLE`.
|
|
144
|
+
|
|
145
|
+
Limits:
|
|
146
|
+
|
|
147
|
+
- **One connection.** A client from `connect()` holds the only connection until `release()`. Other callers wait in order. If you hold a client and call `pool.query()`, the call waits forever. `release()` rolls back a transaction the client left open and resets the session settings and the role, so the next client starts clean.
|
|
148
|
+
- **No concurrency.** Queries run one at a time. Do not use this adapter to test race conditions or lock contention.
|
|
149
|
+
- **Superuser by default.** PGlite connects as the superuser `postgres`, and a superuser bypasses row-level security. Use `createRestrictedPool` when a test must prove isolation.
|
|
150
|
+
- **Only part of `pg.Pool`.** The pool supports `query`, `connect`, `end` and `on`. Callbacks, cursors, `totalCount` and the other pool counters do not exist.
|
|
151
|
+
- PGlite `^0.4.2` is the supported version.
|
|
152
|
+
|
|
96
153
|
## Security
|
|
97
154
|
|
|
98
155
|
- All DDL validates table names against `/^[a-zA-Z_][a-zA-Z0-9_]*$/`.
|
|
99
|
-
- Tenant ID is always set via `set_config($1, true)
|
|
156
|
+
- Tenant ID is always set via `set_config($1, true)`, fully parameterized.
|
|
100
157
|
- `enableRLS()` always applies `FORCE ROW LEVEL SECURITY`, preventing bypass by table owners.
|
|
101
158
|
- Always reset the tenant context when returning connections to the pool; `createTenantPool` handles this for you.
|
|
102
159
|
|
|
@@ -21,7 +21,7 @@ export declare function createDatabase(client: pg.PoolClient | pg.Client, tenant
|
|
|
21
21
|
* Drops the per-tenant database if it exists.
|
|
22
22
|
*
|
|
23
23
|
* IMPORTANT: DROP DATABASE cannot run inside a transaction block.
|
|
24
|
-
* Same constraint as createDatabase
|
|
24
|
+
* Same constraint as createDatabase: use a standalone client.
|
|
25
25
|
*/
|
|
26
26
|
export declare function dropDatabase(client: pg.PoolClient | pg.Client, tenantSlug: string): Promise<void>;
|
|
27
27
|
//# sourceMappingURL=manager.d.ts.map
|
package/dist/database/manager.js
CHANGED
|
@@ -38,7 +38,7 @@ async function databaseExists(client, tenantSlug) {
|
|
|
38
38
|
*/
|
|
39
39
|
async function createDatabase(client, tenantSlug, templateDb) {
|
|
40
40
|
const dbName = getDatabaseName(tenantSlug);
|
|
41
|
-
// Identifiers validated by regex
|
|
41
|
+
// Identifiers validated by regex: no user-supplied interpolation outside of validated values.
|
|
42
42
|
// pg does not support parameterized DDL identifiers, so we construct the SQL string directly.
|
|
43
43
|
let sql = `CREATE DATABASE "${dbName}"`;
|
|
44
44
|
if (templateDb) {
|
|
@@ -54,7 +54,7 @@ async function createDatabase(client, tenantSlug, templateDb) {
|
|
|
54
54
|
* Drops the per-tenant database if it exists.
|
|
55
55
|
*
|
|
56
56
|
* IMPORTANT: DROP DATABASE cannot run inside a transaction block.
|
|
57
|
-
* Same constraint as createDatabase
|
|
57
|
+
* Same constraint as createDatabase: use a standalone client.
|
|
58
58
|
*/
|
|
59
59
|
async function dropDatabase(client, tenantSlug) {
|
|
60
60
|
const dbName = getDatabaseName(tenantSlug);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/database/manager.ts"],"names":[],"mappings":";;AAWA,0CASC;AAMD,wCAUC;AASD,wCAiBC;AAQD,oCAMC;AA3ED,2CAAgD;AAEhD,MAAM,SAAS,GAAG,iBAAiB,CAAC;AACpC,4EAA4E;AAC5E,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;GAGG;AACH,SAAgB,eAAe,CAAC,UAAkB;IAChD,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC;IACzB,MAAM,MAAM,GAAG,GAAG,SAAS,GAAG,UAAU,EAAE,CAAC;IAC3C,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,oBAAoB,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,kCAAkC,UAAU,aAAa,oBAAoB,QAAQ,CACtF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,cAAc,CAClC,MAAiC,EACjC,UAAkB;IAElB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,uEAAuE,EACvE,CAAC,MAAM,CAAC,CACT,CAAC;IACF,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;AAC5B,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,cAAc,CAClC,MAAiC,EACjC,UAAkB,EAClB,UAAmB;IAEnB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/database/manager.ts"],"names":[],"mappings":";;AAWA,0CASC;AAMD,wCAUC;AASD,wCAiBC;AAQD,oCAMC;AA3ED,2CAAgD;AAEhD,MAAM,SAAS,GAAG,iBAAiB,CAAC;AACpC,4EAA4E;AAC5E,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;GAGG;AACH,SAAgB,eAAe,CAAC,UAAkB;IAChD,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC;IACzB,MAAM,MAAM,GAAG,GAAG,SAAS,GAAG,UAAU,EAAE,CAAC;IAC3C,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,oBAAoB,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,kCAAkC,UAAU,aAAa,oBAAoB,QAAQ,CACtF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,cAAc,CAClC,MAAiC,EACjC,UAAkB;IAElB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,uEAAuE,EACvE,CAAC,MAAM,CAAC,CACT,CAAC;IACF,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;AAC5B,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,cAAc,CAClC,MAAiC,EACjC,UAAkB,EAClB,UAAmB;IAEnB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,8FAA8F;IAC9F,8FAA8F;IAC9F,IAAI,GAAG,GAAG,oBAAoB,MAAM,GAAG,CAAC;IACxC,IAAI,UAAU,EAAE,CAAC;QACf,iEAAiE;QACjE,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,oCAAoC,UAAU,GAAG,CAAC,CAAC;QACrE,CAAC;QACD,GAAG,IAAI,cAAc,UAAU,GAAG,CAAC;IACrC,CAAC;IACD,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,YAAY,CAChC,MAAiC,EACjC,UAAkB;IAElB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,MAAM,MAAM,CAAC,KAAK,CAAC,4BAA4B,MAAM,GAAG,CAAC,CAAC;AAC5D,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
2
|
export interface DatabasePoolManagerOptions {
|
|
3
|
-
/** Template connection config (host, port, user, password, ssl, etc.)
|
|
3
|
+
/** Template connection config (host, port, user, password, ssl, etc.). The database name is overridden per tenant. */
|
|
4
4
|
baseConnectionConfig: pg.PoolConfig;
|
|
5
5
|
/** Maximum number of tenant pools to keep open simultaneously. Default: 50. */
|
|
6
6
|
maxPools?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pool-manager.d.ts","sourceRoot":"","sources":["../../src/database/pool-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAGpB,MAAM,WAAW,0BAA0B;IACzC,
|
|
1
|
+
{"version":3,"file":"pool-manager.d.ts","sourceRoot":"","sources":["../../src/database/pool-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAGpB,MAAM,WAAW,0BAA0B;IACzC,sHAAsH;IACtH,oBAAoB,EAAE,EAAE,CAAC,UAAU,CAAC;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AASD;;;;;;;;GAQG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAC3D,8FAA8F;IAC9F,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,OAAO,EAAE,0BAA0B;IAM/C;;;;;;;OAOG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC;IAgCtE;;;;;;;;OAQG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAqBxD;gFAC4E;IACtE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBrE,kEAAkE;IAC5D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAO/B,uGAAuG;IACvG,OAAO,CAAC,iBAAiB;IAKzB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAWtB,qDAAqD;IACrD,QAAQ,IAAI;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE;IAW5D;;;OAGG;IACH,OAAO,CAAC,OAAO;CAgBhB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,9 @@ export { SchemaRawAdapter, createSchemaTenantPool, } from "./adapters/schema-raw
|
|
|
5
5
|
export type { SchemaAdapterOptions } from "./adapters/schema-raw.js";
|
|
6
6
|
export { SchemaPrismaAdapter, withSchemaTenant, } from "./adapters/schema-prisma.js";
|
|
7
7
|
export { createPolicy, dropPolicy, enableRLS, disableRLS, isRLSEnabled, } from "./rls/manager.js";
|
|
8
|
+
export type { CreatePolicyOptions } from "./rls/manager.js";
|
|
8
9
|
export { setTenantContext, resetTenantContext, withTenantContext, withRlsBypass, getCurrentTenantId, } from "./rls/session.js";
|
|
10
|
+
export type { TenantScope, TenantContextOptions } from "./rls/session.js";
|
|
9
11
|
export { createSchema, dropSchema, schemaExists, listTenantSchemas, replicateTableToSchema, tenantSchemaName, } from "./schema/manager.js";
|
|
10
12
|
export { setSchemaSearchPath, resetSearchPath, getCurrentSearchPath, } from "./schema/session.js";
|
|
11
13
|
export { addTenantColumn, enableRLS as enableRLSForMigration, createIsolationPolicy, migrateTable, } from "./migration-helpers.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACrF,OAAO,EACL,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EACL,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,YAAY,EACZ,UAAU,EACV,SAAS,EACT,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACrF,OAAO,EACL,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EACL,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,YAAY,EACZ,UAAU,EACV,SAAS,EACT,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE5D,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE1E,OAAO,EACL,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,eAAe,EACf,SAAS,IAAI,qBAAqB,EAClC,qBAAqB,EACrB,YAAY,GACb,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,eAAe,EACf,cAAc,EACd,cAAc,EACd,YAAY,GACb,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,YAAY,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAE7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,eAAe,IAAI,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACxG,OAAO,EACL,cAAc,EACd,UAAU,IAAI,iBAAiB,EAC/B,eAAe,IAAI,sBAAsB,GAC1C,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qDAAgD;AAAvC,8GAAA,WAAW,OAAA;AAEpB,4CAAiE;AAAxD,oGAAA,UAAU,OAAA;AAAE,0GAAA,gBAAgB,OAAA;AACrC,kDAAqF;AAA5E,0GAAA,aAAa,OAAA;AAAE,6GAAA,UAAU,OAAoB;AACtD,0DAGkC;AAFhC,iHAAA,gBAAgB,OAAA;AAChB,uHAAA,sBAAsB,OAAA;AAGxB,gEAGqC;AAFnC,uHAAA,mBAAmB,OAAA;AACnB,oHAAA,gBAAgB,OAAA;AAGlB,+CAM0B;AALxB,0GAAA,YAAY,OAAA;AACZ,wGAAA,UAAU,OAAA;AACV,uGAAA,SAAS,OAAA;AACT,wGAAA,UAAU,OAAA;AACV,0GAAA,YAAY,OAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qDAAgD;AAAvC,8GAAA,WAAW,OAAA;AAEpB,4CAAiE;AAAxD,oGAAA,UAAU,OAAA;AAAE,0GAAA,gBAAgB,OAAA;AACrC,kDAAqF;AAA5E,0GAAA,aAAa,OAAA;AAAE,6GAAA,UAAU,OAAoB;AACtD,0DAGkC;AAFhC,iHAAA,gBAAgB,OAAA;AAChB,uHAAA,sBAAsB,OAAA;AAGxB,gEAGqC;AAFnC,uHAAA,mBAAmB,OAAA;AACnB,oHAAA,gBAAgB,OAAA;AAGlB,+CAM0B;AALxB,0GAAA,YAAY,OAAA;AACZ,wGAAA,UAAU,OAAA;AACV,uGAAA,SAAS,OAAA;AACT,wGAAA,UAAU,OAAA;AACV,0GAAA,YAAY,OAAA;AAId,+CAM0B;AALxB,8GAAA,gBAAgB,OAAA;AAChB,gHAAA,kBAAkB,OAAA;AAClB,+GAAA,iBAAiB,OAAA;AACjB,2GAAA,aAAa,OAAA;AACb,gHAAA,kBAAkB,OAAA;AAIpB,kDAO6B;AAN3B,0GAAA,YAAY,OAAA;AACZ,wGAAA,UAAU,OAAA;AACV,0GAAA,YAAY,OAAA;AACZ,+GAAA,iBAAiB,OAAA;AACjB,oHAAA,sBAAsB,OAAA;AACtB,8GAAA,gBAAgB,OAAA;AAGlB,kDAI6B;AAH3B,iHAAA,mBAAmB,OAAA;AACnB,6GAAA,eAAe,OAAA;AACf,kHAAA,oBAAoB,OAAA;AAGtB,+DAKgC;AAJ9B,uHAAA,eAAe,OAAA;AACf,6HAAA,SAAS,OAAyB;AAClC,6HAAA,qBAAqB,OAAA;AACrB,oHAAA,YAAY,OAAA;AAGd,0BAA0B;AAC1B,oDAK+B;AAJ7B,6GAAA,eAAe,OAAA;AACf,4GAAA,cAAc,OAAA;AACd,4GAAA,cAAc,OAAA;AACd,0GAAA,YAAY,OAAA;AAGd,8DAAiE;AAAxD,sHAAA,mBAAmB,OAAA;AAG5B,8DAAgE;AAAvD,qHAAA,kBAAkB,OAAA;AAC3B,oEAAsE;AAA7D,2HAAA,qBAAqB,OAAA;AAC9B,wDAAwG;AAA/F,gHAAA,gBAAgB,OAAA;AAAE,wHAAA,eAAe,OAA4B;AACtE,oDAI+B;AAH7B,4GAAA,cAAc,OAAA;AACd,+GAAA,UAAU,OAAqB;AAC/B,oHAAA,eAAe,OAA0B"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
|
+
import { type CreatePolicyOptions } from "./rls/manager.js";
|
|
2
3
|
export declare function addTenantColumn(client: pg.PoolClient, tableName: string): Promise<void>;
|
|
3
4
|
export declare function enableRLS(client: pg.PoolClient, tableName: string): Promise<void>;
|
|
4
|
-
export declare function createIsolationPolicy(client: pg.PoolClient, tableName: string): Promise<void>;
|
|
5
|
+
export declare function createIsolationPolicy(client: pg.PoolClient, tableName: string, options?: CreatePolicyOptions): Promise<void>;
|
|
5
6
|
export declare function migrateTable(client: pg.PoolClient, tableName: string): Promise<void>;
|
|
6
7
|
//# sourceMappingURL=migration-helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration-helpers.d.ts","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"migration-helpers.d.ts","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAA8C,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAUxG,wBAAsB,eAAe,CACnC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf"}
|
|
@@ -4,6 +4,7 @@ exports.addTenantColumn = addTenantColumn;
|
|
|
4
4
|
exports.enableRLS = enableRLS;
|
|
5
5
|
exports.createIsolationPolicy = createIsolationPolicy;
|
|
6
6
|
exports.migrateTable = migrateTable;
|
|
7
|
+
const manager_js_1 = require("./rls/manager.js");
|
|
7
8
|
// Validate table name to prevent SQL injection (only allows alphanumeric + underscores)
|
|
8
9
|
function validateTableName(name) {
|
|
9
10
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
@@ -20,10 +21,16 @@ async function enableRLS(client, tableName) {
|
|
|
20
21
|
await client.query(`ALTER TABLE ${safe} ENABLE ROW LEVEL SECURITY`);
|
|
21
22
|
await client.query(`ALTER TABLE ${safe} FORCE ROW LEVEL SECURITY`);
|
|
22
23
|
}
|
|
23
|
-
async function createIsolationPolicy(client, tableName) {
|
|
24
|
+
async function createIsolationPolicy(client, tableName, options = {}) {
|
|
24
25
|
const safe = validateTableName(tableName);
|
|
26
|
+
if (options.subtreeRead) {
|
|
27
|
+
await (0, manager_js_1.assertSubtreeFunction)(client);
|
|
28
|
+
}
|
|
25
29
|
await client.query(`CREATE POLICY tenant_isolation ON ${safe}
|
|
26
30
|
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid)`);
|
|
31
|
+
if (options.subtreeRead) {
|
|
32
|
+
await client.query(`CREATE POLICY tenant_subtree_read ON ${safe} FOR SELECT USING (${manager_js_1.SUBTREE_READ_FILTER})`);
|
|
33
|
+
}
|
|
27
34
|
}
|
|
28
35
|
async function migrateTable(client, tableName) {
|
|
29
36
|
await addTenantColumn(client, tableName);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration-helpers.js","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"migration-helpers.js","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":";;AAWA,0CAQC;AAED,8BAOC;AAED,sDAkBC;AAED,oCAOC;AAxDD,iDAAwG;AAExG,wFAAwF;AACxF,SAAS,iBAAiB,CAAC,IAAY;IACrC,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAEM,KAAK,UAAU,eAAe,CACnC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAChB,eAAe,IAAI,qCAAqC,CACzD,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,SAAS,CAC7B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,4BAA4B,CAAC,CAAC;IACpE,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,2BAA2B,CAAC,CAAC;AACrE,CAAC;AAEM,KAAK,UAAU,qBAAqB,CACzC,MAAqB,EACrB,SAAiB,EACjB,UAA+B,EAAE;IAEjC,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACxB,MAAM,IAAA,kCAAqB,EAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IACD,MAAM,MAAM,CAAC,KAAK,CAChB,qCAAqC,IAAI;0FAC6C,CACvF,CAAC;IACF,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACxB,MAAM,MAAM,CAAC,KAAK,CAChB,wCAAwC,IAAI,sBAAsB,gCAAmB,GAAG,CACzF,CAAC;IACJ,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACzC,MAAM,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnC,MAAM,qBAAqB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AACjD,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type pg from "pg";
|
|
2
|
+
import type { PGliteInterface, PGliteOptions } from "@electric-sql/pglite" with { "resolution-mode": "import" };
|
|
3
|
+
/**
|
|
4
|
+
* A `pg.Pool`-compatible object over one PGlite instance.
|
|
5
|
+
*
|
|
6
|
+
* Only these members work: `query`, `connect` (a client with `query` and
|
|
7
|
+
* `release`), `end` and `on`. Other `pg.Pool` members are undefined.
|
|
8
|
+
*/
|
|
9
|
+
export type PglitePool = pg.Pool & {
|
|
10
|
+
/** The PGlite instance that runs every query of this pool. */
|
|
11
|
+
readonly pglite: PGliteInterface;
|
|
12
|
+
};
|
|
13
|
+
export interface RestrictedPoolOptions {
|
|
14
|
+
/**
|
|
15
|
+
* The name of the role. It must match `^[a-z_][a-z0-9_]{0,62}$`.
|
|
16
|
+
* Defaults to `stratum_app`.
|
|
17
|
+
*/
|
|
18
|
+
role?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Return a `pg.Pool`-compatible pool that runs every query on one PGlite instance.
|
|
22
|
+
*
|
|
23
|
+
* The pool has one connection. A client from `connect()` holds it until
|
|
24
|
+
* `release()`, and other callers wait in order. `release()` rolls back a
|
|
25
|
+
* transaction the client left open and resets the session settings, so the
|
|
26
|
+
* next client does not inherit them. Queries run as the PGlite
|
|
27
|
+
* superuser, so row-level security does not apply; use
|
|
28
|
+
* {@link createRestrictedPool} for that.
|
|
29
|
+
*
|
|
30
|
+
* @param source - A PGlite instance, or options to create one. When the
|
|
31
|
+
* pool creates the instance, it loads the `ltree` and `uuid_ossp`
|
|
32
|
+
* extensions, and `end()` closes the instance. When you pass an instance,
|
|
33
|
+
* you load the extensions and you close it.
|
|
34
|
+
* @returns The pool, after PGlite is ready.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createPglitePool(source?: PGliteInterface | PGliteOptions): Promise<PglitePool>;
|
|
37
|
+
/**
|
|
38
|
+
* Return a pool that runs every query as a role that is not a superuser, so
|
|
39
|
+
* that row-level security applies.
|
|
40
|
+
*
|
|
41
|
+
* The function creates the role when it does not exist, with NOSUPERUSER and
|
|
42
|
+
* NOBYPASSRLS. It grants the role SELECT, INSERT, UPDATE and DELETE on every
|
|
43
|
+
* table in the `public` schema, and usage of every sequence. Default
|
|
44
|
+
* privileges extend these grants to tables that the current user creates
|
|
45
|
+
* later.
|
|
46
|
+
*
|
|
47
|
+
* The returned pool shares the connection and the lock of `pool`. Each client
|
|
48
|
+
* runs `SET ROLE` when it gets the connection and `RESET ROLE` at release.
|
|
49
|
+
* Its `end()` closes nothing; end the source pool instead.
|
|
50
|
+
*
|
|
51
|
+
* The restricted role is a convenience for tests and demos, not a security
|
|
52
|
+
* boundary: any query can leave it with `RESET ROLE`, because the session
|
|
53
|
+
* belongs to the superuser.
|
|
54
|
+
*
|
|
55
|
+
* @param pool - A pool from {@link createPglitePool} that runs as the superuser.
|
|
56
|
+
* @param options - The role name.
|
|
57
|
+
* @throws Error - The role name is not a plain lowercase identifier.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createRestrictedPool(pool: PglitePool, options?: RestrictedPoolOptions): Promise<PglitePool>;
|
|
60
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pglite/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAW,MAAM,sBAAsB,CAAC,OAAO,iBAAiB,EAAE,QAAQ,EAAE,CAAC;AAMzH;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,EAAE,CAAC,IAAI,GAAG;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;CAClC,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAuID;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,CAAC,EAAE,eAAe,GAAG,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAgBpG;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,UAAU,CAAC,CAoBrB"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPglitePool = createPglitePool;
|
|
4
|
+
exports.createRestrictedPool = createRestrictedPool;
|
|
5
|
+
// pg returns int8 and numeric values as strings, because a JavaScript number
|
|
6
|
+
// loses precision above 2^53. PGlite returns numbers, so these parsers
|
|
7
|
+
// restore the pg behavior.
|
|
8
|
+
const PG_PARSERS = { 20: (value) => value, 1700: (value) => value };
|
|
9
|
+
const ROLE_NAME = /^[a-z_][a-z0-9_]{0,62}$/;
|
|
10
|
+
// A restricted pool must share the lock of its source pool, because both
|
|
11
|
+
// use the same connection.
|
|
12
|
+
const locks = new WeakMap();
|
|
13
|
+
/**
|
|
14
|
+
* Return a lock that gives one holder at a time the connection, in request order.
|
|
15
|
+
*/
|
|
16
|
+
function createLock() {
|
|
17
|
+
let tail = Promise.resolve();
|
|
18
|
+
return () => {
|
|
19
|
+
let unlock;
|
|
20
|
+
const held = new Promise((resolve) => (unlock = resolve));
|
|
21
|
+
const acquired = tail.then(() => unlock);
|
|
22
|
+
tail = tail.then(() => held);
|
|
23
|
+
return acquired;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// pg sends a plain object as JSON text for every parameter type. PGlite
|
|
27
|
+
// serializes by the inferred parameter type and rejects an object for a text
|
|
28
|
+
// parameter, so the conversion happens here.
|
|
29
|
+
function prepareValue(value) {
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
return null;
|
|
32
|
+
if (value !== null &&
|
|
33
|
+
typeof value === "object" &&
|
|
34
|
+
!(value instanceof Date) &&
|
|
35
|
+
!(value instanceof Uint8Array) &&
|
|
36
|
+
!Array.isArray(value)) {
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function toQueryResult(result) {
|
|
42
|
+
// PGlite reports affectedRows 0 for a SELECT. pg reports the number of rows
|
|
43
|
+
// for any statement that returns rows.
|
|
44
|
+
const rowCount = result.fields.length > 0 ? result.rows.length : (result.affectedRows ?? 0);
|
|
45
|
+
return { rows: result.rows, rowCount, fields: result.fields };
|
|
46
|
+
}
|
|
47
|
+
async function run(db, textOrConfig, values) {
|
|
48
|
+
const text = typeof textOrConfig === "string" ? textOrConfig : textOrConfig.text;
|
|
49
|
+
const params = values ?? (typeof textOrConfig === "string" ? undefined : textOrConfig.values);
|
|
50
|
+
if (!params || params.length === 0) {
|
|
51
|
+
// pg uses the simple query protocol when there are no parameters, and
|
|
52
|
+
// that protocol accepts several statements, as migration files hold.
|
|
53
|
+
// PGlite exec() is the same protocol. It returns one result per statement,
|
|
54
|
+
// and pg returns the last one.
|
|
55
|
+
const results = await db.exec(text, { parsers: PG_PARSERS });
|
|
56
|
+
return toQueryResult(results[results.length - 1] ?? { rows: [], fields: [] });
|
|
57
|
+
}
|
|
58
|
+
return toQueryResult(await db.query(text, params.map(prepareValue), { parsers: PG_PARSERS }));
|
|
59
|
+
}
|
|
60
|
+
function makePool(db, acquire, ownsInstance, role) {
|
|
61
|
+
const connect = async () => {
|
|
62
|
+
const unlock = await acquire();
|
|
63
|
+
try {
|
|
64
|
+
if (role)
|
|
65
|
+
await db.exec(`SET ROLE "${role}"`);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
unlock();
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
let released = false;
|
|
72
|
+
return {
|
|
73
|
+
query: (textOrConfig, values) => run(db, textOrConfig, values),
|
|
74
|
+
// pg gives each client its own session and destroys the connection when
|
|
75
|
+
// release() receives an error. This pool has one session for every
|
|
76
|
+
// client, so release cleans it instead: it rolls back a transaction the
|
|
77
|
+
// client left open, and resets the settings and the role the client
|
|
78
|
+
// set. The lock stays held until the cleanup ends, so the next client
|
|
79
|
+
// gets a clean session.
|
|
80
|
+
release: () => {
|
|
81
|
+
if (released)
|
|
82
|
+
return;
|
|
83
|
+
released = true;
|
|
84
|
+
// RESET ALL leaves the role alone, so RESET ROLE follows it.
|
|
85
|
+
db.exec("ROLLBACK; RESET ALL; RESET ROLE").then(unlock, unlock);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
const pool = {
|
|
90
|
+
pglite: db,
|
|
91
|
+
connect,
|
|
92
|
+
async query(textOrConfig, values) {
|
|
93
|
+
const client = await connect();
|
|
94
|
+
try {
|
|
95
|
+
return await client.query(textOrConfig, values);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
client.release();
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
async end() {
|
|
102
|
+
if (ownsInstance && !db.closed)
|
|
103
|
+
await db.close();
|
|
104
|
+
},
|
|
105
|
+
on() {
|
|
106
|
+
return pool;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const result = pool;
|
|
110
|
+
locks.set(result, acquire);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
function isPglite(value) {
|
|
114
|
+
return (typeof value === "object" &&
|
|
115
|
+
value !== null &&
|
|
116
|
+
typeof value.exec === "function" &&
|
|
117
|
+
typeof value.query === "function");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Return a `pg.Pool`-compatible pool that runs every query on one PGlite instance.
|
|
121
|
+
*
|
|
122
|
+
* The pool has one connection. A client from `connect()` holds it until
|
|
123
|
+
* `release()`, and other callers wait in order. `release()` rolls back a
|
|
124
|
+
* transaction the client left open and resets the session settings, so the
|
|
125
|
+
* next client does not inherit them. Queries run as the PGlite
|
|
126
|
+
* superuser, so row-level security does not apply; use
|
|
127
|
+
* {@link createRestrictedPool} for that.
|
|
128
|
+
*
|
|
129
|
+
* @param source - A PGlite instance, or options to create one. When the
|
|
130
|
+
* pool creates the instance, it loads the `ltree` and `uuid_ossp`
|
|
131
|
+
* extensions, and `end()` closes the instance. When you pass an instance,
|
|
132
|
+
* you load the extensions and you close it.
|
|
133
|
+
* @returns The pool, after PGlite is ready.
|
|
134
|
+
*/
|
|
135
|
+
async function createPglitePool(source) {
|
|
136
|
+
if (isPglite(source)) {
|
|
137
|
+
await source.waitReady;
|
|
138
|
+
return makePool(source, createLock(), false, null);
|
|
139
|
+
}
|
|
140
|
+
const [{ PGlite }, { ltree }, { uuid_ossp }] = await Promise.all([
|
|
141
|
+
import("@electric-sql/pglite"),
|
|
142
|
+
import("@electric-sql/pglite/contrib/ltree"),
|
|
143
|
+
import("@electric-sql/pglite/contrib/uuid_ossp"),
|
|
144
|
+
]);
|
|
145
|
+
const options = source ?? {};
|
|
146
|
+
const db = await PGlite.create({
|
|
147
|
+
...options,
|
|
148
|
+
extensions: { ltree, uuid_ossp, ...options.extensions },
|
|
149
|
+
});
|
|
150
|
+
return makePool(db, createLock(), true, null);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Return a pool that runs every query as a role that is not a superuser, so
|
|
154
|
+
* that row-level security applies.
|
|
155
|
+
*
|
|
156
|
+
* The function creates the role when it does not exist, with NOSUPERUSER and
|
|
157
|
+
* NOBYPASSRLS. It grants the role SELECT, INSERT, UPDATE and DELETE on every
|
|
158
|
+
* table in the `public` schema, and usage of every sequence. Default
|
|
159
|
+
* privileges extend these grants to tables that the current user creates
|
|
160
|
+
* later.
|
|
161
|
+
*
|
|
162
|
+
* The returned pool shares the connection and the lock of `pool`. Each client
|
|
163
|
+
* runs `SET ROLE` when it gets the connection and `RESET ROLE` at release.
|
|
164
|
+
* Its `end()` closes nothing; end the source pool instead.
|
|
165
|
+
*
|
|
166
|
+
* The restricted role is a convenience for tests and demos, not a security
|
|
167
|
+
* boundary: any query can leave it with `RESET ROLE`, because the session
|
|
168
|
+
* belongs to the superuser.
|
|
169
|
+
*
|
|
170
|
+
* @param pool - A pool from {@link createPglitePool} that runs as the superuser.
|
|
171
|
+
* @param options - The role name.
|
|
172
|
+
* @throws Error - The role name is not a plain lowercase identifier.
|
|
173
|
+
*/
|
|
174
|
+
async function createRestrictedPool(pool, options = {}) {
|
|
175
|
+
const role = options.role ?? "stratum_app";
|
|
176
|
+
if (!ROLE_NAME.test(role)) {
|
|
177
|
+
throw new Error(`Invalid role name '${role}': use lowercase letters, digits and underscores`);
|
|
178
|
+
}
|
|
179
|
+
await pool.query(`
|
|
180
|
+
DO $$ BEGIN
|
|
181
|
+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${role}') THEN
|
|
182
|
+
CREATE ROLE "${role}" NOLOGIN NOSUPERUSER NOBYPASSRLS;
|
|
183
|
+
END IF;
|
|
184
|
+
END $$;
|
|
185
|
+
GRANT USAGE ON SCHEMA public TO "${role}";
|
|
186
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "${role}";
|
|
187
|
+
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO "${role}";
|
|
188
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "${role}";
|
|
189
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO "${role}";
|
|
190
|
+
`);
|
|
191
|
+
const acquire = locks.get(pool);
|
|
192
|
+
if (!acquire)
|
|
193
|
+
throw new Error("createRestrictedPool needs a pool from createPglitePool");
|
|
194
|
+
return makePool(pool.pglite, acquire, false, role);
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pglite/index.ts"],"names":[],"mappings":"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"}
|
package/dist/rls/manager.d.ts
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The read-only subtree predicate of migration 031. It admits the rows of the
|
|
4
|
+
* current tenant's descendants only when the session set app.tenant_scope to
|
|
5
|
+
* 'subtree'. It goes in a FOR SELECT policy only, so writes stay exact.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SUBTREE_READ_FILTER: string;
|
|
8
|
+
export interface CreatePolicyOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Also create the tenant_subtree_read policy, so that a session in the
|
|
11
|
+
* "subtree" scope reads the rows of the tenant's descendants. Needs
|
|
12
|
+
* migration 031 of @stratum-hq/lib. Default false.
|
|
13
|
+
*/
|
|
14
|
+
subtreeRead?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Throws when the database has no stratum_subtree_tenant_ids() function. The
|
|
18
|
+
* subtree read policy calls it, and without it CREATE POLICY fails with a
|
|
19
|
+
* less clear error.
|
|
20
|
+
*/
|
|
21
|
+
export declare function assertSubtreeFunction(client: pg.PoolClient): Promise<void>;
|
|
22
|
+
export declare function createPolicy(client: pg.PoolClient, tableName: string, options?: CreatePolicyOptions): Promise<void>;
|
|
3
23
|
export declare function dropPolicy(client: pg.PoolClient, tableName: string): Promise<void>;
|
|
4
24
|
export declare function enableRLS(client: pg.PoolClient, tableName: string): Promise<void>;
|
|
5
25
|
export declare function disableRLS(client: pg.PoolClient, tableName: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAKpB;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,QAEuC,CAAC;AAExE,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhF;AAUD,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAsCf;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAWlB"}
|
package/dist/rls/manager.js
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SUBTREE_READ_FILTER = void 0;
|
|
4
|
+
exports.assertSubtreeFunction = assertSubtreeFunction;
|
|
3
5
|
exports.createPolicy = createPolicy;
|
|
4
6
|
exports.dropPolicy = dropPolicy;
|
|
5
7
|
exports.enableRLS = enableRLS;
|
|
6
8
|
exports.disableRLS = disableRLS;
|
|
7
9
|
exports.isRLSEnabled = isRLSEnabled;
|
|
10
|
+
const policy_check_js_1 = require("./policy-check.js");
|
|
11
|
+
const TENANT_FILTER = "tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid";
|
|
12
|
+
/**
|
|
13
|
+
* The read-only subtree predicate of migration 031. It admits the rows of the
|
|
14
|
+
* current tenant's descendants only when the session set app.tenant_scope to
|
|
15
|
+
* 'subtree'. It goes in a FOR SELECT policy only, so writes stay exact.
|
|
16
|
+
*/
|
|
17
|
+
exports.SUBTREE_READ_FILTER = "current_setting('app.tenant_scope', true) = 'subtree' " +
|
|
18
|
+
"AND tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])";
|
|
19
|
+
/**
|
|
20
|
+
* Throws when the database has no stratum_subtree_tenant_ids() function. The
|
|
21
|
+
* subtree read policy calls it, and without it CREATE POLICY fails with a
|
|
22
|
+
* less clear error.
|
|
23
|
+
*/
|
|
24
|
+
async function assertSubtreeFunction(client) {
|
|
25
|
+
const res = await client.query("SELECT to_regprocedure('stratum_subtree_tenant_ids()') IS NOT NULL AS present");
|
|
26
|
+
if (!res.rows[0]?.present) {
|
|
27
|
+
throw new Error("[stratum] The subtree read policy needs the function stratum_subtree_tenant_ids(), " +
|
|
28
|
+
"which migration 031 of @stratum-hq/lib creates. Run the Stratum migrations first.");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
8
31
|
// Validate table name to prevent SQL injection (only allows alphanumeric + underscores)
|
|
9
32
|
function validateTableName(tableName) {
|
|
10
33
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
@@ -12,18 +35,42 @@ function validateTableName(tableName) {
|
|
|
12
35
|
}
|
|
13
36
|
return tableName;
|
|
14
37
|
}
|
|
15
|
-
async function createPolicy(client, tableName) {
|
|
38
|
+
async function createPolicy(client, tableName, options = {}) {
|
|
16
39
|
const safe = validateTableName(tableName);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
40
|
+
if (options.subtreeRead) {
|
|
41
|
+
await assertSubtreeFunction(client);
|
|
42
|
+
}
|
|
43
|
+
// Read every policy on the table that the name resolves to, in whichever
|
|
44
|
+
// schema that is. PostgreSQL ORs permissive policies together, so each one
|
|
45
|
+
// must filter by tenant, and a policy's name proves nothing.
|
|
46
|
+
// Each row also carries the schema of the tenants table, the only schema
|
|
47
|
+
// that may qualify the subtree function (migration 031 creates it there).
|
|
48
|
+
const existing = await client.query(`SELECT p.policyname, p.permissive, p.cmd, p.qual, p.with_check, p.roles::text[] AS roles,
|
|
49
|
+
(SELECT tn.nspname FROM pg_class tc JOIN pg_namespace tn ON tn.oid = tc.relnamespace
|
|
50
|
+
WHERE tc.oid = to_regclass('tenants')) AS tenants_schema
|
|
51
|
+
FROM pg_policies p
|
|
52
|
+
JOIN pg_class c ON c.relname = p.tablename
|
|
53
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = p.schemaname
|
|
54
|
+
WHERE c.oid = to_regclass($1)`, [safe]);
|
|
55
|
+
const issues = (0, policy_check_js_1.tablePolicyIssues)(existing.rows, existing.rows[0]?.tenants_schema ?? undefined);
|
|
56
|
+
if (issues.length > 0) {
|
|
57
|
+
throw new Error(`[stratum] Table ${safe} has row-level security policies that do not isolate it by tenant: ` +
|
|
58
|
+
`${issues.join("; ")} (expected ${TENANT_FILTER}). ` +
|
|
59
|
+
`Drop or correct those policies, then call createPolicy again.`);
|
|
60
|
+
}
|
|
61
|
+
if (!existing.rows.some((p) => p.policyname === "tenant_isolation")) {
|
|
62
|
+
// Cannot use parameterized queries inside DO blocks or for DDL identifiers.
|
|
63
|
+
// Table name is validated via allowlist regex above.
|
|
64
|
+
await client.query(`CREATE POLICY tenant_isolation ON ${safe} USING (${TENANT_FILTER})`);
|
|
65
|
+
}
|
|
66
|
+
if (options.subtreeRead && !existing.rows.some((p) => p.policyname === "tenant_subtree_read")) {
|
|
67
|
+
await client.query(`CREATE POLICY tenant_subtree_read ON ${safe} FOR SELECT USING (${exports.SUBTREE_READ_FILTER})`);
|
|
22
68
|
}
|
|
23
69
|
}
|
|
24
70
|
async function dropPolicy(client, tableName) {
|
|
25
71
|
const safe = validateTableName(tableName);
|
|
26
72
|
await client.query(`DROP POLICY IF EXISTS tenant_isolation ON ${safe}`);
|
|
73
|
+
await client.query(`DROP POLICY IF EXISTS tenant_subtree_read ON ${safe}`);
|
|
27
74
|
}
|
|
28
75
|
async function enableRLS(client, tableName) {
|
|
29
76
|
const safe = validateTableName(tableName);
|
|
@@ -36,7 +83,9 @@ async function disableRLS(client, tableName) {
|
|
|
36
83
|
await client.query(`ALTER TABLE ${safe} DISABLE ROW LEVEL SECURITY`);
|
|
37
84
|
}
|
|
38
85
|
async function isRLSEnabled(client, tableName) {
|
|
39
|
-
const
|
|
86
|
+
const safe = validateTableName(tableName);
|
|
87
|
+
// The table the name resolves to, in whichever schema that is.
|
|
88
|
+
const res = await client.query(`SELECT relrowsecurity FROM pg_class WHERE oid = to_regclass($1)`, [safe]);
|
|
40
89
|
if (res.rows.length === 0) {
|
|
41
90
|
return false;
|
|
42
91
|
}
|
package/dist/rls/manager.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether the row-level security policies on a table restrict rows to
|
|
3
|
+
* the current tenant, from the expressions PostgreSQL stores in pg_policies.
|
|
4
|
+
*
|
|
5
|
+
* `createPolicy` uses this before it keeps an existing tenant_isolation policy
|
|
6
|
+
* or adds one, because a policy's name proves nothing about what it checks, and
|
|
7
|
+
* PostgreSQL ORs permissive policies together: one permissive policy that does
|
|
8
|
+
* not filter by tenant opens the table whatever tenant_isolation says.
|
|
9
|
+
*
|
|
10
|
+
* Rules:
|
|
11
|
+
* - Every permissive policy must filter by tenant for the commands it covers:
|
|
12
|
+
* USING for SELECT, UPDATE, DELETE and ALL, WITH CHECK for INSERT, and WITH
|
|
13
|
+
* CHECK for UPDATE and ALL when it is set. This applies whatever roles the
|
|
14
|
+
* policy names, because this package cannot know which role the
|
|
15
|
+
* application connects as.
|
|
16
|
+
* - Restrictive policies can only narrow access, so they may check anything.
|
|
17
|
+
* - The tenant_isolation policy itself must be permissive, apply to ALL
|
|
18
|
+
* commands, and apply to PUBLIC, like the one `createPolicy` generates and
|
|
19
|
+
* every policy Stratum ships.
|
|
20
|
+
*
|
|
21
|
+
* The check recognises the form this package generates, with the operands in
|
|
22
|
+
* either order, with casts, inside an AND with other conditions, and ORed with
|
|
23
|
+
* Stratum's administrative bypass. Anything else is reported as not filtering
|
|
24
|
+
* by tenant, so an unusual but correct policy is refused, never accepted.
|
|
25
|
+
*
|
|
26
|
+
* It also recognises the subtree read of migration 031: the
|
|
27
|
+
* stratum_subtree_tenant_ids() match ANDed with the app.tenant_scope =
|
|
28
|
+
* 'subtree' check. That form counts only in the USING clause of a SELECT
|
|
29
|
+
* policy. In any other policy, the same clause would let UPDATE or DELETE
|
|
30
|
+
* reach a descendant's rows, and in WITH CHECK it would let a write name a
|
|
31
|
+
* descendant.
|
|
32
|
+
*/
|
|
33
|
+
/** One row of pg_policies. */
|
|
34
|
+
export interface PolicyRow {
|
|
35
|
+
policyname: string;
|
|
36
|
+
/** "PERMISSIVE" or "RESTRICTIVE". */
|
|
37
|
+
permissive: string;
|
|
38
|
+
/** "ALL", "SELECT", "INSERT", "UPDATE" or "DELETE". */
|
|
39
|
+
cmd: string;
|
|
40
|
+
qual: string | null;
|
|
41
|
+
with_check: string | null;
|
|
42
|
+
/** The roles the policy applies to; ["public"] for PUBLIC. */
|
|
43
|
+
roles: string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Every reason the table's policies do not isolate it by tenant. Empty when
|
|
47
|
+
* they do, or when the table has no policies.
|
|
48
|
+
*
|
|
49
|
+
* @param functionSchema - The schema of Stratum's tenants table, the only
|
|
50
|
+
* schema that may qualify stratum_subtree_tenant_ids(). When it is
|
|
51
|
+
* undefined, only the unqualified function counts.
|
|
52
|
+
*/
|
|
53
|
+
export declare function tablePolicyIssues(policies: PolicyRow[], functionSchema?: string): string[];
|
|
54
|
+
//# sourceMappingURL=policy-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy-check.d.ts","sourceRoot":"","sources":["../../src/rls/policy-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,8BAA8B;AAC9B,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,8DAA8D;IAC9D,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAoMD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,EAAE,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAiB1F"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Decides whether the row-level security policies on a table restrict rows to
|
|
4
|
+
* the current tenant, from the expressions PostgreSQL stores in pg_policies.
|
|
5
|
+
*
|
|
6
|
+
* `createPolicy` uses this before it keeps an existing tenant_isolation policy
|
|
7
|
+
* or adds one, because a policy's name proves nothing about what it checks, and
|
|
8
|
+
* PostgreSQL ORs permissive policies together: one permissive policy that does
|
|
9
|
+
* not filter by tenant opens the table whatever tenant_isolation says.
|
|
10
|
+
*
|
|
11
|
+
* Rules:
|
|
12
|
+
* - Every permissive policy must filter by tenant for the commands it covers:
|
|
13
|
+
* USING for SELECT, UPDATE, DELETE and ALL, WITH CHECK for INSERT, and WITH
|
|
14
|
+
* CHECK for UPDATE and ALL when it is set. This applies whatever roles the
|
|
15
|
+
* policy names, because this package cannot know which role the
|
|
16
|
+
* application connects as.
|
|
17
|
+
* - Restrictive policies can only narrow access, so they may check anything.
|
|
18
|
+
* - The tenant_isolation policy itself must be permissive, apply to ALL
|
|
19
|
+
* commands, and apply to PUBLIC, like the one `createPolicy` generates and
|
|
20
|
+
* every policy Stratum ships.
|
|
21
|
+
*
|
|
22
|
+
* The check recognises the form this package generates, with the operands in
|
|
23
|
+
* either order, with casts, inside an AND with other conditions, and ORed with
|
|
24
|
+
* Stratum's administrative bypass. Anything else is reported as not filtering
|
|
25
|
+
* by tenant, so an unusual but correct policy is refused, never accepted.
|
|
26
|
+
*
|
|
27
|
+
* It also recognises the subtree read of migration 031: the
|
|
28
|
+
* stratum_subtree_tenant_ids() match ANDed with the app.tenant_scope =
|
|
29
|
+
* 'subtree' check. That form counts only in the USING clause of a SELECT
|
|
30
|
+
* policy. In any other policy, the same clause would let UPDATE or DELETE
|
|
31
|
+
* reach a descendant's rows, and in WITH CHECK it would let a write name a
|
|
32
|
+
* descendant.
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.tablePolicyIssues = tablePolicyIssues;
|
|
36
|
+
/** The session setting that holds the current tenant (see rls/session.ts). */
|
|
37
|
+
const TENANT_SETTING = "app.current_tenant_id";
|
|
38
|
+
/** The setting Stratum's own policies admit for administrative access. */
|
|
39
|
+
const BYPASS_SETTING = "app.bypass_rls";
|
|
40
|
+
/** The setting that opts a session in to the subtree read scope (migration 031). */
|
|
41
|
+
const SCOPE_SETTING = "app.tenant_scope";
|
|
42
|
+
/** The function that returns the current tenant and its descendants (migration 031). */
|
|
43
|
+
const SUBTREE_FUNCTION = "stratum_subtree_tenant_ids";
|
|
44
|
+
/** Drops whitespace and casts, lower-cases, and keeps string literals intact. */
|
|
45
|
+
function normalize(expr) {
|
|
46
|
+
let out = "";
|
|
47
|
+
for (let i = 0; i < expr.length; i++) {
|
|
48
|
+
const ch = expr[i];
|
|
49
|
+
if (ch === "'") {
|
|
50
|
+
const end = expr.indexOf("'", i + 1);
|
|
51
|
+
const stop = end === -1 ? expr.length : end + 1;
|
|
52
|
+
out += expr.slice(i, stop);
|
|
53
|
+
i = stop - 1;
|
|
54
|
+
}
|
|
55
|
+
else if (!/\s/.test(ch)) {
|
|
56
|
+
out += ch.toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Casts such as ::text, ::uuid and ::character varying (spaces already gone).
|
|
60
|
+
return out.replace(/::[a-z_]+(?:\[\])?/g, "");
|
|
61
|
+
}
|
|
62
|
+
/** Index of the parenthesis that closes the one at `open`. */
|
|
63
|
+
function closingParen(expr, open) {
|
|
64
|
+
let depth = 0;
|
|
65
|
+
for (let i = open; i < expr.length; i++) {
|
|
66
|
+
const ch = expr[i];
|
|
67
|
+
if (ch === "'") {
|
|
68
|
+
const end = expr.indexOf("'", i + 1);
|
|
69
|
+
if (end === -1)
|
|
70
|
+
return -1;
|
|
71
|
+
i = end;
|
|
72
|
+
}
|
|
73
|
+
else if (ch === "(") {
|
|
74
|
+
depth++;
|
|
75
|
+
}
|
|
76
|
+
else if (ch === ")") {
|
|
77
|
+
depth--;
|
|
78
|
+
if (depth === 0)
|
|
79
|
+
return i;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return -1;
|
|
83
|
+
}
|
|
84
|
+
/** Removes parentheses that wrap the whole expression. */
|
|
85
|
+
function stripOuterParens(expr) {
|
|
86
|
+
let e = expr;
|
|
87
|
+
while (e.startsWith("(") && e.endsWith(")") && closingParen(e, 0) === e.length - 1) {
|
|
88
|
+
e = e.slice(1, -1);
|
|
89
|
+
}
|
|
90
|
+
return e;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Splits a normalized expression on a top-level boolean operator. Whitespace
|
|
94
|
+
* is gone, so the operator is found between a closing parenthesis and an
|
|
95
|
+
* opening one, which is how PostgreSQL prints every operand of AND / OR.
|
|
96
|
+
*/
|
|
97
|
+
function splitTopLevel(expr, op) {
|
|
98
|
+
const parts = [];
|
|
99
|
+
let depth = 0;
|
|
100
|
+
let start = 0;
|
|
101
|
+
for (let i = 0; i < expr.length; i++) {
|
|
102
|
+
const ch = expr[i];
|
|
103
|
+
if (ch === "'") {
|
|
104
|
+
const end = expr.indexOf("'", i + 1);
|
|
105
|
+
if (end === -1)
|
|
106
|
+
break;
|
|
107
|
+
i = end;
|
|
108
|
+
}
|
|
109
|
+
else if (ch === "(") {
|
|
110
|
+
depth++;
|
|
111
|
+
}
|
|
112
|
+
else if (ch === ")") {
|
|
113
|
+
depth--;
|
|
114
|
+
if (depth === 0 && expr.startsWith(`${op}(`, i + 1)) {
|
|
115
|
+
parts.push(expr.slice(start, i + 1));
|
|
116
|
+
start = i + 1 + op.length;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
parts.push(expr.slice(start));
|
|
121
|
+
return parts.map(stripOuterParens);
|
|
122
|
+
}
|
|
123
|
+
/** Removes every parenthesis; only safe on an operand with no AND / OR in it. */
|
|
124
|
+
function flatten(expr) {
|
|
125
|
+
return expr.replace(/[()]/g, "");
|
|
126
|
+
}
|
|
127
|
+
const SETTING_READ = `current_setting'${TENANT_SETTING.replace(".", "\\.")}'(?:,(?:true|false))?`;
|
|
128
|
+
/** The current tenant, read directly or through NULLIF(..., ''). */
|
|
129
|
+
const CURRENT_TENANT = `(?:nullif${SETTING_READ},''|${SETTING_READ})`;
|
|
130
|
+
const TENANT_MATCH = new RegExp(`^(?:tenant_id=${CURRENT_TENANT}|${CURRENT_TENANT}=tenant_id)$`);
|
|
131
|
+
const BYPASS_MATCH = new RegExp(`^current_setting'${BYPASS_SETTING.replace(".", "\\.")}'(?:,(?:true|false))?='on'$`);
|
|
132
|
+
const SCOPE_MATCH = new RegExp(`^current_setting'${SCOPE_SETTING.replace(".", "\\.")}'(?:,(?:true|false))?='subtree'$`);
|
|
133
|
+
/**
|
|
134
|
+
* tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[]) after
|
|
135
|
+
* normalize and flatten. PostgreSQL qualifies the function with its schema
|
|
136
|
+
* when that schema is not on the search path of the reading session. Only the
|
|
137
|
+
* schema that holds Stratum's tenants table, where migration 031 creates the
|
|
138
|
+
* function, may qualify it: a function of the same name in another schema
|
|
139
|
+
* could return any ids.
|
|
140
|
+
*/
|
|
141
|
+
function subtreeMatch(functionSchema) {
|
|
142
|
+
const qualifier = functionSchema === undefined ? "" : `(?:${escapeRegExp(quoteIdent(functionSchema).toLowerCase())}\\.)?`;
|
|
143
|
+
return new RegExp(`^tenant_id=anyselect${qualifier}${SUBTREE_FUNCTION}(?:as${SUBTREE_FUNCTION})?$`);
|
|
144
|
+
}
|
|
145
|
+
function escapeRegExp(text) {
|
|
146
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
147
|
+
}
|
|
148
|
+
/** The identifier as PostgreSQL prints it: quoted unless it is a plain lowercase name. */
|
|
149
|
+
function quoteIdent(name) {
|
|
150
|
+
return /^[a-z_][a-z0-9_$]*$/.test(name) ? name : `"${name.replace(/"/g, '""')}"`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* True when the normalized expression only admits rows of the current tenant.
|
|
154
|
+
* `subtree` matches the subtree function call that may appear in the
|
|
155
|
+
* expression, or is null where the subtree read is not allowed.
|
|
156
|
+
*/
|
|
157
|
+
function filtersByTenant(expr, subtree) {
|
|
158
|
+
const e = stripOuterParens(expr);
|
|
159
|
+
const disjuncts = splitTopLevel(e, "or");
|
|
160
|
+
if (disjuncts.length > 1) {
|
|
161
|
+
// Every branch must be safe; Stratum's administrative bypass is one of them.
|
|
162
|
+
return (disjuncts.every((d) => BYPASS_MATCH.test(flatten(d)) || filtersByTenant(d, subtree)) &&
|
|
163
|
+
disjuncts.some((d) => !BYPASS_MATCH.test(flatten(d))));
|
|
164
|
+
}
|
|
165
|
+
const conjuncts = splitTopLevel(e, "and");
|
|
166
|
+
if (conjuncts.length > 1) {
|
|
167
|
+
if (conjuncts.some((c) => filtersByTenant(c, subtree)))
|
|
168
|
+
return true;
|
|
169
|
+
// The subtree read of migration 031: the descendants of the current
|
|
170
|
+
// tenant, only when the session opted in with app.tenant_scope.
|
|
171
|
+
return (subtree !== null &&
|
|
172
|
+
conjuncts.some((c) => SCOPE_MATCH.test(flatten(c))) &&
|
|
173
|
+
conjuncts.some((c) => subtree.test(flatten(c))));
|
|
174
|
+
}
|
|
175
|
+
return TENANT_MATCH.test(flatten(e));
|
|
176
|
+
}
|
|
177
|
+
/** An expression on one line, for messages. */
|
|
178
|
+
function oneLine(expr) {
|
|
179
|
+
return expr === null ? "none" : expr.replace(/\s+/g, " ");
|
|
180
|
+
}
|
|
181
|
+
/** Why one permissive policy lets rows of other tenants through, or null. */
|
|
182
|
+
function permissiveIssue(p, subtree) {
|
|
183
|
+
const name = `policy "${oneLine(p.policyname)}" (${p.cmd})`;
|
|
184
|
+
// INSERT policies have only WITH CHECK; SELECT and DELETE only USING. For
|
|
185
|
+
// ALL and UPDATE a missing WITH CHECK means PostgreSQL reuses USING.
|
|
186
|
+
if (p.cmd !== "INSERT") {
|
|
187
|
+
// Only a SELECT policy may widen to the subtree: in any other policy the
|
|
188
|
+
// same USING clause would let UPDATE or DELETE reach a descendant's rows.
|
|
189
|
+
if (p.qual === null || !filtersByTenant(normalize(p.qual), p.cmd === "SELECT" ? subtree : null)) {
|
|
190
|
+
return `${name} USING (${oneLine(p.qual)}) does not filter by tenant`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (p.cmd === "INSERT" || p.with_check !== null) {
|
|
194
|
+
if (p.with_check === null || !filtersByTenant(normalize(p.with_check), null)) {
|
|
195
|
+
return `${name} WITH CHECK (${oneLine(p.with_check)}) does not filter by tenant`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
/** Why the tenant_isolation policy is not the shape createPolicy generates, or null. */
|
|
201
|
+
function tenantIsolationShapeIssue(p) {
|
|
202
|
+
const name = `policy "${oneLine(p.policyname)}"`;
|
|
203
|
+
if (p.permissive !== "PERMISSIVE") {
|
|
204
|
+
return `${name} is ${p.permissive.toLowerCase()}, not permissive`;
|
|
205
|
+
}
|
|
206
|
+
if (p.cmd !== "ALL") {
|
|
207
|
+
return `${name} applies to ${p.cmd} only, not ALL commands`;
|
|
208
|
+
}
|
|
209
|
+
if (p.roles.length !== 1 || p.roles[0] !== "public") {
|
|
210
|
+
return `${name} applies to roles ${p.roles.join(", ")}, not PUBLIC`;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Every reason the table's policies do not isolate it by tenant. Empty when
|
|
216
|
+
* they do, or when the table has no policies.
|
|
217
|
+
*
|
|
218
|
+
* @param functionSchema - The schema of Stratum's tenants table, the only
|
|
219
|
+
* schema that may qualify stratum_subtree_tenant_ids(). When it is
|
|
220
|
+
* undefined, only the unqualified function counts.
|
|
221
|
+
*/
|
|
222
|
+
function tablePolicyIssues(policies, functionSchema) {
|
|
223
|
+
const subtree = subtreeMatch(functionSchema);
|
|
224
|
+
const issues = [];
|
|
225
|
+
for (const p of policies) {
|
|
226
|
+
if (p.policyname === "tenant_isolation") {
|
|
227
|
+
const shape = tenantIsolationShapeIssue(p);
|
|
228
|
+
if (shape !== null) {
|
|
229
|
+
issues.push(shape);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (p.permissive === "PERMISSIVE") {
|
|
234
|
+
const issue = permissiveIssue(p, subtree);
|
|
235
|
+
if (issue !== null)
|
|
236
|
+
issues.push(issue);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return issues;
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=policy-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy-check.js","sourceRoot":"","sources":["../../src/rls/policy-check.ts"],"names":[],"mappings":"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"}
|
package/dist/rls/session.d.ts
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Which rows a tenant context can read.
|
|
4
|
+
*
|
|
5
|
+
* - "exact": the rows of the tenant only. This is the default.
|
|
6
|
+
* - "subtree": the rows of the tenant and of every descendant, through the
|
|
7
|
+
* tenant_subtree_read policies of migration 031. Writes stay limited to the
|
|
8
|
+
* exact tenant in both scopes, and so do reads of credential-bearing rows
|
|
9
|
+
* (api_keys, webhooks and sensitive config_entries).
|
|
10
|
+
*/
|
|
11
|
+
export type TenantScope = "exact" | "subtree";
|
|
12
|
+
export interface TenantContextOptions {
|
|
13
|
+
/** The read scope. Default "exact". */
|
|
14
|
+
scope?: TenantScope;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sets the tenant and the read scope for the current transaction.
|
|
18
|
+
*
|
|
19
|
+
* It always writes both settings, so a second call in the same transaction
|
|
20
|
+
* replaces an earlier "subtree" scope instead of keeping it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function setTenantContext(client: pg.PoolClient, tenantId: string, options?: TenantContextOptions): Promise<void>;
|
|
3
23
|
export declare function resetTenantContext(client: pg.PoolClient): Promise<void>;
|
|
4
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Runs `fn` in a transaction that is scoped to one tenant.
|
|
26
|
+
*
|
|
27
|
+
* @param options `{ scope: "subtree" }` also lets `fn` read the rows of every
|
|
28
|
+
* descendant of the tenant. Writes stay limited to the tenant.
|
|
29
|
+
* @throws Error when `options.scope` is not "exact" or "subtree".
|
|
30
|
+
*/
|
|
31
|
+
export declare function withTenantContext<T>(pool: pg.Pool, tenantId: string, fn: (client: pg.PoolClient) => Promise<T>, options?: TenantContextOptions): Promise<T>;
|
|
5
32
|
/**
|
|
6
33
|
* Runs `fn` inside a transaction with the RLS bypass flag set, so control-plane
|
|
7
34
|
* / system operations that legitimately span tenant boundaries (provisioning,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/rls/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,QAAQ,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/rls/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,SAAS,CAAC;AAE9C,MAAM,WAAW,oBAAoB;IACnC,uCAAuC;IACvC,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAaD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7E;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,EACzC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,CAAC,CAAC,CAgBZ;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAcZ;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,EAAE,CAAC,UAAU,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMxB"}
|
package/dist/rls/session.js
CHANGED
|
@@ -5,17 +5,40 @@ exports.resetTenantContext = resetTenantContext;
|
|
|
5
5
|
exports.withTenantContext = withTenantContext;
|
|
6
6
|
exports.withRlsBypass = withRlsBypass;
|
|
7
7
|
exports.getCurrentTenantId = getCurrentTenantId;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** The value of app.tenant_scope for a scope. Only 'subtree' widens reads. */
|
|
9
|
+
function scopeSetting(options) {
|
|
10
|
+
const scope = options.scope ?? "exact";
|
|
11
|
+
if (scope !== "exact" && scope !== "subtree") {
|
|
12
|
+
throw new Error(`[stratum] Unknown tenant scope: ${String(scope)} (expected "exact" or "subtree")`);
|
|
13
|
+
}
|
|
14
|
+
return scope === "subtree" ? "subtree" : "";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sets the tenant and the read scope for the current transaction.
|
|
18
|
+
*
|
|
19
|
+
* It always writes both settings, so a second call in the same transaction
|
|
20
|
+
* replaces an earlier "subtree" scope instead of keeping it.
|
|
21
|
+
*/
|
|
22
|
+
async function setTenantContext(client, tenantId, options = {}) {
|
|
23
|
+
await client.query("SELECT set_config('app.current_tenant_id', $1, true), set_config('app.tenant_scope', $2, true)", [tenantId, scopeSetting(options)]);
|
|
10
24
|
}
|
|
11
25
|
async function resetTenantContext(client) {
|
|
12
|
-
await client.query("SELECT set_config('app.current_tenant_id', '', true)");
|
|
26
|
+
await client.query("SELECT set_config('app.current_tenant_id', '', true), set_config('app.tenant_scope', '', true)");
|
|
13
27
|
}
|
|
14
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Runs `fn` in a transaction that is scoped to one tenant.
|
|
30
|
+
*
|
|
31
|
+
* @param options `{ scope: "subtree" }` also lets `fn` read the rows of every
|
|
32
|
+
* descendant of the tenant. Writes stay limited to the tenant.
|
|
33
|
+
* @throws Error when `options.scope` is not "exact" or "subtree".
|
|
34
|
+
*/
|
|
35
|
+
async function withTenantContext(pool, tenantId, fn, options = {}) {
|
|
36
|
+
// Fail on an unknown scope before a connection is taken.
|
|
37
|
+
scopeSetting(options);
|
|
15
38
|
const client = await pool.connect();
|
|
16
39
|
try {
|
|
17
40
|
await client.query("BEGIN");
|
|
18
|
-
await setTenantContext(client, tenantId);
|
|
41
|
+
await setTenantContext(client, tenantId, options);
|
|
19
42
|
const result = await fn(client);
|
|
20
43
|
await client.query("COMMIT");
|
|
21
44
|
return result;
|
package/dist/rls/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/rls/session.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/rls/session.ts"],"names":[],"mappings":";;AAmCA,4CASC;AAED,gDAIC;AASD,8CAqBC;AAeD,sCAiBC;AAED,gDAQC;AAxGD,8EAA8E;AAC9E,SAAS,YAAY,CAAC,OAA6B;IACjD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC;IACvC,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CACb,mCAAmC,MAAM,CAAC,KAAK,CAAC,kCAAkC,CACnF,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;AAC9C,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,gBAAgB,CACpC,MAAqB,EACrB,QAAgB,EAChB,UAAgC,EAAE;IAElC,MAAM,MAAM,CAAC,KAAK,CAChB,gGAAgG,EAChG,CAAC,QAAQ,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,CAClC,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,kBAAkB,CAAC,MAAqB;IAC5D,MAAM,MAAM,CAAC,KAAK,CAChB,gGAAgG,CACjG,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,iBAAiB,CACrC,IAAa,EACb,QAAgB,EAChB,EAAyC,EACzC,UAAgC,EAAE;IAElC,yDAAyD;IACzD,YAAY,CAAC,OAAO,CAAC,CAAC;IACtB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,gBAAgB,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC/B,MAAM,GAAG,CAAC;IACZ,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACI,KAAK,UAAU,aAAa,CACjC,IAAa,EACb,EAAyC;IAEzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,MAAM,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACtE,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC/B,MAAM,GAAG,CAAC;IACZ,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,kBAAkB,CACtC,MAAqB;IAErB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,0EAA0E,CAC3E,CAAC;IACF,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC;IAC3C,OAAO,KAAK,IAAI,IAAI,CAAC;AACvB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stratum-hq/db-adapters",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Multi-tenant PostgreSQL row-level security for pg, Prisma, Drizzle, and Sequelize, plus schema-per-tenant and database-per-tenant isolation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multi-tenancy",
|
|
7
|
+
"multitenancy",
|
|
7
8
|
"multi-tenant",
|
|
8
9
|
"saas",
|
|
9
10
|
"tenant",
|
|
10
11
|
"tenant-isolation",
|
|
11
|
-
"postgresql",
|
|
12
|
-
"postgres",
|
|
13
12
|
"row-level-security",
|
|
14
13
|
"rls",
|
|
14
|
+
"postgresql",
|
|
15
|
+
"prisma",
|
|
16
|
+
"drizzle",
|
|
17
|
+
"drizzle-orm",
|
|
18
|
+
"sequelize",
|
|
19
|
+
"pg",
|
|
20
|
+
"node-postgres",
|
|
21
|
+
"schema-per-tenant",
|
|
22
|
+
"database-per-tenant",
|
|
23
|
+
"postgres",
|
|
15
24
|
"schema-isolation",
|
|
16
25
|
"database",
|
|
17
26
|
"stratum"
|
|
@@ -22,6 +31,10 @@
|
|
|
22
31
|
".": {
|
|
23
32
|
"types": "./dist/index.d.ts",
|
|
24
33
|
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./pglite": {
|
|
36
|
+
"types": "./dist/pglite/index.d.ts",
|
|
37
|
+
"default": "./dist/pglite/index.js"
|
|
25
38
|
}
|
|
26
39
|
},
|
|
27
40
|
"files": [
|
|
@@ -41,18 +54,23 @@
|
|
|
41
54
|
"registry": "https://registry.npmjs.org/"
|
|
42
55
|
},
|
|
43
56
|
"dependencies": {
|
|
44
|
-
"@stratum-hq/core": "^1.
|
|
57
|
+
"@stratum-hq/core": "^1.5.1",
|
|
45
58
|
"pg": "^8.11.0"
|
|
46
59
|
},
|
|
47
60
|
"peerDependencies": {
|
|
61
|
+
"@electric-sql/pglite": "^0.4.2",
|
|
48
62
|
"drizzle-orm": ">=0.29.0"
|
|
49
63
|
},
|
|
50
64
|
"peerDependenciesMeta": {
|
|
65
|
+
"@electric-sql/pglite": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
51
68
|
"drizzle-orm": {
|
|
52
69
|
"optional": true
|
|
53
70
|
}
|
|
54
71
|
},
|
|
55
72
|
"devDependencies": {
|
|
73
|
+
"@electric-sql/pglite": "^0.4.6",
|
|
56
74
|
"@prisma/client": "^5.10.0",
|
|
57
75
|
"@types/pg": "^8.11.0",
|
|
58
76
|
"drizzle-orm": "^0.45.3",
|
|
@@ -61,7 +79,7 @@
|
|
|
61
79
|
},
|
|
62
80
|
"license": "MIT",
|
|
63
81
|
"author": "Christian Crank",
|
|
64
|
-
"homepage": "https://
|
|
82
|
+
"homepage": "https://docs.stratum-hq.org/packages/db-adapters/",
|
|
65
83
|
"bugs": "https://github.com/stratum-hq/Stratum/issues",
|
|
66
84
|
"engines": {
|
|
67
85
|
"node": ">=20.0.0"
|