@nmnmcc/ability 2.0.6 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -32
- package/dist/Authorizer.d.ts +21 -12
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +118 -18
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +27 -18
- package/dist/Condition.js.map +1 -1
- package/dist/Domain.d.ts +9 -0
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +5 -0
- package/dist/Domain.js.map +1 -1
- package/dist/Limits.d.ts +44 -13
- package/dist/Limits.d.ts.map +1 -1
- package/dist/Limits.js +52 -22
- package/dist/Limits.js.map +1 -1
- package/dist/Policy.d.ts +95 -12
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/internal/algebra.js +101 -74
- package/dist/internal/algebra.js.map +1 -1
- package/dist/internal/analysis.js +81 -75
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/authorizer.d.ts.map +1 -1
- package/dist/internal/authorizer.js +16 -11
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +119 -80
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts +33 -10
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +153 -78
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +25 -29
- package/dist/internal/decide.js.map +1 -1
- package/dist/internal/domain.js +19 -5
- package/dist/internal/domain.js.map +1 -1
- package/dist/internal/json.js +120 -76
- package/dist/internal/json.js.map +1 -1
- package/dist/internal/limits.d.ts +28 -0
- package/dist/internal/limits.d.ts.map +1 -1
- package/dist/internal/limits.js +75 -16
- package/dist/internal/limits.js.map +1 -1
- package/dist/internal/nodes.d.ts +7 -0
- package/dist/internal/nodes.d.ts.map +1 -1
- package/dist/internal/nodes.js +86 -47
- package/dist/internal/nodes.js.map +1 -1
- package/dist/internal/patterns.js +50 -15
- package/dist/internal/patterns.js.map +1 -1
- package/dist/internal/policy-json.js +169 -127
- package/dist/internal/policy-json.js.map +1 -1
- package/dist/internal/policy-nodes.d.ts.map +1 -1
- package/dist/internal/policy-nodes.js +17 -14
- package/dist/internal/policy-nodes.js.map +1 -1
- package/dist/internal/policy.d.ts.map +1 -1
- package/dist/internal/policy.js +38 -22
- package/dist/internal/policy.js.map +1 -1
- package/dist/internal/resource.js +1 -1
- package/dist/internal/rule.d.ts.map +1 -1
- package/dist/internal/rule.js +6 -5
- package/dist/internal/rule.js.map +1 -1
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +106 -92
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/trampoline.js +89 -0
- package/dist/internal/trampoline.js.map +1 -0
- package/package.json +1 -1
- package/src/Authorizer.ts +30 -20
- package/src/Condition.ts +308 -83
- package/src/Domain.ts +9 -0
- package/src/Limits.ts +69 -29
- package/src/Policy.ts +131 -15
- package/src/internal/algebra.ts +218 -135
- package/src/internal/analysis.ts +154 -122
- package/src/internal/authorizer.ts +34 -16
- package/src/internal/bind.ts +279 -148
- package/src/internal/catalog.ts +307 -133
- package/src/internal/decide.ts +58 -49
- package/src/internal/domain.ts +39 -3
- package/src/internal/json.ts +212 -117
- package/src/internal/limits.ts +101 -31
- package/src/internal/nodes.ts +101 -59
- package/src/internal/patterns.ts +72 -26
- package/src/internal/policy-json.ts +266 -197
- package/src/internal/policy-nodes.ts +33 -24
- package/src/internal/policy.ts +63 -23
- package/src/internal/rule.ts +16 -3
- package/src/internal/search.ts +186 -136
- package/src/internal/trampoline.ts +194 -0
package/src/internal/analysis.ts
CHANGED
|
@@ -22,11 +22,13 @@ import { decideNode, fitsKind } from "./decide.ts"
|
|
|
22
22
|
import * as DomainData from "./domain.ts"
|
|
23
23
|
import * as Limits from "./limits.ts"
|
|
24
24
|
import * as Nodes from "./nodes.ts"
|
|
25
|
+
import * as Place from "./place.ts"
|
|
25
26
|
import * as PolicyNodes from "./policy-nodes.ts"
|
|
26
27
|
import { nodeOf } from "./policy.ts"
|
|
27
28
|
import * as ResourceData from "./resource.ts"
|
|
28
29
|
import * as RuleData from "./rule.ts"
|
|
29
30
|
import * as Search from "./search.ts"
|
|
31
|
+
import * as Trampoline from "./trampoline.ts"
|
|
30
32
|
|
|
31
33
|
type Any = Policy.Any
|
|
32
34
|
|
|
@@ -50,23 +52,28 @@ interface Build {
|
|
|
50
52
|
readonly not: (value: Ast) => Ast
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
Limits.
|
|
61
|
-
|
|
55
|
+
/** The steps of a residual for a domain with these limits. */
|
|
56
|
+
const buildFor = (limits: Limits.Limits): Build => {
|
|
57
|
+
const most = limits.residualNodes
|
|
58
|
+
const limit = (node: Nodes.Node): Ast => {
|
|
59
|
+
// A residual grows by its nodes, as the conditions of the rules come
|
|
60
|
+
// together. Its values come from the policy, which has a limit of them.
|
|
61
|
+
if (node.size > most) {
|
|
62
|
+
throw Limits.exceeded(
|
|
63
|
+
"A residual condition",
|
|
64
|
+
"residualNodes",
|
|
65
|
+
"nodes",
|
|
66
|
+
node.size,
|
|
67
|
+
most,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
return node
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
and: (values) => limit(Algebra.and(values as ReadonlyArray<Nodes.Node>)),
|
|
74
|
+
or: (values) => limit(Algebra.or(values as ReadonlyArray<Nodes.Node>)),
|
|
75
|
+
not: (value) => (value as Nodes.Node).pipe(Algebra.not, limit),
|
|
62
76
|
}
|
|
63
|
-
return node
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const build: Build = {
|
|
67
|
-
and: (values) => limit(Algebra.and(values as ReadonlyArray<Nodes.Node>)),
|
|
68
|
-
or: (values) => limit(Algebra.or(values as ReadonlyArray<Nodes.Node>)),
|
|
69
|
-
not: (value) => (value as Nodes.Node).pipe(Algebra.not, limit),
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
/** The two residual conditions of a node, with no types. */
|
|
@@ -87,6 +94,10 @@ interface ResidualContext {
|
|
|
87
94
|
readonly descriptor: Resource.Descriptor
|
|
88
95
|
/** The test of `RuleData.fitsOf` for the catalog, for this request. */
|
|
89
96
|
readonly fits: (pattern: string) => boolean
|
|
97
|
+
/** The most parts of a field path in the domain. */
|
|
98
|
+
readonly parts: number
|
|
99
|
+
/** The steps of a residual, with the limits of the domain. */
|
|
100
|
+
readonly build: Build
|
|
90
101
|
/**
|
|
91
102
|
* The condition of each rule, with the principal in it. They are made
|
|
92
103
|
* first, from a flat list of the rules, so the walk does not go down the
|
|
@@ -118,7 +129,7 @@ const firstApplicableResidual = (
|
|
|
118
129
|
values: ReadonlyArray<RuleData.Rule>,
|
|
119
130
|
context: ResidualContext,
|
|
120
131
|
): Pair => {
|
|
121
|
-
const { and, not, or } = build
|
|
132
|
+
const { and, not, or } = context.build
|
|
122
133
|
// The rules go in runs of one tag, from the last run to the first. The
|
|
123
134
|
// conditions of a run go into one node at once. With one node for each
|
|
124
135
|
// rule, each step would flatten the whole condition again.
|
|
@@ -147,90 +158,104 @@ const firstApplicableResidual = (
|
|
|
147
158
|
* Gives the residual pair of a node. A shared child is worked again for each
|
|
148
159
|
* place. The limits of the nodes and of a residual keep this work small.
|
|
149
160
|
*/
|
|
150
|
-
const residualNode = (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
or(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
161
|
+
const residualNode = (root: PolicyNodes.Node, context: ResidualContext): Pair =>
|
|
162
|
+
// The walk of the tree takes the steps of a trampoline, so a deep tree
|
|
163
|
+
// does not fill the stack.
|
|
164
|
+
Trampoline.run(root.depth, (R) => {
|
|
165
|
+
const { and, not, or } = context.build
|
|
166
|
+
const children = (nodes: ReadonlyArray<PolicyNodes.Node>) =>
|
|
167
|
+
R.forEach(nodes, (child) => R.suspend(() => go(child)))
|
|
168
|
+
const go = (node: PolicyNodes.Node): Trampoline.Step<Pair> => {
|
|
169
|
+
switch (node._tag) {
|
|
170
|
+
case "Rules":
|
|
171
|
+
return R.succeed(rulesResidual(node, context))
|
|
172
|
+
case "All":
|
|
173
|
+
if (node.policies.length === 0) {
|
|
174
|
+
return R.succeed({ allow: never, deny: never })
|
|
175
|
+
}
|
|
176
|
+
return R.map(children(node.policies), (pairs) => {
|
|
177
|
+
const deny = or(pairs.map((pair) => pair.deny))
|
|
178
|
+
const allow = and([...pairs.map((pair) => pair.allow), not(deny)])
|
|
179
|
+
return { allow, deny }
|
|
180
|
+
})
|
|
181
|
+
case "Any":
|
|
182
|
+
return R.map(children(node.policies), (pairs) => {
|
|
183
|
+
const allow = or(pairs.map((pair) => pair.allow))
|
|
184
|
+
const deny = and([not(allow), or(pairs.map((pair) => pair.deny))])
|
|
185
|
+
return { allow, deny }
|
|
186
|
+
})
|
|
187
|
+
case "FirstOf":
|
|
188
|
+
// The first child that gives an answer wins.
|
|
189
|
+
return R.map(children(node.policies), (pairs) =>
|
|
190
|
+
Arr.reduceRight(
|
|
191
|
+
pairs,
|
|
192
|
+
{ allow: never, deny: never },
|
|
193
|
+
(rest: Pair, pair): Pair => ({
|
|
194
|
+
allow: or([pair.allow, and([not(pair.deny), rest.allow])]),
|
|
195
|
+
deny: or([pair.deny, and([not(pair.allow), rest.deny])]),
|
|
196
|
+
}),
|
|
170
197
|
),
|
|
171
|
-
])
|
|
172
|
-
return { allow, deny }
|
|
173
|
-
}
|
|
174
|
-
case "permitOverrides": {
|
|
175
|
-
const allow = or(
|
|
176
|
-
node.rules
|
|
177
|
-
.filter((value) => value._tag === "Allow")
|
|
178
|
-
.map((value) => ruleCondition(value, context)),
|
|
179
198
|
)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
case "lastApplicable":
|
|
193
|
-
return firstApplicableResidual(node.rules.slice().reverse(), context)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// falls through: every algorithm returns above
|
|
197
|
-
case "All": {
|
|
198
|
-
if (node.policies.length === 0) {
|
|
199
|
-
return { allow: never, deny: never }
|
|
199
|
+
case "Ceiling":
|
|
200
|
+
return R.flatMap(
|
|
201
|
+
R.suspend(() => go(node.cap)),
|
|
202
|
+
(cap) =>
|
|
203
|
+
R.map(
|
|
204
|
+
R.suspend(() => go(node.policy)),
|
|
205
|
+
(inner) => ({
|
|
206
|
+
allow: and([not(cap.deny), inner.allow]),
|
|
207
|
+
deny: or([cap.deny, inner.deny]),
|
|
208
|
+
}),
|
|
209
|
+
),
|
|
210
|
+
)
|
|
200
211
|
}
|
|
201
|
-
const pairs = node.policies.map((child) => residualNode(child, context))
|
|
202
|
-
const deny = or(pairs.map((pair) => pair.deny))
|
|
203
|
-
const allow = and([...pairs.map((pair) => pair.allow), not(deny)])
|
|
204
|
-
return { allow, deny }
|
|
205
212
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
return go(root)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
/** The residual of a list of rules, by its combining algorithm. */
|
|
217
|
+
const rulesResidual = (
|
|
218
|
+
node: PolicyNodes.Rules,
|
|
219
|
+
context: ResidualContext,
|
|
220
|
+
): Pair => {
|
|
221
|
+
const { and, not, or } = context.build
|
|
222
|
+
switch (node.algorithm) {
|
|
223
|
+
case "denyOverrides": {
|
|
224
|
+
const deny = or(
|
|
225
|
+
node.rules
|
|
226
|
+
.filter((value) => value._tag === "Deny")
|
|
227
|
+
.map((value) => ruleCondition(value, context)),
|
|
228
|
+
)
|
|
229
|
+
const allow = and([
|
|
230
|
+
not(deny),
|
|
231
|
+
or(
|
|
232
|
+
node.rules
|
|
233
|
+
.filter((value) => value._tag === "Allow")
|
|
234
|
+
.map((value) => ruleCondition(value, context)),
|
|
235
|
+
),
|
|
236
|
+
])
|
|
210
237
|
return { allow, deny }
|
|
211
238
|
}
|
|
212
|
-
case "
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
(rest: Pair, child): Pair => {
|
|
218
|
-
const pair = residualNode(child, context)
|
|
219
|
-
return {
|
|
220
|
-
allow: or([pair.allow, and([not(pair.deny), rest.allow])]),
|
|
221
|
-
deny: or([pair.deny, and([not(pair.allow), rest.deny])]),
|
|
222
|
-
}
|
|
223
|
-
},
|
|
239
|
+
case "permitOverrides": {
|
|
240
|
+
const allow = or(
|
|
241
|
+
node.rules
|
|
242
|
+
.filter((value) => value._tag === "Allow")
|
|
243
|
+
.map((value) => ruleCondition(value, context)),
|
|
224
244
|
)
|
|
245
|
+
const deny = and([
|
|
246
|
+
not(allow),
|
|
247
|
+
or(
|
|
248
|
+
node.rules
|
|
249
|
+
.filter((value) => value._tag === "Deny")
|
|
250
|
+
.map((value) => ruleCondition(value, context)),
|
|
251
|
+
),
|
|
252
|
+
])
|
|
253
|
+
return { allow, deny }
|
|
225
254
|
}
|
|
226
|
-
case "
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return
|
|
230
|
-
allow: and([not(cap.deny), inner.allow]),
|
|
231
|
-
deny: or([cap.deny, inner.deny]),
|
|
232
|
-
}
|
|
233
|
-
}
|
|
255
|
+
case "firstApplicable":
|
|
256
|
+
return firstApplicableResidual(node.rules, context)
|
|
257
|
+
case "lastApplicable":
|
|
258
|
+
return firstApplicableResidual(node.rules.slice().reverse(), context)
|
|
234
259
|
}
|
|
235
260
|
}
|
|
236
261
|
|
|
@@ -250,28 +275,15 @@ interface PredicateContext {
|
|
|
250
275
|
const usesPredicate = (
|
|
251
276
|
node: PolicyNodes.Node,
|
|
252
277
|
context: PredicateContext,
|
|
253
|
-
): boolean =>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
Nodes.toNode(value.when).opaque &&
|
|
263
|
-
RuleData.matchesTarget(value, context),
|
|
264
|
-
)
|
|
265
|
-
case "All":
|
|
266
|
-
case "Any":
|
|
267
|
-
case "FirstOf":
|
|
268
|
-
return node.policies.some((child) => usesPredicate(child, context))
|
|
269
|
-
case "Ceiling":
|
|
270
|
-
return (
|
|
271
|
-
usesPredicate(node.cap, context) || usesPredicate(node.policy, context)
|
|
272
|
-
)
|
|
273
|
-
}
|
|
274
|
-
}
|
|
278
|
+
): boolean =>
|
|
279
|
+
node.opaque &&
|
|
280
|
+
Arr.some(
|
|
281
|
+
PolicyNodes.rulesOf(node),
|
|
282
|
+
(value) =>
|
|
283
|
+
value.when !== undefined &&
|
|
284
|
+
Nodes.toNode(value.when).opaque &&
|
|
285
|
+
RuleData.matchesTarget(value, context),
|
|
286
|
+
)
|
|
275
287
|
|
|
276
288
|
/**
|
|
277
289
|
* Makes the scope of a request, with the principal in it. A scope comes from
|
|
@@ -296,7 +308,7 @@ const scopeOf = (
|
|
|
296
308
|
const issues = Bind.ruleIssues(
|
|
297
309
|
domain,
|
|
298
310
|
{ actions: [request.action], resources: [request.kind], when: ast },
|
|
299
|
-
|
|
311
|
+
Place.at(undefined, "within"),
|
|
300
312
|
)
|
|
301
313
|
if (issues.length > 0) {
|
|
302
314
|
throw new globalThis.Error(
|
|
@@ -320,24 +332,36 @@ interface Scoped extends Pair {
|
|
|
320
332
|
readonly scope: Ast
|
|
321
333
|
/** The catalog of the kind, when the request fits the kind. */
|
|
322
334
|
readonly catalog: Catalog.Catalog | undefined
|
|
335
|
+
/** The limits of the domain. */
|
|
336
|
+
readonly limits: Limits.Limits
|
|
323
337
|
}
|
|
324
338
|
|
|
325
339
|
export const residualsAny = (policy: Any, request: ScopedRequest): Pair =>
|
|
326
340
|
scopedResiduals(policy, request)
|
|
327
341
|
|
|
328
342
|
const scopedResiduals = (policy: Any, request: ScopedRequest): Scoped => {
|
|
343
|
+
const limits = policy.domain.limits
|
|
344
|
+
const parts = limits.fieldPathParts
|
|
329
345
|
const descriptor = DomainData.descriptorOf(policy.domain, request.kind)
|
|
330
|
-
if (descriptor === undefined || !fitsKind(descriptor, request)) {
|
|
331
|
-
return {
|
|
346
|
+
if (descriptor === undefined || !fitsKind(descriptor, request, parts)) {
|
|
347
|
+
return {
|
|
348
|
+
scope: never,
|
|
349
|
+
allow: never,
|
|
350
|
+
deny: never,
|
|
351
|
+
catalog: undefined,
|
|
352
|
+
limits,
|
|
353
|
+
}
|
|
332
354
|
}
|
|
333
355
|
const input = { principal: request.principal, path: descriptor.path }
|
|
334
356
|
const scope = scopeOf(request, policy.domain, input)
|
|
357
|
+
const build = buildFor(limits)
|
|
335
358
|
const and = build.and
|
|
336
359
|
const inScope = (pair: Pair): Scoped => ({
|
|
337
360
|
scope,
|
|
338
361
|
allow: and([scope, pair.allow]),
|
|
339
362
|
deny: and([scope, pair.deny]),
|
|
340
363
|
catalog: descriptor.catalog,
|
|
364
|
+
limits,
|
|
341
365
|
})
|
|
342
366
|
if (scope._tag === "False") {
|
|
343
367
|
return inScope({ allow: never, deny: never })
|
|
@@ -348,7 +372,9 @@ const scopedResiduals = (policy: Any, request: ScopedRequest): Scoped => {
|
|
|
348
372
|
field: request.field,
|
|
349
373
|
input,
|
|
350
374
|
descriptor,
|
|
351
|
-
fits: RuleData.fitsOf(descriptor.catalog),
|
|
375
|
+
fits: RuleData.fitsOf(descriptor.catalog, parts),
|
|
376
|
+
parts,
|
|
377
|
+
build,
|
|
352
378
|
}
|
|
353
379
|
const context: ResidualContext = {
|
|
354
380
|
...target,
|
|
@@ -382,10 +408,11 @@ const scopedResiduals = (policy: Any, request: ScopedRequest): Scoped => {
|
|
|
382
408
|
path: descriptor.path,
|
|
383
409
|
},
|
|
384
410
|
descriptor,
|
|
385
|
-
fits: RuleData.fitsOf(descriptor.catalog),
|
|
411
|
+
fits: RuleData.fitsOf(descriptor.catalog, parts),
|
|
412
|
+
parts,
|
|
386
413
|
trace: undefined,
|
|
387
414
|
},
|
|
388
|
-
|
|
415
|
+
undefined,
|
|
389
416
|
)
|
|
390
417
|
// A value of another kind is not a value that the residual is about, so
|
|
391
418
|
// the rules of this kind must not run on it.
|
|
@@ -423,7 +450,12 @@ const coverages = Data.taggedEnum<AnyCoverage>()
|
|
|
423
450
|
|
|
424
451
|
/** Searches for a value of the kind of a scope that makes a condition true. */
|
|
425
452
|
const search = (scoped: Scoped, ast: Ast): Condition.Satisfiability =>
|
|
426
|
-
Search.satisfiable(
|
|
453
|
+
Search.satisfiable(
|
|
454
|
+
ast as Nodes.Node,
|
|
455
|
+
Search.kindsIn(scoped.catalog, scoped.limits.fieldPathParts),
|
|
456
|
+
scoped.limits.searchSteps,
|
|
457
|
+
scoped.limits.searchFields,
|
|
458
|
+
)
|
|
427
459
|
|
|
428
460
|
/** The values in the scope that the policy does not allow. */
|
|
429
461
|
const refusedOf = (scoped: Scoped): Ast =>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// principals and deciders to `unknown`. The public types keep them, so the
|
|
7
7
|
// language service check for `unknown` in an Effect is off here.
|
|
8
8
|
// @effect-diagnostics anyUnknownInErrorContext:off
|
|
9
|
+
import * as Arr from "effect/Array"
|
|
9
10
|
import * as Cache from "effect/Cache"
|
|
10
11
|
import * as Chunk from "effect/Chunk"
|
|
11
12
|
import * as Context from "effect/Context"
|
|
@@ -28,6 +29,7 @@ import * as Decide from "./decide.ts"
|
|
|
28
29
|
import * as DomainData from "./domain.ts"
|
|
29
30
|
import * as Policies from "./policy.ts"
|
|
30
31
|
import * as ResourceData from "./resource.ts"
|
|
32
|
+
import * as Trampoline from "./trampoline.ts"
|
|
31
33
|
|
|
32
34
|
type AnyServiceClass = Authorizer.AnyServiceClass
|
|
33
35
|
|
|
@@ -133,17 +135,27 @@ class PrincipalKey implements Equal.Equal {
|
|
|
133
135
|
|
|
134
136
|
/**
|
|
135
137
|
* The text of the order of the parts of a value: the names of the fields of
|
|
136
|
-
* each object, and the items of each list, set and map,
|
|
137
|
-
* loop sees them. `Equal` does not see
|
|
138
|
-
*
|
|
139
|
-
*
|
|
138
|
+
* each object, and the items of each list, set and map, with each scalar, in
|
|
139
|
+
* the order that a loop sees them. `Equal` does not see the order of the
|
|
140
|
+
* fields of an object or of the items of a map, but a loader may read it, as
|
|
141
|
+
* `Object.values` does. So two keys need the same order too. A scalar is in
|
|
142
|
+
* the text with its type, so the items of a set or a map show their order.
|
|
140
143
|
*/
|
|
141
|
-
const orderOf
|
|
144
|
+
const orderOf: (value: unknown) => string = Trampoline.adaptive<
|
|
145
|
+
unknown,
|
|
146
|
+
string
|
|
147
|
+
>((R, child) => (value, depth) => {
|
|
142
148
|
if (typeof value !== "object" || value === null) {
|
|
143
|
-
return
|
|
149
|
+
return R.succeed(
|
|
150
|
+
typeof value === "string"
|
|
151
|
+
? JSON.stringify(value)
|
|
152
|
+
: `${typeof value}:${String(value)}`,
|
|
153
|
+
)
|
|
144
154
|
}
|
|
155
|
+
const items = (list: ReadonlyArray<unknown>) =>
|
|
156
|
+
R.forEach(list, (item) => child(item, depth + 1))
|
|
145
157
|
if (Array.isArray(value)) {
|
|
146
|
-
return `[${
|
|
158
|
+
return R.map(items(value), (texts) => `[${texts.join(",")}]`)
|
|
147
159
|
}
|
|
148
160
|
if (
|
|
149
161
|
value instanceof Map ||
|
|
@@ -152,15 +164,18 @@ const orderOf = (value: unknown): string => {
|
|
|
152
164
|
HashSet.isHashSet(value) ||
|
|
153
165
|
Chunk.isChunk(value)
|
|
154
166
|
) {
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
.map(
|
|
159
|
-
(name) =>
|
|
160
|
-
`${JSON.stringify(name)}:${orderOf((value as Record<string, unknown>)[name])}`,
|
|
167
|
+
return R.map(
|
|
168
|
+
items(Array.from(value as Iterable<unknown>)),
|
|
169
|
+
(texts) => `<${texts.join(",")}>`,
|
|
161
170
|
)
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
}
|
|
172
|
+
const names = Object.keys(value)
|
|
173
|
+
return R.map(
|
|
174
|
+
items(Arr.map(names, (name) => (value as Record<string, unknown>)[name])),
|
|
175
|
+
(texts) =>
|
|
176
|
+
`{${Arr.map(names, (name, index) => `${JSON.stringify(name)}:${texts[index]!}`).join(",")}}`,
|
|
177
|
+
)
|
|
178
|
+
})
|
|
164
179
|
|
|
165
180
|
/**
|
|
166
181
|
* Gives the JSON codec of a Schema, or `undefined` when the Schema has no JSON
|
|
@@ -370,7 +385,10 @@ export const makeLayer = (
|
|
|
370
385
|
// A request that does not fit its kind gets a "no" at once. The
|
|
371
386
|
// decider must not say yes to an action that the kind does not have.
|
|
372
387
|
const descriptor = DomainData.descriptorOf(domain, kind)
|
|
373
|
-
if (
|
|
388
|
+
if (
|
|
389
|
+
descriptor === undefined ||
|
|
390
|
+
!Decide.fitsKind(descriptor, target, domain.limits.fieldPathParts)
|
|
391
|
+
) {
|
|
374
392
|
return yield* forbidden(target, kind, undefined)
|
|
375
393
|
}
|
|
376
394
|
const fromPolicy: Decision.Decision =
|