@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.
Files changed (66) hide show
  1. package/README.md +199 -31
  2. package/dist/Authorizer.d.ts +64 -39
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js +291 -6
  5. package/dist/Authorizer.js.map +1 -1
  6. package/dist/Condition.d.ts +444 -65
  7. package/dist/Condition.d.ts.map +1 -1
  8. package/dist/Condition.js +1007 -25
  9. package/dist/Condition.js.map +1 -1
  10. package/dist/Domain.d.ts +27 -17
  11. package/dist/Domain.d.ts.map +1 -1
  12. package/dist/Domain.js +52 -10
  13. package/dist/Domain.js.map +1 -1
  14. package/dist/HttpApi.d.ts +84 -40
  15. package/dist/HttpApi.d.ts.map +1 -1
  16. package/dist/HttpApi.js +103 -13
  17. package/dist/HttpApi.js.map +1 -1
  18. package/dist/Policy.d.ts +314 -56
  19. package/dist/Policy.d.ts.map +1 -1
  20. package/dist/Policy.js +1215 -38
  21. package/dist/Policy.js.map +1 -1
  22. package/dist/Principal.js +2 -2
  23. package/dist/Principal.js.map +1 -1
  24. package/dist/Resource.d.ts +171 -20
  25. package/dist/Resource.d.ts.map +1 -1
  26. package/dist/Resource.js +788 -15
  27. package/dist/Resource.js.map +1 -1
  28. package/dist/Rule.d.ts +115 -29
  29. package/dist/Rule.d.ts.map +1 -1
  30. package/dist/Rule.js +219 -12
  31. package/dist/Rule.js.map +1 -1
  32. package/dist/index.js +4 -4
  33. package/package.json +2 -2
  34. package/src/Authorizer.ts +625 -61
  35. package/src/Condition.ts +2403 -112
  36. package/src/Domain.ts +80 -23
  37. package/src/HttpApi.ts +225 -25
  38. package/src/Policy.ts +2836 -96
  39. package/src/Principal.ts +2 -2
  40. package/src/Resource.ts +1544 -29
  41. package/src/Rule.ts +463 -45
  42. package/dist/internal/authorizer.d.ts +0 -28
  43. package/dist/internal/authorizer.d.ts.map +0 -1
  44. package/dist/internal/authorizer.js +0 -153
  45. package/dist/internal/authorizer.js.map +0 -1
  46. package/dist/internal/condition.d.ts +0 -11
  47. package/dist/internal/condition.d.ts.map +0 -1
  48. package/dist/internal/condition.js +0 -393
  49. package/dist/internal/condition.js.map +0 -1
  50. package/dist/internal/domain.js +0 -34
  51. package/dist/internal/domain.js.map +0 -1
  52. package/dist/internal/httpApi.js +0 -41
  53. package/dist/internal/httpApi.js.map +0 -1
  54. package/dist/internal/policy.js +0 -524
  55. package/dist/internal/policy.js.map +0 -1
  56. package/dist/internal/resource.js +0 -66
  57. package/dist/internal/resource.js.map +0 -1
  58. package/dist/internal/rule.js +0 -131
  59. package/dist/internal/rule.js.map +0 -1
  60. package/src/internal/authorizer.ts +0 -271
  61. package/src/internal/condition.ts +0 -607
  62. package/src/internal/domain.ts +0 -56
  63. package/src/internal/httpApi.ts +0 -84
  64. package/src/internal/policy.ts +0 -815
  65. package/src/internal/resource.ts +0 -98
  66. package/src/internal/rule.ts +0 -203
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Authorization tools for Effect programs. Authorization means the work of
4
4
  deciding if a principal may do an act. A principal is the one who asks: a
5
- person, a user token, or a Bot.
5
+ person, an API token, or another service.
6
6
 
7
7
  The library is made with [Effect](https://effect.website) only:
8
8
 
@@ -140,35 +140,35 @@ There are two ways to make a resource:
140
140
 
141
141
  ```ts
142
142
  // A Schema class. Its body may have its own getters and methods.
143
- class Bot extends Resource.Class<Bot>()("Bot", {
143
+ class Document extends Resource.Class<Document>()("Document", {
144
144
  actions: ["read", "update"],
145
- fields: { id: Schema.String, ownerUserId: Schema.NullOr(Schema.String) },
145
+ fields: { id: Schema.String, ownerId: Schema.NullOr(Schema.String) },
146
146
  }) {
147
- get shared() {
148
- return this.ownerUserId === null
147
+ get orphaned() {
148
+ return this.ownerId === null
149
149
  }
150
150
  }
151
151
 
152
152
  // Plain objects with a `_tag` field.
153
- const Trigger = Resource.make("Trigger", {
153
+ const Comment = Resource.make("Comment", {
154
154
  actions: ["read", "write"],
155
- fields: { id: Schema.String, enabled: Schema.Boolean },
155
+ fields: { id: Schema.String, hidden: Schema.Boolean },
156
156
  })
157
- const trigger = Trigger.make({ id: "t1", enabled: true })
157
+ const comment = Comment.make({ id: "c1", hidden: false })
158
158
  ```
159
159
 
160
160
  A resource may have a path. The path puts the resource in a tree, so one rule
161
161
  can cover a part of the tree:
162
162
 
163
163
  ```ts
164
- class Bot extends Resource.Class<Bot>()("Bot", {
164
+ class Document extends Resource.Class<Document>()("Document", {
165
165
  actions: ["read", "update"],
166
166
  fields: {
167
167
  id: Schema.String,
168
168
  organizationId: Schema.String,
169
- ownerUserId: Schema.NullOr(Schema.String),
169
+ teamId: Schema.NullOr(Schema.String),
170
170
  },
171
- path: (f) => ["org", f.organizationId, "user", f.ownerUserId, "bot", f.id],
171
+ path: (f) => ["org", f.organizationId, "team", f.teamId, "doc", f.id],
172
172
  }) {}
173
173
 
174
174
  // Every resource of the organization of the principal.
@@ -184,14 +184,20 @@ _.allow("read", "*", {
184
184
  - The action `"*"` means every action. The resource `"*"` means every resource.
185
185
  - `when` is the condition. It gets a builder with the types of the resource.
186
186
  - `fields` makes the rule about some fields only. `*` stands for any part of one
187
- field name, and `**` for any number of parts: `settings.*` covers
188
- `settings.model`.
187
+ field name, and `**` for any number of parts, also none: `settings.*` covers
188
+ `settings.model`, and `**.secret` covers `secret` and `settings.secret`.
189
189
  - `reason`, `id` and `meta` are kept with the rule. `meta` is extra data, such
190
190
  as the scope of a grant. Give its Schema to `Domain.make({ meta })`.
191
191
 
192
192
  An allow rule with fields covers a request for the whole value: "may update some
193
- fields" is a yes. A deny rule with fields does not stop a request for the whole
194
- value.
193
+ fields" is a yes. This is true only for a kind that has one of these fields. A
194
+ deny rule with fields does not stop a request for the whole value.
195
+
196
+ To know if a kind has a field that a pattern fits, the library walks the Schema
197
+ of the kind: its fields, the keys of its records, and the getters of its
198
+ classes. For some key Schemas it can only try sample keys, so it may not find a
199
+ field that is there. Then the answer for the whole value is "no", never a wrong
200
+ "yes".
195
201
 
196
202
  ## Conditions
197
203
 
@@ -210,14 +216,19 @@ The builder in `when` has these functions:
210
216
 
211
217
  A missing field of a resource has the value `null`. A field of the principal
212
218
  that is `null` or missing is different: a test with it is always false, also
213
- with `ne`. So a guest with no `userId` never matches a Bot with no owner. To
214
- test for `null`, use a fixed value, as in `$.eq("ownerUserId", null)`.
219
+ with `ne`. So a guest with no `userId` never matches a document with no owner.
220
+ To test for `null`, use a fixed value, as in `$.eq("ownerId", null)`.
215
221
 
216
222
  `predicate` is the one way out of plain data. Its test may give a boolean or an
217
223
  `Effect`. The error type and the service type of the `Effect` go into the types
218
224
  of the policy and of every check. A policy with a predicate can not become JSON
219
225
  or a query, and the type system stops you when you try.
220
226
 
227
+ A predicate also keeps the types of the value and the principal that it reads.
228
+ So a predicate for a Document can not go into a rule for a Comment. Make
229
+ conditions with the builder: a node made by hand is not a condition in the
230
+ types.
231
+
221
232
  ## Policies
222
233
 
223
234
  A list of rules has a combining algorithm. It decides which rule gives the
@@ -247,6 +258,12 @@ Put policies together:
247
258
  | `Policy.firstOf([a, b])` | The first policy that allows or denies. |
248
259
  | `Policy.ceiling(policy, cap)` | A deny from `cap` always wins. An allow from `cap` does nothing. |
249
260
 
261
+ A policy tree may have at most 256 levels. A combinator throws an error for a
262
+ deeper tree, and the policy Schema does not decode one. A deeper tree could fill
263
+ the stack of a check. The JSON form of a policy may have at most 1000 levels of
264
+ objects and arrays, so a deep input fails to decode before it fills the stack. A
265
+ field path may have at most 256 parts.
266
+
250
267
  For example, a user token may never do more than its member, and the platform
251
268
  has deny rules for everyone:
252
269
 
@@ -268,6 +285,7 @@ authorizer with a decider asks the decider first.
268
285
  | `Policy.decideSync` | The same, at once. Only for policies with no predicates. |
269
286
  | `Policy.explain` | The decision and the rules that were tested, for an audit log. |
270
287
  | `Policy.residual` | The condition on resource values that the policy allows. |
288
+ | `Policy.coverage` | If the policy allows all, none or some of a part of a kind. |
271
289
  | `Policy.permittedFields` | The fields of a value that the policy lets a request use. |
272
290
 
273
291
  `Policy.residual` puts the principal into every rule and gives a condition on
@@ -275,6 +293,85 @@ the resource fields only. It is `True` when the policy allows every value of the
275
293
  kind, and `False` when it allows none. Use it to ask "may this principal do this
276
294
  act on some value of this kind?", or to make a database query.
277
295
 
296
+ When a rule that the request may use has a predicate, the residual is one
297
+ predicate with the name `"policy"`. It runs the whole policy for each value, so
298
+ it gives the same answers and the same failures as `Policy.decide`.
299
+
300
+ A deep policy tree may give a very large residual. When it would have more than
301
+ 10000 nodes, `Policy.residual` throws an error, and a check in an `Authorizer`
302
+ fails. It throws an error too when a rule that the request may use reads a
303
+ getter of a class: the read may fail when the policy runs, and a smaller
304
+ condition could leave it out.
305
+
306
+ ### Scopes
307
+
308
+ A scope is a part of a kind, such as the posts of one writer. Give it as
309
+ `within`, with the same builder as the condition of a rule. A scope is plain
310
+ data: it may not have a predicate.
311
+
312
+ There are three questions about a scope. Each one is a question about two sets:
313
+ the values in the scope, and the values that the policy allows.
314
+
315
+ | Question | Sets | Function |
316
+ | -------------------------------------- | ------------------- | ------------------------------- |
317
+ | May the principal act on every value? | scope ⊆ allowed | `Policy.coverage` is `All` |
318
+ | Which values may the principal act on? | scope ∩ allowed | `Policy.residual` |
319
+ | May the principal act on some value? | scope ∩ allowed ≠ ∅ | `Policy.coverage` is not `None` |
320
+
321
+ ```ts
322
+ const ownPosts = {
323
+ principal: writer,
324
+ action: "update",
325
+ kind: "Post",
326
+ within: ($) => $.eq("authorId", $.principal("id")),
327
+ } satisfies Policy.KindRequest<typeof Blog>
328
+
329
+ Policy.coverage(policy, ownPosts)._tag // "All"
330
+ Policy.residual(policy, ownPosts) // authorId = "u1"
331
+ ```
332
+
333
+ Use "every value" for an act on a value that does not exist yet. A new post has
334
+ no id, but its author is known, so a check on the scope of its author tells if
335
+ the writer may make it.
336
+
337
+ `Policy.coverage` gives one of four answers. Each one has `condition`, the exact
338
+ list query for the scope:
339
+
340
+ - `All`: the policy allows every value in the scope.
341
+ - `None`: the policy allows no value in the scope. An empty scope is `None` too,
342
+ so a scope with a mistake never gives a "yes".
343
+ - `Some`: the policy allows some values and not others. It gives an example of
344
+ each, `allowed` and `refused`.
345
+ - `Unknown`: the policy has a predicate, a condition tests a field that is not a
346
+ plain scalar, or the search went past its budget.
347
+
348
+ The answer is exact. Each test in a condition compares one field with fixed
349
+ values, so these values cut the values of the field into a few parts, and every
350
+ value in one part gives the same answer. `Condition.satisfiable` tries one value
351
+ from each part, one field at a time. It takes the values of each field from the
352
+ Schema of the resource: its literals, `null` for a nullable or optional field,
353
+ and finite or integer numbers. It does not know other Schema checks, such as a
354
+ minimum length, so a field with such a check is not plain (see below).
355
+
356
+ The search answers only for plain scalar fields: a string, a number, a boolean,
357
+ a literal, `null`, or an optional field or a union of these, with no checks
358
+ other than finite and integer checks, on a path of required fields of plain
359
+ structs. For another field, such as a class, a record, a union of structs, a
360
+ date or a field of `Schema.declare`, a value may have data that the Schema does
361
+ not show, so the answer is `Unknown`. A check on a kind then says no.
362
+ `Resource.isPlainField` tells if a field is plain. This is true for each rule
363
+ that the request may use, also for a test that a smaller condition leaves out: a
364
+ read of a field that is not plain, such as a getter, may fail in a check.
365
+
366
+ A "yes" also needs a value of the whole kind, so each required field of the kind
367
+ must surely have values, also a field that the condition does not test. A
368
+ required field with a check other than finite and integer checks, such as a
369
+ minimum length, may have no value, so then the answer is `Unknown` in place of
370
+ `All` or `Some`. `Resource.hasValue` tells if a Schema surely has a value.
371
+
372
+ "Some" means that some value of the Schema is allowed. It does not mean that
373
+ such a value is in your database. To know that, run the condition as a query.
374
+
278
375
  ## Queries
279
376
 
280
377
  Change a residual condition into a query with `Condition.fold`. You give one
@@ -292,15 +389,19 @@ const toSql = Condition.fold<string>({
292
389
  not: (part) => `NOT ${part}`,
293
390
  })
294
391
 
295
- const where =
296
- yield *
297
- toSql(Policy.residual(policy, { principal, action: "read", kind: "Post" }))
392
+ const program = Effect.gen(function* () {
393
+ const where = yield* toSql(
394
+ Policy.residual(policy, { principal, action: "read", kind: "Post" }),
395
+ )
396
+ })
298
397
  ```
299
398
 
300
399
  `fold` fails with `Untranslatable` on a predicate, unless you give a `predicate`
301
400
  function too. Take care with `null`: in this library `ne` is true when the field
302
401
  is `null`, but in SQL `x <> 'a'` is not true when `x` is `NULL`. Use
303
- `IS DISTINCT FROM` or a similar form.
402
+ `IS DISTINCT FROM` or a similar form. Give the values to the database as bound
403
+ parameters. A value from a principal may have a quote in it, and in the SQL text
404
+ it could change the query. `examples/multi-tenant.ts` shows how.
304
405
 
305
406
  ## JSON
306
407
 
@@ -337,19 +438,32 @@ const AuthLive = Authorizer.layer(Auth, {
337
438
  })
338
439
  ```
339
440
 
441
+ Two principals with the same data get one policy from the cache. So the loader
442
+ must decide by the data of the principal only. When it tests that an object of
443
+ the principal is a given object, as `role === admin` does, give `cache: false`.
444
+
340
445
  Use it in a handler:
341
446
 
342
447
  ```ts
343
- yield * Auth.check({ action: "update", resource: post })
344
- yield * Auth.check({ action: "read", kind: "Post" })
345
- yield *
346
- Auth.guard(
448
+ const handler = Effect.gen(function* () {
449
+ yield* Auth.check({ action: "update", resource: post })
450
+ yield* Auth.check({ action: "read", kind: "Post" })
451
+ yield* Auth.guard(
347
452
  { action: "update", resource: post },
348
453
  {
349
454
  hide: () => PostNotFound.make({ id: post.id }),
350
455
  },
351
456
  )
352
- const where = yield * Auth.filter({ action: "read", kind: "Post" })
457
+ const where = yield* Auth.filter({ action: "read", kind: "Post" })
458
+ // A new post of the writer: every post of this author may be made.
459
+ yield* Auth.check({
460
+ action: "update",
461
+ kind: "Post",
462
+ within: ($) => $.eq("authorId", $.principal("id")),
463
+ })
464
+ const covered = yield* Auth.coverage({ action: "delete", kind: "Post" })
465
+ const showDeleteButton = covered._tag !== "None"
466
+ })
353
467
  ```
354
468
 
355
469
  | Function | What it does |
@@ -357,12 +471,18 @@ const where = yield * Auth.filter({ action: "read", kind: "Post" })
357
471
  | `check` | Succeeds with `void`, or fails with `Forbidden`. |
358
472
  | `guard` | Fails with your error when the principal may not read the value, then checks. |
359
473
  | `filter` | Gives the residual condition, for a list query. |
474
+ | `coverage` | Tells if the principal may act on all, none or some values in a scope. |
360
475
  | `decide` | Gives the decision. It does not fail on a "no". |
361
476
  | `permittedFields` | Gives the fields that the principal may use. |
362
477
  | `invalidate` | Removes a cached policy, after a change to the rules of a principal. |
363
478
 
364
479
  A check on a kind (`{ action, kind }`) passes only when the policy allows the
365
- act on every value of the kind.
480
+ act on every value of the kind. With `within`, it passes only when the policy
481
+ allows the act on every value in the scope, and the scope is not empty. The
482
+ decider is asked only when no value in the scope is denied.
483
+
484
+ `filter` gives `False` when the principal may act on no value. Then the list is
485
+ empty, and that is not an error.
366
486
 
367
487
  The errors of a check:
368
488
 
@@ -374,8 +494,8 @@ The errors of a check:
374
494
  ### Deciders
375
495
 
376
496
  When no rule gives an answer, the authorizer asks the decider, if there is one.
377
- A decider may answer at once, or fail with `Pending` when a person or a Bot must
378
- answer later:
497
+ A decider may answer at once, or fail with `Pending` when a person or another
498
+ service must answer later:
379
499
 
380
500
  ```ts
381
501
  Authorizer.layer(Auth, {
@@ -418,17 +538,65 @@ const health = HttpApiEndpoint.get("health", "/health").annotate(
418
538
  [],
419
539
  )
420
540
 
541
+ // The checks run only in a group that uses the middleware.
542
+ const posts = HttpApiGroup.make("posts")
543
+ .add(update, health)
544
+ .middleware(Authorization)
545
+
421
546
  const AuthorizationLive = AbilityHttpApi.layer(Authorization, Auth)
422
547
  ```
423
548
 
549
+ An act on a part of a kind takes a scope from the path parameters. A list takes
550
+ `lists`: the middleware does not check every value, because the handler gets its
551
+ query with `Auth.filter`, with the same scope. An empty list is not an error,
552
+ unless you give `nonEmpty: true`. Then the middleware needs a coverage of `All`
553
+ or `Some`, so `Unknown` fails too:
554
+
555
+ ```ts
556
+ const create = HttpApiEndpoint.post("create", "/users/:user/posts", {
557
+ params: { user: Schema.String },
558
+ }).annotate(AbilityHttpApi.Required, [
559
+ AbilityHttpApi.requires(Auth, "update", "Post", {
560
+ within: (params) => ($) => $.eq("authorId", params["user"] ?? ""),
561
+ }),
562
+ ])
563
+
564
+ const list = HttpApiEndpoint.get("list", "/users/:user/posts", {
565
+ params: { user: Schema.String },
566
+ }).annotate(AbilityHttpApi.Required, [
567
+ AbilityHttpApi.lists(Auth, "read", "Post", {
568
+ within: (params) => ($) => $.eq("authorId", params["user"] ?? ""),
569
+ }),
570
+ ])
571
+ ```
572
+
573
+ A path parameter may be `undefined`. A scope with `undefined` in it is a defect,
574
+ so give another value for it.
575
+
576
+ The annotations and the layer do nothing alone. Give the middleware to every
577
+ group with `.middleware(Authorization)`, or to the whole API. An endpoint in a
578
+ group without the middleware is not checked.
579
+
424
580
  `GroupRequired` gives a default for a whole group: one list for GET requests and
425
581
  one for every other method. `AbilityHttpApi.errors` gives the errors with HTTP
426
582
  status codes: 403, 503, and 409.
427
583
 
428
584
  ## Examples
429
585
 
586
+ Each example is a small program. Run it with Node.js, which runs `.ts` files
587
+ itself:
588
+
430
589
  ```sh
431
590
  node examples/basic.ts
432
- node examples/authorizer.ts
433
- node examples/openbot-rbac.ts
434
591
  ```
592
+
593
+ | Example | What it shows |
594
+ | ----------------------------- | ------------------------------------------------------------------------------------------------ |
595
+ | `examples/basic.ts` | A blog: a domain, a policy, field rules and a decision with a reason. |
596
+ | `examples/authorizer.ts` | Team documents: an authorizer service with a cache, `guard`, `filter`, coverage and a decider. |
597
+ | `examples/multi-tenant.ts` | A task tracker for many teams: roles, API tokens, a platform limit, scopes and SQL list queries. |
598
+ | `examples/field-access.ts` | A staff list: rules for some fields, field patterns and `permittedFields`. |
599
+ | `examples/stored-policies.ts` | A help desk: policies kept as JSON, `meta` on rules, checks when a policy loads, and `explain`. |
600
+ | `examples/predicates.ts` | Meeting rooms: a predicate that asks an Effect service, its error type, and `provideContext`. |
601
+ | `examples/algorithms.ts` | File sharing: the four combining algorithms, `any`, `firstOf` and `ceiling`. |
602
+ | `examples/http-api.ts` | A notes API: the `effect/http-api` middleware, one-value checks, list scopes and closed routes. |
@@ -1,15 +1,21 @@
1
1
  import { Condition, Opaque } from "./Condition.js";
2
- import { Any, AnyAction, Principal } from "./Domain.js";
2
+ import { Any, CommonAction, Kind, Principal } from "./Domain.js";
3
3
  import { Decision } from "./Decision.js";
4
- import { KindRequest, KindTarget, Policy, Request, Target as Target$1 } from "./Policy.js";
5
- import { Forbidden as Forbidden$1, Pending as Pending$1, PolicyUnavailable as PolicyUnavailable$1 } from "./internal/authorizer.js";
4
+ import { Coverage, FieldlessTarget, KindRequest, KindTarget, Policy, Request, ResidualReads, Target as Target$1 } from "./Policy.js";
6
5
  import * as Context from "effect/Context";
7
6
  import * as Effect from "effect/Effect";
8
7
  import * as Layer from "effect/Layer";
8
+ import * as Schema from "effect/Schema";
9
9
  import * as Duration from "effect/Duration";
10
10
  declare namespace Authorizer_d_exports {
11
11
  export { AnyServiceClass, Authorizer, Config, DeciderRequest, Forbidden, GuardOptions, Options, Pending, PolicyUnavailable, Service, ServiceClass, Target, layer };
12
12
  }
13
+ declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbidden", {
14
+ readonly action: Schema.String;
15
+ readonly kind: Schema.String;
16
+ readonly field: Schema.optionalKey<Schema.String>;
17
+ readonly reason: Schema.optionalKey<Schema.String>;
18
+ }>, import("effect/Cause").YieldableError>;
13
19
  /**
14
20
  * The error when a policy says no.
15
21
  *
@@ -17,13 +23,10 @@ declare namespace Authorizer_d_exports {
17
23
  * @category errors
18
24
  * @since 0.3.0
19
25
  */
20
- export declare const Forbidden: typeof Forbidden$1;
21
- /**
22
- * @stability stable
23
- * @category errors
24
- * @since 0.3.0
25
- */
26
- export type Forbidden = Forbidden$1;
26
+ export declare class Forbidden extends Forbidden_base {}
27
+ declare const PolicyUnavailable_base: Schema.Class<PolicyUnavailable, Schema.TaggedStruct<"PolicyUnavailable", {
28
+ readonly cause: Schema.Defect;
29
+ }>, import("effect/Cause").YieldableError>;
27
30
  /**
28
31
  * The error when the policy of the principal can not be loaded. The cause is
29
32
  * kept, but it is not shown to the client.
@@ -32,13 +35,11 @@ export type Forbidden = Forbidden$1;
32
35
  * @category errors
33
36
  * @since 0.3.0
34
37
  */
35
- export declare const PolicyUnavailable: typeof PolicyUnavailable$1;
36
- /**
37
- * @stability stable
38
- * @category errors
39
- * @since 0.3.0
40
- */
41
- export type PolicyUnavailable = PolicyUnavailable$1;
38
+ export declare class PolicyUnavailable extends PolicyUnavailable_base {}
39
+ declare const Pending_base: Schema.Class<Pending, Schema.TaggedStruct<"Pending", {
40
+ readonly ticket: Schema.String;
41
+ readonly reason: Schema.optionalKey<Schema.String>;
42
+ }>, import("effect/Cause").YieldableError>;
42
43
  /**
43
44
  * The error when a decider must wait for an answer. `ticket` names the
44
45
  * request that waits, so the client can come back for the answer.
@@ -47,13 +48,7 @@ export type PolicyUnavailable = PolicyUnavailable$1;
47
48
  * @category errors
48
49
  * @since 0.3.0
49
50
  */
50
- export declare const Pending: typeof Pending$1;
51
- /**
52
- * @stability stable
53
- * @category errors
54
- * @since 0.3.0
55
- */
56
- export type Pending = Pending$1;
51
+ export declare class Pending extends Pending_base {}
57
52
  /**
58
53
  * The services and the errors of the principal and the decider of an
59
54
  * authorizer.
@@ -75,7 +70,8 @@ type ErrorOf<C> = C extends {
75
70
  /**
76
71
  * What a check is about: an act on a resource value, or an act on a kind of
77
72
  * resource. A check on a kind passes only when the policy allows the act on
78
- * every value of the kind.
73
+ * every value of the kind. With a scope (`within`), it passes when the policy
74
+ * allows the act on every value in the scope, and the scope is not empty.
79
75
  *
80
76
  * @stability stable
81
77
  * @category models
@@ -97,12 +93,23 @@ export type DeciderRequest<D> = Request<D> | KindRequest<D>;
97
93
  * @category models
98
94
  * @since 0.3.0
99
95
  */
100
- export interface GuardOptions<D, E2> {
101
- /** The action that lets the principal see the resource. The default is `"read"`. */
102
- readonly read?: AnyAction<D>;
96
+ export type GuardOptions<D, E2, K extends string = Kind<D>> = {
103
97
  /** The error when the principal may not see the resource. */
104
98
  readonly hide: () => E2;
105
- }
99
+ } & ("read" extends CommonAction<D, K> ? {
100
+ /**
101
+ * The action that lets the principal see the resource. It must be an
102
+ * action of every kind in `K`. The default is `"read"`.
103
+ */
104
+ readonly read?: CommonAction<D, K>;
105
+ } : {
106
+ /**
107
+ * The action that lets the principal see the resource. It must be an
108
+ * action of every kind in `K`. Some kind in `K` has no `"read"`
109
+ * action, so you must give it.
110
+ */
111
+ readonly read: CommonAction<D, K>;
112
+ });
106
113
  /**
107
114
  * The service.
108
115
  *
@@ -114,8 +121,11 @@ export interface Authorizer<D extends Any, E, R> {
114
121
  readonly domain: D;
115
122
  /** The principal of the current request. */
116
123
  readonly principal: Effect.Effect<Principal<D>, E, R>;
117
- /** The policy of the principal of the current request. */
118
- readonly policy: Effect.Effect<Policy<D, E>, PolicyUnavailable | E, R>;
124
+ /**
125
+ * The policy of the principal of the current request. Its predicates have
126
+ * the services of the layer. It may have predicates, so it is opaque.
127
+ */
128
+ readonly policy: Effect.Effect<Policy<D, E, never, Opaque>, PolicyUnavailable | E, R>;
119
129
  /** Gives the decision of the policy. It does not ask the decider. */
120
130
  readonly decide: (target: Target$1<D>) => Effect.Effect<Decision, PolicyUnavailable | E, R>;
121
131
  /** Succeeds when the principal may do the act, and fails when it may not. */
@@ -126,14 +136,22 @@ export interface Authorizer<D extends Any, E, R> {
126
136
  * answer "not found" to a principal that may not know that a resource
127
137
  * exists.
128
138
  */
129
- readonly guard: <E2>(target: Target$1<D>, options: GuardOptions<D, E2>) => Effect.Effect<void, E2 | Forbidden | Pending | PolicyUnavailable | E, R>;
139
+ readonly guard: <const T extends Target$1<D>, E2>(target: T, options: GuardOptions<D, E2, T["resource"]["_tag"]>) => Effect.Effect<void, E2 | Forbidden | Pending | PolicyUnavailable | E, R>;
140
+ /**
141
+ * Gives the condition on resource values that the principal may act on,
142
+ * inside the scope of the target when it has one. Change it into a query
143
+ * with `Condition.fold`. It is `False` when the principal may act on no
144
+ * value, so the list is empty: it is not an error.
145
+ */
146
+ readonly filter: <const T extends KindTarget<D>>(target: T) => Effect.Effect<Condition<E, never, Opaque, ResidualReads<D, T["kind"]>>, PolicyUnavailable | E, R>;
130
147
  /**
131
- * Gives the condition on resource values that the principal may act on.
132
- * Change it into a query with `Condition.fold`.
148
+ * Tells if the principal may act on all, none or some of the values in
149
+ * the scope of the target. See `Policy.coverage`. Use it to show a button
150
+ * only when the answer is not `None`.
133
151
  */
134
- readonly filter: (target: KindTarget<D>) => Effect.Effect<Condition<E, never, Opaque>, PolicyUnavailable | E, R>;
152
+ readonly coverage: <const T extends KindTarget<D>>(target: T) => Effect.Effect<Coverage<E, never, Opaque, ResidualReads<D, T["kind"]>>, PolicyUnavailable | E, R>;
135
153
  /** Gives the fields of a value that the principal may use. */
136
- readonly permittedFields: (target: Omit<Target$1<D>, "field">) => Effect.Effect<ReadonlyArray<string>, PolicyUnavailable | E, R>;
154
+ readonly permittedFields: (target: FieldlessTarget<D>) => Effect.Effect<ReadonlyArray<string>, PolicyUnavailable | E, R>;
137
155
  /**
138
156
  * Removes the cached policy of a principal, or of every principal. Call it
139
157
  * after a change to the rules or roles of a principal.
@@ -152,9 +170,10 @@ export interface ServiceClass<Self, Id extends string, D extends Any, E, R> exte
152
170
  readonly domain: D;
153
171
  readonly decide: (target: Target$1<D>) => Effect.Effect<Decision, PolicyUnavailable | E, R | Self>;
154
172
  readonly check: (target: Target<D>) => Effect.Effect<void, Forbidden | Pending | PolicyUnavailable | E, R | Self>;
155
- readonly guard: <E2>(target: Target$1<D>, options: GuardOptions<D, E2>) => Effect.Effect<void, E2 | Forbidden | Pending | PolicyUnavailable | E, R | Self>;
156
- readonly filter: (target: KindTarget<D>) => Effect.Effect<Condition<E, never, Opaque>, PolicyUnavailable | E, R | Self>;
157
- readonly permittedFields: (target: Omit<Target$1<D>, "field">) => Effect.Effect<ReadonlyArray<string>, PolicyUnavailable | E, R | Self>;
173
+ readonly guard: <const T extends Target$1<D>, E2>(target: T, options: GuardOptions<D, E2, T["resource"]["_tag"]>) => Effect.Effect<void, E2 | Forbidden | Pending | PolicyUnavailable | E, R | Self>;
174
+ readonly filter: <const T extends KindTarget<D>>(target: T) => Effect.Effect<Condition<E, never, Opaque, ResidualReads<D, T["kind"]>>, PolicyUnavailable | E, R | Self>;
175
+ readonly coverage: <const T extends KindTarget<D>>(target: T) => Effect.Effect<Coverage<E, never, Opaque, ResidualReads<D, T["kind"]>>, PolicyUnavailable | E, R | Self>;
176
+ readonly permittedFields: (target: FieldlessTarget<D>) => Effect.Effect<ReadonlyArray<string>, PolicyUnavailable | E, R | Self>;
158
177
  }
159
178
  /**
160
179
  * Any service class made by {@link Service}.
@@ -198,6 +217,12 @@ export interface Options<D extends Any, E, R, EL, RL, RP> {
198
217
  /**
199
218
  * How the policies are kept. The default keeps 1000 policies for 30 seconds.
200
219
  * Give `false` to load the policy for each check.
220
+ *
221
+ * Two principals with the same data get one policy from the cache. So the
222
+ * loader must decide by the data of the principal only. When it tests that
223
+ * an object of the principal is a given object, as `role === admin` does,
224
+ * give `false`. A principal with objects in an array, a set or the keys of
225
+ * a map is not kept, because such tests are usual there.
201
226
  */
202
227
  readonly cache?: {
203
228
  readonly capacity?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"Authorizer.d.ts","names":["internal.Forbidden","internal.PolicyUnavailable","internal.Pending","Policy.Target","Policy.KindTarget","Policy.Request","Policy.KindRequest","Domain.AnyAction","Domain.Any","Domain.Principal","Policy.Policy","Decision.Decision","Condition.Opaque","Condition.Condition"],"sources":["../src/Authorizer.ts"],"mappings":";;;;;;;;;;;;;;;;;;;qBAqDa,kBAAkBA;;;;;;YAOnB,YAAYA;;;;;;;;;qBAUX,0BAA0BC;;;;;;YAQ3B,oBAAoBA;;;;;;;;;qBAUnB,gBAAgBC;;;;;;YAOjB,UAAUA;;;;;;;;;iBAUL;WACP;WACA;;KAGL,WAAW,KAAK;WAAqB,gBAAgB;IAAM;KAC3D,QAAQ,KAAK;WAAqB,aAAa;IAAM;;;;;;;;;;YAW9C,OAAO,KAAKC,SAAc,KAAKC,WAAkB;;;;;;;;YASjD,eAAe,KAAKC,QAAe,KAAKC,YAAmB;;;;;;;;iBAStD,aAAa,GAAG;;WAEvB,OAAOC,UAAiB;;WAExB,YAAY;;;;;;;;;iBAUL,WAAW,UAAUC,KAAY,GAAG;WAC3C,QAAQ;;WAER,WAAW,OAAO,OAAOC,UAAiB,IAAI,GAAG;;WAEjD,QAAQ,OAAO,OAAOC,OAAc,GAAG,IAAI,oBAAoB,GAAG;;WAElE,SACR,QAAQP,SAAc,OAClB,OAAO,OAAOQ,UAAmB,oBAAoB,GAAG;;WAEpD,QACR,QAAQ,OAAO,OACX,OAAO,aAAa,YAAY,UAAU,oBAAoB,GAAG;;;;;;;WAO7D,QAAQ,IAChB,QAAQR,SAAc,IACtB,SAAS,aAAa,GAAG,QACrB,OAAO,aAAa,KAAK,YAAY,UAAU,oBAAoB,GAAG;;;;;WAKlE,SACR,QAAQC,WAAkB,OACtB,OAAO,OACXS,UAAoB,UAAUD,SAC9B,oBAAoB,GACpB;;WAGQ,kBACR,QAAQ,KAAKT,SAAc,iBACvB,OAAO,OAAO,uBAAuB,oBAAoB,GAAG;;;;;WAKxD,aAAa,YAAYM,UAAiB,OAAO,OAAO;;;;;;;;;;iBAWjD,aAChB,MACA,mBACA,UAAUD,KACV,GACA,WACS,QAAQ,aAAa,MAAM,IAAI,WAAW,GAAG,GAAG;WAChD,QAAQ;WACR,SACR,QAAQL,SAAc,OAClB,OAAO,OAAOQ,UAAmB,oBAAoB,GAAG,IAAI;WACxD,QACR,QAAQ,OAAO,OACX,OAAO,aAEX,YAAY,UAAU,oBAAoB,GAC1C,IAAI;WAEI,QAAQ,IAChB,QAAQR,SAAc,IACtB,SAAS,aAAa,GAAG,QACrB,OAAO,aAEX,KAAK,YAAY,UAAU,oBAAoB,GAC/C,IAAI;WAEI,SACR,QAAQC,WAAkB,OACtB,OAAO,OACXS,UAAoB,UAAUD,SAC9B,oBAAoB,GACpB,IAAI;WAEI,kBACR,QAAQ,KAAKT,SAAc,iBACvB,OAAO,OAAO,uBAAuB,oBAAoB,GAAG,IAAI;;;;;;;;;YAU1D,kBAAkB,QAAQ;WAC5B,QAAQK;;;;;;;;;qBAUL,UAAU,MAAM,UAAU,yBAChC,mBACN,UAAUA,KAEV,IAAI,IACJ,QAAQ,MACJ,aAAa,MAAM,IAAI,GAAG,QAAQ,IAAI,WAAW;;;;;;;;iBAUrC,QAAQ,UAAUA,KAAY,GAAG,GAAG,IAAI,IAAI;;WAEnD,WAAW,OAAO,OAAOC,UAAiB,IAAI,GAAG;;;;;;WAMjD,SACR,WAAWA,UAAiB,OACxB,OAAO,OAAOC,OAAc,GAAG,GAAG,UAAU,IAAI;;;;;WAK5C,WACR,SAAS,eAAe,OACpB,OAAO,OAAOC,UAAmB,UAAU,GAAG;;;;;WAK1C;aAEG;aACA,aAAa,SAAS;;;;;;;;;;qBAYtB,QACZ,MACA,mBACA,UAAUH,KACV,GACA,GACA,YACA,YACA,YAEA,SAAS,aAAa,MAAM,IAAI,GAAG,GAAG,IACtC,SAAS,QAAQ,GAAG,QAAQ,IAAI,QAAQ,IAAI,IAAI,IAAI,QAChD,MAAM,MAAM,aAAa,KAAK"}
1
+ {"version":3,"file":"Authorizer.d.ts","names":["Policy.Target","Policy.KindTarget","Policy.Request","Policy.KindRequest","Domain.Kind","Domain.CommonAction","Domain.Any","Domain.Principal","Condition.Opaque","Policy.Policy","Decision.Decision","Policy.ResidualReads","Condition.Condition","Policy.Coverage","Policy.FieldlessTarget"],"sources":["../src/Authorizer.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;qBAgEa,kBAAkB;;;;;;;;;;;;qBAiBlB,0BAA0B;;;;;;;;;;;;;qBAY1B,gBAAgB;;;;;;;;;iBAeZ;WACP;WACA;;KAGL,WAAW,KAAK;WAAqB,gBAAgB;IAAM;KAC3D,QAAQ,KAAK;WAAqB,aAAa;IAAM;;;;;;;;;;;YAY9C,OAAO,KAAKA,SAAc,KAAKC,WAAkB;;;;;;;;YASjD,eAAe,KAAKC,QAAe,KAAKC,YAAmB;;;;;;;;YAS3D,aAAa,GAAG,IAAI,mBAAmBC,KAAY;;WAErD,YAAY;oBACFC,aAAoB,GAAG;;;;;WAM/B,OAAOA,aAAoB,GAAG;;;;;;;WAQ9B,MAAMA,aAAoB,GAAG;;;;;;;;;iBAUxB,WAAW,UAAUC,KAAY,GAAG;WAC3C,QAAQ;;WAER,WAAW,OAAO,OAAOC,UAAiB,IAAI,GAAG;;;;;WAKjD,QAAQ,OAAO,OACvBE,OAAc,GAAG,UAAUD,SAC3B,oBAAoB,GACpB;;WAGQ,SACR,QAAQR,SAAc,OAClB,OAAO,OAAOU,UAAmB,oBAAoB,GAAG;;WAEpD,QACR,QAAQ,OAAO,OACX,OAAO,aAAa,YAAY,UAAU,oBAAoB,GAAG;;;;;;;WAO7D,cAAc,UAAUV,SAAc,IAAI,IAClD,QAAQ,GACR,SAAS,aAAa,GAAG,IAAI,2BACzB,OAAO,aAAa,KAAK,YAAY,UAAU,oBAAoB,GAAG;;;;;;;WAOlE,eAAe,UAAUC,WAAkB,IACnD,QAAQ,MACJ,OAAO,OACXW,UACC,UAEAJ,QACAG,cAAqB,GAAG,aAEzB,oBAAoB,GACpB;;;;;;WAOQ,iBAAiB,UAAUV,WAAkB,IACrD,QAAQ,MACJ,OAAO,OACXY,SACC,UAEAL,QACAG,cAAqB,GAAG,aAEzB,oBAAoB,GACpB;;WAGQ,kBACR,QAAQG,gBAAuB,OAC3B,OAAO,OAAO,uBAAuB,oBAAoB,GAAG;;;;;WAKxD,aAAa,YAAYP,UAAiB,OAAO,OAAO;;;;;;;;;;iBAWjD,aAChB,MACA,mBACA,UAAUD,KACV,GACA,WACS,QAAQ,aAAa,MAAM,IAAI,WAAW,GAAG,GAAG;WAChD,QAAQ;WACR,SACR,QAAQN,SAAc,OAClB,OAAO,OAAOU,UAAmB,oBAAoB,GAAG,IAAI;WACxD,QACR,QAAQ,OAAO,OACX,OAAO,aAEX,YAAY,UAAU,oBAAoB,GAC1C,IAAI;WAEI,cAAc,UAAUV,SAAc,IAAI,IAClD,QAAQ,GACR,SAAS,aAAa,GAAG,IAAI,2BACzB,OAAO,aAEX,KAAK,YAAY,UAAU,oBAAoB,GAC/C,IAAI;WAEI,eAAe,UAAUC,WAAkB,IACnD,QAAQ,MACJ,OAAO,OACXW,UACC,UAEAJ,QACAG,cAAqB,GAAG,aAEzB,oBAAoB,GACpB,IAAI;WAEI,iBAAiB,UAAUV,WAAkB,IACrD,QAAQ,MACJ,OAAO,OACXY,SACC,UAEAL,QACAG,cAAqB,GAAG,aAEzB,oBAAoB,GACpB,IAAI;WAEI,kBACR,QAAQG,gBAAuB,OAC3B,OAAO,OAAO,uBAAuB,oBAAoB,GAAG,IAAI;;;;;;;;;YAU1D,kBAAkB,QAAQ;WAC5B,QAAQR;;;;;;;;;qBAUL,UAAU,MAAM,UAAU,yBAChC,mBACN,UAAUA,KAEV,IAAI,IACJ,QAAQ,MACJ,aAAa,MAAM,IAAI,GAAG,QAAQ,IAAI,WAAW;;;;;;;;iBAmCrC,QAAQ,UAAUA,KAAY,GAAG,GAAG,IAAI,IAAI;;WAEnD,WAAW,OAAO,OAAOC,UAAiB,IAAI,GAAG;;;;;;WAMjD,SACR,WAAWA,UAAiB,OACxB,OAAO,OAAOE,OAAc,GAAG,GAAG,UAAU,IAAI;;;;;WAK5C,WACR,SAAS,eAAe,OACpB,OAAO,OAAOC,UAAmB,UAAU,GAAG;;;;;;;;;;;WAW1C;aAEG;aACA,aAAa,SAAS;;;;;;;;;;qBAgetB,QACZ,MACA,mBACA,UAAUJ,KACV,GACA,GACA,YACA,YACA,YAEA,SAAS,aAAa,MAAM,IAAI,GAAG,GAAG,IACtC,SAAS,QAAQ,GAAG,QAAQ,IAAI,QAAQ,IAAI,IAAI,IAAI,QAChD,MAAM,MAAM,aAAa,KAAK"}