@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.
- package/README.md +112 -69
- package/dist/Authorizer.d.ts +43 -47
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js +5 -290
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +164 -332
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +75 -890
- package/dist/Condition.js.map +1 -1
- package/dist/Decision.d.ts +13 -13
- package/dist/Decision.d.ts.map +1 -1
- package/dist/Decision.js.map +1 -1
- package/dist/Domain.d.ts +10 -21
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +4 -62
- package/dist/Domain.js.map +1 -1
- package/dist/HttpApi.d.ts +31 -7
- package/dist/HttpApi.d.ts.map +1 -1
- package/dist/HttpApi.js +25 -8
- package/dist/HttpApi.js.map +1 -1
- package/dist/Limits.d.ts +114 -0
- package/dist/Limits.d.ts.map +1 -0
- package/dist/Limits.js +132 -0
- package/dist/Limits.js.map +1 -0
- package/dist/Policy.d.ts +276 -269
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +73 -1177
- package/dist/Policy.js.map +1 -1
- package/dist/Resource.d.ts +4 -177
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +4 -803
- package/dist/Resource.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +4 -5
- package/dist/internal/algebra.js +267 -0
- package/dist/internal/algebra.js.map +1 -0
- package/dist/internal/analysis.js +245 -0
- package/dist/internal/analysis.js.map +1 -0
- package/dist/internal/authorizer.d.ts +30 -0
- package/dist/internal/authorizer.d.ts.map +1 -0
- package/dist/internal/authorizer.js +221 -0
- package/dist/internal/authorizer.js.map +1 -0
- package/dist/internal/bind.js +311 -0
- package/dist/internal/bind.js.map +1 -0
- package/dist/internal/catalog.d.ts +56 -0
- package/dist/internal/catalog.d.ts.map +1 -0
- package/dist/internal/catalog.js +292 -0
- package/dist/internal/catalog.js.map +1 -0
- package/dist/internal/decide.js +166 -0
- package/dist/internal/decide.js.map +1 -0
- package/dist/internal/domain.js +32 -0
- package/dist/internal/domain.js.map +1 -0
- package/dist/internal/json.js +134 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/internal/limits.d.ts +14 -0
- package/dist/internal/limits.d.ts.map +1 -0
- package/dist/internal/limits.js +43 -0
- package/dist/internal/limits.js.map +1 -0
- package/dist/internal/nodes.d.ts +121 -0
- package/dist/internal/nodes.d.ts.map +1 -0
- package/dist/internal/nodes.js +277 -0
- package/dist/internal/nodes.js.map +1 -0
- package/dist/internal/patterns.js +110 -0
- package/dist/internal/patterns.js.map +1 -0
- package/dist/internal/policy-json.js +265 -0
- package/dist/internal/policy-json.js.map +1 -0
- package/dist/internal/policy-nodes.d.ts +61 -0
- package/dist/internal/policy-nodes.d.ts.map +1 -0
- package/dist/internal/policy-nodes.js +146 -0
- package/dist/internal/policy-nodes.js.map +1 -0
- package/dist/internal/policy.d.ts +42 -0
- package/dist/internal/policy.d.ts.map +1 -0
- package/dist/internal/policy.js +124 -0
- package/dist/internal/policy.js.map +1 -0
- package/dist/internal/read.js +39 -0
- package/dist/internal/read.js.map +1 -0
- package/dist/internal/resource.js +115 -0
- package/dist/internal/resource.js.map +1 -0
- package/dist/internal/rule.d.ts +26 -0
- package/dist/internal/rule.d.ts.map +1 -0
- package/dist/internal/rule.js +103 -0
- package/dist/internal/rule.js.map +1 -0
- package/dist/internal/search.d.ts +22 -0
- package/dist/internal/search.d.ts.map +1 -0
- package/dist/internal/search.js +274 -0
- package/dist/internal/search.js.map +1 -0
- package/dist/internal/suggest.js +46 -0
- package/dist/internal/suggest.js.map +1 -0
- package/package.json +3 -4
- package/src/Authorizer.ts +67 -583
- package/src/Condition.ts +339 -2095
- package/src/Decision.ts +6 -5
- package/src/Domain.ts +12 -69
- package/src/HttpApi.ts +72 -10
- package/src/Limits.ts +132 -0
- package/src/Policy.ts +391 -2772
- package/src/Resource.ts +7 -1548
- package/src/index.ts +3 -9
- package/src/internal/algebra.ts +482 -0
- package/src/internal/analysis.ts +478 -0
- package/src/internal/authorizer.ts +418 -0
- package/src/internal/bind.ts +648 -0
- package/src/internal/catalog.ts +534 -0
- package/src/internal/decide.ts +402 -0
- package/src/internal/domain.ts +66 -0
- package/src/internal/json.ts +260 -0
- package/src/internal/limits.ts +78 -0
- package/src/internal/nodes.ts +404 -0
- package/src/internal/patterns.ts +211 -0
- package/src/internal/policy-json.ts +487 -0
- package/src/internal/policy-nodes.ts +207 -0
- package/src/internal/policy.ts +264 -0
- package/src/internal/read.ts +55 -0
- package/src/internal/resource.ts +180 -0
- package/src/internal/rule.ts +196 -0
- package/src/internal/search.ts +541 -0
- package/src/internal/suggest.ts +104 -0
- package/dist/Principal.d.ts +0 -35
- package/dist/Principal.d.ts.map +0 -1
- package/dist/Principal.js +0 -37
- package/dist/Principal.js.map +0 -1
- package/dist/Rule.d.ts +0 -275
- package/dist/Rule.d.ts.map +0 -1
- package/dist/Rule.js +0 -257
- package/dist/Rule.js.map +0 -1
- package/src/Principal.ts +0 -41
- package/src/Rule.ts +0 -683
package/src/Authorizer.ts
CHANGED
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
* of the current request.
|
|
4
4
|
*
|
|
5
5
|
* You make the service class, in the same way as `Context.Service`. Give the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* domain, and the Effect that gives the principal of the current request. The
|
|
7
|
+
* error type and the service type of the checks come from that Effect:
|
|
8
8
|
*
|
|
9
9
|
* ```ts
|
|
10
|
-
* class Auth extends Authorizer.Service<Auth
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* ) {}
|
|
10
|
+
* class Auth extends Authorizer.Service<Auth>()("app/Auth", {
|
|
11
|
+
* domain: App,
|
|
12
|
+
* principal: Identity,
|
|
13
|
+
* }) {}
|
|
14
14
|
* ```
|
|
15
15
|
*
|
|
16
|
-
* Then give a layer with {@link layer}. It says
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Then give a layer with {@link layer}. It says how to load the policy of a
|
|
17
|
+
* principal. The policy of each principal is loaded once and kept in a cache
|
|
18
|
+
* for a short time.
|
|
19
19
|
*
|
|
20
20
|
* Then use the service in a handler:
|
|
21
21
|
*
|
|
@@ -34,26 +34,15 @@
|
|
|
34
34
|
* @stability stable
|
|
35
35
|
* @since 0.3.0
|
|
36
36
|
*/
|
|
37
|
-
|
|
38
|
-
// principals and deciders to `unknown`. The public types keep them, so the
|
|
39
|
-
// language service check for `unknown` in an Effect is off here.
|
|
40
|
-
// @effect-diagnostics anyUnknownInErrorContext:off
|
|
41
|
-
import * as Arr from "effect/Array"
|
|
42
|
-
import * as Cache from "effect/Cache"
|
|
43
|
-
import * as Context from "effect/Context"
|
|
37
|
+
import type * as Context from "effect/Context"
|
|
44
38
|
import type * as Duration from "effect/Duration"
|
|
45
|
-
import * as Effect from "effect/Effect"
|
|
46
|
-
import * as
|
|
47
|
-
import * as
|
|
48
|
-
import * as
|
|
49
|
-
import * as
|
|
50
|
-
import * as
|
|
51
|
-
import * as
|
|
52
|
-
import * as Condition from "./Condition.ts"
|
|
53
|
-
import * as Decision from "./Decision.ts"
|
|
54
|
-
import * as Domain from "./Domain.ts"
|
|
55
|
-
import * as Policy from "./Policy.ts"
|
|
56
|
-
import * as Resource from "./Resource.ts"
|
|
39
|
+
import type * as Effect from "effect/Effect"
|
|
40
|
+
import type * as Layer from "effect/Layer"
|
|
41
|
+
import type * as Condition from "./Condition.ts"
|
|
42
|
+
import type * as Decision from "./Decision.ts"
|
|
43
|
+
import type * as Domain from "./Domain.ts"
|
|
44
|
+
import * as Internal from "./internal/authorizer.ts"
|
|
45
|
+
import type * as Policy from "./Policy.ts"
|
|
57
46
|
|
|
58
47
|
/**
|
|
59
48
|
* The error when a policy says no.
|
|
@@ -62,14 +51,14 @@ import * as Resource from "./Resource.ts"
|
|
|
62
51
|
* @category errors
|
|
63
52
|
* @since 0.3.0
|
|
64
53
|
*/
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
54
|
+
export const Forbidden: typeof Internal.Forbidden = Internal.Forbidden
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @stability stable
|
|
58
|
+
* @category errors
|
|
59
|
+
* @since 0.3.0
|
|
60
|
+
*/
|
|
61
|
+
export type Forbidden = Internal.Forbidden
|
|
73
62
|
|
|
74
63
|
/**
|
|
75
64
|
* The error when the policy of the principal can not be loaded. The cause is
|
|
@@ -79,9 +68,15 @@ export class Forbidden extends Schema.TaggedError<Forbidden>(
|
|
|
79
68
|
* @category errors
|
|
80
69
|
* @since 0.3.0
|
|
81
70
|
*/
|
|
82
|
-
export
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
export const PolicyUnavailable: typeof Internal.PolicyUnavailable =
|
|
72
|
+
Internal.PolicyUnavailable
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @stability stable
|
|
76
|
+
* @category errors
|
|
77
|
+
* @since 0.3.0
|
|
78
|
+
*/
|
|
79
|
+
export type PolicyUnavailable = Internal.PolicyUnavailable
|
|
85
80
|
|
|
86
81
|
/**
|
|
87
82
|
* The error when a decider must wait for an answer. `ticket` names the
|
|
@@ -91,28 +86,14 @@ export class PolicyUnavailable extends Schema.TaggedError<PolicyUnavailable>(
|
|
|
91
86
|
* @category errors
|
|
92
87
|
* @since 0.3.0
|
|
93
88
|
*/
|
|
94
|
-
export
|
|
95
|
-
"@nmnmcc/ability/Authorizer/Pending",
|
|
96
|
-
)("Pending", {
|
|
97
|
-
ticket: Schema.String,
|
|
98
|
-
reason: Schema.optionalKey(Schema.String),
|
|
99
|
-
}) {}
|
|
89
|
+
export const Pending: typeof Internal.Pending = Internal.Pending
|
|
100
90
|
|
|
101
91
|
/**
|
|
102
|
-
* The services and the errors of the principal and the decider of an
|
|
103
|
-
* authorizer.
|
|
104
|
-
*
|
|
105
92
|
* @stability stable
|
|
106
|
-
* @category
|
|
93
|
+
* @category errors
|
|
107
94
|
* @since 0.3.0
|
|
108
95
|
*/
|
|
109
|
-
export
|
|
110
|
-
readonly requires?: unknown
|
|
111
|
-
readonly error?: unknown
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
type RequiresOf<C> = C extends { readonly requires: infer R } ? R : never
|
|
115
|
-
type ErrorOf<C> = C extends { readonly error: infer E } ? E : never
|
|
96
|
+
export type Pending = Internal.Pending
|
|
116
97
|
|
|
117
98
|
/**
|
|
118
99
|
* What a check is about: an act on a resource value, or an act on a kind of
|
|
@@ -178,7 +159,7 @@ export interface Authorizer<D extends Domain.Any, E, R> {
|
|
|
178
159
|
* the services of the layer. It may have predicates, so it is opaque.
|
|
179
160
|
*/
|
|
180
161
|
readonly policy: Effect.Effect<
|
|
181
|
-
Policy.Policy<D,
|
|
162
|
+
Policy.Policy<D, Condition.Opaque<E>>,
|
|
182
163
|
PolicyUnavailable | E,
|
|
183
164
|
R
|
|
184
165
|
>
|
|
@@ -210,10 +191,8 @@ export interface Authorizer<D extends Domain.Any, E, R> {
|
|
|
210
191
|
target: T,
|
|
211
192
|
) => Effect.Effect<
|
|
212
193
|
Condition.Condition<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
Condition.Opaque,
|
|
216
|
-
Policy.ResidualReads<D, T["kind"]>
|
|
194
|
+
Policy.ResidualReads<D, T["kind"]>,
|
|
195
|
+
Condition.Opaque<E>
|
|
217
196
|
>,
|
|
218
197
|
PolicyUnavailable | E,
|
|
219
198
|
R
|
|
@@ -226,12 +205,7 @@ export interface Authorizer<D extends Domain.Any, E, R> {
|
|
|
226
205
|
readonly coverage: <const T extends Policy.KindTarget<D>>(
|
|
227
206
|
target: T,
|
|
228
207
|
) => Effect.Effect<
|
|
229
|
-
Policy.Coverage<
|
|
230
|
-
E,
|
|
231
|
-
never,
|
|
232
|
-
Condition.Opaque,
|
|
233
|
-
Policy.ResidualReads<D, T["kind"]>
|
|
234
|
-
>,
|
|
208
|
+
Policy.Coverage<Policy.ResidualReads<D, T["kind"]>, Condition.Opaque<E>>,
|
|
235
209
|
PolicyUnavailable | E,
|
|
236
210
|
R
|
|
237
211
|
>
|
|
@@ -262,6 +236,8 @@ export interface ServiceClass<
|
|
|
262
236
|
R,
|
|
263
237
|
> extends Context.ServiceClass<Self, Id, Authorizer<D, E, R>> {
|
|
264
238
|
readonly domain: D
|
|
239
|
+
/** The Effect that gives the principal of the current request. */
|
|
240
|
+
readonly principal: Effect.Effect<Domain.Principal<D>, E, R>
|
|
265
241
|
readonly decide: (
|
|
266
242
|
target: Policy.Target<D>,
|
|
267
243
|
) => Effect.Effect<Decision.Decision, PolicyUnavailable | E, R | Self>
|
|
@@ -284,10 +260,8 @@ export interface ServiceClass<
|
|
|
284
260
|
target: T,
|
|
285
261
|
) => Effect.Effect<
|
|
286
262
|
Condition.Condition<
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
Condition.Opaque,
|
|
290
|
-
Policy.ResidualReads<D, T["kind"]>
|
|
263
|
+
Policy.ResidualReads<D, T["kind"]>,
|
|
264
|
+
Condition.Opaque<E>
|
|
291
265
|
>,
|
|
292
266
|
PolicyUnavailable | E,
|
|
293
267
|
R | Self
|
|
@@ -295,12 +269,7 @@ export interface ServiceClass<
|
|
|
295
269
|
readonly coverage: <const T extends Policy.KindTarget<D>>(
|
|
296
270
|
target: T,
|
|
297
271
|
) => Effect.Effect<
|
|
298
|
-
Policy.Coverage<
|
|
299
|
-
E,
|
|
300
|
-
never,
|
|
301
|
-
Condition.Opaque,
|
|
302
|
-
Policy.ResidualReads<D, T["kind"]>
|
|
303
|
-
>,
|
|
272
|
+
Policy.Coverage<Policy.ResidualReads<D, T["kind"]>, Condition.Opaque<E>>,
|
|
304
273
|
PolicyUnavailable | E,
|
|
305
274
|
R | Self
|
|
306
275
|
>
|
|
@@ -318,6 +287,7 @@ export interface ServiceClass<
|
|
|
318
287
|
*/
|
|
319
288
|
export type AnyServiceClass = Context.Key<any, any> & {
|
|
320
289
|
readonly domain: Domain.Any
|
|
290
|
+
readonly principal: Effect.Effect<any, any, any>
|
|
321
291
|
}
|
|
322
292
|
|
|
323
293
|
/**
|
|
@@ -327,39 +297,19 @@ export type AnyServiceClass = Context.Key<any, any> & {
|
|
|
327
297
|
* @category constructors
|
|
328
298
|
* @since 0.3.0
|
|
329
299
|
*/
|
|
330
|
-
export const Service: <Self
|
|
300
|
+
export const Service: <Self>() => <
|
|
331
301
|
const Id extends string,
|
|
332
302
|
D extends Domain.Any,
|
|
303
|
+
E = never,
|
|
304
|
+
R = never,
|
|
333
305
|
>(
|
|
334
306
|
id: Id,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
static readonly decide = (target: AnyTarget) =>
|
|
342
|
-
this.use((service: any) => service.decide(target))
|
|
343
|
-
static readonly check = (target: AnyTarget) =>
|
|
344
|
-
this.use((service: any) => service.check(target))
|
|
345
|
-
static readonly guard = (target: AnyTarget, options: unknown) =>
|
|
346
|
-
this.use((service: any) => service.guard(target, options))
|
|
347
|
-
static readonly filter = (target: AnyTarget) =>
|
|
348
|
-
this.use((service: any) => service.filter(target))
|
|
349
|
-
static readonly coverage = (target: AnyTarget) =>
|
|
350
|
-
this.use((service: any) => service.coverage(target))
|
|
351
|
-
static readonly permittedFields = (target: AnyTarget) =>
|
|
352
|
-
this.use((service: any) => service.permittedFields(target))
|
|
353
|
-
}
|
|
354
|
-
}) as never
|
|
355
|
-
|
|
356
|
-
type AnyTarget = {
|
|
357
|
-
readonly action: string
|
|
358
|
-
readonly resource?: object
|
|
359
|
-
readonly kind?: string
|
|
360
|
-
readonly field?: string
|
|
361
|
-
readonly within?: unknown
|
|
362
|
-
}
|
|
307
|
+
options: {
|
|
308
|
+
readonly domain: D
|
|
309
|
+
/** Gives the principal of the current request. */
|
|
310
|
+
readonly principal: Effect.Effect<Domain.Principal<NoInfer<D>>, E, R>
|
|
311
|
+
},
|
|
312
|
+
) => ServiceClass<Self, Id, D, E, R> = Internal.Service as never
|
|
363
313
|
|
|
364
314
|
/**
|
|
365
315
|
* The options of {@link layer}.
|
|
@@ -369,8 +319,6 @@ type AnyTarget = {
|
|
|
369
319
|
* @since 0.3.0
|
|
370
320
|
*/
|
|
371
321
|
export interface Options<D extends Domain.Any, E, R, EL, RL, RP> {
|
|
372
|
-
/** Gives the principal of the current request. */
|
|
373
|
-
readonly principal: Effect.Effect<Domain.Principal<D>, E, R>
|
|
374
322
|
/**
|
|
375
323
|
* Loads the policy of a principal. A failure becomes
|
|
376
324
|
* {@link PolicyUnavailable}. The services of the predicates of the policy
|
|
@@ -378,7 +326,7 @@ export interface Options<D extends Domain.Any, E, R, EL, RL, RP> {
|
|
|
378
326
|
*/
|
|
379
327
|
readonly policy: (
|
|
380
328
|
principal: Domain.Principal<D>,
|
|
381
|
-
) => Effect.Effect<Policy.Policy<D, E, RP
|
|
329
|
+
) => Effect.Effect<Policy.Policy<D, Condition.Opaque<E, RP>>, EL, RL>
|
|
382
330
|
/**
|
|
383
331
|
* Answers when no rule gives an answer. Without a decider, such a request
|
|
384
332
|
* fails with {@link Forbidden}.
|
|
@@ -390,488 +338,24 @@ export interface Options<D extends Domain.Any, E, R, EL, RL, RP> {
|
|
|
390
338
|
* How the policies are kept. The default keeps 1000 policies for 30 seconds.
|
|
391
339
|
* Give `false` to load the policy for each check.
|
|
392
340
|
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
341
|
+
* The key of a principal is the text of its JSON form, from the Schema of
|
|
342
|
+
* the principal of the domain. A principal is kept only when the value that
|
|
343
|
+
* the Schema decodes from that form is `Equal` to it. The loader always
|
|
344
|
+
* gets that decoded copy, also with no cache, so two principals with one
|
|
345
|
+
* key get one policy, and the cache can not change an answer. So the loader
|
|
346
|
+
* must decide by the data of the principal, not by the identity of its
|
|
347
|
+
* objects. A principal with no JSON form is not kept, and the loader gets
|
|
348
|
+
* it as it is. Give `key` to make the key in another way.
|
|
398
349
|
*/
|
|
399
350
|
readonly cache?:
|
|
400
351
|
| {
|
|
401
352
|
readonly capacity?: number
|
|
402
353
|
readonly timeToLive?: Duration.Input
|
|
354
|
+
readonly key?: (principal: Domain.Principal<D>) => string | undefined
|
|
403
355
|
}
|
|
404
356
|
| false
|
|
405
357
|
}
|
|
406
358
|
|
|
407
|
-
/**
|
|
408
|
-
* A cache key for a principal. Two keys are equal when they have the same
|
|
409
|
-
* text, so two principals with the same value share one policy. The key also
|
|
410
|
-
* keeps the principal, so the cache can load its policy. A plain `Data` value
|
|
411
|
-
* can not do both, so the key gives its own equality with `Equal.Equal`.
|
|
412
|
-
*/
|
|
413
|
-
class PrincipalKey implements Equal.Equal {
|
|
414
|
-
readonly principal: object
|
|
415
|
-
readonly text: string
|
|
416
|
-
constructor(principal: object, text: string) {
|
|
417
|
-
this.principal = principal
|
|
418
|
-
this.text = text
|
|
419
|
-
}
|
|
420
|
-
[Equal.symbol](that: unknown): boolean {
|
|
421
|
-
return that instanceof PrincipalKey && that.text === this.text
|
|
422
|
-
}
|
|
423
|
-
[Hash.symbol](): number {
|
|
424
|
-
return Hash.string(this.text)
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Tests if an array has a plain field at each index below its length. With
|
|
430
|
-
* one more own key for `length`, it then has no other own keys.
|
|
431
|
-
*/
|
|
432
|
-
const hasEveryIndex = (value: Array<unknown>): boolean =>
|
|
433
|
-
Arr.every(
|
|
434
|
-
Array.from({ length: value.length }, (_, index) => index),
|
|
435
|
-
(index) => isPlainField(value, String(index)),
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Tests if an own key of an object holds plain data that the text shows: it
|
|
440
|
-
* can be seen in a loop over the keys, and it is not a getter.
|
|
441
|
-
*/
|
|
442
|
-
const isPlainField = (value: object, key: string): boolean => {
|
|
443
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, key)
|
|
444
|
-
return (
|
|
445
|
-
descriptor !== undefined &&
|
|
446
|
-
descriptor.enumerable === true &&
|
|
447
|
-
"value" in descriptor
|
|
448
|
-
)
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const isObjectLike = (value: unknown): boolean =>
|
|
452
|
-
(typeof value === "object" && value !== null) || typeof value === "function"
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* The number of each class that a key has seen. Each layer has its own, so
|
|
456
|
-
* no state is kept for the whole program. A class is a function, and `Equal`
|
|
457
|
-
* compares functions by the function itself.
|
|
458
|
-
*/
|
|
459
|
-
type ClassIds = MutableHashMap.MutableHashMap<object, number>
|
|
460
|
-
|
|
461
|
-
/** Gives a number that is only for this class, in this layer. */
|
|
462
|
-
const classId = (classes: ClassIds, constructor: object): string => {
|
|
463
|
-
const id = Option.getOrElse(MutableHashMap.get(classes, constructor), () => {
|
|
464
|
-
const fresh = MutableHashMap.size(classes)
|
|
465
|
-
MutableHashMap.set(classes, constructor, fresh)
|
|
466
|
-
return fresh
|
|
467
|
-
})
|
|
468
|
-
return `#${String(id)}`
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/** The most levels of objects in a principal that the cache takes. */
|
|
472
|
-
const maxKeyDepth = 256
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Gives a text that names a principal value. Two values get one text only when
|
|
476
|
-
* they have the same types and the same data. So a string `"1"` and a bigint
|
|
477
|
-
* `1n` get two texts. The text keeps the class of each object, by a number
|
|
478
|
-
* that is its own, because two classes may have one name.
|
|
479
|
-
*
|
|
480
|
-
* It reads only the fields of plain objects and of `Schema.Class` values, and
|
|
481
|
-
* only when each own key is a plain field that a loop over the keys can see.
|
|
482
|
-
* An object of another class may keep its data where the text can not see it,
|
|
483
|
-
* as a `URL` does. So it gives `undefined` for such an object, unless it is a
|
|
484
|
-
* plain `Date`, `URL`, array, `Map` or `Set` with no extra own keys. It also
|
|
485
|
-
* gives `undefined` for a function, a symbol, or a value with one object in two
|
|
486
|
-
* places. The text can not show that two places hold the same object, and a
|
|
487
|
-
* loader may test it, as `set.has(value)` does.
|
|
488
|
-
* The private fields (`#name`) of a class can not be seen, so a principal
|
|
489
|
-
* class must not keep its data in them.
|
|
490
|
-
*
|
|
491
|
-
* It also gives `undefined` for a value with more than {@link maxKeyDepth}
|
|
492
|
-
* levels of objects. The walk calls itself for each level, so a deeper value
|
|
493
|
-
* could fill the stack. Such a principal is not cached.
|
|
494
|
-
*/
|
|
495
|
-
const keyText = (
|
|
496
|
-
value: unknown,
|
|
497
|
-
seen: Set<object>,
|
|
498
|
-
classes: ClassIds,
|
|
499
|
-
depth: number = 0,
|
|
500
|
-
): string | undefined => {
|
|
501
|
-
const plain = (text: string): string => text
|
|
502
|
-
switch (typeof value) {
|
|
503
|
-
case "string":
|
|
504
|
-
return plain(`s${JSON.stringify(value)}`)
|
|
505
|
-
case "number":
|
|
506
|
-
return plain(Object.is(value, -0) ? "n-0" : `n${String(value)}`)
|
|
507
|
-
case "bigint":
|
|
508
|
-
return plain(`b${String(value)}`)
|
|
509
|
-
case "boolean":
|
|
510
|
-
return plain(value ? "true" : "false")
|
|
511
|
-
case "undefined":
|
|
512
|
-
return plain("undefined")
|
|
513
|
-
case "object":
|
|
514
|
-
break
|
|
515
|
-
default:
|
|
516
|
-
return undefined
|
|
517
|
-
}
|
|
518
|
-
if (value === null) {
|
|
519
|
-
return plain("null")
|
|
520
|
-
}
|
|
521
|
-
if (seen.has(value) || depth >= maxKeyDepth) {
|
|
522
|
-
return undefined
|
|
523
|
-
}
|
|
524
|
-
seen.add(value)
|
|
525
|
-
const list = (
|
|
526
|
-
items: ReadonlyArray<unknown>,
|
|
527
|
-
wrap: (texts: ReadonlyArray<string>) => string,
|
|
528
|
-
): string | undefined => {
|
|
529
|
-
const texts = Arr.map(items, (item) =>
|
|
530
|
-
keyText(item, seen, classes, depth + 1),
|
|
531
|
-
)
|
|
532
|
-
return Arr.every(texts, (text) => text !== undefined)
|
|
533
|
-
? wrap(texts as ReadonlyArray<string>)
|
|
534
|
-
: undefined
|
|
535
|
-
}
|
|
536
|
-
const prototype: unknown = Object.getPrototypeOf(value)
|
|
537
|
-
// The own keys of the value. A built-in value keeps its data inside, so it
|
|
538
|
-
// may have no own keys, and the elements of an array are its own keys. An
|
|
539
|
-
// extra own key is data that the text does not show.
|
|
540
|
-
const own = Reflect.ownKeys(value)
|
|
541
|
-
// The entries of a map or a set keep their order, because a loader may read
|
|
542
|
-
// the first one.
|
|
543
|
-
if (prototype === Date.prototype && own.length === 0) {
|
|
544
|
-
return `d${String((value as Date).getTime())}`
|
|
545
|
-
}
|
|
546
|
-
if (prototype === URL.prototype && own.length === 0) {
|
|
547
|
-
return `u${JSON.stringify((value as URL).href)}`
|
|
548
|
-
}
|
|
549
|
-
if (
|
|
550
|
-
prototype === Array.prototype &&
|
|
551
|
-
own.length === (value as Array<unknown>).length + 1 &&
|
|
552
|
-
hasEveryIndex(value as Array<unknown>) &&
|
|
553
|
-
// An array finds an object by the object itself, as in
|
|
554
|
-
// `roles.includes(admin)`.
|
|
555
|
-
!Arr.some(value as Array<unknown>, isObjectLike)
|
|
556
|
-
) {
|
|
557
|
-
return list(value as Array<unknown>, (texts) => `[${texts.join(",")}]`)
|
|
558
|
-
}
|
|
559
|
-
if (
|
|
560
|
-
prototype === Map.prototype &&
|
|
561
|
-
own.length === 0 &&
|
|
562
|
-
// A map finds an object key by the object itself, not by its data, as
|
|
563
|
-
// in `map.get(role)`. The text can not show which object it is.
|
|
564
|
-
!Arr.some(Array.from((value as Map<unknown, unknown>).keys()), isObjectLike)
|
|
565
|
-
) {
|
|
566
|
-
// The keys and the values go in one list, so a value that is an object
|
|
567
|
-
// does not go through the test of the elements of an array.
|
|
568
|
-
return list(
|
|
569
|
-
Arr.flatMap(Array.from(value as Map<unknown, unknown>), ([key, item]) => [
|
|
570
|
-
key,
|
|
571
|
-
item,
|
|
572
|
-
]),
|
|
573
|
-
(texts) => `Map{${texts.join(",")}}`,
|
|
574
|
-
)
|
|
575
|
-
}
|
|
576
|
-
if (
|
|
577
|
-
prototype === Set.prototype &&
|
|
578
|
-
own.length === 0 &&
|
|
579
|
-
// A set finds an object by the object itself, as in `set.has(role)`.
|
|
580
|
-
!Arr.some(Array.from(value as Set<unknown>), isObjectLike)
|
|
581
|
-
) {
|
|
582
|
-
return list(
|
|
583
|
-
Array.from(value as Set<unknown>),
|
|
584
|
-
(texts) => `Set{${texts.join(",")}}`,
|
|
585
|
-
)
|
|
586
|
-
}
|
|
587
|
-
if (
|
|
588
|
-
(prototype === null ||
|
|
589
|
-
prototype === Object.prototype ||
|
|
590
|
-
// The prototype must be the prototype of the class itself. A value
|
|
591
|
-
// made with `Object.create(user)` has `user` as its prototype, and
|
|
592
|
-
// the data of `user` is not in the own keys. Such a prototype may
|
|
593
|
-
// have its own `constructor` key, so the test compares the
|
|
594
|
-
// prototype with the `prototype` of the class.
|
|
595
|
-
(Schema.isSchema((prototype as object).constructor) &&
|
|
596
|
-
(prototype as object).constructor.prototype === prototype)) &&
|
|
597
|
-
Arr.every(own, (key) => typeof key === "string" && isPlainField(value, key))
|
|
598
|
-
) {
|
|
599
|
-
// The keys keep their order, because a loader may read the first one.
|
|
600
|
-
const keys = own as ReadonlyArray<string>
|
|
601
|
-
const name =
|
|
602
|
-
prototype === null
|
|
603
|
-
? ""
|
|
604
|
-
: classId(classes, (prototype as object).constructor)
|
|
605
|
-
return list(
|
|
606
|
-
Arr.map(keys, (key) => (value as Record<string, unknown>)[key]),
|
|
607
|
-
(texts) =>
|
|
608
|
-
`${name}{${Arr.map(keys, (key, index) => `${JSON.stringify(key)}:${texts[index]!}`).join(",")}}`,
|
|
609
|
-
)
|
|
610
|
-
}
|
|
611
|
-
return undefined
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const forbidden = (
|
|
615
|
-
target: AnyTarget,
|
|
616
|
-
kind: string,
|
|
617
|
-
reason: string | undefined,
|
|
618
|
-
): Forbidden =>
|
|
619
|
-
Forbidden.make({
|
|
620
|
-
action: target.action,
|
|
621
|
-
kind,
|
|
622
|
-
...(target.field === undefined ? {} : { field: target.field }),
|
|
623
|
-
...(reason === undefined ? {} : { reason }),
|
|
624
|
-
})
|
|
625
|
-
|
|
626
|
-
const makeLayer = (
|
|
627
|
-
service: AnyServiceClass,
|
|
628
|
-
options: {
|
|
629
|
-
readonly principal: Effect.Effect<object, unknown, unknown>
|
|
630
|
-
readonly policy: (
|
|
631
|
-
principal: object,
|
|
632
|
-
) => Effect.Effect<Policy.Any, unknown, unknown>
|
|
633
|
-
readonly decider?: (
|
|
634
|
-
request: object,
|
|
635
|
-
) => Effect.Effect<Decision.Decision, unknown, unknown>
|
|
636
|
-
readonly cache?:
|
|
637
|
-
| { readonly capacity?: number; readonly timeToLive?: Duration.Input }
|
|
638
|
-
| false
|
|
639
|
-
},
|
|
640
|
-
) => {
|
|
641
|
-
const domain: Domain.Any = service.domain
|
|
642
|
-
// It gives `undefined` when the principal has no key. Such a principal does
|
|
643
|
-
// not use the cache.
|
|
644
|
-
const classes: ClassIds = MutableHashMap.empty()
|
|
645
|
-
const keyOf = (principal: object): PrincipalKey | undefined => {
|
|
646
|
-
// One set of seen objects for the walk: the walk adds each object to it,
|
|
647
|
-
// and the set goes away when the key is made.
|
|
648
|
-
const text = keyText(principal, new Set(), classes)
|
|
649
|
-
return text === undefined ? undefined : new PrincipalKey(principal, text)
|
|
650
|
-
}
|
|
651
|
-
return Layer.effect(
|
|
652
|
-
service,
|
|
653
|
-
Effect.gen(function* () {
|
|
654
|
-
// The services of the policy loader and of the predicates come from the
|
|
655
|
-
// layer, so a check does not need them.
|
|
656
|
-
const services = yield* Effect.context<never>()
|
|
657
|
-
|
|
658
|
-
// The predicates of the policy get the services of the layer, so the
|
|
659
|
-
// policy and its residual conditions need no services.
|
|
660
|
-
// A policy of another domain may have the same types, but its resources
|
|
661
|
-
// may have other paths. So it fails closed, as a policy that can not be
|
|
662
|
-
// loaded.
|
|
663
|
-
const load = (principal: object) =>
|
|
664
|
-
options.policy(principal).pipe(
|
|
665
|
-
Effect.provideContext(services),
|
|
666
|
-
Effect.filterOrFail(
|
|
667
|
-
(value) => value.domain === domain,
|
|
668
|
-
() =>
|
|
669
|
-
new globalThis.Error(
|
|
670
|
-
"The loaded policy is not a policy of this domain",
|
|
671
|
-
),
|
|
672
|
-
),
|
|
673
|
-
Effect.mapError((cause) => PolicyUnavailable.make({ cause })),
|
|
674
|
-
Effect.map((value) =>
|
|
675
|
-
Policy.provideContext(value, services as never),
|
|
676
|
-
),
|
|
677
|
-
)
|
|
678
|
-
|
|
679
|
-
const cache =
|
|
680
|
-
options.cache === false
|
|
681
|
-
? undefined
|
|
682
|
-
: yield* Cache.make({
|
|
683
|
-
capacity: options.cache?.capacity ?? 1000,
|
|
684
|
-
timeToLive: options.cache?.timeToLive ?? "30 seconds",
|
|
685
|
-
lookup: (key: PrincipalKey) => load(key.principal),
|
|
686
|
-
})
|
|
687
|
-
|
|
688
|
-
const policyFor = (principal: object) => {
|
|
689
|
-
const key = cache === undefined ? undefined : keyOf(principal)
|
|
690
|
-
if (cache === undefined || key === undefined) {
|
|
691
|
-
return load(principal)
|
|
692
|
-
}
|
|
693
|
-
// A failed lookup is kept in the cache. Remove it, so the next check
|
|
694
|
-
// tries again.
|
|
695
|
-
return Cache.get(cache, key).pipe(
|
|
696
|
-
Effect.tapError(() => Cache.invalidate(cache, key)),
|
|
697
|
-
)
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const withServices = <A, E>(
|
|
701
|
-
effect: Effect.Effect<A, E, unknown>,
|
|
702
|
-
): Effect.Effect<A, E, unknown> => Effect.provideContext(effect, services)
|
|
703
|
-
|
|
704
|
-
const current = Effect.gen(function* () {
|
|
705
|
-
const principal = yield* options.principal
|
|
706
|
-
const value = yield* policyFor(principal)
|
|
707
|
-
return { principal, policy: value }
|
|
708
|
-
})
|
|
709
|
-
|
|
710
|
-
// The principal and its policy, loaded one time. A check that asks
|
|
711
|
-
// the policy two times, such as `guard`, uses one snapshot for both,
|
|
712
|
-
// so the two answers come from one principal.
|
|
713
|
-
type Snapshot = Effect.Success<typeof current>
|
|
714
|
-
|
|
715
|
-
const decideWith = Effect.fnUntraced(function* (
|
|
716
|
-
{ principal, policy: value }: Snapshot,
|
|
717
|
-
target: AnyTarget,
|
|
718
|
-
) {
|
|
719
|
-
const decision: Decision.Decision = yield* withServices(
|
|
720
|
-
Policy.decide(value, { ...target, principal } as never),
|
|
721
|
-
)
|
|
722
|
-
return decision
|
|
723
|
-
})
|
|
724
|
-
|
|
725
|
-
const decide = (target: AnyTarget) =>
|
|
726
|
-
Effect.flatMap(current, (snapshot) => decideWith(snapshot, target))
|
|
727
|
-
|
|
728
|
-
const kindDecision = (
|
|
729
|
-
value: Policy.Any,
|
|
730
|
-
principal: object,
|
|
731
|
-
target: AnyTarget,
|
|
732
|
-
): Decision.Decision => {
|
|
733
|
-
const request = { ...target, principal } as never
|
|
734
|
-
const covered = Policy.coverage(value, request)
|
|
735
|
-
if (covered._tag === "All") {
|
|
736
|
-
return Decision.Allow({})
|
|
737
|
-
}
|
|
738
|
-
// The decider may answer for the scope only when no value in it is
|
|
739
|
-
// denied, and when no predicate may fail. Else some value may get a
|
|
740
|
-
// "no" from the policy, so the answer for the scope is "no". An
|
|
741
|
-
// unknown answer may hide a denied value, so it is a "no" too.
|
|
742
|
-
// An empty scope never passes, so the decider is not asked for it.
|
|
743
|
-
// A scope that the search can not say has a value may be empty, so
|
|
744
|
-
// the decider is not asked for it either.
|
|
745
|
-
if (
|
|
746
|
-
covered._tag !== "Unknown" &&
|
|
747
|
-
Policy.scopeHasValue(value, request)
|
|
748
|
-
) {
|
|
749
|
-
const deny: Condition.Ast = Policy.residuals(value, request).deny
|
|
750
|
-
const schema = Domain.descriptorOf(
|
|
751
|
-
value.domain,
|
|
752
|
-
target.kind ?? "",
|
|
753
|
-
)?.schema
|
|
754
|
-
if (
|
|
755
|
-
deny._tag === "False" ||
|
|
756
|
-
Condition.satisfiable(deny as Condition.Condition, { schema })
|
|
757
|
-
._tag === "Unsatisfiable"
|
|
758
|
-
) {
|
|
759
|
-
return Decision.NotApplicable
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
return Decision.Deny({})
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const checkWith = Effect.fnUntraced(function* (
|
|
766
|
-
{ principal, policy: value }: Snapshot,
|
|
767
|
-
target: AnyTarget,
|
|
768
|
-
) {
|
|
769
|
-
const kind =
|
|
770
|
-
target.resource === undefined
|
|
771
|
-
? (target.kind ?? "")
|
|
772
|
-
: (Resource.kindOf(target.resource) ?? "")
|
|
773
|
-
// A request that does not fit its kind gets a "no" at once. The
|
|
774
|
-
// decider must not say yes to an action that the kind does not have.
|
|
775
|
-
const descriptor = Domain.descriptorOf(domain, kind)
|
|
776
|
-
if (descriptor === undefined || !Policy.fitsKind(descriptor, target)) {
|
|
777
|
-
return yield* forbidden(target, kind, undefined)
|
|
778
|
-
}
|
|
779
|
-
const fromPolicy: Decision.Decision =
|
|
780
|
-
target.resource === undefined
|
|
781
|
-
? kindDecision(value, principal, target)
|
|
782
|
-
: yield* withServices(
|
|
783
|
-
Policy.decide(value, { ...target, principal } as never),
|
|
784
|
-
)
|
|
785
|
-
// When no rule gives an answer, the decider gives it.
|
|
786
|
-
const decision: Decision.Decision =
|
|
787
|
-
fromPolicy._tag === "NotApplicable" && options.decider !== undefined
|
|
788
|
-
? yield* options.decider({ ...target, principal })
|
|
789
|
-
: fromPolicy
|
|
790
|
-
if (decision._tag !== "Allow") {
|
|
791
|
-
return yield* forbidden(target, kind, Decision.reason(decision))
|
|
792
|
-
}
|
|
793
|
-
})
|
|
794
|
-
|
|
795
|
-
const check = (target: AnyTarget) =>
|
|
796
|
-
Effect.flatMap(current, (snapshot) => checkWith(snapshot, target))
|
|
797
|
-
|
|
798
|
-
const guard = Effect.fnUntraced(function* (
|
|
799
|
-
target: AnyTarget,
|
|
800
|
-
guardOptions: {
|
|
801
|
-
readonly read?: string
|
|
802
|
-
readonly hide: () => unknown
|
|
803
|
-
},
|
|
804
|
-
) {
|
|
805
|
-
const snapshot = yield* current
|
|
806
|
-
const read = guardOptions.read ?? "read"
|
|
807
|
-
const { field: _field, ...rest } = target
|
|
808
|
-
const visible: Decision.Decision = yield* decideWith(snapshot, {
|
|
809
|
-
...rest,
|
|
810
|
-
action: read,
|
|
811
|
-
})
|
|
812
|
-
if (visible._tag !== "Allow") {
|
|
813
|
-
return yield* Effect.fail(guardOptions.hide())
|
|
814
|
-
}
|
|
815
|
-
if (target.action !== read || target.field !== undefined) {
|
|
816
|
-
yield* checkWith(snapshot, target)
|
|
817
|
-
}
|
|
818
|
-
})
|
|
819
|
-
|
|
820
|
-
// The filter is the condition of the coverage. When the coverage is
|
|
821
|
-
// not known, it is the residual condition in the scope, which throws
|
|
822
|
-
// an error when there is none, so the filter fails.
|
|
823
|
-
const filter = (
|
|
824
|
-
target: AnyTarget,
|
|
825
|
-
): Effect.Effect<Condition.Ast, unknown, unknown> =>
|
|
826
|
-
Effect.map(current, ({ principal, policy: value }): Condition.Ast => {
|
|
827
|
-
const request = { ...target, principal } as never
|
|
828
|
-
const covered = Policy.coverage(value, request)
|
|
829
|
-
return covered._tag === "Unknown"
|
|
830
|
-
? Policy.residual(value, request)
|
|
831
|
-
: (covered.condition as Condition.Ast)
|
|
832
|
-
})
|
|
833
|
-
|
|
834
|
-
const coverage = (
|
|
835
|
-
target: AnyTarget,
|
|
836
|
-
): Effect.Effect<Policy.Coverage<any, any, any, any>, unknown, unknown> =>
|
|
837
|
-
Effect.map(current, ({ principal, policy: value }) =>
|
|
838
|
-
Policy.coverage(value, { ...target, principal } as never),
|
|
839
|
-
)
|
|
840
|
-
|
|
841
|
-
const permittedFields = Effect.fnUntraced(function* (target: AnyTarget) {
|
|
842
|
-
const { principal, policy: value } = yield* current
|
|
843
|
-
return yield* withServices(
|
|
844
|
-
Policy.permittedFields(value, { ...target, principal } as never),
|
|
845
|
-
)
|
|
846
|
-
})
|
|
847
|
-
|
|
848
|
-
const invalidate = (principal?: object) => {
|
|
849
|
-
if (cache === undefined) {
|
|
850
|
-
return Effect.void
|
|
851
|
-
}
|
|
852
|
-
if (principal === undefined) {
|
|
853
|
-
return Cache.invalidateAll(cache)
|
|
854
|
-
}
|
|
855
|
-
const key = keyOf(principal)
|
|
856
|
-
return key === undefined ? Effect.void : Cache.invalidate(cache, key)
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
return {
|
|
860
|
-
domain,
|
|
861
|
-
principal: options.principal,
|
|
862
|
-
policy: Effect.map(current, ({ policy: value }) => value),
|
|
863
|
-
decide,
|
|
864
|
-
check,
|
|
865
|
-
guard,
|
|
866
|
-
filter,
|
|
867
|
-
coverage,
|
|
868
|
-
permittedFields,
|
|
869
|
-
invalidate,
|
|
870
|
-
} as never
|
|
871
|
-
}),
|
|
872
|
-
)
|
|
873
|
-
}
|
|
874
|
-
|
|
875
359
|
/**
|
|
876
360
|
* Makes the layer of an authorizer.
|
|
877
361
|
*
|
|
@@ -891,4 +375,4 @@ export const layer: <
|
|
|
891
375
|
>(
|
|
892
376
|
service: ServiceClass<Self, Id, D, E, R>,
|
|
893
377
|
options: Options<D, NoInfer<E>, NoInfer<R>, EL, RL, RP>,
|
|
894
|
-
) => Layer.Layer<Self, never, RL | RP> = makeLayer as never
|
|
378
|
+
) => Layer.Layer<Self, never, RL | RP> = Internal.makeLayer as never
|