@nmnmcc/ability 2.0.6 → 2.1.1
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 +56 -32
- 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 +118 -18
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +27 -18
- 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 +95 -12
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/internal/algebra.js +101 -74
- package/dist/internal/algebra.js.map +1 -1
- 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 +16 -11
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +119 -80
- 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 +153 -78
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +25 -29
- 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 +7 -0
- package/dist/internal/nodes.d.ts.map +1 -1
- package/dist/internal/nodes.js +86 -47
- 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 +17 -14
- 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 +89 -0
- package/dist/internal/trampoline.js.map +1 -0
- package/package.json +1 -1
- package/src/Authorizer.ts +30 -20
- package/src/Condition.ts +308 -83
- package/src/Domain.ts +9 -0
- package/src/Limits.ts +69 -29
- package/src/Policy.ts +131 -15
- package/src/internal/algebra.ts +218 -135
- package/src/internal/analysis.ts +154 -122
- package/src/internal/authorizer.ts +34 -16
- package/src/internal/bind.ts +279 -148
- package/src/internal/catalog.ts +307 -133
- package/src/internal/decide.ts +58 -49
- 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 +101 -59
- package/src/internal/patterns.ts +72 -26
- package/src/internal/policy-json.ts +266 -197
- package/src/internal/policy-nodes.ts +33 -24
- 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 +194 -0
package/src/internal/nodes.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type * as Effect from "effect/Effect"
|
|
|
23
23
|
import { hasProperty } from "effect/Predicate"
|
|
24
24
|
import * as Schema from "effect/Schema"
|
|
25
25
|
import * as Limits from "./limits.ts"
|
|
26
|
+
import * as Trampoline from "./trampoline.ts"
|
|
26
27
|
|
|
27
28
|
/** A value that a condition can compare. */
|
|
28
29
|
export type Scalar = string | number | boolean | null
|
|
@@ -83,14 +84,17 @@ const checkPath = (path: unknown): string => {
|
|
|
83
84
|
if (typeof path !== "string") {
|
|
84
85
|
throw new Error("A field path must be a string")
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
// A node does not know its domain, so it checks the ceiling. The binding
|
|
88
|
+
// to a domain checks the limit of the domain.
|
|
89
|
+
const most = Limits.ceilings.fieldPathParts
|
|
90
|
+
const parts = path.split(".", most + 1).length
|
|
91
|
+
if (parts > most) {
|
|
88
92
|
throw Limits.exceeded(
|
|
89
93
|
"A field path",
|
|
90
94
|
"fieldPathParts",
|
|
91
95
|
"parts",
|
|
92
96
|
parts,
|
|
93
|
-
|
|
97
|
+
most,
|
|
94
98
|
)
|
|
95
99
|
}
|
|
96
100
|
return path
|
|
@@ -124,37 +128,46 @@ const checkOperands = (values: unknown): ReadonlyArray<Operand> => {
|
|
|
124
128
|
|
|
125
129
|
interface Facts {
|
|
126
130
|
readonly size: number
|
|
131
|
+
readonly weight: number
|
|
127
132
|
readonly depth: number
|
|
128
133
|
readonly opaque: boolean
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
const leaf: Facts = { size: 1, depth: 1, opaque: false }
|
|
136
|
+
const leaf: Facts = { size: 1, weight: 1, depth: 1, opaque: false }
|
|
132
137
|
|
|
133
|
-
/**
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
/**
|
|
139
|
+
* The facts of a test with values, as `In` and `Under`. Each value counts in
|
|
140
|
+
* the weight, since a walk that reads the test reads its values too. It stops
|
|
141
|
+
* a test past the limits.
|
|
142
|
+
*/
|
|
143
|
+
const valuesFacts = (values: ReadonlyArray<Operand>): Facts =>
|
|
144
|
+
checkWeight({ ...leaf, weight: 1 + values.length })
|
|
145
|
+
|
|
146
|
+
const checkWeight = (facts: Facts): Facts => {
|
|
147
|
+
// A node does not know its domain, so it checks the ceiling. The binding
|
|
148
|
+
// to a domain, and a decoder, check their limits.
|
|
149
|
+
if (facts.weight > Limits.ceilings.conditionNodes) {
|
|
137
150
|
throw Limits.exceeded(
|
|
138
151
|
"A condition",
|
|
139
152
|
"conditionNodes",
|
|
140
|
-
"nodes",
|
|
141
|
-
|
|
142
|
-
Limits.conditionNodes,
|
|
153
|
+
"nodes and values",
|
|
154
|
+
facts.weight,
|
|
155
|
+
Limits.ceilings.conditionNodes,
|
|
143
156
|
)
|
|
144
157
|
}
|
|
158
|
+
return facts
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The facts of a node with children. It stops a node past the limits. */
|
|
162
|
+
const factsOf = (children: ReadonlyArray<Node>): Facts => {
|
|
163
|
+
const size = Arr.reduce(children, 1, (total, child) => total + child.size)
|
|
164
|
+
const weight = Arr.reduce(children, 1, (total, child) => total + child.weight)
|
|
165
|
+
checkWeight({ ...leaf, weight })
|
|
145
166
|
const depth =
|
|
146
167
|
1 + Arr.reduce(children, 0, (top, child) => Math.max(top, child.depth))
|
|
147
|
-
if (depth > Limits.conditionDepth) {
|
|
148
|
-
throw Limits.exceeded(
|
|
149
|
-
"A condition",
|
|
150
|
-
"conditionDepth",
|
|
151
|
-
"levels",
|
|
152
|
-
depth,
|
|
153
|
-
Limits.conditionDepth,
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
168
|
return {
|
|
157
169
|
size,
|
|
170
|
+
weight,
|
|
158
171
|
depth,
|
|
159
172
|
opaque: Arr.some(children, (child) => child.opaque),
|
|
160
173
|
}
|
|
@@ -183,6 +196,15 @@ abstract class Base extends Data.Class {
|
|
|
183
196
|
return this.#facts.size
|
|
184
197
|
}
|
|
185
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The number of nodes and of the values of their `In` and `Under` tests,
|
|
201
|
+
* with a shared child counted for each place. A walk that reads the tests
|
|
202
|
+
* does this much work, so the limits of the nodes count it.
|
|
203
|
+
*/
|
|
204
|
+
get weight(): number {
|
|
205
|
+
return this.#facts.weight
|
|
206
|
+
}
|
|
207
|
+
|
|
186
208
|
/** The number of levels. */
|
|
187
209
|
get depth(): number {
|
|
188
210
|
return this.#facts.depth
|
|
@@ -244,10 +266,8 @@ export class In extends Base {
|
|
|
244
266
|
readonly field: string
|
|
245
267
|
readonly values: ReadonlyArray<Operand>
|
|
246
268
|
}) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
leaf,
|
|
250
|
-
)
|
|
269
|
+
const values = checkOperands(props.values)
|
|
270
|
+
super({ field: checkPath(props.field), values }, valuesFacts(values))
|
|
251
271
|
}
|
|
252
272
|
}
|
|
253
273
|
|
|
@@ -265,7 +285,8 @@ export class Under extends Base {
|
|
|
265
285
|
readonly _tag = "Under"
|
|
266
286
|
declare readonly prefix: ReadonlyArray<Operand>
|
|
267
287
|
constructor(props: { readonly prefix: ReadonlyArray<Operand> }) {
|
|
268
|
-
|
|
288
|
+
const prefix = checkOperands(props.prefix)
|
|
289
|
+
super({ prefix }, valuesFacts(prefix))
|
|
269
290
|
}
|
|
270
291
|
}
|
|
271
292
|
|
|
@@ -333,7 +354,7 @@ export class Predicate extends Base {
|
|
|
333
354
|
}
|
|
334
355
|
super(
|
|
335
356
|
{ name: props.name, run: props.run },
|
|
336
|
-
{ size: 1, depth: 1, opaque: true },
|
|
357
|
+
{ size: 1, weight: 1, depth: 1, opaque: true },
|
|
337
358
|
)
|
|
338
359
|
}
|
|
339
360
|
}
|
|
@@ -358,40 +379,61 @@ const operandJson = (operand: Operand): unknown =>
|
|
|
358
379
|
}
|
|
359
380
|
: { _tag: "Principal", path: operand.path }
|
|
360
381
|
|
|
361
|
-
/**
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Gives the JSON form of a condition with no predicate. The walk takes the
|
|
384
|
+
* steps of a trampoline, so a deep condition does not fill the stack.
|
|
385
|
+
*/
|
|
386
|
+
export const toJson = (root: Node): unknown =>
|
|
387
|
+
Trampoline.run(root.depth, (R) => {
|
|
388
|
+
const go = (node: Node): Trampoline.Step<unknown> => {
|
|
389
|
+
switch (node._tag) {
|
|
390
|
+
case "True":
|
|
391
|
+
case "False":
|
|
392
|
+
return R.succeed({ _tag: node._tag })
|
|
393
|
+
case "Compare":
|
|
394
|
+
return R.succeed({
|
|
395
|
+
_tag: "Compare",
|
|
396
|
+
op: node.op,
|
|
397
|
+
field: node.field,
|
|
398
|
+
value: operandJson(node.value),
|
|
399
|
+
})
|
|
400
|
+
case "In":
|
|
401
|
+
return R.succeed({
|
|
402
|
+
_tag: "In",
|
|
403
|
+
field: node.field,
|
|
404
|
+
values: Arr.map(node.values, operandJson),
|
|
405
|
+
})
|
|
406
|
+
case "Exists":
|
|
407
|
+
return R.succeed({ _tag: "Exists", field: node.field })
|
|
408
|
+
case "Under":
|
|
409
|
+
return R.succeed({
|
|
410
|
+
_tag: "Under",
|
|
411
|
+
prefix: Arr.map(node.prefix, operandJson),
|
|
412
|
+
})
|
|
413
|
+
case "And":
|
|
414
|
+
case "Or": {
|
|
415
|
+
const tag = node._tag
|
|
416
|
+
return R.map(
|
|
417
|
+
R.forEach(node.conditions, (child) => R.suspend(() => go(child))),
|
|
418
|
+
(conditions) => ({ _tag: tag, conditions }),
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
case "Not":
|
|
422
|
+
return R.map(
|
|
423
|
+
R.suspend(() => go(node.condition)),
|
|
424
|
+
(condition) => ({
|
|
425
|
+
_tag: "Not",
|
|
426
|
+
condition,
|
|
427
|
+
}),
|
|
428
|
+
)
|
|
429
|
+
case "Predicate":
|
|
430
|
+
throw new Error(
|
|
431
|
+
`The predicate "${node.name}" can not become JSON. A condition with a predicate has no JSON form.`,
|
|
432
|
+
)
|
|
379
433
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return { _tag: "Under", prefix: Arr.map(node.prefix, operandJson) }
|
|
384
|
-
case "And":
|
|
385
|
-
case "Or":
|
|
386
|
-
return { _tag: node._tag, conditions: Arr.map(node.conditions, toJson) }
|
|
387
|
-
case "Not":
|
|
388
|
-
return { _tag: "Not", condition: toJson(node.condition) }
|
|
389
|
-
case "Predicate":
|
|
390
|
-
throw new Error(
|
|
391
|
-
`The predicate "${node.name}" can not become JSON. A condition with a predicate has no JSON form.`,
|
|
392
|
-
)
|
|
393
|
-
}
|
|
394
|
-
}
|
|
434
|
+
}
|
|
435
|
+
return go(root)
|
|
436
|
+
})
|
|
395
437
|
|
|
396
438
|
/** Tests if a value is a node. Only a constructor of a node makes one. */
|
|
397
439
|
export const isNode = (u: unknown): u is Node => u instanceof Base
|
package/src/internal/patterns.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* number of names times the number of parts.
|
|
15
15
|
*/
|
|
16
16
|
import * as Arr from "effect/Array"
|
|
17
|
+
import * as Option from "effect/Option"
|
|
17
18
|
import * as Result from "effect/Result"
|
|
18
19
|
import * as Catalog from "./catalog.ts"
|
|
19
20
|
import * as Limits from "./limits.ts"
|
|
@@ -39,13 +40,17 @@ const partOf = (text: string): Part =>
|
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Reads a pattern. It fails with the problem when the pattern has `**`
|
|
42
|
-
* inside a part, or has more parts than `patternParts
|
|
43
|
+
* inside a part, or has more parts than `parts`, the `patternParts` of a
|
|
44
|
+
* domain.
|
|
43
45
|
*/
|
|
44
|
-
export const parse = (
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
export const parse = (
|
|
47
|
+
text: string,
|
|
48
|
+
parts: number = Limits.patternParts,
|
|
49
|
+
): Result.Result<Pattern, string> => {
|
|
50
|
+
const texts = text.split(".", parts + 1)
|
|
51
|
+
if (texts.length > parts) {
|
|
47
52
|
return Result.fail(
|
|
48
|
-
`The field pattern "${text}" has more than ${String(
|
|
53
|
+
`The field pattern "${text}" has more than ${String(parts)} parts. The most is ${String(parts)}.`,
|
|
49
54
|
)
|
|
50
55
|
}
|
|
51
56
|
if (Arr.some(texts, (part) => part !== "**" && part.includes("**"))) {
|
|
@@ -63,11 +68,15 @@ export const parse = (text: string): Result.Result<Pattern, string> => {
|
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/**
|
|
66
|
-
* Reads a pattern of a bound rule. The binding of the rule took the pattern
|
|
67
|
-
* so
|
|
71
|
+
* Reads a pattern of a bound rule. The binding of the rule took the pattern
|
|
72
|
+
* with the limits of its domain, so this reads it up to the ceiling, and a
|
|
73
|
+
* problem here is a defect.
|
|
68
74
|
*/
|
|
69
75
|
export const parseBound = (text: string): Pattern =>
|
|
70
|
-
Result.getOrThrowWith(
|
|
76
|
+
Result.getOrThrowWith(
|
|
77
|
+
parse(text, Limits.ceilings.patternParts),
|
|
78
|
+
(problem) => new Error(problem),
|
|
79
|
+
)
|
|
71
80
|
|
|
72
81
|
/** Tests if a name matches the pieces of a part with stars. */
|
|
73
82
|
const globMatches = (pieces: ReadonlyArray<string>, name: string): boolean => {
|
|
@@ -189,27 +198,64 @@ export const matchesBelow = (
|
|
|
189
198
|
(state) => state < pattern.length,
|
|
190
199
|
)
|
|
191
200
|
|
|
192
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Tests if a pattern matches some field of a catalog, with at most `parts`
|
|
203
|
+
* parts. A recursive Schema has no end of fields, so the search goes over the
|
|
204
|
+
* pairs of a place of the catalog and the places of the match: each pair
|
|
205
|
+
* comes one time, a level after a level, so the search is short, and it
|
|
206
|
+
* keeps a queue, so a deep Schema does not fill the stack. The set of the
|
|
207
|
+
* pairs is native, with keys that are numbers, and the set and the queue
|
|
208
|
+
* belong to this call.
|
|
209
|
+
*/
|
|
193
210
|
export const fitsCatalog = (
|
|
194
211
|
pattern: Pattern,
|
|
195
212
|
catalog: Catalog.Catalog,
|
|
213
|
+
parts: number = Limits.fieldPathParts,
|
|
196
214
|
): boolean => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
215
|
+
interface Visit {
|
|
216
|
+
readonly branch: Catalog.Branch
|
|
217
|
+
readonly states: States
|
|
218
|
+
readonly depth: number
|
|
219
|
+
}
|
|
220
|
+
const seen = new Set<string>()
|
|
221
|
+
const next = (visit: Visit): ReadonlyArray<Visit> => {
|
|
222
|
+
if (visit.states.length === 0 || visit.depth === parts) {
|
|
223
|
+
return []
|
|
224
|
+
}
|
|
225
|
+
const steps: ReadonlyArray<readonly [Catalog.Segment, number]> = [
|
|
226
|
+
...visit.branch.children,
|
|
227
|
+
...(visit.branch.key === undefined || visit.branch.keyBranch === undefined
|
|
228
|
+
? []
|
|
229
|
+
: [[visit.branch.key, visit.branch.keyBranch] as const]),
|
|
230
|
+
]
|
|
231
|
+
return Arr.flatMap(steps, ([segment, id]) => {
|
|
232
|
+
const states = step(pattern, visit.states, segment)
|
|
233
|
+
const key = `${String(id)}|${states.join(",")}`
|
|
234
|
+
if (states.length === 0 || seen.has(key)) {
|
|
235
|
+
return []
|
|
236
|
+
}
|
|
237
|
+
seen.add(key)
|
|
238
|
+
return [{ branch: catalog.branches[id]!, states, depth: visit.depth + 1 }]
|
|
239
|
+
})
|
|
213
240
|
}
|
|
214
|
-
|
|
241
|
+
const queue: Array<Visit> = [
|
|
242
|
+
{ branch: catalog.root, states: start(pattern), depth: 0 },
|
|
243
|
+
]
|
|
244
|
+
// The walk stops at the first field that the pattern matches.
|
|
245
|
+
const found = (visit: Visit) =>
|
|
246
|
+
visit.depth > 0 && isDone(pattern, visit.states)
|
|
247
|
+
return Arr.some(
|
|
248
|
+
Arr.unfold(0, (index) => {
|
|
249
|
+
const visit = queue[index]
|
|
250
|
+
if (visit === undefined) {
|
|
251
|
+
return Option.none()
|
|
252
|
+
}
|
|
253
|
+
if (found(visit)) {
|
|
254
|
+
return Option.some([true, queue.length] as const)
|
|
255
|
+
}
|
|
256
|
+
queue.push(...next(visit))
|
|
257
|
+
return Option.some([false, index + 1] as const)
|
|
258
|
+
}),
|
|
259
|
+
(match) => match,
|
|
260
|
+
)
|
|
215
261
|
}
|