@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.
- package/README.md +353 -170
- package/dist/Authorizer.d.ts +216 -0
- package/dist/Authorizer.d.ts.map +1 -0
- package/dist/Authorizer.js +55 -0
- package/dist/Authorizer.js.map +1 -0
- package/dist/Condition.d.ts +536 -0
- package/dist/Condition.d.ts.map +1 -0
- package/dist/Condition.js +141 -0
- package/dist/Condition.js.map +1 -0
- package/dist/Decision.d.ts +131 -0
- package/dist/Decision.d.ts.map +1 -0
- package/dist/Decision.js +83 -0
- package/dist/Decision.js.map +1 -0
- package/dist/Domain.d.ts +148 -0
- package/dist/Domain.d.ts.map +1 -0
- package/dist/Domain.js +34 -0
- package/dist/Domain.js.map +1 -0
- package/dist/HttpApi.d.ts +129 -0
- package/dist/HttpApi.d.ts.map +1 -0
- package/dist/HttpApi.js +95 -0
- package/dist/HttpApi.js.map +1 -0
- package/dist/Policy.d.ts +429 -0
- package/dist/Policy.d.ts.map +1 -0
- package/dist/Policy.js +162 -0
- package/dist/Policy.js.map +1 -0
- package/dist/Principal.d.ts +35 -0
- package/dist/Principal.d.ts.map +1 -0
- package/dist/Principal.js +37 -0
- package/dist/Principal.js.map +1 -0
- package/dist/Resource.d.ts +213 -0
- package/dist/Resource.d.ts.map +1 -0
- package/dist/Resource.js +71 -0
- package/dist/Resource.js.map +1 -0
- package/dist/Rule.d.ts +189 -0
- package/dist/Rule.d.ts.map +1 -0
- package/dist/Rule.js +50 -0
- package/dist/Rule.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.js +1 -2
- package/dist/index.d.ts +9 -4
- package/dist/index.js +9 -4
- package/dist/internal/authorizer.d.ts +28 -0
- package/dist/internal/authorizer.d.ts.map +1 -0
- package/dist/internal/authorizer.js +153 -0
- package/dist/internal/authorizer.js.map +1 -0
- package/dist/internal/condition.d.ts +11 -0
- package/dist/internal/condition.d.ts.map +1 -0
- package/dist/internal/condition.js +393 -0
- package/dist/internal/condition.js.map +1 -0
- package/dist/internal/domain.js +34 -0
- package/dist/internal/domain.js.map +1 -0
- package/dist/internal/httpApi.js +41 -0
- package/dist/internal/httpApi.js.map +1 -0
- package/dist/internal/policy.js +524 -0
- package/dist/internal/policy.js.map +1 -0
- package/dist/internal/resource.js +66 -0
- package/dist/internal/resource.js.map +1 -0
- package/dist/internal/rule.js +131 -0
- package/dist/internal/rule.js.map +1 -0
- package/package.json +61 -74
- package/src/Authorizer.ts +330 -0
- package/src/Condition.ts +706 -0
- package/src/Decision.ts +117 -0
- package/src/Domain.ts +189 -0
- package/src/HttpApi.ts +148 -0
- package/src/Policy.ts +575 -0
- package/src/Principal.ts +41 -0
- package/src/Resource.ts +290 -0
- package/src/Rule.ts +265 -0
- package/src/index.ts +57 -0
- package/src/internal/authorizer.ts +271 -0
- package/src/internal/condition.ts +607 -0
- package/src/internal/domain.ts +56 -0
- package/src/internal/httpApi.ts +84 -0
- package/src/internal/policy.ts +815 -0
- package/src/internal/resource.ts +98 -0
- package/src/internal/rule.ts +203 -0
- package/dist/Ability.d.ts +0 -516
- package/dist/Ability.js +0 -211
- package/dist/AbilityExtra.d.ts +0 -79
- package/dist/AbilityExtra.js +0 -195
- package/dist/AbilityRef.d.ts +0 -110
- package/dist/AbilityRef.js +0 -160
- package/dist/internal/ability.d.ts +0 -114
- package/dist/internal/ability.js +0 -624
- 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
|
+
}
|