@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.
- package/README.md +112 -69
- package/dist/Authorizer.d.ts +43 -47
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js +5 -290
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +164 -332
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +75 -890
- package/dist/Condition.js.map +1 -1
- package/dist/Decision.d.ts +13 -13
- package/dist/Decision.d.ts.map +1 -1
- package/dist/Decision.js.map +1 -1
- package/dist/Domain.d.ts +10 -21
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +4 -62
- package/dist/Domain.js.map +1 -1
- package/dist/HttpApi.d.ts +31 -7
- package/dist/HttpApi.d.ts.map +1 -1
- package/dist/HttpApi.js +25 -8
- package/dist/HttpApi.js.map +1 -1
- package/dist/Limits.d.ts +114 -0
- package/dist/Limits.d.ts.map +1 -0
- package/dist/Limits.js +132 -0
- package/dist/Limits.js.map +1 -0
- package/dist/Policy.d.ts +276 -269
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +73 -1177
- package/dist/Policy.js.map +1 -1
- package/dist/Resource.d.ts +4 -177
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +4 -803
- package/dist/Resource.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +4 -5
- package/dist/internal/algebra.js +267 -0
- package/dist/internal/algebra.js.map +1 -0
- package/dist/internal/analysis.js +245 -0
- package/dist/internal/analysis.js.map +1 -0
- package/dist/internal/authorizer.d.ts +30 -0
- package/dist/internal/authorizer.d.ts.map +1 -0
- package/dist/internal/authorizer.js +221 -0
- package/dist/internal/authorizer.js.map +1 -0
- package/dist/internal/bind.js +311 -0
- package/dist/internal/bind.js.map +1 -0
- package/dist/internal/catalog.d.ts +56 -0
- package/dist/internal/catalog.d.ts.map +1 -0
- package/dist/internal/catalog.js +292 -0
- package/dist/internal/catalog.js.map +1 -0
- package/dist/internal/decide.js +166 -0
- package/dist/internal/decide.js.map +1 -0
- package/dist/internal/domain.js +32 -0
- package/dist/internal/domain.js.map +1 -0
- package/dist/internal/json.js +134 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/internal/limits.d.ts +14 -0
- package/dist/internal/limits.d.ts.map +1 -0
- package/dist/internal/limits.js +43 -0
- package/dist/internal/limits.js.map +1 -0
- package/dist/internal/nodes.d.ts +121 -0
- package/dist/internal/nodes.d.ts.map +1 -0
- package/dist/internal/nodes.js +277 -0
- package/dist/internal/nodes.js.map +1 -0
- package/dist/internal/patterns.js +110 -0
- package/dist/internal/patterns.js.map +1 -0
- package/dist/internal/policy-json.js +265 -0
- package/dist/internal/policy-json.js.map +1 -0
- package/dist/internal/policy-nodes.d.ts +61 -0
- package/dist/internal/policy-nodes.d.ts.map +1 -0
- package/dist/internal/policy-nodes.js +146 -0
- package/dist/internal/policy-nodes.js.map +1 -0
- package/dist/internal/policy.d.ts +42 -0
- package/dist/internal/policy.d.ts.map +1 -0
- package/dist/internal/policy.js +124 -0
- package/dist/internal/policy.js.map +1 -0
- package/dist/internal/read.js +39 -0
- package/dist/internal/read.js.map +1 -0
- package/dist/internal/resource.js +115 -0
- package/dist/internal/resource.js.map +1 -0
- package/dist/internal/rule.d.ts +26 -0
- package/dist/internal/rule.d.ts.map +1 -0
- package/dist/internal/rule.js +103 -0
- package/dist/internal/rule.js.map +1 -0
- package/dist/internal/search.d.ts +22 -0
- package/dist/internal/search.d.ts.map +1 -0
- package/dist/internal/search.js +274 -0
- package/dist/internal/search.js.map +1 -0
- package/dist/internal/suggest.js +46 -0
- package/dist/internal/suggest.js.map +1 -0
- package/package.json +3 -4
- package/src/Authorizer.ts +67 -583
- package/src/Condition.ts +339 -2095
- package/src/Decision.ts +6 -5
- package/src/Domain.ts +12 -69
- package/src/HttpApi.ts +72 -10
- package/src/Limits.ts +132 -0
- package/src/Policy.ts +391 -2772
- package/src/Resource.ts +7 -1548
- package/src/index.ts +3 -9
- package/src/internal/algebra.ts +482 -0
- package/src/internal/analysis.ts +478 -0
- package/src/internal/authorizer.ts +418 -0
- package/src/internal/bind.ts +648 -0
- package/src/internal/catalog.ts +534 -0
- package/src/internal/decide.ts +402 -0
- package/src/internal/domain.ts +66 -0
- package/src/internal/json.ts +260 -0
- package/src/internal/limits.ts +78 -0
- package/src/internal/nodes.ts +404 -0
- package/src/internal/patterns.ts +211 -0
- package/src/internal/policy-json.ts +487 -0
- package/src/internal/policy-nodes.ts +207 -0
- package/src/internal/policy.ts +264 -0
- package/src/internal/read.ts +55 -0
- package/src/internal/resource.ts +180 -0
- package/src/internal/rule.ts +196 -0
- package/src/internal/search.ts +541 -0
- package/src/internal/suggest.ts +104 -0
- package/dist/Principal.d.ts +0 -35
- package/dist/Principal.d.ts.map +0 -1
- package/dist/Principal.js +0 -37
- package/dist/Principal.js.map +0 -1
- package/dist/Rule.d.ts +0 -275
- package/dist/Rule.d.ts.map +0 -1
- package/dist/Rule.js +0 -257
- package/dist/Rule.js.map +0 -1
- package/src/Principal.ts +0 -41
- 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
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
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
|
|
55
|
-
import * as
|
|
56
|
-
import * as
|
|
57
|
-
import * as
|
|
58
|
-
import * as
|
|
59
|
-
import * as
|
|
60
|
-
import * as
|
|
61
|
-
import * as
|
|
62
|
-
import * as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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`. `
|
|
167
|
-
*
|
|
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
|
|
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
|
|
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
|
|
134
|
+
export type DomainOf<P> = P extends Policy<infer D, any> ? D : never
|
|
202
135
|
|
|
203
136
|
/**
|
|
204
|
-
* The
|
|
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
|
|
211
|
-
|
|
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:
|
|
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
|
-
*
|
|
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
|
|
398
|
-
|
|
399
|
-
readonly
|
|
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
|
-
|
|
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
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
336
|
+
* Any rule.
|
|
337
|
+
*
|
|
338
|
+
* @stability stable
|
|
339
|
+
* @category models
|
|
340
|
+
* @since 0.3.0
|
|
408
341
|
*/
|
|
409
|
-
|
|
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
|
-
*
|
|
413
|
-
*
|
|
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
|
-
|
|
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
|
|
434
|
-
*
|
|
356
|
+
* The kind name of a target.
|
|
357
|
+
*
|
|
358
|
+
* @stability stable
|
|
359
|
+
* @category utility types
|
|
360
|
+
* @since 0.3.0
|
|
435
361
|
*/
|
|
436
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
|
507
|
-
*
|
|
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
|
-
|
|
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
|
|
542
|
-
*
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
|
554
|
-
|
|
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
|
-
|
|
557
|
-
|
|
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
|
-
*
|
|
425
|
+
* The value type of the kinds `K`.
|
|
561
426
|
*
|
|
562
427
|
* @stability stable
|
|
563
|
-
* @category
|
|
428
|
+
* @category utility types
|
|
564
429
|
* @since 0.3.0
|
|
565
430
|
*/
|
|
566
|
-
export
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
|
614
|
-
*
|
|
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
|
|
619
|
-
*
|
|
620
|
-
*
|
|
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.
|
|
526
|
+
* @since 2.0.0
|
|
629
527
|
*/
|
|
630
|
-
export const
|
|
631
|
-
|
|
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
|
-
*
|
|
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.
|
|
543
|
+
* @since 2.0.0
|
|
656
544
|
*/
|
|
657
|
-
export const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
|
684
|
-
*
|
|
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
|
|
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
|
-
*
|
|
711
|
-
*
|
|
575
|
+
* @stability stable
|
|
576
|
+
* @category errors
|
|
577
|
+
* @since 2.0.0
|
|
712
578
|
*/
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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,
|
|
780
|
-
cap: Policy<D,
|
|
781
|
-
): <
|
|
782
|
-
<D extends Domain.Any,
|
|
783
|
-
policy: Policy<D,
|
|
784
|
-
cap: Policy<D,
|
|
785
|
-
): Policy<D,
|
|
786
|
-
} =
|
|
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,
|
|
878
|
-
policy: Policy<D,
|
|
879
|
-
|
|
880
|
-
<D
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
<
|
|
1203
|
-
|
|
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
|
|
1217
|
-
|
|
1218
|
-
|
|
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.
|
|
1236
|
-
*
|
|
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
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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
|
|
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
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
<
|
|
1331
|
-
|
|
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
|
|
1946
|
-
*
|
|
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
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
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
|
-
|
|
2011
|
-
|
|
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<
|
|
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<
|
|
2024
|
-
readonly reason: "predicate" | "budget"
|
|
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
|
|
2255
|
-
* resource
|
|
2256
|
-
*
|
|
2257
|
-
*
|
|
2258
|
-
*
|
|
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`
|
|
2261
|
-
*
|
|
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
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
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
|
|
932
|
+
export const schema: <D extends Domain.Any>(
|
|
3278
933
|
domain: D,
|
|
3279
|
-
)
|
|
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
|