@kysera/rls 0.7.3 → 0.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.
- package/README.md +389 -279
- package/dist/index.d.ts +89 -20
- package/dist/index.js +210 -130
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/native/index.js +3 -14
- package/dist/native/index.js.map +1 -1
- package/dist/{types-6eCXh_Jd.d.ts → types-Dowjd6zG.d.ts} +3 -3
- package/package.json +16 -7
- package/src/context/index.ts +4 -4
- package/src/context/manager.ts +45 -45
- package/src/context/storage.ts +3 -3
- package/src/context/types.ts +1 -5
- package/src/errors.ts +62 -77
- package/src/index.ts +13 -13
- package/src/native/README.md +49 -46
- package/src/native/index.ts +3 -6
- package/src/native/migration.ts +29 -27
- package/src/native/postgres.ts +63 -74
- package/src/plugin.ts +286 -159
- package/src/policy/builder.ts +46 -33
- package/src/policy/index.ts +4 -4
- package/src/policy/registry.ts +100 -105
- package/src/policy/schema.ts +58 -71
- package/src/policy/types.ts +58 -58
- package/src/transformer/index.ts +2 -2
- package/src/transformer/mutation.ts +95 -98
- package/src/transformer/select.ts +59 -43
- package/src/utils/helpers.ts +57 -50
- package/src/utils/index.ts +13 -2
- package/src/utils/type-utils.ts +155 -0
- package/src/version.ts +7 -0
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { silentLogger } from '@kysera/core';
|
|
2
|
-
import { getRawDb } from '@kysera/executor';
|
|
3
|
-
import {
|
|
1
|
+
import { DatabaseError, silentLogger } from '@kysera/core';
|
|
2
|
+
import { isRepositoryLike, getRawDb } from '@kysera/executor';
|
|
3
|
+
import { z } from 'zod';
|
|
4
4
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
5
|
+
import { sql } from 'kysely';
|
|
5
6
|
|
|
6
7
|
// src/errors.ts
|
|
7
8
|
var RLSErrorCodes = {
|
|
@@ -18,8 +19,7 @@ var RLSErrorCodes = {
|
|
|
18
19
|
/** RLS policy evaluation threw an error */
|
|
19
20
|
RLS_POLICY_EVALUATION_ERROR: "RLS_POLICY_EVALUATION_ERROR"
|
|
20
21
|
};
|
|
21
|
-
var RLSError = class extends
|
|
22
|
-
code;
|
|
22
|
+
var RLSError = class extends DatabaseError {
|
|
23
23
|
/**
|
|
24
24
|
* Creates a new RLS error
|
|
25
25
|
*
|
|
@@ -27,21 +27,8 @@ var RLSError = class extends Error {
|
|
|
27
27
|
* @param code - RLS error code
|
|
28
28
|
*/
|
|
29
29
|
constructor(message, code) {
|
|
30
|
-
super(message);
|
|
30
|
+
super(message, code);
|
|
31
31
|
this.name = "RLSError";
|
|
32
|
-
this.code = code;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Serializes the error to JSON
|
|
36
|
-
*
|
|
37
|
-
* @returns JSON representation of the error
|
|
38
|
-
*/
|
|
39
|
-
toJSON() {
|
|
40
|
-
return {
|
|
41
|
-
name: this.name,
|
|
42
|
-
message: this.message,
|
|
43
|
-
code: this.code
|
|
44
|
-
};
|
|
45
32
|
}
|
|
46
33
|
};
|
|
47
34
|
var RLSContextError = class extends RLSError {
|
|
@@ -198,10 +185,7 @@ function validateSchema(schema) {
|
|
|
198
185
|
if (!config) continue;
|
|
199
186
|
const tableConfig = config;
|
|
200
187
|
if (!Array.isArray(tableConfig.policies)) {
|
|
201
|
-
throw new RLSSchemaError(
|
|
202
|
-
`Invalid policies for table "${table}": must be an array`,
|
|
203
|
-
{ table }
|
|
204
|
-
);
|
|
188
|
+
throw new RLSSchemaError(`Invalid policies for table "${table}": must be an array`, { table });
|
|
205
189
|
}
|
|
206
190
|
for (let i = 0; i < tableConfig.policies.length; i++) {
|
|
207
191
|
const policy = tableConfig.policies[i];
|
|
@@ -226,19 +210,15 @@ function validateSchema(schema) {
|
|
|
226
210
|
}
|
|
227
211
|
}
|
|
228
212
|
if (tableConfig.defaultDeny !== void 0 && typeof tableConfig.defaultDeny !== "boolean") {
|
|
229
|
-
throw new RLSSchemaError(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
);
|
|
213
|
+
throw new RLSSchemaError(`Invalid defaultDeny for table "${table}": must be a boolean`, {
|
|
214
|
+
table
|
|
215
|
+
});
|
|
233
216
|
}
|
|
234
217
|
}
|
|
235
218
|
}
|
|
236
219
|
function validatePolicy(policy, table, index) {
|
|
237
220
|
if (!policy.type) {
|
|
238
|
-
throw new RLSSchemaError(
|
|
239
|
-
`Policy ${index} for table "${table}" missing type`,
|
|
240
|
-
{ table, index }
|
|
241
|
-
);
|
|
221
|
+
throw new RLSSchemaError(`Policy ${index} for table "${table}" missing type`, { table, index });
|
|
242
222
|
}
|
|
243
223
|
const validTypes = ["allow", "deny", "filter", "validate"];
|
|
244
224
|
if (!validTypes.includes(policy.type)) {
|
|
@@ -248,10 +228,10 @@ function validatePolicy(policy, table, index) {
|
|
|
248
228
|
);
|
|
249
229
|
}
|
|
250
230
|
if (!policy.operation) {
|
|
251
|
-
throw new RLSSchemaError(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
);
|
|
231
|
+
throw new RLSSchemaError(`Policy ${index} for table "${table}" missing operation`, {
|
|
232
|
+
table,
|
|
233
|
+
index
|
|
234
|
+
});
|
|
255
235
|
}
|
|
256
236
|
const validOps = ["read", "create", "update", "delete", "all"];
|
|
257
237
|
const ops = Array.isArray(policy.operation) ? policy.operation : [policy.operation];
|
|
@@ -264,10 +244,10 @@ function validatePolicy(policy, table, index) {
|
|
|
264
244
|
}
|
|
265
245
|
}
|
|
266
246
|
if (policy.condition === void 0 || policy.condition === null) {
|
|
267
|
-
throw new RLSSchemaError(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
);
|
|
247
|
+
throw new RLSSchemaError(`Policy ${index} for table "${table}" missing condition`, {
|
|
248
|
+
table,
|
|
249
|
+
index
|
|
250
|
+
});
|
|
271
251
|
}
|
|
272
252
|
if (typeof policy.condition !== "function" && typeof policy.condition !== "string") {
|
|
273
253
|
throw new RLSSchemaError(
|
|
@@ -276,16 +256,16 @@ function validatePolicy(policy, table, index) {
|
|
|
276
256
|
);
|
|
277
257
|
}
|
|
278
258
|
if (policy.priority !== void 0 && typeof policy.priority !== "number") {
|
|
279
|
-
throw new RLSSchemaError(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
);
|
|
259
|
+
throw new RLSSchemaError(`Policy ${index} for table "${table}" priority must be a number`, {
|
|
260
|
+
table,
|
|
261
|
+
index
|
|
262
|
+
});
|
|
283
263
|
}
|
|
284
264
|
if (policy.name !== void 0 && typeof policy.name !== "string") {
|
|
285
|
-
throw new RLSSchemaError(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
);
|
|
265
|
+
throw new RLSSchemaError(`Policy ${index} for table "${table}" name must be a string`, {
|
|
266
|
+
table,
|
|
267
|
+
index
|
|
268
|
+
});
|
|
289
269
|
}
|
|
290
270
|
}
|
|
291
271
|
function mergeRLSSchemas(...schemas) {
|
|
@@ -296,10 +276,7 @@ function mergeRLSSchemas(...schemas) {
|
|
|
296
276
|
const existingConfig = merged[table];
|
|
297
277
|
const newConfig = config;
|
|
298
278
|
if (existingConfig) {
|
|
299
|
-
existingConfig.policies = [
|
|
300
|
-
...existingConfig.policies,
|
|
301
|
-
...newConfig.policies
|
|
302
|
-
];
|
|
279
|
+
existingConfig.policies = [...existingConfig.policies, ...newConfig.policies];
|
|
303
280
|
if (newConfig.skipFor) {
|
|
304
281
|
const existingSkipFor = existingConfig.skipFor ?? [];
|
|
305
282
|
const combinedSkipFor = [...existingSkipFor, ...newConfig.skipFor];
|
|
@@ -695,7 +672,7 @@ var RLSContextManager = class {
|
|
|
695
672
|
* Run an async function within an RLS context
|
|
696
673
|
*/
|
|
697
674
|
async runAsync(context, fn) {
|
|
698
|
-
return rlsStorage.run(context, fn);
|
|
675
|
+
return await rlsStorage.run(context, fn);
|
|
699
676
|
}
|
|
700
677
|
/**
|
|
701
678
|
* Get current RLS context
|
|
@@ -788,7 +765,7 @@ var RLSContextManager = class {
|
|
|
788
765
|
...currentCtx,
|
|
789
766
|
auth: { ...currentCtx.auth, isSystem: true }
|
|
790
767
|
};
|
|
791
|
-
return this.runAsync(systemCtx, fn);
|
|
768
|
+
return await this.runAsync(systemCtx, fn);
|
|
792
769
|
}
|
|
793
770
|
};
|
|
794
771
|
var rlsContext = new RLSContextManager();
|
|
@@ -796,7 +773,32 @@ function withRLSContext(context, fn) {
|
|
|
796
773
|
return rlsContext.run(context, fn);
|
|
797
774
|
}
|
|
798
775
|
async function withRLSContextAsync(context, fn) {
|
|
799
|
-
return rlsContext.runAsync(context, fn);
|
|
776
|
+
return await rlsContext.runAsync(context, fn);
|
|
777
|
+
}
|
|
778
|
+
function createQualifiedColumn(table, column) {
|
|
779
|
+
return `${table}.${column}`;
|
|
780
|
+
}
|
|
781
|
+
function applyWhereCondition(qb, column, operator, value) {
|
|
782
|
+
return qb.where(column, operator, value);
|
|
783
|
+
}
|
|
784
|
+
function createRawCondition(expression) {
|
|
785
|
+
return sql`${sql.raw(expression)}`;
|
|
786
|
+
}
|
|
787
|
+
function selectFromDynamicTable(db, table) {
|
|
788
|
+
return db.selectFrom(table).selectAll();
|
|
789
|
+
}
|
|
790
|
+
function whereIdEquals(qb, id, primaryKeyColumn = "id") {
|
|
791
|
+
return qb.where(primaryKeyColumn, "=", id);
|
|
792
|
+
}
|
|
793
|
+
function transformQueryBuilder(qb, operation, transform) {
|
|
794
|
+
if (operation !== "select") {
|
|
795
|
+
return qb;
|
|
796
|
+
}
|
|
797
|
+
const transformed = transform(qb);
|
|
798
|
+
return transformed;
|
|
799
|
+
}
|
|
800
|
+
function hasRawDb(executor) {
|
|
801
|
+
return "__rawDb" in executor && executor.__rawDb !== void 0;
|
|
800
802
|
}
|
|
801
803
|
|
|
802
804
|
// src/transformer/select.ts
|
|
@@ -822,7 +824,12 @@ var SelectTransformer = class {
|
|
|
822
824
|
transform(qb, table) {
|
|
823
825
|
const ctx = rlsContext.getContextOrNull();
|
|
824
826
|
if (!ctx) {
|
|
825
|
-
return
|
|
827
|
+
return applyWhereCondition(
|
|
828
|
+
qb,
|
|
829
|
+
createRawCondition("FALSE"),
|
|
830
|
+
"=",
|
|
831
|
+
true
|
|
832
|
+
);
|
|
826
833
|
}
|
|
827
834
|
if (ctx.auth.isSystem) {
|
|
828
835
|
return qb;
|
|
@@ -867,18 +874,14 @@ var SelectTransformer = class {
|
|
|
867
874
|
/**
|
|
868
875
|
* Apply filter conditions to query builder
|
|
869
876
|
*
|
|
870
|
-
*
|
|
871
|
-
*
|
|
872
|
-
* Kysely's type system is designed for static query building where column names
|
|
873
|
-
* are known at compile time. RLS policies work with dynamic column names at runtime.
|
|
877
|
+
* Uses type-safe wrappers from utils/type-utils.ts to encapsulate the boundary
|
|
878
|
+
* between runtime policy conditions and Kysely's compile-time type system.
|
|
874
879
|
*
|
|
875
880
|
* Type safety is maintained through:
|
|
876
881
|
* 1. Policy conditions are validated during schema registration
|
|
877
882
|
* 2. Column names come from policy definitions (developer-controlled)
|
|
878
883
|
* 3. Values are type-checked by the policy condition functions
|
|
879
884
|
*
|
|
880
|
-
* This is the same pattern used in @kysera/repository/table-operations.ts
|
|
881
|
-
*
|
|
882
885
|
* @param qb - Query builder to modify
|
|
883
886
|
* @param conditions - WHERE clause conditions
|
|
884
887
|
* @param table - Table name (for qualified column names)
|
|
@@ -887,19 +890,24 @@ var SelectTransformer = class {
|
|
|
887
890
|
applyConditions(qb, conditions, table) {
|
|
888
891
|
let result = qb;
|
|
889
892
|
for (const [column, value] of Object.entries(conditions)) {
|
|
890
|
-
const qualifiedColumn =
|
|
893
|
+
const qualifiedColumn = createQualifiedColumn(table, column);
|
|
891
894
|
if (value === null) {
|
|
892
|
-
result = result
|
|
895
|
+
result = applyWhereCondition(result, qualifiedColumn, "is", null);
|
|
893
896
|
} else if (value === void 0) {
|
|
894
897
|
continue;
|
|
895
898
|
} else if (Array.isArray(value)) {
|
|
896
899
|
if (value.length === 0) {
|
|
897
|
-
result =
|
|
900
|
+
result = applyWhereCondition(
|
|
901
|
+
result,
|
|
902
|
+
createRawCondition("FALSE"),
|
|
903
|
+
"=",
|
|
904
|
+
true
|
|
905
|
+
);
|
|
898
906
|
} else {
|
|
899
|
-
result = result
|
|
907
|
+
result = applyWhereCondition(result, qualifiedColumn, "in", value);
|
|
900
908
|
}
|
|
901
909
|
} else {
|
|
902
|
-
result = result
|
|
910
|
+
result = applyWhereCondition(result, qualifiedColumn, "=", value);
|
|
903
911
|
}
|
|
904
912
|
}
|
|
905
913
|
return result;
|
|
@@ -907,6 +915,7 @@ var SelectTransformer = class {
|
|
|
907
915
|
};
|
|
908
916
|
|
|
909
917
|
// src/transformer/mutation.ts
|
|
918
|
+
var DEFAULT_CHUNK_SIZE = 100;
|
|
910
919
|
var MutationGuard = class {
|
|
911
920
|
constructor(registry) {
|
|
912
921
|
this.registry = registry;
|
|
@@ -1060,11 +1069,7 @@ var MutationGuard = class {
|
|
|
1060
1069
|
async checkMutation(table, operation, row, data) {
|
|
1061
1070
|
const ctx = rlsContext.getContextOrNull();
|
|
1062
1071
|
if (!ctx) {
|
|
1063
|
-
throw new RLSPolicyViolation(
|
|
1064
|
-
operation,
|
|
1065
|
-
table,
|
|
1066
|
-
"No RLS context available"
|
|
1067
|
-
);
|
|
1072
|
+
throw new RLSPolicyViolation(operation, table, "No RLS context available");
|
|
1068
1073
|
}
|
|
1069
1074
|
if (ctx.auth.isSystem) {
|
|
1070
1075
|
return;
|
|
@@ -1078,11 +1083,7 @@ var MutationGuard = class {
|
|
|
1078
1083
|
const evalCtx = this.createEvalContext(ctx, table, operation, row, data);
|
|
1079
1084
|
const result = await this.evaluatePolicy(deny2.evaluate, evalCtx, deny2.name);
|
|
1080
1085
|
if (result) {
|
|
1081
|
-
throw new RLSPolicyViolation(
|
|
1082
|
-
operation,
|
|
1083
|
-
table,
|
|
1084
|
-
`Denied by policy: ${deny2.name}`
|
|
1085
|
-
);
|
|
1086
|
+
throw new RLSPolicyViolation(operation, table, `Denied by policy: ${deny2.name}`);
|
|
1086
1087
|
}
|
|
1087
1088
|
}
|
|
1088
1089
|
if ((operation === "create" || operation === "update") && data) {
|
|
@@ -1091,22 +1092,14 @@ var MutationGuard = class {
|
|
|
1091
1092
|
const evalCtx = this.createEvalContext(ctx, table, operation, row, data);
|
|
1092
1093
|
const result = await this.evaluatePolicy(validate2.evaluate, evalCtx, validate2.name);
|
|
1093
1094
|
if (!result) {
|
|
1094
|
-
throw new RLSPolicyViolation(
|
|
1095
|
-
operation,
|
|
1096
|
-
table,
|
|
1097
|
-
`Validation failed: ${validate2.name}`
|
|
1098
|
-
);
|
|
1095
|
+
throw new RLSPolicyViolation(operation, table, `Validation failed: ${validate2.name}`);
|
|
1099
1096
|
}
|
|
1100
1097
|
}
|
|
1101
1098
|
}
|
|
1102
1099
|
const allows = this.registry.getAllows(table, operation);
|
|
1103
1100
|
const defaultDeny = this.registry.hasDefaultDeny(table);
|
|
1104
1101
|
if (defaultDeny && allows.length === 0) {
|
|
1105
|
-
throw new RLSPolicyViolation(
|
|
1106
|
-
operation,
|
|
1107
|
-
table,
|
|
1108
|
-
"No allow policies defined (default deny)"
|
|
1109
|
-
);
|
|
1102
|
+
throw new RLSPolicyViolation(operation, table, "No allow policies defined (default deny)");
|
|
1110
1103
|
}
|
|
1111
1104
|
if (allows.length > 0) {
|
|
1112
1105
|
let allowed = false;
|
|
@@ -1119,11 +1112,7 @@ var MutationGuard = class {
|
|
|
1119
1112
|
}
|
|
1120
1113
|
}
|
|
1121
1114
|
if (!allowed) {
|
|
1122
|
-
throw new RLSPolicyViolation(
|
|
1123
|
-
operation,
|
|
1124
|
-
table,
|
|
1125
|
-
"No allow policies matched"
|
|
1126
|
-
);
|
|
1115
|
+
throw new RLSPolicyViolation(operation, table, "No allow policies matched");
|
|
1127
1116
|
}
|
|
1128
1117
|
}
|
|
1129
1118
|
}
|
|
@@ -1165,11 +1154,12 @@ var MutationGuard = class {
|
|
|
1165
1154
|
}
|
|
1166
1155
|
}
|
|
1167
1156
|
/**
|
|
1168
|
-
* Filter
|
|
1169
|
-
*
|
|
1157
|
+
* Filter rows based on read policies.
|
|
1158
|
+
* Uses parallel processing with chunking for performance.
|
|
1170
1159
|
*
|
|
1171
1160
|
* @param table - Table name
|
|
1172
|
-
* @param rows -
|
|
1161
|
+
* @param rows - Rows to filter
|
|
1162
|
+
* @param chunkSize - Number of rows to process in parallel (default: 100)
|
|
1173
1163
|
* @returns Filtered array containing only accessible rows
|
|
1174
1164
|
*
|
|
1175
1165
|
* @example
|
|
@@ -1177,34 +1167,63 @@ var MutationGuard = class {
|
|
|
1177
1167
|
* const guard = new MutationGuard(registry);
|
|
1178
1168
|
* const allPosts = await db.selectFrom('posts').selectAll().execute();
|
|
1179
1169
|
* const accessiblePosts = await guard.filterRows('posts', allPosts);
|
|
1170
|
+
*
|
|
1171
|
+
* // With custom chunk size for large datasets
|
|
1172
|
+
* const accessiblePosts = await guard.filterRows('posts', allPosts, 50);
|
|
1180
1173
|
* ```
|
|
1181
1174
|
*/
|
|
1182
|
-
async filterRows(table, rows) {
|
|
1175
|
+
async filterRows(table, rows, chunkSize = DEFAULT_CHUNK_SIZE) {
|
|
1176
|
+
if (rows.length === 0) return [];
|
|
1183
1177
|
const results = [];
|
|
1184
|
-
for (
|
|
1185
|
-
|
|
1186
|
-
|
|
1178
|
+
for (let i = 0; i < rows.length; i += chunkSize) {
|
|
1179
|
+
const chunk = rows.slice(i, i + chunkSize);
|
|
1180
|
+
const chunkResults = await Promise.all(
|
|
1181
|
+
chunk.map(async (row) => {
|
|
1182
|
+
const allowed = await this.checkRead(table, row);
|
|
1183
|
+
return allowed ? row : null;
|
|
1184
|
+
})
|
|
1185
|
+
);
|
|
1186
|
+
for (const row of chunkResults) {
|
|
1187
|
+
if (row !== null) {
|
|
1188
|
+
results.push(row);
|
|
1189
|
+
}
|
|
1187
1190
|
}
|
|
1188
1191
|
}
|
|
1189
1192
|
return results;
|
|
1190
1193
|
}
|
|
1191
1194
|
};
|
|
1195
|
+
|
|
1196
|
+
// src/version.ts
|
|
1197
|
+
var RAW_VERSION = "__VERSION__";
|
|
1198
|
+
var VERSION = RAW_VERSION.startsWith("__") ? "0.0.0-dev" : RAW_VERSION;
|
|
1199
|
+
var RLSPluginOptionsSchema = z.object({
|
|
1200
|
+
excludeTables: z.array(z.string()).optional(),
|
|
1201
|
+
bypassRoles: z.array(z.string()).optional(),
|
|
1202
|
+
requireContext: z.boolean().optional(),
|
|
1203
|
+
allowUnfilteredQueries: z.boolean().optional(),
|
|
1204
|
+
auditDecisions: z.boolean().optional(),
|
|
1205
|
+
primaryKeyColumn: z.string().optional()
|
|
1206
|
+
});
|
|
1192
1207
|
function rlsPlugin(options) {
|
|
1193
1208
|
const {
|
|
1194
1209
|
schema,
|
|
1195
|
-
|
|
1210
|
+
excludeTables = [],
|
|
1196
1211
|
bypassRoles = [],
|
|
1197
1212
|
logger = silentLogger,
|
|
1198
|
-
requireContext =
|
|
1213
|
+
requireContext = true,
|
|
1214
|
+
// SECURITY: Changed to true for secure-by-default (CRIT-2 fix)
|
|
1215
|
+
allowUnfilteredQueries = false,
|
|
1216
|
+
// SECURITY: Explicit opt-in for unfiltered queries
|
|
1199
1217
|
auditDecisions = false,
|
|
1200
|
-
onViolation
|
|
1218
|
+
onViolation,
|
|
1219
|
+
primaryKeyColumn = "id"
|
|
1201
1220
|
} = options;
|
|
1202
1221
|
let registry;
|
|
1203
1222
|
let selectTransformer;
|
|
1204
1223
|
let mutationGuard;
|
|
1205
1224
|
return {
|
|
1206
1225
|
name: "@kysera/rls",
|
|
1207
|
-
version:
|
|
1226
|
+
version: VERSION,
|
|
1208
1227
|
// Run after soft-delete (priority 0), before audit
|
|
1209
1228
|
priority: 50,
|
|
1210
1229
|
// No dependencies by default
|
|
@@ -1212,10 +1231,10 @@ function rlsPlugin(options) {
|
|
|
1212
1231
|
/**
|
|
1213
1232
|
* Initialize plugin - compile policies
|
|
1214
1233
|
*/
|
|
1215
|
-
|
|
1234
|
+
onInit(_executor) {
|
|
1216
1235
|
logger.info?.("[RLS] Initializing RLS plugin", {
|
|
1217
1236
|
tables: Object.keys(schema).length,
|
|
1218
|
-
|
|
1237
|
+
excludeTables: excludeTables.length,
|
|
1219
1238
|
bypassRoles: bypassRoles.length
|
|
1220
1239
|
});
|
|
1221
1240
|
registry = new PolicyRegistry(schema);
|
|
@@ -1224,6 +1243,13 @@ function rlsPlugin(options) {
|
|
|
1224
1243
|
mutationGuard = new MutationGuard(registry);
|
|
1225
1244
|
logger.info?.("[RLS] RLS plugin initialized successfully");
|
|
1226
1245
|
},
|
|
1246
|
+
/**
|
|
1247
|
+
* Cleanup resources when executor is destroyed
|
|
1248
|
+
*/
|
|
1249
|
+
async onDestroy() {
|
|
1250
|
+
registry.clear();
|
|
1251
|
+
logger.info?.("[RLS] RLS plugin destroyed, cleared policy registry");
|
|
1252
|
+
},
|
|
1227
1253
|
/**
|
|
1228
1254
|
* Intercept queries to apply RLS filtering
|
|
1229
1255
|
*
|
|
@@ -1233,7 +1259,7 @@ function rlsPlugin(options) {
|
|
|
1233
1259
|
*/
|
|
1234
1260
|
interceptQuery(qb, context) {
|
|
1235
1261
|
const { operation, table, metadata } = context;
|
|
1236
|
-
if (
|
|
1262
|
+
if (excludeTables.includes(table)) {
|
|
1237
1263
|
logger.debug?.(`[RLS] Skipping RLS for excluded table: ${table}`);
|
|
1238
1264
|
return qb;
|
|
1239
1265
|
}
|
|
@@ -1244,9 +1270,29 @@ function rlsPlugin(options) {
|
|
|
1244
1270
|
const ctx = rlsContext.getContextOrNull();
|
|
1245
1271
|
if (!ctx) {
|
|
1246
1272
|
if (requireContext) {
|
|
1247
|
-
throw new RLSContextError(
|
|
1273
|
+
throw new RLSContextError(
|
|
1274
|
+
`RLS context required but not found for ${operation} on ${table}. This prevents unfiltered database access. Either provide RLS context or set 'requireContext: false' with 'allowUnfilteredQueries: true' if intentional.`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
if (!allowUnfilteredQueries) {
|
|
1278
|
+
logger.warn?.(
|
|
1279
|
+
`[RLS] Missing context for ${operation} on ${table}. Queries will return empty results for security. Set 'allowUnfilteredQueries: true' to allow unfiltered access (not recommended).`
|
|
1280
|
+
);
|
|
1281
|
+
if (operation === "select") {
|
|
1282
|
+
return transformQueryBuilder(qb, operation, (selectQb) => {
|
|
1283
|
+
return applyWhereCondition(
|
|
1284
|
+
selectQb,
|
|
1285
|
+
createRawCondition("FALSE"),
|
|
1286
|
+
"=",
|
|
1287
|
+
true
|
|
1288
|
+
);
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
return qb;
|
|
1248
1292
|
}
|
|
1249
|
-
logger.warn?.(
|
|
1293
|
+
logger.warn?.(
|
|
1294
|
+
`[RLS] No context for ${operation} on ${table}. Allowing unfiltered query due to 'allowUnfilteredQueries: true'. This may expose sensitive data.`
|
|
1295
|
+
);
|
|
1250
1296
|
return qb;
|
|
1251
1297
|
}
|
|
1252
1298
|
if (ctx.auth.isSystem) {
|
|
@@ -1259,7 +1305,12 @@ function rlsPlugin(options) {
|
|
|
1259
1305
|
}
|
|
1260
1306
|
if (operation === "select") {
|
|
1261
1307
|
try {
|
|
1262
|
-
const transformed =
|
|
1308
|
+
const transformed = transformQueryBuilder(
|
|
1309
|
+
qb,
|
|
1310
|
+
operation,
|
|
1311
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1312
|
+
(selectQb) => selectTransformer.transform(selectQb, table)
|
|
1313
|
+
);
|
|
1263
1314
|
if (auditDecisions) {
|
|
1264
1315
|
logger.info?.("[RLS] Filter applied", {
|
|
1265
1316
|
table,
|
|
@@ -1286,12 +1337,13 @@ function rlsPlugin(options) {
|
|
|
1286
1337
|
* Also adds utility methods for bypassing RLS and checking access.
|
|
1287
1338
|
*/
|
|
1288
1339
|
extendRepository(repo) {
|
|
1289
|
-
|
|
1290
|
-
if (!("tableName" in baseRepo) || !("executor" in baseRepo)) {
|
|
1340
|
+
if (!isRepositoryLike(repo)) {
|
|
1291
1341
|
return repo;
|
|
1292
1342
|
}
|
|
1343
|
+
const baseRepo = repo;
|
|
1293
1344
|
const table = baseRepo.tableName;
|
|
1294
|
-
if (
|
|
1345
|
+
if (excludeTables.includes(table)) {
|
|
1346
|
+
logger.debug?.(`[RLS] Skipping repository extension for excluded table: ${table}`);
|
|
1295
1347
|
return repo;
|
|
1296
1348
|
}
|
|
1297
1349
|
if (!registry.hasTable(table)) {
|
|
@@ -1304,7 +1356,7 @@ function rlsPlugin(options) {
|
|
|
1304
1356
|
const originalDelete = baseRepo.delete?.bind(baseRepo);
|
|
1305
1357
|
const originalFindById = baseRepo.findById?.bind(baseRepo);
|
|
1306
1358
|
const rawDb = getRawDb(baseRepo.executor);
|
|
1307
|
-
const
|
|
1359
|
+
const hasRawDbInstance = hasRawDb(baseRepo.executor);
|
|
1308
1360
|
const extendedRepo = {
|
|
1309
1361
|
...baseRepo,
|
|
1310
1362
|
/**
|
|
@@ -1312,7 +1364,10 @@ function rlsPlugin(options) {
|
|
|
1312
1364
|
*/
|
|
1313
1365
|
async create(data) {
|
|
1314
1366
|
if (!originalCreate) {
|
|
1315
|
-
throw new RLSError(
|
|
1367
|
+
throw new RLSError(
|
|
1368
|
+
"Repository does not support create operation",
|
|
1369
|
+
RLSErrorCodes.RLS_POLICY_INVALID
|
|
1370
|
+
);
|
|
1316
1371
|
}
|
|
1317
1372
|
const ctx = rlsContext.getContextOrNull();
|
|
1318
1373
|
if (ctx && !ctx.auth.isSystem && !bypassRoles.some((role) => ctx.auth.roles.includes(role))) {
|
|
@@ -1335,27 +1390,34 @@ function rlsPlugin(options) {
|
|
|
1335
1390
|
throw error;
|
|
1336
1391
|
}
|
|
1337
1392
|
}
|
|
1338
|
-
return originalCreate(data);
|
|
1393
|
+
return await originalCreate(data);
|
|
1339
1394
|
},
|
|
1340
1395
|
/**
|
|
1341
1396
|
* Wrapped update with RLS check
|
|
1342
1397
|
*/
|
|
1343
1398
|
async update(id, data) {
|
|
1344
1399
|
if (!originalUpdate) {
|
|
1345
|
-
throw new RLSError(
|
|
1400
|
+
throw new RLSError(
|
|
1401
|
+
"Repository does not support update operation",
|
|
1402
|
+
RLSErrorCodes.RLS_POLICY_INVALID
|
|
1403
|
+
);
|
|
1346
1404
|
}
|
|
1347
1405
|
const ctx = rlsContext.getContextOrNull();
|
|
1348
1406
|
if (ctx && !ctx.auth.isSystem && !bypassRoles.some((role) => ctx.auth.roles.includes(role))) {
|
|
1349
1407
|
let existingRow;
|
|
1350
|
-
if (
|
|
1351
|
-
|
|
1408
|
+
if (hasRawDbInstance) {
|
|
1409
|
+
const query = selectFromDynamicTable(rawDb, table);
|
|
1410
|
+
existingRow = await whereIdEquals(query, id, primaryKeyColumn).executeTakeFirst();
|
|
1352
1411
|
} else if (originalFindById) {
|
|
1353
1412
|
existingRow = await originalFindById(id);
|
|
1354
1413
|
} else {
|
|
1355
|
-
throw new RLSError(
|
|
1414
|
+
throw new RLSError(
|
|
1415
|
+
"Repository does not support update operation",
|
|
1416
|
+
RLSErrorCodes.RLS_POLICY_INVALID
|
|
1417
|
+
);
|
|
1356
1418
|
}
|
|
1357
1419
|
if (!existingRow) {
|
|
1358
|
-
return originalUpdate(id, data);
|
|
1420
|
+
return await originalUpdate(id, data);
|
|
1359
1421
|
}
|
|
1360
1422
|
try {
|
|
1361
1423
|
await mutationGuard.checkUpdate(
|
|
@@ -1381,27 +1443,34 @@ function rlsPlugin(options) {
|
|
|
1381
1443
|
throw error;
|
|
1382
1444
|
}
|
|
1383
1445
|
}
|
|
1384
|
-
return originalUpdate(id, data);
|
|
1446
|
+
return await originalUpdate(id, data);
|
|
1385
1447
|
},
|
|
1386
1448
|
/**
|
|
1387
1449
|
* Wrapped delete with RLS check
|
|
1388
1450
|
*/
|
|
1389
1451
|
async delete(id) {
|
|
1390
1452
|
if (!originalDelete) {
|
|
1391
|
-
throw new RLSError(
|
|
1453
|
+
throw new RLSError(
|
|
1454
|
+
"Repository does not support delete operation",
|
|
1455
|
+
RLSErrorCodes.RLS_POLICY_INVALID
|
|
1456
|
+
);
|
|
1392
1457
|
}
|
|
1393
1458
|
const ctx = rlsContext.getContextOrNull();
|
|
1394
1459
|
if (ctx && !ctx.auth.isSystem && !bypassRoles.some((role) => ctx.auth.roles.includes(role))) {
|
|
1395
1460
|
let existingRow;
|
|
1396
|
-
if (
|
|
1397
|
-
|
|
1461
|
+
if (hasRawDbInstance) {
|
|
1462
|
+
const query = selectFromDynamicTable(rawDb, table);
|
|
1463
|
+
existingRow = await whereIdEquals(query, id, primaryKeyColumn).executeTakeFirst();
|
|
1398
1464
|
} else if (originalFindById) {
|
|
1399
1465
|
existingRow = await originalFindById(id);
|
|
1400
1466
|
} else {
|
|
1401
|
-
throw new RLSError(
|
|
1467
|
+
throw new RLSError(
|
|
1468
|
+
"Repository does not support delete operation",
|
|
1469
|
+
RLSErrorCodes.RLS_POLICY_INVALID
|
|
1470
|
+
);
|
|
1402
1471
|
}
|
|
1403
1472
|
if (!existingRow) {
|
|
1404
|
-
return originalDelete(id);
|
|
1473
|
+
return await originalDelete(id);
|
|
1405
1474
|
}
|
|
1406
1475
|
try {
|
|
1407
1476
|
await mutationGuard.checkDelete(table, existingRow);
|
|
@@ -1423,7 +1492,7 @@ function rlsPlugin(options) {
|
|
|
1423
1492
|
throw error;
|
|
1424
1493
|
}
|
|
1425
1494
|
}
|
|
1426
|
-
return originalDelete(id);
|
|
1495
|
+
return await originalDelete(id);
|
|
1427
1496
|
},
|
|
1428
1497
|
/**
|
|
1429
1498
|
* Bypass RLS for specific operation
|
|
@@ -1438,7 +1507,7 @@ function rlsPlugin(options) {
|
|
|
1438
1507
|
* ```
|
|
1439
1508
|
*/
|
|
1440
1509
|
async withoutRLS(fn) {
|
|
1441
|
-
return rlsContext.asSystemAsync(fn);
|
|
1510
|
+
return await rlsContext.asSystemAsync(fn);
|
|
1442
1511
|
},
|
|
1443
1512
|
/**
|
|
1444
1513
|
* Check if current user can perform operation on a row
|
|
@@ -1508,7 +1577,18 @@ function createEvaluationContext(rlsCtx, options) {
|
|
|
1508
1577
|
return ctx;
|
|
1509
1578
|
}
|
|
1510
1579
|
function isAsyncFunction(fn) {
|
|
1511
|
-
|
|
1580
|
+
if (!(fn instanceof Function)) {
|
|
1581
|
+
return false;
|
|
1582
|
+
}
|
|
1583
|
+
if (fn.constructor.name === "AsyncFunction") {
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1586
|
+
try {
|
|
1587
|
+
const result = fn();
|
|
1588
|
+
return result instanceof Promise;
|
|
1589
|
+
} catch {
|
|
1590
|
+
return false;
|
|
1591
|
+
}
|
|
1512
1592
|
}
|
|
1513
1593
|
async function safeEvaluate(fn, defaultValue) {
|
|
1514
1594
|
try {
|
|
@@ -1517,7 +1597,7 @@ async function safeEvaluate(fn, defaultValue) {
|
|
|
1517
1597
|
return await result;
|
|
1518
1598
|
}
|
|
1519
1599
|
return result;
|
|
1520
|
-
} catch (
|
|
1600
|
+
} catch (_error) {
|
|
1521
1601
|
return defaultValue;
|
|
1522
1602
|
}
|
|
1523
1603
|
}
|
|
@@ -1559,6 +1639,6 @@ function normalizeOperations(operation) {
|
|
|
1559
1639
|
return [operation];
|
|
1560
1640
|
}
|
|
1561
1641
|
|
|
1562
|
-
export { PolicyRegistry, RLSContextError, RLSContextValidationError, RLSError, RLSErrorCodes, RLSPolicyEvaluationError, RLSPolicyViolation, RLSSchemaError, allow, createEvaluationContext, createRLSContext, deepMerge, defineRLSSchema, deny, filter, hashString, isAsyncFunction, mergeRLSSchemas, normalizeOperations, rlsContext, rlsPlugin, safeEvaluate, validate, withRLSContext, withRLSContextAsync };
|
|
1642
|
+
export { PolicyRegistry, RLSContextError, RLSContextValidationError, RLSError, RLSErrorCodes, RLSPluginOptionsSchema, RLSPolicyEvaluationError, RLSPolicyViolation, RLSSchemaError, allow, createEvaluationContext, createRLSContext, deepMerge, defineRLSSchema, deny, filter, hashString, isAsyncFunction, mergeRLSSchemas, normalizeOperations, rlsContext, rlsPlugin, safeEvaluate, validate, withRLSContext, withRLSContextAsync };
|
|
1563
1643
|
//# sourceMappingURL=index.js.map
|
|
1564
1644
|
//# sourceMappingURL=index.js.map
|