@nmnmcc/ability 1.0.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +112 -69
  2. package/dist/Authorizer.d.ts +43 -47
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js +5 -290
  5. package/dist/Authorizer.js.map +1 -1
  6. package/dist/Condition.d.ts +164 -332
  7. package/dist/Condition.d.ts.map +1 -1
  8. package/dist/Condition.js +75 -890
  9. package/dist/Condition.js.map +1 -1
  10. package/dist/Decision.d.ts +13 -13
  11. package/dist/Decision.d.ts.map +1 -1
  12. package/dist/Decision.js.map +1 -1
  13. package/dist/Domain.d.ts +10 -21
  14. package/dist/Domain.d.ts.map +1 -1
  15. package/dist/Domain.js +4 -62
  16. package/dist/Domain.js.map +1 -1
  17. package/dist/HttpApi.d.ts +31 -7
  18. package/dist/HttpApi.d.ts.map +1 -1
  19. package/dist/HttpApi.js +25 -8
  20. package/dist/HttpApi.js.map +1 -1
  21. package/dist/Limits.d.ts +114 -0
  22. package/dist/Limits.d.ts.map +1 -0
  23. package/dist/Limits.js +132 -0
  24. package/dist/Limits.js.map +1 -0
  25. package/dist/Policy.d.ts +276 -269
  26. package/dist/Policy.d.ts.map +1 -1
  27. package/dist/Policy.js +73 -1177
  28. package/dist/Policy.js.map +1 -1
  29. package/dist/Resource.d.ts +4 -177
  30. package/dist/Resource.d.ts.map +1 -1
  31. package/dist/Resource.js +4 -803
  32. package/dist/Resource.js.map +1 -1
  33. package/dist/index.d.ts +3 -4
  34. package/dist/index.js +4 -5
  35. package/dist/internal/algebra.js +267 -0
  36. package/dist/internal/algebra.js.map +1 -0
  37. package/dist/internal/analysis.js +245 -0
  38. package/dist/internal/analysis.js.map +1 -0
  39. package/dist/internal/authorizer.d.ts +30 -0
  40. package/dist/internal/authorizer.d.ts.map +1 -0
  41. package/dist/internal/authorizer.js +221 -0
  42. package/dist/internal/authorizer.js.map +1 -0
  43. package/dist/internal/bind.js +311 -0
  44. package/dist/internal/bind.js.map +1 -0
  45. package/dist/internal/catalog.d.ts +56 -0
  46. package/dist/internal/catalog.d.ts.map +1 -0
  47. package/dist/internal/catalog.js +292 -0
  48. package/dist/internal/catalog.js.map +1 -0
  49. package/dist/internal/decide.js +166 -0
  50. package/dist/internal/decide.js.map +1 -0
  51. package/dist/internal/domain.js +32 -0
  52. package/dist/internal/domain.js.map +1 -0
  53. package/dist/internal/json.js +134 -0
  54. package/dist/internal/json.js.map +1 -0
  55. package/dist/internal/limits.d.ts +14 -0
  56. package/dist/internal/limits.d.ts.map +1 -0
  57. package/dist/internal/limits.js +43 -0
  58. package/dist/internal/limits.js.map +1 -0
  59. package/dist/internal/nodes.d.ts +121 -0
  60. package/dist/internal/nodes.d.ts.map +1 -0
  61. package/dist/internal/nodes.js +277 -0
  62. package/dist/internal/nodes.js.map +1 -0
  63. package/dist/internal/patterns.js +110 -0
  64. package/dist/internal/patterns.js.map +1 -0
  65. package/dist/internal/policy-json.js +265 -0
  66. package/dist/internal/policy-json.js.map +1 -0
  67. package/dist/internal/policy-nodes.d.ts +61 -0
  68. package/dist/internal/policy-nodes.d.ts.map +1 -0
  69. package/dist/internal/policy-nodes.js +146 -0
  70. package/dist/internal/policy-nodes.js.map +1 -0
  71. package/dist/internal/policy.d.ts +42 -0
  72. package/dist/internal/policy.d.ts.map +1 -0
  73. package/dist/internal/policy.js +124 -0
  74. package/dist/internal/policy.js.map +1 -0
  75. package/dist/internal/read.js +39 -0
  76. package/dist/internal/read.js.map +1 -0
  77. package/dist/internal/resource.js +115 -0
  78. package/dist/internal/resource.js.map +1 -0
  79. package/dist/internal/rule.d.ts +26 -0
  80. package/dist/internal/rule.d.ts.map +1 -0
  81. package/dist/internal/rule.js +103 -0
  82. package/dist/internal/rule.js.map +1 -0
  83. package/dist/internal/search.d.ts +22 -0
  84. package/dist/internal/search.d.ts.map +1 -0
  85. package/dist/internal/search.js +274 -0
  86. package/dist/internal/search.js.map +1 -0
  87. package/dist/internal/suggest.js +46 -0
  88. package/dist/internal/suggest.js.map +1 -0
  89. package/package.json +3 -4
  90. package/src/Authorizer.ts +67 -583
  91. package/src/Condition.ts +339 -2095
  92. package/src/Decision.ts +6 -5
  93. package/src/Domain.ts +12 -69
  94. package/src/HttpApi.ts +72 -10
  95. package/src/Limits.ts +132 -0
  96. package/src/Policy.ts +391 -2772
  97. package/src/Resource.ts +7 -1548
  98. package/src/index.ts +3 -9
  99. package/src/internal/algebra.ts +482 -0
  100. package/src/internal/analysis.ts +478 -0
  101. package/src/internal/authorizer.ts +418 -0
  102. package/src/internal/bind.ts +648 -0
  103. package/src/internal/catalog.ts +534 -0
  104. package/src/internal/decide.ts +402 -0
  105. package/src/internal/domain.ts +66 -0
  106. package/src/internal/json.ts +260 -0
  107. package/src/internal/limits.ts +78 -0
  108. package/src/internal/nodes.ts +404 -0
  109. package/src/internal/patterns.ts +211 -0
  110. package/src/internal/policy-json.ts +487 -0
  111. package/src/internal/policy-nodes.ts +207 -0
  112. package/src/internal/policy.ts +264 -0
  113. package/src/internal/read.ts +55 -0
  114. package/src/internal/resource.ts +180 -0
  115. package/src/internal/rule.ts +196 -0
  116. package/src/internal/search.ts +541 -0
  117. package/src/internal/suggest.ts +104 -0
  118. package/dist/Principal.d.ts +0 -35
  119. package/dist/Principal.d.ts.map +0 -1
  120. package/dist/Principal.js +0 -37
  121. package/dist/Principal.js.map +0 -1
  122. package/dist/Rule.d.ts +0 -275
  123. package/dist/Rule.d.ts.map +0 -1
  124. package/dist/Rule.js +0 -257
  125. package/dist/Rule.js.map +0 -1
  126. package/src/Principal.ts +0 -41
  127. package/src/Rule.ts +0 -683
package/src/Principal.ts DELETED
@@ -1,41 +0,0 @@
1
- /**
2
- * A principal is the one who asks to do an act. It may be a person, an API
3
- * token, or another service.
4
- *
5
- * A principal is a Schema. Make it with {@link Class}, in the same way as
6
- * `Schema.Class`, or use any `Schema.Struct`. Conditions can use the fields of
7
- * the principal. See `Condition.Builder.principal`.
8
- *
9
- * @stability stable
10
- * @since 0.3.0
11
- */
12
- import * as Schema from "effect/Schema"
13
-
14
- /**
15
- * Any Schema that can be a principal. Its type must be an object.
16
- *
17
- * @stability stable
18
- * @category models
19
- * @since 0.3.0
20
- */
21
- export type Any = Schema.Top & { readonly Type: object }
22
-
23
- /**
24
- * Makes a principal as a Schema class. It is the same as `Schema.Class`.
25
- *
26
- * @example
27
- * ```ts
28
- * import { Schema } from "effect"
29
- * import { Principal } from "@nmnmcc/ability"
30
- *
31
- * class Caller extends Principal.Class<Caller>("Caller")({
32
- * userId: Schema.String,
33
- * organizationId: Schema.String,
34
- * }) {}
35
- * ```
36
- *
37
- * @stability stable
38
- * @category constructors
39
- * @since 0.3.0
40
- */
41
- export const Class: typeof Schema.Class = Schema.Class
package/src/Rule.ts DELETED
@@ -1,683 +0,0 @@
1
- /**
2
- * A rule says that a principal may (`Allow`) or may not (`Deny`) do some
3
- * actions on some resources. A rule may have:
4
- * - a condition, which must be true for the rule to apply;
5
- * - a list of fields, for rules about parts of a resource;
6
- * - a reason, an id, and extra data (`meta`).
7
- *
8
- * Rules do not change, and two rules with the same data are equal. A rule
9
- * works in a generator, so you can write `yield* _.allow(...)` inside
10
- * `Policy.define`.
11
- *
12
- * The action `"*"` means every action. The resource `"*"` means every
13
- * resource.
14
- *
15
- * Field names in a rule may have patterns. `*` stands for any part of one
16
- * field name, and `**` stands for any number of parts. So `address.*` covers
17
- * `address.city`, and `**` covers every field.
18
- *
19
- * @stability stable
20
- * @since 0.3.0
21
- */
22
- import * as Arr from "effect/Array"
23
- import * as Data from "effect/Data"
24
- import * as HashSet from "effect/HashSet"
25
- import type * as Pipeable from "effect/Pipeable"
26
- import * as Condition from "./Condition.ts"
27
- import type * as Domain from "./Domain.ts"
28
- import * as Resource from "./Resource.ts"
29
-
30
- /**
31
- * A rule. `E`, `R` and `O` come from its condition. See
32
- * `Condition.Condition`. `D` is the domain of the rule. A predicate reads the
33
- * values and the principal of its domain, so a rule may go only into a policy
34
- * of its own domain. A rule from {@link make} has the domain `any`. `I` is
35
- * what the predicates of its condition read. See `Condition.Reads`.
36
- *
37
- * @stability stable
38
- * @category models
39
- * @since 0.3.0
40
- */
41
- export interface Rule<E = never, R = never, O = never, D = any, I = unknown>
42
- extends Shape<E, R, O, I>, Pipeable.Pipeable {
43
- readonly "~Rule": Condition.Variance<E, R, O>
44
- readonly "~Domain": (domain: D) => D
45
- [Symbol.iterator](): Generator<
46
- Rule<E, R, O, D, I>,
47
- Rule<E, R, O, D, I>,
48
- unknown
49
- >
50
- }
51
-
52
- /**
53
- * The data of a rule.
54
- *
55
- * @stability stable
56
- * @category models
57
- * @since 1.0.0
58
- */
59
- export interface Shape<E = never, R = never, O = never, I = unknown> {
60
- readonly _tag: "Allow" | "Deny"
61
- readonly actions: ReadonlyArray<string>
62
- readonly resources: ReadonlyArray<string>
63
- readonly when?: Condition.Condition<E, R, O, I>
64
- readonly fields?: ReadonlyArray<string>
65
- readonly reason?: string
66
- readonly id?: string
67
- readonly meta?: unknown
68
- /**
69
- * The domain of the builder that made the rule. Two domains may have the
70
- * same types but other paths, so the types can not tell them apart. It is
71
- * not in the JSON form of a rule.
72
- */
73
- readonly domain?: Domain.Any
74
- }
75
-
76
- /**
77
- * Any rule.
78
- *
79
- * @stability stable
80
- * @category models
81
- * @since 0.3.0
82
- */
83
- // Every rule fits `never` as what it reads, and a condition taken from such a
84
- // rule fits no rule that reads a value or a principal.
85
- export type Any = Rule<any, any, any, any, never>
86
-
87
- /**
88
- * The error type of a rule.
89
- *
90
- * @stability stable
91
- * @category utility types
92
- * @since 0.3.0
93
- */
94
- export type Error<A> = A extends {
95
- readonly "~Rule"?: Condition.Variance<infer E, infer _R, infer _O>
96
- }
97
- ? E
98
- : never
99
-
100
- /**
101
- * The service type of a rule.
102
- *
103
- * @stability stable
104
- * @category utility types
105
- * @since 0.3.0
106
- */
107
- export type Services<A> = A extends {
108
- readonly "~Rule"?: Condition.Variance<infer _E, infer R, infer _O>
109
- }
110
- ? R
111
- : never
112
-
113
- /**
114
- * `Condition.Opaque` when a rule has a predicate.
115
- *
116
- * @stability stable
117
- * @category utility types
118
- * @since 0.3.0
119
- */
120
- export type OpaqueOf<A> = A extends {
121
- readonly "~Rule"?: Condition.Variance<infer _E, infer _R, infer O>
122
- }
123
- ? O
124
- : never
125
-
126
- /**
127
- * A resource in a rule: a kind name, a resource of the domain, or `"*"`.
128
- *
129
- * @stability stable
130
- * @category models
131
- * @since 0.3.0
132
- */
133
- export type Target<D> = Domain.Kind<D> | Domain.Resources<D> | "*"
134
-
135
- /**
136
- * The kind name of a target.
137
- *
138
- * @stability stable
139
- * @category utility types
140
- * @since 0.3.0
141
- */
142
- export type KindOf<T> = T extends string ? T : Resource.Kind<T>
143
-
144
- /**
145
- * The actions that a rule may use for the kinds `K`.
146
- *
147
- * @stability stable
148
- * @category utility types
149
- * @since 0.3.0
150
- */
151
- export type ActionFor<D, K> =
152
- | "*"
153
- | ("*" extends K
154
- ? Domain.AnyAction<D>
155
- : Domain.ActionOf<D, Extract<K, string>>)
156
-
157
- /**
158
- * The targets in the resource argument of a rule: the one target, or the
159
- * targets of a list.
160
- *
161
- * @stability stable
162
- * @category utility types
163
- * @since 1.0.0
164
- */
165
- export type TargetOf<Res> = Res extends ReadonlyArray<infer X> ? X : Res
166
-
167
- /**
168
- * The actions that a rule may use for its resource argument. A list of
169
- * resources may use an action of any of them. One target with a union type
170
- * may use only an action of every kind in the union, because its value may be
171
- * any one of them. When the argument may be a list or one target, the rule
172
- * may use only an action that each of these shapes allows.
173
- *
174
- * @stability stable
175
- * @category utility types
176
- * @since 1.0.0
177
- */
178
- export type ActionForTargets<D, Res> = (
179
- Res extends ReadonlyArray<infer X>
180
- ? (action: ActionFor<D, KindOf<X>>) => void
181
- : (action: ActionForTarget<D, Res>) => void
182
- ) extends (action: infer A) => void
183
- ? A
184
- : never
185
-
186
- /** The actions for one target that is not a list. */
187
- type ActionForTarget<D, Res> =
188
- | "*"
189
- | ("*" extends KindOf<Res>
190
- ? Domain.AnyAction<D>
191
- : Domain.CommonAction<D, Extract<KindOf<Res>, string>>)
192
-
193
- /**
194
- * The data type of the kinds `K`. For `"*"` it has no fields.
195
- *
196
- * @stability stable
197
- * @category utility types
198
- * @since 0.3.0
199
- */
200
- export type DataFor<D, K> = "*" extends K
201
- ? {}
202
- : Domain.DataOf<D, Extract<K, string>>
203
-
204
- /**
205
- * The value type of the kinds `K`.
206
- *
207
- * @stability stable
208
- * @category utility types
209
- * @since 0.3.0
210
- */
211
- export type ValueFor<D, K> = "*" extends K
212
- ? Domain.AnyValue<D>
213
- : Domain.ValueOf<D, Extract<K, string>>
214
-
215
- /**
216
- * A field name or a field pattern of the kinds `K`.
217
- *
218
- * @stability stable
219
- * @category models
220
- * @since 0.3.0
221
- */
222
- export type FieldFor<D, K> =
223
- Condition.FieldSelector<DataFor<D, K>> | `${string}*${string}`
224
-
225
- /**
226
- * A condition that a rule for the kinds `K` may have. Its predicates may read
227
- * only the values of these kinds and the principal of the domain.
228
- *
229
- * @stability stable
230
- * @category models
231
- * @since 1.0.0
232
- */
233
- export type ConditionFor<D, K> = Condition.Condition<
234
- any,
235
- any,
236
- any,
237
- Condition.Reads<ValueFor<D, K>, Domain.Principal<D>>
238
- >
239
-
240
- /**
241
- * The options of a rule.
242
- *
243
- * @stability stable
244
- * @category models
245
- * @since 0.3.0
246
- */
247
- export interface Options<D, K, C extends Condition.Any> {
248
- /** The condition. It gets a builder with the types of the resource. */
249
- readonly when?: (
250
- builder: Condition.Builder<
251
- DataFor<D, K>,
252
- ValueFor<D, K>,
253
- Domain.Principal<D>
254
- >,
255
- ) => C & Condition.Checked<C>
256
- readonly fields?: ReadonlyArray<FieldFor<D, K>>
257
- readonly reason?: string
258
- readonly id?: string
259
- readonly meta?: Domain.Meta<D>
260
- }
261
-
262
- type SingleOrArray<A> = A | ReadonlyArray<A>
263
-
264
- /**
265
- * Makes rules with the types of a domain.
266
- *
267
- * @stability stable
268
- * @category models
269
- * @since 0.3.0
270
- */
271
- export interface Builder<D> {
272
- readonly allow: <
273
- const Res extends SingleOrArray<Target<D>>,
274
- const A extends ActionForTargets<D, Res>,
275
- const C extends ConditionFor<D, KindOf<TargetOf<Res>>> =
276
- Condition.Condition,
277
- >(
278
- action: SingleOrArray<A>,
279
- resource: Res,
280
- options?: Options<D, KindOf<TargetOf<Res>>, C>,
281
- ) => Rule<
282
- Condition.Error<C>,
283
- Condition.Services<C>,
284
- Condition.OpaqueOf<C>,
285
- D,
286
- Condition.ReadsOf<C>
287
- >
288
- readonly deny: <
289
- const Res extends SingleOrArray<Target<D>>,
290
- const A extends ActionForTargets<D, Res>,
291
- const C extends ConditionFor<D, KindOf<TargetOf<Res>>> =
292
- Condition.Condition,
293
- >(
294
- action: SingleOrArray<A>,
295
- resource: Res,
296
- options?: Options<D, KindOf<TargetOf<Res>>, C>,
297
- ) => Rule<
298
- Condition.Error<C>,
299
- Condition.Services<C>,
300
- Condition.OpaqueOf<C>,
301
- D,
302
- Condition.ReadsOf<C>
303
- >
304
- }
305
-
306
- /**
307
- * The class of every rule value. A rule works in a generator: it gives
308
- * itself, so `yield* _.allow(...)` gives the rule to `Policy.define`.
309
- */
310
- class RuleData extends Data.Class<Shape<unknown, unknown, unknown, any>> {
311
- *[Symbol.iterator](): Generator<this, this, unknown> {
312
- yield this
313
- return this
314
- }
315
- }
316
-
317
- /**
318
- * Makes a rule from its data. It does not check the data against a domain.
319
- * Use {@link builder} for rules with types, or `Policy.schema` for rules from
320
- * JSON.
321
- *
322
- * @stability stable
323
- * @category constructors
324
- * @since 1.0.0
325
- */
326
- export const make = <E = never, R = never, O = never, I = unknown>(
327
- data: Shape<E, R, O, I>,
328
- ): Rule<E, R, O, any, I> => {
329
- // A missing option is left out, so two rules with the same data are equal.
330
- const shape: Shape<E, R, O, I> = {
331
- _tag: data._tag,
332
- actions: [...data.actions],
333
- resources: [...data.resources],
334
- ...(data.when === undefined ? {} : { when: data.when }),
335
- ...(data.fields === undefined ? {} : { fields: [...data.fields] }),
336
- ...(data.reason === undefined ? {} : { reason: data.reason }),
337
- ...(data.id === undefined ? {} : { id: data.id }),
338
- ...(data.meta === undefined ? {} : { meta: data.meta }),
339
- ...(data.domain === undefined ? {} : { domain: data.domain }),
340
- }
341
- return new RuleData(shape) as never
342
- }
343
-
344
- const toArray = <A>(value: A | ReadonlyArray<A>): ReadonlyArray<A> =>
345
- Array.isArray(value) ? value : [value as A]
346
-
347
- const kindName = (target: unknown): string =>
348
- Resource.isResource(target)
349
- ? Resource.descriptor(target).kind
350
- : (target as string)
351
-
352
- /**
353
- * Gives the domain of the builder that made a rule. A rule from {@link make}
354
- * has none, unless its data gives one.
355
- *
356
- * @stability stable
357
- * @category getters
358
- * @since 1.0.0
359
- */
360
- export const domainOf = (rule: Any): Domain.Any | undefined => rule.domain
361
-
362
- const makeFromOptions =
363
- (tag: "Allow" | "Deny", domain: Domain.Any) =>
364
- (
365
- action: string | ReadonlyArray<string>,
366
- resource: unknown,
367
- options?: {
368
- readonly when?: (builder: never) => Condition.Any
369
- readonly fields?: ReadonlyArray<string>
370
- readonly reason?: string
371
- readonly id?: string
372
- readonly meta?: unknown
373
- },
374
- ): Any =>
375
- make({
376
- _tag: tag,
377
- actions: toArray(action),
378
- resources: Arr.map(toArray(resource), kindName),
379
- ...(options?.when === undefined
380
- ? {}
381
- : { when: options.when(Condition.builder() as never) }),
382
- ...(options?.fields === undefined ? {} : { fields: options.fields }),
383
- ...(options?.reason === undefined ? {} : { reason: options.reason }),
384
- ...(options?.id === undefined ? {} : { id: options.id }),
385
- ...(options?.meta === undefined ? {} : { meta: options.meta }),
386
- domain,
387
- })
388
-
389
- /**
390
- * Makes a rule builder for a domain.
391
- *
392
- * @example
393
- * ```ts
394
- * const _ = Rule.builder(App)
395
- * const rule = _.allow("read", Document, {
396
- * when: ($) => $.eq("ownerId", $.principal("id")),
397
- * })
398
- * ```
399
- *
400
- * @stability stable
401
- * @category constructors
402
- * @since 0.3.0
403
- */
404
- export const builder = <D extends Domain.Any>(domain: D): Builder<D> =>
405
- ({
406
- allow: makeFromOptions("Allow", domain),
407
- deny: makeFromOptions("Deny", domain),
408
- }) as never
409
-
410
- /**
411
- * Tests if a value is a rule.
412
- *
413
- * @stability stable
414
- * @category guards
415
- * @since 0.3.0
416
- */
417
- export const isRule = (u: unknown): u is Any => u instanceof RuleData
418
-
419
- /**
420
- * A part of a field pattern: `"**"`, `"*"`, or one plain Unicode code point.
421
- */
422
- type Token = string
423
-
424
- /**
425
- * Gives the parts of a pattern. It reads the pattern by Unicode code points,
426
- * as `run` reads the field. One star is `*`. Two or more stars next to each
427
- * other match what `**` matches, so they are one part, `**`. So two star parts
428
- * are never next to each other.
429
- */
430
- const tokensOf = (pattern: string): ReadonlyArray<Token> =>
431
- Arr.map(normalize(pattern).match(/\*+|[^*]/gu) ?? [], (part) =>
432
- part.startsWith("*") ? (part.length === 1 ? "*" : "**") : part,
433
- )
434
-
435
- /**
436
- * Puts each run of whole `**` parts together into one part. `a.**.**` matches
437
- * what `a.**` matches, and with one part the rule of {@link skips} for no
438
- * parts covers the whole run.
439
- */
440
- const normalize = (pattern: string): string =>
441
- Arr.dedupeAdjacentWith(
442
- pattern.split("."),
443
- (left, right) => isGlobstar(left) && isGlobstar(right),
444
- ).join(".")
445
-
446
- /** Tests if a part of a pattern is only stars, two or more of them. */
447
- const isGlobstar = (part: string): boolean => /^\*{2,}$/u.test(part)
448
-
449
- const isStar = (token: Token | undefined): boolean =>
450
- token === "*" || token === "**"
451
-
452
- type States = HashSet.HashSet<number>
453
-
454
- /**
455
- * Gives the places that a match may reach from `state` with no input. A star
456
- * may match nothing. A `**` that is a whole part may also match no parts, so
457
- * the dot next to it may be left out too: `**.secret` and `settings.**.secret`
458
- * match `secret` and `settings.secret`, and `settings.**` matches `settings`.
459
- */
460
- const skips = (
461
- tokens: ReadonlyArray<Token>,
462
- state: number,
463
- ): ReadonlyArray<number> => {
464
- const token = tokens[state]
465
- const startsPart = state === 0 || tokens[state - 1] === "."
466
- return [
467
- ...(isStar(token) ? [state + 1] : []),
468
- ...(token === "**" && startsPart && tokens[state + 1] === "."
469
- ? [state + 2]
470
- : []),
471
- ...(token === "." &&
472
- tokens[state + 1] === "**" &&
473
- state + 2 === tokens.length &&
474
- state > 0
475
- ? [state + 2]
476
- : []),
477
- ]
478
- }
479
-
480
- /**
481
- * Adds the places that a match may reach with no input. See {@link skips}.
482
- * Each step reads only the places that the step before found, so each place
483
- * is read one time.
484
- */
485
- const close = (
486
- tokens: ReadonlyArray<Token>,
487
- states: States,
488
- found: States = states,
489
- ): States => {
490
- const after = HashSet.filter(
491
- HashSet.fromIterable(
492
- Arr.flatMap(Array.from(found), (state) => skips(tokens, state)),
493
- ),
494
- (state) => !HashSet.has(states, state),
495
- )
496
- return HashSet.size(after) === 0
497
- ? states
498
- : close(tokens, HashSet.union(states, after), after)
499
- }
500
-
501
- /**
502
- * Gives the places in the pattern after one more character of the input. A
503
- * star that takes the character stays at its place. It has taken part of a
504
- * name, so it may not match no parts any more: only the place after it is
505
- * added, with no other skips from {@link skips}.
506
- */
507
- const step = (
508
- tokens: ReadonlyArray<Token>,
509
- states: States,
510
- char: string,
511
- ): States => {
512
- const list = Array.from(states)
513
- const stars = Arr.filter(list, (state) => {
514
- const token = tokens[state]
515
- return token === "**" || (token === "*" && char !== ".")
516
- })
517
- const moved = Arr.flatMap(list, (state) =>
518
- !isStar(tokens[state]) && tokens[state] === char ? [state + 1] : [],
519
- )
520
- return HashSet.union(
521
- HashSet.fromIterable(stars),
522
- close(
523
- tokens,
524
- HashSet.fromIterable([...moved, ...Arr.map(stars, (state) => state + 1)]),
525
- ),
526
- )
527
- }
528
-
529
- /**
530
- * Gives every place in the pattern that a match may have reached after
531
- * `input`. It keeps a set of places, and it does not go back, so the time is
532
- * at most the length of the input times the length of the pattern. A regular
533
- * expression may take a very long time on a pattern with many stars.
534
- */
535
- const run = (tokens: ReadonlyArray<Token>, input: string): States =>
536
- Arr.reduce(
537
- Array.from(input),
538
- close(tokens, HashSet.make(0)),
539
- (states, char) =>
540
- HashSet.size(states) === 0 ? states : step(tokens, states, char),
541
- )
542
-
543
- /**
544
- * Tests if a field name matches a field name or a field pattern of a rule.
545
- *
546
- * @stability stable
547
- * @category predicates
548
- * @since 0.3.0
549
- */
550
- export const matchesField = (pattern: string, field: string): boolean => {
551
- if (!pattern.includes("*")) {
552
- return pattern === field
553
- }
554
- const tokens = tokensOf(pattern)
555
- return HashSet.has(run(tokens, field), tokens.length)
556
- }
557
-
558
- /**
559
- * Tests if a field pattern may match a field inside `field`, such as
560
- * `settings.secret` inside `settings`, or `settings.` for the empty key of a
561
- * record.
562
- *
563
- * @stability stable
564
- * @category predicates
565
- * @since 1.0.0
566
- */
567
- export const matchesInside = (pattern: string, field: string): boolean =>
568
- // The name of a part may be empty, as a key of a record may be. So a match
569
- // that ends just after the dot names a part too.
570
- HashSet.size(run(tokensOf(pattern), `${field}.`)) > 0
571
-
572
- /**
573
- * Tests if a field pattern may match a field of a kind: a field in its list,
574
- * a path that the Schema has, such as a key of a record, or a getter. See
575
- * `Resource.hasMatchingPath` and `Resource.hasMatchingGetter`.
576
- */
577
- const fitsSomeField = (
578
- pattern: string,
579
- descriptor: Resource.Descriptor,
580
- ): boolean =>
581
- Arr.some(descriptor.fieldPaths, (field) => matchesField(pattern, field)) ||
582
- // A pattern with more parts than a field path may have fits no other
583
- // field, and it is not cut into lists of parts.
584
- (pattern.split(".", maxPatternParts + 1).length <= maxPatternParts &&
585
- Arr.some(
586
- partLists(pattern),
587
- (parts) =>
588
- Resource.hasMatchingPath(descriptor.schema, parts, matchesField) ||
589
- Resource.hasMatchingGetter(descriptor.schema, parts, matchesField),
590
- ))
591
-
592
- /** The most parts of a pattern that the walk of the Schema takes. */
593
- const maxPatternParts = 256
594
-
595
- /**
596
- * The lists of parts that a pattern may stand for. A part that is only stars
597
- * is `**`. One `**` inside a part, as in `**value`, may take the dots of more
598
- * parts, so such a part gives two lists: one with the part as it is, with
599
- * `*` for `**`, and one with the part cut at the stars, as in `*`, `**`,
600
- * `*value`. A part with two such runs stays as it is, with `*` for `**`, so
601
- * it may miss a field but never fits one too many. The number of lists is
602
- * kept small.
603
- */
604
- const partLists = (pattern: string): ReadonlyArray<ReadonlyArray<string>> =>
605
- Arr.take(
606
- Arr.reduce(
607
- normalize(pattern).split("."),
608
- [[]] as ReadonlyArray<ReadonlyArray<string>>,
609
- (lists, part) => {
610
- const choices: ReadonlyArray<ReadonlyArray<string>> = isGlobstar(part)
611
- ? [["**"]]
612
- : // Only one run of stars: with two, the cut would leave out the
613
- // text between them, and the lists would fit more fields.
614
- (part.match(/\*{2,}/gu) ?? []).length === 1
615
- ? [
616
- [part.replace(/\*{2,}/gu, "*")],
617
- [
618
- `${part.slice(0, part.indexOf("**"))}*`,
619
- "**",
620
- `*${part.slice(part.lastIndexOf("**") + 2)}`,
621
- ],
622
- ]
623
- : [[part]]
624
- return Arr.take(
625
- Arr.flatMap(lists, (list) =>
626
- Arr.map(choices, (choice) => [...list, ...choice]),
627
- ),
628
- 16,
629
- )
630
- },
631
- ),
632
- 16,
633
- )
634
-
635
- /**
636
- * Tests the parts of a rule that do not need the resource value: the action,
637
- * the kind and the field.
638
- *
639
- * A rule with an empty list of fields covers no request. A rule with fields
640
- * covers a request with no field only when it is an allow rule. So "may update some fields" is a yes. A deny rule for some fields does
641
- * not stop a request for the whole value. With the `descriptor` of the kind,
642
- * such an allow rule covers the request only when one of its patterns may
643
- * match a field of the kind. A grant of fields that the kind does not have is
644
- * not a grant of some fields.
645
- *
646
- * @stability stable
647
- * @category predicates
648
- * @since 1.0.0
649
- */
650
- export const matchesTarget = (
651
- rule: Any,
652
- target: {
653
- readonly action: string
654
- readonly kind: string
655
- readonly field?: string | undefined
656
- readonly descriptor?: Resource.Descriptor | undefined
657
- },
658
- ): boolean => {
659
- if (!rule.actions.includes("*") && !rule.actions.includes(target.action)) {
660
- return false
661
- }
662
- if (!rule.resources.includes("*") && !rule.resources.includes(target.kind)) {
663
- return false
664
- }
665
- if (rule.fields === undefined) {
666
- return true
667
- }
668
- // A rule for no fields is a rule for nothing. A grant with its last field
669
- // taken away must not cover the whole value.
670
- if (rule.fields.length === 0) {
671
- return false
672
- }
673
- if (target.field === undefined) {
674
- const descriptor = target.descriptor
675
- return (
676
- rule._tag === "Allow" &&
677
- (descriptor === undefined ||
678
- Arr.some(rule.fields, (pattern) => fitsSomeField(pattern, descriptor)))
679
- )
680
- }
681
- const field = target.field
682
- return rule.fields.some((pattern) => matchesField(pattern, field))
683
- }