@kysera/rls 0.8.1 → 0.8.3
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 +819 -3
- package/dist/index.d.ts +2841 -11
- package/dist/index.js +2451 -1
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/{types-Dowjd6zG.d.ts → types-CyqksFKU.d.ts} +72 -1
- package/package.json +11 -6
- package/src/audit/index.ts +25 -0
- package/src/audit/logger.ts +465 -0
- package/src/audit/types.ts +625 -0
- package/src/composition/builder.ts +556 -0
- package/src/composition/index.ts +43 -0
- package/src/composition/types.ts +415 -0
- package/src/field-access/index.ts +38 -0
- package/src/field-access/processor.ts +442 -0
- package/src/field-access/registry.ts +259 -0
- package/src/field-access/types.ts +453 -0
- package/src/index.ts +180 -2
- package/src/policy/builder.ts +187 -10
- package/src/policy/types.ts +84 -0
- package/src/rebac/index.ts +30 -0
- package/src/rebac/registry.ts +303 -0
- package/src/rebac/transformer.ts +391 -0
- package/src/rebac/types.ts +412 -0
- package/src/resolvers/index.ts +30 -0
- package/src/resolvers/manager.ts +507 -0
- package/src/resolvers/types.ts +447 -0
- package/src/testing/index.ts +554 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { R as RLSSchema, O as Operation, P as PolicyCondition, a as PolicyHints, b as
|
|
2
|
-
export {
|
|
1
|
+
import { R as RLSSchema, O as Operation, P as PolicyCondition, a as PolicyHints, b as PolicyActivationCondition, C as ConditionalPolicyDefinition, F as FilterCondition, T as TableRLSConfig, c as PolicyDefinition, d as CompiledPolicy, e as CompiledFilterPolicy, f as RLSContext, g as RLSAuthContext, h as RLSRequestContext, i as PolicyEvaluationContext } from './types-CyqksFKU.js';
|
|
2
|
+
export { k as PolicyActivationContext, j as PolicyType } from './types-CyqksFKU.js';
|
|
3
3
|
import { KyseraLogger, DatabaseError, ErrorCode } from '@kysera/core';
|
|
4
4
|
import { Plugin } from '@kysera/executor';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
-
import 'kysely';
|
|
6
|
+
import { SelectQueryBuilder } from 'kysely';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* RLS schema definition and validation
|
|
@@ -95,6 +95,24 @@ interface PolicyOptions {
|
|
|
95
95
|
priority?: number;
|
|
96
96
|
/** Performance optimization hints */
|
|
97
97
|
hints?: PolicyHints;
|
|
98
|
+
/**
|
|
99
|
+
* Condition that determines if this policy is active
|
|
100
|
+
* The policy will only be evaluated if this returns true
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* // Only apply in production
|
|
105
|
+
* allow('read', () => true, {
|
|
106
|
+
* condition: ctx => ctx.meta?.environment === 'production'
|
|
107
|
+
* })
|
|
108
|
+
*
|
|
109
|
+
* // Feature-gated policy
|
|
110
|
+
* filter('read', ctx => ({ strict: true }), {
|
|
111
|
+
* condition: ctx => ctx.meta?.features?.strictMode
|
|
112
|
+
* })
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
condition?: PolicyActivationCondition;
|
|
98
116
|
}
|
|
99
117
|
/**
|
|
100
118
|
* Create an allow policy
|
|
@@ -118,7 +136,7 @@ interface PolicyOptions {
|
|
|
118
136
|
* })
|
|
119
137
|
* ```
|
|
120
138
|
*/
|
|
121
|
-
declare function allow(operation: Operation | Operation[], condition: PolicyCondition, options?: PolicyOptions):
|
|
139
|
+
declare function allow(operation: Operation | Operation[], condition: PolicyCondition, options?: PolicyOptions): ConditionalPolicyDefinition;
|
|
122
140
|
/**
|
|
123
141
|
* Create a deny policy
|
|
124
142
|
* Blocks access when condition evaluates to true (overrides allow)
|
|
@@ -142,7 +160,7 @@ declare function allow(operation: Operation | Operation[], condition: PolicyCond
|
|
|
142
160
|
* })
|
|
143
161
|
* ```
|
|
144
162
|
*/
|
|
145
|
-
declare function deny(operation: Operation | Operation[], condition?: PolicyCondition, options?: PolicyOptions):
|
|
163
|
+
declare function deny(operation: Operation | Operation[], condition?: PolicyCondition, options?: PolicyOptions): ConditionalPolicyDefinition;
|
|
146
164
|
/**
|
|
147
165
|
* Create a filter policy
|
|
148
166
|
* Adds WHERE conditions to SELECT queries
|
|
@@ -179,7 +197,7 @@ declare function deny(operation: Operation | Operation[], condition?: PolicyCond
|
|
|
179
197
|
* })
|
|
180
198
|
* ```
|
|
181
199
|
*/
|
|
182
|
-
declare function filter(operation: 'read' | 'all', condition: FilterCondition, options?: PolicyOptions):
|
|
200
|
+
declare function filter(operation: 'read' | 'all', condition: FilterCondition, options?: PolicyOptions): ConditionalPolicyDefinition;
|
|
183
201
|
/**
|
|
184
202
|
* Create a validate policy
|
|
185
203
|
* Validates mutation data before execution
|
|
@@ -204,7 +222,78 @@ declare function filter(operation: 'read' | 'all', condition: FilterCondition, o
|
|
|
204
222
|
* })
|
|
205
223
|
* ```
|
|
206
224
|
*/
|
|
207
|
-
declare function validate(operation: 'create' | 'update' | 'all', condition: PolicyCondition, options?: PolicyOptions):
|
|
225
|
+
declare function validate(operation: 'create' | 'update' | 'all', condition: PolicyCondition, options?: PolicyOptions): ConditionalPolicyDefinition;
|
|
226
|
+
/**
|
|
227
|
+
* Create a policy that is only active in specific environments
|
|
228
|
+
*
|
|
229
|
+
* @param environments - Environments where the policy is active
|
|
230
|
+
* @param policyFn - Function that creates the policy
|
|
231
|
+
* @returns Policy with environment condition
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```typescript
|
|
235
|
+
* // Policy only active in production
|
|
236
|
+
* const prodPolicy = whenEnvironment(['production'], () =>
|
|
237
|
+
* allow('read', () => true, { name: 'prod-read' })
|
|
238
|
+
* );
|
|
239
|
+
*
|
|
240
|
+
* // Policy active in staging and production
|
|
241
|
+
* const nonDevPolicy = whenEnvironment(['staging', 'production'], () =>
|
|
242
|
+
* filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }))
|
|
243
|
+
* );
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
declare function whenEnvironment(environments: string[], policyFn: () => ConditionalPolicyDefinition): ConditionalPolicyDefinition;
|
|
247
|
+
/**
|
|
248
|
+
* Create a policy that is only active when a feature flag is enabled
|
|
249
|
+
*
|
|
250
|
+
* @param feature - Feature flag name
|
|
251
|
+
* @param policyFn - Function that creates the policy
|
|
252
|
+
* @returns Policy with feature flag condition
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* // Policy only active when 'strict_rls' feature is enabled
|
|
257
|
+
* const strictPolicy = whenFeature('strict_rls', () =>
|
|
258
|
+
* deny('delete', () => true, { name: 'strict-no-delete' })
|
|
259
|
+
* );
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
declare function whenFeature(feature: string, policyFn: () => ConditionalPolicyDefinition): ConditionalPolicyDefinition;
|
|
263
|
+
/**
|
|
264
|
+
* Create a policy that is only active during specific hours
|
|
265
|
+
*
|
|
266
|
+
* @param startHour - Start hour (0-23)
|
|
267
|
+
* @param endHour - End hour (0-23)
|
|
268
|
+
* @param policyFn - Function that creates the policy
|
|
269
|
+
* @returns Policy with time-based condition
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```typescript
|
|
273
|
+
* // Policy only active during business hours (9 AM - 5 PM)
|
|
274
|
+
* const businessHoursPolicy = whenTimeRange(9, 17, () =>
|
|
275
|
+
* allow('update', () => true, { name: 'business-hours-update' })
|
|
276
|
+
* );
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
declare function whenTimeRange(startHour: number, endHour: number, policyFn: () => ConditionalPolicyDefinition): ConditionalPolicyDefinition;
|
|
280
|
+
/**
|
|
281
|
+
* Create a policy that is only active when a custom condition is met
|
|
282
|
+
*
|
|
283
|
+
* @param condition - Custom activation condition
|
|
284
|
+
* @param policyFn - Function that creates the policy
|
|
285
|
+
* @returns Policy with custom condition
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```typescript
|
|
289
|
+
* // Policy only active when user is in beta program
|
|
290
|
+
* const betaPolicy = whenCondition(
|
|
291
|
+
* ctx => ctx.meta?.betaUser === true,
|
|
292
|
+
* () => allow('read', () => true, { name: 'beta-read' })
|
|
293
|
+
* );
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
declare function whenCondition(condition: PolicyActivationCondition, policyFn: () => ConditionalPolicyDefinition): ConditionalPolicyDefinition;
|
|
208
297
|
|
|
209
298
|
/**
|
|
210
299
|
* Policy Registry
|
|
@@ -630,13 +719,27 @@ interface RLSPluginOptions<DB = unknown> {
|
|
|
630
719
|
* Note: 'schema' and 'onViolation' are not included as they are complex runtime objects.
|
|
631
720
|
*/
|
|
632
721
|
declare const RLSPluginOptionsSchema: z.ZodObject<{
|
|
633
|
-
excludeTables: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
634
|
-
bypassRoles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
722
|
+
excludeTables: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
723
|
+
bypassRoles: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
635
724
|
requireContext: z.ZodOptional<z.ZodBoolean>;
|
|
636
725
|
allowUnfilteredQueries: z.ZodOptional<z.ZodBoolean>;
|
|
637
726
|
auditDecisions: z.ZodOptional<z.ZodBoolean>;
|
|
638
727
|
primaryKeyColumn: z.ZodOptional<z.ZodString>;
|
|
639
|
-
}, z.
|
|
728
|
+
}, "strip", z.ZodTypeAny, {
|
|
729
|
+
excludeTables?: string[] | undefined;
|
|
730
|
+
bypassRoles?: string[] | undefined;
|
|
731
|
+
requireContext?: boolean | undefined;
|
|
732
|
+
allowUnfilteredQueries?: boolean | undefined;
|
|
733
|
+
auditDecisions?: boolean | undefined;
|
|
734
|
+
primaryKeyColumn?: string | undefined;
|
|
735
|
+
}, {
|
|
736
|
+
excludeTables?: string[] | undefined;
|
|
737
|
+
bypassRoles?: string[] | undefined;
|
|
738
|
+
requireContext?: boolean | undefined;
|
|
739
|
+
allowUnfilteredQueries?: boolean | undefined;
|
|
740
|
+
auditDecisions?: boolean | undefined;
|
|
741
|
+
primaryKeyColumn?: string | undefined;
|
|
742
|
+
}>;
|
|
640
743
|
/**
|
|
641
744
|
* Create RLS plugin for Kysera
|
|
642
745
|
*
|
|
@@ -806,4 +909,2731 @@ declare function hashString(str: string): string;
|
|
|
806
909
|
*/
|
|
807
910
|
declare function normalizeOperations(operation: Operation | Operation[]): Operation[];
|
|
808
911
|
|
|
809
|
-
|
|
912
|
+
/**
|
|
913
|
+
* Context Resolver Types
|
|
914
|
+
*
|
|
915
|
+
* Provides infrastructure for pre-resolving async data before RLS policy evaluation.
|
|
916
|
+
* This allows synchronous filters to access data that would otherwise require async lookups.
|
|
917
|
+
*
|
|
918
|
+
* @module @kysera/rls/resolvers/types
|
|
919
|
+
*/
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Base interface for resolved data that can be added to RLS context
|
|
923
|
+
*
|
|
924
|
+
* @example
|
|
925
|
+
* ```typescript
|
|
926
|
+
* interface MyResolvedData extends ResolvedData {
|
|
927
|
+
* organizationIds: string[];
|
|
928
|
+
* permissions: Set<string>;
|
|
929
|
+
* employeeRoles: Map<string, string[]>;
|
|
930
|
+
* }
|
|
931
|
+
* ```
|
|
932
|
+
*/
|
|
933
|
+
interface ResolvedData {
|
|
934
|
+
/**
|
|
935
|
+
* Timestamp when data was resolved
|
|
936
|
+
* Used for cache validation
|
|
937
|
+
*/
|
|
938
|
+
resolvedAt: Date;
|
|
939
|
+
/**
|
|
940
|
+
* Cache key used for this resolution (if cached)
|
|
941
|
+
*/
|
|
942
|
+
cacheKey?: string;
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Extended auth context with pre-resolved data
|
|
946
|
+
*
|
|
947
|
+
* @typeParam TUser - Custom user type
|
|
948
|
+
* @typeParam TResolved - Type of pre-resolved data
|
|
949
|
+
*
|
|
950
|
+
* @example
|
|
951
|
+
* ```typescript
|
|
952
|
+
* interface OrgPermissions extends ResolvedData {
|
|
953
|
+
* organizationIds: string[];
|
|
954
|
+
* orgPermissions: Map<string, Set<string>>;
|
|
955
|
+
* isOrgOwner: (orgId: string) => boolean;
|
|
956
|
+
* hasOrgPermission: (orgId: string, permission: string) => boolean;
|
|
957
|
+
* }
|
|
958
|
+
*
|
|
959
|
+
* type EnhancedAuth = EnhancedRLSAuthContext<User, OrgPermissions>;
|
|
960
|
+
*
|
|
961
|
+
* // Use in policy
|
|
962
|
+
* filter('read', ctx => ({
|
|
963
|
+
* organization_id: ctx.auth.resolved.organizationIds
|
|
964
|
+
* }));
|
|
965
|
+
* ```
|
|
966
|
+
*/
|
|
967
|
+
interface EnhancedRLSAuthContext<TUser = unknown, TResolved extends ResolvedData = ResolvedData> extends RLSAuthContext<TUser> {
|
|
968
|
+
/**
|
|
969
|
+
* Pre-resolved data available synchronously in policies
|
|
970
|
+
*
|
|
971
|
+
* This data is populated by ContextResolvers before entering the RLS context.
|
|
972
|
+
* Use this for async data lookups that policies need synchronously.
|
|
973
|
+
*/
|
|
974
|
+
resolved: TResolved;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Extended RLS context with enhanced auth containing resolved data
|
|
978
|
+
*
|
|
979
|
+
* @typeParam TUser - Custom user type
|
|
980
|
+
* @typeParam TResolved - Type of pre-resolved data
|
|
981
|
+
* @typeParam TMeta - Custom metadata type
|
|
982
|
+
*/
|
|
983
|
+
interface EnhancedRLSContext<TUser = unknown, TResolved extends ResolvedData = ResolvedData, TMeta = unknown> extends Omit<RLSContext<TUser, TMeta>, 'auth'> {
|
|
984
|
+
auth: EnhancedRLSAuthContext<TUser, TResolved>;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Base context for resolver input (before resolution)
|
|
988
|
+
*/
|
|
989
|
+
interface BaseResolverContext {
|
|
990
|
+
auth: {
|
|
991
|
+
userId: string | number;
|
|
992
|
+
roles: string[];
|
|
993
|
+
tenantId?: string | number;
|
|
994
|
+
organizationIds?: (string | number)[];
|
|
995
|
+
permissions?: string[];
|
|
996
|
+
attributes?: Record<string, unknown>;
|
|
997
|
+
isSystem?: boolean;
|
|
998
|
+
};
|
|
999
|
+
timestamp: Date;
|
|
1000
|
+
meta?: unknown;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Context resolver that enriches base context with pre-resolved data
|
|
1004
|
+
*
|
|
1005
|
+
* Resolvers are responsible for fetching async data and making it available
|
|
1006
|
+
* synchronously in policy evaluation contexts.
|
|
1007
|
+
*
|
|
1008
|
+
* @typeParam TResolved - Type of resolved data this resolver produces
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* ```typescript
|
|
1012
|
+
* const orgPermissionResolver: ContextResolver<OrgPermissions> = {
|
|
1013
|
+
* name: 'org-permissions',
|
|
1014
|
+
*
|
|
1015
|
+
* async resolve(base) {
|
|
1016
|
+
* const employments = await db.selectFrom('employees')
|
|
1017
|
+
* .where('user_id', '=', base.auth.userId)
|
|
1018
|
+
* .where('status', '=', 'active')
|
|
1019
|
+
* .execute();
|
|
1020
|
+
*
|
|
1021
|
+
* const orgPermissions = new Map<string, Set<string>>();
|
|
1022
|
+
* // ... resolve permissions ...
|
|
1023
|
+
*
|
|
1024
|
+
* return {
|
|
1025
|
+
* resolvedAt: new Date(),
|
|
1026
|
+
* organizationIds: employments.map(e => e.organization_id),
|
|
1027
|
+
* orgPermissions,
|
|
1028
|
+
* isOrgOwner: (orgId) => employments.some(e => e.organization_id === orgId && e.is_owner),
|
|
1029
|
+
* hasOrgPermission: (orgId, permission) => {
|
|
1030
|
+
* const perms = orgPermissions.get(orgId);
|
|
1031
|
+
* return perms?.has('*') || perms?.has(permission) || false;
|
|
1032
|
+
* }
|
|
1033
|
+
* };
|
|
1034
|
+
* },
|
|
1035
|
+
*
|
|
1036
|
+
* cacheKey: (base) => `rls:org-perms:${base.auth.userId}`,
|
|
1037
|
+
* cacheTtl: 300 // 5 minutes
|
|
1038
|
+
* };
|
|
1039
|
+
* ```
|
|
1040
|
+
*/
|
|
1041
|
+
interface ContextResolver<TResolved extends ResolvedData = ResolvedData> {
|
|
1042
|
+
/**
|
|
1043
|
+
* Unique name for this resolver
|
|
1044
|
+
* Used for logging and debugging
|
|
1045
|
+
*/
|
|
1046
|
+
name: string;
|
|
1047
|
+
/**
|
|
1048
|
+
* Resolve async data for the context
|
|
1049
|
+
*
|
|
1050
|
+
* @param base - Base context with user info
|
|
1051
|
+
* @returns Pre-resolved data to be added to context
|
|
1052
|
+
*/
|
|
1053
|
+
resolve(base: BaseResolverContext): Promise<TResolved>;
|
|
1054
|
+
/**
|
|
1055
|
+
* Generate cache key for this context
|
|
1056
|
+
* Return undefined to disable caching for this resolver
|
|
1057
|
+
*
|
|
1058
|
+
* @param base - Base context
|
|
1059
|
+
* @returns Cache key string or undefined
|
|
1060
|
+
*/
|
|
1061
|
+
cacheKey?(base: BaseResolverContext): string | undefined;
|
|
1062
|
+
/**
|
|
1063
|
+
* Cache TTL in seconds
|
|
1064
|
+
* @default 300 (5 minutes)
|
|
1065
|
+
*/
|
|
1066
|
+
cacheTtl?: number;
|
|
1067
|
+
/**
|
|
1068
|
+
* Whether this resolver is required
|
|
1069
|
+
* If true, resolution failure will throw an error
|
|
1070
|
+
* If false, the resolver will be skipped on failure
|
|
1071
|
+
*
|
|
1072
|
+
* @default true
|
|
1073
|
+
*/
|
|
1074
|
+
required?: boolean;
|
|
1075
|
+
/**
|
|
1076
|
+
* Dependencies on other resolvers (by name)
|
|
1077
|
+
* This resolver will wait for dependencies to complete first
|
|
1078
|
+
*/
|
|
1079
|
+
dependsOn?: string[];
|
|
1080
|
+
/**
|
|
1081
|
+
* Priority for resolver execution order (higher = earlier)
|
|
1082
|
+
* @default 0
|
|
1083
|
+
*/
|
|
1084
|
+
priority?: number;
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Combined result of multiple resolvers
|
|
1088
|
+
*
|
|
1089
|
+
* @typeParam T - Union type of all resolved data types
|
|
1090
|
+
*/
|
|
1091
|
+
interface CompositeResolvedData<T extends Record<string, ResolvedData>> extends ResolvedData {
|
|
1092
|
+
/**
|
|
1093
|
+
* Individual resolver results keyed by resolver name
|
|
1094
|
+
*/
|
|
1095
|
+
resolvers: T;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Cache provider interface for storing resolved context data
|
|
1099
|
+
*/
|
|
1100
|
+
interface ResolverCacheProvider {
|
|
1101
|
+
/**
|
|
1102
|
+
* Get cached data
|
|
1103
|
+
* @param key - Cache key
|
|
1104
|
+
* @returns Cached data or null if not found/expired
|
|
1105
|
+
*/
|
|
1106
|
+
get<T>(key: string): Promise<T | null>;
|
|
1107
|
+
/**
|
|
1108
|
+
* Set cached data
|
|
1109
|
+
* @param key - Cache key
|
|
1110
|
+
* @param value - Data to cache
|
|
1111
|
+
* @param ttlSeconds - Time to live in seconds
|
|
1112
|
+
*/
|
|
1113
|
+
set<T>(key: string, value: T, ttlSeconds: number): Promise<void>;
|
|
1114
|
+
/**
|
|
1115
|
+
* Delete cached data
|
|
1116
|
+
* @param key - Cache key
|
|
1117
|
+
*/
|
|
1118
|
+
delete(key: string): Promise<void>;
|
|
1119
|
+
/**
|
|
1120
|
+
* Delete all cached data matching a pattern
|
|
1121
|
+
* @param pattern - Pattern to match (e.g., "rls:org-perms:*")
|
|
1122
|
+
*/
|
|
1123
|
+
deletePattern?(pattern: string): Promise<void>;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* In-memory cache provider implementation
|
|
1127
|
+
*
|
|
1128
|
+
* Suitable for single-instance deployments or testing.
|
|
1129
|
+
* For distributed systems, use a Redis-based provider.
|
|
1130
|
+
*/
|
|
1131
|
+
declare class InMemoryCacheProvider implements ResolverCacheProvider {
|
|
1132
|
+
private cache;
|
|
1133
|
+
get<T>(key: string): Promise<T | null>;
|
|
1134
|
+
set<T>(key: string, value: T, ttlSeconds: number): Promise<void>;
|
|
1135
|
+
delete(key: string): Promise<void>;
|
|
1136
|
+
deletePattern(pattern: string): Promise<void>;
|
|
1137
|
+
/**
|
|
1138
|
+
* Clear all cached entries
|
|
1139
|
+
*/
|
|
1140
|
+
clear(): void;
|
|
1141
|
+
/**
|
|
1142
|
+
* Get current cache size
|
|
1143
|
+
*/
|
|
1144
|
+
get size(): number;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Options for ResolverManager
|
|
1148
|
+
*/
|
|
1149
|
+
interface ResolverManagerOptions {
|
|
1150
|
+
/**
|
|
1151
|
+
* Cache provider for storing resolved data
|
|
1152
|
+
* @default InMemoryCacheProvider
|
|
1153
|
+
*/
|
|
1154
|
+
cacheProvider?: ResolverCacheProvider;
|
|
1155
|
+
/**
|
|
1156
|
+
* Default cache TTL in seconds
|
|
1157
|
+
* @default 300 (5 minutes)
|
|
1158
|
+
*/
|
|
1159
|
+
defaultCacheTtl?: number;
|
|
1160
|
+
/**
|
|
1161
|
+
* Whether to run resolvers in parallel when possible
|
|
1162
|
+
* @default true
|
|
1163
|
+
*/
|
|
1164
|
+
parallelResolution?: boolean;
|
|
1165
|
+
/**
|
|
1166
|
+
* Maximum time (ms) to wait for a single resolver
|
|
1167
|
+
* @default 5000 (5 seconds)
|
|
1168
|
+
*/
|
|
1169
|
+
resolverTimeout?: number;
|
|
1170
|
+
/**
|
|
1171
|
+
* Logger for resolver operations
|
|
1172
|
+
*/
|
|
1173
|
+
logger?: {
|
|
1174
|
+
debug?: (message: string, context?: Record<string, unknown>) => void;
|
|
1175
|
+
info?: (message: string, context?: Record<string, unknown>) => void;
|
|
1176
|
+
warn?: (message: string, context?: Record<string, unknown>) => void;
|
|
1177
|
+
error?: (message: string, context?: Record<string, unknown>) => void;
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Common resolved data for organization-based permissions
|
|
1182
|
+
*
|
|
1183
|
+
* Pre-built pattern for multi-organization systems where users can
|
|
1184
|
+
* belong to multiple organizations with different roles/permissions.
|
|
1185
|
+
*/
|
|
1186
|
+
interface OrganizationResolvedData extends ResolvedData {
|
|
1187
|
+
/**
|
|
1188
|
+
* List of organization IDs the user belongs to
|
|
1189
|
+
*/
|
|
1190
|
+
organizationIds: (string | number)[];
|
|
1191
|
+
/**
|
|
1192
|
+
* Map of organization ID to user's permissions in that org
|
|
1193
|
+
*/
|
|
1194
|
+
orgPermissions: Map<string | number, Set<string>>;
|
|
1195
|
+
/**
|
|
1196
|
+
* Map of organization ID to user's roles in that org
|
|
1197
|
+
*/
|
|
1198
|
+
orgRoles: Map<string | number, string[]>;
|
|
1199
|
+
/**
|
|
1200
|
+
* Check if user is owner of an organization
|
|
1201
|
+
* @param orgId - Organization ID
|
|
1202
|
+
*/
|
|
1203
|
+
isOrgOwner(orgId: string | number): boolean;
|
|
1204
|
+
/**
|
|
1205
|
+
* Check if user has a specific permission in an organization
|
|
1206
|
+
* @param orgId - Organization ID
|
|
1207
|
+
* @param permission - Permission to check
|
|
1208
|
+
*/
|
|
1209
|
+
hasOrgPermission(orgId: string | number, permission: string): boolean;
|
|
1210
|
+
/**
|
|
1211
|
+
* Check if user has a specific role in an organization
|
|
1212
|
+
* @param orgId - Organization ID
|
|
1213
|
+
* @param role - Role to check
|
|
1214
|
+
*/
|
|
1215
|
+
hasOrgRole(orgId: string | number, role: string): boolean;
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Common resolved data for tenant-based systems
|
|
1219
|
+
*/
|
|
1220
|
+
interface TenantResolvedData extends ResolvedData {
|
|
1221
|
+
/**
|
|
1222
|
+
* Current tenant ID (resolved from user context)
|
|
1223
|
+
*/
|
|
1224
|
+
tenantId: string | number;
|
|
1225
|
+
/**
|
|
1226
|
+
* Tenant-specific settings/restrictions
|
|
1227
|
+
*/
|
|
1228
|
+
tenantSettings?: Record<string, unknown>;
|
|
1229
|
+
/**
|
|
1230
|
+
* Tenant-specific feature flags
|
|
1231
|
+
*/
|
|
1232
|
+
tenantFeatures?: Set<string>;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Common resolved data for hierarchical permissions
|
|
1236
|
+
*
|
|
1237
|
+
* For systems with resource hierarchies (e.g., team -> project -> task)
|
|
1238
|
+
*/
|
|
1239
|
+
interface HierarchyResolvedData extends ResolvedData {
|
|
1240
|
+
/**
|
|
1241
|
+
* Resources the user has direct access to
|
|
1242
|
+
*/
|
|
1243
|
+
directAccess: Set<string>;
|
|
1244
|
+
/**
|
|
1245
|
+
* Resources the user has inherited access to (through hierarchy)
|
|
1246
|
+
*/
|
|
1247
|
+
inheritedAccess: Set<string>;
|
|
1248
|
+
/**
|
|
1249
|
+
* Check if user can access a resource (direct or inherited)
|
|
1250
|
+
* @param resourceId - Resource ID
|
|
1251
|
+
*/
|
|
1252
|
+
canAccess(resourceId: string): boolean;
|
|
1253
|
+
/**
|
|
1254
|
+
* Get the access level for a resource
|
|
1255
|
+
* @param resourceId - Resource ID
|
|
1256
|
+
* @returns Access level or null if no access
|
|
1257
|
+
*/
|
|
1258
|
+
getAccessLevel(resourceId: string): string | null;
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Combined resolved data type for common use cases
|
|
1262
|
+
*/
|
|
1263
|
+
type CommonResolvedData = OrganizationResolvedData & TenantResolvedData;
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Context Resolver Manager
|
|
1267
|
+
*
|
|
1268
|
+
* Orchestrates the resolution of context data from multiple resolvers,
|
|
1269
|
+
* handling caching, dependencies, and parallel execution.
|
|
1270
|
+
*
|
|
1271
|
+
* @module @kysera/rls/resolvers/manager
|
|
1272
|
+
*/
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Manages context resolvers and orchestrates context resolution
|
|
1276
|
+
*
|
|
1277
|
+
* The ResolverManager is responsible for:
|
|
1278
|
+
* - Registering and organizing resolvers
|
|
1279
|
+
* - Resolving context data in the correct order (respecting dependencies)
|
|
1280
|
+
* - Caching resolved data
|
|
1281
|
+
* - Handling resolver failures
|
|
1282
|
+
*
|
|
1283
|
+
* @example
|
|
1284
|
+
* ```typescript
|
|
1285
|
+
* const manager = new ResolverManager({
|
|
1286
|
+
* cacheProvider: new RedisCacheProvider(redis),
|
|
1287
|
+
* defaultCacheTtl: 300,
|
|
1288
|
+
* parallelResolution: true
|
|
1289
|
+
* });
|
|
1290
|
+
*
|
|
1291
|
+
* // Register resolvers
|
|
1292
|
+
* manager.register(orgPermissionResolver);
|
|
1293
|
+
* manager.register(tenantSettingsResolver);
|
|
1294
|
+
*
|
|
1295
|
+
* // Resolve context
|
|
1296
|
+
* const enhancedCtx = await manager.resolve({
|
|
1297
|
+
* auth: { userId: '123', roles: ['user'] },
|
|
1298
|
+
* timestamp: new Date()
|
|
1299
|
+
* });
|
|
1300
|
+
*
|
|
1301
|
+
* // Use in RLS
|
|
1302
|
+
* await rlsContext.runAsync(enhancedCtx, async () => {
|
|
1303
|
+
* // Policies can access resolved data synchronously
|
|
1304
|
+
* });
|
|
1305
|
+
* ```
|
|
1306
|
+
*/
|
|
1307
|
+
declare class ResolverManager<TResolved extends ResolvedData = ResolvedData> {
|
|
1308
|
+
private resolvers;
|
|
1309
|
+
private cacheProvider;
|
|
1310
|
+
private defaultCacheTtl;
|
|
1311
|
+
private parallelResolution;
|
|
1312
|
+
private resolverTimeout;
|
|
1313
|
+
private logger;
|
|
1314
|
+
constructor(options?: ResolverManagerOptions);
|
|
1315
|
+
/**
|
|
1316
|
+
* Register a context resolver
|
|
1317
|
+
*
|
|
1318
|
+
* @param resolver - Resolver to register
|
|
1319
|
+
* @throws RLSError if resolver with same name already exists
|
|
1320
|
+
*/
|
|
1321
|
+
register<T extends ResolvedData>(resolver: ContextResolver<T>): void;
|
|
1322
|
+
/**
|
|
1323
|
+
* Unregister a context resolver
|
|
1324
|
+
*
|
|
1325
|
+
* @param name - Name of resolver to unregister
|
|
1326
|
+
* @returns true if resolver was removed, false if it didn't exist
|
|
1327
|
+
*/
|
|
1328
|
+
unregister(name: string): boolean;
|
|
1329
|
+
/**
|
|
1330
|
+
* Check if a resolver is registered
|
|
1331
|
+
*
|
|
1332
|
+
* @param name - Resolver name
|
|
1333
|
+
*/
|
|
1334
|
+
hasResolver(name: string): boolean;
|
|
1335
|
+
/**
|
|
1336
|
+
* Get all registered resolver names
|
|
1337
|
+
*/
|
|
1338
|
+
getResolverNames(): string[];
|
|
1339
|
+
/**
|
|
1340
|
+
* Resolve context data using all registered resolvers
|
|
1341
|
+
*
|
|
1342
|
+
* @param baseContext - Base context to resolve
|
|
1343
|
+
* @returns Enhanced context with resolved data
|
|
1344
|
+
*
|
|
1345
|
+
* @example
|
|
1346
|
+
* ```typescript
|
|
1347
|
+
* const baseCtx = {
|
|
1348
|
+
* auth: { userId: '123', roles: ['user'], tenantId: 'acme' },
|
|
1349
|
+
* timestamp: new Date()
|
|
1350
|
+
* };
|
|
1351
|
+
*
|
|
1352
|
+
* const enhancedCtx = await manager.resolve(baseCtx);
|
|
1353
|
+
* // enhancedCtx.auth.resolved contains all resolved data
|
|
1354
|
+
* ```
|
|
1355
|
+
*/
|
|
1356
|
+
resolve(baseContext: BaseResolverContext): Promise<EnhancedRLSContext<unknown, TResolved>>;
|
|
1357
|
+
/**
|
|
1358
|
+
* Resolve a single resolver (useful for partial updates)
|
|
1359
|
+
*
|
|
1360
|
+
* @param name - Resolver name
|
|
1361
|
+
* @param baseContext - Base context
|
|
1362
|
+
* @returns Resolved data from the specific resolver
|
|
1363
|
+
*/
|
|
1364
|
+
resolveOne<T extends ResolvedData>(name: string, baseContext: BaseResolverContext): Promise<T | null>;
|
|
1365
|
+
/**
|
|
1366
|
+
* Invalidate cached data for a user
|
|
1367
|
+
*
|
|
1368
|
+
* @param userId - User ID whose cache should be invalidated
|
|
1369
|
+
* @param resolverName - Optional specific resolver to invalidate
|
|
1370
|
+
*/
|
|
1371
|
+
invalidateCache(userId: string | number, resolverName?: string): Promise<void>;
|
|
1372
|
+
/**
|
|
1373
|
+
* Clear all cached data
|
|
1374
|
+
*/
|
|
1375
|
+
clearCache(): Promise<void>;
|
|
1376
|
+
/**
|
|
1377
|
+
* Get resolvers in dependency order (topological sort)
|
|
1378
|
+
*/
|
|
1379
|
+
private getResolverOrder;
|
|
1380
|
+
/**
|
|
1381
|
+
* Resolve resolvers sequentially
|
|
1382
|
+
*/
|
|
1383
|
+
private resolveSequential;
|
|
1384
|
+
/**
|
|
1385
|
+
* Resolve resolvers in parallel (respecting dependencies)
|
|
1386
|
+
*/
|
|
1387
|
+
private resolveParallel;
|
|
1388
|
+
/**
|
|
1389
|
+
* Resolve a single resolver with caching
|
|
1390
|
+
*/
|
|
1391
|
+
private resolveWithCache;
|
|
1392
|
+
/**
|
|
1393
|
+
* Execute a promise with timeout
|
|
1394
|
+
*/
|
|
1395
|
+
private withTimeout;
|
|
1396
|
+
/**
|
|
1397
|
+
* Merge resolved data from multiple resolvers
|
|
1398
|
+
*/
|
|
1399
|
+
private mergeResolvedData;
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Create a context resolver manager with common defaults
|
|
1403
|
+
*
|
|
1404
|
+
* @param options - Manager options
|
|
1405
|
+
* @returns Configured ResolverManager
|
|
1406
|
+
*/
|
|
1407
|
+
declare function createResolverManager<TResolved extends ResolvedData = ResolvedData>(options?: ResolverManagerOptions): ResolverManager<TResolved>;
|
|
1408
|
+
/**
|
|
1409
|
+
* Helper to create a context resolver
|
|
1410
|
+
*
|
|
1411
|
+
* @param config - Resolver configuration
|
|
1412
|
+
* @returns ContextResolver instance
|
|
1413
|
+
*
|
|
1414
|
+
* @example
|
|
1415
|
+
* ```typescript
|
|
1416
|
+
* const resolver = createResolver({
|
|
1417
|
+
* name: 'org-permissions',
|
|
1418
|
+
* resolve: async (base) => {
|
|
1419
|
+
* const orgs = await getEmployeeOrganizations(base.auth.userId);
|
|
1420
|
+
* return {
|
|
1421
|
+
* resolvedAt: new Date(),
|
|
1422
|
+
* organizationIds: orgs.map(o => o.id)
|
|
1423
|
+
* };
|
|
1424
|
+
* },
|
|
1425
|
+
* cacheKey: (base) => `rls:org:${base.auth.userId}`,
|
|
1426
|
+
* cacheTtl: 300
|
|
1427
|
+
* });
|
|
1428
|
+
* ```
|
|
1429
|
+
*/
|
|
1430
|
+
declare function createResolver<TResolved extends ResolvedData>(config: ContextResolver<TResolved>): ContextResolver<TResolved>;
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* ReBAC (Relationship-Based Access Control) Types
|
|
1434
|
+
*
|
|
1435
|
+
* Provides type definitions for relationship-based filtering in RLS policies.
|
|
1436
|
+
* ReBAC allows policies to be defined based on relationships between entities
|
|
1437
|
+
* in the database, enabling complex access control patterns like
|
|
1438
|
+
* "show products if user is employee of product's shop's organization".
|
|
1439
|
+
*
|
|
1440
|
+
* @module @kysera/rls/rebac/types
|
|
1441
|
+
*/
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* A single step in a relationship path
|
|
1445
|
+
*
|
|
1446
|
+
* Defines how to join from one table to another.
|
|
1447
|
+
*
|
|
1448
|
+
* @example
|
|
1449
|
+
* ```typescript
|
|
1450
|
+
* // Simple join: products.shop_id -> shops.id
|
|
1451
|
+
* const step: RelationshipStep = {
|
|
1452
|
+
* from: 'products',
|
|
1453
|
+
* to: 'shops',
|
|
1454
|
+
* fromColumn: 'shop_id',
|
|
1455
|
+
* toColumn: 'id'
|
|
1456
|
+
* };
|
|
1457
|
+
* ```
|
|
1458
|
+
*/
|
|
1459
|
+
interface RelationshipStep {
|
|
1460
|
+
/**
|
|
1461
|
+
* Source table name
|
|
1462
|
+
*/
|
|
1463
|
+
from: string;
|
|
1464
|
+
/**
|
|
1465
|
+
* Target table name
|
|
1466
|
+
*/
|
|
1467
|
+
to: string;
|
|
1468
|
+
/**
|
|
1469
|
+
* Column in source table for the join
|
|
1470
|
+
* @default 'id' on target table side, '{to}_id' on source side
|
|
1471
|
+
*/
|
|
1472
|
+
fromColumn?: string;
|
|
1473
|
+
/**
|
|
1474
|
+
* Column in target table for the join
|
|
1475
|
+
* @default 'id'
|
|
1476
|
+
*/
|
|
1477
|
+
toColumn?: string;
|
|
1478
|
+
/**
|
|
1479
|
+
* Optional alias for the target table in the join
|
|
1480
|
+
* Useful when joining the same table multiple times
|
|
1481
|
+
*/
|
|
1482
|
+
alias?: string;
|
|
1483
|
+
/**
|
|
1484
|
+
* Join type
|
|
1485
|
+
* @default 'inner'
|
|
1486
|
+
*/
|
|
1487
|
+
joinType?: 'inner' | 'left' | 'right';
|
|
1488
|
+
/**
|
|
1489
|
+
* Additional conditions for this join step
|
|
1490
|
+
* @example { 'shops.deleted_at': null, 'shops.status': 'active' }
|
|
1491
|
+
*/
|
|
1492
|
+
additionalConditions?: Record<string, unknown>;
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Complete relationship path definition
|
|
1496
|
+
*
|
|
1497
|
+
* Defines a chain of relationships from a source table to a target.
|
|
1498
|
+
*
|
|
1499
|
+
* @example
|
|
1500
|
+
* ```typescript
|
|
1501
|
+
* // Path: products -> shops -> organizations -> employees
|
|
1502
|
+
* const path: RelationshipPath = {
|
|
1503
|
+
* name: 'orgEmployee',
|
|
1504
|
+
* steps: [
|
|
1505
|
+
* { from: 'products', to: 'shops', fromColumn: 'shop_id' },
|
|
1506
|
+
* { from: 'shops', to: 'organizations', fromColumn: 'organization_id' },
|
|
1507
|
+
* { from: 'organizations', to: 'employees', toColumn: 'organization_id' }
|
|
1508
|
+
* ]
|
|
1509
|
+
* };
|
|
1510
|
+
* ```
|
|
1511
|
+
*/
|
|
1512
|
+
interface RelationshipPath {
|
|
1513
|
+
/**
|
|
1514
|
+
* Unique name for this relationship path
|
|
1515
|
+
* Used in policy definitions to reference this path
|
|
1516
|
+
*/
|
|
1517
|
+
name: string;
|
|
1518
|
+
/**
|
|
1519
|
+
* Steps in the relationship chain
|
|
1520
|
+
*/
|
|
1521
|
+
steps: RelationshipStep[];
|
|
1522
|
+
/**
|
|
1523
|
+
* Optional description for documentation
|
|
1524
|
+
*/
|
|
1525
|
+
description?: string;
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Condition to apply at the end of a relationship path
|
|
1529
|
+
*
|
|
1530
|
+
* @typeParam TCtx - Policy evaluation context type
|
|
1531
|
+
*/
|
|
1532
|
+
type RelationshipCondition<TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> = ((ctx: TCtx) => Record<string, unknown>) | Record<string, unknown>;
|
|
1533
|
+
/**
|
|
1534
|
+
* ReBAC policy definition
|
|
1535
|
+
*
|
|
1536
|
+
* Extends standard policy definition with relationship-based filtering.
|
|
1537
|
+
*/
|
|
1538
|
+
interface ReBAcPolicyDefinition<TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> extends Omit<PolicyDefinition, 'condition'> {
|
|
1539
|
+
/**
|
|
1540
|
+
* Name of the relationship path to use (defined in relationships config)
|
|
1541
|
+
*/
|
|
1542
|
+
relationshipPath: string;
|
|
1543
|
+
/**
|
|
1544
|
+
* Conditions to apply at the end of the relationship
|
|
1545
|
+
* These conditions filter the final table in the relationship chain.
|
|
1546
|
+
*
|
|
1547
|
+
* @example
|
|
1548
|
+
* ```typescript
|
|
1549
|
+
* // Filter employees table at end of relationship
|
|
1550
|
+
* endCondition: ctx => ({
|
|
1551
|
+
* user_id: ctx.auth.userId,
|
|
1552
|
+
* status: 'active'
|
|
1553
|
+
* })
|
|
1554
|
+
* ```
|
|
1555
|
+
*/
|
|
1556
|
+
endCondition: RelationshipCondition<TCtx>;
|
|
1557
|
+
/**
|
|
1558
|
+
* Whether this is a permissive or restrictive policy
|
|
1559
|
+
* - 'allow': Row is accessible if relationship exists
|
|
1560
|
+
* - 'deny': Row is NOT accessible if relationship exists
|
|
1561
|
+
* @default 'allow'
|
|
1562
|
+
*/
|
|
1563
|
+
policyType?: 'allow' | 'deny';
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* ReBAC configuration for a single table
|
|
1567
|
+
*/
|
|
1568
|
+
interface TableReBAcConfig {
|
|
1569
|
+
/**
|
|
1570
|
+
* Relationship paths available for this table
|
|
1571
|
+
*/
|
|
1572
|
+
relationships: RelationshipPath[];
|
|
1573
|
+
/**
|
|
1574
|
+
* ReBAC policies for this table
|
|
1575
|
+
*/
|
|
1576
|
+
policies: ReBAcPolicyDefinition[];
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Complete ReBAC schema for all tables
|
|
1580
|
+
*
|
|
1581
|
+
* @typeParam DB - Database schema type
|
|
1582
|
+
*/
|
|
1583
|
+
type ReBAcSchema<DB> = {
|
|
1584
|
+
[K in keyof DB]?: TableReBAcConfig;
|
|
1585
|
+
};
|
|
1586
|
+
/**
|
|
1587
|
+
* Compiled relationship path ready for query generation
|
|
1588
|
+
*/
|
|
1589
|
+
interface CompiledRelationshipPath {
|
|
1590
|
+
/**
|
|
1591
|
+
* Path name
|
|
1592
|
+
*/
|
|
1593
|
+
name: string;
|
|
1594
|
+
/**
|
|
1595
|
+
* Compiled join steps with defaults filled in
|
|
1596
|
+
*/
|
|
1597
|
+
steps: Required<RelationshipStep>[];
|
|
1598
|
+
/**
|
|
1599
|
+
* Source table (first table in the chain)
|
|
1600
|
+
*/
|
|
1601
|
+
sourceTable: string;
|
|
1602
|
+
/**
|
|
1603
|
+
* Target table (final table in the chain)
|
|
1604
|
+
*/
|
|
1605
|
+
targetTable: string;
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Compiled ReBAC policy ready for evaluation
|
|
1609
|
+
*/
|
|
1610
|
+
interface CompiledReBAcPolicy<TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> {
|
|
1611
|
+
/**
|
|
1612
|
+
* Policy name
|
|
1613
|
+
*/
|
|
1614
|
+
name: string;
|
|
1615
|
+
/**
|
|
1616
|
+
* Policy type (allow/deny)
|
|
1617
|
+
*/
|
|
1618
|
+
type: 'allow' | 'deny';
|
|
1619
|
+
/**
|
|
1620
|
+
* Operations this policy applies to
|
|
1621
|
+
*/
|
|
1622
|
+
operations: Set<string>;
|
|
1623
|
+
/**
|
|
1624
|
+
* Compiled relationship path
|
|
1625
|
+
*/
|
|
1626
|
+
relationshipPath: CompiledRelationshipPath;
|
|
1627
|
+
/**
|
|
1628
|
+
* Function to get end conditions
|
|
1629
|
+
*/
|
|
1630
|
+
getEndConditions: (ctx: TCtx) => Record<string, unknown>;
|
|
1631
|
+
/**
|
|
1632
|
+
* Priority for policy evaluation
|
|
1633
|
+
*/
|
|
1634
|
+
priority: number;
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Generated EXISTS subquery for ReBAC filtering
|
|
1638
|
+
*/
|
|
1639
|
+
interface ReBAcSubquery {
|
|
1640
|
+
/**
|
|
1641
|
+
* SQL for the EXISTS subquery
|
|
1642
|
+
*/
|
|
1643
|
+
sql: string;
|
|
1644
|
+
/**
|
|
1645
|
+
* Parameter values for the subquery
|
|
1646
|
+
*/
|
|
1647
|
+
parameters: unknown[];
|
|
1648
|
+
/**
|
|
1649
|
+
* Whether this is an allow (EXISTS) or deny (NOT EXISTS) check
|
|
1650
|
+
*/
|
|
1651
|
+
isNegated: boolean;
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Options for ReBAC query generation
|
|
1655
|
+
*/
|
|
1656
|
+
interface ReBAcQueryOptions {
|
|
1657
|
+
/**
|
|
1658
|
+
* Table alias for the main query table
|
|
1659
|
+
* @default table name
|
|
1660
|
+
*/
|
|
1661
|
+
mainTableAlias?: string;
|
|
1662
|
+
/**
|
|
1663
|
+
* Whether to use qualified column names
|
|
1664
|
+
* @default true
|
|
1665
|
+
*/
|
|
1666
|
+
qualifyColumns?: boolean;
|
|
1667
|
+
/**
|
|
1668
|
+
* Database dialect for query generation
|
|
1669
|
+
* @default 'postgres'
|
|
1670
|
+
*/
|
|
1671
|
+
dialect?: 'postgres' | 'mysql' | 'sqlite';
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Common relationship pattern: Resource belongs to organization via owner
|
|
1675
|
+
*
|
|
1676
|
+
* @param resourceTable - Table containing the resource
|
|
1677
|
+
* @param organizationColumn - Column linking to organization
|
|
1678
|
+
*
|
|
1679
|
+
* @example
|
|
1680
|
+
* ```typescript
|
|
1681
|
+
* const path = orgMembershipPath('products', 'organization_id');
|
|
1682
|
+
* // Creates path: products -> organizations -> employees
|
|
1683
|
+
* ```
|
|
1684
|
+
*/
|
|
1685
|
+
declare function orgMembershipPath(resourceTable: string, organizationColumn?: string): RelationshipPath;
|
|
1686
|
+
/**
|
|
1687
|
+
* Common relationship pattern: Resource belongs to shop's organization
|
|
1688
|
+
*
|
|
1689
|
+
* @param resourceTable - Table containing the resource
|
|
1690
|
+
* @param shopColumn - Column linking to shop
|
|
1691
|
+
*
|
|
1692
|
+
* @example
|
|
1693
|
+
* ```typescript
|
|
1694
|
+
* const path = shopOrgMembershipPath('products', 'shop_id');
|
|
1695
|
+
* // Creates path: products -> shops -> organizations -> employees
|
|
1696
|
+
* ```
|
|
1697
|
+
*/
|
|
1698
|
+
declare function shopOrgMembershipPath(resourceTable: string, shopColumn?: string): RelationshipPath;
|
|
1699
|
+
/**
|
|
1700
|
+
* Common relationship pattern: Hierarchical team access
|
|
1701
|
+
*
|
|
1702
|
+
* @param resourceTable - Table containing the resource
|
|
1703
|
+
* @param teamColumn - Column linking to team
|
|
1704
|
+
*
|
|
1705
|
+
* @example
|
|
1706
|
+
* ```typescript
|
|
1707
|
+
* const path = teamHierarchyPath('tasks', 'team_id');
|
|
1708
|
+
* // Creates path: tasks -> teams -> team_members
|
|
1709
|
+
* ```
|
|
1710
|
+
*/
|
|
1711
|
+
declare function teamHierarchyPath(resourceTable: string, teamColumn?: string): RelationshipPath;
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* ReBAC Policy Registry
|
|
1715
|
+
*
|
|
1716
|
+
* Manages relationship definitions and ReBAC policies for RLS.
|
|
1717
|
+
*
|
|
1718
|
+
* @module @kysera/rls/rebac/registry
|
|
1719
|
+
*/
|
|
1720
|
+
|
|
1721
|
+
/**
|
|
1722
|
+
* ReBAC Registry
|
|
1723
|
+
*
|
|
1724
|
+
* Manages relationship paths and ReBAC policies across tables.
|
|
1725
|
+
*
|
|
1726
|
+
* @example
|
|
1727
|
+
* ```typescript
|
|
1728
|
+
* const registry = new ReBAcRegistry();
|
|
1729
|
+
*
|
|
1730
|
+
* // Register relationship paths and policies
|
|
1731
|
+
* registry.loadSchema({
|
|
1732
|
+
* products: {
|
|
1733
|
+
* relationships: [
|
|
1734
|
+
* shopOrgMembershipPath('products', 'shop_id')
|
|
1735
|
+
* ],
|
|
1736
|
+
* policies: [
|
|
1737
|
+
* {
|
|
1738
|
+
* type: 'filter',
|
|
1739
|
+
* operation: 'read',
|
|
1740
|
+
* relationshipPath: 'products_shop_org_membership',
|
|
1741
|
+
* endCondition: ctx => ({
|
|
1742
|
+
* user_id: ctx.auth.userId,
|
|
1743
|
+
* status: 'active'
|
|
1744
|
+
* })
|
|
1745
|
+
* }
|
|
1746
|
+
* ]
|
|
1747
|
+
* }
|
|
1748
|
+
* });
|
|
1749
|
+
*
|
|
1750
|
+
* // Get policies for a table
|
|
1751
|
+
* const policies = registry.getPolicies('products', 'read');
|
|
1752
|
+
* ```
|
|
1753
|
+
*/
|
|
1754
|
+
declare class ReBAcRegistry<DB = unknown> {
|
|
1755
|
+
private tables;
|
|
1756
|
+
private globalRelationships;
|
|
1757
|
+
private logger;
|
|
1758
|
+
constructor(schema?: ReBAcSchema<DB>, options?: {
|
|
1759
|
+
logger?: KyseraLogger;
|
|
1760
|
+
});
|
|
1761
|
+
/**
|
|
1762
|
+
* Load ReBAC schema
|
|
1763
|
+
*/
|
|
1764
|
+
loadSchema(schema: ReBAcSchema<DB>): void;
|
|
1765
|
+
/**
|
|
1766
|
+
* Register ReBAC configuration for a single table
|
|
1767
|
+
*/
|
|
1768
|
+
registerTable(table: string, config: TableReBAcConfig): void;
|
|
1769
|
+
/**
|
|
1770
|
+
* Register a global relationship path (available to all tables)
|
|
1771
|
+
*/
|
|
1772
|
+
registerRelationship(path: RelationshipPath): void;
|
|
1773
|
+
/**
|
|
1774
|
+
* Get ReBAC policies for a table and operation
|
|
1775
|
+
*/
|
|
1776
|
+
getPolicies(table: string, operation: Operation): CompiledReBAcPolicy[];
|
|
1777
|
+
/**
|
|
1778
|
+
* Get a specific relationship path
|
|
1779
|
+
*/
|
|
1780
|
+
getRelationship(name: string, table?: string): CompiledRelationshipPath | undefined;
|
|
1781
|
+
/**
|
|
1782
|
+
* Check if table has ReBAC configuration
|
|
1783
|
+
*/
|
|
1784
|
+
hasTable(table: string): boolean;
|
|
1785
|
+
/**
|
|
1786
|
+
* Get all registered table names
|
|
1787
|
+
*/
|
|
1788
|
+
getTables(): string[];
|
|
1789
|
+
/**
|
|
1790
|
+
* Clear all registrations
|
|
1791
|
+
*/
|
|
1792
|
+
clear(): void;
|
|
1793
|
+
/**
|
|
1794
|
+
* Compile a relationship path definition
|
|
1795
|
+
*/
|
|
1796
|
+
private compileRelationshipPath;
|
|
1797
|
+
/**
|
|
1798
|
+
* Compile a ReBAC policy definition
|
|
1799
|
+
*/
|
|
1800
|
+
private compilePolicy;
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Create a ReBAC registry
|
|
1804
|
+
*/
|
|
1805
|
+
declare function createReBAcRegistry<DB = unknown>(schema?: ReBAcSchema<DB>, options?: {
|
|
1806
|
+
logger?: KyseraLogger;
|
|
1807
|
+
}): ReBAcRegistry<DB>;
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* ReBAC Query Transformer
|
|
1811
|
+
*
|
|
1812
|
+
* Transforms queries to apply relationship-based access control policies.
|
|
1813
|
+
* Generates EXISTS subqueries that filter rows based on relationship chains.
|
|
1814
|
+
*
|
|
1815
|
+
* @module @kysera/rls/rebac/transformer
|
|
1816
|
+
*/
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* ReBAC query transformer
|
|
1820
|
+
*
|
|
1821
|
+
* Applies relationship-based access control to SELECT queries by generating
|
|
1822
|
+
* EXISTS subqueries that follow relationship paths.
|
|
1823
|
+
*
|
|
1824
|
+
* @example
|
|
1825
|
+
* ```typescript
|
|
1826
|
+
* const transformer = new ReBAcTransformer(registry);
|
|
1827
|
+
*
|
|
1828
|
+
* // Transform query
|
|
1829
|
+
* let query = db.selectFrom('products').selectAll();
|
|
1830
|
+
* query = transformer.transform(query, 'products', 'read');
|
|
1831
|
+
*
|
|
1832
|
+
* // Generated SQL includes EXISTS subquery:
|
|
1833
|
+
* // SELECT * FROM products p
|
|
1834
|
+
* // WHERE EXISTS (
|
|
1835
|
+
* // SELECT 1 FROM shops s
|
|
1836
|
+
* // JOIN organizations o ON s.organization_id = o.id
|
|
1837
|
+
* // JOIN employees e ON e.organization_id = o.id
|
|
1838
|
+
* // WHERE s.id = p.shop_id
|
|
1839
|
+
* // AND e.user_id = $1
|
|
1840
|
+
* // AND e.status = 'active'
|
|
1841
|
+
* // )
|
|
1842
|
+
* ```
|
|
1843
|
+
*/
|
|
1844
|
+
declare class ReBAcTransformer<DB = unknown> {
|
|
1845
|
+
private registry;
|
|
1846
|
+
private options;
|
|
1847
|
+
constructor(registry: ReBAcRegistry<DB>, options?: ReBAcQueryOptions);
|
|
1848
|
+
/**
|
|
1849
|
+
* Transform a SELECT query by applying ReBAC policies
|
|
1850
|
+
*
|
|
1851
|
+
* @param qb - Query builder to transform
|
|
1852
|
+
* @param table - Table being queried
|
|
1853
|
+
* @param operation - Operation being performed
|
|
1854
|
+
* @returns Transformed query builder
|
|
1855
|
+
*/
|
|
1856
|
+
transform<TB extends keyof DB & string, O>(qb: SelectQueryBuilder<DB, TB, O>, table: string, operation?: Operation): SelectQueryBuilder<DB, TB, O>;
|
|
1857
|
+
/**
|
|
1858
|
+
* Generate EXISTS condition SQL for a policy
|
|
1859
|
+
*
|
|
1860
|
+
* This method can be used to get the raw SQL for debugging or manual query building.
|
|
1861
|
+
*
|
|
1862
|
+
* @param policy - ReBAC policy to generate SQL for
|
|
1863
|
+
* @param ctx - RLS context
|
|
1864
|
+
* @param mainTable - Main query table
|
|
1865
|
+
* @param mainTableAlias - Alias for main table
|
|
1866
|
+
* @returns SQL string and parameters
|
|
1867
|
+
*/
|
|
1868
|
+
generateExistsSql(policy: CompiledReBAcPolicy, ctx: RLSContext, mainTable: string, mainTableAlias?: string): {
|
|
1869
|
+
sql: string;
|
|
1870
|
+
params: unknown[];
|
|
1871
|
+
};
|
|
1872
|
+
/**
|
|
1873
|
+
* Apply a single ReBAC policy to a query
|
|
1874
|
+
*
|
|
1875
|
+
* NOTE: Uses type casting for dynamic SQL because Kysely's type system
|
|
1876
|
+
* requires compile-time known types, but ReBAC policies work with
|
|
1877
|
+
* runtime-generated EXISTS clauses.
|
|
1878
|
+
*/
|
|
1879
|
+
private applyPolicy;
|
|
1880
|
+
/**
|
|
1881
|
+
* Create evaluation context for policy conditions
|
|
1882
|
+
*/
|
|
1883
|
+
private createEvalContext;
|
|
1884
|
+
/**
|
|
1885
|
+
* Quote an identifier for the target dialect
|
|
1886
|
+
*/
|
|
1887
|
+
private quote;
|
|
1888
|
+
/**
|
|
1889
|
+
* Generate parameter placeholder for the target dialect
|
|
1890
|
+
*/
|
|
1891
|
+
private param;
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Create a ReBAC allow policy
|
|
1895
|
+
*
|
|
1896
|
+
* Rows are accessible if the relationship EXISTS with the given end conditions.
|
|
1897
|
+
*
|
|
1898
|
+
* @param operation - Operation(s) this policy applies to
|
|
1899
|
+
* @param relationshipPath - Name of the relationship path to use
|
|
1900
|
+
* @param endCondition - Conditions to apply at the end of the relationship
|
|
1901
|
+
* @param options - Additional policy options
|
|
1902
|
+
*
|
|
1903
|
+
* @example
|
|
1904
|
+
* ```typescript
|
|
1905
|
+
* // Allow read if user is employee of product's shop's organization
|
|
1906
|
+
* allowRelation('read', 'products_shop_org_membership', ctx => ({
|
|
1907
|
+
* user_id: ctx.auth.userId,
|
|
1908
|
+
* status: 'active'
|
|
1909
|
+
* }))
|
|
1910
|
+
* ```
|
|
1911
|
+
*/
|
|
1912
|
+
declare function allowRelation(operation: Operation | Operation[], relationshipPath: string, endCondition: ((ctx: PolicyEvaluationContext) => Record<string, unknown>) | Record<string, unknown>, options?: {
|
|
1913
|
+
name?: string;
|
|
1914
|
+
priority?: number;
|
|
1915
|
+
}): ReBAcPolicyDefinition;
|
|
1916
|
+
/**
|
|
1917
|
+
* Create a ReBAC deny policy
|
|
1918
|
+
*
|
|
1919
|
+
* Rows are NOT accessible if the relationship EXISTS with the given conditions.
|
|
1920
|
+
*
|
|
1921
|
+
* @param operation - Operation(s) this policy applies to
|
|
1922
|
+
* @param relationshipPath - Name of the relationship path to use
|
|
1923
|
+
* @param endCondition - Conditions to apply at the end of the relationship
|
|
1924
|
+
* @param options - Additional policy options
|
|
1925
|
+
*
|
|
1926
|
+
* @example
|
|
1927
|
+
* ```typescript
|
|
1928
|
+
* // Deny access if user is blocked in the organization
|
|
1929
|
+
* denyRelation('all', 'products_shop_org_membership', ctx => ({
|
|
1930
|
+
* user_id: ctx.auth.userId,
|
|
1931
|
+
* status: 'blocked'
|
|
1932
|
+
* }))
|
|
1933
|
+
* ```
|
|
1934
|
+
*/
|
|
1935
|
+
declare function denyRelation(operation: Operation | Operation[], relationshipPath: string, endCondition: ((ctx: PolicyEvaluationContext) => Record<string, unknown>) | Record<string, unknown>, options?: {
|
|
1936
|
+
name?: string;
|
|
1937
|
+
priority?: number;
|
|
1938
|
+
}): ReBAcPolicyDefinition;
|
|
1939
|
+
/**
|
|
1940
|
+
* Create a ReBAC transformer
|
|
1941
|
+
*/
|
|
1942
|
+
declare function createReBAcTransformer<DB = unknown>(registry: ReBAcRegistry<DB>, options?: ReBAcQueryOptions): ReBAcTransformer<DB>;
|
|
1943
|
+
|
|
1944
|
+
/**
|
|
1945
|
+
* Field-Level Access Control Types
|
|
1946
|
+
*
|
|
1947
|
+
* Provides type definitions for controlling access to individual columns
|
|
1948
|
+
* based on context. This allows hiding sensitive fields from unauthorized users.
|
|
1949
|
+
*
|
|
1950
|
+
* @module @kysera/rls/field-access/types
|
|
1951
|
+
*/
|
|
1952
|
+
|
|
1953
|
+
/**
|
|
1954
|
+
* Operations that can be controlled at field level
|
|
1955
|
+
*/
|
|
1956
|
+
type FieldOperation = 'read' | 'write';
|
|
1957
|
+
/**
|
|
1958
|
+
* Field access condition function
|
|
1959
|
+
*
|
|
1960
|
+
* Returns true if the field is accessible, false otherwise.
|
|
1961
|
+
*
|
|
1962
|
+
* @typeParam TCtx - Policy evaluation context type
|
|
1963
|
+
*/
|
|
1964
|
+
type FieldAccessCondition<TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> = (ctx: TCtx) => boolean | Promise<boolean>;
|
|
1965
|
+
/**
|
|
1966
|
+
* Configuration for a single field's access control
|
|
1967
|
+
*
|
|
1968
|
+
* @example
|
|
1969
|
+
* ```typescript
|
|
1970
|
+
* const emailConfig: FieldAccessConfig = {
|
|
1971
|
+
* read: ctx => ctx.auth.userId === ctx.row.id || ctx.auth.roles.includes('admin'),
|
|
1972
|
+
* write: ctx => ctx.auth.userId === ctx.row.id
|
|
1973
|
+
* };
|
|
1974
|
+
* ```
|
|
1975
|
+
*/
|
|
1976
|
+
interface FieldAccessConfig<TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> {
|
|
1977
|
+
/**
|
|
1978
|
+
* Condition for read access
|
|
1979
|
+
* If undefined, uses table default
|
|
1980
|
+
*/
|
|
1981
|
+
read?: FieldAccessCondition<TCtx>;
|
|
1982
|
+
/**
|
|
1983
|
+
* Condition for write access
|
|
1984
|
+
* If undefined, uses table default
|
|
1985
|
+
*/
|
|
1986
|
+
write?: FieldAccessCondition<TCtx>;
|
|
1987
|
+
/**
|
|
1988
|
+
* Value to use when field is not readable
|
|
1989
|
+
* @default null
|
|
1990
|
+
*/
|
|
1991
|
+
maskedValue?: unknown;
|
|
1992
|
+
/**
|
|
1993
|
+
* Whether to completely omit the field when not readable
|
|
1994
|
+
* @default false (uses maskedValue instead)
|
|
1995
|
+
*/
|
|
1996
|
+
omitWhenHidden?: boolean;
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Table field access configuration
|
|
2000
|
+
*
|
|
2001
|
+
* @typeParam TRow - Type of the database row
|
|
2002
|
+
* @typeParam TCtx - Policy evaluation context type
|
|
2003
|
+
*
|
|
2004
|
+
* @example
|
|
2005
|
+
* ```typescript
|
|
2006
|
+
* const usersFieldAccess: TableFieldAccessConfig<User> = {
|
|
2007
|
+
* default: 'allow',
|
|
2008
|
+
* fields: {
|
|
2009
|
+
* email: {
|
|
2010
|
+
* read: ctx => ctx.auth.userId === ctx.row.id || ctx.auth.roles.includes('admin')
|
|
2011
|
+
* },
|
|
2012
|
+
* password_hash: {
|
|
2013
|
+
* read: () => false,
|
|
2014
|
+
* write: () => false
|
|
2015
|
+
* },
|
|
2016
|
+
* mfa_totp_secret: {
|
|
2017
|
+
* read: ctx => ctx.auth.userId === ctx.row.id,
|
|
2018
|
+
* omitWhenHidden: true
|
|
2019
|
+
* }
|
|
2020
|
+
* }
|
|
2021
|
+
* };
|
|
2022
|
+
* ```
|
|
2023
|
+
*/
|
|
2024
|
+
interface TableFieldAccessConfig<TRow = unknown, TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> {
|
|
2025
|
+
/**
|
|
2026
|
+
* Default access policy for fields not explicitly configured
|
|
2027
|
+
* - 'allow': All fields are accessible by default
|
|
2028
|
+
* - 'deny': Only explicitly allowed fields are accessible
|
|
2029
|
+
* @default 'allow'
|
|
2030
|
+
*/
|
|
2031
|
+
default?: 'allow' | 'deny';
|
|
2032
|
+
/**
|
|
2033
|
+
* Field-specific access configurations
|
|
2034
|
+
*/
|
|
2035
|
+
fields: {
|
|
2036
|
+
[K in keyof TRow]?: FieldAccessConfig<TCtx>;
|
|
2037
|
+
};
|
|
2038
|
+
/**
|
|
2039
|
+
* Roles that bypass field access control
|
|
2040
|
+
*/
|
|
2041
|
+
skipFor?: string[];
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Complete field access schema for all tables
|
|
2045
|
+
*
|
|
2046
|
+
* @typeParam DB - Database schema type
|
|
2047
|
+
*/
|
|
2048
|
+
type FieldAccessSchema<DB> = {
|
|
2049
|
+
[K in keyof DB]?: TableFieldAccessConfig<DB[K]>;
|
|
2050
|
+
};
|
|
2051
|
+
/**
|
|
2052
|
+
* Compiled field access configuration ready for evaluation
|
|
2053
|
+
*/
|
|
2054
|
+
interface CompiledFieldAccess {
|
|
2055
|
+
/**
|
|
2056
|
+
* Field name
|
|
2057
|
+
*/
|
|
2058
|
+
field: string;
|
|
2059
|
+
/**
|
|
2060
|
+
* Compiled read condition
|
|
2061
|
+
* Returns true if field is readable
|
|
2062
|
+
*/
|
|
2063
|
+
canRead: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>;
|
|
2064
|
+
/**
|
|
2065
|
+
* Compiled write condition
|
|
2066
|
+
* Returns true if field is writable
|
|
2067
|
+
*/
|
|
2068
|
+
canWrite: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>;
|
|
2069
|
+
/**
|
|
2070
|
+
* Value to use when field is masked
|
|
2071
|
+
*/
|
|
2072
|
+
maskedValue: unknown;
|
|
2073
|
+
/**
|
|
2074
|
+
* Whether to omit the field entirely when hidden
|
|
2075
|
+
*/
|
|
2076
|
+
omitWhenHidden: boolean;
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Compiled table field access configuration
|
|
2080
|
+
*/
|
|
2081
|
+
interface CompiledTableFieldAccess {
|
|
2082
|
+
/**
|
|
2083
|
+
* Table name
|
|
2084
|
+
*/
|
|
2085
|
+
table: string;
|
|
2086
|
+
/**
|
|
2087
|
+
* Default access policy
|
|
2088
|
+
*/
|
|
2089
|
+
defaultAccess: 'allow' | 'deny';
|
|
2090
|
+
/**
|
|
2091
|
+
* Roles that bypass field access
|
|
2092
|
+
*/
|
|
2093
|
+
skipFor: string[];
|
|
2094
|
+
/**
|
|
2095
|
+
* Field-specific configurations
|
|
2096
|
+
*/
|
|
2097
|
+
fields: Map<string, CompiledFieldAccess>;
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Result of field access evaluation
|
|
2101
|
+
*/
|
|
2102
|
+
interface FieldAccessResult {
|
|
2103
|
+
/**
|
|
2104
|
+
* Whether the field is accessible
|
|
2105
|
+
*/
|
|
2106
|
+
accessible: boolean;
|
|
2107
|
+
/**
|
|
2108
|
+
* If not accessible, the reason
|
|
2109
|
+
*/
|
|
2110
|
+
reason?: string;
|
|
2111
|
+
/**
|
|
2112
|
+
* Value to use (original or masked)
|
|
2113
|
+
*/
|
|
2114
|
+
value: unknown;
|
|
2115
|
+
/**
|
|
2116
|
+
* Whether the field should be omitted entirely
|
|
2117
|
+
*/
|
|
2118
|
+
omit: boolean;
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Result of applying field access to a row
|
|
2122
|
+
*/
|
|
2123
|
+
interface MaskedRow<T = Record<string, unknown>> {
|
|
2124
|
+
/**
|
|
2125
|
+
* The row with field access applied
|
|
2126
|
+
*/
|
|
2127
|
+
data: Partial<T>;
|
|
2128
|
+
/**
|
|
2129
|
+
* Fields that were masked
|
|
2130
|
+
*/
|
|
2131
|
+
maskedFields: string[];
|
|
2132
|
+
/**
|
|
2133
|
+
* Fields that were omitted
|
|
2134
|
+
*/
|
|
2135
|
+
omittedFields: string[];
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* Options for field access processing
|
|
2139
|
+
*/
|
|
2140
|
+
interface FieldAccessOptions {
|
|
2141
|
+
/**
|
|
2142
|
+
* Whether to throw an error when accessing a denied field
|
|
2143
|
+
* @default false (returns masked value instead)
|
|
2144
|
+
*/
|
|
2145
|
+
throwOnDenied?: boolean;
|
|
2146
|
+
/**
|
|
2147
|
+
* Whether to include metadata about masked fields in the result
|
|
2148
|
+
* @default false
|
|
2149
|
+
*/
|
|
2150
|
+
includeMetadata?: boolean;
|
|
2151
|
+
/**
|
|
2152
|
+
* Fields to explicitly include (whitelist)
|
|
2153
|
+
* If specified, only these fields are processed
|
|
2154
|
+
*/
|
|
2155
|
+
includeFields?: string[];
|
|
2156
|
+
/**
|
|
2157
|
+
* Fields to explicitly exclude (blacklist)
|
|
2158
|
+
* These fields are never included regardless of access
|
|
2159
|
+
*/
|
|
2160
|
+
excludeFields?: string[];
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Always deny access to a field
|
|
2164
|
+
*
|
|
2165
|
+
* @example
|
|
2166
|
+
* ```typescript
|
|
2167
|
+
* const config = {
|
|
2168
|
+
* fields: {
|
|
2169
|
+
* password_hash: neverAccessible(),
|
|
2170
|
+
* api_secret: neverAccessible()
|
|
2171
|
+
* }
|
|
2172
|
+
* };
|
|
2173
|
+
* ```
|
|
2174
|
+
*/
|
|
2175
|
+
declare function neverAccessible(): FieldAccessConfig;
|
|
2176
|
+
/**
|
|
2177
|
+
* Only the resource owner can access this field
|
|
2178
|
+
*
|
|
2179
|
+
* @param ownerField - Field name containing the owner ID
|
|
2180
|
+
*
|
|
2181
|
+
* @example
|
|
2182
|
+
* ```typescript
|
|
2183
|
+
* const config = {
|
|
2184
|
+
* fields: {
|
|
2185
|
+
* email: ownerOnly('user_id'),
|
|
2186
|
+
* phone: ownerOnly('user_id')
|
|
2187
|
+
* }
|
|
2188
|
+
* };
|
|
2189
|
+
* ```
|
|
2190
|
+
*/
|
|
2191
|
+
declare function ownerOnly(ownerField?: string): FieldAccessConfig;
|
|
2192
|
+
/**
|
|
2193
|
+
* Owner or users with specific roles can access this field
|
|
2194
|
+
*
|
|
2195
|
+
* @param roles - Roles that can access besides owner
|
|
2196
|
+
* @param ownerField - Field name containing the owner ID
|
|
2197
|
+
*
|
|
2198
|
+
* @example
|
|
2199
|
+
* ```typescript
|
|
2200
|
+
* const config = {
|
|
2201
|
+
* fields: {
|
|
2202
|
+
* email: ownerOrRoles(['admin', 'support'], 'user_id'),
|
|
2203
|
+
* address: ownerOrRoles(['admin'], 'user_id')
|
|
2204
|
+
* }
|
|
2205
|
+
* };
|
|
2206
|
+
* ```
|
|
2207
|
+
*/
|
|
2208
|
+
declare function ownerOrRoles(roles: string[], ownerField?: string): FieldAccessConfig;
|
|
2209
|
+
/**
|
|
2210
|
+
* Only users with specific roles can access this field
|
|
2211
|
+
*
|
|
2212
|
+
* @param roles - Roles that can access
|
|
2213
|
+
*
|
|
2214
|
+
* @example
|
|
2215
|
+
* ```typescript
|
|
2216
|
+
* const config = {
|
|
2217
|
+
* fields: {
|
|
2218
|
+
* internal_notes: rolesOnly(['admin', 'moderator']),
|
|
2219
|
+
* audit_log: rolesOnly(['admin'])
|
|
2220
|
+
* }
|
|
2221
|
+
* };
|
|
2222
|
+
* ```
|
|
2223
|
+
*/
|
|
2224
|
+
declare function rolesOnly(roles: string[]): FieldAccessConfig;
|
|
2225
|
+
/**
|
|
2226
|
+
* Field is read-only (no write access)
|
|
2227
|
+
*
|
|
2228
|
+
* @param readCondition - Optional condition for read access
|
|
2229
|
+
*
|
|
2230
|
+
* @example
|
|
2231
|
+
* ```typescript
|
|
2232
|
+
* const config = {
|
|
2233
|
+
* fields: {
|
|
2234
|
+
* created_at: readOnly(),
|
|
2235
|
+
* version: readOnly()
|
|
2236
|
+
* }
|
|
2237
|
+
* };
|
|
2238
|
+
* ```
|
|
2239
|
+
*/
|
|
2240
|
+
declare function readOnly(readCondition?: FieldAccessCondition): FieldAccessConfig;
|
|
2241
|
+
/**
|
|
2242
|
+
* Field has public read access but restricted write
|
|
2243
|
+
*
|
|
2244
|
+
* @param writeCondition - Condition for write access
|
|
2245
|
+
*
|
|
2246
|
+
* @example
|
|
2247
|
+
* ```typescript
|
|
2248
|
+
* const config = {
|
|
2249
|
+
* fields: {
|
|
2250
|
+
* display_name: publicReadRestrictedWrite(ctx => ctx.auth.userId === ctx.row.id),
|
|
2251
|
+
* bio: publicReadRestrictedWrite(ctx => ctx.auth.userId === ctx.row.id)
|
|
2252
|
+
* }
|
|
2253
|
+
* };
|
|
2254
|
+
* ```
|
|
2255
|
+
*/
|
|
2256
|
+
declare function publicReadRestrictedWrite(writeCondition: FieldAccessCondition): FieldAccessConfig;
|
|
2257
|
+
/**
|
|
2258
|
+
* Mask field value with custom masking function
|
|
2259
|
+
*
|
|
2260
|
+
* @param maskFn - Function to mask the value
|
|
2261
|
+
* @param readCondition - Condition for full read access
|
|
2262
|
+
*
|
|
2263
|
+
* @example
|
|
2264
|
+
* ```typescript
|
|
2265
|
+
* const config = {
|
|
2266
|
+
* fields: {
|
|
2267
|
+
* email: maskedField(
|
|
2268
|
+
* value => value.replace(/(.{2}).*@/, '$1***@'),
|
|
2269
|
+
* ctx => ctx.auth.userId === ctx.row.id
|
|
2270
|
+
* ),
|
|
2271
|
+
* phone: maskedField(
|
|
2272
|
+
* value => value.replace(/\d(?=\d{4})/g, '*'),
|
|
2273
|
+
* ctx => ctx.auth.userId === ctx.row.id
|
|
2274
|
+
* )
|
|
2275
|
+
* }
|
|
2276
|
+
* };
|
|
2277
|
+
* ```
|
|
2278
|
+
*/
|
|
2279
|
+
declare function maskedField(maskFn: (value: unknown) => unknown, readCondition: FieldAccessCondition): FieldAccessConfig & {
|
|
2280
|
+
maskFn: (value: unknown) => unknown;
|
|
2281
|
+
};
|
|
2282
|
+
|
|
2283
|
+
/**
|
|
2284
|
+
* Field Access Registry
|
|
2285
|
+
*
|
|
2286
|
+
* Manages field-level access control configurations across tables.
|
|
2287
|
+
*
|
|
2288
|
+
* @module @kysera/rls/field-access/registry
|
|
2289
|
+
*/
|
|
2290
|
+
|
|
2291
|
+
/**
|
|
2292
|
+
* Field Access Registry
|
|
2293
|
+
*
|
|
2294
|
+
* Manages field-level access control configurations for all tables.
|
|
2295
|
+
*
|
|
2296
|
+
* @example
|
|
2297
|
+
* ```typescript
|
|
2298
|
+
* const registry = new FieldAccessRegistry();
|
|
2299
|
+
*
|
|
2300
|
+
* registry.loadSchema<Database>({
|
|
2301
|
+
* users: {
|
|
2302
|
+
* default: 'allow',
|
|
2303
|
+
* fields: {
|
|
2304
|
+
* email: ownerOrRoles(['admin'], 'id'),
|
|
2305
|
+
* password_hash: neverAccessible(),
|
|
2306
|
+
* mfa_secret: ownerOnly('id')
|
|
2307
|
+
* }
|
|
2308
|
+
* }
|
|
2309
|
+
* });
|
|
2310
|
+
*
|
|
2311
|
+
* // Check if field is accessible
|
|
2312
|
+
* const canRead = await registry.canReadField('users', 'email', evalCtx);
|
|
2313
|
+
* ```
|
|
2314
|
+
*/
|
|
2315
|
+
declare class FieldAccessRegistry<DB = unknown> {
|
|
2316
|
+
private tables;
|
|
2317
|
+
private logger;
|
|
2318
|
+
constructor(schema?: FieldAccessSchema<DB>, options?: {
|
|
2319
|
+
logger?: KyseraLogger;
|
|
2320
|
+
});
|
|
2321
|
+
/**
|
|
2322
|
+
* Load field access schema
|
|
2323
|
+
*/
|
|
2324
|
+
loadSchema(schema: FieldAccessSchema<DB>): void;
|
|
2325
|
+
/**
|
|
2326
|
+
* Register field access configuration for a table
|
|
2327
|
+
*/
|
|
2328
|
+
registerTable(table: string, config: TableFieldAccessConfig): void;
|
|
2329
|
+
/**
|
|
2330
|
+
* Check if a field is readable in the current context
|
|
2331
|
+
*
|
|
2332
|
+
* @param table - Table name
|
|
2333
|
+
* @param field - Field name
|
|
2334
|
+
* @param ctx - Evaluation context
|
|
2335
|
+
* @returns True if field is readable
|
|
2336
|
+
*/
|
|
2337
|
+
canReadField(table: string, field: string, ctx: PolicyEvaluationContext): Promise<boolean>;
|
|
2338
|
+
/**
|
|
2339
|
+
* Check if a field is writable in the current context
|
|
2340
|
+
*
|
|
2341
|
+
* @param table - Table name
|
|
2342
|
+
* @param field - Field name
|
|
2343
|
+
* @param ctx - Evaluation context
|
|
2344
|
+
* @returns True if field is writable
|
|
2345
|
+
*/
|
|
2346
|
+
canWriteField(table: string, field: string, ctx: PolicyEvaluationContext): Promise<boolean>;
|
|
2347
|
+
/**
|
|
2348
|
+
* Get field configuration
|
|
2349
|
+
*
|
|
2350
|
+
* @param table - Table name
|
|
2351
|
+
* @param field - Field name
|
|
2352
|
+
* @returns Compiled field access config or undefined
|
|
2353
|
+
*/
|
|
2354
|
+
getFieldConfig(table: string, field: string): CompiledFieldAccess | undefined;
|
|
2355
|
+
/**
|
|
2356
|
+
* Get table configuration
|
|
2357
|
+
*
|
|
2358
|
+
* @param table - Table name
|
|
2359
|
+
* @returns Compiled table field access config or undefined
|
|
2360
|
+
*/
|
|
2361
|
+
getTableConfig(table: string): CompiledTableFieldAccess | undefined;
|
|
2362
|
+
/**
|
|
2363
|
+
* Check if table has field access configuration
|
|
2364
|
+
*/
|
|
2365
|
+
hasTable(table: string): boolean;
|
|
2366
|
+
/**
|
|
2367
|
+
* Get all registered table names
|
|
2368
|
+
*/
|
|
2369
|
+
getTables(): string[];
|
|
2370
|
+
/**
|
|
2371
|
+
* Get all fields with explicit configuration for a table
|
|
2372
|
+
*
|
|
2373
|
+
* @param table - Table name
|
|
2374
|
+
* @returns Array of field names
|
|
2375
|
+
*/
|
|
2376
|
+
getConfiguredFields(table: string): string[];
|
|
2377
|
+
/**
|
|
2378
|
+
* Clear all configurations
|
|
2379
|
+
*/
|
|
2380
|
+
clear(): void;
|
|
2381
|
+
/**
|
|
2382
|
+
* Compile a field access configuration
|
|
2383
|
+
*/
|
|
2384
|
+
private compileFieldConfig;
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* Create a field access registry
|
|
2388
|
+
*/
|
|
2389
|
+
declare function createFieldAccessRegistry<DB = unknown>(schema?: FieldAccessSchema<DB>, options?: {
|
|
2390
|
+
logger?: KyseraLogger;
|
|
2391
|
+
}): FieldAccessRegistry<DB>;
|
|
2392
|
+
|
|
2393
|
+
/**
|
|
2394
|
+
* Field Access Processor
|
|
2395
|
+
*
|
|
2396
|
+
* Applies field-level access control to database rows and mutation data.
|
|
2397
|
+
*
|
|
2398
|
+
* @module @kysera/rls/field-access/processor
|
|
2399
|
+
*/
|
|
2400
|
+
|
|
2401
|
+
/**
|
|
2402
|
+
* Field Access Processor
|
|
2403
|
+
*
|
|
2404
|
+
* Applies field-level access control rules to rows and mutation data.
|
|
2405
|
+
*
|
|
2406
|
+
* @example
|
|
2407
|
+
* ```typescript
|
|
2408
|
+
* const processor = new FieldAccessProcessor(registry);
|
|
2409
|
+
*
|
|
2410
|
+
* // Mask fields in a row
|
|
2411
|
+
* const result = await processor.maskRow('users', user, {
|
|
2412
|
+
* includeMetadata: true
|
|
2413
|
+
* });
|
|
2414
|
+
*
|
|
2415
|
+
* console.log(result.data); // Row with masked fields
|
|
2416
|
+
* console.log(result.maskedFields); // ['email', 'phone']
|
|
2417
|
+
* console.log(result.omittedFields); // ['mfa_secret']
|
|
2418
|
+
*
|
|
2419
|
+
* // Validate write access
|
|
2420
|
+
* await processor.validateWrite('users', { email: 'new@example.com' });
|
|
2421
|
+
* ```
|
|
2422
|
+
*/
|
|
2423
|
+
declare class FieldAccessProcessor<DB = unknown> {
|
|
2424
|
+
private registry;
|
|
2425
|
+
private defaultMaskValue;
|
|
2426
|
+
constructor(registry: FieldAccessRegistry<DB>, defaultMaskValue?: unknown);
|
|
2427
|
+
/**
|
|
2428
|
+
* Apply field access control to a single row
|
|
2429
|
+
*
|
|
2430
|
+
* @param table - Table name
|
|
2431
|
+
* @param row - Row data
|
|
2432
|
+
* @param options - Processing options
|
|
2433
|
+
* @returns Masked row with metadata
|
|
2434
|
+
*/
|
|
2435
|
+
maskRow<T extends Record<string, unknown>>(table: string, row: T, options?: FieldAccessOptions): Promise<MaskedRow<T>>;
|
|
2436
|
+
/**
|
|
2437
|
+
* Apply field access control to multiple rows
|
|
2438
|
+
*
|
|
2439
|
+
* @param table - Table name
|
|
2440
|
+
* @param rows - Array of rows
|
|
2441
|
+
* @param options - Processing options
|
|
2442
|
+
* @returns Array of masked rows
|
|
2443
|
+
*/
|
|
2444
|
+
maskRows<T extends Record<string, unknown>>(table: string, rows: T[], options?: FieldAccessOptions): Promise<MaskedRow<T>[]>;
|
|
2445
|
+
/**
|
|
2446
|
+
* Validate that all fields in mutation data are writable
|
|
2447
|
+
*
|
|
2448
|
+
* @param table - Table name
|
|
2449
|
+
* @param data - Mutation data
|
|
2450
|
+
* @param existingRow - Existing row (for update operations)
|
|
2451
|
+
* @throws RLSPolicyViolation if any field is not writable
|
|
2452
|
+
*/
|
|
2453
|
+
validateWrite(table: string, data: Record<string, unknown>, existingRow?: Record<string, unknown>): Promise<void>;
|
|
2454
|
+
/**
|
|
2455
|
+
* Filter mutation data to only include writable fields
|
|
2456
|
+
*
|
|
2457
|
+
* @param table - Table name
|
|
2458
|
+
* @param data - Mutation data
|
|
2459
|
+
* @param existingRow - Existing row (for update operations)
|
|
2460
|
+
* @returns Filtered data with only writable fields
|
|
2461
|
+
*/
|
|
2462
|
+
filterWritableFields(table: string, data: Record<string, unknown>, existingRow?: Record<string, unknown>): Promise<{
|
|
2463
|
+
data: Record<string, unknown>;
|
|
2464
|
+
removedFields: string[];
|
|
2465
|
+
}>;
|
|
2466
|
+
/**
|
|
2467
|
+
* Get list of readable fields for a table
|
|
2468
|
+
*
|
|
2469
|
+
* @param table - Table name
|
|
2470
|
+
* @param row - Row data (for context-dependent fields)
|
|
2471
|
+
* @returns Array of readable field names
|
|
2472
|
+
*/
|
|
2473
|
+
getReadableFields(table: string, row: Record<string, unknown>): Promise<string[]>;
|
|
2474
|
+
/**
|
|
2475
|
+
* Get list of writable fields for a table
|
|
2476
|
+
*
|
|
2477
|
+
* @param table - Table name
|
|
2478
|
+
* @param row - Existing row data (for context-dependent fields)
|
|
2479
|
+
* @returns Array of writable field names
|
|
2480
|
+
*/
|
|
2481
|
+
getWritableFields(table: string, row: Record<string, unknown>): Promise<string[]>;
|
|
2482
|
+
/**
|
|
2483
|
+
* Get current RLS context
|
|
2484
|
+
*/
|
|
2485
|
+
private getContext;
|
|
2486
|
+
/**
|
|
2487
|
+
* Create evaluation context
|
|
2488
|
+
*/
|
|
2489
|
+
private createEvalContext;
|
|
2490
|
+
/**
|
|
2491
|
+
* Evaluate field access for a specific field
|
|
2492
|
+
*/
|
|
2493
|
+
private evaluateFieldAccess;
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Create a field access processor
|
|
2497
|
+
*/
|
|
2498
|
+
declare function createFieldAccessProcessor<DB = unknown>(registry: FieldAccessRegistry<DB>, defaultMaskValue?: unknown): FieldAccessProcessor<DB>;
|
|
2499
|
+
|
|
2500
|
+
/**
|
|
2501
|
+
* Policy Composition Types
|
|
2502
|
+
*
|
|
2503
|
+
* Provides types for creating reusable, composable RLS policies.
|
|
2504
|
+
*
|
|
2505
|
+
* @module @kysera/rls/composition/types
|
|
2506
|
+
*/
|
|
2507
|
+
|
|
2508
|
+
/**
|
|
2509
|
+
* A named, reusable policy template
|
|
2510
|
+
*
|
|
2511
|
+
* Can be composed with other policies and applied to multiple tables.
|
|
2512
|
+
*
|
|
2513
|
+
* @example
|
|
2514
|
+
* ```typescript
|
|
2515
|
+
* const tenantIsolation = definePolicy({
|
|
2516
|
+
* name: 'tenantIsolation',
|
|
2517
|
+
* type: 'filter',
|
|
2518
|
+
* operation: 'read',
|
|
2519
|
+
* filter: ctx => ({ tenant_id: ctx.auth.tenantId }),
|
|
2520
|
+
* priority: 1000
|
|
2521
|
+
* });
|
|
2522
|
+
* ```
|
|
2523
|
+
*/
|
|
2524
|
+
interface ReusablePolicy {
|
|
2525
|
+
/**
|
|
2526
|
+
* Unique name for this policy
|
|
2527
|
+
*/
|
|
2528
|
+
name: string;
|
|
2529
|
+
/**
|
|
2530
|
+
* Description for documentation
|
|
2531
|
+
*/
|
|
2532
|
+
description?: string;
|
|
2533
|
+
/**
|
|
2534
|
+
* Policy definitions (can include multiple policies)
|
|
2535
|
+
*/
|
|
2536
|
+
policies: PolicyDefinition[];
|
|
2537
|
+
/**
|
|
2538
|
+
* Tags for categorization
|
|
2539
|
+
*/
|
|
2540
|
+
tags?: string[];
|
|
2541
|
+
}
|
|
2542
|
+
/**
|
|
2543
|
+
* Configuration for a reusable policy template
|
|
2544
|
+
*/
|
|
2545
|
+
interface ReusablePolicyConfig {
|
|
2546
|
+
/**
|
|
2547
|
+
* Policy name
|
|
2548
|
+
*/
|
|
2549
|
+
name: string;
|
|
2550
|
+
/**
|
|
2551
|
+
* Description
|
|
2552
|
+
*/
|
|
2553
|
+
description?: string;
|
|
2554
|
+
/**
|
|
2555
|
+
* Tags for categorization
|
|
2556
|
+
*/
|
|
2557
|
+
tags?: string[];
|
|
2558
|
+
}
|
|
2559
|
+
/**
|
|
2560
|
+
* Extended table RLS configuration with policy composition support
|
|
2561
|
+
*/
|
|
2562
|
+
interface ComposableTableConfig {
|
|
2563
|
+
/**
|
|
2564
|
+
* Reusable policies to extend from
|
|
2565
|
+
* Policies are applied in order (first = lowest priority)
|
|
2566
|
+
*/
|
|
2567
|
+
extends?: ReusablePolicy[];
|
|
2568
|
+
/**
|
|
2569
|
+
* Additional table-specific policies
|
|
2570
|
+
*/
|
|
2571
|
+
policies?: PolicyDefinition[];
|
|
2572
|
+
/**
|
|
2573
|
+
* Whether to allow access by default when no policies match
|
|
2574
|
+
* @default true
|
|
2575
|
+
*/
|
|
2576
|
+
defaultDeny?: boolean;
|
|
2577
|
+
/**
|
|
2578
|
+
* Roles that bypass RLS
|
|
2579
|
+
*/
|
|
2580
|
+
skipFor?: string[];
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* Complete schema with composition support
|
|
2584
|
+
*
|
|
2585
|
+
* @typeParam DB - Database schema type
|
|
2586
|
+
*/
|
|
2587
|
+
type ComposableRLSSchema<DB> = {
|
|
2588
|
+
[K in keyof DB]?: ComposableTableConfig;
|
|
2589
|
+
};
|
|
2590
|
+
/**
|
|
2591
|
+
* Base policy that can be extended
|
|
2592
|
+
*/
|
|
2593
|
+
interface BasePolicyDefinition {
|
|
2594
|
+
/**
|
|
2595
|
+
* Unique identifier for this base policy
|
|
2596
|
+
*/
|
|
2597
|
+
id: string;
|
|
2598
|
+
/**
|
|
2599
|
+
* Human-readable name
|
|
2600
|
+
*/
|
|
2601
|
+
name: string;
|
|
2602
|
+
/**
|
|
2603
|
+
* Description
|
|
2604
|
+
*/
|
|
2605
|
+
description?: string;
|
|
2606
|
+
/**
|
|
2607
|
+
* Policies included in this base
|
|
2608
|
+
*/
|
|
2609
|
+
policies: PolicyDefinition[];
|
|
2610
|
+
/**
|
|
2611
|
+
* Other base policies this extends
|
|
2612
|
+
*/
|
|
2613
|
+
extends?: string[];
|
|
2614
|
+
/**
|
|
2615
|
+
* Priority offset applied to all policies
|
|
2616
|
+
* @default 0
|
|
2617
|
+
*/
|
|
2618
|
+
priorityOffset?: number;
|
|
2619
|
+
}
|
|
2620
|
+
/**
|
|
2621
|
+
* Policy inheritance chain resolution
|
|
2622
|
+
*/
|
|
2623
|
+
interface ResolvedInheritance {
|
|
2624
|
+
/**
|
|
2625
|
+
* Final merged policies
|
|
2626
|
+
*/
|
|
2627
|
+
policies: PolicyDefinition[];
|
|
2628
|
+
/**
|
|
2629
|
+
* Chain of base policies used
|
|
2630
|
+
*/
|
|
2631
|
+
inheritanceChain: string[];
|
|
2632
|
+
/**
|
|
2633
|
+
* Any conflicts detected
|
|
2634
|
+
*/
|
|
2635
|
+
conflicts: {
|
|
2636
|
+
policy: string;
|
|
2637
|
+
reason: string;
|
|
2638
|
+
}[];
|
|
2639
|
+
}
|
|
2640
|
+
/**
|
|
2641
|
+
* Multi-tenancy policy configuration
|
|
2642
|
+
*/
|
|
2643
|
+
interface TenantIsolationConfig {
|
|
2644
|
+
/**
|
|
2645
|
+
* Column name for tenant ID
|
|
2646
|
+
* @default 'tenant_id'
|
|
2647
|
+
*/
|
|
2648
|
+
tenantColumn?: string;
|
|
2649
|
+
/**
|
|
2650
|
+
* Operations to apply tenant isolation to
|
|
2651
|
+
* @default ['read', 'create', 'update', 'delete']
|
|
2652
|
+
*/
|
|
2653
|
+
operations?: Operation[];
|
|
2654
|
+
/**
|
|
2655
|
+
* Whether to validate tenant on create/update
|
|
2656
|
+
* @default true
|
|
2657
|
+
*/
|
|
2658
|
+
validateOnMutation?: boolean;
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Ownership policy configuration
|
|
2662
|
+
*/
|
|
2663
|
+
interface OwnershipConfig {
|
|
2664
|
+
/**
|
|
2665
|
+
* Column name for owner ID
|
|
2666
|
+
* @default 'owner_id' or 'user_id'
|
|
2667
|
+
*/
|
|
2668
|
+
ownerColumn?: string;
|
|
2669
|
+
/**
|
|
2670
|
+
* Operations owners can perform
|
|
2671
|
+
* @default ['read', 'update', 'delete']
|
|
2672
|
+
*/
|
|
2673
|
+
ownerOperations?: Operation[];
|
|
2674
|
+
/**
|
|
2675
|
+
* Whether owners can delete
|
|
2676
|
+
* @default true
|
|
2677
|
+
*/
|
|
2678
|
+
canDelete?: boolean;
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* Soft delete policy configuration
|
|
2682
|
+
*/
|
|
2683
|
+
interface SoftDeleteConfig {
|
|
2684
|
+
/**
|
|
2685
|
+
* Column name for soft delete flag
|
|
2686
|
+
* @default 'deleted_at'
|
|
2687
|
+
*/
|
|
2688
|
+
deletedColumn?: string;
|
|
2689
|
+
/**
|
|
2690
|
+
* Whether to filter soft-deleted rows on read
|
|
2691
|
+
* @default true
|
|
2692
|
+
*/
|
|
2693
|
+
filterOnRead?: boolean;
|
|
2694
|
+
/**
|
|
2695
|
+
* Whether to prevent hard deletes
|
|
2696
|
+
* @default true
|
|
2697
|
+
*/
|
|
2698
|
+
preventHardDelete?: boolean;
|
|
2699
|
+
}
|
|
2700
|
+
/**
|
|
2701
|
+
* Status-based access configuration
|
|
2702
|
+
*/
|
|
2703
|
+
interface StatusAccessConfig {
|
|
2704
|
+
/**
|
|
2705
|
+
* Column name for status
|
|
2706
|
+
* @default 'status'
|
|
2707
|
+
*/
|
|
2708
|
+
statusColumn?: string;
|
|
2709
|
+
/**
|
|
2710
|
+
* Statuses that are publicly readable
|
|
2711
|
+
*/
|
|
2712
|
+
publicStatuses?: string[];
|
|
2713
|
+
/**
|
|
2714
|
+
* Statuses that can be updated
|
|
2715
|
+
*/
|
|
2716
|
+
editableStatuses?: string[];
|
|
2717
|
+
/**
|
|
2718
|
+
* Statuses that can be deleted
|
|
2719
|
+
*/
|
|
2720
|
+
deletableStatuses?: string[];
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
/**
|
|
2724
|
+
* Policy Composition Builder
|
|
2725
|
+
*
|
|
2726
|
+
* Factory functions for creating reusable, composable RLS policies.
|
|
2727
|
+
*
|
|
2728
|
+
* @module @kysera/rls/composition/builder
|
|
2729
|
+
*/
|
|
2730
|
+
|
|
2731
|
+
/**
|
|
2732
|
+
* Create a reusable policy template
|
|
2733
|
+
*
|
|
2734
|
+
* @param config - Policy configuration
|
|
2735
|
+
* @param policies - Array of policy definitions
|
|
2736
|
+
* @returns Reusable policy template
|
|
2737
|
+
*
|
|
2738
|
+
* @example
|
|
2739
|
+
* ```typescript
|
|
2740
|
+
* const tenantPolicy = definePolicy(
|
|
2741
|
+
* {
|
|
2742
|
+
* name: 'tenantIsolation',
|
|
2743
|
+
* description: 'Filter by tenant_id',
|
|
2744
|
+
* tags: ['multi-tenant']
|
|
2745
|
+
* },
|
|
2746
|
+
* [
|
|
2747
|
+
* filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }), {
|
|
2748
|
+
* priority: 1000,
|
|
2749
|
+
* name: 'tenant-filter'
|
|
2750
|
+
* }),
|
|
2751
|
+
* validate('create', ctx => ctx.data?.tenant_id === ctx.auth.tenantId, {
|
|
2752
|
+
* name: 'tenant-validate'
|
|
2753
|
+
* })
|
|
2754
|
+
* ]
|
|
2755
|
+
* );
|
|
2756
|
+
* ```
|
|
2757
|
+
*/
|
|
2758
|
+
declare function definePolicy(config: ReusablePolicyConfig, policies: PolicyDefinition[]): ReusablePolicy;
|
|
2759
|
+
/**
|
|
2760
|
+
* Create a filter-only policy
|
|
2761
|
+
*
|
|
2762
|
+
* @param name - Policy name
|
|
2763
|
+
* @param filterFn - Filter condition
|
|
2764
|
+
* @param options - Additional options
|
|
2765
|
+
* @returns Reusable filter policy
|
|
2766
|
+
*/
|
|
2767
|
+
declare function defineFilterPolicy(name: string, filterFn: (ctx: PolicyEvaluationContext) => Record<string, unknown>, options?: {
|
|
2768
|
+
priority?: number;
|
|
2769
|
+
}): ReusablePolicy;
|
|
2770
|
+
/**
|
|
2771
|
+
* Create an allow-based policy
|
|
2772
|
+
*
|
|
2773
|
+
* @param name - Policy name
|
|
2774
|
+
* @param operation - Operations to allow
|
|
2775
|
+
* @param condition - Allow condition
|
|
2776
|
+
* @param options - Additional options
|
|
2777
|
+
* @returns Reusable allow policy
|
|
2778
|
+
*/
|
|
2779
|
+
declare function defineAllowPolicy(name: string, operation: Operation | Operation[], condition: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>, options?: {
|
|
2780
|
+
priority?: number;
|
|
2781
|
+
}): ReusablePolicy;
|
|
2782
|
+
/**
|
|
2783
|
+
* Create a deny-based policy
|
|
2784
|
+
*
|
|
2785
|
+
* @param name - Policy name
|
|
2786
|
+
* @param operation - Operations to deny
|
|
2787
|
+
* @param condition - Deny condition (optional - if not provided, always denies)
|
|
2788
|
+
* @param options - Additional options
|
|
2789
|
+
* @returns Reusable deny policy
|
|
2790
|
+
*/
|
|
2791
|
+
declare function defineDenyPolicy(name: string, operation: Operation | Operation[], condition?: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>, options?: {
|
|
2792
|
+
priority?: number;
|
|
2793
|
+
}): ReusablePolicy;
|
|
2794
|
+
/**
|
|
2795
|
+
* Create a validation policy
|
|
2796
|
+
*
|
|
2797
|
+
* @param name - Policy name
|
|
2798
|
+
* @param operation - Operations to validate
|
|
2799
|
+
* @param condition - Validation condition
|
|
2800
|
+
* @param options - Additional options
|
|
2801
|
+
* @returns Reusable validate policy
|
|
2802
|
+
*/
|
|
2803
|
+
declare function defineValidatePolicy(name: string, operation: 'create' | 'update' | 'all', condition: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>, options?: {
|
|
2804
|
+
priority?: number;
|
|
2805
|
+
}): ReusablePolicy;
|
|
2806
|
+
/**
|
|
2807
|
+
* Create a combined policy with multiple types
|
|
2808
|
+
*
|
|
2809
|
+
* @param name - Policy name
|
|
2810
|
+
* @param config - Policy configurations
|
|
2811
|
+
* @returns Reusable combined policy
|
|
2812
|
+
*/
|
|
2813
|
+
declare function defineCombinedPolicy(name: string, config: {
|
|
2814
|
+
filter?: (ctx: PolicyEvaluationContext) => Record<string, unknown>;
|
|
2815
|
+
allow?: Record<string, (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>>;
|
|
2816
|
+
deny?: Record<string, (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>>;
|
|
2817
|
+
validate?: {
|
|
2818
|
+
create?: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>;
|
|
2819
|
+
update?: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>;
|
|
2820
|
+
};
|
|
2821
|
+
}): ReusablePolicy;
|
|
2822
|
+
/**
|
|
2823
|
+
* Create a tenant isolation policy
|
|
2824
|
+
*
|
|
2825
|
+
* Automatically filters by tenant_id and validates mutations.
|
|
2826
|
+
*
|
|
2827
|
+
* @param config - Tenant isolation configuration
|
|
2828
|
+
* @returns Reusable tenant isolation policy
|
|
2829
|
+
*/
|
|
2830
|
+
declare function createTenantIsolationPolicy(config?: TenantIsolationConfig): ReusablePolicy;
|
|
2831
|
+
/**
|
|
2832
|
+
* Create an ownership policy
|
|
2833
|
+
*
|
|
2834
|
+
* Allows owners to read/update/delete their own resources.
|
|
2835
|
+
*
|
|
2836
|
+
* @param config - Ownership configuration
|
|
2837
|
+
* @returns Reusable ownership policy
|
|
2838
|
+
*/
|
|
2839
|
+
declare function createOwnershipPolicy(config?: OwnershipConfig): ReusablePolicy;
|
|
2840
|
+
/**
|
|
2841
|
+
* Create a soft delete policy
|
|
2842
|
+
*
|
|
2843
|
+
* Filters out soft-deleted rows and optionally prevents hard deletes.
|
|
2844
|
+
*
|
|
2845
|
+
* @param config - Soft delete configuration
|
|
2846
|
+
* @returns Reusable soft delete policy
|
|
2847
|
+
*/
|
|
2848
|
+
declare function createSoftDeletePolicy(config?: SoftDeleteConfig): ReusablePolicy;
|
|
2849
|
+
/**
|
|
2850
|
+
* Create a status-based access policy
|
|
2851
|
+
*
|
|
2852
|
+
* Controls access based on resource status.
|
|
2853
|
+
*
|
|
2854
|
+
* @param config - Status access configuration
|
|
2855
|
+
* @returns Reusable status policy
|
|
2856
|
+
*/
|
|
2857
|
+
declare function createStatusAccessPolicy(config: StatusAccessConfig): ReusablePolicy;
|
|
2858
|
+
/**
|
|
2859
|
+
* Create an admin bypass policy
|
|
2860
|
+
*
|
|
2861
|
+
* Allows admin roles to perform all operations.
|
|
2862
|
+
*
|
|
2863
|
+
* @param roles - Roles that have admin access
|
|
2864
|
+
* @returns Reusable admin policy
|
|
2865
|
+
*/
|
|
2866
|
+
declare function createAdminPolicy(roles: string[]): ReusablePolicy;
|
|
2867
|
+
/**
|
|
2868
|
+
* Compose multiple reusable policies into one
|
|
2869
|
+
*
|
|
2870
|
+
* @param name - Name for the composed policy
|
|
2871
|
+
* @param policies - Policies to compose
|
|
2872
|
+
* @returns Composed policy
|
|
2873
|
+
*/
|
|
2874
|
+
declare function composePolicies(name: string, policies: ReusablePolicy[]): ReusablePolicy;
|
|
2875
|
+
/**
|
|
2876
|
+
* Extend a reusable policy with additional policies
|
|
2877
|
+
*
|
|
2878
|
+
* @param base - Base policy to extend
|
|
2879
|
+
* @param additional - Additional policies to add
|
|
2880
|
+
* @returns Extended policy
|
|
2881
|
+
*/
|
|
2882
|
+
declare function extendPolicy(base: ReusablePolicy, additional: PolicyDefinition[]): ReusablePolicy;
|
|
2883
|
+
/**
|
|
2884
|
+
* Override policies from a base with new conditions
|
|
2885
|
+
*
|
|
2886
|
+
* @param base - Base policy
|
|
2887
|
+
* @param overrides - Policy name to new policy mapping
|
|
2888
|
+
* @returns Policy with overrides applied
|
|
2889
|
+
*/
|
|
2890
|
+
declare function overridePolicy(base: ReusablePolicy, overrides: Record<string, PolicyDefinition>): ReusablePolicy;
|
|
2891
|
+
|
|
2892
|
+
/**
|
|
2893
|
+
* Audit Trail Types
|
|
2894
|
+
*
|
|
2895
|
+
* Provides type definitions for auditing RLS policy decisions.
|
|
2896
|
+
*
|
|
2897
|
+
* @module @kysera/rls/audit/types
|
|
2898
|
+
*/
|
|
2899
|
+
|
|
2900
|
+
/**
|
|
2901
|
+
* RLS policy decision result
|
|
2902
|
+
*/
|
|
2903
|
+
type AuditDecision = 'allow' | 'deny' | 'filter';
|
|
2904
|
+
/**
|
|
2905
|
+
* RLS audit event
|
|
2906
|
+
*
|
|
2907
|
+
* Represents a single policy evaluation event for audit logging.
|
|
2908
|
+
*
|
|
2909
|
+
* @example
|
|
2910
|
+
* ```typescript
|
|
2911
|
+
* const event: RLSAuditEvent = {
|
|
2912
|
+
* timestamp: new Date(),
|
|
2913
|
+
* userId: '123',
|
|
2914
|
+
* operation: 'update',
|
|
2915
|
+
* table: 'posts',
|
|
2916
|
+
* policyName: 'ownership-allow',
|
|
2917
|
+
* decision: 'allow',
|
|
2918
|
+
* context: { rowId: '456', tenantId: 'acme' }
|
|
2919
|
+
* };
|
|
2920
|
+
* ```
|
|
2921
|
+
*/
|
|
2922
|
+
interface RLSAuditEvent {
|
|
2923
|
+
/**
|
|
2924
|
+
* Timestamp of the event
|
|
2925
|
+
*/
|
|
2926
|
+
timestamp: Date;
|
|
2927
|
+
/**
|
|
2928
|
+
* User ID who performed the action
|
|
2929
|
+
*/
|
|
2930
|
+
userId: string | number;
|
|
2931
|
+
/**
|
|
2932
|
+
* Tenant ID (if multi-tenant)
|
|
2933
|
+
*/
|
|
2934
|
+
tenantId?: string | number;
|
|
2935
|
+
/**
|
|
2936
|
+
* Database operation
|
|
2937
|
+
*/
|
|
2938
|
+
operation: Operation;
|
|
2939
|
+
/**
|
|
2940
|
+
* Table name
|
|
2941
|
+
*/
|
|
2942
|
+
table: string;
|
|
2943
|
+
/**
|
|
2944
|
+
* Name of the policy that made the decision
|
|
2945
|
+
*/
|
|
2946
|
+
policyName?: string;
|
|
2947
|
+
/**
|
|
2948
|
+
* Decision result
|
|
2949
|
+
*/
|
|
2950
|
+
decision: AuditDecision;
|
|
2951
|
+
/**
|
|
2952
|
+
* Reason for the decision (especially for denials)
|
|
2953
|
+
*/
|
|
2954
|
+
reason?: string;
|
|
2955
|
+
/**
|
|
2956
|
+
* Additional context about the event
|
|
2957
|
+
*/
|
|
2958
|
+
context?: Record<string, unknown>;
|
|
2959
|
+
/**
|
|
2960
|
+
* Row ID(s) affected
|
|
2961
|
+
*/
|
|
2962
|
+
rowIds?: (string | number)[];
|
|
2963
|
+
/**
|
|
2964
|
+
* Hash of the query (for grouping similar queries)
|
|
2965
|
+
*/
|
|
2966
|
+
queryHash?: string;
|
|
2967
|
+
/**
|
|
2968
|
+
* Request ID for tracing
|
|
2969
|
+
*/
|
|
2970
|
+
requestId?: string;
|
|
2971
|
+
/**
|
|
2972
|
+
* IP address of the requester
|
|
2973
|
+
*/
|
|
2974
|
+
ipAddress?: string;
|
|
2975
|
+
/**
|
|
2976
|
+
* User agent string
|
|
2977
|
+
*/
|
|
2978
|
+
userAgent?: string;
|
|
2979
|
+
/**
|
|
2980
|
+
* Duration of policy evaluation in milliseconds
|
|
2981
|
+
*/
|
|
2982
|
+
durationMs?: number;
|
|
2983
|
+
/**
|
|
2984
|
+
* Whether this event was filtered from logging
|
|
2985
|
+
* (set by filtering rules but still available for debugging)
|
|
2986
|
+
*/
|
|
2987
|
+
filtered?: boolean;
|
|
2988
|
+
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Adapter for persisting audit events
|
|
2991
|
+
*
|
|
2992
|
+
* Implement this interface to store audit events in your preferred backend.
|
|
2993
|
+
*
|
|
2994
|
+
* @example
|
|
2995
|
+
* ```typescript
|
|
2996
|
+
* class DatabaseAuditAdapter implements RLSAuditAdapter {
|
|
2997
|
+
* constructor(private db: Kysely<AuditDB>) {}
|
|
2998
|
+
*
|
|
2999
|
+
* async log(event: RLSAuditEvent): Promise<void> {
|
|
3000
|
+
* await this.db.insertInto('rls_audit_log')
|
|
3001
|
+
* .values({
|
|
3002
|
+
* user_id: event.userId,
|
|
3003
|
+
* operation: event.operation,
|
|
3004
|
+
* table_name: event.table,
|
|
3005
|
+
* decision: event.decision,
|
|
3006
|
+
* context: JSON.stringify(event.context),
|
|
3007
|
+
* created_at: event.timestamp
|
|
3008
|
+
* })
|
|
3009
|
+
* .execute();
|
|
3010
|
+
* }
|
|
3011
|
+
*
|
|
3012
|
+
* async logBatch(events: RLSAuditEvent[]): Promise<void> {
|
|
3013
|
+
* await this.db.insertInto('rls_audit_log')
|
|
3014
|
+
* .values(events.map(e => ({
|
|
3015
|
+
* user_id: e.userId,
|
|
3016
|
+
* operation: e.operation,
|
|
3017
|
+
* table_name: e.table,
|
|
3018
|
+
* decision: e.decision,
|
|
3019
|
+
* context: JSON.stringify(e.context),
|
|
3020
|
+
* created_at: e.timestamp
|
|
3021
|
+
* })))
|
|
3022
|
+
* .execute();
|
|
3023
|
+
* }
|
|
3024
|
+
* }
|
|
3025
|
+
* ```
|
|
3026
|
+
*/
|
|
3027
|
+
interface RLSAuditAdapter {
|
|
3028
|
+
/**
|
|
3029
|
+
* Log a single audit event
|
|
3030
|
+
*
|
|
3031
|
+
* @param event - Event to log
|
|
3032
|
+
*/
|
|
3033
|
+
log(event: RLSAuditEvent): Promise<void>;
|
|
3034
|
+
/**
|
|
3035
|
+
* Log multiple audit events (for batch processing)
|
|
3036
|
+
*
|
|
3037
|
+
* @param events - Events to log
|
|
3038
|
+
*/
|
|
3039
|
+
logBatch?(events: RLSAuditEvent[]): Promise<void>;
|
|
3040
|
+
/**
|
|
3041
|
+
* Flush any buffered events
|
|
3042
|
+
*/
|
|
3043
|
+
flush?(): Promise<void>;
|
|
3044
|
+
/**
|
|
3045
|
+
* Close the adapter and release resources
|
|
3046
|
+
*/
|
|
3047
|
+
close?(): Promise<void>;
|
|
3048
|
+
}
|
|
3049
|
+
/**
|
|
3050
|
+
* Configuration for table-specific audit settings
|
|
3051
|
+
*/
|
|
3052
|
+
interface TableAuditConfig {
|
|
3053
|
+
/**
|
|
3054
|
+
* Whether audit is enabled for this table
|
|
3055
|
+
* @default true (if audit is globally enabled)
|
|
3056
|
+
*/
|
|
3057
|
+
enabled?: boolean;
|
|
3058
|
+
/**
|
|
3059
|
+
* Log allowed decisions
|
|
3060
|
+
* @default false
|
|
3061
|
+
*/
|
|
3062
|
+
logAllowed?: boolean;
|
|
3063
|
+
/**
|
|
3064
|
+
* Log denied decisions
|
|
3065
|
+
* @default true
|
|
3066
|
+
*/
|
|
3067
|
+
logDenied?: boolean;
|
|
3068
|
+
/**
|
|
3069
|
+
* Log filter applications
|
|
3070
|
+
* @default false
|
|
3071
|
+
*/
|
|
3072
|
+
logFilters?: boolean;
|
|
3073
|
+
/**
|
|
3074
|
+
* Context fields to include in audit logs
|
|
3075
|
+
* If empty, includes all available context
|
|
3076
|
+
*/
|
|
3077
|
+
includeContext?: string[];
|
|
3078
|
+
/**
|
|
3079
|
+
* Context fields to exclude from audit logs
|
|
3080
|
+
*/
|
|
3081
|
+
excludeContext?: string[];
|
|
3082
|
+
/**
|
|
3083
|
+
* Whether to include row data in audit logs
|
|
3084
|
+
* @default false (for privacy)
|
|
3085
|
+
*/
|
|
3086
|
+
includeRowData?: boolean;
|
|
3087
|
+
/**
|
|
3088
|
+
* Whether to include mutation data in audit logs
|
|
3089
|
+
* @default false (for privacy)
|
|
3090
|
+
*/
|
|
3091
|
+
includeMutationData?: boolean;
|
|
3092
|
+
/**
|
|
3093
|
+
* Custom filter function to determine if an event should be logged
|
|
3094
|
+
*/
|
|
3095
|
+
filter?: (event: RLSAuditEvent) => boolean;
|
|
3096
|
+
}
|
|
3097
|
+
/**
|
|
3098
|
+
* Global audit configuration
|
|
3099
|
+
*/
|
|
3100
|
+
interface AuditConfig {
|
|
3101
|
+
/**
|
|
3102
|
+
* Audit adapter for persisting events
|
|
3103
|
+
*/
|
|
3104
|
+
adapter: RLSAuditAdapter;
|
|
3105
|
+
/**
|
|
3106
|
+
* Whether audit is enabled globally
|
|
3107
|
+
* @default true
|
|
3108
|
+
*/
|
|
3109
|
+
enabled?: boolean;
|
|
3110
|
+
/**
|
|
3111
|
+
* Default settings for all tables
|
|
3112
|
+
*/
|
|
3113
|
+
defaults?: Omit<TableAuditConfig, 'enabled'>;
|
|
3114
|
+
/**
|
|
3115
|
+
* Table-specific audit configurations
|
|
3116
|
+
*/
|
|
3117
|
+
tables?: Record<string, TableAuditConfig>;
|
|
3118
|
+
/**
|
|
3119
|
+
* Buffer size for batch logging
|
|
3120
|
+
* Events are batched until this size is reached
|
|
3121
|
+
* @default 100
|
|
3122
|
+
*/
|
|
3123
|
+
bufferSize?: number;
|
|
3124
|
+
/**
|
|
3125
|
+
* Maximum time to buffer events before flushing (ms)
|
|
3126
|
+
* @default 5000 (5 seconds)
|
|
3127
|
+
*/
|
|
3128
|
+
flushInterval?: number;
|
|
3129
|
+
/**
|
|
3130
|
+
* Whether to log asynchronously (fire-and-forget)
|
|
3131
|
+
* @default true (for performance)
|
|
3132
|
+
*/
|
|
3133
|
+
async?: boolean;
|
|
3134
|
+
/**
|
|
3135
|
+
* Error handler for audit failures
|
|
3136
|
+
*/
|
|
3137
|
+
onError?: (error: Error, events: RLSAuditEvent[]) => void;
|
|
3138
|
+
/**
|
|
3139
|
+
* Sample rate for audit logging (0.0 to 1.0)
|
|
3140
|
+
* Use for high-traffic systems to reduce log volume
|
|
3141
|
+
* @default 1.0 (log all)
|
|
3142
|
+
*/
|
|
3143
|
+
sampleRate?: number;
|
|
3144
|
+
}
|
|
3145
|
+
/**
|
|
3146
|
+
* Query parameters for retrieving audit events
|
|
3147
|
+
*/
|
|
3148
|
+
interface AuditQueryParams {
|
|
3149
|
+
/**
|
|
3150
|
+
* Filter by user ID
|
|
3151
|
+
*/
|
|
3152
|
+
userId?: string | number;
|
|
3153
|
+
/**
|
|
3154
|
+
* Filter by tenant ID
|
|
3155
|
+
*/
|
|
3156
|
+
tenantId?: string | number;
|
|
3157
|
+
/**
|
|
3158
|
+
* Filter by table name
|
|
3159
|
+
*/
|
|
3160
|
+
table?: string;
|
|
3161
|
+
/**
|
|
3162
|
+
* Filter by operation
|
|
3163
|
+
*/
|
|
3164
|
+
operation?: Operation;
|
|
3165
|
+
/**
|
|
3166
|
+
* Filter by decision
|
|
3167
|
+
*/
|
|
3168
|
+
decision?: AuditDecision;
|
|
3169
|
+
/**
|
|
3170
|
+
* Start timestamp (inclusive)
|
|
3171
|
+
*/
|
|
3172
|
+
startTime?: Date;
|
|
3173
|
+
/**
|
|
3174
|
+
* End timestamp (exclusive)
|
|
3175
|
+
*/
|
|
3176
|
+
endTime?: Date;
|
|
3177
|
+
/**
|
|
3178
|
+
* Filter by request ID
|
|
3179
|
+
*/
|
|
3180
|
+
requestId?: string;
|
|
3181
|
+
/**
|
|
3182
|
+
* Maximum results to return
|
|
3183
|
+
*/
|
|
3184
|
+
limit?: number;
|
|
3185
|
+
/**
|
|
3186
|
+
* Offset for pagination
|
|
3187
|
+
*/
|
|
3188
|
+
offset?: number;
|
|
3189
|
+
}
|
|
3190
|
+
/**
|
|
3191
|
+
* Aggregated audit statistics
|
|
3192
|
+
*/
|
|
3193
|
+
interface AuditStats {
|
|
3194
|
+
/**
|
|
3195
|
+
* Total number of events
|
|
3196
|
+
*/
|
|
3197
|
+
totalEvents: number;
|
|
3198
|
+
/**
|
|
3199
|
+
* Events by decision type
|
|
3200
|
+
*/
|
|
3201
|
+
byDecision: Record<AuditDecision, number>;
|
|
3202
|
+
/**
|
|
3203
|
+
* Events by operation
|
|
3204
|
+
*/
|
|
3205
|
+
byOperation: Record<Operation, number>;
|
|
3206
|
+
/**
|
|
3207
|
+
* Events by table
|
|
3208
|
+
*/
|
|
3209
|
+
byTable: Record<string, number>;
|
|
3210
|
+
/**
|
|
3211
|
+
* Top denied users
|
|
3212
|
+
*/
|
|
3213
|
+
topDeniedUsers?: {
|
|
3214
|
+
userId: string | number;
|
|
3215
|
+
count: number;
|
|
3216
|
+
}[];
|
|
3217
|
+
/**
|
|
3218
|
+
* Time range of stats
|
|
3219
|
+
*/
|
|
3220
|
+
timeRange: {
|
|
3221
|
+
start: Date;
|
|
3222
|
+
end: Date;
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
/**
|
|
3226
|
+
* Simple console-based audit adapter for development/testing
|
|
3227
|
+
*
|
|
3228
|
+
* @example
|
|
3229
|
+
* ```typescript
|
|
3230
|
+
* const adapter = new ConsoleAuditAdapter({
|
|
3231
|
+
* format: 'json',
|
|
3232
|
+
* colors: true
|
|
3233
|
+
* });
|
|
3234
|
+
* ```
|
|
3235
|
+
*/
|
|
3236
|
+
interface ConsoleAuditAdapterOptions {
|
|
3237
|
+
/**
|
|
3238
|
+
* Output format
|
|
3239
|
+
* @default 'text'
|
|
3240
|
+
*/
|
|
3241
|
+
format?: 'text' | 'json';
|
|
3242
|
+
/**
|
|
3243
|
+
* Use colors in output (for text format)
|
|
3244
|
+
* @default true
|
|
3245
|
+
*/
|
|
3246
|
+
colors?: boolean;
|
|
3247
|
+
/**
|
|
3248
|
+
* Include timestamp in output
|
|
3249
|
+
* @default true
|
|
3250
|
+
*/
|
|
3251
|
+
includeTimestamp?: boolean;
|
|
3252
|
+
}
|
|
3253
|
+
/**
|
|
3254
|
+
* Console audit adapter implementation
|
|
3255
|
+
*/
|
|
3256
|
+
declare class ConsoleAuditAdapter implements RLSAuditAdapter {
|
|
3257
|
+
private options;
|
|
3258
|
+
constructor(options?: ConsoleAuditAdapterOptions);
|
|
3259
|
+
log(event: RLSAuditEvent): Promise<void>;
|
|
3260
|
+
logBatch(events: RLSAuditEvent[]): Promise<void>;
|
|
3261
|
+
private getPrefix;
|
|
3262
|
+
}
|
|
3263
|
+
/**
|
|
3264
|
+
* In-memory audit adapter for testing
|
|
3265
|
+
*
|
|
3266
|
+
* Stores events in memory for later retrieval and assertion.
|
|
3267
|
+
*/
|
|
3268
|
+
declare class InMemoryAuditAdapter implements RLSAuditAdapter {
|
|
3269
|
+
private events;
|
|
3270
|
+
private maxSize;
|
|
3271
|
+
constructor(maxSize?: number);
|
|
3272
|
+
log(event: RLSAuditEvent): Promise<void>;
|
|
3273
|
+
logBatch(events: RLSAuditEvent[]): Promise<void>;
|
|
3274
|
+
/**
|
|
3275
|
+
* Get all logged events
|
|
3276
|
+
*/
|
|
3277
|
+
getEvents(): RLSAuditEvent[];
|
|
3278
|
+
/**
|
|
3279
|
+
* Query events
|
|
3280
|
+
*/
|
|
3281
|
+
query(params: AuditQueryParams): RLSAuditEvent[];
|
|
3282
|
+
/**
|
|
3283
|
+
* Get statistics
|
|
3284
|
+
*/
|
|
3285
|
+
getStats(params?: Pick<AuditQueryParams, 'startTime' | 'endTime'>): AuditStats;
|
|
3286
|
+
/**
|
|
3287
|
+
* Clear all events
|
|
3288
|
+
*/
|
|
3289
|
+
clear(): void;
|
|
3290
|
+
/**
|
|
3291
|
+
* Get event count
|
|
3292
|
+
*/
|
|
3293
|
+
get size(): number;
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
/**
|
|
3297
|
+
* Audit Logger
|
|
3298
|
+
*
|
|
3299
|
+
* Manages audit event logging with buffering and filtering.
|
|
3300
|
+
*
|
|
3301
|
+
* @module @kysera/rls/audit/logger
|
|
3302
|
+
*/
|
|
3303
|
+
|
|
3304
|
+
/**
|
|
3305
|
+
* Audit Logger
|
|
3306
|
+
*
|
|
3307
|
+
* Manages RLS audit event logging with buffering, filtering, and sampling.
|
|
3308
|
+
*
|
|
3309
|
+
* @example
|
|
3310
|
+
* ```typescript
|
|
3311
|
+
* const logger = new AuditLogger({
|
|
3312
|
+
* adapter: new DatabaseAuditAdapter(db),
|
|
3313
|
+
* bufferSize: 50,
|
|
3314
|
+
* flushInterval: 5000,
|
|
3315
|
+
* defaults: {
|
|
3316
|
+
* logAllowed: false,
|
|
3317
|
+
* logDenied: true,
|
|
3318
|
+
* logFilters: false
|
|
3319
|
+
* },
|
|
3320
|
+
* tables: {
|
|
3321
|
+
* sensitive_data: {
|
|
3322
|
+
* logAllowed: true,
|
|
3323
|
+
* includeContext: ['requestId', 'ipAddress']
|
|
3324
|
+
* }
|
|
3325
|
+
* }
|
|
3326
|
+
* });
|
|
3327
|
+
*
|
|
3328
|
+
* // Log an event
|
|
3329
|
+
* await logger.logDecision('update', 'posts', 'allow', 'ownership-allow');
|
|
3330
|
+
*
|
|
3331
|
+
* // Ensure all events are flushed
|
|
3332
|
+
* await logger.flush();
|
|
3333
|
+
* ```
|
|
3334
|
+
*/
|
|
3335
|
+
declare class AuditLogger {
|
|
3336
|
+
private adapter;
|
|
3337
|
+
private config;
|
|
3338
|
+
private buffer;
|
|
3339
|
+
private flushTimer;
|
|
3340
|
+
private isShuttingDown;
|
|
3341
|
+
constructor(config: AuditConfig);
|
|
3342
|
+
/**
|
|
3343
|
+
* Log a policy decision
|
|
3344
|
+
*
|
|
3345
|
+
* @param operation - Database operation
|
|
3346
|
+
* @param table - Table name
|
|
3347
|
+
* @param decision - Decision result
|
|
3348
|
+
* @param policyName - Name of the policy
|
|
3349
|
+
* @param options - Additional options
|
|
3350
|
+
*/
|
|
3351
|
+
logDecision(operation: Operation, table: string, decision: AuditDecision, policyName?: string, options?: {
|
|
3352
|
+
reason?: string;
|
|
3353
|
+
rowIds?: (string | number)[];
|
|
3354
|
+
queryHash?: string;
|
|
3355
|
+
durationMs?: number;
|
|
3356
|
+
context?: Record<string, unknown>;
|
|
3357
|
+
}): Promise<void>;
|
|
3358
|
+
/**
|
|
3359
|
+
* Log an allow decision
|
|
3360
|
+
*/
|
|
3361
|
+
logAllow(operation: Operation, table: string, policyName?: string, options?: {
|
|
3362
|
+
reason?: string;
|
|
3363
|
+
rowIds?: (string | number)[];
|
|
3364
|
+
context?: Record<string, unknown>;
|
|
3365
|
+
}): Promise<void>;
|
|
3366
|
+
/**
|
|
3367
|
+
* Log a deny decision
|
|
3368
|
+
*/
|
|
3369
|
+
logDeny(operation: Operation, table: string, policyName?: string, options?: {
|
|
3370
|
+
reason?: string;
|
|
3371
|
+
rowIds?: (string | number)[];
|
|
3372
|
+
context?: Record<string, unknown>;
|
|
3373
|
+
}): Promise<void>;
|
|
3374
|
+
/**
|
|
3375
|
+
* Log a filter application
|
|
3376
|
+
*/
|
|
3377
|
+
logFilter(table: string, policyName?: string, options?: {
|
|
3378
|
+
context?: Record<string, unknown>;
|
|
3379
|
+
}): Promise<void>;
|
|
3380
|
+
/**
|
|
3381
|
+
* Flush buffered events
|
|
3382
|
+
*/
|
|
3383
|
+
flush(): Promise<void>;
|
|
3384
|
+
/**
|
|
3385
|
+
* Close the logger
|
|
3386
|
+
*/
|
|
3387
|
+
close(): Promise<void>;
|
|
3388
|
+
/**
|
|
3389
|
+
* Get buffer size
|
|
3390
|
+
*/
|
|
3391
|
+
get bufferSize(): number;
|
|
3392
|
+
/**
|
|
3393
|
+
* Check if logger is enabled
|
|
3394
|
+
*/
|
|
3395
|
+
get enabled(): boolean;
|
|
3396
|
+
/**
|
|
3397
|
+
* Enable or disable logging
|
|
3398
|
+
*/
|
|
3399
|
+
setEnabled(enabled: boolean): void;
|
|
3400
|
+
/**
|
|
3401
|
+
* Get table-specific config with defaults
|
|
3402
|
+
*/
|
|
3403
|
+
private getTableConfig;
|
|
3404
|
+
/**
|
|
3405
|
+
* Check if decision should be logged
|
|
3406
|
+
*/
|
|
3407
|
+
private shouldLog;
|
|
3408
|
+
/**
|
|
3409
|
+
* Build audit event
|
|
3410
|
+
*/
|
|
3411
|
+
private buildEvent;
|
|
3412
|
+
/**
|
|
3413
|
+
* Build context object with filtering
|
|
3414
|
+
*/
|
|
3415
|
+
private buildContext;
|
|
3416
|
+
/**
|
|
3417
|
+
* Log event to buffer or directly
|
|
3418
|
+
*/
|
|
3419
|
+
private logEvent;
|
|
3420
|
+
/**
|
|
3421
|
+
* Start the flush timer
|
|
3422
|
+
*/
|
|
3423
|
+
private startFlushTimer;
|
|
3424
|
+
}
|
|
3425
|
+
/**
|
|
3426
|
+
* Create an audit logger
|
|
3427
|
+
*/
|
|
3428
|
+
declare function createAuditLogger(config: AuditConfig): AuditLogger;
|
|
3429
|
+
|
|
3430
|
+
/**
|
|
3431
|
+
* Policy Testing Utilities
|
|
3432
|
+
*
|
|
3433
|
+
* Provides tools for unit testing RLS policies without a database.
|
|
3434
|
+
*
|
|
3435
|
+
* @module @kysera/rls/testing
|
|
3436
|
+
*/
|
|
3437
|
+
|
|
3438
|
+
/**
|
|
3439
|
+
* Result of policy evaluation
|
|
3440
|
+
*/
|
|
3441
|
+
interface PolicyEvaluationResult {
|
|
3442
|
+
/**
|
|
3443
|
+
* Whether the operation is allowed
|
|
3444
|
+
*/
|
|
3445
|
+
allowed: boolean;
|
|
3446
|
+
/**
|
|
3447
|
+
* Name of the policy that made the decision
|
|
3448
|
+
*/
|
|
3449
|
+
policyName?: string;
|
|
3450
|
+
/**
|
|
3451
|
+
* Type of decision
|
|
3452
|
+
*/
|
|
3453
|
+
decisionType: 'allow' | 'deny' | 'default';
|
|
3454
|
+
/**
|
|
3455
|
+
* Reason for the decision
|
|
3456
|
+
*/
|
|
3457
|
+
reason?: string;
|
|
3458
|
+
/**
|
|
3459
|
+
* All policies that were evaluated
|
|
3460
|
+
*/
|
|
3461
|
+
evaluatedPolicies: {
|
|
3462
|
+
name: string;
|
|
3463
|
+
type: 'allow' | 'deny' | 'validate';
|
|
3464
|
+
result: boolean;
|
|
3465
|
+
}[];
|
|
3466
|
+
}
|
|
3467
|
+
/**
|
|
3468
|
+
* Result of filter evaluation
|
|
3469
|
+
*/
|
|
3470
|
+
interface FilterEvaluationResult {
|
|
3471
|
+
/**
|
|
3472
|
+
* Generated filter conditions
|
|
3473
|
+
*/
|
|
3474
|
+
conditions: Record<string, unknown>;
|
|
3475
|
+
/**
|
|
3476
|
+
* Names of all filters applied
|
|
3477
|
+
*/
|
|
3478
|
+
appliedFilters: string[];
|
|
3479
|
+
}
|
|
3480
|
+
/**
|
|
3481
|
+
* Test context for policy evaluation
|
|
3482
|
+
*/
|
|
3483
|
+
interface TestContext<TRow = Record<string, unknown>> {
|
|
3484
|
+
/**
|
|
3485
|
+
* Auth context
|
|
3486
|
+
*/
|
|
3487
|
+
auth: RLSAuthContext;
|
|
3488
|
+
/**
|
|
3489
|
+
* Row data (for read/update/delete operations)
|
|
3490
|
+
*/
|
|
3491
|
+
row?: TRow;
|
|
3492
|
+
/**
|
|
3493
|
+
* Mutation data (for create/update operations)
|
|
3494
|
+
*/
|
|
3495
|
+
data?: Record<string, unknown>;
|
|
3496
|
+
/**
|
|
3497
|
+
* Additional metadata
|
|
3498
|
+
*/
|
|
3499
|
+
meta?: Record<string, unknown>;
|
|
3500
|
+
}
|
|
3501
|
+
/**
|
|
3502
|
+
* Policy Tester
|
|
3503
|
+
*
|
|
3504
|
+
* Test RLS policies without a database connection.
|
|
3505
|
+
*
|
|
3506
|
+
* @example
|
|
3507
|
+
* ```typescript
|
|
3508
|
+
* const tester = createPolicyTester(rlsSchema);
|
|
3509
|
+
*
|
|
3510
|
+
* describe('Post RLS Policies', () => {
|
|
3511
|
+
* it('should allow owner to update their post', async () => {
|
|
3512
|
+
* const result = await tester.evaluate('posts', 'update', {
|
|
3513
|
+
* auth: { userId: 'user-1', roles: ['user'] },
|
|
3514
|
+
* row: { id: 'post-1', author_id: 'user-1', status: 'draft' }
|
|
3515
|
+
* });
|
|
3516
|
+
*
|
|
3517
|
+
* expect(result.allowed).toBe(true);
|
|
3518
|
+
* });
|
|
3519
|
+
*
|
|
3520
|
+
* it('should deny non-owner update', async () => {
|
|
3521
|
+
* const result = await tester.evaluate('posts', 'update', {
|
|
3522
|
+
* auth: { userId: 'user-2', roles: ['user'] },
|
|
3523
|
+
* row: { id: 'post-1', author_id: 'user-1', status: 'draft' }
|
|
3524
|
+
* });
|
|
3525
|
+
*
|
|
3526
|
+
* expect(result.allowed).toBe(false);
|
|
3527
|
+
* expect(result.reason).toContain('not owner');
|
|
3528
|
+
* });
|
|
3529
|
+
*
|
|
3530
|
+
* it('should apply filters correctly', async () => {
|
|
3531
|
+
* const filters = await tester.getFilters('posts', 'read', {
|
|
3532
|
+
* auth: { userId: 'user-1', tenantId: 'tenant-1', roles: [] }
|
|
3533
|
+
* });
|
|
3534
|
+
*
|
|
3535
|
+
* expect(filters.conditions).toEqual({
|
|
3536
|
+
* tenant_id: 'tenant-1',
|
|
3537
|
+
* deleted_at: null
|
|
3538
|
+
* });
|
|
3539
|
+
* });
|
|
3540
|
+
* });
|
|
3541
|
+
* ```
|
|
3542
|
+
*/
|
|
3543
|
+
declare class PolicyTester<DB = unknown> {
|
|
3544
|
+
private registry;
|
|
3545
|
+
constructor(schema: RLSSchema<DB>);
|
|
3546
|
+
/**
|
|
3547
|
+
* Evaluate policies for an operation
|
|
3548
|
+
*
|
|
3549
|
+
* @param table - Table name
|
|
3550
|
+
* @param operation - Operation to test
|
|
3551
|
+
* @param context - Test context
|
|
3552
|
+
* @returns Evaluation result
|
|
3553
|
+
*/
|
|
3554
|
+
evaluate(table: string, operation: Operation, context: TestContext): Promise<PolicyEvaluationResult>;
|
|
3555
|
+
/**
|
|
3556
|
+
* Get filter conditions for read operations
|
|
3557
|
+
*
|
|
3558
|
+
* @param table - Table name
|
|
3559
|
+
* @param operation - Must be 'read'
|
|
3560
|
+
* @param context - Test context
|
|
3561
|
+
* @returns Filter conditions
|
|
3562
|
+
*/
|
|
3563
|
+
getFilters(table: string, _operation: 'read', context: Pick<TestContext, 'auth' | 'meta'>): FilterEvaluationResult;
|
|
3564
|
+
/**
|
|
3565
|
+
* Test if a specific policy allows the operation
|
|
3566
|
+
*
|
|
3567
|
+
* @param table - Table name
|
|
3568
|
+
* @param policyName - Name of the policy to test
|
|
3569
|
+
* @param context - Test context
|
|
3570
|
+
* @returns True if policy allows
|
|
3571
|
+
*/
|
|
3572
|
+
testPolicy(table: string, policyName: string, context: TestContext): Promise<{
|
|
3573
|
+
found: boolean;
|
|
3574
|
+
result?: boolean;
|
|
3575
|
+
}>;
|
|
3576
|
+
/**
|
|
3577
|
+
* List all policies for a table
|
|
3578
|
+
*/
|
|
3579
|
+
listPolicies(table: string): {
|
|
3580
|
+
allows: string[];
|
|
3581
|
+
denies: string[];
|
|
3582
|
+
filters: string[];
|
|
3583
|
+
validates: string[];
|
|
3584
|
+
};
|
|
3585
|
+
/**
|
|
3586
|
+
* Get all registered tables
|
|
3587
|
+
*/
|
|
3588
|
+
getTables(): string[];
|
|
3589
|
+
/**
|
|
3590
|
+
* Evaluate a single policy
|
|
3591
|
+
*/
|
|
3592
|
+
private evaluatePolicy;
|
|
3593
|
+
}
|
|
3594
|
+
/**
|
|
3595
|
+
* Create a policy tester
|
|
3596
|
+
*
|
|
3597
|
+
* @param schema - RLS schema to test
|
|
3598
|
+
* @returns PolicyTester instance
|
|
3599
|
+
*/
|
|
3600
|
+
declare function createPolicyTester<DB = unknown>(schema: RLSSchema<DB>): PolicyTester<DB>;
|
|
3601
|
+
/**
|
|
3602
|
+
* Create a test auth context
|
|
3603
|
+
*
|
|
3604
|
+
* @param overrides - Values to override
|
|
3605
|
+
* @returns RLSAuthContext for testing
|
|
3606
|
+
*/
|
|
3607
|
+
declare function createTestAuthContext(overrides: Partial<RLSAuthContext> & {
|
|
3608
|
+
userId: string | number;
|
|
3609
|
+
}): RLSAuthContext;
|
|
3610
|
+
/**
|
|
3611
|
+
* Create a test row
|
|
3612
|
+
*
|
|
3613
|
+
* @param data - Row data
|
|
3614
|
+
* @returns Row object
|
|
3615
|
+
*/
|
|
3616
|
+
declare function createTestRow<T extends Record<string, unknown>>(data: T): T;
|
|
3617
|
+
/**
|
|
3618
|
+
* Assertion helpers for policy testing
|
|
3619
|
+
*/
|
|
3620
|
+
declare const policyAssertions: {
|
|
3621
|
+
/**
|
|
3622
|
+
* Assert that the result is allowed
|
|
3623
|
+
*/
|
|
3624
|
+
assertAllowed(result: PolicyEvaluationResult, message?: string): void;
|
|
3625
|
+
/**
|
|
3626
|
+
* Assert that the result is denied
|
|
3627
|
+
*/
|
|
3628
|
+
assertDenied(result: PolicyEvaluationResult, message?: string): void;
|
|
3629
|
+
/**
|
|
3630
|
+
* Assert that a specific policy made the decision
|
|
3631
|
+
*/
|
|
3632
|
+
assertPolicyUsed(result: PolicyEvaluationResult, policyName: string, message?: string): void;
|
|
3633
|
+
/**
|
|
3634
|
+
* Assert that filters include expected conditions
|
|
3635
|
+
*/
|
|
3636
|
+
assertFiltersInclude(result: FilterEvaluationResult, expected: Record<string, unknown>, message?: string): void;
|
|
3637
|
+
};
|
|
3638
|
+
|
|
3639
|
+
export { type AuditConfig, type AuditDecision, AuditLogger, type AuditQueryParams, type AuditStats, type BasePolicyDefinition, type BaseResolverContext, type CommonResolvedData, type CompiledFieldAccess, CompiledFilterPolicy, CompiledPolicy, type CompiledReBAcPolicy, type CompiledRelationshipPath, type CompiledTableFieldAccess, type ComposableRLSSchema, type ComposableTableConfig, type CompositeResolvedData, ConditionalPolicyDefinition, ConsoleAuditAdapter, type ConsoleAuditAdapterOptions, type ContextResolver, type CreateRLSContextOptions, type EnhancedRLSAuthContext, type EnhancedRLSContext, type FieldAccessCondition, type FieldAccessConfig, type FieldAccessOptions, FieldAccessProcessor, FieldAccessRegistry, type FieldAccessResult, type FieldAccessSchema, type FieldOperation, FilterCondition, type FilterEvaluationResult, type HierarchyResolvedData, InMemoryAuditAdapter, InMemoryCacheProvider, type MaskedRow, Operation, type OrganizationResolvedData, type OwnershipConfig, PolicyActivationCondition, PolicyCondition, PolicyDefinition, PolicyEvaluationContext, type PolicyEvaluationResult, PolicyHints, type PolicyOptions, PolicyRegistry, PolicyTester, type RLSAuditAdapter, type RLSAuditEvent, RLSAuthContext, RLSContext, RLSContextError, RLSContextValidationError, RLSError, type RLSErrorCode, RLSErrorCodes, type RLSPluginOptions, RLSPluginOptionsSchema, RLSPolicyEvaluationError, RLSPolicyViolation, RLSRequestContext, RLSSchema, RLSSchemaError, type ReBAcPolicyDefinition, type ReBAcQueryOptions, ReBAcRegistry, type ReBAcSchema, type ReBAcSubquery, ReBAcTransformer, type RelationshipCondition, type RelationshipPath, type RelationshipStep, type ResolvedData, type ResolvedInheritance, type ResolverCacheProvider, ResolverManager, type ResolverManagerOptions, type ReusablePolicy, type ReusablePolicyConfig, type SoftDeleteConfig, type StatusAccessConfig, type TableAuditConfig, type TableFieldAccessConfig, TableRLSConfig, type TableReBAcConfig, type TenantIsolationConfig, type TenantResolvedData, type TestContext, allow, allowRelation, composePolicies, createAdminPolicy, createAuditLogger, createEvaluationContext, createFieldAccessProcessor, createFieldAccessRegistry, createOwnershipPolicy, createPolicyTester, createRLSContext, createReBAcRegistry, createReBAcTransformer, createResolver, createResolverManager, createSoftDeletePolicy, createStatusAccessPolicy, createTenantIsolationPolicy, createTestAuthContext, createTestRow, deepMerge, defineAllowPolicy, defineCombinedPolicy, defineDenyPolicy, defineFilterPolicy, definePolicy, defineRLSSchema, defineValidatePolicy, deny, denyRelation, extendPolicy, filter, hashString, isAsyncFunction, maskedField, mergeRLSSchemas, neverAccessible, normalizeOperations, orgMembershipPath, overridePolicy, ownerOnly, ownerOrRoles, policyAssertions, publicReadRestrictedWrite, readOnly, rlsContext, rlsPlugin, rolesOnly, safeEvaluate, shopOrgMembershipPath, teamHierarchyPath, validate, whenCondition, whenEnvironment, whenFeature, whenTimeRange, withRLSContext, withRLSContextAsync };
|