@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
package/dist/Policy.js
CHANGED
|
@@ -1,167 +1,70 @@
|
|
|
1
1
|
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { builder as builder$1, domainOf as domainOf$1, isRule, make, matchesInside, matchesTarget } from "./Rule.js";
|
|
7
|
-
import * as Arr from "effect/Array";
|
|
8
|
-
import * as Effect from "effect/Effect";
|
|
9
|
-
import * as Option from "effect/Option";
|
|
10
|
-
import * as Schema from "effect/Schema";
|
|
11
|
-
import * as Data from "effect/Data";
|
|
12
|
-
import { dual } from "effect/Function";
|
|
13
|
-
import * as HashMap from "effect/HashMap";
|
|
14
|
-
import * as HashSet from "effect/HashSet";
|
|
15
|
-
import { hasProperty } from "effect/Predicate";
|
|
16
|
-
import * as SchemaIssue from "effect/SchemaIssue";
|
|
17
|
-
import * as SchemaTransformation from "effect/SchemaTransformation";
|
|
18
|
-
import * as Chunk from "effect/Chunk";
|
|
19
|
-
import * as Ref from "effect/Ref";
|
|
2
|
+
import { builder as builder$1, makePlain } from "./internal/rule.js";
|
|
3
|
+
import { PolicyError as PolicyError$1, ceiling as ceiling$1, combine, define as define$1, provideContext as provideContext$1, schema as schema$1 } from "./internal/policy.js";
|
|
4
|
+
import { decideAny, decideSync as decideSync$1, explain as explain$1, permittedFields as permittedFields$1 } from "./internal/decide.js";
|
|
5
|
+
import { coverageAny, residual as residual$1 } from "./internal/analysis.js";
|
|
20
6
|
// src/Policy.ts
|
|
21
7
|
var Policy_exports = /* @__PURE__ */ __exportAll({
|
|
8
|
+
PolicyError: () => PolicyError,
|
|
22
9
|
all: () => all,
|
|
23
10
|
any: () => any,
|
|
11
|
+
builder: () => builder,
|
|
24
12
|
ceiling: () => ceiling,
|
|
25
13
|
coverage: () => coverage,
|
|
26
14
|
decide: () => decide,
|
|
27
15
|
decideSync: () => decideSync,
|
|
28
16
|
define: () => define,
|
|
29
|
-
empty: () => empty,
|
|
30
17
|
explain: () => explain,
|
|
31
18
|
firstOf: () => firstOf,
|
|
32
|
-
fitsKind: () => fitsKind,
|
|
33
|
-
isPolicy: () => isPolicy,
|
|
34
19
|
permittedFields: () => permittedFields,
|
|
35
20
|
provideContext: () => provideContext,
|
|
36
21
|
residual: () => residual,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
schema: () => schema,
|
|
40
|
-
scopeHasValue: () => scopeHasValue,
|
|
41
|
-
scopeIsEmpty: () => scopeIsEmpty
|
|
22
|
+
rule: () => rule,
|
|
23
|
+
schema: () => schema
|
|
42
24
|
});
|
|
43
|
-
const nodes = Data.taggedEnum();
|
|
44
25
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
default: return 1;
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
/**
|
|
63
|
-
* The number of nodes of the JSON form of a tree, with the conditions of its
|
|
64
|
-
* rules.
|
|
65
|
-
*/
|
|
66
|
-
const encodedSize = (node, memo = /* @__PURE__ */ new Map(), conditions = /* @__PURE__ */ new Map()) => remember(memo, node, () => {
|
|
67
|
-
switch (node._tag) {
|
|
68
|
-
case "Rules": return Arr.reduce(node.rules, 1, (size, value) => size + 1 + (value.when === void 0 ? 0 : conditionSize(value.when, conditions)));
|
|
69
|
-
case "All":
|
|
70
|
-
case "Any":
|
|
71
|
-
case "FirstOf": return Arr.reduce(node.policies, 1, (size, child) => size + encodedSize(child, memo, conditions));
|
|
72
|
-
case "Ceiling": return 1 + encodedSize(node.cap, memo, conditions) + encodedSize(node.policy, memo, conditions);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
/** Gives the JSON form of a tree, when it is not too large. */
|
|
76
|
-
const encodeTree = (node) => {
|
|
77
|
-
if (encodedSize(node) > maxEncodedNodes) throw new globalThis.Error(tooLargeMessage);
|
|
78
|
-
return encodeNode(node);
|
|
79
|
-
};
|
|
80
|
-
const tooLargeMessage = `The JSON form of this policy would have more than ${String(maxEncodedNodes)} nodes`;
|
|
81
|
-
const encodeNode = (node) => {
|
|
82
|
-
switch (node._tag) {
|
|
83
|
-
case "Rules": return {
|
|
84
|
-
_tag: "Rules",
|
|
85
|
-
algorithm: node.algorithm,
|
|
86
|
-
rules: node.rules.map(({ domain: _domain, ...value }) => ({ ...value }))
|
|
87
|
-
};
|
|
88
|
-
case "All":
|
|
89
|
-
case "Any":
|
|
90
|
-
case "FirstOf": return {
|
|
91
|
-
_tag: node._tag,
|
|
92
|
-
policies: node.policies.map(encodeNode)
|
|
93
|
-
};
|
|
94
|
-
case "Ceiling": return {
|
|
95
|
-
_tag: "Ceiling",
|
|
96
|
-
cap: encodeNode(node.cap),
|
|
97
|
-
policy: encodeNode(node.policy)
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
/**
|
|
102
|
-
* The tree of a policy as a value that `JSON.stringify` keeps with no loss.
|
|
103
|
-
* See `Condition.toJsonValue`.
|
|
26
|
+
* Makes rules with the types of a domain. `Policy.define` gives one to its
|
|
27
|
+
* generator. Use this one to make rules outside of a generator.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const _ = Policy.builder(App)
|
|
32
|
+
* const rule = _.allow("read", Document, {
|
|
33
|
+
* when: ($) => $.eq("ownerId", $.principal("id")),
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @stability stable
|
|
38
|
+
* @category constructors
|
|
39
|
+
* @since 2.0.0
|
|
104
40
|
*/
|
|
105
|
-
const
|
|
106
|
-
const json = (child) => jsonNode(child, meta);
|
|
107
|
-
switch (node._tag) {
|
|
108
|
-
case "Rules": return {
|
|
109
|
-
...node,
|
|
110
|
-
rules: Arr.map(node.rules, (value) => ({
|
|
111
|
-
...value,
|
|
112
|
-
...value.when === void 0 ? {} : { when: toJsonValue(value.when) },
|
|
113
|
-
...value.meta === void 0 || meta === void 0 ? {} : { meta: Schema.encodeUnknownSync(meta)(value.meta) }
|
|
114
|
-
}))
|
|
115
|
-
};
|
|
116
|
-
case "All":
|
|
117
|
-
case "Any":
|
|
118
|
-
case "FirstOf": return {
|
|
119
|
-
...node,
|
|
120
|
-
policies: Arr.map(node.policies, json)
|
|
121
|
-
};
|
|
122
|
-
case "Ceiling": return {
|
|
123
|
-
...node,
|
|
124
|
-
cap: json(node.cap),
|
|
125
|
-
policy: json(node.policy)
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
};
|
|
41
|
+
const builder = (domain) => builder$1(domain);
|
|
129
42
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
43
|
+
* Makes a rule from its data, with no types. It does not check the data
|
|
44
|
+
* against a domain: a policy does that when the rule goes into it. Use
|
|
45
|
+
* {@link builder} for rules with types, or {@link schema} for rules from JSON.
|
|
46
|
+
*
|
|
47
|
+
* The condition may not have a predicate. A policy checks the fields of a
|
|
48
|
+
* condition against the resources of its rule, but it can not check what
|
|
49
|
+
* the code of a predicate reads. A rule from the builder keeps that in its
|
|
50
|
+
* types; this one could give a predicate of one kind to another kind.
|
|
51
|
+
*
|
|
52
|
+
* @stability stable
|
|
53
|
+
* @category constructors
|
|
54
|
+
* @since 2.0.0
|
|
132
55
|
*/
|
|
133
|
-
|
|
134
|
-
toJSON() {
|
|
135
|
-
return jsonNode(encodeTree(this.node), this.domain.meta);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
/** The node of a policy, with no types, for the code below. */
|
|
139
|
-
const nodeOf = (policy) => policy.node;
|
|
140
|
-
const fromNode = (domain, node) => new PolicyData({
|
|
141
|
-
domain,
|
|
142
|
-
node
|
|
143
|
-
});
|
|
56
|
+
const rule = makePlain;
|
|
144
57
|
/**
|
|
145
|
-
*
|
|
58
|
+
* The error when a rule does not fit its domain: it names a kind, an action,
|
|
59
|
+
* a field or a field of the principal that the domain does not have, or it
|
|
60
|
+
* tests a field with a value that the field can not hold. It has one issue
|
|
61
|
+
* for each problem, with its path, and a suggestion when a name has a mistake.
|
|
146
62
|
*
|
|
147
63
|
* @stability stable
|
|
148
|
-
* @category
|
|
149
|
-
* @since 0.
|
|
64
|
+
* @category errors
|
|
65
|
+
* @since 2.0.0
|
|
150
66
|
*/
|
|
151
|
-
const
|
|
152
|
-
const node = nodes.Rules({
|
|
153
|
-
algorithm: options?.algorithm ?? "denyOverrides",
|
|
154
|
-
rules: Array.from(values)
|
|
155
|
-
});
|
|
156
|
-
if (!Arr.every(node.rules, isRule)) throw new globalThis.Error("A value in the list is not a rule");
|
|
157
|
-
if (Arr.some(node.rules, (value) => {
|
|
158
|
-
const origin = domainOf$1(value);
|
|
159
|
-
return origin !== void 0 && origin !== domain;
|
|
160
|
-
})) throw new globalThis.Error("A rule comes from the builder of another domain");
|
|
161
|
-
const issues = validate(domain, encodeNode(node));
|
|
162
|
-
if (issues.length > 0) throw new globalThis.Error(`A rule does not fit the domain: ${issues.map((issue) => issue.issue).join("; ")}`);
|
|
163
|
-
return fromNode(domain, node);
|
|
164
|
-
};
|
|
67
|
+
const PolicyError = PolicyError$1;
|
|
165
68
|
/**
|
|
166
69
|
* Makes a policy from a generator function that gives rules. It is pure and it
|
|
167
70
|
* gives its result at once.
|
|
@@ -180,50 +83,7 @@ const rules = (domain, values, options) => {
|
|
|
180
83
|
* @category constructors
|
|
181
84
|
* @since 0.3.0
|
|
182
85
|
*/
|
|
183
|
-
const define =
|
|
184
|
-
const iterator = body(builder$1(domain));
|
|
185
|
-
const collected = Arr.unfold(iterator.next(), (state) => state.done === true ? Option.none() : Option.some([state.value, iterator.next(state.value)]));
|
|
186
|
-
return rules(domain, collected, options);
|
|
187
|
-
};
|
|
188
|
-
/**
|
|
189
|
-
* A policy with no rules. It is not applicable to any request.
|
|
190
|
-
*
|
|
191
|
-
* @stability stable
|
|
192
|
-
* @category constructors
|
|
193
|
-
* @since 0.3.0
|
|
194
|
-
*/
|
|
195
|
-
const empty = (domain) => rules(domain, []);
|
|
196
|
-
const sameDomain = (policies) => {
|
|
197
|
-
if (!Arr.every(policies, isPolicy)) throw new globalThis.Error("A value in the list is not a policy");
|
|
198
|
-
const domain = policies[0].domain;
|
|
199
|
-
if (!Arr.every(policies, (policy) => policy.domain === domain)) throw new globalThis.Error("Policies from two domains can not be put together");
|
|
200
|
-
return domain;
|
|
201
|
-
};
|
|
202
|
-
/**
|
|
203
|
-
* The most levels in a policy tree. A walk over a tree calls itself for each
|
|
204
|
-
* level, so a much deeper tree could fill the stack and stop a check.
|
|
205
|
-
*/
|
|
206
|
-
const maxDepth = 256;
|
|
207
|
-
/** The number of levels of a policy tree. */
|
|
208
|
-
const heightOf = (node, memo = /* @__PURE__ */ new Map()) => remember(memo, node, () => {
|
|
209
|
-
const height = (child) => heightOf(child, memo);
|
|
210
|
-
switch (node._tag) {
|
|
211
|
-
case "Rules": return 1;
|
|
212
|
-
case "All":
|
|
213
|
-
case "Any":
|
|
214
|
-
case "FirstOf": return 1 + Arr.reduce(node.policies, 0, (top, child) => Math.max(top, height(child)));
|
|
215
|
-
case "Ceiling": return 1 + Math.max(height(node.cap), height(node.policy));
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
/**
|
|
219
|
-
* Makes a policy from a new node. Its children come from policies, so each
|
|
220
|
-
* has at most `maxDepth` levels, and the walk of `heightOf` is safe.
|
|
221
|
-
*/
|
|
222
|
-
const fromCombined = (domain, node) => {
|
|
223
|
-
if (heightOf(node) > maxDepth) throw new globalThis.Error(`A policy tree may have at most ${maxDepth} levels`);
|
|
224
|
-
return fromNode(domain, node);
|
|
225
|
-
};
|
|
226
|
-
const combine = (tag) => (policies) => fromCombined(sameDomain(policies), nodes[tag]({ policies: policies.map(nodeOf) }));
|
|
86
|
+
const define = define$1;
|
|
227
87
|
/**
|
|
228
88
|
* Every policy must allow, and one deny is enough to deny. With some policies
|
|
229
89
|
* not applicable and none that deny, the result is not applicable.
|
|
@@ -257,49 +117,7 @@ const firstOf = combine("FirstOf");
|
|
|
257
117
|
* @category combinators
|
|
258
118
|
* @since 0.3.0
|
|
259
119
|
*/
|
|
260
|
-
const ceiling =
|
|
261
|
-
cap: nodeOf(cap),
|
|
262
|
-
policy: nodeOf(policy)
|
|
263
|
-
})));
|
|
264
|
-
/**
|
|
265
|
-
* Gives the answer for a key from a memo, or works it out and keeps it. One
|
|
266
|
-
* node may be a child of many nodes, so each walk over a tree keeps its work
|
|
267
|
-
* on a node in a memo that the walk makes for itself. No memo lives longer
|
|
268
|
-
* than one call.
|
|
269
|
-
*
|
|
270
|
-
* The memo is a native `Map`, because it must find a node by the node itself.
|
|
271
|
-
* An Effect `HashMap` finds a key by `Equal`, and the nodes are Effect data,
|
|
272
|
-
* so it would hash the whole structure of a node, and a shared child again for
|
|
273
|
-
* each path to it.
|
|
274
|
-
*/
|
|
275
|
-
const remember = (memo, key, work) => {
|
|
276
|
-
if (memo.has(key)) return memo.get(key);
|
|
277
|
-
const result = work();
|
|
278
|
-
memo.set(key, result);
|
|
279
|
-
return result;
|
|
280
|
-
};
|
|
281
|
-
const provideNode = (node, context, memo = {
|
|
282
|
-
done: /* @__PURE__ */ new Map(),
|
|
283
|
-
predicates: /* @__PURE__ */ new Map()
|
|
284
|
-
}) => !nodeHasPredicate(node, memo.predicates) ? node : remember(memo.done, node, () => provideOnce(node, context, memo));
|
|
285
|
-
const provideOnce = (node, context, memo) => {
|
|
286
|
-
switch (node._tag) {
|
|
287
|
-
case "Rules": return nodes.Rules({
|
|
288
|
-
algorithm: node.algorithm,
|
|
289
|
-
rules: node.rules.map((value) => value.when === void 0 || !hasPredicate(value.when) ? value : make({
|
|
290
|
-
...value,
|
|
291
|
-
when: provideContext$1(value.when, context)
|
|
292
|
-
}))
|
|
293
|
-
});
|
|
294
|
-
case "All":
|
|
295
|
-
case "Any":
|
|
296
|
-
case "FirstOf": return nodes[node._tag]({ policies: Arr.map(node.policies, (child) => provideNode(child, context, memo)) });
|
|
297
|
-
case "Ceiling": return nodes.Ceiling({
|
|
298
|
-
cap: provideNode(node.cap, context, memo),
|
|
299
|
-
policy: provideNode(node.policy, context, memo)
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
};
|
|
120
|
+
const ceiling = ceiling$1;
|
|
303
121
|
/**
|
|
304
122
|
* Gives the services in `context` to every predicate of a policy, so the
|
|
305
123
|
* policy needs no services when it runs.
|
|
@@ -308,119 +126,7 @@ const provideOnce = (node, context, memo) => {
|
|
|
308
126
|
* @category services
|
|
309
127
|
* @since 1.0.0
|
|
310
128
|
*/
|
|
311
|
-
const provideContext =
|
|
312
|
-
const ruleApplies = (value, context, path) => {
|
|
313
|
-
if (!matchesTarget(value, context)) return Effect.succeed(false);
|
|
314
|
-
const test = value.when === void 0 ? Effect.succeed(true) : evaluate(value.when, context.input);
|
|
315
|
-
const trace = context.trace;
|
|
316
|
-
return trace === void 0 ? test : Effect.tap(test, (applies) => Ref.update(trace, Chunk.append({
|
|
317
|
-
path,
|
|
318
|
-
rule: value,
|
|
319
|
-
applies
|
|
320
|
-
})));
|
|
321
|
-
};
|
|
322
|
-
const fromRule = (value) => value._tag === "Allow" ? Allow({ rule: value }) : value.reason === void 0 ? Deny({ rule: value }) : Deny({
|
|
323
|
-
rule: value,
|
|
324
|
-
reason: value.reason
|
|
325
|
-
});
|
|
326
|
-
/** Gives the first rule that applies, in order. */
|
|
327
|
-
const firstApplying = (candidates, context, path) => Effect.map(Effect.findFirst(candidates, ([value, index]) => ruleApplies(value, context, [...path, index])), Option.map(([value]) => value));
|
|
328
|
-
/** Gives the first result that is found, and tests `next` only when none is. */
|
|
329
|
-
const orElse = (first, next) => Effect.filterOrElse(first, Option.isSome, next);
|
|
330
|
-
const decideRules = (node, context, path) => {
|
|
331
|
-
const indexed = Arr.map(node.rules, (value, index) => [value, index]);
|
|
332
|
-
const denies = Arr.filter(indexed, ([value]) => value._tag === "Deny");
|
|
333
|
-
const allows = Arr.filter(indexed, ([value]) => value._tag === "Allow");
|
|
334
|
-
const first = (candidates) => firstApplying(candidates, context, path);
|
|
335
|
-
const found = (() => {
|
|
336
|
-
switch (node.algorithm) {
|
|
337
|
-
case "denyOverrides": return orElse(first(denies), () => first(allows));
|
|
338
|
-
case "permitOverrides": return orElse(first(allows), () => first(denies));
|
|
339
|
-
case "firstApplicable": return first(indexed);
|
|
340
|
-
case "lastApplicable": return first(Arr.reverse(indexed));
|
|
341
|
-
}
|
|
342
|
-
})();
|
|
343
|
-
return Effect.map(found, Option.match({
|
|
344
|
-
onNone: () => NotApplicable,
|
|
345
|
-
onSome: fromRule
|
|
346
|
-
}));
|
|
347
|
-
};
|
|
348
|
-
const decideNode = (node, context, path) => {
|
|
349
|
-
if (usesPredicate(node, context)) return decideOnce(node, context, path);
|
|
350
|
-
const cached = context.decisions.get(node);
|
|
351
|
-
return cached !== void 0 ? Effect.succeed(cached) : Effect.tap(decideOnce(node, context, path), (decision) => Effect.sync(() => context.decisions.set(node, decision)));
|
|
352
|
-
};
|
|
353
|
-
/**
|
|
354
|
-
* Decides the children in order. After each child, `step` stops with a
|
|
355
|
-
* decision, or gives the state for the next child. After the last child,
|
|
356
|
-
* `end` gives the decision. A child after a stop is not decided, so its
|
|
357
|
-
* predicates do not run.
|
|
358
|
-
*/
|
|
359
|
-
const walkChildren = (children, context, path, start, step, end) => {
|
|
360
|
-
const go = (index, state) => index === children.length ? Effect.succeed(end(state)) : Effect.flatMap(decideNode(children[index], context, [...path, index]), (decision) => {
|
|
361
|
-
const next = step(state, decision);
|
|
362
|
-
return next._tag === "Stop" ? Effect.succeed(next.decision) : go(index + 1, next.state);
|
|
363
|
-
});
|
|
364
|
-
return go(0, start);
|
|
365
|
-
};
|
|
366
|
-
const stop = (decision) => ({
|
|
367
|
-
_tag: "Stop",
|
|
368
|
-
decision
|
|
369
|
-
});
|
|
370
|
-
const next = (state) => ({
|
|
371
|
-
_tag: "Next",
|
|
372
|
-
state
|
|
373
|
-
});
|
|
374
|
-
const decideOnce = (node, context, path) => {
|
|
375
|
-
switch (node._tag) {
|
|
376
|
-
case "Rules": return decideRules(node, context, path);
|
|
377
|
-
case "All": return walkChildren(node.policies, context, path, {
|
|
378
|
-
firstAllow: Option.none(),
|
|
379
|
-
allAllow: node.policies.length > 0
|
|
380
|
-
}, (state, decision) => decision._tag === "Deny" ? stop(decision) : next({
|
|
381
|
-
firstAllow: Option.orElse(state.firstAllow, () => decision._tag === "Allow" ? Option.some(decision) : Option.none()),
|
|
382
|
-
allAllow: state.allAllow && decision._tag === "Allow"
|
|
383
|
-
}), (state) => state.allAllow ? Option.getOrElse(state.firstAllow, () => NotApplicable) : NotApplicable);
|
|
384
|
-
case "Any": return walkChildren(node.policies, context, path, Option.none(), (firstDeny, decision) => decision._tag === "Allow" ? stop(decision) : next(Option.orElse(firstDeny, () => decision._tag === "Deny" ? Option.some(decision) : Option.none())), (firstDeny) => Option.getOrElse(firstDeny, () => NotApplicable));
|
|
385
|
-
case "FirstOf": return walkChildren(node.policies, context, path, void 0, (_, decision) => decision._tag === "NotApplicable" ? next(void 0) : stop(decision), () => NotApplicable);
|
|
386
|
-
case "Ceiling": return Effect.filterOrElse(decideNode(node.cap, context, [...path, 0]), (capped) => capped._tag === "Deny", () => decideNode(node.policy, context, [...path, 1]));
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
/**
|
|
390
|
-
* Tests if a request fits its kind: the action is an action of the kind, and
|
|
391
|
-
* the field, if there is one, is a field of the kind. The types check this,
|
|
392
|
-
* but a value with a union type, or a value from outside, may get past them.
|
|
393
|
-
* A rule for every action (`"*"`) must not cover an action that the kind does
|
|
394
|
-
* not have, so such a request is not applicable.
|
|
395
|
-
*
|
|
396
|
-
* @stability stable
|
|
397
|
-
* @category predicates
|
|
398
|
-
* @since 1.0.0
|
|
399
|
-
*/
|
|
400
|
-
const fitsKind = (descriptor, request) => descriptor.actions.includes(request.action) && (request.field === void 0 || descriptor.fieldPaths.includes(request.field) || hasPath(descriptor.schema, request.field) || hasGetter(descriptor.schema, request.field));
|
|
401
|
-
const makeContext = (domain, request, trace) => {
|
|
402
|
-
const kind = kindOf(request.resource);
|
|
403
|
-
const descriptor = kind === void 0 ? void 0 : descriptorOf(domain, kind);
|
|
404
|
-
if (kind === void 0 || descriptor === void 0 || !fitsKind(descriptor, request)) return;
|
|
405
|
-
return {
|
|
406
|
-
action: request.action,
|
|
407
|
-
kind,
|
|
408
|
-
field: request.field,
|
|
409
|
-
input: {
|
|
410
|
-
value: request.resource,
|
|
411
|
-
principal: request.principal,
|
|
412
|
-
path: descriptor.path
|
|
413
|
-
},
|
|
414
|
-
descriptor,
|
|
415
|
-
trace,
|
|
416
|
-
predicates: /* @__PURE__ */ new Map(),
|
|
417
|
-
decisions: /* @__PURE__ */ new Map()
|
|
418
|
-
};
|
|
419
|
-
};
|
|
420
|
-
const decideAny = (policy, request) => Effect.suspend(() => {
|
|
421
|
-
const context = makeContext(policy.domain, request, void 0);
|
|
422
|
-
return context === void 0 ? Effect.succeed(NotApplicable) : decideNode(nodeOf(policy), context, []);
|
|
423
|
-
});
|
|
129
|
+
const provideContext = provideContext$1;
|
|
424
130
|
/**
|
|
425
131
|
* Gives the decision of a policy for a request.
|
|
426
132
|
*
|
|
@@ -428,7 +134,7 @@ const decideAny = (policy, request) => Effect.suspend(() => {
|
|
|
428
134
|
* @category evaluation
|
|
429
135
|
* @since 0.3.0
|
|
430
136
|
*/
|
|
431
|
-
const decide =
|
|
137
|
+
const decide = decideAny;
|
|
432
138
|
/**
|
|
433
139
|
* Gives the decision of a policy with no predicates, at once.
|
|
434
140
|
*
|
|
@@ -436,48 +142,22 @@ const decide = dual(2, decideAny);
|
|
|
436
142
|
* @category evaluation
|
|
437
143
|
* @since 0.3.0
|
|
438
144
|
*/
|
|
439
|
-
const decideSync =
|
|
145
|
+
const decideSync = decideSync$1;
|
|
440
146
|
/**
|
|
441
147
|
* Gives the decision of a policy and the rules that were tested to make it.
|
|
442
148
|
*
|
|
443
|
-
* One node may be a child of many nodes.
|
|
444
|
-
*
|
|
445
|
-
* and are in the steps, only the first time. So a small tree with many shared
|
|
446
|
-
* nodes gives a short list of steps.
|
|
149
|
+
* One node may be a child of many nodes. Its rules are tested, and are in the
|
|
150
|
+
* steps, each time that the decision comes to it.
|
|
447
151
|
*
|
|
448
152
|
* @stability stable
|
|
449
153
|
* @category evaluation
|
|
450
154
|
* @since 0.3.0
|
|
451
155
|
*/
|
|
452
|
-
const explain =
|
|
453
|
-
const trace = yield* Ref.make(Chunk.empty());
|
|
454
|
-
const context = makeContext(policy.domain, request, trace);
|
|
455
|
-
if (context === void 0) return {
|
|
456
|
-
decision: NotApplicable,
|
|
457
|
-
steps: []
|
|
458
|
-
};
|
|
459
|
-
return {
|
|
460
|
-
decision: yield* decideNode(nodeOf(policy), context, []),
|
|
461
|
-
steps: Chunk.toReadonlyArray(yield* Ref.get(trace))
|
|
462
|
-
};
|
|
463
|
-
}));
|
|
464
|
-
/**
|
|
465
|
-
* Tests if a deny rule of the request names a part inside `field`.
|
|
466
|
-
*/
|
|
467
|
-
const deniesInside = (node, target, field, seen = /* @__PURE__ */ new Set()) => {
|
|
468
|
-
if (seen.has(node)) return false;
|
|
469
|
-
seen.add(node);
|
|
470
|
-
switch (node._tag) {
|
|
471
|
-
case "Rules": return node.rules.some((value) => value._tag === "Deny" && value.fields !== void 0 && value.fields.some((pattern) => matchesInside(pattern, field)) && (value.actions.includes("*") || value.actions.includes(target.action)) && (value.resources.includes("*") || value.resources.includes(target.kind)));
|
|
472
|
-
case "All":
|
|
473
|
-
case "Any":
|
|
474
|
-
case "FirstOf": return node.policies.some((child) => deniesInside(child, target, field, seen));
|
|
475
|
-
case "Ceiling": return deniesInside(node.cap, target, field, seen) || deniesInside(node.policy, target, field, seen);
|
|
476
|
-
}
|
|
477
|
-
};
|
|
156
|
+
const explain = explain$1;
|
|
478
157
|
/**
|
|
479
158
|
* Gives the fields of a resource value that the policy lets a request use. It
|
|
480
|
-
* tests each field of
|
|
159
|
+
* tests each field that a list of fields shows: each scalar, record or array
|
|
160
|
+
* of the resource Schema, with dots for nested fields.
|
|
481
161
|
*
|
|
482
162
|
* A field that holds more than one value, such as a record, is left out when a
|
|
483
163
|
* deny rule names a part of it. The list can not name that part, and the whole
|
|
@@ -487,283 +167,7 @@ const deniesInside = (node, target, field, seen = /* @__PURE__ */ new Set()) =>
|
|
|
487
167
|
* @category evaluation
|
|
488
168
|
* @since 0.3.0
|
|
489
169
|
*/
|
|
490
|
-
const permittedFields =
|
|
491
|
-
const kind = kindOf(request.resource);
|
|
492
|
-
const descriptor = kind === void 0 ? void 0 : descriptorOf(policy.domain, kind);
|
|
493
|
-
if (descriptor === void 0) return Effect.succeed([]);
|
|
494
|
-
return Effect.filter(descriptor.fieldPaths, (field) => Effect.map(decideAny(policy, {
|
|
495
|
-
...request,
|
|
496
|
-
field
|
|
497
|
-
}), (result) => result._tag === "Allow" && !(descriptor.openPaths.includes(field) && deniesInside(nodeOf(policy), {
|
|
498
|
-
...request,
|
|
499
|
-
kind: descriptor.kind
|
|
500
|
-
}, field))));
|
|
501
|
-
});
|
|
502
|
-
const always = always$1;
|
|
503
|
-
const never = never$1;
|
|
504
|
-
/**
|
|
505
|
-
* The most nodes that a residual condition may have. A policy tree may use a
|
|
506
|
-
* condition of a child two times, in its allow condition and in its deny
|
|
507
|
-
* condition, so a small policy may give a very large condition. Past this
|
|
508
|
-
* size, `residual` throws an error.
|
|
509
|
-
*/
|
|
510
|
-
const maxResidualSize = 1e4;
|
|
511
|
-
/**
|
|
512
|
-
* The most levels that a residual condition may have. The rules of an ordered
|
|
513
|
-
* algorithm, such as `firstApplicable`, give one level for each run of rules
|
|
514
|
-
* with one tag. A walk over a condition calls itself for each level, so a much
|
|
515
|
-
* deeper condition could fill the stack. Past this depth, `residual` throws an
|
|
516
|
-
* error.
|
|
517
|
-
*/
|
|
518
|
-
const maxResidualDepth = 500;
|
|
519
|
-
/**
|
|
520
|
-
* A residual condition is too large. See {@link maxResidualSize} and
|
|
521
|
-
* {@link maxResidualDepth}.
|
|
522
|
-
*/
|
|
523
|
-
var TooLarge = class {};
|
|
524
|
-
const makeBuild = () => {
|
|
525
|
-
const simplify = simplifier();
|
|
526
|
-
const sizes = /* @__PURE__ */ new Map();
|
|
527
|
-
/** Gives the number of nodes in a condition, as JSON would have them. */
|
|
528
|
-
const sizeOf = (ast) => remember(sizes, ast, () => {
|
|
529
|
-
switch (ast._tag) {
|
|
530
|
-
case "And":
|
|
531
|
-
case "Or": return Arr.reduce(ast.conditions, 1, (size, child) => size + sizeOf(child));
|
|
532
|
-
case "Not": return 1 + sizeOf(ast.condition);
|
|
533
|
-
default: return 1;
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
const depths = /* @__PURE__ */ new Map();
|
|
537
|
-
/**
|
|
538
|
-
* Gives the number of levels in a condition. Each child went through
|
|
539
|
-
* `limit`, so this walk is not deeper than `maxResidualDepth`.
|
|
540
|
-
*/
|
|
541
|
-
const depthOf = (ast) => remember(depths, ast, () => {
|
|
542
|
-
switch (ast._tag) {
|
|
543
|
-
case "And":
|
|
544
|
-
case "Or": return 1 + Arr.reduce(ast.conditions, 0, (top, child) => Math.max(top, depthOf(child)));
|
|
545
|
-
case "Not": return 1 + depthOf(ast.condition);
|
|
546
|
-
default: return 1;
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
const limit = (ast) => {
|
|
550
|
-
if (sizeOf(ast) > maxResidualSize || depthOf(ast) > maxResidualDepth) throw new TooLarge();
|
|
551
|
-
return ast;
|
|
552
|
-
};
|
|
553
|
-
return {
|
|
554
|
-
and: (values) => limit(simplify(andAll(values))),
|
|
555
|
-
or: (values) => limit(simplify(orAll(values))),
|
|
556
|
-
not: (value) => limit(simplify(not(value)))
|
|
557
|
-
};
|
|
558
|
-
};
|
|
559
|
-
const ruleCondition = (value, context) => !matchesTarget(value, context) ? never : value.when === void 0 ? always : partial(value.when, context.input);
|
|
560
|
-
/**
|
|
561
|
-
* Gives the conditions for "the first rule that applies is an allow rule" and
|
|
562
|
-
* "the first rule that applies is a deny rule".
|
|
563
|
-
*/
|
|
564
|
-
const firstApplicableResidual = (values, context) => {
|
|
565
|
-
const { and, not, or } = context.build;
|
|
566
|
-
const runs = Arr.isReadonlyArrayNonEmpty(values) ? Arr.groupWith(values, (a, b) => a._tag === b._tag) : [];
|
|
567
|
-
return Arr.reduceRight(runs, {
|
|
568
|
-
allow: never,
|
|
569
|
-
deny: never
|
|
570
|
-
}, (rest, run) => {
|
|
571
|
-
const applies = or(Arr.map(run, (value) => ruleCondition(value, context)));
|
|
572
|
-
return run[0]._tag === "Allow" ? {
|
|
573
|
-
allow: or([applies, rest.allow]),
|
|
574
|
-
deny: and([not(applies), rest.deny])
|
|
575
|
-
} : {
|
|
576
|
-
allow: and([not(applies), rest.allow]),
|
|
577
|
-
deny: or([applies, rest.deny])
|
|
578
|
-
};
|
|
579
|
-
});
|
|
580
|
-
};
|
|
581
|
-
const residualNode = (node, context) => remember(context.pairs, node, () => residualOnce(node, context));
|
|
582
|
-
const residualOnce = (node, context) => {
|
|
583
|
-
const { and, not, or } = context.build;
|
|
584
|
-
switch (node._tag) {
|
|
585
|
-
case "Rules": switch (node.algorithm) {
|
|
586
|
-
case "denyOverrides": {
|
|
587
|
-
const deny = or(node.rules.filter((value) => value._tag === "Deny").map((value) => ruleCondition(value, context)));
|
|
588
|
-
return {
|
|
589
|
-
allow: and([not(deny), or(node.rules.filter((value) => value._tag === "Allow").map((value) => ruleCondition(value, context)))]),
|
|
590
|
-
deny
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
case "permitOverrides": {
|
|
594
|
-
const allow = or(node.rules.filter((value) => value._tag === "Allow").map((value) => ruleCondition(value, context)));
|
|
595
|
-
return {
|
|
596
|
-
allow,
|
|
597
|
-
deny: and([not(allow), or(node.rules.filter((value) => value._tag === "Deny").map((value) => ruleCondition(value, context)))])
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
case "firstApplicable": return firstApplicableResidual(node.rules, context);
|
|
601
|
-
case "lastApplicable": return firstApplicableResidual(node.rules.slice().reverse(), context);
|
|
602
|
-
}
|
|
603
|
-
case "All": {
|
|
604
|
-
if (node.policies.length === 0) return {
|
|
605
|
-
allow: never,
|
|
606
|
-
deny: never
|
|
607
|
-
};
|
|
608
|
-
const pairs = node.policies.map((child) => residualNode(child, context));
|
|
609
|
-
const deny = or(pairs.map((pair) => pair.deny));
|
|
610
|
-
return {
|
|
611
|
-
allow: and([...pairs.map((pair) => pair.allow), not(deny)]),
|
|
612
|
-
deny
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
case "Any": {
|
|
616
|
-
const pairs = node.policies.map((child) => residualNode(child, context));
|
|
617
|
-
const allow = or(pairs.map((pair) => pair.allow));
|
|
618
|
-
return {
|
|
619
|
-
allow,
|
|
620
|
-
deny: and([not(allow), or(pairs.map((pair) => pair.deny))])
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
case "FirstOf": return Arr.reduceRight(node.policies, {
|
|
624
|
-
allow: never,
|
|
625
|
-
deny: never
|
|
626
|
-
}, (rest, child) => {
|
|
627
|
-
const pair = residualNode(child, context);
|
|
628
|
-
return {
|
|
629
|
-
allow: or([pair.allow, and([not(pair.deny), rest.allow])]),
|
|
630
|
-
deny: or([pair.deny, and([not(pair.allow), rest.deny])])
|
|
631
|
-
};
|
|
632
|
-
});
|
|
633
|
-
case "Ceiling": {
|
|
634
|
-
const cap = residualNode(node.cap, context);
|
|
635
|
-
const inner = residualNode(node.policy, context);
|
|
636
|
-
return {
|
|
637
|
-
allow: and([not(cap.deny), inner.allow]),
|
|
638
|
-
deny: or([cap.deny, inner.deny])
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
};
|
|
643
|
-
/** The name of the predicate of a residual condition that runs `decide`. */
|
|
644
|
-
const policyPredicate = "policy";
|
|
645
|
-
/** Tests if a rule of a policy tree has a predicate. */
|
|
646
|
-
const nodeHasPredicate = (node, memo = /* @__PURE__ */ new Map()) => remember(memo, node, () => {
|
|
647
|
-
const test = (child) => nodeHasPredicate(child, memo);
|
|
648
|
-
switch (node._tag) {
|
|
649
|
-
case "Rules": return Arr.some(node.rules, (value) => value.when !== void 0 && hasPredicate(value.when));
|
|
650
|
-
case "All":
|
|
651
|
-
case "Any":
|
|
652
|
-
case "FirstOf": return Arr.some(node.policies, test);
|
|
653
|
-
case "Ceiling": return test(node.cap) || test(node.policy);
|
|
654
|
-
}
|
|
655
|
-
});
|
|
656
|
-
const usesPredicate = (node, context) => remember(context.predicates, node, () => usesPredicateOnce(node, context));
|
|
657
|
-
const usesPredicateOnce = (node, context) => {
|
|
658
|
-
switch (node._tag) {
|
|
659
|
-
case "Rules": return node.rules.some((value) => value.when !== void 0 && hasPredicate(value.when) && matchesTarget(value, context));
|
|
660
|
-
case "All":
|
|
661
|
-
case "Any":
|
|
662
|
-
case "FirstOf": return node.policies.some((child) => usesPredicate(child, context));
|
|
663
|
-
case "Ceiling": return usesPredicate(node.cap, context) || usesPredicate(node.policy, context);
|
|
664
|
-
}
|
|
665
|
-
};
|
|
666
|
-
const isScopeValue = (operand) => operand._tag === "Principal" || operand.value === null || typeof operand.value === "string" || typeof operand.value === "number" || typeof operand.value === "boolean";
|
|
667
|
-
/**
|
|
668
|
-
* Gives the problem of a scope that is not plain data, or `undefined` when it
|
|
669
|
-
* is plain data. A predicate, or a value that a condition can not hold, such
|
|
670
|
-
* as `undefined` from a route parameter, is a defect: such a scope could make
|
|
671
|
-
* a check pass on the wrong values. A shared node is tested one time.
|
|
672
|
-
*/
|
|
673
|
-
const scopeProblem = (ast, memo = /* @__PURE__ */ new Map()) => remember(memo, ast, () => {
|
|
674
|
-
switch (ast._tag) {
|
|
675
|
-
case "Predicate": return "can not have a predicate";
|
|
676
|
-
case "Compare": return isScopeValue(ast.value) ? null : `has a value for "${ast.field}" that is not a scalar`;
|
|
677
|
-
case "In": return Arr.every(ast.values, isScopeValue) ? null : `has a value for "${ast.field}" that is not a scalar`;
|
|
678
|
-
case "Under": return Arr.every(ast.prefix, isScopeValue) ? null : "has a path part that is not a scalar";
|
|
679
|
-
case "And":
|
|
680
|
-
case "Or": return Option.getOrNull(Arr.findFirst(Arr.map(ast.conditions, (child) => scopeProblem(child, memo)), (problem) => problem !== void 0)) ?? null;
|
|
681
|
-
case "Not": return scopeProblem(ast.condition, memo) ?? null;
|
|
682
|
-
default: return null;
|
|
683
|
-
}
|
|
684
|
-
}) ?? void 0;
|
|
685
|
-
/**
|
|
686
|
-
* Gives the condition of a scope, with the principal and the path in it.
|
|
687
|
-
* With no scope, it is `True`.
|
|
688
|
-
*/
|
|
689
|
-
/**
|
|
690
|
-
* The fields that a scope reads, before it is made smaller. A smaller scope
|
|
691
|
-
* may leave out a read that fails, such as a getter.
|
|
692
|
-
*/
|
|
693
|
-
const scopeFields = (within) => within === void 0 ? [] : fields(within(builder()));
|
|
694
|
-
const scopeOf = (within, input) => {
|
|
695
|
-
if (within === void 0) return always;
|
|
696
|
-
const ast = within(builder());
|
|
697
|
-
const problem = scopeProblem(ast);
|
|
698
|
-
if (problem !== void 0) throw new globalThis.Error(`A scope ${problem}`);
|
|
699
|
-
return partial(ast, input);
|
|
700
|
-
};
|
|
701
|
-
const residualsAny = (policy, request) => scopedResiduals(policy, request);
|
|
702
|
-
const scopedResiduals = (policy, request) => {
|
|
703
|
-
const descriptor = descriptorOf(policy.domain, request.kind);
|
|
704
|
-
if (descriptor === void 0 || !fitsKind(descriptor, request)) return {
|
|
705
|
-
scope: never,
|
|
706
|
-
allow: never,
|
|
707
|
-
deny: never,
|
|
708
|
-
schema: void 0
|
|
709
|
-
};
|
|
710
|
-
if (scopeReadsGetter(request, descriptor)) throw new globalThis.Error("The scope of this request reads a getter, so it has no residual condition");
|
|
711
|
-
const input = {
|
|
712
|
-
principal: request.principal,
|
|
713
|
-
path: descriptor.path
|
|
714
|
-
};
|
|
715
|
-
const scope = scopeOf(request.within, input);
|
|
716
|
-
const build = makeBuild();
|
|
717
|
-
const and = build.and;
|
|
718
|
-
const inScope = (pair) => ({
|
|
719
|
-
scope,
|
|
720
|
-
allow: and([scope, pair.allow]),
|
|
721
|
-
deny: and([scope, pair.deny]),
|
|
722
|
-
schema: descriptor.schema
|
|
723
|
-
});
|
|
724
|
-
if (scope._tag === "False") return inScope({
|
|
725
|
-
allow: never,
|
|
726
|
-
deny: never
|
|
727
|
-
});
|
|
728
|
-
const context = {
|
|
729
|
-
action: request.action,
|
|
730
|
-
kind: request.kind,
|
|
731
|
-
field: request.field,
|
|
732
|
-
input,
|
|
733
|
-
descriptor,
|
|
734
|
-
pairs: /* @__PURE__ */ new Map(),
|
|
735
|
-
predicates: /* @__PURE__ */ new Map(),
|
|
736
|
-
build
|
|
737
|
-
};
|
|
738
|
-
if (!usesPredicate(nodeOf(policy), context)) {
|
|
739
|
-
if (readsGetter(policy, request, descriptor)) throw new globalThis.Error("A rule of this request reads a getter, so it has no residual condition");
|
|
740
|
-
try {
|
|
741
|
-
return inScope(residualNode(nodeOf(policy), context));
|
|
742
|
-
} catch (error) {
|
|
743
|
-
if (error instanceof TooLarge) throw new globalThis.Error(`The residual condition of this request would have more than ${String(maxResidualSize)} nodes or ${String(maxResidualDepth)} levels`);
|
|
744
|
-
throw error;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
const decision = (value) => decideNode(policy.node, {
|
|
748
|
-
action: request.action,
|
|
749
|
-
kind: request.kind,
|
|
750
|
-
field: request.field,
|
|
751
|
-
input: {
|
|
752
|
-
value,
|
|
753
|
-
principal: request.principal,
|
|
754
|
-
path: descriptor.path
|
|
755
|
-
},
|
|
756
|
-
descriptor,
|
|
757
|
-
trace: void 0,
|
|
758
|
-
predicates: /* @__PURE__ */ new Map(),
|
|
759
|
-
decisions: /* @__PURE__ */ new Map()
|
|
760
|
-
}, []);
|
|
761
|
-
const test = (tag) => builder().predicate(policyPredicate, (value) => kindOf(value) === request.kind ? Effect.map(decision(value), (result) => result._tag === tag) : Effect.succeed(false));
|
|
762
|
-
return inScope({
|
|
763
|
-
allow: test("Allow"),
|
|
764
|
-
deny: test("Deny")
|
|
765
|
-
});
|
|
766
|
-
};
|
|
170
|
+
const permittedFields = permittedFields$1;
|
|
767
171
|
/**
|
|
768
172
|
* Gives the condition on resource values for which the policy allows a
|
|
769
173
|
* request. It has no principal fields and no `Under` nodes, so you can give it
|
|
@@ -783,557 +187,49 @@ const scopedResiduals = (policy, request) => {
|
|
|
783
187
|
*
|
|
784
188
|
* A policy tree may use the condition of a child two times, so a small policy
|
|
785
189
|
* may give a very large condition. When the condition would have more than
|
|
786
|
-
* 10000 nodes
|
|
787
|
-
*
|
|
788
|
-
* a defect, so the check fails.
|
|
190
|
+
* 10000 nodes, this function throws an error. In an `Authorizer`, the error
|
|
191
|
+
* is a defect, so the check fails.
|
|
789
192
|
*
|
|
790
193
|
* @stability stable
|
|
791
194
|
* @category evaluation
|
|
792
195
|
* @since 0.3.0
|
|
793
196
|
*/
|
|
794
|
-
const residual =
|
|
795
|
-
/**
|
|
796
|
-
* Gives both residual conditions of a request: the values that the policy
|
|
797
|
-
* allows, and the values that it denies. A value in neither is not
|
|
798
|
-
* applicable. See {@link residual}.
|
|
799
|
-
*
|
|
800
|
-
* @stability stable
|
|
801
|
-
* @category evaluation
|
|
802
|
-
* @since 1.0.0
|
|
803
|
-
*/
|
|
804
|
-
const residuals = dual(2, residualsAny);
|
|
805
|
-
const coverages = Data.taggedEnum();
|
|
806
|
-
/**
|
|
807
|
-
* Tests if a rule that the request may use, or its scope, reads a getter of
|
|
808
|
-
* a class, in any branch of a union.
|
|
809
|
-
*/
|
|
810
|
-
const readsGetter = (policy, request, descriptor) => scopeReadsGetter(request, descriptor) || !readsOnly(nodeOf(policy), targetOf(request, descriptor), (field) => !readsGetterAt(descriptor.schema, field), /* @__PURE__ */ new Map());
|
|
811
|
-
/**
|
|
812
|
-
* Tests if a read of a field may read a getter of a class: at the field, or
|
|
813
|
-
* at a field on the way to it, in any branch of a union.
|
|
814
|
-
*/
|
|
815
|
-
const readsGetterAt = (schema, field) => {
|
|
816
|
-
const parts = field.split(".");
|
|
817
|
-
return Arr.some(Arr.range(1, parts.length), (count) => hasGetter(schema, parts.slice(0, count).join(".")));
|
|
818
|
-
};
|
|
819
|
-
/**
|
|
820
|
-
* Tests if the scope of a request reads a getter of a class. A scope is made
|
|
821
|
-
* smaller in each residual, also next to a predicate that runs `decide`, so
|
|
822
|
-
* such a scope has no residual.
|
|
823
|
-
*/
|
|
824
|
-
const scopeReadsGetter = (request, descriptor) => !Arr.every(scopeFields(request.within), (field) => !readsGetterAt(descriptor.schema, field));
|
|
825
|
-
/**
|
|
826
|
-
* Tests if a request has no residual: its scope reads a getter, or it has a
|
|
827
|
-
* plain residual and a rule that it may use reads a getter. A residual with
|
|
828
|
-
* a predicate runs `decide`, so the reads of its rules stay.
|
|
829
|
-
*/
|
|
830
|
-
const hasNoResidual = (policy, request, descriptor) => scopeReadsGetter(request, descriptor) || !usesPredicate(nodeOf(policy), targetOf(request, descriptor)) && readsGetter(policy, request, descriptor);
|
|
831
|
-
/** The parts of a request that tell which rules it may use. */
|
|
832
|
-
const targetOf = (request, descriptor) => ({
|
|
833
|
-
action: request.action,
|
|
834
|
-
kind: request.kind,
|
|
835
|
-
field: request.field,
|
|
836
|
-
descriptor,
|
|
837
|
-
predicates: /* @__PURE__ */ new Map()
|
|
838
|
-
});
|
|
839
|
-
/**
|
|
840
|
-
* Tests if each field that a rule that the request may use reads passes
|
|
841
|
-
* `test`. It looks at the conditions before they are made smaller. A smaller
|
|
842
|
-
* condition may leave out a field, but a read of that field, such as a
|
|
843
|
-
* getter, may fail when `decide` runs. A shared node is visited one time.
|
|
844
|
-
*/
|
|
845
|
-
const readsOnly = (node, target, test, memo) => remember(memo, node, () => {
|
|
846
|
-
switch (node._tag) {
|
|
847
|
-
case "Rules": return Arr.every(node.rules, (value) => value.when === void 0 || !matchesTarget(value, target) || Arr.every(fields(value.when), test));
|
|
848
|
-
case "All":
|
|
849
|
-
case "Any":
|
|
850
|
-
case "FirstOf": return Arr.every(node.policies, (child) => readsOnly(child, target, test, memo));
|
|
851
|
-
case "Ceiling": return readsOnly(node.cap, target, test, memo) && readsOnly(node.policy, target, test, memo);
|
|
852
|
-
}
|
|
853
|
-
});
|
|
854
|
-
const coverageAny = (policy, request) => {
|
|
855
|
-
const descriptor = descriptorOf(policy.domain, request.kind);
|
|
856
|
-
if (descriptor !== void 0 && hasNoResidual(policy, request, descriptor)) return coverages.Unknown({
|
|
857
|
-
condition: never,
|
|
858
|
-
reason: "schema"
|
|
859
|
-
});
|
|
860
|
-
const scoped = scopedResiduals(policy, request);
|
|
861
|
-
const allowed = scoped.allow;
|
|
862
|
-
if (descriptor !== void 0) {
|
|
863
|
-
const plain = (field) => isPlainField(descriptor.schema, field);
|
|
864
|
-
if (readsGetter(policy, request, descriptor) || !Arr.every(scopeFields(request.within), plain) || !readsOnly(nodeOf(policy), targetOf(request, descriptor), plain, /* @__PURE__ */ new Map())) return coverages.Unknown({
|
|
865
|
-
condition: allowed,
|
|
866
|
-
reason: "schema"
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
const options = { schema: scoped.schema };
|
|
870
|
-
const some = allowed._tag === "False" ? void 0 : satisfiable(allowed, options);
|
|
871
|
-
if (some === void 0 || some._tag === "Unsatisfiable") return coverages.None({ condition: never });
|
|
872
|
-
if (some._tag === "Unknown") return coverages.Unknown({
|
|
873
|
-
condition: allowed,
|
|
874
|
-
reason: some.reason
|
|
875
|
-
});
|
|
876
|
-
const refused = simplify(and(scoped.scope, not(allowed)));
|
|
877
|
-
const missing = refused._tag === "False" ? void 0 : satisfiable(refused, options);
|
|
878
|
-
if (missing === void 0 || missing._tag === "Unsatisfiable") return coverages.All({ condition: scoped.scope });
|
|
879
|
-
if (missing._tag === "Unknown") return coverages.Unknown({
|
|
880
|
-
condition: allowed,
|
|
881
|
-
reason: missing.reason
|
|
882
|
-
});
|
|
883
|
-
return coverages.Some({
|
|
884
|
-
condition: allowed,
|
|
885
|
-
allowed: some.example,
|
|
886
|
-
refused: missing.example
|
|
887
|
-
});
|
|
888
|
-
};
|
|
889
|
-
/**
|
|
890
|
-
* Tests if the scope of a request has a value of the Schema of its kind. The
|
|
891
|
-
* answer is `true` only when the search finds such a value. When the search
|
|
892
|
-
* can not say, the answer is `false`. So a "yes" from it is safe to act on,
|
|
893
|
-
* as for the question "may the decider answer for this scope?".
|
|
894
|
-
*
|
|
895
|
-
* @stability stable
|
|
896
|
-
* @category evaluation
|
|
897
|
-
* @since 1.0.0
|
|
898
|
-
*/
|
|
899
|
-
const scopeHasValue = (policy, request) => {
|
|
900
|
-
const scoped = scopedResiduals(policy, request);
|
|
901
|
-
return satisfiable(scoped.scope, { schema: scoped.schema })._tag === "Satisfiable";
|
|
902
|
-
};
|
|
903
|
-
/**
|
|
904
|
-
* Tests if the scope of a request has no value of the Schema of its kind. A
|
|
905
|
-
* scope with a mistake, such as two values for one field, is empty. With no
|
|
906
|
-
* scope, the answer is `false` when the kind has a value. When the search can
|
|
907
|
-
* not say, the answer is `false`.
|
|
908
|
-
*
|
|
909
|
-
* @stability stable
|
|
910
|
-
* @category evaluation
|
|
911
|
-
* @since 1.0.0
|
|
912
|
-
*/
|
|
913
|
-
const scopeIsEmpty = (policy, request) => {
|
|
914
|
-
const scoped = scopedResiduals(policy, request);
|
|
915
|
-
return satisfiable(scoped.scope, { schema: scoped.schema })._tag === "Unsatisfiable";
|
|
916
|
-
};
|
|
197
|
+
const residual = residual$1;
|
|
917
198
|
/**
|
|
918
199
|
* Tells how much of a scope the policy allows: all of it, none of it, or some
|
|
919
200
|
* of it. See {@link Coverage}. With no scope (`within`), the scope is every
|
|
920
201
|
* value of the kind.
|
|
921
202
|
*
|
|
922
|
-
* The answer comes from {@link Condition.satisfiable} with the
|
|
923
|
-
* resource
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
*
|
|
203
|
+
* The answer comes from {@link Condition.satisfiable} with the shape of the
|
|
204
|
+
* Schema of the resource. `All` and `None` hold for every value of the
|
|
205
|
+
* Schema. `Some` means that, by the shape of the Schema, a value may get
|
|
206
|
+
* either answer: a check such as a minimum length may still stop one of the
|
|
207
|
+
* examples. `Unknown` comes only from a predicate or from the budget of the
|
|
208
|
+
* search.
|
|
927
209
|
*
|
|
928
|
-
* `Some`
|
|
929
|
-
*
|
|
210
|
+
* `Some` does not mean that such a value is in your database. To know that,
|
|
211
|
+
* run `condition` as a query.
|
|
930
212
|
*
|
|
931
213
|
* @stability stable
|
|
932
214
|
* @category evaluation
|
|
933
215
|
* @since 1.0.0
|
|
934
216
|
*/
|
|
935
|
-
const coverage =
|
|
936
|
-
const AlgorithmSchema = Schema.Literals([
|
|
937
|
-
"denyOverrides",
|
|
938
|
-
"permitOverrides",
|
|
939
|
-
"firstApplicable",
|
|
940
|
-
"lastApplicable"
|
|
941
|
-
]);
|
|
942
|
-
const encodedRuleSchema = (meta) => Schema.Struct({
|
|
943
|
-
_tag: Schema.Literals(["Allow", "Deny"]),
|
|
944
|
-
actions: Schema.Array(Schema.String),
|
|
945
|
-
resources: Schema.Array(Schema.String),
|
|
946
|
-
when: Schema.optionalKey(schema$1),
|
|
947
|
-
fields: Schema.String.pipe(Schema.Array, Schema.optionalKey),
|
|
948
|
-
reason: Schema.optionalKey(Schema.String),
|
|
949
|
-
id: Schema.optionalKey(Schema.String),
|
|
950
|
-
meta: Schema.optionalKey(meta ?? Schema.Never)
|
|
951
|
-
});
|
|
952
|
-
/**
|
|
953
|
-
* The most levels of objects and arrays in the JSON form of a policy. The
|
|
954
|
-
* Schema of a policy calls itself for each level, so a much deeper input
|
|
955
|
-
* could fill the stack before the checks of the policy run. A policy tree of
|
|
956
|
-
* `maxDepth` levels with deep conditions stays well below this.
|
|
957
|
-
*/
|
|
958
|
-
const maxJsonDepth = 1e3;
|
|
959
|
-
/**
|
|
960
|
-
* The most objects and arrays in the JSON form of an input, with a shared one
|
|
961
|
-
* counted for each place. The decoder writes out a shared one each time, so
|
|
962
|
-
* an input with few objects may give a very large tree.
|
|
963
|
-
*/
|
|
964
|
-
const maxInputNodes = 1e6;
|
|
965
|
-
/**
|
|
966
|
-
* Measures a value, or gives `undefined` when it is higher than `levels`. A
|
|
967
|
-
* shared object is measured one time. The walk stops at that height, so it
|
|
968
|
-
* does not fill the stack itself. The memo is a native `Map`, because it
|
|
969
|
-
* must find an object by the object itself.
|
|
970
|
-
*/
|
|
971
|
-
const measure = (input, levels, memo) => {
|
|
972
|
-
if (typeof input !== "object" || input === null) return {
|
|
973
|
-
size: 0,
|
|
974
|
-
height: 0
|
|
975
|
-
};
|
|
976
|
-
const cached = memo.get(input);
|
|
977
|
-
if (cached !== void 0 && ("failed" in cached ? levels <= cached.failed : true)) return "failed" in cached || cached.height > levels ? void 0 : cached;
|
|
978
|
-
const result = levels === 0 || !isPlainData(input) ? void 0 : Arr.reduce(Object.values(input), {
|
|
979
|
-
size: 1,
|
|
980
|
-
height: 1
|
|
981
|
-
}, (total, child) => {
|
|
982
|
-
const measured = total === void 0 ? void 0 : measure(child, levels - 1, memo);
|
|
983
|
-
return total === void 0 || measured === void 0 ? void 0 : {
|
|
984
|
-
size: total.size + measured.size,
|
|
985
|
-
height: Math.max(total.height, 1 + measured.height)
|
|
986
|
-
};
|
|
987
|
-
});
|
|
988
|
-
memo.set(input, result ?? { failed: levels });
|
|
989
|
-
return result;
|
|
990
|
-
};
|
|
991
|
-
/**
|
|
992
|
-
* Tests if each own key of an object is a plain data key that a loop over the
|
|
993
|
-
* keys sees. The `length` of an array is the one key that is not seen. The
|
|
994
|
-
* decoder reads keys of the prototypes too, so a prototype other than the
|
|
995
|
-
* one of a plain object or an array may have only methods, as the prototype
|
|
996
|
-
* of a class has.
|
|
997
|
-
*/
|
|
998
|
-
const isPlainData = (input) => Arr.every(Reflect.ownKeys(input), (key) => {
|
|
999
|
-
const property = Object.getOwnPropertyDescriptor(input, key);
|
|
1000
|
-
return property !== void 0 && "value" in property && (property.enumerable === true || Array.isArray(input) && key === "length");
|
|
1001
|
-
}) && hasOnlyMethods(Object.getPrototypeOf(input));
|
|
1002
|
-
/** Tests if the prototypes from `prototype` up have only methods. */
|
|
1003
|
-
const hasOnlyMethods = (prototype) => prototype === null || prototype === Object.prototype || prototype === Array.prototype || typeof prototype === "object" && Arr.every(Reflect.ownKeys(prototype), (key) => {
|
|
1004
|
-
const property = Object.getOwnPropertyDescriptor(prototype, key);
|
|
1005
|
-
return typeof key === "symbol" || key === "constructor" || property !== void 0 && "value" in property && typeof property.value === "function";
|
|
1006
|
-
}) && hasOnlyMethods(Object.getPrototypeOf(prototype));
|
|
1007
|
-
/** Tests if a value is low and small enough to decode. */
|
|
1008
|
-
const fitsLimits = (input) => {
|
|
1009
|
-
const measured = measure(input, maxJsonDepth, /* @__PURE__ */ new Map());
|
|
1010
|
-
return measured === void 0 ? `A policy may have at most ${String(maxJsonDepth)} levels of JSON` : measured.size > maxInputNodes ? `A policy may have at most ${String(maxInputNodes)} objects and arrays in JSON` : true;
|
|
1011
|
-
};
|
|
1012
|
-
const encodedNodeSchema = (meta) => {
|
|
1013
|
-
const self = Schema.Union([
|
|
1014
|
-
Schema.TaggedStruct("Rules", {
|
|
1015
|
-
algorithm: AlgorithmSchema,
|
|
1016
|
-
rules: Schema.Array(encodedRuleSchema(meta))
|
|
1017
|
-
}),
|
|
1018
|
-
Schema.TaggedStruct("All", { policies: Schema.Array(Schema.suspend(() => self)) }),
|
|
1019
|
-
Schema.TaggedStruct("Any", { policies: Schema.Array(Schema.suspend(() => self)) }),
|
|
1020
|
-
Schema.TaggedStruct("FirstOf", { policies: Schema.Array(Schema.suspend(() => self)) }),
|
|
1021
|
-
Schema.TaggedStruct("Ceiling", {
|
|
1022
|
-
cap: Schema.suspend(() => self),
|
|
1023
|
-
policy: Schema.suspend(() => self)
|
|
1024
|
-
})
|
|
1025
|
-
]);
|
|
1026
|
-
return self;
|
|
1027
|
-
};
|
|
1028
|
-
const principalRefs = (ast) => Array.from(refsIn(ast, /* @__PURE__ */ new Map()));
|
|
1029
|
-
/** The principal fields of a node. A shared node is visited one time. */
|
|
1030
|
-
const refsIn = (node, memo) => remember(memo, node, () => {
|
|
1031
|
-
const refs = (operands) => HashSet.fromIterable(Arr.flatMap(operands, (operand) => operand._tag === "Principal" ? [operand.path] : []));
|
|
1032
|
-
switch (node._tag) {
|
|
1033
|
-
case "Compare": return refs([node.value]);
|
|
1034
|
-
case "In": return refs(node.values);
|
|
1035
|
-
case "Under": return refs(node.prefix);
|
|
1036
|
-
case "And":
|
|
1037
|
-
case "Or": return Arr.reduce(node.conditions, HashSet.empty(), (all, child) => HashSet.union(all, refsIn(child, memo)));
|
|
1038
|
-
case "Not": return refsIn(node.condition, memo);
|
|
1039
|
-
default: return HashSet.empty();
|
|
1040
|
-
}
|
|
1041
|
-
});
|
|
1042
|
-
const literalTests = (node) => Arr.map(Array.from(literalTestsIn(node, /* @__PURE__ */ new Map())), (text) => {
|
|
1043
|
-
const [field, value] = JSON.parse(text);
|
|
1044
|
-
return [field, hasProperty(value, "number") ? Number(value.number) : value];
|
|
1045
|
-
});
|
|
1046
|
-
/**
|
|
1047
|
-
* The JSON text of a test. JSON has no `Infinity`, `-Infinity` or `NaN`, so
|
|
1048
|
-
* such a number goes in as its text.
|
|
1049
|
-
*/
|
|
1050
|
-
const literalText = (field, value) => JSON.stringify([field, typeof value === "number" && !Number.isFinite(value) ? { number: String(value) } : value]);
|
|
1051
|
-
/**
|
|
1052
|
-
* The tests of fields with fixed values in a node, each as the JSON text of a
|
|
1053
|
-
* {@link LiteralTest}. A set of texts keeps each test one time, so a node
|
|
1054
|
-
* with many shared parts gives a small set. A shared node is visited one time.
|
|
1055
|
-
*/
|
|
1056
|
-
const literalTestsIn = (node, memo) => remember(memo, node, () => {
|
|
1057
|
-
const tests = (field, operands) => HashSet.fromIterable(Arr.flatMap(operands, (operand) => operand._tag === "Literal" ? [literalText(field, operand.value)] : []));
|
|
1058
|
-
switch (node._tag) {
|
|
1059
|
-
case "Compare": return tests(node.field, [node.value]);
|
|
1060
|
-
case "In": return tests(node.field, node.values);
|
|
1061
|
-
case "And":
|
|
1062
|
-
case "Or": return Arr.reduce(node.conditions, HashSet.empty(), (all, child) => HashSet.union(all, literalTestsIn(child, memo)));
|
|
1063
|
-
case "Not": return literalTestsIn(node.condition, memo);
|
|
1064
|
-
default: return HashSet.empty();
|
|
1065
|
-
}
|
|
1066
|
-
});
|
|
1067
|
-
const principalTests = (node) => Arr.map(Array.from(principalTestsIn(node, /* @__PURE__ */ new Map())), (text) => JSON.parse(text));
|
|
1068
|
-
/**
|
|
1069
|
-
* The tests of fields with principal fields in a node, each as the JSON text
|
|
1070
|
-
* of a {@link PrincipalTest}. See {@link literalTestsIn}.
|
|
1071
|
-
*/
|
|
1072
|
-
const principalTestsIn = (node, memo) => remember(memo, node, () => {
|
|
1073
|
-
const tests = (field, operands) => HashSet.fromIterable(Arr.flatMap(operands, (operand) => operand._tag === "Principal" ? [JSON.stringify([field, operand.path])] : []));
|
|
1074
|
-
switch (node._tag) {
|
|
1075
|
-
case "Compare": return tests(node.field, [node.value]);
|
|
1076
|
-
case "In": return tests(node.field, node.values);
|
|
1077
|
-
case "And":
|
|
1078
|
-
case "Or": return Arr.reduce(node.conditions, HashSet.empty(), (all, child) => HashSet.union(all, principalTestsIn(child, memo)));
|
|
1079
|
-
case "Not": return principalTestsIn(node.condition, memo);
|
|
1080
|
-
default: return HashSet.empty();
|
|
1081
|
-
}
|
|
1082
|
-
});
|
|
1083
|
-
/**
|
|
1084
|
-
* Tests if a node has a fixed value that is not a {@link Condition.Scalar},
|
|
1085
|
-
* such as `undefined`. A shared node is visited one time.
|
|
1086
|
-
*/
|
|
1087
|
-
const hasBadLiteralIn = (node, memo) => remember(memo, node, () => {
|
|
1088
|
-
const bad = (operands) => Arr.some(operands, (operand) => operand._tag === "Literal" && operand.value !== null && typeof operand.value !== "string" && typeof operand.value !== "number" && typeof operand.value !== "boolean");
|
|
1089
|
-
switch (node._tag) {
|
|
1090
|
-
case "Compare": return bad([node.value]);
|
|
1091
|
-
case "In": return bad(node.values);
|
|
1092
|
-
case "Under": return bad(node.prefix);
|
|
1093
|
-
case "And":
|
|
1094
|
-
case "Or": return Arr.some(node.conditions, (child) => hasBadLiteralIn(child, memo));
|
|
1095
|
-
case "Not": return hasBadLiteralIn(node.condition, memo);
|
|
1096
|
-
default: return false;
|
|
1097
|
-
}
|
|
1098
|
-
});
|
|
1099
|
-
/**
|
|
1100
|
-
* The principal fields in the prefixes of `Under` nodes, each as the JSON text
|
|
1101
|
-
* of the place in the prefix and the path of the field. See
|
|
1102
|
-
* {@link literalTestsIn}.
|
|
1103
|
-
*/
|
|
1104
|
-
/**
|
|
1105
|
-
* The fixed values in the prefixes of `Under` nodes, each as the JSON text of
|
|
1106
|
-
* the place in the prefix, the value, and the fixed strings of the whole
|
|
1107
|
-
* prefix. A number that JSON can not hold is left out: it fits no field
|
|
1108
|
-
* that a path names. See {@link underRefsIn}.
|
|
1109
|
-
*/
|
|
1110
|
-
const underLiteralsIn = (node, memo) => remember(memo, node, () => {
|
|
1111
|
-
switch (node._tag) {
|
|
1112
|
-
case "Under": {
|
|
1113
|
-
const strings = Arr.map(node.prefix, (operand) => operand._tag === "Literal" && typeof operand.value === "string" ? operand.value : null);
|
|
1114
|
-
return HashSet.fromIterable(Arr.flatMap(node.prefix, (operand, index) => operand._tag === "Literal" && (operand.value === null || typeof operand.value === "string" || typeof operand.value === "boolean" || Number.isFinite(operand.value)) ? [JSON.stringify([
|
|
1115
|
-
index,
|
|
1116
|
-
operand.value,
|
|
1117
|
-
strings
|
|
1118
|
-
])] : []));
|
|
1119
|
-
}
|
|
1120
|
-
case "And":
|
|
1121
|
-
case "Or": return Arr.reduce(node.conditions, HashSet.empty(), (all, child) => HashSet.union(all, underLiteralsIn(child, memo)));
|
|
1122
|
-
case "Not": return underLiteralsIn(node.condition, memo);
|
|
1123
|
-
default: return HashSet.empty();
|
|
1124
|
-
}
|
|
1125
|
-
});
|
|
1126
|
-
const underLiterals = (node) => Arr.map(Array.from(underLiteralsIn(node, /* @__PURE__ */ new Map())), (text) => JSON.parse(text));
|
|
1127
|
-
const underRefsIn = (node, memo) => remember(memo, node, () => {
|
|
1128
|
-
switch (node._tag) {
|
|
1129
|
-
case "Under": {
|
|
1130
|
-
const strings = Arr.map(node.prefix, (operand) => operand._tag === "Literal" && typeof operand.value === "string" ? operand.value : null);
|
|
1131
|
-
return HashSet.fromIterable(Arr.flatMap(node.prefix, (operand, index) => operand._tag === "Principal" ? [JSON.stringify([
|
|
1132
|
-
index,
|
|
1133
|
-
operand.path,
|
|
1134
|
-
strings
|
|
1135
|
-
])] : []));
|
|
1136
|
-
}
|
|
1137
|
-
case "And":
|
|
1138
|
-
case "Or": return Arr.reduce(node.conditions, HashSet.empty(), (all, child) => HashSet.union(all, underRefsIn(child, memo)));
|
|
1139
|
-
case "Not": return underRefsIn(node.condition, memo);
|
|
1140
|
-
default: return HashSet.empty();
|
|
1141
|
-
}
|
|
1142
|
-
});
|
|
1143
|
-
const underRefs = (node) => Arr.map(Array.from(underRefsIn(node, /* @__PURE__ */ new Map())), (text) => JSON.parse(text));
|
|
1144
|
-
/**
|
|
1145
|
-
* Tests if the path of a resource may start with a prefix: it is as long as
|
|
1146
|
-
* the prefix, and each fixed string of the prefix is the string of the path
|
|
1147
|
-
* at its place, when the path has a string there.
|
|
1148
|
-
*/
|
|
1149
|
-
const mayStartWith = (descriptor, strings) => (descriptor.path?.length ?? 0) >= strings.length && Arr.every(strings, (string, index) => {
|
|
1150
|
-
const segment = descriptor.path?.[index];
|
|
1151
|
-
return string === null || typeof segment !== "string" || segment === string;
|
|
1152
|
-
});
|
|
1153
|
-
/**
|
|
1154
|
-
* Tests if a principal field may hold the part of a path at `index`: a string
|
|
1155
|
-
* of the path, or a value of the field that the path names there.
|
|
1156
|
-
*/
|
|
1157
|
-
const fitsPathPart = (descriptor, index, principal) => {
|
|
1158
|
-
const segment = descriptor.path?.[index];
|
|
1159
|
-
return segment === void 0 ? false : typeof segment === "string" ? fitsDomain(principal, segment) : domainsMeet(domainOf(descriptor.schema, segment.field), principal);
|
|
1160
|
-
};
|
|
1161
|
-
/**
|
|
1162
|
-
* The fields of ordered tests (`lt`, `lte`, `gt`, `gte`) in a node, each as
|
|
1163
|
-
* the JSON text of the field and the operand. See {@link literalTestsIn}.
|
|
1164
|
-
*/
|
|
1165
|
-
const orderedTestsIn = (node, memo) => remember(memo, node, () => {
|
|
1166
|
-
switch (node._tag) {
|
|
1167
|
-
case "Compare": return node.op === "eq" || node.op === "ne" ? HashSet.empty() : HashSet.make(JSON.stringify([node.field, node.value._tag === "Literal" ? {
|
|
1168
|
-
_tag: "Literal",
|
|
1169
|
-
type: typeof node.value.value
|
|
1170
|
-
} : node.value]));
|
|
1171
|
-
case "And":
|
|
1172
|
-
case "Or": return Arr.reduce(node.conditions, HashSet.empty(), (all, child) => HashSet.union(all, orderedTestsIn(child, memo)));
|
|
1173
|
-
case "Not": return orderedTestsIn(node.condition, memo);
|
|
1174
|
-
default: return HashSet.empty();
|
|
1175
|
-
}
|
|
1176
|
-
});
|
|
1177
|
-
const orderedTests = (node) => Arr.map(Array.from(orderedTestsIn(node, /* @__PURE__ */ new Map())), (text) => JSON.parse(text));
|
|
1178
|
-
/**
|
|
1179
|
-
* Tests if a field may hold a value with an order: a number or a string. An
|
|
1180
|
-
* ordered test is false for any other value.
|
|
1181
|
-
*/
|
|
1182
|
-
const isOrdered = (domain) => domain.string || domain.number !== "none" || Arr.some(domain.literals, (value) => typeof value === "string" || typeof value === "number");
|
|
1183
|
-
/**
|
|
1184
|
-
* Tests if two fields may hold values of one ordered type: two strings or two
|
|
1185
|
-
* numbers. An ordered test of two values of other types is false.
|
|
1186
|
-
*/
|
|
1187
|
-
const orderedMeet = (left, right) => {
|
|
1188
|
-
const strings = (domain) => domain.string || Arr.some(domain.literals, (value) => typeof value === "string");
|
|
1189
|
-
const numbers = (domain) => domain.number !== "none" || Arr.some(domain.literals, (value) => typeof value === "number");
|
|
1190
|
-
return strings(left) && strings(right) || numbers(left) && numbers(right);
|
|
1191
|
-
};
|
|
1192
|
-
/**
|
|
1193
|
-
* Tests if two fields may hold one value: a value of one type, or a single
|
|
1194
|
-
* value that the other field may hold.
|
|
1195
|
-
*/
|
|
1196
|
-
const domainsMeet = (left, right) => left.string && right.string || left.boolean && right.boolean || left.number !== "none" && right.number !== "none" || Arr.some(left.literals, (value) => fitsDomain(right, value)) || Arr.some(right.literals, (value) => fitsDomain(left, value));
|
|
1197
|
-
/**
|
|
1198
|
-
* Tests if a field with this domain may hold a value of the type of `value`.
|
|
1199
|
-
* `null` always fits: a test with `null` is a test that the field is missing.
|
|
1200
|
-
*/
|
|
1201
|
-
const fitsDomain = (domain, value) => value === null || domain.literals.includes(value) || typeof value === "string" && domain.string || typeof value === "boolean" && domain.boolean || typeof value === "number" && domain.number !== "none";
|
|
1202
|
-
/**
|
|
1203
|
-
* Checks the names in an encoded policy against the domain. It gives one
|
|
1204
|
-
* message for each problem.
|
|
1205
|
-
*/
|
|
1206
|
-
const validate = (domain, root) => {
|
|
1207
|
-
const principalPaths = domain.principal.pipe(fieldPaths, HashSet.fromIterable);
|
|
1208
|
-
const ruleIssues = (value, at) => {
|
|
1209
|
-
const all = value.resources.includes("*");
|
|
1210
|
-
const named = Array.from(HashSet.fromIterable(Arr.filter(value.resources, (kind) => kind !== "*")));
|
|
1211
|
-
const descriptors = Arr.flatMap(named, (kind) => {
|
|
1212
|
-
const descriptor = descriptorOf(domain, kind);
|
|
1213
|
-
return descriptor === void 0 ? [] : [descriptor];
|
|
1214
|
-
});
|
|
1215
|
-
const known = all ? Array.from(HashMap.values(domain.resources)) : descriptors;
|
|
1216
|
-
const applicable = Arr.filter(descriptors, (descriptor) => value.actions.includes("*") || Arr.some(value.actions, (action) => descriptor.actions.includes(action)));
|
|
1217
|
-
const usedFields = [...Arr.filter(value.fields ?? [], (field) => !field.includes("*")), ...value.when === void 0 ? [] : fields(value.when)];
|
|
1218
|
-
return [
|
|
1219
|
-
...Arr.map(Arr.filter(named, (kind) => !HashMap.has(domain.resources, kind)), (kind) => ({
|
|
1220
|
-
path: [...at, "resources"],
|
|
1221
|
-
issue: `There is no resource with the kind "${kind}"`
|
|
1222
|
-
})),
|
|
1223
|
-
...Arr.map(Arr.filter(value.actions, (action) => action !== "*" && !Arr.some(known, (descriptor) => descriptor.actions.includes(action))), (action) => ({
|
|
1224
|
-
path: [...at, "actions"],
|
|
1225
|
-
issue: `The action "${action}" is not an action of these resources`
|
|
1226
|
-
})),
|
|
1227
|
-
...Arr.map(Arr.filter(usedFields, (field) => all || descriptors.length === 0 || !Arr.every(descriptors, (descriptor) => descriptor.fieldPaths.includes(field) || hasPath(descriptor.schema, field) || hasGetter(descriptor.schema, field))), (field) => ({
|
|
1228
|
-
path: at,
|
|
1229
|
-
issue: `The field "${field}" is not a field of every resource of the rule`
|
|
1230
|
-
})),
|
|
1231
|
-
...Arr.map(Arr.filter(value.when === void 0 ? [] : principalRefs(value.when), (ref) => !HashSet.has(principalPaths, ref) && !hasPath(domain.principal, ref) && !hasGetter(domain.principal, ref)), (ref) => ({
|
|
1232
|
-
path: [...at, "when"],
|
|
1233
|
-
issue: `The principal has no field "${ref}"`
|
|
1234
|
-
})),
|
|
1235
|
-
...Arr.map(Arr.filter(value.when === void 0 ? [] : literalTests(value.when), ([field, literal]) => Arr.some(applicable, (descriptor) => !fitsDomain(domainOf(descriptor.schema, field), literal))), ([field, literal]) => ({
|
|
1236
|
-
path: [...at, "when"],
|
|
1237
|
-
issue: `The field "${field}" can not hold the value ${JSON.stringify(literal)}`
|
|
1238
|
-
})),
|
|
1239
|
-
...Arr.map(Arr.filter(value.when === void 0 ? [] : principalTests(value.when), ([field, ref]) => Arr.some(applicable, (descriptor) => !domainsMeet(domainOf(descriptor.schema, field), domainOf(domain.principal, ref)))), ([field, ref]) => ({
|
|
1240
|
-
path: [...at, "when"],
|
|
1241
|
-
issue: `The field "${field}" can not hold a value of the principal field "${ref}"`
|
|
1242
|
-
})),
|
|
1243
|
-
...value.when !== void 0 && !isAst(value.when) ? [{
|
|
1244
|
-
path: [...at, "when"],
|
|
1245
|
-
issue: "The condition is not a whole condition tree"
|
|
1246
|
-
}] : [],
|
|
1247
|
-
...value.when !== void 0 && hasBadLiteralIn(value.when, /* @__PURE__ */ new Map()) ? [{
|
|
1248
|
-
path: [...at, "when"],
|
|
1249
|
-
issue: "A test has a fixed value that is not a string, a number, a boolean or null"
|
|
1250
|
-
}] : [],
|
|
1251
|
-
...Arr.map(Arr.filter(value.when === void 0 ? [] : underRefs(value.when), ([index, ref, strings]) => Arr.some(Arr.filter(all ? Arr.filter(known, (descriptor) => descriptor.path?.[index] !== void 0 && (value.actions.includes("*") || Arr.some(value.actions, (action) => descriptor.actions.includes(action)))) : applicable, (descriptor) => mayStartWith(descriptor, strings)), (descriptor) => !fitsPathPart(descriptor, index, domainOf(domain.principal, ref)))), ([, ref]) => ({
|
|
1252
|
-
path: [...at, "when"],
|
|
1253
|
-
issue: `The principal field "${ref}" can not hold a part of the path of the resource`
|
|
1254
|
-
})),
|
|
1255
|
-
...Arr.map(Arr.filter(value.when === void 0 ? [] : underLiterals(value.when), ([index, literal, strings]) => Arr.some(Arr.filter(all ? Arr.filter(known, (descriptor) => value.actions.includes("*") || Arr.some(value.actions, (action) => descriptor.actions.includes(action))) : applicable, (descriptor) => mayStartWith(descriptor, strings)), (descriptor) => {
|
|
1256
|
-
const segment = descriptor.path?.[index];
|
|
1257
|
-
return segment !== void 0 && typeof segment !== "string" && !fitsDomain(domainOf(descriptor.schema, segment.field), literal);
|
|
1258
|
-
})), ([index, literal]) => ({
|
|
1259
|
-
path: [...at, "when"],
|
|
1260
|
-
issue: `The path part ${String(index)} can not hold the value ${JSON.stringify(literal)}`
|
|
1261
|
-
})),
|
|
1262
|
-
...Arr.map(Arr.filter(value.when === void 0 ? [] : orderedTests(value.when), ([field, operand]) => operand._tag === "Literal" && operand.type !== "string" && operand.type !== "number" || Arr.some(applicable, (descriptor) => {
|
|
1263
|
-
const fieldDomain = domainOf(descriptor.schema, field);
|
|
1264
|
-
return operand._tag === "Literal" ? !isOrdered(fieldDomain) : !orderedMeet(fieldDomain, domainOf(domain.principal, operand.path));
|
|
1265
|
-
})), ([field]) => ({
|
|
1266
|
-
path: [...at, "when"],
|
|
1267
|
-
issue: `The field "${field}" has an ordered test with a value that has no order`
|
|
1268
|
-
}))
|
|
1269
|
-
];
|
|
1270
|
-
};
|
|
1271
|
-
const issuesOf = (node, path, level) => {
|
|
1272
|
-
if (level > maxDepth) return [{
|
|
1273
|
-
path,
|
|
1274
|
-
issue: `A policy tree may have at most ${maxDepth} levels`
|
|
1275
|
-
}];
|
|
1276
|
-
switch (node._tag) {
|
|
1277
|
-
case "Rules": return Arr.flatMap(node.rules, (value, index) => ruleIssues(value, [
|
|
1278
|
-
...path,
|
|
1279
|
-
"rules",
|
|
1280
|
-
index
|
|
1281
|
-
]));
|
|
1282
|
-
case "All":
|
|
1283
|
-
case "Any":
|
|
1284
|
-
case "FirstOf": return Arr.flatMap(node.policies, (child, index) => issuesOf(child, [
|
|
1285
|
-
...path,
|
|
1286
|
-
"policies",
|
|
1287
|
-
index
|
|
1288
|
-
], level + 1));
|
|
1289
|
-
case "Ceiling": return [...issuesOf(node.cap, [...path, "cap"], level + 1), ...issuesOf(node.policy, [...path, "policy"], level + 1)];
|
|
1290
|
-
}
|
|
1291
|
-
};
|
|
1292
|
-
return issuesOf(root, [], 1);
|
|
1293
|
-
};
|
|
1294
|
-
/**
|
|
1295
|
-
* Makes a policy tree from its JSON form. Each rule gets the domain of the
|
|
1296
|
-
* Schema, as a rule from the builder of the domain has it.
|
|
1297
|
-
*/
|
|
1298
|
-
const decodeNode = (node, domain) => {
|
|
1299
|
-
switch (node._tag) {
|
|
1300
|
-
case "Rules": return nodes.Rules({
|
|
1301
|
-
algorithm: node.algorithm,
|
|
1302
|
-
rules: node.rules.map((value) => make({
|
|
1303
|
-
...value,
|
|
1304
|
-
domain
|
|
1305
|
-
}))
|
|
1306
|
-
});
|
|
1307
|
-
case "All":
|
|
1308
|
-
case "Any":
|
|
1309
|
-
case "FirstOf": return nodes[node._tag]({ policies: node.policies.map((child) => decodeNode(child, domain)) });
|
|
1310
|
-
case "Ceiling": return nodes.Ceiling({
|
|
1311
|
-
cap: decodeNode(node.cap, domain),
|
|
1312
|
-
policy: decodeNode(node.policy, domain)
|
|
1313
|
-
});
|
|
1314
|
-
}
|
|
1315
|
-
};
|
|
1316
|
-
/**
|
|
1317
|
-
* Tests if a value is a policy.
|
|
1318
|
-
*
|
|
1319
|
-
* @stability stable
|
|
1320
|
-
* @category guards
|
|
1321
|
-
* @since 0.3.0
|
|
1322
|
-
*/
|
|
1323
|
-
const isPolicy = (u) => u instanceof PolicyData;
|
|
217
|
+
const coverage = coverageAny;
|
|
1324
218
|
/**
|
|
1325
219
|
* The Schema of the policies of a domain with no predicates. Decoding checks
|
|
1326
220
|
* every kind, action, field and principal field against the domain, and the
|
|
1327
221
|
* `meta` of each rule against the `meta` Schema of the domain.
|
|
1328
222
|
*
|
|
223
|
+
* The decoder takes at most 256 levels of policies and 500 levels of
|
|
224
|
+
* conditions, and at most 1000000 objects and array items, with a shared
|
|
225
|
+
* object counted for each place. It reads only the own data fields of the
|
|
226
|
+
* input.
|
|
227
|
+
*
|
|
1329
228
|
* @stability stable
|
|
1330
229
|
* @category schemas
|
|
1331
230
|
* @since 0.3.0
|
|
1332
231
|
*/
|
|
1333
|
-
const schema =
|
|
1334
|
-
|
|
1335
|
-
encode: (policy, options) => encodedSize(policy.node) > maxEncodedNodes ? Effect.fail(new SchemaIssue.InvalidValue({ message: tooLargeMessage }, policy, options)) : Effect.succeed(encodeNode(policy.node))
|
|
1336
|
-
})));
|
|
1337
|
-
export { Policy_exports, all, any, ceiling, coverage, decide, decideSync, define, empty, explain, firstOf, fitsKind, isPolicy, permittedFields, provideContext, residual, residuals, rules, schema, scopeHasValue, scopeIsEmpty };
|
|
232
|
+
const schema = schema$1;
|
|
233
|
+
export { PolicyError, Policy_exports, all, any, builder, ceiling, coverage, decide, decideSync, define, explain, firstOf, permittedFields, provideContext, residual, rule, schema };
|
|
1338
234
|
|
|
1339
235
|
//# sourceMappingURL=Policy.js.map
|