@nmnmcc/ability 0.1.0 → 0.3.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 (85) hide show
  1. package/README.md +353 -170
  2. package/dist/Authorizer.d.ts +216 -0
  3. package/dist/Authorizer.d.ts.map +1 -0
  4. package/dist/Authorizer.js +55 -0
  5. package/dist/Authorizer.js.map +1 -0
  6. package/dist/Condition.d.ts +536 -0
  7. package/dist/Condition.d.ts.map +1 -0
  8. package/dist/Condition.js +141 -0
  9. package/dist/Condition.js.map +1 -0
  10. package/dist/Decision.d.ts +131 -0
  11. package/dist/Decision.d.ts.map +1 -0
  12. package/dist/Decision.js +83 -0
  13. package/dist/Decision.js.map +1 -0
  14. package/dist/Domain.d.ts +148 -0
  15. package/dist/Domain.d.ts.map +1 -0
  16. package/dist/Domain.js +34 -0
  17. package/dist/Domain.js.map +1 -0
  18. package/dist/HttpApi.d.ts +129 -0
  19. package/dist/HttpApi.d.ts.map +1 -0
  20. package/dist/HttpApi.js +95 -0
  21. package/dist/HttpApi.js.map +1 -0
  22. package/dist/Policy.d.ts +429 -0
  23. package/dist/Policy.d.ts.map +1 -0
  24. package/dist/Policy.js +162 -0
  25. package/dist/Policy.js.map +1 -0
  26. package/dist/Principal.d.ts +35 -0
  27. package/dist/Principal.d.ts.map +1 -0
  28. package/dist/Principal.js +37 -0
  29. package/dist/Principal.js.map +1 -0
  30. package/dist/Resource.d.ts +213 -0
  31. package/dist/Resource.d.ts.map +1 -0
  32. package/dist/Resource.js +71 -0
  33. package/dist/Resource.js.map +1 -0
  34. package/dist/Rule.d.ts +189 -0
  35. package/dist/Rule.d.ts.map +1 -0
  36. package/dist/Rule.js +50 -0
  37. package/dist/Rule.js.map +1 -0
  38. package/dist/_virtual/_rolldown/runtime.js +1 -2
  39. package/dist/index.d.ts +9 -4
  40. package/dist/index.js +9 -4
  41. package/dist/internal/authorizer.d.ts +28 -0
  42. package/dist/internal/authorizer.d.ts.map +1 -0
  43. package/dist/internal/authorizer.js +153 -0
  44. package/dist/internal/authorizer.js.map +1 -0
  45. package/dist/internal/condition.d.ts +11 -0
  46. package/dist/internal/condition.d.ts.map +1 -0
  47. package/dist/internal/condition.js +393 -0
  48. package/dist/internal/condition.js.map +1 -0
  49. package/dist/internal/domain.js +34 -0
  50. package/dist/internal/domain.js.map +1 -0
  51. package/dist/internal/httpApi.js +41 -0
  52. package/dist/internal/httpApi.js.map +1 -0
  53. package/dist/internal/policy.js +524 -0
  54. package/dist/internal/policy.js.map +1 -0
  55. package/dist/internal/resource.js +66 -0
  56. package/dist/internal/resource.js.map +1 -0
  57. package/dist/internal/rule.js +131 -0
  58. package/dist/internal/rule.js.map +1 -0
  59. package/package.json +61 -74
  60. package/src/Authorizer.ts +330 -0
  61. package/src/Condition.ts +706 -0
  62. package/src/Decision.ts +117 -0
  63. package/src/Domain.ts +189 -0
  64. package/src/HttpApi.ts +148 -0
  65. package/src/Policy.ts +575 -0
  66. package/src/Principal.ts +41 -0
  67. package/src/Resource.ts +290 -0
  68. package/src/Rule.ts +265 -0
  69. package/src/index.ts +57 -0
  70. package/src/internal/authorizer.ts +271 -0
  71. package/src/internal/condition.ts +607 -0
  72. package/src/internal/domain.ts +56 -0
  73. package/src/internal/httpApi.ts +84 -0
  74. package/src/internal/policy.ts +815 -0
  75. package/src/internal/resource.ts +98 -0
  76. package/src/internal/rule.ts +203 -0
  77. package/dist/Ability.d.ts +0 -516
  78. package/dist/Ability.js +0 -211
  79. package/dist/AbilityExtra.d.ts +0 -79
  80. package/dist/AbilityExtra.js +0 -195
  81. package/dist/AbilityRef.d.ts +0 -110
  82. package/dist/AbilityRef.js +0 -160
  83. package/dist/internal/ability.d.ts +0 -114
  84. package/dist/internal/ability.js +0 -624
  85. package/dist/internal/serializable.js +0 -22
@@ -0,0 +1,271 @@
1
+ // The service erases the error type and the service type of predicates,
2
+ // principals and deciders to `unknown`. The public types in
3
+ // `src/Authorizer.ts` keep them, so the language service check for `unknown`
4
+ // in an Effect is off here.
5
+ // @effect-diagnostics anyUnknownInErrorContext:off
6
+ import * as Cache from "effect/Cache"
7
+ import * as Context from "effect/Context"
8
+ import * as Effect from "effect/Effect"
9
+ import * as Equal from "effect/Equal"
10
+ import * as Hash from "effect/Hash"
11
+ import * as Layer from "effect/Layer"
12
+ import * as Schema from "effect/Schema"
13
+ import type * as Authorizer from "../Authorizer.ts"
14
+ import type * as Condition from "../Condition.ts"
15
+ import * as Decision from "../Decision.ts"
16
+ import type * as Domain from "../Domain.ts"
17
+ import type * as Policy from "../Policy.ts"
18
+ import { kindOf } from "./domain.ts"
19
+ import * as policy from "./policy.ts"
20
+
21
+ /** @internal */
22
+ export class Forbidden extends Schema.TaggedError<Forbidden>(
23
+ "@nmnmcc/ability/Authorizer/Forbidden",
24
+ )("Forbidden", {
25
+ action: Schema.String,
26
+ kind: Schema.String,
27
+ field: Schema.optionalKey(Schema.String),
28
+ reason: Schema.optionalKey(Schema.String),
29
+ }) {}
30
+
31
+ /** @internal */
32
+ export class PolicyUnavailable extends Schema.TaggedError<PolicyUnavailable>(
33
+ "@nmnmcc/ability/Authorizer/PolicyUnavailable",
34
+ )("PolicyUnavailable", { cause: Schema.Defect() }) {}
35
+
36
+ /** @internal */
37
+ export class Pending extends Schema.TaggedError<Pending>(
38
+ "@nmnmcc/ability/Authorizer/Pending",
39
+ )("Pending", {
40
+ ticket: Schema.String,
41
+ reason: Schema.optionalKey(Schema.String),
42
+ }) {}
43
+
44
+ type AnyTarget = {
45
+ readonly action: string
46
+ readonly resource?: object
47
+ readonly kind?: string
48
+ readonly field?: string
49
+ }
50
+
51
+ /** @internal */
52
+ export const Service =
53
+ () =>
54
+ (id: string, domain: Domain.Any): Authorizer.AnyServiceClass => {
55
+ const Base = Context.Service<any, any>()(id)
56
+ return class extends Base {
57
+ static readonly domain = domain
58
+ static readonly decide = (target: AnyTarget) =>
59
+ this.use((service: any) => service.decide(target))
60
+ static readonly check = (target: AnyTarget) =>
61
+ this.use((service: any) => service.check(target))
62
+ static readonly guard = (target: AnyTarget, options: unknown) =>
63
+ this.use((service: any) => service.guard(target, options))
64
+ static readonly filter = (target: AnyTarget) =>
65
+ this.use((service: any) => service.filter(target))
66
+ static readonly permittedFields = (target: AnyTarget) =>
67
+ this.use((service: any) => service.permittedFields(target))
68
+ } as never
69
+ }
70
+
71
+ /**
72
+ * A cache key for a principal. Two principals with the same encoded value are
73
+ * the same key.
74
+ */
75
+ class PrincipalKey implements Equal.Equal {
76
+ readonly principal: object
77
+ readonly json: string
78
+ constructor(principal: object, json: string) {
79
+ this.principal = principal
80
+ this.json = json
81
+ }
82
+ [Equal.symbol](that: unknown): boolean {
83
+ return that instanceof PrincipalKey && that.json === this.json
84
+ }
85
+ [Hash.symbol](): number {
86
+ return Hash.string(this.json)
87
+ }
88
+ }
89
+
90
+ const forbidden = (
91
+ target: AnyTarget,
92
+ kind: string,
93
+ reason: string | undefined,
94
+ ): Forbidden =>
95
+ Forbidden.make({
96
+ action: target.action,
97
+ kind,
98
+ ...(target.field === undefined ? {} : { field: target.field }),
99
+ ...(reason === undefined ? {} : { reason }),
100
+ })
101
+
102
+ /** @internal */
103
+ export const layer = (
104
+ service: Authorizer.AnyServiceClass,
105
+ options: {
106
+ readonly principal: Effect.Effect<object, unknown, unknown>
107
+ readonly policy: (
108
+ principal: object,
109
+ ) => Effect.Effect<Policy.Any, unknown, unknown>
110
+ readonly decider?: (
111
+ request: object,
112
+ ) => Effect.Effect<Decision.Decision, unknown, unknown>
113
+ readonly cache?:
114
+ { readonly capacity?: number; readonly timeToLive?: any } | false
115
+ },
116
+ ) => {
117
+ const domain: Domain.Any = service.domain
118
+ const encode = Schema.encodeUnknownSync(
119
+ domain.principal as unknown as Schema.Codec<unknown, unknown>,
120
+ )
121
+ const keyOf = (principal: object) =>
122
+ new PrincipalKey(principal, JSON.stringify(encode(principal)))
123
+ return Layer.effect(
124
+ service,
125
+ Effect.gen(function* () {
126
+ // The services of the policy loader and of the predicates come from the
127
+ // layer, so a check does not need them.
128
+ const services = yield* Effect.context<never>()
129
+
130
+ const load = (principal: object) =>
131
+ options.policy(principal).pipe(
132
+ Effect.provideContext(services),
133
+ Effect.mapError((cause) => PolicyUnavailable.make({ cause })),
134
+ )
135
+
136
+ const cache =
137
+ options.cache === false
138
+ ? undefined
139
+ : yield* Cache.make({
140
+ capacity: options.cache?.capacity ?? 1000,
141
+ timeToLive: options.cache?.timeToLive ?? "30 seconds",
142
+ lookup: (key: PrincipalKey) => load(key.principal),
143
+ })
144
+
145
+ const policyFor = (principal: object) => {
146
+ if (cache === undefined) {
147
+ return load(principal)
148
+ }
149
+ const key = keyOf(principal)
150
+ // A failed lookup is kept in the cache. Remove it, so the next check
151
+ // tries again.
152
+ return Cache.get(cache, key).pipe(
153
+ Effect.tapError(() => Cache.invalidate(cache, key)),
154
+ )
155
+ }
156
+
157
+ const withServices = <A, E>(
158
+ effect: Effect.Effect<A, E, unknown>,
159
+ ): Effect.Effect<A, E, unknown> => Effect.provideContext(effect, services)
160
+
161
+ const current = Effect.gen(function* () {
162
+ const principal = yield* options.principal
163
+ const value = yield* policyFor(principal)
164
+ return { principal, policy: value }
165
+ })
166
+
167
+ const decide = Effect.fnUntraced(function* (target: AnyTarget) {
168
+ const { principal, policy: value } = yield* current
169
+ const decision: Decision.Decision = yield* withServices(
170
+ policy.decideOf(value, { ...target, principal } as never),
171
+ )
172
+ return decision
173
+ })
174
+
175
+ const kindDecision = (
176
+ value: Policy.Any,
177
+ principal: object,
178
+ target: AnyTarget,
179
+ ): Decision.Decision => {
180
+ const pair = policy.residualPair(value, {
181
+ ...target,
182
+ principal,
183
+ } as never)
184
+ if (pair.allow._tag === "True") {
185
+ return Decision.Allow({})
186
+ }
187
+ if (pair.deny._tag === "True") {
188
+ return Decision.Deny({})
189
+ }
190
+ return Decision.NotApplicable
191
+ }
192
+
193
+ const check = Effect.fnUntraced(function* (target: AnyTarget) {
194
+ const { principal, policy: value } = yield* current
195
+ const kind =
196
+ target.resource === undefined
197
+ ? (target.kind ?? "")
198
+ : (kindOf(target.resource) ?? "")
199
+ let decision: Decision.Decision =
200
+ target.resource === undefined
201
+ ? kindDecision(value, principal, target)
202
+ : yield* withServices(
203
+ policy.decideOf(value, { ...target, principal } as never),
204
+ )
205
+ if (
206
+ decision._tag === "NotApplicable" &&
207
+ options.decider !== undefined
208
+ ) {
209
+ decision = yield* options.decider({ ...target, principal })
210
+ }
211
+ if (decision._tag !== "Allow") {
212
+ return yield* forbidden(target, kind, Decision.reason(decision))
213
+ }
214
+ })
215
+
216
+ const guard = Effect.fnUntraced(function* (
217
+ target: AnyTarget,
218
+ guardOptions: {
219
+ readonly read?: string
220
+ readonly hide: () => unknown
221
+ },
222
+ ) {
223
+ const read = guardOptions.read ?? "read"
224
+ const { field: _field, ...rest } = target
225
+ const visible: Decision.Decision = yield* decide({
226
+ ...rest,
227
+ action: read,
228
+ })
229
+ if (visible._tag !== "Allow") {
230
+ return yield* Effect.fail(guardOptions.hide())
231
+ }
232
+ if (target.action !== read || target.field !== undefined) {
233
+ yield* check(target)
234
+ }
235
+ })
236
+
237
+ const filter = (
238
+ target: AnyTarget,
239
+ ): Effect.Effect<Condition.Ast, unknown, unknown> =>
240
+ Effect.map(current, ({ principal, policy: value }) =>
241
+ policy.residualOf(value, { ...target, principal } as never),
242
+ )
243
+
244
+ const permittedFields = Effect.fnUntraced(function* (target: AnyTarget) {
245
+ const { principal, policy: value } = yield* current
246
+ return yield* withServices(
247
+ policy.permittedFieldsOf(value, { ...target, principal } as never),
248
+ )
249
+ })
250
+
251
+ const invalidate = (principal?: object) =>
252
+ cache === undefined
253
+ ? Effect.void
254
+ : principal === undefined
255
+ ? Cache.invalidateAll(cache)
256
+ : Cache.invalidate(cache, keyOf(principal))
257
+
258
+ return {
259
+ domain,
260
+ principal: options.principal,
261
+ policy: Effect.map(current, ({ policy: value }) => value),
262
+ decide,
263
+ check,
264
+ guard,
265
+ filter,
266
+ permittedFields,
267
+ invalidate,
268
+ } as never
269
+ }),
270
+ )
271
+ }