@nmnmcc/ability 2.0.7 → 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 +50 -27
- 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 +116 -16
- 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 +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 +147 -72
- 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.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 +308 -83
- package/src/Domain.ts +9 -0
- package/src/Limits.ts +69 -29
- package/src/Policy.ts +131 -15
- 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 +295 -112
- 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 +65 -47
- package/src/internal/patterns.ts +72 -26
- 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/bind.ts
CHANGED
|
@@ -17,6 +17,7 @@ import * as Nodes from "./nodes.ts"
|
|
|
17
17
|
import * as Patterns from "./patterns.ts"
|
|
18
18
|
import * as Place from "./place.ts"
|
|
19
19
|
import { withSuggestion } from "./suggest.ts"
|
|
20
|
+
import * as Trampoline from "./trampoline.ts"
|
|
20
21
|
|
|
21
22
|
/** One problem of a rule or a policy, with its path. */
|
|
22
23
|
export interface Issue {
|
|
@@ -54,13 +55,16 @@ interface Memo {
|
|
|
54
55
|
readonly issues: { count: number; noted: boolean }
|
|
55
56
|
/** The steps of the checks of patterns so far. It belongs to one binding. */
|
|
56
57
|
readonly work: { steps: number; noted: boolean }
|
|
58
|
+
/** The limits of the domain. */
|
|
59
|
+
readonly limits: Limits.Limits
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
const makeMemo = (): Memo => ({
|
|
62
|
+
const makeMemo = (limits: Limits.Limits): Memo => ({
|
|
60
63
|
suggestions: new Map(),
|
|
61
64
|
fits: new Map(),
|
|
62
65
|
issues: { count: 0, noted: false },
|
|
63
66
|
work: { steps: 0, noted: false },
|
|
67
|
+
limits,
|
|
64
68
|
})
|
|
65
69
|
|
|
66
70
|
/**
|
|
@@ -164,13 +168,23 @@ const fieldIssue = (
|
|
|
164
168
|
field: string,
|
|
165
169
|
at: ReadonlyArray<PropertyKey>,
|
|
166
170
|
): ReadonlyArray<Issue> => {
|
|
171
|
+
const parts = context.memo.limits.fieldPathParts
|
|
172
|
+
if (field.split(".", parts + 1).length > parts) {
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
path: at,
|
|
176
|
+
issue: `The field path "${field}" has more than ${String(parts)} parts. The most is ${String(parts)}.`,
|
|
177
|
+
},
|
|
178
|
+
]
|
|
179
|
+
}
|
|
167
180
|
const message = `The field "${field}" is not a field of every resource of the rule`
|
|
168
181
|
if (context.all || context.named.length === 0) {
|
|
169
182
|
return [{ path: at, issue: message }]
|
|
170
183
|
}
|
|
171
184
|
const without = Arr.filter(
|
|
172
185
|
context.named,
|
|
173
|
-
(descriptor) =>
|
|
186
|
+
(descriptor) =>
|
|
187
|
+
Catalog.lookup(descriptor.catalog, field, parts) === undefined,
|
|
174
188
|
)
|
|
175
189
|
return without.length === 0
|
|
176
190
|
? []
|
|
@@ -203,7 +217,14 @@ const readIssues = (
|
|
|
203
217
|
}
|
|
204
218
|
return Arr.some(
|
|
205
219
|
context.named,
|
|
206
|
-
(descriptor) =>
|
|
220
|
+
(descriptor) =>
|
|
221
|
+
!(
|
|
222
|
+
Catalog.lookup(
|
|
223
|
+
descriptor.catalog,
|
|
224
|
+
field,
|
|
225
|
+
context.memo.limits.fieldPathParts,
|
|
226
|
+
)?.plain ?? true
|
|
227
|
+
),
|
|
207
228
|
)
|
|
208
229
|
? [
|
|
209
230
|
{
|
|
@@ -225,7 +246,7 @@ const patternIssues = (
|
|
|
225
246
|
pattern: string,
|
|
226
247
|
at: ReadonlyArray<PropertyKey>,
|
|
227
248
|
): ReadonlyArray<Issue> =>
|
|
228
|
-
Result.match(Patterns.parse(pattern), {
|
|
249
|
+
Result.match(Patterns.parse(pattern, memo.limits.patternParts), {
|
|
229
250
|
onFailure: (issue) => [{ path: at, issue }],
|
|
230
251
|
onSuccess: (parsed) => {
|
|
231
252
|
// A rule with no known resource has an issue for that already. The
|
|
@@ -238,22 +259,27 @@ const patternIssues = (
|
|
|
238
259
|
const key = `${descriptor.kind}\u0000${pattern}`
|
|
239
260
|
return (
|
|
240
261
|
Option.getOrElse(Option.fromUndefinedOr(memo.fits.get(key)), () => {
|
|
241
|
-
// Each check of a pattern reads the
|
|
262
|
+
// Each check of a pattern reads the steps of a catalog. The
|
|
242
263
|
// work of one binding has a limit, so very many patterns stop
|
|
243
264
|
// with one issue, and the policy does not load.
|
|
244
265
|
memo.work.steps +=
|
|
245
|
-
|
|
246
|
-
|
|
266
|
+
Math.max(descriptor.catalog.edges, 1) *
|
|
267
|
+
Math.max(parsed.length, 1)
|
|
268
|
+
if (memo.work.steps > memo.limits.searchSteps) {
|
|
247
269
|
return true
|
|
248
270
|
}
|
|
249
|
-
const found = Patterns.fitsCatalog(
|
|
271
|
+
const found = Patterns.fitsCatalog(
|
|
272
|
+
parsed,
|
|
273
|
+
descriptor.catalog,
|
|
274
|
+
memo.limits.fieldPathParts,
|
|
275
|
+
)
|
|
250
276
|
memo.fits.set(key, found)
|
|
251
277
|
return found
|
|
252
278
|
}) || before
|
|
253
279
|
)
|
|
254
280
|
})
|
|
255
281
|
// Past the limit of the work, `ruleIssues` gives one issue for it.
|
|
256
|
-
return fits || memo.work.steps >
|
|
282
|
+
return fits || memo.work.steps > memo.limits.searchSteps
|
|
257
283
|
? []
|
|
258
284
|
: [
|
|
259
285
|
{
|
|
@@ -270,7 +296,11 @@ const fieldsAt = (
|
|
|
270
296
|
field: string,
|
|
271
297
|
): ReadonlyArray<Catalog.Field> =>
|
|
272
298
|
Arr.flatMap(context.applicable, (descriptor) => {
|
|
273
|
-
const found = Catalog.lookup(
|
|
299
|
+
const found = Catalog.lookup(
|
|
300
|
+
descriptor.catalog,
|
|
301
|
+
field,
|
|
302
|
+
context.memo.limits.fieldPathParts,
|
|
303
|
+
)
|
|
274
304
|
return found === undefined ? [] : [found]
|
|
275
305
|
})
|
|
276
306
|
|
|
@@ -280,7 +310,11 @@ const principalField = (
|
|
|
280
310
|
ref: string,
|
|
281
311
|
at: ReadonlyArray<PropertyKey>,
|
|
282
312
|
): Catalog.Field | Issue => {
|
|
283
|
-
const found = Catalog.lookup(
|
|
313
|
+
const found = Catalog.lookup(
|
|
314
|
+
context.domain.principalCatalog,
|
|
315
|
+
ref,
|
|
316
|
+
context.memo.limits.fieldPathParts,
|
|
317
|
+
)
|
|
284
318
|
// A read of the principal that is not plain data is a defect, as a read
|
|
285
319
|
// of a resource is: see `readIssues`.
|
|
286
320
|
if (found !== undefined && !found.plain) {
|
|
@@ -333,7 +367,7 @@ const distinctFields = (
|
|
|
333
367
|
*/
|
|
334
368
|
const spendChecks = (memo: Memo, values: number, fields: number): boolean => {
|
|
335
369
|
memo.work.steps += values * Math.max(fields, 1)
|
|
336
|
-
return memo.work.steps <=
|
|
370
|
+
return memo.work.steps <= memo.limits.searchSteps
|
|
337
371
|
}
|
|
338
372
|
|
|
339
373
|
/** The issues of one operand of a test of a field, with its distinct fields. */
|
|
@@ -490,8 +524,11 @@ const notWhole = (at: ReadonlyArray<PropertyKey>): Issue => ({
|
|
|
490
524
|
*/
|
|
491
525
|
const maxIssues = 100
|
|
492
526
|
|
|
493
|
-
/**
|
|
494
|
-
|
|
527
|
+
/**
|
|
528
|
+
* The place of a part of a policy, which a walk turns into a full path only
|
|
529
|
+
* for an issue: see `Place`.
|
|
530
|
+
*/
|
|
531
|
+
type At = Place.Place | undefined
|
|
495
532
|
|
|
496
533
|
/**
|
|
497
534
|
* Puts the path of a node before the paths of its issues. A walk keeps no
|
|
@@ -499,7 +536,7 @@ type LazyPath = () => ReadonlyArray<PropertyKey>
|
|
|
499
536
|
*/
|
|
500
537
|
const under = (
|
|
501
538
|
memo: Memo,
|
|
502
|
-
at:
|
|
539
|
+
at: At,
|
|
503
540
|
issues: ReadonlyArray<Issue>,
|
|
504
541
|
): ReadonlyArray<Issue> => {
|
|
505
542
|
if (issues.length === 0) {
|
|
@@ -508,7 +545,7 @@ const under = (
|
|
|
508
545
|
const room = maxIssues - memo.issues.count
|
|
509
546
|
memo.issues.count += issues.length
|
|
510
547
|
const kept = Arr.map(Arr.take(issues, Math.max(room, 0)), (issue) => ({
|
|
511
|
-
path: [...at
|
|
548
|
+
path: [...Place.pathOf(at), ...issue.path],
|
|
512
549
|
issue: issue.issue,
|
|
513
550
|
}))
|
|
514
551
|
const first = memo.issues.count > maxIssues && !memo.issues.noted
|
|
@@ -533,86 +570,95 @@ const under = (
|
|
|
533
570
|
*/
|
|
534
571
|
const conditionIssues = (
|
|
535
572
|
context: Context,
|
|
536
|
-
|
|
537
|
-
|
|
573
|
+
root: Nodes.Node,
|
|
574
|
+
rootAt: At,
|
|
538
575
|
seen: Set<Nodes.Node>,
|
|
539
|
-
): ReadonlyArray<Issue> =>
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
576
|
+
): ReadonlyArray<Issue> =>
|
|
577
|
+
// The walk takes the steps of a trampoline, so a deep condition does not
|
|
578
|
+
// fill the stack.
|
|
579
|
+
Trampoline.run(root.depth, (R) => {
|
|
580
|
+
// A test looks its field up in each resource of the rule. A rule for
|
|
581
|
+
// very many kinds with very many tests is very much work, so it counts.
|
|
582
|
+
// Past the limit, `ruleIssues` gives one issue for it, and the policy
|
|
583
|
+
// does not load.
|
|
584
|
+
const room = (values: number) =>
|
|
585
|
+
spendChecks(context.memo, 1 + values, context.named.length)
|
|
586
|
+
const leaf = (node: Nodes.Node, at: At): ReadonlyArray<Issue> => {
|
|
587
|
+
const here = (issues: ReadonlyArray<Issue>) =>
|
|
588
|
+
under(context.memo, at, issues)
|
|
589
|
+
switch (node._tag) {
|
|
590
|
+
case "Compare": {
|
|
591
|
+
if (!room(1)) {
|
|
592
|
+
return []
|
|
593
|
+
}
|
|
594
|
+
const exists = readIssues(context, node.field, ["field"])
|
|
595
|
+
return here(
|
|
596
|
+
exists.length > 0
|
|
597
|
+
? exists
|
|
598
|
+
: operandIssues(
|
|
599
|
+
context,
|
|
600
|
+
node.field,
|
|
601
|
+
distinctFields(context, node.field),
|
|
602
|
+
node.op !== "eq" && node.op !== "ne",
|
|
603
|
+
node.value,
|
|
604
|
+
["value"],
|
|
605
|
+
),
|
|
606
|
+
)
|
|
607
|
+
}
|
|
608
|
+
case "In": {
|
|
609
|
+
if (!room(0)) {
|
|
610
|
+
return []
|
|
611
|
+
}
|
|
612
|
+
const exists = readIssues(context, node.field, ["field"])
|
|
613
|
+
if (exists.length > 0) {
|
|
614
|
+
return here(exists)
|
|
615
|
+
}
|
|
616
|
+
const fields = distinctFields(context, node.field)
|
|
617
|
+
if (!spendChecks(context.memo, node.values.length, fields.length)) {
|
|
618
|
+
return []
|
|
619
|
+
}
|
|
620
|
+
return here(
|
|
621
|
+
Arr.flatMap(node.values, (operand, index) =>
|
|
622
|
+
operandIssues(context, node.field, fields, false, operand, [
|
|
623
|
+
"values",
|
|
624
|
+
index,
|
|
625
|
+
]),
|
|
567
626
|
),
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
case "Exists":
|
|
630
|
+
return room(0) ? here(readIssues(context, node.field, ["field"])) : []
|
|
631
|
+
case "Under":
|
|
632
|
+
return here(underIssues(context, node.prefix, []))
|
|
633
|
+
default:
|
|
634
|
+
return []
|
|
573
635
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
636
|
+
}
|
|
637
|
+
const go = (
|
|
638
|
+
node: Nodes.Node,
|
|
639
|
+
at: At,
|
|
640
|
+
): Trampoline.Step<ReadonlyArray<Issue>> => {
|
|
641
|
+
if (seen.has(node)) {
|
|
642
|
+
return R.succeed([])
|
|
577
643
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
644
|
+
seen.add(node)
|
|
645
|
+
switch (node._tag) {
|
|
646
|
+
case "And":
|
|
647
|
+
case "Or":
|
|
648
|
+
return R.map(
|
|
649
|
+
R.forEach(node.conditions, (child, index) =>
|
|
650
|
+
R.suspend(() => go(child, Place.at(at, "conditions", index))),
|
|
651
|
+
),
|
|
652
|
+
Arr.flatten,
|
|
653
|
+
)
|
|
654
|
+
case "Not":
|
|
655
|
+
return R.suspend(() => go(node.condition, Place.at(at, "condition")))
|
|
656
|
+
default:
|
|
657
|
+
return R.succeed(leaf(node, at))
|
|
581
658
|
}
|
|
582
|
-
return here(
|
|
583
|
-
Arr.flatMap(node.values, (operand, index) =>
|
|
584
|
-
operandIssues(context, node.field, fields, false, operand, [
|
|
585
|
-
"values",
|
|
586
|
-
index,
|
|
587
|
-
]),
|
|
588
|
-
),
|
|
589
|
-
)
|
|
590
659
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
case "Under":
|
|
594
|
-
return here(underIssues(context, node.prefix, []))
|
|
595
|
-
case "And":
|
|
596
|
-
case "Or":
|
|
597
|
-
return Arr.flatMap(node.conditions, (child, index) =>
|
|
598
|
-
conditionIssues(
|
|
599
|
-
context,
|
|
600
|
-
child,
|
|
601
|
-
() => [...at(), "conditions", index],
|
|
602
|
-
seen,
|
|
603
|
-
),
|
|
604
|
-
)
|
|
605
|
-
case "Not":
|
|
606
|
-
return conditionIssues(
|
|
607
|
-
context,
|
|
608
|
-
node.condition,
|
|
609
|
-
() => [...at(), "condition"],
|
|
610
|
-
seen,
|
|
611
|
-
)
|
|
612
|
-
default:
|
|
613
|
-
return []
|
|
614
|
-
}
|
|
615
|
-
}
|
|
660
|
+
return go(root, rootAt)
|
|
661
|
+
})
|
|
616
662
|
|
|
617
663
|
const hasAction = (
|
|
618
664
|
descriptor: Resource.Descriptor,
|
|
@@ -623,15 +669,15 @@ const hasAction = (
|
|
|
623
669
|
|
|
624
670
|
/**
|
|
625
671
|
* Checks one rule against a domain. It gives one issue for each problem, up
|
|
626
|
-
* to the most issues of the binding. `at`
|
|
627
|
-
* is made only for an issue that the binding keeps: a rule with
|
|
628
|
-
* problems deep in a tree does not make a full path for each one.
|
|
672
|
+
* to the most issues of the binding. `at` is the place of the rule, and its
|
|
673
|
+
* full path is made only for an issue that the binding keeps: a rule with
|
|
674
|
+
* very many problems deep in a tree does not make a full path for each one.
|
|
629
675
|
*/
|
|
630
676
|
export const ruleIssues = (
|
|
631
677
|
domain: Domain.Any,
|
|
632
678
|
rule: RuleData,
|
|
633
|
-
at:
|
|
634
|
-
memo: Memo = makeMemo(),
|
|
679
|
+
at: At,
|
|
680
|
+
memo: Memo = makeMemo(domain.limits),
|
|
635
681
|
): ReadonlyArray<Issue> => {
|
|
636
682
|
const here = (issues: ReadonlyArray<Issue>) => under(memo, at, issues)
|
|
637
683
|
const all = rule.resources.includes("*")
|
|
@@ -673,21 +719,26 @@ export const ruleIssues = (
|
|
|
673
719
|
: fieldIssue(context, field, ["fields", index]),
|
|
674
720
|
),
|
|
675
721
|
)
|
|
722
|
+
const conditionNodes = memo.limits.conditionNodes
|
|
676
723
|
const whenIssues =
|
|
677
724
|
rule.when === undefined
|
|
678
725
|
? []
|
|
679
|
-
: Nodes.isNode(rule.when)
|
|
680
|
-
?
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
726
|
+
: !Nodes.isNode(rule.when)
|
|
727
|
+
? here([notWhole(["when"])])
|
|
728
|
+
: // The constructors of the nodes check the ceiling, so the binding
|
|
729
|
+
// checks the limit of the domain.
|
|
730
|
+
rule.when.weight > conditionNodes
|
|
731
|
+
? here([
|
|
732
|
+
{
|
|
733
|
+
path: ["when"],
|
|
734
|
+
issue: `A condition has ${String(rule.when.weight)} nodes and values. The most is ${String(conditionNodes)}.`,
|
|
735
|
+
},
|
|
736
|
+
])
|
|
737
|
+
: conditionIssues(context, rule.when, Place.at(at, "when"), new Set())
|
|
687
738
|
// The work is known only after every check of the rule. Past the limit, a
|
|
688
739
|
// check gives no issue, so this issue must come, or the policy would load
|
|
689
740
|
// with checks left out.
|
|
690
|
-
const work = memo.work.steps >
|
|
741
|
+
const work = memo.work.steps > memo.limits.searchSteps && !memo.work.noted
|
|
691
742
|
memo.work.noted ||= work
|
|
692
743
|
return [
|
|
693
744
|
...Arr.flatMap(
|
|
@@ -732,8 +783,8 @@ export const ruleIssues = (
|
|
|
732
783
|
...(work
|
|
733
784
|
? [
|
|
734
785
|
{
|
|
735
|
-
path: at
|
|
736
|
-
issue: `The policy takes more than ${String(
|
|
786
|
+
path: Place.pathOf(at),
|
|
787
|
+
issue: `The policy takes more than ${String(memo.limits.searchSteps)} steps to check: its field patterns, and the fields and values of its tests on each resource. The most is ${String(memo.limits.searchSteps)}.`,
|
|
737
788
|
},
|
|
738
789
|
]
|
|
739
790
|
: []),
|
|
@@ -760,46 +811,52 @@ export const treeIssues = (
|
|
|
760
811
|
): ReadonlyArray<Issue> => {
|
|
761
812
|
// The rules come out of the tree first, and each one is checked after.
|
|
762
813
|
// A check of a condition goes down its levels, so a check inside the walk
|
|
763
|
-
// of the tree would go down the levels of both.
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
)
|
|
772
|
-
if (level > Limits.policyDepth) {
|
|
773
|
-
return [
|
|
774
|
-
{
|
|
775
|
-
path: Place.pathOf(place),
|
|
776
|
-
issue: `A policy tree has more than ${String(Limits.policyDepth)} levels. The most is ${String(Limits.policyDepth)}.`,
|
|
777
|
-
},
|
|
778
|
-
]
|
|
779
|
-
}
|
|
814
|
+
// of the tree would go down the levels of both. The place of each rule
|
|
815
|
+
// shares the path of its parent. The tree may be the JSON form, with no
|
|
816
|
+
// depth that a node knows, so the walk is a walk of the trampoline for an
|
|
817
|
+
// input of an unknown depth.
|
|
818
|
+
type Found = Issue | readonly [RuleData, Place.Place]
|
|
819
|
+
const rulesOf = Trampoline.adaptive<
|
|
820
|
+
{ readonly node: TreeNode; readonly at: At },
|
|
821
|
+
ReadonlyArray<Found>
|
|
822
|
+
>((R, child) => ({ node, at }, level) => {
|
|
780
823
|
switch (node._tag) {
|
|
781
824
|
case "Rules":
|
|
782
|
-
return
|
|
783
|
-
|
|
784
|
-
|
|
825
|
+
return R.succeed(
|
|
826
|
+
Arr.map(
|
|
827
|
+
node.rules,
|
|
828
|
+
(rule, index) => [rule, Place.at(at, "rules", index)] as const,
|
|
829
|
+
),
|
|
785
830
|
)
|
|
786
831
|
case "All":
|
|
787
832
|
case "Any":
|
|
788
833
|
case "FirstOf":
|
|
789
|
-
return
|
|
790
|
-
|
|
834
|
+
return R.map(
|
|
835
|
+
R.forEach(node.policies, (inner, index) =>
|
|
836
|
+
child(
|
|
837
|
+
{ node: inner, at: Place.at(at, "policies", index) },
|
|
838
|
+
level + 1,
|
|
839
|
+
),
|
|
840
|
+
),
|
|
841
|
+
Arr.flatten,
|
|
791
842
|
)
|
|
792
843
|
case "Ceiling":
|
|
793
|
-
return
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
844
|
+
return R.map(
|
|
845
|
+
R.forEach(
|
|
846
|
+
[
|
|
847
|
+
{ node: node.cap, at: Place.at(at, "cap") },
|
|
848
|
+
{ node: node.policy, at: Place.at(at, "policy") },
|
|
849
|
+
],
|
|
850
|
+
(part) => child(part, level + 1),
|
|
851
|
+
),
|
|
852
|
+
Arr.flatten,
|
|
853
|
+
)
|
|
797
854
|
}
|
|
798
|
-
}
|
|
799
|
-
const memo = makeMemo()
|
|
800
|
-
return Arr.flatMap(rulesOf(root, undefined
|
|
855
|
+
})
|
|
856
|
+
const memo = makeMemo(domain.limits)
|
|
857
|
+
return Arr.flatMap(rulesOf({ node: root, at: undefined }), (found) =>
|
|
801
858
|
Array.isArray(found)
|
|
802
|
-
? ruleIssues(domain, found[0],
|
|
859
|
+
? ruleIssues(domain, found[0], found[1], memo)
|
|
803
860
|
: [found as Issue],
|
|
804
861
|
)
|
|
805
862
|
}
|