@prosopo/user-access-policy 3.14.14 → 3.14.15
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/.turbo/turbo-build$colon$cjs.log +2 -5
- package/.turbo/turbo-build$colon$tsc.log +14 -17
- package/.turbo/turbo-build.log +3 -9
- package/CHANGELOG.md +18 -0
- package/LICENSE +201 -0
- package/package.json +83 -81
- package/src/.export.ts +0 -65
- package/src/api/.export.ts +0 -25
- package/src/api/accessRulesApiClient.ts +0 -13
- package/src/api/delete/.export.ts +0 -18
- package/src/api/delete/deleteAllRules.ts +0 -47
- package/src/api/delete/deleteRuleGroups.ts +0 -96
- package/src/api/delete/deleteRules.ts +0 -87
- package/src/api/read/.export.ts +0 -25
- package/src/api/read/fetchRules.ts +0 -96
- package/src/api/read/findRuleIds.ts +0 -97
- package/src/api/read/getMissingIds.ts +0 -81
- package/src/api/ruleApiRoutes.ts +0 -146
- package/src/api/rulesApiClient.ts +0 -154
- package/src/api/write/.export.ts +0 -15
- package/src/api/write/insertRules.ts +0 -185
- package/src/api/write/rehashRules.ts +0 -85
- package/src/classifyBrowser.ts +0 -77
- package/src/classifyOs.ts +0 -74
- package/src/headerMatch.ts +0 -180
- package/src/matchedRule.ts +0 -99
- package/src/mongoose/.export.ts +0 -15
- package/src/mongoose/mongooseRuleSchema.ts +0 -75
- package/src/redis/.export.ts +0 -17
- package/src/redis/reader/redisAggregate.ts +0 -157
- package/src/redis/reader/redisRulesQuery.ts +0 -268
- package/src/redis/reader/redisRulesReader.ts +0 -738
- package/src/redis/reader/redisRulesSplitQuery.ts +0 -234
- package/src/redis/redisClient.ts +0 -125
- package/src/redis/redisRuleIndex.ts +0 -121
- package/src/redis/redisRulesStorage.ts +0 -68
- package/src/redis/redisRulesWriter.ts +0 -172
- package/src/rule.ts +0 -122
- package/src/ruleInput/.export.ts +0 -19
- package/src/ruleInput/policyInput.ts +0 -120
- package/src/ruleInput/ruleInput.ts +0 -123
- package/src/ruleInput/userScopeInput.ts +0 -142
- package/src/ruleRecord.ts +0 -77
- package/src/rulesStorage.ts +0 -90
- package/src/tests/blockingRuleShapes.unit.test.ts +0 -244
- package/src/tests/classifyBrowser.unit.test.ts +0 -138
- package/src/tests/classifyOs.unit.test.ts +0 -97
- package/src/tests/headerMatch.unit.test.ts +0 -350
- package/src/tests/insertRulesEndpoint.unit.test.ts +0 -89
- package/src/tests/matchedRule.unit.test.ts +0 -184
- package/src/tests/policyInput.unit.test.ts +0 -218
- package/src/tests/redis/deferredPoolClause.benchmark.ts +0 -249
- package/src/tests/redis/reader/redisAggregate.unit.test.ts +0 -121
- package/src/tests/redis/reader/redisRulesQuery.unit.test.ts +0 -349
- package/src/tests/redis/reader/redisRulesSplitQuery.unit.test.ts +0 -297
- package/src/tests/redis/redisRulesReaderLoad.benchmark.integration.test.ts +0 -520
- package/src/tests/redis/redisRulesReaderRank.benchmark.integration.test.ts +0 -350
- package/src/tests/redis/redisRulesStorage.integration.test.ts +0 -1811
- package/src/tests/testLogger.ts +0 -38
- package/src/tests/transformRule.unit.test.ts +0 -345
- package/src/transformRule.ts +0 -133
- package/tsconfig.cjs.json +0 -41
- package/tsconfig.json +0 -47
- package/tsconfig.tsbuildinfo +0 -1
- package/tsconfig.types.json +0 -9
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import { describe, expect, it } from "vitest";
|
|
16
|
-
import { OS_NAMES, type OsName, classifyOs } from "#policy/classifyOs.js";
|
|
17
|
-
|
|
18
|
-
describe("classifyOs", () => {
|
|
19
|
-
const cases: Array<{ name: string; ua: string; expected: OsName }> = [
|
|
20
|
-
{
|
|
21
|
-
name: "Windows 10 Chrome",
|
|
22
|
-
ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
23
|
-
expected: "windows",
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
name: "macOS Safari",
|
|
27
|
-
ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
|
|
28
|
-
expected: "macos",
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
name: "iPhone Safari",
|
|
32
|
-
ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
|
|
33
|
-
expected: "ios",
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
name: "iPad Safari",
|
|
37
|
-
ua: "Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
|
|
38
|
-
expected: "ios",
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: "Android Chrome",
|
|
42
|
-
ua: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
|
|
43
|
-
expected: "android",
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
name: "Linux desktop Firefox",
|
|
47
|
-
ua: "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0",
|
|
48
|
-
expected: "linux",
|
|
49
|
-
},
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
for (const { name, ua, expected } of cases) {
|
|
53
|
-
it(`classifies ${name} as ${expected}`, () => {
|
|
54
|
-
expect(classifyOs(ua)).toBe(expected);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
it("returns 'unknown' for an empty User-Agent", () => {
|
|
59
|
-
expect(classifyOs("")).toBe("unknown");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("returns 'unknown' for an undefined User-Agent", () => {
|
|
63
|
-
expect(classifyOs(undefined)).toBe("unknown");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("returns 'unknown' for an unrecognised User-Agent", () => {
|
|
67
|
-
expect(classifyOs("curl/8.4.0")).toBe("unknown");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("prefers iOS over macOS when the UA carries both signatures", () => {
|
|
71
|
-
// iOS UAs contain the "like Mac OS X" token, so the iphone/ipad check
|
|
72
|
-
// must win or every iPhone would be mislabelled macOS.
|
|
73
|
-
expect(
|
|
74
|
-
classifyOs(
|
|
75
|
-
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Mobile",
|
|
76
|
-
),
|
|
77
|
-
).toBe("ios");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("prefers Android over Linux when the UA carries both signatures", () => {
|
|
81
|
-
// Android UAs contain "Linux", so the android check must come first.
|
|
82
|
-
expect(classifyOs("Mozilla/5.0 (Linux; Android 14) Mobile")).toBe(
|
|
83
|
-
"android",
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("is case-insensitive", () => {
|
|
88
|
-
expect(classifyOs("WINDOWS NT 10.0")).toBe("windows");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("only ever returns a value from OS_NAMES", () => {
|
|
92
|
-
const uas = [...cases.map((c) => c.ua), "", "curl/8.4.0", "garbage"];
|
|
93
|
-
for (const ua of uas) {
|
|
94
|
-
expect(OS_NAMES).toContain(classifyOs(ua));
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
});
|
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import { describe, expect, it } from "vitest";
|
|
16
|
-
import {
|
|
17
|
-
type HeaderOperator,
|
|
18
|
-
accessRuleHeaderMatches,
|
|
19
|
-
decodeHeaderValueList,
|
|
20
|
-
encodeHeaderValueList,
|
|
21
|
-
evaluateHeaderCondition,
|
|
22
|
-
isHeaderOperator,
|
|
23
|
-
} from "#policy/headerMatch.js";
|
|
24
|
-
|
|
25
|
-
describe("isHeaderOperator", () => {
|
|
26
|
-
it("accepts the six known operators", () => {
|
|
27
|
-
for (const op of [
|
|
28
|
-
"equals",
|
|
29
|
-
"contains",
|
|
30
|
-
"notEquals",
|
|
31
|
-
"notContains",
|
|
32
|
-
"notEqualsAny",
|
|
33
|
-
"notContainsAny",
|
|
34
|
-
]) {
|
|
35
|
-
expect(isHeaderOperator(op)).toBe(true);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("rejects unknown / undefined values", () => {
|
|
40
|
-
expect(isHeaderOperator("startsWith")).toBe(false);
|
|
41
|
-
expect(isHeaderOperator("")).toBe(false);
|
|
42
|
-
expect(isHeaderOperator(undefined)).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe("evaluateHeaderCondition", () => {
|
|
47
|
-
const headers: Record<string, string> = {
|
|
48
|
-
"x-app-version": "1.2.3-beta",
|
|
49
|
-
"cf-worker": "example.com",
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const cases: ReadonlyArray<{
|
|
53
|
-
name: string;
|
|
54
|
-
op: HeaderOperator;
|
|
55
|
-
value: string;
|
|
56
|
-
header: string;
|
|
57
|
-
expected: boolean;
|
|
58
|
-
}> = [
|
|
59
|
-
// equals: present + exact
|
|
60
|
-
{
|
|
61
|
-
name: "equals hit",
|
|
62
|
-
op: "equals",
|
|
63
|
-
value: "example.com",
|
|
64
|
-
header: "cf-worker",
|
|
65
|
-
expected: true,
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: "equals miss (different value)",
|
|
69
|
-
op: "equals",
|
|
70
|
-
value: "evil.com",
|
|
71
|
-
header: "cf-worker",
|
|
72
|
-
expected: false,
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: "equals miss (header absent)",
|
|
76
|
-
op: "equals",
|
|
77
|
-
value: "anything",
|
|
78
|
-
header: "x-missing",
|
|
79
|
-
expected: false,
|
|
80
|
-
},
|
|
81
|
-
// contains: substring
|
|
82
|
-
{
|
|
83
|
-
name: "contains hit",
|
|
84
|
-
op: "contains",
|
|
85
|
-
value: "beta",
|
|
86
|
-
header: "x-app-version",
|
|
87
|
-
expected: true,
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
name: "contains miss (no substring)",
|
|
91
|
-
op: "contains",
|
|
92
|
-
value: "alpha",
|
|
93
|
-
header: "x-app-version",
|
|
94
|
-
expected: false,
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
name: "contains miss (header absent)",
|
|
98
|
-
op: "contains",
|
|
99
|
-
value: "beta",
|
|
100
|
-
header: "x-missing",
|
|
101
|
-
expected: false,
|
|
102
|
-
},
|
|
103
|
-
// notEquals: allow-list "equals" — blocks when NOT exactly equal
|
|
104
|
-
{
|
|
105
|
-
name: "notEquals blocks a different value",
|
|
106
|
-
op: "notEquals",
|
|
107
|
-
value: "example.com",
|
|
108
|
-
header: "cf-worker",
|
|
109
|
-
expected: false,
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
name: "notEquals lets an exact match through",
|
|
113
|
-
op: "notEquals",
|
|
114
|
-
value: "evil.com",
|
|
115
|
-
header: "cf-worker",
|
|
116
|
-
expected: true,
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
name: "notEquals blocks an absent header",
|
|
120
|
-
op: "notEquals",
|
|
121
|
-
value: "secret",
|
|
122
|
-
header: "x-secret",
|
|
123
|
-
expected: true,
|
|
124
|
-
},
|
|
125
|
-
// notContains: allow-list "contains" — blocks when substring absent
|
|
126
|
-
{
|
|
127
|
-
name: "notContains blocks when substring present",
|
|
128
|
-
op: "notContains",
|
|
129
|
-
value: "beta",
|
|
130
|
-
header: "x-app-version",
|
|
131
|
-
expected: false,
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
name: "notContains lets a substring hit through",
|
|
135
|
-
op: "notContains",
|
|
136
|
-
value: "alpha",
|
|
137
|
-
header: "x-app-version",
|
|
138
|
-
expected: true,
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: "notContains blocks an absent header",
|
|
142
|
-
op: "notContains",
|
|
143
|
-
value: "beta",
|
|
144
|
-
header: "x-missing",
|
|
145
|
-
expected: true,
|
|
146
|
-
},
|
|
147
|
-
];
|
|
148
|
-
|
|
149
|
-
for (const c of cases) {
|
|
150
|
-
it(c.name, () => {
|
|
151
|
-
expect(evaluateHeaderCondition(c.header, c.op, c.value, headers)).toBe(
|
|
152
|
-
c.expected,
|
|
153
|
-
);
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
it("matches header names case-insensitively", () => {
|
|
158
|
-
expect(
|
|
159
|
-
evaluateHeaderCondition("CF-Worker", "equals", "example.com", headers),
|
|
160
|
-
).toBe(true);
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe("accessRuleHeaderMatches", () => {
|
|
165
|
-
const headers: Record<string, string> = { "x-debug": "1" };
|
|
166
|
-
|
|
167
|
-
it("passes trivially when the rule has no header condition", () => {
|
|
168
|
-
expect(accessRuleHeaderMatches({}, headers)).toBe(true);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("evaluates a well-formed header condition", () => {
|
|
172
|
-
expect(
|
|
173
|
-
accessRuleHeaderMatches(
|
|
174
|
-
{ headerName: "x-debug", headerOperator: "equals", headerValue: "1" },
|
|
175
|
-
headers,
|
|
176
|
-
),
|
|
177
|
-
).toBe(true);
|
|
178
|
-
expect(
|
|
179
|
-
accessRuleHeaderMatches(
|
|
180
|
-
{ headerName: "x-debug", headerOperator: "equals", headerValue: "0" },
|
|
181
|
-
headers,
|
|
182
|
-
),
|
|
183
|
-
).toBe(false);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("never matches a rule with a header name but an invalid operator", () => {
|
|
187
|
-
expect(
|
|
188
|
-
accessRuleHeaderMatches(
|
|
189
|
-
{ headerName: "x-debug", headerOperator: "wat", headerValue: "1" },
|
|
190
|
-
headers,
|
|
191
|
-
),
|
|
192
|
-
).toBe(false);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("treats a missing headerValue as an empty string", () => {
|
|
196
|
-
// "contains empty string" is always true for a present header.
|
|
197
|
-
expect(
|
|
198
|
-
accessRuleHeaderMatches(
|
|
199
|
-
{ headerName: "x-debug", headerOperator: "contains" },
|
|
200
|
-
headers,
|
|
201
|
-
),
|
|
202
|
-
).toBe(true);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
describe("encode/decodeHeaderValueList", () => {
|
|
207
|
-
it("round-trips a list of values", () => {
|
|
208
|
-
const values = ["ios", "android", "web"];
|
|
209
|
-
expect(decodeHeaderValueList(encodeHeaderValueList(values))).toEqual(
|
|
210
|
-
values,
|
|
211
|
-
);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("round-trips values containing the characters a delimiter would break on", () => {
|
|
215
|
-
const values = ["en-GB,en;q=0.9", 'a"b', "c\\d", "[bracketed]"];
|
|
216
|
-
expect(decodeHeaderValueList(encodeHeaderValueList(values))).toEqual(
|
|
217
|
-
values,
|
|
218
|
-
);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("treats a plain (non list-encoded) value as a one-value list", () => {
|
|
222
|
-
expect(decodeHeaderValueList("ios")).toEqual(["ios"]);
|
|
223
|
-
expect(decodeHeaderValueList("")).toEqual([""]);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("rejects a list-shaped value that is not a usable list", () => {
|
|
227
|
-
expect(decodeHeaderValueList("[")).toBeUndefined();
|
|
228
|
-
expect(decodeHeaderValueList("[not json]")).toBeUndefined();
|
|
229
|
-
expect(decodeHeaderValueList("[]")).toBeUndefined();
|
|
230
|
-
expect(decodeHeaderValueList("[1,2]")).toBeUndefined();
|
|
231
|
-
expect(decodeHeaderValueList('["ok", null]')).toBeUndefined();
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
describe("evaluateHeaderCondition — multi-value allow-list operators", () => {
|
|
236
|
-
const headers: Record<string, string> = {
|
|
237
|
-
"x-app": "android",
|
|
238
|
-
"user-agent": "Mozilla/5.0 (Linux; Android 14) Chrome/120",
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
const allowedApps = encodeHeaderValueList(["ios", "android"]);
|
|
242
|
-
|
|
243
|
-
it("does not block a request whose header is one of the allowed values", () => {
|
|
244
|
-
expect(
|
|
245
|
-
evaluateHeaderCondition("x-app", "notEqualsAny", allowedApps, headers),
|
|
246
|
-
).toBe(false);
|
|
247
|
-
expect(
|
|
248
|
-
evaluateHeaderCondition(
|
|
249
|
-
"x-app",
|
|
250
|
-
"notEqualsAny",
|
|
251
|
-
encodeHeaderValueList(["android"]),
|
|
252
|
-
headers,
|
|
253
|
-
),
|
|
254
|
-
).toBe(false);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("blocks a request whose header matches none of the allowed values", () => {
|
|
258
|
-
expect(
|
|
259
|
-
evaluateHeaderCondition(
|
|
260
|
-
"x-app",
|
|
261
|
-
"notEqualsAny",
|
|
262
|
-
encodeHeaderValueList(["ios", "web"]),
|
|
263
|
-
headers,
|
|
264
|
-
),
|
|
265
|
-
).toBe(true);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it("blocks a request that omits the header entirely", () => {
|
|
269
|
-
expect(
|
|
270
|
-
evaluateHeaderCondition(
|
|
271
|
-
"x-missing",
|
|
272
|
-
"notEqualsAny",
|
|
273
|
-
allowedApps,
|
|
274
|
-
headers,
|
|
275
|
-
),
|
|
276
|
-
).toBe(true);
|
|
277
|
-
expect(
|
|
278
|
-
evaluateHeaderCondition(
|
|
279
|
-
"x-missing",
|
|
280
|
-
"notContainsAny",
|
|
281
|
-
allowedApps,
|
|
282
|
-
headers,
|
|
283
|
-
),
|
|
284
|
-
).toBe(true);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it("matches a substring of the header for notContainsAny", () => {
|
|
288
|
-
expect(
|
|
289
|
-
evaluateHeaderCondition(
|
|
290
|
-
"user-agent",
|
|
291
|
-
"notContainsAny",
|
|
292
|
-
encodeHeaderValueList(["Android", "iPhone"]),
|
|
293
|
-
headers,
|
|
294
|
-
),
|
|
295
|
-
).toBe(false);
|
|
296
|
-
expect(
|
|
297
|
-
evaluateHeaderCondition(
|
|
298
|
-
"user-agent",
|
|
299
|
-
"notContainsAny",
|
|
300
|
-
encodeHeaderValueList(["iPhone", "Windows"]),
|
|
301
|
-
headers,
|
|
302
|
-
),
|
|
303
|
-
).toBe(true);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it("is exact for notEqualsAny — a substring is not a match", () => {
|
|
307
|
-
expect(
|
|
308
|
-
evaluateHeaderCondition(
|
|
309
|
-
"x-app",
|
|
310
|
-
"notEqualsAny",
|
|
311
|
-
encodeHeaderValueList(["droid"]),
|
|
312
|
-
headers,
|
|
313
|
-
),
|
|
314
|
-
).toBe(true);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it("never fires on a malformed value list, rather than blocking everything", () => {
|
|
318
|
-
for (const operator of ["notEqualsAny", "notContainsAny"] as const) {
|
|
319
|
-
expect(evaluateHeaderCondition("x-app", operator, "[]", headers)).toBe(
|
|
320
|
-
false,
|
|
321
|
-
);
|
|
322
|
-
expect(
|
|
323
|
-
evaluateHeaderCondition("x-missing", operator, "[bad", headers),
|
|
324
|
-
).toBe(false);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it("is reachable through accessRuleHeaderMatches", () => {
|
|
329
|
-
expect(
|
|
330
|
-
accessRuleHeaderMatches(
|
|
331
|
-
{
|
|
332
|
-
headerName: "X-App",
|
|
333
|
-
headerOperator: "notEqualsAny",
|
|
334
|
-
headerValue: allowedApps,
|
|
335
|
-
},
|
|
336
|
-
headers,
|
|
337
|
-
),
|
|
338
|
-
).toBe(false);
|
|
339
|
-
expect(
|
|
340
|
-
accessRuleHeaderMatches(
|
|
341
|
-
{
|
|
342
|
-
headerName: "X-App",
|
|
343
|
-
headerOperator: "notEqualsAny",
|
|
344
|
-
headerValue: encodeHeaderValueList(["ios"]),
|
|
345
|
-
},
|
|
346
|
-
headers,
|
|
347
|
-
),
|
|
348
|
-
).toBe(true);
|
|
349
|
-
});
|
|
350
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import type { Logger } from "@prosopo/logger";
|
|
16
|
-
import { describe, expect, it, vi } from "vitest";
|
|
17
|
-
import { InsertRulesEndpoint } from "#policy/api/write/insertRules.js";
|
|
18
|
-
import { AccessPolicyType } from "#policy/rule.js";
|
|
19
|
-
import type { AccessRulesWriter } from "#policy/rulesStorage.js";
|
|
20
|
-
|
|
21
|
-
const makeMockLogger = (): Logger =>
|
|
22
|
-
({
|
|
23
|
-
trace: vi.fn(),
|
|
24
|
-
debug: vi.fn(),
|
|
25
|
-
info: vi.fn(),
|
|
26
|
-
warn: vi.fn(),
|
|
27
|
-
error: vi.fn(),
|
|
28
|
-
fatal: vi.fn(),
|
|
29
|
-
log: vi.fn(),
|
|
30
|
-
setLogLevel: vi.fn(),
|
|
31
|
-
getLogLevel: vi.fn().mockReturnValue("info"),
|
|
32
|
-
with: vi.fn().mockReturnThis(),
|
|
33
|
-
getScope: vi.fn().mockReturnValue("test"),
|
|
34
|
-
getPretty: vi.fn().mockReturnValue(false),
|
|
35
|
-
setPretty: vi.fn(),
|
|
36
|
-
getPrintStack: vi.fn().mockReturnValue(false),
|
|
37
|
-
setPrintStack: vi.fn(),
|
|
38
|
-
getFormat: vi.fn().mockReturnValue("json"),
|
|
39
|
-
setFormat: vi.fn(),
|
|
40
|
-
}) satisfies Logger;
|
|
41
|
-
|
|
42
|
-
const makeWriter = (): AccessRulesWriter => ({
|
|
43
|
-
insertRules: vi.fn().mockResolvedValue(["rule-1", "rule-2"]),
|
|
44
|
-
deleteRules: vi.fn().mockResolvedValue(undefined),
|
|
45
|
-
deleteAllRules: vi.fn().mockResolvedValue(0),
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const sampleArgs = [
|
|
49
|
-
{
|
|
50
|
-
accessPolicy: { type: AccessPolicyType.Block },
|
|
51
|
-
userScopes: [{ userId: "user-1" }, { userId: "user-2" }],
|
|
52
|
-
},
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
describe("InsertRulesEndpoint logger selection", () => {
|
|
56
|
-
it("uses the per-request logger when one is provided to processRequest", async () => {
|
|
57
|
-
const ctorLogger = makeMockLogger();
|
|
58
|
-
const requestLogger = makeMockLogger();
|
|
59
|
-
const endpoint = new InsertRulesEndpoint(makeWriter(), ctorLogger);
|
|
60
|
-
|
|
61
|
-
// processRequest races against a 5s timeout and resolves PROCESSING first;
|
|
62
|
-
// the actual insert + log happens after. Wait long enough for the .then()
|
|
63
|
-
// chain to fire.
|
|
64
|
-
await endpoint.processRequest(sampleArgs, requestLogger);
|
|
65
|
-
await new Promise((r) => setImmediate(r));
|
|
66
|
-
|
|
67
|
-
expect(requestLogger.info).toHaveBeenCalled();
|
|
68
|
-
const calls = (requestLogger.info as ReturnType<typeof vi.fn>).mock.calls;
|
|
69
|
-
const msgs = calls.map(([fn]) => (fn as () => { msg: string })().msg);
|
|
70
|
-
expect(msgs).toContain("Endpoint inserted access rules");
|
|
71
|
-
|
|
72
|
-
// Constructor logger must not have been touched for the request-path log.
|
|
73
|
-
expect(ctorLogger.info).not.toHaveBeenCalled();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("falls back to the constructor logger when no request logger is provided", async () => {
|
|
77
|
-
const ctorLogger = makeMockLogger();
|
|
78
|
-
const endpoint = new InsertRulesEndpoint(makeWriter(), ctorLogger);
|
|
79
|
-
|
|
80
|
-
await endpoint.processRequest(sampleArgs);
|
|
81
|
-
await new Promise((r) => setImmediate(r));
|
|
82
|
-
|
|
83
|
-
expect(ctorLogger.info).toHaveBeenCalled();
|
|
84
|
-
const msgs = (ctorLogger.info as ReturnType<typeof vi.fn>).mock.calls.map(
|
|
85
|
-
([fn]) => (fn as () => { msg: string })().msg,
|
|
86
|
-
);
|
|
87
|
-
expect(msgs).toContain("Endpoint inserted access rules");
|
|
88
|
-
});
|
|
89
|
-
});
|