@nmnmcc/ability 2.0.7 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -29
- package/dist/Authorizer.d.ts +21 -12
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +121 -19
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +32 -21
- package/dist/Condition.js.map +1 -1
- package/dist/Domain.d.ts +9 -0
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +5 -0
- package/dist/Domain.js.map +1 -1
- package/dist/Limits.d.ts +44 -13
- package/dist/Limits.d.ts.map +1 -1
- package/dist/Limits.js +52 -22
- package/dist/Limits.js.map +1 -1
- package/dist/Policy.d.ts +102 -18
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +7 -6
- package/dist/Policy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/internal/algebra.js +2 -2
- package/dist/internal/analysis.js +81 -75
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/authorizer.d.ts.map +1 -1
- package/dist/internal/authorizer.js +11 -7
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +86 -64
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts +33 -10
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +170 -77
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +27 -31
- package/dist/internal/decide.js.map +1 -1
- package/dist/internal/domain.js +19 -5
- package/dist/internal/domain.js.map +1 -1
- package/dist/internal/json.js +120 -76
- package/dist/internal/json.js.map +1 -1
- package/dist/internal/limits.d.ts +28 -0
- package/dist/internal/limits.d.ts.map +1 -1
- package/dist/internal/limits.js +75 -16
- package/dist/internal/limits.js.map +1 -1
- package/dist/internal/nodes.d.ts.map +1 -1
- package/dist/internal/nodes.js +53 -44
- package/dist/internal/nodes.js.map +1 -1
- package/dist/internal/patterns.js +50 -15
- package/dist/internal/patterns.js.map +1 -1
- package/dist/internal/policy-json.js +169 -127
- package/dist/internal/policy-json.js.map +1 -1
- package/dist/internal/policy-nodes.d.ts.map +1 -1
- package/dist/internal/policy-nodes.js +16 -13
- package/dist/internal/policy-nodes.js.map +1 -1
- package/dist/internal/policy.d.ts.map +1 -1
- package/dist/internal/policy.js +38 -22
- package/dist/internal/policy.js.map +1 -1
- package/dist/internal/resource.js +1 -1
- package/dist/internal/rule.d.ts.map +1 -1
- package/dist/internal/rule.js +6 -5
- package/dist/internal/rule.js.map +1 -1
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +106 -92
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/trampoline.js +19 -5
- package/dist/internal/trampoline.js.map +1 -1
- package/package.json +1 -1
- package/src/Authorizer.ts +30 -20
- package/src/Condition.ts +317 -86
- package/src/Domain.ts +9 -0
- package/src/Limits.ts +69 -29
- package/src/Policy.ts +143 -21
- package/src/internal/analysis.ts +154 -124
- package/src/internal/authorizer.ts +29 -14
- package/src/internal/bind.ts +193 -136
- package/src/internal/catalog.ts +320 -118
- package/src/internal/decide.ts +61 -50
- package/src/internal/domain.ts +39 -3
- package/src/internal/json.ts +212 -117
- package/src/internal/limits.ts +101 -31
- package/src/internal/nodes.ts +65 -47
- package/src/internal/patterns.ts +79 -25
- package/src/internal/policy-json.ts +266 -197
- package/src/internal/policy-nodes.ts +31 -23
- package/src/internal/policy.ts +63 -23
- package/src/internal/rule.ts +16 -3
- package/src/internal/search.ts +186 -136
- package/src/internal/trampoline.ts +51 -4
package/src/internal/decide.ts
CHANGED
|
@@ -19,7 +19,8 @@ import type * as Policy from "../Policy.ts"
|
|
|
19
19
|
import type * as Resource from "../Resource.ts"
|
|
20
20
|
import * as Catalog from "./catalog.ts"
|
|
21
21
|
import * as DomainData from "./domain.ts"
|
|
22
|
-
import
|
|
22
|
+
import * as Place from "./place.ts"
|
|
23
|
+
import * as PolicyNodes from "./policy-nodes.ts"
|
|
23
24
|
import { nodeOf } from "./policy.ts"
|
|
24
25
|
import * as ResourceData from "./resource.ts"
|
|
25
26
|
import * as RuleData from "./rule.ts"
|
|
@@ -39,14 +40,23 @@ interface DecideContext {
|
|
|
39
40
|
readonly descriptor: Resource.Descriptor
|
|
40
41
|
/** The test of `RuleData.fitsOf` for the catalog, for this decision. */
|
|
41
42
|
readonly fits: (pattern: string) => boolean
|
|
43
|
+
/** The most parts of a field path in the domain. */
|
|
44
|
+
readonly parts: number
|
|
42
45
|
/** The rules that were tested, for {@link explain}. */
|
|
43
46
|
readonly trace: Ref.Ref<Chunk.Chunk<Step>> | undefined
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The place of a node or a rule in a tree: the index of each child, then
|
|
51
|
+
* the index of the rule. It shares the path of its parent, and a decision
|
|
52
|
+
* makes the full path only for a step of `explain`: see `Place`.
|
|
53
|
+
*/
|
|
54
|
+
type At = Place.Place | undefined
|
|
55
|
+
|
|
46
56
|
const ruleApplies = (
|
|
47
57
|
value: RuleData.Rule,
|
|
48
58
|
context: DecideContext,
|
|
49
|
-
|
|
59
|
+
at: Place.Place,
|
|
50
60
|
): Effect.Effect<boolean, unknown, unknown> => {
|
|
51
61
|
if (!RuleData.matchesTarget(value, context)) {
|
|
52
62
|
return Effect.succeed(false)
|
|
@@ -62,7 +72,14 @@ const ruleApplies = (
|
|
|
62
72
|
? test
|
|
63
73
|
: Effect.tap(test, (applies) =>
|
|
64
74
|
// A chunk adds a step with no copy of the steps before.
|
|
65
|
-
Ref.update(
|
|
75
|
+
Ref.update(
|
|
76
|
+
trace,
|
|
77
|
+
Chunk.append({
|
|
78
|
+
path: Place.pathOf(at) as ReadonlyArray<number>,
|
|
79
|
+
rule: typed(value),
|
|
80
|
+
applies,
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
66
83
|
)
|
|
67
84
|
}
|
|
68
85
|
|
|
@@ -80,11 +97,11 @@ const fromRule = (value: RuleData.Rule): Decision.Decision =>
|
|
|
80
97
|
const firstApplying = (
|
|
81
98
|
candidates: ReadonlyArray<readonly [RuleData.Rule, number]>,
|
|
82
99
|
context: DecideContext,
|
|
83
|
-
|
|
100
|
+
at: At,
|
|
84
101
|
): Effect.Effect<Option.Option<RuleData.Rule>, unknown, unknown> =>
|
|
85
102
|
Effect.map(
|
|
86
103
|
Effect.findFirst(candidates, ([value, index]) =>
|
|
87
|
-
ruleApplies(value, context,
|
|
104
|
+
ruleApplies(value, context, Place.at(at, index)),
|
|
88
105
|
),
|
|
89
106
|
Option.map(([value]) => value),
|
|
90
107
|
)
|
|
@@ -99,13 +116,13 @@ const orElse = <A, E, R>(
|
|
|
99
116
|
const decideRules = (
|
|
100
117
|
node: PolicyNodes.Rules,
|
|
101
118
|
context: DecideContext,
|
|
102
|
-
|
|
119
|
+
at: At,
|
|
103
120
|
): Effect.Effect<Decision.Decision, unknown, unknown> => {
|
|
104
121
|
const indexed = Arr.map(node.rules, (value, index) => [value, index] as const)
|
|
105
122
|
const denies = Arr.filter(indexed, ([value]) => value._tag === "Deny")
|
|
106
123
|
const allows = Arr.filter(indexed, ([value]) => value._tag === "Allow")
|
|
107
124
|
const first = (candidates: ReadonlyArray<readonly [RuleData.Rule, number]>) =>
|
|
108
|
-
firstApplying(candidates, context,
|
|
125
|
+
firstApplying(candidates, context, at)
|
|
109
126
|
const found = (() => {
|
|
110
127
|
switch (node.algorithm) {
|
|
111
128
|
case "denyOverrides":
|
|
@@ -138,7 +155,7 @@ type WalkStep<S> =
|
|
|
138
155
|
const walkChildren = <S>(
|
|
139
156
|
children: ReadonlyArray<PolicyNodes.Node>,
|
|
140
157
|
context: DecideContext,
|
|
141
|
-
|
|
158
|
+
at: At,
|
|
142
159
|
start: S,
|
|
143
160
|
step: (state: S, decision: Decision.Decision) => WalkStep<S>,
|
|
144
161
|
end: (state: S) => Decision.Decision,
|
|
@@ -150,7 +167,11 @@ const walkChildren = <S>(
|
|
|
150
167
|
index === children.length
|
|
151
168
|
? Effect.succeed(end(state))
|
|
152
169
|
: Effect.flatMap(
|
|
153
|
-
|
|
170
|
+
// Each call for a child is a step of the Effect, so a deep tree
|
|
171
|
+
// does not fill the stack.
|
|
172
|
+
Effect.suspend(() =>
|
|
173
|
+
decideNode(children[index]!, context, Place.at(at, index)),
|
|
174
|
+
),
|
|
154
175
|
(decision) => {
|
|
155
176
|
const next = step(state, decision)
|
|
156
177
|
return next._tag === "Stop"
|
|
@@ -176,17 +197,17 @@ const next = <S>(state: S): WalkStep<S> => ({ _tag: "Next", state })
|
|
|
176
197
|
export const decideNode = (
|
|
177
198
|
node: PolicyNodes.Node,
|
|
178
199
|
context: DecideContext,
|
|
179
|
-
|
|
200
|
+
at: At,
|
|
180
201
|
): Effect.Effect<Decision.Decision, unknown, unknown> => {
|
|
181
202
|
switch (node._tag) {
|
|
182
203
|
case "Rules":
|
|
183
|
-
return decideRules(node, context,
|
|
204
|
+
return decideRules(node, context, at)
|
|
184
205
|
case "All":
|
|
185
206
|
// One deny is enough to deny. Every child must allow.
|
|
186
207
|
return walkChildren(
|
|
187
208
|
node.policies,
|
|
188
209
|
context,
|
|
189
|
-
|
|
210
|
+
at,
|
|
190
211
|
{
|
|
191
212
|
firstAllow: Option.none<Decision.Decision>(),
|
|
192
213
|
allAllow: node.policies.length > 0,
|
|
@@ -212,7 +233,7 @@ export const decideNode = (
|
|
|
212
233
|
return walkChildren(
|
|
213
234
|
node.policies,
|
|
214
235
|
context,
|
|
215
|
-
|
|
236
|
+
at,
|
|
216
237
|
Option.none<Decision.Decision>(),
|
|
217
238
|
(firstDeny, decision) =>
|
|
218
239
|
decision._tag === "Allow"
|
|
@@ -231,7 +252,7 @@ export const decideNode = (
|
|
|
231
252
|
return walkChildren(
|
|
232
253
|
node.policies,
|
|
233
254
|
context,
|
|
234
|
-
|
|
255
|
+
at,
|
|
235
256
|
undefined,
|
|
236
257
|
(_, decision) =>
|
|
237
258
|
decision._tag === "NotApplicable" ? next(undefined) : stop(decision),
|
|
@@ -240,9 +261,9 @@ export const decideNode = (
|
|
|
240
261
|
case "Ceiling":
|
|
241
262
|
// A deny of the cap wins. Else the policy decides.
|
|
242
263
|
return Effect.filterOrElse(
|
|
243
|
-
decideNode(node.cap, context,
|
|
264
|
+
Effect.suspend(() => decideNode(node.cap, context, Place.at(at, 0))),
|
|
244
265
|
(capped) => capped._tag === "Deny",
|
|
245
|
-
() => decideNode(node.policy, context,
|
|
266
|
+
() => decideNode(node.policy, context, Place.at(at, 1)),
|
|
246
267
|
)
|
|
247
268
|
}
|
|
248
269
|
}
|
|
@@ -254,10 +275,11 @@ export const decideNode = (
|
|
|
254
275
|
export const fitsKind = (
|
|
255
276
|
descriptor: Resource.Descriptor,
|
|
256
277
|
request: { readonly action: string; readonly field?: string | undefined },
|
|
278
|
+
parts: number,
|
|
257
279
|
): boolean =>
|
|
258
280
|
descriptor.actions.includes(request.action) &&
|
|
259
281
|
(request.field === undefined ||
|
|
260
|
-
(Catalog.lookup(descriptor.catalog, request.field)?.listed ?? false))
|
|
282
|
+
(Catalog.lookup(descriptor.catalog, request.field, parts)?.listed ?? false))
|
|
261
283
|
|
|
262
284
|
const makeContext = (
|
|
263
285
|
domain: Domain.Any,
|
|
@@ -270,7 +292,7 @@ const makeContext = (
|
|
|
270
292
|
if (
|
|
271
293
|
kind === undefined ||
|
|
272
294
|
descriptor === undefined ||
|
|
273
|
-
!fitsKind(descriptor, request)
|
|
295
|
+
!fitsKind(descriptor, request, domain.limits.fieldPathParts)
|
|
274
296
|
) {
|
|
275
297
|
return undefined
|
|
276
298
|
}
|
|
@@ -284,7 +306,8 @@ const makeContext = (
|
|
|
284
306
|
path: descriptor.path,
|
|
285
307
|
},
|
|
286
308
|
descriptor,
|
|
287
|
-
fits: RuleData.fitsOf(descriptor.catalog),
|
|
309
|
+
fits: RuleData.fitsOf(descriptor.catalog, domain.limits.fieldPathParts),
|
|
310
|
+
parts: domain.limits.fieldPathParts,
|
|
288
311
|
trace,
|
|
289
312
|
}
|
|
290
313
|
}
|
|
@@ -298,7 +321,7 @@ export const decideAny = (
|
|
|
298
321
|
const context = makeContext(policy.domain, request, undefined)
|
|
299
322
|
return context === undefined
|
|
300
323
|
? Effect.succeed(Decision.NotApplicable)
|
|
301
|
-
: decideNode(nodeOf(policy), context,
|
|
324
|
+
: decideNode(nodeOf(policy), context, undefined)
|
|
302
325
|
})
|
|
303
326
|
|
|
304
327
|
/** Gives the decision of a policy with no predicates, at once. */
|
|
@@ -326,7 +349,7 @@ export const explain = (
|
|
|
326
349
|
if (context === undefined) {
|
|
327
350
|
return { decision: Decision.NotApplicable, steps: [] }
|
|
328
351
|
}
|
|
329
|
-
const decision = yield* decideNode(nodeOf(policy), context,
|
|
352
|
+
const decision = yield* decideNode(nodeOf(policy), context, undefined)
|
|
330
353
|
return { decision, steps: Chunk.toReadonlyArray(yield* Ref.get(trace)) }
|
|
331
354
|
})
|
|
332
355
|
|
|
@@ -334,35 +357,19 @@ export const explain = (
|
|
|
334
357
|
* Tests if a deny rule of the request names a part inside `field`.
|
|
335
358
|
*/
|
|
336
359
|
const deniesInside = (
|
|
337
|
-
|
|
360
|
+
rules: ReadonlyArray<RuleData.Rule>,
|
|
338
361
|
target: { readonly action: string; readonly kind: string },
|
|
339
362
|
field: string,
|
|
340
|
-
): boolean =>
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
(value.actions.includes("*") ||
|
|
351
|
-
value.actions.includes(target.action)) &&
|
|
352
|
-
(value.resources.includes("*") ||
|
|
353
|
-
value.resources.includes(target.kind)),
|
|
354
|
-
)
|
|
355
|
-
case "All":
|
|
356
|
-
case "Any":
|
|
357
|
-
case "FirstOf":
|
|
358
|
-
return node.policies.some((child) => deniesInside(child, target, field))
|
|
359
|
-
case "Ceiling":
|
|
360
|
-
return (
|
|
361
|
-
deniesInside(node.cap, target, field) ||
|
|
362
|
-
deniesInside(node.policy, target, field)
|
|
363
|
-
)
|
|
364
|
-
}
|
|
365
|
-
}
|
|
363
|
+
): boolean =>
|
|
364
|
+
Arr.some(
|
|
365
|
+
rules,
|
|
366
|
+
(value) =>
|
|
367
|
+
value._tag === "Deny" &&
|
|
368
|
+
value.fields !== undefined &&
|
|
369
|
+
value.fields.some((pattern) => RuleData.matchesInside(pattern, field)) &&
|
|
370
|
+
(value.actions.includes("*") || value.actions.includes(target.action)) &&
|
|
371
|
+
(value.resources.includes("*") || value.resources.includes(target.kind)),
|
|
372
|
+
)
|
|
366
373
|
|
|
367
374
|
/** Gives the fields of a resource value that the policy lets a request use. */
|
|
368
375
|
export const permittedFields = (
|
|
@@ -377,9 +384,13 @@ export const permittedFields = (
|
|
|
377
384
|
if (descriptor === undefined) {
|
|
378
385
|
return Effect.succeed([])
|
|
379
386
|
}
|
|
387
|
+
// The rules of the tree, in one flat list for all the fields.
|
|
388
|
+
const rules = policy.pipe(nodeOf, PolicyNodes.rulesOf)
|
|
380
389
|
// The fields with names only: a list can not name every key of a record.
|
|
381
390
|
const listed = Arr.filter(
|
|
382
|
-
|
|
391
|
+
// The list for the limit of the parts of a path of the domain, as the
|
|
392
|
+
// decision of each field reads it.
|
|
393
|
+
Catalog.fieldsOf(descriptor.catalog, policy.domain.limits.fieldPathParts),
|
|
383
394
|
(field) =>
|
|
384
395
|
field.listed &&
|
|
385
396
|
Arr.every(field.path, (segment) => typeof segment === "string"),
|
|
@@ -393,7 +404,7 @@ export const permittedFields = (
|
|
|
393
404
|
!(
|
|
394
405
|
field.open &&
|
|
395
406
|
deniesInside(
|
|
396
|
-
|
|
407
|
+
rules,
|
|
397
408
|
{ ...request, kind: descriptor.kind },
|
|
398
409
|
field.text,
|
|
399
410
|
)
|
package/src/internal/domain.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type * as Schema from "effect/Schema"
|
|
|
9
9
|
import type * as Domain from "../Domain.ts"
|
|
10
10
|
import type * as Resource from "../Resource.ts"
|
|
11
11
|
import * as Catalog from "./catalog.ts"
|
|
12
|
+
import * as Limits from "./limits.ts"
|
|
12
13
|
import * as ResourceData from "./resource.ts"
|
|
13
14
|
|
|
14
15
|
/** The class of every domain value. `isDomain` uses it to know a domain. */
|
|
@@ -17,14 +18,38 @@ class DomainData extends Data.Class<{
|
|
|
17
18
|
readonly principalCatalog: Catalog.Catalog
|
|
18
19
|
readonly resources: HashMap.HashMap<string, Resource.Descriptor>
|
|
19
20
|
readonly meta: Schema.Top | undefined
|
|
21
|
+
readonly limits: Limits.Limits
|
|
20
22
|
}> {}
|
|
21
23
|
|
|
22
|
-
/**
|
|
24
|
+
/** Stops a catalog with more fields in its list than the domain takes. */
|
|
25
|
+
const checkCatalog = (
|
|
26
|
+
catalog: Catalog.Catalog,
|
|
27
|
+
limits: Limits.Limits,
|
|
28
|
+
name: string,
|
|
29
|
+
): void => {
|
|
30
|
+
const fields = Catalog.fieldsOf(catalog, limits.fieldPathParts).length
|
|
31
|
+
if (fields > limits.catalogFields) {
|
|
32
|
+
throw Limits.exceeded(
|
|
33
|
+
name,
|
|
34
|
+
"catalogFields",
|
|
35
|
+
"fields",
|
|
36
|
+
fields,
|
|
37
|
+
limits.catalogFields,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Makes a domain. Each resource must have its own kind name. The limits that
|
|
44
|
+
* the domain does not set have their defaults.
|
|
45
|
+
*/
|
|
23
46
|
export const make = (options: {
|
|
24
47
|
readonly principal: Schema.Top
|
|
25
48
|
readonly resources: ReadonlyArray<Resource.Any>
|
|
26
49
|
readonly meta?: Schema.Top
|
|
50
|
+
readonly limits?: Partial<Limits.Limits>
|
|
27
51
|
}): DomainData => {
|
|
52
|
+
const limits = Limits.make(options.limits)
|
|
28
53
|
const descriptors = Arr.map(options.resources, ResourceData.descriptor)
|
|
29
54
|
const kinds = Arr.map(descriptors, (descriptor) => descriptor.kind)
|
|
30
55
|
// Two resources with one kind name is a mistake in the code, not in the
|
|
@@ -46,12 +71,23 @@ export const make = (options: {
|
|
|
46
71
|
),
|
|
47
72
|
)
|
|
48
73
|
// The catalog checks the field names of the principal, so a bad name is a
|
|
49
|
-
// defect when the domain is made.
|
|
74
|
+
// defect when the domain is made. A Schema with more fields than the
|
|
75
|
+
// domain takes is a defect too.
|
|
76
|
+
const principalCatalog = Catalog.catalogOf(options.principal.ast)
|
|
77
|
+
checkCatalog(principalCatalog, limits, "The Schema of the principal")
|
|
78
|
+
Arr.forEach(descriptors, (descriptor) =>
|
|
79
|
+
checkCatalog(
|
|
80
|
+
descriptor.catalog,
|
|
81
|
+
limits,
|
|
82
|
+
`The Schema of the resource "${descriptor.kind}"`,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
50
85
|
return new DomainData({
|
|
51
86
|
principal: options.principal,
|
|
52
|
-
principalCatalog
|
|
87
|
+
principalCatalog,
|
|
53
88
|
resources,
|
|
54
89
|
meta: options.meta,
|
|
90
|
+
limits,
|
|
55
91
|
})
|
|
56
92
|
}
|
|
57
93
|
|