@nmnmcc/ability 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -87
- 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 +28 -11
- 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 +274 -272
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +71 -1180
- package/dist/Policy.js.map +1 -1
- package/dist/Resource.d.ts +8 -181
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +8 -807
- 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 +286 -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 +255 -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 +93 -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 +4 -5
- package/src/Authorizer.ts +69 -585
- package/src/Condition.ts +339 -2095
- package/src/Decision.ts +6 -5
- package/src/Domain.ts +13 -70
- package/src/HttpApi.ts +75 -13
- package/src/Limits.ts +132 -0
- package/src/Policy.ts +386 -2772
- package/src/Resource.ts +14 -1555
- 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 +596 -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 +484 -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 +181 -0
- package/src/internal/search.ts +541 -0
- package/src/internal/suggest.ts +104 -0
- package/dist/Principal.d.ts +0 -35
- package/dist/Principal.d.ts.map +0 -1
- package/dist/Principal.js +0 -37
- package/dist/Principal.js.map +0 -1
- package/dist/Rule.d.ts +0 -275
- package/dist/Rule.d.ts.map +0 -1
- package/dist/Rule.js +0 -257
- package/dist/Rule.js.map +0 -1
- package/src/Principal.ts +0 -41
- package/src/Rule.ts +0 -683
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSON form of a condition. The decoder is a plain walk with two limits:
|
|
3
|
+
* it goes at most `conditionDepth` levels down, and it reads at most
|
|
4
|
+
* `jsonNodes` objects and array items, with a shared object counted for each
|
|
5
|
+
* place. So no input can fill the stack or take a very long time. It makes
|
|
6
|
+
* each node with its constructor, and it reads only the own data fields of
|
|
7
|
+
* the input, as a condition reads a value.
|
|
8
|
+
*/
|
|
9
|
+
import * as Arr from "effect/Array"
|
|
10
|
+
import * as Data from "effect/Data"
|
|
11
|
+
import * as Result from "effect/Result"
|
|
12
|
+
import type * as SchemaIssue from "effect/SchemaIssue"
|
|
13
|
+
import * as Limits from "./limits.ts"
|
|
14
|
+
import * as Nodes from "./nodes.ts"
|
|
15
|
+
import { readPart } from "./read.ts"
|
|
16
|
+
|
|
17
|
+
type Node = Nodes.Node
|
|
18
|
+
|
|
19
|
+
/** Gives the JSON form of a condition with no predicate. See `Nodes.toJson`. */
|
|
20
|
+
export const toJson = (node: Node): unknown => Nodes.toJson(node)
|
|
21
|
+
|
|
22
|
+
/** A problem of an input, with the path of the problem. */
|
|
23
|
+
export class Problem extends Data.TaggedError("Problem")<{
|
|
24
|
+
readonly path: ReadonlyArray<PropertyKey>
|
|
25
|
+
readonly message: string
|
|
26
|
+
/** The issue of a Schema that the decode used, as for `meta`. */
|
|
27
|
+
readonly issue?: SchemaIssue.Issue | undefined
|
|
28
|
+
}> {}
|
|
29
|
+
|
|
30
|
+
/** The count of the objects and items that one decode has read. It belongs to one decode. */
|
|
31
|
+
export interface Budget {
|
|
32
|
+
used: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const problem = (path: ReadonlyArray<PropertyKey>, message: string) =>
|
|
36
|
+
new Problem({ path, message })
|
|
37
|
+
|
|
38
|
+
/** Counts `count` more objects or items, and stops past the limit. */
|
|
39
|
+
export const spend = (
|
|
40
|
+
budget: Budget,
|
|
41
|
+
count: number,
|
|
42
|
+
path: ReadonlyArray<PropertyKey>,
|
|
43
|
+
): void => {
|
|
44
|
+
budget.used += count
|
|
45
|
+
if (budget.used > Limits.jsonNodes) {
|
|
46
|
+
throw problem(
|
|
47
|
+
path,
|
|
48
|
+
`The input has more than ${String(Limits.jsonNodes)} objects and items. The most is ${String(Limits.jsonNodes)}.`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Reads a field of the input. A field that is not plain data is a problem. */
|
|
54
|
+
export const read = (
|
|
55
|
+
input: unknown,
|
|
56
|
+
name: string,
|
|
57
|
+
path: ReadonlyArray<PropertyKey>,
|
|
58
|
+
): unknown => {
|
|
59
|
+
try {
|
|
60
|
+
return readPart(input, name)
|
|
61
|
+
} catch {
|
|
62
|
+
throw problem([...path, name], `The field "${name}" is not plain data`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const readString = (
|
|
67
|
+
input: unknown,
|
|
68
|
+
name: string,
|
|
69
|
+
path: ReadonlyArray<PropertyKey>,
|
|
70
|
+
): string => {
|
|
71
|
+
const value = read(input, name, path)
|
|
72
|
+
if (typeof value !== "string") {
|
|
73
|
+
throw problem([...path, name], `The field "${name}" must be a string`)
|
|
74
|
+
}
|
|
75
|
+
return value
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Reads the items of an array. */
|
|
79
|
+
export const readList = (
|
|
80
|
+
input: unknown,
|
|
81
|
+
name: string,
|
|
82
|
+
path: ReadonlyArray<PropertyKey>,
|
|
83
|
+
budget: Budget,
|
|
84
|
+
): ReadonlyArray<unknown> => {
|
|
85
|
+
const value = read(input, name, path)
|
|
86
|
+
const at = [...path, name]
|
|
87
|
+
if (!Array.isArray(value)) {
|
|
88
|
+
throw problem(at, `The field "${name}" must be an array`)
|
|
89
|
+
}
|
|
90
|
+
const list: ReadonlyArray<unknown> = value
|
|
91
|
+
spend(budget, list.length, at)
|
|
92
|
+
// `Array.makeBy` of Effect gives at least one item, so it can not make an
|
|
93
|
+
// empty list. An array-like object with a length makes one with no read
|
|
94
|
+
// of the items before the read rule.
|
|
95
|
+
return Array.from({ length: list.length }, (_, index) =>
|
|
96
|
+
read(list, String(index), at),
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const isJsonScalar = (value: unknown): value is Nodes.Scalar =>
|
|
101
|
+
Nodes.isScalar(value) && (typeof value !== "number" || Number.isFinite(value))
|
|
102
|
+
|
|
103
|
+
const decodeOperand = (
|
|
104
|
+
input: unknown,
|
|
105
|
+
path: ReadonlyArray<PropertyKey>,
|
|
106
|
+
): Nodes.Operand => {
|
|
107
|
+
const tag = read(input, "_tag", path)
|
|
108
|
+
if (tag === "Literal") {
|
|
109
|
+
// A missing value is not `null`: a test with `null` has its own
|
|
110
|
+
// meaning, and a lost value must not become one.
|
|
111
|
+
const own =
|
|
112
|
+
typeof input === "object" && input !== null
|
|
113
|
+
? Object.getOwnPropertyDescriptor(input, "value")
|
|
114
|
+
: undefined
|
|
115
|
+
if (own === undefined || ("value" in own && own.value === undefined)) {
|
|
116
|
+
throw problem([...path, "value"], "A fixed value is missing")
|
|
117
|
+
}
|
|
118
|
+
const value = read(input, "value", path)
|
|
119
|
+
if (!isJsonScalar(value)) {
|
|
120
|
+
throw problem(
|
|
121
|
+
[...path, "value"],
|
|
122
|
+
"A fixed value must be a string, a finite number, a boolean or null",
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
return Nodes.literal(value)
|
|
126
|
+
}
|
|
127
|
+
if (tag === "Principal") {
|
|
128
|
+
return Nodes.principalRef(readString(input, "path", path))
|
|
129
|
+
}
|
|
130
|
+
throw problem(
|
|
131
|
+
[...path, "_tag"],
|
|
132
|
+
'An operand must have the tag "Literal" or "Principal"',
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const decodeOperands = (
|
|
137
|
+
input: unknown,
|
|
138
|
+
name: string,
|
|
139
|
+
path: ReadonlyArray<PropertyKey>,
|
|
140
|
+
budget: Budget,
|
|
141
|
+
): ReadonlyArray<Nodes.Operand> =>
|
|
142
|
+
Arr.map(readList(input, name, path, budget), (operand, index) =>
|
|
143
|
+
decodeOperand(operand, [...path, name, index]),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const compareOperators: ReadonlyArray<string> = [
|
|
147
|
+
"eq",
|
|
148
|
+
"ne",
|
|
149
|
+
"lt",
|
|
150
|
+
"lte",
|
|
151
|
+
"gt",
|
|
152
|
+
"gte",
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
const decodeNode = (
|
|
156
|
+
input: unknown,
|
|
157
|
+
path: ReadonlyArray<PropertyKey>,
|
|
158
|
+
depth: number,
|
|
159
|
+
budget: Budget,
|
|
160
|
+
): Node => {
|
|
161
|
+
if (depth > Limits.conditionDepth) {
|
|
162
|
+
throw problem(
|
|
163
|
+
path,
|
|
164
|
+
`A condition has more than ${String(Limits.conditionDepth)} levels. The most is ${String(Limits.conditionDepth)}.`,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
if (typeof input !== "object" || input === null) {
|
|
168
|
+
throw problem(path, "A condition node must be an object")
|
|
169
|
+
}
|
|
170
|
+
spend(budget, 1, path)
|
|
171
|
+
const children = (name: string) =>
|
|
172
|
+
Arr.map(readList(input, name, path, budget), (child, index) =>
|
|
173
|
+
decodeNode(child, [...path, name, index], depth + 1, budget),
|
|
174
|
+
)
|
|
175
|
+
try {
|
|
176
|
+
return makeNode(input, path, depth, budget, children)
|
|
177
|
+
} catch (error) {
|
|
178
|
+
// A node past a limit, such as a field path with too many parts, is a
|
|
179
|
+
// problem at its place.
|
|
180
|
+
if (error instanceof Limits.LimitExceeded) {
|
|
181
|
+
throw problem(path, error.message)
|
|
182
|
+
}
|
|
183
|
+
throw error
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const makeNode = (
|
|
188
|
+
input: object,
|
|
189
|
+
path: ReadonlyArray<PropertyKey>,
|
|
190
|
+
depth: number,
|
|
191
|
+
budget: Budget,
|
|
192
|
+
children: (name: string) => ReadonlyArray<Node>,
|
|
193
|
+
): Node => {
|
|
194
|
+
const tag = read(input, "_tag", path)
|
|
195
|
+
switch (tag) {
|
|
196
|
+
case "True":
|
|
197
|
+
return Nodes.always
|
|
198
|
+
case "False":
|
|
199
|
+
return Nodes.never
|
|
200
|
+
case "Compare": {
|
|
201
|
+
const op = readString(input, "op", path)
|
|
202
|
+
if (!compareOperators.includes(op)) {
|
|
203
|
+
throw problem([...path, "op"], `"${op}" is not a compare operator`)
|
|
204
|
+
}
|
|
205
|
+
return new Nodes.Compare({
|
|
206
|
+
op: op as Nodes.CompareOperator,
|
|
207
|
+
field: readString(input, "field", path),
|
|
208
|
+
value: decodeOperand(read(input, "value", path), [...path, "value"]),
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
case "In":
|
|
212
|
+
return new Nodes.In({
|
|
213
|
+
field: readString(input, "field", path),
|
|
214
|
+
values: decodeOperands(input, "values", path, budget),
|
|
215
|
+
})
|
|
216
|
+
case "Exists":
|
|
217
|
+
return new Nodes.Exists({ field: readString(input, "field", path) })
|
|
218
|
+
case "Under":
|
|
219
|
+
return new Nodes.Under({
|
|
220
|
+
prefix: decodeOperands(input, "prefix", path, budget),
|
|
221
|
+
})
|
|
222
|
+
case "And":
|
|
223
|
+
return new Nodes.And(children("conditions"))
|
|
224
|
+
case "Or":
|
|
225
|
+
return new Nodes.Or(children("conditions"))
|
|
226
|
+
case "Not":
|
|
227
|
+
return new Nodes.Not(
|
|
228
|
+
decodeNode(
|
|
229
|
+
read(input, "condition", path),
|
|
230
|
+
[...path, "condition"],
|
|
231
|
+
depth + 1,
|
|
232
|
+
budget,
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
default:
|
|
236
|
+
throw problem([...path, "_tag"], "The node has no known tag")
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Decodes the JSON form of a condition at a path, with the budget of a larger
|
|
242
|
+
* decode. It throws a {@link Problem}.
|
|
243
|
+
*/
|
|
244
|
+
export const decodeAt = (
|
|
245
|
+
input: unknown,
|
|
246
|
+
path: ReadonlyArray<PropertyKey>,
|
|
247
|
+
budget: Budget,
|
|
248
|
+
): Node => decodeNode(input, path, 1, budget)
|
|
249
|
+
|
|
250
|
+
/** Decodes the JSON form of a condition. */
|
|
251
|
+
export const decode = (input: unknown): Result.Result<Node, Problem> => {
|
|
252
|
+
try {
|
|
253
|
+
return Result.succeed(decodeNode(input, [], 1, { used: 0 }))
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error instanceof Problem) {
|
|
256
|
+
return Result.fail(error)
|
|
257
|
+
}
|
|
258
|
+
throw error
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one table of limits. Each limit keeps a walk, a decoder or a search
|
|
3
|
+
* bounded, so no input can fill the stack or take a very long time. The
|
|
4
|
+
* values are the same in the types and at run time where both have one, as
|
|
5
|
+
* for the parts of a field path.
|
|
6
|
+
*/
|
|
7
|
+
import * as Schema from "effect/Schema"
|
|
8
|
+
|
|
9
|
+
/** The most parts of a field path, as in `settings.model`. */
|
|
10
|
+
export const fieldPathParts = 5
|
|
11
|
+
|
|
12
|
+
/** The most parts of a field pattern, as in `**.secret`. */
|
|
13
|
+
export const patternParts = 16
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The most nodes of one condition. A child that a condition uses in many
|
|
17
|
+
* places is counted for each place.
|
|
18
|
+
*/
|
|
19
|
+
export const conditionNodes = 100_000
|
|
20
|
+
|
|
21
|
+
/** The most levels of one condition. */
|
|
22
|
+
export const conditionDepth = 500
|
|
23
|
+
|
|
24
|
+
/** The most nodes of one residual condition. */
|
|
25
|
+
export const residualNodes = 10_000
|
|
26
|
+
|
|
27
|
+
/** The most levels of a policy tree. */
|
|
28
|
+
export const policyDepth = 256
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The most nodes of a policy tree, with its rules and the nodes of their
|
|
32
|
+
* conditions. A part that the tree uses in many places is counted for each
|
|
33
|
+
* place.
|
|
34
|
+
*/
|
|
35
|
+
export const policyNodes = 100_000
|
|
36
|
+
|
|
37
|
+
/** The most objects and arrays in one input to a decoder. */
|
|
38
|
+
export const jsonNodes = 1_000_000
|
|
39
|
+
|
|
40
|
+
/** The most fields in the catalog of one Schema. */
|
|
41
|
+
export const catalogFields = 10_000
|
|
42
|
+
|
|
43
|
+
/** The most steps of one search for a value. */
|
|
44
|
+
export const searchSteps = 1_000_000
|
|
45
|
+
|
|
46
|
+
/** The most fields that one search for a value may test. */
|
|
47
|
+
export const searchFields = 256
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The error when an input goes past a limit. It names the limit, the size of
|
|
51
|
+
* the input, and the most that the limit takes.
|
|
52
|
+
*/
|
|
53
|
+
export class LimitExceeded extends Schema.TaggedError<LimitExceeded>(
|
|
54
|
+
"@nmnmcc/ability/Limits/LimitExceeded",
|
|
55
|
+
)("LimitExceeded", {
|
|
56
|
+
limit: Schema.String,
|
|
57
|
+
size: Schema.Finite,
|
|
58
|
+
max: Schema.Finite,
|
|
59
|
+
message: Schema.String,
|
|
60
|
+
}) {}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Makes the error for a limit, with one form of message for every limit, as
|
|
64
|
+
* in "A condition has 100001 nodes. The most is 100000."
|
|
65
|
+
*/
|
|
66
|
+
export const exceeded = (
|
|
67
|
+
thing: string,
|
|
68
|
+
limit: string,
|
|
69
|
+
unit: string,
|
|
70
|
+
size: number,
|
|
71
|
+
max: number,
|
|
72
|
+
): LimitExceeded =>
|
|
73
|
+
LimitExceeded.make({
|
|
74
|
+
limit,
|
|
75
|
+
size,
|
|
76
|
+
max,
|
|
77
|
+
message: `${thing} has ${String(size)} ${unit}. The most is ${String(max)}.`,
|
|
78
|
+
})
|