@nmnmcc/ability 0.3.0 → 1.0.0

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 (63) hide show
  1. package/README.md +169 -14
  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 +101 -11
  17. package/dist/HttpApi.js.map +1 -1
  18. package/dist/Policy.d.ts +311 -53
  19. package/dist/Policy.d.ts.map +1 -1
  20. package/dist/Policy.js +1212 -35
  21. package/dist/Policy.js.map +1 -1
  22. package/dist/Resource.d.ts +167 -16
  23. package/dist/Resource.d.ts.map +1 -1
  24. package/dist/Resource.js +784 -11
  25. package/dist/Resource.js.map +1 -1
  26. package/dist/Rule.d.ts +113 -27
  27. package/dist/Rule.d.ts.map +1 -1
  28. package/dist/Rule.js +217 -10
  29. package/dist/Rule.js.map +1 -1
  30. package/dist/index.js +4 -4
  31. package/package.json +1 -1
  32. package/src/Authorizer.ts +623 -59
  33. package/src/Condition.ts +2403 -112
  34. package/src/Domain.ts +79 -22
  35. package/src/HttpApi.ts +223 -23
  36. package/src/Policy.ts +2833 -93
  37. package/src/Resource.ts +1537 -22
  38. package/src/Rule.ts +461 -43
  39. package/dist/internal/authorizer.d.ts +0 -28
  40. package/dist/internal/authorizer.d.ts.map +0 -1
  41. package/dist/internal/authorizer.js +0 -153
  42. package/dist/internal/authorizer.js.map +0 -1
  43. package/dist/internal/condition.d.ts +0 -11
  44. package/dist/internal/condition.d.ts.map +0 -1
  45. package/dist/internal/condition.js +0 -393
  46. package/dist/internal/condition.js.map +0 -1
  47. package/dist/internal/domain.js +0 -34
  48. package/dist/internal/domain.js.map +0 -1
  49. package/dist/internal/httpApi.js +0 -41
  50. package/dist/internal/httpApi.js.map +0 -1
  51. package/dist/internal/policy.js +0 -524
  52. package/dist/internal/policy.js.map +0 -1
  53. package/dist/internal/resource.js +0 -66
  54. package/dist/internal/resource.js.map +0 -1
  55. package/dist/internal/rule.js +0 -131
  56. package/dist/internal/rule.js.map +0 -1
  57. package/src/internal/authorizer.ts +0 -271
  58. package/src/internal/condition.ts +0 -607
  59. package/src/internal/domain.ts +0 -56
  60. package/src/internal/httpApi.ts +0 -84
  61. package/src/internal/policy.ts +0 -815
  62. package/src/internal/resource.ts +0 -98
  63. package/src/internal/rule.ts +0 -203
@@ -1,815 +0,0 @@
1
- // A policy keeps the error type and the service type of its predicates only in
2
- // the public types. Inside this file they are erased to `unknown`, so the
3
- // language service check for `unknown` in an Effect is off here.
4
- // @effect-diagnostics anyUnknownInErrorContext:off
5
- import * as Effect from "effect/Effect"
6
- import * as Equal from "effect/Equal"
7
- import * as Formatter from "effect/Formatter"
8
- import { dual } from "effect/Function"
9
- import * as Hash from "effect/Hash"
10
- import * as Inspectable from "effect/Inspectable"
11
- import * as Pipeable from "effect/Pipeable"
12
- import * as Predicate from "effect/Predicate"
13
- import * as Schema from "effect/Schema"
14
- import * as SchemaTransformation from "effect/SchemaTransformation"
15
- import type * as Condition from "../Condition.ts"
16
- import * as Decision from "../Decision.ts"
17
- import type * as Domain from "../Domain.ts"
18
- import type * as Policy from "../Policy.ts"
19
- import type * as Rule from "../Rule.ts"
20
- import * as condition from "./condition.ts"
21
- import { getDescriptor, kindOf } from "./domain.ts"
22
- import * as rule from "./rule.ts"
23
-
24
- /** @internal */
25
- export const TypeId = "~@nmnmcc/ability/Policy" as const
26
-
27
- type Node = Policy.Node
28
- type Ast = Condition.Ast
29
-
30
- interface PolicyData {
31
- readonly domain: Domain.Any
32
- readonly node: Node
33
- }
34
-
35
- /** @internal */
36
- export const encodeNode = (node: Node): Policy.EncodedNode => {
37
- switch (node._tag) {
38
- case "Rules":
39
- return {
40
- _tag: "Rules",
41
- algorithm: node.algorithm,
42
- rules: node.rules.map(
43
- (value) => rule.toData(value) as Policy.EncodedRule,
44
- ),
45
- }
46
- case "All":
47
- case "Any":
48
- case "FirstOf":
49
- return { _tag: node._tag, policies: node.policies.map(encodeNode) }
50
- case "Ceiling":
51
- return {
52
- _tag: "Ceiling",
53
- cap: encodeNode(node.cap),
54
- policy: encodeNode(node.policy),
55
- }
56
- }
57
- }
58
-
59
- const Proto = {
60
- ...Pipeable.Prototype,
61
- [TypeId]: TypeId,
62
- [Equal.symbol](this: PolicyData, that: unknown): boolean {
63
- return (
64
- isPolicy(that) &&
65
- this.domain === (that as unknown as PolicyData).domain &&
66
- Equal.equals(this.node, (that as unknown as PolicyData).node)
67
- )
68
- },
69
- [Hash.symbol](this: PolicyData): number {
70
- return Hash.hash(this.node)
71
- },
72
- toJSON(this: PolicyData) {
73
- return encodeNode(this.node)
74
- },
75
- toString(this: PolicyData) {
76
- return Formatter.format(encodeNode(this.node), { space: 2 })
77
- },
78
- [Inspectable.NodeInspectSymbol](this: PolicyData) {
79
- return encodeNode(this.node)
80
- },
81
- }
82
-
83
- /** @internal */
84
- export const isPolicy = (u: unknown): u is Policy.Policy<Domain.Any> =>
85
- Predicate.hasProperty(u, TypeId)
86
-
87
- /** @internal */
88
- export const fromNode = (domain: Domain.Any, node: Node): any => {
89
- const self = Object.create(Proto) as { domain: Domain.Any; node: Node }
90
- self.domain = domain
91
- self.node = node
92
- return Object.freeze(self)
93
- }
94
-
95
- const nodeOf = (policy: unknown): Node => (policy as PolicyData).node
96
- const domainOf = (policy: unknown): Domain.Any => (policy as PolicyData).domain
97
-
98
- /** @internal */
99
- export const rules = (
100
- domain: Domain.Any,
101
- values: Iterable<Rule.Any>,
102
- options?: { readonly algorithm?: Policy.Algorithm },
103
- ): any =>
104
- fromNode(
105
- domain,
106
- Object.freeze({
107
- _tag: "Rules",
108
- algorithm: options?.algorithm ?? "denyOverrides",
109
- rules: Object.freeze(Array.from(values)),
110
- }),
111
- )
112
-
113
- /** @internal */
114
- export const empty = (domain: Domain.Any): any => rules(domain, [])
115
-
116
- /** @internal */
117
- export const define =
118
- (domain: Domain.Any, options?: { readonly algorithm?: Policy.Algorithm }) =>
119
- (body: (builder: unknown) => Generator<Rule.Any, unknown, unknown>): any => {
120
- const collected: Array<Rule.Any> = []
121
- const iterator = body(rule.builder(domain))
122
- let state = iterator.next()
123
- while (state.done !== true) {
124
- collected.push(state.value)
125
- state = iterator.next(state.value)
126
- }
127
- return rules(domain, collected, options)
128
- }
129
-
130
- const combine =
131
- (tag: "All" | "Any" | "FirstOf") =>
132
- (policies: ReadonlyArray<unknown>): any => {
133
- const domain = domainOf(policies[0])
134
- for (const policy of policies) {
135
- if (domainOf(policy) !== domain) {
136
- throw new Error("Policies from two domains can not be put together")
137
- }
138
- }
139
- return fromNode(
140
- domain,
141
- Object.freeze({
142
- _tag: tag,
143
- policies: Object.freeze(policies.map(nodeOf)),
144
- }),
145
- )
146
- }
147
-
148
- /** @internal */
149
- export const all = combine("All")
150
- /** @internal */
151
- export const any = combine("Any")
152
- /** @internal */
153
- export const firstOf = combine("FirstOf")
154
-
155
- /** @internal */
156
- export const ceiling = dual(2, (policy: unknown, cap: unknown): any => {
157
- if (domainOf(policy) !== domainOf(cap)) {
158
- throw new Error("Policies from two domains can not be put together")
159
- }
160
- return fromNode(
161
- domainOf(policy),
162
- Object.freeze({
163
- _tag: "Ceiling",
164
- cap: nodeOf(cap),
165
- policy: nodeOf(policy),
166
- }),
167
- )
168
- })
169
-
170
- // Decisions
171
-
172
- interface Context {
173
- readonly principal: object
174
- readonly action: string
175
- readonly kind: string
176
- readonly value: object
177
- readonly field: string | undefined
178
- readonly input: Condition.Input
179
- readonly trace: Array<Policy.Step> | undefined
180
- }
181
-
182
- const ruleApplies = (
183
- value: Rule.Any,
184
- context: Context,
185
- path: ReadonlyArray<number>,
186
- ): Effect.Effect<boolean, unknown, unknown> => {
187
- if (!rule.matchesTarget(value, context.action, context.kind, context.field)) {
188
- return Effect.succeed(false)
189
- }
190
- const test =
191
- value.when === undefined
192
- ? Effect.succeed(true)
193
- : condition.evaluateOf(value.when, context.input)
194
- return context.trace === undefined
195
- ? test
196
- : Effect.tap(test, (applies) =>
197
- Effect.sync(() => {
198
- context.trace!.push({ path, rule: value, applies })
199
- }),
200
- )
201
- }
202
-
203
- const fromRule = (value: Rule.Any): Decision.Decision =>
204
- value._tag === "Allow"
205
- ? Decision.Allow({ rule: value })
206
- : value.reason === undefined
207
- ? Decision.Deny({ rule: value })
208
- : Decision.Deny({ rule: value, reason: value.reason })
209
-
210
- const firstApplying = Effect.fnUntraced(function* (
211
- candidates: ReadonlyArray<readonly [Rule.Any, number]>,
212
- context: Context,
213
- path: ReadonlyArray<number>,
214
- ): Effect.fn.Return<Rule.Any | undefined, unknown, unknown> {
215
- for (const [value, index] of candidates) {
216
- if (yield* ruleApplies(value, context, [...path, index])) {
217
- return value
218
- }
219
- }
220
- return undefined
221
- })
222
-
223
- const decideRules = Effect.fnUntraced(function* (
224
- node: Policy.RulesNode,
225
- context: Context,
226
- path: ReadonlyArray<number>,
227
- ): Effect.fn.Return<Decision.Decision, unknown, unknown> {
228
- const indexed = node.rules.map((value, index) => [value, index] as const)
229
- const denies = indexed.filter(([value]) => value._tag === "Deny")
230
- const allows = indexed.filter(([value]) => value._tag === "Allow")
231
- let found: Rule.Any | undefined
232
- switch (node.algorithm) {
233
- case "denyOverrides":
234
- found =
235
- (yield* firstApplying(denies, context, path)) ??
236
- (yield* firstApplying(allows, context, path))
237
- break
238
- case "permitOverrides":
239
- found =
240
- (yield* firstApplying(allows, context, path)) ??
241
- (yield* firstApplying(denies, context, path))
242
- break
243
- case "firstApplicable":
244
- found = yield* firstApplying(indexed, context, path)
245
- break
246
- case "lastApplicable":
247
- found = yield* firstApplying(indexed.slice().reverse(), context, path)
248
- break
249
- }
250
- return found === undefined ? Decision.NotApplicable : fromRule(found)
251
- })
252
-
253
- const decideNode = (
254
- node: Node,
255
- context: Context,
256
- path: ReadonlyArray<number>,
257
- ): Effect.Effect<Decision.Decision, unknown, unknown> => {
258
- switch (node._tag) {
259
- case "Rules":
260
- return decideRules(node, context, path)
261
- case "All":
262
- return Effect.gen(function* () {
263
- let firstAllow: Decision.Decision | undefined
264
- let allAllow = node.policies.length > 0
265
- for (const [index, child] of node.policies.entries()) {
266
- const result = yield* decideNode(child, context, [...path, index])
267
- if (result._tag === "Deny") {
268
- return result
269
- }
270
- if (result._tag === "Allow") {
271
- firstAllow ??= result
272
- } else {
273
- allAllow = false
274
- }
275
- }
276
- return allAllow && firstAllow !== undefined
277
- ? firstAllow
278
- : Decision.NotApplicable
279
- })
280
- case "Any":
281
- return Effect.gen(function* () {
282
- let firstDeny: Decision.Decision | undefined
283
- for (const [index, child] of node.policies.entries()) {
284
- const result = yield* decideNode(child, context, [...path, index])
285
- if (result._tag === "Allow") {
286
- return result
287
- }
288
- if (result._tag === "Deny") {
289
- firstDeny ??= result
290
- }
291
- }
292
- return firstDeny ?? Decision.NotApplicable
293
- })
294
- case "FirstOf":
295
- return Effect.gen(function* () {
296
- for (const [index, child] of node.policies.entries()) {
297
- const result = yield* decideNode(child, context, [...path, index])
298
- if (result._tag !== "NotApplicable") {
299
- return result
300
- }
301
- }
302
- return Decision.NotApplicable
303
- })
304
- case "Ceiling":
305
- return Effect.gen(function* () {
306
- const capped = yield* decideNode(node.cap, context, [...path, 0])
307
- if (capped._tag === "Deny") {
308
- return capped
309
- }
310
- return yield* decideNode(node.policy, context, [...path, 1])
311
- })
312
- }
313
- }
314
-
315
- const makeContext = (
316
- domain: Domain.Any,
317
- request: Policy.AnyRequest,
318
- trace: Array<Policy.Step> | undefined,
319
- ): Context | undefined => {
320
- const kind = kindOf(request.resource)
321
- const descriptor =
322
- kind === undefined ? undefined : getDescriptor(domain, kind)
323
- if (kind === undefined || descriptor === undefined) {
324
- return undefined
325
- }
326
- return {
327
- principal: request.principal,
328
- action: request.action,
329
- kind,
330
- value: request.resource,
331
- field: request.field,
332
- input: {
333
- value: request.resource,
334
- principal: request.principal,
335
- path: descriptor.path,
336
- },
337
- trace,
338
- }
339
- }
340
-
341
- /** @internal */
342
- export const decideOf = (
343
- policy: unknown,
344
- request: Policy.AnyRequest,
345
- ): Effect.Effect<Decision.Decision, unknown, unknown> => {
346
- const context = makeContext(domainOf(policy), request, undefined)
347
- return context === undefined
348
- ? Effect.succeed(Decision.NotApplicable)
349
- : decideNode(nodeOf(policy), context, [])
350
- }
351
-
352
- /** @internal */
353
- export const decide = dual(2, decideOf)
354
-
355
- /** @internal */
356
- export const decideSync = dual(
357
- 2,
358
- (policy: unknown, request: Policy.AnyRequest): Decision.Decision =>
359
- Effect.runSync(
360
- // The public type of `decideSync` takes only policies with no
361
- // predicates, so this Effect has no error and needs no services.
362
- // @effect-diagnostics-next-line unsafeEffectTypeAssertion:off
363
- decideOf(policy, request) as Effect.Effect<Decision.Decision>,
364
- ),
365
- )
366
-
367
- /** @internal */
368
- export const explainOf = (
369
- policy: unknown,
370
- request: Policy.AnyRequest,
371
- ): Effect.Effect<Policy.Explanation, unknown, unknown> => {
372
- const trace: Array<Policy.Step> = []
373
- const context = makeContext(domainOf(policy), request, trace)
374
- if (context === undefined) {
375
- return Effect.succeed({ decision: Decision.NotApplicable, steps: [] })
376
- }
377
- return Effect.map(decideNode(nodeOf(policy), context, []), (decision) => ({
378
- decision,
379
- steps: Object.freeze(trace),
380
- }))
381
- }
382
-
383
- /** @internal */
384
- export const explain = dual(2, explainOf)
385
-
386
- /** @internal */
387
- export const permittedFieldsOf = Effect.fnUntraced(function* (
388
- policy: unknown,
389
- request: Omit<Policy.AnyRequest, "field">,
390
- ): Effect.fn.Return<ReadonlyArray<string>, unknown, unknown> {
391
- const domain = domainOf(policy)
392
- const kind = kindOf(request.resource)
393
- const descriptor =
394
- kind === undefined ? undefined : getDescriptor(domain, kind)
395
- if (descriptor === undefined) {
396
- return []
397
- }
398
- const output: Array<string> = []
399
- for (const field of descriptor.fieldPaths) {
400
- const result: Decision.Decision = yield* decideOf(policy, {
401
- ...request,
402
- field,
403
- })
404
- if (result._tag === "Allow") {
405
- output.push(field)
406
- }
407
- }
408
- return Object.freeze(output)
409
- })
410
-
411
- /** @internal */
412
- export const permittedFields = dual(2, permittedFieldsOf)
413
-
414
- // Residual conditions
415
-
416
- interface Pair {
417
- readonly allow: Ast
418
- readonly deny: Ast
419
- }
420
-
421
- const { always, never, simplify } = condition
422
- const and = (...values: ReadonlyArray<Ast>): Ast =>
423
- simplify(condition.and(...values))
424
- const or = (...values: ReadonlyArray<Ast>): Ast =>
425
- simplify(condition.or(...values))
426
- const not = (value: Ast): Ast => simplify(condition.not(value))
427
-
428
- interface ResidualContext {
429
- readonly principal: object
430
- readonly action: string
431
- readonly kind: string
432
- readonly field: string | undefined
433
- readonly input: {
434
- readonly principal: object
435
- readonly path: Condition.Input["path"]
436
- }
437
- }
438
-
439
- const ruleCondition = (value: Rule.Any, context: ResidualContext): Ast =>
440
- !rule.matchesTarget(value, context.action, context.kind, context.field)
441
- ? never
442
- : value.when === undefined
443
- ? always
444
- : condition.partialOf(value.when, context.input)
445
-
446
- /**
447
- * Gives the conditions for "the first rule that applies is an allow rule" and
448
- * "the first rule that applies is a deny rule".
449
- */
450
- const firstApplicableResidual = (
451
- values: ReadonlyArray<Rule.Any>,
452
- context: ResidualContext,
453
- ): Pair => {
454
- let allow: Ast = never
455
- let deny: Ast = never
456
- for (const value of values.slice().reverse()) {
457
- const applies = ruleCondition(value, context)
458
- if (value._tag === "Allow") {
459
- allow = or(applies, allow)
460
- deny = and(not(applies), deny)
461
- } else {
462
- deny = or(applies, deny)
463
- allow = and(not(applies), allow)
464
- }
465
- }
466
- return { allow, deny }
467
- }
468
-
469
- const residualNode = (node: Node, context: ResidualContext): Pair => {
470
- switch (node._tag) {
471
- case "Rules": {
472
- switch (node.algorithm) {
473
- case "denyOverrides": {
474
- const deny = or(
475
- ...node.rules
476
- .filter((value) => value._tag === "Deny")
477
- .map((value) => ruleCondition(value, context)),
478
- )
479
- const allow = and(
480
- or(
481
- ...node.rules
482
- .filter((value) => value._tag === "Allow")
483
- .map((value) => ruleCondition(value, context)),
484
- ),
485
- not(deny),
486
- )
487
- return { allow, deny }
488
- }
489
- case "permitOverrides": {
490
- const allow = or(
491
- ...node.rules
492
- .filter((value) => value._tag === "Allow")
493
- .map((value) => ruleCondition(value, context)),
494
- )
495
- const deny = and(
496
- or(
497
- ...node.rules
498
- .filter((value) => value._tag === "Deny")
499
- .map((value) => ruleCondition(value, context)),
500
- ),
501
- not(allow),
502
- )
503
- return { allow, deny }
504
- }
505
- case "firstApplicable":
506
- return firstApplicableResidual(node.rules, context)
507
- case "lastApplicable":
508
- return firstApplicableResidual(node.rules.slice().reverse(), context)
509
- }
510
- }
511
- // falls through: every algorithm returns above
512
- case "All": {
513
- if (node.policies.length === 0) {
514
- return { allow: never, deny: never }
515
- }
516
- const pairs = node.policies.map((child) => residualNode(child, context))
517
- const deny = or(...pairs.map((pair) => pair.deny))
518
- const allow = and(...pairs.map((pair) => pair.allow), not(deny))
519
- return { allow, deny }
520
- }
521
- case "Any": {
522
- const pairs = node.policies.map((child) => residualNode(child, context))
523
- const allow = or(...pairs.map((pair) => pair.allow))
524
- const deny = and(or(...pairs.map((pair) => pair.deny)), not(allow))
525
- return { allow, deny }
526
- }
527
- case "FirstOf": {
528
- let allow: Ast = never
529
- let deny: Ast = never
530
- for (const child of node.policies.slice().reverse()) {
531
- const pair = residualNode(child, context)
532
- const nextAllow = or(pair.allow, and(not(pair.deny), allow))
533
- deny = or(pair.deny, and(not(pair.allow), deny))
534
- allow = nextAllow
535
- }
536
- return { allow, deny }
537
- }
538
- case "Ceiling": {
539
- const cap = residualNode(node.cap, context)
540
- const inner = residualNode(node.policy, context)
541
- return {
542
- allow: and(not(cap.deny), inner.allow),
543
- deny: or(cap.deny, inner.deny),
544
- }
545
- }
546
- }
547
- }
548
-
549
- /** @internal */
550
- export const residualPair = (
551
- policy: unknown,
552
- request: Policy.KindRequest<Domain.Any>,
553
- ): Pair => {
554
- const domain = domainOf(policy)
555
- const descriptor = getDescriptor(domain, request.kind)
556
- if (descriptor === undefined) {
557
- return { allow: never, deny: never }
558
- }
559
- return residualNode(nodeOf(policy), {
560
- principal: request.principal,
561
- action: request.action,
562
- kind: request.kind,
563
- field: request.field,
564
- input: { principal: request.principal, path: descriptor.path },
565
- })
566
- }
567
-
568
- /** @internal */
569
- export const residualOf = (
570
- policy: unknown,
571
- request: Policy.KindRequest<Domain.Any>,
572
- ): Ast => residualPair(policy, request).allow
573
-
574
- /** @internal */
575
- export const residual = dual(2, residualOf)
576
-
577
- // Schema
578
-
579
- const Algorithm = Schema.Literals([
580
- "denyOverrides",
581
- "permitOverrides",
582
- "firstApplicable",
583
- "lastApplicable",
584
- ])
585
-
586
- const encodedRuleSchema = (meta: Schema.Top | undefined) =>
587
- Schema.Struct({
588
- _tag: Schema.Literals(["Allow", "Deny"]),
589
- actions: Schema.Array(Schema.String),
590
- resources: Schema.Array(Schema.String),
591
- when: Schema.optionalKey(condition.schema),
592
- fields: Schema.String.pipe(Schema.Array, Schema.optionalKey),
593
- reason: Schema.optionalKey(Schema.String),
594
- id: Schema.optionalKey(Schema.String),
595
- meta: Schema.optionalKey(
596
- (meta ?? Schema.Never) as Schema.Codec<unknown, unknown>,
597
- ),
598
- })
599
-
600
- const encodedNodeSchema = (
601
- meta: Schema.Top | undefined,
602
- ): Schema.Codec<Policy.EncodedNode> => {
603
- const self: Schema.Codec<Policy.EncodedNode> = Schema.Union([
604
- Schema.TaggedStruct("Rules", {
605
- algorithm: Algorithm,
606
- rules: Schema.Array(encodedRuleSchema(meta)),
607
- }),
608
- Schema.TaggedStruct("All", {
609
- policies: Schema.Array(Schema.suspend(() => self)),
610
- }),
611
- Schema.TaggedStruct("Any", {
612
- policies: Schema.Array(Schema.suspend(() => self)),
613
- }),
614
- Schema.TaggedStruct("FirstOf", {
615
- policies: Schema.Array(Schema.suspend(() => self)),
616
- }),
617
- Schema.TaggedStruct("Ceiling", {
618
- cap: Schema.suspend(() => self),
619
- policy: Schema.suspend(() => self),
620
- }),
621
- ]) as never
622
- return self
623
- }
624
-
625
- const principalFieldPaths = (domain: Domain.Any): ReadonlySet<string> => {
626
- const output = new Set<string>()
627
- const visit = (schema: unknown, prefix: string): void => {
628
- if (
629
- !Predicate.hasProperty(schema, "fields") ||
630
- !Predicate.isObject(schema.fields)
631
- ) {
632
- return
633
- }
634
- const fields = schema.fields as Record<string, unknown>
635
- for (const key of Object.keys(fields)) {
636
- const path = prefix === "" ? key : `${prefix}.${key}`
637
- output.add(path)
638
- visit(fields[key], path)
639
- }
640
- }
641
- visit(domain.principal, "")
642
- return output
643
- }
644
-
645
- const principalRefs = (ast: Condition.Encoded | Ast): ReadonlyArray<string> => {
646
- const output: Array<string> = []
647
- const visitOperand = (operand: Condition.Operand) => {
648
- if (operand._tag === "Principal") output.push(operand.path)
649
- }
650
- const visit = (node: Condition.Encoded | Ast): void => {
651
- switch (node._tag) {
652
- case "Compare":
653
- visitOperand(node.value)
654
- return
655
- case "In":
656
- node.values.forEach(visitOperand)
657
- return
658
- case "Under":
659
- node.prefix.forEach(visitOperand)
660
- return
661
- case "And":
662
- case "Or":
663
- ;(node.conditions as ReadonlyArray<Ast>).forEach(visit)
664
- return
665
- case "Not":
666
- visit(node.condition)
667
- return
668
- default:
669
- return
670
- }
671
- }
672
- visit(ast)
673
- return output
674
- }
675
-
676
- /**
677
- * Checks the names in an encoded policy against the domain. It gives one
678
- * message for each problem.
679
- */
680
- const validate = (
681
- domain: Domain.Any,
682
- root: Policy.EncodedNode,
683
- ): ReadonlyArray<Schema.FilterIssue> => {
684
- const issues: Array<Schema.FilterIssue> = []
685
- const principalPaths = principalFieldPaths(domain)
686
- const visit = (
687
- node: Policy.EncodedNode,
688
- path: ReadonlyArray<PropertyKey>,
689
- ) => {
690
- switch (node._tag) {
691
- case "Rules":
692
- node.rules.forEach((value, index) => {
693
- const at = [...path, "rules", index]
694
- const kinds = value.resources.includes("*")
695
- ? undefined
696
- : value.resources
697
- const descriptors = []
698
- for (const kind of value.resources) {
699
- if (kind === "*") continue
700
- const descriptor = getDescriptor(domain, kind)
701
- if (descriptor === undefined) {
702
- issues.push({
703
- path: [...at, "resources"],
704
- issue: `There is no resource with the kind "${kind}"`,
705
- })
706
- } else {
707
- descriptors.push(descriptor)
708
- }
709
- }
710
- const known =
711
- kinds === undefined
712
- ? Array.from(domain.resources.values())
713
- : descriptors
714
- for (const action of value.actions) {
715
- if (action === "*") continue
716
- if (
717
- !known.some((descriptor) => descriptor.actions.includes(action))
718
- ) {
719
- issues.push({
720
- path: [...at, "actions"],
721
- issue: `The action "${action}" is not an action of these resources`,
722
- })
723
- }
724
- }
725
- const usedFields = [
726
- ...(value.fields ?? []).filter((field) => !field.includes("*")),
727
- ...(value.when === undefined
728
- ? []
729
- : condition.fields(value.when as Ast)),
730
- ]
731
- for (const field of usedFields) {
732
- const everywhere =
733
- kinds !== undefined &&
734
- descriptors.length > 0 &&
735
- descriptors.every((descriptor) =>
736
- descriptor.fieldPaths.includes(field),
737
- )
738
- if (!everywhere) {
739
- issues.push({
740
- path: at,
741
- issue: `The field "${field}" is not a field of every resource of the rule`,
742
- })
743
- }
744
- }
745
- if (value.when !== undefined) {
746
- for (const ref of principalRefs(value.when)) {
747
- if (!principalPaths.has(ref)) {
748
- issues.push({
749
- path: [...at, "when"],
750
- issue: `The principal has no field "${ref}"`,
751
- })
752
- }
753
- }
754
- }
755
- })
756
- return
757
- case "All":
758
- case "Any":
759
- case "FirstOf":
760
- node.policies.forEach((child, index) =>
761
- visit(child, [...path, "policies", index]),
762
- )
763
- return
764
- case "Ceiling":
765
- visit(node.cap, [...path, "cap"])
766
- visit(node.policy, [...path, "policy"])
767
- return
768
- }
769
- }
770
- visit(root, [])
771
- return issues
772
- }
773
-
774
- const decodeNode = (node: Policy.EncodedNode): Node => {
775
- switch (node._tag) {
776
- case "Rules":
777
- return Object.freeze({
778
- _tag: "Rules",
779
- algorithm: node.algorithm,
780
- rules: Object.freeze(
781
- node.rules.map((value) => rule.make(value as rule.RuleData)),
782
- ),
783
- })
784
- case "All":
785
- case "Any":
786
- case "FirstOf":
787
- return Object.freeze({
788
- _tag: node._tag,
789
- policies: Object.freeze(node.policies.map(decodeNode)),
790
- })
791
- case "Ceiling":
792
- return Object.freeze({
793
- _tag: "Ceiling",
794
- cap: decodeNode(node.cap),
795
- policy: decodeNode(node.policy),
796
- })
797
- }
798
- }
799
-
800
- /** @internal */
801
- export const schema = (domain: Domain.Any): any =>
802
- encodedNodeSchema(domain.meta).pipe(
803
- Schema.check(Schema.makeFilter((node) => validate(domain, node))),
804
- Schema.decodeTo(
805
- Schema.declare(
806
- (u): u is Policy.Policy<Domain.Any> =>
807
- isPolicy(u) && domainOf(u) === domain,
808
- ),
809
- SchemaTransformation.transform({
810
- decode: (node: Policy.EncodedNode) =>
811
- fromNode(domain, decodeNode(node)),
812
- encode: (policy: Policy.Policy<Domain.Any>) => encodeNode(policy.node),
813
- }),
814
- ),
815
- )