@kysera/rls 0.6.0 → 0.6.1
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 +27 -1
- package/dist/index.d.ts +36 -1
- package/dist/index.js +97 -21
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/errors.ts +82 -0
- package/src/index.ts +1 -0
- package/src/plugin.ts +2 -2
- package/src/transformer/mutation.ts +30 -22
- package/src/transformer/select.ts +18 -3
package/README.md
CHANGED
|
@@ -921,6 +921,7 @@ import {
|
|
|
921
921
|
RLSError,
|
|
922
922
|
RLSContextError,
|
|
923
923
|
RLSPolicyViolation,
|
|
924
|
+
RLSPolicyEvaluationError,
|
|
924
925
|
RLSSchemaError,
|
|
925
926
|
RLSContextValidationError,
|
|
926
927
|
RLSErrorCodes,
|
|
@@ -945,7 +946,7 @@ try {
|
|
|
945
946
|
|
|
946
947
|
#### `RLSPolicyViolation`
|
|
947
948
|
|
|
948
|
-
Thrown when a database operation is denied by RLS policies.
|
|
949
|
+
Thrown when a database operation is denied by RLS policies (legitimate access denial).
|
|
949
950
|
|
|
950
951
|
```typescript
|
|
951
952
|
try {
|
|
@@ -963,6 +964,30 @@ try {
|
|
|
963
964
|
}
|
|
964
965
|
```
|
|
965
966
|
|
|
967
|
+
#### `RLSPolicyEvaluationError`
|
|
968
|
+
|
|
969
|
+
Thrown when a policy fails to evaluate due to an error in the policy code itself (bug in policy implementation). This is distinct from `RLSPolicyViolation`, which represents legitimate access denial.
|
|
970
|
+
|
|
971
|
+
```typescript
|
|
972
|
+
try {
|
|
973
|
+
// Policy throws an error during evaluation
|
|
974
|
+
await orm.posts.findAll();
|
|
975
|
+
} catch (error) {
|
|
976
|
+
if (error instanceof RLSPolicyEvaluationError) {
|
|
977
|
+
console.error('Policy evaluation failed:', {
|
|
978
|
+
operation: error.operation, // 'read'
|
|
979
|
+
table: error.table, // 'posts'
|
|
980
|
+
policyName: error.policyName, // 'tenant_filter'
|
|
981
|
+
originalError: error.originalError, // The underlying error
|
|
982
|
+
message: error.message, // Error message with context
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// Original stack trace is preserved for debugging
|
|
986
|
+
console.error('Stack trace:', error.stack);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
```
|
|
990
|
+
|
|
966
991
|
### Handling Violations
|
|
967
992
|
|
|
968
993
|
```typescript
|
|
@@ -1152,6 +1177,7 @@ export {
|
|
|
1152
1177
|
RLSError,
|
|
1153
1178
|
RLSContextError,
|
|
1154
1179
|
RLSPolicyViolation,
|
|
1180
|
+
RLSPolicyEvaluationError,
|
|
1155
1181
|
RLSSchemaError,
|
|
1156
1182
|
RLSContextValidationError,
|
|
1157
1183
|
RLSErrorCodes,
|
package/dist/index.d.ts
CHANGED
|
@@ -347,6 +347,8 @@ declare const RLSErrorCodes: {
|
|
|
347
347
|
readonly RLS_SCHEMA_INVALID: ErrorCode;
|
|
348
348
|
/** RLS context validation failed */
|
|
349
349
|
readonly RLS_CONTEXT_INVALID: ErrorCode;
|
|
350
|
+
/** RLS policy evaluation threw an error */
|
|
351
|
+
readonly RLS_POLICY_EVALUATION_ERROR: ErrorCode;
|
|
350
352
|
};
|
|
351
353
|
/**
|
|
352
354
|
* Type for RLS error codes
|
|
@@ -469,6 +471,39 @@ declare class RLSPolicyViolation extends RLSError {
|
|
|
469
471
|
constructor(operation: string, table: string, reason: string, policyName?: string);
|
|
470
472
|
toJSON(): Record<string, unknown>;
|
|
471
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Error thrown when a policy condition throws an error during evaluation
|
|
476
|
+
*
|
|
477
|
+
* This error is distinct from RLSPolicyViolation - it indicates a bug in the
|
|
478
|
+
* policy condition function itself, not a legitimate access denial.
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```typescript
|
|
482
|
+
* // A policy with a bug
|
|
483
|
+
* allow('read', ctx => {
|
|
484
|
+
* return ctx.row.someField.value; // Throws if someField is undefined
|
|
485
|
+
* });
|
|
486
|
+
*
|
|
487
|
+
* // This will throw RLSPolicyEvaluationError, not RLSPolicyViolation
|
|
488
|
+
* ```
|
|
489
|
+
*/
|
|
490
|
+
declare class RLSPolicyEvaluationError extends RLSError {
|
|
491
|
+
readonly operation: string;
|
|
492
|
+
readonly table: string;
|
|
493
|
+
readonly policyName?: string;
|
|
494
|
+
readonly originalError?: Error;
|
|
495
|
+
/**
|
|
496
|
+
* Creates a new policy evaluation error
|
|
497
|
+
*
|
|
498
|
+
* @param operation - Database operation being performed
|
|
499
|
+
* @param table - Table name where error occurred
|
|
500
|
+
* @param message - Error message from the policy
|
|
501
|
+
* @param policyName - Name of the policy that threw
|
|
502
|
+
* @param originalError - The original error thrown by the policy
|
|
503
|
+
*/
|
|
504
|
+
constructor(operation: string, table: string, message: string, policyName?: string, originalError?: Error);
|
|
505
|
+
toJSON(): Record<string, unknown>;
|
|
506
|
+
}
|
|
472
507
|
/**
|
|
473
508
|
* Error thrown when RLS schema validation fails
|
|
474
509
|
*
|
|
@@ -702,4 +737,4 @@ declare function hashString(str: string): string;
|
|
|
702
737
|
*/
|
|
703
738
|
declare function normalizeOperations(operation: Operation | Operation[]): Operation[];
|
|
704
739
|
|
|
705
|
-
export { CompiledFilterPolicy, CompiledPolicy, type CreateRLSContextOptions, FilterCondition, Operation, PolicyCondition, PolicyDefinition, PolicyEvaluationContext, PolicyHints, type PolicyOptions, PolicyRegistry, RLSAuthContext, RLSContext, RLSContextError, RLSContextValidationError, RLSError, type RLSErrorCode, RLSErrorCodes, type RLSPluginOptions, RLSPolicyViolation, RLSRequestContext, RLSSchema, RLSSchemaError, TableRLSConfig, allow, createEvaluationContext, createRLSContext, deepMerge, defineRLSSchema, deny, filter, hashString, isAsyncFunction, mergeRLSSchemas, normalizeOperations, rlsContext, rlsPlugin, safeEvaluate, validate, withRLSContext, withRLSContextAsync };
|
|
740
|
+
export { CompiledFilterPolicy, CompiledPolicy, type CreateRLSContextOptions, FilterCondition, Operation, PolicyCondition, PolicyDefinition, PolicyEvaluationContext, PolicyHints, type PolicyOptions, PolicyRegistry, RLSAuthContext, RLSContext, RLSContextError, RLSContextValidationError, RLSError, type RLSErrorCode, RLSErrorCodes, type RLSPluginOptions, RLSPolicyEvaluationError, RLSPolicyViolation, RLSRequestContext, RLSSchema, RLSSchemaError, TableRLSConfig, allow, createEvaluationContext, createRLSContext, deepMerge, defineRLSSchema, deny, filter, hashString, isAsyncFunction, mergeRLSSchemas, normalizeOperations, rlsContext, rlsPlugin, safeEvaluate, validate, withRLSContext, withRLSContextAsync };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { silentLogger } from '@kysera/core';
|
|
2
|
+
import { sql } from 'kysely';
|
|
2
3
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
3
4
|
|
|
4
5
|
// src/errors.ts
|
|
@@ -12,7 +13,9 @@ var RLSErrorCodes = {
|
|
|
12
13
|
/** RLS schema definition is invalid */
|
|
13
14
|
RLS_SCHEMA_INVALID: "RLS_SCHEMA_INVALID",
|
|
14
15
|
/** RLS context validation failed */
|
|
15
|
-
RLS_CONTEXT_INVALID: "RLS_CONTEXT_INVALID"
|
|
16
|
+
RLS_CONTEXT_INVALID: "RLS_CONTEXT_INVALID",
|
|
17
|
+
/** RLS policy evaluation threw an error */
|
|
18
|
+
RLS_POLICY_EVALUATION_ERROR: "RLS_POLICY_EVALUATION_ERROR"
|
|
16
19
|
};
|
|
17
20
|
var RLSError = class extends Error {
|
|
18
21
|
code;
|
|
@@ -110,6 +113,59 @@ var RLSPolicyViolation = class extends RLSError {
|
|
|
110
113
|
return json;
|
|
111
114
|
}
|
|
112
115
|
};
|
|
116
|
+
var RLSPolicyEvaluationError = class extends RLSError {
|
|
117
|
+
operation;
|
|
118
|
+
table;
|
|
119
|
+
policyName;
|
|
120
|
+
originalError;
|
|
121
|
+
/**
|
|
122
|
+
* Creates a new policy evaluation error
|
|
123
|
+
*
|
|
124
|
+
* @param operation - Database operation being performed
|
|
125
|
+
* @param table - Table name where error occurred
|
|
126
|
+
* @param message - Error message from the policy
|
|
127
|
+
* @param policyName - Name of the policy that threw
|
|
128
|
+
* @param originalError - The original error thrown by the policy
|
|
129
|
+
*/
|
|
130
|
+
constructor(operation, table, message, policyName, originalError) {
|
|
131
|
+
super(
|
|
132
|
+
`RLS policy evaluation error during ${operation} on ${table}: ${message}`,
|
|
133
|
+
RLSErrorCodes.RLS_POLICY_EVALUATION_ERROR
|
|
134
|
+
);
|
|
135
|
+
this.name = "RLSPolicyEvaluationError";
|
|
136
|
+
this.operation = operation;
|
|
137
|
+
this.table = table;
|
|
138
|
+
if (policyName !== void 0) {
|
|
139
|
+
this.policyName = policyName;
|
|
140
|
+
}
|
|
141
|
+
if (originalError !== void 0) {
|
|
142
|
+
this.originalError = originalError;
|
|
143
|
+
if (originalError.stack) {
|
|
144
|
+
this.stack = `${this.stack}
|
|
145
|
+
|
|
146
|
+
Caused by:
|
|
147
|
+
${originalError.stack}`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
toJSON() {
|
|
152
|
+
const json = {
|
|
153
|
+
...super.toJSON(),
|
|
154
|
+
operation: this.operation,
|
|
155
|
+
table: this.table
|
|
156
|
+
};
|
|
157
|
+
if (this.policyName !== void 0) {
|
|
158
|
+
json["policyName"] = this.policyName;
|
|
159
|
+
}
|
|
160
|
+
if (this.originalError !== void 0) {
|
|
161
|
+
json["originalError"] = {
|
|
162
|
+
name: this.originalError.name,
|
|
163
|
+
message: this.originalError.message
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return json;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
113
169
|
var RLSSchemaError = class extends RLSError {
|
|
114
170
|
details;
|
|
115
171
|
/**
|
|
@@ -810,6 +866,18 @@ var SelectTransformer = class {
|
|
|
810
866
|
/**
|
|
811
867
|
* Apply filter conditions to query builder
|
|
812
868
|
*
|
|
869
|
+
* NOTE ON TYPE CASTS:
|
|
870
|
+
* The `as any` casts in this method are intentional architectural boundaries.
|
|
871
|
+
* Kysely's type system is designed for static query building where column names
|
|
872
|
+
* are known at compile time. RLS policies work with dynamic column names at runtime.
|
|
873
|
+
*
|
|
874
|
+
* Type safety is maintained through:
|
|
875
|
+
* 1. Policy conditions are validated during schema registration
|
|
876
|
+
* 2. Column names come from policy definitions (developer-controlled)
|
|
877
|
+
* 3. Values are type-checked by the policy condition functions
|
|
878
|
+
*
|
|
879
|
+
* This is the same pattern used in @kysera/repository/table-operations.ts
|
|
880
|
+
*
|
|
813
881
|
* @param qb - Query builder to modify
|
|
814
882
|
* @param conditions - WHERE clause conditions
|
|
815
883
|
* @param table - Table name (for qualified column names)
|
|
@@ -825,7 +893,7 @@ var SelectTransformer = class {
|
|
|
825
893
|
continue;
|
|
826
894
|
} else if (Array.isArray(value)) {
|
|
827
895
|
if (value.length === 0) {
|
|
828
|
-
result = result.where(
|
|
896
|
+
result = result.where(sql`FALSE`);
|
|
829
897
|
} else {
|
|
830
898
|
result = result.where(qualifiedColumn, "in", value);
|
|
831
899
|
}
|
|
@@ -839,8 +907,7 @@ var SelectTransformer = class {
|
|
|
839
907
|
|
|
840
908
|
// src/transformer/mutation.ts
|
|
841
909
|
var MutationGuard = class {
|
|
842
|
-
|
|
843
|
-
constructor(registry, _executor) {
|
|
910
|
+
constructor(registry) {
|
|
844
911
|
this.registry = registry;
|
|
845
912
|
}
|
|
846
913
|
/**
|
|
@@ -852,7 +919,7 @@ var MutationGuard = class {
|
|
|
852
919
|
*
|
|
853
920
|
* @example
|
|
854
921
|
* ```typescript
|
|
855
|
-
* const guard = new MutationGuard(registry
|
|
922
|
+
* const guard = new MutationGuard(registry);
|
|
856
923
|
* await guard.checkCreate('posts', { title: 'Hello', tenant_id: 1 });
|
|
857
924
|
* ```
|
|
858
925
|
*/
|
|
@@ -869,7 +936,7 @@ var MutationGuard = class {
|
|
|
869
936
|
*
|
|
870
937
|
* @example
|
|
871
938
|
* ```typescript
|
|
872
|
-
* const guard = new MutationGuard(registry
|
|
939
|
+
* const guard = new MutationGuard(registry);
|
|
873
940
|
* const existingPost = await db.selectFrom('posts').where('id', '=', 1).selectAll().executeTakeFirst();
|
|
874
941
|
* await guard.checkUpdate('posts', existingPost, { title: 'Updated' });
|
|
875
942
|
* ```
|
|
@@ -886,7 +953,7 @@ var MutationGuard = class {
|
|
|
886
953
|
*
|
|
887
954
|
* @example
|
|
888
955
|
* ```typescript
|
|
889
|
-
* const guard = new MutationGuard(registry
|
|
956
|
+
* const guard = new MutationGuard(registry);
|
|
890
957
|
* const existingPost = await db.selectFrom('posts').where('id', '=', 1).selectAll().executeTakeFirst();
|
|
891
958
|
* await guard.checkDelete('posts', existingPost);
|
|
892
959
|
* ```
|
|
@@ -903,7 +970,7 @@ var MutationGuard = class {
|
|
|
903
970
|
*
|
|
904
971
|
* @example
|
|
905
972
|
* ```typescript
|
|
906
|
-
* const guard = new MutationGuard(registry
|
|
973
|
+
* const guard = new MutationGuard(registry);
|
|
907
974
|
* const post = await db.selectFrom('posts').where('id', '=', 1).selectAll().executeTakeFirst();
|
|
908
975
|
* const canRead = await guard.checkRead('posts', post);
|
|
909
976
|
* ```
|
|
@@ -913,7 +980,7 @@ var MutationGuard = class {
|
|
|
913
980
|
await this.checkMutation(table, "read", row);
|
|
914
981
|
return true;
|
|
915
982
|
} catch (error) {
|
|
916
|
-
if (error instanceof RLSPolicyViolation) {
|
|
983
|
+
if (error instanceof RLSPolicyViolation || error instanceof RLSPolicyEvaluationError) {
|
|
917
984
|
return false;
|
|
918
985
|
}
|
|
919
986
|
throw error;
|
|
@@ -933,7 +1000,7 @@ var MutationGuard = class {
|
|
|
933
1000
|
await this.checkMutation(table, operation, row, data);
|
|
934
1001
|
return true;
|
|
935
1002
|
} catch (error) {
|
|
936
|
-
if (error instanceof RLSPolicyViolation) {
|
|
1003
|
+
if (error instanceof RLSPolicyViolation || error instanceof RLSPolicyEvaluationError) {
|
|
937
1004
|
return false;
|
|
938
1005
|
}
|
|
939
1006
|
throw error;
|
|
@@ -973,7 +1040,7 @@ var MutationGuard = class {
|
|
|
973
1040
|
...ctx.meta !== void 0 && { meta: ctx.meta }
|
|
974
1041
|
};
|
|
975
1042
|
for (const validate2 of validates) {
|
|
976
|
-
const result = await this.evaluatePolicy(validate2.evaluate, evalCtx);
|
|
1043
|
+
const result = await this.evaluatePolicy(validate2.evaluate, evalCtx, validate2.name);
|
|
977
1044
|
if (!result) {
|
|
978
1045
|
return false;
|
|
979
1046
|
}
|
|
@@ -1008,7 +1075,7 @@ var MutationGuard = class {
|
|
|
1008
1075
|
const denies = this.registry.getDenies(table, operation);
|
|
1009
1076
|
for (const deny2 of denies) {
|
|
1010
1077
|
const evalCtx = this.createEvalContext(ctx, table, operation, row, data);
|
|
1011
|
-
const result = await this.evaluatePolicy(deny2.evaluate, evalCtx);
|
|
1078
|
+
const result = await this.evaluatePolicy(deny2.evaluate, evalCtx, deny2.name);
|
|
1012
1079
|
if (result) {
|
|
1013
1080
|
throw new RLSPolicyViolation(
|
|
1014
1081
|
operation,
|
|
@@ -1021,7 +1088,7 @@ var MutationGuard = class {
|
|
|
1021
1088
|
const validates = this.registry.getValidates(table, operation);
|
|
1022
1089
|
for (const validate2 of validates) {
|
|
1023
1090
|
const evalCtx = this.createEvalContext(ctx, table, operation, row, data);
|
|
1024
|
-
const result = await this.evaluatePolicy(validate2.evaluate, evalCtx);
|
|
1091
|
+
const result = await this.evaluatePolicy(validate2.evaluate, evalCtx, validate2.name);
|
|
1025
1092
|
if (!result) {
|
|
1026
1093
|
throw new RLSPolicyViolation(
|
|
1027
1094
|
operation,
|
|
@@ -1044,7 +1111,7 @@ var MutationGuard = class {
|
|
|
1044
1111
|
let allowed = false;
|
|
1045
1112
|
for (const allow2 of allows) {
|
|
1046
1113
|
const evalCtx = this.createEvalContext(ctx, table, operation, row, data);
|
|
1047
|
-
const result = await this.evaluatePolicy(allow2.evaluate, evalCtx);
|
|
1114
|
+
const result = await this.evaluatePolicy(allow2.evaluate, evalCtx, allow2.name);
|
|
1048
1115
|
if (result) {
|
|
1049
1116
|
allowed = true;
|
|
1050
1117
|
break;
|
|
@@ -1074,16 +1141,25 @@ var MutationGuard = class {
|
|
|
1074
1141
|
}
|
|
1075
1142
|
/**
|
|
1076
1143
|
* Evaluate a policy condition
|
|
1144
|
+
*
|
|
1145
|
+
* @param condition - Policy condition function
|
|
1146
|
+
* @param evalCtx - Policy evaluation context
|
|
1147
|
+
* @param policyName - Name of the policy being evaluated (for error reporting)
|
|
1148
|
+
* @returns Boolean result of policy evaluation
|
|
1149
|
+
* @throws RLSPolicyEvaluationError if the policy throws an error
|
|
1077
1150
|
*/
|
|
1078
|
-
async evaluatePolicy(condition, evalCtx) {
|
|
1151
|
+
async evaluatePolicy(condition, evalCtx, policyName) {
|
|
1079
1152
|
try {
|
|
1080
1153
|
const result = condition(evalCtx);
|
|
1081
1154
|
return result instanceof Promise ? await result : result;
|
|
1082
1155
|
} catch (error) {
|
|
1083
|
-
|
|
1156
|
+
const originalError = error instanceof Error ? error : void 0;
|
|
1157
|
+
throw new RLSPolicyEvaluationError(
|
|
1084
1158
|
evalCtx.operation ?? "unknown",
|
|
1085
1159
|
evalCtx.table ?? "unknown",
|
|
1086
|
-
|
|
1160
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
1161
|
+
policyName,
|
|
1162
|
+
originalError
|
|
1087
1163
|
);
|
|
1088
1164
|
}
|
|
1089
1165
|
}
|
|
@@ -1097,7 +1173,7 @@ var MutationGuard = class {
|
|
|
1097
1173
|
*
|
|
1098
1174
|
* @example
|
|
1099
1175
|
* ```typescript
|
|
1100
|
-
* const guard = new MutationGuard(registry
|
|
1176
|
+
* const guard = new MutationGuard(registry);
|
|
1101
1177
|
* const allPosts = await db.selectFrom('posts').selectAll().execute();
|
|
1102
1178
|
* const accessiblePosts = await guard.filterRows('posts', allPosts);
|
|
1103
1179
|
* ```
|
|
@@ -1135,7 +1211,7 @@ function rlsPlugin(options) {
|
|
|
1135
1211
|
/**
|
|
1136
1212
|
* Initialize plugin - compile policies
|
|
1137
1213
|
*/
|
|
1138
|
-
async onInit(
|
|
1214
|
+
async onInit(_executor) {
|
|
1139
1215
|
logger.info?.("[RLS] Initializing RLS plugin", {
|
|
1140
1216
|
tables: Object.keys(schema).length,
|
|
1141
1217
|
skipTables: skipTables.length,
|
|
@@ -1144,7 +1220,7 @@ function rlsPlugin(options) {
|
|
|
1144
1220
|
registry = new PolicyRegistry(schema);
|
|
1145
1221
|
registry.validate();
|
|
1146
1222
|
selectTransformer = new SelectTransformer(registry);
|
|
1147
|
-
mutationGuard = new MutationGuard(registry
|
|
1223
|
+
mutationGuard = new MutationGuard(registry);
|
|
1148
1224
|
logger.info?.("[RLS] RLS plugin initialized successfully");
|
|
1149
1225
|
},
|
|
1150
1226
|
/**
|
|
@@ -1466,6 +1542,6 @@ function normalizeOperations(operation) {
|
|
|
1466
1542
|
return [operation];
|
|
1467
1543
|
}
|
|
1468
1544
|
|
|
1469
|
-
export { PolicyRegistry, RLSContextError, RLSContextValidationError, RLSError, RLSErrorCodes, RLSPolicyViolation, RLSSchemaError, allow, createEvaluationContext, createRLSContext, deepMerge, defineRLSSchema, deny, filter, hashString, isAsyncFunction, mergeRLSSchemas, normalizeOperations, rlsContext, rlsPlugin, safeEvaluate, validate, withRLSContext, withRLSContextAsync };
|
|
1545
|
+
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 };
|
|
1470
1546
|
//# sourceMappingURL=index.js.map
|
|
1471
1547
|
//# sourceMappingURL=index.js.map
|