@nmnmcc/ability 1.0.1 → 2.0.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.
Files changed (127) hide show
  1. package/README.md +112 -69
  2. package/dist/Authorizer.d.ts +43 -47
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js +5 -290
  5. package/dist/Authorizer.js.map +1 -1
  6. package/dist/Condition.d.ts +164 -332
  7. package/dist/Condition.d.ts.map +1 -1
  8. package/dist/Condition.js +75 -890
  9. package/dist/Condition.js.map +1 -1
  10. package/dist/Decision.d.ts +13 -13
  11. package/dist/Decision.d.ts.map +1 -1
  12. package/dist/Decision.js.map +1 -1
  13. package/dist/Domain.d.ts +10 -21
  14. package/dist/Domain.d.ts.map +1 -1
  15. package/dist/Domain.js +4 -62
  16. package/dist/Domain.js.map +1 -1
  17. package/dist/HttpApi.d.ts +31 -7
  18. package/dist/HttpApi.d.ts.map +1 -1
  19. package/dist/HttpApi.js +25 -8
  20. package/dist/HttpApi.js.map +1 -1
  21. package/dist/Limits.d.ts +114 -0
  22. package/dist/Limits.d.ts.map +1 -0
  23. package/dist/Limits.js +132 -0
  24. package/dist/Limits.js.map +1 -0
  25. package/dist/Policy.d.ts +276 -269
  26. package/dist/Policy.d.ts.map +1 -1
  27. package/dist/Policy.js +73 -1177
  28. package/dist/Policy.js.map +1 -1
  29. package/dist/Resource.d.ts +4 -177
  30. package/dist/Resource.d.ts.map +1 -1
  31. package/dist/Resource.js +4 -803
  32. package/dist/Resource.js.map +1 -1
  33. package/dist/index.d.ts +3 -4
  34. package/dist/index.js +4 -5
  35. package/dist/internal/algebra.js +267 -0
  36. package/dist/internal/algebra.js.map +1 -0
  37. package/dist/internal/analysis.js +245 -0
  38. package/dist/internal/analysis.js.map +1 -0
  39. package/dist/internal/authorizer.d.ts +30 -0
  40. package/dist/internal/authorizer.d.ts.map +1 -0
  41. package/dist/internal/authorizer.js +221 -0
  42. package/dist/internal/authorizer.js.map +1 -0
  43. package/dist/internal/bind.js +311 -0
  44. package/dist/internal/bind.js.map +1 -0
  45. package/dist/internal/catalog.d.ts +56 -0
  46. package/dist/internal/catalog.d.ts.map +1 -0
  47. package/dist/internal/catalog.js +292 -0
  48. package/dist/internal/catalog.js.map +1 -0
  49. package/dist/internal/decide.js +166 -0
  50. package/dist/internal/decide.js.map +1 -0
  51. package/dist/internal/domain.js +32 -0
  52. package/dist/internal/domain.js.map +1 -0
  53. package/dist/internal/json.js +134 -0
  54. package/dist/internal/json.js.map +1 -0
  55. package/dist/internal/limits.d.ts +14 -0
  56. package/dist/internal/limits.d.ts.map +1 -0
  57. package/dist/internal/limits.js +43 -0
  58. package/dist/internal/limits.js.map +1 -0
  59. package/dist/internal/nodes.d.ts +121 -0
  60. package/dist/internal/nodes.d.ts.map +1 -0
  61. package/dist/internal/nodes.js +277 -0
  62. package/dist/internal/nodes.js.map +1 -0
  63. package/dist/internal/patterns.js +110 -0
  64. package/dist/internal/patterns.js.map +1 -0
  65. package/dist/internal/policy-json.js +265 -0
  66. package/dist/internal/policy-json.js.map +1 -0
  67. package/dist/internal/policy-nodes.d.ts +61 -0
  68. package/dist/internal/policy-nodes.d.ts.map +1 -0
  69. package/dist/internal/policy-nodes.js +146 -0
  70. package/dist/internal/policy-nodes.js.map +1 -0
  71. package/dist/internal/policy.d.ts +42 -0
  72. package/dist/internal/policy.d.ts.map +1 -0
  73. package/dist/internal/policy.js +124 -0
  74. package/dist/internal/policy.js.map +1 -0
  75. package/dist/internal/read.js +39 -0
  76. package/dist/internal/read.js.map +1 -0
  77. package/dist/internal/resource.js +115 -0
  78. package/dist/internal/resource.js.map +1 -0
  79. package/dist/internal/rule.d.ts +26 -0
  80. package/dist/internal/rule.d.ts.map +1 -0
  81. package/dist/internal/rule.js +103 -0
  82. package/dist/internal/rule.js.map +1 -0
  83. package/dist/internal/search.d.ts +22 -0
  84. package/dist/internal/search.d.ts.map +1 -0
  85. package/dist/internal/search.js +274 -0
  86. package/dist/internal/search.js.map +1 -0
  87. package/dist/internal/suggest.js +46 -0
  88. package/dist/internal/suggest.js.map +1 -0
  89. package/package.json +3 -4
  90. package/src/Authorizer.ts +67 -583
  91. package/src/Condition.ts +339 -2095
  92. package/src/Decision.ts +6 -5
  93. package/src/Domain.ts +12 -69
  94. package/src/HttpApi.ts +72 -10
  95. package/src/Limits.ts +132 -0
  96. package/src/Policy.ts +391 -2772
  97. package/src/Resource.ts +7 -1548
  98. package/src/index.ts +3 -9
  99. package/src/internal/algebra.ts +482 -0
  100. package/src/internal/analysis.ts +478 -0
  101. package/src/internal/authorizer.ts +418 -0
  102. package/src/internal/bind.ts +648 -0
  103. package/src/internal/catalog.ts +534 -0
  104. package/src/internal/decide.ts +402 -0
  105. package/src/internal/domain.ts +66 -0
  106. package/src/internal/json.ts +260 -0
  107. package/src/internal/limits.ts +78 -0
  108. package/src/internal/nodes.ts +404 -0
  109. package/src/internal/patterns.ts +211 -0
  110. package/src/internal/policy-json.ts +487 -0
  111. package/src/internal/policy-nodes.ts +207 -0
  112. package/src/internal/policy.ts +264 -0
  113. package/src/internal/read.ts +55 -0
  114. package/src/internal/resource.ts +180 -0
  115. package/src/internal/rule.ts +196 -0
  116. package/src/internal/search.ts +541 -0
  117. package/src/internal/suggest.ts +104 -0
  118. package/dist/Principal.d.ts +0 -35
  119. package/dist/Principal.d.ts.map +0 -1
  120. package/dist/Principal.js +0 -37
  121. package/dist/Principal.js.map +0 -1
  122. package/dist/Rule.d.ts +0 -275
  123. package/dist/Rule.d.ts.map +0 -1
  124. package/dist/Rule.js +0 -257
  125. package/dist/Rule.js.map +0 -1
  126. package/src/Principal.ts +0 -41
  127. package/src/Rule.ts +0 -683
package/src/Policy.ts CHANGED
@@ -25,41 +25,29 @@
25
25
  * this kind?"
26
26
  * - {@link permittedFields}: the fields that the policy lets a request use.
27
27
  *
28
- * Each of them also takes the request first and the policy later. In a `pipe`,
29
- * the policy gives the domain. Outside a `pipe`, give the domain by hand, as
30
- * in `Policy.decide<typeof App>(request)`. Without it the request has the type
31
- * `never`, so TypeScript can still check it.
28
+ * Each of them takes the policy first and the request second, as in
29
+ * `Policy.decide(policy, request)`. The policy gives the domain, so
30
+ * TypeScript checks the request against it.
32
31
  *
33
32
  * A policy without predicates can become JSON with {@link schema}.
34
33
  *
35
34
  * @stability stable
36
35
  * @since 0.3.0
37
36
  */
38
- // A policy keeps the error type and the service type of its predicates in the
39
- // public types. The code below works on the nodes with no types, where they
40
- // are `unknown`, so the language service check for `unknown` in an Effect is
41
- // off here.
42
- // @effect-diagnostics anyUnknownInErrorContext:off
43
37
  import type * as Context from "effect/Context"
44
- import * as Data from "effect/Data"
45
- import * as Arr from "effect/Array"
46
- import * as Chunk from "effect/Chunk"
47
- import * as Effect from "effect/Effect"
48
- import { dual } from "effect/Function"
49
- import * as HashMap from "effect/HashMap"
50
- import * as HashSet from "effect/HashSet"
51
- import * as Option from "effect/Option"
52
- import { hasProperty } from "effect/Predicate"
38
+ import type * as Data from "effect/Data"
39
+ import type * as Effect from "effect/Effect"
53
40
  import type * as Pipeable from "effect/Pipeable"
54
- import * as Ref from "effect/Ref"
55
- import * as Schema from "effect/Schema"
56
- import * as SchemaIssue from "effect/SchemaIssue"
57
- import * as SchemaTransformation from "effect/SchemaTransformation"
58
- import * as Condition from "./Condition.ts"
59
- import * as Decision from "./Decision.ts"
60
- import * as Domain from "./Domain.ts"
61
- import * as Resource from "./Resource.ts"
62
- import * as Rule from "./Rule.ts"
41
+ import type * as Schema from "effect/Schema"
42
+ import type * as Condition from "./Condition.ts"
43
+ import * as Analysis from "./internal/analysis.ts"
44
+ import * as Decide from "./internal/decide.ts"
45
+ import * as Internal from "./internal/policy.ts"
46
+ import type * as PolicyNodes from "./internal/policy-nodes.ts"
47
+ import * as RuleData from "./internal/rule.ts"
48
+ import type * as Decision from "./Decision.ts"
49
+ import type * as Domain from "./Domain.ts"
50
+ import type * as Resource from "./Resource.ts"
63
51
 
64
52
  /**
65
53
  * @stability stable
@@ -70,117 +58,62 @@ export type Algorithm =
70
58
  "denyOverrides" | "permitOverrides" | "firstApplicable" | "lastApplicable"
71
59
 
72
60
  /**
61
+ * A list of rules with a combining algorithm.
62
+ *
73
63
  * @stability stable
74
64
  * @category models
75
65
  * @since 0.3.0
76
66
  */
77
- export interface RulesNode {
78
- readonly _tag: "Rules"
79
- readonly algorithm: Algorithm
80
- readonly rules: ReadonlyArray<Rule.Any>
81
- }
67
+ export type RulesNode = PolicyNodes.Rules
82
68
 
83
69
  /**
84
70
  * @stability stable
85
71
  * @category models
86
72
  * @since 0.3.0
87
73
  */
88
- export interface AllNode {
89
- readonly _tag: "All"
90
- readonly policies: ReadonlyArray<Node>
91
- }
74
+ export type AllNode = PolicyNodes.All
92
75
 
93
76
  /**
94
77
  * @stability stable
95
78
  * @category models
96
79
  * @since 0.3.0
97
80
  */
98
- export interface AnyNode {
99
- readonly _tag: "Any"
100
- readonly policies: ReadonlyArray<Node>
101
- }
81
+ export type AnyNode = PolicyNodes.Any
102
82
 
103
83
  /**
104
84
  * @stability stable
105
85
  * @category models
106
86
  * @since 0.3.0
107
87
  */
108
- export interface FirstOfNode {
109
- readonly _tag: "FirstOf"
110
- readonly policies: ReadonlyArray<Node>
111
- }
88
+ export type FirstOfNode = PolicyNodes.FirstOf
112
89
 
113
90
  /**
114
91
  * @stability stable
115
92
  * @category models
116
93
  * @since 0.3.0
117
94
  */
118
- export interface CeilingNode {
119
- readonly _tag: "Ceiling"
120
- readonly cap: Node
121
- readonly policy: Node
122
- }
95
+ export type CeilingNode = PolicyNodes.Ceiling
123
96
 
124
97
  /**
125
- * The nodes of a policy tree.
98
+ * The nodes of a policy tree, without types. Each node is a class, and only
99
+ * the functions of this module and the Schema make one.
126
100
  *
127
101
  * @stability stable
128
102
  * @category models
129
103
  * @since 0.3.0
130
104
  */
131
- export type Node = RulesNode | AllNode | AnyNode | FirstOfNode | CeilingNode
132
-
133
- /**
134
- * The tree of a policy with the types of its predicates and its domain `D`.
135
- * Each rule and each child has the types too, and `"~Node"` is only in the types. A node made by
136
- * hand does not have it. So the node of one policy, or a part of it, can not
137
- * go into a policy with other types.
138
- *
139
- * @stability stable
140
- * @category models
141
- * @since 1.0.0
142
- */
143
- export type TypedNode<E = never, R = never, O = never, D = any> = (
144
- | {
145
- readonly _tag: "Rules"
146
- readonly algorithm: Algorithm
147
- // The rules may read different values, so a node keeps no one type of
148
- // what they read. With `never`, a condition taken from a node fits no
149
- // rule that reads a value or a principal.
150
- readonly rules: ReadonlyArray<Rule.Rule<E, R, O, D, never>>
151
- }
152
- | {
153
- readonly _tag: "All" | "Any" | "FirstOf"
154
- readonly policies: ReadonlyArray<TypedNode<E, R, O, D>>
155
- }
156
- | {
157
- readonly _tag: "Ceiling"
158
- readonly cap: TypedNode<E, R, O, D>
159
- readonly policy: TypedNode<E, R, O, D>
160
- }
161
- ) & {
162
- readonly "~Node": Condition.Variance<E, R, O>
163
- }
105
+ export type Node = PolicyNodes.Node
164
106
 
165
107
  /**
166
- * A policy for the domain `D`. `E`, `R` and `O` come from the conditions of
167
- * its rules. See `Condition.Condition`.
108
+ * A policy for the domain `D`. `X` is `Condition.Opaque` when a rule of the
109
+ * policy has a predicate, and `never` when the policy is plain data. A plain
110
+ * policy can become JSON, and `decideSync` takes it.
168
111
  *
169
112
  * @stability stable
170
113
  * @category models
171
114
  * @since 0.3.0
172
115
  */
173
- export interface Policy<D extends Domain.Any, E = never, R = never, O = never>
174
- extends Pipeable.Pipeable {
175
- readonly domain: D
176
- readonly node: TypedNode<E, R, O, D>
177
- readonly "~Policy": Condition.Variance<E, R, O>
178
- /**
179
- * Only in the types. A policy reads the values and the principal of its
180
- * domain, so a policy of one domain is not a policy of a wider domain.
181
- */
182
- readonly "~Domain": (domain: D) => D
183
- }
116
+ export type Policy<D extends Domain.Any, X = never> = Internal.PolicyData<D, X>
184
117
 
185
118
  /**
186
119
  * Any policy.
@@ -189,7 +122,7 @@ export interface Policy<D extends Domain.Any, E = never, R = never, O = never>
189
122
  * @category models
190
123
  * @since 0.3.0
191
124
  */
192
- export type Any = Policy<any, any, any, any>
125
+ export type Any = Policy<any, any>
193
126
 
194
127
  /**
195
128
  * The domain of a policy.
@@ -198,45 +131,17 @@ export type Any = Policy<any, any, any, any>
198
131
  * @category utility types
199
132
  * @since 0.3.0
200
133
  */
201
- export type DomainOf<P> = P extends Policy<infer D, any, any, any> ? D : never
134
+ export type DomainOf<P> = P extends Policy<infer D, any> ? D : never
202
135
 
203
136
  /**
204
- * The error type of a policy.
137
+ * The mark of a policy or a rule: `Condition.Opaque` when it has a predicate.
205
138
  *
206
139
  * @stability stable
207
140
  * @category utility types
208
141
  * @since 0.3.0
209
142
  */
210
- export type Error<P> = P extends {
211
- readonly "~Policy"?: Condition.Variance<infer E, infer _R, infer _O>
212
- }
213
- ? E
214
- : never
215
-
216
- /**
217
- * The service type of a policy.
218
- *
219
- * @stability stable
220
- * @category utility types
221
- * @since 0.3.0
222
- */
223
- export type Services<P> = P extends {
224
- readonly "~Policy"?: Condition.Variance<infer _E, infer R, infer _O>
225
- }
226
- ? R
227
- : never
228
-
229
- /**
230
- * `Condition.Opaque` when a policy has a predicate.
231
- *
232
- * @stability stable
233
- * @category utility types
234
- * @since 0.3.0
235
- */
236
- export type OpaqueOf<P> = P extends {
237
- readonly "~Policy"?: Condition.Variance<infer _E, infer _R, infer O>
238
- }
239
- ? O
143
+ export type OpaqueOf<P> = P extends { readonly "~Opaque": () => infer X }
144
+ ? X
240
145
  : never
241
146
 
242
147
  /**
@@ -288,9 +193,6 @@ export type Scope<D, K extends string> = (
288
193
  Domain.Principal<D>
289
194
  >,
290
195
  ) => Condition.Condition<
291
- never,
292
- never,
293
- never,
294
196
  Condition.Reads<Domain.ValueOf<D, K>, Domain.Principal<D>>
295
197
  >
296
198
 
@@ -370,7 +272,7 @@ export interface AnyRequest {
370
272
  */
371
273
  export interface Step {
372
274
  readonly path: ReadonlyArray<number>
373
- readonly rule: Rule.Any
275
+ readonly rule: AnyRule
374
276
  readonly applies: boolean
375
277
  }
376
278
 
@@ -387,345 +289,329 @@ export interface Explanation {
387
289
  readonly steps: ReadonlyArray<Step>
388
290
  }
389
291
 
292
+ // Rules
293
+
390
294
  /**
391
- * The options of a list of rules.
295
+ * A rule for the domain `D`. `I` is what the tests of its condition read, and
296
+ * `X` is its mark: see `Condition.Condition`. A predicate reads the values and
297
+ * the principal of its domain, so a rule may go only into a policy of its own
298
+ * domain. A rule from {@link rule} has the domain `any`.
392
299
  *
393
300
  * @stability stable
394
301
  * @category models
395
302
  * @since 0.3.0
396
303
  */
397
- export interface Options {
398
- /** The default is `denyOverrides`. */
399
- readonly algorithm?: Algorithm
304
+ export interface Rule<D = any, I = unknown, X = never>
305
+ extends RuleShape<I, X>, Pipeable.Pipeable {
306
+ readonly "~Opaque": () => X
307
+ readonly "~Domain": (domain: D) => D
308
+ [Symbol.iterator](): Generator<Rule<D, I, X>, Rule<D, I, X>, unknown>
400
309
  }
401
310
 
402
- const nodes = Data.taggedEnum<Node>()
311
+ /**
312
+ * The data of a rule.
313
+ *
314
+ * @stability stable
315
+ * @category models
316
+ * @since 1.0.0
317
+ */
318
+ export interface RuleShape<I = unknown, X = never> {
319
+ readonly _tag: "Allow" | "Deny"
320
+ readonly actions: ReadonlyArray<string>
321
+ readonly resources: ReadonlyArray<string>
322
+ readonly when?: Condition.Condition<I, X>
323
+ readonly fields?: ReadonlyArray<string>
324
+ readonly reason?: string
325
+ readonly id?: string
326
+ readonly meta?: unknown
327
+ /**
328
+ * The domain of the builder that made the rule. Two domains may have the
329
+ * same types but other paths, so the types can not tell them apart. It is
330
+ * not in the JSON form of a rule.
331
+ */
332
+ readonly domain?: Domain.Any
333
+ }
403
334
 
404
335
  /**
405
- * The most nodes that the JSON form of a policy may have. A tree may use one
406
- * child in many places, and the JSON form writes it out each time, so a small
407
- * tree may have a very large JSON form.
336
+ * Any rule.
337
+ *
338
+ * @stability stable
339
+ * @category models
340
+ * @since 0.3.0
408
341
  */
409
- const maxEncodedNodes = 100_000
342
+ // Every rule fits `never` as what it reads, and a condition taken from such a
343
+ // rule fits no rule that reads a value or a principal.
344
+ export type AnyRule = Rule<any, never, any>
410
345
 
411
346
  /**
412
- * The number of nodes of the JSON form of a condition. A shared child counts
413
- * one time for each place, as the JSON form writes it out each time.
347
+ * A resource in a rule: a kind name, a resource of the domain, or `"*"`.
348
+ *
349
+ * @stability stable
350
+ * @category models
351
+ * @since 0.3.0
414
352
  */
415
- const conditionSize = (ast: Ast, memo: Map<Ast, number>): number =>
416
- remember(memo, ast, () => {
417
- switch (ast._tag) {
418
- case "And":
419
- case "Or":
420
- return Arr.reduce(
421
- ast.conditions,
422
- 1,
423
- (size, child) => size + conditionSize(child, memo),
424
- )
425
- case "Not":
426
- return 1 + conditionSize(ast.condition, memo)
427
- default:
428
- return 1
429
- }
430
- })
353
+ export type RuleTarget<D> = Domain.Kind<D> | Domain.Resources<D> | "*"
431
354
 
432
355
  /**
433
- * The number of nodes of the JSON form of a tree, with the conditions of its
434
- * rules.
356
+ * The kind name of a target.
357
+ *
358
+ * @stability stable
359
+ * @category utility types
360
+ * @since 0.3.0
435
361
  */
436
- const encodedSize = (
437
- node: Node,
438
- memo: Map<Node, number> = new Map(),
439
- conditions: Map<Ast, number> = new Map(),
440
- ): number =>
441
- remember(memo, node, () => {
442
- switch (node._tag) {
443
- case "Rules":
444
- return Arr.reduce(
445
- node.rules,
446
- 1,
447
- (size, value) =>
448
- size +
449
- 1 +
450
- (value.when === undefined
451
- ? 0
452
- : conditionSize(value.when as Ast, conditions)),
453
- )
454
- case "All":
455
- case "Any":
456
- case "FirstOf":
457
- return Arr.reduce(
458
- node.policies,
459
- 1,
460
- (size, child) => size + encodedSize(child, memo, conditions),
461
- )
462
- case "Ceiling":
463
- return (
464
- 1 +
465
- encodedSize(node.cap, memo, conditions) +
466
- encodedSize(node.policy, memo, conditions)
467
- )
468
- }
469
- })
470
-
471
- /** Gives the JSON form of a tree, when it is not too large. */
472
- const encodeTree = (node: Node): EncodedNode => {
473
- if (encodedSize(node) > maxEncodedNodes) {
474
- throw new globalThis.Error(tooLargeMessage)
475
- }
476
- return encodeNode(node)
477
- }
362
+ export type KindOf<T> = T extends string ? T : Resource.Kind<T>
478
363
 
479
- const tooLargeMessage = `The JSON form of this policy would have more than ${String(maxEncodedNodes)} nodes`
480
-
481
- const encodeNode = (node: Node): EncodedNode => {
482
- switch (node._tag) {
483
- case "Rules":
484
- return {
485
- _tag: "Rules",
486
- algorithm: node.algorithm,
487
- // The domain of a rule is not in its JSON form.
488
- rules: node.rules.map(
489
- ({ domain: _domain, ...value }) => ({ ...value }) as EncodedRule,
490
- ),
491
- }
492
- case "All":
493
- case "Any":
494
- case "FirstOf":
495
- return { _tag: node._tag, policies: node.policies.map(encodeNode) }
496
- case "Ceiling":
497
- return {
498
- _tag: "Ceiling",
499
- cap: encodeNode(node.cap),
500
- policy: encodeNode(node.policy),
501
- }
502
- }
503
- }
364
+ /**
365
+ * The actions that a rule may use for the kinds `K`.
366
+ *
367
+ * @stability stable
368
+ * @category utility types
369
+ * @since 0.3.0
370
+ */
371
+ export type ActionFor<D, K> =
372
+ | "*"
373
+ | ("*" extends K
374
+ ? Domain.AnyAction<D>
375
+ : Domain.ActionOf<D, Extract<K, string>>)
504
376
 
505
377
  /**
506
- * The tree of a policy as a value that `JSON.stringify` keeps with no loss.
507
- * See `Condition.toJsonValue`.
378
+ * The targets in the resource argument of a rule: the one target, or the
379
+ * targets of a list.
380
+ *
381
+ * @stability stable
382
+ * @category utility types
383
+ * @since 1.0.0
508
384
  */
509
- const jsonNode = (node: EncodedNode, meta: Schema.Top | undefined): unknown => {
510
- const json = (child: EncodedNode) => jsonNode(child, meta)
511
- switch (node._tag) {
512
- case "Rules":
513
- return {
514
- ...node,
515
- rules: Arr.map(node.rules, (value) => ({
516
- ...value,
517
- ...(value.when === undefined
518
- ? {}
519
- : { when: Condition.toJsonValue(value.when as Ast) }),
520
- // The `meta` of a rule is a decoded value. Its JSON form comes
521
- // from the `meta` Schema of the domain, as in `schema`.
522
- ...(value.meta === undefined || meta === undefined
523
- ? {}
524
- : {
525
- meta: Schema.encodeUnknownSync(
526
- meta as Schema.Codec<unknown, unknown>,
527
- )(value.meta),
528
- }),
529
- })),
530
- }
531
- case "All":
532
- case "Any":
533
- case "FirstOf":
534
- return { ...node, policies: Arr.map(node.policies, json) }
535
- case "Ceiling":
536
- return { ...node, cap: json(node.cap), policy: json(node.policy) }
537
- }
538
- }
385
+ export type TargetOf<Res> = Res extends ReadonlyArray<infer X> ? X : Res
539
386
 
540
387
  /**
541
- * The class of every policy value. Two policies of one domain with the same
542
- * tree are equal. As JSON, a policy is its tree.
388
+ * The actions that a rule may use for its resource argument. A list of
389
+ * resources may use an action of any of them. One target with a union type
390
+ * may use only an action of every kind in the union, because its value may be
391
+ * any one of them. When the argument may be a list or one target, the rule
392
+ * may use only an action that each of these shapes allows.
393
+ *
394
+ * @stability stable
395
+ * @category utility types
396
+ * @since 1.0.0
543
397
  */
544
- class PolicyData extends Data.Class<{
545
- readonly domain: Domain.Any
546
- readonly node: Node
547
- }> {
548
- toJSON(): unknown {
549
- return jsonNode(encodeTree(this.node), this.domain.meta)
550
- }
551
- }
398
+ export type ActionForTargets<D, Res> = (
399
+ Res extends ReadonlyArray<infer X>
400
+ ? (action: ActionFor<D, KindOf<X>>) => void
401
+ : (action: ActionForTarget<D, Res>) => void
402
+ ) extends (action: infer A) => void
403
+ ? A
404
+ : never
552
405
 
553
- /** The node of a policy, with no types, for the code below. */
554
- const nodeOf = (policy: Any): Node => policy.node as Node
406
+ /** The actions for one target that is not a list. */
407
+ type ActionForTarget<D, Res> =
408
+ | "*"
409
+ | ("*" extends KindOf<Res>
410
+ ? Domain.AnyAction<D>
411
+ : Domain.CommonAction<D, Extract<KindOf<Res>, string>>)
555
412
 
556
- const fromNode = (domain: Domain.Any, node: Node): any =>
557
- new PolicyData({ domain, node })
413
+ /**
414
+ * The data type of the kinds `K`. For `"*"` it has no fields.
415
+ *
416
+ * @stability stable
417
+ * @category utility types
418
+ * @since 0.3.0
419
+ */
420
+ export type DataFor<D, K> = "*" extends K
421
+ ? {}
422
+ : Domain.DataOf<D, Extract<K, string>>
558
423
 
559
424
  /**
560
- * Makes a policy from a list of rules.
425
+ * The value type of the kinds `K`.
561
426
  *
562
427
  * @stability stable
563
- * @category constructors
428
+ * @category utility types
564
429
  * @since 0.3.0
565
430
  */
566
- export const rules: <
567
- D extends Domain.Any,
568
- Rules extends Rule.Rule<any, any, any, D, never>,
569
- >(
570
- domain: D,
571
- rules: Iterable<Rules>,
572
- options?: Options,
573
- ) => Policy<
574
- D,
575
- Rule.Error<Rules>,
576
- Rule.Services<Rules>,
577
- Rule.OpaqueOf<Rules>
578
- > = (domain, values, options) => {
579
- const node = nodes.Rules({
580
- algorithm: options?.algorithm ?? "denyOverrides",
581
- rules: Array.from(values),
582
- })
583
- // A value that is not a rule, such as a copy of a rule made with a spread,
584
- // may keep the type fields of another rule, and it does not work in a
585
- // generator. It is a mistake in the code, so it is a defect.
586
- if (!Arr.every(node.rules, Rule.isRule)) {
587
- throw new globalThis.Error("A value in the list is not a rule")
588
- }
589
- // A rule from a builder of another domain is a mistake in the code, so it
590
- // is a defect. Two domains may have the same types, so the types do not
591
- // find it.
592
- if (
593
- Arr.some(node.rules, (value) => {
594
- const origin = Rule.domainOf(value)
595
- return origin !== undefined && origin !== domain
596
- })
597
- ) {
598
- throw new globalThis.Error(
599
- "A rule comes from the builder of another domain",
600
- )
601
- }
602
- // A rule from `Rule.make` keeps no domain, so its names are checked here.
603
- const issues = validate(domain, encodeNode(node))
604
- if (issues.length > 0) {
605
- throw new globalThis.Error(
606
- `A rule does not fit the domain: ${issues.map((issue) => issue.issue).join("; ")}`,
607
- )
608
- }
609
- return fromNode(domain, node)
431
+ export type ValueFor<D, K> = "*" extends K
432
+ ? Domain.AnyValue<D>
433
+ : Domain.ValueOf<D, Extract<K, string>>
434
+
435
+ /**
436
+ * A field name or a field pattern of the kinds `K`.
437
+ *
438
+ * @stability stable
439
+ * @category models
440
+ * @since 0.3.0
441
+ */
442
+ export type FieldFor<D, K> =
443
+ Condition.FieldName<DataFor<D, K>> | `${string}*${string}`
444
+
445
+ /**
446
+ * A condition that a rule for the kinds `K` may have. Its predicates may read
447
+ * only the values of these kinds and the principal of the domain.
448
+ *
449
+ * @stability stable
450
+ * @category models
451
+ * @since 1.0.0
452
+ */
453
+ export type ConditionFor<D, K> = Condition.Condition<
454
+ Condition.Reads<ValueFor<D, K>, Domain.Principal<D>>,
455
+ any
456
+ >
457
+
458
+ /**
459
+ * The options of a rule.
460
+ *
461
+ * @stability stable
462
+ * @category models
463
+ * @since 0.3.0
464
+ */
465
+ export interface RuleOptions<D, K, C extends Condition.Any> {
466
+ /** The condition. It gets a builder with the types of the resource. */
467
+ readonly when?: (
468
+ builder: Condition.Builder<
469
+ DataFor<D, K>,
470
+ ValueFor<D, K>,
471
+ Domain.Principal<D>
472
+ >,
473
+ ) => C
474
+ readonly fields?: ReadonlyArray<FieldFor<D, K>>
475
+ readonly reason?: string
476
+ readonly id?: string
477
+ readonly meta?: Domain.Meta<D>
610
478
  }
611
479
 
480
+ type SingleOrArray<A> = A | ReadonlyArray<A>
481
+
612
482
  /**
613
- * Makes a policy from a generator function that gives rules. It is pure and it
614
- * gives its result at once.
483
+ * Makes rules with the types of a domain.
484
+ *
485
+ * @stability stable
486
+ * @category models
487
+ * @since 0.3.0
488
+ */
489
+ export interface Builder<D> {
490
+ readonly allow: <
491
+ const Res extends SingleOrArray<RuleTarget<D>>,
492
+ const A extends ActionForTargets<D, Res>,
493
+ const C extends ConditionFor<D, KindOf<TargetOf<Res>>> =
494
+ Condition.Condition,
495
+ >(
496
+ action: SingleOrArray<A>,
497
+ resource: Res,
498
+ options?: RuleOptions<D, KindOf<TargetOf<Res>>, C>,
499
+ ) => Rule<D, Condition.ReadsOf<C>, Condition.OpaqueOf<C>>
500
+ readonly deny: <
501
+ const Res extends SingleOrArray<RuleTarget<D>>,
502
+ const A extends ActionForTargets<D, Res>,
503
+ const C extends ConditionFor<D, KindOf<TargetOf<Res>>> =
504
+ Condition.Condition,
505
+ >(
506
+ action: SingleOrArray<A>,
507
+ resource: Res,
508
+ options?: RuleOptions<D, KindOf<TargetOf<Res>>, C>,
509
+ ) => Rule<D, Condition.ReadsOf<C>, Condition.OpaqueOf<C>>
510
+ }
511
+
512
+ /**
513
+ * Makes rules with the types of a domain. `Policy.define` gives one to its
514
+ * generator. Use this one to make rules outside of a generator.
615
515
  *
616
516
  * @example
617
517
  * ```ts
618
- * const policy = Policy.define(App)(function* (_) {
619
- * yield* _.allow("read", Document)
620
- * yield* _.allow(["update", "delete"], Document, {
621
- * when: ($) => $.eq("ownerId", $.principal("id")),
622
- * })
518
+ * const _ = Policy.builder(App)
519
+ * const rule = _.allow("read", Document, {
520
+ * when: ($) => $.eq("ownerId", $.principal("id")),
623
521
  * })
624
522
  * ```
625
523
  *
626
524
  * @stability stable
627
525
  * @category constructors
628
- * @since 0.3.0
526
+ * @since 2.0.0
629
527
  */
630
- export const define =
631
- <D extends Domain.Any>(domain: D, options?: Options) =>
632
- <Rules extends Rule.Rule<any, any, any, D, never>, A>(
633
- body: (builder: Rule.Builder<D>) => Generator<Rules, A, unknown>,
634
- ): Policy<
635
- D,
636
- Rule.Error<Rules>,
637
- Rule.Services<Rules>,
638
- Rule.OpaqueOf<Rules>
639
- > => {
640
- const iterator = body(Rule.builder(domain))
641
- // Each rule goes back into the generator, as `yield*` gives it.
642
- const collected = Arr.unfold(iterator.next(), (state) =>
643
- state.done === true
644
- ? Option.none()
645
- : Option.some([state.value, iterator.next(state.value)] as const),
646
- )
647
- return rules(domain, collected, options)
648
- }
528
+ export const builder = <D extends Domain.Any>(domain: D): Builder<D> =>
529
+ RuleData.builder(domain) as never
649
530
 
650
531
  /**
651
- * A policy with no rules. It is not applicable to any request.
532
+ * Makes a rule from its data, with no types. It does not check the data
533
+ * against a domain: a policy does that when the rule goes into it. Use
534
+ * {@link builder} for rules with types, or {@link schema} for rules from JSON.
535
+ *
536
+ * The condition may not have a predicate. A policy checks the fields of a
537
+ * condition against the resources of its rule, but it can not check what
538
+ * the code of a predicate reads. A rule from the builder keeps that in its
539
+ * types; this one could give a predicate of one kind to another kind.
652
540
  *
653
541
  * @stability stable
654
542
  * @category constructors
655
- * @since 0.3.0
543
+ * @since 2.0.0
656
544
  */
657
- export const empty = <D extends Domain.Any>(domain: D): Policy<D> =>
658
- rules(domain, [])
659
-
660
- type Combined<D extends Domain.Any, Ps extends ReadonlyArray<Any>> = Policy<
661
- D,
662
- Error<Ps[number]>,
663
- Services<Ps[number]>,
664
- OpaqueOf<Ps[number]>
665
- >
545
+ export const rule: <I = unknown>(
546
+ data: RuleShape<I, never>,
547
+ // The types come from the data only, not from where the rule goes.
548
+ ) => Rule<any, NoInfer<I>> = RuleData.makePlain as never
666
549
 
667
- const sameDomain = (policies: ReadonlyArray<Any>): Domain.Any => {
668
- // A copy of a policy made with a spread may keep the type fields of another
669
- // policy. Only a policy value goes into a tree.
670
- if (!Arr.every(policies, isPolicy)) {
671
- throw new globalThis.Error("A value in the list is not a policy")
672
- }
673
- const domain = policies[0]!.domain
674
- if (!Arr.every(policies, (policy) => policy.domain === domain)) {
675
- throw new globalThis.Error(
676
- "Policies from two domains can not be put together",
677
- )
678
- }
679
- return domain
550
+ /**
551
+ * The options of a list of rules.
552
+ *
553
+ * @stability stable
554
+ * @category models
555
+ * @since 0.3.0
556
+ */
557
+ export interface Options {
558
+ /** The default is `denyOverrides`. */
559
+ readonly algorithm?: Algorithm
680
560
  }
681
561
 
682
562
  /**
683
- * The most levels in a policy tree. A walk over a tree calls itself for each
684
- * level, so a much deeper tree could fill the stack and stop a check.
563
+ * The error when a rule does not fit its domain: it names a kind, an action,
564
+ * a field or a field of the principal that the domain does not have, or it
565
+ * tests a field with a value that the field can not hold. It has one issue
566
+ * for each problem, with its path, and a suggestion when a name has a mistake.
567
+ *
568
+ * @stability stable
569
+ * @category errors
570
+ * @since 2.0.0
685
571
  */
686
- const maxDepth = 256
687
-
688
- /** The number of levels of a policy tree. */
689
- const heightOf = (node: Node, memo: Map<Node, number> = new Map()): number =>
690
- remember(memo, node, () => {
691
- const height = (child: Node) => heightOf(child, memo)
692
- switch (node._tag) {
693
- case "Rules":
694
- return 1
695
- case "All":
696
- case "Any":
697
- case "FirstOf":
698
- return (
699
- 1 +
700
- Arr.reduce(node.policies, 0, (top, child) =>
701
- Math.max(top, height(child)),
702
- )
703
- )
704
- case "Ceiling":
705
- return 1 + Math.max(height(node.cap), height(node.policy))
706
- }
707
- })
572
+ export const PolicyError: typeof Internal.PolicyError = Internal.PolicyError
708
573
 
709
574
  /**
710
- * Makes a policy from a new node. Its children come from policies, so each
711
- * has at most `maxDepth` levels, and the walk of `heightOf` is safe.
575
+ * @stability stable
576
+ * @category errors
577
+ * @since 2.0.0
712
578
  */
713
- const fromCombined = (domain: Domain.Any, node: Node): any => {
714
- if (heightOf(node) > maxDepth) {
715
- throw new globalThis.Error(
716
- `A policy tree may have at most ${maxDepth} levels`,
717
- )
718
- }
719
- return fromNode(domain, node)
720
- }
579
+ export type PolicyError = Internal.PolicyError
580
+
581
+ /** The mark of a union of rules: `Condition.Opaque` when one has a predicate. */
582
+ type RulesOpaque<Rules> = Rules extends { readonly "~Opaque": () => infer X }
583
+ ? X
584
+ : never
585
+
586
+ /**
587
+ * Makes a policy from a generator function that gives rules. It is pure and it
588
+ * gives its result at once.
589
+ *
590
+ * @example
591
+ * ```ts
592
+ * const policy = Policy.define(App)(function* (_) {
593
+ * yield* _.allow("read", Document)
594
+ * yield* _.allow(["update", "delete"], Document, {
595
+ * when: ($) => $.eq("ownerId", $.principal("id")),
596
+ * })
597
+ * })
598
+ * ```
599
+ *
600
+ * @stability stable
601
+ * @category constructors
602
+ * @since 0.3.0
603
+ */
604
+ export const define: <D extends Domain.Any>(
605
+ domain: D,
606
+ options?: Options,
607
+ ) => <Rules extends Rule<D, never, any>, A>(
608
+ body: (builder: Builder<D>) => Generator<Rules, A, unknown>,
609
+ ) => Policy<D, RulesOpaque<Rules>> = Internal.define as never
721
610
 
722
- const combine =
723
- (tag: "All" | "Any" | "FirstOf") =>
724
- (policies: ReadonlyArray<Any>): any =>
725
- fromCombined(
726
- sameDomain(policies),
727
- nodes[tag]({ policies: policies.map(nodeOf) }),
728
- )
611
+ type Combined<D extends Domain.Any, Ps extends ReadonlyArray<Any>> = Policy<
612
+ D,
613
+ OpaqueOf<Ps[number]>
614
+ >
729
615
 
730
616
  /**
731
617
  * Every policy must allow, and one deny is enough to deny. With some policies
@@ -735,11 +621,9 @@ const combine =
735
621
  * @category combinators
736
622
  * @since 0.3.0
737
623
  */
738
- export const all: <
739
- const Ps extends readonly [Policy<any, any, any, any>, ...Array<Any>],
740
- >(
624
+ export const all: <const Ps extends readonly [Any, ...Array<Any>]>(
741
625
  policies: Ps,
742
- ) => Combined<DomainOf<Ps[0]>, Ps> = combine("All")
626
+ ) => Combined<DomainOf<Ps[0]>, Ps> = Internal.combine("All")
743
627
 
744
628
  /**
745
629
  * One allow is enough. Without an allow, one deny gives a deny.
@@ -748,11 +632,9 @@ export const all: <
748
632
  * @category combinators
749
633
  * @since 0.3.0
750
634
  */
751
- export const any: <
752
- const Ps extends readonly [Policy<any, any, any, any>, ...Array<Any>],
753
- >(
635
+ export const any: <const Ps extends readonly [Any, ...Array<Any>]>(
754
636
  policies: Ps,
755
- ) => Combined<DomainOf<Ps[0]>, Ps> = combine("Any")
637
+ ) => Combined<DomainOf<Ps[0]>, Ps> = Internal.combine("Any")
756
638
 
757
639
  /**
758
640
  * The first policy that allows or denies gives the answer.
@@ -761,11 +643,9 @@ export const any: <
761
643
  * @category combinators
762
644
  * @since 0.3.0
763
645
  */
764
- export const firstOf: <
765
- const Ps extends readonly [Policy<any, any, any, any>, ...Array<Any>],
766
- >(
646
+ export const firstOf: <const Ps extends readonly [Any, ...Array<Any>]>(
767
647
  policies: Ps,
768
- ) => Combined<DomainOf<Ps[0]>, Ps> = combine("FirstOf")
648
+ ) => Combined<DomainOf<Ps[0]>, Ps> = Internal.combine("FirstOf")
769
649
 
770
650
  /**
771
651
  * Puts a limit on a policy. A deny from `cap` always wins. An allow from `cap`
@@ -776,92 +656,14 @@ export const firstOf: <
776
656
  * @since 0.3.0
777
657
  */
778
658
  export const ceiling: {
779
- <D extends Domain.Any, E2, R2, O2>(
780
- cap: Policy<D, E2, R2, O2>,
781
- ): <E, R, O>(policy: Policy<D, E, R, O>) => Policy<D, E | E2, R | R2, O | O2>
782
- <D extends Domain.Any, E, R, O, E2, R2, O2>(
783
- policy: Policy<D, E, R, O>,
784
- cap: Policy<D, E2, R2, O2>,
785
- ): Policy<D, E | E2, R | R2, O | O2>
786
- } = dual(2, (policy: Any, cap: Any): any =>
787
- fromCombined(
788
- sameDomain([policy, cap]),
789
- nodes.Ceiling({ cap: nodeOf(cap), policy: nodeOf(policy) }),
790
- ),
791
- )
792
-
793
- /**
794
- * Gives the answer for a key from a memo, or works it out and keeps it. One
795
- * node may be a child of many nodes, so each walk over a tree keeps its work
796
- * on a node in a memo that the walk makes for itself. No memo lives longer
797
- * than one call.
798
- *
799
- * The memo is a native `Map`, because it must find a node by the node itself.
800
- * An Effect `HashMap` finds a key by `Equal`, and the nodes are Effect data,
801
- * so it would hash the whole structure of a node, and a shared child again for
802
- * each path to it.
803
- */
804
- const remember = <K, A>(memo: Map<K, A>, key: K, work: () => A): A => {
805
- // A result may be `undefined`, as the result of a fold may be, so the test
806
- // asks for the key.
807
- if (memo.has(key)) {
808
- return memo.get(key) as A
809
- }
810
- const result = work()
811
- memo.set(key, result)
812
- return result
813
- }
814
-
815
- /** The memos of one walk over a policy tree. */
816
- interface TreeMemo {
817
- readonly done: Map<Node, Node>
818
- readonly predicates: Map<Node, boolean>
819
- }
820
-
821
- const provideNode = (
822
- node: Node,
823
- context: Context.Context<never>,
824
- memo: TreeMemo = { done: new Map(), predicates: new Map() },
825
- ): Node =>
826
- // A node with no predicate needs no services, and it stays the same node.
827
- // One node may be a child of many nodes, and it gives one new node.
828
- !nodeHasPredicate(node, memo.predicates)
829
- ? node
830
- : remember(memo.done, node, () => provideOnce(node, context, memo))
831
-
832
- const provideOnce = (
833
- node: Node,
834
- context: Context.Context<never>,
835
- memo: TreeMemo,
836
- ): Node => {
837
- switch (node._tag) {
838
- case "Rules":
839
- return nodes.Rules({
840
- algorithm: node.algorithm,
841
- rules: node.rules.map((value) =>
842
- value.when === undefined || !Condition.hasPredicate(value.when)
843
- ? value
844
- : Rule.make({
845
- ...value,
846
- when: Condition.provideContext(value.when, context as never),
847
- }),
848
- ),
849
- })
850
- case "All":
851
- case "Any":
852
- case "FirstOf":
853
- return nodes[node._tag]({
854
- policies: Arr.map(node.policies, (child) =>
855
- provideNode(child, context, memo),
856
- ),
857
- })
858
- case "Ceiling":
859
- return nodes.Ceiling({
860
- cap: provideNode(node.cap, context, memo),
861
- policy: provideNode(node.policy, context, memo),
862
- })
863
- }
864
- }
659
+ <D extends Domain.Any, X2>(
660
+ cap: Policy<D, X2>,
661
+ ): <X>(policy: Policy<D, X>) => Policy<D, X | X2>
662
+ <D extends Domain.Any, X, X2>(
663
+ policy: Policy<D, X>,
664
+ cap: Policy<D, X2>,
665
+ ): Policy<D, X | X2>
666
+ } = Internal.ceiling as never
865
667
 
866
668
  /**
867
669
  * Gives the services in `context` to every predicate of a policy, so the
@@ -874,318 +676,18 @@ const provideOnce = (
874
676
  export const provideContext: {
875
677
  <R>(
876
678
  context: Context.Context<R>,
877
- ): <D extends Domain.Any, E, O>(
878
- policy: Policy<D, E, R, O>,
879
- ) => Policy<D, E, never, O>
880
- <D extends Domain.Any, E, R, O>(
881
- policy: Policy<D, E, R, O>,
882
- context: Context.Context<R>,
883
- ): Policy<D, E, never, O>
884
- } = dual(2, (policy: Any, context: Context.Context<never>): any =>
885
- fromNode(policy.domain, provideNode(nodeOf(policy), context)),
886
- )
679
+ ): <D extends Domain.Any, X>(
680
+ policy: Policy<D, X> &
681
+ ([Condition.ServicesOf<X>] extends [R] ? unknown : never),
682
+ ) => Policy<D, Condition.Provided<X>>
683
+ <D extends Domain.Any, X>(
684
+ policy: Policy<D, X>,
685
+ context: Context.Context<Condition.ServicesOf<X>>,
686
+ ): Policy<D, Condition.Provided<X>>
687
+ } = Internal.provideContext as never
887
688
 
888
689
  // Decisions
889
690
 
890
- interface DecideContext {
891
- readonly action: string
892
- readonly kind: string
893
- readonly field: string | undefined
894
- readonly input: Condition.Input
895
- /** The resource of the kind, for the fields that a rule may grant. */
896
- readonly descriptor: Resource.Descriptor
897
- /** The rules that were tested, for {@link explain}. */
898
- readonly trace: Ref.Ref<Chunk.Chunk<Step>> | undefined
899
- readonly predicates: Map<Node, boolean>
900
- /**
901
- * The decisions of nodes with no predicates. One node may be a child of
902
- * many nodes, and its decision is the same each time, so it is made only
903
- * one time. A tree with shared nodes may have very many paths.
904
- */
905
- readonly decisions: Map<Node, Decision.Decision>
906
- }
907
-
908
- const ruleApplies = (
909
- value: Rule.Any,
910
- context: DecideContext,
911
- path: ReadonlyArray<number>,
912
- ): Effect.Effect<boolean, unknown, unknown> => {
913
- if (!Rule.matchesTarget(value, context)) {
914
- return Effect.succeed(false)
915
- }
916
- const test =
917
- value.when === undefined
918
- ? Effect.succeed(true)
919
- : // A rule of a node keeps no type of what it reads. The decision gives
920
- // it the value and the principal of the request.
921
- Condition.evaluate(value.when as Condition.Any, context.input)
922
- const trace = context.trace
923
- return trace === undefined
924
- ? test
925
- : Effect.tap(test, (applies) =>
926
- // A chunk adds a step with no copy of the steps before.
927
- Ref.update(trace, Chunk.append({ path, rule: value, applies })),
928
- )
929
- }
930
-
931
- const fromRule = (value: Rule.Any): Decision.Decision =>
932
- value._tag === "Allow"
933
- ? Decision.Allow({ rule: value })
934
- : value.reason === undefined
935
- ? Decision.Deny({ rule: value })
936
- : Decision.Deny({ rule: value, reason: value.reason })
937
-
938
- /** Gives the first rule that applies, in order. */
939
- const firstApplying = (
940
- candidates: ReadonlyArray<readonly [Rule.Any, number]>,
941
- context: DecideContext,
942
- path: ReadonlyArray<number>,
943
- ): Effect.Effect<Option.Option<Rule.Any>, unknown, unknown> =>
944
- Effect.map(
945
- Effect.findFirst(candidates, ([value, index]) =>
946
- ruleApplies(value, context, [...path, index]),
947
- ),
948
- Option.map(([value]) => value),
949
- )
950
-
951
- /** Gives the first result that is found, and tests `next` only when none is. */
952
- const orElse = <A, E, R>(
953
- first: Effect.Effect<Option.Option<A>, E, R>,
954
- next: () => Effect.Effect<Option.Option<A>, E, R>,
955
- ): Effect.Effect<Option.Option<A>, E, R> =>
956
- Effect.filterOrElse(first, Option.isSome, next)
957
-
958
- const decideRules = (
959
- node: RulesNode,
960
- context: DecideContext,
961
- path: ReadonlyArray<number>,
962
- ): Effect.Effect<Decision.Decision, unknown, unknown> => {
963
- const indexed = Arr.map(node.rules, (value, index) => [value, index] as const)
964
- const denies = Arr.filter(indexed, ([value]) => value._tag === "Deny")
965
- const allows = Arr.filter(indexed, ([value]) => value._tag === "Allow")
966
- const first = (candidates: ReadonlyArray<readonly [Rule.Any, number]>) =>
967
- firstApplying(candidates, context, path)
968
- const found = (() => {
969
- switch (node.algorithm) {
970
- case "denyOverrides":
971
- return orElse(first(denies), () => first(allows))
972
- case "permitOverrides":
973
- return orElse(first(allows), () => first(denies))
974
- case "firstApplicable":
975
- return first(indexed)
976
- case "lastApplicable":
977
- return first(Arr.reverse(indexed))
978
- }
979
- })()
980
- return Effect.map(
981
- found,
982
- Option.match({ onNone: () => Decision.NotApplicable, onSome: fromRule }),
983
- )
984
- }
985
-
986
- const decideNode = (
987
- node: Node,
988
- context: DecideContext,
989
- path: ReadonlyArray<number>,
990
- ): Effect.Effect<Decision.Decision, unknown, unknown> => {
991
- // A predicate that the request may use must run each time. A predicate of
992
- // a rule for another action, kind or field never runs, so the decision may
993
- // be kept. A trace keeps the steps of a node only the first time, so a
994
- // shared tree does not give a step for each path to a node.
995
- if (usesPredicate(node, context)) {
996
- return decideOnce(node, context, path)
997
- }
998
- const cached = context.decisions.get(node)
999
- return cached !== undefined
1000
- ? Effect.succeed(cached)
1001
- : Effect.tap(decideOnce(node, context, path), (decision) =>
1002
- Effect.sync(() => context.decisions.set(node, decision)),
1003
- )
1004
- }
1005
-
1006
- /** The next step of a walk over children: stop with a decision, or go on. */
1007
- type WalkStep<S> =
1008
- | { readonly _tag: "Stop"; readonly decision: Decision.Decision }
1009
- | { readonly _tag: "Next"; readonly state: S }
1010
-
1011
- /**
1012
- * Decides the children in order. After each child, `step` stops with a
1013
- * decision, or gives the state for the next child. After the last child,
1014
- * `end` gives the decision. A child after a stop is not decided, so its
1015
- * predicates do not run.
1016
- */
1017
- const walkChildren = <S>(
1018
- children: ReadonlyArray<Node>,
1019
- context: DecideContext,
1020
- path: ReadonlyArray<number>,
1021
- start: S,
1022
- step: (state: S, decision: Decision.Decision) => WalkStep<S>,
1023
- end: (state: S) => Decision.Decision,
1024
- ): Effect.Effect<Decision.Decision, unknown, unknown> => {
1025
- const go = (
1026
- index: number,
1027
- state: S,
1028
- ): Effect.Effect<Decision.Decision, unknown, unknown> =>
1029
- index === children.length
1030
- ? Effect.succeed(end(state))
1031
- : Effect.flatMap(
1032
- decideNode(children[index]!, context, [...path, index]),
1033
- (decision) => {
1034
- const next = step(state, decision)
1035
- return next._tag === "Stop"
1036
- ? Effect.succeed(next.decision)
1037
- : go(index + 1, next.state)
1038
- },
1039
- )
1040
- return go(0, start)
1041
- }
1042
-
1043
- const stop = (decision: Decision.Decision): WalkStep<never> => ({
1044
- _tag: "Stop",
1045
- decision,
1046
- })
1047
-
1048
- const next = <S>(state: S): WalkStep<S> => ({ _tag: "Next", state })
1049
-
1050
- const decideOnce = (
1051
- node: Node,
1052
- context: DecideContext,
1053
- path: ReadonlyArray<number>,
1054
- ): Effect.Effect<Decision.Decision, unknown, unknown> => {
1055
- switch (node._tag) {
1056
- case "Rules":
1057
- return decideRules(node, context, path)
1058
- case "All":
1059
- // One deny is enough to deny. Every child must allow.
1060
- return walkChildren(
1061
- node.policies,
1062
- context,
1063
- path,
1064
- {
1065
- firstAllow: Option.none<Decision.Decision>(),
1066
- allAllow: node.policies.length > 0,
1067
- },
1068
- (state, decision) =>
1069
- decision._tag === "Deny"
1070
- ? stop(decision)
1071
- : next({
1072
- firstAllow: Option.orElse(state.firstAllow, () =>
1073
- decision._tag === "Allow"
1074
- ? Option.some(decision)
1075
- : Option.none(),
1076
- ),
1077
- allAllow: state.allAllow && decision._tag === "Allow",
1078
- }),
1079
- (state) =>
1080
- state.allAllow
1081
- ? Option.getOrElse(state.firstAllow, () => Decision.NotApplicable)
1082
- : Decision.NotApplicable,
1083
- )
1084
- case "Any":
1085
- // One allow is enough. Without one, the first deny denies.
1086
- return walkChildren(
1087
- node.policies,
1088
- context,
1089
- path,
1090
- Option.none<Decision.Decision>(),
1091
- (firstDeny, decision) =>
1092
- decision._tag === "Allow"
1093
- ? stop(decision)
1094
- : next(
1095
- Option.orElse(firstDeny, () =>
1096
- decision._tag === "Deny"
1097
- ? Option.some(decision)
1098
- : Option.none(),
1099
- ),
1100
- ),
1101
- (firstDeny) =>
1102
- Option.getOrElse(firstDeny, () => Decision.NotApplicable),
1103
- )
1104
- case "FirstOf":
1105
- return walkChildren(
1106
- node.policies,
1107
- context,
1108
- path,
1109
- undefined,
1110
- (_, decision) =>
1111
- decision._tag === "NotApplicable" ? next(undefined) : stop(decision),
1112
- () => Decision.NotApplicable,
1113
- )
1114
- case "Ceiling":
1115
- // A deny of the cap wins. Else the policy decides.
1116
- return Effect.filterOrElse(
1117
- decideNode(node.cap, context, [...path, 0]),
1118
- (capped) => capped._tag === "Deny",
1119
- () => decideNode(node.policy, context, [...path, 1]),
1120
- )
1121
- }
1122
- }
1123
-
1124
- /**
1125
- * Tests if a request fits its kind: the action is an action of the kind, and
1126
- * the field, if there is one, is a field of the kind. The types check this,
1127
- * but a value with a union type, or a value from outside, may get past them.
1128
- * A rule for every action (`"*"`) must not cover an action that the kind does
1129
- * not have, so such a request is not applicable.
1130
- *
1131
- * @stability stable
1132
- * @category predicates
1133
- * @since 1.0.0
1134
- */
1135
- export const fitsKind = (
1136
- descriptor: Resource.Descriptor,
1137
- request: { readonly action: string; readonly field?: string | undefined },
1138
- ): boolean =>
1139
- descriptor.actions.includes(request.action) &&
1140
- (request.field === undefined ||
1141
- descriptor.fieldPaths.includes(request.field) ||
1142
- Resource.hasPath(descriptor.schema, request.field) ||
1143
- Resource.hasGetter(descriptor.schema, request.field))
1144
-
1145
- const makeContext = (
1146
- domain: Domain.Any,
1147
- request: AnyRequest,
1148
- trace: Ref.Ref<Chunk.Chunk<Step>> | undefined,
1149
- ): DecideContext | undefined => {
1150
- const kind = Resource.kindOf(request.resource)
1151
- const descriptor =
1152
- kind === undefined ? undefined : Domain.descriptorOf(domain, kind)
1153
- if (
1154
- kind === undefined ||
1155
- descriptor === undefined ||
1156
- !fitsKind(descriptor, request)
1157
- ) {
1158
- return undefined
1159
- }
1160
- return {
1161
- action: request.action,
1162
- kind,
1163
- field: request.field,
1164
- input: {
1165
- value: request.resource,
1166
- principal: request.principal,
1167
- path: descriptor.path,
1168
- },
1169
- descriptor,
1170
- trace,
1171
- predicates: new Map(),
1172
- decisions: new Map(),
1173
- }
1174
- }
1175
-
1176
- const decideAny = (
1177
- policy: Any,
1178
- request: AnyRequest,
1179
- ): Effect.Effect<Decision.Decision, unknown, unknown> =>
1180
- // Each run of the Effect needs its own store of decisions, because the
1181
- // resource value may change between two runs.
1182
- Effect.suspend(() => {
1183
- const context = makeContext(policy.domain, request, undefined)
1184
- return context === undefined
1185
- ? Effect.succeed(Decision.NotApplicable)
1186
- : decideNode(nodeOf(policy), context, [])
1187
- })
1188
-
1189
691
  /**
1190
692
  * Gives the decision of a policy for a request.
1191
693
  *
@@ -1193,17 +695,14 @@ const decideAny = (
1193
695
  * @category evaluation
1194
696
  * @since 0.3.0
1195
697
  */
1196
- export const decide: {
1197
- <D extends Domain.Any = never>(
1198
- request: Request<D>,
1199
- ): <E, R, O>(
1200
- policy: Policy<D, E, R, O>,
1201
- ) => Effect.Effect<Decision.Decision, E, R>
1202
- <D extends Domain.Any, E, R, O>(
1203
- policy: Policy<D, E, R, O>,
1204
- request: Request<NoInfer<D>>,
1205
- ): Effect.Effect<Decision.Decision, E, R>
1206
- } = dual(2, decideAny) as never
698
+ export const decide: <D extends Domain.Any, X>(
699
+ policy: Policy<D, X>,
700
+ request: Request<NoInfer<D>>,
701
+ ) => Effect.Effect<
702
+ Decision.Decision,
703
+ Condition.ErrorOf<X>,
704
+ Condition.ServicesOf<X>
705
+ > = Decide.decideAny as never
1207
706
 
1208
707
  /**
1209
708
  * Gives the decision of a policy with no predicates, at once.
@@ -1212,106 +711,31 @@ export const decide: {
1212
711
  * @category evaluation
1213
712
  * @since 0.3.0
1214
713
  */
1215
- export const decideSync: {
1216
- <D extends Domain.Any = never>(
1217
- request: Request<D>,
1218
- ): (policy: Policy<D>) => Decision.Decision
1219
- <D extends Domain.Any>(
1220
- policy: Policy<D>,
1221
- request: Request<NoInfer<D>>,
1222
- ): Decision.Decision
1223
- } = dual(2, (policy: Any, request: AnyRequest): Decision.Decision =>
1224
- Effect.runSync(
1225
- // The public type of `decideSync` takes only policies with no
1226
- // predicates, so this Effect has no error and needs no services.
1227
- // @effect-diagnostics-next-line unsafeEffectTypeAssertion:off
1228
- decideAny(policy, request) as Effect.Effect<Decision.Decision>,
1229
- ),
1230
- )
714
+ export const decideSync: <D extends Domain.Any>(
715
+ policy: Policy<D>,
716
+ request: Request<NoInfer<D>>,
717
+ ) => Decision.Decision = Decide.decideSync
1231
718
 
1232
719
  /**
1233
720
  * Gives the decision of a policy and the rules that were tested to make it.
1234
721
  *
1235
- * One node may be a child of many nodes. A node with no predicate that the
1236
- * request may use gives the same decision each time, so its rules are tested,
1237
- * and are in the steps, only the first time. So a small tree with many shared
1238
- * nodes gives a short list of steps.
722
+ * One node may be a child of many nodes. Its rules are tested, and are in the
723
+ * steps, each time that the decision comes to it.
1239
724
  *
1240
725
  * @stability stable
1241
726
  * @category evaluation
1242
727
  * @since 0.3.0
1243
728
  */
1244
- export const explain: {
1245
- <D extends Domain.Any = never>(
1246
- request: Request<D>,
1247
- ): <E, R, O>(policy: Policy<D, E, R, O>) => Effect.Effect<Explanation, E, R>
1248
- <D extends Domain.Any, E, R, O>(
1249
- policy: Policy<D, E, R, O>,
1250
- request: Request<NoInfer<D>>,
1251
- ): Effect.Effect<Explanation, E, R>
1252
- } = dual(
1253
- 2,
1254
- (
1255
- policy: Any,
1256
- request: AnyRequest,
1257
- ): Effect.Effect<Explanation, unknown, unknown> =>
1258
- // Each run of the Effect needs its own trace, so the trace is made inside
1259
- // the Effect.
1260
- Effect.gen(function* () {
1261
- const trace = yield* Ref.make(Chunk.empty<Step>())
1262
- const context = makeContext(policy.domain, request, trace)
1263
- if (context === undefined) {
1264
- return { decision: Decision.NotApplicable, steps: [] }
1265
- }
1266
- const decision = yield* decideNode(nodeOf(policy), context, [])
1267
- return { decision, steps: Chunk.toReadonlyArray(yield* Ref.get(trace)) }
1268
- }),
1269
- ) as never
1270
-
1271
- /**
1272
- * Tests if a deny rule of the request names a part inside `field`.
1273
- */
1274
- const deniesInside = (
1275
- node: Node,
1276
- target: { readonly action: string; readonly kind: string },
1277
- field: string,
1278
- // A native set: it must find a node by the node itself. See `remember`.
1279
- seen: Set<Node> = new Set(),
1280
- ): boolean => {
1281
- // One node may be a child of many nodes. It is tested one time.
1282
- if (seen.has(node)) {
1283
- return false
1284
- }
1285
- seen.add(node)
1286
- switch (node._tag) {
1287
- case "Rules":
1288
- return node.rules.some(
1289
- (value) =>
1290
- value._tag === "Deny" &&
1291
- value.fields !== undefined &&
1292
- value.fields.some((pattern) => Rule.matchesInside(pattern, field)) &&
1293
- (value.actions.includes("*") ||
1294
- value.actions.includes(target.action)) &&
1295
- (value.resources.includes("*") ||
1296
- value.resources.includes(target.kind)),
1297
- )
1298
- case "All":
1299
- case "Any":
1300
- case "FirstOf":
1301
- return node.policies.some((child) =>
1302
- deniesInside(child, target, field, seen),
1303
- )
1304
- case "Ceiling":
1305
- return (
1306
- deniesInside(node.cap, target, field, seen) ||
1307
- deniesInside(node.policy, target, field, seen)
1308
- )
1309
- }
1310
- }
729
+ export const explain: <D extends Domain.Any, X>(
730
+ policy: Policy<D, X>,
731
+ request: Request<NoInfer<D>>,
732
+ ) => Effect.Effect<Explanation, Condition.ErrorOf<X>, Condition.ServicesOf<X>> =
733
+ Decide.explain as never
1311
734
 
1312
735
  /**
1313
736
  * Gives the fields of a resource value that the policy lets a request use. It
1314
- * tests each field of the resource Schema, with dots for nested fields.
737
+ * tests each field that a list of fields shows: each scalar, record or array
738
+ * of the resource Schema, with dots for nested fields.
1315
739
  *
1316
740
  * A field that holds more than one value, such as a record, is left out when a
1317
741
  * deny rule names a part of it. The list can not name that part, and the whole
@@ -1321,91 +745,17 @@ const deniesInside = (
1321
745
  * @category evaluation
1322
746
  * @since 0.3.0
1323
747
  */
1324
- export const permittedFields: {
1325
- <D extends Domain.Any = never>(
1326
- request: FieldlessRequest<D>,
1327
- ): <E, R, O>(
1328
- policy: Policy<D, E, R, O>,
1329
- ) => Effect.Effect<ReadonlyArray<string>, E, R>
1330
- <D extends Domain.Any, E, R, O>(
1331
- policy: Policy<D, E, R, O>,
1332
- request: FieldlessRequest<NoInfer<D>>,
1333
- ): Effect.Effect<ReadonlyArray<string>, E, R>
1334
- } = dual(
1335
- 2,
1336
- (
1337
- policy: Any,
1338
- request: Omit<AnyRequest, "field">,
1339
- ): Effect.Effect<ReadonlyArray<string>, unknown, unknown> => {
1340
- const kind = Resource.kindOf(request.resource)
1341
- const descriptor: Resource.Descriptor | undefined =
1342
- kind === undefined ? undefined : Domain.descriptorOf(policy.domain, kind)
1343
- if (descriptor === undefined) {
1344
- return Effect.succeed([])
1345
- }
1346
- return Effect.filter(descriptor.fieldPaths, (field) =>
1347
- Effect.map(
1348
- decideAny(policy, { ...request, field }),
1349
- (result) =>
1350
- result._tag === "Allow" &&
1351
- !(
1352
- descriptor.openPaths.includes(field) &&
1353
- deniesInside(
1354
- nodeOf(policy),
1355
- { ...request, kind: descriptor.kind },
1356
- field,
1357
- )
1358
- ),
1359
- ),
1360
- )
1361
- },
1362
- ) as never
748
+ export const permittedFields: <D extends Domain.Any, X>(
749
+ policy: Policy<D, X>,
750
+ request: FieldlessRequest<NoInfer<D>>,
751
+ ) => Effect.Effect<
752
+ ReadonlyArray<string>,
753
+ Condition.ErrorOf<X>,
754
+ Condition.ServicesOf<X>
755
+ > = Decide.permittedFields as never
1363
756
 
1364
757
  // Residual conditions
1365
758
 
1366
- type Ast = Condition.Ast
1367
-
1368
- /**
1369
- * The two residual conditions of a request. See {@link residuals}.
1370
- *
1371
- * @stability stable
1372
- * @category models
1373
- * @since 1.0.0
1374
- */
1375
- export interface Residuals<E = never, R = never, O = never, I = unknown> {
1376
- /** The condition on resource values for which the policy allows. */
1377
- readonly allow: Condition.Condition<E, R, O, I>
1378
- /** The condition on resource values for which the policy denies. */
1379
- readonly deny: Condition.Condition<E, R, O, I>
1380
- }
1381
-
1382
- /**
1383
- * A request about a kind of resource, with no types.
1384
- *
1385
- * @stability stable
1386
- * @category models
1387
- * @since 1.0.0
1388
- */
1389
- export interface AnyKindRequest {
1390
- readonly principal: object
1391
- readonly action: string
1392
- readonly kind: string
1393
- readonly field?: string | undefined
1394
- /**
1395
- * A scope needs the types of the domain for its builder. With the request
1396
- * first, the domain is not known yet, so give the policy first.
1397
- */
1398
- readonly within?: never
1399
- }
1400
-
1401
- /**
1402
- * `unknown` when the request `Req` is a request of the type `T`, and `never`
1403
- * when it is not. With the request first and the policy later, the request is
1404
- * checked when the policy, and so the domain, is known. For a union, each
1405
- * member must be such a request.
1406
- */
1407
- type Fits<Req, T> = [Req] extends [T] ? unknown : never
1408
-
1409
759
  /**
1410
760
  * What a residual condition reads: a value of the kind `K`. The principal is
1411
761
  * already in it. A residual is for one kind, so when `K` is a union of kinds,
@@ -1425,504 +775,6 @@ export type ResidualReads<D, K> = Condition.Reads<
1425
775
  unknown
1426
776
  >
1427
777
 
1428
- const always: Ast = Condition.always
1429
- const never: Ast = Condition.never
1430
-
1431
- /**
1432
- * The most nodes that a residual condition may have. A policy tree may use a
1433
- * condition of a child two times, in its allow condition and in its deny
1434
- * condition, so a small policy may give a very large condition. Past this
1435
- * size, `residual` throws an error.
1436
- */
1437
- const maxResidualSize = 10_000
1438
-
1439
- /**
1440
- * The most levels that a residual condition may have. The rules of an ordered
1441
- * algorithm, such as `firstApplicable`, give one level for each run of rules
1442
- * with one tag. A walk over a condition calls itself for each level, so a much
1443
- * deeper condition could fill the stack. Past this depth, `residual` throws an
1444
- * error.
1445
- */
1446
- const maxResidualDepth = 500
1447
-
1448
- /**
1449
- * A residual condition is too large. See {@link maxResidualSize} and
1450
- * {@link maxResidualDepth}.
1451
- */
1452
- class TooLarge {}
1453
-
1454
- /**
1455
- * Makes conditions for one residual, with no types. Each step simplifies the
1456
- * condition, and it stops the work when the condition is too large. The
1457
- * steps keep their work for the parts they have seen, for this residual only.
1458
- */
1459
- interface Build {
1460
- // A list goes in as an array, not as arguments: a call may take only so
1461
- // many arguments, and a policy may have more rules.
1462
- readonly and: (values: ReadonlyArray<Ast>) => Ast
1463
- readonly or: (values: ReadonlyArray<Ast>) => Ast
1464
- readonly not: (value: Ast) => Ast
1465
- }
1466
-
1467
- const makeBuild = (): Build => {
1468
- const simplify = Condition.simplifier()
1469
- const sizes = new Map<Ast, number>()
1470
- /** Gives the number of nodes in a condition, as JSON would have them. */
1471
- const sizeOf = (ast: Ast): number =>
1472
- remember(sizes, ast, () => {
1473
- switch (ast._tag) {
1474
- case "And":
1475
- case "Or":
1476
- return Arr.reduce(
1477
- ast.conditions,
1478
- 1,
1479
- (size, child) => size + sizeOf(child),
1480
- )
1481
- case "Not":
1482
- return 1 + sizeOf(ast.condition)
1483
- default:
1484
- return 1
1485
- }
1486
- })
1487
- const depths = new Map<Ast, number>()
1488
- /**
1489
- * Gives the number of levels in a condition. Each child went through
1490
- * `limit`, so this walk is not deeper than `maxResidualDepth`.
1491
- */
1492
- const depthOf = (ast: Ast): number =>
1493
- remember(depths, ast, () => {
1494
- switch (ast._tag) {
1495
- case "And":
1496
- case "Or":
1497
- return (
1498
- 1 +
1499
- Arr.reduce(ast.conditions, 0, (top, child) =>
1500
- Math.max(top, depthOf(child)),
1501
- )
1502
- )
1503
- case "Not":
1504
- return 1 + depthOf(ast.condition)
1505
- default:
1506
- return 1
1507
- }
1508
- })
1509
- const limit = (ast: Ast): Ast => {
1510
- if (sizeOf(ast) > maxResidualSize || depthOf(ast) > maxResidualDepth) {
1511
- throw new TooLarge()
1512
- }
1513
- return ast
1514
- }
1515
- // These steps work on nodes with no types, so they leave out the type
1516
- // field of a condition.
1517
- return {
1518
- and: (values) =>
1519
- limit(simplify(Condition.andAll(values as ReadonlyArray<never>))),
1520
- or: (values) =>
1521
- limit(simplify(Condition.orAll(values as ReadonlyArray<never>))),
1522
- not: (value) => limit(simplify(Condition.not(value as never))),
1523
- }
1524
- }
1525
-
1526
- /** The two residual conditions of a node, with no types. */
1527
- interface Pair {
1528
- readonly allow: Ast
1529
- readonly deny: Ast
1530
- }
1531
-
1532
- interface ResidualContext {
1533
- readonly action: string
1534
- readonly kind: string
1535
- readonly field: string | undefined
1536
- readonly input: {
1537
- readonly principal: object
1538
- readonly path: Condition.Input["path"]
1539
- }
1540
- /** The resource of the kind, for the fields that a rule may grant. */
1541
- readonly descriptor: Resource.Descriptor
1542
- /**
1543
- * The work done on each node. One node may be a child of many nodes, so
1544
- * the work on it is done only one time.
1545
- */
1546
- readonly pairs: Map<Node, Pair>
1547
- readonly predicates: Map<Node, boolean>
1548
- readonly build: Build
1549
- }
1550
-
1551
- const ruleCondition = (value: Rule.Any, context: ResidualContext): Ast =>
1552
- !Rule.matchesTarget(value, context)
1553
- ? never
1554
- : value.when === undefined
1555
- ? always
1556
- : Condition.partial(value.when as Condition.Any, context.input)
1557
-
1558
- /**
1559
- * Gives the conditions for "the first rule that applies is an allow rule" and
1560
- * "the first rule that applies is a deny rule".
1561
- */
1562
- const firstApplicableResidual = (
1563
- values: ReadonlyArray<Rule.Any>,
1564
- context: ResidualContext,
1565
- ): Pair => {
1566
- const { and, not, or } = context.build
1567
- // The rules go in runs of one tag, from the last run to the first. The
1568
- // conditions of a run go into one node at once. With one node for each
1569
- // rule, each step would flatten the whole condition again.
1570
- const runs = Arr.isReadonlyArrayNonEmpty(values)
1571
- ? Arr.groupWith(values, (a, b) => a._tag === b._tag)
1572
- : []
1573
- return Arr.reduceRight(
1574
- runs,
1575
- { allow: never, deny: never },
1576
- (rest: Pair, run): Pair => {
1577
- const applies = or(Arr.map(run, (value) => ruleCondition(value, context)))
1578
- return run[0]._tag === "Allow"
1579
- ? {
1580
- allow: or([applies, rest.allow]),
1581
- deny: and([not(applies), rest.deny]),
1582
- }
1583
- : {
1584
- allow: and([not(applies), rest.allow]),
1585
- deny: or([applies, rest.deny]),
1586
- }
1587
- },
1588
- )
1589
- }
1590
-
1591
- const residualNode = (node: Node, context: ResidualContext): Pair =>
1592
- remember(context.pairs, node, () => residualOnce(node, context))
1593
-
1594
- const residualOnce = (node: Node, context: ResidualContext): Pair => {
1595
- const { and, not, or } = context.build
1596
- switch (node._tag) {
1597
- case "Rules": {
1598
- switch (node.algorithm) {
1599
- case "denyOverrides": {
1600
- const deny = or(
1601
- node.rules
1602
- .filter((value) => value._tag === "Deny")
1603
- .map((value) => ruleCondition(value, context)),
1604
- )
1605
- const allow = and([
1606
- not(deny),
1607
- or(
1608
- node.rules
1609
- .filter((value) => value._tag === "Allow")
1610
- .map((value) => ruleCondition(value, context)),
1611
- ),
1612
- ])
1613
- return { allow, deny }
1614
- }
1615
- case "permitOverrides": {
1616
- const allow = or(
1617
- node.rules
1618
- .filter((value) => value._tag === "Allow")
1619
- .map((value) => ruleCondition(value, context)),
1620
- )
1621
- const deny = and([
1622
- not(allow),
1623
- or(
1624
- node.rules
1625
- .filter((value) => value._tag === "Deny")
1626
- .map((value) => ruleCondition(value, context)),
1627
- ),
1628
- ])
1629
- return { allow, deny }
1630
- }
1631
- case "firstApplicable":
1632
- return firstApplicableResidual(node.rules, context)
1633
- case "lastApplicable":
1634
- return firstApplicableResidual(node.rules.slice().reverse(), context)
1635
- }
1636
- }
1637
- // falls through: every algorithm returns above
1638
- case "All": {
1639
- if (node.policies.length === 0) {
1640
- return { allow: never, deny: never }
1641
- }
1642
- const pairs = node.policies.map((child) => residualNode(child, context))
1643
- const deny = or(pairs.map((pair) => pair.deny))
1644
- const allow = and([...pairs.map((pair) => pair.allow), not(deny)])
1645
- return { allow, deny }
1646
- }
1647
- case "Any": {
1648
- const pairs = node.policies.map((child) => residualNode(child, context))
1649
- const allow = or(pairs.map((pair) => pair.allow))
1650
- const deny = and([not(allow), or(pairs.map((pair) => pair.deny))])
1651
- return { allow, deny }
1652
- }
1653
- case "FirstOf": {
1654
- // The first child that gives an answer wins.
1655
- return Arr.reduceRight(
1656
- node.policies,
1657
- { allow: never, deny: never },
1658
- (rest: Pair, child): Pair => {
1659
- const pair = residualNode(child, context)
1660
- return {
1661
- allow: or([pair.allow, and([not(pair.deny), rest.allow])]),
1662
- deny: or([pair.deny, and([not(pair.allow), rest.deny])]),
1663
- }
1664
- },
1665
- )
1666
- }
1667
- case "Ceiling": {
1668
- const cap = residualNode(node.cap, context)
1669
- const inner = residualNode(node.policy, context)
1670
- return {
1671
- allow: and([not(cap.deny), inner.allow]),
1672
- deny: or([cap.deny, inner.deny]),
1673
- }
1674
- }
1675
- }
1676
- }
1677
-
1678
- /** The name of the predicate of a residual condition that runs `decide`. */
1679
- const policyPredicate = "policy"
1680
-
1681
- /** Tests if a rule of a policy tree has a predicate. */
1682
- const nodeHasPredicate = (
1683
- node: Node,
1684
- memo: Map<Node, boolean> = new Map(),
1685
- ): boolean =>
1686
- remember(memo, node, () => {
1687
- const test = (child: Node) => nodeHasPredicate(child, memo)
1688
- switch (node._tag) {
1689
- case "Rules":
1690
- return Arr.some(
1691
- node.rules,
1692
- (value) =>
1693
- value.when !== undefined && Condition.hasPredicate(value.when),
1694
- )
1695
- case "All":
1696
- case "Any":
1697
- case "FirstOf":
1698
- return Arr.some(node.policies, test)
1699
- case "Ceiling":
1700
- return test(node.cap) || test(node.policy)
1701
- }
1702
- })
1703
-
1704
- /** Tests if a rule that the request may use has a predicate. */
1705
- /** What {@link usesPredicate} needs: the request and a memo. */
1706
- interface PredicateContext {
1707
- readonly action: string
1708
- readonly kind: string
1709
- readonly field: string | undefined
1710
- readonly descriptor: Resource.Descriptor
1711
- readonly predicates: Map<Node, boolean>
1712
- }
1713
-
1714
- const usesPredicate = (node: Node, context: PredicateContext): boolean =>
1715
- remember(context.predicates, node, () => usesPredicateOnce(node, context))
1716
-
1717
- const usesPredicateOnce = (node: Node, context: PredicateContext): boolean => {
1718
- switch (node._tag) {
1719
- case "Rules":
1720
- return node.rules.some(
1721
- (value) =>
1722
- value.when !== undefined &&
1723
- Condition.hasPredicate(value.when) &&
1724
- Rule.matchesTarget(value, context),
1725
- )
1726
- case "All":
1727
- case "Any":
1728
- case "FirstOf":
1729
- return node.policies.some((child) => usesPredicate(child, context))
1730
- case "Ceiling":
1731
- return (
1732
- usesPredicate(node.cap, context) || usesPredicate(node.policy, context)
1733
- )
1734
- }
1735
- }
1736
-
1737
- const isScopeValue = (operand: Condition.Operand): boolean =>
1738
- operand._tag === "Principal" ||
1739
- operand.value === null ||
1740
- typeof operand.value === "string" ||
1741
- typeof operand.value === "number" ||
1742
- typeof operand.value === "boolean"
1743
-
1744
- /**
1745
- * Gives the problem of a scope that is not plain data, or `undefined` when it
1746
- * is plain data. A predicate, or a value that a condition can not hold, such
1747
- * as `undefined` from a route parameter, is a defect: such a scope could make
1748
- * a check pass on the wrong values. A shared node is tested one time.
1749
- */
1750
- const scopeProblem = (
1751
- ast: Ast,
1752
- memo: Map<Ast, string | null> = new Map(),
1753
- ): string | undefined =>
1754
- remember(memo, ast, (): string | null => {
1755
- switch (ast._tag) {
1756
- case "Predicate":
1757
- return "can not have a predicate"
1758
- case "Compare":
1759
- return isScopeValue(ast.value)
1760
- ? null
1761
- : `has a value for "${ast.field}" that is not a scalar`
1762
- case "In":
1763
- return Arr.every(ast.values, isScopeValue)
1764
- ? null
1765
- : `has a value for "${ast.field}" that is not a scalar`
1766
- case "Under":
1767
- return Arr.every(ast.prefix, isScopeValue)
1768
- ? null
1769
- : "has a path part that is not a scalar"
1770
- case "And":
1771
- case "Or":
1772
- return (
1773
- Option.getOrNull(
1774
- Arr.findFirst(
1775
- Arr.map(ast.conditions, (child) => scopeProblem(child, memo)),
1776
- (problem) => problem !== undefined,
1777
- ),
1778
- ) ?? null
1779
- )
1780
- case "Not":
1781
- return scopeProblem(ast.condition, memo) ?? null
1782
- default:
1783
- return null
1784
- }
1785
- }) ?? undefined
1786
-
1787
- /**
1788
- * Gives the condition of a scope, with the principal and the path in it.
1789
- * With no scope, it is `True`.
1790
- */
1791
- /**
1792
- * The fields that a scope reads, before it is made smaller. A smaller scope
1793
- * may leave out a read that fails, such as a getter.
1794
- */
1795
- const scopeFields = (within: unknown): ReadonlyArray<string> =>
1796
- within === undefined
1797
- ? []
1798
- : Condition.fields(
1799
- (within as (builder: unknown) => Ast)(Condition.builder()),
1800
- )
1801
-
1802
- const scopeOf = (within: unknown, input: ResidualContext["input"]): Ast => {
1803
- if (within === undefined) {
1804
- return always
1805
- }
1806
- const ast = (within as (builder: unknown) => Ast)(Condition.builder())
1807
- const problem = scopeProblem(ast)
1808
- if (problem !== undefined) {
1809
- throw new globalThis.Error(`A scope ${problem}`)
1810
- }
1811
- return Condition.partial(ast as Condition.Condition, input)
1812
- }
1813
-
1814
- /** A request about a kind with no types, with the scope of a target. */
1815
- interface ScopedRequest {
1816
- readonly principal: object
1817
- readonly action: string
1818
- readonly kind: string
1819
- readonly field?: string | undefined
1820
- readonly within?: unknown
1821
- }
1822
-
1823
- /** The scope of a request, and the residual conditions inside it. */
1824
- interface Scoped extends Pair {
1825
- readonly scope: Ast
1826
- readonly schema: Schema.Top | undefined
1827
- }
1828
-
1829
- const residualsAny = (policy: Any, request: ScopedRequest): Pair =>
1830
- scopedResiduals(policy, request)
1831
-
1832
- const scopedResiduals = (policy: Any, request: ScopedRequest): Scoped => {
1833
- const descriptor = Domain.descriptorOf(policy.domain, request.kind)
1834
- if (descriptor === undefined || !fitsKind(descriptor, request)) {
1835
- return { scope: never, allow: never, deny: never, schema: undefined }
1836
- }
1837
- // A scope is made smaller below, and that may leave out a read of a
1838
- // getter that fails. So such a scope has no residual.
1839
- if (scopeReadsGetter(request, descriptor)) {
1840
- throw new globalThis.Error(
1841
- "The scope of this request reads a getter, so it has no residual condition",
1842
- )
1843
- }
1844
- const input = { principal: request.principal, path: descriptor.path }
1845
- const scope = scopeOf(request.within, input)
1846
- const build = makeBuild()
1847
- const and = build.and
1848
- const inScope = (pair: Pair): Scoped => ({
1849
- scope,
1850
- allow: and([scope, pair.allow]),
1851
- deny: and([scope, pair.deny]),
1852
- schema: descriptor.schema,
1853
- })
1854
- if (scope._tag === "False") {
1855
- return inScope({ allow: never, deny: never })
1856
- }
1857
- const context: ResidualContext = {
1858
- action: request.action,
1859
- kind: request.kind,
1860
- field: request.field,
1861
- input,
1862
- descriptor,
1863
- pairs: new Map(),
1864
- predicates: new Map(),
1865
- build,
1866
- }
1867
- if (!usesPredicate(nodeOf(policy), context)) {
1868
- // A read of a getter may fail when `decide` runs, and a smaller
1869
- // condition may leave the read out. So a plain residual can not agree
1870
- // with `decide`, and there is none.
1871
- if (readsGetter(policy, request, descriptor)) {
1872
- throw new globalThis.Error(
1873
- "A rule of this request reads a getter, so it has no residual condition",
1874
- )
1875
- }
1876
- try {
1877
- return inScope(residualNode(nodeOf(policy), context))
1878
- } catch (error) {
1879
- // The type of the residual of a plain policy says that it has no
1880
- // predicate, so it can not be the predicate below. The policy is too
1881
- // large for a residual, and that is a defect.
1882
- if (error instanceof TooLarge) {
1883
- throw new globalThis.Error(
1884
- `The residual condition of this request would have more than ${String(maxResidualSize)} nodes or ${String(maxResidualDepth)} levels`,
1885
- )
1886
- }
1887
- throw error
1888
- }
1889
- }
1890
- // A predicate may fail, and it may give another answer each time it runs.
1891
- // A tree of `And` and `Or` nodes can not run the predicates in the same
1892
- // order and the same number of times as `decide`. So the residual is one
1893
- // predicate that runs `decide` for each value.
1894
- const decision = (value: object) =>
1895
- decideNode(
1896
- policy.node,
1897
- {
1898
- action: request.action,
1899
- kind: request.kind,
1900
- field: request.field,
1901
- input: {
1902
- value,
1903
- principal: request.principal,
1904
- path: descriptor.path,
1905
- },
1906
- descriptor,
1907
- trace: undefined,
1908
- predicates: new Map(),
1909
- decisions: new Map(),
1910
- },
1911
- [],
1912
- )
1913
- // A value of another kind is not a value that the residual is about, so
1914
- // the rules of this kind must not run on it.
1915
- const test = (tag: Decision.Decision["_tag"]): Ast =>
1916
- Condition.builder<any, any, any>().predicate(policyPredicate, (value) =>
1917
- Resource.kindOf(value) === request.kind
1918
- ? Effect.map(decision(value), (result) => result._tag === tag)
1919
- : Effect.succeed(false),
1920
- )
1921
- // The scope comes first in each `And`, so the policy runs only on values
1922
- // in the scope.
1923
- return inScope({ allow: test("Allow"), deny: test("Deny") })
1924
- }
1925
-
1926
778
  /**
1927
779
  * Gives the condition on resource values for which the policy allows a
1928
780
  * request. It has no principal fields and no `Under` nodes, so you can give it
@@ -1942,50 +794,22 @@ const scopedResiduals = (policy: Any, request: ScopedRequest): Scoped => {
1942
794
  *
1943
795
  * A policy tree may use the condition of a child two times, so a small policy
1944
796
  * may give a very large condition. When the condition would have more than
1945
- * 10000 nodes or 500 levels, this function throws an error. In an
1946
- * `Authorizer`, the error is
1947
- * a defect, so the check fails.
797
+ * 10000 nodes, this function throws an error. In an `Authorizer`, the error
798
+ * is a defect, so the check fails.
1948
799
  *
1949
800
  * @stability stable
1950
801
  * @category evaluation
1951
802
  * @since 0.3.0
1952
803
  */
1953
- export const residual: {
1954
- <const Req extends AnyKindRequest>(
1955
- request: Req,
1956
- ): <D extends Domain.Any, E, R, O>(
1957
- policy: Policy<D, E, R, O> & Fits<Req, KindRequest<D>>,
1958
- ) => Condition.Condition<E, R, O, ResidualReads<D, Req["kind"]>>
1959
- <D extends Domain.Any, E, R, O, const Req extends KindRequest<NoInfer<D>>>(
1960
- policy: Policy<D, E, R, O>,
1961
- request: Req,
1962
- ): Condition.Condition<E, R, O, ResidualReads<D, Req["kind"]>>
1963
- } = dual(
1964
- 2,
1965
- (policy: Any, request: KindRequest<Domain.Any>): Ast =>
1966
- residualsAny(policy, request).allow,
1967
- ) as never
1968
-
1969
- /**
1970
- * Gives both residual conditions of a request: the values that the policy
1971
- * allows, and the values that it denies. A value in neither is not
1972
- * applicable. See {@link residual}.
1973
- *
1974
- * @stability stable
1975
- * @category evaluation
1976
- * @since 1.0.0
1977
- */
1978
- export const residuals: {
1979
- <const Req extends AnyKindRequest>(
1980
- request: Req,
1981
- ): <D extends Domain.Any, E, R, O>(
1982
- policy: Policy<D, E, R, O> & Fits<Req, KindRequest<D>>,
1983
- ) => Residuals<E, R, O, ResidualReads<D, Req["kind"]>>
1984
- <D extends Domain.Any, E, R, O, const Req extends KindRequest<NoInfer<D>>>(
1985
- policy: Policy<D, E, R, O>,
1986
- request: Req,
1987
- ): Residuals<E, R, O, ResidualReads<D, Req["kind"]>>
1988
- } = dual(2, residualsAny) as never
804
+ export const residual: <
805
+ D extends Domain.Any,
806
+ X,
807
+ const Req extends KindRequest<NoInfer<D>>,
808
+ >(
809
+ policy: Policy<D, X>,
810
+ request: Req,
811
+ ) => Condition.Condition<ResidualReads<D, Req["kind"]>, X> =
812
+ Analysis.residual as never
1989
813
 
1990
814
  /**
1991
815
  * How much of a scope a policy allows. In each case, `condition` is the exact
@@ -2006,275 +830,47 @@ export const residuals: {
2006
830
  * @category models
2007
831
  * @since 1.0.0
2008
832
  */
2009
- export type Coverage<
2010
- E = never,
2011
- R = never,
2012
- O = never,
2013
- I = unknown,
2014
- > = Data.TaggedEnum<{
2015
- All: { readonly condition: Condition.Condition<E, R, O, I> }
2016
- None: { readonly condition: Condition.Condition<E, R, O, I> }
833
+ export type Coverage<I = unknown, X = never> = Data.TaggedEnum<{
834
+ All: { readonly condition: Condition.Condition<I, X> }
835
+ None: { readonly condition: Condition.Condition<I, X> }
2017
836
  Some: {
2018
- readonly condition: Condition.Condition<E, R, O, I>
837
+ readonly condition: Condition.Condition<I, X>
2019
838
  readonly allowed: Condition.Example
2020
839
  readonly refused: Condition.Example
2021
840
  }
2022
841
  Unknown: {
2023
- readonly condition: Condition.Condition<E, R, O, I>
2024
- readonly reason: "predicate" | "budget" | "schema"
842
+ readonly condition: Condition.Condition<I, X>
843
+ readonly reason: "predicate" | "budget"
2025
844
  }
2026
845
  }>
2027
846
 
2028
- /** A {@link Coverage} with no types. */
2029
- type AnyCoverage = Data.TaggedEnum<{
2030
- All: { readonly condition: Ast }
2031
- None: { readonly condition: Ast }
2032
- Some: {
2033
- readonly condition: Ast
2034
- readonly allowed: Condition.Example
2035
- readonly refused: Condition.Example
2036
- }
2037
- Unknown: {
2038
- readonly condition: Ast
2039
- readonly reason: "predicate" | "budget" | "schema"
2040
- }
2041
- }>
2042
-
2043
- const coverages = Data.taggedEnum<AnyCoverage>()
2044
-
2045
- /**
2046
- * Tests if a rule that the request may use, or its scope, reads a getter of
2047
- * a class, in any branch of a union.
2048
- */
2049
- const readsGetter = (
2050
- policy: Any,
2051
- request: ScopedRequest,
2052
- descriptor: Resource.Descriptor,
2053
- ): boolean =>
2054
- scopeReadsGetter(request, descriptor) ||
2055
- !readsOnly(
2056
- nodeOf(policy),
2057
- targetOf(request, descriptor),
2058
- (field) => !readsGetterAt(descriptor.schema, field),
2059
- new Map(),
2060
- )
2061
-
2062
- /**
2063
- * Tests if a read of a field may read a getter of a class: at the field, or
2064
- * at a field on the way to it, in any branch of a union.
2065
- */
2066
- const readsGetterAt = (schema: Schema.Top, field: string): boolean => {
2067
- const parts = field.split(".")
2068
- return Arr.some(Arr.range(1, parts.length), (count) =>
2069
- Resource.hasGetter(schema, parts.slice(0, count).join(".")),
2070
- )
2071
- }
2072
-
2073
- /**
2074
- * Tests if the scope of a request reads a getter of a class. A scope is made
2075
- * smaller in each residual, also next to a predicate that runs `decide`, so
2076
- * such a scope has no residual.
2077
- */
2078
- const scopeReadsGetter = (
2079
- request: ScopedRequest,
2080
- descriptor: Resource.Descriptor,
2081
- ): boolean =>
2082
- !Arr.every(
2083
- scopeFields(request.within),
2084
- (field) => !readsGetterAt(descriptor.schema, field),
2085
- )
2086
-
2087
- /**
2088
- * Tests if a request has no residual: its scope reads a getter, or it has a
2089
- * plain residual and a rule that it may use reads a getter. A residual with
2090
- * a predicate runs `decide`, so the reads of its rules stay.
2091
- */
2092
- const hasNoResidual = (
2093
- policy: Any,
2094
- request: ScopedRequest,
2095
- descriptor: Resource.Descriptor,
2096
- ): boolean =>
2097
- scopeReadsGetter(request, descriptor) ||
2098
- (!usesPredicate(nodeOf(policy), targetOf(request, descriptor)) &&
2099
- readsGetter(policy, request, descriptor))
2100
-
2101
- /** The parts of a request that tell which rules it may use. */
2102
- const targetOf = (
2103
- request: ScopedRequest,
2104
- descriptor: Resource.Descriptor,
2105
- ): PredicateContext => ({
2106
- action: request.action,
2107
- kind: request.kind,
2108
- field: request.field,
2109
- descriptor,
2110
- predicates: new Map(),
2111
- })
2112
-
2113
- /**
2114
- * Tests if each field that a rule that the request may use reads passes
2115
- * `test`. It looks at the conditions before they are made smaller. A smaller
2116
- * condition may leave out a field, but a read of that field, such as a
2117
- * getter, may fail when `decide` runs. A shared node is visited one time.
2118
- */
2119
- const readsOnly = (
2120
- node: Node,
2121
- target: PredicateContext,
2122
- test: (field: string) => boolean,
2123
- memo: Map<Node, boolean>,
2124
- ): boolean =>
2125
- remember(memo, node, () => {
2126
- switch (node._tag) {
2127
- case "Rules":
2128
- return Arr.every(
2129
- node.rules,
2130
- (value) =>
2131
- value.when === undefined ||
2132
- !Rule.matchesTarget(value, target) ||
2133
- Arr.every(Condition.fields(value.when as Ast), test),
2134
- )
2135
- case "All":
2136
- case "Any":
2137
- case "FirstOf":
2138
- return Arr.every(node.policies, (child) =>
2139
- readsOnly(child, target, test, memo),
2140
- )
2141
- case "Ceiling":
2142
- return (
2143
- readsOnly(node.cap, target, test, memo) &&
2144
- readsOnly(node.policy, target, test, memo)
2145
- )
2146
- }
2147
- })
2148
-
2149
- const coverageAny = (policy: Any, request: ScopedRequest): AnyCoverage => {
2150
- const descriptor = Domain.descriptorOf(policy.domain, request.kind)
2151
- // A request with no residual has the condition `False`.
2152
- if (descriptor !== undefined && hasNoResidual(policy, request, descriptor)) {
2153
- return coverages.Unknown({ condition: never, reason: "schema" })
2154
- }
2155
- const scoped = scopedResiduals(policy, request)
2156
- const allowed = scoped.allow
2157
- // Each field that a rule or the scope reads must be plain, also a field
2158
- // that a smaller condition leaves out. The condition is the residual.
2159
- if (descriptor !== undefined) {
2160
- const plain = (field: string) =>
2161
- Resource.isPlainField(descriptor.schema, field)
2162
- if (
2163
- readsGetter(policy, request, descriptor) ||
2164
- !Arr.every(scopeFields(request.within), plain) ||
2165
- !readsOnly(
2166
- nodeOf(policy),
2167
- targetOf(request, descriptor),
2168
- plain,
2169
- new Map(),
2170
- )
2171
- ) {
2172
- return coverages.Unknown({ condition: allowed, reason: "schema" })
2173
- }
2174
- }
2175
-
2176
- const options = { schema: scoped.schema }
2177
- const some =
2178
- allowed._tag === "False"
2179
- ? undefined
2180
- : Condition.satisfiable(allowed as Condition.Condition, options)
2181
- if (some === undefined || some._tag === "Unsatisfiable") {
2182
- return coverages.None({ condition: never })
2183
- }
2184
- if (some._tag === "Unknown") {
2185
- return coverages.Unknown({ condition: allowed, reason: some.reason })
2186
- }
2187
- // Only two nodes more than `allowed`, so it needs no size limit.
2188
- const refused: Ast = Condition.simplify(
2189
- Condition.and(
2190
- scoped.scope as Condition.Condition,
2191
- Condition.not(allowed as Condition.Condition),
2192
- ),
2193
- )
2194
- const missing =
2195
- refused._tag === "False"
2196
- ? undefined
2197
- : Condition.satisfiable(refused as Condition.Condition, options)
2198
- if (missing === undefined || missing._tag === "Unsatisfiable") {
2199
- return coverages.All({ condition: scoped.scope })
2200
- }
2201
- if (missing._tag === "Unknown") {
2202
- return coverages.Unknown({ condition: allowed, reason: missing.reason })
2203
- }
2204
- return coverages.Some({
2205
- condition: allowed,
2206
- allowed: some.example,
2207
- refused: missing.example,
2208
- })
2209
- }
2210
-
2211
- /**
2212
- * Tests if the scope of a request has a value of the Schema of its kind. The
2213
- * answer is `true` only when the search finds such a value. When the search
2214
- * can not say, the answer is `false`. So a "yes" from it is safe to act on,
2215
- * as for the question "may the decider answer for this scope?".
2216
- *
2217
- * @stability stable
2218
- * @category evaluation
2219
- * @since 1.0.0
2220
- */
2221
- export const scopeHasValue = (policy: Any, request: ScopedRequest): boolean => {
2222
- const scoped = scopedResiduals(policy, request)
2223
- return (
2224
- Condition.satisfiable(scoped.scope as Condition.Any, {
2225
- schema: scoped.schema,
2226
- })._tag === "Satisfiable"
2227
- )
2228
- }
2229
-
2230
- /**
2231
- * Tests if the scope of a request has no value of the Schema of its kind. A
2232
- * scope with a mistake, such as two values for one field, is empty. With no
2233
- * scope, the answer is `false` when the kind has a value. When the search can
2234
- * not say, the answer is `false`.
2235
- *
2236
- * @stability stable
2237
- * @category evaluation
2238
- * @since 1.0.0
2239
- */
2240
- export const scopeIsEmpty = (policy: Any, request: ScopedRequest): boolean => {
2241
- const scoped = scopedResiduals(policy, request)
2242
- return (
2243
- Condition.satisfiable(scoped.scope as Condition.Any, {
2244
- schema: scoped.schema,
2245
- })._tag === "Unsatisfiable"
2246
- )
2247
- }
2248
-
2249
847
  /**
2250
848
  * Tells how much of a scope the policy allows: all of it, none of it, or some
2251
849
  * of it. See {@link Coverage}. With no scope (`within`), the scope is every
2252
850
  * value of the kind.
2253
851
  *
2254
- * The answer comes from {@link Condition.satisfiable} with the Schema of the
2255
- * resource, so it is exact for the values of the Schema. When a condition
2256
- * tests a field that is not a plain scalar (see `Resource.isPlainField`),
2257
- * such as a field with a check other than a finite or integer check, the
2258
- * answer is `Unknown`, and a check on the kind says no.
852
+ * The answer comes from {@link Condition.satisfiable} with the shape of the
853
+ * Schema of the resource. `All` and `None` hold for every value of the
854
+ * Schema. `Some` means that, by the shape of the Schema, a value may get
855
+ * either answer: a check such as a minimum length may still stop one of the
856
+ * examples. `Unknown` comes only from a predicate or from the budget of the
857
+ * search.
2259
858
  *
2260
- * `Some` means that some value of the Schema is allowed. It does not mean that
2261
- * such a value is in your database. To know that, run `condition` as a query.
859
+ * `Some` does not mean that such a value is in your database. To know that,
860
+ * run `condition` as a query.
2262
861
  *
2263
862
  * @stability stable
2264
863
  * @category evaluation
2265
864
  * @since 1.0.0
2266
865
  */
2267
- export const coverage: {
2268
- <const Req extends AnyKindRequest>(
2269
- request: Req,
2270
- ): <D extends Domain.Any, E, R, O>(
2271
- policy: Policy<D, E, R, O> & Fits<Req, KindRequest<D>>,
2272
- ) => Coverage<E, R, O, ResidualReads<D, Req["kind"]>>
2273
- <D extends Domain.Any, E, R, O, const Req extends KindRequest<NoInfer<D>>>(
2274
- policy: Policy<D, E, R, O>,
2275
- request: Req,
2276
- ): Coverage<E, R, O, ResidualReads<D, Req["kind"]>>
2277
- } = dual(2, coverageAny) as never
866
+ export const coverage: <
867
+ D extends Domain.Any,
868
+ X,
869
+ const Req extends KindRequest<NoInfer<D>>,
870
+ >(
871
+ policy: Policy<D, X>,
872
+ request: Req,
873
+ ) => Coverage<ResidualReads<D, Req["kind"]>, X> = Analysis.coverageAny as never
2278
874
 
2279
875
  // Schema
2280
876
 
@@ -2319,997 +915,20 @@ export type EncodedNode =
2319
915
  readonly policy: EncodedNode
2320
916
  }
2321
917
 
2322
- const AlgorithmSchema = Schema.Literals([
2323
- "denyOverrides",
2324
- "permitOverrides",
2325
- "firstApplicable",
2326
- "lastApplicable",
2327
- ])
2328
-
2329
- const encodedRuleSchema = (meta: Schema.Top | undefined) =>
2330
- Schema.Struct({
2331
- _tag: Schema.Literals(["Allow", "Deny"]),
2332
- actions: Schema.Array(Schema.String),
2333
- resources: Schema.Array(Schema.String),
2334
- when: Schema.optionalKey(Condition.schema),
2335
- fields: Schema.String.pipe(Schema.Array, Schema.optionalKey),
2336
- reason: Schema.optionalKey(Schema.String),
2337
- id: Schema.optionalKey(Schema.String),
2338
- meta: Schema.optionalKey(
2339
- (meta ?? Schema.Never) as Schema.Codec<unknown, unknown>,
2340
- ),
2341
- })
2342
-
2343
- /**
2344
- * The most levels of objects and arrays in the JSON form of a policy. The
2345
- * Schema of a policy calls itself for each level, so a much deeper input
2346
- * could fill the stack before the checks of the policy run. A policy tree of
2347
- * `maxDepth` levels with deep conditions stays well below this.
2348
- */
2349
- const maxJsonDepth = 1000
2350
-
2351
- /**
2352
- * The most objects and arrays in the JSON form of an input, with a shared one
2353
- * counted for each place. The decoder writes out a shared one each time, so
2354
- * an input with few objects may give a very large tree.
2355
- */
2356
- const maxInputNodes = 1_000_000
2357
-
2358
- /** The size and the height of a value, in objects and arrays. */
2359
- interface Measure {
2360
- readonly size: number
2361
- readonly height: number
2362
- }
2363
-
2364
- /**
2365
- * Measures a value, or gives `undefined` when it is higher than `levels`. A
2366
- * shared object is measured one time. The walk stops at that height, so it
2367
- * does not fill the stack itself. The memo is a native `Map`, because it
2368
- * must find an object by the object itself.
2369
- */
2370
- const measure = (
2371
- input: unknown,
2372
- levels: number,
2373
- memo: Map<object, Measure | { readonly failed: number }>,
2374
- ): Measure | undefined => {
2375
- if (typeof input !== "object" || input === null) {
2376
- return { size: 0, height: 0 }
2377
- }
2378
- const cached = memo.get(input)
2379
- // A value that was too high for some levels is too high for fewer.
2380
- if (
2381
- cached !== undefined &&
2382
- ("failed" in cached ? levels <= cached.failed : true)
2383
- ) {
2384
- return "failed" in cached || cached.height > levels ? undefined : cached
2385
- }
2386
- // After the first child that is too high, the fold measures no more
2387
- // children. The walk has few frames for each level, so it does not fill
2388
- // the stack at the most levels.
2389
- // An input with a key that a loop over the keys does not see, or with a
2390
- // getter, may give the decoder more than the measure sees. Such an input
2391
- // is not plain data, so it fails.
2392
- const result =
2393
- levels === 0 || !isPlainData(input)
2394
- ? undefined
2395
- : Arr.reduce(
2396
- Object.values(input),
2397
- { size: 1, height: 1 } as Measure | undefined,
2398
- (total, child) => {
2399
- const measured =
2400
- total === undefined ? undefined : measure(child, levels - 1, memo)
2401
- return total === undefined || measured === undefined
2402
- ? undefined
2403
- : {
2404
- size: total.size + measured.size,
2405
- height: Math.max(total.height, 1 + measured.height),
2406
- }
2407
- },
2408
- )
2409
- memo.set(input, result ?? { failed: levels })
2410
- return result
2411
- }
2412
-
2413
- /**
2414
- * Tests if each own key of an object is a plain data key that a loop over the
2415
- * keys sees. The `length` of an array is the one key that is not seen. The
2416
- * decoder reads keys of the prototypes too, so a prototype other than the
2417
- * one of a plain object or an array may have only methods, as the prototype
2418
- * of a class has.
2419
- */
2420
- const isPlainData = (input: object): boolean =>
2421
- Arr.every(Reflect.ownKeys(input), (key) => {
2422
- const property = Object.getOwnPropertyDescriptor(input, key)
2423
- return (
2424
- property !== undefined &&
2425
- "value" in property &&
2426
- (property.enumerable === true ||
2427
- (Array.isArray(input) && key === "length"))
2428
- )
2429
- }) && hasOnlyMethods(Object.getPrototypeOf(input))
2430
-
2431
- /** Tests if the prototypes from `prototype` up have only methods. */
2432
- const hasOnlyMethods = (prototype: unknown): boolean =>
2433
- prototype === null ||
2434
- prototype === Object.prototype ||
2435
- prototype === Array.prototype ||
2436
- (typeof prototype === "object" &&
2437
- Arr.every(Reflect.ownKeys(prototype), (key) => {
2438
- const property = Object.getOwnPropertyDescriptor(prototype, key)
2439
- return (
2440
- typeof key === "symbol" ||
2441
- key === "constructor" ||
2442
- (property !== undefined &&
2443
- "value" in property &&
2444
- typeof property.value === "function")
2445
- )
2446
- }) &&
2447
- hasOnlyMethods(Object.getPrototypeOf(prototype)))
2448
-
2449
- /** Tests if a value is low and small enough to decode. */
2450
- const fitsLimits = (input: unknown): string | true => {
2451
- const measured = measure(input, maxJsonDepth, new Map())
2452
- return measured === undefined
2453
- ? `A policy may have at most ${String(maxJsonDepth)} levels of JSON`
2454
- : measured.size > maxInputNodes
2455
- ? `A policy may have at most ${String(maxInputNodes)} objects and arrays in JSON`
2456
- : true
2457
- }
2458
-
2459
- const encodedNodeSchema = (
2460
- meta: Schema.Top | undefined,
2461
- ): Schema.Codec<EncodedNode> => {
2462
- const self: Schema.Codec<EncodedNode> = Schema.Union([
2463
- Schema.TaggedStruct("Rules", {
2464
- algorithm: AlgorithmSchema,
2465
- rules: Schema.Array(encodedRuleSchema(meta)),
2466
- }),
2467
- Schema.TaggedStruct("All", {
2468
- policies: Schema.Array(Schema.suspend(() => self)),
2469
- }),
2470
- Schema.TaggedStruct("Any", {
2471
- policies: Schema.Array(Schema.suspend(() => self)),
2472
- }),
2473
- Schema.TaggedStruct("FirstOf", {
2474
- policies: Schema.Array(Schema.suspend(() => self)),
2475
- }),
2476
- Schema.TaggedStruct("Ceiling", {
2477
- cap: Schema.suspend(() => self),
2478
- policy: Schema.suspend(() => self),
2479
- }),
2480
- ]) as never
2481
- return self
2482
- }
2483
-
2484
- type ConditionNode = Condition.Encoded | Ast
2485
-
2486
- const principalRefs = (ast: ConditionNode): ReadonlyArray<string> =>
2487
- Array.from(refsIn(ast, new Map()))
2488
-
2489
- /** The principal fields of a node. A shared node is visited one time. */
2490
- const refsIn = (
2491
- node: ConditionNode,
2492
- memo: Map<ConditionNode, HashSet.HashSet<string>>,
2493
- ): HashSet.HashSet<string> =>
2494
- remember(memo, node, () => {
2495
- const refs = (operands: ReadonlyArray<Condition.Operand>) =>
2496
- HashSet.fromIterable(
2497
- Arr.flatMap(operands, (operand) =>
2498
- operand._tag === "Principal" ? [operand.path] : [],
2499
- ),
2500
- )
2501
- switch (node._tag) {
2502
- case "Compare":
2503
- return refs([node.value])
2504
- case "In":
2505
- return refs(node.values)
2506
- case "Under":
2507
- return refs(node.prefix)
2508
- case "And":
2509
- case "Or":
2510
- return Arr.reduce(
2511
- node.conditions as ReadonlyArray<ConditionNode>,
2512
- HashSet.empty<string>(),
2513
- (all, child) => HashSet.union(all, refsIn(child, memo)),
2514
- )
2515
- case "Not":
2516
- return refsIn(node.condition, memo)
2517
- default:
2518
- return HashSet.empty()
2519
- }
2520
- })
2521
-
2522
- interface Issue {
2523
- readonly path: ReadonlyArray<PropertyKey>
2524
- readonly issue: string
2525
- }
2526
-
2527
- /** A test of a field with a fixed value, as in `eq("level", 3)`. */
2528
- type LiteralTest = readonly [field: string, value: Condition.Scalar]
2529
-
2530
- const literalTests = (node: ConditionNode): ReadonlyArray<LiteralTest> =>
2531
- Arr.map(Array.from(literalTestsIn(node, new Map())), (text) => {
2532
- const [field, value] = JSON.parse(text) as readonly [string, unknown]
2533
- return [
2534
- field,
2535
- hasProperty(value, "number") ? Number(value.number) : value,
2536
- ] as LiteralTest
2537
- })
2538
-
2539
- /**
2540
- * The JSON text of a test. JSON has no `Infinity`, `-Infinity` or `NaN`, so
2541
- * such a number goes in as its text.
2542
- */
2543
- const literalText = (field: string, value: Condition.Scalar): string =>
2544
- JSON.stringify([
2545
- field,
2546
- typeof value === "number" && !Number.isFinite(value)
2547
- ? { number: String(value) }
2548
- : value,
2549
- ])
2550
-
2551
- /**
2552
- * The tests of fields with fixed values in a node, each as the JSON text of a
2553
- * {@link LiteralTest}. A set of texts keeps each test one time, so a node
2554
- * with many shared parts gives a small set. A shared node is visited one time.
2555
- */
2556
- const literalTestsIn = (
2557
- node: ConditionNode,
2558
- memo: Map<ConditionNode, HashSet.HashSet<string>>,
2559
- ): HashSet.HashSet<string> =>
2560
- remember(memo, node, () => {
2561
- const tests = (
2562
- field: string,
2563
- operands: ReadonlyArray<Condition.Operand>,
2564
- ): HashSet.HashSet<string> =>
2565
- HashSet.fromIterable(
2566
- Arr.flatMap(operands, (operand) =>
2567
- operand._tag === "Literal" ? [literalText(field, operand.value)] : [],
2568
- ),
2569
- )
2570
- switch (node._tag) {
2571
- case "Compare":
2572
- return tests(node.field, [node.value])
2573
- case "In":
2574
- return tests(node.field, node.values)
2575
- case "And":
2576
- case "Or":
2577
- return Arr.reduce(
2578
- node.conditions as ReadonlyArray<ConditionNode>,
2579
- HashSet.empty<string>(),
2580
- (all, child) => HashSet.union(all, literalTestsIn(child, memo)),
2581
- )
2582
- case "Not":
2583
- return literalTestsIn(node.condition, memo)
2584
- default:
2585
- return HashSet.empty()
2586
- }
2587
- })
2588
-
2589
- /** A test of a field with a field of the principal, as in `eq("ownerId", $.principal("id"))`. */
2590
- type PrincipalTest = readonly [field: string, ref: string]
2591
-
2592
- const principalTests = (node: ConditionNode): ReadonlyArray<PrincipalTest> =>
2593
- Arr.map(
2594
- Array.from(principalTestsIn(node, new Map())),
2595
- (text) => JSON.parse(text) as PrincipalTest,
2596
- )
2597
-
2598
- /**
2599
- * The tests of fields with principal fields in a node, each as the JSON text
2600
- * of a {@link PrincipalTest}. See {@link literalTestsIn}.
2601
- */
2602
- const principalTestsIn = (
2603
- node: ConditionNode,
2604
- memo: Map<ConditionNode, HashSet.HashSet<string>>,
2605
- ): HashSet.HashSet<string> =>
2606
- remember(memo, node, () => {
2607
- const tests = (
2608
- field: string,
2609
- operands: ReadonlyArray<Condition.Operand>,
2610
- ): HashSet.HashSet<string> =>
2611
- HashSet.fromIterable(
2612
- Arr.flatMap(operands, (operand) =>
2613
- operand._tag === "Principal"
2614
- ? [JSON.stringify([field, operand.path])]
2615
- : [],
2616
- ),
2617
- )
2618
- switch (node._tag) {
2619
- case "Compare":
2620
- return tests(node.field, [node.value])
2621
- case "In":
2622
- return tests(node.field, node.values)
2623
- case "And":
2624
- case "Or":
2625
- return Arr.reduce(
2626
- node.conditions as ReadonlyArray<ConditionNode>,
2627
- HashSet.empty<string>(),
2628
- (all, child) => HashSet.union(all, principalTestsIn(child, memo)),
2629
- )
2630
- case "Not":
2631
- return principalTestsIn(node.condition, memo)
2632
- default:
2633
- return HashSet.empty()
2634
- }
2635
- })
2636
-
2637
- /**
2638
- * Tests if a node has a fixed value that is not a {@link Condition.Scalar},
2639
- * such as `undefined`. A shared node is visited one time.
2640
- */
2641
- const hasBadLiteralIn = (
2642
- node: ConditionNode,
2643
- memo: Map<ConditionNode, boolean>,
2644
- ): boolean =>
2645
- remember(memo, node, () => {
2646
- const bad = (operands: ReadonlyArray<Condition.Operand>) =>
2647
- Arr.some(
2648
- operands,
2649
- (operand) =>
2650
- operand._tag === "Literal" &&
2651
- operand.value !== null &&
2652
- typeof operand.value !== "string" &&
2653
- typeof operand.value !== "number" &&
2654
- typeof operand.value !== "boolean",
2655
- )
2656
- switch (node._tag) {
2657
- case "Compare":
2658
- return bad([node.value])
2659
- case "In":
2660
- return bad(node.values)
2661
- case "Under":
2662
- return bad(node.prefix)
2663
- case "And":
2664
- case "Or":
2665
- return Arr.some(
2666
- node.conditions as ReadonlyArray<ConditionNode>,
2667
- (child) => hasBadLiteralIn(child, memo),
2668
- )
2669
- case "Not":
2670
- return hasBadLiteralIn(node.condition, memo)
2671
- default:
2672
- return false
2673
- }
2674
- })
2675
-
2676
- /**
2677
- * The principal fields in the prefixes of `Under` nodes, each as the JSON text
2678
- * of the place in the prefix and the path of the field. See
2679
- * {@link literalTestsIn}.
2680
- */
2681
- /**
2682
- * The fixed values in the prefixes of `Under` nodes, each as the JSON text of
2683
- * the place in the prefix, the value, and the fixed strings of the whole
2684
- * prefix. A number that JSON can not hold is left out: it fits no field
2685
- * that a path names. See {@link underRefsIn}.
2686
- */
2687
- const underLiteralsIn = (
2688
- node: ConditionNode,
2689
- memo: Map<ConditionNode, HashSet.HashSet<string>>,
2690
- ): HashSet.HashSet<string> =>
2691
- remember(memo, node, () => {
2692
- switch (node._tag) {
2693
- case "Under": {
2694
- const strings = Arr.map(node.prefix, (operand) =>
2695
- operand._tag === "Literal" && typeof operand.value === "string"
2696
- ? operand.value
2697
- : null,
2698
- )
2699
- return HashSet.fromIterable(
2700
- Arr.flatMap(node.prefix, (operand, index) =>
2701
- operand._tag === "Literal" &&
2702
- (operand.value === null ||
2703
- typeof operand.value === "string" ||
2704
- typeof operand.value === "boolean" ||
2705
- Number.isFinite(operand.value))
2706
- ? [JSON.stringify([index, operand.value, strings])]
2707
- : [],
2708
- ),
2709
- )
2710
- }
2711
- case "And":
2712
- case "Or":
2713
- return Arr.reduce(
2714
- node.conditions as ReadonlyArray<ConditionNode>,
2715
- HashSet.empty<string>(),
2716
- (all, child) => HashSet.union(all, underLiteralsIn(child, memo)),
2717
- )
2718
- case "Not":
2719
- return underLiteralsIn(node.condition, memo)
2720
- default:
2721
- return HashSet.empty()
2722
- }
2723
- })
2724
-
2725
- /** A fixed value in a path test. See {@link underLiteralsIn}. */
2726
- type UnderLiteral = readonly [
2727
- index: number,
2728
- value: Condition.Scalar,
2729
- strings: ReadonlyArray<string | null>,
2730
- ]
2731
-
2732
- const underLiterals = (node: ConditionNode): ReadonlyArray<UnderLiteral> =>
2733
- Arr.map(
2734
- Array.from(underLiteralsIn(node, new Map())),
2735
- (text) => JSON.parse(text) as UnderLiteral,
2736
- )
2737
-
2738
- const underRefsIn = (
2739
- node: ConditionNode,
2740
- memo: Map<ConditionNode, HashSet.HashSet<string>>,
2741
- ): HashSet.HashSet<string> =>
2742
- remember(memo, node, () => {
2743
- switch (node._tag) {
2744
- case "Under": {
2745
- // The fixed strings of the prefix, so the check can leave out a
2746
- // resource whose path can not match them.
2747
- const strings = Arr.map(node.prefix, (operand) =>
2748
- operand._tag === "Literal" && typeof operand.value === "string"
2749
- ? operand.value
2750
- : null,
2751
- )
2752
- return HashSet.fromIterable(
2753
- Arr.flatMap(node.prefix, (operand, index) =>
2754
- operand._tag === "Principal"
2755
- ? [JSON.stringify([index, operand.path, strings])]
2756
- : [],
2757
- ),
2758
- )
2759
- }
2760
- case "And":
2761
- case "Or":
2762
- return Arr.reduce(
2763
- node.conditions as ReadonlyArray<ConditionNode>,
2764
- HashSet.empty<string>(),
2765
- (all, child) => HashSet.union(all, underRefsIn(child, memo)),
2766
- )
2767
- case "Not":
2768
- return underRefsIn(node.condition, memo)
2769
- default:
2770
- return HashSet.empty()
2771
- }
2772
- })
2773
-
2774
- /**
2775
- * A principal field in a path test: its place in the prefix, its path, and
2776
- * the fixed strings of the whole prefix, or `null` for another part.
2777
- */
2778
- type UnderRef = readonly [
2779
- index: number,
2780
- ref: string,
2781
- strings: ReadonlyArray<string | null>,
2782
- ]
2783
-
2784
- const underRefs = (node: ConditionNode): ReadonlyArray<UnderRef> =>
2785
- Arr.map(
2786
- Array.from(underRefsIn(node, new Map())),
2787
- (text) => JSON.parse(text) as UnderRef,
2788
- )
2789
-
2790
- /**
2791
- * Tests if the path of a resource may start with a prefix: it is as long as
2792
- * the prefix, and each fixed string of the prefix is the string of the path
2793
- * at its place, when the path has a string there.
2794
- */
2795
- const mayStartWith = (
2796
- descriptor: Resource.Descriptor,
2797
- strings: ReadonlyArray<string | null>,
2798
- ): boolean =>
2799
- // A path shorter than the prefix never matches.
2800
- (descriptor.path?.length ?? 0) >= strings.length &&
2801
- Arr.every(strings, (string, index) => {
2802
- const segment = descriptor.path?.[index]
2803
- return string === null || typeof segment !== "string" || segment === string
2804
- })
2805
-
2806
- /**
2807
- * Tests if a principal field may hold the part of a path at `index`: a string
2808
- * of the path, or a value of the field that the path names there.
2809
- */
2810
- const fitsPathPart = (
2811
- descriptor: Resource.Descriptor,
2812
- index: number,
2813
- principal: Resource.FieldDomain,
2814
- ): boolean => {
2815
- const segment = descriptor.path?.[index]
2816
- return segment === undefined
2817
- ? false
2818
- : typeof segment === "string"
2819
- ? fitsDomain(principal, segment)
2820
- : domainsMeet(
2821
- Resource.domainOf(descriptor.schema, segment.field),
2822
- principal,
2823
- )
2824
- }
2825
-
2826
- /**
2827
- * The fields of ordered tests (`lt`, `lte`, `gt`, `gte`) in a node, each as
2828
- * the JSON text of the field and the operand. See {@link literalTestsIn}.
2829
- */
2830
- const orderedTestsIn = (
2831
- node: ConditionNode,
2832
- memo: Map<ConditionNode, HashSet.HashSet<string>>,
2833
- ): HashSet.HashSet<string> =>
2834
- remember(memo, node, () => {
2835
- switch (node._tag) {
2836
- case "Compare":
2837
- return node.op === "eq" || node.op === "ne"
2838
- ? HashSet.empty()
2839
- : HashSet.make(
2840
- JSON.stringify([
2841
- node.field,
2842
- // The type of a fixed value, not the value: JSON has no
2843
- // `Infinity`.
2844
- node.value._tag === "Literal"
2845
- ? { _tag: "Literal", type: typeof node.value.value }
2846
- : node.value,
2847
- ]),
2848
- )
2849
- case "And":
2850
- case "Or":
2851
- return Arr.reduce(
2852
- node.conditions as ReadonlyArray<ConditionNode>,
2853
- HashSet.empty<string>(),
2854
- (all, child) => HashSet.union(all, orderedTestsIn(child, memo)),
2855
- )
2856
- case "Not":
2857
- return orderedTestsIn(node.condition, memo)
2858
- default:
2859
- return HashSet.empty()
2860
- }
2861
- })
2862
-
2863
- /** The operand of an ordered test: the type of a fixed value, or a principal field. */
2864
- type OrderedOperand =
2865
- | { readonly _tag: "Literal"; readonly type: string }
2866
- | { readonly _tag: "Principal"; readonly path: string }
2867
-
2868
- const orderedTests = (
2869
- node: ConditionNode,
2870
- ): ReadonlyArray<readonly [field: string, operand: OrderedOperand]> =>
2871
- Arr.map(
2872
- Array.from(orderedTestsIn(node, new Map())),
2873
- (text) =>
2874
- JSON.parse(text) as readonly [field: string, operand: OrderedOperand],
2875
- )
2876
-
2877
- /**
2878
- * Tests if a field may hold a value with an order: a number or a string. An
2879
- * ordered test is false for any other value.
2880
- */
2881
- const isOrdered = (domain: Resource.FieldDomain): boolean =>
2882
- domain.string ||
2883
- domain.number !== "none" ||
2884
- Arr.some(
2885
- domain.literals,
2886
- (value) => typeof value === "string" || typeof value === "number",
2887
- )
2888
-
2889
- /**
2890
- * Tests if two fields may hold values of one ordered type: two strings or two
2891
- * numbers. An ordered test of two values of other types is false.
2892
- */
2893
- const orderedMeet = (
2894
- left: Resource.FieldDomain,
2895
- right: Resource.FieldDomain,
2896
- ): boolean => {
2897
- const strings = (domain: Resource.FieldDomain) =>
2898
- domain.string ||
2899
- Arr.some(domain.literals, (value) => typeof value === "string")
2900
- const numbers = (domain: Resource.FieldDomain) =>
2901
- domain.number !== "none" ||
2902
- Arr.some(domain.literals, (value) => typeof value === "number")
2903
- return (strings(left) && strings(right)) || (numbers(left) && numbers(right))
2904
- }
2905
-
2906
- /**
2907
- * Tests if two fields may hold one value: a value of one type, or a single
2908
- * value that the other field may hold.
2909
- */
2910
- const domainsMeet = (
2911
- left: Resource.FieldDomain,
2912
- right: Resource.FieldDomain,
2913
- ): boolean =>
2914
- // A principal field that is `null` is a missing value, and a test with a
2915
- // missing value is false. A principal field that holds an object is not a
2916
- // scalar, and it is a missing value too. So only scalars can be shared.
2917
- (left.string && right.string) ||
2918
- (left.boolean && right.boolean) ||
2919
- (left.number !== "none" && right.number !== "none") ||
2920
- Arr.some(left.literals, (value) => fitsDomain(right, value)) ||
2921
- Arr.some(right.literals, (value) => fitsDomain(left, value))
2922
-
2923
- /**
2924
- * Tests if a field with this domain may hold a value of the type of `value`.
2925
- * `null` always fits: a test with `null` is a test that the field is missing.
2926
- */
2927
- const fitsDomain = (
2928
- domain: Resource.FieldDomain,
2929
- value: Condition.Scalar,
2930
- ): boolean =>
2931
- value === null ||
2932
- domain.literals.includes(value) ||
2933
- (typeof value === "string" && domain.string) ||
2934
- (typeof value === "boolean" && domain.boolean) ||
2935
- (typeof value === "number" && domain.number !== "none")
2936
-
2937
- /**
2938
- * Checks the names in an encoded policy against the domain. It gives one
2939
- * message for each problem.
2940
- */
2941
- const validate = (
2942
- domain: Domain.Any,
2943
- root: EncodedNode,
2944
- ): ReadonlyArray<Issue> => {
2945
- const principalPaths = domain.principal.pipe(
2946
- Resource.fieldPaths,
2947
- HashSet.fromIterable,
2948
- )
2949
- const ruleIssues = (
2950
- value: EncodedRule,
2951
- at: ReadonlyArray<PropertyKey>,
2952
- ): ReadonlyArray<Issue> => {
2953
- const all = value.resources.includes("*")
2954
- // Each kind is checked one time, also when the list names it again. The
2955
- // order of the kinds does not change the issues, so a set is enough.
2956
- const named = Array.from(
2957
- HashSet.fromIterable(Arr.filter(value.resources, (kind) => kind !== "*")),
2958
- )
2959
- const descriptors = Arr.flatMap(named, (kind) => {
2960
- const descriptor = Domain.descriptorOf(domain, kind)
2961
- return descriptor === undefined ? [] : [descriptor]
2962
- })
2963
- const known = all
2964
- ? Array.from(HashMap.values(domain.resources))
2965
- : descriptors
2966
- // The kinds of the rule with one of its actions. The rule never runs on
2967
- // another kind, so the types of its tests are checked only on these.
2968
- const applicable = Arr.filter(
2969
- descriptors,
2970
- (descriptor) =>
2971
- value.actions.includes("*") ||
2972
- Arr.some(value.actions, (action) =>
2973
- descriptor.actions.includes(action),
2974
- ),
2975
- )
2976
- const usedFields = [
2977
- ...Arr.filter(value.fields ?? [], (field) => !field.includes("*")),
2978
- ...(value.when === undefined ? [] : Condition.fields(value.when as Ast)),
2979
- ]
2980
- return [
2981
- ...Arr.map(
2982
- Arr.filter(named, (kind) => !HashMap.has(domain.resources, kind)),
2983
- (kind): Issue => ({
2984
- path: [...at, "resources"],
2985
- issue: `There is no resource with the kind "${kind}"`,
2986
- }),
2987
- ),
2988
- ...Arr.map(
2989
- Arr.filter(
2990
- value.actions,
2991
- (action) =>
2992
- action !== "*" &&
2993
- !Arr.some(known, (descriptor) =>
2994
- descriptor.actions.includes(action),
2995
- ),
2996
- ),
2997
- (action): Issue => ({
2998
- path: [...at, "actions"],
2999
- issue: `The action "${action}" is not an action of these resources`,
3000
- }),
3001
- ),
3002
- ...Arr.map(
3003
- Arr.filter(
3004
- usedFields,
3005
- (field) =>
3006
- all ||
3007
- descriptors.length === 0 ||
3008
- !Arr.every(
3009
- descriptors,
3010
- (descriptor) =>
3011
- descriptor.fieldPaths.includes(field) ||
3012
- // A key of a record, or a field deep in a recursive
3013
- // Schema, or a getter of a nested class.
3014
- Resource.hasPath(descriptor.schema, field) ||
3015
- Resource.hasGetter(descriptor.schema, field),
3016
- ),
3017
- ),
3018
- (field): Issue => ({
3019
- path: at,
3020
- issue: `The field "${field}" is not a field of every resource of the rule`,
3021
- }),
3022
- ),
3023
- ...Arr.map(
3024
- Arr.filter(
3025
- value.when === undefined ? [] : principalRefs(value.when),
3026
- (ref) =>
3027
- !HashSet.has(principalPaths, ref) &&
3028
- !Resource.hasPath(domain.principal, ref) &&
3029
- !Resource.hasGetter(domain.principal, ref),
3030
- ),
3031
- (ref): Issue => ({
3032
- path: [...at, "when"],
3033
- issue: `The principal has no field "${ref}"`,
3034
- }),
3035
- ),
3036
- // A test with a value of another type than the field never matches.
3037
- // The types stop it in a typed rule, so here it is a mistake, and in
3038
- // a deny rule it would let values through.
3039
- ...Arr.map(
3040
- Arr.filter(
3041
- value.when === undefined ? [] : literalTests(value.when),
3042
- ([field, literal]) =>
3043
- Arr.some(
3044
- applicable,
3045
- (descriptor) =>
3046
- !fitsDomain(
3047
- Resource.domainOf(descriptor.schema, field),
3048
- literal,
3049
- ),
3050
- ),
3051
- ),
3052
- ([field, literal]): Issue => ({
3053
- path: [...at, "when"],
3054
- issue: `The field "${field}" can not hold the value ${JSON.stringify(literal)}`,
3055
- }),
3056
- ),
3057
- ...Arr.map(
3058
- Arr.filter(
3059
- value.when === undefined ? [] : principalTests(value.when),
3060
- ([field, ref]) =>
3061
- Arr.some(
3062
- applicable,
3063
- (descriptor) =>
3064
- !domainsMeet(
3065
- Resource.domainOf(descriptor.schema, field),
3066
- Resource.domainOf(domain.principal, ref),
3067
- ),
3068
- ),
3069
- ),
3070
- ([field, ref]): Issue => ({
3071
- path: [...at, "when"],
3072
- issue: `The field "${field}" can not hold a value of the principal field "${ref}"`,
3073
- }),
3074
- ),
3075
- // A condition with a node that is not known would be skipped when it
3076
- // runs, so it is not taken.
3077
- ...(value.when !== undefined && !Condition.isAst(value.when)
3078
- ? [
3079
- {
3080
- path: [...at, "when"],
3081
- issue: "The condition is not a whole condition tree",
3082
- } satisfies Issue,
3083
- ]
3084
- : []),
3085
- // A fixed value such as `undefined` is not a value of a test. Its
3086
- // JSON text would be `null`, and the test would mean another thing.
3087
- ...(value.when !== undefined && hasBadLiteralIn(value.when, new Map())
3088
- ? [
3089
- {
3090
- path: [...at, "when"],
3091
- issue:
3092
- "A test has a fixed value that is not a string, a number, a boolean or null",
3093
- } satisfies Issue,
3094
- ]
3095
- : []),
3096
- // A principal field in a path test must hold a value that the path
3097
- // may have at that place. Else the test is always false.
3098
- ...Arr.map(
3099
- Arr.filter(
3100
- value.when === undefined ? [] : underRefs(value.when),
3101
- ([index, ref, strings]) =>
3102
- // A rule for every resource may run on each of them that has a
3103
- // part of its path at this place.
3104
- Arr.some(
3105
- // The rule runs only on a kind with one of its actions, and
3106
- // the test can match only a path that starts with the fixed
3107
- // strings of the prefix.
3108
- Arr.filter(
3109
- all
3110
- ? Arr.filter(
3111
- known,
3112
- (descriptor) =>
3113
- descriptor.path?.[index] !== undefined &&
3114
- (value.actions.includes("*") ||
3115
- Arr.some(value.actions, (action) =>
3116
- descriptor.actions.includes(action),
3117
- )),
3118
- )
3119
- : applicable,
3120
- (descriptor) => mayStartWith(descriptor, strings),
3121
- ),
3122
- (descriptor) =>
3123
- !fitsPathPart(
3124
- descriptor,
3125
- index,
3126
- Resource.domainOf(domain.principal, ref),
3127
- ),
3128
- ),
3129
- ),
3130
- ([, ref]): Issue => ({
3131
- path: [...at, "when"],
3132
- issue: `The principal field "${ref}" can not hold a part of the path of the resource`,
3133
- }),
3134
- ),
3135
- // A fixed value in a path test at the place of a field of the path
3136
- // must be a value of that field. Else the test is always false.
3137
- ...Arr.map(
3138
- Arr.filter(
3139
- value.when === undefined ? [] : underLiterals(value.when),
3140
- ([index, literal, strings]) =>
3141
- Arr.some(
3142
- Arr.filter(
3143
- all
3144
- ? Arr.filter(
3145
- known,
3146
- (descriptor) =>
3147
- value.actions.includes("*") ||
3148
- Arr.some(value.actions, (action) =>
3149
- descriptor.actions.includes(action),
3150
- ),
3151
- )
3152
- : applicable,
3153
- (descriptor) => mayStartWith(descriptor, strings),
3154
- ),
3155
- (descriptor) => {
3156
- const segment = descriptor.path?.[index]
3157
- return (
3158
- segment !== undefined &&
3159
- typeof segment !== "string" &&
3160
- !fitsDomain(
3161
- Resource.domainOf(descriptor.schema, segment.field),
3162
- literal,
3163
- )
3164
- )
3165
- },
3166
- ),
3167
- ),
3168
- ([index, literal]): Issue => ({
3169
- path: [...at, "when"],
3170
- issue: `The path part ${String(index)} can not hold the value ${JSON.stringify(literal)}`,
3171
- }),
3172
- ),
3173
- // An ordered test compares numbers with numbers and strings with
3174
- // strings. With another value it is always false.
3175
- ...Arr.map(
3176
- Arr.filter(
3177
- value.when === undefined ? [] : orderedTests(value.when),
3178
- ([field, operand]) =>
3179
- (operand._tag === "Literal" &&
3180
- operand.type !== "string" &&
3181
- operand.type !== "number") ||
3182
- Arr.some(applicable, (descriptor) => {
3183
- const fieldDomain = Resource.domainOf(descriptor.schema, field)
3184
- // A principal field must share an ordered type with the
3185
- // field: a string and a number never compare.
3186
- return operand._tag === "Literal"
3187
- ? !isOrdered(fieldDomain)
3188
- : !orderedMeet(
3189
- fieldDomain,
3190
- Resource.domainOf(domain.principal, operand.path),
3191
- )
3192
- }),
3193
- ),
3194
- ([field]): Issue => ({
3195
- path: [...at, "when"],
3196
- issue: `The field "${field}" has an ordered test with a value that has no order`,
3197
- }),
3198
- ),
3199
- ]
3200
- }
3201
- const issuesOf = (
3202
- node: EncodedNode,
3203
- path: ReadonlyArray<PropertyKey>,
3204
- level: number,
3205
- ): ReadonlyArray<Issue> => {
3206
- if (level > maxDepth) {
3207
- return [
3208
- { path, issue: `A policy tree may have at most ${maxDepth} levels` },
3209
- ]
3210
- }
3211
- switch (node._tag) {
3212
- case "Rules":
3213
- return Arr.flatMap(node.rules, (value, index) =>
3214
- ruleIssues(value, [...path, "rules", index]),
3215
- )
3216
- case "All":
3217
- case "Any":
3218
- case "FirstOf":
3219
- return Arr.flatMap(node.policies, (child, index) =>
3220
- issuesOf(child, [...path, "policies", index], level + 1),
3221
- )
3222
- case "Ceiling":
3223
- return [
3224
- ...issuesOf(node.cap, [...path, "cap"], level + 1),
3225
- ...issuesOf(node.policy, [...path, "policy"], level + 1),
3226
- ]
3227
- }
3228
- }
3229
- return issuesOf(root, [], 1)
3230
- }
3231
-
3232
- /**
3233
- * Makes a policy tree from its JSON form. Each rule gets the domain of the
3234
- * Schema, as a rule from the builder of the domain has it.
3235
- */
3236
- const decodeNode = (node: EncodedNode, domain: Domain.Any): Node => {
3237
- switch (node._tag) {
3238
- case "Rules":
3239
- return nodes.Rules({
3240
- algorithm: node.algorithm,
3241
- rules: node.rules.map((value) =>
3242
- Rule.make({ ...(value as Rule.Shape), domain }),
3243
- ),
3244
- })
3245
- case "All":
3246
- case "Any":
3247
- case "FirstOf":
3248
- return nodes[node._tag]({
3249
- policies: node.policies.map((child) => decodeNode(child, domain)),
3250
- })
3251
- case "Ceiling":
3252
- return nodes.Ceiling({
3253
- cap: decodeNode(node.cap, domain),
3254
- policy: decodeNode(node.policy, domain),
3255
- })
3256
- }
3257
- }
3258
-
3259
- /**
3260
- * Tests if a value is a policy.
3261
- *
3262
- * @stability stable
3263
- * @category guards
3264
- * @since 0.3.0
3265
- */
3266
- export const isPolicy = (u: unknown): u is Any => u instanceof PolicyData
3267
-
3268
918
  /**
3269
919
  * The Schema of the policies of a domain with no predicates. Decoding checks
3270
920
  * every kind, action, field and principal field against the domain, and the
3271
921
  * `meta` of each rule against the `meta` Schema of the domain.
3272
922
  *
923
+ * The decoder takes at most 256 levels of policies and 500 levels of
924
+ * conditions, and at most 1000000 objects and array items, with a shared
925
+ * object counted for each place. It reads only the own data fields of the
926
+ * input.
927
+ *
3273
928
  * @stability stable
3274
929
  * @category schemas
3275
930
  * @since 0.3.0
3276
931
  */
3277
- export const schema = <D extends Domain.Any>(
932
+ export const schema: <D extends Domain.Any>(
3278
933
  domain: D,
3279
- ): Schema.Codec<Policy<D>, EncodedNode> =>
3280
- // The input is any value until the checks of `encodedNodeSchema` run, so
3281
- // this guard says yes to every object.
3282
- Schema.declare(
3283
- (input): input is EncodedNode =>
3284
- typeof input === "object" && input !== null,
3285
- ).pipe(
3286
- Schema.check(Schema.makeFilter(fitsLimits)),
3287
- Schema.decodeTo(
3288
- encodedNodeSchema(domain.meta),
3289
- SchemaTransformation.passthrough(),
3290
- ),
3291
- Schema.check(Schema.makeFilter((node) => validate(domain, node))),
3292
- Schema.decodeTo(
3293
- // A policy with a predicate is not a plain policy, and it can not
3294
- // become JSON.
3295
- Schema.declare(
3296
- (u): u is Policy<D> =>
3297
- isPolicy(u) && u.domain === domain && !nodeHasPredicate(u.node),
3298
- ),
3299
- SchemaTransformation.transformEffect({
3300
- decode: (node: EncodedNode) =>
3301
- Effect.succeed(fromNode(domain, decodeNode(node, domain))),
3302
- // A tree whose JSON form is too large fails as a Schema issue.
3303
- encode: (policy: Policy<D>, options) =>
3304
- encodedSize(policy.node) > maxEncodedNodes
3305
- ? Effect.fail(
3306
- new SchemaIssue.InvalidValue(
3307
- { message: tooLargeMessage },
3308
- policy,
3309
- options,
3310
- ),
3311
- )
3312
- : Effect.succeed(encodeNode(policy.node)),
3313
- }),
3314
- ),
3315
- )
934
+ ) => Schema.Codec<Policy<D>, EncodedNode> = Internal.schema as never