@blamejs/core 0.7.4 → 0.7.18
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/CHANGELOG.md +423 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -319
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
|
@@ -1,267 +1,267 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* b.db.declareRowPolicy — declarative Postgres ROW LEVEL SECURITY policy
|
|
4
|
-
* migration spec.
|
|
5
|
-
*
|
|
6
|
-
* Returns a migration-shape object that b.externalDb.migrate(...) applies
|
|
7
|
-
* against a Postgres backend. Generates:
|
|
8
|
-
*
|
|
9
|
-
* ALTER TABLE <schema>.<table> ENABLE ROW LEVEL SECURITY; -- idempotent
|
|
10
|
-
* CREATE POLICY <name> ON <schema>.<table>
|
|
11
|
-
* [AS PERMISSIVE | RESTRICTIVE]
|
|
12
|
-
* FOR <command>
|
|
13
|
-
* [TO <role>]
|
|
14
|
-
* USING (<expr>)
|
|
15
|
-
* [WITH CHECK (<expr>)];
|
|
16
|
-
*
|
|
17
|
-
* Pairs with b.externalDb.transaction({ sessionGucs: { 'app.tenant_id': uuid } })
|
|
18
|
-
* for the per-request `SET LOCAL` plumbing. The recommended tenant-per-row
|
|
19
|
-
* shape:
|
|
20
|
-
*
|
|
21
|
-
* b.db.declareRowPolicy({
|
|
22
|
-
* schema: "public",
|
|
23
|
-
* table: "sessions",
|
|
24
|
-
* name: "tenant_isolation",
|
|
25
|
-
* role: "app_user",
|
|
26
|
-
* using: "tenant_id = current_setting('app.tenant_id')::uuid",
|
|
27
|
-
* withCheck: "tenant_id = current_setting('app.tenant_id')::uuid",
|
|
28
|
-
* command: "ALL",
|
|
29
|
-
* });
|
|
30
|
-
*
|
|
31
|
-
* await b.externalDb.transaction(async function (tx) {
|
|
32
|
-
* return await tx.query("SELECT * FROM sessions WHERE _id = $1", [sid]);
|
|
33
|
-
* }, { sessionGucs: { "app.tenant_id": req.user.tenantId } });
|
|
34
|
-
*
|
|
35
|
-
* Postgres-only: SQLite + MySQL have no equivalent grammar. Apply throws
|
|
36
|
-
* NOT_SUPPORTED at migration-apply time when the targeted backend's
|
|
37
|
-
* dialect isn't "postgres".
|
|
38
|
-
*
|
|
39
|
-
* Validation at declareRowPolicy() call time — bad shape throws here, not
|
|
40
|
-
* at apply time:
|
|
41
|
-
* - schema, table, name, role → safeSql.validateIdentifier
|
|
42
|
-
* - command ∈ {ALL, SELECT, INSERT, UPDATE, DELETE}
|
|
43
|
-
* - permissive boolean
|
|
44
|
-
* - using / withCheck operator-supplied SQL strings; semicolons rejected
|
|
45
|
-
*
|
|
46
|
-
* Audit metadata emitted on apply:
|
|
47
|
-
* {
|
|
48
|
-
* policy: "schema.table.name",
|
|
49
|
-
* table: "schema.table",
|
|
50
|
-
* role: "...",
|
|
51
|
-
* command: "ALL"|...,
|
|
52
|
-
* permissive: true|false,
|
|
53
|
-
* hasWithCheck: bool,
|
|
54
|
-
* }
|
|
55
|
-
*/
|
|
56
|
-
var safeSql = require("./safe-sql");
|
|
57
|
-
var validateOpts = require("./validate-opts");
|
|
58
|
-
var { defineClass } = require("./framework-error");
|
|
59
|
-
|
|
60
|
-
var DeclareRowPolicyError = defineClass("DeclareRowPolicyError", { alwaysPermanent: true });
|
|
61
|
-
|
|
62
|
-
var ALLOWED_OPTS = [
|
|
63
|
-
"schema", "table", "name", "role",
|
|
64
|
-
"using", "withCheck", "command", "permissive", "backend",
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
var ALLOWED_COMMANDS = ["ALL", "SELECT", "INSERT", "UPDATE", "DELETE"];
|
|
68
|
-
|
|
69
|
-
function _err(code, message) {
|
|
70
|
-
return new DeclareRowPolicyError(code, message);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function _validateIdent(where, value) {
|
|
74
|
-
try {
|
|
75
|
-
safeSql.validateIdentifier(value, { allowReserved: true });
|
|
76
|
-
} catch (e) {
|
|
77
|
-
throw _err("declare-row-policy/bad-identifier",
|
|
78
|
-
where + ": invalid identifier '" + value + "': " + ((e && e.message) || String(e)));
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function _validateExpression(where, value) {
|
|
83
|
-
if (typeof value !== "string") {
|
|
84
|
-
throw _err("declare-row-policy/bad-type", where + " must be a string");
|
|
85
|
-
}
|
|
86
|
-
if (value.length === 0) {
|
|
87
|
-
throw _err("declare-row-policy/empty-expression", where + " must be a non-empty boolean expression");
|
|
88
|
-
}
|
|
89
|
-
if (value.indexOf(";") !== -1) {
|
|
90
|
-
throw _err("declare-row-policy/bad-expression",
|
|
91
|
-
where + " must not contain ';' — use a single boolean expression");
|
|
92
|
-
}
|
|
93
|
-
return value;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function _validateOpts(opts) {
|
|
97
|
-
if (!opts || typeof opts !== "object") {
|
|
98
|
-
throw _err("declare-row-policy/bad-opts", "declareRowPolicy requires an opts object");
|
|
99
|
-
}
|
|
100
|
-
for (var k in opts) {
|
|
101
|
-
if (Object.prototype.hasOwnProperty.call(opts, k) && ALLOWED_OPTS.indexOf(k) === -1) {
|
|
102
|
-
throw _err("declare-row-policy/unknown-opt",
|
|
103
|
-
"unknown opt '" + k + "'. Allowed: " + ALLOWED_OPTS.join(", "));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
validateOpts.requireNonEmptyString(opts.schema, "schema", DeclareRowPolicyError, "declare-row-policy/missing-opt");
|
|
108
|
-
_validateIdent("schema", opts.schema);
|
|
109
|
-
|
|
110
|
-
validateOpts.requireNonEmptyString(opts.table, "table", DeclareRowPolicyError, "declare-row-policy/missing-opt");
|
|
111
|
-
_validateIdent("table", opts.table);
|
|
112
|
-
|
|
113
|
-
validateOpts.requireNonEmptyString(opts.name, "name", DeclareRowPolicyError, "declare-row-policy/missing-opt");
|
|
114
|
-
_validateIdent("name", opts.name);
|
|
115
|
-
|
|
116
|
-
var role = null;
|
|
117
|
-
if (opts.role !== undefined && opts.role !== null) {
|
|
118
|
-
if (typeof opts.role !== "string" || opts.role.length === 0) {
|
|
119
|
-
throw _err("declare-row-policy/bad-type", "role must be a non-empty string");
|
|
120
|
-
}
|
|
121
|
-
_validateIdent("role", opts.role);
|
|
122
|
-
role = opts.role;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (opts.using === undefined || opts.using === null) {
|
|
126
|
-
throw _err("declare-row-policy/missing-opt", "using is required (USING expression)");
|
|
127
|
-
}
|
|
128
|
-
var using = _validateExpression("using", opts.using);
|
|
129
|
-
|
|
130
|
-
var withCheck = null;
|
|
131
|
-
if (opts.withCheck !== undefined && opts.withCheck !== null) {
|
|
132
|
-
withCheck = _validateExpression("withCheck", opts.withCheck);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
var command = "ALL";
|
|
136
|
-
if (opts.command !== undefined && opts.command !== null) {
|
|
137
|
-
if (typeof opts.command !== "string") {
|
|
138
|
-
throw _err("declare-row-policy/bad-type", "command must be a string");
|
|
139
|
-
}
|
|
140
|
-
var upper = opts.command.toUpperCase();
|
|
141
|
-
if (ALLOWED_COMMANDS.indexOf(upper) === -1) {
|
|
142
|
-
throw _err("declare-row-policy/bad-command",
|
|
143
|
-
"command must be one of " + ALLOWED_COMMANDS.join(", ") + ", got '" + opts.command + "'");
|
|
144
|
-
}
|
|
145
|
-
command = upper;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
var permissive = true;
|
|
149
|
-
if (opts.permissive !== undefined && opts.permissive !== null) {
|
|
150
|
-
if (typeof opts.permissive !== "boolean") {
|
|
151
|
-
throw _err("declare-row-policy/bad-type", "permissive must be a boolean");
|
|
152
|
-
}
|
|
153
|
-
permissive = opts.permissive;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (opts.backend !== undefined && opts.backend !== null) {
|
|
157
|
-
if (typeof opts.backend !== "string" || opts.backend.length === 0) {
|
|
158
|
-
throw _err("declare-row-policy/bad-type", "backend must be a non-empty string");
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
schema: opts.schema,
|
|
164
|
-
table: opts.table,
|
|
165
|
-
name: opts.name,
|
|
166
|
-
role: role,
|
|
167
|
-
using: using,
|
|
168
|
-
withCheck: withCheck,
|
|
169
|
-
command: command,
|
|
170
|
-
permissive: permissive,
|
|
171
|
-
backend: opts.backend || null,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function _ensureBackendIsPostgres(externalDb, backendName) {
|
|
176
|
-
var list = externalDb.listBackends();
|
|
177
|
-
var found = null;
|
|
178
|
-
for (var i = 0; i < list.length; i++) {
|
|
179
|
-
if (list[i].name === backendName) { found = list[i]; break; }
|
|
180
|
-
}
|
|
181
|
-
if (!found) {
|
|
182
|
-
throw _err("declare-row-policy/unknown-backend",
|
|
183
|
-
"no externalDb backend named '" + backendName + "' — declared backends: " +
|
|
184
|
-
list.map(function (b) { return b.name; }).join(", "));
|
|
185
|
-
}
|
|
186
|
-
if (found.dialect !== "postgres") {
|
|
187
|
-
throw _err("declare-row-policy/not-supported",
|
|
188
|
-
"declareRowPolicy is Postgres-only; backend '" + backendName + "' has dialect='" +
|
|
189
|
-
found.dialect + "'. Write the policy as a hand-rolled migration for this dialect.");
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function declareRowPolicy(opts) {
|
|
194
|
-
var spec = _validateOpts(opts);
|
|
195
|
-
var qTable = safeSql.quoteQualified([spec.schema, spec.table], "postgres");
|
|
196
|
-
var qPolicy = safeSql.quoteIdentifier(spec.name, "postgres");
|
|
197
|
-
var qRole = spec.role ? safeSql.quoteIdentifier(spec.role, "postgres") : null;
|
|
198
|
-
|
|
199
|
-
var description = "declareRowPolicy " + spec.schema + "." + spec.table + "." + spec.name;
|
|
200
|
-
|
|
201
|
-
async function up(xdb, ctx) {
|
|
202
|
-
if (ctx && ctx.externalDb && ctx.backendName) {
|
|
203
|
-
_ensureBackendIsPostgres(ctx.externalDb, ctx.backendName);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Idempotent ENABLE — Postgres has no IF NOT EXISTS for this. Read
|
|
207
|
-
// the current setting from pg_class and skip the ALTER when already
|
|
208
|
-
// on, so re-running a migration set in a partially-applied state
|
|
209
|
-
// doesn't fail with a no-op error from the lock acquisition.
|
|
210
|
-
var rlsCheck = await xdb.query(
|
|
211
|
-
"SELECT relrowsecurity FROM pg_class c " +
|
|
212
|
-
"JOIN pg_namespace n ON n.oid = c.relnamespace " +
|
|
213
|
-
"WHERE n.nspname = $1 AND c.relname = $2",
|
|
214
|
-
[spec.schema, spec.table]
|
|
215
|
-
);
|
|
216
|
-
var rows = (rlsCheck && rlsCheck.rows) || [];
|
|
217
|
-
if (rows.length === 0) {
|
|
218
|
-
throw _err("declare-row-policy/table-not-found",
|
|
219
|
-
"source table '" + spec.schema + "." + spec.table +
|
|
220
|
-
"' not found (does it exist? does the migration role have visibility?)");
|
|
221
|
-
}
|
|
222
|
-
if (!rows[0].relrowsecurity) {
|
|
223
|
-
await xdb.query("ALTER TABLE " + qTable + " ENABLE ROW LEVEL SECURITY", []);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// CREATE POLICY assembled in canonical order: name → table → AS
|
|
227
|
-
// PERMISSIVE/RESTRICTIVE → FOR command → TO role → USING → WITH CHECK.
|
|
228
|
-
var sql = "CREATE POLICY " + qPolicy + " ON " + qTable;
|
|
229
|
-
sql += " AS " + (spec.permissive ? "PERMISSIVE" : "RESTRICTIVE");
|
|
230
|
-
sql += " FOR " + spec.command;
|
|
231
|
-
if (qRole) sql += " TO " + qRole;
|
|
232
|
-
sql += " USING (" + spec.using + ")";
|
|
233
|
-
if (spec.withCheck) sql += " WITH CHECK (" + spec.withCheck + ")";
|
|
234
|
-
|
|
235
|
-
await xdb.query(sql, []);
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
policy: spec.schema + "." + spec.table + "." + spec.name,
|
|
239
|
-
table: spec.schema + "." + spec.table,
|
|
240
|
-
role: spec.role,
|
|
241
|
-
command: spec.command,
|
|
242
|
-
permissive: spec.permissive,
|
|
243
|
-
hasWithCheck: !!spec.withCheck,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async function down(xdb, ctx) {
|
|
248
|
-
if (ctx && ctx.externalDb && ctx.backendName) {
|
|
249
|
-
_ensureBackendIsPostgres(ctx.externalDb, ctx.backendName);
|
|
250
|
-
}
|
|
251
|
-
await xdb.query("DROP POLICY IF EXISTS " + qPolicy + " ON " + qTable, []);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
description: description,
|
|
256
|
-
target: "externalDb",
|
|
257
|
-
backend: spec.backend,
|
|
258
|
-
up: up,
|
|
259
|
-
down: down,
|
|
260
|
-
_spec: spec,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
module.exports = {
|
|
265
|
-
declareRowPolicy: declareRowPolicy,
|
|
266
|
-
DeclareRowPolicyError: DeclareRowPolicyError,
|
|
267
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.db.declareRowPolicy — declarative Postgres ROW LEVEL SECURITY policy
|
|
4
|
+
* migration spec.
|
|
5
|
+
*
|
|
6
|
+
* Returns a migration-shape object that b.externalDb.migrate(...) applies
|
|
7
|
+
* against a Postgres backend. Generates:
|
|
8
|
+
*
|
|
9
|
+
* ALTER TABLE <schema>.<table> ENABLE ROW LEVEL SECURITY; -- idempotent
|
|
10
|
+
* CREATE POLICY <name> ON <schema>.<table>
|
|
11
|
+
* [AS PERMISSIVE | RESTRICTIVE]
|
|
12
|
+
* FOR <command>
|
|
13
|
+
* [TO <role>]
|
|
14
|
+
* USING (<expr>)
|
|
15
|
+
* [WITH CHECK (<expr>)];
|
|
16
|
+
*
|
|
17
|
+
* Pairs with b.externalDb.transaction({ sessionGucs: { 'app.tenant_id': uuid } })
|
|
18
|
+
* for the per-request `SET LOCAL` plumbing. The recommended tenant-per-row
|
|
19
|
+
* shape:
|
|
20
|
+
*
|
|
21
|
+
* b.db.declareRowPolicy({
|
|
22
|
+
* schema: "public",
|
|
23
|
+
* table: "sessions",
|
|
24
|
+
* name: "tenant_isolation",
|
|
25
|
+
* role: "app_user",
|
|
26
|
+
* using: "tenant_id = current_setting('app.tenant_id')::uuid",
|
|
27
|
+
* withCheck: "tenant_id = current_setting('app.tenant_id')::uuid",
|
|
28
|
+
* command: "ALL",
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* await b.externalDb.transaction(async function (tx) {
|
|
32
|
+
* return await tx.query("SELECT * FROM sessions WHERE _id = $1", [sid]);
|
|
33
|
+
* }, { sessionGucs: { "app.tenant_id": req.user.tenantId } });
|
|
34
|
+
*
|
|
35
|
+
* Postgres-only: SQLite + MySQL have no equivalent grammar. Apply throws
|
|
36
|
+
* NOT_SUPPORTED at migration-apply time when the targeted backend's
|
|
37
|
+
* dialect isn't "postgres".
|
|
38
|
+
*
|
|
39
|
+
* Validation at declareRowPolicy() call time — bad shape throws here, not
|
|
40
|
+
* at apply time:
|
|
41
|
+
* - schema, table, name, role → safeSql.validateIdentifier
|
|
42
|
+
* - command ∈ {ALL, SELECT, INSERT, UPDATE, DELETE}
|
|
43
|
+
* - permissive boolean
|
|
44
|
+
* - using / withCheck operator-supplied SQL strings; semicolons rejected
|
|
45
|
+
*
|
|
46
|
+
* Audit metadata emitted on apply:
|
|
47
|
+
* {
|
|
48
|
+
* policy: "schema.table.name",
|
|
49
|
+
* table: "schema.table",
|
|
50
|
+
* role: "...",
|
|
51
|
+
* command: "ALL"|...,
|
|
52
|
+
* permissive: true|false,
|
|
53
|
+
* hasWithCheck: bool,
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
56
|
+
var safeSql = require("./safe-sql");
|
|
57
|
+
var validateOpts = require("./validate-opts");
|
|
58
|
+
var { defineClass } = require("./framework-error");
|
|
59
|
+
|
|
60
|
+
var DeclareRowPolicyError = defineClass("DeclareRowPolicyError", { alwaysPermanent: true });
|
|
61
|
+
|
|
62
|
+
var ALLOWED_OPTS = [
|
|
63
|
+
"schema", "table", "name", "role",
|
|
64
|
+
"using", "withCheck", "command", "permissive", "backend",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
var ALLOWED_COMMANDS = ["ALL", "SELECT", "INSERT", "UPDATE", "DELETE"];
|
|
68
|
+
|
|
69
|
+
function _err(code, message) {
|
|
70
|
+
return new DeclareRowPolicyError(code, message);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _validateIdent(where, value) {
|
|
74
|
+
try {
|
|
75
|
+
safeSql.validateIdentifier(value, { allowReserved: true });
|
|
76
|
+
} catch (e) {
|
|
77
|
+
throw _err("declare-row-policy/bad-identifier",
|
|
78
|
+
where + ": invalid identifier '" + value + "': " + ((e && e.message) || String(e)));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _validateExpression(where, value) {
|
|
83
|
+
if (typeof value !== "string") {
|
|
84
|
+
throw _err("declare-row-policy/bad-type", where + " must be a string");
|
|
85
|
+
}
|
|
86
|
+
if (value.length === 0) {
|
|
87
|
+
throw _err("declare-row-policy/empty-expression", where + " must be a non-empty boolean expression");
|
|
88
|
+
}
|
|
89
|
+
if (value.indexOf(";") !== -1) {
|
|
90
|
+
throw _err("declare-row-policy/bad-expression",
|
|
91
|
+
where + " must not contain ';' — use a single boolean expression");
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function _validateOpts(opts) {
|
|
97
|
+
if (!opts || typeof opts !== "object") {
|
|
98
|
+
throw _err("declare-row-policy/bad-opts", "declareRowPolicy requires an opts object");
|
|
99
|
+
}
|
|
100
|
+
for (var k in opts) {
|
|
101
|
+
if (Object.prototype.hasOwnProperty.call(opts, k) && ALLOWED_OPTS.indexOf(k) === -1) {
|
|
102
|
+
throw _err("declare-row-policy/unknown-opt",
|
|
103
|
+
"unknown opt '" + k + "'. Allowed: " + ALLOWED_OPTS.join(", "));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
validateOpts.requireNonEmptyString(opts.schema, "schema", DeclareRowPolicyError, "declare-row-policy/missing-opt");
|
|
108
|
+
_validateIdent("schema", opts.schema);
|
|
109
|
+
|
|
110
|
+
validateOpts.requireNonEmptyString(opts.table, "table", DeclareRowPolicyError, "declare-row-policy/missing-opt");
|
|
111
|
+
_validateIdent("table", opts.table);
|
|
112
|
+
|
|
113
|
+
validateOpts.requireNonEmptyString(opts.name, "name", DeclareRowPolicyError, "declare-row-policy/missing-opt");
|
|
114
|
+
_validateIdent("name", opts.name);
|
|
115
|
+
|
|
116
|
+
var role = null;
|
|
117
|
+
if (opts.role !== undefined && opts.role !== null) {
|
|
118
|
+
if (typeof opts.role !== "string" || opts.role.length === 0) {
|
|
119
|
+
throw _err("declare-row-policy/bad-type", "role must be a non-empty string");
|
|
120
|
+
}
|
|
121
|
+
_validateIdent("role", opts.role);
|
|
122
|
+
role = opts.role;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (opts.using === undefined || opts.using === null) {
|
|
126
|
+
throw _err("declare-row-policy/missing-opt", "using is required (USING expression)");
|
|
127
|
+
}
|
|
128
|
+
var using = _validateExpression("using", opts.using);
|
|
129
|
+
|
|
130
|
+
var withCheck = null;
|
|
131
|
+
if (opts.withCheck !== undefined && opts.withCheck !== null) {
|
|
132
|
+
withCheck = _validateExpression("withCheck", opts.withCheck);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
var command = "ALL";
|
|
136
|
+
if (opts.command !== undefined && opts.command !== null) {
|
|
137
|
+
if (typeof opts.command !== "string") {
|
|
138
|
+
throw _err("declare-row-policy/bad-type", "command must be a string");
|
|
139
|
+
}
|
|
140
|
+
var upper = opts.command.toUpperCase();
|
|
141
|
+
if (ALLOWED_COMMANDS.indexOf(upper) === -1) {
|
|
142
|
+
throw _err("declare-row-policy/bad-command",
|
|
143
|
+
"command must be one of " + ALLOWED_COMMANDS.join(", ") + ", got '" + opts.command + "'");
|
|
144
|
+
}
|
|
145
|
+
command = upper;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
var permissive = true;
|
|
149
|
+
if (opts.permissive !== undefined && opts.permissive !== null) {
|
|
150
|
+
if (typeof opts.permissive !== "boolean") {
|
|
151
|
+
throw _err("declare-row-policy/bad-type", "permissive must be a boolean");
|
|
152
|
+
}
|
|
153
|
+
permissive = opts.permissive;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (opts.backend !== undefined && opts.backend !== null) {
|
|
157
|
+
if (typeof opts.backend !== "string" || opts.backend.length === 0) {
|
|
158
|
+
throw _err("declare-row-policy/bad-type", "backend must be a non-empty string");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
schema: opts.schema,
|
|
164
|
+
table: opts.table,
|
|
165
|
+
name: opts.name,
|
|
166
|
+
role: role,
|
|
167
|
+
using: using,
|
|
168
|
+
withCheck: withCheck,
|
|
169
|
+
command: command,
|
|
170
|
+
permissive: permissive,
|
|
171
|
+
backend: opts.backend || null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function _ensureBackendIsPostgres(externalDb, backendName) {
|
|
176
|
+
var list = externalDb.listBackends();
|
|
177
|
+
var found = null;
|
|
178
|
+
for (var i = 0; i < list.length; i++) {
|
|
179
|
+
if (list[i].name === backendName) { found = list[i]; break; }
|
|
180
|
+
}
|
|
181
|
+
if (!found) {
|
|
182
|
+
throw _err("declare-row-policy/unknown-backend",
|
|
183
|
+
"no externalDb backend named '" + backendName + "' — declared backends: " +
|
|
184
|
+
list.map(function (b) { return b.name; }).join(", "));
|
|
185
|
+
}
|
|
186
|
+
if (found.dialect !== "postgres") {
|
|
187
|
+
throw _err("declare-row-policy/not-supported",
|
|
188
|
+
"declareRowPolicy is Postgres-only; backend '" + backendName + "' has dialect='" +
|
|
189
|
+
found.dialect + "'. Write the policy as a hand-rolled migration for this dialect.");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function declareRowPolicy(opts) {
|
|
194
|
+
var spec = _validateOpts(opts);
|
|
195
|
+
var qTable = safeSql.quoteQualified([spec.schema, spec.table], "postgres");
|
|
196
|
+
var qPolicy = safeSql.quoteIdentifier(spec.name, "postgres");
|
|
197
|
+
var qRole = spec.role ? safeSql.quoteIdentifier(spec.role, "postgres") : null;
|
|
198
|
+
|
|
199
|
+
var description = "declareRowPolicy " + spec.schema + "." + spec.table + "." + spec.name;
|
|
200
|
+
|
|
201
|
+
async function up(xdb, ctx) {
|
|
202
|
+
if (ctx && ctx.externalDb && ctx.backendName) {
|
|
203
|
+
_ensureBackendIsPostgres(ctx.externalDb, ctx.backendName);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Idempotent ENABLE — Postgres has no IF NOT EXISTS for this. Read
|
|
207
|
+
// the current setting from pg_class and skip the ALTER when already
|
|
208
|
+
// on, so re-running a migration set in a partially-applied state
|
|
209
|
+
// doesn't fail with a no-op error from the lock acquisition.
|
|
210
|
+
var rlsCheck = await xdb.query(
|
|
211
|
+
"SELECT relrowsecurity FROM pg_class c " +
|
|
212
|
+
"JOIN pg_namespace n ON n.oid = c.relnamespace " +
|
|
213
|
+
"WHERE n.nspname = $1 AND c.relname = $2",
|
|
214
|
+
[spec.schema, spec.table]
|
|
215
|
+
);
|
|
216
|
+
var rows = (rlsCheck && rlsCheck.rows) || [];
|
|
217
|
+
if (rows.length === 0) {
|
|
218
|
+
throw _err("declare-row-policy/table-not-found",
|
|
219
|
+
"source table '" + spec.schema + "." + spec.table +
|
|
220
|
+
"' not found (does it exist? does the migration role have visibility?)");
|
|
221
|
+
}
|
|
222
|
+
if (!rows[0].relrowsecurity) {
|
|
223
|
+
await xdb.query("ALTER TABLE " + qTable + " ENABLE ROW LEVEL SECURITY", []);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// CREATE POLICY assembled in canonical order: name → table → AS
|
|
227
|
+
// PERMISSIVE/RESTRICTIVE → FOR command → TO role → USING → WITH CHECK.
|
|
228
|
+
var sql = "CREATE POLICY " + qPolicy + " ON " + qTable;
|
|
229
|
+
sql += " AS " + (spec.permissive ? "PERMISSIVE" : "RESTRICTIVE");
|
|
230
|
+
sql += " FOR " + spec.command;
|
|
231
|
+
if (qRole) sql += " TO " + qRole;
|
|
232
|
+
sql += " USING (" + spec.using + ")";
|
|
233
|
+
if (spec.withCheck) sql += " WITH CHECK (" + spec.withCheck + ")";
|
|
234
|
+
|
|
235
|
+
await xdb.query(sql, []);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
policy: spec.schema + "." + spec.table + "." + spec.name,
|
|
239
|
+
table: spec.schema + "." + spec.table,
|
|
240
|
+
role: spec.role,
|
|
241
|
+
command: spec.command,
|
|
242
|
+
permissive: spec.permissive,
|
|
243
|
+
hasWithCheck: !!spec.withCheck,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function down(xdb, ctx) {
|
|
248
|
+
if (ctx && ctx.externalDb && ctx.backendName) {
|
|
249
|
+
_ensureBackendIsPostgres(ctx.externalDb, ctx.backendName);
|
|
250
|
+
}
|
|
251
|
+
await xdb.query("DROP POLICY IF EXISTS " + qPolicy + " ON " + qTable, []);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
description: description,
|
|
256
|
+
target: "externalDb",
|
|
257
|
+
backend: spec.backend,
|
|
258
|
+
up: up,
|
|
259
|
+
down: down,
|
|
260
|
+
_spec: spec,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
declareRowPolicy: declareRowPolicy,
|
|
266
|
+
DeclareRowPolicyError: DeclareRowPolicyError,
|
|
267
|
+
};
|