@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/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,30 +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
|
-
|
|
256
|
-
|
|
257
|
-
return [
|
|
258
|
-
{
|
|
259
|
-
path: at,
|
|
260
|
-
issue: `The field patterns of the policy take more than ${String(Limits.searchSteps)} steps to check. The most is ${String(Limits.searchSteps)}.`,
|
|
261
|
-
},
|
|
262
|
-
]
|
|
263
|
-
}
|
|
264
|
-
return fits
|
|
281
|
+
// Past the limit of the work, `ruleIssues` gives one issue for it.
|
|
282
|
+
return fits || memo.work.steps > memo.limits.searchSteps
|
|
265
283
|
? []
|
|
266
284
|
: [
|
|
267
285
|
{
|
|
@@ -278,7 +296,11 @@ const fieldsAt = (
|
|
|
278
296
|
field: string,
|
|
279
297
|
): ReadonlyArray<Catalog.Field> =>
|
|
280
298
|
Arr.flatMap(context.applicable, (descriptor) => {
|
|
281
|
-
const found = Catalog.lookup(
|
|
299
|
+
const found = Catalog.lookup(
|
|
300
|
+
descriptor.catalog,
|
|
301
|
+
field,
|
|
302
|
+
context.memo.limits.fieldPathParts,
|
|
303
|
+
)
|
|
282
304
|
return found === undefined ? [] : [found]
|
|
283
305
|
})
|
|
284
306
|
|
|
@@ -288,7 +310,11 @@ const principalField = (
|
|
|
288
310
|
ref: string,
|
|
289
311
|
at: ReadonlyArray<PropertyKey>,
|
|
290
312
|
): Catalog.Field | Issue => {
|
|
291
|
-
const found = Catalog.lookup(
|
|
313
|
+
const found = Catalog.lookup(
|
|
314
|
+
context.domain.principalCatalog,
|
|
315
|
+
ref,
|
|
316
|
+
context.memo.limits.fieldPathParts,
|
|
317
|
+
)
|
|
292
318
|
// A read of the principal that is not plain data is a defect, as a read
|
|
293
319
|
// of a resource is: see `readIssues`.
|
|
294
320
|
if (found !== undefined && !found.plain) {
|
|
@@ -314,15 +340,45 @@ const principalField = (
|
|
|
314
340
|
const isIssue = (value: Catalog.Field | Issue): value is Issue =>
|
|
315
341
|
"issue" in value
|
|
316
342
|
|
|
317
|
-
/**
|
|
343
|
+
/**
|
|
344
|
+
* The fields that a test reads, with one field for each distinct kind of
|
|
345
|
+
* value. A rule for very many kinds may have one field type in all of them,
|
|
346
|
+
* so each value of the test is checked one time for each distinct kind, not
|
|
347
|
+
* one time for each resource. The kinds are found by their text in a native
|
|
348
|
+
* `Map`, which belongs to this call.
|
|
349
|
+
*/
|
|
350
|
+
const distinctFields = (
|
|
351
|
+
context: Context,
|
|
352
|
+
field: string,
|
|
353
|
+
): ReadonlyArray<Catalog.Field> =>
|
|
354
|
+
Array.from(
|
|
355
|
+
new Map(
|
|
356
|
+
Arr.map(
|
|
357
|
+
fieldsAt(context, field),
|
|
358
|
+
(found) => [JSON.stringify(found.kinds), found] as const,
|
|
359
|
+
),
|
|
360
|
+
).values(),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Counts the work of the checks of the values of a test, as the work of the
|
|
365
|
+
* field patterns counts. Past the limit, the values are not checked, and
|
|
366
|
+
* `ruleIssues` gives one issue for the work.
|
|
367
|
+
*/
|
|
368
|
+
const spendChecks = (memo: Memo, values: number, fields: number): boolean => {
|
|
369
|
+
memo.work.steps += values * Math.max(fields, 1)
|
|
370
|
+
return memo.work.steps <= memo.limits.searchSteps
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The issues of one operand of a test of a field, with its distinct fields. */
|
|
318
374
|
const operandIssues = (
|
|
319
375
|
context: Context,
|
|
320
376
|
field: string,
|
|
377
|
+
fields: ReadonlyArray<Catalog.Field>,
|
|
321
378
|
ordered: boolean,
|
|
322
379
|
operand: Operand,
|
|
323
380
|
at: ReadonlyArray<PropertyKey>,
|
|
324
381
|
): ReadonlyArray<Issue> => {
|
|
325
|
-
const fields = fieldsAt(context, field)
|
|
326
382
|
if (operand._tag === "Literal") {
|
|
327
383
|
const value = operand.value
|
|
328
384
|
if (ordered && typeof value !== "string" && typeof value !== "number") {
|
|
@@ -468,8 +524,11 @@ const notWhole = (at: ReadonlyArray<PropertyKey>): Issue => ({
|
|
|
468
524
|
*/
|
|
469
525
|
const maxIssues = 100
|
|
470
526
|
|
|
471
|
-
/**
|
|
472
|
-
|
|
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
|
|
473
532
|
|
|
474
533
|
/**
|
|
475
534
|
* Puts the path of a node before the paths of its issues. A walk keeps no
|
|
@@ -477,7 +536,7 @@ type LazyPath = () => ReadonlyArray<PropertyKey>
|
|
|
477
536
|
*/
|
|
478
537
|
const under = (
|
|
479
538
|
memo: Memo,
|
|
480
|
-
at:
|
|
539
|
+
at: At,
|
|
481
540
|
issues: ReadonlyArray<Issue>,
|
|
482
541
|
): ReadonlyArray<Issue> => {
|
|
483
542
|
if (issues.length === 0) {
|
|
@@ -486,7 +545,7 @@ const under = (
|
|
|
486
545
|
const room = maxIssues - memo.issues.count
|
|
487
546
|
memo.issues.count += issues.length
|
|
488
547
|
const kept = Arr.map(Arr.take(issues, Math.max(room, 0)), (issue) => ({
|
|
489
|
-
path: [...at
|
|
548
|
+
path: [...Place.pathOf(at), ...issue.path],
|
|
490
549
|
issue: issue.issue,
|
|
491
550
|
}))
|
|
492
551
|
const first = memo.issues.count > maxIssues && !memo.issues.noted
|
|
@@ -511,68 +570,95 @@ const under = (
|
|
|
511
570
|
*/
|
|
512
571
|
const conditionIssues = (
|
|
513
572
|
context: Context,
|
|
514
|
-
|
|
515
|
-
|
|
573
|
+
root: Nodes.Node,
|
|
574
|
+
rootAt: At,
|
|
516
575
|
seen: Set<Nodes.Node>,
|
|
517
|
-
): ReadonlyArray<Issue> =>
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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, [
|
|
545
623
|
"values",
|
|
546
624
|
index,
|
|
547
625
|
]),
|
|
548
626
|
),
|
|
549
|
-
|
|
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 []
|
|
635
|
+
}
|
|
550
636
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
637
|
+
const go = (
|
|
638
|
+
node: Nodes.Node,
|
|
639
|
+
at: At,
|
|
640
|
+
): Trampoline.Step<ReadonlyArray<Issue>> => {
|
|
641
|
+
if (seen.has(node)) {
|
|
642
|
+
return R.succeed([])
|
|
643
|
+
}
|
|
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))
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return go(root, rootAt)
|
|
661
|
+
})
|
|
576
662
|
|
|
577
663
|
const hasAction = (
|
|
578
664
|
descriptor: Resource.Descriptor,
|
|
@@ -582,15 +668,18 @@ const hasAction = (
|
|
|
582
668
|
Arr.some(actions, (action) => descriptor.actions.includes(action))
|
|
583
669
|
|
|
584
670
|
/**
|
|
585
|
-
* Checks one rule against a domain. It gives one issue for each problem
|
|
586
|
-
*
|
|
671
|
+
* Checks one rule against a domain. It gives one issue for each problem, up
|
|
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.
|
|
587
675
|
*/
|
|
588
676
|
export const ruleIssues = (
|
|
589
677
|
domain: Domain.Any,
|
|
590
678
|
rule: RuleData,
|
|
591
|
-
at:
|
|
592
|
-
memo: Memo = makeMemo(),
|
|
679
|
+
at: At,
|
|
680
|
+
memo: Memo = makeMemo(domain.limits),
|
|
593
681
|
): ReadonlyArray<Issue> => {
|
|
682
|
+
const here = (issues: ReadonlyArray<Issue>) => under(memo, at, issues)
|
|
594
683
|
const all = rule.resources.includes("*")
|
|
595
684
|
// Each kind is checked one time, also when the list names it again. The
|
|
596
685
|
// names come from a policy, so a native `Set` finds the copies:
|
|
@@ -623,46 +712,82 @@ export const ruleIssues = (
|
|
|
623
712
|
const actions = Array.from(
|
|
624
713
|
new Set(Arr.flatMap(known, (descriptor) => descriptor.actions)),
|
|
625
714
|
)
|
|
715
|
+
const fieldIssues = Arr.flatMap(rule.fields ?? [], (field, index) =>
|
|
716
|
+
here(
|
|
717
|
+
field.includes("*")
|
|
718
|
+
? patternIssues(memo, known, field, ["fields", index])
|
|
719
|
+
: fieldIssue(context, field, ["fields", index]),
|
|
720
|
+
),
|
|
721
|
+
)
|
|
722
|
+
const conditionNodes = memo.limits.conditionNodes
|
|
723
|
+
const whenIssues =
|
|
724
|
+
rule.when === undefined
|
|
725
|
+
? []
|
|
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())
|
|
738
|
+
// The work is known only after every check of the rule. Past the limit, a
|
|
739
|
+
// check gives no issue, so this issue must come, or the policy would load
|
|
740
|
+
// with checks left out.
|
|
741
|
+
const work = memo.work.steps > memo.limits.searchSteps && !memo.work.noted
|
|
742
|
+
memo.work.noted ||= work
|
|
626
743
|
return [
|
|
627
|
-
...Arr.
|
|
744
|
+
...Arr.flatMap(
|
|
628
745
|
Arr.filter(names, (kind) => !HashMap.has(domain.resources, kind)),
|
|
629
|
-
(kind
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
746
|
+
(kind: string) =>
|
|
747
|
+
here([
|
|
748
|
+
{
|
|
749
|
+
path: ["resources"],
|
|
750
|
+
issue: suggestIn(
|
|
751
|
+
memo,
|
|
752
|
+
`There is no resource with the kind "${kind}"`,
|
|
753
|
+
kind,
|
|
754
|
+
"~kinds",
|
|
755
|
+
() => kinds,
|
|
756
|
+
),
|
|
757
|
+
},
|
|
758
|
+
]),
|
|
639
759
|
),
|
|
640
|
-
...Arr.
|
|
760
|
+
...Arr.flatMap(
|
|
641
761
|
Arr.filter(
|
|
642
762
|
rule.actions,
|
|
643
763
|
(action) => action !== "*" && !actions.includes(action),
|
|
644
764
|
),
|
|
645
|
-
(action
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
? patternIssues(memo, known, field, [...at, "fields", index])
|
|
659
|
-
: fieldIssue(context, field, [...at, "fields", index]),
|
|
765
|
+
(action: string) =>
|
|
766
|
+
here([
|
|
767
|
+
{
|
|
768
|
+
path: ["actions"],
|
|
769
|
+
issue: suggestIn(
|
|
770
|
+
memo,
|
|
771
|
+
`The action "${action}" is not an action of these resources`,
|
|
772
|
+
action,
|
|
773
|
+
`~actions\u0000${names.join(",")}`,
|
|
774
|
+
() => actions,
|
|
775
|
+
),
|
|
776
|
+
},
|
|
777
|
+
]),
|
|
660
778
|
),
|
|
661
|
-
...
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
779
|
+
...fieldIssues,
|
|
780
|
+
...whenIssues,
|
|
781
|
+
// The issue of the work is not one of the most issues of the binding,
|
|
782
|
+
// so a policy past the limit always says why.
|
|
783
|
+
...(work
|
|
784
|
+
? [
|
|
785
|
+
{
|
|
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)}.`,
|
|
788
|
+
},
|
|
789
|
+
]
|
|
790
|
+
: []),
|
|
666
791
|
]
|
|
667
792
|
}
|
|
668
793
|
|
|
@@ -686,46 +811,52 @@ export const treeIssues = (
|
|
|
686
811
|
): ReadonlyArray<Issue> => {
|
|
687
812
|
// The rules come out of the tree first, and each one is checked after.
|
|
688
813
|
// A check of a condition goes down its levels, so a check inside the walk
|
|
689
|
-
// of the tree would go down the levels of both.
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
)
|
|
698
|
-
if (level > Limits.policyDepth) {
|
|
699
|
-
return [
|
|
700
|
-
{
|
|
701
|
-
path: Place.pathOf(place),
|
|
702
|
-
issue: `A policy tree has more than ${String(Limits.policyDepth)} levels. The most is ${String(Limits.policyDepth)}.`,
|
|
703
|
-
},
|
|
704
|
-
]
|
|
705
|
-
}
|
|
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) => {
|
|
706
823
|
switch (node._tag) {
|
|
707
824
|
case "Rules":
|
|
708
|
-
return
|
|
709
|
-
|
|
710
|
-
|
|
825
|
+
return R.succeed(
|
|
826
|
+
Arr.map(
|
|
827
|
+
node.rules,
|
|
828
|
+
(rule, index) => [rule, Place.at(at, "rules", index)] as const,
|
|
829
|
+
),
|
|
711
830
|
)
|
|
712
831
|
case "All":
|
|
713
832
|
case "Any":
|
|
714
833
|
case "FirstOf":
|
|
715
|
-
return
|
|
716
|
-
|
|
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,
|
|
717
842
|
)
|
|
718
843
|
case "Ceiling":
|
|
719
|
-
return
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
+
)
|
|
723
854
|
}
|
|
724
|
-
}
|
|
725
|
-
const memo = makeMemo()
|
|
726
|
-
return Arr.flatMap(rulesOf(root, undefined
|
|
855
|
+
})
|
|
856
|
+
const memo = makeMemo(domain.limits)
|
|
857
|
+
return Arr.flatMap(rulesOf({ node: root, at: undefined }), (found) =>
|
|
727
858
|
Array.isArray(found)
|
|
728
|
-
? ruleIssues(domain, found[0],
|
|
859
|
+
? ruleIssues(domain, found[0], found[1], memo)
|
|
729
860
|
: [found as Issue],
|
|
730
861
|
)
|
|
731
862
|
}
|