@nmnmcc/ability 2.0.7 → 2.1.2
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 +52 -29
- 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 +121 -19
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +32 -21
- 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 +102 -18
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +7 -6
- package/dist/Policy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/internal/algebra.js +2 -2
- 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 +11 -7
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +86 -64
- 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 +170 -77
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +27 -31
- 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.map +1 -1
- package/dist/internal/nodes.js +53 -44
- 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 +16 -13
- 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 +19 -5
- package/dist/internal/trampoline.js.map +1 -1
- package/package.json +1 -1
- package/src/Authorizer.ts +30 -20
- package/src/Condition.ts +317 -86
- package/src/Domain.ts +9 -0
- package/src/Limits.ts +69 -29
- package/src/Policy.ts +143 -21
- package/src/internal/analysis.ts +154 -124
- package/src/internal/authorizer.ts +29 -14
- package/src/internal/bind.ts +193 -136
- package/src/internal/catalog.ts +320 -118
- package/src/internal/decide.ts +61 -50
- 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 +65 -47
- package/src/internal/patterns.ts +79 -25
- package/src/internal/policy-json.ts +266 -197
- package/src/internal/policy-nodes.ts +31 -23
- 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 +51 -4
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,25 +52,28 @@ interface Build {
|
|
|
50
52
|
readonly not: (value: Ast) => Ast
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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),
|
|
64
76
|
}
|
|
65
|
-
return node
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const build: Build = {
|
|
69
|
-
and: (values) => limit(Algebra.and(values as ReadonlyArray<Nodes.Node>)),
|
|
70
|
-
or: (values) => limit(Algebra.or(values as ReadonlyArray<Nodes.Node>)),
|
|
71
|
-
not: (value) => (value as Nodes.Node).pipe(Algebra.not, limit),
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
/** The two residual conditions of a node, with no types. */
|
|
@@ -89,6 +94,10 @@ interface ResidualContext {
|
|
|
89
94
|
readonly descriptor: Resource.Descriptor
|
|
90
95
|
/** The test of `RuleData.fitsOf` for the catalog, for this request. */
|
|
91
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
|
|
92
101
|
/**
|
|
93
102
|
* The condition of each rule, with the principal in it. They are made
|
|
94
103
|
* first, from a flat list of the rules, so the walk does not go down the
|
|
@@ -120,7 +129,7 @@ const firstApplicableResidual = (
|
|
|
120
129
|
values: ReadonlyArray<RuleData.Rule>,
|
|
121
130
|
context: ResidualContext,
|
|
122
131
|
): Pair => {
|
|
123
|
-
const { and, not, or } = build
|
|
132
|
+
const { and, not, or } = context.build
|
|
124
133
|
// The rules go in runs of one tag, from the last run to the first. The
|
|
125
134
|
// conditions of a run go into one node at once. With one node for each
|
|
126
135
|
// rule, each step would flatten the whole condition again.
|
|
@@ -149,90 +158,104 @@ const firstApplicableResidual = (
|
|
|
149
158
|
* Gives the residual pair of a node. A shared child is worked again for each
|
|
150
159
|
* place. The limits of the nodes and of a residual keep this work small.
|
|
151
160
|
*/
|
|
152
|
-
const residualNode = (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
or(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
}),
|
|
172
197
|
),
|
|
173
|
-
])
|
|
174
|
-
return { allow, deny }
|
|
175
|
-
}
|
|
176
|
-
case "permitOverrides": {
|
|
177
|
-
const allow = or(
|
|
178
|
-
node.rules
|
|
179
|
-
.filter((value) => value._tag === "Allow")
|
|
180
|
-
.map((value) => ruleCondition(value, context)),
|
|
181
198
|
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
case "lastApplicable":
|
|
195
|
-
return firstApplicableResidual(node.rules.slice().reverse(), context)
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
// falls through: every algorithm returns above
|
|
199
|
-
case "All": {
|
|
200
|
-
if (node.policies.length === 0) {
|
|
201
|
-
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
|
+
)
|
|
202
211
|
}
|
|
203
|
-
const pairs = node.policies.map((child) => residualNode(child, context))
|
|
204
|
-
const deny = or(pairs.map((pair) => pair.deny))
|
|
205
|
-
const allow = and([...pairs.map((pair) => pair.allow), not(deny)])
|
|
206
|
-
return { allow, deny }
|
|
207
212
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
])
|
|
212
237
|
return { allow, deny }
|
|
213
238
|
}
|
|
214
|
-
case "
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
(rest: Pair, child): Pair => {
|
|
220
|
-
const pair = residualNode(child, context)
|
|
221
|
-
return {
|
|
222
|
-
allow: or([pair.allow, and([not(pair.deny), rest.allow])]),
|
|
223
|
-
deny: or([pair.deny, and([not(pair.allow), rest.deny])]),
|
|
224
|
-
}
|
|
225
|
-
},
|
|
239
|
+
case "permitOverrides": {
|
|
240
|
+
const allow = or(
|
|
241
|
+
node.rules
|
|
242
|
+
.filter((value) => value._tag === "Allow")
|
|
243
|
+
.map((value) => ruleCondition(value, context)),
|
|
226
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 }
|
|
227
254
|
}
|
|
228
|
-
case "
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return
|
|
232
|
-
allow: and([not(cap.deny), inner.allow]),
|
|
233
|
-
deny: or([cap.deny, inner.deny]),
|
|
234
|
-
}
|
|
235
|
-
}
|
|
255
|
+
case "firstApplicable":
|
|
256
|
+
return firstApplicableResidual(node.rules, context)
|
|
257
|
+
case "lastApplicable":
|
|
258
|
+
return firstApplicableResidual(node.rules.slice().reverse(), context)
|
|
236
259
|
}
|
|
237
260
|
}
|
|
238
261
|
|
|
@@ -252,28 +275,15 @@ interface PredicateContext {
|
|
|
252
275
|
const usesPredicate = (
|
|
253
276
|
node: PolicyNodes.Node,
|
|
254
277
|
context: PredicateContext,
|
|
255
|
-
): boolean =>
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
Nodes.toNode(value.when).opaque &&
|
|
265
|
-
RuleData.matchesTarget(value, context),
|
|
266
|
-
)
|
|
267
|
-
case "All":
|
|
268
|
-
case "Any":
|
|
269
|
-
case "FirstOf":
|
|
270
|
-
return node.policies.some((child) => usesPredicate(child, context))
|
|
271
|
-
case "Ceiling":
|
|
272
|
-
return (
|
|
273
|
-
usesPredicate(node.cap, context) || usesPredicate(node.policy, context)
|
|
274
|
-
)
|
|
275
|
-
}
|
|
276
|
-
}
|
|
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
|
+
)
|
|
277
287
|
|
|
278
288
|
/**
|
|
279
289
|
* Makes the scope of a request, with the principal in it. A scope comes from
|
|
@@ -298,7 +308,7 @@ const scopeOf = (
|
|
|
298
308
|
const issues = Bind.ruleIssues(
|
|
299
309
|
domain,
|
|
300
310
|
{ actions: [request.action], resources: [request.kind], when: ast },
|
|
301
|
-
(
|
|
311
|
+
Place.at(undefined, "within"),
|
|
302
312
|
)
|
|
303
313
|
if (issues.length > 0) {
|
|
304
314
|
throw new globalThis.Error(
|
|
@@ -322,24 +332,36 @@ interface Scoped extends Pair {
|
|
|
322
332
|
readonly scope: Ast
|
|
323
333
|
/** The catalog of the kind, when the request fits the kind. */
|
|
324
334
|
readonly catalog: Catalog.Catalog | undefined
|
|
335
|
+
/** The limits of the domain. */
|
|
336
|
+
readonly limits: Limits.Limits
|
|
325
337
|
}
|
|
326
338
|
|
|
327
339
|
export const residualsAny = (policy: Any, request: ScopedRequest): Pair =>
|
|
328
340
|
scopedResiduals(policy, request)
|
|
329
341
|
|
|
330
342
|
const scopedResiduals = (policy: Any, request: ScopedRequest): Scoped => {
|
|
343
|
+
const limits = policy.domain.limits
|
|
344
|
+
const parts = limits.fieldPathParts
|
|
331
345
|
const descriptor = DomainData.descriptorOf(policy.domain, request.kind)
|
|
332
|
-
if (descriptor === undefined || !fitsKind(descriptor, request)) {
|
|
333
|
-
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
|
+
}
|
|
334
354
|
}
|
|
335
355
|
const input = { principal: request.principal, path: descriptor.path }
|
|
336
356
|
const scope = scopeOf(request, policy.domain, input)
|
|
357
|
+
const build = buildFor(limits)
|
|
337
358
|
const and = build.and
|
|
338
359
|
const inScope = (pair: Pair): Scoped => ({
|
|
339
360
|
scope,
|
|
340
361
|
allow: and([scope, pair.allow]),
|
|
341
362
|
deny: and([scope, pair.deny]),
|
|
342
363
|
catalog: descriptor.catalog,
|
|
364
|
+
limits,
|
|
343
365
|
})
|
|
344
366
|
if (scope._tag === "False") {
|
|
345
367
|
return inScope({ allow: never, deny: never })
|
|
@@ -350,7 +372,9 @@ const scopedResiduals = (policy: Any, request: ScopedRequest): Scoped => {
|
|
|
350
372
|
field: request.field,
|
|
351
373
|
input,
|
|
352
374
|
descriptor,
|
|
353
|
-
fits: RuleData.fitsOf(descriptor.catalog),
|
|
375
|
+
fits: RuleData.fitsOf(descriptor.catalog, parts),
|
|
376
|
+
parts,
|
|
377
|
+
build,
|
|
354
378
|
}
|
|
355
379
|
const context: ResidualContext = {
|
|
356
380
|
...target,
|
|
@@ -384,10 +408,11 @@ const scopedResiduals = (policy: Any, request: ScopedRequest): Scoped => {
|
|
|
384
408
|
path: descriptor.path,
|
|
385
409
|
},
|
|
386
410
|
descriptor,
|
|
387
|
-
fits: RuleData.fitsOf(descriptor.catalog),
|
|
411
|
+
fits: RuleData.fitsOf(descriptor.catalog, parts),
|
|
412
|
+
parts,
|
|
388
413
|
trace: undefined,
|
|
389
414
|
},
|
|
390
|
-
|
|
415
|
+
undefined,
|
|
391
416
|
)
|
|
392
417
|
// A value of another kind is not a value that the residual is about, so
|
|
393
418
|
// the rules of this kind must not run on it.
|
|
@@ -425,7 +450,12 @@ const coverages = Data.taggedEnum<AnyCoverage>()
|
|
|
425
450
|
|
|
426
451
|
/** Searches for a value of the kind of a scope that makes a condition true. */
|
|
427
452
|
const search = (scoped: Scoped, ast: Ast): Condition.Satisfiability =>
|
|
428
|
-
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
|
+
)
|
|
429
459
|
|
|
430
460
|
/** The values in the scope that the policy does not allow. */
|
|
431
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
|
|
|
@@ -139,14 +141,21 @@ class PrincipalKey implements Equal.Equal {
|
|
|
139
141
|
* `Object.values` does. So two keys need the same order too. A scalar is in
|
|
140
142
|
* the text with its type, so the items of a set or a map show their order.
|
|
141
143
|
*/
|
|
142
|
-
const orderOf
|
|
144
|
+
const orderOf: (value: unknown) => string = Trampoline.adaptive<
|
|
145
|
+
unknown,
|
|
146
|
+
string
|
|
147
|
+
>((R, child) => (value, depth) => {
|
|
143
148
|
if (typeof value !== "object" || value === null) {
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
return R.succeed(
|
|
150
|
+
typeof value === "string"
|
|
151
|
+
? JSON.stringify(value)
|
|
152
|
+
: `${typeof value}:${String(value)}`,
|
|
153
|
+
)
|
|
147
154
|
}
|
|
155
|
+
const items = (list: ReadonlyArray<unknown>) =>
|
|
156
|
+
R.forEach(list, (item) => child(item, depth + 1))
|
|
148
157
|
if (Array.isArray(value)) {
|
|
149
|
-
return `[${
|
|
158
|
+
return R.map(items(value), (texts) => `[${texts.join(",")}]`)
|
|
150
159
|
}
|
|
151
160
|
if (
|
|
152
161
|
value instanceof Map ||
|
|
@@ -155,15 +164,18 @@ const orderOf = (value: unknown): string => {
|
|
|
155
164
|
HashSet.isHashSet(value) ||
|
|
156
165
|
Chunk.isChunk(value)
|
|
157
166
|
) {
|
|
158
|
-
return
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.map(
|
|
162
|
-
(name) =>
|
|
163
|
-
`${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(",")}>`,
|
|
164
170
|
)
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
})
|
|
167
179
|
|
|
168
180
|
/**
|
|
169
181
|
* Gives the JSON codec of a Schema, or `undefined` when the Schema has no JSON
|
|
@@ -373,7 +385,10 @@ export const makeLayer = (
|
|
|
373
385
|
// A request that does not fit its kind gets a "no" at once. The
|
|
374
386
|
// decider must not say yes to an action that the kind does not have.
|
|
375
387
|
const descriptor = DomainData.descriptorOf(domain, kind)
|
|
376
|
-
if (
|
|
388
|
+
if (
|
|
389
|
+
descriptor === undefined ||
|
|
390
|
+
!Decide.fitsKind(descriptor, target, domain.limits.fieldPathParts)
|
|
391
|
+
) {
|
|
377
392
|
return yield* forbidden(target, kind, undefined)
|
|
378
393
|
}
|
|
379
394
|
const fromPolicy: Decision.Decision =
|