@stratum-hq/lib 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +29 -2
  2. package/dist/crypto.d.ts.map +1 -1
  3. package/dist/crypto.js +50 -10
  4. package/dist/crypto.js.map +1 -1
  5. package/dist/index.d.ts +7 -3
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +17 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/migrate-schemas.d.ts +4 -0
  10. package/dist/migrate-schemas.d.ts.map +1 -1
  11. package/dist/migrate-schemas.js +16 -6
  12. package/dist/migrate-schemas.js.map +1 -1
  13. package/dist/migrate.d.ts +24 -0
  14. package/dist/migrate.d.ts.map +1 -1
  15. package/dist/migrate.js +44 -6
  16. package/dist/migrate.js.map +1 -1
  17. package/dist/migration-sql.d.ts +25 -0
  18. package/dist/migration-sql.d.ts.map +1 -0
  19. package/dist/migration-sql.js +64 -0
  20. package/dist/migration-sql.js.map +1 -0
  21. package/dist/migrations/029_tenant_parent_cycle_guard.sql +2 -2
  22. package/dist/migrations/031_subtree_read_scope.sql +6 -6
  23. package/dist/migrations/032_control_role.sql +809 -0
  24. package/dist/migrations/033_api_keys_tenant_index.sql +14 -0
  25. package/dist/pinned-query.d.ts +29 -0
  26. package/dist/pinned-query.d.ts.map +1 -0
  27. package/dist/pinned-query.js +60 -0
  28. package/dist/pinned-query.js.map +1 -0
  29. package/dist/pool-helpers.d.ts +2 -0
  30. package/dist/pool-helpers.d.ts.map +1 -1
  31. package/dist/pool-helpers.js +17 -3
  32. package/dist/pool-helpers.js.map +1 -1
  33. package/dist/role-model.d.ts +178 -0
  34. package/dist/role-model.d.ts.map +1 -0
  35. package/dist/role-model.js +670 -0
  36. package/dist/role-model.js.map +1 -0
  37. package/dist/services/api-key-service.d.ts +12 -2
  38. package/dist/services/api-key-service.d.ts.map +1 -1
  39. package/dist/services/api-key-service.js +33 -9
  40. package/dist/services/api-key-service.js.map +1 -1
  41. package/dist/services/config-service.d.ts +15 -6
  42. package/dist/services/config-service.d.ts.map +1 -1
  43. package/dist/services/config-service.js +96 -50
  44. package/dist/services/config-service.js.map +1 -1
  45. package/dist/services/tenant-service.d.ts.map +1 -1
  46. package/dist/services/tenant-service.js +4 -4
  47. package/dist/services/tenant-service.js.map +1 -1
  48. package/dist/stratum-policies.d.ts +51 -0
  49. package/dist/stratum-policies.d.ts.map +1 -0
  50. package/dist/stratum-policies.js +217 -0
  51. package/dist/stratum-policies.js.map +1 -0
  52. package/dist/stratum-tables.d.ts.map +1 -1
  53. package/dist/stratum-tables.js +1 -0
  54. package/dist/stratum-tables.js.map +1 -1
  55. package/dist/stratum.d.ts +52 -7
  56. package/dist/stratum.d.ts.map +1 -1
  57. package/dist/stratum.js +75 -14
  58. package/dist/stratum.js.map +1 -1
  59. package/package.json +5 -5
@@ -0,0 +1,14 @@
1
+ -- Migration 033: Index api_keys.tenant_id.
2
+ --
3
+ -- Listing, revoking and purging a tenant's API keys select by tenant_id, and
4
+ -- `stratum doctor` warns about every tenant_id column without an index. The
5
+ -- column is nullable (global keys), and a plain btree serves both.
6
+ --
7
+ -- Safe to re-run, and safe in every tenant schema of migrateAllSchemas: the
8
+ -- table name is unqualified, so the index is created next to the api_keys
9
+ -- table that the search_path resolves.
10
+ --
11
+ -- This statement blocks writes to api_keys until the index is built. On a
12
+ -- large table, an operator can build it first with CREATE INDEX CONCURRENTLY
13
+ -- and the same name and definition. IF NOT EXISTS then skips this statement.
14
+ CREATE INDEX IF NOT EXISTS idx_api_keys_tenant_id ON api_keys (tenant_id);
@@ -0,0 +1,29 @@
1
+ import type pg from "pg";
2
+ /**
3
+ * The search path of the catalog queries that Stratum runs as a privileged
4
+ * login: a superuser, the admin login of adminPool, or the login that runs
5
+ * the migrations. PostgreSQL never looks up functions or operators in
6
+ * pg_temp, so with this path every unqualified function, operator and type
7
+ * resolves in pg_catalog only, and nothing another role created in a schema
8
+ * of the caller's search path is chosen in place of a built-in. Queries that
9
+ * run under it name Stratum objects with their schema.
10
+ */
11
+ export declare const PINNED_SEARCH_PATH = "pg_catalog, pg_temp";
12
+ /**
13
+ * Runs `fn` in a transaction on one connection of `pool` whose search_path is
14
+ * {@link PINNED_SEARCH_PATH} for that transaction. It commits when `fn`
15
+ * resolves and rolls back when it throws.
16
+ */
17
+ export declare function withPinnedSearchPath<T>(pool: pg.Pool, fn: (client: pg.PoolClient) => Promise<T>): Promise<T>;
18
+ /** Runs one query with the search path pinned; see {@link withPinnedSearchPath}. */
19
+ export declare function pinnedQuery<R extends pg.QueryResultRow = pg.QueryResultRow>(pool: pg.Pool, text: string, values?: unknown[]): Promise<pg.QueryResult<R>>;
20
+ /**
21
+ * The schema in which the search path of `db` finds the table `table`
22
+ * (default tenants), or null when it finds none. Call it on the caller's
23
+ * search path, before pinning it. Its query names every function, operator
24
+ * and type with pg_catalog, so it resolves nothing else through that path.
25
+ */
26
+ export declare function schemaOfTable(db: pg.Pool | pg.PoolClient, table?: string): Promise<string | null>;
27
+ /** A SQL identifier, quoted. */
28
+ export declare function quoteIdentifier(name: string): string;
29
+ //# sourceMappingURL=pinned-query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinned-query.d.ts","sourceRoot":"","sources":["../src/pinned-query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAEzB;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,wBAAwB,CAAC;AAExD;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAclH;AAED,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,CAAC,SAAS,EAAE,CAAC,cAAc,GAAG,EAAE,CAAC,cAAc,EACzE,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAE5B;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,UAAU,EAAE,KAAK,SAAY,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQ1G;AAED,gCAAgC;AAChC,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD"}
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PINNED_SEARCH_PATH = void 0;
4
+ exports.withPinnedSearchPath = withPinnedSearchPath;
5
+ exports.pinnedQuery = pinnedQuery;
6
+ exports.schemaOfTable = schemaOfTable;
7
+ exports.quoteIdentifier = quoteIdentifier;
8
+ /**
9
+ * The search path of the catalog queries that Stratum runs as a privileged
10
+ * login: a superuser, the admin login of adminPool, or the login that runs
11
+ * the migrations. PostgreSQL never looks up functions or operators in
12
+ * pg_temp, so with this path every unqualified function, operator and type
13
+ * resolves in pg_catalog only, and nothing another role created in a schema
14
+ * of the caller's search path is chosen in place of a built-in. Queries that
15
+ * run under it name Stratum objects with their schema.
16
+ */
17
+ exports.PINNED_SEARCH_PATH = "pg_catalog, pg_temp";
18
+ /**
19
+ * Runs `fn` in a transaction on one connection of `pool` whose search_path is
20
+ * {@link PINNED_SEARCH_PATH} for that transaction. It commits when `fn`
21
+ * resolves and rolls back when it throws.
22
+ */
23
+ async function withPinnedSearchPath(pool, fn) {
24
+ const client = await pool.connect();
25
+ try {
26
+ await client.query("BEGIN");
27
+ await client.query(`SET LOCAL search_path = ${exports.PINNED_SEARCH_PATH}`);
28
+ const result = await fn(client);
29
+ await client.query("COMMIT");
30
+ return result;
31
+ }
32
+ catch (err) {
33
+ await client.query("ROLLBACK").catch(() => undefined);
34
+ throw err;
35
+ }
36
+ finally {
37
+ client.release();
38
+ }
39
+ }
40
+ /** Runs one query with the search path pinned; see {@link withPinnedSearchPath}. */
41
+ function pinnedQuery(pool, text, values) {
42
+ return withPinnedSearchPath(pool, (client) => client.query(text, values));
43
+ }
44
+ /**
45
+ * The schema in which the search path of `db` finds the table `table`
46
+ * (default tenants), or null when it finds none. Call it on the caller's
47
+ * search path, before pinning it. Its query names every function, operator
48
+ * and type with pg_catalog, so it resolves nothing else through that path.
49
+ */
50
+ async function schemaOfTable(db, table = "tenants") {
51
+ const res = await db.query(`SELECT n.nspname::pg_catalog.text AS nsp
52
+ FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid OPERATOR(pg_catalog.=) c.relnamespace
53
+ WHERE c.oid OPERATOR(pg_catalog.=) pg_catalog.to_regclass($1::pg_catalog.text)::pg_catalog.oid`, [table]);
54
+ return res.rows[0]?.nsp ?? null;
55
+ }
56
+ /** A SQL identifier, quoted. */
57
+ function quoteIdentifier(name) {
58
+ return `"${name.replace(/"/g, '""')}"`;
59
+ }
60
+ //# sourceMappingURL=pinned-query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinned-query.js","sourceRoot":"","sources":["../src/pinned-query.ts"],"names":[],"mappings":";;;AAkBA,oDAcC;AAGD,kCAMC;AAQD,sCAQC;AAGD,0CAEC;AA5DD;;;;;;;;GAQG;AACU,QAAA,kBAAkB,GAAG,qBAAqB,CAAC;AAExD;;;;GAIG;AACI,KAAK,UAAU,oBAAoB,CAAI,IAAa,EAAE,EAAyC;IACpG,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,2BAA2B,0BAAkB,EAAE,CAAC,CAAC;QACpE,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,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACtD,MAAM,GAAG,CAAC;IACZ,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED,oFAAoF;AACpF,SAAgB,WAAW,CACzB,IAAa,EACb,IAAY,EACZ,MAAkB;IAElB,OAAO,oBAAoB,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAI,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,aAAa,CAAC,EAA2B,EAAE,KAAK,GAAG,SAAS;IAChF,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,KAAK,CACxB;;qGAEiG,EACjG,CAAC,KAAK,CAAC,CACR,CAAC;IACF,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,IAAI,CAAC;AAClC,CAAC;AAED,gCAAgC;AAChC,SAAgB,eAAe,CAAC,IAAY;IAC1C,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC;AACzC,CAAC"}
@@ -1,4 +1,6 @@
1
1
  import pg from "pg";
2
+ /** Marks `pool` as an admin pool: the helpers do not set app.bypass_rls on it. */
3
+ export declare function markAdminPool(pool: pg.Pool): void;
2
4
  export declare function withClient<T>(pool: pg.Pool, fn: (client: pg.PoolClient) => Promise<T>): Promise<T>;
3
5
  export declare function withTransaction<T>(pool: pg.Pool, fn: (client: pg.PoolClient) => Promise<T>): Promise<T>;
4
6
  //# sourceMappingURL=pool-helpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pool-helpers.d.ts","sourceRoot":"","sources":["../src/pool-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAuCpB,wBAAsB,UAAU,CAAC,CAAC,EAChC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAeZ;AAED,wBAAsB,eAAe,CAAC,CAAC,EACrC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAeZ"}
1
+ {"version":3,"file":"pool-helpers.d.ts","sourceRoot":"","sources":["../src/pool-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AA0BpB,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,IAAI,CAEjD;AAwBD,wBAAsB,UAAU,CAAC,CAAC,EAChC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAeZ;AAED,wBAAsB,eAAe,CAAC,CAAC,EACrC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAeZ"}
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.markAdminPool = markAdminPool;
3
4
  exports.withClient = withClient;
4
5
  exports.withTransaction = withTransaction;
5
6
  // The Stratum library is the trusted CONTROL PLANE: it manages the whole tenant
@@ -17,7 +18,20 @@ exports.withTransaction = withTransaction;
17
18
  //
18
19
  // This is the single chokepoint for all lib database access: every service goes
19
20
  // through withClient / withTransaction, so no service function needs to change.
20
- async function enterBypass(client) {
21
+ //
22
+ // A pool given to Stratum as `adminPool` logs in as a member of the control
23
+ // role (migration 032), whose stratum_control_plane policies admit every row.
24
+ // Such a pool needs no bypass setting, so the helpers do not set it there. A
25
+ // misconfigured admin login then fails closed instead of falling back on the
26
+ // legacy setting.
27
+ const adminPools = new WeakSet();
28
+ /** Marks `pool` as an admin pool: the helpers do not set app.bypass_rls on it. */
29
+ function markAdminPool(pool) {
30
+ adminPools.add(pool);
31
+ }
32
+ async function enterBypass(pool, client) {
33
+ if (adminPools.has(pool))
34
+ return;
21
35
  // SET LOCAL (not session SET) so the flag is transaction scoped and cannot
22
36
  // leak across pooled connections. Equivalent to
23
37
  // set_config('app.bypass_rls', 'on', true).
@@ -42,7 +56,7 @@ async function withClient(pool, fn) {
42
56
  let rollbackErr;
43
57
  try {
44
58
  await client.query("BEGIN");
45
- await enterBypass(client);
59
+ await enterBypass(pool, client);
46
60
  const result = await fn(client);
47
61
  await client.query("COMMIT");
48
62
  return result;
@@ -60,7 +74,7 @@ async function withTransaction(pool, fn) {
60
74
  let rollbackErr;
61
75
  try {
62
76
  await client.query("BEGIN");
63
- await enterBypass(client);
77
+ await enterBypass(pool, client);
64
78
  const result = await fn(client);
65
79
  await client.query("COMMIT");
66
80
  return result;
@@ -1 +1 @@
1
- {"version":3,"file":"pool-helpers.js","sourceRoot":"","sources":["../src/pool-helpers.ts"],"names":[],"mappings":";;AAuCA,gCAkBC;AAED,0CAkBC;AA3ED,gFAAgF;AAChF,4EAA4E;AAC5E,8EAA8E;AAC9E,iFAAiF;AACjF,wBAAwB;AACxB,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,+EAA+E;AAC/E,gFAAgF;AAChF,iEAAiE;AACjE,kDAAkD;AAClD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAEhF,KAAK,UAAU,WAAW,CAAC,MAAqB;IAC9C,2EAA2E;IAC3E,gDAAgD;IAChD,4CAA4C;IAC5C,MAAM,MAAM,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;AACxD,CAAC;AAED,2EAA2E;AAC3E,4EAA4E;AAC5E,yEAAyE;AACzE,6EAA6E;AAC7E,6BAA6B;AAC7B,KAAK,UAAU,QAAQ,CAAC,MAAqB;IAC3C,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC/B,OAAO,SAAS,CAAC;IACnB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7D,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,UAAU,CAC9B,IAAa,EACb,EAAyC;IAEzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,IAAI,WAA8B,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;QAC1B,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,WAAW,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,GAAG,CAAC;IACZ,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,eAAe,CACnC,IAAa,EACb,EAAyC;IAEzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,IAAI,WAA8B,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;QAC1B,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,WAAW,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,GAAG,CAAC;IACZ,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"pool-helpers.js","sourceRoot":"","sources":["../src/pool-helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,178 @@
1
+ import type pg from "pg";
2
+ import type { StratumLogger } from "./logger.js";
3
+ /**
4
+ * The role model of migration 032.
5
+ *
6
+ * - The control role (default stratum_control) is NOLOGIN. Every Stratum
7
+ * table has a stratum_control_plane policy for it, and it owns the
8
+ * SECURITY DEFINER helpers.
9
+ * - The admin login backs the library's adminPool. It is a member of the
10
+ * control role (INHERIT), owns the Stratum objects and runs the migrations.
11
+ * It needs neither SUPERUSER nor BYPASSRLS.
12
+ * - The application login is not a member of the control role, owns nothing
13
+ * of Stratum's, and may read only the tables in APP_READ_TABLES.
14
+ */
15
+ /** The Stratum tables the application role may read; it may write none. */
16
+ export declare const APP_READ_TABLES: readonly string[];
17
+ /**
18
+ * The md5 of the body (pg_proc.prosrc) of each Stratum function that
19
+ * stratum_apply_control_role() does not re-create, as the migrations define
20
+ * it last. A unit test derives them from the migration files.
21
+ */
22
+ export declare const STRATUM_FUNCTION_BODY_MD5: Readonly<Record<string, string>>;
23
+ export interface BootstrapRolesOptions {
24
+ /** The admin login to make a member of the control role. Omit to skip that grant. */
25
+ adminRole?: string;
26
+ /** The application login to limit to the recommended grants. Omit to skip that section. */
27
+ appRole?: string;
28
+ /** The control role. Default `stratum_control`. */
29
+ controlRole?: string;
30
+ /** The schema of the Stratum tables. Default `public`. */
31
+ schema?: string;
32
+ }
33
+ /**
34
+ * The SQL a database administrator runs to set up the role model, as a
35
+ * superuser, or a role with CREATEROLE that owns the Stratum tables. It is
36
+ * idempotent.
37
+ *
38
+ * - Creates the NOLOGIN control role when it does not exist, and lets it use
39
+ * and create objects in the schema (it owns the 032 helpers).
40
+ * - With `adminRole`: makes the admin login a member of the control role
41
+ * (WITH INHERIT TRUE, SET TRUE on PostgreSQL 16 and later), and moves the
42
+ * Stratum tables and functions that `appRole` owns to it.
43
+ * - Applies the control role to the Stratum objects through
44
+ * stratum_apply_control_role() of migration 032, when it exists. This is
45
+ * what activates the hardening when the migration could not (its migrating
46
+ * role could neither create nor join the control role). It needs a
47
+ * superuser or the owner of the Stratum tables.
48
+ * - Revokes CREATE on the schema from PUBLIC, and with `appRole` from the
49
+ * application login, so that only roles granted CREATE by name can add
50
+ * objects to the schema of the Stratum tables.
51
+ * - With `appRole`: removes the application login from the control role,
52
+ * revokes its privileges on every Stratum table, and grants it SELECT on
53
+ * the read-list tables only. This part acts on the tables that exist, so
54
+ * run it again after a migration that adds a table.
55
+ *
56
+ * The login roles themselves, with their passwords, are yours to create.
57
+ *
58
+ * @throws Error when a name is not a plain lowercase identifier.
59
+ */
60
+ export declare function bootstrapRolesSql(options?: BootstrapRolesOptions): string;
61
+ /**
62
+ * The PL/pgSQL statements of the integrity check: they collect, in
63
+ * v_problems (text[]), the objects of the schema v_ns (oid) that the Stratum
64
+ * migrations did not put there. Whoever owned the Stratum tables before
65
+ * (often the application login) could have attached them, and they would run
66
+ * with the rights of whoever writes to the tables later: the admin login, the
67
+ * migrating role, or a superuser. They check, in that schema:
68
+ *
69
+ * - every Stratum relation (STRATUM_TABLES, _migrations included) is a table;
70
+ * - no rule is attached to one of them;
71
+ * - every trigger calls a Stratum function;
72
+ * - policies, column defaults, constraints, triggers and indexes of them
73
+ * call only functions and operators of pg_catalog, of an extension, or of
74
+ * Stratum;
75
+ * - every column has a type of pg_catalog or of an extension;
76
+ * - the schema holds no operator, and no function or aggregate named like a
77
+ * function of pg_catalog or of an extension, other than those of
78
+ * extensions and of Stratum. Such an object can be chosen in place of the
79
+ * built-in one by a query that has the schema on its search path;
80
+ * - the Stratum functions that stratum_apply_control_role() does not
81
+ * re-create have the bodies the migrations give them, and
82
+ * stratum_apply_control_role() itself is SECURITY INVOKER with its pinned
83
+ * search_path.
84
+ *
85
+ * They expect the variables v_ns oid, v_tables text[], v_functions text[],
86
+ * v_problems text[] and r record, and run with search_path = pg_catalog,
87
+ * pg_temp. bootstrapRolesSql() raises when they find anything; migration 032
88
+ * renders the same statements (a unit test keeps them identical) and then
89
+ * warns and leaves the control role unapplied.
90
+ */
91
+ export declare function integrityChecksPlpgsql(): string;
92
+ /** The login of `pool`. */
93
+ export declare function currentLogin(pool: pg.Pool): Promise<string>;
94
+ export interface RoleModelCheckOptions {
95
+ /** The library's admin pool, or undefined in the legacy single-pool mode. */
96
+ adminPool?: pg.Pool;
97
+ appPool: pg.Pool;
98
+ /** The configured control role, or undefined to use the database's. */
99
+ controlRole?: string;
100
+ /** Throw, instead of warn, when the application role is misconfigured. */
101
+ strict: boolean;
102
+ logger: StratumLogger;
103
+ }
104
+ /**
105
+ * Reports whether the control-role hardening of migration 032 is active and,
106
+ * with an adminPool, checks the admin and application logins against the
107
+ * role model, warning about each problem. With `strict`, a misconfigured
108
+ * application login throws instead. In every mode it reports an
109
+ * application login that can create objects in the schema of the Stratum
110
+ * tables; with an adminPool and `strict` that is an error too. With an
111
+ * adminPool it also warns when a schema the application login can create
112
+ * would come first on the admin login's search path.
113
+ */
114
+ export declare function checkRoleModel(options: RoleModelCheckOptions): Promise<void>;
115
+ /** What {@link inspectRoleModel} found. */
116
+ export interface RoleModelReport {
117
+ /** Whether migration 032 ran (the stratum_security table exists). */
118
+ migrated: boolean;
119
+ /** Whether the control role is applied: stratum_control_plane policies exist. */
120
+ hardeningActive: boolean;
121
+ /** The control role checked against: the option, else the database's, else the default. */
122
+ controlRole: string;
123
+ /** Problems with the admin login, or null when no adminPool was given. */
124
+ adminIssues: string[] | null;
125
+ /** Problems with the application login, or null when no appPool was given. */
126
+ appIssues: string[] | null;
127
+ /**
128
+ * Whether the legacy app.bypass_rls switch is on. Null when it cannot be
129
+ * read: before 032, without an adminPool, or when the admin login cannot
130
+ * see the stratum_security row.
131
+ */
132
+ legacyBypass: boolean | null;
133
+ /**
134
+ * The roles, other than superusers, that are members of the control role,
135
+ * directly or through another role. Only the library's admin login (and
136
+ * roles you chose to give the control plane's rights) should be listed.
137
+ */
138
+ controlMembers: {
139
+ role: string;
140
+ login: boolean;
141
+ }[];
142
+ /** The admin login that was checked, or null when none was given. */
143
+ adminLogin: string | null;
144
+ /**
145
+ * Why a schema the application login can create would come first on the
146
+ * admin login's search path ("$user"), or null when it would not or when
147
+ * either login was not given.
148
+ */
149
+ searchPathIssue: string | null;
150
+ }
151
+ export interface InspectRoleModelOptions {
152
+ /** A pool that logs in as the application login, which is checked. */
153
+ appPool?: pg.Pool;
154
+ /** A pool that logs in as the admin login, which is checked. */
155
+ adminPool?: pg.Pool;
156
+ /**
157
+ * A pool to read the catalog through when the logins are given by name
158
+ * (`appRole`, `adminRole`), for example a superuser's.
159
+ */
160
+ pool?: pg.Pool;
161
+ /** Check this application role by name, through `pool`, instead of the login of `appPool`. */
162
+ appRole?: string;
163
+ /** Check this admin role by name, through `pool`, instead of the login of `adminPool`. */
164
+ adminRole?: string;
165
+ /** The control role. Default: the database's, else `stratum_control`. */
166
+ controlRole?: string;
167
+ }
168
+ /**
169
+ * Checks the logins of a database against the role model of migration 032,
170
+ * without logging or throwing. `stratum health`, `stratum doctor` and
171
+ * `stratum db roles` use it; Stratum.initialize() applies the same checks.
172
+ */
173
+ export declare function inspectRoleModel(options: InspectRoleModelOptions): Promise<RoleModelReport>;
174
+ /** Warns once per process that Stratum runs without an adminPool. */
175
+ export declare function warnNoAdminPool(logger: StratumLogger): void;
176
+ /** Warns once per process that an API key with a legacy SHA-256 hash authenticated. */
177
+ export declare function warnLegacyKeyHash(logger: StratumLogger): void;
178
+ //# sourceMappingURL=role-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-model.d.ts","sourceRoot":"","sources":["../src/role-model.ts"],"names":[],"mappings":"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"}