toride 0.0.1 → 0.2.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/dist/chunk-24PMDTLE.js +2366 -0
- package/dist/chunk-475CNU63.js +37 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +224 -0
- package/dist/client-RqwW0K_-.d.ts +368 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +8 -0
- package/dist/index.d.ts +309 -0
- package/dist/index.js +246 -0
- package/package.json +47 -4
- package/index.js +0 -1
|
@@ -0,0 +1,2366 @@
|
|
|
1
|
+
// src/policy/schema.ts
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
var AttributeTypeSchema = v.picklist(["string", "number", "boolean"]);
|
|
4
|
+
var ActorDeclarationSchema = v.object({
|
|
5
|
+
attributes: v.record(v.string(), AttributeTypeSchema)
|
|
6
|
+
});
|
|
7
|
+
var ConditionOperatorSchema = v.union([
|
|
8
|
+
v.object({ eq: v.unknown() }),
|
|
9
|
+
v.object({ neq: v.unknown() }),
|
|
10
|
+
v.object({ gt: v.unknown() }),
|
|
11
|
+
v.object({ gte: v.unknown() }),
|
|
12
|
+
v.object({ lt: v.unknown() }),
|
|
13
|
+
v.object({ lte: v.unknown() }),
|
|
14
|
+
v.object({ in: v.union([v.array(v.unknown()), v.string()]) }),
|
|
15
|
+
v.object({ includes: v.unknown() }),
|
|
16
|
+
v.object({ exists: v.boolean() }),
|
|
17
|
+
v.object({ startsWith: v.string() }),
|
|
18
|
+
v.object({ endsWith: v.string() }),
|
|
19
|
+
v.object({ contains: v.string() }),
|
|
20
|
+
v.object({ custom: v.string() })
|
|
21
|
+
]);
|
|
22
|
+
var ConditionValueSchema = v.union([
|
|
23
|
+
v.string(),
|
|
24
|
+
v.number(),
|
|
25
|
+
v.boolean(),
|
|
26
|
+
ConditionOperatorSchema
|
|
27
|
+
]);
|
|
28
|
+
var SimpleConditionsSchema = v.record(v.string(), ConditionValueSchema);
|
|
29
|
+
var ConditionExpressionSchema = v.union([
|
|
30
|
+
v.object({ any: v.array(v.lazy(() => ConditionExpressionSchema)) }),
|
|
31
|
+
v.object({ all: v.array(v.lazy(() => ConditionExpressionSchema)) }),
|
|
32
|
+
SimpleConditionsSchema
|
|
33
|
+
]);
|
|
34
|
+
var RelationDefSchema = v.string();
|
|
35
|
+
var DerivedRoleEntrySchema = v.object({
|
|
36
|
+
role: v.string(),
|
|
37
|
+
from_global_role: v.optional(v.string()),
|
|
38
|
+
from_role: v.optional(v.string()),
|
|
39
|
+
on_relation: v.optional(v.string()),
|
|
40
|
+
from_relation: v.optional(v.string()),
|
|
41
|
+
actor_type: v.optional(v.string()),
|
|
42
|
+
when: v.optional(ConditionExpressionSchema)
|
|
43
|
+
});
|
|
44
|
+
var RuleSchema = v.object({
|
|
45
|
+
effect: v.picklist(["permit", "forbid"]),
|
|
46
|
+
roles: v.optional(v.array(v.string())),
|
|
47
|
+
permissions: v.array(v.string()),
|
|
48
|
+
when: ConditionExpressionSchema
|
|
49
|
+
});
|
|
50
|
+
var FieldAccessDefSchema = v.object({
|
|
51
|
+
read: v.optional(v.array(v.string())),
|
|
52
|
+
update: v.optional(v.array(v.string()))
|
|
53
|
+
});
|
|
54
|
+
var ResourceBlockSchema = v.object({
|
|
55
|
+
roles: v.array(v.string()),
|
|
56
|
+
permissions: v.array(v.string()),
|
|
57
|
+
attributes: v.optional(v.record(v.string(), AttributeTypeSchema)),
|
|
58
|
+
relations: v.optional(v.record(v.string(), v.string())),
|
|
59
|
+
grants: v.optional(v.record(v.string(), v.array(v.string()))),
|
|
60
|
+
derived_roles: v.optional(v.array(DerivedRoleEntrySchema)),
|
|
61
|
+
rules: v.optional(v.array(RuleSchema)),
|
|
62
|
+
field_access: v.optional(v.record(v.string(), FieldAccessDefSchema))
|
|
63
|
+
});
|
|
64
|
+
var GlobalRoleSchema = v.object({
|
|
65
|
+
actor_type: v.string(),
|
|
66
|
+
when: ConditionExpressionSchema
|
|
67
|
+
});
|
|
68
|
+
var ResourceRefSchema = v.object({
|
|
69
|
+
type: v.string(),
|
|
70
|
+
id: v.string(),
|
|
71
|
+
attributes: v.optional(v.record(v.string(), v.unknown()))
|
|
72
|
+
});
|
|
73
|
+
var ActorRefSchema = v.object({
|
|
74
|
+
type: v.string(),
|
|
75
|
+
id: v.string(),
|
|
76
|
+
attributes: v.record(v.string(), v.unknown())
|
|
77
|
+
});
|
|
78
|
+
var TestCaseSchema = v.object({
|
|
79
|
+
name: v.string(),
|
|
80
|
+
actor: ActorRefSchema,
|
|
81
|
+
resolvers: v.optional(v.record(v.string(), v.record(v.string(), v.unknown()))),
|
|
82
|
+
action: v.string(),
|
|
83
|
+
resource: ResourceRefSchema,
|
|
84
|
+
expected: v.picklist(["allow", "deny"])
|
|
85
|
+
});
|
|
86
|
+
var PolicySchema = v.object({
|
|
87
|
+
version: v.literal("1"),
|
|
88
|
+
actors: v.record(v.string(), ActorDeclarationSchema),
|
|
89
|
+
global_roles: v.optional(v.record(v.string(), GlobalRoleSchema)),
|
|
90
|
+
resources: v.record(v.string(), ResourceBlockSchema),
|
|
91
|
+
tests: v.optional(v.array(TestCaseSchema))
|
|
92
|
+
});
|
|
93
|
+
var TestFileSchema = v.object({
|
|
94
|
+
policy: v.string(),
|
|
95
|
+
tests: v.array(TestCaseSchema)
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// src/types.ts
|
|
99
|
+
var ValidationError = class extends Error {
|
|
100
|
+
path;
|
|
101
|
+
constructor(message, path) {
|
|
102
|
+
super(message);
|
|
103
|
+
this.name = "ValidationError";
|
|
104
|
+
this.path = path;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var CycleError = class extends Error {
|
|
108
|
+
path;
|
|
109
|
+
constructor(message, path) {
|
|
110
|
+
super(message);
|
|
111
|
+
this.name = "CycleError";
|
|
112
|
+
this.path = path;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var DepthLimitError = class extends Error {
|
|
116
|
+
limit;
|
|
117
|
+
limitType;
|
|
118
|
+
constructor(message, limit, limitType) {
|
|
119
|
+
super(message);
|
|
120
|
+
this.name = "DepthLimitError";
|
|
121
|
+
this.limit = limit;
|
|
122
|
+
this.limitType = limitType;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// src/policy/validator.ts
|
|
127
|
+
function extractActorAttributes(condition) {
|
|
128
|
+
const attrs = [];
|
|
129
|
+
if ("any" in condition && Array.isArray(condition.any)) {
|
|
130
|
+
for (const sub of condition.any) {
|
|
131
|
+
attrs.push(...extractActorAttributes(sub));
|
|
132
|
+
}
|
|
133
|
+
return attrs;
|
|
134
|
+
}
|
|
135
|
+
if ("all" in condition && Array.isArray(condition.all)) {
|
|
136
|
+
for (const sub of condition.all) {
|
|
137
|
+
attrs.push(...extractActorAttributes(sub));
|
|
138
|
+
}
|
|
139
|
+
return attrs;
|
|
140
|
+
}
|
|
141
|
+
for (const key of Object.keys(condition)) {
|
|
142
|
+
if (key.startsWith("$actor.")) {
|
|
143
|
+
const attrName = key.slice("$actor.".length);
|
|
144
|
+
const topLevel = attrName.split(".")[0];
|
|
145
|
+
attrs.push(topLevel);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return attrs;
|
|
149
|
+
}
|
|
150
|
+
function validateActorAttributeRefs(condition, actorTypes, path, errors) {
|
|
151
|
+
const referencedAttrs = extractActorAttributes(condition);
|
|
152
|
+
if (referencedAttrs.length === 0) return;
|
|
153
|
+
for (const attr of referencedAttrs) {
|
|
154
|
+
for (const [typeName, decl] of actorTypes) {
|
|
155
|
+
if (!(attr in decl.attributes)) {
|
|
156
|
+
errors.push({
|
|
157
|
+
message: `${path} references $actor.${attr} which is not declared in actor type "${typeName}"`,
|
|
158
|
+
path
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function collectErrors(policy) {
|
|
165
|
+
const errors = [];
|
|
166
|
+
const declaredActorTypes = new Set(Object.keys(policy.actors));
|
|
167
|
+
const declaredGlobalRoles = new Set(
|
|
168
|
+
policy.global_roles ? Object.keys(policy.global_roles) : []
|
|
169
|
+
);
|
|
170
|
+
const declaredResourceTypes = new Set(Object.keys(policy.resources));
|
|
171
|
+
if (policy.global_roles) {
|
|
172
|
+
for (const [roleName, globalRole] of Object.entries(policy.global_roles)) {
|
|
173
|
+
if (!declaredActorTypes.has(globalRole.actor_type)) {
|
|
174
|
+
errors.push({
|
|
175
|
+
message: `global_roles.${roleName}.actor_type references undeclared actor type "${globalRole.actor_type}"`,
|
|
176
|
+
path: `global_roles.${roleName}.actor_type`
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
const actorDecl = policy.actors[globalRole.actor_type];
|
|
180
|
+
validateActorAttributeRefs(
|
|
181
|
+
globalRole.when,
|
|
182
|
+
/* @__PURE__ */ new Map([[globalRole.actor_type, actorDecl]]),
|
|
183
|
+
`global_roles.${roleName}.when`,
|
|
184
|
+
errors
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const [resName, resource] of Object.entries(policy.resources)) {
|
|
190
|
+
const declaredRoles = new Set(resource.roles);
|
|
191
|
+
const declaredPermissions = new Set(resource.permissions);
|
|
192
|
+
const declaredRelations = new Set(
|
|
193
|
+
resource.relations ? Object.keys(resource.relations) : []
|
|
194
|
+
);
|
|
195
|
+
if (resource.relations) {
|
|
196
|
+
for (const [relName, relDef] of Object.entries(resource.relations)) {
|
|
197
|
+
if (!declaredResourceTypes.has(relDef) && !declaredActorTypes.has(relDef)) {
|
|
198
|
+
errors.push({
|
|
199
|
+
message: `resources.${resName}.relations.${relName} references undeclared resource type "${relDef}"`,
|
|
200
|
+
path: `resources.${resName}.relations.${relName}`
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (resource.grants) {
|
|
206
|
+
for (const [roleName, permissions] of Object.entries(resource.grants)) {
|
|
207
|
+
if (!declaredRoles.has(roleName)) {
|
|
208
|
+
errors.push({
|
|
209
|
+
message: `resources.${resName}.grants references undeclared role "${roleName}"`,
|
|
210
|
+
path: `resources.${resName}.grants.${roleName}`
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
for (const perm of permissions) {
|
|
214
|
+
if (perm !== "all" && !declaredPermissions.has(perm)) {
|
|
215
|
+
errors.push({
|
|
216
|
+
message: `resources.${resName}.grants.${roleName} references undeclared permission "${perm}"`,
|
|
217
|
+
path: `resources.${resName}.grants.${roleName}`
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (resource.derived_roles) {
|
|
224
|
+
for (let i = 0; i < resource.derived_roles.length; i++) {
|
|
225
|
+
const entry = resource.derived_roles[i];
|
|
226
|
+
const path = `resources.${resName}.derived_roles[${i}]`;
|
|
227
|
+
if (!declaredRoles.has(entry.role)) {
|
|
228
|
+
errors.push({
|
|
229
|
+
message: `${path} references undeclared role "${entry.role}"`,
|
|
230
|
+
path
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const patternErrors = validateDerivedRolePatternsCollect(entry, path);
|
|
234
|
+
errors.push(...patternErrors);
|
|
235
|
+
if (entry.from_global_role !== void 0) {
|
|
236
|
+
if (!declaredGlobalRoles.has(entry.from_global_role)) {
|
|
237
|
+
errors.push({
|
|
238
|
+
message: `${path} references undeclared global role "${entry.from_global_role}"`,
|
|
239
|
+
path
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (entry.on_relation !== void 0) {
|
|
244
|
+
if (!declaredRelations.has(entry.on_relation)) {
|
|
245
|
+
errors.push({
|
|
246
|
+
message: `${path} references undeclared relation "${entry.on_relation}"`,
|
|
247
|
+
path
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (entry.from_relation !== void 0) {
|
|
252
|
+
if (!declaredRelations.has(entry.from_relation)) {
|
|
253
|
+
errors.push({
|
|
254
|
+
message: `${path} references undeclared relation "${entry.from_relation}"`,
|
|
255
|
+
path
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (entry.actor_type !== void 0) {
|
|
260
|
+
if (!declaredActorTypes.has(entry.actor_type)) {
|
|
261
|
+
errors.push({
|
|
262
|
+
message: `${path} references undeclared actor type "${entry.actor_type}"`,
|
|
263
|
+
path
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (entry.when) {
|
|
268
|
+
if (entry.actor_type && declaredActorTypes.has(entry.actor_type)) {
|
|
269
|
+
const actorDecl = policy.actors[entry.actor_type];
|
|
270
|
+
validateActorAttributeRefs(
|
|
271
|
+
entry.when,
|
|
272
|
+
/* @__PURE__ */ new Map([[entry.actor_type, actorDecl]]),
|
|
273
|
+
`${path}.when`,
|
|
274
|
+
errors
|
|
275
|
+
);
|
|
276
|
+
} else if (!entry.actor_type) {
|
|
277
|
+
const allActors = new Map(
|
|
278
|
+
Object.entries(policy.actors)
|
|
279
|
+
);
|
|
280
|
+
validateActorAttributeRefs(
|
|
281
|
+
entry.when,
|
|
282
|
+
allActors,
|
|
283
|
+
`${path}.when`,
|
|
284
|
+
errors
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (resource.rules) {
|
|
291
|
+
for (let i = 0; i < resource.rules.length; i++) {
|
|
292
|
+
const rule = resource.rules[i];
|
|
293
|
+
const path = `resources.${resName}.rules[${i}]`;
|
|
294
|
+
for (const perm of rule.permissions) {
|
|
295
|
+
if (!declaredPermissions.has(perm)) {
|
|
296
|
+
errors.push({
|
|
297
|
+
message: `${path}.permissions references undeclared permission "${perm}"`,
|
|
298
|
+
path: `${path}.permissions`
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (rule.roles) {
|
|
303
|
+
for (const roleName of rule.roles) {
|
|
304
|
+
if (!declaredRoles.has(roleName)) {
|
|
305
|
+
errors.push({
|
|
306
|
+
message: `${path}.roles references undeclared role "${roleName}"`,
|
|
307
|
+
path: `${path}.roles`
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const allActors = new Map(Object.entries(policy.actors));
|
|
313
|
+
validateActorAttributeRefs(
|
|
314
|
+
rule.when,
|
|
315
|
+
allActors,
|
|
316
|
+
`${path}.when`,
|
|
317
|
+
errors
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (resource.field_access) {
|
|
322
|
+
for (const [fieldName, fieldDef] of Object.entries(
|
|
323
|
+
resource.field_access
|
|
324
|
+
)) {
|
|
325
|
+
const fpath = `resources.${resName}.field_access.${fieldName}`;
|
|
326
|
+
if (fieldDef.read) {
|
|
327
|
+
for (const roleName of fieldDef.read) {
|
|
328
|
+
if (!declaredRoles.has(roleName)) {
|
|
329
|
+
errors.push({
|
|
330
|
+
message: `${fpath}.read references undeclared role "${roleName}"`,
|
|
331
|
+
path: `${fpath}.read`
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (fieldDef.update) {
|
|
337
|
+
for (const roleName of fieldDef.update) {
|
|
338
|
+
if (!declaredRoles.has(roleName)) {
|
|
339
|
+
errors.push({
|
|
340
|
+
message: `${fpath}.update references undeclared role "${roleName}"`,
|
|
341
|
+
path: `${fpath}.update`
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return errors;
|
|
350
|
+
}
|
|
351
|
+
function validateDerivedRolePatternsCollect(entry, path) {
|
|
352
|
+
const errors = [];
|
|
353
|
+
const hasGlobal = entry.from_global_role !== void 0;
|
|
354
|
+
const hasFromRole = entry.from_role !== void 0;
|
|
355
|
+
const hasOnRelation = entry.on_relation !== void 0;
|
|
356
|
+
const hasFromRelation = entry.from_relation !== void 0;
|
|
357
|
+
const hasActorType = entry.actor_type !== void 0;
|
|
358
|
+
const hasWhen = entry.when !== void 0;
|
|
359
|
+
if (hasFromRole !== hasOnRelation) {
|
|
360
|
+
errors.push({
|
|
361
|
+
message: `${path}: from_role and on_relation must be specified together`,
|
|
362
|
+
path
|
|
363
|
+
});
|
|
364
|
+
return errors;
|
|
365
|
+
}
|
|
366
|
+
const patternCount = [
|
|
367
|
+
hasGlobal,
|
|
368
|
+
hasFromRole && hasOnRelation,
|
|
369
|
+
hasFromRelation
|
|
370
|
+
].filter(Boolean).length;
|
|
371
|
+
if (patternCount > 1) {
|
|
372
|
+
errors.push({
|
|
373
|
+
message: `${path}: derived role entry specifies conflicting derivation patterns`,
|
|
374
|
+
path
|
|
375
|
+
});
|
|
376
|
+
return errors;
|
|
377
|
+
}
|
|
378
|
+
if (patternCount === 1) {
|
|
379
|
+
if (hasGlobal && (hasActorType || hasWhen || hasFromRelation || hasFromRole)) {
|
|
380
|
+
errors.push({
|
|
381
|
+
message: `${path}: from_global_role cannot be combined with other derivation fields`,
|
|
382
|
+
path
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
if (hasFromRelation && (hasActorType || hasWhen || hasGlobal || hasFromRole)) {
|
|
386
|
+
errors.push({
|
|
387
|
+
message: `${path}: from_relation cannot be combined with other derivation fields`,
|
|
388
|
+
path
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return errors;
|
|
392
|
+
}
|
|
393
|
+
if (!hasWhen && !hasActorType) {
|
|
394
|
+
errors.push({
|
|
395
|
+
message: `${path}: derived role entry must specify a derivation pattern (from_global_role, from_role+on_relation, from_relation, or when)`,
|
|
396
|
+
path
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return errors;
|
|
400
|
+
}
|
|
401
|
+
function collectWarnings(policy) {
|
|
402
|
+
const warnings = [];
|
|
403
|
+
for (const [resName, resource] of Object.entries(policy.resources)) {
|
|
404
|
+
const declaredRoles = new Set(resource.roles);
|
|
405
|
+
const usedRoles = /* @__PURE__ */ new Set();
|
|
406
|
+
if (resource.grants) {
|
|
407
|
+
for (const roleName of Object.keys(resource.grants)) {
|
|
408
|
+
usedRoles.add(roleName);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (resource.derived_roles) {
|
|
412
|
+
for (const entry of resource.derived_roles) {
|
|
413
|
+
usedRoles.add(entry.role);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (resource.rules) {
|
|
417
|
+
for (const rule of resource.rules) {
|
|
418
|
+
if (rule.roles) {
|
|
419
|
+
for (const roleName of rule.roles) {
|
|
420
|
+
usedRoles.add(roleName);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (resource.field_access) {
|
|
426
|
+
for (const fieldDef of Object.values(resource.field_access)) {
|
|
427
|
+
if (fieldDef.read) {
|
|
428
|
+
for (const roleName of fieldDef.read) {
|
|
429
|
+
usedRoles.add(roleName);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (fieldDef.update) {
|
|
433
|
+
for (const roleName of fieldDef.update) {
|
|
434
|
+
usedRoles.add(roleName);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
for (const role of declaredRoles) {
|
|
440
|
+
if (!usedRoles.has(role)) {
|
|
441
|
+
warnings.push({
|
|
442
|
+
message: `resources.${resName}.roles includes "${role}" which is never used in grants, derived_roles, rules, or field_access`,
|
|
443
|
+
path: `resources.${resName}.roles`
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (resource.rules && resource.grants) {
|
|
448
|
+
const grantedPermissions = /* @__PURE__ */ new Set();
|
|
449
|
+
for (const perms of Object.values(resource.grants)) {
|
|
450
|
+
for (const perm of perms) {
|
|
451
|
+
if (perm === "all") {
|
|
452
|
+
for (const p of resource.permissions) {
|
|
453
|
+
grantedPermissions.add(p);
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
grantedPermissions.add(perm);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (let i = 0; i < resource.rules.length; i++) {
|
|
461
|
+
const rule = resource.rules[i];
|
|
462
|
+
const path = `resources.${resName}.rules[${i}]`;
|
|
463
|
+
if (rule.effect === "forbid") {
|
|
464
|
+
const unreachablePerms = rule.permissions.filter(
|
|
465
|
+
(p) => !grantedPermissions.has(p)
|
|
466
|
+
);
|
|
467
|
+
if (unreachablePerms.length === rule.permissions.length) {
|
|
468
|
+
warnings.push({
|
|
469
|
+
message: `${path} is unreachable: none of its permissions [${unreachablePerms.join(", ")}] are granted to any role`,
|
|
470
|
+
path
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (resource.derived_roles && resource.derived_roles.length > 1) {
|
|
477
|
+
const seen = /* @__PURE__ */ new Set();
|
|
478
|
+
for (let i = 0; i < resource.derived_roles.length; i++) {
|
|
479
|
+
const entry = resource.derived_roles[i];
|
|
480
|
+
const key = JSON.stringify({
|
|
481
|
+
role: entry.role,
|
|
482
|
+
from_global_role: entry.from_global_role,
|
|
483
|
+
from_role: entry.from_role,
|
|
484
|
+
on_relation: entry.on_relation,
|
|
485
|
+
from_relation: entry.from_relation,
|
|
486
|
+
actor_type: entry.actor_type,
|
|
487
|
+
when: entry.when
|
|
488
|
+
});
|
|
489
|
+
if (seen.has(key)) {
|
|
490
|
+
warnings.push({
|
|
491
|
+
message: `resources.${resName}.derived_roles[${i}] is a duplicate (redundant) derivation entry`,
|
|
492
|
+
path: `resources.${resName}.derived_roles[${i}]`
|
|
493
|
+
});
|
|
494
|
+
} else {
|
|
495
|
+
seen.add(key);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return warnings;
|
|
501
|
+
}
|
|
502
|
+
function validatePolicy(policy) {
|
|
503
|
+
const errors = collectErrors(policy);
|
|
504
|
+
if (errors.length > 0) {
|
|
505
|
+
const messages = errors.map((e) => e.message);
|
|
506
|
+
const firstError = errors[0];
|
|
507
|
+
throw new ValidationError(
|
|
508
|
+
messages.length === 1 ? messages[0] : messages.join("; "),
|
|
509
|
+
firstError.path
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function validatePolicyResult(policy) {
|
|
514
|
+
return { errors: collectErrors(policy) };
|
|
515
|
+
}
|
|
516
|
+
function validatePolicyStrict(policy) {
|
|
517
|
+
const errors = collectErrors(policy);
|
|
518
|
+
const warnings = collectWarnings(policy);
|
|
519
|
+
return { errors, warnings };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/testing/test-parser.ts
|
|
523
|
+
import * as YAML from "yaml";
|
|
524
|
+
import * as v2 from "valibot";
|
|
525
|
+
function parseInlineTests(policy) {
|
|
526
|
+
return {
|
|
527
|
+
policy,
|
|
528
|
+
tests: policy.tests ?? []
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function parseTestFile(yamlContent) {
|
|
532
|
+
let raw;
|
|
533
|
+
try {
|
|
534
|
+
raw = YAML.parse(yamlContent, { prettyErrors: true });
|
|
535
|
+
} catch (err) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`YAML parse error: ${err instanceof Error ? err.message : String(err)}`
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
const result = v2.safeParse(TestFileSchema, raw);
|
|
541
|
+
if (!result.success) {
|
|
542
|
+
const issue = result.issues[0];
|
|
543
|
+
const path = issue?.path?.map((p) => String(p.key)).join(".") ?? "";
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Test file validation failed: ${issue?.message ?? "unknown error"}${path ? ` at ${path}` : ""}`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
policyPath: result.output.policy,
|
|
550
|
+
tests: result.output.tests
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/evaluation/condition.ts
|
|
555
|
+
var DEFAULT_MAX_CONDITION_DEPTH = 3;
|
|
556
|
+
var DEFAULT_MAX_COMBINATOR_DEPTH = 10;
|
|
557
|
+
var OPERATOR_KEYS = /* @__PURE__ */ new Set([
|
|
558
|
+
"eq",
|
|
559
|
+
"neq",
|
|
560
|
+
"gt",
|
|
561
|
+
"gte",
|
|
562
|
+
"lt",
|
|
563
|
+
"lte",
|
|
564
|
+
"in",
|
|
565
|
+
"includes",
|
|
566
|
+
"exists",
|
|
567
|
+
"startsWith",
|
|
568
|
+
"endsWith",
|
|
569
|
+
"contains",
|
|
570
|
+
"custom"
|
|
571
|
+
]);
|
|
572
|
+
var UNDEFINED_SENTINEL = /* @__PURE__ */ Symbol("UNDEFINED");
|
|
573
|
+
async function evaluateCondition(condition, actor, resource, cache, env, resourceBlock, policy, options, combinatorDepth = 0) {
|
|
574
|
+
const maxDepth = options?.maxConditionDepth ?? DEFAULT_MAX_CONDITION_DEPTH;
|
|
575
|
+
const maxCombinatorDepth = options?.maxCombinatorDepth ?? DEFAULT_MAX_COMBINATOR_DEPTH;
|
|
576
|
+
if ("any" in condition && Array.isArray(condition.any)) {
|
|
577
|
+
if (combinatorDepth >= maxCombinatorDepth) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
const items = condition.any;
|
|
581
|
+
for (const item of items) {
|
|
582
|
+
if (await evaluateCondition(item, actor, resource, cache, env, resourceBlock, policy, options, combinatorDepth + 1)) {
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
if ("all" in condition && Array.isArray(condition.all)) {
|
|
589
|
+
if (combinatorDepth >= maxCombinatorDepth) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
const items = condition.all;
|
|
593
|
+
for (const item of items) {
|
|
594
|
+
if (!await evaluateCondition(item, actor, resource, cache, env, resourceBlock, policy, options, combinatorDepth + 1)) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
const entries = Object.entries(condition);
|
|
601
|
+
for (const [key, conditionValue] of entries) {
|
|
602
|
+
const matched = await evaluatePair(
|
|
603
|
+
key,
|
|
604
|
+
conditionValue,
|
|
605
|
+
actor,
|
|
606
|
+
resource,
|
|
607
|
+
cache,
|
|
608
|
+
env,
|
|
609
|
+
resourceBlock,
|
|
610
|
+
policy,
|
|
611
|
+
maxDepth,
|
|
612
|
+
options
|
|
613
|
+
);
|
|
614
|
+
if (!matched) return false;
|
|
615
|
+
}
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
async function evaluatePair(key, conditionValue, actor, resource, cache, env, resourceBlock, policy, maxDepth, options) {
|
|
619
|
+
const leftValue = await resolveValue(
|
|
620
|
+
key,
|
|
621
|
+
actor,
|
|
622
|
+
resource,
|
|
623
|
+
cache,
|
|
624
|
+
env,
|
|
625
|
+
resourceBlock,
|
|
626
|
+
policy,
|
|
627
|
+
maxDepth
|
|
628
|
+
);
|
|
629
|
+
if (isOperator(conditionValue)) {
|
|
630
|
+
return evaluateOperator(
|
|
631
|
+
leftValue,
|
|
632
|
+
conditionValue,
|
|
633
|
+
actor,
|
|
634
|
+
resource,
|
|
635
|
+
cache,
|
|
636
|
+
env,
|
|
637
|
+
resourceBlock,
|
|
638
|
+
policy,
|
|
639
|
+
maxDepth,
|
|
640
|
+
options
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
const rightValue = await resolveRightValue(
|
|
644
|
+
conditionValue,
|
|
645
|
+
actor,
|
|
646
|
+
resource,
|
|
647
|
+
cache,
|
|
648
|
+
env,
|
|
649
|
+
resourceBlock,
|
|
650
|
+
policy,
|
|
651
|
+
maxDepth
|
|
652
|
+
);
|
|
653
|
+
if (Array.isArray(leftValue)) {
|
|
654
|
+
if (rightValue === UNDEFINED_SENTINEL || rightValue === null || rightValue === void 0) {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
return leftValue.some((v3) => {
|
|
658
|
+
if (v3 === UNDEFINED_SENTINEL || v3 === null || v3 === void 0) return false;
|
|
659
|
+
return v3 === rightValue;
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
if (leftValue === UNDEFINED_SENTINEL || leftValue === null || leftValue === void 0) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
if (rightValue === UNDEFINED_SENTINEL || rightValue === null || rightValue === void 0) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
return leftValue === rightValue;
|
|
669
|
+
}
|
|
670
|
+
async function resolveValue(path, actor, resource, cache, env, resourceBlock, policy, maxDepth) {
|
|
671
|
+
if (path.startsWith("$actor.")) {
|
|
672
|
+
const attrPath = path.slice(7);
|
|
673
|
+
return getNestedAttribute(actor.attributes, attrPath);
|
|
674
|
+
}
|
|
675
|
+
if (path.startsWith("$resource.")) {
|
|
676
|
+
const attrPath = path.slice(10);
|
|
677
|
+
return resolveResourcePath(
|
|
678
|
+
attrPath,
|
|
679
|
+
resource,
|
|
680
|
+
cache,
|
|
681
|
+
resourceBlock,
|
|
682
|
+
policy,
|
|
683
|
+
maxDepth
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (path.startsWith("$env.")) {
|
|
687
|
+
const attrName = path.slice(5);
|
|
688
|
+
const val = env[attrName];
|
|
689
|
+
return val === void 0 ? UNDEFINED_SENTINEL : val;
|
|
690
|
+
}
|
|
691
|
+
return UNDEFINED_SENTINEL;
|
|
692
|
+
}
|
|
693
|
+
function isResourceRef(value) {
|
|
694
|
+
if (typeof value !== "object" || value === null) return false;
|
|
695
|
+
const obj = value;
|
|
696
|
+
return typeof obj.type === "string" && typeof obj.id === "string";
|
|
697
|
+
}
|
|
698
|
+
async function resolveResourcePath(path, resource, cache, resourceBlock, policy, depthRemaining) {
|
|
699
|
+
const parts = path.split(".");
|
|
700
|
+
if (parts.length === 1) {
|
|
701
|
+
try {
|
|
702
|
+
const attrs = await cache.resolve(resource, resourceBlock);
|
|
703
|
+
const val = attrs[parts[0]];
|
|
704
|
+
return val === void 0 ? UNDEFINED_SENTINEL : val;
|
|
705
|
+
} catch {
|
|
706
|
+
return UNDEFINED_SENTINEL;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (depthRemaining <= 0) {
|
|
710
|
+
return UNDEFINED_SENTINEL;
|
|
711
|
+
}
|
|
712
|
+
const relationName = parts[0];
|
|
713
|
+
const remainingPath = parts.slice(1).join(".");
|
|
714
|
+
const relationDef = resourceBlock.relations?.[relationName];
|
|
715
|
+
if (!relationDef) {
|
|
716
|
+
try {
|
|
717
|
+
const attrs = await cache.resolve(resource, resourceBlock);
|
|
718
|
+
const val = getNestedAttribute(attrs, path);
|
|
719
|
+
return val;
|
|
720
|
+
} catch {
|
|
721
|
+
return UNDEFINED_SENTINEL;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
const attrs = await cache.resolve(resource, resourceBlock);
|
|
726
|
+
const relValue = attrs[relationName];
|
|
727
|
+
if (relValue === null || relValue === void 0) {
|
|
728
|
+
return UNDEFINED_SENTINEL;
|
|
729
|
+
}
|
|
730
|
+
if (Array.isArray(relValue)) {
|
|
731
|
+
const results = [];
|
|
732
|
+
for (const item of relValue) {
|
|
733
|
+
if (!isResourceRef(item)) continue;
|
|
734
|
+
const relatedRef = item;
|
|
735
|
+
const relatedBlock = policy.resources[relatedRef.type] ?? {
|
|
736
|
+
roles: [],
|
|
737
|
+
permissions: []
|
|
738
|
+
};
|
|
739
|
+
const val = await resolveResourcePath(
|
|
740
|
+
remainingPath,
|
|
741
|
+
relatedRef,
|
|
742
|
+
cache,
|
|
743
|
+
relatedBlock,
|
|
744
|
+
policy,
|
|
745
|
+
depthRemaining - 1
|
|
746
|
+
);
|
|
747
|
+
results.push(val);
|
|
748
|
+
}
|
|
749
|
+
return results.length > 0 ? results : UNDEFINED_SENTINEL;
|
|
750
|
+
}
|
|
751
|
+
if (isResourceRef(relValue)) {
|
|
752
|
+
const relatedRef = relValue;
|
|
753
|
+
const relatedBlock = policy.resources[relatedRef.type] ?? {
|
|
754
|
+
roles: [],
|
|
755
|
+
permissions: []
|
|
756
|
+
};
|
|
757
|
+
return resolveResourcePath(
|
|
758
|
+
remainingPath,
|
|
759
|
+
relatedRef,
|
|
760
|
+
cache,
|
|
761
|
+
relatedBlock,
|
|
762
|
+
policy,
|
|
763
|
+
depthRemaining - 1
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
if (typeof relValue === "object") {
|
|
767
|
+
const val = getNestedAttribute(relValue, remainingPath);
|
|
768
|
+
return val;
|
|
769
|
+
}
|
|
770
|
+
return UNDEFINED_SENTINEL;
|
|
771
|
+
} catch {
|
|
772
|
+
return UNDEFINED_SENTINEL;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
var FORBIDDEN_PROPS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
776
|
+
function getNestedAttribute(obj, path) {
|
|
777
|
+
const parts = path.split(".");
|
|
778
|
+
let current = obj;
|
|
779
|
+
for (const part of parts) {
|
|
780
|
+
if (FORBIDDEN_PROPS.has(part)) return UNDEFINED_SENTINEL;
|
|
781
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
782
|
+
return UNDEFINED_SENTINEL;
|
|
783
|
+
}
|
|
784
|
+
current = current[part];
|
|
785
|
+
}
|
|
786
|
+
return current === void 0 ? UNDEFINED_SENTINEL : current;
|
|
787
|
+
}
|
|
788
|
+
async function resolveRightValue(value, actor, resource, cache, env, resourceBlock, policy, maxDepth) {
|
|
789
|
+
if (typeof value === "string" && isCrossReference(value)) {
|
|
790
|
+
return resolveValue(value, actor, resource, cache, env, resourceBlock, policy, maxDepth);
|
|
791
|
+
}
|
|
792
|
+
return value;
|
|
793
|
+
}
|
|
794
|
+
function isCrossReference(value) {
|
|
795
|
+
return value.startsWith("$actor.") || value.startsWith("$resource.") || value.startsWith("$env.");
|
|
796
|
+
}
|
|
797
|
+
function isOperator(value) {
|
|
798
|
+
if (typeof value !== "object" || value === null) return false;
|
|
799
|
+
const keys = Object.keys(value);
|
|
800
|
+
return keys.length === 1 && OPERATOR_KEYS.has(keys[0]);
|
|
801
|
+
}
|
|
802
|
+
async function evaluateOperator(leftValue, operator, actor, resource, cache, env, resourceBlock, policy, maxDepth, options) {
|
|
803
|
+
const op = operator;
|
|
804
|
+
const opKey = Object.keys(op)[0];
|
|
805
|
+
const opValue = op[opKey];
|
|
806
|
+
if (opKey === "custom") {
|
|
807
|
+
return evaluateCustom(
|
|
808
|
+
opValue,
|
|
809
|
+
actor,
|
|
810
|
+
resource,
|
|
811
|
+
env,
|
|
812
|
+
options
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
if (opKey === "exists") {
|
|
816
|
+
const exists = leftValue !== UNDEFINED_SENTINEL && leftValue !== void 0 && leftValue !== null;
|
|
817
|
+
return opValue === true ? exists : !exists;
|
|
818
|
+
}
|
|
819
|
+
const rightValue = await resolveRightValue(
|
|
820
|
+
opValue,
|
|
821
|
+
actor,
|
|
822
|
+
resource,
|
|
823
|
+
cache,
|
|
824
|
+
env,
|
|
825
|
+
resourceBlock,
|
|
826
|
+
policy,
|
|
827
|
+
maxDepth
|
|
828
|
+
);
|
|
829
|
+
if (Array.isArray(leftValue)) {
|
|
830
|
+
return evaluateAnySemantics(leftValue, opKey, rightValue);
|
|
831
|
+
}
|
|
832
|
+
if (leftValue === UNDEFINED_SENTINEL || leftValue === null || leftValue === void 0) {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
return evaluateOperatorPrimitive(leftValue, opKey, rightValue);
|
|
836
|
+
}
|
|
837
|
+
function evaluateAnySemantics(values, opKey, rightValue) {
|
|
838
|
+
if (opKey === "includes") {
|
|
839
|
+
return values.some((v3) => v3 === rightValue);
|
|
840
|
+
}
|
|
841
|
+
return values.some((v3) => {
|
|
842
|
+
if (v3 === UNDEFINED_SENTINEL || v3 === null || v3 === void 0) return false;
|
|
843
|
+
return evaluateOperatorPrimitive(v3, opKey, rightValue);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
function evaluateOperatorPrimitive(left, opKey, right) {
|
|
847
|
+
switch (opKey) {
|
|
848
|
+
case "eq":
|
|
849
|
+
if (right === UNDEFINED_SENTINEL || right === null || right === void 0) return false;
|
|
850
|
+
return left === right;
|
|
851
|
+
case "neq":
|
|
852
|
+
if (right === UNDEFINED_SENTINEL || right === null || right === void 0) return false;
|
|
853
|
+
return left !== right;
|
|
854
|
+
case "gt":
|
|
855
|
+
if (right === UNDEFINED_SENTINEL || right === null || right === void 0) return false;
|
|
856
|
+
return left > right;
|
|
857
|
+
case "gte":
|
|
858
|
+
if (right === UNDEFINED_SENTINEL || right === null || right === void 0) return false;
|
|
859
|
+
return left >= right;
|
|
860
|
+
case "lt":
|
|
861
|
+
if (right === UNDEFINED_SENTINEL || right === null || right === void 0) return false;
|
|
862
|
+
return left < right;
|
|
863
|
+
case "lte":
|
|
864
|
+
if (right === UNDEFINED_SENTINEL || right === null || right === void 0) return false;
|
|
865
|
+
return left <= right;
|
|
866
|
+
case "in": {
|
|
867
|
+
if (right === UNDEFINED_SENTINEL || right === null || right === void 0) return false;
|
|
868
|
+
if (Array.isArray(right)) {
|
|
869
|
+
return right.includes(left);
|
|
870
|
+
}
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
case "includes": {
|
|
874
|
+
if (right === UNDEFINED_SENTINEL || right === null || right === void 0) return false;
|
|
875
|
+
if (Array.isArray(left)) {
|
|
876
|
+
return left.includes(right);
|
|
877
|
+
}
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
case "startsWith":
|
|
881
|
+
if (typeof left !== "string" || typeof right !== "string") return false;
|
|
882
|
+
return left.startsWith(right);
|
|
883
|
+
case "endsWith":
|
|
884
|
+
if (typeof left !== "string" || typeof right !== "string") return false;
|
|
885
|
+
return left.endsWith(right);
|
|
886
|
+
case "contains":
|
|
887
|
+
if (typeof left !== "string" || typeof right !== "string") return false;
|
|
888
|
+
return left.includes(right);
|
|
889
|
+
default:
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
async function evaluateCustom(evaluatorName, actor, resource, env, options) {
|
|
894
|
+
const evaluator = options?.customEvaluators?.[evaluatorName];
|
|
895
|
+
if (!evaluator) {
|
|
896
|
+
return options?.ruleEffect === "forbid";
|
|
897
|
+
}
|
|
898
|
+
try {
|
|
899
|
+
return await evaluator(actor, resource, env);
|
|
900
|
+
} catch {
|
|
901
|
+
return options?.ruleEffect === "forbid";
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/evaluation/role-resolver.ts
|
|
906
|
+
var DEFAULT_MAX_DERIVED_ROLE_DEPTH = 5;
|
|
907
|
+
async function resolveRoles(actor, resource, cache, resourceBlock, policy, options) {
|
|
908
|
+
const maxDepth = options?.maxDerivedRoleDepth ?? DEFAULT_MAX_DERIVED_ROLE_DEPTH;
|
|
909
|
+
const direct = [];
|
|
910
|
+
const derived = [];
|
|
911
|
+
const derivedEntries = resourceBlock.derived_roles ?? [];
|
|
912
|
+
const visited = /* @__PURE__ */ new Set();
|
|
913
|
+
visited.add(`${resource.type}:${resource.id}`);
|
|
914
|
+
for (const entry of derivedEntries) {
|
|
915
|
+
try {
|
|
916
|
+
const traces = await evaluateDerivedRole(
|
|
917
|
+
entry,
|
|
918
|
+
actor,
|
|
919
|
+
resource,
|
|
920
|
+
cache,
|
|
921
|
+
resourceBlock,
|
|
922
|
+
policy,
|
|
923
|
+
maxDepth,
|
|
924
|
+
maxDepth,
|
|
925
|
+
visited
|
|
926
|
+
);
|
|
927
|
+
derived.push(...traces);
|
|
928
|
+
} catch (e) {
|
|
929
|
+
if (e instanceof CycleError || e instanceof DepthLimitError) {
|
|
930
|
+
throw e;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return { direct, derived };
|
|
935
|
+
}
|
|
936
|
+
async function evaluateDerivedRole(entry, actor, resource, cache, resourceBlock, policy, depthRemaining, maxDepth, visited) {
|
|
937
|
+
if (entry.from_global_role !== void 0) {
|
|
938
|
+
return evaluateGlobalRole(entry, actor, policy);
|
|
939
|
+
}
|
|
940
|
+
if (entry.from_role !== void 0 && entry.on_relation !== void 0) {
|
|
941
|
+
return evaluateRelationRole(
|
|
942
|
+
entry,
|
|
943
|
+
actor,
|
|
944
|
+
resource,
|
|
945
|
+
cache,
|
|
946
|
+
policy,
|
|
947
|
+
depthRemaining,
|
|
948
|
+
maxDepth,
|
|
949
|
+
visited
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
if (entry.from_relation !== void 0) {
|
|
953
|
+
return evaluateRelationIdentity(entry, actor, resource, cache);
|
|
954
|
+
}
|
|
955
|
+
if (entry.actor_type !== void 0 && entry.when !== void 0) {
|
|
956
|
+
return evaluateActorTypeCondition(entry, actor, resource, cache, resourceBlock, policy);
|
|
957
|
+
}
|
|
958
|
+
if (entry.when !== void 0) {
|
|
959
|
+
return evaluateWhenOnly(entry, actor, resource, cache, resourceBlock, policy);
|
|
960
|
+
}
|
|
961
|
+
return [];
|
|
962
|
+
}
|
|
963
|
+
function evaluateGlobalRole(entry, actor, policy) {
|
|
964
|
+
const globalRoleName = entry.from_global_role;
|
|
965
|
+
const globalRole = policy.global_roles?.[globalRoleName];
|
|
966
|
+
if (!globalRole) return [];
|
|
967
|
+
if (actor.type !== globalRole.actor_type) return [];
|
|
968
|
+
if (!evaluateActorCondition(globalRole.when, actor)) return [];
|
|
969
|
+
return [
|
|
970
|
+
{
|
|
971
|
+
role: entry.role,
|
|
972
|
+
via: `global_role:${globalRoleName}`
|
|
973
|
+
}
|
|
974
|
+
];
|
|
975
|
+
}
|
|
976
|
+
async function evaluateRelationRole(entry, actor, resource, cache, policy, depthRemaining, maxDepth, visited) {
|
|
977
|
+
if (depthRemaining <= 0) {
|
|
978
|
+
throw new DepthLimitError(
|
|
979
|
+
`Derived role chain exceeded maximum depth of ${maxDepth}`,
|
|
980
|
+
maxDepth,
|
|
981
|
+
"derivation"
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
const relationName = entry.on_relation;
|
|
985
|
+
const requiredRole = entry.from_role;
|
|
986
|
+
let relatedRefs;
|
|
987
|
+
try {
|
|
988
|
+
const attrs = await cache.resolve(resource);
|
|
989
|
+
const relValue = attrs[relationName];
|
|
990
|
+
if (!relValue) return [];
|
|
991
|
+
if (Array.isArray(relValue)) {
|
|
992
|
+
relatedRefs = relValue.filter(
|
|
993
|
+
(r) => r != null && typeof r === "object" && "type" in r && "id" in r
|
|
994
|
+
);
|
|
995
|
+
} else if (typeof relValue === "object" && relValue !== null && "type" in relValue && "id" in relValue) {
|
|
996
|
+
relatedRefs = [relValue];
|
|
997
|
+
} else {
|
|
998
|
+
return [];
|
|
999
|
+
}
|
|
1000
|
+
} catch {
|
|
1001
|
+
return [];
|
|
1002
|
+
}
|
|
1003
|
+
const traces = [];
|
|
1004
|
+
for (const relatedRef of relatedRefs) {
|
|
1005
|
+
const refKey = `${relatedRef.type}:${relatedRef.id}`;
|
|
1006
|
+
if (visited.has(refKey)) {
|
|
1007
|
+
throw new CycleError(
|
|
1008
|
+
`Cycle detected in derived role resolution: ${refKey} already visited`,
|
|
1009
|
+
[...visited, refKey]
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
const branchVisited = new Set(visited);
|
|
1013
|
+
branchVisited.add(refKey);
|
|
1014
|
+
let hasRole = false;
|
|
1015
|
+
const relatedBlock = policy.resources[relatedRef.type];
|
|
1016
|
+
if (relatedBlock?.derived_roles?.length) {
|
|
1017
|
+
const relatedResult = await resolveRolesRecursive(
|
|
1018
|
+
actor,
|
|
1019
|
+
relatedRef,
|
|
1020
|
+
cache,
|
|
1021
|
+
relatedBlock,
|
|
1022
|
+
policy,
|
|
1023
|
+
depthRemaining - 1,
|
|
1024
|
+
maxDepth,
|
|
1025
|
+
branchVisited
|
|
1026
|
+
);
|
|
1027
|
+
if (relatedResult.derived.some((d) => d.role === requiredRole)) {
|
|
1028
|
+
hasRole = true;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (hasRole) {
|
|
1032
|
+
traces.push({
|
|
1033
|
+
role: entry.role,
|
|
1034
|
+
via: `from_role:${requiredRole} on ${relationName} -> ${relatedRef.type}:${relatedRef.id}`
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return traces;
|
|
1039
|
+
}
|
|
1040
|
+
async function resolveRolesRecursive(actor, resource, cache, resourceBlock, policy, depthRemaining, maxDepth, visited) {
|
|
1041
|
+
if (depthRemaining <= 0) {
|
|
1042
|
+
throw new DepthLimitError(
|
|
1043
|
+
`Derived role chain exceeded maximum depth of ${maxDepth}`,
|
|
1044
|
+
maxDepth,
|
|
1045
|
+
"derivation"
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
const direct = [];
|
|
1049
|
+
const derived = [];
|
|
1050
|
+
const derivedEntries = resourceBlock.derived_roles ?? [];
|
|
1051
|
+
for (const entry of derivedEntries) {
|
|
1052
|
+
try {
|
|
1053
|
+
const traces = await evaluateDerivedRole(
|
|
1054
|
+
entry,
|
|
1055
|
+
actor,
|
|
1056
|
+
resource,
|
|
1057
|
+
cache,
|
|
1058
|
+
resourceBlock,
|
|
1059
|
+
policy,
|
|
1060
|
+
depthRemaining,
|
|
1061
|
+
maxDepth,
|
|
1062
|
+
visited
|
|
1063
|
+
);
|
|
1064
|
+
derived.push(...traces);
|
|
1065
|
+
} catch (e) {
|
|
1066
|
+
if (e instanceof CycleError || e instanceof DepthLimitError) {
|
|
1067
|
+
throw e;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return { direct, derived };
|
|
1072
|
+
}
|
|
1073
|
+
async function evaluateRelationIdentity(entry, actor, resource, cache) {
|
|
1074
|
+
const relationName = entry.from_relation;
|
|
1075
|
+
let relatedRefs;
|
|
1076
|
+
try {
|
|
1077
|
+
const attrs = await cache.resolve(resource);
|
|
1078
|
+
const relValue = attrs[relationName];
|
|
1079
|
+
if (!relValue) return [];
|
|
1080
|
+
if (Array.isArray(relValue)) {
|
|
1081
|
+
relatedRefs = relValue.filter(
|
|
1082
|
+
(r) => r != null && typeof r === "object" && "type" in r && "id" in r
|
|
1083
|
+
);
|
|
1084
|
+
} else if (typeof relValue === "object" && relValue !== null && "type" in relValue && "id" in relValue) {
|
|
1085
|
+
relatedRefs = [relValue];
|
|
1086
|
+
} else {
|
|
1087
|
+
return [];
|
|
1088
|
+
}
|
|
1089
|
+
} catch {
|
|
1090
|
+
return [];
|
|
1091
|
+
}
|
|
1092
|
+
for (const ref of relatedRefs) {
|
|
1093
|
+
if (ref.type === actor.type && ref.id === actor.id) {
|
|
1094
|
+
return [
|
|
1095
|
+
{
|
|
1096
|
+
role: entry.role,
|
|
1097
|
+
via: `identity on ${relationName} -> ${ref.type}:${ref.id}`
|
|
1098
|
+
}
|
|
1099
|
+
];
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return [];
|
|
1103
|
+
}
|
|
1104
|
+
async function evaluateActorTypeCondition(entry, actor, resource, cache, resourceBlock, policy) {
|
|
1105
|
+
if (actor.type !== entry.actor_type) return [];
|
|
1106
|
+
const matched = await evaluateRoleCondition(entry.when, actor, resource, cache, resourceBlock, policy);
|
|
1107
|
+
if (!matched) return [];
|
|
1108
|
+
return [
|
|
1109
|
+
{
|
|
1110
|
+
role: entry.role,
|
|
1111
|
+
via: `actor_type:${entry.actor_type} + when condition`
|
|
1112
|
+
}
|
|
1113
|
+
];
|
|
1114
|
+
}
|
|
1115
|
+
async function evaluateWhenOnly(entry, actor, resource, cache, resourceBlock, policy) {
|
|
1116
|
+
const matched = await evaluateRoleCondition(entry.when, actor, resource, cache, resourceBlock, policy);
|
|
1117
|
+
if (!matched) return [];
|
|
1118
|
+
return [
|
|
1119
|
+
{
|
|
1120
|
+
role: entry.role,
|
|
1121
|
+
via: `when condition`
|
|
1122
|
+
}
|
|
1123
|
+
];
|
|
1124
|
+
}
|
|
1125
|
+
async function evaluateRoleCondition(condition, actor, resource, cache, resourceBlock, policy) {
|
|
1126
|
+
try {
|
|
1127
|
+
return await evaluateCondition(
|
|
1128
|
+
condition,
|
|
1129
|
+
actor,
|
|
1130
|
+
resource,
|
|
1131
|
+
cache,
|
|
1132
|
+
{},
|
|
1133
|
+
// empty env — $env conditions will resolve to undefined -> fail-closed
|
|
1134
|
+
resourceBlock,
|
|
1135
|
+
policy
|
|
1136
|
+
);
|
|
1137
|
+
} catch {
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
var MAX_ACTOR_COMBINATOR_DEPTH = 10;
|
|
1142
|
+
function evaluateActorCondition(condition, actor, combinatorDepth = 0) {
|
|
1143
|
+
if ("any" in condition && Array.isArray(condition.any)) {
|
|
1144
|
+
if (combinatorDepth >= MAX_ACTOR_COMBINATOR_DEPTH) return false;
|
|
1145
|
+
return condition.any.some(
|
|
1146
|
+
(c) => evaluateActorCondition(c, actor, combinatorDepth + 1)
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
if ("all" in condition && Array.isArray(condition.all)) {
|
|
1150
|
+
if (combinatorDepth >= MAX_ACTOR_COMBINATOR_DEPTH) return false;
|
|
1151
|
+
return condition.all.every(
|
|
1152
|
+
(c) => evaluateActorCondition(c, actor, combinatorDepth + 1)
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
const entries = Object.entries(condition);
|
|
1156
|
+
for (const [key, expectedValue] of entries) {
|
|
1157
|
+
if (key.startsWith("$actor.")) {
|
|
1158
|
+
const attrName = key.slice(7);
|
|
1159
|
+
const actualValue = actor.attributes[attrName];
|
|
1160
|
+
if (actualValue === void 0 || actualValue === null) return false;
|
|
1161
|
+
if (actualValue !== expectedValue) return false;
|
|
1162
|
+
}
|
|
1163
|
+
if (key.startsWith("$resource.") || key.startsWith("$env.")) return false;
|
|
1164
|
+
}
|
|
1165
|
+
return true;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/evaluation/rule-engine.ts
|
|
1169
|
+
function expandGrants(roles, grants, allPermissions) {
|
|
1170
|
+
const permissionSet = /* @__PURE__ */ new Set();
|
|
1171
|
+
for (const role of roles) {
|
|
1172
|
+
const roleGrants = grants[role];
|
|
1173
|
+
if (!roleGrants) continue;
|
|
1174
|
+
for (const perm of roleGrants) {
|
|
1175
|
+
if (perm === "all") {
|
|
1176
|
+
for (const p of allPermissions) {
|
|
1177
|
+
permissionSet.add(p);
|
|
1178
|
+
}
|
|
1179
|
+
} else {
|
|
1180
|
+
permissionSet.add(perm);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return permissionSet;
|
|
1185
|
+
}
|
|
1186
|
+
async function evaluate(actor, action, resource, resourceBlock, cache, policy, options) {
|
|
1187
|
+
const resolvedRoles = await resolveRoles(
|
|
1188
|
+
actor,
|
|
1189
|
+
resource,
|
|
1190
|
+
cache,
|
|
1191
|
+
resourceBlock,
|
|
1192
|
+
policy,
|
|
1193
|
+
options
|
|
1194
|
+
);
|
|
1195
|
+
const derivedRoleNames = resolvedRoles.derived.map((d) => d.role);
|
|
1196
|
+
const allRoles = [
|
|
1197
|
+
.../* @__PURE__ */ new Set([...resolvedRoles.direct, ...derivedRoleNames])
|
|
1198
|
+
];
|
|
1199
|
+
const grants = resourceBlock.grants ?? {};
|
|
1200
|
+
const grantedPermissionSet = expandGrants(
|
|
1201
|
+
allRoles,
|
|
1202
|
+
grants,
|
|
1203
|
+
resourceBlock.permissions
|
|
1204
|
+
);
|
|
1205
|
+
const grantedPermissions = [...grantedPermissionSet];
|
|
1206
|
+
let allowed = grantedPermissionSet.has(action);
|
|
1207
|
+
const rules = resourceBlock.rules ?? [];
|
|
1208
|
+
const matchedRules = [];
|
|
1209
|
+
const env = options?.env ?? {};
|
|
1210
|
+
if (rules.length > 0) {
|
|
1211
|
+
const { permitGranted, forbidMatched, ruleResults } = await evaluateRules(
|
|
1212
|
+
rules,
|
|
1213
|
+
action,
|
|
1214
|
+
allRoles,
|
|
1215
|
+
actor,
|
|
1216
|
+
resource,
|
|
1217
|
+
cache,
|
|
1218
|
+
env,
|
|
1219
|
+
resourceBlock,
|
|
1220
|
+
policy,
|
|
1221
|
+
options
|
|
1222
|
+
);
|
|
1223
|
+
matchedRules.push(...ruleResults);
|
|
1224
|
+
if (forbidMatched) {
|
|
1225
|
+
allowed = false;
|
|
1226
|
+
} else if (!allowed && permitGranted) {
|
|
1227
|
+
allowed = true;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
let finalDecision;
|
|
1231
|
+
if (matchedRules.some((r) => r.effect === "forbid" && r.matched)) {
|
|
1232
|
+
finalDecision = `Denied: action "${action}" is forbidden by rule`;
|
|
1233
|
+
} else if (allowed) {
|
|
1234
|
+
finalDecision = `Allowed: action "${action}" is granted via roles [${allRoles.join(", ")}]`;
|
|
1235
|
+
} else {
|
|
1236
|
+
finalDecision = `Denied: action "${action}" is not granted (default-deny)`;
|
|
1237
|
+
}
|
|
1238
|
+
return {
|
|
1239
|
+
allowed,
|
|
1240
|
+
resolvedRoles,
|
|
1241
|
+
grantedPermissions,
|
|
1242
|
+
matchedRules,
|
|
1243
|
+
finalDecision
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
async function evaluateRules(rules, action, allRoles, actor, resource, cache, env, resourceBlock, policy, options) {
|
|
1247
|
+
let permitGranted = false;
|
|
1248
|
+
let forbidMatched = false;
|
|
1249
|
+
const ruleResults = [];
|
|
1250
|
+
for (const rule of rules) {
|
|
1251
|
+
if (!rule.permissions.includes(action)) continue;
|
|
1252
|
+
if (rule.roles && rule.roles.length > 0) {
|
|
1253
|
+
if (!rule.roles.some((r) => allRoles.includes(r))) {
|
|
1254
|
+
ruleResults.push({
|
|
1255
|
+
effect: rule.effect,
|
|
1256
|
+
matched: false,
|
|
1257
|
+
rule,
|
|
1258
|
+
resolvedValues: {}
|
|
1259
|
+
});
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (forbidMatched && rule.effect === "permit") {
|
|
1264
|
+
ruleResults.push({
|
|
1265
|
+
effect: rule.effect,
|
|
1266
|
+
matched: false,
|
|
1267
|
+
rule,
|
|
1268
|
+
resolvedValues: {}
|
|
1269
|
+
});
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
let conditionMatched;
|
|
1273
|
+
try {
|
|
1274
|
+
conditionMatched = await evaluateCondition(
|
|
1275
|
+
rule.when,
|
|
1276
|
+
actor,
|
|
1277
|
+
resource,
|
|
1278
|
+
cache,
|
|
1279
|
+
env,
|
|
1280
|
+
resourceBlock,
|
|
1281
|
+
policy,
|
|
1282
|
+
{
|
|
1283
|
+
maxConditionDepth: options?.maxConditionDepth,
|
|
1284
|
+
customEvaluators: options?.customEvaluators,
|
|
1285
|
+
ruleEffect: rule.effect
|
|
1286
|
+
}
|
|
1287
|
+
);
|
|
1288
|
+
} catch {
|
|
1289
|
+
conditionMatched = rule.effect === "forbid";
|
|
1290
|
+
}
|
|
1291
|
+
ruleResults.push({
|
|
1292
|
+
effect: rule.effect,
|
|
1293
|
+
matched: conditionMatched,
|
|
1294
|
+
rule,
|
|
1295
|
+
resolvedValues: {}
|
|
1296
|
+
});
|
|
1297
|
+
if (conditionMatched) {
|
|
1298
|
+
if (rule.effect === "forbid") {
|
|
1299
|
+
forbidMatched = true;
|
|
1300
|
+
} else if (rule.effect === "permit") {
|
|
1301
|
+
permitGranted = true;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return { permitGranted, forbidMatched, ruleResults };
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// src/evaluation/cache.ts
|
|
1309
|
+
function isResourceRef2(value) {
|
|
1310
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1311
|
+
const obj = value;
|
|
1312
|
+
return typeof obj.type === "string" && typeof obj.id === "string";
|
|
1313
|
+
}
|
|
1314
|
+
var AttributeCache = class {
|
|
1315
|
+
cache = /* @__PURE__ */ new Map();
|
|
1316
|
+
/** Pre-seeded inline attributes from cascading (T017). */
|
|
1317
|
+
seededInline = /* @__PURE__ */ new Map();
|
|
1318
|
+
resolvers;
|
|
1319
|
+
constructor(resolvers) {
|
|
1320
|
+
this.resolvers = resolvers ?? {};
|
|
1321
|
+
}
|
|
1322
|
+
key(type, id) {
|
|
1323
|
+
return `${type}:${id}`;
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Resolve all attributes for a resource reference.
|
|
1327
|
+
* Merges inline attributes with resolver results (inline takes precedence).
|
|
1328
|
+
* Results are cached by `${type}:${id}`.
|
|
1329
|
+
*
|
|
1330
|
+
* @param ref The resource reference
|
|
1331
|
+
* @param resourceBlock Optional resource block for FR-016 validation of resolver results
|
|
1332
|
+
*/
|
|
1333
|
+
async resolve(ref, resourceBlock) {
|
|
1334
|
+
const k = this.key(ref.type, ref.id);
|
|
1335
|
+
if (!this.cache.has(k)) {
|
|
1336
|
+
this.cache.set(k, this.doResolve(ref, resourceBlock));
|
|
1337
|
+
}
|
|
1338
|
+
return this.cache.get(k);
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Seed inline attributes for a resource from cascading (T017).
|
|
1342
|
+
* Called when a relation-target value has extra fields beyond type/id.
|
|
1343
|
+
* These are merged into the cache entry as pre-populated inline attributes.
|
|
1344
|
+
*/
|
|
1345
|
+
seedInline(type, id, attrs) {
|
|
1346
|
+
const k = this.key(type, id);
|
|
1347
|
+
const existing = this.seededInline.get(k);
|
|
1348
|
+
if (existing) {
|
|
1349
|
+
this.seededInline.set(k, { ...attrs, ...existing });
|
|
1350
|
+
} else {
|
|
1351
|
+
this.seededInline.set(k, attrs);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
async doResolve(ref, resourceBlock) {
|
|
1355
|
+
const k = this.key(ref.type, ref.id);
|
|
1356
|
+
const seeded = this.seededInline.get(k) ?? {};
|
|
1357
|
+
const inline = ref.attributes ?? {};
|
|
1358
|
+
const allInline = { ...seeded, ...inline };
|
|
1359
|
+
const resolver = this.resolvers[ref.type];
|
|
1360
|
+
if (!resolver) {
|
|
1361
|
+
this.processCascadingInline(allInline, resourceBlock);
|
|
1362
|
+
return allInline;
|
|
1363
|
+
}
|
|
1364
|
+
const resolved = await resolver(ref);
|
|
1365
|
+
if (resourceBlock?.relations) {
|
|
1366
|
+
this.validateResolverRelations(resolved, allInline, resourceBlock.relations, ref.type);
|
|
1367
|
+
}
|
|
1368
|
+
const merged = { ...resolved, ...allInline };
|
|
1369
|
+
this.processCascadingInline(merged, resourceBlock);
|
|
1370
|
+
return merged;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* T017: Extract extra fields from ResourceRef-shaped relation values
|
|
1374
|
+
* and seed them into the cache for the referenced resource.
|
|
1375
|
+
*/
|
|
1376
|
+
processCascadingInline(attrs, resourceBlock) {
|
|
1377
|
+
if (!resourceBlock?.relations) return;
|
|
1378
|
+
for (const [fieldName, targetType] of Object.entries(resourceBlock.relations)) {
|
|
1379
|
+
const value = attrs[fieldName];
|
|
1380
|
+
if (!isResourceRef2(value)) continue;
|
|
1381
|
+
const { type, id, ...extra } = value;
|
|
1382
|
+
if (Object.keys(extra).length > 0) {
|
|
1383
|
+
this.seedInline(targetType, id, extra);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* T018: Validate that resolver-returned values for declared relation fields
|
|
1389
|
+
* are valid ResourceRefs. Throws ValidationError if not.
|
|
1390
|
+
* Only validates fields that come from the resolver (not inline).
|
|
1391
|
+
*/
|
|
1392
|
+
validateResolverRelations(resolverResult, inlineAttrs, relations, resourceType) {
|
|
1393
|
+
for (const [fieldName, targetType] of Object.entries(relations)) {
|
|
1394
|
+
if (fieldName in inlineAttrs) continue;
|
|
1395
|
+
const value = resolverResult[fieldName];
|
|
1396
|
+
if (value === void 0 || value === null) continue;
|
|
1397
|
+
if (Array.isArray(value)) {
|
|
1398
|
+
for (let i = 0; i < value.length; i++) {
|
|
1399
|
+
if (!isResourceRef2(value[i])) {
|
|
1400
|
+
throw new ValidationError(
|
|
1401
|
+
`Resolver for "${resourceType}" returned invalid value for relation field "${fieldName}[${i}]": expected ResourceRef with type/id, got ${typeof value[i] === "object" ? JSON.stringify(value[i]) : String(value[i])}. Declared relation target type: "${targetType}".`,
|
|
1402
|
+
`resolvers.${resourceType}.${fieldName}[${i}]`
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
if (!isResourceRef2(value)) {
|
|
1409
|
+
throw new ValidationError(
|
|
1410
|
+
`Resolver for "${resourceType}" returned invalid value for relation field "${fieldName}": expected ResourceRef with type/id, got ${typeof value === "object" ? JSON.stringify(value) : String(value)}. Declared relation target type: "${targetType}".`,
|
|
1411
|
+
`resolvers.${resourceType}.${fieldName}`
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
// src/partial/constraint-builder.ts
|
|
1419
|
+
var OPERATOR_KEYS2 = /* @__PURE__ */ new Set([
|
|
1420
|
+
"eq",
|
|
1421
|
+
"neq",
|
|
1422
|
+
"gt",
|
|
1423
|
+
"gte",
|
|
1424
|
+
"lt",
|
|
1425
|
+
"lte",
|
|
1426
|
+
"in",
|
|
1427
|
+
"includes",
|
|
1428
|
+
"exists",
|
|
1429
|
+
"startsWith",
|
|
1430
|
+
"endsWith",
|
|
1431
|
+
"contains",
|
|
1432
|
+
"custom"
|
|
1433
|
+
]);
|
|
1434
|
+
async function buildConstraints(actor, action, resourceType, cache, policy, options) {
|
|
1435
|
+
const resourceBlock = policy.resources[resourceType];
|
|
1436
|
+
if (!resourceBlock) {
|
|
1437
|
+
return { forbidden: true };
|
|
1438
|
+
}
|
|
1439
|
+
const env = options?.env ?? {};
|
|
1440
|
+
const rolesGrantingAction = findRolesGrantingAction(action, resourceBlock);
|
|
1441
|
+
if (rolesGrantingAction.length === 0) {
|
|
1442
|
+
const permitRules2 = (resourceBlock.rules ?? []).filter(
|
|
1443
|
+
(r) => r.effect === "permit" && r.permissions.includes(action)
|
|
1444
|
+
);
|
|
1445
|
+
if (permitRules2.length === 0) {
|
|
1446
|
+
return { forbidden: true };
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
const roleConstraintCache = /* @__PURE__ */ new Map();
|
|
1450
|
+
const pathConstraints = [];
|
|
1451
|
+
async function getRoleConstraints(role) {
|
|
1452
|
+
const cached = roleConstraintCache.get(role);
|
|
1453
|
+
if (cached !== void 0) return cached;
|
|
1454
|
+
const constraints = [];
|
|
1455
|
+
const derivedEntries = resourceBlock.derived_roles ?? [];
|
|
1456
|
+
for (const entry of derivedEntries) {
|
|
1457
|
+
if (entry.role !== role) continue;
|
|
1458
|
+
const constraint = await evaluateDerivedRoleConstraint(
|
|
1459
|
+
entry,
|
|
1460
|
+
actor,
|
|
1461
|
+
resourceType,
|
|
1462
|
+
cache,
|
|
1463
|
+
policy,
|
|
1464
|
+
resourceBlock,
|
|
1465
|
+
env
|
|
1466
|
+
);
|
|
1467
|
+
if (constraint !== null) constraints.push(constraint);
|
|
1468
|
+
}
|
|
1469
|
+
roleConstraintCache.set(role, constraints);
|
|
1470
|
+
return constraints;
|
|
1471
|
+
}
|
|
1472
|
+
for (const role of rolesGrantingAction) {
|
|
1473
|
+
const roleResults = await getRoleConstraints(role);
|
|
1474
|
+
pathConstraints.push(...roleResults);
|
|
1475
|
+
}
|
|
1476
|
+
const permitRules = (resourceBlock.rules ?? []).filter(
|
|
1477
|
+
(r) => r.effect === "permit" && r.permissions.includes(action)
|
|
1478
|
+
);
|
|
1479
|
+
for (const rule of permitRules) {
|
|
1480
|
+
if (rule.roles && rule.roles.length > 0) {
|
|
1481
|
+
const derivableRoleConstraints = [];
|
|
1482
|
+
for (const role of rule.roles) {
|
|
1483
|
+
const rc = await getRoleConstraints(role);
|
|
1484
|
+
derivableRoleConstraints.push(...rc);
|
|
1485
|
+
}
|
|
1486
|
+
if (derivableRoleConstraints.length === 0) continue;
|
|
1487
|
+
const ruleConstraint = conditionToConstraint(rule.when, actor, env);
|
|
1488
|
+
if (ruleConstraint !== null) {
|
|
1489
|
+
pathConstraints.push(
|
|
1490
|
+
simplify({
|
|
1491
|
+
type: "and",
|
|
1492
|
+
children: [
|
|
1493
|
+
derivableRoleConstraints.length === 1 ? derivableRoleConstraints[0] : { type: "or", children: derivableRoleConstraints },
|
|
1494
|
+
ruleConstraint
|
|
1495
|
+
]
|
|
1496
|
+
})
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
} else {
|
|
1500
|
+
const ruleConstraint = conditionToConstraint(rule.when, actor, env);
|
|
1501
|
+
if (ruleConstraint !== null && pathConstraints.length > 0) {
|
|
1502
|
+
pathConstraints.push(ruleConstraint);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
if (pathConstraints.length === 0) {
|
|
1507
|
+
return { forbidden: true };
|
|
1508
|
+
}
|
|
1509
|
+
let combined;
|
|
1510
|
+
if (pathConstraints.length === 1) {
|
|
1511
|
+
combined = pathConstraints[0];
|
|
1512
|
+
} else {
|
|
1513
|
+
combined = { type: "or", children: pathConstraints };
|
|
1514
|
+
}
|
|
1515
|
+
const forbidRules = (resourceBlock.rules ?? []).filter(
|
|
1516
|
+
(r) => r.effect === "forbid" && r.permissions.includes(action)
|
|
1517
|
+
);
|
|
1518
|
+
for (const rule of forbidRules) {
|
|
1519
|
+
const forbidConstraint = conditionToConstraint(rule.when, actor, env);
|
|
1520
|
+
if (forbidConstraint !== null) {
|
|
1521
|
+
combined = {
|
|
1522
|
+
type: "and",
|
|
1523
|
+
children: [combined, { type: "not", child: forbidConstraint }]
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
combined = simplify(combined);
|
|
1528
|
+
if (combined.type === "always") {
|
|
1529
|
+
return { unrestricted: true };
|
|
1530
|
+
}
|
|
1531
|
+
if (combined.type === "never") {
|
|
1532
|
+
return { forbidden: true };
|
|
1533
|
+
}
|
|
1534
|
+
return { constraints: combined };
|
|
1535
|
+
}
|
|
1536
|
+
function findRolesGrantingAction(action, resourceBlock) {
|
|
1537
|
+
const grants = resourceBlock.grants ?? {};
|
|
1538
|
+
const roles = [];
|
|
1539
|
+
for (const [role, permissions] of Object.entries(grants)) {
|
|
1540
|
+
if (permissions.includes(action) || permissions.includes("all")) {
|
|
1541
|
+
roles.push(role);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return roles;
|
|
1545
|
+
}
|
|
1546
|
+
async function evaluateDerivedRoleConstraint(entry, actor, _resourceType, _cache, policy, resourceBlock, env) {
|
|
1547
|
+
if (entry.from_global_role !== void 0) {
|
|
1548
|
+
return evaluateGlobalRoleConstraint(entry, actor, policy);
|
|
1549
|
+
}
|
|
1550
|
+
if (entry.from_role !== void 0 && entry.on_relation !== void 0) {
|
|
1551
|
+
return evaluateRelationRoleConstraint(entry, actor, resourceBlock, policy);
|
|
1552
|
+
}
|
|
1553
|
+
if (entry.from_relation !== void 0) {
|
|
1554
|
+
return evaluateRelationIdentityConstraint(entry, actor, resourceBlock);
|
|
1555
|
+
}
|
|
1556
|
+
if (entry.actor_type !== void 0 && entry.when !== void 0) {
|
|
1557
|
+
return evaluateActorTypeConstraint(entry, actor, env);
|
|
1558
|
+
}
|
|
1559
|
+
if (entry.when !== void 0) {
|
|
1560
|
+
return evaluateWhenOnlyConstraint(entry, actor, env);
|
|
1561
|
+
}
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
function evaluateGlobalRoleConstraint(entry, actor, policy) {
|
|
1565
|
+
const globalRoleName = entry.from_global_role;
|
|
1566
|
+
const globalRole = policy.global_roles?.[globalRoleName];
|
|
1567
|
+
if (!globalRole) return null;
|
|
1568
|
+
if (actor.type !== globalRole.actor_type) return null;
|
|
1569
|
+
if (!evaluateActorCondition2(globalRole.when, actor)) return null;
|
|
1570
|
+
return { type: "always" };
|
|
1571
|
+
}
|
|
1572
|
+
function evaluateRelationRoleConstraint(entry, actor, resourceBlock, policy) {
|
|
1573
|
+
const relationName = entry.on_relation;
|
|
1574
|
+
const requiredRole = entry.from_role;
|
|
1575
|
+
const relationDef = resourceBlock.relations?.[relationName];
|
|
1576
|
+
if (!relationDef) return null;
|
|
1577
|
+
const targetResourceType = relationDef;
|
|
1578
|
+
return {
|
|
1579
|
+
type: "relation",
|
|
1580
|
+
field: relationName,
|
|
1581
|
+
resourceType: targetResourceType,
|
|
1582
|
+
constraint: {
|
|
1583
|
+
type: "has_role",
|
|
1584
|
+
actorId: actor.id,
|
|
1585
|
+
actorType: actor.type,
|
|
1586
|
+
role: requiredRole
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
function evaluateRelationIdentityConstraint(entry, actor, resourceBlock) {
|
|
1591
|
+
const relationName = entry.from_relation;
|
|
1592
|
+
const relationDef = resourceBlock.relations?.[relationName];
|
|
1593
|
+
if (!relationDef) {
|
|
1594
|
+
return {
|
|
1595
|
+
type: "field_eq",
|
|
1596
|
+
field: relationName,
|
|
1597
|
+
value: actor.id
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
return {
|
|
1601
|
+
type: "relation",
|
|
1602
|
+
field: relationName,
|
|
1603
|
+
resourceType: relationDef,
|
|
1604
|
+
constraint: {
|
|
1605
|
+
type: "and",
|
|
1606
|
+
children: [
|
|
1607
|
+
{ type: "field_eq", field: "id", value: actor.id },
|
|
1608
|
+
{ type: "field_eq", field: "type", value: actor.type }
|
|
1609
|
+
]
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
function evaluateActorTypeConstraint(entry, actor, env) {
|
|
1614
|
+
if (actor.type !== entry.actor_type) return null;
|
|
1615
|
+
return conditionToConstraint(entry.when, actor, env);
|
|
1616
|
+
}
|
|
1617
|
+
function evaluateWhenOnlyConstraint(entry, actor, env) {
|
|
1618
|
+
return conditionToConstraint(entry.when, actor, env);
|
|
1619
|
+
}
|
|
1620
|
+
function conditionToConstraint(condition, actor, env) {
|
|
1621
|
+
if ("any" in condition && Array.isArray(condition.any)) {
|
|
1622
|
+
const items = condition.any;
|
|
1623
|
+
const children2 = [];
|
|
1624
|
+
for (const item of items) {
|
|
1625
|
+
const child = conditionToConstraint(item, actor, env);
|
|
1626
|
+
if (child !== null) children2.push(child);
|
|
1627
|
+
}
|
|
1628
|
+
if (children2.length === 0) return null;
|
|
1629
|
+
if (children2.length === 1) return children2[0];
|
|
1630
|
+
return { type: "or", children: children2 };
|
|
1631
|
+
}
|
|
1632
|
+
if ("all" in condition && Array.isArray(condition.all)) {
|
|
1633
|
+
const items = condition.all;
|
|
1634
|
+
const children2 = [];
|
|
1635
|
+
for (const item of items) {
|
|
1636
|
+
const child = conditionToConstraint(item, actor, env);
|
|
1637
|
+
if (child !== null) children2.push(child);
|
|
1638
|
+
}
|
|
1639
|
+
if (children2.length === 0) return { type: "always" };
|
|
1640
|
+
if (children2.length === 1) return children2[0];
|
|
1641
|
+
return { type: "and", children: children2 };
|
|
1642
|
+
}
|
|
1643
|
+
const entries = Object.entries(condition);
|
|
1644
|
+
const children = [];
|
|
1645
|
+
for (const [key, conditionValue] of entries) {
|
|
1646
|
+
const constraint = pairToConstraint(key, conditionValue, actor, env);
|
|
1647
|
+
if (constraint !== null) {
|
|
1648
|
+
children.push(constraint);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
if (children.length === 0) return { type: "always" };
|
|
1652
|
+
if (children.length === 1) return children[0];
|
|
1653
|
+
return { type: "and", children };
|
|
1654
|
+
}
|
|
1655
|
+
function pairToConstraint(key, conditionValue, actor, env) {
|
|
1656
|
+
if (key.startsWith("$actor.")) {
|
|
1657
|
+
const attrPath = key.slice(7);
|
|
1658
|
+
const actorValue = getNestedAttribute2(actor.attributes, attrPath);
|
|
1659
|
+
if (isOperator2(conditionValue)) {
|
|
1660
|
+
return evaluateActorOperatorConstraint(actorValue, conditionValue, actor, env);
|
|
1661
|
+
}
|
|
1662
|
+
const rightValue = resolveStaticValue(conditionValue, actor, env);
|
|
1663
|
+
if (rightValue === void 0 || rightValue === null) return null;
|
|
1664
|
+
if (actorValue === void 0 || actorValue === null) return null;
|
|
1665
|
+
return actorValue === rightValue ? { type: "always" } : { type: "never" };
|
|
1666
|
+
}
|
|
1667
|
+
if (key.startsWith("$resource.")) {
|
|
1668
|
+
const field = key.slice(10);
|
|
1669
|
+
if (isOperator2(conditionValue)) {
|
|
1670
|
+
return operatorToConstraint(field, conditionValue, actor, env);
|
|
1671
|
+
}
|
|
1672
|
+
const rightValue = resolveStaticValue(conditionValue, actor, env);
|
|
1673
|
+
if (rightValue === void 0 || rightValue === null) return null;
|
|
1674
|
+
return { type: "field_eq", field, value: rightValue };
|
|
1675
|
+
}
|
|
1676
|
+
if (key.startsWith("$env.")) {
|
|
1677
|
+
const envPath = key.slice(5);
|
|
1678
|
+
const envValue = env[envPath];
|
|
1679
|
+
if (envValue === void 0 || envValue === null) return { type: "never" };
|
|
1680
|
+
if (isOperator2(conditionValue)) {
|
|
1681
|
+
return evaluateActorOperatorConstraint(envValue, conditionValue, actor, env);
|
|
1682
|
+
}
|
|
1683
|
+
const rightValue = resolveStaticValue(conditionValue, actor, env);
|
|
1684
|
+
if (rightValue === void 0 || rightValue === null) return null;
|
|
1685
|
+
return envValue === rightValue ? { type: "always" } : { type: "never" };
|
|
1686
|
+
}
|
|
1687
|
+
return null;
|
|
1688
|
+
}
|
|
1689
|
+
function operatorToConstraint(field, operator, actor, env) {
|
|
1690
|
+
const op = operator;
|
|
1691
|
+
const opKey = Object.keys(op)[0];
|
|
1692
|
+
const opValue = op[opKey];
|
|
1693
|
+
if (opKey === "custom") {
|
|
1694
|
+
return { type: "unknown", name: opValue };
|
|
1695
|
+
}
|
|
1696
|
+
if (opKey === "exists") {
|
|
1697
|
+
return { type: "field_exists", field, exists: opValue };
|
|
1698
|
+
}
|
|
1699
|
+
const resolvedValue = resolveStaticValue(opValue, actor, env);
|
|
1700
|
+
switch (opKey) {
|
|
1701
|
+
case "eq":
|
|
1702
|
+
return { type: "field_eq", field, value: resolvedValue };
|
|
1703
|
+
case "neq":
|
|
1704
|
+
return { type: "field_neq", field, value: resolvedValue };
|
|
1705
|
+
case "gt":
|
|
1706
|
+
return { type: "field_gt", field, value: resolvedValue };
|
|
1707
|
+
case "gte":
|
|
1708
|
+
return { type: "field_gte", field, value: resolvedValue };
|
|
1709
|
+
case "lt":
|
|
1710
|
+
return { type: "field_lt", field, value: resolvedValue };
|
|
1711
|
+
case "lte":
|
|
1712
|
+
return { type: "field_lte", field, value: resolvedValue };
|
|
1713
|
+
case "in":
|
|
1714
|
+
if (Array.isArray(resolvedValue)) {
|
|
1715
|
+
return { type: "field_in", field, values: resolvedValue };
|
|
1716
|
+
}
|
|
1717
|
+
return { type: "field_in", field, values: [resolvedValue] };
|
|
1718
|
+
case "includes":
|
|
1719
|
+
return { type: "field_includes", field, value: resolvedValue };
|
|
1720
|
+
case "startsWith":
|
|
1721
|
+
case "endsWith":
|
|
1722
|
+
case "contains":
|
|
1723
|
+
return { type: "field_contains", field, value: resolvedValue };
|
|
1724
|
+
default:
|
|
1725
|
+
return null;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
function evaluateActorOperatorConstraint(leftValue, operator, actor, env) {
|
|
1729
|
+
const op = operator;
|
|
1730
|
+
const opKey = Object.keys(op)[0];
|
|
1731
|
+
const opValue = op[opKey];
|
|
1732
|
+
if (opKey === "custom") {
|
|
1733
|
+
return { type: "unknown", name: opValue };
|
|
1734
|
+
}
|
|
1735
|
+
if (opKey === "exists") {
|
|
1736
|
+
const exists = leftValue !== void 0 && leftValue !== null;
|
|
1737
|
+
return (opValue === true ? exists : !exists) ? { type: "always" } : { type: "never" };
|
|
1738
|
+
}
|
|
1739
|
+
if (typeof opValue === "string" && opValue.startsWith("$resource.")) {
|
|
1740
|
+
const field = opValue.slice(10);
|
|
1741
|
+
if (leftValue === void 0 || leftValue === null) return { type: "never" };
|
|
1742
|
+
return actorResourceCrossConstraint(leftValue, opKey, field);
|
|
1743
|
+
}
|
|
1744
|
+
const resolvedRight = resolveStaticValue(opValue, actor, env);
|
|
1745
|
+
if (leftValue === void 0 || leftValue === null) return { type: "never" };
|
|
1746
|
+
if (resolvedRight === void 0 || resolvedRight === null) return { type: "never" };
|
|
1747
|
+
let result;
|
|
1748
|
+
switch (opKey) {
|
|
1749
|
+
case "eq":
|
|
1750
|
+
result = leftValue === resolvedRight;
|
|
1751
|
+
break;
|
|
1752
|
+
case "neq":
|
|
1753
|
+
result = leftValue !== resolvedRight;
|
|
1754
|
+
break;
|
|
1755
|
+
case "gt":
|
|
1756
|
+
result = leftValue > resolvedRight;
|
|
1757
|
+
break;
|
|
1758
|
+
case "gte":
|
|
1759
|
+
result = leftValue >= resolvedRight;
|
|
1760
|
+
break;
|
|
1761
|
+
case "lt":
|
|
1762
|
+
result = leftValue < resolvedRight;
|
|
1763
|
+
break;
|
|
1764
|
+
case "lte":
|
|
1765
|
+
result = leftValue <= resolvedRight;
|
|
1766
|
+
break;
|
|
1767
|
+
case "in":
|
|
1768
|
+
result = Array.isArray(resolvedRight) ? resolvedRight.includes(leftValue) : false;
|
|
1769
|
+
break;
|
|
1770
|
+
case "includes":
|
|
1771
|
+
result = Array.isArray(leftValue) ? leftValue.includes(resolvedRight) : false;
|
|
1772
|
+
break;
|
|
1773
|
+
default:
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
return result ? { type: "always" } : { type: "never" };
|
|
1777
|
+
}
|
|
1778
|
+
function actorResourceCrossConstraint(leftValue, opKey, resourceField) {
|
|
1779
|
+
switch (opKey) {
|
|
1780
|
+
case "eq":
|
|
1781
|
+
return { type: "field_eq", field: resourceField, value: leftValue };
|
|
1782
|
+
case "neq":
|
|
1783
|
+
return { type: "field_neq", field: resourceField, value: leftValue };
|
|
1784
|
+
case "in":
|
|
1785
|
+
return { type: "field_includes", field: resourceField, value: leftValue };
|
|
1786
|
+
case "includes":
|
|
1787
|
+
if (Array.isArray(leftValue)) {
|
|
1788
|
+
return { type: "field_in", field: resourceField, values: leftValue };
|
|
1789
|
+
}
|
|
1790
|
+
return null;
|
|
1791
|
+
case "gt":
|
|
1792
|
+
return { type: "field_lt", field: resourceField, value: leftValue };
|
|
1793
|
+
case "gte":
|
|
1794
|
+
return { type: "field_lte", field: resourceField, value: leftValue };
|
|
1795
|
+
case "lt":
|
|
1796
|
+
return { type: "field_gt", field: resourceField, value: leftValue };
|
|
1797
|
+
case "lte":
|
|
1798
|
+
return { type: "field_gte", field: resourceField, value: leftValue };
|
|
1799
|
+
default:
|
|
1800
|
+
return { type: "never" };
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function resolveStaticValue(value, actor, env) {
|
|
1804
|
+
if (typeof value === "string") {
|
|
1805
|
+
if (value.startsWith("$actor.")) {
|
|
1806
|
+
const attrPath = value.slice(7);
|
|
1807
|
+
return getNestedAttribute2(actor.attributes, attrPath);
|
|
1808
|
+
}
|
|
1809
|
+
if (value.startsWith("$env.")) {
|
|
1810
|
+
const envPath = value.slice(5);
|
|
1811
|
+
return env[envPath];
|
|
1812
|
+
}
|
|
1813
|
+
if (value.startsWith("$resource.")) {
|
|
1814
|
+
return value;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
return value;
|
|
1818
|
+
}
|
|
1819
|
+
var FORBIDDEN_PROPS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1820
|
+
function getNestedAttribute2(obj, path) {
|
|
1821
|
+
const parts = path.split(".");
|
|
1822
|
+
let current = obj;
|
|
1823
|
+
for (const part of parts) {
|
|
1824
|
+
if (FORBIDDEN_PROPS2.has(part)) return void 0;
|
|
1825
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
1826
|
+
return void 0;
|
|
1827
|
+
}
|
|
1828
|
+
current = current[part];
|
|
1829
|
+
}
|
|
1830
|
+
return current;
|
|
1831
|
+
}
|
|
1832
|
+
function evaluateActorCondition2(condition, actor) {
|
|
1833
|
+
if ("any" in condition && Array.isArray(condition.any)) {
|
|
1834
|
+
return condition.any.some(
|
|
1835
|
+
(c) => evaluateActorCondition2(c, actor)
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
if ("all" in condition && Array.isArray(condition.all)) {
|
|
1839
|
+
return condition.all.every(
|
|
1840
|
+
(c) => evaluateActorCondition2(c, actor)
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
const entries = Object.entries(condition);
|
|
1844
|
+
for (const [key, expectedValue] of entries) {
|
|
1845
|
+
if (key.startsWith("$actor.")) {
|
|
1846
|
+
const attrName = key.slice(7);
|
|
1847
|
+
const actualValue = getNestedAttribute2(actor.attributes, attrName);
|
|
1848
|
+
if (actualValue === void 0 || actualValue === null) return false;
|
|
1849
|
+
if (actualValue !== expectedValue) return false;
|
|
1850
|
+
} else if (key.startsWith("$resource.") || key.startsWith("$env.")) {
|
|
1851
|
+
return false;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
return true;
|
|
1855
|
+
}
|
|
1856
|
+
function isOperator2(value) {
|
|
1857
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1858
|
+
const keys = Object.keys(value);
|
|
1859
|
+
return keys.length === 1 && OPERATOR_KEYS2.has(keys[0]);
|
|
1860
|
+
}
|
|
1861
|
+
var MAX_SIMPLIFY_DEPTH = 100;
|
|
1862
|
+
function simplify(constraint, _depth = 0) {
|
|
1863
|
+
if (_depth > MAX_SIMPLIFY_DEPTH) {
|
|
1864
|
+
return constraint;
|
|
1865
|
+
}
|
|
1866
|
+
switch (constraint.type) {
|
|
1867
|
+
case "and": {
|
|
1868
|
+
let children = constraint.children.map((c) => simplify(c, _depth + 1));
|
|
1869
|
+
children = children.filter((c) => c.type !== "always");
|
|
1870
|
+
if (children.some((c) => c.type === "never")) {
|
|
1871
|
+
return { type: "never" };
|
|
1872
|
+
}
|
|
1873
|
+
if (children.length === 0) return { type: "always" };
|
|
1874
|
+
if (children.length === 1) return children[0];
|
|
1875
|
+
return { type: "and", children };
|
|
1876
|
+
}
|
|
1877
|
+
case "or": {
|
|
1878
|
+
let children = constraint.children.map((c) => simplify(c, _depth + 1));
|
|
1879
|
+
children = children.filter((c) => c.type !== "never");
|
|
1880
|
+
if (children.some((c) => c.type === "always")) {
|
|
1881
|
+
return { type: "always" };
|
|
1882
|
+
}
|
|
1883
|
+
if (children.length === 0) return { type: "never" };
|
|
1884
|
+
if (children.length === 1) return children[0];
|
|
1885
|
+
return { type: "or", children };
|
|
1886
|
+
}
|
|
1887
|
+
case "not": {
|
|
1888
|
+
const child = simplify(constraint.child, _depth + 1);
|
|
1889
|
+
if (child.type === "always") return { type: "never" };
|
|
1890
|
+
if (child.type === "never") return { type: "always" };
|
|
1891
|
+
if (child.type === "not") return child.child;
|
|
1892
|
+
return { type: "not", child };
|
|
1893
|
+
}
|
|
1894
|
+
case "relation": {
|
|
1895
|
+
const simplified = simplify(constraint.constraint, _depth + 1);
|
|
1896
|
+
return { type: "relation", field: constraint.field, resourceType: constraint.resourceType, constraint: simplified };
|
|
1897
|
+
}
|
|
1898
|
+
default:
|
|
1899
|
+
return constraint;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// src/partial/translator.ts
|
|
1904
|
+
var MAX_TRANSLATE_DEPTH = 100;
|
|
1905
|
+
function translateConstraints(constraint, adapter, _depth = 0) {
|
|
1906
|
+
if (_depth > MAX_TRANSLATE_DEPTH) {
|
|
1907
|
+
throw new Error(
|
|
1908
|
+
`translateConstraints exceeded maximum recursion depth (${MAX_TRANSLATE_DEPTH}). The constraint AST may be malformed or excessively nested.`
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
switch (constraint.type) {
|
|
1912
|
+
// Leaf constraints
|
|
1913
|
+
case "field_eq":
|
|
1914
|
+
case "field_neq":
|
|
1915
|
+
case "field_gt":
|
|
1916
|
+
case "field_gte":
|
|
1917
|
+
case "field_lt":
|
|
1918
|
+
case "field_lte":
|
|
1919
|
+
case "field_in":
|
|
1920
|
+
case "field_nin":
|
|
1921
|
+
case "field_exists":
|
|
1922
|
+
case "field_includes":
|
|
1923
|
+
case "field_contains":
|
|
1924
|
+
return adapter.translate(constraint);
|
|
1925
|
+
// Relation constraint
|
|
1926
|
+
case "relation": {
|
|
1927
|
+
const childQuery = translateConstraints(constraint.constraint, adapter, _depth + 1);
|
|
1928
|
+
return adapter.relation(constraint.field, constraint.resourceType, childQuery);
|
|
1929
|
+
}
|
|
1930
|
+
// Has role constraint
|
|
1931
|
+
case "has_role":
|
|
1932
|
+
return adapter.hasRole(constraint.actorId, constraint.actorType, constraint.role);
|
|
1933
|
+
// Unknown constraint (custom evaluator)
|
|
1934
|
+
case "unknown":
|
|
1935
|
+
return adapter.unknown(constraint.name);
|
|
1936
|
+
// Combinators
|
|
1937
|
+
case "and": {
|
|
1938
|
+
const queries = constraint.children.map((c) => translateConstraints(c, adapter, _depth + 1));
|
|
1939
|
+
return adapter.and(queries);
|
|
1940
|
+
}
|
|
1941
|
+
case "or": {
|
|
1942
|
+
const queries = constraint.children.map((c) => translateConstraints(c, adapter, _depth + 1));
|
|
1943
|
+
return adapter.or(queries);
|
|
1944
|
+
}
|
|
1945
|
+
case "not": {
|
|
1946
|
+
const childQuery = translateConstraints(constraint.child, adapter, _depth + 1);
|
|
1947
|
+
return adapter.not(childQuery);
|
|
1948
|
+
}
|
|
1949
|
+
// Terminal nodes - should not reach the translator
|
|
1950
|
+
case "always":
|
|
1951
|
+
throw new Error(
|
|
1952
|
+
'Constraint node "always" should be simplified out before translation. Use buildConstraints() which returns ConstraintResult with unrestricted/forbidden sentinels.'
|
|
1953
|
+
);
|
|
1954
|
+
case "never":
|
|
1955
|
+
throw new Error(
|
|
1956
|
+
'Constraint node "never" should be simplified out before translation. Use buildConstraints() which returns ConstraintResult with unrestricted/forbidden sentinels.'
|
|
1957
|
+
);
|
|
1958
|
+
default: {
|
|
1959
|
+
const _exhaustive = constraint;
|
|
1960
|
+
throw new Error(`Unknown constraint type: ${constraint.type}`);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// src/snapshot.ts
|
|
1966
|
+
async function snapshot(engine, actor, resources, options) {
|
|
1967
|
+
const entries = await Promise.all(
|
|
1968
|
+
resources.map(async (resource) => {
|
|
1969
|
+
const key = `${resource.type}:${resource.id}`;
|
|
1970
|
+
const actions = await engine.permittedActions(actor, resource, options);
|
|
1971
|
+
return [key, actions];
|
|
1972
|
+
})
|
|
1973
|
+
);
|
|
1974
|
+
return Object.fromEntries(entries);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// src/field-access.ts
|
|
1978
|
+
async function canField(engine, actor, operation, resource, field, fieldAccess, options) {
|
|
1979
|
+
if (!fieldAccess) {
|
|
1980
|
+
return engine.can(actor, operation, resource, options);
|
|
1981
|
+
}
|
|
1982
|
+
const fieldDef = fieldAccess[field];
|
|
1983
|
+
if (!fieldDef || !fieldDef[operation]) {
|
|
1984
|
+
return engine.can(actor, operation, resource, options);
|
|
1985
|
+
}
|
|
1986
|
+
const allowedRoles = fieldDef[operation];
|
|
1987
|
+
const actorRoles = await engine.resolvedRoles(actor, resource, options);
|
|
1988
|
+
return actorRoles.some((role) => allowedRoles.includes(role));
|
|
1989
|
+
}
|
|
1990
|
+
async function permittedFields(engine, actor, operation, resource, fieldAccess, options) {
|
|
1991
|
+
if (!fieldAccess) {
|
|
1992
|
+
return [];
|
|
1993
|
+
}
|
|
1994
|
+
const fieldNames = Object.keys(fieldAccess);
|
|
1995
|
+
if (fieldNames.length === 0) {
|
|
1996
|
+
return [];
|
|
1997
|
+
}
|
|
1998
|
+
const actorRoles = await engine.resolvedRoles(actor, resource, options);
|
|
1999
|
+
let hasResourcePermission;
|
|
2000
|
+
const permitted = [];
|
|
2001
|
+
for (const fieldName of fieldNames) {
|
|
2002
|
+
const fieldDef = fieldAccess[fieldName];
|
|
2003
|
+
const allowedRoles = fieldDef?.[operation];
|
|
2004
|
+
if (allowedRoles) {
|
|
2005
|
+
if (actorRoles.some((role) => allowedRoles.includes(role))) {
|
|
2006
|
+
permitted.push(fieldName);
|
|
2007
|
+
}
|
|
2008
|
+
} else {
|
|
2009
|
+
if (hasResourcePermission === void 0) {
|
|
2010
|
+
hasResourcePermission = await engine.can(actor, operation, resource, options);
|
|
2011
|
+
}
|
|
2012
|
+
if (hasResourcePermission) {
|
|
2013
|
+
permitted.push(fieldName);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
return permitted;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// src/engine.ts
|
|
2021
|
+
var Toride = class {
|
|
2022
|
+
policy;
|
|
2023
|
+
resolvers;
|
|
2024
|
+
options;
|
|
2025
|
+
constructor(options) {
|
|
2026
|
+
this.policy = options.policy;
|
|
2027
|
+
this.resolvers = options.resolvers ?? {};
|
|
2028
|
+
this.options = options;
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Check if an actor can perform an action on a resource.
|
|
2032
|
+
* Default-deny: returns false if resource type is unknown or no grants match.
|
|
2033
|
+
*/
|
|
2034
|
+
async can(actor, action, resource, options) {
|
|
2035
|
+
const a = actor;
|
|
2036
|
+
const r = resource;
|
|
2037
|
+
const act = action;
|
|
2038
|
+
const result = await this.evaluateInternal(a, act, r, options);
|
|
2039
|
+
this.fireDecisionEvent(a, act, r, result);
|
|
2040
|
+
return result.allowed;
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* T097: Atomic policy swap. In-flight checks capture the resource block
|
|
2044
|
+
* at the start of evaluateInternal, so they complete with the old policy.
|
|
2045
|
+
* JS single-threaded nature ensures the assignment is atomic.
|
|
2046
|
+
*/
|
|
2047
|
+
setPolicy(policy) {
|
|
2048
|
+
this.policy = policy;
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* T068: Return full ExplainResult with role derivation traces,
|
|
2052
|
+
* granted permissions, matched rules, and human-readable final decision.
|
|
2053
|
+
*/
|
|
2054
|
+
async explain(actor, action, resource, options) {
|
|
2055
|
+
const a = actor;
|
|
2056
|
+
const r = resource;
|
|
2057
|
+
const act = action;
|
|
2058
|
+
const result = await this.evaluateInternal(a, act, r, options);
|
|
2059
|
+
this.fireDecisionEvent(a, act, r, result);
|
|
2060
|
+
return result;
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* T069: Check all declared permissions for a resource and return permitted ones.
|
|
2064
|
+
* Uses a shared cache across all per-action evaluations.
|
|
2065
|
+
*/
|
|
2066
|
+
async permittedActions(actor, resource, options) {
|
|
2067
|
+
const a = actor;
|
|
2068
|
+
const r = resource;
|
|
2069
|
+
const resourceBlock = this.policy.resources[r.type];
|
|
2070
|
+
if (!resourceBlock) {
|
|
2071
|
+
return [];
|
|
2072
|
+
}
|
|
2073
|
+
const sharedCache = new AttributeCache(this.resolvers);
|
|
2074
|
+
const permitted = [];
|
|
2075
|
+
for (const action of resourceBlock.permissions) {
|
|
2076
|
+
const result = await this.evaluateInternal(
|
|
2077
|
+
a,
|
|
2078
|
+
action,
|
|
2079
|
+
r,
|
|
2080
|
+
options,
|
|
2081
|
+
sharedCache
|
|
2082
|
+
);
|
|
2083
|
+
if (result.allowed) {
|
|
2084
|
+
permitted.push(action);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return permitted;
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* T083: Generate a PermissionSnapshot for a list of resources.
|
|
2091
|
+
* Calls permittedActions() for each resource and returns a map
|
|
2092
|
+
* keyed by "Type:id" with arrays of permitted action strings.
|
|
2093
|
+
* Suitable for serializing to the client via TorideClient.
|
|
2094
|
+
*/
|
|
2095
|
+
async snapshot(actor, resources, options) {
|
|
2096
|
+
return snapshot(
|
|
2097
|
+
this,
|
|
2098
|
+
actor,
|
|
2099
|
+
resources,
|
|
2100
|
+
options
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* T095: Check if an actor can perform a field-level operation on a specific field.
|
|
2105
|
+
* Restricted fields require the actor to have a role listed in field_access.
|
|
2106
|
+
* Unlisted fields are unrestricted: any actor with the resource-level permission can access them.
|
|
2107
|
+
*/
|
|
2108
|
+
async canField(actor, operation, resource, field, options) {
|
|
2109
|
+
const r = resource;
|
|
2110
|
+
const resourceBlock = this.policy.resources[r.type];
|
|
2111
|
+
if (!resourceBlock) {
|
|
2112
|
+
return false;
|
|
2113
|
+
}
|
|
2114
|
+
return canField(
|
|
2115
|
+
this,
|
|
2116
|
+
actor,
|
|
2117
|
+
operation,
|
|
2118
|
+
r,
|
|
2119
|
+
field,
|
|
2120
|
+
resourceBlock.field_access,
|
|
2121
|
+
options
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* T095: Return the list of declared field_access field names the actor can access
|
|
2126
|
+
* for the given operation. Only returns explicitly declared fields.
|
|
2127
|
+
*/
|
|
2128
|
+
async permittedFields(actor, operation, resource, options) {
|
|
2129
|
+
const r = resource;
|
|
2130
|
+
const resourceBlock = this.policy.resources[r.type];
|
|
2131
|
+
if (!resourceBlock) {
|
|
2132
|
+
return [];
|
|
2133
|
+
}
|
|
2134
|
+
return permittedFields(
|
|
2135
|
+
this,
|
|
2136
|
+
actor,
|
|
2137
|
+
operation,
|
|
2138
|
+
r,
|
|
2139
|
+
resourceBlock.field_access,
|
|
2140
|
+
options
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* T070: Return flat deduplicated list of all resolved roles (direct + derived).
|
|
2145
|
+
*/
|
|
2146
|
+
async resolvedRoles(actor, resource, options) {
|
|
2147
|
+
const a = actor;
|
|
2148
|
+
const r = resource;
|
|
2149
|
+
const resourceBlock = this.policy.resources[r.type];
|
|
2150
|
+
if (!resourceBlock) {
|
|
2151
|
+
return [];
|
|
2152
|
+
}
|
|
2153
|
+
const action = resourceBlock.permissions[0] ?? "__resolvedRoles__";
|
|
2154
|
+
const result = await this.evaluateInternal(a, action, r, options);
|
|
2155
|
+
const directRoles = result.resolvedRoles.direct;
|
|
2156
|
+
const derivedRoleNames = result.resolvedRoles.derived.map((d) => d.role);
|
|
2157
|
+
return [.../* @__PURE__ */ new Set([...directRoles, ...derivedRoleNames])];
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* T071: Evaluate multiple checks for the same actor with a shared resolver cache.
|
|
2161
|
+
* Returns boolean[] in the same order as the input checks.
|
|
2162
|
+
*/
|
|
2163
|
+
async canBatch(actor, checks, options) {
|
|
2164
|
+
if (checks.length === 0) {
|
|
2165
|
+
return [];
|
|
2166
|
+
}
|
|
2167
|
+
const a = actor;
|
|
2168
|
+
const sharedCache = new AttributeCache(this.resolvers);
|
|
2169
|
+
const results = [];
|
|
2170
|
+
for (const check of checks) {
|
|
2171
|
+
const r = check.resource;
|
|
2172
|
+
const act = check.action;
|
|
2173
|
+
const result = await this.evaluateInternal(
|
|
2174
|
+
a,
|
|
2175
|
+
act,
|
|
2176
|
+
r,
|
|
2177
|
+
options,
|
|
2178
|
+
sharedCache
|
|
2179
|
+
);
|
|
2180
|
+
this.fireDecisionEvent(a, act, r, result);
|
|
2181
|
+
results.push(result.allowed);
|
|
2182
|
+
}
|
|
2183
|
+
return results;
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* T064: Build constraint AST for partial evaluation / data filtering.
|
|
2187
|
+
* Returns ConstraintResult with unrestricted/forbidden sentinels or constraint AST.
|
|
2188
|
+
*/
|
|
2189
|
+
async buildConstraints(actor, action, resourceType, options) {
|
|
2190
|
+
const a = actor;
|
|
2191
|
+
const act = action;
|
|
2192
|
+
const rt = resourceType;
|
|
2193
|
+
const cache = new AttributeCache(this.resolvers);
|
|
2194
|
+
const constraintResult = await buildConstraints(
|
|
2195
|
+
a,
|
|
2196
|
+
act,
|
|
2197
|
+
rt,
|
|
2198
|
+
cache,
|
|
2199
|
+
this.policy,
|
|
2200
|
+
{
|
|
2201
|
+
env: options?.env,
|
|
2202
|
+
maxDerivedRoleDepth: this.options.maxDerivedRoleDepth,
|
|
2203
|
+
customEvaluators: this.options.customEvaluators
|
|
2204
|
+
}
|
|
2205
|
+
);
|
|
2206
|
+
this.fireQueryEvent(a, act, rt, constraintResult);
|
|
2207
|
+
return constraintResult;
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* T064: Translate constraint AST using an adapter.
|
|
2211
|
+
* Dispatches each constraint node to the adapter's methods.
|
|
2212
|
+
*/
|
|
2213
|
+
translateConstraints(constraints, adapter) {
|
|
2214
|
+
return translateConstraints(constraints, adapter);
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* T072: Fire onDecision audit callback via microtask (non-blocking).
|
|
2218
|
+
* Errors are silently swallowed to prevent audit failures from affecting authorization.
|
|
2219
|
+
*/
|
|
2220
|
+
fireDecisionEvent(actor, action, resource, result) {
|
|
2221
|
+
const callback = this.options.onDecision;
|
|
2222
|
+
if (!callback) return;
|
|
2223
|
+
const directRoles = result.resolvedRoles.direct;
|
|
2224
|
+
const derivedRoleNames = result.resolvedRoles.derived.map((d) => d.role);
|
|
2225
|
+
const resolvedRoles = [.../* @__PURE__ */ new Set([...directRoles, ...derivedRoleNames])];
|
|
2226
|
+
const event = {
|
|
2227
|
+
actor,
|
|
2228
|
+
action,
|
|
2229
|
+
resource,
|
|
2230
|
+
allowed: result.allowed,
|
|
2231
|
+
resolvedRoles,
|
|
2232
|
+
matchedRules: result.matchedRules.map((r) => ({
|
|
2233
|
+
effect: r.effect,
|
|
2234
|
+
matched: r.matched
|
|
2235
|
+
})),
|
|
2236
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2237
|
+
};
|
|
2238
|
+
queueMicrotask(() => {
|
|
2239
|
+
try {
|
|
2240
|
+
callback(event);
|
|
2241
|
+
} catch {
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* T072: Fire onQuery audit callback via microtask (non-blocking).
|
|
2247
|
+
* Errors are silently swallowed.
|
|
2248
|
+
*/
|
|
2249
|
+
fireQueryEvent(actor, action, resourceType, constraintResult) {
|
|
2250
|
+
const callback = this.options.onQuery;
|
|
2251
|
+
if (!callback) return;
|
|
2252
|
+
let resultType;
|
|
2253
|
+
if ("unrestricted" in constraintResult && constraintResult.unrestricted) {
|
|
2254
|
+
resultType = "unrestricted";
|
|
2255
|
+
} else if ("forbidden" in constraintResult && constraintResult.forbidden) {
|
|
2256
|
+
resultType = "forbidden";
|
|
2257
|
+
} else {
|
|
2258
|
+
resultType = "constrained";
|
|
2259
|
+
}
|
|
2260
|
+
const event = {
|
|
2261
|
+
actor,
|
|
2262
|
+
action,
|
|
2263
|
+
resourceType,
|
|
2264
|
+
resultType,
|
|
2265
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2266
|
+
};
|
|
2267
|
+
queueMicrotask(() => {
|
|
2268
|
+
try {
|
|
2269
|
+
callback(event);
|
|
2270
|
+
} catch {
|
|
2271
|
+
}
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Evaluate with full ExplainResult (shared code path for can(), explain(), and helpers).
|
|
2276
|
+
* Accepts an optional pre-existing AttributeCache for shared-cache scenarios (canBatch, permittedActions).
|
|
2277
|
+
*/
|
|
2278
|
+
async evaluateInternal(actor, action, resource, checkOptions, existingCache) {
|
|
2279
|
+
const resourceBlock = this.policy.resources[resource.type];
|
|
2280
|
+
if (!resourceBlock) {
|
|
2281
|
+
return {
|
|
2282
|
+
allowed: false,
|
|
2283
|
+
resolvedRoles: { direct: [], derived: [] },
|
|
2284
|
+
grantedPermissions: [],
|
|
2285
|
+
matchedRules: [],
|
|
2286
|
+
finalDecision: `Denied: unknown resource type "${resource.type}"`
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
const cache = existingCache ?? new AttributeCache(this.resolvers);
|
|
2290
|
+
const env = checkOptions?.env ?? {};
|
|
2291
|
+
try {
|
|
2292
|
+
return await evaluate(actor, action, resource, resourceBlock, cache, this.policy, {
|
|
2293
|
+
maxDerivedRoleDepth: this.options.maxDerivedRoleDepth,
|
|
2294
|
+
maxConditionDepth: this.options.maxConditionDepth,
|
|
2295
|
+
customEvaluators: this.options.customEvaluators,
|
|
2296
|
+
env
|
|
2297
|
+
});
|
|
2298
|
+
} catch {
|
|
2299
|
+
return {
|
|
2300
|
+
allowed: false,
|
|
2301
|
+
resolvedRoles: { direct: [], derived: [] },
|
|
2302
|
+
grantedPermissions: [],
|
|
2303
|
+
matchedRules: [],
|
|
2304
|
+
finalDecision: `Denied: evaluation error (fail-closed)`
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
function createToride(options) {
|
|
2310
|
+
return new Toride(options);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
// src/testing/mock-resolver.ts
|
|
2314
|
+
function createMockResolver(testCase) {
|
|
2315
|
+
const { resolvers: mockData } = testCase;
|
|
2316
|
+
if (!mockData) return {};
|
|
2317
|
+
const typeSet = /* @__PURE__ */ new Set();
|
|
2318
|
+
for (const key of Object.keys(mockData)) {
|
|
2319
|
+
const colonIndex = key.indexOf(":");
|
|
2320
|
+
if (colonIndex > 0) {
|
|
2321
|
+
typeSet.add(key.substring(0, colonIndex));
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
const resolvers = {};
|
|
2325
|
+
for (const type of typeSet) {
|
|
2326
|
+
resolvers[type] = async (ref) => {
|
|
2327
|
+
const key = `${ref.type}:${ref.id}`;
|
|
2328
|
+
return mockData[key] ?? {};
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
return resolvers;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// src/testing/test-runner.ts
|
|
2335
|
+
async function runTestCases(policy, tests) {
|
|
2336
|
+
const results = [];
|
|
2337
|
+
for (const tc of tests) {
|
|
2338
|
+
const resolvers = createMockResolver(tc);
|
|
2339
|
+
const engine = new Toride({ policy, resolvers });
|
|
2340
|
+
const allowed = await engine.can(tc.actor, tc.action, tc.resource);
|
|
2341
|
+
const actual = allowed ? "allow" : "deny";
|
|
2342
|
+
results.push({
|
|
2343
|
+
name: tc.name,
|
|
2344
|
+
passed: actual === tc.expected,
|
|
2345
|
+
expected: tc.expected,
|
|
2346
|
+
actual
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
return results;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
export {
|
|
2353
|
+
PolicySchema,
|
|
2354
|
+
ValidationError,
|
|
2355
|
+
CycleError,
|
|
2356
|
+
DepthLimitError,
|
|
2357
|
+
validatePolicy,
|
|
2358
|
+
validatePolicyResult,
|
|
2359
|
+
validatePolicyStrict,
|
|
2360
|
+
parseInlineTests,
|
|
2361
|
+
parseTestFile,
|
|
2362
|
+
Toride,
|
|
2363
|
+
createToride,
|
|
2364
|
+
createMockResolver,
|
|
2365
|
+
runTestCases
|
|
2366
|
+
};
|