@nmnmcc/ability 0.3.0 → 1.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 +199 -31
- package/dist/Authorizer.d.ts +64 -39
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js +291 -6
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +444 -65
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +1007 -25
- package/dist/Condition.js.map +1 -1
- package/dist/Domain.d.ts +27 -17
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +52 -10
- package/dist/Domain.js.map +1 -1
- package/dist/HttpApi.d.ts +84 -40
- package/dist/HttpApi.d.ts.map +1 -1
- package/dist/HttpApi.js +103 -13
- package/dist/HttpApi.js.map +1 -1
- package/dist/Policy.d.ts +314 -56
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +1215 -38
- package/dist/Policy.js.map +1 -1
- package/dist/Principal.js +2 -2
- package/dist/Principal.js.map +1 -1
- package/dist/Resource.d.ts +171 -20
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +788 -15
- package/dist/Resource.js.map +1 -1
- package/dist/Rule.d.ts +115 -29
- package/dist/Rule.d.ts.map +1 -1
- package/dist/Rule.js +219 -12
- package/dist/Rule.js.map +1 -1
- package/dist/index.js +4 -4
- package/package.json +2 -2
- package/src/Authorizer.ts +625 -61
- package/src/Condition.ts +2403 -112
- package/src/Domain.ts +80 -23
- package/src/HttpApi.ts +225 -25
- package/src/Policy.ts +2836 -96
- package/src/Principal.ts +2 -2
- package/src/Resource.ts +1544 -29
- package/src/Rule.ts +463 -45
- package/dist/internal/authorizer.d.ts +0 -28
- package/dist/internal/authorizer.d.ts.map +0 -1
- package/dist/internal/authorizer.js +0 -153
- package/dist/internal/authorizer.js.map +0 -1
- package/dist/internal/condition.d.ts +0 -11
- package/dist/internal/condition.d.ts.map +0 -1
- package/dist/internal/condition.js +0 -393
- package/dist/internal/condition.js.map +0 -1
- package/dist/internal/domain.js +0 -34
- package/dist/internal/domain.js.map +0 -1
- package/dist/internal/httpApi.js +0 -41
- package/dist/internal/httpApi.js.map +0 -1
- package/dist/internal/policy.js +0 -524
- package/dist/internal/policy.js.map +0 -1
- package/dist/internal/resource.js +0 -66
- package/dist/internal/resource.js.map +0 -1
- package/dist/internal/rule.js +0 -131
- package/dist/internal/rule.js.map +0 -1
- package/src/internal/authorizer.ts +0 -271
- package/src/internal/condition.ts +0 -607
- package/src/internal/domain.ts +0 -56
- package/src/internal/httpApi.ts +0 -84
- package/src/internal/policy.ts +0 -815
- package/src/internal/resource.ts +0 -98
- package/src/internal/rule.ts +0 -203
package/src/Rule.ts
CHANGED
|
@@ -19,49 +19,58 @@
|
|
|
19
19
|
* @stability stable
|
|
20
20
|
* @since 0.3.0
|
|
21
21
|
*/
|
|
22
|
-
import
|
|
23
|
-
import
|
|
22
|
+
import * as Arr from "effect/Array"
|
|
23
|
+
import * as Data from "effect/Data"
|
|
24
|
+
import * as HashSet from "effect/HashSet"
|
|
24
25
|
import type * as Pipeable from "effect/Pipeable"
|
|
25
|
-
import
|
|
26
|
+
import * as Condition from "./Condition.ts"
|
|
26
27
|
import type * as Domain from "./Domain.ts"
|
|
27
|
-
import * as
|
|
28
|
-
import type * as Resource from "./Resource.ts"
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @stability stable
|
|
32
|
-
* @category type IDs
|
|
33
|
-
* @since 0.3.0
|
|
34
|
-
*/
|
|
35
|
-
export const TypeId: "~@nmnmcc/ability/Rule" = internal.TypeId
|
|
28
|
+
import * as Resource from "./Resource.ts"
|
|
36
29
|
|
|
37
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
|
+
*
|
|
38
37
|
* @stability stable
|
|
39
|
-
* @category
|
|
38
|
+
* @category models
|
|
40
39
|
* @since 0.3.0
|
|
41
40
|
*/
|
|
42
|
-
export
|
|
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
|
+
}
|
|
43
51
|
|
|
44
52
|
/**
|
|
45
|
-
*
|
|
46
|
-
* `Condition.Condition`.
|
|
53
|
+
* The data of a rule.
|
|
47
54
|
*
|
|
48
55
|
* @stability stable
|
|
49
56
|
* @category models
|
|
50
|
-
* @since 0.
|
|
57
|
+
* @since 1.0.0
|
|
51
58
|
*/
|
|
52
|
-
export interface
|
|
53
|
-
extends Equal.Equal, Inspectable.Inspectable, Pipeable.Pipeable {
|
|
54
|
-
readonly [TypeId]: TypeId
|
|
59
|
+
export interface Shape<E = never, R = never, O = never, I = unknown> {
|
|
55
60
|
readonly _tag: "Allow" | "Deny"
|
|
56
61
|
readonly actions: ReadonlyArray<string>
|
|
57
62
|
readonly resources: ReadonlyArray<string>
|
|
58
|
-
readonly when?: Condition.
|
|
63
|
+
readonly when?: Condition.Condition<E, R, O, I>
|
|
59
64
|
readonly fields?: ReadonlyArray<string>
|
|
60
65
|
readonly reason?: string
|
|
61
66
|
readonly id?: string
|
|
62
67
|
readonly meta?: unknown
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
/**
|
|
@@ -71,7 +80,9 @@ export interface Rule<E = never, R = never, O = never>
|
|
|
71
80
|
* @category models
|
|
72
81
|
* @since 0.3.0
|
|
73
82
|
*/
|
|
74
|
-
|
|
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>
|
|
75
86
|
|
|
76
87
|
/**
|
|
77
88
|
* The error type of a rule.
|
|
@@ -143,6 +154,42 @@ export type ActionFor<D, K> =
|
|
|
143
154
|
? Domain.AnyAction<D>
|
|
144
155
|
: Domain.ActionOf<D, Extract<K, string>>)
|
|
145
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
|
+
|
|
146
193
|
/**
|
|
147
194
|
* The data type of the kinds `K`. For `"*"` it has no fields.
|
|
148
195
|
*
|
|
@@ -173,7 +220,22 @@ export type ValueFor<D, K> = "*" extends K
|
|
|
173
220
|
* @since 0.3.0
|
|
174
221
|
*/
|
|
175
222
|
export type FieldFor<D, K> =
|
|
176
|
-
Condition.
|
|
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
|
+
>
|
|
177
239
|
|
|
178
240
|
/**
|
|
179
241
|
* The options of a rule.
|
|
@@ -190,7 +252,7 @@ export interface Options<D, K, C extends Condition.Any> {
|
|
|
190
252
|
ValueFor<D, K>,
|
|
191
253
|
Domain.Principal<D>
|
|
192
254
|
>,
|
|
193
|
-
) => C
|
|
255
|
+
) => C & Condition.Checked<C>
|
|
194
256
|
readonly fields?: ReadonlyArray<FieldFor<D, K>>
|
|
195
257
|
readonly reason?: string
|
|
196
258
|
readonly id?: string
|
|
@@ -208,33 +270,130 @@ type SingleOrArray<A> = A | ReadonlyArray<A>
|
|
|
208
270
|
*/
|
|
209
271
|
export interface Builder<D> {
|
|
210
272
|
readonly allow: <
|
|
211
|
-
const
|
|
212
|
-
const A extends
|
|
213
|
-
const C extends
|
|
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,
|
|
214
277
|
>(
|
|
215
278
|
action: SingleOrArray<A>,
|
|
216
|
-
resource:
|
|
217
|
-
options?: Options<D, KindOf<
|
|
218
|
-
) => Rule<
|
|
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
|
+
>
|
|
219
288
|
readonly deny: <
|
|
220
|
-
const
|
|
221
|
-
const A extends
|
|
222
|
-
const C extends
|
|
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,
|
|
223
293
|
>(
|
|
224
294
|
action: SingleOrArray<A>,
|
|
225
|
-
resource:
|
|
226
|
-
options?: Options<D, KindOf<
|
|
227
|
-
) => Rule<
|
|
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
|
+
>
|
|
228
304
|
}
|
|
229
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
|
+
|
|
230
389
|
/**
|
|
231
390
|
* Makes a rule builder for a domain.
|
|
232
391
|
*
|
|
233
392
|
* @example
|
|
234
393
|
* ```ts
|
|
235
394
|
* const _ = Rule.builder(App)
|
|
236
|
-
* const rule = _.allow("read",
|
|
237
|
-
* when: ($) => $.eq("
|
|
395
|
+
* const rule = _.allow("read", Document, {
|
|
396
|
+
* when: ($) => $.eq("ownerId", $.principal("id")),
|
|
238
397
|
* })
|
|
239
398
|
* ```
|
|
240
399
|
*
|
|
@@ -242,8 +401,11 @@ export interface Builder<D> {
|
|
|
242
401
|
* @category constructors
|
|
243
402
|
* @since 0.3.0
|
|
244
403
|
*/
|
|
245
|
-
export const builder
|
|
246
|
-
|
|
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
|
|
247
409
|
|
|
248
410
|
/**
|
|
249
411
|
* Tests if a value is a rule.
|
|
@@ -252,7 +414,131 @@ export const builder: <D extends Domain.Any>(domain: D) => Builder<D> =
|
|
|
252
414
|
* @category guards
|
|
253
415
|
* @since 0.3.0
|
|
254
416
|
*/
|
|
255
|
-
export const isRule
|
|
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
|
+
)
|
|
256
542
|
|
|
257
543
|
/**
|
|
258
544
|
* Tests if a field name matches a field name or a field pattern of a rule.
|
|
@@ -261,5 +547,137 @@ export const isRule: (u: unknown) => u is Any = internal.isRule
|
|
|
261
547
|
* @category predicates
|
|
262
548
|
* @since 0.3.0
|
|
263
549
|
*/
|
|
264
|
-
export const matchesField
|
|
265
|
-
|
|
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
|
+
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import "../Domain.js";
|
|
2
|
-
import "../Decision.js";
|
|
3
|
-
import "../Policy.js";
|
|
4
|
-
import "../Authorizer.js";
|
|
5
|
-
import "effect/Effect";
|
|
6
|
-
import "effect/Layer";
|
|
7
|
-
import * as Schema from "effect/Schema";
|
|
8
|
-
declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbidden", {
|
|
9
|
-
readonly action: Schema.String;
|
|
10
|
-
readonly kind: Schema.String;
|
|
11
|
-
readonly field: Schema.optionalKey<Schema.String>;
|
|
12
|
-
readonly reason: Schema.optionalKey<Schema.String>;
|
|
13
|
-
}>, import("effect/Cause").YieldableError>;
|
|
14
|
-
/** @internal */
|
|
15
|
-
export declare class Forbidden extends Forbidden_base {}
|
|
16
|
-
declare const PolicyUnavailable_base: Schema.Class<PolicyUnavailable, Schema.TaggedStruct<"PolicyUnavailable", {
|
|
17
|
-
readonly cause: Schema.Defect;
|
|
18
|
-
}>, import("effect/Cause").YieldableError>;
|
|
19
|
-
/** @internal */
|
|
20
|
-
export declare class PolicyUnavailable extends PolicyUnavailable_base {}
|
|
21
|
-
declare const Pending_base: Schema.Class<Pending, Schema.TaggedStruct<"Pending", {
|
|
22
|
-
readonly ticket: Schema.String;
|
|
23
|
-
readonly reason: Schema.optionalKey<Schema.String>;
|
|
24
|
-
}>, import("effect/Cause").YieldableError>;
|
|
25
|
-
/** @internal */
|
|
26
|
-
export declare class Pending extends Pending_base {}
|
|
27
|
-
export {};
|
|
28
|
-
//# sourceMappingURL=authorizer.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"authorizer.d.ts","names":[],"sources":["../../src/internal/authorizer.ts"],"mappings":";;;;;;;;;;;;;;qBAqBa,kBAAkB;;;;;qBAUlB,0BAA0B;;;;;;qBAK1B,gBAAgB"}
|