@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.
Files changed (87) hide show
  1. package/README.md +52 -29
  2. package/dist/Authorizer.d.ts +21 -12
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js.map +1 -1
  5. package/dist/Condition.d.ts +121 -19
  6. package/dist/Condition.d.ts.map +1 -1
  7. package/dist/Condition.js +32 -21
  8. package/dist/Condition.js.map +1 -1
  9. package/dist/Domain.d.ts +9 -0
  10. package/dist/Domain.d.ts.map +1 -1
  11. package/dist/Domain.js +5 -0
  12. package/dist/Domain.js.map +1 -1
  13. package/dist/Limits.d.ts +44 -13
  14. package/dist/Limits.d.ts.map +1 -1
  15. package/dist/Limits.js +52 -22
  16. package/dist/Limits.js.map +1 -1
  17. package/dist/Policy.d.ts +102 -18
  18. package/dist/Policy.d.ts.map +1 -1
  19. package/dist/Policy.js +7 -6
  20. package/dist/Policy.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/internal/algebra.js +2 -2
  23. package/dist/internal/analysis.js +81 -75
  24. package/dist/internal/analysis.js.map +1 -1
  25. package/dist/internal/authorizer.d.ts.map +1 -1
  26. package/dist/internal/authorizer.js +11 -7
  27. package/dist/internal/authorizer.js.map +1 -1
  28. package/dist/internal/bind.js +86 -64
  29. package/dist/internal/bind.js.map +1 -1
  30. package/dist/internal/catalog.d.ts +33 -10
  31. package/dist/internal/catalog.d.ts.map +1 -1
  32. package/dist/internal/catalog.js +170 -77
  33. package/dist/internal/catalog.js.map +1 -1
  34. package/dist/internal/decide.js +27 -31
  35. package/dist/internal/decide.js.map +1 -1
  36. package/dist/internal/domain.js +19 -5
  37. package/dist/internal/domain.js.map +1 -1
  38. package/dist/internal/json.js +120 -76
  39. package/dist/internal/json.js.map +1 -1
  40. package/dist/internal/limits.d.ts +28 -0
  41. package/dist/internal/limits.d.ts.map +1 -1
  42. package/dist/internal/limits.js +75 -16
  43. package/dist/internal/limits.js.map +1 -1
  44. package/dist/internal/nodes.d.ts.map +1 -1
  45. package/dist/internal/nodes.js +53 -44
  46. package/dist/internal/nodes.js.map +1 -1
  47. package/dist/internal/patterns.js +50 -15
  48. package/dist/internal/patterns.js.map +1 -1
  49. package/dist/internal/policy-json.js +169 -127
  50. package/dist/internal/policy-json.js.map +1 -1
  51. package/dist/internal/policy-nodes.d.ts.map +1 -1
  52. package/dist/internal/policy-nodes.js +16 -13
  53. package/dist/internal/policy-nodes.js.map +1 -1
  54. package/dist/internal/policy.d.ts.map +1 -1
  55. package/dist/internal/policy.js +38 -22
  56. package/dist/internal/policy.js.map +1 -1
  57. package/dist/internal/resource.js +1 -1
  58. package/dist/internal/rule.d.ts.map +1 -1
  59. package/dist/internal/rule.js +6 -5
  60. package/dist/internal/rule.js.map +1 -1
  61. package/dist/internal/search.d.ts.map +1 -1
  62. package/dist/internal/search.js +106 -92
  63. package/dist/internal/search.js.map +1 -1
  64. package/dist/internal/trampoline.js +19 -5
  65. package/dist/internal/trampoline.js.map +1 -1
  66. package/package.json +1 -1
  67. package/src/Authorizer.ts +30 -20
  68. package/src/Condition.ts +317 -86
  69. package/src/Domain.ts +9 -0
  70. package/src/Limits.ts +69 -29
  71. package/src/Policy.ts +143 -21
  72. package/src/internal/analysis.ts +154 -124
  73. package/src/internal/authorizer.ts +29 -14
  74. package/src/internal/bind.ts +193 -136
  75. package/src/internal/catalog.ts +320 -118
  76. package/src/internal/decide.ts +61 -50
  77. package/src/internal/domain.ts +39 -3
  78. package/src/internal/json.ts +212 -117
  79. package/src/internal/limits.ts +101 -31
  80. package/src/internal/nodes.ts +65 -47
  81. package/src/internal/patterns.ts +79 -25
  82. package/src/internal/policy-json.ts +266 -197
  83. package/src/internal/policy-nodes.ts +31 -23
  84. package/src/internal/policy.ts +63 -23
  85. package/src/internal/rule.ts +16 -3
  86. package/src/internal/search.ts +186 -136
  87. package/src/internal/trampoline.ts +51 -4
@@ -19,7 +19,8 @@ import type * as Policy from "../Policy.ts"
19
19
  import type * as Resource from "../Resource.ts"
20
20
  import * as Catalog from "./catalog.ts"
21
21
  import * as DomainData from "./domain.ts"
22
- import type * as PolicyNodes from "./policy-nodes.ts"
22
+ import * as Place from "./place.ts"
23
+ import * as PolicyNodes from "./policy-nodes.ts"
23
24
  import { nodeOf } from "./policy.ts"
24
25
  import * as ResourceData from "./resource.ts"
25
26
  import * as RuleData from "./rule.ts"
@@ -39,14 +40,23 @@ interface DecideContext {
39
40
  readonly descriptor: Resource.Descriptor
40
41
  /** The test of `RuleData.fitsOf` for the catalog, for this decision. */
41
42
  readonly fits: (pattern: string) => boolean
43
+ /** The most parts of a field path in the domain. */
44
+ readonly parts: number
42
45
  /** The rules that were tested, for {@link explain}. */
43
46
  readonly trace: Ref.Ref<Chunk.Chunk<Step>> | undefined
44
47
  }
45
48
 
49
+ /**
50
+ * The place of a node or a rule in a tree: the index of each child, then
51
+ * the index of the rule. It shares the path of its parent, and a decision
52
+ * makes the full path only for a step of `explain`: see `Place`.
53
+ */
54
+ type At = Place.Place | undefined
55
+
46
56
  const ruleApplies = (
47
57
  value: RuleData.Rule,
48
58
  context: DecideContext,
49
- path: ReadonlyArray<number>,
59
+ at: Place.Place,
50
60
  ): Effect.Effect<boolean, unknown, unknown> => {
51
61
  if (!RuleData.matchesTarget(value, context)) {
52
62
  return Effect.succeed(false)
@@ -62,7 +72,14 @@ const ruleApplies = (
62
72
  ? test
63
73
  : Effect.tap(test, (applies) =>
64
74
  // A chunk adds a step with no copy of the steps before.
65
- Ref.update(trace, Chunk.append({ path, rule: typed(value), applies })),
75
+ Ref.update(
76
+ trace,
77
+ Chunk.append({
78
+ path: Place.pathOf(at) as ReadonlyArray<number>,
79
+ rule: typed(value),
80
+ applies,
81
+ }),
82
+ ),
66
83
  )
67
84
  }
68
85
 
@@ -80,11 +97,11 @@ const fromRule = (value: RuleData.Rule): Decision.Decision =>
80
97
  const firstApplying = (
81
98
  candidates: ReadonlyArray<readonly [RuleData.Rule, number]>,
82
99
  context: DecideContext,
83
- path: ReadonlyArray<number>,
100
+ at: At,
84
101
  ): Effect.Effect<Option.Option<RuleData.Rule>, unknown, unknown> =>
85
102
  Effect.map(
86
103
  Effect.findFirst(candidates, ([value, index]) =>
87
- ruleApplies(value, context, [...path, index]),
104
+ ruleApplies(value, context, Place.at(at, index)),
88
105
  ),
89
106
  Option.map(([value]) => value),
90
107
  )
@@ -99,13 +116,13 @@ const orElse = <A, E, R>(
99
116
  const decideRules = (
100
117
  node: PolicyNodes.Rules,
101
118
  context: DecideContext,
102
- path: ReadonlyArray<number>,
119
+ at: At,
103
120
  ): Effect.Effect<Decision.Decision, unknown, unknown> => {
104
121
  const indexed = Arr.map(node.rules, (value, index) => [value, index] as const)
105
122
  const denies = Arr.filter(indexed, ([value]) => value._tag === "Deny")
106
123
  const allows = Arr.filter(indexed, ([value]) => value._tag === "Allow")
107
124
  const first = (candidates: ReadonlyArray<readonly [RuleData.Rule, number]>) =>
108
- firstApplying(candidates, context, path)
125
+ firstApplying(candidates, context, at)
109
126
  const found = (() => {
110
127
  switch (node.algorithm) {
111
128
  case "denyOverrides":
@@ -138,7 +155,7 @@ type WalkStep<S> =
138
155
  const walkChildren = <S>(
139
156
  children: ReadonlyArray<PolicyNodes.Node>,
140
157
  context: DecideContext,
141
- path: ReadonlyArray<number>,
158
+ at: At,
142
159
  start: S,
143
160
  step: (state: S, decision: Decision.Decision) => WalkStep<S>,
144
161
  end: (state: S) => Decision.Decision,
@@ -150,7 +167,11 @@ const walkChildren = <S>(
150
167
  index === children.length
151
168
  ? Effect.succeed(end(state))
152
169
  : Effect.flatMap(
153
- decideNode(children[index]!, context, [...path, index]),
170
+ // Each call for a child is a step of the Effect, so a deep tree
171
+ // does not fill the stack.
172
+ Effect.suspend(() =>
173
+ decideNode(children[index]!, context, Place.at(at, index)),
174
+ ),
154
175
  (decision) => {
155
176
  const next = step(state, decision)
156
177
  return next._tag === "Stop"
@@ -176,17 +197,17 @@ const next = <S>(state: S): WalkStep<S> => ({ _tag: "Next", state })
176
197
  export const decideNode = (
177
198
  node: PolicyNodes.Node,
178
199
  context: DecideContext,
179
- path: ReadonlyArray<number>,
200
+ at: At,
180
201
  ): Effect.Effect<Decision.Decision, unknown, unknown> => {
181
202
  switch (node._tag) {
182
203
  case "Rules":
183
- return decideRules(node, context, path)
204
+ return decideRules(node, context, at)
184
205
  case "All":
185
206
  // One deny is enough to deny. Every child must allow.
186
207
  return walkChildren(
187
208
  node.policies,
188
209
  context,
189
- path,
210
+ at,
190
211
  {
191
212
  firstAllow: Option.none<Decision.Decision>(),
192
213
  allAllow: node.policies.length > 0,
@@ -212,7 +233,7 @@ export const decideNode = (
212
233
  return walkChildren(
213
234
  node.policies,
214
235
  context,
215
- path,
236
+ at,
216
237
  Option.none<Decision.Decision>(),
217
238
  (firstDeny, decision) =>
218
239
  decision._tag === "Allow"
@@ -231,7 +252,7 @@ export const decideNode = (
231
252
  return walkChildren(
232
253
  node.policies,
233
254
  context,
234
- path,
255
+ at,
235
256
  undefined,
236
257
  (_, decision) =>
237
258
  decision._tag === "NotApplicable" ? next(undefined) : stop(decision),
@@ -240,9 +261,9 @@ export const decideNode = (
240
261
  case "Ceiling":
241
262
  // A deny of the cap wins. Else the policy decides.
242
263
  return Effect.filterOrElse(
243
- decideNode(node.cap, context, [...path, 0]),
264
+ Effect.suspend(() => decideNode(node.cap, context, Place.at(at, 0))),
244
265
  (capped) => capped._tag === "Deny",
245
- () => decideNode(node.policy, context, [...path, 1]),
266
+ () => decideNode(node.policy, context, Place.at(at, 1)),
246
267
  )
247
268
  }
248
269
  }
@@ -254,10 +275,11 @@ export const decideNode = (
254
275
  export const fitsKind = (
255
276
  descriptor: Resource.Descriptor,
256
277
  request: { readonly action: string; readonly field?: string | undefined },
278
+ parts: number,
257
279
  ): boolean =>
258
280
  descriptor.actions.includes(request.action) &&
259
281
  (request.field === undefined ||
260
- (Catalog.lookup(descriptor.catalog, request.field)?.listed ?? false))
282
+ (Catalog.lookup(descriptor.catalog, request.field, parts)?.listed ?? false))
261
283
 
262
284
  const makeContext = (
263
285
  domain: Domain.Any,
@@ -270,7 +292,7 @@ const makeContext = (
270
292
  if (
271
293
  kind === undefined ||
272
294
  descriptor === undefined ||
273
- !fitsKind(descriptor, request)
295
+ !fitsKind(descriptor, request, domain.limits.fieldPathParts)
274
296
  ) {
275
297
  return undefined
276
298
  }
@@ -284,7 +306,8 @@ const makeContext = (
284
306
  path: descriptor.path,
285
307
  },
286
308
  descriptor,
287
- fits: RuleData.fitsOf(descriptor.catalog),
309
+ fits: RuleData.fitsOf(descriptor.catalog, domain.limits.fieldPathParts),
310
+ parts: domain.limits.fieldPathParts,
288
311
  trace,
289
312
  }
290
313
  }
@@ -298,7 +321,7 @@ export const decideAny = (
298
321
  const context = makeContext(policy.domain, request, undefined)
299
322
  return context === undefined
300
323
  ? Effect.succeed(Decision.NotApplicable)
301
- : decideNode(nodeOf(policy), context, [])
324
+ : decideNode(nodeOf(policy), context, undefined)
302
325
  })
303
326
 
304
327
  /** Gives the decision of a policy with no predicates, at once. */
@@ -326,7 +349,7 @@ export const explain = (
326
349
  if (context === undefined) {
327
350
  return { decision: Decision.NotApplicable, steps: [] }
328
351
  }
329
- const decision = yield* decideNode(nodeOf(policy), context, [])
352
+ const decision = yield* decideNode(nodeOf(policy), context, undefined)
330
353
  return { decision, steps: Chunk.toReadonlyArray(yield* Ref.get(trace)) }
331
354
  })
332
355
 
@@ -334,35 +357,19 @@ export const explain = (
334
357
  * Tests if a deny rule of the request names a part inside `field`.
335
358
  */
336
359
  const deniesInside = (
337
- node: PolicyNodes.Node,
360
+ rules: ReadonlyArray<RuleData.Rule>,
338
361
  target: { readonly action: string; readonly kind: string },
339
362
  field: string,
340
- ): boolean => {
341
- switch (node._tag) {
342
- case "Rules":
343
- return node.rules.some(
344
- (value) =>
345
- value._tag === "Deny" &&
346
- value.fields !== undefined &&
347
- value.fields.some((pattern) =>
348
- RuleData.matchesInside(pattern, field),
349
- ) &&
350
- (value.actions.includes("*") ||
351
- value.actions.includes(target.action)) &&
352
- (value.resources.includes("*") ||
353
- value.resources.includes(target.kind)),
354
- )
355
- case "All":
356
- case "Any":
357
- case "FirstOf":
358
- return node.policies.some((child) => deniesInside(child, target, field))
359
- case "Ceiling":
360
- return (
361
- deniesInside(node.cap, target, field) ||
362
- deniesInside(node.policy, target, field)
363
- )
364
- }
365
- }
363
+ ): boolean =>
364
+ Arr.some(
365
+ rules,
366
+ (value) =>
367
+ value._tag === "Deny" &&
368
+ value.fields !== undefined &&
369
+ value.fields.some((pattern) => RuleData.matchesInside(pattern, field)) &&
370
+ (value.actions.includes("*") || value.actions.includes(target.action)) &&
371
+ (value.resources.includes("*") || value.resources.includes(target.kind)),
372
+ )
366
373
 
367
374
  /** Gives the fields of a resource value that the policy lets a request use. */
368
375
  export const permittedFields = (
@@ -377,9 +384,13 @@ export const permittedFields = (
377
384
  if (descriptor === undefined) {
378
385
  return Effect.succeed([])
379
386
  }
387
+ // The rules of the tree, in one flat list for all the fields.
388
+ const rules = policy.pipe(nodeOf, PolicyNodes.rulesOf)
380
389
  // The fields with names only: a list can not name every key of a record.
381
390
  const listed = Arr.filter(
382
- descriptor.catalog.fields,
391
+ // The list for the limit of the parts of a path of the domain, as the
392
+ // decision of each field reads it.
393
+ Catalog.fieldsOf(descriptor.catalog, policy.domain.limits.fieldPathParts),
383
394
  (field) =>
384
395
  field.listed &&
385
396
  Arr.every(field.path, (segment) => typeof segment === "string"),
@@ -393,7 +404,7 @@ export const permittedFields = (
393
404
  !(
394
405
  field.open &&
395
406
  deniesInside(
396
- nodeOf(policy),
407
+ rules,
397
408
  { ...request, kind: descriptor.kind },
398
409
  field.text,
399
410
  )
@@ -9,6 +9,7 @@ import type * as Schema from "effect/Schema"
9
9
  import type * as Domain from "../Domain.ts"
10
10
  import type * as Resource from "../Resource.ts"
11
11
  import * as Catalog from "./catalog.ts"
12
+ import * as Limits from "./limits.ts"
12
13
  import * as ResourceData from "./resource.ts"
13
14
 
14
15
  /** The class of every domain value. `isDomain` uses it to know a domain. */
@@ -17,14 +18,38 @@ class DomainData extends Data.Class<{
17
18
  readonly principalCatalog: Catalog.Catalog
18
19
  readonly resources: HashMap.HashMap<string, Resource.Descriptor>
19
20
  readonly meta: Schema.Top | undefined
21
+ readonly limits: Limits.Limits
20
22
  }> {}
21
23
 
22
- /** Makes a domain. Each resource must have its own kind name. */
24
+ /** Stops a catalog with more fields in its list than the domain takes. */
25
+ const checkCatalog = (
26
+ catalog: Catalog.Catalog,
27
+ limits: Limits.Limits,
28
+ name: string,
29
+ ): void => {
30
+ const fields = Catalog.fieldsOf(catalog, limits.fieldPathParts).length
31
+ if (fields > limits.catalogFields) {
32
+ throw Limits.exceeded(
33
+ name,
34
+ "catalogFields",
35
+ "fields",
36
+ fields,
37
+ limits.catalogFields,
38
+ )
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Makes a domain. Each resource must have its own kind name. The limits that
44
+ * the domain does not set have their defaults.
45
+ */
23
46
  export const make = (options: {
24
47
  readonly principal: Schema.Top
25
48
  readonly resources: ReadonlyArray<Resource.Any>
26
49
  readonly meta?: Schema.Top
50
+ readonly limits?: Partial<Limits.Limits>
27
51
  }): DomainData => {
52
+ const limits = Limits.make(options.limits)
28
53
  const descriptors = Arr.map(options.resources, ResourceData.descriptor)
29
54
  const kinds = Arr.map(descriptors, (descriptor) => descriptor.kind)
30
55
  // Two resources with one kind name is a mistake in the code, not in the
@@ -46,12 +71,23 @@ export const make = (options: {
46
71
  ),
47
72
  )
48
73
  // The catalog checks the field names of the principal, so a bad name is a
49
- // defect when the domain is made.
74
+ // defect when the domain is made. A Schema with more fields than the
75
+ // domain takes is a defect too.
76
+ const principalCatalog = Catalog.catalogOf(options.principal.ast)
77
+ checkCatalog(principalCatalog, limits, "The Schema of the principal")
78
+ Arr.forEach(descriptors, (descriptor) =>
79
+ checkCatalog(
80
+ descriptor.catalog,
81
+ limits,
82
+ `The Schema of the resource "${descriptor.kind}"`,
83
+ ),
84
+ )
50
85
  return new DomainData({
51
86
  principal: options.principal,
52
- principalCatalog: Catalog.catalogOf(options.principal.ast),
87
+ principalCatalog,
53
88
  resources,
54
89
  meta: options.meta,
90
+ limits,
55
91
  })
56
92
  }
57
93