@kysera/rls 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +640 -796
- package/dist/index.d.ts +2 -2
- package/dist/index.js +22 -5
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/index.ts +1 -1
- package/src/plugin.ts +46 -8
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kysera/rls",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Row-Level Security plugin for
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "Row-Level Security plugin for Kysely - declarative policies, query transformation, native RLS support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"zod": "^4.1.13",
|
|
27
|
-
"@kysera/core": "0.
|
|
28
|
-
"@kysera/
|
|
27
|
+
"@kysera/core": "0.7.1",
|
|
28
|
+
"@kysera/executor": "0.7.1",
|
|
29
|
+
"@kysera/repository": "0.7.1"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
"@types/pg": "^8.15.6",
|
|
34
35
|
"@vitest/coverage-v8": "^4.0.15",
|
|
35
36
|
"better-sqlite3": "^12.5.0",
|
|
36
|
-
"kysely": "^0.28.
|
|
37
|
+
"kysely": "^0.28.9",
|
|
37
38
|
"mysql2": "^3.15.2",
|
|
38
39
|
"pg": "^8.16.3",
|
|
39
40
|
"tsup": "^8.5.1",
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
},
|
|
43
44
|
"keywords": [
|
|
44
45
|
"kysely",
|
|
45
|
-
"
|
|
46
|
+
"data-access",
|
|
46
47
|
"database",
|
|
47
48
|
"typescript",
|
|
48
49
|
"sql",
|
package/src/index.ts
CHANGED
package/src/plugin.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* @module @kysera/rls
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { Plugin, QueryBuilderContext
|
|
13
|
+
import type { Plugin, QueryBuilderContext } from '@kysera/executor';
|
|
14
|
+
import { getRawDb } from '@kysera/executor';
|
|
14
15
|
import type { Kysely } from 'kysely';
|
|
15
16
|
import type { RLSSchema, Operation } from './policy/types.js';
|
|
16
17
|
import { PolicyRegistry } from './policy/registry.js';
|
|
@@ -85,7 +86,7 @@ interface BaseRepository {
|
|
|
85
86
|
* },
|
|
86
87
|
* });
|
|
87
88
|
*
|
|
88
|
-
* // Create
|
|
89
|
+
* // Create repository with RLS plugin
|
|
89
90
|
* const orm = await createORM(db, [
|
|
90
91
|
* rlsPlugin({ schema }),
|
|
91
92
|
* ]);
|
|
@@ -124,7 +125,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
124
125
|
|
|
125
126
|
return {
|
|
126
127
|
name: '@kysera/rls',
|
|
127
|
-
version: '0.
|
|
128
|
+
version: '0.7.0',
|
|
128
129
|
|
|
129
130
|
// Run after soft-delete (priority 0), before audit
|
|
130
131
|
priority: 50,
|
|
@@ -160,7 +161,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
160
161
|
* it applies filter policies as WHERE conditions. For mutations, it marks
|
|
161
162
|
* that RLS validation is required (performed in extendRepository).
|
|
162
163
|
*/
|
|
163
|
-
interceptQuery<QB
|
|
164
|
+
interceptQuery<QB>(
|
|
164
165
|
qb: QB,
|
|
165
166
|
context: QueryBuilderContext
|
|
166
167
|
): QB {
|
|
@@ -265,6 +266,11 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
265
266
|
const originalDelete = baseRepo.delete?.bind(baseRepo);
|
|
266
267
|
const originalFindById = baseRepo.findById?.bind(baseRepo);
|
|
267
268
|
|
|
269
|
+
// Get raw db for internal queries that need to bypass RLS
|
|
270
|
+
// If executor doesn't have __rawDb (e.g., in tests), we'll use originalFindById
|
|
271
|
+
const rawDb = getRawDb(baseRepo.executor);
|
|
272
|
+
const hasRawDb = (baseRepo.executor as any).__rawDb !== undefined;
|
|
273
|
+
|
|
268
274
|
const extendedRepo = {
|
|
269
275
|
...baseRepo,
|
|
270
276
|
|
|
@@ -309,7 +315,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
309
315
|
* Wrapped update with RLS check
|
|
310
316
|
*/
|
|
311
317
|
async update(id: unknown, data: unknown): Promise<unknown> {
|
|
312
|
-
if (!originalUpdate
|
|
318
|
+
if (!originalUpdate) {
|
|
313
319
|
throw new RLSError('Repository does not support update operation', RLSErrorCodes.RLS_POLICY_INVALID);
|
|
314
320
|
}
|
|
315
321
|
|
|
@@ -318,7 +324,23 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
318
324
|
if (ctx && !ctx.auth.isSystem &&
|
|
319
325
|
!bypassRoles.some(role => ctx.auth.roles.includes(role))) {
|
|
320
326
|
// Fetch existing row for policy evaluation
|
|
321
|
-
|
|
327
|
+
// Use raw db if available to bypass RLS filtering and prevent self-interception
|
|
328
|
+
let existingRow: unknown;
|
|
329
|
+
|
|
330
|
+
if (hasRawDb) {
|
|
331
|
+
// Use raw db to bypass RLS filtering
|
|
332
|
+
existingRow = await rawDb
|
|
333
|
+
.selectFrom(table as any)
|
|
334
|
+
.selectAll()
|
|
335
|
+
.where('id' as any, '=', id as any)
|
|
336
|
+
.executeTakeFirst();
|
|
337
|
+
} else if (originalFindById) {
|
|
338
|
+
// Fallback to originalFindById for tests/mocks
|
|
339
|
+
existingRow = await originalFindById(id);
|
|
340
|
+
} else {
|
|
341
|
+
throw new RLSError('Repository does not support update operation', RLSErrorCodes.RLS_POLICY_INVALID);
|
|
342
|
+
}
|
|
343
|
+
|
|
322
344
|
if (!existingRow) {
|
|
323
345
|
// Let the original method handle not found
|
|
324
346
|
return originalUpdate(id, data);
|
|
@@ -357,7 +379,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
357
379
|
* Wrapped delete with RLS check
|
|
358
380
|
*/
|
|
359
381
|
async delete(id: unknown): Promise<unknown> {
|
|
360
|
-
if (!originalDelete
|
|
382
|
+
if (!originalDelete) {
|
|
361
383
|
throw new RLSError('Repository does not support delete operation', RLSErrorCodes.RLS_POLICY_INVALID);
|
|
362
384
|
}
|
|
363
385
|
|
|
@@ -366,7 +388,23 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
366
388
|
if (ctx && !ctx.auth.isSystem &&
|
|
367
389
|
!bypassRoles.some(role => ctx.auth.roles.includes(role))) {
|
|
368
390
|
// Fetch existing row for policy evaluation
|
|
369
|
-
|
|
391
|
+
// Use raw db if available to bypass RLS filtering and prevent self-interception
|
|
392
|
+
let existingRow: unknown;
|
|
393
|
+
|
|
394
|
+
if (hasRawDb) {
|
|
395
|
+
// Use raw db to bypass RLS filtering
|
|
396
|
+
existingRow = await rawDb
|
|
397
|
+
.selectFrom(table as any)
|
|
398
|
+
.selectAll()
|
|
399
|
+
.where('id' as any, '=', id as any)
|
|
400
|
+
.executeTakeFirst();
|
|
401
|
+
} else if (originalFindById) {
|
|
402
|
+
// Fallback to originalFindById for tests/mocks
|
|
403
|
+
existingRow = await originalFindById(id);
|
|
404
|
+
} else {
|
|
405
|
+
throw new RLSError('Repository does not support delete operation', RLSErrorCodes.RLS_POLICY_INVALID);
|
|
406
|
+
}
|
|
407
|
+
|
|
370
408
|
if (!existingRow) {
|
|
371
409
|
// Let the original method handle not found
|
|
372
410
|
return originalDelete(id);
|