@nmnmcc/ability 2.0.6 → 2.0.7
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 +6 -5
- package/dist/Condition.d.ts +2 -2
- package/dist/internal/algebra.js +100 -73
- package/dist/internal/algebra.js.map +1 -1
- package/dist/internal/analysis.js +1 -1
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/authorizer.js +6 -5
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +52 -35
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.js +6 -6
- package/dist/internal/catalog.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 +34 -4
- package/dist/internal/nodes.js.map +1 -1
- package/dist/internal/policy-nodes.d.ts.map +1 -1
- package/dist/internal/policy-nodes.js +1 -1
- package/dist/internal/policy-nodes.js.map +1 -1
- package/dist/internal/trampoline.js +75 -0
- package/dist/internal/trampoline.js.map +1 -0
- package/package.json +1 -1
- package/src/internal/algebra.ts +218 -135
- package/src/internal/analysis.ts +3 -1
- package/src/internal/authorizer.ts +8 -5
- package/src/internal/bind.ts +131 -57
- package/src/internal/catalog.ts +15 -24
- package/src/internal/nodes.ts +37 -13
- package/src/internal/policy-nodes.ts +2 -1
- package/src/internal/trampoline.ts +147 -0
package/src/internal/algebra.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* The work on condition nodes: smart constructors, simplification, the step
|
|
3
3
|
* that puts the principal in, tests on a value, and folds.
|
|
4
4
|
*
|
|
5
|
-
* The constructors of the nodes keep each condition small
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The constructors of the nodes keep each condition small, so each walk here
|
|
6
|
+
* has no memo. A shared child is walked again for each place, and the limits
|
|
7
|
+
* of the nodes keep that work small. A walk takes the steps of a trampoline,
|
|
8
|
+
* so a deep condition does not fill the stack: see `Trampoline`.
|
|
8
9
|
*/
|
|
9
10
|
// A predicate gives an Effect with an error type and a service type that this
|
|
10
11
|
// code does not know, so they are `unknown` here.
|
|
@@ -16,6 +17,7 @@ import * as Option from "effect/Option"
|
|
|
16
17
|
import type * as Resource from "../Resource.ts"
|
|
17
18
|
import * as Nodes from "./nodes.ts"
|
|
18
19
|
import { readPath } from "./read.ts"
|
|
20
|
+
import * as Trampoline from "./trampoline.ts"
|
|
19
21
|
|
|
20
22
|
type Node = Nodes.Node
|
|
21
23
|
|
|
@@ -252,20 +254,41 @@ export const not = (child: Node): Node => {
|
|
|
252
254
|
}
|
|
253
255
|
}
|
|
254
256
|
|
|
257
|
+
type Step<A> = Trampoline.Step<A>
|
|
258
|
+
|
|
259
|
+
/** The children of a node, each walked by `go` as a step of `R`. */
|
|
260
|
+
const eachChild = <A>(
|
|
261
|
+
R: Trampoline.Runner,
|
|
262
|
+
children: ReadonlyArray<Node>,
|
|
263
|
+
go: (child: Node) => Step<A>,
|
|
264
|
+
): Step<ReadonlyArray<A>> =>
|
|
265
|
+
R.forEach(children, (child) => R.suspend(() => go(child)))
|
|
266
|
+
|
|
255
267
|
/** Makes a condition simple, from its leaves up. */
|
|
256
|
-
export const simplify = (node: Node): Node =>
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
268
|
+
export const simplify = (node: Node): Node =>
|
|
269
|
+
Trampoline.run(node.depth, (R) => {
|
|
270
|
+
const go = (current: Node): Step<Node> => {
|
|
271
|
+
switch (current._tag) {
|
|
272
|
+
case "And":
|
|
273
|
+
case "Or": {
|
|
274
|
+
const tag = current._tag
|
|
275
|
+
return R.map(eachChild(R, current.conditions, go), (children) =>
|
|
276
|
+
junction(tag, children),
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
case "Not":
|
|
280
|
+
return R.map(
|
|
281
|
+
R.suspend(() => go(current.condition)),
|
|
282
|
+
not,
|
|
283
|
+
)
|
|
284
|
+
case "In":
|
|
285
|
+
return R.succeed(current.values.length === 0 ? never : current)
|
|
286
|
+
default:
|
|
287
|
+
return R.succeed(current)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return go(node)
|
|
291
|
+
})
|
|
269
292
|
|
|
270
293
|
// Tests on a value
|
|
271
294
|
|
|
@@ -359,69 +382,99 @@ const testUnder = (
|
|
|
359
382
|
})
|
|
360
383
|
}
|
|
361
384
|
|
|
362
|
-
/**
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
385
|
+
/**
|
|
386
|
+
* The test of a simple condition with no predicate on a value, as a walk.
|
|
387
|
+
* The children of `And` and `Or` run in order, and they stop at the first
|
|
388
|
+
* child that gives the answer.
|
|
389
|
+
*/
|
|
390
|
+
const testWith = (input: Input) => (R: Trampoline.Runner) => {
|
|
391
|
+
const go = (node: Node): Step<boolean> => {
|
|
392
|
+
switch (node._tag) {
|
|
393
|
+
case "True":
|
|
394
|
+
return R.succeed(true)
|
|
395
|
+
case "False":
|
|
396
|
+
return R.succeed(false)
|
|
397
|
+
case "Compare":
|
|
398
|
+
return R.succeed(
|
|
399
|
+
testCompare(
|
|
400
|
+
node.op,
|
|
401
|
+
getField(input.value, node.field),
|
|
402
|
+
resolve(node.value, input.principal),
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
case "In": {
|
|
406
|
+
const field = getField(input.value, node.field)
|
|
407
|
+
return R.succeed(
|
|
408
|
+
node.values.some(
|
|
409
|
+
(operand) => resolve(operand, input.principal) === field,
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
case "Exists":
|
|
414
|
+
return R.succeed(getField(input.value, node.field) !== null)
|
|
415
|
+
case "Under":
|
|
416
|
+
return R.succeed(testUnder(node.prefix, input))
|
|
417
|
+
case "And":
|
|
418
|
+
return R.every(node.conditions, (child) => R.suspend(() => go(child)))
|
|
419
|
+
case "Or":
|
|
420
|
+
return R.some(node.conditions, (child) => R.suspend(() => go(child)))
|
|
421
|
+
case "Not":
|
|
422
|
+
return R.map(
|
|
423
|
+
R.suspend(() => go(node.condition)),
|
|
424
|
+
(value) => !value,
|
|
425
|
+
)
|
|
426
|
+
case "Predicate":
|
|
427
|
+
// The types stop this. A predicate needs `evaluate`.
|
|
428
|
+
throw new Error(
|
|
429
|
+
`The predicate "${node.name}" needs an Effect. Use Condition.evaluate.`,
|
|
430
|
+
)
|
|
380
431
|
}
|
|
381
|
-
case "Exists":
|
|
382
|
-
return getField(input.value, node.field) !== null
|
|
383
|
-
case "Under":
|
|
384
|
-
return testUnder(node.prefix, input)
|
|
385
|
-
case "And":
|
|
386
|
-
return Arr.every(node.conditions, (child) => testSync(child, input))
|
|
387
|
-
case "Or":
|
|
388
|
-
return Arr.some(node.conditions, (child) => testSync(child, input))
|
|
389
|
-
case "Not":
|
|
390
|
-
return !testSync(node.condition, input)
|
|
391
|
-
case "Predicate":
|
|
392
|
-
// The types stop this. A predicate needs `evaluate`.
|
|
393
|
-
throw new Error(
|
|
394
|
-
`The predicate "${node.name}" needs an Effect. Use Condition.evaluate.`,
|
|
395
|
-
)
|
|
396
432
|
}
|
|
433
|
+
return go
|
|
397
434
|
}
|
|
398
435
|
|
|
436
|
+
/** Tests a simple condition with no predicate on a value. */
|
|
437
|
+
const testSync = (node: Node, input: Input): boolean =>
|
|
438
|
+
Trampoline.run(node.depth, (R) => testWith(input)(R)(node))
|
|
439
|
+
|
|
399
440
|
/**
|
|
400
441
|
* Tests a simple condition on a value. The children of `And` and `Or` run in
|
|
401
|
-
* order, and they stop at the first child that gives the answer.
|
|
442
|
+
* order, and they stop at the first child that gives the answer. Each call
|
|
443
|
+
* for a child is a step of the Effect, so a deep condition does not fill the
|
|
444
|
+
* stack. A part with no predicate runs as a walk in the same loop.
|
|
402
445
|
*/
|
|
403
446
|
const test = (
|
|
404
447
|
node: Node,
|
|
405
448
|
input: Input,
|
|
406
449
|
): Effect.Effect<boolean, unknown, unknown> => {
|
|
407
450
|
if (!node.opaque) {
|
|
408
|
-
return
|
|
451
|
+
return node.depth <= Trampoline.directDepth
|
|
452
|
+
? Effect.sync(() => testSync(node, input))
|
|
453
|
+
: node.pipe(testWith(input)(Trampoline.loop), Trampoline.toEffect)
|
|
409
454
|
}
|
|
410
455
|
switch (node._tag) {
|
|
411
456
|
case "Predicate":
|
|
412
457
|
return Nodes.runOf(node)(input.value, input.principal)
|
|
413
458
|
case "Not":
|
|
414
|
-
return Effect.map(
|
|
459
|
+
return Effect.map(
|
|
460
|
+
Effect.suspend(() => test(node.condition, input)),
|
|
461
|
+
(value) => !value,
|
|
462
|
+
)
|
|
415
463
|
case "And":
|
|
416
464
|
return Effect.map(
|
|
417
465
|
Effect.findFirst(node.conditions, (child) =>
|
|
418
|
-
Effect.map(
|
|
466
|
+
Effect.map(
|
|
467
|
+
Effect.suspend(() => test(child, input)),
|
|
468
|
+
(value) => !value,
|
|
469
|
+
),
|
|
419
470
|
),
|
|
420
471
|
Option.isNone,
|
|
421
472
|
)
|
|
422
473
|
case "Or":
|
|
423
474
|
return Effect.map(
|
|
424
|
-
Effect.findFirst(node.conditions, (child) =>
|
|
475
|
+
Effect.findFirst(node.conditions, (child) =>
|
|
476
|
+
Effect.suspend(() => test(child, input)),
|
|
477
|
+
),
|
|
425
478
|
Option.isSome,
|
|
426
479
|
)
|
|
427
480
|
default:
|
|
@@ -501,96 +554,126 @@ export interface PartialInput {
|
|
|
501
554
|
* resource. The result is simple. It has no principal fields and no `Under`
|
|
502
555
|
* nodes. A predicate keeps the principal.
|
|
503
556
|
*/
|
|
504
|
-
export const partial = (node: Node, input: PartialInput): Node =>
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
557
|
+
export const partial = (node: Node, input: PartialInput): Node =>
|
|
558
|
+
Trampoline.run(node.depth, (R) => {
|
|
559
|
+
const bind = (current: Node): Step<Node> => {
|
|
560
|
+
switch (current._tag) {
|
|
561
|
+
case "Compare": {
|
|
562
|
+
const value = bindOperand(current.value, input.principal)
|
|
563
|
+
return R.succeed(
|
|
564
|
+
value === undefined
|
|
565
|
+
? never
|
|
566
|
+
: value === current.value
|
|
567
|
+
? current
|
|
568
|
+
: new Nodes.Compare({
|
|
569
|
+
op: current.op,
|
|
570
|
+
field: current.field,
|
|
571
|
+
value,
|
|
572
|
+
}),
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
case "In": {
|
|
576
|
+
const values = Arr.flatMap(current.values, (operand) =>
|
|
577
|
+
Option.toArray(
|
|
578
|
+
Option.fromUndefinedOr(bindOperand(operand, input.principal)),
|
|
579
|
+
),
|
|
580
|
+
)
|
|
581
|
+
return R.succeed(
|
|
582
|
+
values.length === 0
|
|
583
|
+
? never
|
|
584
|
+
: new Nodes.In({ field: current.field, values }),
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
case "Under":
|
|
588
|
+
return R.succeed(
|
|
589
|
+
expandUnder(current.prefix, input.principal, input.path),
|
|
590
|
+
)
|
|
591
|
+
case "And":
|
|
592
|
+
return R.map(eachChild(R, current.conditions, bind), and)
|
|
593
|
+
case "Or":
|
|
594
|
+
return R.map(eachChild(R, current.conditions, bind), or)
|
|
595
|
+
case "Not":
|
|
596
|
+
return R.map(
|
|
597
|
+
R.suspend(() => bind(current.condition)),
|
|
598
|
+
not,
|
|
599
|
+
)
|
|
600
|
+
case "Predicate": {
|
|
601
|
+
const run = Nodes.runOf(current)
|
|
602
|
+
return R.succeed(
|
|
603
|
+
new Nodes.Predicate({
|
|
604
|
+
name: current.name,
|
|
605
|
+
run: (value) => run(value, input.principal),
|
|
606
|
+
}),
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
default:
|
|
610
|
+
return R.succeed(current)
|
|
524
611
|
}
|
|
525
|
-
case "Under":
|
|
526
|
-
return expandUnder(current.prefix, input.principal, input.path)
|
|
527
|
-
case "And":
|
|
528
|
-
return and(Arr.map(current.conditions, bind))
|
|
529
|
-
case "Or":
|
|
530
|
-
return or(Arr.map(current.conditions, bind))
|
|
531
|
-
case "Not":
|
|
532
|
-
return current.condition.pipe(bind, not)
|
|
533
|
-
case "Predicate": {
|
|
534
|
-
const run = Nodes.runOf(current)
|
|
535
|
-
return new Nodes.Predicate({
|
|
536
|
-
name: current.name,
|
|
537
|
-
run: (value) => run(value, input.principal),
|
|
538
|
-
})
|
|
539
|
-
}
|
|
540
|
-
default:
|
|
541
|
-
return current
|
|
542
612
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
613
|
+
return bind(node)
|
|
614
|
+
})
|
|
546
615
|
|
|
547
616
|
/** Gives the services in `context` to every predicate of a condition. */
|
|
548
|
-
export const provide = (node: Node, context: Context.Context<never>): Node =>
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
617
|
+
export const provide = (node: Node, context: Context.Context<never>): Node =>
|
|
618
|
+
Trampoline.run(node.depth, (R) => {
|
|
619
|
+
const go = (current: Node): Step<Node> => {
|
|
620
|
+
if (!current.opaque) {
|
|
621
|
+
return R.succeed(current)
|
|
622
|
+
}
|
|
623
|
+
switch (current._tag) {
|
|
624
|
+
case "Predicate": {
|
|
625
|
+
const run = Nodes.runOf(current)
|
|
626
|
+
return R.succeed(
|
|
627
|
+
new Nodes.Predicate({
|
|
628
|
+
name: current.name,
|
|
629
|
+
run: (value, principal) =>
|
|
630
|
+
Effect.provideContext(run(value, principal), context),
|
|
631
|
+
}),
|
|
632
|
+
)
|
|
633
|
+
}
|
|
634
|
+
case "And":
|
|
635
|
+
return R.map(
|
|
636
|
+
eachChild(R, current.conditions, go),
|
|
637
|
+
(children) => new Nodes.And(children),
|
|
638
|
+
)
|
|
639
|
+
case "Or":
|
|
640
|
+
return R.map(
|
|
641
|
+
eachChild(R, current.conditions, go),
|
|
642
|
+
(children) => new Nodes.Or(children),
|
|
643
|
+
)
|
|
644
|
+
case "Not":
|
|
645
|
+
return R.map(
|
|
646
|
+
R.suspend(() => go(current.condition)),
|
|
647
|
+
(child) => new Nodes.Not(child),
|
|
648
|
+
)
|
|
649
|
+
default:
|
|
650
|
+
return R.succeed(current)
|
|
651
|
+
}
|
|
560
652
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
Arr.map(node.conditions, (child) => provide(child, context)),
|
|
564
|
-
)
|
|
565
|
-
case "Or":
|
|
566
|
-
return new Nodes.Or(
|
|
567
|
-
Arr.map(node.conditions, (child) => provide(child, context)),
|
|
568
|
-
)
|
|
569
|
-
case "Not":
|
|
570
|
-
return new Nodes.Not(provide(node.condition, context))
|
|
571
|
-
default:
|
|
572
|
-
return node
|
|
573
|
-
}
|
|
574
|
-
}
|
|
653
|
+
return go(node)
|
|
654
|
+
})
|
|
575
655
|
|
|
576
656
|
/** The fields that a condition tests, in the order that they first come. */
|
|
577
657
|
export const fields = (node: Node): ReadonlyArray<string> => {
|
|
578
|
-
const all = (
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
658
|
+
const all = Trampoline.run(node.depth, (R) => {
|
|
659
|
+
const go = (current: Node): Step<ReadonlyArray<string>> => {
|
|
660
|
+
switch (current._tag) {
|
|
661
|
+
case "Compare":
|
|
662
|
+
case "In":
|
|
663
|
+
case "Exists":
|
|
664
|
+
return R.succeed([current.field])
|
|
665
|
+
case "And":
|
|
666
|
+
case "Or":
|
|
667
|
+
return R.map(eachChild(R, current.conditions, go), Arr.flatten)
|
|
668
|
+
case "Not":
|
|
669
|
+
return R.suspend(() => go(current.condition))
|
|
670
|
+
default:
|
|
671
|
+
return R.succeed([])
|
|
672
|
+
}
|
|
591
673
|
}
|
|
592
|
-
|
|
674
|
+
return go(node)
|
|
675
|
+
})
|
|
593
676
|
// A native `Set`, as in `dedupe`: a field of a condition that is not bound
|
|
594
677
|
// to a domain may be any string.
|
|
595
|
-
return Array.from(new Set(all
|
|
678
|
+
return Array.from(new Set(all))
|
|
596
679
|
}
|
package/src/internal/analysis.ts
CHANGED
|
@@ -51,6 +51,8 @@ interface Build {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const limit = (node: Nodes.Node): Ast => {
|
|
54
|
+
// A residual grows by its nodes, as the conditions of the rules come
|
|
55
|
+
// together. Its values come from the policy, which has a limit of them.
|
|
54
56
|
if (node.size > Limits.residualNodes) {
|
|
55
57
|
throw Limits.exceeded(
|
|
56
58
|
"A residual condition",
|
|
@@ -296,7 +298,7 @@ const scopeOf = (
|
|
|
296
298
|
const issues = Bind.ruleIssues(
|
|
297
299
|
domain,
|
|
298
300
|
{ actions: [request.action], resources: [request.kind], when: ast },
|
|
299
|
-
["within"],
|
|
301
|
+
() => ["within"],
|
|
300
302
|
)
|
|
301
303
|
if (issues.length > 0) {
|
|
302
304
|
throw new globalThis.Error(
|
|
@@ -133,14 +133,17 @@ class PrincipalKey implements Equal.Equal {
|
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
135
|
* The text of the order of the parts of a value: the names of the fields of
|
|
136
|
-
* each object, and the items of each list, set and map,
|
|
137
|
-
* loop sees them. `Equal` does not see
|
|
138
|
-
*
|
|
139
|
-
*
|
|
136
|
+
* each object, and the items of each list, set and map, with each scalar, in
|
|
137
|
+
* the order that a loop sees them. `Equal` does not see the order of the
|
|
138
|
+
* fields of an object or of the items of a map, but a loader may read it, as
|
|
139
|
+
* `Object.values` does. So two keys need the same order too. A scalar is in
|
|
140
|
+
* the text with its type, so the items of a set or a map show their order.
|
|
140
141
|
*/
|
|
141
142
|
const orderOf = (value: unknown): string => {
|
|
142
143
|
if (typeof value !== "object" || value === null) {
|
|
143
|
-
return ""
|
|
144
|
+
return typeof value === "string"
|
|
145
|
+
? JSON.stringify(value)
|
|
146
|
+
: `${typeof value}:${String(value)}`
|
|
144
147
|
}
|
|
145
148
|
if (Array.isArray(value)) {
|
|
146
149
|
return `[${value.map(orderOf).join(",")}]`
|