@prosopo/user-access-policy 3.6.0 → 3.7.12
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 +21 -19
- package/.turbo/turbo-build$colon$tsc.log +16 -13
- package/.turbo/turbo-build.log +22 -20
- package/CHANGELOG.md +339 -0
- package/dist/api/delete/deleteAllRules.d.ts +2 -2
- package/dist/api/delete/deleteAllRules.d.ts.map +1 -1
- package/dist/api/delete/deleteAllRules.js +3 -2
- package/dist/api/delete/deleteAllRules.js.map +1 -1
- package/dist/api/delete/deleteRuleGroups.d.ts +2 -2
- package/dist/api/delete/deleteRuleGroups.d.ts.map +1 -1
- package/dist/api/delete/deleteRuleGroups.js +3 -2
- package/dist/api/delete/deleteRuleGroups.js.map +1 -1
- package/dist/api/delete/deleteRules.d.ts +2 -2
- package/dist/api/delete/deleteRules.d.ts.map +1 -1
- package/dist/api/delete/deleteRules.js +3 -2
- package/dist/api/delete/deleteRules.js.map +1 -1
- package/dist/api/read/fetchRules.d.ts +2 -2
- package/dist/api/read/fetchRules.d.ts.map +1 -1
- package/dist/api/read/fetchRules.js +4 -3
- package/dist/api/read/fetchRules.js.map +1 -1
- package/dist/api/read/findRuleIds.d.ts +2 -2
- package/dist/api/read/findRuleIds.d.ts.map +1 -1
- package/dist/api/read/findRuleIds.js +3 -2
- package/dist/api/read/findRuleIds.js.map +1 -1
- package/dist/api/read/getMissingIds.d.ts +2 -2
- package/dist/api/read/getMissingIds.d.ts.map +1 -1
- package/dist/api/read/getMissingIds.js +4 -3
- package/dist/api/read/getMissingIds.js.map +1 -1
- package/dist/api/ruleApiRoutes.d.ts +1 -1
- package/dist/api/ruleApiRoutes.d.ts.map +1 -1
- package/dist/api/ruleApiRoutes.js.map +1 -1
- package/dist/api/rulesApiClient.d.ts +9 -9
- package/dist/api/rulesApiClient.d.ts.map +1 -1
- package/dist/api/rulesApiClient.js +18 -19
- package/dist/api/rulesApiClient.js.map +1 -1
- package/dist/api/write/insertRules.d.ts +2 -2
- package/dist/api/write/insertRules.d.ts.map +1 -1
- package/dist/api/write/insertRules.js +7 -6
- package/dist/api/write/insertRules.js.map +1 -1
- package/dist/api/write/rehashRules.d.ts +2 -2
- package/dist/api/write/rehashRules.d.ts.map +1 -1
- package/dist/api/write/rehashRules.js +7 -6
- package/dist/api/write/rehashRules.js.map +1 -1
- package/dist/cjs/api/delete/deleteAllRules.cjs +3 -2
- package/dist/cjs/api/delete/deleteRuleGroups.cjs +3 -2
- package/dist/cjs/api/delete/deleteRules.cjs +3 -2
- package/dist/cjs/api/read/fetchRules.cjs +4 -3
- package/dist/cjs/api/read/findRuleIds.cjs +3 -2
- package/dist/cjs/api/read/getMissingIds.cjs +4 -3
- package/dist/cjs/api/rulesApiClient.cjs +18 -19
- package/dist/cjs/api/write/insertRules.cjs +9 -8
- package/dist/cjs/api/write/rehashRules.cjs +7 -6
- package/dist/cjs/mongoose/mongooseRuleSchema.cjs +2 -1
- package/dist/cjs/redis/reader/redisRulesQuery.cjs +6 -0
- package/dist/cjs/redis/reader/redisRulesReader.cjs +13 -4
- package/dist/cjs/redis/redisRuleIndex.cjs +2 -1
- package/dist/cjs/ruleInput/userScopeInput.cjs +2 -1
- package/dist/cjs/ruleRecord.cjs +2 -1
- package/dist/mongoose/mongooseRuleSchema.d.ts.map +1 -1
- package/dist/mongoose/mongooseRuleSchema.js +2 -1
- package/dist/mongoose/mongooseRuleSchema.js.map +1 -1
- package/dist/redis/reader/redisAggregate.d.ts +1 -1
- package/dist/redis/reader/redisRulesQuery.d.ts.map +1 -1
- package/dist/redis/reader/redisRulesQuery.js +6 -0
- package/dist/redis/reader/redisRulesQuery.js.map +1 -1
- package/dist/redis/reader/redisRulesReader.d.ts +1 -1
- package/dist/redis/reader/redisRulesReader.d.ts.map +1 -1
- package/dist/redis/reader/redisRulesReader.js +14 -5
- package/dist/redis/reader/redisRulesReader.js.map +1 -1
- package/dist/redis/redisClient.d.ts +1 -1
- package/dist/redis/redisRuleIndex.d.ts.map +1 -1
- package/dist/redis/redisRuleIndex.js +2 -1
- package/dist/redis/redisRuleIndex.js.map +1 -1
- package/dist/redis/redisRulesStorage.d.ts +1 -1
- package/dist/redis/redisRulesWriter.d.ts +1 -1
- package/dist/redis/redisRulesWriter.d.ts.map +1 -1
- package/dist/redis/redisRulesWriter.js.map +1 -1
- package/dist/rule.d.ts +1 -0
- package/dist/rule.d.ts.map +1 -1
- package/dist/ruleInput/ruleInput.d.ts +6 -0
- package/dist/ruleInput/ruleInput.d.ts.map +1 -1
- package/dist/ruleInput/userScopeInput.d.ts +8 -0
- package/dist/ruleInput/userScopeInput.d.ts.map +1 -1
- package/dist/ruleInput/userScopeInput.js +2 -1
- package/dist/ruleInput/userScopeInput.js.map +1 -1
- package/dist/ruleRecord.d.ts +2 -2
- package/dist/ruleRecord.d.ts.map +1 -1
- package/dist/ruleRecord.js +2 -1
- package/dist/ruleRecord.js.map +1 -1
- package/dist/tests/insertRulesEndpoint.unit.test.d.ts +2 -0
- package/dist/tests/insertRulesEndpoint.unit.test.d.ts.map +1 -0
- package/dist/tests/insertRulesEndpoint.unit.test.js +57 -0
- package/dist/tests/insertRulesEndpoint.unit.test.js.map +1 -0
- package/dist/tests/redis/reader/redisRulesQuery.unit.test.js +42 -3
- package/dist/tests/redis/reader/redisRulesQuery.unit.test.js.map +1 -1
- package/dist/tests/redis/redisRulesStorage.integration.test.js +126 -1
- package/dist/tests/redis/redisRulesStorage.integration.test.js.map +1 -1
- package/dist/tests/testLogger.d.ts +1 -1
- package/dist/tests/transformRule.unit.test.js +1 -0
- package/dist/tests/transformRule.unit.test.js.map +1 -1
- package/package.json +15 -10
- package/src/.export.ts +44 -0
- package/src/api/.export.ts +25 -0
- package/src/api/accessRulesApiClient.ts +13 -0
- package/src/api/delete/.export.ts +18 -0
- package/src/api/delete/deleteAllRules.ts +47 -0
- package/src/api/delete/deleteRuleGroups.ts +96 -0
- package/src/api/delete/deleteRules.ts +81 -0
- package/src/api/read/.export.ts +25 -0
- package/src/api/read/fetchRules.ts +88 -0
- package/src/api/read/findRuleIds.ts +95 -0
- package/src/api/read/getMissingIds.ts +81 -0
- package/src/api/ruleApiRoutes.ts +146 -0
- package/src/api/rulesApiClient.ts +154 -0
- package/src/api/write/.export.ts +15 -0
- package/src/api/write/insertRules.ts +183 -0
- package/src/api/write/rehashRules.ts +85 -0
- package/src/mongoose/.export.ts +15 -0
- package/src/mongoose/mongooseRuleSchema.ts +65 -0
- package/src/redis/.export.ts +17 -0
- package/src/redis/reader/redisAggregate.ts +103 -0
- package/src/redis/reader/redisRulesQuery.ts +217 -0
- package/src/redis/reader/redisRulesReader.ts +318 -0
- package/src/redis/redisClient.ts +120 -0
- package/src/redis/redisRuleIndex.ts +85 -0
- package/src/redis/redisRulesStorage.ts +68 -0
- package/src/redis/redisRulesWriter.ts +158 -0
- package/src/rule.ts +59 -0
- package/src/ruleInput/.export.ts +19 -0
- package/src/ruleInput/policyInput.ts +51 -0
- package/src/ruleInput/ruleInput.ts +103 -0
- package/src/ruleInput/userScopeInput.ts +108 -0
- package/src/ruleRecord.ts +69 -0
- package/src/rulesStorage.ts +72 -0
- package/src/tests/insertRulesEndpoint.unit.test.ts +89 -0
- package/src/tests/policyInput.unit.test.ts +150 -0
- package/src/tests/redis/reader/redisRulesQuery.unit.test.ts +284 -0
- package/src/tests/redis/redisRulesStorage.integration.test.ts +1156 -0
- package/src/tests/testLogger.ts +38 -0
- package/src/tests/transformRule.unit.test.ts +255 -0
- package/src/transformRule.ts +128 -0
- package/tsconfig.cjs.json +41 -0
- package/tsconfig.json +47 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsconfig.types.json +9 -0
|
@@ -0,0 +1,1156 @@
|
|
|
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 { chunkIntoBatches, executeBatchesSequentially } from "@prosopo/common";
|
|
16
|
+
import { LogLevel, type Logger, getLogger } from "@prosopo/logger";
|
|
17
|
+
import {
|
|
18
|
+
type RedisConnection,
|
|
19
|
+
createTestRedisConnection,
|
|
20
|
+
setupRedisIndex,
|
|
21
|
+
} from "@prosopo/redis-client";
|
|
22
|
+
import { randomAsHex } from "@prosopo/util-crypto";
|
|
23
|
+
import type { RedisClientType } from "redis";
|
|
24
|
+
import {
|
|
25
|
+
afterAll,
|
|
26
|
+
afterEach,
|
|
27
|
+
beforeAll,
|
|
28
|
+
beforeEach,
|
|
29
|
+
describe,
|
|
30
|
+
expect,
|
|
31
|
+
test,
|
|
32
|
+
} from "vitest";
|
|
33
|
+
import { RedisRulesReader } from "#policy/redis/reader/redisRulesReader.js";
|
|
34
|
+
import {
|
|
35
|
+
ACCESS_RULE_REDIS_KEY_PREFIX,
|
|
36
|
+
accessRulesRedisIndex,
|
|
37
|
+
getAccessRuleRedisKey,
|
|
38
|
+
} from "#policy/redis/redisRuleIndex.js";
|
|
39
|
+
import {
|
|
40
|
+
RedisRulesWriter,
|
|
41
|
+
getRedisRuleValue,
|
|
42
|
+
} from "#policy/redis/redisRulesWriter.js";
|
|
43
|
+
import { AccessPolicyType, type AccessRule } from "#policy/rule.js";
|
|
44
|
+
import {
|
|
45
|
+
type AccessRulesReader,
|
|
46
|
+
type AccessRulesWriter,
|
|
47
|
+
FilterScopeMatch,
|
|
48
|
+
} from "#policy/rulesStorage.js";
|
|
49
|
+
|
|
50
|
+
describe("redisAccessRulesStorage", () => {
|
|
51
|
+
let redisConnection: RedisConnection;
|
|
52
|
+
let redisClient: RedisClientType;
|
|
53
|
+
let indexName: string;
|
|
54
|
+
|
|
55
|
+
const mockLogger = new Proxy(
|
|
56
|
+
{},
|
|
57
|
+
{
|
|
58
|
+
get: () => () => {},
|
|
59
|
+
},
|
|
60
|
+
) as unknown as Logger;
|
|
61
|
+
|
|
62
|
+
const getUniqueString = () => Math.random().toString(36).substring(2, 15);
|
|
63
|
+
const getIndexRecordsCount = async (indexName: string): Promise<number> =>
|
|
64
|
+
(await redisClient.ft.info(indexName)).num_docs;
|
|
65
|
+
|
|
66
|
+
const insertRules = async (rules: AccessRule[]) => {
|
|
67
|
+
const ruleBatches = chunkIntoBatches(rules, 1000);
|
|
68
|
+
|
|
69
|
+
await executeBatchesSequentially(ruleBatches, async (batchRules) => {
|
|
70
|
+
const queries = redisClient.multi();
|
|
71
|
+
|
|
72
|
+
batchRules.map((rule) => {
|
|
73
|
+
const ruleKey = getAccessRuleRedisKey(rule);
|
|
74
|
+
const ruleValue = getRedisRuleValue(rule);
|
|
75
|
+
|
|
76
|
+
queries.hSet(ruleKey, ruleValue);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await queries.exec();
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
beforeAll(async () => {
|
|
84
|
+
redisConnection = createTestRedisConnection();
|
|
85
|
+
|
|
86
|
+
redisClient = await setupRedisIndex(
|
|
87
|
+
redisConnection,
|
|
88
|
+
accessRulesRedisIndex,
|
|
89
|
+
mockLogger,
|
|
90
|
+
).getClient();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Move cleanup to afterEach
|
|
94
|
+
afterEach(async () => {
|
|
95
|
+
if (indexName) {
|
|
96
|
+
try {
|
|
97
|
+
// Drop index and all documents (DD) created by THIS specific test
|
|
98
|
+
await redisClient.ft.dropIndex(indexName, { DD: true });
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.error(`Failed to cleanup index ${indexName}`, e);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}, 120_000);
|
|
104
|
+
|
|
105
|
+
beforeEach(async () => {
|
|
106
|
+
indexName = randomAsHex(16);
|
|
107
|
+
|
|
108
|
+
const result = setupRedisIndex(
|
|
109
|
+
redisConnection,
|
|
110
|
+
{ ...accessRulesRedisIndex, name: indexName },
|
|
111
|
+
mockLogger,
|
|
112
|
+
);
|
|
113
|
+
redisClient = await result.getClient();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe(
|
|
117
|
+
"writer",
|
|
118
|
+
() => {
|
|
119
|
+
let accessRulesWriter: AccessRulesWriter;
|
|
120
|
+
|
|
121
|
+
beforeAll(() => {
|
|
122
|
+
accessRulesWriter = new RedisRulesWriter(redisClient, mockLogger);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("inserts rule", async () => {
|
|
126
|
+
const testIndexName = indexName;
|
|
127
|
+
// given
|
|
128
|
+
const accessRule: AccessRule = {
|
|
129
|
+
type: AccessPolicyType.Block,
|
|
130
|
+
clientId: "clientId",
|
|
131
|
+
};
|
|
132
|
+
const accessRuleKey = getAccessRuleRedisKey(accessRule);
|
|
133
|
+
|
|
134
|
+
// when
|
|
135
|
+
await accessRulesWriter.insertRules([
|
|
136
|
+
{
|
|
137
|
+
rule: accessRule,
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
// then
|
|
142
|
+
const insertedAccessRule = await redisClient.hGetAll(accessRuleKey);
|
|
143
|
+
const indexRecordsCount = await getIndexRecordsCount(testIndexName);
|
|
144
|
+
|
|
145
|
+
expect(insertedAccessRule).toEqual(accessRule);
|
|
146
|
+
expect(indexRecordsCount).toEqual(1);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("inserts time limited rule", async () => {
|
|
150
|
+
// given
|
|
151
|
+
const accessRule: AccessRule = {
|
|
152
|
+
type: AccessPolicyType.Block,
|
|
153
|
+
clientId: "clientId",
|
|
154
|
+
};
|
|
155
|
+
const accessRuleKey = getAccessRuleRedisKey(accessRule);
|
|
156
|
+
// 1 hour from now.
|
|
157
|
+
const expireIn = 60 * 60; // seconds
|
|
158
|
+
const expirationTimestamp = new Date(
|
|
159
|
+
Date.now() + expireIn * 1000,
|
|
160
|
+
).getTime();
|
|
161
|
+
const expirationTimestampInSeconds = Math.floor(
|
|
162
|
+
expirationTimestamp / 1000,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// when
|
|
166
|
+
await accessRulesWriter.insertRules([
|
|
167
|
+
{
|
|
168
|
+
rule: accessRule,
|
|
169
|
+
expiresUnixTimestamp: expirationTimestampInSeconds,
|
|
170
|
+
},
|
|
171
|
+
]);
|
|
172
|
+
const ruleKey = getAccessRuleRedisKey(accessRule);
|
|
173
|
+
// then
|
|
174
|
+
const insertedAccessRule = await redisClient.hGetAll(accessRuleKey);
|
|
175
|
+
const insertedExpirationResult = await redisClient.expireAt(
|
|
176
|
+
ruleKey,
|
|
177
|
+
expirationTimestampInSeconds,
|
|
178
|
+
);
|
|
179
|
+
const indexRecordsCount = await getIndexRecordsCount(indexName);
|
|
180
|
+
|
|
181
|
+
const recordExpirySeconds = await redisClient.ttl(ruleKey);
|
|
182
|
+
|
|
183
|
+
expect(insertedAccessRule).toEqual(accessRule);
|
|
184
|
+
expect(insertedExpirationResult).toBe(1);
|
|
185
|
+
expect(recordExpirySeconds).toBeLessThanOrEqual(
|
|
186
|
+
expirationTimestampInSeconds,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(indexRecordsCount).toBe(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("deletes rules", async () => {
|
|
193
|
+
// given
|
|
194
|
+
const johnAccessRule: AccessRule = {
|
|
195
|
+
type: AccessPolicyType.Block,
|
|
196
|
+
clientId: getUniqueString(),
|
|
197
|
+
};
|
|
198
|
+
const johnAccessRuleKey = getAccessRuleRedisKey(johnAccessRule);
|
|
199
|
+
|
|
200
|
+
const doeAccessRule: AccessRule = {
|
|
201
|
+
type: AccessPolicyType.Block,
|
|
202
|
+
clientId: getUniqueString(),
|
|
203
|
+
};
|
|
204
|
+
const doeAccessRuleKey = getAccessRuleRedisKey(doeAccessRule);
|
|
205
|
+
|
|
206
|
+
await insertRules([johnAccessRule, doeAccessRule]);
|
|
207
|
+
|
|
208
|
+
// when
|
|
209
|
+
await accessRulesWriter.deleteRules([
|
|
210
|
+
johnAccessRuleKey.slice(ACCESS_RULE_REDIS_KEY_PREFIX.length),
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
// then
|
|
214
|
+
const presentAccessRule = await redisClient.hGetAll(doeAccessRuleKey);
|
|
215
|
+
const indexRecordsCount = await getIndexRecordsCount(indexName);
|
|
216
|
+
|
|
217
|
+
expect(presentAccessRule).toEqual(doeAccessRule);
|
|
218
|
+
expect(indexRecordsCount).toBe(1);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("deletes all rules", async () => {
|
|
222
|
+
// given
|
|
223
|
+
const johnAccessRule: AccessRule = {
|
|
224
|
+
type: AccessPolicyType.Block,
|
|
225
|
+
clientId: getUniqueString(),
|
|
226
|
+
};
|
|
227
|
+
const doeAccessRule: AccessRule = {
|
|
228
|
+
type: AccessPolicyType.Block,
|
|
229
|
+
clientId: getUniqueString(),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
await insertRules([johnAccessRule, doeAccessRule]);
|
|
233
|
+
|
|
234
|
+
// when
|
|
235
|
+
await accessRulesWriter.deleteAllRules();
|
|
236
|
+
|
|
237
|
+
// then
|
|
238
|
+
const indexRecordsCount = await getIndexRecordsCount(indexName);
|
|
239
|
+
|
|
240
|
+
expect(indexRecordsCount).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("deletes all rules when there are 1 million rules", async () => {
|
|
244
|
+
// given
|
|
245
|
+
const rulesCount = 1_000_000;
|
|
246
|
+
const batchSize = 10_000;
|
|
247
|
+
const numBatches = Math.ceil(rulesCount / batchSize);
|
|
248
|
+
|
|
249
|
+
// Insert rules in batches to avoid memory exhaustion
|
|
250
|
+
// Don't create 1M objects in memory at once!
|
|
251
|
+
for (let i = 0; i < numBatches; i++) {
|
|
252
|
+
const currentBatchSize = Math.min(
|
|
253
|
+
batchSize,
|
|
254
|
+
rulesCount - i * batchSize,
|
|
255
|
+
);
|
|
256
|
+
const batchRules: AccessRule[] = Array.from(
|
|
257
|
+
{ length: currentBatchSize },
|
|
258
|
+
() => ({
|
|
259
|
+
type: AccessPolicyType.Block,
|
|
260
|
+
clientId: getUniqueString(),
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
await insertRules(batchRules);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// verify that there are 1 million rules in the database
|
|
268
|
+
const beforeDeleteIndexRecordsCount =
|
|
269
|
+
await getIndexRecordsCount(indexName);
|
|
270
|
+
expect(beforeDeleteIndexRecordsCount).toBe(rulesCount);
|
|
271
|
+
|
|
272
|
+
// when
|
|
273
|
+
await accessRulesWriter.deleteAllRules();
|
|
274
|
+
|
|
275
|
+
// then
|
|
276
|
+
const afterDeleteIndexRecordsCount =
|
|
277
|
+
await getIndexRecordsCount(indexName);
|
|
278
|
+
|
|
279
|
+
expect(afterDeleteIndexRecordsCount).toBe(0);
|
|
280
|
+
});
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
timeout: 240_000,
|
|
284
|
+
},
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
describe("reader", () => {
|
|
288
|
+
let accessRulesReader: AccessRulesReader;
|
|
289
|
+
|
|
290
|
+
beforeAll(async () => {
|
|
291
|
+
accessRulesReader = new RedisRulesReader(
|
|
292
|
+
redisClient,
|
|
293
|
+
getLogger(LogLevel.enum.info, "RedisAccessRulesReader"),
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("finds client and global rules by greedy policy scope match", async () => {
|
|
298
|
+
// given
|
|
299
|
+
const johnId = getUniqueString();
|
|
300
|
+
const johnAccessRule: AccessRule = {
|
|
301
|
+
type: AccessPolicyType.Block,
|
|
302
|
+
clientId: johnId,
|
|
303
|
+
};
|
|
304
|
+
const doeAccessRule: AccessRule = {
|
|
305
|
+
type: AccessPolicyType.Block,
|
|
306
|
+
clientId: getUniqueString(),
|
|
307
|
+
};
|
|
308
|
+
const globalAccessRule: AccessRule = {
|
|
309
|
+
type: AccessPolicyType.Block,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
await insertRules([johnAccessRule, doeAccessRule, globalAccessRule]);
|
|
313
|
+
|
|
314
|
+
// when
|
|
315
|
+
const foundByClientAccessRules = await accessRulesReader.findRules({
|
|
316
|
+
policyScope: {
|
|
317
|
+
clientId: johnId,
|
|
318
|
+
},
|
|
319
|
+
policyScopeMatch: FilterScopeMatch.Greedy,
|
|
320
|
+
});
|
|
321
|
+
const foundAccessRules = await accessRulesReader.findRules({
|
|
322
|
+
policyScopeMatch: FilterScopeMatch.Greedy,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// then
|
|
326
|
+
const indexRecordsCount = await getIndexRecordsCount(indexName);
|
|
327
|
+
|
|
328
|
+
expect(indexRecordsCount).toBe(3);
|
|
329
|
+
expect(foundByClientAccessRules).toEqual([
|
|
330
|
+
johnAccessRule,
|
|
331
|
+
globalAccessRule,
|
|
332
|
+
]);
|
|
333
|
+
expect(foundAccessRules).toEqual([
|
|
334
|
+
johnAccessRule,
|
|
335
|
+
doeAccessRule,
|
|
336
|
+
globalAccessRule,
|
|
337
|
+
]);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("finds client or global rules by exact policy scope match", async () => {
|
|
341
|
+
// given
|
|
342
|
+
const johnId = getUniqueString();
|
|
343
|
+
|
|
344
|
+
const johnAccessRule: AccessRule = {
|
|
345
|
+
type: AccessPolicyType.Block,
|
|
346
|
+
clientId: johnId,
|
|
347
|
+
};
|
|
348
|
+
const doeAccessRule: AccessRule = {
|
|
349
|
+
type: AccessPolicyType.Block,
|
|
350
|
+
clientId: getUniqueString(),
|
|
351
|
+
};
|
|
352
|
+
const globalAccessRule: AccessRule = {
|
|
353
|
+
type: AccessPolicyType.Block,
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
await insertRules([johnAccessRule, doeAccessRule, globalAccessRule]);
|
|
357
|
+
|
|
358
|
+
// when
|
|
359
|
+
const foundClientAccessRules = await accessRulesReader.findRules({
|
|
360
|
+
policyScope: {
|
|
361
|
+
clientId: johnId,
|
|
362
|
+
},
|
|
363
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
364
|
+
});
|
|
365
|
+
const foundGlobalAccessRules = await accessRulesReader.findRules(
|
|
366
|
+
{
|
|
367
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
368
|
+
},
|
|
369
|
+
false,
|
|
370
|
+
false,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// then
|
|
374
|
+
const indexRecordsCount = await getIndexRecordsCount(indexName);
|
|
375
|
+
|
|
376
|
+
expect(indexRecordsCount).toBe(3);
|
|
377
|
+
expect(foundClientAccessRules).toEqual([johnAccessRule]);
|
|
378
|
+
expect(foundGlobalAccessRules).toEqual([globalAccessRule]);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("finds rules by greedy user scope match", async () => {
|
|
382
|
+
// given
|
|
383
|
+
|
|
384
|
+
const johnId = getUniqueString();
|
|
385
|
+
const johnIpAccessRule: AccessRule = {
|
|
386
|
+
type: AccessPolicyType.Block,
|
|
387
|
+
clientId: johnId,
|
|
388
|
+
numericIp: BigInt(100),
|
|
389
|
+
};
|
|
390
|
+
const doeIpAccessRule: AccessRule = {
|
|
391
|
+
type: AccessPolicyType.Block,
|
|
392
|
+
clientId: getUniqueString(),
|
|
393
|
+
numericIp: BigInt(100),
|
|
394
|
+
};
|
|
395
|
+
const johnHeaderAccessRule: AccessRule = {
|
|
396
|
+
type: AccessPolicyType.Block,
|
|
397
|
+
headersHash:
|
|
398
|
+
"00110110100001111101001101100101101101001000011111010011011001010101000110000111110100110110010111010100100011011101101010000011",
|
|
399
|
+
};
|
|
400
|
+
const globalJa4AccessRule: AccessRule = {
|
|
401
|
+
type: AccessPolicyType.Block,
|
|
402
|
+
ja4Hash: "windows",
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
await insertRules([
|
|
406
|
+
johnIpAccessRule,
|
|
407
|
+
doeIpAccessRule,
|
|
408
|
+
johnHeaderAccessRule,
|
|
409
|
+
globalJa4AccessRule,
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
// when
|
|
413
|
+
const foundAccessRules = await accessRulesReader.findRules({
|
|
414
|
+
policyScope: {
|
|
415
|
+
clientId: johnId,
|
|
416
|
+
},
|
|
417
|
+
userScope: {
|
|
418
|
+
numericIp: BigInt(100),
|
|
419
|
+
ja4Hash: "windows",
|
|
420
|
+
},
|
|
421
|
+
userScopeMatch: FilterScopeMatch.Greedy,
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// then
|
|
425
|
+
const indexRecordsCount = await getIndexRecordsCount(indexName);
|
|
426
|
+
|
|
427
|
+
expect(indexRecordsCount).toBe(4);
|
|
428
|
+
expect(foundAccessRules).toEqual([johnIpAccessRule, globalJa4AccessRule]);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("finds rules by exact user scope match", async () => {
|
|
432
|
+
// given
|
|
433
|
+
|
|
434
|
+
const johnId = getUniqueString();
|
|
435
|
+
|
|
436
|
+
const johnTargetAccessRule: AccessRule = {
|
|
437
|
+
type: AccessPolicyType.Block,
|
|
438
|
+
clientId: johnId,
|
|
439
|
+
numericIp: BigInt(100),
|
|
440
|
+
ja4Hash: "windows",
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const doeTargetAccessRule: AccessRule = {
|
|
444
|
+
type: AccessPolicyType.Block,
|
|
445
|
+
clientId: getUniqueString(),
|
|
446
|
+
numericIp: BigInt(100),
|
|
447
|
+
ja4Hash: "windows",
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const johnHeaderAccessRule: AccessRule = {
|
|
451
|
+
type: AccessPolicyType.Block,
|
|
452
|
+
headersHash:
|
|
453
|
+
"00110110100001111101001101100101101101001000011111010011011001010101000110000111110100110110010111010100100011011101101010000011",
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const globalTargetAccessRule: AccessRule = {
|
|
457
|
+
type: AccessPolicyType.Block,
|
|
458
|
+
numericIp: BigInt(100),
|
|
459
|
+
ja4Hash: "windows",
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const globalJa4AccessRule: AccessRule = {
|
|
463
|
+
type: AccessPolicyType.Block,
|
|
464
|
+
ja4Hash: "windows",
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
await insertRules([
|
|
468
|
+
johnTargetAccessRule,
|
|
469
|
+
doeTargetAccessRule,
|
|
470
|
+
johnHeaderAccessRule,
|
|
471
|
+
globalTargetAccessRule,
|
|
472
|
+
globalJa4AccessRule,
|
|
473
|
+
]);
|
|
474
|
+
|
|
475
|
+
// when
|
|
476
|
+
const foundAccessRules = await accessRulesReader.findRules({
|
|
477
|
+
policyScope: {
|
|
478
|
+
clientId: johnId,
|
|
479
|
+
},
|
|
480
|
+
userScope: {
|
|
481
|
+
numericIp: BigInt(100),
|
|
482
|
+
ja4Hash: "windows",
|
|
483
|
+
},
|
|
484
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// then
|
|
488
|
+
const indexRecordsCount = await getIndexRecordsCount(indexName);
|
|
489
|
+
|
|
490
|
+
expect(indexRecordsCount).toBe(5);
|
|
491
|
+
expect(foundAccessRules).toEqual([
|
|
492
|
+
johnTargetAccessRule,
|
|
493
|
+
globalTargetAccessRule,
|
|
494
|
+
]);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test("finds rules by greedy ip match", async () => {
|
|
498
|
+
// given
|
|
499
|
+
const johnId = getUniqueString();
|
|
500
|
+
|
|
501
|
+
const johnIpMask_0_100_AccessRule: AccessRule = {
|
|
502
|
+
clientId: johnId,
|
|
503
|
+
type: AccessPolicyType.Block,
|
|
504
|
+
numericIpMaskMin: BigInt(0),
|
|
505
|
+
numericIpMaskMax: BigInt(100),
|
|
506
|
+
};
|
|
507
|
+
const johnIp_100_AccessRule: AccessRule = {
|
|
508
|
+
clientId: johnId,
|
|
509
|
+
type: AccessPolicyType.Block,
|
|
510
|
+
numericIp: BigInt(100),
|
|
511
|
+
};
|
|
512
|
+
const globalIpMask_100_200_AccessRule: AccessRule = {
|
|
513
|
+
type: AccessPolicyType.Block,
|
|
514
|
+
numericIpMaskMin: BigInt(100),
|
|
515
|
+
numericIpMaskMax: BigInt(200),
|
|
516
|
+
};
|
|
517
|
+
const doeIpMask_200_300AccessRule: AccessRule = {
|
|
518
|
+
clientId: getUniqueString(),
|
|
519
|
+
type: AccessPolicyType.Block,
|
|
520
|
+
numericIpMaskMin: BigInt(200),
|
|
521
|
+
numericIpMaskMax: BigInt(300),
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
await insertRules([
|
|
525
|
+
johnIpMask_0_100_AccessRule,
|
|
526
|
+
johnIp_100_AccessRule,
|
|
527
|
+
globalIpMask_100_200_AccessRule,
|
|
528
|
+
doeIpMask_200_300AccessRule,
|
|
529
|
+
]);
|
|
530
|
+
|
|
531
|
+
// when
|
|
532
|
+
const ip_0_accessRules = await accessRulesReader.findRules({
|
|
533
|
+
policyScope: {
|
|
534
|
+
clientId: johnId,
|
|
535
|
+
},
|
|
536
|
+
userScope: {
|
|
537
|
+
numericIp: BigInt(0),
|
|
538
|
+
},
|
|
539
|
+
userScopeMatch: FilterScopeMatch.Greedy,
|
|
540
|
+
});
|
|
541
|
+
const ip_99_accessRules = await accessRulesReader.findRules({
|
|
542
|
+
policyScope: {
|
|
543
|
+
clientId: johnId,
|
|
544
|
+
},
|
|
545
|
+
userScope: {
|
|
546
|
+
numericIp: BigInt(99),
|
|
547
|
+
},
|
|
548
|
+
userScopeMatch: FilterScopeMatch.Greedy,
|
|
549
|
+
});
|
|
550
|
+
const ip_100_accessRules = await accessRulesReader.findRules({
|
|
551
|
+
policyScope: {
|
|
552
|
+
clientId: johnId,
|
|
553
|
+
},
|
|
554
|
+
userScope: {
|
|
555
|
+
numericIp: BigInt(100),
|
|
556
|
+
},
|
|
557
|
+
userScopeMatch: FilterScopeMatch.Greedy,
|
|
558
|
+
});
|
|
559
|
+
const ip_101_accessRules = await accessRulesReader.findRules({
|
|
560
|
+
policyScope: {
|
|
561
|
+
clientId: johnId,
|
|
562
|
+
},
|
|
563
|
+
userScope: {
|
|
564
|
+
numericIp: BigInt(101),
|
|
565
|
+
},
|
|
566
|
+
userScopeMatch: FilterScopeMatch.Greedy,
|
|
567
|
+
});
|
|
568
|
+
const ip_201_accessRules = await accessRulesReader.findRules({
|
|
569
|
+
policyScope: {
|
|
570
|
+
clientId: johnId,
|
|
571
|
+
},
|
|
572
|
+
userScope: {
|
|
573
|
+
numericIp: BigInt(201),
|
|
574
|
+
},
|
|
575
|
+
userScopeMatch: FilterScopeMatch.Greedy,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// then
|
|
579
|
+
const indexRecordsCount = await getIndexRecordsCount(indexName);
|
|
580
|
+
|
|
581
|
+
expect(indexRecordsCount).toBe(4);
|
|
582
|
+
|
|
583
|
+
expect(ip_0_accessRules).toEqual([johnIpMask_0_100_AccessRule]);
|
|
584
|
+
expect(ip_99_accessRules).toEqual([johnIpMask_0_100_AccessRule]);
|
|
585
|
+
expect(ip_100_accessRules).toEqual([
|
|
586
|
+
johnIpMask_0_100_AccessRule,
|
|
587
|
+
johnIp_100_AccessRule,
|
|
588
|
+
globalIpMask_100_200_AccessRule,
|
|
589
|
+
]);
|
|
590
|
+
expect(ip_101_accessRules).toEqual([globalIpMask_100_200_AccessRule]);
|
|
591
|
+
expect(ip_201_accessRules).toEqual([]);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("finds rules by exact ip match with exact policy match", async () => {
|
|
595
|
+
// given
|
|
596
|
+
const johnId = getUniqueString();
|
|
597
|
+
|
|
598
|
+
const johnIpMask_0_100_AccessRule: AccessRule = {
|
|
599
|
+
clientId: johnId,
|
|
600
|
+
type: AccessPolicyType.Block,
|
|
601
|
+
numericIpMaskMin: BigInt(0),
|
|
602
|
+
numericIpMaskMax: BigInt(100),
|
|
603
|
+
};
|
|
604
|
+
const johnIp_100_AccessRule: AccessRule = {
|
|
605
|
+
clientId: johnId,
|
|
606
|
+
type: AccessPolicyType.Block,
|
|
607
|
+
numericIp: BigInt(100),
|
|
608
|
+
};
|
|
609
|
+
const globalIpMask_100_200_AccessRule: AccessRule = {
|
|
610
|
+
type: AccessPolicyType.Block,
|
|
611
|
+
numericIpMaskMin: BigInt(100),
|
|
612
|
+
numericIpMaskMax: BigInt(200),
|
|
613
|
+
};
|
|
614
|
+
const globalIp_100_AccessRule: AccessRule = {
|
|
615
|
+
type: AccessPolicyType.Block,
|
|
616
|
+
numericIp: BigInt(100),
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
await insertRules([
|
|
620
|
+
johnIpMask_0_100_AccessRule,
|
|
621
|
+
johnIp_100_AccessRule,
|
|
622
|
+
globalIpMask_100_200_AccessRule,
|
|
623
|
+
globalIp_100_AccessRule,
|
|
624
|
+
]);
|
|
625
|
+
|
|
626
|
+
// when
|
|
627
|
+
const ip_0_accessRules = await accessRulesReader.findRules({
|
|
628
|
+
policyScope: {
|
|
629
|
+
clientId: johnId,
|
|
630
|
+
},
|
|
631
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
632
|
+
userScope: {
|
|
633
|
+
numericIp: BigInt(0),
|
|
634
|
+
},
|
|
635
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
636
|
+
});
|
|
637
|
+
const ip_100_accessRules = await accessRulesReader.findRules({
|
|
638
|
+
policyScope: {
|
|
639
|
+
clientId: johnId,
|
|
640
|
+
},
|
|
641
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
642
|
+
userScope: {
|
|
643
|
+
numericIp: BigInt(100),
|
|
644
|
+
},
|
|
645
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// then
|
|
649
|
+
const indexRecordsCount = await getIndexRecordsCount(indexName);
|
|
650
|
+
|
|
651
|
+
expect(indexRecordsCount).toBe(4);
|
|
652
|
+
|
|
653
|
+
expect(ip_0_accessRules).toEqual([johnIpMask_0_100_AccessRule]);
|
|
654
|
+
expect(ip_100_accessRules).toEqual([
|
|
655
|
+
johnIpMask_0_100_AccessRule,
|
|
656
|
+
johnIp_100_AccessRule,
|
|
657
|
+
]);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test("finds rules by exact ip match with exact policy match 2", async () => {
|
|
661
|
+
// given
|
|
662
|
+
const johnId = getUniqueString();
|
|
663
|
+
const johnIp = BigInt(100);
|
|
664
|
+
|
|
665
|
+
const johnIpMask_0_100_AccessRule: AccessRule = {
|
|
666
|
+
clientId: johnId,
|
|
667
|
+
type: AccessPolicyType.Block,
|
|
668
|
+
numericIpMaskMin: BigInt(0),
|
|
669
|
+
numericIpMaskMax: johnIp,
|
|
670
|
+
};
|
|
671
|
+
const johnIp_100_AccessRule: AccessRule = {
|
|
672
|
+
clientId: johnId,
|
|
673
|
+
type: AccessPolicyType.Block,
|
|
674
|
+
numericIp: johnIp,
|
|
675
|
+
};
|
|
676
|
+
const globalIpMask_100_200_AccessRule: AccessRule = {
|
|
677
|
+
type: AccessPolicyType.Block,
|
|
678
|
+
numericIpMaskMin: johnIp,
|
|
679
|
+
numericIpMaskMax: BigInt(200),
|
|
680
|
+
};
|
|
681
|
+
const globalIp_100_AccessRule: AccessRule = {
|
|
682
|
+
type: AccessPolicyType.Block,
|
|
683
|
+
numericIp: johnIp,
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
await insertRules([
|
|
687
|
+
johnIpMask_0_100_AccessRule,
|
|
688
|
+
johnIp_100_AccessRule,
|
|
689
|
+
globalIpMask_100_200_AccessRule,
|
|
690
|
+
globalIp_100_AccessRule,
|
|
691
|
+
]);
|
|
692
|
+
|
|
693
|
+
// when
|
|
694
|
+
const ip_0_accessRules = await accessRulesReader.findRules({
|
|
695
|
+
policyScope: {
|
|
696
|
+
clientId: johnId,
|
|
697
|
+
},
|
|
698
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
699
|
+
userScope: {
|
|
700
|
+
numericIp: BigInt(0),
|
|
701
|
+
},
|
|
702
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
703
|
+
});
|
|
704
|
+
const ip_100_accessRules = await accessRulesReader.findRules({
|
|
705
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
706
|
+
userScope: {
|
|
707
|
+
numericIp: johnIp,
|
|
708
|
+
},
|
|
709
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// then
|
|
713
|
+
const indexRecordsCount = await getIndexRecordsCount(indexName);
|
|
714
|
+
|
|
715
|
+
expect(indexRecordsCount).toBe(4);
|
|
716
|
+
|
|
717
|
+
expect(ip_0_accessRules).toEqual([johnIpMask_0_100_AccessRule]);
|
|
718
|
+
expect(ip_100_accessRules).toEqual([
|
|
719
|
+
globalIpMask_100_200_AccessRule,
|
|
720
|
+
globalIp_100_AccessRule,
|
|
721
|
+
]);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
test("does not find rules when some criteria do not match and user scope match is exact", async () => {
|
|
725
|
+
// given
|
|
726
|
+
const accessRule: AccessRule = {
|
|
727
|
+
type: AccessPolicyType.Restrict,
|
|
728
|
+
clientId: "clientId",
|
|
729
|
+
userAgentHash: "userAgentHash1",
|
|
730
|
+
ja4Hash: "ja4Hash",
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// when
|
|
734
|
+
await insertRules([accessRule]);
|
|
735
|
+
|
|
736
|
+
// then
|
|
737
|
+
const query = {
|
|
738
|
+
policyScope: {
|
|
739
|
+
clientId: "clientId",
|
|
740
|
+
},
|
|
741
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
742
|
+
userScope: {
|
|
743
|
+
ja4Hash: "ja4Hash",
|
|
744
|
+
userAgentHash: "userAgentHash2",
|
|
745
|
+
},
|
|
746
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const foundAccessRules = await accessRulesReader.findRules(query);
|
|
750
|
+
expect(foundAccessRules).toEqual([]);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test("does not find rules when query is more exact than rule and user scope match is exact", async () => {
|
|
754
|
+
// given
|
|
755
|
+
const accessRule: AccessRule = {
|
|
756
|
+
type: AccessPolicyType.Restrict,
|
|
757
|
+
clientId: "clientId",
|
|
758
|
+
ja4Hash: "ja4Hash",
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// when
|
|
762
|
+
await insertRules([accessRule]);
|
|
763
|
+
|
|
764
|
+
// then
|
|
765
|
+
const query = {
|
|
766
|
+
policyScope: {
|
|
767
|
+
clientId: "clientId",
|
|
768
|
+
},
|
|
769
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
770
|
+
userScope: {
|
|
771
|
+
ja4Hash: "ja4Hash",
|
|
772
|
+
userAgentHash: "userAgentHash",
|
|
773
|
+
},
|
|
774
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const foundAccessRules = await accessRulesReader.findRules(query);
|
|
778
|
+
expect(foundAccessRules).toEqual([]);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test("does find rules when query is more exact than rule and user scope match is greedy", async () => {
|
|
782
|
+
// given
|
|
783
|
+
const accessRule: AccessRule = {
|
|
784
|
+
type: AccessPolicyType.Restrict,
|
|
785
|
+
clientId: "clientId",
|
|
786
|
+
ja4Hash: "ja4Hash",
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
// when
|
|
790
|
+
await insertRules([accessRule]);
|
|
791
|
+
|
|
792
|
+
// then
|
|
793
|
+
const query = {
|
|
794
|
+
policyScope: {
|
|
795
|
+
clientId: "clientId",
|
|
796
|
+
},
|
|
797
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
798
|
+
userScope: {
|
|
799
|
+
ja4Hash: "ja4Hash",
|
|
800
|
+
userAgentHash: "userAgentHash",
|
|
801
|
+
},
|
|
802
|
+
userScopeMatch: FilterScopeMatch.Greedy,
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
const foundAccessRules = await accessRulesReader.findRules(query);
|
|
806
|
+
expect(foundAccessRules).toEqual([accessRule]);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test("does find rules when some criteria do not match and user scope match is exact", async () => {
|
|
810
|
+
// given
|
|
811
|
+
const accessRule: AccessRule = {
|
|
812
|
+
type: AccessPolicyType.Restrict,
|
|
813
|
+
clientId: "clientId",
|
|
814
|
+
userAgentHash: "userAgentHash",
|
|
815
|
+
ja4Hash: "ja4Hash",
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// when
|
|
819
|
+
getAccessRuleRedisKey(accessRule);
|
|
820
|
+
await insertRules([accessRule]);
|
|
821
|
+
|
|
822
|
+
// then
|
|
823
|
+
const query = {
|
|
824
|
+
policyScope: {
|
|
825
|
+
clientId: "clientId",
|
|
826
|
+
},
|
|
827
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
828
|
+
userScope: {
|
|
829
|
+
ja4Hash: "ja4Hash",
|
|
830
|
+
userAgentHash: "userAgentHash",
|
|
831
|
+
},
|
|
832
|
+
userScopeMatch: FilterScopeMatch.Greedy,
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
const foundAccessRules = await accessRulesReader.findRules(query);
|
|
836
|
+
expect(foundAccessRules).toEqual([accessRule]);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
test("if an ip rule and an ip mask rule is applied for a single IP at client level, both rules are returned with the more specific IP rule being first in the list", async () => {
|
|
840
|
+
// given
|
|
841
|
+
const accessRule: AccessRule = {
|
|
842
|
+
type: AccessPolicyType.Restrict,
|
|
843
|
+
clientId: "clientId",
|
|
844
|
+
numericIp: BigInt(10),
|
|
845
|
+
};
|
|
846
|
+
const ipRangeAccessRule: AccessRule = {
|
|
847
|
+
type: AccessPolicyType.Restrict,
|
|
848
|
+
clientId: "clientId",
|
|
849
|
+
numericIpMaskMin: BigInt(10),
|
|
850
|
+
numericIpMaskMax: BigInt(20),
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
// when
|
|
854
|
+
await insertRules([accessRule, ipRangeAccessRule]);
|
|
855
|
+
|
|
856
|
+
// then
|
|
857
|
+
|
|
858
|
+
const query = {
|
|
859
|
+
policyScope: {
|
|
860
|
+
clientId: "clientId",
|
|
861
|
+
},
|
|
862
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
863
|
+
userScope: {
|
|
864
|
+
numericIp: BigInt(10),
|
|
865
|
+
},
|
|
866
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
const foundAccessRules = await accessRulesReader.findRules(query);
|
|
870
|
+
expect(foundAccessRules).toEqual([accessRule, ipRangeAccessRule]);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
test("finds rules by headHash with exact match", async () => {
|
|
874
|
+
// given
|
|
875
|
+
const headHash1 = "abc123def456";
|
|
876
|
+
const headHash2 = "xyz789ghi012";
|
|
877
|
+
|
|
878
|
+
const headHashAccessRule: AccessRule = {
|
|
879
|
+
type: AccessPolicyType.Block,
|
|
880
|
+
clientId: "clientId",
|
|
881
|
+
headHash: headHash1,
|
|
882
|
+
};
|
|
883
|
+
const otherHeadHashAccessRule: AccessRule = {
|
|
884
|
+
type: AccessPolicyType.Block,
|
|
885
|
+
clientId: "clientId",
|
|
886
|
+
headHash: headHash2,
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
await insertRules([headHashAccessRule, otherHeadHashAccessRule]);
|
|
890
|
+
|
|
891
|
+
// when
|
|
892
|
+
const foundAccessRules = await accessRulesReader.findRules({
|
|
893
|
+
policyScope: {
|
|
894
|
+
clientId: "clientId",
|
|
895
|
+
},
|
|
896
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
897
|
+
userScope: {
|
|
898
|
+
headHash: headHash1,
|
|
899
|
+
},
|
|
900
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// then
|
|
904
|
+
expect(foundAccessRules).toEqual([headHashAccessRule]);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
test("finds rules by coords with exact match", async () => {
|
|
908
|
+
// given
|
|
909
|
+
const coords1 = "[[[100,200]]]";
|
|
910
|
+
const coords2 = "[[[300,400]]]";
|
|
911
|
+
|
|
912
|
+
const coordsAccessRule: AccessRule = {
|
|
913
|
+
type: AccessPolicyType.Block,
|
|
914
|
+
clientId: "clientId",
|
|
915
|
+
coords: coords1,
|
|
916
|
+
};
|
|
917
|
+
const otherCoordsAccessRule: AccessRule = {
|
|
918
|
+
type: AccessPolicyType.Block,
|
|
919
|
+
clientId: "clientId",
|
|
920
|
+
coords: coords2,
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
await insertRules([coordsAccessRule, otherCoordsAccessRule]);
|
|
924
|
+
|
|
925
|
+
// when
|
|
926
|
+
const foundAccessRules = await accessRulesReader.findRules({
|
|
927
|
+
policyScope: {
|
|
928
|
+
clientId: "clientId",
|
|
929
|
+
},
|
|
930
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
931
|
+
userScope: {
|
|
932
|
+
coords: coords1,
|
|
933
|
+
},
|
|
934
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
// then
|
|
938
|
+
expect(foundAccessRules).toEqual([coordsAccessRule]);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
test("finds rules by combined headHash and coords with exact match", async () => {
|
|
942
|
+
// given
|
|
943
|
+
const headHash1 = "abc123def456";
|
|
944
|
+
const coords1 = "[[[100,200]]]";
|
|
945
|
+
|
|
946
|
+
const combinedAccessRule: AccessRule = {
|
|
947
|
+
type: AccessPolicyType.Block,
|
|
948
|
+
clientId: "clientId",
|
|
949
|
+
headHash: headHash1,
|
|
950
|
+
coords: coords1,
|
|
951
|
+
};
|
|
952
|
+
const headHashOnlyAccessRule: AccessRule = {
|
|
953
|
+
type: AccessPolicyType.Restrict,
|
|
954
|
+
clientId: "clientId",
|
|
955
|
+
headHash: headHash1,
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
await insertRules([combinedAccessRule, headHashOnlyAccessRule]);
|
|
959
|
+
|
|
960
|
+
// when
|
|
961
|
+
const foundAccessRules = await accessRulesReader.findRules({
|
|
962
|
+
policyScope: {
|
|
963
|
+
clientId: "clientId",
|
|
964
|
+
},
|
|
965
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
966
|
+
userScope: {
|
|
967
|
+
headHash: headHash1,
|
|
968
|
+
coords: coords1,
|
|
969
|
+
},
|
|
970
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// then
|
|
974
|
+
expect(foundAccessRules).toEqual([combinedAccessRule]);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
test("finds rules by countryCode with exact match", async () => {
|
|
978
|
+
// given
|
|
979
|
+
const countryCode1 = "US";
|
|
980
|
+
const countryCode2 = "GB";
|
|
981
|
+
|
|
982
|
+
const countryCodeAccessRule: AccessRule = {
|
|
983
|
+
type: AccessPolicyType.Block,
|
|
984
|
+
clientId: "clientId",
|
|
985
|
+
countryCode: countryCode1,
|
|
986
|
+
};
|
|
987
|
+
const otherCountryCodeAccessRule: AccessRule = {
|
|
988
|
+
type: AccessPolicyType.Block,
|
|
989
|
+
clientId: "clientId",
|
|
990
|
+
countryCode: countryCode2,
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
await insertRules([countryCodeAccessRule, otherCountryCodeAccessRule]);
|
|
994
|
+
|
|
995
|
+
// when
|
|
996
|
+
const foundAccessRules = await accessRulesReader.findRules({
|
|
997
|
+
policyScope: {
|
|
998
|
+
clientId: "clientId",
|
|
999
|
+
},
|
|
1000
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
1001
|
+
userScope: {
|
|
1002
|
+
countryCode: countryCode1,
|
|
1003
|
+
},
|
|
1004
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
// then
|
|
1008
|
+
expect(foundAccessRules).toEqual([countryCodeAccessRule]);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
test("finds rules by combined countryCode and userId with exact match", async () => {
|
|
1012
|
+
// given
|
|
1013
|
+
const countryCode1 = "US";
|
|
1014
|
+
const userId1 = "user123";
|
|
1015
|
+
|
|
1016
|
+
const combinedAccessRule: AccessRule = {
|
|
1017
|
+
type: AccessPolicyType.Block,
|
|
1018
|
+
clientId: "clientId",
|
|
1019
|
+
countryCode: countryCode1,
|
|
1020
|
+
userId: userId1,
|
|
1021
|
+
};
|
|
1022
|
+
const countryCodeOnlyAccessRule: AccessRule = {
|
|
1023
|
+
type: AccessPolicyType.Restrict,
|
|
1024
|
+
clientId: "clientId",
|
|
1025
|
+
countryCode: countryCode1,
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
await insertRules([combinedAccessRule, countryCodeOnlyAccessRule]);
|
|
1029
|
+
|
|
1030
|
+
// when
|
|
1031
|
+
const foundAccessRules = await accessRulesReader.findRules({
|
|
1032
|
+
policyScope: {
|
|
1033
|
+
clientId: "clientId",
|
|
1034
|
+
},
|
|
1035
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
1036
|
+
userScope: {
|
|
1037
|
+
countryCode: countryCode1,
|
|
1038
|
+
userId: userId1,
|
|
1039
|
+
},
|
|
1040
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// then
|
|
1044
|
+
expect(foundAccessRules).toEqual([combinedAccessRule]);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
test("returns remaining rules when a matched document's hash key has been deleted", async () => {
|
|
1048
|
+
// given
|
|
1049
|
+
const clientId = getUniqueString();
|
|
1050
|
+
|
|
1051
|
+
const survivingRule: AccessRule = {
|
|
1052
|
+
type: AccessPolicyType.Block,
|
|
1053
|
+
clientId: clientId,
|
|
1054
|
+
userId: "user1",
|
|
1055
|
+
};
|
|
1056
|
+
const deletedRule: AccessRule = {
|
|
1057
|
+
type: AccessPolicyType.Block,
|
|
1058
|
+
clientId: clientId,
|
|
1059
|
+
userId: "user2",
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
await insertRules([survivingRule, deletedRule]);
|
|
1063
|
+
|
|
1064
|
+
// Delete the hash key for deletedRule directly, bypassing the writer.
|
|
1065
|
+
// The index may still reference it briefly, simulating the race
|
|
1066
|
+
// condition that caused the @redis/search documentValue(null) crash.
|
|
1067
|
+
const deletedRuleKey = getAccessRuleRedisKey(deletedRule);
|
|
1068
|
+
await redisClient.del(deletedRuleKey);
|
|
1069
|
+
|
|
1070
|
+
// when
|
|
1071
|
+
const foundAccessRules = await accessRulesReader.findRules({
|
|
1072
|
+
policyScope: {
|
|
1073
|
+
clientId: clientId,
|
|
1074
|
+
},
|
|
1075
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
1076
|
+
userScope: {
|
|
1077
|
+
userId: "user1",
|
|
1078
|
+
},
|
|
1079
|
+
userScopeMatch: FilterScopeMatch.Greedy,
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
// then - should not throw and should return the surviving rule
|
|
1083
|
+
expect(foundAccessRules).toEqual([survivingRule]);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
test("returns empty array without throwing when all matched documents have been deleted", async () => {
|
|
1087
|
+
// given
|
|
1088
|
+
const clientId = getUniqueString();
|
|
1089
|
+
|
|
1090
|
+
const rule: AccessRule = {
|
|
1091
|
+
type: AccessPolicyType.Block,
|
|
1092
|
+
clientId: clientId,
|
|
1093
|
+
userId: "user1",
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
await insertRules([rule]);
|
|
1097
|
+
|
|
1098
|
+
// Delete the hash key directly
|
|
1099
|
+
const ruleKey = getAccessRuleRedisKey(rule);
|
|
1100
|
+
await redisClient.del(ruleKey);
|
|
1101
|
+
|
|
1102
|
+
// when
|
|
1103
|
+
const foundAccessRules = await accessRulesReader.findRules({
|
|
1104
|
+
policyScope: {
|
|
1105
|
+
clientId: clientId,
|
|
1106
|
+
},
|
|
1107
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
1108
|
+
userScope: {
|
|
1109
|
+
userId: "user1",
|
|
1110
|
+
},
|
|
1111
|
+
userScopeMatch: FilterScopeMatch.Greedy,
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// then - should not throw, should return empty array
|
|
1115
|
+
expect(foundAccessRules).toEqual([]);
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
test("finds rules with matchingFieldsOnly when only userId is set and all IP fields are missing", async () => {
|
|
1119
|
+
// This is the exact scenario from the production error where the query
|
|
1120
|
+
// contained duplicate ismissing(@numericIpMaskMin) ismissing(@numericIpMaskMax)
|
|
1121
|
+
// given
|
|
1122
|
+
const clientId = getUniqueString();
|
|
1123
|
+
const userId = getUniqueString();
|
|
1124
|
+
|
|
1125
|
+
const accessRule: AccessRule = {
|
|
1126
|
+
type: AccessPolicyType.Block,
|
|
1127
|
+
clientId: clientId,
|
|
1128
|
+
userId: userId,
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
await insertRules([accessRule]);
|
|
1132
|
+
|
|
1133
|
+
// when - matchingFieldsOnly=true adds ismissing for all schema fields not in the scope
|
|
1134
|
+
const foundAccessRules = await accessRulesReader.findRules(
|
|
1135
|
+
{
|
|
1136
|
+
policyScope: {
|
|
1137
|
+
clientId: clientId,
|
|
1138
|
+
},
|
|
1139
|
+
policyScopeMatch: FilterScopeMatch.Exact,
|
|
1140
|
+
userScope: {
|
|
1141
|
+
userId: userId,
|
|
1142
|
+
},
|
|
1143
|
+
userScopeMatch: FilterScopeMatch.Exact,
|
|
1144
|
+
},
|
|
1145
|
+
true,
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
// then
|
|
1149
|
+
expect(foundAccessRules).toEqual([accessRule]);
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
afterAll(async () => {
|
|
1154
|
+
await redisClient.flushAll();
|
|
1155
|
+
});
|
|
1156
|
+
});
|