@kysera/rls 0.7.3 → 0.8.0
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 +389 -279
- package/dist/index.d.ts +89 -20
- package/dist/index.js +210 -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 +286 -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/policy/builder.ts
CHANGED
|
@@ -13,19 +13,19 @@ import type {
|
|
|
13
13
|
PolicyDefinition,
|
|
14
14
|
PolicyCondition,
|
|
15
15
|
FilterCondition,
|
|
16
|
-
PolicyHints
|
|
17
|
-
} from './types.js'
|
|
16
|
+
PolicyHints
|
|
17
|
+
} from './types.js'
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Options for policy definitions
|
|
21
21
|
*/
|
|
22
22
|
export interface PolicyOptions {
|
|
23
23
|
/** Policy name for debugging and identification */
|
|
24
|
-
name?: string
|
|
24
|
+
name?: string
|
|
25
25
|
/** Priority (higher runs first, deny policies default to 100) */
|
|
26
|
-
priority?: number
|
|
26
|
+
priority?: number
|
|
27
27
|
/** Performance optimization hints */
|
|
28
|
-
hints?: PolicyHints
|
|
28
|
+
hints?: PolicyHints
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -58,19 +58,19 @@ export function allow(
|
|
|
58
58
|
const policy: PolicyDefinition = {
|
|
59
59
|
type: 'allow',
|
|
60
60
|
operation,
|
|
61
|
-
condition: condition
|
|
62
|
-
priority: options?.priority ?? 0
|
|
63
|
-
}
|
|
61
|
+
condition: condition,
|
|
62
|
+
priority: options?.priority ?? 0
|
|
63
|
+
}
|
|
64
64
|
|
|
65
65
|
if (options?.name !== undefined) {
|
|
66
|
-
policy.name = options.name
|
|
66
|
+
policy.name = options.name
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
if (options?.hints !== undefined) {
|
|
70
|
-
policy.hints = options.hints
|
|
70
|
+
policy.hints = options.hints
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
return policy
|
|
73
|
+
return policy
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
@@ -104,36 +104,51 @@ export function deny(
|
|
|
104
104
|
const policy: PolicyDefinition = {
|
|
105
105
|
type: 'deny',
|
|
106
106
|
operation,
|
|
107
|
-
condition:
|
|
108
|
-
priority: options?.priority ?? 100
|
|
109
|
-
}
|
|
107
|
+
condition: condition ?? (() => true),
|
|
108
|
+
priority: options?.priority ?? 100 // Deny policies run first by default
|
|
109
|
+
}
|
|
110
110
|
|
|
111
111
|
if (options?.name !== undefined) {
|
|
112
|
-
policy.name = options.name
|
|
112
|
+
policy.name = options.name
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
if (options?.hints !== undefined) {
|
|
116
|
-
policy.hints = options.hints
|
|
116
|
+
policy.hints = options.hints
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
return policy
|
|
119
|
+
return policy
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* Create a filter policy
|
|
124
124
|
* Adds WHERE conditions to SELECT queries
|
|
125
125
|
*
|
|
126
|
+
* **IMPORTANT**: Filter conditions must be synchronous functions.
|
|
127
|
+
* Async filter policies are not currently supported because filters are applied
|
|
128
|
+
* directly to query builders at query construction time.
|
|
129
|
+
*
|
|
126
130
|
* @example
|
|
127
131
|
* ```typescript
|
|
128
|
-
* // Filter by tenant
|
|
132
|
+
* // ✅ CORRECT: Filter by tenant (synchronous)
|
|
129
133
|
* filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }))
|
|
130
134
|
*
|
|
131
|
-
* // Filter by organization with soft delete
|
|
135
|
+
* // ✅ CORRECT: Filter by organization with soft delete
|
|
132
136
|
* filter('read', ctx => ({
|
|
133
137
|
* organization_id: ctx.auth.organizationIds?.[0],
|
|
134
138
|
* deleted_at: null
|
|
135
139
|
* }))
|
|
136
140
|
*
|
|
141
|
+
* // ❌ WRONG: Async filter (not supported)
|
|
142
|
+
* // filter('read', async ctx => {
|
|
143
|
+
* // const tenantId = await fetchTenantId(ctx.auth.userId)
|
|
144
|
+
* // return { tenant_id: tenantId }
|
|
145
|
+
* // })
|
|
146
|
+
*
|
|
147
|
+
* // ✅ WORKAROUND: Fetch data before creating context
|
|
148
|
+
* // const tenantId = await fetchTenantId(userId)
|
|
149
|
+
* // const ctx = createRLSContext({ auth: { userId, tenantId, roles: [] } })
|
|
150
|
+
* // filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }))
|
|
151
|
+
*
|
|
137
152
|
* // Named filter
|
|
138
153
|
* filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }), {
|
|
139
154
|
* name: 'tenant-filter'
|
|
@@ -149,18 +164,18 @@ export function filter(
|
|
|
149
164
|
type: 'filter',
|
|
150
165
|
operation: operation === 'all' ? 'read' : operation,
|
|
151
166
|
condition: condition as unknown as PolicyCondition,
|
|
152
|
-
priority: options?.priority ?? 0
|
|
153
|
-
}
|
|
167
|
+
priority: options?.priority ?? 0
|
|
168
|
+
}
|
|
154
169
|
|
|
155
170
|
if (options?.name !== undefined) {
|
|
156
|
-
policy.name = options.name
|
|
171
|
+
policy.name = options.name
|
|
157
172
|
}
|
|
158
173
|
|
|
159
174
|
if (options?.hints !== undefined) {
|
|
160
|
-
policy.hints = options.hints
|
|
175
|
+
policy.hints = options.hints
|
|
161
176
|
}
|
|
162
177
|
|
|
163
|
-
return policy
|
|
178
|
+
return policy
|
|
164
179
|
}
|
|
165
180
|
|
|
166
181
|
/**
|
|
@@ -192,24 +207,22 @@ export function validate(
|
|
|
192
207
|
condition: PolicyCondition,
|
|
193
208
|
options?: PolicyOptions
|
|
194
209
|
): PolicyDefinition {
|
|
195
|
-
const ops: Operation[] = operation === 'all'
|
|
196
|
-
? ['create', 'update']
|
|
197
|
-
: [operation];
|
|
210
|
+
const ops: Operation[] = operation === 'all' ? ['create', 'update'] : [operation]
|
|
198
211
|
|
|
199
212
|
const policy: PolicyDefinition = {
|
|
200
213
|
type: 'validate',
|
|
201
214
|
operation: ops,
|
|
202
|
-
condition: condition
|
|
203
|
-
priority: options?.priority ?? 0
|
|
204
|
-
}
|
|
215
|
+
condition: condition,
|
|
216
|
+
priority: options?.priority ?? 0
|
|
217
|
+
}
|
|
205
218
|
|
|
206
219
|
if (options?.name !== undefined) {
|
|
207
|
-
policy.name = options.name
|
|
220
|
+
policy.name = options.name
|
|
208
221
|
}
|
|
209
222
|
|
|
210
223
|
if (options?.hints !== undefined) {
|
|
211
|
-
policy.hints = options.hints
|
|
224
|
+
policy.hints = options.hints
|
|
212
225
|
}
|
|
213
226
|
|
|
214
|
-
return policy
|
|
227
|
+
return policy
|
|
215
228
|
}
|
package/src/policy/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Exports all policy-related types, builders, and schema functions.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export * from './types.js'
|
|
8
|
-
export { allow, deny, filter, validate, type PolicyOptions } from './builder.js'
|
|
9
|
-
export { defineRLSSchema, mergeRLSSchemas } from './schema.js'
|
|
10
|
-
export { PolicyRegistry } from './registry.js'
|
|
7
|
+
export type * from './types.js'
|
|
8
|
+
export { allow, deny, filter, validate, type PolicyOptions } from './builder.js'
|
|
9
|
+
export { defineRLSSchema, mergeRLSSchemas } from './schema.js'
|
|
10
|
+
export { PolicyRegistry } from './registry.js'
|
package/src/policy/registry.ts
CHANGED
|
@@ -15,31 +15,32 @@ import type {
|
|
|
15
15
|
TableRLSConfig,
|
|
16
16
|
CompiledPolicy,
|
|
17
17
|
CompiledFilterPolicy,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import {
|
|
18
|
+
PolicyEvaluationContext
|
|
19
|
+
} from './types.js'
|
|
20
|
+
import { RLSSchemaError } from '../errors.js'
|
|
21
|
+
import { silentLogger, type KyseraLogger } from '@kysera/core'
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Internal compiled policy with operations as Set for efficient lookup
|
|
24
25
|
*/
|
|
25
26
|
interface InternalCompiledPolicy {
|
|
26
|
-
name: string
|
|
27
|
-
operations: Set<Operation
|
|
28
|
-
type: 'allow' | 'deny' | 'validate'
|
|
29
|
-
evaluate: (ctx:
|
|
30
|
-
priority: number
|
|
27
|
+
name: string
|
|
28
|
+
operations: Set<Operation>
|
|
29
|
+
type: 'allow' | 'deny' | 'validate'
|
|
30
|
+
evaluate: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>
|
|
31
|
+
priority: number
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Table policy configuration
|
|
35
36
|
*/
|
|
36
37
|
interface TablePolicyConfig {
|
|
37
|
-
allows: InternalCompiledPolicy[]
|
|
38
|
-
denies: InternalCompiledPolicy[]
|
|
39
|
-
filters: CompiledFilterPolicy[]
|
|
40
|
-
validates: InternalCompiledPolicy[]
|
|
41
|
-
skipFor: string[]
|
|
42
|
-
defaultDeny: boolean
|
|
38
|
+
allows: InternalCompiledPolicy[]
|
|
39
|
+
denies: InternalCompiledPolicy[]
|
|
40
|
+
filters: CompiledFilterPolicy[]
|
|
41
|
+
validates: InternalCompiledPolicy[]
|
|
42
|
+
skipFor: string[] // Role names that bypass RLS
|
|
43
|
+
defaultDeny: boolean
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
@@ -47,14 +48,14 @@ interface TablePolicyConfig {
|
|
|
47
48
|
* Manages and provides access to RLS policies
|
|
48
49
|
*/
|
|
49
50
|
export class PolicyRegistry<DB = unknown> {
|
|
50
|
-
private tables = new Map<string, TablePolicyConfig>()
|
|
51
|
-
private compiled = false
|
|
52
|
-
private logger: KyseraLogger
|
|
51
|
+
private tables = new Map<string, TablePolicyConfig>()
|
|
52
|
+
private compiled = false
|
|
53
|
+
private logger: KyseraLogger
|
|
53
54
|
|
|
54
55
|
constructor(schema?: RLSSchema<DB>, options?: { logger?: KyseraLogger }) {
|
|
55
|
-
this.logger = options?.logger ?? silentLogger
|
|
56
|
+
this.logger = options?.logger ?? silentLogger
|
|
56
57
|
if (schema) {
|
|
57
|
-
this.loadSchema(schema)
|
|
58
|
+
this.loadSchema(schema)
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -77,10 +78,10 @@ export class PolicyRegistry<DB = unknown> {
|
|
|
77
78
|
*/
|
|
78
79
|
loadSchema(schema: RLSSchema<DB>): void {
|
|
79
80
|
for (const [table, config] of Object.entries(schema)) {
|
|
80
|
-
if (!config) continue
|
|
81
|
-
this.registerTable(table, config as TableRLSConfig)
|
|
81
|
+
if (!config) continue
|
|
82
|
+
this.registerTable(table, config as TableRLSConfig)
|
|
82
83
|
}
|
|
83
|
-
this.compiled = true
|
|
84
|
+
this.compiled = true
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
/**
|
|
@@ -96,49 +97,49 @@ export class PolicyRegistry<DB = unknown> {
|
|
|
96
97
|
filters: [],
|
|
97
98
|
validates: [],
|
|
98
99
|
skipFor: config.skipFor ?? [],
|
|
99
|
-
defaultDeny: config.defaultDeny ?? true
|
|
100
|
-
}
|
|
100
|
+
defaultDeny: config.defaultDeny ?? true
|
|
101
|
+
}
|
|
101
102
|
|
|
102
103
|
// Compile and categorize policies
|
|
103
104
|
for (let i = 0; i < config.policies.length; i++) {
|
|
104
|
-
const policy = config.policies[i]
|
|
105
|
-
if (!policy) continue
|
|
105
|
+
const policy = config.policies[i]
|
|
106
|
+
if (!policy) continue
|
|
106
107
|
|
|
107
|
-
const policyName = policy.name ?? `${table}_policy_${i}
|
|
108
|
+
const policyName = policy.name ?? `${table}_policy_${i}`
|
|
108
109
|
|
|
109
110
|
try {
|
|
110
111
|
if (policy.type === 'filter') {
|
|
111
|
-
const compiled = this.compileFilterPolicy(policy, policyName)
|
|
112
|
-
tableConfig.filters.push(compiled)
|
|
112
|
+
const compiled = this.compileFilterPolicy(policy, policyName)
|
|
113
|
+
tableConfig.filters.push(compiled)
|
|
113
114
|
} else {
|
|
114
|
-
const compiled = this.compilePolicy(policy, policyName)
|
|
115
|
+
const compiled = this.compilePolicy(policy, policyName)
|
|
115
116
|
|
|
116
117
|
switch (policy.type) {
|
|
117
118
|
case 'allow':
|
|
118
|
-
tableConfig.allows.push(compiled)
|
|
119
|
-
break
|
|
119
|
+
tableConfig.allows.push(compiled)
|
|
120
|
+
break
|
|
120
121
|
case 'deny':
|
|
121
|
-
tableConfig.denies.push(compiled)
|
|
122
|
-
break
|
|
122
|
+
tableConfig.denies.push(compiled)
|
|
123
|
+
break
|
|
123
124
|
case 'validate':
|
|
124
|
-
tableConfig.validates.push(compiled)
|
|
125
|
-
break
|
|
125
|
+
tableConfig.validates.push(compiled)
|
|
126
|
+
break
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
} catch (error) {
|
|
129
130
|
throw new RLSSchemaError(
|
|
130
131
|
`Failed to compile policy "${policyName}" for table "${table}": ${error instanceof Error ? error.message : String(error)}`,
|
|
131
132
|
{ table, policy: policyName }
|
|
132
|
-
)
|
|
133
|
+
)
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
// Sort by priority (higher priority first)
|
|
137
|
-
tableConfig.allows.sort((a, b) => b.priority - a.priority)
|
|
138
|
-
tableConfig.denies.sort((a, b) => b.priority - a.priority)
|
|
139
|
-
tableConfig.validates.sort((a, b) => b.priority - a.priority)
|
|
138
|
+
tableConfig.allows.sort((a, b) => b.priority - a.priority)
|
|
139
|
+
tableConfig.denies.sort((a, b) => b.priority - a.priority)
|
|
140
|
+
tableConfig.validates.sort((a, b) => b.priority - a.priority)
|
|
140
141
|
|
|
141
|
-
this.tables.set(table, tableConfig)
|
|
142
|
+
this.tables.set(table, tableConfig)
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
/**
|
|
@@ -147,48 +148,48 @@ export class PolicyRegistry<DB = unknown> {
|
|
|
147
148
|
* @overload Register a full schema
|
|
148
149
|
* @overload Register policies for a single table (deprecated)
|
|
149
150
|
*/
|
|
150
|
-
register(schemaOrTable: RLSSchema<DB>): void
|
|
151
|
+
register(schemaOrTable: RLSSchema<DB>): void
|
|
151
152
|
register(
|
|
152
153
|
schemaOrTable: keyof DB & string,
|
|
153
154
|
policies: PolicyDefinition[],
|
|
154
155
|
options?: {
|
|
155
|
-
skipFor?: string[]
|
|
156
|
-
defaultDeny?: boolean
|
|
156
|
+
skipFor?: string[]
|
|
157
|
+
defaultDeny?: boolean
|
|
157
158
|
}
|
|
158
|
-
): void
|
|
159
|
+
): void
|
|
159
160
|
register(
|
|
160
161
|
schemaOrTable: RLSSchema<DB> | (keyof DB & string),
|
|
161
162
|
policies?: PolicyDefinition[],
|
|
162
163
|
options?: {
|
|
163
|
-
skipFor?: string[]
|
|
164
|
-
defaultDeny?: boolean
|
|
164
|
+
skipFor?: string[] // Role names that bypass RLS
|
|
165
|
+
defaultDeny?: boolean
|
|
165
166
|
}
|
|
166
167
|
): void {
|
|
167
168
|
// If first argument is an object with policies, treat as schema
|
|
168
169
|
if (typeof schemaOrTable === 'object' && schemaOrTable !== null) {
|
|
169
|
-
this.loadSchema(schemaOrTable)
|
|
170
|
-
return
|
|
170
|
+
this.loadSchema(schemaOrTable)
|
|
171
|
+
return
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
// Otherwise, treat as table-based registration
|
|
174
|
-
const table = schemaOrTable
|
|
175
|
+
const table = schemaOrTable
|
|
175
176
|
if (!policies) {
|
|
176
|
-
throw new RLSSchemaError('Policies are required when registering by table name', { table })
|
|
177
|
+
throw new RLSSchemaError('Policies are required when registering by table name', { table })
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
const config: TableRLSConfig = {
|
|
180
|
-
policies
|
|
181
|
-
}
|
|
181
|
+
policies
|
|
182
|
+
}
|
|
182
183
|
|
|
183
184
|
if (options?.skipFor !== undefined) {
|
|
184
|
-
config.skipFor = options.skipFor
|
|
185
|
+
config.skipFor = options.skipFor
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
if (options?.defaultDeny !== undefined) {
|
|
188
|
-
config.defaultDeny = options.defaultDeny
|
|
189
|
+
config.defaultDeny = options.defaultDeny
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
this.registerTable(table, config)
|
|
192
|
+
this.registerTable(table, config)
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
/**
|
|
@@ -199,22 +200,20 @@ export class PolicyRegistry<DB = unknown> {
|
|
|
199
200
|
* @returns Compiled policy ready for evaluation
|
|
200
201
|
*/
|
|
201
202
|
private compilePolicy(policy: PolicyDefinition, name: string): InternalCompiledPolicy {
|
|
202
|
-
const operations = Array.isArray(policy.operation)
|
|
203
|
-
? policy.operation
|
|
204
|
-
: [policy.operation];
|
|
203
|
+
const operations = Array.isArray(policy.operation) ? policy.operation : [policy.operation]
|
|
205
204
|
|
|
206
205
|
// Expand 'all' to all operations
|
|
207
206
|
const expandedOps = operations.flatMap(op =>
|
|
208
207
|
op === 'all' ? (['read', 'create', 'update', 'delete'] as const) : [op]
|
|
209
|
-
) as Operation[]
|
|
208
|
+
) as Operation[]
|
|
210
209
|
|
|
211
210
|
return {
|
|
212
211
|
name,
|
|
213
212
|
operations: new Set(expandedOps),
|
|
214
213
|
type: policy.type as 'allow' | 'deny' | 'validate',
|
|
215
|
-
evaluate: policy.condition as (ctx:
|
|
216
|
-
priority: policy.priority ?? (policy.type === 'deny' ? 100 : 0)
|
|
217
|
-
}
|
|
214
|
+
evaluate: policy.condition as (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>,
|
|
215
|
+
priority: policy.priority ?? (policy.type === 'deny' ? 100 : 0)
|
|
216
|
+
}
|
|
218
217
|
}
|
|
219
218
|
|
|
220
219
|
/**
|
|
@@ -225,13 +224,13 @@ export class PolicyRegistry<DB = unknown> {
|
|
|
225
224
|
* @returns Compiled filter policy
|
|
226
225
|
*/
|
|
227
226
|
private compileFilterPolicy(policy: PolicyDefinition, name: string): CompiledFilterPolicy {
|
|
228
|
-
const condition = policy.condition as unknown as FilterCondition
|
|
227
|
+
const condition = policy.condition as unknown as FilterCondition
|
|
229
228
|
|
|
230
229
|
return {
|
|
231
230
|
operation: 'read',
|
|
232
|
-
getConditions: condition as (ctx:
|
|
233
|
-
name
|
|
234
|
-
}
|
|
231
|
+
getConditions: condition as (ctx: PolicyEvaluationContext) => Record<string, unknown>,
|
|
232
|
+
name
|
|
233
|
+
}
|
|
235
234
|
}
|
|
236
235
|
|
|
237
236
|
/**
|
|
@@ -243,89 +242,85 @@ export class PolicyRegistry<DB = unknown> {
|
|
|
243
242
|
type: internal.type,
|
|
244
243
|
operation: Array.from(internal.operations),
|
|
245
244
|
evaluate: internal.evaluate,
|
|
246
|
-
priority: internal.priority
|
|
247
|
-
}
|
|
245
|
+
priority: internal.priority
|
|
246
|
+
}
|
|
248
247
|
}
|
|
249
248
|
|
|
250
249
|
/**
|
|
251
250
|
* Get allow policies for a table and operation
|
|
252
251
|
*/
|
|
253
252
|
getAllows(table: string, operation: Operation): CompiledPolicy[] {
|
|
254
|
-
const config = this.tables.get(table)
|
|
255
|
-
if (!config) return []
|
|
253
|
+
const config = this.tables.get(table)
|
|
254
|
+
if (!config) return []
|
|
256
255
|
|
|
257
|
-
return config.allows
|
|
258
|
-
.filter(p => p.operations.has(operation))
|
|
259
|
-
.map(p => this.toCompiledPolicy(p));
|
|
256
|
+
return config.allows.filter(p => p.operations.has(operation)).map(p => this.toCompiledPolicy(p))
|
|
260
257
|
}
|
|
261
258
|
|
|
262
259
|
/**
|
|
263
260
|
* Get deny policies for a table and operation
|
|
264
261
|
*/
|
|
265
262
|
getDenies(table: string, operation: Operation): CompiledPolicy[] {
|
|
266
|
-
const config = this.tables.get(table)
|
|
267
|
-
if (!config) return []
|
|
263
|
+
const config = this.tables.get(table)
|
|
264
|
+
if (!config) return []
|
|
268
265
|
|
|
269
|
-
return config.denies
|
|
270
|
-
.filter(p => p.operations.has(operation))
|
|
271
|
-
.map(p => this.toCompiledPolicy(p));
|
|
266
|
+
return config.denies.filter(p => p.operations.has(operation)).map(p => this.toCompiledPolicy(p))
|
|
272
267
|
}
|
|
273
268
|
|
|
274
269
|
/**
|
|
275
270
|
* Get validate policies for a table and operation
|
|
276
271
|
*/
|
|
277
272
|
getValidates(table: string, operation: Operation): CompiledPolicy[] {
|
|
278
|
-
const config = this.tables.get(table)
|
|
279
|
-
if (!config) return []
|
|
273
|
+
const config = this.tables.get(table)
|
|
274
|
+
if (!config) return []
|
|
280
275
|
|
|
281
276
|
return config.validates
|
|
282
277
|
.filter(p => p.operations.has(operation))
|
|
283
|
-
.map(p => this.toCompiledPolicy(p))
|
|
278
|
+
.map(p => this.toCompiledPolicy(p))
|
|
284
279
|
}
|
|
285
280
|
|
|
286
281
|
/**
|
|
287
282
|
* Get filter policies for a table
|
|
288
283
|
*/
|
|
289
284
|
getFilters(table: string): CompiledFilterPolicy[] {
|
|
290
|
-
const config = this.tables.get(table)
|
|
291
|
-
return config?.filters ?? []
|
|
285
|
+
const config = this.tables.get(table)
|
|
286
|
+
return config?.filters ?? []
|
|
292
287
|
}
|
|
293
288
|
|
|
294
289
|
/**
|
|
295
290
|
* Get roles that skip RLS for a table
|
|
296
291
|
*/
|
|
297
292
|
getSkipFor(table: string): string[] {
|
|
298
|
-
const config = this.tables.get(table)
|
|
299
|
-
return config?.skipFor ?? []
|
|
293
|
+
const config = this.tables.get(table)
|
|
294
|
+
return config?.skipFor ?? []
|
|
300
295
|
}
|
|
301
296
|
|
|
302
297
|
/**
|
|
303
298
|
* Check if table has default deny
|
|
304
299
|
*/
|
|
305
300
|
hasDefaultDeny(table: string): boolean {
|
|
306
|
-
const config = this.tables.get(table)
|
|
307
|
-
return config?.defaultDeny ?? true
|
|
301
|
+
const config = this.tables.get(table)
|
|
302
|
+
return config?.defaultDeny ?? true
|
|
308
303
|
}
|
|
309
304
|
|
|
310
305
|
/**
|
|
311
306
|
* Check if a table is registered
|
|
312
307
|
*/
|
|
313
308
|
hasTable(table: string): boolean {
|
|
314
|
-
return this.tables.has(table)
|
|
309
|
+
return this.tables.has(table)
|
|
315
310
|
}
|
|
316
311
|
|
|
317
312
|
/**
|
|
318
313
|
* Get all registered table names
|
|
319
314
|
*/
|
|
320
315
|
getTables(): string[] {
|
|
321
|
-
return Array.from(this.tables.keys())
|
|
316
|
+
return Array.from(this.tables.keys())
|
|
322
317
|
}
|
|
323
318
|
|
|
324
319
|
/**
|
|
325
320
|
* Check if registry is compiled
|
|
326
321
|
*/
|
|
327
322
|
isCompiled(): boolean {
|
|
328
|
-
return this.compiled
|
|
323
|
+
return this.compiled
|
|
329
324
|
}
|
|
330
325
|
|
|
331
326
|
/**
|
|
@@ -342,45 +337,45 @@ export class PolicyRegistry<DB = unknown> {
|
|
|
342
337
|
config.allows.length > 0 ||
|
|
343
338
|
config.denies.length > 0 ||
|
|
344
339
|
config.filters.length > 0 ||
|
|
345
|
-
config.validates.length > 0
|
|
340
|
+
config.validates.length > 0
|
|
346
341
|
|
|
347
342
|
if (!hasPolicy && !config.defaultDeny) {
|
|
348
343
|
// Warning: table has no policies and defaultDeny is false
|
|
349
344
|
this.logger.warn?.(
|
|
350
345
|
`[RLS] Table "${table}" has no policies and defaultDeny is false. ` +
|
|
351
346
|
`All operations will be allowed.`
|
|
352
|
-
)
|
|
347
|
+
)
|
|
353
348
|
}
|
|
354
349
|
|
|
355
350
|
// Warn if skipFor includes operations that have policies
|
|
356
351
|
if (config.skipFor.length > 0) {
|
|
357
|
-
const opsWithPolicies = new Set<Operation>()
|
|
352
|
+
const opsWithPolicies = new Set<Operation>()
|
|
358
353
|
|
|
359
354
|
for (const allow of config.allows) {
|
|
360
|
-
allow.operations.forEach(op => opsWithPolicies.add(op))
|
|
355
|
+
allow.operations.forEach(op => opsWithPolicies.add(op))
|
|
361
356
|
}
|
|
362
357
|
for (const deny of config.denies) {
|
|
363
|
-
deny.operations.forEach(op => opsWithPolicies.add(op))
|
|
358
|
+
deny.operations.forEach(op => opsWithPolicies.add(op))
|
|
364
359
|
}
|
|
365
360
|
for (const validate of config.validates) {
|
|
366
|
-
validate.operations.forEach(op => opsWithPolicies.add(op))
|
|
361
|
+
validate.operations.forEach(op => opsWithPolicies.add(op))
|
|
367
362
|
}
|
|
368
363
|
if (config.filters.length > 0) {
|
|
369
|
-
opsWithPolicies.add('read')
|
|
364
|
+
opsWithPolicies.add('read')
|
|
370
365
|
}
|
|
371
366
|
|
|
372
367
|
const skippedOpsWithPolicies = config.skipFor.filter(op => {
|
|
373
368
|
// 'all' means skip all operations
|
|
374
|
-
if (op === 'all') return opsWithPolicies.size > 0
|
|
369
|
+
if (op === 'all') return opsWithPolicies.size > 0
|
|
375
370
|
// Check if this is an operation name (for backwards compatibility)
|
|
376
|
-
return opsWithPolicies.has(op as Operation)
|
|
377
|
-
})
|
|
371
|
+
return opsWithPolicies.has(op as Operation)
|
|
372
|
+
})
|
|
378
373
|
|
|
379
374
|
if (skippedOpsWithPolicies.length > 0) {
|
|
380
375
|
this.logger.warn?.(
|
|
381
376
|
`[RLS] Table "${table}" has skipFor operations that also have policies: ${skippedOpsWithPolicies.join(', ')}. ` +
|
|
382
377
|
`The policies will be ignored for these operations.`
|
|
383
|
-
)
|
|
378
|
+
)
|
|
384
379
|
}
|
|
385
380
|
}
|
|
386
381
|
}
|
|
@@ -390,14 +385,14 @@ export class PolicyRegistry<DB = unknown> {
|
|
|
390
385
|
* Clear all policies
|
|
391
386
|
*/
|
|
392
387
|
clear(): void {
|
|
393
|
-
this.tables.clear()
|
|
394
|
-
this.compiled = false
|
|
388
|
+
this.tables.clear()
|
|
389
|
+
this.compiled = false
|
|
395
390
|
}
|
|
396
391
|
|
|
397
392
|
/**
|
|
398
393
|
* Remove policies for a specific table
|
|
399
394
|
*/
|
|
400
395
|
remove(table: string): void {
|
|
401
|
-
this.tables.delete(table)
|
|
396
|
+
this.tables.delete(table)
|
|
402
397
|
}
|
|
403
398
|
}
|