@kysera/rls 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1341 -0
- package/dist/index.d.ts +705 -0
- package/dist/index.js +1471 -0
- package/dist/index.js.map +1 -0
- package/dist/native/index.d.ts +91 -0
- package/dist/native/index.js +253 -0
- package/dist/native/index.js.map +1 -0
- package/dist/types-Dtg6Lt1k.d.ts +633 -0
- package/package.json +93 -0
- package/src/context/index.ts +9 -0
- package/src/context/manager.ts +203 -0
- package/src/context/storage.ts +8 -0
- package/src/context/types.ts +5 -0
- package/src/errors.ts +280 -0
- package/src/index.ts +95 -0
- package/src/native/README.md +315 -0
- package/src/native/index.ts +11 -0
- package/src/native/migration.ts +92 -0
- package/src/native/postgres.ts +263 -0
- package/src/plugin.ts +464 -0
- package/src/policy/builder.ts +215 -0
- package/src/policy/index.ts +10 -0
- package/src/policy/registry.ts +403 -0
- package/src/policy/schema.ts +257 -0
- package/src/policy/types.ts +742 -0
- package/src/transformer/index.ts +2 -0
- package/src/transformer/mutation.ts +372 -0
- package/src/transformer/select.ts +150 -0
- package/src/utils/helpers.ts +139 -0
- package/src/utils/index.ts +12 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { R as RLSSchema, O as Operation, P as PolicyCondition, a as PolicyHints, b as PolicyDefinition, F as FilterCondition, T as TableRLSConfig, C as CompiledPolicy, c as CompiledFilterPolicy, d as RLSContext, e as RLSAuthContext, f as RLSRequestContext, g as PolicyEvaluationContext } from './types-Dtg6Lt1k.js';
|
|
2
|
+
export { h as PolicyType } from './types-Dtg6Lt1k.js';
|
|
3
|
+
import { KyseraLogger, ErrorCode } from '@kysera/core';
|
|
4
|
+
import { Plugin } from '@kysera/repository';
|
|
5
|
+
import 'kysely';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* RLS schema definition and validation
|
|
9
|
+
*
|
|
10
|
+
* Provides functions to define, validate, and merge RLS schemas.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Define RLS schema with full type safety
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* interface Database {
|
|
19
|
+
* users: { id: number; email: string; tenant_id: number };
|
|
20
|
+
* posts: { id: number; user_id: number; tenant_id: number };
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* const schema = defineRLSSchema<Database>({
|
|
24
|
+
* users: {
|
|
25
|
+
* policies: [
|
|
26
|
+
* // Users can read their own records
|
|
27
|
+
* allow('read', ctx => ctx.auth.userId === ctx.row.id),
|
|
28
|
+
* // Filter by tenant
|
|
29
|
+
* filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })),
|
|
30
|
+
* // Admins bypass all checks
|
|
31
|
+
* allow('all', ctx => ctx.auth.roles.includes('admin')),
|
|
32
|
+
* ],
|
|
33
|
+
* },
|
|
34
|
+
* posts: {
|
|
35
|
+
* policies: [
|
|
36
|
+
* // Filter posts by tenant
|
|
37
|
+
* filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })),
|
|
38
|
+
* // Users can only edit their own posts
|
|
39
|
+
* allow(['update', 'delete'], ctx => ctx.auth.userId === ctx.row.user_id),
|
|
40
|
+
* // Validate new posts belong to user's tenant
|
|
41
|
+
* validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId),
|
|
42
|
+
* ],
|
|
43
|
+
* defaultDeny: true, // Require explicit allow
|
|
44
|
+
* },
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare function defineRLSSchema<DB>(schema: RLSSchema<DB>): RLSSchema<DB>;
|
|
49
|
+
/**
|
|
50
|
+
* Merge multiple RLS schemas
|
|
51
|
+
* Later schemas override earlier ones for the same table
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const baseSchema = defineRLSSchema<Database>({
|
|
56
|
+
* users: {
|
|
57
|
+
* policies: [
|
|
58
|
+
* filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })),
|
|
59
|
+
* ],
|
|
60
|
+
* },
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* const adminSchema = defineRLSSchema<Database>({
|
|
64
|
+
* users: {
|
|
65
|
+
* policies: [
|
|
66
|
+
* allow('all', ctx => ctx.auth.roles.includes('admin')),
|
|
67
|
+
* ],
|
|
68
|
+
* },
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* // Merged schema will have both filters and admin allow
|
|
72
|
+
* const merged = mergeRLSSchemas(baseSchema, adminSchema);
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
declare function mergeRLSSchemas<DB>(...schemas: RLSSchema<DB>[]): RLSSchema<DB>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Fluent policy builders for Row-Level Security
|
|
79
|
+
*
|
|
80
|
+
* Provides intuitive builder functions for creating RLS policies:
|
|
81
|
+
* - allow: Grants access when condition is true
|
|
82
|
+
* - deny: Blocks access when condition is true (overrides allow)
|
|
83
|
+
* - filter: Adds WHERE conditions to SELECT queries
|
|
84
|
+
* - validate: Validates mutation data before execution
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Options for policy definitions
|
|
89
|
+
*/
|
|
90
|
+
interface PolicyOptions {
|
|
91
|
+
/** Policy name for debugging and identification */
|
|
92
|
+
name?: string;
|
|
93
|
+
/** Priority (higher runs first, deny policies default to 100) */
|
|
94
|
+
priority?: number;
|
|
95
|
+
/** Performance optimization hints */
|
|
96
|
+
hints?: PolicyHints;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Create an allow policy
|
|
100
|
+
* Grants access when condition evaluates to true
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* // Allow users to read their own records
|
|
105
|
+
* allow('read', ctx => ctx.auth.userId === ctx.row.userId)
|
|
106
|
+
*
|
|
107
|
+
* // Allow admins to do everything
|
|
108
|
+
* allow('all', ctx => ctx.auth.roles.includes('admin'))
|
|
109
|
+
*
|
|
110
|
+
* // Allow with multiple operations
|
|
111
|
+
* allow(['read', 'update'], ctx => ctx.auth.userId === ctx.row.userId)
|
|
112
|
+
*
|
|
113
|
+
* // Named policy with priority
|
|
114
|
+
* allow('read', ctx => ctx.auth.roles.includes('verified'), {
|
|
115
|
+
* name: 'verified-users-only',
|
|
116
|
+
* priority: 10
|
|
117
|
+
* })
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
declare function allow(operation: Operation | Operation[], condition: PolicyCondition, options?: PolicyOptions): PolicyDefinition;
|
|
121
|
+
/**
|
|
122
|
+
* Create a deny policy
|
|
123
|
+
* Blocks access when condition evaluates to true (overrides allow)
|
|
124
|
+
* If no condition is provided, always denies
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* // Deny access to banned users
|
|
129
|
+
* deny('all', ctx => ctx.auth.attributes?.banned === true)
|
|
130
|
+
*
|
|
131
|
+
* // Deny deletions on archived records
|
|
132
|
+
* deny('delete', ctx => ctx.row.archived === true)
|
|
133
|
+
*
|
|
134
|
+
* // Deny all access to sensitive table
|
|
135
|
+
* deny('all')
|
|
136
|
+
*
|
|
137
|
+
* // Named deny with high priority
|
|
138
|
+
* deny('all', ctx => ctx.auth.attributes?.suspended === true, {
|
|
139
|
+
* name: 'block-suspended-users',
|
|
140
|
+
* priority: 200
|
|
141
|
+
* })
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
declare function deny(operation: Operation | Operation[], condition?: PolicyCondition, options?: PolicyOptions): PolicyDefinition;
|
|
145
|
+
/**
|
|
146
|
+
* Create a filter policy
|
|
147
|
+
* Adds WHERE conditions to SELECT queries
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* // Filter by tenant
|
|
152
|
+
* filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }))
|
|
153
|
+
*
|
|
154
|
+
* // Filter by organization with soft delete
|
|
155
|
+
* filter('read', ctx => ({
|
|
156
|
+
* organization_id: ctx.auth.organizationIds?.[0],
|
|
157
|
+
* deleted_at: null
|
|
158
|
+
* }))
|
|
159
|
+
*
|
|
160
|
+
* // Named filter
|
|
161
|
+
* filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }), {
|
|
162
|
+
* name: 'tenant-filter'
|
|
163
|
+
* })
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
declare function filter(operation: 'read' | 'all', condition: FilterCondition, options?: PolicyOptions): PolicyDefinition;
|
|
167
|
+
/**
|
|
168
|
+
* Create a validate policy
|
|
169
|
+
* Validates mutation data before execution
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* // Validate user can only set their own user_id
|
|
174
|
+
* validate('create', ctx => ctx.data.userId === ctx.auth.userId)
|
|
175
|
+
*
|
|
176
|
+
* // Validate status transitions
|
|
177
|
+
* validate('update', ctx => {
|
|
178
|
+
* const { status } = ctx.data;
|
|
179
|
+
* return !status || ['draft', 'published'].includes(status);
|
|
180
|
+
* })
|
|
181
|
+
*
|
|
182
|
+
* // Apply to both create and update
|
|
183
|
+
* validate('all', ctx => ctx.data.price >= 0)
|
|
184
|
+
*
|
|
185
|
+
* // Named validation
|
|
186
|
+
* validate('create', ctx => validateEmail(ctx.data.email), {
|
|
187
|
+
* name: 'validate-email'
|
|
188
|
+
* })
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
declare function validate(operation: 'create' | 'update' | 'all', condition: PolicyCondition, options?: PolicyOptions): PolicyDefinition;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Policy Registry
|
|
195
|
+
* Central registry for managing RLS policies across all tables
|
|
196
|
+
*
|
|
197
|
+
* The PolicyRegistry compiles and stores RLS policies for efficient runtime lookup.
|
|
198
|
+
* It categorizes policies by type (allow/deny/filter/validate) and operation,
|
|
199
|
+
* and provides methods to query policies for specific tables and operations.
|
|
200
|
+
*/
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Policy Registry
|
|
204
|
+
* Manages and provides access to RLS policies
|
|
205
|
+
*/
|
|
206
|
+
declare class PolicyRegistry<DB = unknown> {
|
|
207
|
+
private tables;
|
|
208
|
+
private compiled;
|
|
209
|
+
private logger;
|
|
210
|
+
constructor(schema?: RLSSchema<DB>, options?: {
|
|
211
|
+
logger?: KyseraLogger;
|
|
212
|
+
});
|
|
213
|
+
/**
|
|
214
|
+
* Load and compile policies from schema
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```typescript
|
|
218
|
+
* const registry = new PolicyRegistry<Database>();
|
|
219
|
+
* registry.loadSchema({
|
|
220
|
+
* users: {
|
|
221
|
+
* policies: [
|
|
222
|
+
* allow('read', ctx => ctx.auth.userId === ctx.row.id),
|
|
223
|
+
* filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })),
|
|
224
|
+
* ],
|
|
225
|
+
* defaultDeny: true,
|
|
226
|
+
* },
|
|
227
|
+
* });
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
loadSchema(schema: RLSSchema<DB>): void;
|
|
231
|
+
/**
|
|
232
|
+
* Register policies for a single table
|
|
233
|
+
*
|
|
234
|
+
* @param table - Table name
|
|
235
|
+
* @param config - Table RLS configuration
|
|
236
|
+
*/
|
|
237
|
+
registerTable(table: string, config: TableRLSConfig): void;
|
|
238
|
+
/**
|
|
239
|
+
* Register policies - supports both schema and table-based registration
|
|
240
|
+
*
|
|
241
|
+
* @overload Register a full schema
|
|
242
|
+
* @overload Register policies for a single table (deprecated)
|
|
243
|
+
*/
|
|
244
|
+
register(schemaOrTable: RLSSchema<DB>): void;
|
|
245
|
+
register(schemaOrTable: keyof DB & string, policies: PolicyDefinition[], options?: {
|
|
246
|
+
skipFor?: string[];
|
|
247
|
+
defaultDeny?: boolean;
|
|
248
|
+
}): void;
|
|
249
|
+
/**
|
|
250
|
+
* Compile a policy definition into an internal compiled policy
|
|
251
|
+
*
|
|
252
|
+
* @param policy - Policy definition to compile
|
|
253
|
+
* @param name - Policy name for debugging
|
|
254
|
+
* @returns Compiled policy ready for evaluation
|
|
255
|
+
*/
|
|
256
|
+
private compilePolicy;
|
|
257
|
+
/**
|
|
258
|
+
* Compile a filter policy
|
|
259
|
+
*
|
|
260
|
+
* @param policy - Filter policy definition
|
|
261
|
+
* @param name - Policy name for debugging
|
|
262
|
+
* @returns Compiled filter policy
|
|
263
|
+
*/
|
|
264
|
+
private compileFilterPolicy;
|
|
265
|
+
/**
|
|
266
|
+
* Convert internal compiled policy to public CompiledPolicy
|
|
267
|
+
*/
|
|
268
|
+
private toCompiledPolicy;
|
|
269
|
+
/**
|
|
270
|
+
* Get allow policies for a table and operation
|
|
271
|
+
*/
|
|
272
|
+
getAllows(table: string, operation: Operation): CompiledPolicy[];
|
|
273
|
+
/**
|
|
274
|
+
* Get deny policies for a table and operation
|
|
275
|
+
*/
|
|
276
|
+
getDenies(table: string, operation: Operation): CompiledPolicy[];
|
|
277
|
+
/**
|
|
278
|
+
* Get validate policies for a table and operation
|
|
279
|
+
*/
|
|
280
|
+
getValidates(table: string, operation: Operation): CompiledPolicy[];
|
|
281
|
+
/**
|
|
282
|
+
* Get filter policies for a table
|
|
283
|
+
*/
|
|
284
|
+
getFilters(table: string): CompiledFilterPolicy[];
|
|
285
|
+
/**
|
|
286
|
+
* Get roles that skip RLS for a table
|
|
287
|
+
*/
|
|
288
|
+
getSkipFor(table: string): string[];
|
|
289
|
+
/**
|
|
290
|
+
* Check if table has default deny
|
|
291
|
+
*/
|
|
292
|
+
hasDefaultDeny(table: string): boolean;
|
|
293
|
+
/**
|
|
294
|
+
* Check if a table is registered
|
|
295
|
+
*/
|
|
296
|
+
hasTable(table: string): boolean;
|
|
297
|
+
/**
|
|
298
|
+
* Get all registered table names
|
|
299
|
+
*/
|
|
300
|
+
getTables(): string[];
|
|
301
|
+
/**
|
|
302
|
+
* Check if registry is compiled
|
|
303
|
+
*/
|
|
304
|
+
isCompiled(): boolean;
|
|
305
|
+
/**
|
|
306
|
+
* Validate that all policies are properly defined
|
|
307
|
+
*
|
|
308
|
+
* This method checks for common issues:
|
|
309
|
+
* - Tables with no policies and defaultDeny=false (warns)
|
|
310
|
+
* - Tables with skipFor operations but no corresponding policies
|
|
311
|
+
*/
|
|
312
|
+
validate(): void;
|
|
313
|
+
/**
|
|
314
|
+
* Clear all policies
|
|
315
|
+
*/
|
|
316
|
+
clear(): void;
|
|
317
|
+
/**
|
|
318
|
+
* Remove policies for a specific table
|
|
319
|
+
*/
|
|
320
|
+
remove(table: string): void;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* RLS Error Classes
|
|
325
|
+
*
|
|
326
|
+
* This module provides specialized error classes for Row-Level Security operations.
|
|
327
|
+
* All errors extend the base RLSError class and use unified error codes from @kysera/core
|
|
328
|
+
* for consistency across the Kysera ecosystem.
|
|
329
|
+
*
|
|
330
|
+
* @module @kysera/rls/errors
|
|
331
|
+
*/
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* RLS-specific error codes
|
|
335
|
+
*
|
|
336
|
+
* These codes extend the unified error codes from @kysera/core with
|
|
337
|
+
* RLS-specific error conditions.
|
|
338
|
+
*/
|
|
339
|
+
declare const RLSErrorCodes: {
|
|
340
|
+
/** RLS context is missing or not set */
|
|
341
|
+
readonly RLS_CONTEXT_MISSING: ErrorCode;
|
|
342
|
+
/** RLS policy violation occurred */
|
|
343
|
+
readonly RLS_POLICY_VIOLATION: ErrorCode;
|
|
344
|
+
/** RLS policy definition is invalid */
|
|
345
|
+
readonly RLS_POLICY_INVALID: ErrorCode;
|
|
346
|
+
/** RLS schema definition is invalid */
|
|
347
|
+
readonly RLS_SCHEMA_INVALID: ErrorCode;
|
|
348
|
+
/** RLS context validation failed */
|
|
349
|
+
readonly RLS_CONTEXT_INVALID: ErrorCode;
|
|
350
|
+
};
|
|
351
|
+
/**
|
|
352
|
+
* Type for RLS error codes
|
|
353
|
+
*/
|
|
354
|
+
type RLSErrorCode = typeof RLSErrorCodes[keyof typeof RLSErrorCodes];
|
|
355
|
+
/**
|
|
356
|
+
* Base class for all RLS-related errors
|
|
357
|
+
*
|
|
358
|
+
* Provides common error functionality including error codes and JSON serialization.
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```typescript
|
|
362
|
+
* throw new RLSError('Something went wrong', RLSErrorCodes.RLS_POLICY_INVALID);
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
declare class RLSError extends Error {
|
|
366
|
+
readonly code: RLSErrorCode;
|
|
367
|
+
/**
|
|
368
|
+
* Creates a new RLS error
|
|
369
|
+
*
|
|
370
|
+
* @param message - Error message
|
|
371
|
+
* @param code - RLS error code
|
|
372
|
+
*/
|
|
373
|
+
constructor(message: string, code: RLSErrorCode);
|
|
374
|
+
/**
|
|
375
|
+
* Serializes the error to JSON
|
|
376
|
+
*
|
|
377
|
+
* @returns JSON representation of the error
|
|
378
|
+
*/
|
|
379
|
+
toJSON(): Record<string, unknown>;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Error thrown when RLS context is missing
|
|
383
|
+
*
|
|
384
|
+
* This error occurs when an operation requiring RLS context is executed
|
|
385
|
+
* outside of a context scope (i.e., without calling withRLSContext()).
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* ```typescript
|
|
389
|
+
* // This will throw RLSContextError
|
|
390
|
+
* const result = await db.selectFrom('posts').execute();
|
|
391
|
+
*
|
|
392
|
+
* // Correct usage with context
|
|
393
|
+
* await withRLSContext(rlsContext, async () => {
|
|
394
|
+
* const result = await db.selectFrom('posts').execute();
|
|
395
|
+
* });
|
|
396
|
+
* ```
|
|
397
|
+
*/
|
|
398
|
+
declare class RLSContextError extends RLSError {
|
|
399
|
+
/**
|
|
400
|
+
* Creates a new RLS context error
|
|
401
|
+
*
|
|
402
|
+
* @param message - Error message (defaults to standard message)
|
|
403
|
+
*/
|
|
404
|
+
constructor(message?: string);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Error thrown when RLS context validation fails
|
|
408
|
+
*
|
|
409
|
+
* This error occurs when the provided RLS context is invalid or missing
|
|
410
|
+
* required fields.
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```typescript
|
|
414
|
+
* // Missing required userId field
|
|
415
|
+
* const invalidContext = {
|
|
416
|
+
* auth: {
|
|
417
|
+
* roles: ['user']
|
|
418
|
+
* // userId is missing!
|
|
419
|
+
* },
|
|
420
|
+
* timestamp: new Date()
|
|
421
|
+
* };
|
|
422
|
+
*
|
|
423
|
+
* // This will throw RLSContextValidationError
|
|
424
|
+
* validateRLSContext(invalidContext);
|
|
425
|
+
* ```
|
|
426
|
+
*/
|
|
427
|
+
declare class RLSContextValidationError extends RLSError {
|
|
428
|
+
readonly field: string;
|
|
429
|
+
/**
|
|
430
|
+
* Creates a new context validation error
|
|
431
|
+
*
|
|
432
|
+
* @param message - Error message
|
|
433
|
+
* @param field - Field that failed validation
|
|
434
|
+
*/
|
|
435
|
+
constructor(message: string, field: string);
|
|
436
|
+
toJSON(): Record<string, unknown>;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Error thrown when an RLS policy violation occurs
|
|
440
|
+
*
|
|
441
|
+
* This error is thrown when a database operation is denied by RLS policies.
|
|
442
|
+
* It provides detailed information about the violation including the operation,
|
|
443
|
+
* table, and reason for denial.
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```typescript
|
|
447
|
+
* // User tries to update a post they don't own
|
|
448
|
+
* throw new RLSPolicyViolation(
|
|
449
|
+
* 'update',
|
|
450
|
+
* 'posts',
|
|
451
|
+
* 'User does not own this post',
|
|
452
|
+
* 'ownership_policy'
|
|
453
|
+
* );
|
|
454
|
+
* ```
|
|
455
|
+
*/
|
|
456
|
+
declare class RLSPolicyViolation extends RLSError {
|
|
457
|
+
readonly operation: string;
|
|
458
|
+
readonly table: string;
|
|
459
|
+
readonly reason: string;
|
|
460
|
+
readonly policyName?: string;
|
|
461
|
+
/**
|
|
462
|
+
* Creates a new policy violation error
|
|
463
|
+
*
|
|
464
|
+
* @param operation - Database operation that was denied (read, create, update, delete)
|
|
465
|
+
* @param table - Table name where violation occurred
|
|
466
|
+
* @param reason - Reason for the policy violation
|
|
467
|
+
* @param policyName - Name of the policy that denied access (optional)
|
|
468
|
+
*/
|
|
469
|
+
constructor(operation: string, table: string, reason: string, policyName?: string);
|
|
470
|
+
toJSON(): Record<string, unknown>;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Error thrown when RLS schema validation fails
|
|
474
|
+
*
|
|
475
|
+
* This error occurs when the RLS schema definition is invalid or contains
|
|
476
|
+
* configuration errors.
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* ```typescript
|
|
480
|
+
* // Invalid policy definition
|
|
481
|
+
* const invalidSchema = {
|
|
482
|
+
* posts: {
|
|
483
|
+
* policies: [
|
|
484
|
+
* {
|
|
485
|
+
* type: 'invalid-type', // Invalid policy type!
|
|
486
|
+
* operation: 'read',
|
|
487
|
+
* condition: (ctx) => true
|
|
488
|
+
* }
|
|
489
|
+
* ]
|
|
490
|
+
* }
|
|
491
|
+
* };
|
|
492
|
+
*
|
|
493
|
+
* // This will throw RLSSchemaError
|
|
494
|
+
* validateRLSSchema(invalidSchema);
|
|
495
|
+
* ```
|
|
496
|
+
*/
|
|
497
|
+
declare class RLSSchemaError extends RLSError {
|
|
498
|
+
readonly details: Record<string, unknown>;
|
|
499
|
+
/**
|
|
500
|
+
* Creates a new schema validation error
|
|
501
|
+
*
|
|
502
|
+
* @param message - Error message
|
|
503
|
+
* @param details - Additional details about the validation failure
|
|
504
|
+
*/
|
|
505
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
506
|
+
toJSON(): Record<string, unknown>;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* RLS Plugin for Kysera Repository
|
|
511
|
+
*
|
|
512
|
+
* Implements Row-Level Security as a Kysera plugin, providing:
|
|
513
|
+
* - Automatic query filtering for SELECT operations
|
|
514
|
+
* - Policy enforcement for CREATE, UPDATE, DELETE operations
|
|
515
|
+
* - Repository method extensions for RLS-aware operations
|
|
516
|
+
* - System context bypass for privileged operations
|
|
517
|
+
*
|
|
518
|
+
* @module @kysera/rls
|
|
519
|
+
*/
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* RLS Plugin configuration options
|
|
523
|
+
*/
|
|
524
|
+
interface RLSPluginOptions<DB = unknown> {
|
|
525
|
+
/** RLS policy schema */
|
|
526
|
+
schema: RLSSchema<DB>;
|
|
527
|
+
/** Tables to skip RLS for (always bypass policies) */
|
|
528
|
+
skipTables?: string[];
|
|
529
|
+
/** Roles that bypass RLS entirely (e.g., ['admin', 'superuser']) */
|
|
530
|
+
bypassRoles?: string[];
|
|
531
|
+
/** Logger instance for RLS operations */
|
|
532
|
+
logger?: KyseraLogger;
|
|
533
|
+
/** Require RLS context for all operations (throws if missing) */
|
|
534
|
+
requireContext?: boolean;
|
|
535
|
+
/** Enable audit logging of policy decisions */
|
|
536
|
+
auditDecisions?: boolean;
|
|
537
|
+
/** Custom error handler for policy violations */
|
|
538
|
+
onViolation?: (violation: RLSPolicyViolation) => void;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Create RLS plugin for Kysera
|
|
542
|
+
*
|
|
543
|
+
* The RLS plugin provides declarative row-level security for your database operations.
|
|
544
|
+
* It automatically filters SELECT queries and validates mutations (CREATE, UPDATE, DELETE)
|
|
545
|
+
* against your policy schema.
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```typescript
|
|
549
|
+
* import { rlsPlugin, defineRLSSchema, allow, filter } from '@kysera/rls';
|
|
550
|
+
* import { createORM } from '@kysera/repository';
|
|
551
|
+
*
|
|
552
|
+
* // Define your RLS schema
|
|
553
|
+
* const schema = defineRLSSchema<Database>({
|
|
554
|
+
* resources: {
|
|
555
|
+
* policies: [
|
|
556
|
+
* // Filter reads by tenant
|
|
557
|
+
* filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })),
|
|
558
|
+
* // Allow updates for resource owners
|
|
559
|
+
* allow('update', ctx => ctx.auth.userId === ctx.row.owner_id),
|
|
560
|
+
* // Validate creates belong to user's tenant
|
|
561
|
+
* validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId),
|
|
562
|
+
* ],
|
|
563
|
+
* },
|
|
564
|
+
* });
|
|
565
|
+
*
|
|
566
|
+
* // Create ORM with RLS plugin
|
|
567
|
+
* const orm = await createORM(db, [
|
|
568
|
+
* rlsPlugin({ schema }),
|
|
569
|
+
* ]);
|
|
570
|
+
*
|
|
571
|
+
* // Use within RLS context
|
|
572
|
+
* await rlsContext.runAsync(
|
|
573
|
+
* {
|
|
574
|
+
* auth: { userId: 1, tenantId: 100, roles: ['user'], isSystem: false },
|
|
575
|
+
* timestamp: new Date(),
|
|
576
|
+
* },
|
|
577
|
+
* async () => {
|
|
578
|
+
* // All queries automatically filtered by tenant_id
|
|
579
|
+
* const resources = await orm.resources.findAll();
|
|
580
|
+
* }
|
|
581
|
+
* );
|
|
582
|
+
* ```
|
|
583
|
+
*
|
|
584
|
+
* @param options - Plugin configuration options
|
|
585
|
+
* @returns Kysera plugin instance
|
|
586
|
+
*/
|
|
587
|
+
declare function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin;
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Options for creating RLS context
|
|
591
|
+
*/
|
|
592
|
+
interface CreateRLSContextOptions<TUser = unknown, TMeta = unknown> {
|
|
593
|
+
auth: RLSAuthContext<TUser>;
|
|
594
|
+
request?: Partial<RLSRequestContext>;
|
|
595
|
+
meta?: TMeta;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Create a new RLS context
|
|
599
|
+
*/
|
|
600
|
+
declare function createRLSContext<TUser = unknown, TMeta = unknown>(options: CreateRLSContextOptions<TUser, TMeta>): RLSContext<TUser, TMeta>;
|
|
601
|
+
/**
|
|
602
|
+
* RLS Context Manager
|
|
603
|
+
* Manages RLS context using AsyncLocalStorage for automatic propagation
|
|
604
|
+
*/
|
|
605
|
+
declare class RLSContextManager {
|
|
606
|
+
/**
|
|
607
|
+
* Run a synchronous function within an RLS context
|
|
608
|
+
*/
|
|
609
|
+
run<T>(context: RLSContext, fn: () => T): T;
|
|
610
|
+
/**
|
|
611
|
+
* Run an async function within an RLS context
|
|
612
|
+
*/
|
|
613
|
+
runAsync<T>(context: RLSContext, fn: () => Promise<T>): Promise<T>;
|
|
614
|
+
/**
|
|
615
|
+
* Get current RLS context
|
|
616
|
+
* @throws RLSContextError if no context is set
|
|
617
|
+
*/
|
|
618
|
+
getContext(): RLSContext;
|
|
619
|
+
/**
|
|
620
|
+
* Get current RLS context or null if not set
|
|
621
|
+
*/
|
|
622
|
+
getContextOrNull(): RLSContext | null;
|
|
623
|
+
/**
|
|
624
|
+
* Check if running within RLS context
|
|
625
|
+
*/
|
|
626
|
+
hasContext(): boolean;
|
|
627
|
+
/**
|
|
628
|
+
* Get current auth context
|
|
629
|
+
* @throws RLSContextError if no context is set
|
|
630
|
+
*/
|
|
631
|
+
getAuth(): RLSAuthContext;
|
|
632
|
+
/**
|
|
633
|
+
* Get current user ID
|
|
634
|
+
* @throws RLSContextError if no context is set
|
|
635
|
+
*/
|
|
636
|
+
getUserId(): string | number;
|
|
637
|
+
/**
|
|
638
|
+
* Get current tenant ID
|
|
639
|
+
* @throws RLSContextError if no context is set
|
|
640
|
+
*/
|
|
641
|
+
getTenantId(): string | number | undefined;
|
|
642
|
+
/**
|
|
643
|
+
* Check if current user has a specific role
|
|
644
|
+
*/
|
|
645
|
+
hasRole(role: string): boolean;
|
|
646
|
+
/**
|
|
647
|
+
* Check if current user has a specific permission
|
|
648
|
+
*/
|
|
649
|
+
hasPermission(permission: string): boolean;
|
|
650
|
+
/**
|
|
651
|
+
* Check if current context is a system context (bypasses RLS)
|
|
652
|
+
*/
|
|
653
|
+
isSystem(): boolean;
|
|
654
|
+
/**
|
|
655
|
+
* Create a system context for operations that should bypass RLS
|
|
656
|
+
*/
|
|
657
|
+
asSystem<T>(fn: () => T): T;
|
|
658
|
+
/**
|
|
659
|
+
* Create a system context for async operations
|
|
660
|
+
*/
|
|
661
|
+
asSystemAsync<T>(fn: () => Promise<T>): Promise<T>;
|
|
662
|
+
}
|
|
663
|
+
declare const rlsContext: RLSContextManager;
|
|
664
|
+
/**
|
|
665
|
+
* Convenience function to run code within RLS context
|
|
666
|
+
*/
|
|
667
|
+
declare function withRLSContext<T>(context: RLSContext, fn: () => T): T;
|
|
668
|
+
/**
|
|
669
|
+
* Convenience function to run async code within RLS context
|
|
670
|
+
*/
|
|
671
|
+
declare function withRLSContextAsync<T>(context: RLSContext, fn: () => Promise<T>): Promise<T>;
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Utility helper functions for RLS
|
|
675
|
+
*/
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Create a policy evaluation context from RLS context
|
|
679
|
+
*/
|
|
680
|
+
declare function createEvaluationContext<TRow = unknown, TData = unknown>(rlsCtx: RLSContext, options?: {
|
|
681
|
+
row?: TRow;
|
|
682
|
+
data?: TData;
|
|
683
|
+
}): PolicyEvaluationContext<unknown, TRow, TData>;
|
|
684
|
+
/**
|
|
685
|
+
* Check if a condition function is async
|
|
686
|
+
*/
|
|
687
|
+
declare function isAsyncFunction(fn: unknown): fn is (...args: unknown[]) => Promise<unknown>;
|
|
688
|
+
/**
|
|
689
|
+
* Safely evaluate a policy condition
|
|
690
|
+
*/
|
|
691
|
+
declare function safeEvaluate<T>(fn: () => T | Promise<T>, defaultValue: T): Promise<T>;
|
|
692
|
+
/**
|
|
693
|
+
* Deep merge two objects
|
|
694
|
+
*/
|
|
695
|
+
declare function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial<T>): T;
|
|
696
|
+
/**
|
|
697
|
+
* Create a simple hash for cache keys
|
|
698
|
+
*/
|
|
699
|
+
declare function hashString(str: string): string;
|
|
700
|
+
/**
|
|
701
|
+
* Normalize operations to array format
|
|
702
|
+
*/
|
|
703
|
+
declare function normalizeOperations(operation: Operation | Operation[]): Operation[];
|
|
704
|
+
|
|
705
|
+
export { CompiledFilterPolicy, CompiledPolicy, type CreateRLSContextOptions, FilterCondition, Operation, PolicyCondition, PolicyDefinition, PolicyEvaluationContext, PolicyHints, type PolicyOptions, PolicyRegistry, RLSAuthContext, RLSContext, RLSContextError, RLSContextValidationError, RLSError, type RLSErrorCode, RLSErrorCodes, type RLSPluginOptions, RLSPolicyViolation, RLSRequestContext, RLSSchema, RLSSchemaError, TableRLSConfig, allow, createEvaluationContext, createRLSContext, deepMerge, defineRLSSchema, deny, filter, hashString, isAsyncFunction, mergeRLSSchemas, normalizeOperations, rlsContext, rlsPlugin, safeEvaluate, validate, withRLSContext, withRLSContextAsync };
|