@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/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
|
|
@@ -140,13 +144,15 @@ const valuesFacts = (values: ReadonlyArray<Operand>): Facts =>
|
|
|
140
144
|
checkWeight({ ...leaf, weight: 1 + values.length })
|
|
141
145
|
|
|
142
146
|
const checkWeight = (facts: Facts): Facts => {
|
|
143
|
-
|
|
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) {
|
|
144
150
|
throw Limits.exceeded(
|
|
145
151
|
"A condition",
|
|
146
152
|
"conditionNodes",
|
|
147
153
|
"nodes and values",
|
|
148
154
|
facts.weight,
|
|
149
|
-
Limits.conditionNodes,
|
|
155
|
+
Limits.ceilings.conditionNodes,
|
|
150
156
|
)
|
|
151
157
|
}
|
|
152
158
|
return facts
|
|
@@ -159,15 +165,6 @@ const factsOf = (children: ReadonlyArray<Node>): Facts => {
|
|
|
159
165
|
checkWeight({ ...leaf, weight })
|
|
160
166
|
const depth =
|
|
161
167
|
1 + Arr.reduce(children, 0, (top, child) => Math.max(top, child.depth))
|
|
162
|
-
if (depth > Limits.conditionDepth) {
|
|
163
|
-
throw Limits.exceeded(
|
|
164
|
-
"A condition",
|
|
165
|
-
"conditionDepth",
|
|
166
|
-
"levels",
|
|
167
|
-
depth,
|
|
168
|
-
Limits.conditionDepth,
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
168
|
return {
|
|
172
169
|
size,
|
|
173
170
|
weight,
|
|
@@ -382,40 +379,61 @@ const operandJson = (operand: Operand): unknown =>
|
|
|
382
379
|
}
|
|
383
380
|
: { _tag: "Principal", path: operand.path }
|
|
384
381
|
|
|
385
|
-
/**
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
+
)
|
|
403
433
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return { _tag: "Under", prefix: Arr.map(node.prefix, operandJson) }
|
|
408
|
-
case "And":
|
|
409
|
-
case "Or":
|
|
410
|
-
return { _tag: node._tag, conditions: Arr.map(node.conditions, toJson) }
|
|
411
|
-
case "Not":
|
|
412
|
-
return { _tag: "Not", condition: toJson(node.condition) }
|
|
413
|
-
case "Predicate":
|
|
414
|
-
throw new Error(
|
|
415
|
-
`The predicate "${node.name}" can not become JSON. A condition with a predicate has no JSON form.`,
|
|
416
|
-
)
|
|
417
|
-
}
|
|
418
|
-
}
|
|
434
|
+
}
|
|
435
|
+
return go(root)
|
|
436
|
+
})
|
|
419
437
|
|
|
420
438
|
/** Tests if a value is a node. Only a constructor of a node makes one. */
|
|
421
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,72 @@ 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 one place of the match. There are at
|
|
205
|
+
* most the places of the catalog times the parts of the pattern of them, so
|
|
206
|
+
* the work is the steps of the catalog times the parts of the pattern, as the
|
|
207
|
+
* binding counts it. Each pair comes one time, a level after a level, so the
|
|
208
|
+
* search finds the shortest field first, and it keeps a queue, so a deep
|
|
209
|
+
* Schema does not fill the stack. The set of the pairs is native, and the set
|
|
210
|
+
* and the queue belong to this call.
|
|
211
|
+
*/
|
|
193
212
|
export const fitsCatalog = (
|
|
194
213
|
pattern: Pattern,
|
|
195
214
|
catalog: Catalog.Catalog,
|
|
215
|
+
parts: number = Limits.fieldPathParts,
|
|
196
216
|
): boolean => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
):
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
(
|
|
211
|
-
|
|
217
|
+
interface Visit {
|
|
218
|
+
readonly branch: Catalog.Branch
|
|
219
|
+
readonly state: number
|
|
220
|
+
readonly depth: number
|
|
221
|
+
}
|
|
222
|
+
const seen = new Set<string>()
|
|
223
|
+
const visits = (
|
|
224
|
+
id: number,
|
|
225
|
+
states: States,
|
|
226
|
+
depth: number,
|
|
227
|
+
): ReadonlyArray<Visit> =>
|
|
228
|
+
Arr.flatMap(states, (state) => {
|
|
229
|
+
const key = `${String(id)}|${String(state)}`
|
|
230
|
+
if (seen.has(key)) {
|
|
231
|
+
return []
|
|
232
|
+
}
|
|
233
|
+
seen.add(key)
|
|
234
|
+
return [{ branch: catalog.branches[id]!, state, depth }]
|
|
235
|
+
})
|
|
236
|
+
const next = (visit: Visit): ReadonlyArray<Visit> => {
|
|
237
|
+
if (visit.depth === parts) {
|
|
238
|
+
return []
|
|
239
|
+
}
|
|
240
|
+
const branch = visit.branch
|
|
241
|
+
const steps: ReadonlyArray<readonly [Catalog.Segment, number]> =
|
|
242
|
+
branch.key === undefined || branch.keyBranch === undefined
|
|
243
|
+
? branch.children
|
|
244
|
+
: [...branch.children, [branch.key, branch.keyBranch] as const]
|
|
245
|
+
return Arr.flatMap(steps, ([segment, id]) =>
|
|
246
|
+
visits(id, step(pattern, [visit.state], segment), visit.depth + 1),
|
|
212
247
|
)
|
|
213
248
|
}
|
|
214
|
-
|
|
249
|
+
const queue: Array<Visit> = [...visits(catalog.root.id, start(pattern), 0)]
|
|
250
|
+
// The walk stops at the first field that the pattern matches. The visits
|
|
251
|
+
// go into the queue one at a time: a spread into a call would pass one
|
|
252
|
+
// argument for each, past the most arguments of a call.
|
|
253
|
+
return Arr.some(
|
|
254
|
+
Arr.unfold(0, (index) => {
|
|
255
|
+
const visit = queue[index]
|
|
256
|
+
if (visit === undefined) {
|
|
257
|
+
return Option.none()
|
|
258
|
+
}
|
|
259
|
+
if (visit.depth > 0 && visit.state === pattern.length) {
|
|
260
|
+
return Option.some([true, queue.length] as const)
|
|
261
|
+
}
|
|
262
|
+
Arr.forEach(next(visit), (found) => {
|
|
263
|
+
queue.push(found)
|
|
264
|
+
})
|
|
265
|
+
return Option.some([false, index + 1] as const)
|
|
266
|
+
}),
|
|
267
|
+
(match) => match,
|
|
268
|
+
)
|
|
215
269
|
}
|