@kysera/rls 0.7.3 → 0.7.4
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 +390 -276
- package/dist/index.d.ts +95 -19
- package/dist/index.js +219 -130
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/native/index.js +3 -14
- package/dist/native/index.js.map +1 -1
- package/dist/{types-6eCXh_Jd.d.ts → types-Dowjd6zG.d.ts} +3 -3
- package/package.json +16 -7
- package/src/context/index.ts +4 -4
- package/src/context/manager.ts +45 -45
- package/src/context/storage.ts +3 -3
- package/src/context/types.ts +1 -5
- package/src/errors.ts +62 -77
- package/src/index.ts +13 -13
- package/src/native/README.md +49 -46
- package/src/native/index.ts +3 -6
- package/src/native/migration.ts +29 -27
- package/src/native/postgres.ts +63 -74
- package/src/plugin.ts +306 -159
- package/src/policy/builder.ts +46 -33
- package/src/policy/index.ts +4 -4
- package/src/policy/registry.ts +100 -105
- package/src/policy/schema.ts +58 -71
- package/src/policy/types.ts +58 -58
- package/src/transformer/index.ts +2 -2
- package/src/transformer/mutation.ts +95 -98
- package/src/transformer/select.ts +59 -43
- package/src/utils/helpers.ts +57 -50
- package/src/utils/index.ts +13 -2
- package/src/utils/type-utils.ts +155 -0
- package/src/version.ts +7 -0
package/src/errors.ts
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* RLS Error Classes
|
|
3
3
|
*
|
|
4
4
|
* This module provides specialized error classes for Row-Level Security operations.
|
|
5
|
-
* All errors extend
|
|
6
|
-
*
|
|
5
|
+
* All errors extend base error classes from @kysera/core for consistency across
|
|
6
|
+
* the Kysera ecosystem.
|
|
7
7
|
*
|
|
8
8
|
* @module @kysera/rls/errors
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import
|
|
11
|
+
import { DatabaseError } from '@kysera/core'
|
|
12
|
+
import type { ErrorCode } from '@kysera/core'
|
|
12
13
|
|
|
13
14
|
// ============================================================================
|
|
14
15
|
// RLS Error Codes
|
|
@@ -32,13 +33,13 @@ export const RLSErrorCodes = {
|
|
|
32
33
|
/** RLS context validation failed */
|
|
33
34
|
RLS_CONTEXT_INVALID: 'RLS_CONTEXT_INVALID' as ErrorCode,
|
|
34
35
|
/** RLS policy evaluation threw an error */
|
|
35
|
-
RLS_POLICY_EVALUATION_ERROR: 'RLS_POLICY_EVALUATION_ERROR' as ErrorCode
|
|
36
|
-
} as const
|
|
36
|
+
RLS_POLICY_EVALUATION_ERROR: 'RLS_POLICY_EVALUATION_ERROR' as ErrorCode
|
|
37
|
+
} as const
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Type for RLS error codes
|
|
40
41
|
*/
|
|
41
|
-
export type RLSErrorCode = typeof RLSErrorCodes[keyof typeof RLSErrorCodes]
|
|
42
|
+
export type RLSErrorCode = (typeof RLSErrorCodes)[keyof typeof RLSErrorCodes]
|
|
42
43
|
|
|
43
44
|
// ============================================================================
|
|
44
45
|
// Base RLS Error
|
|
@@ -47,6 +48,7 @@ export type RLSErrorCode = typeof RLSErrorCodes[keyof typeof RLSErrorCodes];
|
|
|
47
48
|
/**
|
|
48
49
|
* Base class for all RLS-related errors
|
|
49
50
|
*
|
|
51
|
+
* Extends DatabaseError from @kysera/core for consistency with other Kysera packages.
|
|
50
52
|
* Provides common error functionality including error codes and JSON serialization.
|
|
51
53
|
*
|
|
52
54
|
* @example
|
|
@@ -54,9 +56,7 @@ export type RLSErrorCode = typeof RLSErrorCodes[keyof typeof RLSErrorCodes];
|
|
|
54
56
|
* throw new RLSError('Something went wrong', RLSErrorCodes.RLS_POLICY_INVALID);
|
|
55
57
|
* ```
|
|
56
58
|
*/
|
|
57
|
-
export class RLSError extends
|
|
58
|
-
public readonly code: RLSErrorCode;
|
|
59
|
-
|
|
59
|
+
export class RLSError extends DatabaseError {
|
|
60
60
|
/**
|
|
61
61
|
* Creates a new RLS error
|
|
62
62
|
*
|
|
@@ -64,22 +64,8 @@ export class RLSError extends Error {
|
|
|
64
64
|
* @param code - RLS error code
|
|
65
65
|
*/
|
|
66
66
|
constructor(message: string, code: RLSErrorCode) {
|
|
67
|
-
super(message)
|
|
68
|
-
this.name = 'RLSError'
|
|
69
|
-
this.code = code;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Serializes the error to JSON
|
|
74
|
-
*
|
|
75
|
-
* @returns JSON representation of the error
|
|
76
|
-
*/
|
|
77
|
-
toJSON(): Record<string, unknown> {
|
|
78
|
-
return {
|
|
79
|
-
name: this.name,
|
|
80
|
-
message: this.message,
|
|
81
|
-
code: this.code,
|
|
82
|
-
};
|
|
67
|
+
super(message, code)
|
|
68
|
+
this.name = 'RLSError'
|
|
83
69
|
}
|
|
84
70
|
}
|
|
85
71
|
|
|
@@ -110,15 +96,17 @@ export class RLSContextError extends RLSError {
|
|
|
110
96
|
*
|
|
111
97
|
* @param message - Error message (defaults to standard message)
|
|
112
98
|
*/
|
|
113
|
-
constructor(message
|
|
114
|
-
super(message, RLSErrorCodes.RLS_CONTEXT_MISSING)
|
|
115
|
-
this.name = 'RLSContextError'
|
|
99
|
+
constructor(message = 'No RLS context found. Ensure code runs within withRLSContext()') {
|
|
100
|
+
super(message, RLSErrorCodes.RLS_CONTEXT_MISSING)
|
|
101
|
+
this.name = 'RLSContextError'
|
|
116
102
|
}
|
|
117
103
|
}
|
|
118
104
|
|
|
119
105
|
/**
|
|
120
106
|
* Error thrown when RLS context validation fails
|
|
121
107
|
*
|
|
108
|
+
* Extends RLSError as context validation failures are RLS-specific errors.
|
|
109
|
+
*
|
|
122
110
|
* This error occurs when the provided RLS context is invalid or missing
|
|
123
111
|
* required fields.
|
|
124
112
|
*
|
|
@@ -138,7 +126,7 @@ export class RLSContextError extends RLSError {
|
|
|
138
126
|
* ```
|
|
139
127
|
*/
|
|
140
128
|
export class RLSContextValidationError extends RLSError {
|
|
141
|
-
public readonly field: string
|
|
129
|
+
public readonly field: string
|
|
142
130
|
|
|
143
131
|
/**
|
|
144
132
|
* Creates a new context validation error
|
|
@@ -147,16 +135,16 @@ export class RLSContextValidationError extends RLSError {
|
|
|
147
135
|
* @param field - Field that failed validation
|
|
148
136
|
*/
|
|
149
137
|
constructor(message: string, field: string) {
|
|
150
|
-
super(message, RLSErrorCodes.RLS_CONTEXT_INVALID)
|
|
151
|
-
this.name = 'RLSContextValidationError'
|
|
152
|
-
this.field = field
|
|
138
|
+
super(message, RLSErrorCodes.RLS_CONTEXT_INVALID)
|
|
139
|
+
this.name = 'RLSContextValidationError'
|
|
140
|
+
this.field = field
|
|
153
141
|
}
|
|
154
142
|
|
|
155
143
|
override toJSON(): Record<string, unknown> {
|
|
156
144
|
return {
|
|
157
145
|
...super.toJSON(),
|
|
158
|
-
field: this.field
|
|
159
|
-
}
|
|
146
|
+
field: this.field
|
|
147
|
+
}
|
|
160
148
|
}
|
|
161
149
|
}
|
|
162
150
|
|
|
@@ -183,10 +171,10 @@ export class RLSContextValidationError extends RLSError {
|
|
|
183
171
|
* ```
|
|
184
172
|
*/
|
|
185
173
|
export class RLSPolicyViolation extends RLSError {
|
|
186
|
-
public readonly operation: string
|
|
187
|
-
public readonly table: string
|
|
188
|
-
public readonly reason: string
|
|
189
|
-
public readonly policyName?: string
|
|
174
|
+
public readonly operation: string
|
|
175
|
+
public readonly table: string
|
|
176
|
+
public readonly reason: string
|
|
177
|
+
public readonly policyName?: string
|
|
190
178
|
|
|
191
179
|
/**
|
|
192
180
|
* Creates a new policy violation error
|
|
@@ -196,22 +184,17 @@ export class RLSPolicyViolation extends RLSError {
|
|
|
196
184
|
* @param reason - Reason for the policy violation
|
|
197
185
|
* @param policyName - Name of the policy that denied access (optional)
|
|
198
186
|
*/
|
|
199
|
-
constructor(
|
|
200
|
-
operation: string,
|
|
201
|
-
table: string,
|
|
202
|
-
reason: string,
|
|
203
|
-
policyName?: string
|
|
204
|
-
) {
|
|
187
|
+
constructor(operation: string, table: string, reason: string, policyName?: string) {
|
|
205
188
|
super(
|
|
206
189
|
`RLS policy violation: ${operation} on ${table} - ${reason}`,
|
|
207
190
|
RLSErrorCodes.RLS_POLICY_VIOLATION
|
|
208
|
-
)
|
|
209
|
-
this.name = 'RLSPolicyViolation'
|
|
210
|
-
this.operation = operation
|
|
211
|
-
this.table = table
|
|
212
|
-
this.reason = reason
|
|
191
|
+
)
|
|
192
|
+
this.name = 'RLSPolicyViolation'
|
|
193
|
+
this.operation = operation
|
|
194
|
+
this.table = table
|
|
195
|
+
this.reason = reason
|
|
213
196
|
if (policyName !== undefined) {
|
|
214
|
-
this.policyName = policyName
|
|
197
|
+
this.policyName = policyName
|
|
215
198
|
}
|
|
216
199
|
}
|
|
217
200
|
|
|
@@ -220,12 +203,12 @@ export class RLSPolicyViolation extends RLSError {
|
|
|
220
203
|
...super.toJSON(),
|
|
221
204
|
operation: this.operation,
|
|
222
205
|
table: this.table,
|
|
223
|
-
reason: this.reason
|
|
224
|
-
}
|
|
206
|
+
reason: this.reason
|
|
207
|
+
}
|
|
225
208
|
if (this.policyName !== undefined) {
|
|
226
|
-
json['policyName'] = this.policyName
|
|
209
|
+
json['policyName'] = this.policyName
|
|
227
210
|
}
|
|
228
|
-
return json
|
|
211
|
+
return json
|
|
229
212
|
}
|
|
230
213
|
}
|
|
231
214
|
|
|
@@ -250,10 +233,10 @@ export class RLSPolicyViolation extends RLSError {
|
|
|
250
233
|
* ```
|
|
251
234
|
*/
|
|
252
235
|
export class RLSPolicyEvaluationError extends RLSError {
|
|
253
|
-
public readonly operation: string
|
|
254
|
-
public readonly table: string
|
|
255
|
-
public readonly policyName?: string
|
|
256
|
-
public readonly originalError?: Error
|
|
236
|
+
public readonly operation: string
|
|
237
|
+
public readonly table: string
|
|
238
|
+
public readonly policyName?: string
|
|
239
|
+
public readonly originalError?: Error
|
|
257
240
|
|
|
258
241
|
/**
|
|
259
242
|
* Creates a new policy evaluation error
|
|
@@ -274,18 +257,18 @@ export class RLSPolicyEvaluationError extends RLSError {
|
|
|
274
257
|
super(
|
|
275
258
|
`RLS policy evaluation error during ${operation} on ${table}: ${message}`,
|
|
276
259
|
RLSErrorCodes.RLS_POLICY_EVALUATION_ERROR
|
|
277
|
-
)
|
|
278
|
-
this.name = 'RLSPolicyEvaluationError'
|
|
279
|
-
this.operation = operation
|
|
280
|
-
this.table = table
|
|
260
|
+
)
|
|
261
|
+
this.name = 'RLSPolicyEvaluationError'
|
|
262
|
+
this.operation = operation
|
|
263
|
+
this.table = table
|
|
281
264
|
if (policyName !== undefined) {
|
|
282
|
-
this.policyName = policyName
|
|
265
|
+
this.policyName = policyName
|
|
283
266
|
}
|
|
284
267
|
if (originalError !== undefined) {
|
|
285
|
-
this.originalError = originalError
|
|
268
|
+
this.originalError = originalError
|
|
286
269
|
// Preserve the original stack trace for debugging
|
|
287
270
|
if (originalError.stack) {
|
|
288
|
-
this.stack = `${this.stack}\n\nCaused by:\n${originalError.stack}
|
|
271
|
+
this.stack = `${this.stack}\n\nCaused by:\n${originalError.stack}`
|
|
289
272
|
}
|
|
290
273
|
}
|
|
291
274
|
}
|
|
@@ -294,18 +277,18 @@ export class RLSPolicyEvaluationError extends RLSError {
|
|
|
294
277
|
const json: Record<string, unknown> = {
|
|
295
278
|
...super.toJSON(),
|
|
296
279
|
operation: this.operation,
|
|
297
|
-
table: this.table
|
|
298
|
-
}
|
|
280
|
+
table: this.table
|
|
281
|
+
}
|
|
299
282
|
if (this.policyName !== undefined) {
|
|
300
|
-
json['policyName'] = this.policyName
|
|
283
|
+
json['policyName'] = this.policyName
|
|
301
284
|
}
|
|
302
285
|
if (this.originalError !== undefined) {
|
|
303
286
|
json['originalError'] = {
|
|
304
287
|
name: this.originalError.name,
|
|
305
|
-
message: this.originalError.message
|
|
306
|
-
}
|
|
288
|
+
message: this.originalError.message
|
|
289
|
+
}
|
|
307
290
|
}
|
|
308
|
-
return json
|
|
291
|
+
return json
|
|
309
292
|
}
|
|
310
293
|
}
|
|
311
294
|
|
|
@@ -316,6 +299,8 @@ export class RLSPolicyEvaluationError extends RLSError {
|
|
|
316
299
|
/**
|
|
317
300
|
* Error thrown when RLS schema validation fails
|
|
318
301
|
*
|
|
302
|
+
* Extends RLSError as schema validation failures are RLS-specific errors.
|
|
303
|
+
*
|
|
319
304
|
* This error occurs when the RLS schema definition is invalid or contains
|
|
320
305
|
* configuration errors.
|
|
321
306
|
*
|
|
@@ -339,7 +324,7 @@ export class RLSPolicyEvaluationError extends RLSError {
|
|
|
339
324
|
* ```
|
|
340
325
|
*/
|
|
341
326
|
export class RLSSchemaError extends RLSError {
|
|
342
|
-
public readonly details: Record<string, unknown
|
|
327
|
+
public readonly details: Record<string, unknown>
|
|
343
328
|
|
|
344
329
|
/**
|
|
345
330
|
* Creates a new schema validation error
|
|
@@ -348,15 +333,15 @@ export class RLSSchemaError extends RLSError {
|
|
|
348
333
|
* @param details - Additional details about the validation failure
|
|
349
334
|
*/
|
|
350
335
|
constructor(message: string, details: Record<string, unknown> = {}) {
|
|
351
|
-
super(message, RLSErrorCodes.RLS_SCHEMA_INVALID)
|
|
352
|
-
this.name = 'RLSSchemaError'
|
|
353
|
-
this.details = details
|
|
336
|
+
super(message, RLSErrorCodes.RLS_SCHEMA_INVALID)
|
|
337
|
+
this.name = 'RLSSchemaError'
|
|
338
|
+
this.details = details
|
|
354
339
|
}
|
|
355
340
|
|
|
356
341
|
override toJSON(): Record<string, unknown> {
|
|
357
342
|
return {
|
|
358
343
|
...super.toJSON(),
|
|
359
|
-
details: this.details
|
|
360
|
-
}
|
|
344
|
+
details: this.details
|
|
345
|
+
}
|
|
361
346
|
}
|
|
362
347
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,20 +12,20 @@
|
|
|
12
12
|
// ============================================================================
|
|
13
13
|
|
|
14
14
|
// Schema definition
|
|
15
|
-
export { defineRLSSchema, mergeRLSSchemas } from './policy/schema.js'
|
|
15
|
+
export { defineRLSSchema, mergeRLSSchemas } from './policy/schema.js'
|
|
16
16
|
|
|
17
17
|
// Policy builders
|
|
18
|
-
export { allow, deny, filter, validate, type PolicyOptions } from './policy/builder.js'
|
|
18
|
+
export { allow, deny, filter, validate, type PolicyOptions } from './policy/builder.js'
|
|
19
19
|
|
|
20
20
|
// Policy registry (for advanced use cases)
|
|
21
|
-
export { PolicyRegistry } from './policy/registry.js'
|
|
21
|
+
export { PolicyRegistry } from './policy/registry.js'
|
|
22
22
|
|
|
23
23
|
// ============================================================================
|
|
24
24
|
// Plugin
|
|
25
25
|
// ============================================================================
|
|
26
26
|
|
|
27
|
-
export { rlsPlugin } from './plugin.js'
|
|
28
|
-
export type { RLSPluginOptions } from './plugin.js'
|
|
27
|
+
export { rlsPlugin, RLSPluginOptionsSchema } from './plugin.js'
|
|
28
|
+
export type { RLSPluginOptions } from './plugin.js'
|
|
29
29
|
|
|
30
30
|
// ============================================================================
|
|
31
31
|
// Context Management
|
|
@@ -36,8 +36,8 @@ export {
|
|
|
36
36
|
createRLSContext,
|
|
37
37
|
withRLSContext,
|
|
38
38
|
withRLSContextAsync,
|
|
39
|
-
type CreateRLSContextOptions
|
|
40
|
-
} from './context/index.js'
|
|
39
|
+
type CreateRLSContextOptions
|
|
40
|
+
} from './context/index.js'
|
|
41
41
|
|
|
42
42
|
// ============================================================================
|
|
43
43
|
// Types
|
|
@@ -64,8 +64,8 @@ export type {
|
|
|
64
64
|
// Evaluation types
|
|
65
65
|
PolicyEvaluationContext,
|
|
66
66
|
CompiledPolicy,
|
|
67
|
-
CompiledFilterPolicy
|
|
68
|
-
} from './policy/types.js'
|
|
67
|
+
CompiledFilterPolicy
|
|
68
|
+
} from './policy/types.js'
|
|
69
69
|
|
|
70
70
|
// ============================================================================
|
|
71
71
|
// Errors
|
|
@@ -79,8 +79,8 @@ export {
|
|
|
79
79
|
RLSSchemaError,
|
|
80
80
|
RLSContextValidationError,
|
|
81
81
|
RLSErrorCodes,
|
|
82
|
-
type RLSErrorCode
|
|
83
|
-
} from './errors.js'
|
|
82
|
+
type RLSErrorCode
|
|
83
|
+
} from './errors.js'
|
|
84
84
|
|
|
85
85
|
// ============================================================================
|
|
86
86
|
// Utilities
|
|
@@ -92,5 +92,5 @@ export {
|
|
|
92
92
|
isAsyncFunction,
|
|
93
93
|
safeEvaluate,
|
|
94
94
|
deepMerge,
|
|
95
|
-
hashString
|
|
96
|
-
} from './utils/index.js'
|
|
95
|
+
hashString
|
|
96
|
+
} from './utils/index.js'
|
package/src/native/README.md
CHANGED
|
@@ -14,14 +14,14 @@ This module provides native PostgreSQL Row-Level Security (RLS) policy generatio
|
|
|
14
14
|
### 1. Define RLS Schema with Native PostgreSQL Support
|
|
15
15
|
|
|
16
16
|
```typescript
|
|
17
|
-
import type { RLSSchema } from '@kysera/rls'
|
|
17
|
+
import type { RLSSchema } from '@kysera/rls'
|
|
18
18
|
|
|
19
19
|
interface Database {
|
|
20
20
|
users: {
|
|
21
|
-
id: number
|
|
22
|
-
email: string
|
|
23
|
-
tenant_id: string
|
|
24
|
-
}
|
|
21
|
+
id: number
|
|
22
|
+
email: string
|
|
23
|
+
tenant_id: string
|
|
24
|
+
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const rlsSchema: RLSSchema<Database> = {
|
|
@@ -33,59 +33,59 @@ const rlsSchema: RLSSchema<Database> = {
|
|
|
33
33
|
name: 'users_read_own',
|
|
34
34
|
condition: () => true, // ORM-side condition
|
|
35
35
|
using: 'id = rls_current_user_id()::integer', // Native PostgreSQL
|
|
36
|
-
role: 'authenticated'
|
|
37
|
-
}
|
|
38
|
-
]
|
|
39
|
-
}
|
|
40
|
-
}
|
|
36
|
+
role: 'authenticated'
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
### 2. Generate PostgreSQL Statements
|
|
44
44
|
|
|
45
45
|
```typescript
|
|
46
|
-
import { PostgresRLSGenerator } from '@kysera/rls/native'
|
|
46
|
+
import { PostgresRLSGenerator } from '@kysera/rls/native'
|
|
47
47
|
|
|
48
|
-
const generator = new PostgresRLSGenerator()
|
|
48
|
+
const generator = new PostgresRLSGenerator()
|
|
49
49
|
|
|
50
50
|
// Generate RLS policies
|
|
51
51
|
const statements = generator.generateStatements(rlsSchema, {
|
|
52
52
|
schemaName: 'public',
|
|
53
53
|
policyPrefix: 'app_rls',
|
|
54
|
-
force: true
|
|
55
|
-
})
|
|
54
|
+
force: true // Force RLS on table owners
|
|
55
|
+
})
|
|
56
56
|
|
|
57
57
|
// Generate context functions
|
|
58
|
-
const contextFunctions = generator.generateContextFunctions()
|
|
58
|
+
const contextFunctions = generator.generateContextFunctions()
|
|
59
59
|
|
|
60
60
|
// Generate cleanup statements
|
|
61
61
|
const dropStatements = generator.generateDropStatements(rlsSchema, {
|
|
62
62
|
schemaName: 'public',
|
|
63
|
-
policyPrefix: 'app_rls'
|
|
64
|
-
})
|
|
63
|
+
policyPrefix: 'app_rls'
|
|
64
|
+
})
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
### 3. Generate Kysely Migration
|
|
68
68
|
|
|
69
69
|
```typescript
|
|
70
|
-
import { RLSMigrationGenerator } from '@kysera/rls/native'
|
|
70
|
+
import { RLSMigrationGenerator } from '@kysera/rls/native'
|
|
71
71
|
|
|
72
|
-
const migrationGenerator = new RLSMigrationGenerator()
|
|
72
|
+
const migrationGenerator = new RLSMigrationGenerator()
|
|
73
73
|
|
|
74
74
|
const migrationContent = migrationGenerator.generateMigration(rlsSchema, {
|
|
75
75
|
name: 'setup_rls',
|
|
76
76
|
schemaName: 'public',
|
|
77
77
|
policyPrefix: 'app_rls',
|
|
78
78
|
includeContextFunctions: true,
|
|
79
|
-
force: true
|
|
80
|
-
})
|
|
79
|
+
force: true
|
|
80
|
+
})
|
|
81
81
|
|
|
82
82
|
// Get suggested filename with timestamp
|
|
83
|
-
const filename = migrationGenerator.generateFilename('setup_rls')
|
|
83
|
+
const filename = migrationGenerator.generateFilename('setup_rls')
|
|
84
84
|
// Example: 20231208_123456_setup_rls.ts
|
|
85
85
|
|
|
86
86
|
// Write to migrations directory
|
|
87
|
-
import fs from 'fs'
|
|
88
|
-
fs.writeFileSync(`migrations/${filename}`, migrationContent)
|
|
87
|
+
import fs from 'fs'
|
|
88
|
+
fs.writeFileSync(`migrations/${filename}`, migrationContent)
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
### 4. Sync Context to PostgreSQL Session
|
|
@@ -141,13 +141,13 @@ Extend your Kysera policy definitions with native PostgreSQL support:
|
|
|
141
141
|
|
|
142
142
|
### Operations
|
|
143
143
|
|
|
144
|
-
| Kysera
|
|
145
|
-
|
|
146
|
-
| `read`
|
|
147
|
-
| `create` | `INSERT`
|
|
148
|
-
| `update` | `UPDATE`
|
|
149
|
-
| `delete` | `DELETE`
|
|
150
|
-
| `all`
|
|
144
|
+
| Kysera | PostgreSQL |
|
|
145
|
+
| -------- | ---------- |
|
|
146
|
+
| `read` | `SELECT` |
|
|
147
|
+
| `create` | `INSERT` |
|
|
148
|
+
| `update` | `UPDATE` |
|
|
149
|
+
| `delete` | `DELETE` |
|
|
150
|
+
| `all` | `ALL` |
|
|
151
151
|
|
|
152
152
|
## Context Functions
|
|
153
153
|
|
|
@@ -192,20 +192,21 @@ const rlsSchema: RLSSchema<Database> = {
|
|
|
192
192
|
name: 'posts_read_tenant',
|
|
193
193
|
condition: () => true,
|
|
194
194
|
using: 'tenant_id = rls_current_tenant_id()',
|
|
195
|
-
role: 'authenticated'
|
|
195
|
+
role: 'authenticated'
|
|
196
196
|
},
|
|
197
197
|
{
|
|
198
198
|
type: 'allow',
|
|
199
199
|
operation: 'create',
|
|
200
200
|
name: 'posts_create_own',
|
|
201
201
|
condition: () => true,
|
|
202
|
-
withCheck:
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
withCheck:
|
|
203
|
+
'user_id = rls_current_user_id()::integer AND tenant_id = rls_current_tenant_id()',
|
|
204
|
+
role: 'authenticated'
|
|
205
|
+
}
|
|
205
206
|
],
|
|
206
|
-
defaultDeny: true
|
|
207
|
-
}
|
|
208
|
-
}
|
|
207
|
+
defaultDeny: true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
209
210
|
```
|
|
210
211
|
|
|
211
212
|
### Role-Based Access Control
|
|
@@ -219,21 +220,21 @@ const rlsSchema: RLSSchema<Database> = {
|
|
|
219
220
|
operation: 'all',
|
|
220
221
|
name: 'admin_full_access',
|
|
221
222
|
condition: () => true,
|
|
222
|
-
using:
|
|
223
|
-
role: 'authenticated'
|
|
223
|
+
using: "rls_has_role('admin')",
|
|
224
|
+
role: 'authenticated'
|
|
224
225
|
},
|
|
225
226
|
{
|
|
226
227
|
type: 'deny',
|
|
227
228
|
operation: 'all',
|
|
228
229
|
name: 'deny_non_admin',
|
|
229
230
|
condition: () => true,
|
|
230
|
-
using:
|
|
231
|
-
role: 'authenticated'
|
|
232
|
-
}
|
|
231
|
+
using: "NOT rls_has_role('admin')",
|
|
232
|
+
role: 'authenticated'
|
|
233
|
+
}
|
|
233
234
|
],
|
|
234
|
-
defaultDeny: true
|
|
235
|
-
}
|
|
236
|
-
}
|
|
235
|
+
defaultDeny: true
|
|
236
|
+
}
|
|
237
|
+
}
|
|
237
238
|
```
|
|
238
239
|
|
|
239
240
|
### System Bypass
|
|
@@ -267,6 +268,7 @@ CREATE INDEX idx_posts_user ON posts(user_id);
|
|
|
267
268
|
## Migration Workflow
|
|
268
269
|
|
|
269
270
|
1. **Generate Migration**:
|
|
271
|
+
|
|
270
272
|
```bash
|
|
271
273
|
npx tsx -e "import { RLSMigrationGenerator } from './src/native'; ..."
|
|
272
274
|
```
|
|
@@ -274,6 +276,7 @@ CREATE INDEX idx_posts_user ON posts(user_id);
|
|
|
274
276
|
2. **Review Generated SQL**: Check migration file before applying
|
|
275
277
|
|
|
276
278
|
3. **Run Migration**:
|
|
279
|
+
|
|
277
280
|
```bash
|
|
278
281
|
npx kysely migrate:latest
|
|
279
282
|
```
|
package/src/native/index.ts
CHANGED
|
@@ -2,10 +2,7 @@ export {
|
|
|
2
2
|
PostgresRLSGenerator,
|
|
3
3
|
syncContextToPostgres,
|
|
4
4
|
clearPostgresContext,
|
|
5
|
-
type PostgresRLSOptions
|
|
6
|
-
} from './postgres.js'
|
|
5
|
+
type PostgresRLSOptions
|
|
6
|
+
} from './postgres.js'
|
|
7
7
|
|
|
8
|
-
export {
|
|
9
|
-
RLSMigrationGenerator,
|
|
10
|
-
type MigrationOptions,
|
|
11
|
-
} from './migration.js';
|
|
8
|
+
export { RLSMigrationGenerator, type MigrationOptions } from './migration.js'
|
package/src/native/migration.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type { RLSSchema } from '../policy/types.js'
|
|
2
|
-
import { PostgresRLSGenerator, type PostgresRLSOptions } from './postgres.js'
|
|
1
|
+
import type { RLSSchema } from '../policy/types.js'
|
|
2
|
+
import { PostgresRLSGenerator, type PostgresRLSOptions } from './postgres.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Options for migration generation
|
|
6
6
|
*/
|
|
7
7
|
export interface MigrationOptions extends PostgresRLSOptions {
|
|
8
8
|
/** Migration name */
|
|
9
|
-
name?: string
|
|
9
|
+
name?: string
|
|
10
10
|
/** Include context functions in migration */
|
|
11
|
-
includeContextFunctions?: boolean
|
|
11
|
+
includeContextFunctions?: boolean
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -16,27 +16,20 @@ export interface MigrationOptions extends PostgresRLSOptions {
|
|
|
16
16
|
* Generates Kysely migration files for RLS policies
|
|
17
17
|
*/
|
|
18
18
|
export class RLSMigrationGenerator {
|
|
19
|
-
private generator = new PostgresRLSGenerator()
|
|
19
|
+
private generator = new PostgresRLSGenerator()
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Generate migration file content
|
|
23
23
|
*/
|
|
24
|
-
generateMigration<DB>(
|
|
25
|
-
|
|
26
|
-
options: MigrationOptions = {}
|
|
27
|
-
): string {
|
|
28
|
-
const {
|
|
29
|
-
name = 'rls_policies',
|
|
30
|
-
includeContextFunctions = true,
|
|
31
|
-
...generatorOptions
|
|
32
|
-
} = options;
|
|
24
|
+
generateMigration<DB>(schema: RLSSchema<DB>, options: MigrationOptions = {}): string {
|
|
25
|
+
const { name = 'rls_policies', includeContextFunctions = true, ...generatorOptions } = options
|
|
33
26
|
|
|
34
|
-
const upStatements = this.generator.generateStatements(schema, generatorOptions)
|
|
35
|
-
const downStatements = this.generator.generateDropStatements(schema, generatorOptions)
|
|
27
|
+
const upStatements = this.generator.generateStatements(schema, generatorOptions)
|
|
28
|
+
const downStatements = this.generator.generateDropStatements(schema, generatorOptions)
|
|
36
29
|
|
|
37
30
|
const contextFunctions = includeContextFunctions
|
|
38
31
|
? this.generator.generateContextFunctions()
|
|
39
|
-
: ''
|
|
32
|
+
: ''
|
|
40
33
|
|
|
41
34
|
return `import { Kysely, sql } from 'kysely';
|
|
42
35
|
|
|
@@ -48,16 +41,22 @@ export class RLSMigrationGenerator {
|
|
|
48
41
|
*/
|
|
49
42
|
|
|
50
43
|
export async function up(db: Kysely<any>): Promise<void> {
|
|
51
|
-
${
|
|
44
|
+
${
|
|
45
|
+
includeContextFunctions
|
|
46
|
+
? ` // Create RLS context functions
|
|
52
47
|
await sql.raw(\`${this.escapeTemplate(contextFunctions)}\`).execute(db);
|
|
53
48
|
|
|
54
|
-
`
|
|
49
|
+
`
|
|
50
|
+
: ''
|
|
51
|
+
} // Enable RLS and create policies
|
|
55
52
|
${upStatements.map(s => ` await sql.raw(\`${this.escapeTemplate(s)}\`).execute(db);`).join('\n')}
|
|
56
53
|
}
|
|
57
54
|
|
|
58
55
|
export async function down(db: Kysely<any>): Promise<void> {
|
|
59
56
|
${downStatements.map(s => ` await sql.raw(\`${this.escapeTemplate(s)}\`).execute(db);`).join('\n')}
|
|
60
|
-
${
|
|
57
|
+
${
|
|
58
|
+
includeContextFunctions
|
|
59
|
+
? `
|
|
61
60
|
// Drop RLS context functions
|
|
62
61
|
await sql.raw(\`
|
|
63
62
|
DROP FUNCTION IF EXISTS rls_current_user_id();
|
|
@@ -67,26 +66,29 @@ ${includeContextFunctions ? `
|
|
|
67
66
|
DROP FUNCTION IF EXISTS rls_current_permissions();
|
|
68
67
|
DROP FUNCTION IF EXISTS rls_has_permission(text);
|
|
69
68
|
DROP FUNCTION IF EXISTS rls_is_system();
|
|
70
|
-
\`).execute(db);`
|
|
69
|
+
\`).execute(db);`
|
|
70
|
+
: ''
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
}
|
|
73
|
+
`
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
77
|
* Escape template literal for embedding in string
|
|
77
78
|
*/
|
|
78
79
|
private escapeTemplate(str: string): string {
|
|
79
|
-
return str.replace(/`/g, '\\`').replace(/\$/g, '\\$')
|
|
80
|
+
return str.replace(/`/g, '\\`').replace(/\$/g, '\\$')
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
/**
|
|
83
84
|
* Generate migration filename with timestamp
|
|
84
85
|
*/
|
|
85
|
-
generateFilename(name
|
|
86
|
-
const timestamp = new Date()
|
|
86
|
+
generateFilename(name = 'rls_policies'): string {
|
|
87
|
+
const timestamp = new Date()
|
|
88
|
+
.toISOString()
|
|
87
89
|
.replace(/[-:]/g, '')
|
|
88
90
|
.replace('T', '_')
|
|
89
|
-
.replace(/\..+/, '')
|
|
90
|
-
return `${timestamp}_${name}.ts
|
|
91
|
+
.replace(/\..+/, '')
|
|
92
|
+
return `${timestamp}_${name}.ts`
|
|
91
93
|
}
|
|
92
94
|
}
|