@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
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy Composition Builder
|
|
3
|
+
*
|
|
4
|
+
* Factory functions for creating reusable, composable RLS policies.
|
|
5
|
+
*
|
|
6
|
+
* @module @kysera/rls/composition/builder
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ReusablePolicy,
|
|
11
|
+
ReusablePolicyConfig,
|
|
12
|
+
TenantIsolationConfig,
|
|
13
|
+
OwnershipConfig,
|
|
14
|
+
SoftDeleteConfig,
|
|
15
|
+
StatusAccessConfig
|
|
16
|
+
} from './types.js'
|
|
17
|
+
import type { PolicyDefinition, PolicyEvaluationContext, Operation } from '../policy/types.js'
|
|
18
|
+
import { allow, deny, filter, validate } from '../policy/builder.js'
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Core Policy Builder
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a reusable policy template
|
|
26
|
+
*
|
|
27
|
+
* @param config - Policy configuration
|
|
28
|
+
* @param policies - Array of policy definitions
|
|
29
|
+
* @returns Reusable policy template
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const tenantPolicy = definePolicy(
|
|
34
|
+
* {
|
|
35
|
+
* name: 'tenantIsolation',
|
|
36
|
+
* description: 'Filter by tenant_id',
|
|
37
|
+
* tags: ['multi-tenant']
|
|
38
|
+
* },
|
|
39
|
+
* [
|
|
40
|
+
* filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }), {
|
|
41
|
+
* priority: 1000,
|
|
42
|
+
* name: 'tenant-filter'
|
|
43
|
+
* }),
|
|
44
|
+
* validate('create', ctx => ctx.data?.tenant_id === ctx.auth.tenantId, {
|
|
45
|
+
* name: 'tenant-validate'
|
|
46
|
+
* })
|
|
47
|
+
* ]
|
|
48
|
+
* );
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function definePolicy(
|
|
52
|
+
config: ReusablePolicyConfig,
|
|
53
|
+
policies: PolicyDefinition[]
|
|
54
|
+
): ReusablePolicy {
|
|
55
|
+
const result: ReusablePolicy = {
|
|
56
|
+
name: config.name,
|
|
57
|
+
policies
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (config.description !== undefined) {
|
|
61
|
+
result.description = config.description
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (config.tags !== undefined) {
|
|
65
|
+
result.tags = config.tags
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a filter-only policy
|
|
73
|
+
*
|
|
74
|
+
* @param name - Policy name
|
|
75
|
+
* @param filterFn - Filter condition
|
|
76
|
+
* @param options - Additional options
|
|
77
|
+
* @returns Reusable filter policy
|
|
78
|
+
*/
|
|
79
|
+
export function defineFilterPolicy(
|
|
80
|
+
name: string,
|
|
81
|
+
filterFn: (ctx: PolicyEvaluationContext) => Record<string, unknown>,
|
|
82
|
+
options?: { priority?: number }
|
|
83
|
+
): ReusablePolicy {
|
|
84
|
+
return {
|
|
85
|
+
name,
|
|
86
|
+
policies: [
|
|
87
|
+
filter('read', filterFn, {
|
|
88
|
+
name: `${name}-filter`,
|
|
89
|
+
...(options?.priority !== undefined && { priority: options.priority })
|
|
90
|
+
})
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create an allow-based policy
|
|
97
|
+
*
|
|
98
|
+
* @param name - Policy name
|
|
99
|
+
* @param operation - Operations to allow
|
|
100
|
+
* @param condition - Allow condition
|
|
101
|
+
* @param options - Additional options
|
|
102
|
+
* @returns Reusable allow policy
|
|
103
|
+
*/
|
|
104
|
+
export function defineAllowPolicy(
|
|
105
|
+
name: string,
|
|
106
|
+
operation: Operation | Operation[],
|
|
107
|
+
condition: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>,
|
|
108
|
+
options?: { priority?: number }
|
|
109
|
+
): ReusablePolicy {
|
|
110
|
+
return {
|
|
111
|
+
name,
|
|
112
|
+
policies: [
|
|
113
|
+
allow(operation, condition, {
|
|
114
|
+
name: `${name}-allow`,
|
|
115
|
+
...(options?.priority !== undefined && { priority: options.priority })
|
|
116
|
+
})
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a deny-based policy
|
|
123
|
+
*
|
|
124
|
+
* @param name - Policy name
|
|
125
|
+
* @param operation - Operations to deny
|
|
126
|
+
* @param condition - Deny condition (optional - if not provided, always denies)
|
|
127
|
+
* @param options - Additional options
|
|
128
|
+
* @returns Reusable deny policy
|
|
129
|
+
*/
|
|
130
|
+
export function defineDenyPolicy(
|
|
131
|
+
name: string,
|
|
132
|
+
operation: Operation | Operation[],
|
|
133
|
+
condition?: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>,
|
|
134
|
+
options?: { priority?: number }
|
|
135
|
+
): ReusablePolicy {
|
|
136
|
+
return {
|
|
137
|
+
name,
|
|
138
|
+
policies: [
|
|
139
|
+
deny(operation, condition, {
|
|
140
|
+
name: `${name}-deny`,
|
|
141
|
+
priority: options?.priority ?? 100
|
|
142
|
+
})
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create a validation policy
|
|
149
|
+
*
|
|
150
|
+
* @param name - Policy name
|
|
151
|
+
* @param operation - Operations to validate
|
|
152
|
+
* @param condition - Validation condition
|
|
153
|
+
* @param options - Additional options
|
|
154
|
+
* @returns Reusable validate policy
|
|
155
|
+
*/
|
|
156
|
+
export function defineValidatePolicy(
|
|
157
|
+
name: string,
|
|
158
|
+
operation: 'create' | 'update' | 'all',
|
|
159
|
+
condition: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>,
|
|
160
|
+
options?: { priority?: number }
|
|
161
|
+
): ReusablePolicy {
|
|
162
|
+
return {
|
|
163
|
+
name,
|
|
164
|
+
policies: [
|
|
165
|
+
validate(operation, condition, {
|
|
166
|
+
name: `${name}-validate`,
|
|
167
|
+
...(options?.priority !== undefined && { priority: options.priority })
|
|
168
|
+
})
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Create a combined policy with multiple types
|
|
175
|
+
*
|
|
176
|
+
* @param name - Policy name
|
|
177
|
+
* @param config - Policy configurations
|
|
178
|
+
* @returns Reusable combined policy
|
|
179
|
+
*/
|
|
180
|
+
export function defineCombinedPolicy(
|
|
181
|
+
name: string,
|
|
182
|
+
config: {
|
|
183
|
+
filter?: (ctx: PolicyEvaluationContext) => Record<string, unknown>
|
|
184
|
+
allow?: Record<string, (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>>
|
|
185
|
+
deny?: Record<string, (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>>
|
|
186
|
+
validate?: {
|
|
187
|
+
create?: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>
|
|
188
|
+
update?: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
): ReusablePolicy {
|
|
192
|
+
const policies: PolicyDefinition[] = []
|
|
193
|
+
|
|
194
|
+
// Add filter policy
|
|
195
|
+
if (config.filter) {
|
|
196
|
+
policies.push(
|
|
197
|
+
filter('read', config.filter, {
|
|
198
|
+
name: `${name}-filter`
|
|
199
|
+
})
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Add allow policies
|
|
204
|
+
if (config.allow) {
|
|
205
|
+
for (const [op, condition] of Object.entries(config.allow)) {
|
|
206
|
+
if (condition) {
|
|
207
|
+
policies.push(
|
|
208
|
+
allow(op as Operation, condition, {
|
|
209
|
+
name: `${name}-allow-${op}`
|
|
210
|
+
})
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Add deny policies
|
|
217
|
+
if (config.deny) {
|
|
218
|
+
for (const [op, condition] of Object.entries(config.deny)) {
|
|
219
|
+
if (condition) {
|
|
220
|
+
policies.push(
|
|
221
|
+
deny(op as Operation, condition, {
|
|
222
|
+
name: `${name}-deny-${op}`,
|
|
223
|
+
priority: 100
|
|
224
|
+
})
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Add validate policies
|
|
231
|
+
if (config.validate) {
|
|
232
|
+
if (config.validate.create) {
|
|
233
|
+
policies.push(
|
|
234
|
+
validate('create', config.validate.create, {
|
|
235
|
+
name: `${name}-validate-create`
|
|
236
|
+
})
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
if (config.validate.update) {
|
|
240
|
+
policies.push(
|
|
241
|
+
validate('update', config.validate.update, {
|
|
242
|
+
name: `${name}-validate-update`
|
|
243
|
+
})
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
name,
|
|
250
|
+
policies
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ============================================================================
|
|
255
|
+
// Common Policy Patterns
|
|
256
|
+
// ============================================================================
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Create a tenant isolation policy
|
|
260
|
+
*
|
|
261
|
+
* Automatically filters by tenant_id and validates mutations.
|
|
262
|
+
*
|
|
263
|
+
* @param config - Tenant isolation configuration
|
|
264
|
+
* @returns Reusable tenant isolation policy
|
|
265
|
+
*/
|
|
266
|
+
export function createTenantIsolationPolicy(config: TenantIsolationConfig = {}): ReusablePolicy {
|
|
267
|
+
const { tenantColumn = 'tenant_id', validateOnMutation = true } = config
|
|
268
|
+
|
|
269
|
+
const policies: PolicyDefinition[] = [
|
|
270
|
+
// Filter reads by tenant
|
|
271
|
+
filter('read', ctx => ({ [tenantColumn]: ctx.auth.tenantId }), {
|
|
272
|
+
name: 'tenant-isolation-filter',
|
|
273
|
+
priority: 1000
|
|
274
|
+
})
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
// Validate tenant on mutations
|
|
278
|
+
if (validateOnMutation) {
|
|
279
|
+
policies.push(
|
|
280
|
+
validate('create', ctx => {
|
|
281
|
+
const data = ctx.data as Record<string, unknown> | undefined
|
|
282
|
+
return data?.[tenantColumn] === ctx.auth.tenantId
|
|
283
|
+
}, {
|
|
284
|
+
name: 'tenant-isolation-validate-create'
|
|
285
|
+
}),
|
|
286
|
+
validate('update', ctx => {
|
|
287
|
+
const data = ctx.data as Record<string, unknown> | undefined
|
|
288
|
+
// Cannot change tenant on update
|
|
289
|
+
if (data?.[tenantColumn] !== undefined) {
|
|
290
|
+
return data[tenantColumn] === ctx.auth.tenantId
|
|
291
|
+
}
|
|
292
|
+
return true
|
|
293
|
+
}, {
|
|
294
|
+
name: 'tenant-isolation-validate-update'
|
|
295
|
+
})
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
name: 'tenantIsolation',
|
|
301
|
+
description: `Filter by ${tenantColumn} for multi-tenancy`,
|
|
302
|
+
policies,
|
|
303
|
+
tags: ['multi-tenant', 'isolation']
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Create an ownership policy
|
|
309
|
+
*
|
|
310
|
+
* Allows owners to read/update/delete their own resources.
|
|
311
|
+
*
|
|
312
|
+
* @param config - Ownership configuration
|
|
313
|
+
* @returns Reusable ownership policy
|
|
314
|
+
*/
|
|
315
|
+
export function createOwnershipPolicy(config: OwnershipConfig = {}): ReusablePolicy {
|
|
316
|
+
const { ownerColumn = 'owner_id', ownerOperations = ['read', 'update', 'delete'], canDelete = true } = config
|
|
317
|
+
|
|
318
|
+
const policies: PolicyDefinition[] = []
|
|
319
|
+
|
|
320
|
+
// Filter ops to only those allowed
|
|
321
|
+
const ops = ownerOperations.filter(op => op !== 'delete' || canDelete)
|
|
322
|
+
|
|
323
|
+
if (ops.length > 0) {
|
|
324
|
+
policies.push(
|
|
325
|
+
allow(ops, ctx => {
|
|
326
|
+
const row = ctx.row as Record<string, unknown> | undefined
|
|
327
|
+
return ctx.auth.userId === row?.[ownerColumn]
|
|
328
|
+
}, {
|
|
329
|
+
name: 'ownership-allow'
|
|
330
|
+
})
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Explicit deny for delete if not allowed
|
|
335
|
+
if (!canDelete && ownerOperations.includes('delete')) {
|
|
336
|
+
policies.push(
|
|
337
|
+
deny('delete', () => true, {
|
|
338
|
+
name: 'ownership-no-delete',
|
|
339
|
+
priority: 150
|
|
340
|
+
})
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
name: 'ownership',
|
|
346
|
+
description: `Owner access via ${ownerColumn}`,
|
|
347
|
+
policies,
|
|
348
|
+
tags: ['ownership']
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Create a soft delete policy
|
|
354
|
+
*
|
|
355
|
+
* Filters out soft-deleted rows and optionally prevents hard deletes.
|
|
356
|
+
*
|
|
357
|
+
* @param config - Soft delete configuration
|
|
358
|
+
* @returns Reusable soft delete policy
|
|
359
|
+
*/
|
|
360
|
+
export function createSoftDeletePolicy(config: SoftDeleteConfig = {}): ReusablePolicy {
|
|
361
|
+
const { deletedColumn = 'deleted_at', filterOnRead = true, preventHardDelete = true } = config
|
|
362
|
+
|
|
363
|
+
const policies: PolicyDefinition[] = []
|
|
364
|
+
|
|
365
|
+
// Filter soft-deleted rows
|
|
366
|
+
if (filterOnRead) {
|
|
367
|
+
policies.push(
|
|
368
|
+
filter('read', () => ({ [deletedColumn]: null }), {
|
|
369
|
+
name: 'soft-delete-filter',
|
|
370
|
+
priority: 900
|
|
371
|
+
})
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Prevent hard deletes
|
|
376
|
+
if (preventHardDelete) {
|
|
377
|
+
policies.push(
|
|
378
|
+
deny('delete', () => true, {
|
|
379
|
+
name: 'soft-delete-no-hard-delete',
|
|
380
|
+
priority: 150
|
|
381
|
+
})
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
name: 'softDelete',
|
|
387
|
+
description: `Soft delete via ${deletedColumn}`,
|
|
388
|
+
policies,
|
|
389
|
+
tags: ['soft-delete']
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Create a status-based access policy
|
|
395
|
+
*
|
|
396
|
+
* Controls access based on resource status.
|
|
397
|
+
*
|
|
398
|
+
* @param config - Status access configuration
|
|
399
|
+
* @returns Reusable status policy
|
|
400
|
+
*/
|
|
401
|
+
export function createStatusAccessPolicy(config: StatusAccessConfig): ReusablePolicy {
|
|
402
|
+
const { statusColumn = 'status', publicStatuses = [], editableStatuses = [], deletableStatuses = [] } = config
|
|
403
|
+
|
|
404
|
+
const policies: PolicyDefinition[] = []
|
|
405
|
+
|
|
406
|
+
// Allow public read for certain statuses
|
|
407
|
+
if (publicStatuses.length > 0) {
|
|
408
|
+
policies.push(
|
|
409
|
+
allow('read', ctx => {
|
|
410
|
+
const row = ctx.row as Record<string, unknown> | undefined
|
|
411
|
+
return publicStatuses.includes(row?.[statusColumn] as string)
|
|
412
|
+
}, {
|
|
413
|
+
name: 'status-public-read'
|
|
414
|
+
})
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Restrict updates to certain statuses
|
|
419
|
+
if (editableStatuses.length > 0) {
|
|
420
|
+
policies.push(
|
|
421
|
+
deny('update', ctx => {
|
|
422
|
+
const row = ctx.row as Record<string, unknown> | undefined
|
|
423
|
+
return !editableStatuses.includes(row?.[statusColumn] as string)
|
|
424
|
+
}, {
|
|
425
|
+
name: 'status-restrict-update',
|
|
426
|
+
priority: 100
|
|
427
|
+
})
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Restrict deletes to certain statuses
|
|
432
|
+
if (deletableStatuses.length > 0) {
|
|
433
|
+
policies.push(
|
|
434
|
+
deny('delete', ctx => {
|
|
435
|
+
const row = ctx.row as Record<string, unknown> | undefined
|
|
436
|
+
return !deletableStatuses.includes(row?.[statusColumn] as string)
|
|
437
|
+
}, {
|
|
438
|
+
name: 'status-restrict-delete',
|
|
439
|
+
priority: 100
|
|
440
|
+
})
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
name: 'statusAccess',
|
|
446
|
+
description: `Status-based access via ${statusColumn}`,
|
|
447
|
+
policies,
|
|
448
|
+
tags: ['status']
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Create an admin bypass policy
|
|
454
|
+
*
|
|
455
|
+
* Allows admin roles to perform all operations.
|
|
456
|
+
*
|
|
457
|
+
* @param roles - Roles that have admin access
|
|
458
|
+
* @returns Reusable admin policy
|
|
459
|
+
*/
|
|
460
|
+
export function createAdminPolicy(roles: string[]): ReusablePolicy {
|
|
461
|
+
return {
|
|
462
|
+
name: 'adminBypass',
|
|
463
|
+
description: `Admin access for roles: ${roles.join(', ')}`,
|
|
464
|
+
policies: [
|
|
465
|
+
allow('all', ctx => roles.some(r => ctx.auth.roles.includes(r)), {
|
|
466
|
+
name: 'admin-bypass',
|
|
467
|
+
priority: 500
|
|
468
|
+
})
|
|
469
|
+
],
|
|
470
|
+
tags: ['admin']
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ============================================================================
|
|
475
|
+
// Policy Composition Functions
|
|
476
|
+
// ============================================================================
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Compose multiple reusable policies into one
|
|
480
|
+
*
|
|
481
|
+
* @param name - Name for the composed policy
|
|
482
|
+
* @param policies - Policies to compose
|
|
483
|
+
* @returns Composed policy
|
|
484
|
+
*/
|
|
485
|
+
export function composePolicies(name: string, policies: ReusablePolicy[]): ReusablePolicy {
|
|
486
|
+
const allPolicies: PolicyDefinition[] = []
|
|
487
|
+
const allTags = new Set<string>()
|
|
488
|
+
|
|
489
|
+
for (const policy of policies) {
|
|
490
|
+
allPolicies.push(...policy.policies)
|
|
491
|
+
policy.tags?.forEach(tag => allTags.add(tag))
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
name,
|
|
496
|
+
description: `Composed from: ${policies.map(p => p.name).join(', ')}`,
|
|
497
|
+
policies: allPolicies,
|
|
498
|
+
tags: Array.from(allTags)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Extend a reusable policy with additional policies
|
|
504
|
+
*
|
|
505
|
+
* @param base - Base policy to extend
|
|
506
|
+
* @param additional - Additional policies to add
|
|
507
|
+
* @returns Extended policy
|
|
508
|
+
*/
|
|
509
|
+
export function extendPolicy(base: ReusablePolicy, additional: PolicyDefinition[]): ReusablePolicy {
|
|
510
|
+
const result: ReusablePolicy = {
|
|
511
|
+
name: `${base.name}_extended`,
|
|
512
|
+
policies: [...base.policies, ...additional]
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (base.description !== undefined) {
|
|
516
|
+
result.description = base.description
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (base.tags !== undefined) {
|
|
520
|
+
result.tags = base.tags
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return result
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Override policies from a base with new conditions
|
|
528
|
+
*
|
|
529
|
+
* @param base - Base policy
|
|
530
|
+
* @param overrides - Policy name to new policy mapping
|
|
531
|
+
* @returns Policy with overrides applied
|
|
532
|
+
*/
|
|
533
|
+
export function overridePolicy(
|
|
534
|
+
base: ReusablePolicy,
|
|
535
|
+
overrides: Record<string, PolicyDefinition>
|
|
536
|
+
): ReusablePolicy {
|
|
537
|
+
const newPolicies = base.policies.map(policy => {
|
|
538
|
+
const override = policy.name ? overrides[policy.name] : undefined
|
|
539
|
+
return override ?? policy
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
const result: ReusablePolicy = {
|
|
543
|
+
name: `${base.name}_overridden`,
|
|
544
|
+
policies: newPolicies
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (base.description !== undefined) {
|
|
548
|
+
result.description = base.description
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (base.tags !== undefined) {
|
|
552
|
+
result.tags = base.tags
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return result
|
|
556
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy Composition Module
|
|
3
|
+
*
|
|
4
|
+
* Provides tools for creating reusable, composable RLS policies.
|
|
5
|
+
*
|
|
6
|
+
* @module @kysera/rls/composition
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
export type {
|
|
11
|
+
ReusablePolicy,
|
|
12
|
+
ReusablePolicyConfig,
|
|
13
|
+
ComposableTableConfig,
|
|
14
|
+
ComposableRLSSchema,
|
|
15
|
+
BasePolicyDefinition,
|
|
16
|
+
ResolvedInheritance,
|
|
17
|
+
TenantIsolationConfig,
|
|
18
|
+
OwnershipConfig,
|
|
19
|
+
SoftDeleteConfig,
|
|
20
|
+
StatusAccessConfig
|
|
21
|
+
} from './types.js'
|
|
22
|
+
|
|
23
|
+
// Builders
|
|
24
|
+
export {
|
|
25
|
+
definePolicy,
|
|
26
|
+
defineFilterPolicy,
|
|
27
|
+
defineAllowPolicy,
|
|
28
|
+
defineDenyPolicy,
|
|
29
|
+
defineValidatePolicy,
|
|
30
|
+
defineCombinedPolicy
|
|
31
|
+
} from './builder.js'
|
|
32
|
+
|
|
33
|
+
// Common patterns
|
|
34
|
+
export {
|
|
35
|
+
createTenantIsolationPolicy,
|
|
36
|
+
createOwnershipPolicy,
|
|
37
|
+
createSoftDeletePolicy,
|
|
38
|
+
createStatusAccessPolicy,
|
|
39
|
+
createAdminPolicy
|
|
40
|
+
} from './builder.js'
|
|
41
|
+
|
|
42
|
+
// Composition functions
|
|
43
|
+
export { composePolicies, extendPolicy, overridePolicy } from './builder.js'
|