@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/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,
|
|
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
|
|
143
|
+
class Document extends Resource.Class<Document>()("Document", {
|
|
144
144
|
actions: ["read", "update"],
|
|
145
|
-
fields: { id: Schema.String,
|
|
145
|
+
fields: { id: Schema.String, ownerId: Schema.NullOr(Schema.String) },
|
|
146
146
|
}) {
|
|
147
|
-
get
|
|
148
|
-
return this.
|
|
147
|
+
get orphaned() {
|
|
148
|
+
return this.ownerId === null
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
// Plain objects with a `_tag` field.
|
|
153
|
-
const
|
|
153
|
+
const Comment = Resource.make("Comment", {
|
|
154
154
|
actions: ["read", "write"],
|
|
155
|
-
fields: { id: Schema.String,
|
|
155
|
+
fields: { id: Schema.String, hidden: Schema.Boolean },
|
|
156
156
|
})
|
|
157
|
-
const
|
|
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
|
|
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
|
-
|
|
169
|
+
teamId: Schema.NullOr(Schema.String),
|
|
170
170
|
},
|
|
171
|
-
path: (f) => ["org", f.organizationId, "
|
|
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.
|
|
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
|
|
214
|
-
test for `null`, use a fixed value, as in `$.eq("
|
|
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
|
|
296
|
-
yield
|
|
297
|
-
|
|
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
|
-
|
|
344
|
-
yield
|
|
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
|
|
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
|
|
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. |
|
package/dist/Authorizer.d.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { Condition, Opaque } from "./Condition.js";
|
|
2
|
-
import { Any,
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
118
|
-
|
|
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: <
|
|
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
|
-
*
|
|
132
|
-
*
|
|
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
|
|
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:
|
|
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: <
|
|
156
|
-
readonly filter: (target:
|
|
157
|
-
readonly
|
|
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;
|
package/dist/Authorizer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Authorizer.d.ts","names":["
|
|
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":"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"}
|