@nmnmcc/ability 2.0.7 → 2.1.2
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 +52 -29
- package/dist/Authorizer.d.ts +21 -12
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +121 -19
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +32 -21
- package/dist/Condition.js.map +1 -1
- package/dist/Domain.d.ts +9 -0
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +5 -0
- package/dist/Domain.js.map +1 -1
- package/dist/Limits.d.ts +44 -13
- package/dist/Limits.d.ts.map +1 -1
- package/dist/Limits.js +52 -22
- package/dist/Limits.js.map +1 -1
- package/dist/Policy.d.ts +102 -18
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +7 -6
- package/dist/Policy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/internal/algebra.js +2 -2
- package/dist/internal/analysis.js +81 -75
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/authorizer.d.ts.map +1 -1
- package/dist/internal/authorizer.js +11 -7
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +86 -64
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts +33 -10
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +170 -77
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +27 -31
- package/dist/internal/decide.js.map +1 -1
- package/dist/internal/domain.js +19 -5
- package/dist/internal/domain.js.map +1 -1
- package/dist/internal/json.js +120 -76
- package/dist/internal/json.js.map +1 -1
- package/dist/internal/limits.d.ts +28 -0
- package/dist/internal/limits.d.ts.map +1 -1
- package/dist/internal/limits.js +75 -16
- package/dist/internal/limits.js.map +1 -1
- package/dist/internal/nodes.d.ts.map +1 -1
- package/dist/internal/nodes.js +53 -44
- package/dist/internal/nodes.js.map +1 -1
- package/dist/internal/patterns.js +50 -15
- package/dist/internal/patterns.js.map +1 -1
- package/dist/internal/policy-json.js +169 -127
- package/dist/internal/policy-json.js.map +1 -1
- package/dist/internal/policy-nodes.d.ts.map +1 -1
- package/dist/internal/policy-nodes.js +16 -13
- package/dist/internal/policy-nodes.js.map +1 -1
- package/dist/internal/policy.d.ts.map +1 -1
- package/dist/internal/policy.js +38 -22
- package/dist/internal/policy.js.map +1 -1
- package/dist/internal/resource.js +1 -1
- package/dist/internal/rule.d.ts.map +1 -1
- package/dist/internal/rule.js +6 -5
- package/dist/internal/rule.js.map +1 -1
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +106 -92
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/trampoline.js +19 -5
- package/dist/internal/trampoline.js.map +1 -1
- package/package.json +1 -1
- package/src/Authorizer.ts +30 -20
- package/src/Condition.ts +317 -86
- package/src/Domain.ts +9 -0
- package/src/Limits.ts +69 -29
- package/src/Policy.ts +143 -21
- package/src/internal/analysis.ts +154 -124
- package/src/internal/authorizer.ts +29 -14
- package/src/internal/bind.ts +193 -136
- package/src/internal/catalog.ts +320 -118
- package/src/internal/decide.ts +61 -50
- package/src/internal/domain.ts +39 -3
- package/src/internal/json.ts +212 -117
- package/src/internal/limits.ts +101 -31
- package/src/internal/nodes.ts +65 -47
- package/src/internal/patterns.ts +79 -25
- package/src/internal/policy-json.ts +266 -197
- package/src/internal/policy-nodes.ts +31 -23
- package/src/internal/policy.ts +63 -23
- package/src/internal/rule.ts +16 -3
- package/src/internal/search.ts +186 -136
- package/src/internal/trampoline.ts +51 -4
package/src/internal/json.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The JSON form of a condition. The decoder is a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The JSON form of a condition. The decoder is a walk of the trampoline, so a
|
|
3
|
+
* deep input does not fill the stack. It reads at most `jsonNodes` objects
|
|
4
|
+
* and array items, with a shared object counted for each place, so no input
|
|
5
|
+
* can take a very long time. It makes
|
|
6
6
|
* each node with its constructor, and it reads only the own data fields of
|
|
7
7
|
* the input, as a condition reads a value.
|
|
8
8
|
*/
|
|
@@ -12,7 +12,9 @@ import * as Result from "effect/Result"
|
|
|
12
12
|
import type * as SchemaIssue from "effect/SchemaIssue"
|
|
13
13
|
import * as Limits from "./limits.ts"
|
|
14
14
|
import * as Nodes from "./nodes.ts"
|
|
15
|
+
import * as Place from "./place.ts"
|
|
15
16
|
import { readPart } from "./read.ts"
|
|
17
|
+
import * as Trampoline from "./trampoline.ts"
|
|
16
18
|
|
|
17
19
|
type Node = Nodes.Node
|
|
18
20
|
|
|
@@ -27,84 +29,123 @@ export class Problem extends Data.TaggedError("Problem")<{
|
|
|
27
29
|
readonly issue?: SchemaIssue.Issue | undefined
|
|
28
30
|
}> {}
|
|
29
31
|
|
|
30
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* The count of the objects and items that one decode has read, with the
|
|
34
|
+
* limits of the decode. It belongs to one decode.
|
|
35
|
+
*/
|
|
31
36
|
export interface Budget {
|
|
32
37
|
used: number
|
|
38
|
+
readonly limits: Limits.Limits
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
export const problem = (path: ReadonlyArray<PropertyKey>, message: string) =>
|
|
36
42
|
new Problem({ path, message })
|
|
37
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The problem at a place of the input. A decoder keeps the place of each
|
|
46
|
+
* part, which shares the path of its parent, and makes the full path only for
|
|
47
|
+
* a problem: see `Place`.
|
|
48
|
+
*/
|
|
49
|
+
export const problemAt = (at: Place.Place | undefined, message: string) =>
|
|
50
|
+
problem(Place.pathOf(at), message)
|
|
51
|
+
|
|
38
52
|
/** Counts `count` more objects or items, and stops past the limit. */
|
|
39
53
|
export const spend = (
|
|
40
54
|
budget: Budget,
|
|
41
55
|
count: number,
|
|
42
|
-
|
|
56
|
+
at: Place.Place | undefined,
|
|
43
57
|
): void => {
|
|
44
58
|
budget.used += count
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
const most = budget.limits.jsonNodes
|
|
60
|
+
if (budget.used > most) {
|
|
61
|
+
throw problemAt(
|
|
62
|
+
at,
|
|
63
|
+
`The input has more than ${String(most)} objects and items. The most is ${String(most)}.`,
|
|
49
64
|
)
|
|
50
65
|
}
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Reads a field of the input at the place `at`. A field that is not plain
|
|
70
|
+
* data is a problem.
|
|
71
|
+
*/
|
|
54
72
|
export const read = (
|
|
55
73
|
input: unknown,
|
|
56
74
|
name: string,
|
|
57
|
-
|
|
75
|
+
at: Place.Place | undefined,
|
|
58
76
|
): unknown => {
|
|
59
77
|
try {
|
|
60
78
|
return readPart(input, name)
|
|
61
79
|
} catch {
|
|
62
|
-
throw
|
|
80
|
+
throw problemAt(Place.at(at, name), `The field "${name}" is not plain data`)
|
|
63
81
|
}
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
export const readString = (
|
|
67
85
|
input: unknown,
|
|
68
86
|
name: string,
|
|
69
|
-
|
|
87
|
+
at: Place.Place | undefined,
|
|
70
88
|
): string => {
|
|
71
|
-
const value = read(input, name,
|
|
89
|
+
const value = read(input, name, at)
|
|
72
90
|
if (typeof value !== "string") {
|
|
73
|
-
throw
|
|
91
|
+
throw problemAt(Place.at(at, name), `The field "${name}" must be a string`)
|
|
74
92
|
}
|
|
75
93
|
return value
|
|
76
94
|
}
|
|
77
95
|
|
|
78
|
-
/** Reads the items of an array. */
|
|
96
|
+
/** Reads the items of an array. The place of an item is `at`, `name`, index. */
|
|
79
97
|
export const readList = (
|
|
80
98
|
input: unknown,
|
|
81
99
|
name: string,
|
|
82
|
-
|
|
100
|
+
at: Place.Place | undefined,
|
|
83
101
|
budget: Budget,
|
|
84
102
|
): ReadonlyArray<unknown> => {
|
|
85
|
-
const value = read(input, name,
|
|
86
|
-
const
|
|
103
|
+
const value = read(input, name, at)
|
|
104
|
+
const place = Place.at(at, name)
|
|
87
105
|
if (!Array.isArray(value)) {
|
|
88
|
-
throw
|
|
106
|
+
throw problemAt(place, `The field "${name}" must be an array`)
|
|
89
107
|
}
|
|
90
108
|
const list: ReadonlyArray<unknown> = value
|
|
91
|
-
spend(budget, list.length,
|
|
109
|
+
spend(budget, list.length, place)
|
|
92
110
|
// `Array.makeBy` of Effect gives at least one item, so it can not make an
|
|
93
111
|
// empty list. An array-like object with a length makes one with no read
|
|
94
112
|
// of the items before the read rule.
|
|
95
113
|
return Array.from({ length: list.length }, (_, index) =>
|
|
96
|
-
read(list, String(index),
|
|
114
|
+
read(list, String(index), place),
|
|
97
115
|
)
|
|
98
116
|
}
|
|
99
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Reads a field path, and stops one with more parts than the limit of the
|
|
120
|
+
* decode. A node checks the ceiling only, since it does not know its domain.
|
|
121
|
+
*/
|
|
122
|
+
const readPath = (
|
|
123
|
+
input: unknown,
|
|
124
|
+
name: string,
|
|
125
|
+
at: Place.Place | undefined,
|
|
126
|
+
budget: Budget,
|
|
127
|
+
): string => {
|
|
128
|
+
const path = readString(input, name, at)
|
|
129
|
+
const most = budget.limits.fieldPathParts
|
|
130
|
+
const parts = path.split(".", most + 1).length
|
|
131
|
+
if (parts > most) {
|
|
132
|
+
throw problemAt(
|
|
133
|
+
Place.at(at, name),
|
|
134
|
+
`A field path has more than ${String(most)} parts. The most is ${String(most)}.`,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
return path
|
|
138
|
+
}
|
|
139
|
+
|
|
100
140
|
const isJsonScalar = (value: unknown): value is Nodes.Scalar =>
|
|
101
141
|
Nodes.isScalar(value) && (typeof value !== "number" || Number.isFinite(value))
|
|
102
142
|
|
|
103
143
|
const decodeOperand = (
|
|
104
144
|
input: unknown,
|
|
105
|
-
|
|
145
|
+
at: Place.Place | undefined,
|
|
146
|
+
budget: Budget,
|
|
106
147
|
): Nodes.Operand => {
|
|
107
|
-
const tag = read(input, "_tag",
|
|
148
|
+
const tag = read(input, "_tag", at)
|
|
108
149
|
if (tag === "Literal") {
|
|
109
150
|
// A missing value is not `null`: a test with `null` has its own
|
|
110
151
|
// meaning, and a lost value must not become one.
|
|
@@ -113,22 +154,22 @@ const decodeOperand = (
|
|
|
113
154
|
? Object.getOwnPropertyDescriptor(input, "value")
|
|
114
155
|
: undefined
|
|
115
156
|
if (own === undefined || ("value" in own && own.value === undefined)) {
|
|
116
|
-
throw
|
|
157
|
+
throw problemAt(Place.at(at, "value"), "A fixed value is missing")
|
|
117
158
|
}
|
|
118
|
-
const value = read(input, "value",
|
|
159
|
+
const value = read(input, "value", at)
|
|
119
160
|
if (!isJsonScalar(value)) {
|
|
120
|
-
throw
|
|
121
|
-
|
|
161
|
+
throw problemAt(
|
|
162
|
+
Place.at(at, "value"),
|
|
122
163
|
"A fixed value must be a string, a finite number, a boolean or null",
|
|
123
164
|
)
|
|
124
165
|
}
|
|
125
166
|
return Nodes.literal(value)
|
|
126
167
|
}
|
|
127
168
|
if (tag === "Principal") {
|
|
128
|
-
return Nodes.principalRef(
|
|
169
|
+
return Nodes.principalRef(readPath(input, "path", at, budget))
|
|
129
170
|
}
|
|
130
|
-
throw
|
|
131
|
-
|
|
171
|
+
throw problemAt(
|
|
172
|
+
Place.at(at, "_tag"),
|
|
132
173
|
'An operand must have the tag "Literal" or "Principal"',
|
|
133
174
|
)
|
|
134
175
|
}
|
|
@@ -136,96 +177,148 @@ const decodeOperand = (
|
|
|
136
177
|
const decodeOperands = (
|
|
137
178
|
input: unknown,
|
|
138
179
|
name: string,
|
|
139
|
-
|
|
180
|
+
at: Place.Place | undefined,
|
|
140
181
|
budget: Budget,
|
|
141
182
|
): ReadonlyArray<Nodes.Operand> =>
|
|
142
|
-
Arr.map(readList(input, name,
|
|
143
|
-
decodeOperand(operand,
|
|
183
|
+
Arr.map(readList(input, name, at, budget), (operand, index) =>
|
|
184
|
+
decodeOperand(operand, Place.at(at, name, index), budget),
|
|
144
185
|
)
|
|
145
186
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
budget: Budget,
|
|
151
|
-
): Node => {
|
|
152
|
-
if (depth > Limits.conditionDepth) {
|
|
153
|
-
throw problem(
|
|
154
|
-
path,
|
|
155
|
-
`A condition has more than ${String(Limits.conditionDepth)} levels. The most is ${String(Limits.conditionDepth)}.`,
|
|
156
|
-
)
|
|
157
|
-
}
|
|
158
|
-
if (typeof input !== "object" || input === null) {
|
|
159
|
-
throw problem(path, "A condition node must be an object")
|
|
160
|
-
}
|
|
161
|
-
spend(budget, 1, path)
|
|
162
|
-
const children = (name: string) =>
|
|
163
|
-
Arr.map(readList(input, name, path, budget), (child, index) =>
|
|
164
|
-
decodeNode(child, [...path, name, index], depth + 1, budget),
|
|
165
|
-
)
|
|
166
|
-
try {
|
|
167
|
-
return makeNode(input, path, depth, budget, children)
|
|
168
|
-
} catch (error) {
|
|
169
|
-
// A node past a limit, such as a field path with too many parts, is a
|
|
170
|
-
// problem at its place.
|
|
171
|
-
if (error instanceof Limits.LimitExceeded) {
|
|
172
|
-
throw problem(path, error.message)
|
|
173
|
-
}
|
|
174
|
-
throw error
|
|
175
|
-
}
|
|
187
|
+
/** A part of the input to decode, with its place. */
|
|
188
|
+
interface Part {
|
|
189
|
+
readonly input: unknown
|
|
190
|
+
readonly at: Place.Place | undefined
|
|
176
191
|
}
|
|
177
192
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
/**
|
|
194
|
+
* The decoder of a condition, with the budget of one decode. It is a walk of
|
|
195
|
+
* the trampoline for an input of unknown depth, so a deep input does not fill
|
|
196
|
+
* the stack.
|
|
197
|
+
*/
|
|
198
|
+
const decoder = (budget: Budget): ((part: Part) => Node) => {
|
|
199
|
+
// The nodes of this condition that the decoder has read. A condition with
|
|
200
|
+
// more nodes than `conditionNodes` does not load, so the decoder stops at
|
|
201
|
+
// the first node past it, and it does not read the rest of a very large
|
|
202
|
+
// input. The count belongs to the decode of one condition.
|
|
203
|
+
const count = { nodes: 0 }
|
|
204
|
+
return Trampoline.adaptive<Part, Node>((R, child) => (part, depth) => {
|
|
205
|
+
const { input, at } = part
|
|
206
|
+
if (typeof input !== "object" || input === null) {
|
|
207
|
+
throw problemAt(at, "A condition node must be an object")
|
|
208
|
+
}
|
|
209
|
+
spend(budget, 1, at)
|
|
210
|
+
count.nodes += 1
|
|
211
|
+
if (count.nodes > budget.limits.conditionNodes) {
|
|
212
|
+
throw problemAt(
|
|
213
|
+
at,
|
|
214
|
+
`A condition has more than ${String(budget.limits.conditionNodes)} nodes and values. The most is ${String(budget.limits.conditionNodes)}.`,
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
// A node past a limit, such as a field path with too many parts, is a
|
|
218
|
+
// problem at its place. The constructor checks the ceilings, and the
|
|
219
|
+
// decoder checks the limits of the decode.
|
|
220
|
+
const make = (build: () => Node): Node => {
|
|
221
|
+
const node = (() => {
|
|
222
|
+
try {
|
|
223
|
+
return build()
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error instanceof Limits.LimitExceeded) {
|
|
226
|
+
throw problemAt(at, error.message)
|
|
227
|
+
}
|
|
228
|
+
throw error
|
|
229
|
+
}
|
|
230
|
+
})()
|
|
231
|
+
const most = budget.limits.conditionNodes
|
|
232
|
+
if (node.weight > most) {
|
|
233
|
+
throw problemAt(
|
|
234
|
+
at,
|
|
235
|
+
`A condition has ${String(node.weight)} nodes and values. The most is ${String(most)}.`,
|
|
236
|
+
)
|
|
195
237
|
}
|
|
196
|
-
return
|
|
197
|
-
op,
|
|
198
|
-
field: readString(input, "field", path),
|
|
199
|
-
value: decodeOperand(read(input, "value", path), [...path, "value"]),
|
|
200
|
-
})
|
|
238
|
+
return node
|
|
201
239
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
values: decodeOperands(input, "values", path, budget),
|
|
206
|
-
})
|
|
207
|
-
case "Exists":
|
|
208
|
-
return new Nodes.Exists({ field: readString(input, "field", path) })
|
|
209
|
-
case "Under":
|
|
210
|
-
return new Nodes.Under({
|
|
211
|
-
prefix: decodeOperands(input, "prefix", path, budget),
|
|
212
|
-
})
|
|
213
|
-
case "And":
|
|
214
|
-
return new Nodes.And(children("conditions"))
|
|
215
|
-
case "Or":
|
|
216
|
-
return new Nodes.Or(children("conditions"))
|
|
217
|
-
case "Not":
|
|
218
|
-
return new Nodes.Not(
|
|
219
|
-
decodeNode(
|
|
220
|
-
read(input, "condition", path),
|
|
221
|
-
[...path, "condition"],
|
|
222
|
-
depth + 1,
|
|
223
|
-
budget,
|
|
224
|
-
),
|
|
240
|
+
const children = (name: string) =>
|
|
241
|
+
R.forEach(readList(input, name, at, budget), (item, index) =>
|
|
242
|
+
child({ input: item, at: Place.at(at, name, index) }, depth + 1),
|
|
225
243
|
)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
244
|
+
const tag = read(input, "_tag", at)
|
|
245
|
+
switch (tag) {
|
|
246
|
+
case "True":
|
|
247
|
+
return R.succeed(Nodes.always)
|
|
248
|
+
case "False":
|
|
249
|
+
return R.succeed(Nodes.never)
|
|
250
|
+
case "Compare": {
|
|
251
|
+
const op = readString(input, "op", at)
|
|
252
|
+
if (!Nodes.isCompareOperator(op)) {
|
|
253
|
+
throw problemAt(
|
|
254
|
+
Place.at(at, "op"),
|
|
255
|
+
`"${op}" is not a compare operator`,
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
return R.succeed(
|
|
259
|
+
make(
|
|
260
|
+
() =>
|
|
261
|
+
new Nodes.Compare({
|
|
262
|
+
op,
|
|
263
|
+
field: readPath(input, "field", at, budget),
|
|
264
|
+
value: decodeOperand(
|
|
265
|
+
read(input, "value", at),
|
|
266
|
+
Place.at(at, "value"),
|
|
267
|
+
budget,
|
|
268
|
+
),
|
|
269
|
+
}),
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
case "In":
|
|
274
|
+
return R.succeed(
|
|
275
|
+
make(
|
|
276
|
+
() =>
|
|
277
|
+
new Nodes.In({
|
|
278
|
+
field: readPath(input, "field", at, budget),
|
|
279
|
+
values: decodeOperands(input, "values", at, budget),
|
|
280
|
+
}),
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
case "Exists":
|
|
284
|
+
return R.succeed(
|
|
285
|
+
make(
|
|
286
|
+
() =>
|
|
287
|
+
new Nodes.Exists({ field: readPath(input, "field", at, budget) }),
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
case "Under":
|
|
291
|
+
return R.succeed(
|
|
292
|
+
make(
|
|
293
|
+
() =>
|
|
294
|
+
new Nodes.Under({
|
|
295
|
+
prefix: decodeOperands(input, "prefix", at, budget),
|
|
296
|
+
}),
|
|
297
|
+
),
|
|
298
|
+
)
|
|
299
|
+
case "And":
|
|
300
|
+
return R.map(children("conditions"), (conditions) =>
|
|
301
|
+
make(() => new Nodes.And(conditions)),
|
|
302
|
+
)
|
|
303
|
+
case "Or":
|
|
304
|
+
return R.map(children("conditions"), (conditions) =>
|
|
305
|
+
make(() => new Nodes.Or(conditions)),
|
|
306
|
+
)
|
|
307
|
+
case "Not":
|
|
308
|
+
return R.map(
|
|
309
|
+
child(
|
|
310
|
+
{
|
|
311
|
+
input: read(input, "condition", at),
|
|
312
|
+
at: Place.at(at, "condition"),
|
|
313
|
+
},
|
|
314
|
+
depth + 1,
|
|
315
|
+
),
|
|
316
|
+
(condition) => make(() => new Nodes.Not(condition)),
|
|
317
|
+
)
|
|
318
|
+
default:
|
|
319
|
+
throw problemAt(Place.at(at, "_tag"), "The node has no known tag")
|
|
320
|
+
}
|
|
321
|
+
})
|
|
229
322
|
}
|
|
230
323
|
|
|
231
324
|
/**
|
|
@@ -234,14 +327,16 @@ const makeNode = (
|
|
|
234
327
|
*/
|
|
235
328
|
export const decodeAt = (
|
|
236
329
|
input: unknown,
|
|
237
|
-
|
|
330
|
+
at: Place.Place | undefined,
|
|
238
331
|
budget: Budget,
|
|
239
|
-
): Node =>
|
|
332
|
+
): Node => decoder(budget)({ input, at })
|
|
240
333
|
|
|
241
|
-
/** Decodes the JSON form of a condition. */
|
|
334
|
+
/** Decodes the JSON form of a condition, with the default limits. */
|
|
242
335
|
export const decode = (input: unknown): Result.Result<Node, Problem> => {
|
|
243
336
|
try {
|
|
244
|
-
return Result.succeed(
|
|
337
|
+
return Result.succeed(
|
|
338
|
+
decoder({ used: 0, limits: Limits.defaults })({ input, at: undefined }),
|
|
339
|
+
)
|
|
245
340
|
} catch (error) {
|
|
246
341
|
if (error instanceof Problem) {
|
|
247
342
|
return Result.fail(error)
|
package/src/internal/limits.ts
CHANGED
|
@@ -1,50 +1,120 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one table of limits. Each limit keeps a walk, a decoder or a search
|
|
3
|
-
* bounded, so no input can
|
|
4
|
-
*
|
|
5
|
-
* for the parts of a field path.
|
|
3
|
+
* bounded, so no input can take a very long time or very much memory. A
|
|
4
|
+
* domain may set its own limits, up to the ceilings.
|
|
6
5
|
*/
|
|
7
6
|
import * as Schema from "effect/Schema"
|
|
8
7
|
|
|
9
|
-
/** The
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
/** The values of all the limits. */
|
|
9
|
+
export interface Limits {
|
|
10
|
+
/** The most parts of a field path, as in `settings.model`. */
|
|
11
|
+
readonly fieldPathParts: number
|
|
12
|
+
/** The most parts of a field pattern, as in `**.secret`. */
|
|
13
|
+
readonly patternParts: number
|
|
14
|
+
/**
|
|
15
|
+
* The most nodes of one condition. A child that a condition uses in many
|
|
16
|
+
* places is counted for each place.
|
|
17
|
+
*/
|
|
18
|
+
readonly conditionNodes: number
|
|
19
|
+
/** The most nodes of one residual condition. */
|
|
20
|
+
readonly residualNodes: number
|
|
21
|
+
/**
|
|
22
|
+
* The most nodes of a policy tree, with its rules and the nodes of their
|
|
23
|
+
* conditions. A part that the tree uses in many places is counted for
|
|
24
|
+
* each place.
|
|
25
|
+
*/
|
|
26
|
+
readonly policyNodes: number
|
|
27
|
+
/** The most objects and arrays in one input to a decoder. */
|
|
28
|
+
readonly jsonNodes: number
|
|
29
|
+
/** The most fields in the list of the fields of one Schema. */
|
|
30
|
+
readonly catalogFields: number
|
|
31
|
+
/** The most steps of one search for a value. */
|
|
32
|
+
readonly searchSteps: number
|
|
33
|
+
/** The most fields that one search for a value may test. */
|
|
34
|
+
readonly searchFields: number
|
|
35
|
+
}
|
|
14
36
|
|
|
15
37
|
/**
|
|
16
|
-
* The
|
|
17
|
-
*
|
|
38
|
+
* The limits of a domain that sets none. A walk does not fill the stack at
|
|
39
|
+
* any depth (see `Trampoline`), and the levels of a condition or of a policy
|
|
40
|
+
* tree are at most its nodes, so the limits of the nodes keep the time and
|
|
41
|
+
* the memory of a deep input small too. There is no limit of levels.
|
|
18
42
|
*/
|
|
19
|
-
export const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
43
|
+
export const defaults: Limits = {
|
|
44
|
+
fieldPathParts: 1000,
|
|
45
|
+
patternParts: 1000,
|
|
46
|
+
conditionNodes: 1_000_000,
|
|
47
|
+
residualNodes: 100_000,
|
|
48
|
+
policyNodes: 1_000_000,
|
|
49
|
+
jsonNodes: 10_000_000,
|
|
50
|
+
catalogFields: 100_000,
|
|
51
|
+
// A search is the work of one check of a kind, as in the Authorizer, so
|
|
52
|
+
// its steps are a limit of the time of one request.
|
|
53
|
+
searchSteps: 1_000_000,
|
|
54
|
+
searchFields: 1000,
|
|
55
|
+
}
|
|
23
56
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The most that a domain may set for each limit. The constructors of the
|
|
59
|
+
* nodes check these, since a node does not know its domain. The binding to a
|
|
60
|
+
* domain checks the limits of the domain.
|
|
61
|
+
*/
|
|
62
|
+
export const ceilings: Limits = {
|
|
63
|
+
fieldPathParts: 10_000,
|
|
64
|
+
patternParts: 10_000,
|
|
65
|
+
conditionNodes: 100_000_000,
|
|
66
|
+
residualNodes: 100_000_000,
|
|
67
|
+
policyNodes: 100_000_000,
|
|
68
|
+
jsonNodes: 1_000_000_000,
|
|
69
|
+
catalogFields: 10_000_000,
|
|
70
|
+
searchSteps: 1_000_000_000,
|
|
71
|
+
searchFields: 100_000,
|
|
72
|
+
}
|
|
26
73
|
|
|
27
|
-
/** The
|
|
28
|
-
|
|
74
|
+
/** The Schema of one limit: a whole number from 1 up to its ceiling. */
|
|
75
|
+
const limit = (name: keyof Limits) =>
|
|
76
|
+
Schema.optionalKey(
|
|
77
|
+
Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: ceilings[name] })),
|
|
78
|
+
)
|
|
29
79
|
|
|
30
80
|
/**
|
|
31
|
-
* The
|
|
32
|
-
*
|
|
33
|
-
*
|
|
81
|
+
* The Schema of the limits that a domain sets. Each one is a whole number
|
|
82
|
+
* from 1 up to its ceiling, and a limit that the domain leaves out has its
|
|
83
|
+
* default.
|
|
34
84
|
*/
|
|
35
|
-
export const
|
|
85
|
+
export const Options = Schema.Struct({
|
|
86
|
+
fieldPathParts: limit("fieldPathParts"),
|
|
87
|
+
patternParts: limit("patternParts"),
|
|
88
|
+
conditionNodes: limit("conditionNodes"),
|
|
89
|
+
residualNodes: limit("residualNodes"),
|
|
90
|
+
policyNodes: limit("policyNodes"),
|
|
91
|
+
jsonNodes: limit("jsonNodes"),
|
|
92
|
+
catalogFields: limit("catalogFields"),
|
|
93
|
+
searchSteps: limit("searchSteps"),
|
|
94
|
+
searchFields: limit("searchFields"),
|
|
95
|
+
})
|
|
36
96
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Makes the limits of a domain from the ones that it sets. A value that is
|
|
99
|
+
* not a whole number from 1 up to its ceiling is a mistake in the code, so
|
|
100
|
+
* it is a defect.
|
|
101
|
+
*/
|
|
102
|
+
export const make = (options: unknown = {}): Limits => ({
|
|
103
|
+
...defaults,
|
|
104
|
+
...Schema.decodeUnknownSync(Options)(options),
|
|
105
|
+
})
|
|
42
106
|
|
|
43
|
-
|
|
44
|
-
export const searchSteps = 1_000_000
|
|
107
|
+
// The default of each limit, by its name.
|
|
45
108
|
|
|
46
|
-
|
|
47
|
-
export const
|
|
109
|
+
export const fieldPathParts = defaults.fieldPathParts
|
|
110
|
+
export const patternParts = defaults.patternParts
|
|
111
|
+
export const conditionNodes = defaults.conditionNodes
|
|
112
|
+
export const residualNodes = defaults.residualNodes
|
|
113
|
+
export const policyNodes = defaults.policyNodes
|
|
114
|
+
export const jsonNodes = defaults.jsonNodes
|
|
115
|
+
export const catalogFields = defaults.catalogFields
|
|
116
|
+
export const searchSteps = defaults.searchSteps
|
|
117
|
+
export const searchFields = defaults.searchFields
|
|
48
118
|
|
|
49
119
|
/**
|
|
50
120
|
* The error when an input goes past a limit. It names the limit, the size of
|