@nmnmcc/ability 1.0.1 → 2.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 +112 -69
- package/dist/Authorizer.d.ts +43 -47
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js +5 -290
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +164 -332
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +75 -890
- package/dist/Condition.js.map +1 -1
- package/dist/Decision.d.ts +13 -13
- package/dist/Decision.d.ts.map +1 -1
- package/dist/Decision.js.map +1 -1
- package/dist/Domain.d.ts +10 -21
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +4 -62
- package/dist/Domain.js.map +1 -1
- package/dist/HttpApi.d.ts +31 -7
- package/dist/HttpApi.d.ts.map +1 -1
- package/dist/HttpApi.js +25 -8
- package/dist/HttpApi.js.map +1 -1
- package/dist/Limits.d.ts +114 -0
- package/dist/Limits.d.ts.map +1 -0
- package/dist/Limits.js +132 -0
- package/dist/Limits.js.map +1 -0
- package/dist/Policy.d.ts +276 -269
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +73 -1177
- package/dist/Policy.js.map +1 -1
- package/dist/Resource.d.ts +4 -177
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +4 -803
- package/dist/Resource.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +4 -5
- package/dist/internal/algebra.js +267 -0
- package/dist/internal/algebra.js.map +1 -0
- package/dist/internal/analysis.js +245 -0
- package/dist/internal/analysis.js.map +1 -0
- package/dist/internal/authorizer.d.ts +30 -0
- package/dist/internal/authorizer.d.ts.map +1 -0
- package/dist/internal/authorizer.js +221 -0
- package/dist/internal/authorizer.js.map +1 -0
- package/dist/internal/bind.js +311 -0
- package/dist/internal/bind.js.map +1 -0
- package/dist/internal/catalog.d.ts +56 -0
- package/dist/internal/catalog.d.ts.map +1 -0
- package/dist/internal/catalog.js +292 -0
- package/dist/internal/catalog.js.map +1 -0
- package/dist/internal/decide.js +166 -0
- package/dist/internal/decide.js.map +1 -0
- package/dist/internal/domain.js +32 -0
- package/dist/internal/domain.js.map +1 -0
- package/dist/internal/json.js +134 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/internal/limits.d.ts +14 -0
- package/dist/internal/limits.d.ts.map +1 -0
- package/dist/internal/limits.js +43 -0
- package/dist/internal/limits.js.map +1 -0
- package/dist/internal/nodes.d.ts +121 -0
- package/dist/internal/nodes.d.ts.map +1 -0
- package/dist/internal/nodes.js +277 -0
- package/dist/internal/nodes.js.map +1 -0
- package/dist/internal/patterns.js +110 -0
- package/dist/internal/patterns.js.map +1 -0
- package/dist/internal/policy-json.js +265 -0
- package/dist/internal/policy-json.js.map +1 -0
- package/dist/internal/policy-nodes.d.ts +61 -0
- package/dist/internal/policy-nodes.d.ts.map +1 -0
- package/dist/internal/policy-nodes.js +146 -0
- package/dist/internal/policy-nodes.js.map +1 -0
- package/dist/internal/policy.d.ts +42 -0
- package/dist/internal/policy.d.ts.map +1 -0
- package/dist/internal/policy.js +124 -0
- package/dist/internal/policy.js.map +1 -0
- package/dist/internal/read.js +39 -0
- package/dist/internal/read.js.map +1 -0
- package/dist/internal/resource.js +115 -0
- package/dist/internal/resource.js.map +1 -0
- package/dist/internal/rule.d.ts +26 -0
- package/dist/internal/rule.d.ts.map +1 -0
- package/dist/internal/rule.js +103 -0
- package/dist/internal/rule.js.map +1 -0
- package/dist/internal/search.d.ts +22 -0
- package/dist/internal/search.d.ts.map +1 -0
- package/dist/internal/search.js +274 -0
- package/dist/internal/search.js.map +1 -0
- package/dist/internal/suggest.js +46 -0
- package/dist/internal/suggest.js.map +1 -0
- package/package.json +3 -4
- package/src/Authorizer.ts +67 -583
- package/src/Condition.ts +339 -2095
- package/src/Decision.ts +6 -5
- package/src/Domain.ts +12 -69
- package/src/HttpApi.ts +72 -10
- package/src/Limits.ts +132 -0
- package/src/Policy.ts +391 -2772
- package/src/Resource.ts +7 -1548
- package/src/index.ts +3 -9
- package/src/internal/algebra.ts +482 -0
- package/src/internal/analysis.ts +478 -0
- package/src/internal/authorizer.ts +418 -0
- package/src/internal/bind.ts +648 -0
- package/src/internal/catalog.ts +534 -0
- package/src/internal/decide.ts +402 -0
- package/src/internal/domain.ts +66 -0
- package/src/internal/json.ts +260 -0
- package/src/internal/limits.ts +78 -0
- package/src/internal/nodes.ts +404 -0
- package/src/internal/patterns.ts +211 -0
- package/src/internal/policy-json.ts +487 -0
- package/src/internal/policy-nodes.ts +207 -0
- package/src/internal/policy.ts +264 -0
- package/src/internal/read.ts +55 -0
- package/src/internal/resource.ts +180 -0
- package/src/internal/rule.ts +196 -0
- package/src/internal/search.ts +541 -0
- package/src/internal/suggest.ts +104 -0
- package/dist/Principal.d.ts +0 -35
- package/dist/Principal.d.ts.map +0 -1
- package/dist/Principal.js +0 -37
- package/dist/Principal.js.map +0 -1
- package/dist/Rule.d.ts +0 -275
- package/dist/Rule.d.ts.map +0 -1
- package/dist/Rule.js +0 -257
- package/dist/Rule.js.map +0 -1
- package/src/Principal.ts +0 -41
- package/src/Rule.ts +0 -683
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The decision walk of a policy: `decide`, `explain` and `permittedFields`.
|
|
3
|
+
* The types are in `Policy.ts`.
|
|
4
|
+
*/
|
|
5
|
+
// A policy keeps the error type and the service type of its predicates in the
|
|
6
|
+
// public types. The code below works on the nodes with no types, where they
|
|
7
|
+
// are `unknown`, so the language service check for `unknown` in an Effect is
|
|
8
|
+
// off here.
|
|
9
|
+
// @effect-diagnostics anyUnknownInErrorContext:off
|
|
10
|
+
import * as Arr from "effect/Array"
|
|
11
|
+
import * as Chunk from "effect/Chunk"
|
|
12
|
+
import * as Effect from "effect/Effect"
|
|
13
|
+
import * as Option from "effect/Option"
|
|
14
|
+
import * as Ref from "effect/Ref"
|
|
15
|
+
import * 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 Resource from "../Resource.ts"
|
|
20
|
+
import * as Catalog from "./catalog.ts"
|
|
21
|
+
import * as DomainData from "./domain.ts"
|
|
22
|
+
import type * as PolicyNodes from "./policy-nodes.ts"
|
|
23
|
+
import { nodeOf } from "./policy.ts"
|
|
24
|
+
import * as ResourceData from "./resource.ts"
|
|
25
|
+
import * as RuleData from "./rule.ts"
|
|
26
|
+
|
|
27
|
+
type Any = Policy.Any
|
|
28
|
+
type AnyRequest = Policy.AnyRequest
|
|
29
|
+
type AnyRule = Policy.AnyRule
|
|
30
|
+
type Explanation = Policy.Explanation
|
|
31
|
+
type Step = Policy.Step
|
|
32
|
+
|
|
33
|
+
interface DecideContext {
|
|
34
|
+
readonly action: string
|
|
35
|
+
readonly kind: string
|
|
36
|
+
readonly field: string | undefined
|
|
37
|
+
readonly input: Condition.Input
|
|
38
|
+
/** The resource of the kind, for the fields that a rule may grant. */
|
|
39
|
+
readonly descriptor: Resource.Descriptor
|
|
40
|
+
/** The rules that were tested, for {@link explain}. */
|
|
41
|
+
readonly trace: Ref.Ref<Chunk.Chunk<Step>> | undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ruleApplies = (
|
|
45
|
+
value: RuleData.Rule,
|
|
46
|
+
context: DecideContext,
|
|
47
|
+
path: ReadonlyArray<number>,
|
|
48
|
+
): Effect.Effect<boolean, unknown, unknown> => {
|
|
49
|
+
if (!RuleData.matchesTarget(value, context)) {
|
|
50
|
+
return Effect.succeed(false)
|
|
51
|
+
}
|
|
52
|
+
const test =
|
|
53
|
+
value.when === undefined
|
|
54
|
+
? Effect.succeed(true)
|
|
55
|
+
: // A rule of a node keeps no type of what it reads. The decision gives
|
|
56
|
+
// it the value and the principal of the request.
|
|
57
|
+
Condition.evaluate(value.when as Condition.Any, context.input)
|
|
58
|
+
const trace = context.trace
|
|
59
|
+
return trace === undefined
|
|
60
|
+
? test
|
|
61
|
+
: Effect.tap(test, (applies) =>
|
|
62
|
+
// A chunk adds a step with no copy of the steps before.
|
|
63
|
+
Ref.update(trace, Chunk.append({ path, rule: typed(value), applies })),
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A rule of a node, with the type of a rule of no domain. */
|
|
68
|
+
const typed = (value: RuleData.Rule): AnyRule => value as never
|
|
69
|
+
|
|
70
|
+
const fromRule = (value: RuleData.Rule): Decision.Decision =>
|
|
71
|
+
value._tag === "Allow"
|
|
72
|
+
? Decision.Allow({ rule: typed(value) })
|
|
73
|
+
: value.reason === undefined
|
|
74
|
+
? Decision.Deny({ rule: typed(value) })
|
|
75
|
+
: Decision.Deny({ rule: typed(value), reason: value.reason })
|
|
76
|
+
|
|
77
|
+
/** Gives the first rule that applies, in order. */
|
|
78
|
+
const firstApplying = (
|
|
79
|
+
candidates: ReadonlyArray<readonly [RuleData.Rule, number]>,
|
|
80
|
+
context: DecideContext,
|
|
81
|
+
path: ReadonlyArray<number>,
|
|
82
|
+
): Effect.Effect<Option.Option<RuleData.Rule>, unknown, unknown> =>
|
|
83
|
+
Effect.map(
|
|
84
|
+
Effect.findFirst(candidates, ([value, index]) =>
|
|
85
|
+
ruleApplies(value, context, [...path, index]),
|
|
86
|
+
),
|
|
87
|
+
Option.map(([value]) => value),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
/** Gives the first result that is found, and tests `next` only when none is. */
|
|
91
|
+
const orElse = <A, E, R>(
|
|
92
|
+
first: Effect.Effect<Option.Option<A>, E, R>,
|
|
93
|
+
next: () => Effect.Effect<Option.Option<A>, E, R>,
|
|
94
|
+
): Effect.Effect<Option.Option<A>, E, R> =>
|
|
95
|
+
Effect.filterOrElse(first, Option.isSome, next)
|
|
96
|
+
|
|
97
|
+
const decideRules = (
|
|
98
|
+
node: PolicyNodes.Rules,
|
|
99
|
+
context: DecideContext,
|
|
100
|
+
path: ReadonlyArray<number>,
|
|
101
|
+
): Effect.Effect<Decision.Decision, unknown, unknown> => {
|
|
102
|
+
const indexed = Arr.map(node.rules, (value, index) => [value, index] as const)
|
|
103
|
+
const denies = Arr.filter(indexed, ([value]) => value._tag === "Deny")
|
|
104
|
+
const allows = Arr.filter(indexed, ([value]) => value._tag === "Allow")
|
|
105
|
+
const first = (candidates: ReadonlyArray<readonly [RuleData.Rule, number]>) =>
|
|
106
|
+
firstApplying(candidates, context, path)
|
|
107
|
+
const found = (() => {
|
|
108
|
+
switch (node.algorithm) {
|
|
109
|
+
case "denyOverrides":
|
|
110
|
+
return orElse(first(denies), () => first(allows))
|
|
111
|
+
case "permitOverrides":
|
|
112
|
+
return orElse(first(allows), () => first(denies))
|
|
113
|
+
case "firstApplicable":
|
|
114
|
+
return first(indexed)
|
|
115
|
+
case "lastApplicable":
|
|
116
|
+
return first(Arr.reverse(indexed))
|
|
117
|
+
}
|
|
118
|
+
})()
|
|
119
|
+
return Effect.map(
|
|
120
|
+
found,
|
|
121
|
+
Option.match({ onNone: () => Decision.NotApplicable, onSome: fromRule }),
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The next step of a walk over children: stop with a decision, or go on. */
|
|
126
|
+
type WalkStep<S> =
|
|
127
|
+
| { readonly _tag: "Stop"; readonly decision: Decision.Decision }
|
|
128
|
+
| { readonly _tag: "Next"; readonly state: S }
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Decides the children in order. After each child, `step` stops with a
|
|
132
|
+
* decision, or gives the state for the next child. After the last child,
|
|
133
|
+
* `end` gives the decision. A child after a stop is not decided, so its
|
|
134
|
+
* predicates do not run.
|
|
135
|
+
*/
|
|
136
|
+
const walkChildren = <S>(
|
|
137
|
+
children: ReadonlyArray<PolicyNodes.Node>,
|
|
138
|
+
context: DecideContext,
|
|
139
|
+
path: ReadonlyArray<number>,
|
|
140
|
+
start: S,
|
|
141
|
+
step: (state: S, decision: Decision.Decision) => WalkStep<S>,
|
|
142
|
+
end: (state: S) => Decision.Decision,
|
|
143
|
+
): Effect.Effect<Decision.Decision, unknown, unknown> => {
|
|
144
|
+
const go = (
|
|
145
|
+
index: number,
|
|
146
|
+
state: S,
|
|
147
|
+
): Effect.Effect<Decision.Decision, unknown, unknown> =>
|
|
148
|
+
index === children.length
|
|
149
|
+
? Effect.succeed(end(state))
|
|
150
|
+
: Effect.flatMap(
|
|
151
|
+
decideNode(children[index]!, context, [...path, index]),
|
|
152
|
+
(decision) => {
|
|
153
|
+
const next = step(state, decision)
|
|
154
|
+
return next._tag === "Stop"
|
|
155
|
+
? Effect.succeed(next.decision)
|
|
156
|
+
: go(index + 1, next.state)
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
return go(0, start)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const stop = (decision: Decision.Decision): WalkStep<never> => ({
|
|
163
|
+
_tag: "Stop",
|
|
164
|
+
decision,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const next = <S>(state: S): WalkStep<S> => ({ _tag: "Next", state })
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Decides a node. A shared child is decided again for each place, so its
|
|
171
|
+
* predicates run each time, as the policy says. The limits of the nodes keep
|
|
172
|
+
* this work small.
|
|
173
|
+
*/
|
|
174
|
+
export const decideNode = (
|
|
175
|
+
node: PolicyNodes.Node,
|
|
176
|
+
context: DecideContext,
|
|
177
|
+
path: ReadonlyArray<number>,
|
|
178
|
+
): Effect.Effect<Decision.Decision, unknown, unknown> => {
|
|
179
|
+
switch (node._tag) {
|
|
180
|
+
case "Rules":
|
|
181
|
+
return decideRules(node, context, path)
|
|
182
|
+
case "All":
|
|
183
|
+
// One deny is enough to deny. Every child must allow.
|
|
184
|
+
return walkChildren(
|
|
185
|
+
node.policies,
|
|
186
|
+
context,
|
|
187
|
+
path,
|
|
188
|
+
{
|
|
189
|
+
firstAllow: Option.none<Decision.Decision>(),
|
|
190
|
+
allAllow: node.policies.length > 0,
|
|
191
|
+
},
|
|
192
|
+
(state, decision) =>
|
|
193
|
+
decision._tag === "Deny"
|
|
194
|
+
? stop(decision)
|
|
195
|
+
: next({
|
|
196
|
+
firstAllow: Option.orElse(state.firstAllow, () =>
|
|
197
|
+
decision._tag === "Allow"
|
|
198
|
+
? Option.some(decision)
|
|
199
|
+
: Option.none(),
|
|
200
|
+
),
|
|
201
|
+
allAllow: state.allAllow && decision._tag === "Allow",
|
|
202
|
+
}),
|
|
203
|
+
(state) =>
|
|
204
|
+
state.allAllow
|
|
205
|
+
? Option.getOrElse(state.firstAllow, () => Decision.NotApplicable)
|
|
206
|
+
: Decision.NotApplicable,
|
|
207
|
+
)
|
|
208
|
+
case "Any":
|
|
209
|
+
// One allow is enough. Without one, the first deny denies.
|
|
210
|
+
return walkChildren(
|
|
211
|
+
node.policies,
|
|
212
|
+
context,
|
|
213
|
+
path,
|
|
214
|
+
Option.none<Decision.Decision>(),
|
|
215
|
+
(firstDeny, decision) =>
|
|
216
|
+
decision._tag === "Allow"
|
|
217
|
+
? stop(decision)
|
|
218
|
+
: next(
|
|
219
|
+
Option.orElse(firstDeny, () =>
|
|
220
|
+
decision._tag === "Deny"
|
|
221
|
+
? Option.some(decision)
|
|
222
|
+
: Option.none(),
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
(firstDeny) =>
|
|
226
|
+
Option.getOrElse(firstDeny, () => Decision.NotApplicable),
|
|
227
|
+
)
|
|
228
|
+
case "FirstOf":
|
|
229
|
+
return walkChildren(
|
|
230
|
+
node.policies,
|
|
231
|
+
context,
|
|
232
|
+
path,
|
|
233
|
+
undefined,
|
|
234
|
+
(_, decision) =>
|
|
235
|
+
decision._tag === "NotApplicable" ? next(undefined) : stop(decision),
|
|
236
|
+
() => Decision.NotApplicable,
|
|
237
|
+
)
|
|
238
|
+
case "Ceiling":
|
|
239
|
+
// A deny of the cap wins. Else the policy decides.
|
|
240
|
+
return Effect.filterOrElse(
|
|
241
|
+
decideNode(node.cap, context, [...path, 0]),
|
|
242
|
+
(capped) => capped._tag === "Deny",
|
|
243
|
+
() => decideNode(node.policy, context, [...path, 1]),
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Tests if a request fits its kind: the action is an action of the kind, and
|
|
250
|
+
* the field, if there is one, is a field of the kind.
|
|
251
|
+
*/
|
|
252
|
+
export const fitsKind = (
|
|
253
|
+
descriptor: Resource.Descriptor,
|
|
254
|
+
request: { readonly action: string; readonly field?: string | undefined },
|
|
255
|
+
): boolean =>
|
|
256
|
+
descriptor.actions.includes(request.action) &&
|
|
257
|
+
(request.field === undefined ||
|
|
258
|
+
(Catalog.lookup(descriptor.catalog, request.field)?.listed ?? false))
|
|
259
|
+
|
|
260
|
+
const makeContext = (
|
|
261
|
+
domain: Domain.Any,
|
|
262
|
+
request: AnyRequest,
|
|
263
|
+
trace: Ref.Ref<Chunk.Chunk<Step>> | undefined,
|
|
264
|
+
): DecideContext | undefined => {
|
|
265
|
+
const kind = ResourceData.kindOf(request.resource)
|
|
266
|
+
const descriptor =
|
|
267
|
+
kind === undefined ? undefined : DomainData.descriptorOf(domain, kind)
|
|
268
|
+
if (
|
|
269
|
+
kind === undefined ||
|
|
270
|
+
descriptor === undefined ||
|
|
271
|
+
!fitsKind(descriptor, request)
|
|
272
|
+
) {
|
|
273
|
+
return undefined
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
action: request.action,
|
|
277
|
+
kind,
|
|
278
|
+
field: request.field,
|
|
279
|
+
input: {
|
|
280
|
+
value: request.resource,
|
|
281
|
+
principal: request.principal,
|
|
282
|
+
path: descriptor.path,
|
|
283
|
+
},
|
|
284
|
+
descriptor,
|
|
285
|
+
trace,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export const decideAny = (
|
|
290
|
+
policy: Any,
|
|
291
|
+
request: AnyRequest,
|
|
292
|
+
): Effect.Effect<Decision.Decision, unknown, unknown> =>
|
|
293
|
+
// The context reads the resource value, so it is made for each run.
|
|
294
|
+
Effect.suspend(() => {
|
|
295
|
+
const context = makeContext(policy.domain, request, undefined)
|
|
296
|
+
return context === undefined
|
|
297
|
+
? Effect.succeed(Decision.NotApplicable)
|
|
298
|
+
: decideNode(nodeOf(policy), context, [])
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
/** Gives the decision of a policy with no predicates, at once. */
|
|
302
|
+
export const decideSync = (
|
|
303
|
+
policy: Any,
|
|
304
|
+
request: AnyRequest,
|
|
305
|
+
): Decision.Decision =>
|
|
306
|
+
Effect.runSync(
|
|
307
|
+
// The public type of `decideSync` takes only policies with no
|
|
308
|
+
// predicates, so this Effect has no error and needs no services.
|
|
309
|
+
// @effect-diagnostics-next-line unsafeEffectTypeAssertion:off
|
|
310
|
+
decideAny(policy, request) as Effect.Effect<Decision.Decision>,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
/** Gives the decision of a policy and the rules that were tested to make it. */
|
|
314
|
+
export const explain = (
|
|
315
|
+
policy: Any,
|
|
316
|
+
request: AnyRequest,
|
|
317
|
+
): Effect.Effect<Explanation, unknown, unknown> =>
|
|
318
|
+
// Each run of the Effect needs its own trace, so the trace is made inside
|
|
319
|
+
// the Effect.
|
|
320
|
+
Effect.gen(function* () {
|
|
321
|
+
const trace = yield* Ref.make(Chunk.empty<Step>())
|
|
322
|
+
const context = makeContext(policy.domain, request, trace)
|
|
323
|
+
if (context === undefined) {
|
|
324
|
+
return { decision: Decision.NotApplicable, steps: [] }
|
|
325
|
+
}
|
|
326
|
+
const decision = yield* decideNode(nodeOf(policy), context, [])
|
|
327
|
+
return { decision, steps: Chunk.toReadonlyArray(yield* Ref.get(trace)) }
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Tests if a deny rule of the request names a part inside `field`.
|
|
332
|
+
*/
|
|
333
|
+
const deniesInside = (
|
|
334
|
+
node: PolicyNodes.Node,
|
|
335
|
+
target: { readonly action: string; readonly kind: string },
|
|
336
|
+
field: string,
|
|
337
|
+
): boolean => {
|
|
338
|
+
switch (node._tag) {
|
|
339
|
+
case "Rules":
|
|
340
|
+
return node.rules.some(
|
|
341
|
+
(value) =>
|
|
342
|
+
value._tag === "Deny" &&
|
|
343
|
+
value.fields !== undefined &&
|
|
344
|
+
value.fields.some((pattern) =>
|
|
345
|
+
RuleData.matchesInside(pattern, field),
|
|
346
|
+
) &&
|
|
347
|
+
(value.actions.includes("*") ||
|
|
348
|
+
value.actions.includes(target.action)) &&
|
|
349
|
+
(value.resources.includes("*") ||
|
|
350
|
+
value.resources.includes(target.kind)),
|
|
351
|
+
)
|
|
352
|
+
case "All":
|
|
353
|
+
case "Any":
|
|
354
|
+
case "FirstOf":
|
|
355
|
+
return node.policies.some((child) => deniesInside(child, target, field))
|
|
356
|
+
case "Ceiling":
|
|
357
|
+
return (
|
|
358
|
+
deniesInside(node.cap, target, field) ||
|
|
359
|
+
deniesInside(node.policy, target, field)
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Gives the fields of a resource value that the policy lets a request use. */
|
|
365
|
+
export const permittedFields = (
|
|
366
|
+
policy: Any,
|
|
367
|
+
request: Omit<AnyRequest, "field">,
|
|
368
|
+
): Effect.Effect<ReadonlyArray<string>, unknown, unknown> => {
|
|
369
|
+
const kind = ResourceData.kindOf(request.resource)
|
|
370
|
+
const descriptor: Resource.Descriptor | undefined =
|
|
371
|
+
kind === undefined
|
|
372
|
+
? undefined
|
|
373
|
+
: DomainData.descriptorOf(policy.domain, kind)
|
|
374
|
+
if (descriptor === undefined) {
|
|
375
|
+
return Effect.succeed([])
|
|
376
|
+
}
|
|
377
|
+
// The fields with names only: a list can not name every key of a record.
|
|
378
|
+
const listed = Arr.filter(
|
|
379
|
+
descriptor.catalog.fields,
|
|
380
|
+
(field) =>
|
|
381
|
+
field.listed &&
|
|
382
|
+
Arr.every(field.path, (segment) => typeof segment === "string"),
|
|
383
|
+
)
|
|
384
|
+
return Effect.map(
|
|
385
|
+
Effect.filter(listed, (field) =>
|
|
386
|
+
Effect.map(
|
|
387
|
+
decideAny(policy, { ...request, field: field.text }),
|
|
388
|
+
(result) =>
|
|
389
|
+
result._tag === "Allow" &&
|
|
390
|
+
!(
|
|
391
|
+
field.open &&
|
|
392
|
+
deniesInside(
|
|
393
|
+
nodeOf(policy),
|
|
394
|
+
{ ...request, kind: descriptor.kind },
|
|
395
|
+
field.text,
|
|
396
|
+
)
|
|
397
|
+
),
|
|
398
|
+
),
|
|
399
|
+
),
|
|
400
|
+
Arr.map((field) => field.text),
|
|
401
|
+
)
|
|
402
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values of domains. The types are in `Domain.ts`.
|
|
3
|
+
*/
|
|
4
|
+
import * as Arr from "effect/Array"
|
|
5
|
+
import * as Data from "effect/Data"
|
|
6
|
+
import * as HashMap from "effect/HashMap"
|
|
7
|
+
import * as Option from "effect/Option"
|
|
8
|
+
import type * as Schema from "effect/Schema"
|
|
9
|
+
import type * as Domain from "../Domain.ts"
|
|
10
|
+
import type * as Resource from "../Resource.ts"
|
|
11
|
+
import * as Catalog from "./catalog.ts"
|
|
12
|
+
import * as ResourceData from "./resource.ts"
|
|
13
|
+
|
|
14
|
+
/** The class of every domain value. `isDomain` uses it to know a domain. */
|
|
15
|
+
class DomainData extends Data.Class<{
|
|
16
|
+
readonly principal: Schema.Top
|
|
17
|
+
readonly principalCatalog: Catalog.Catalog
|
|
18
|
+
readonly resources: HashMap.HashMap<string, Resource.Descriptor>
|
|
19
|
+
readonly meta: Schema.Top | undefined
|
|
20
|
+
}> {}
|
|
21
|
+
|
|
22
|
+
/** Makes a domain. Each resource must have its own kind name. */
|
|
23
|
+
export const make = (options: {
|
|
24
|
+
readonly principal: Schema.Top
|
|
25
|
+
readonly resources: ReadonlyArray<Resource.Any>
|
|
26
|
+
readonly meta?: Schema.Top
|
|
27
|
+
}): DomainData => {
|
|
28
|
+
const descriptors = Arr.map(options.resources, ResourceData.descriptor)
|
|
29
|
+
const kinds = Arr.map(descriptors, (descriptor) => descriptor.kind)
|
|
30
|
+
// Two resources with one kind name is a mistake in the code, not in the
|
|
31
|
+
// data, so it is a defect.
|
|
32
|
+
const twice = Arr.findFirst(
|
|
33
|
+
kinds,
|
|
34
|
+
(kind, index) => kinds.indexOf(kind) !== index,
|
|
35
|
+
)
|
|
36
|
+
if (Option.isSome(twice)) {
|
|
37
|
+
throw new Error(`Two resources have the kind name "${twice.value}"`)
|
|
38
|
+
}
|
|
39
|
+
if (kinds.includes("*")) {
|
|
40
|
+
throw new Error(`A resource may not have the kind name "*"`)
|
|
41
|
+
}
|
|
42
|
+
const resources = HashMap.fromIterable(
|
|
43
|
+
Arr.map(
|
|
44
|
+
descriptors,
|
|
45
|
+
(descriptor) => [descriptor.kind, descriptor] as const,
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
// The catalog checks the field names of the principal, so a bad name is a
|
|
49
|
+
// defect when the domain is made.
|
|
50
|
+
return new DomainData({
|
|
51
|
+
principal: options.principal,
|
|
52
|
+
principalCatalog: Catalog.catalogOf(options.principal.ast),
|
|
53
|
+
resources,
|
|
54
|
+
meta: options.meta,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Gives the descriptor of the resource with a kind name. */
|
|
59
|
+
export const descriptorOf = (
|
|
60
|
+
domain: Domain.Any,
|
|
61
|
+
kind: string,
|
|
62
|
+
): Resource.Descriptor | undefined =>
|
|
63
|
+
Option.getOrUndefined(HashMap.get(domain.resources, kind))
|
|
64
|
+
|
|
65
|
+
/** Tests if a value is a domain. */
|
|
66
|
+
export const isDomain = (u: unknown): u is Domain.Any => u instanceof DomainData
|