@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.
Files changed (127) hide show
  1. package/README.md +112 -69
  2. package/dist/Authorizer.d.ts +43 -47
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js +5 -290
  5. package/dist/Authorizer.js.map +1 -1
  6. package/dist/Condition.d.ts +164 -332
  7. package/dist/Condition.d.ts.map +1 -1
  8. package/dist/Condition.js +75 -890
  9. package/dist/Condition.js.map +1 -1
  10. package/dist/Decision.d.ts +13 -13
  11. package/dist/Decision.d.ts.map +1 -1
  12. package/dist/Decision.js.map +1 -1
  13. package/dist/Domain.d.ts +10 -21
  14. package/dist/Domain.d.ts.map +1 -1
  15. package/dist/Domain.js +4 -62
  16. package/dist/Domain.js.map +1 -1
  17. package/dist/HttpApi.d.ts +31 -7
  18. package/dist/HttpApi.d.ts.map +1 -1
  19. package/dist/HttpApi.js +25 -8
  20. package/dist/HttpApi.js.map +1 -1
  21. package/dist/Limits.d.ts +114 -0
  22. package/dist/Limits.d.ts.map +1 -0
  23. package/dist/Limits.js +132 -0
  24. package/dist/Limits.js.map +1 -0
  25. package/dist/Policy.d.ts +276 -269
  26. package/dist/Policy.d.ts.map +1 -1
  27. package/dist/Policy.js +73 -1177
  28. package/dist/Policy.js.map +1 -1
  29. package/dist/Resource.d.ts +4 -177
  30. package/dist/Resource.d.ts.map +1 -1
  31. package/dist/Resource.js +4 -803
  32. package/dist/Resource.js.map +1 -1
  33. package/dist/index.d.ts +3 -4
  34. package/dist/index.js +4 -5
  35. package/dist/internal/algebra.js +267 -0
  36. package/dist/internal/algebra.js.map +1 -0
  37. package/dist/internal/analysis.js +245 -0
  38. package/dist/internal/analysis.js.map +1 -0
  39. package/dist/internal/authorizer.d.ts +30 -0
  40. package/dist/internal/authorizer.d.ts.map +1 -0
  41. package/dist/internal/authorizer.js +221 -0
  42. package/dist/internal/authorizer.js.map +1 -0
  43. package/dist/internal/bind.js +311 -0
  44. package/dist/internal/bind.js.map +1 -0
  45. package/dist/internal/catalog.d.ts +56 -0
  46. package/dist/internal/catalog.d.ts.map +1 -0
  47. package/dist/internal/catalog.js +292 -0
  48. package/dist/internal/catalog.js.map +1 -0
  49. package/dist/internal/decide.js +166 -0
  50. package/dist/internal/decide.js.map +1 -0
  51. package/dist/internal/domain.js +32 -0
  52. package/dist/internal/domain.js.map +1 -0
  53. package/dist/internal/json.js +134 -0
  54. package/dist/internal/json.js.map +1 -0
  55. package/dist/internal/limits.d.ts +14 -0
  56. package/dist/internal/limits.d.ts.map +1 -0
  57. package/dist/internal/limits.js +43 -0
  58. package/dist/internal/limits.js.map +1 -0
  59. package/dist/internal/nodes.d.ts +121 -0
  60. package/dist/internal/nodes.d.ts.map +1 -0
  61. package/dist/internal/nodes.js +277 -0
  62. package/dist/internal/nodes.js.map +1 -0
  63. package/dist/internal/patterns.js +110 -0
  64. package/dist/internal/patterns.js.map +1 -0
  65. package/dist/internal/policy-json.js +265 -0
  66. package/dist/internal/policy-json.js.map +1 -0
  67. package/dist/internal/policy-nodes.d.ts +61 -0
  68. package/dist/internal/policy-nodes.d.ts.map +1 -0
  69. package/dist/internal/policy-nodes.js +146 -0
  70. package/dist/internal/policy-nodes.js.map +1 -0
  71. package/dist/internal/policy.d.ts +42 -0
  72. package/dist/internal/policy.d.ts.map +1 -0
  73. package/dist/internal/policy.js +124 -0
  74. package/dist/internal/policy.js.map +1 -0
  75. package/dist/internal/read.js +39 -0
  76. package/dist/internal/read.js.map +1 -0
  77. package/dist/internal/resource.js +115 -0
  78. package/dist/internal/resource.js.map +1 -0
  79. package/dist/internal/rule.d.ts +26 -0
  80. package/dist/internal/rule.d.ts.map +1 -0
  81. package/dist/internal/rule.js +103 -0
  82. package/dist/internal/rule.js.map +1 -0
  83. package/dist/internal/search.d.ts +22 -0
  84. package/dist/internal/search.d.ts.map +1 -0
  85. package/dist/internal/search.js +274 -0
  86. package/dist/internal/search.js.map +1 -0
  87. package/dist/internal/suggest.js +46 -0
  88. package/dist/internal/suggest.js.map +1 -0
  89. package/package.json +3 -4
  90. package/src/Authorizer.ts +67 -583
  91. package/src/Condition.ts +339 -2095
  92. package/src/Decision.ts +6 -5
  93. package/src/Domain.ts +12 -69
  94. package/src/HttpApi.ts +72 -10
  95. package/src/Limits.ts +132 -0
  96. package/src/Policy.ts +391 -2772
  97. package/src/Resource.ts +7 -1548
  98. package/src/index.ts +3 -9
  99. package/src/internal/algebra.ts +482 -0
  100. package/src/internal/analysis.ts +478 -0
  101. package/src/internal/authorizer.ts +418 -0
  102. package/src/internal/bind.ts +648 -0
  103. package/src/internal/catalog.ts +534 -0
  104. package/src/internal/decide.ts +402 -0
  105. package/src/internal/domain.ts +66 -0
  106. package/src/internal/json.ts +260 -0
  107. package/src/internal/limits.ts +78 -0
  108. package/src/internal/nodes.ts +404 -0
  109. package/src/internal/patterns.ts +211 -0
  110. package/src/internal/policy-json.ts +487 -0
  111. package/src/internal/policy-nodes.ts +207 -0
  112. package/src/internal/policy.ts +264 -0
  113. package/src/internal/read.ts +55 -0
  114. package/src/internal/resource.ts +180 -0
  115. package/src/internal/rule.ts +196 -0
  116. package/src/internal/search.ts +541 -0
  117. package/src/internal/suggest.ts +104 -0
  118. package/dist/Principal.d.ts +0 -35
  119. package/dist/Principal.d.ts.map +0 -1
  120. package/dist/Principal.js +0 -37
  121. package/dist/Principal.js.map +0 -1
  122. package/dist/Rule.d.ts +0 -275
  123. package/dist/Rule.d.ts.map +0 -1
  124. package/dist/Rule.js +0 -257
  125. package/dist/Rule.js.map +0 -1
  126. package/src/Principal.ts +0 -41
  127. 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
+ })