@o3co/auth.policy-verifier.builtins 0.2.0 → 0.3.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.ja.md +101 -0
- package/README.md +101 -0
- package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrLiteralCompare.test.mjs +253 -0
- package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrLiteralEqual.test.mjs +138 -0
- package/dist/__tests__/rules/AttrLiteralIn.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrLiteralIn.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrLiteralIn.test.mjs +160 -0
- package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrLiteralNotEqual.test.mjs +157 -0
- package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrLiteralNotIn.test.mjs +168 -0
- package/dist/__tests__/rules/AttrMatchRule.test.mjs +8 -0
- package/dist/__tests__/rules/AttrPairCompare.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrPairCompare.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrPairCompare.test.mjs +319 -0
- package/dist/__tests__/rules/AttrPairEqual.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrPairEqual.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrPairEqual.test.mjs +155 -0
- package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrPairNotEqual.test.mjs +155 -0
- package/dist/__tests__/rules/_sharedValidation.test.d.mts +2 -0
- package/dist/__tests__/rules/_sharedValidation.test.d.mts.map +1 -0
- package/dist/__tests__/rules/_sharedValidation.test.mjs +325 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +8 -0
- package/dist/rules/AttrLiteralCompare.d.mts +31 -0
- package/dist/rules/AttrLiteralCompare.d.mts.map +1 -0
- package/dist/rules/AttrLiteralCompare.mjs +36 -0
- package/dist/rules/AttrLiteralEqual.d.mts +37 -0
- package/dist/rules/AttrLiteralEqual.d.mts.map +1 -0
- package/dist/rules/AttrLiteralEqual.mjs +43 -0
- package/dist/rules/AttrLiteralIn.d.mts +38 -0
- package/dist/rules/AttrLiteralIn.d.mts.map +1 -0
- package/dist/rules/AttrLiteralIn.mjs +46 -0
- package/dist/rules/AttrLiteralNotEqual.d.mts +36 -0
- package/dist/rules/AttrLiteralNotEqual.d.mts.map +1 -0
- package/dist/rules/AttrLiteralNotEqual.mjs +42 -0
- package/dist/rules/AttrLiteralNotIn.d.mts +33 -0
- package/dist/rules/AttrLiteralNotIn.d.mts.map +1 -0
- package/dist/rules/AttrLiteralNotIn.mjs +41 -0
- package/dist/rules/AttrMatchRule.d.mts +7 -34
- package/dist/rules/AttrMatchRule.d.mts.map +1 -1
- package/dist/rules/AttrMatchRule.mjs +7 -32
- package/dist/rules/AttrPairCompare.d.mts +35 -0
- package/dist/rules/AttrPairCompare.d.mts.map +1 -0
- package/dist/rules/AttrPairCompare.mjs +43 -0
- package/dist/rules/AttrPairEqual.d.mts +35 -0
- package/dist/rules/AttrPairEqual.d.mts.map +1 -0
- package/dist/rules/AttrPairEqual.mjs +44 -0
- package/dist/rules/AttrPairNotEqual.d.mts +34 -0
- package/dist/rules/AttrPairNotEqual.d.mts.map +1 -0
- package/dist/rules/AttrPairNotEqual.mjs +43 -0
- package/dist/rules/_sharedValidation.d.mts +87 -0
- package/dist/rules/_sharedValidation.d.mts.map +1 -0
- package/dist/rules/_sharedValidation.mjs +189 -0
- package/package.json +5 -2
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { applyCompare, COMPARE_OPS, computeValuesKey, requireAttrName, requireCompareOp, requireHomogeneousLiteralArray, requireLiteralValue, requireNumber, requireOptionalGroup, } from "#/rules/_sharedValidation.mjs";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// requireAttrName
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
describe("requireAttrName", () => {
|
|
7
|
+
it("returns the input string when value is a valid non-empty string", () => {
|
|
8
|
+
expect(requireAttrName("MyClass", "a", "role")).toBe("role");
|
|
9
|
+
});
|
|
10
|
+
it("returns the input string for fieldName 'b'", () => {
|
|
11
|
+
expect(requireAttrName("MyClass", "b", "score")).toBe("score");
|
|
12
|
+
});
|
|
13
|
+
it("throws with className and fieldName 'a' in message when value is undefined", () => {
|
|
14
|
+
expect(() => requireAttrName("MyClass", "a", undefined)).toThrow(/MyClass.*'a'.*non-empty string.*got undefined/);
|
|
15
|
+
});
|
|
16
|
+
it("throws with className and fieldName 'b' in message when value is undefined", () => {
|
|
17
|
+
expect(() => requireAttrName("MyClass", "b", undefined)).toThrow(/MyClass.*'b'.*non-empty string.*got undefined/);
|
|
18
|
+
});
|
|
19
|
+
it("throws with 'empty string' in message when value is ''", () => {
|
|
20
|
+
expect(() => requireAttrName("MyClass", "a", "")).toThrow(/MyClass.*'a'.*got empty string/);
|
|
21
|
+
});
|
|
22
|
+
it("throws with 'got number' in message when value is 42", () => {
|
|
23
|
+
expect(() => requireAttrName("MyClass", "a", 42)).toThrow(/MyClass.*'a'.*got number/);
|
|
24
|
+
});
|
|
25
|
+
it("throws with 'null' in message when value is null", () => {
|
|
26
|
+
expect(() => requireAttrName("MyClass", "a", null)).toThrow(/null/);
|
|
27
|
+
});
|
|
28
|
+
it("throws when value contains ':' (reserved as ruleType separator)", () => {
|
|
29
|
+
// Regression guard for an authorization-weakening bug: with ':' allowed,
|
|
30
|
+
// (a="x:y", b="z") and (a="x", b="y:z") would both produce the same
|
|
31
|
+
// `attr_pair_equal:x:y:z` default ruleType and be silently OR-combined
|
|
32
|
+
// by the evaluator. Fail-fast at construction instead.
|
|
33
|
+
expect(() => requireAttrName("MyClass", "a", "name:space")).toThrow(/must not contain ':'.*got 'name:space'/);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// requireLiteralValue
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
describe("requireLiteralValue", () => {
|
|
40
|
+
it("returns the value when it is a string", () => {
|
|
41
|
+
expect(requireLiteralValue("MyClass", "v", "hello")).toBe("hello");
|
|
42
|
+
});
|
|
43
|
+
it("returns the value when it is a number", () => {
|
|
44
|
+
expect(requireLiteralValue("MyClass", "v", 42)).toBe(42);
|
|
45
|
+
});
|
|
46
|
+
it("returns the value when it is a boolean", () => {
|
|
47
|
+
expect(requireLiteralValue("MyClass", "v", false)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it("throws with 'null' in message when value is null", () => {
|
|
50
|
+
expect(() => requireLiteralValue("MyClass", "v", null)).toThrow(/null/);
|
|
51
|
+
});
|
|
52
|
+
it("throws with 'undefined' in message when value is undefined", () => {
|
|
53
|
+
expect(() => requireLiteralValue("MyClass", "v", undefined)).toThrow(/undefined/);
|
|
54
|
+
});
|
|
55
|
+
it("throws with 'object' in message when value is a plain object", () => {
|
|
56
|
+
expect(() => requireLiteralValue("MyClass", "v", {})).toThrow(/object/);
|
|
57
|
+
});
|
|
58
|
+
it("throws with 'object' in message when value is an array (typeof [] === 'object')", () => {
|
|
59
|
+
expect(() => requireLiteralValue("MyClass", "v", [])).toThrow(/object/);
|
|
60
|
+
});
|
|
61
|
+
it("throws on NaN (NaN === NaN is false; would invert Equal/NotEqual rules)", () => {
|
|
62
|
+
// Regression guard for an authorization-weakening bug: with NaN accepted,
|
|
63
|
+
// AttrLiteralNotEqual({ v: NaN }) would always pass for any numeric
|
|
64
|
+
// attribute because `x !== NaN` is always true. Must fail-fast in
|
|
65
|
+
// construction instead.
|
|
66
|
+
expect(() => requireLiteralValue("MyClass", "v", Number.NaN)).toThrow(/must not be NaN/);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// requireNumber
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
describe("requireNumber", () => {
|
|
73
|
+
it("returns 0 (zero)", () => {
|
|
74
|
+
expect(requireNumber("MyClass", "v", 0)).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
it("returns -0 (negative zero)", () => {
|
|
77
|
+
expect(requireNumber("MyClass", "v", -0)).toBe(-0);
|
|
78
|
+
});
|
|
79
|
+
it("returns a large finite number", () => {
|
|
80
|
+
expect(requireNumber("MyClass", "v", 1e308)).toBe(1e308);
|
|
81
|
+
});
|
|
82
|
+
it("returns Infinity without throwing (Infinity is a valid number per spec)", () => {
|
|
83
|
+
expect(requireNumber("MyClass", "v", Infinity)).toBe(Infinity);
|
|
84
|
+
});
|
|
85
|
+
it("returns -Infinity without throwing", () => {
|
|
86
|
+
expect(requireNumber("MyClass", "v", -Infinity)).toBe(-Infinity);
|
|
87
|
+
});
|
|
88
|
+
it("throws on NaN with message matching /must not be NaN/", () => {
|
|
89
|
+
expect(() => requireNumber("MyClass", "v", NaN)).toThrow(/must not be NaN/);
|
|
90
|
+
});
|
|
91
|
+
it("throws on a string '42' with message containing 'got string'", () => {
|
|
92
|
+
expect(() => requireNumber("MyClass", "v", "42")).toThrow(/got string/);
|
|
93
|
+
});
|
|
94
|
+
it("throws on null with message containing 'null'", () => {
|
|
95
|
+
expect(() => requireNumber("MyClass", "v", null)).toThrow(/null/);
|
|
96
|
+
});
|
|
97
|
+
it("throws on undefined with message containing 'undefined'", () => {
|
|
98
|
+
expect(() => requireNumber("MyClass", "v", undefined)).toThrow(/undefined/);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// requireHomogeneousLiteralArray
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
describe("requireHomogeneousLiteralArray", () => {
|
|
105
|
+
it("returns a non-empty all-string array", () => {
|
|
106
|
+
const arr = ["a", "b", "c"];
|
|
107
|
+
expect(requireHomogeneousLiteralArray("MyClass", arr)).toEqual(arr);
|
|
108
|
+
});
|
|
109
|
+
it("returns a non-empty all-number array", () => {
|
|
110
|
+
const arr = [1, 2, 3];
|
|
111
|
+
expect(requireHomogeneousLiteralArray("MyClass", arr)).toEqual(arr);
|
|
112
|
+
});
|
|
113
|
+
it("returns a non-empty all-boolean array", () => {
|
|
114
|
+
const arr = [true, false];
|
|
115
|
+
expect(requireHomogeneousLiteralArray("MyClass", arr)).toEqual(arr);
|
|
116
|
+
});
|
|
117
|
+
it("throws with 'got string' when value is not an array", () => {
|
|
118
|
+
expect(() => requireHomogeneousLiteralArray("MyClass", "abc")).toThrow(/got string/);
|
|
119
|
+
});
|
|
120
|
+
it("throws with 'undefined' when value is undefined", () => {
|
|
121
|
+
expect(() => requireHomogeneousLiteralArray("MyClass", undefined)).toThrow(/undefined/);
|
|
122
|
+
});
|
|
123
|
+
it("throws with 'null' when value is null", () => {
|
|
124
|
+
expect(() => requireHomogeneousLiteralArray("MyClass", null)).toThrow(/null/);
|
|
125
|
+
});
|
|
126
|
+
it("throws with 'non-empty array' on empty array", () => {
|
|
127
|
+
expect(() => requireHomogeneousLiteralArray("MyClass", [])).toThrow(/non-empty array/);
|
|
128
|
+
});
|
|
129
|
+
it("throws with 'null' when an element is null", () => {
|
|
130
|
+
expect(() => requireHomogeneousLiteralArray("MyClass", [null])).toThrow(/null/);
|
|
131
|
+
});
|
|
132
|
+
it("throws with 'undefined' when an element is undefined", () => {
|
|
133
|
+
expect(() => requireHomogeneousLiteralArray("MyClass", [undefined])).toThrow(/undefined/);
|
|
134
|
+
});
|
|
135
|
+
it("throws with 'object' when an element is an object", () => {
|
|
136
|
+
expect(() => requireHomogeneousLiteralArray("MyClass", [{}])).toThrow(/object/);
|
|
137
|
+
});
|
|
138
|
+
it("throws with mixed types message for ['a', 1] (left-to-right Set insertion order)", () => {
|
|
139
|
+
expect(() => requireHomogeneousLiteralArray("MyClass", ["a", 1])).toThrow(/mixed types \[string, number\]/);
|
|
140
|
+
});
|
|
141
|
+
it("throws when an element is NaN (would silently mismatch AttrLiteralIn/NotIn)", () => {
|
|
142
|
+
// Regression guard: AttrLiteralIn uses strict equality against each element;
|
|
143
|
+
// a NaN element could never match any attribute value because NaN !== NaN.
|
|
144
|
+
// Must fail-fast at construction rather than ship an always-false element.
|
|
145
|
+
expect(() => requireHomogeneousLiteralArray("MyClass", [1, Number.NaN, 3])).toThrow(/must not contain NaN/);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// requireCompareOp
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
describe("requireCompareOp", () => {
|
|
152
|
+
it("returns 'lt' for valid op 'lt'", () => {
|
|
153
|
+
expect(requireCompareOp("MyClass", "lt")).toBe("lt");
|
|
154
|
+
});
|
|
155
|
+
it("returns 'le' for valid op 'le'", () => {
|
|
156
|
+
expect(requireCompareOp("MyClass", "le")).toBe("le");
|
|
157
|
+
});
|
|
158
|
+
it("returns 'gt' for valid op 'gt'", () => {
|
|
159
|
+
expect(requireCompareOp("MyClass", "gt")).toBe("gt");
|
|
160
|
+
});
|
|
161
|
+
it("returns 'ge' for valid op 'ge'", () => {
|
|
162
|
+
expect(requireCompareOp("MyClass", "ge")).toBe("ge");
|
|
163
|
+
});
|
|
164
|
+
it("throws on invalid string 'eq' with message listing valid ops joined by ' | '", () => {
|
|
165
|
+
expect(() => requireCompareOp("MyClass", "eq")).toThrow(/lt \| le \| gt \| ge/);
|
|
166
|
+
});
|
|
167
|
+
it("throws on empty string '' with message listing valid ops", () => {
|
|
168
|
+
expect(() => requireCompareOp("MyClass", "")).toThrow(/lt \| le \| gt \| ge/);
|
|
169
|
+
});
|
|
170
|
+
it("throws on symbolic string '<' with message listing valid ops", () => {
|
|
171
|
+
expect(() => requireCompareOp("MyClass", "<")).toThrow(/lt \| le \| gt \| ge/);
|
|
172
|
+
});
|
|
173
|
+
it("throws on number 42 with message containing the type label", () => {
|
|
174
|
+
expect(() => requireCompareOp("MyClass", 42)).toThrow(/got number/);
|
|
175
|
+
});
|
|
176
|
+
it("throws on null with message containing the type label", () => {
|
|
177
|
+
expect(() => requireCompareOp("MyClass", null)).toThrow(/null/);
|
|
178
|
+
});
|
|
179
|
+
it("throws on undefined with message containing the type label", () => {
|
|
180
|
+
expect(() => requireCompareOp("MyClass", undefined)).toThrow(/undefined/);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// requireOptionalGroup
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
describe("requireOptionalGroup", () => {
|
|
187
|
+
it("returns undefined when value is undefined", () => {
|
|
188
|
+
expect(requireOptionalGroup("MyClass", undefined)).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
it("returns the input string when value is a non-empty string", () => {
|
|
191
|
+
expect(requireOptionalGroup("MyClass", "my-group")).toBe("my-group");
|
|
192
|
+
});
|
|
193
|
+
it("throws with 'empty string' message on ''", () => {
|
|
194
|
+
expect(() => requireOptionalGroup("MyClass", "")).toThrow(/empty string/);
|
|
195
|
+
});
|
|
196
|
+
it("throws with 'got number' message on 42", () => {
|
|
197
|
+
expect(() => requireOptionalGroup("MyClass", 42)).toThrow(/got number/);
|
|
198
|
+
});
|
|
199
|
+
it("throws with 'null' in message on null", () => {
|
|
200
|
+
expect(() => requireOptionalGroup("MyClass", null)).toThrow(/null/);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// computeValuesKey
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
describe("computeValuesKey", () => {
|
|
207
|
+
it("returns a string matching /{type}:{count}:{8-hex-chars}/ format", () => {
|
|
208
|
+
expect(computeValuesKey(["admin"])).toMatch(/^(string|number|boolean):[0-9]+:[0-9a-f]{8}$/);
|
|
209
|
+
});
|
|
210
|
+
it("sort determinism: same content in different order produces the same key", () => {
|
|
211
|
+
expect(computeValuesKey(["b", "a"])).toBe(computeValuesKey(["a", "b"]));
|
|
212
|
+
});
|
|
213
|
+
it("count discrimination: ['a'] !== ['a', 'b'] (distinct content → distinct key)", () => {
|
|
214
|
+
expect(computeValuesKey(["a"])).not.toBe(computeValuesKey(["a", "b"]));
|
|
215
|
+
});
|
|
216
|
+
it("dedup parity: ['a'] === ['a', 'a'] (duplicates do not affect the Set-based rule behavior, so they must not affect ruleType either)", () => {
|
|
217
|
+
// AttrLiteralIn / AttrLiteralNotIn use `new Set(values)` for verify(),
|
|
218
|
+
// so duplicate elements have no semantic effect. The ruleType must
|
|
219
|
+
// mirror that — otherwise two logically-equivalent rules would be
|
|
220
|
+
// placed in different evaluator groups and be AND-combined, when
|
|
221
|
+
// the caller intended them to be OR-combined (shared ruleType).
|
|
222
|
+
expect(computeValuesKey(["a"])).toBe(computeValuesKey(["a", "a"]));
|
|
223
|
+
});
|
|
224
|
+
it("content discrimination: ['a', 'b'] !== ['a', 'c'] (hash differs)", () => {
|
|
225
|
+
expect(computeValuesKey(["a", "b"])).not.toBe(computeValuesKey(["a", "c"]));
|
|
226
|
+
});
|
|
227
|
+
it("type segment correctness: string array starts with 'string:'", () => {
|
|
228
|
+
expect(computeValuesKey(["admin"])).toMatch(/^string:/);
|
|
229
|
+
});
|
|
230
|
+
it("type segment correctness: number array starts with 'number:'", () => {
|
|
231
|
+
expect(computeValuesKey([1])).toMatch(/^number:/);
|
|
232
|
+
});
|
|
233
|
+
it("type segment correctness: boolean array starts with 'boolean:'", () => {
|
|
234
|
+
expect(computeValuesKey([true])).toMatch(/^boolean:/);
|
|
235
|
+
});
|
|
236
|
+
it("type divergence: computeValuesKey(['1']) !== computeValuesKey([1]) (type segment differs)", () => {
|
|
237
|
+
expect(computeValuesKey(["1"])).not.toBe(computeValuesKey([1]));
|
|
238
|
+
});
|
|
239
|
+
it("separator collision guard: ['a,b', 'c'] !== ['a', 'b,c'] (values containing commas do not collide)", () => {
|
|
240
|
+
// Regression guard: a naive implementation of the form
|
|
241
|
+
// values.map(String).sort().join(",")
|
|
242
|
+
// would produce the same string "a,b,c" for both inputs and thus the
|
|
243
|
+
// same ruleType. That would cause the evaluator to OR what the caller
|
|
244
|
+
// meant as two independent set constraints, weakening authorization.
|
|
245
|
+
// The canonical form JSON.stringify-es each element before joining,
|
|
246
|
+
// so quotes/escapes disambiguate the element boundary.
|
|
247
|
+
expect(computeValuesKey(["a,b", "c"])).not.toBe(computeValuesKey(["a", "b,c"]));
|
|
248
|
+
});
|
|
249
|
+
it("pure / referentially transparent: two calls with the same input return the same string", () => {
|
|
250
|
+
const input = ["x", "y", "z"];
|
|
251
|
+
expect(computeValuesKey(input)).toBe(computeValuesKey(input));
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// applyCompare
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
describe("applyCompare", () => {
|
|
258
|
+
describe("op=lt", () => {
|
|
259
|
+
it("returns true when left < right (1 < 2)", () => {
|
|
260
|
+
expect(applyCompare("lt", 1, 2)).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
it("returns false when left === right (2 < 2 is false)", () => {
|
|
263
|
+
expect(applyCompare("lt", 2, 2)).toBe(false);
|
|
264
|
+
});
|
|
265
|
+
it("returns false when left > right (3 < 2 is false)", () => {
|
|
266
|
+
expect(applyCompare("lt", 3, 2)).toBe(false);
|
|
267
|
+
});
|
|
268
|
+
it("edge: -Infinity < 0 returns true", () => {
|
|
269
|
+
expect(applyCompare("lt", -Infinity, 0)).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
describe("op=le", () => {
|
|
273
|
+
it("returns true when left < right (1 <= 2)", () => {
|
|
274
|
+
expect(applyCompare("le", 1, 2)).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
it("returns true when left === right (2 <= 2)", () => {
|
|
277
|
+
expect(applyCompare("le", 2, 2)).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
it("returns false when left > right (3 <= 2 is false)", () => {
|
|
280
|
+
expect(applyCompare("le", 3, 2)).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
it("edge: 0 >= -0 returns true (ge symmetry check; 0 <= -0 also true)", () => {
|
|
283
|
+
expect(applyCompare("le", 0, -0)).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
describe("op=gt", () => {
|
|
287
|
+
it("returns true when left > right (3 > 2)", () => {
|
|
288
|
+
expect(applyCompare("gt", 3, 2)).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
it("returns false when left === right (2 > 2 is false)", () => {
|
|
291
|
+
expect(applyCompare("gt", 2, 2)).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
it("returns false when left < right (1 > 2 is false)", () => {
|
|
294
|
+
expect(applyCompare("gt", 1, 2)).toBe(false);
|
|
295
|
+
});
|
|
296
|
+
it("edge: Infinity > 1e308 returns true", () => {
|
|
297
|
+
expect(applyCompare("gt", Infinity, 1e308)).toBe(true);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
describe("op=ge", () => {
|
|
301
|
+
it("returns true when left > right (3 >= 2)", () => {
|
|
302
|
+
expect(applyCompare("ge", 3, 2)).toBe(true);
|
|
303
|
+
});
|
|
304
|
+
it("returns true when left === right (2 >= 2)", () => {
|
|
305
|
+
expect(applyCompare("ge", 2, 2)).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
it("returns false when left < right (1 >= 2 is false)", () => {
|
|
308
|
+
expect(applyCompare("ge", 1, 2)).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
it("edge: 0 >= -0 returns true", () => {
|
|
311
|
+
expect(applyCompare("ge", 0, -0)).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// COMPARE_OPS
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
describe("COMPARE_OPS", () => {
|
|
319
|
+
it("exposes exactly 4 values", () => {
|
|
320
|
+
expect(COMPARE_OPS).toHaveLength(4);
|
|
321
|
+
});
|
|
322
|
+
it("contains 'lt', 'le', 'gt', 'ge' in order", () => {
|
|
323
|
+
expect(COMPARE_OPS).toEqual(["lt", "le", "gt", "ge"]);
|
|
324
|
+
});
|
|
325
|
+
});
|
package/dist/index.d.mts
CHANGED
|
@@ -4,7 +4,15 @@ export { StaticPermissionCollector } from "./collectors/StaticPermissionCollecto
|
|
|
4
4
|
export { StaticRoleCollector } from "./collectors/StaticRoleCollector.mjs";
|
|
5
5
|
export { builtinCollectorsModule } from "./module.mjs";
|
|
6
6
|
export { DotNotationResourceParser } from "./resource/DotNotationResourceParser.mjs";
|
|
7
|
+
export { AttrLiteralCompare, type AttrLiteralCompareConfig } from "./rules/AttrLiteralCompare.mjs";
|
|
8
|
+
export { AttrLiteralEqual, type AttrLiteralEqualConfig } from "./rules/AttrLiteralEqual.mjs";
|
|
9
|
+
export { AttrLiteralIn, type AttrLiteralInConfig } from "./rules/AttrLiteralIn.mjs";
|
|
10
|
+
export { AttrLiteralNotEqual, type AttrLiteralNotEqualConfig, } from "./rules/AttrLiteralNotEqual.mjs";
|
|
11
|
+
export { AttrLiteralNotIn, type AttrLiteralNotInConfig } from "./rules/AttrLiteralNotIn.mjs";
|
|
7
12
|
export { AttrMatchRule, type AttrMatchRuleConfig } from "./rules/AttrMatchRule.mjs";
|
|
13
|
+
export { AttrPairCompare, type AttrPairCompareConfig } from "./rules/AttrPairCompare.mjs";
|
|
14
|
+
export { AttrPairEqual, type AttrPairEqualConfig } from "./rules/AttrPairEqual.mjs";
|
|
15
|
+
export { AttrPairNotEqual, type AttrPairNotEqualConfig } from "./rules/AttrPairNotEqual.mjs";
|
|
8
16
|
export { HasPermission } from "./rules/HasPermission.mjs";
|
|
9
17
|
export { HasScope } from "./rules/HasScope.mjs";
|
|
10
18
|
export { ResourceActionPermissionRuleCollector } from "./rules/ResourceActionPermissionRuleCollector.mjs";
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAC/E,OAAO,EAAE,yBAAyB,EAAE,MAAM,4CAA4C,CAAC;AACvF,OAAO,EAAE,yBAAyB,EAAE,MAAM,4CAA4C,CAAC;AACvF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAE3E,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AAErF,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,qCAAqC,EAAE,MAAM,mDAAmD,CAAC;AAC1G,OAAO,EAAE,gCAAgC,EAAE,MAAM,8CAA8C,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAC/E,OAAO,EAAE,yBAAyB,EAAE,MAAM,4CAA4C,CAAC;AACvF,OAAO,EAAE,yBAAyB,EAAE,MAAM,4CAA4C,CAAC;AACvF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAE3E,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AAErF,OAAO,EAAE,kBAAkB,EAAE,KAAK,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AACnG,OAAO,EAAE,gBAAgB,EAAE,KAAK,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC7F,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EACN,mBAAmB,EACnB,KAAK,yBAAyB,GAC9B,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,KAAK,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC7F,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,eAAe,EAAE,KAAK,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAC1F,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,gBAAgB,EAAE,KAAK,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC7F,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,qCAAqC,EAAE,MAAM,mDAAmD,CAAC;AAC1G,OAAO,EAAE,gCAAgC,EAAE,MAAM,8CAA8C,CAAC"}
|
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,15 @@ export { builtinCollectorsModule } from "./module.mjs";
|
|
|
8
8
|
// Resource
|
|
9
9
|
export { DotNotationResourceParser } from "./resource/DotNotationResourceParser.mjs";
|
|
10
10
|
// Rules
|
|
11
|
+
export { AttrLiteralCompare } from "./rules/AttrLiteralCompare.mjs";
|
|
12
|
+
export { AttrLiteralEqual } from "./rules/AttrLiteralEqual.mjs";
|
|
13
|
+
export { AttrLiteralIn } from "./rules/AttrLiteralIn.mjs";
|
|
14
|
+
export { AttrLiteralNotEqual, } from "./rules/AttrLiteralNotEqual.mjs";
|
|
15
|
+
export { AttrLiteralNotIn } from "./rules/AttrLiteralNotIn.mjs";
|
|
11
16
|
export { AttrMatchRule } from "./rules/AttrMatchRule.mjs";
|
|
17
|
+
export { AttrPairCompare } from "./rules/AttrPairCompare.mjs";
|
|
18
|
+
export { AttrPairEqual } from "./rules/AttrPairEqual.mjs";
|
|
19
|
+
export { AttrPairNotEqual } from "./rules/AttrPairNotEqual.mjs";
|
|
12
20
|
export { HasPermission } from "./rules/HasPermission.mjs";
|
|
13
21
|
export { HasScope } from "./rules/HasScope.mjs";
|
|
14
22
|
export { ResourceActionPermissionRuleCollector } from "./rules/ResourceActionPermissionRuleCollector.mjs";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Attributes, Rule } from "@o3co/auth.policy-verifier.core";
|
|
2
|
+
import { type CompareOp } from "./_sharedValidation.mjs";
|
|
3
|
+
export interface AttrLiteralCompareConfig {
|
|
4
|
+
a: string;
|
|
5
|
+
op: CompareOp;
|
|
6
|
+
v: number;
|
|
7
|
+
group?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Rule that passes when a named attribute is present, is a number, and
|
|
11
|
+
* satisfies the configured comparison operator against the configured literal
|
|
12
|
+
* number `v`. NaN attributes always return false. Missing, null, or non-number
|
|
13
|
+
* attributes return false (safe-deny). NaN is rejected at construction time.
|
|
14
|
+
*
|
|
15
|
+
* ## Grouping and the default ruleType
|
|
16
|
+
*
|
|
17
|
+
* The evaluator groups rules by `ruleType`, ORs within a group, and ANDs
|
|
18
|
+
* across groups. The default `ruleType` is derived from `a`, `op`, and `v`:
|
|
19
|
+
* `attr_literal_compare:{a}:{op}:{String(v)}`
|
|
20
|
+
*
|
|
21
|
+
* Pass an explicit `group` string to override the default ruleType entirely.
|
|
22
|
+
*/
|
|
23
|
+
export declare class AttrLiteralCompare implements Rule {
|
|
24
|
+
private readonly config;
|
|
25
|
+
readonly ruleType: string;
|
|
26
|
+
readonly code = "attr_compare_violated";
|
|
27
|
+
readonly message: string;
|
|
28
|
+
constructor(config: AttrLiteralCompareConfig);
|
|
29
|
+
verify(attrs: Attributes): boolean;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=AttrLiteralCompare.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AttrLiteralCompare.d.mts","sourceRoot":"","sources":["../../src/rules/AttrLiteralCompare.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAEN,KAAK,SAAS,EAKd,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,wBAAwB;IACxC,CAAC,EAAE,MAAM,CAAC;IACV,EAAE,EAAE,SAAS,CAAC;IACd,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,kBAAmB,YAAW,IAAI;IAKlC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,2BAA2B;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEI,MAAM,EAAE,wBAAwB;IAU7D,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;CAKlC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { applyCompare, requireAttrName, requireCompareOp, requireNumber, requireOptionalGroup, } from "./_sharedValidation.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* Rule that passes when a named attribute is present, is a number, and
|
|
4
|
+
* satisfies the configured comparison operator against the configured literal
|
|
5
|
+
* number `v`. NaN attributes always return false. Missing, null, or non-number
|
|
6
|
+
* attributes return false (safe-deny). NaN is rejected at construction time.
|
|
7
|
+
*
|
|
8
|
+
* ## Grouping and the default ruleType
|
|
9
|
+
*
|
|
10
|
+
* The evaluator groups rules by `ruleType`, ORs within a group, and ANDs
|
|
11
|
+
* across groups. The default `ruleType` is derived from `a`, `op`, and `v`:
|
|
12
|
+
* `attr_literal_compare:{a}:{op}:{String(v)}`
|
|
13
|
+
*
|
|
14
|
+
* Pass an explicit `group` string to override the default ruleType entirely.
|
|
15
|
+
*/
|
|
16
|
+
export class AttrLiteralCompare {
|
|
17
|
+
config;
|
|
18
|
+
ruleType;
|
|
19
|
+
code = "attr_compare_violated";
|
|
20
|
+
message;
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.config = config;
|
|
23
|
+
requireAttrName("AttrLiteralCompare", "a", config.a);
|
|
24
|
+
requireNumber("AttrLiteralCompare", "v", config.v);
|
|
25
|
+
requireCompareOp("AttrLiteralCompare", config.op);
|
|
26
|
+
const group = requireOptionalGroup("AttrLiteralCompare", config.group);
|
|
27
|
+
this.ruleType = group ?? `attr_literal_compare:${config.a}:${config.op}:${String(config.v)}`;
|
|
28
|
+
this.message = `Attribute constraint not satisfied: ${config.a} must be ${config.op} ${String(config.v)}.`;
|
|
29
|
+
}
|
|
30
|
+
verify(attrs) {
|
|
31
|
+
const x = attrs.get(this.config.a);
|
|
32
|
+
if (typeof x !== "number")
|
|
33
|
+
return false;
|
|
34
|
+
return applyCompare(this.config.op, x, this.config.v);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Attributes, Rule } from "@o3co/auth.policy-verifier.core";
|
|
2
|
+
import { type LiteralValue } from "./_sharedValidation.mjs";
|
|
3
|
+
export interface AttrLiteralEqualConfig {
|
|
4
|
+
a: string;
|
|
5
|
+
v: LiteralValue;
|
|
6
|
+
group?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Rule that passes when a named attribute is present and strictly equal to a
|
|
10
|
+
* configured literal value (string, number, or boolean). No type coercion is
|
|
11
|
+
* performed: the attribute value must be the same type and value as `v`.
|
|
12
|
+
*
|
|
13
|
+
* ## Grouping and the default ruleType
|
|
14
|
+
*
|
|
15
|
+
* The evaluator groups rules by `ruleType`, ORs within a group, and ANDs
|
|
16
|
+
* across groups. The default `ruleType` is derived from `a`, `typeof v`, and
|
|
17
|
+
* `String(v)` so that distinct literal requirements are AND-combined by default:
|
|
18
|
+
* `attr_literal_equal:{a}:{typeof v}:{String(v)}`
|
|
19
|
+
*
|
|
20
|
+
* The `typeof v` segment is required because `String(v)` collapses distinct-
|
|
21
|
+
* type literals that stringify the same way (e.g. `true` vs `"true"`, `1` vs
|
|
22
|
+
* `"1"`). Without it, two rules on the same attribute with different-type
|
|
23
|
+
* literals would share a `ruleType` and be OR-combined by the evaluator,
|
|
24
|
+
* silently weakening authorization.
|
|
25
|
+
*
|
|
26
|
+
* Pass a shared `group` string to two instances to opt into OR semantics
|
|
27
|
+
* (e.g. "role is admin" OR "role is superuser" under one group).
|
|
28
|
+
*/
|
|
29
|
+
export declare class AttrLiteralEqual implements Rule {
|
|
30
|
+
private readonly config;
|
|
31
|
+
readonly ruleType: string;
|
|
32
|
+
readonly code = "attr_not_equal";
|
|
33
|
+
readonly message: string;
|
|
34
|
+
constructor(config: AttrLiteralEqualConfig);
|
|
35
|
+
verify(attrs: Attributes): boolean;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=AttrLiteralEqual.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AttrLiteralEqual.d.mts","sourceRoot":"","sources":["../../src/rules/AttrLiteralEqual.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EACN,KAAK,YAAY,EAIjB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,sBAAsB;IACtC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,YAAY,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,gBAAiB,YAAW,IAAI;IAKhC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,oBAAoB;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEI,MAAM,EAAE,sBAAsB;IAU3D,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;CAKlC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { requireAttrName, requireLiteralValue, requireOptionalGroup, } from "./_sharedValidation.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* Rule that passes when a named attribute is present and strictly equal to a
|
|
4
|
+
* configured literal value (string, number, or boolean). No type coercion is
|
|
5
|
+
* performed: the attribute value must be the same type and value as `v`.
|
|
6
|
+
*
|
|
7
|
+
* ## Grouping and the default ruleType
|
|
8
|
+
*
|
|
9
|
+
* The evaluator groups rules by `ruleType`, ORs within a group, and ANDs
|
|
10
|
+
* across groups. The default `ruleType` is derived from `a`, `typeof v`, and
|
|
11
|
+
* `String(v)` so that distinct literal requirements are AND-combined by default:
|
|
12
|
+
* `attr_literal_equal:{a}:{typeof v}:{String(v)}`
|
|
13
|
+
*
|
|
14
|
+
* The `typeof v` segment is required because `String(v)` collapses distinct-
|
|
15
|
+
* type literals that stringify the same way (e.g. `true` vs `"true"`, `1` vs
|
|
16
|
+
* `"1"`). Without it, two rules on the same attribute with different-type
|
|
17
|
+
* literals would share a `ruleType` and be OR-combined by the evaluator,
|
|
18
|
+
* silently weakening authorization.
|
|
19
|
+
*
|
|
20
|
+
* Pass a shared `group` string to two instances to opt into OR semantics
|
|
21
|
+
* (e.g. "role is admin" OR "role is superuser" under one group).
|
|
22
|
+
*/
|
|
23
|
+
export class AttrLiteralEqual {
|
|
24
|
+
config;
|
|
25
|
+
ruleType;
|
|
26
|
+
code = "attr_not_equal";
|
|
27
|
+
message;
|
|
28
|
+
constructor(config) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
requireAttrName("AttrLiteralEqual", "a", config.a);
|
|
31
|
+
requireLiteralValue("AttrLiteralEqual", "v", config.v);
|
|
32
|
+
const group = requireOptionalGroup("AttrLiteralEqual", config.group);
|
|
33
|
+
this.ruleType =
|
|
34
|
+
group ?? `attr_literal_equal:${config.a}:${typeof config.v}:${String(config.v)}`;
|
|
35
|
+
this.message = `Attribute constraint not satisfied: ${config.a} must equal ${String(config.v)}.`;
|
|
36
|
+
}
|
|
37
|
+
verify(attrs) {
|
|
38
|
+
const x = attrs.get(this.config.a);
|
|
39
|
+
if (typeof x !== typeof this.config.v)
|
|
40
|
+
return false;
|
|
41
|
+
return x === this.config.v;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Attributes, Rule } from "@o3co/auth.policy-verifier.core";
|
|
2
|
+
import { type LiteralValue } from "./_sharedValidation.mjs";
|
|
3
|
+
export interface AttrLiteralInConfig {
|
|
4
|
+
a: string;
|
|
5
|
+
values: LiteralValue[];
|
|
6
|
+
group?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Rule that passes when a named attribute is present, matches the element type
|
|
10
|
+
* of the configured values array, and is included in that array. No type
|
|
11
|
+
* coercion is performed. If the attribute is missing, null, or of the wrong
|
|
12
|
+
* type the rule returns false (safe-deny).
|
|
13
|
+
*
|
|
14
|
+
* ## Grouping and the default ruleType
|
|
15
|
+
*
|
|
16
|
+
* The evaluator groups rules by `ruleType`, ORs within a group, and ANDs
|
|
17
|
+
* across groups. The default `ruleType` is derived from `a` and a stable
|
|
18
|
+
* content-hash key over the values array:
|
|
19
|
+
* `attr_literal_in:{a}:{valuesKey}`
|
|
20
|
+
*
|
|
21
|
+
* Two instances with the same `a` but different `values` will produce distinct
|
|
22
|
+
* ruleTypes and thus be AND-combined by the evaluator. Two instances with the
|
|
23
|
+
* same `a` and logically equivalent `values` (regardless of insertion order)
|
|
24
|
+
* will share a ruleType and be OR-combined.
|
|
25
|
+
*
|
|
26
|
+
* Pass an explicit `group` string to override the default ruleType entirely.
|
|
27
|
+
*/
|
|
28
|
+
export declare class AttrLiteralIn implements Rule {
|
|
29
|
+
private readonly config;
|
|
30
|
+
readonly ruleType: string;
|
|
31
|
+
readonly code = "attr_not_in_set";
|
|
32
|
+
readonly message: string;
|
|
33
|
+
private readonly elementType;
|
|
34
|
+
private readonly valuesSet;
|
|
35
|
+
constructor(config: AttrLiteralInConfig);
|
|
36
|
+
verify(attrs: Attributes): boolean;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=AttrLiteralIn.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AttrLiteralIn.d.mts","sourceRoot":"","sources":["../../src/rules/AttrLiteralIn.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAEN,KAAK,YAAY,EAIjB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,mBAAmB;IACnC,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,aAAc,YAAW,IAAI;IAQ7B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAPnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,qBAAqB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;gBAEjB,MAAM,EAAE,mBAAmB;IAaxD,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;CAKlC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { computeValuesKey, requireAttrName, requireHomogeneousLiteralArray, requireOptionalGroup, } from "./_sharedValidation.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* Rule that passes when a named attribute is present, matches the element type
|
|
4
|
+
* of the configured values array, and is included in that array. No type
|
|
5
|
+
* coercion is performed. If the attribute is missing, null, or of the wrong
|
|
6
|
+
* type the rule returns false (safe-deny).
|
|
7
|
+
*
|
|
8
|
+
* ## Grouping and the default ruleType
|
|
9
|
+
*
|
|
10
|
+
* The evaluator groups rules by `ruleType`, ORs within a group, and ANDs
|
|
11
|
+
* across groups. The default `ruleType` is derived from `a` and a stable
|
|
12
|
+
* content-hash key over the values array:
|
|
13
|
+
* `attr_literal_in:{a}:{valuesKey}`
|
|
14
|
+
*
|
|
15
|
+
* Two instances with the same `a` but different `values` will produce distinct
|
|
16
|
+
* ruleTypes and thus be AND-combined by the evaluator. Two instances with the
|
|
17
|
+
* same `a` and logically equivalent `values` (regardless of insertion order)
|
|
18
|
+
* will share a ruleType and be OR-combined.
|
|
19
|
+
*
|
|
20
|
+
* Pass an explicit `group` string to override the default ruleType entirely.
|
|
21
|
+
*/
|
|
22
|
+
export class AttrLiteralIn {
|
|
23
|
+
config;
|
|
24
|
+
ruleType;
|
|
25
|
+
code = "attr_not_in_set";
|
|
26
|
+
message;
|
|
27
|
+
elementType;
|
|
28
|
+
valuesSet;
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.config = config;
|
|
31
|
+
requireAttrName("AttrLiteralIn", "a", config.a);
|
|
32
|
+
const values = requireHomogeneousLiteralArray("AttrLiteralIn", config.values);
|
|
33
|
+
const group = requireOptionalGroup("AttrLiteralIn", config.group);
|
|
34
|
+
this.elementType = typeof values[0];
|
|
35
|
+
this.valuesSet = new Set(values);
|
|
36
|
+
const valuesKey = computeValuesKey(values);
|
|
37
|
+
this.ruleType = group ?? `attr_literal_in:${config.a}:${valuesKey}`;
|
|
38
|
+
this.message = `Attribute constraint not satisfied: ${config.a} must be one of [${values.map(String).join(", ")}].`;
|
|
39
|
+
}
|
|
40
|
+
verify(attrs) {
|
|
41
|
+
const x = attrs.get(this.config.a);
|
|
42
|
+
if (typeof x !== this.elementType)
|
|
43
|
+
return false;
|
|
44
|
+
return this.valuesSet.has(x);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Attributes, Rule } from "@o3co/auth.policy-verifier.core";
|
|
2
|
+
import { type LiteralValue } from "./_sharedValidation.mjs";
|
|
3
|
+
export interface AttrLiteralNotEqualConfig {
|
|
4
|
+
a: string;
|
|
5
|
+
v: LiteralValue;
|
|
6
|
+
group?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Rule that passes when a named attribute is present, matches the type of the
|
|
10
|
+
* configured literal value, and is strictly NOT equal to it. No type coercion
|
|
11
|
+
* is performed. If the attribute is missing, null, or of the wrong type the
|
|
12
|
+
* rule returns false (safe-deny).
|
|
13
|
+
*
|
|
14
|
+
* ## Grouping and the default ruleType
|
|
15
|
+
*
|
|
16
|
+
* The evaluator groups rules by `ruleType`, ORs within a group, and ANDs
|
|
17
|
+
* across groups. The default `ruleType` is derived from `a`, `typeof v`, and
|
|
18
|
+
* `String(v)`:
|
|
19
|
+
* `attr_literal_not_equal:{a}:{typeof v}:{String(v)}`
|
|
20
|
+
*
|
|
21
|
+
* The `typeof v` segment prevents silent `ruleType` collisions between
|
|
22
|
+
* different-type literals that stringify the same way (e.g. `1` vs `"1"`),
|
|
23
|
+
* which would otherwise be OR-combined by the evaluator against the intent
|
|
24
|
+
* of two independent constraints. See AttrLiteralEqual for the same rationale.
|
|
25
|
+
*
|
|
26
|
+
* Pass a shared `group` string to two instances to opt into OR semantics.
|
|
27
|
+
*/
|
|
28
|
+
export declare class AttrLiteralNotEqual implements Rule {
|
|
29
|
+
private readonly config;
|
|
30
|
+
readonly ruleType: string;
|
|
31
|
+
readonly code = "attr_equal";
|
|
32
|
+
readonly message: string;
|
|
33
|
+
constructor(config: AttrLiteralNotEqualConfig);
|
|
34
|
+
verify(attrs: Attributes): boolean;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=AttrLiteralNotEqual.d.mts.map
|