@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,85 @@
|
|
|
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 {
|
|
16
|
+
type ApiEndpoint,
|
|
17
|
+
type ApiEndpointResponse,
|
|
18
|
+
ApiEndpointResponseStatus,
|
|
19
|
+
} from "@prosopo/api-route";
|
|
20
|
+
import type { Logger } from "@prosopo/logger";
|
|
21
|
+
import type { AccessRulesStorage } from "#policy/rulesStorage.js";
|
|
22
|
+
|
|
23
|
+
export class RehashRulesEndpoint implements ApiEndpoint<undefined> {
|
|
24
|
+
public constructor(
|
|
25
|
+
private readonly accessRulesStorage: AccessRulesStorage,
|
|
26
|
+
private readonly logger: Logger,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
public getRequestArgsSchema(): undefined {}
|
|
30
|
+
|
|
31
|
+
async processRequest(logger?: Logger): Promise<ApiEndpointResponse> {
|
|
32
|
+
const log = logger ?? this.logger;
|
|
33
|
+
await this.accessRulesStorage.fetchAllRuleIds(async (ruleIds: string[]) => {
|
|
34
|
+
log.info(() => ({
|
|
35
|
+
msg: "Fetched rule ids batch",
|
|
36
|
+
data: {
|
|
37
|
+
count: ruleIds.length,
|
|
38
|
+
ruleIds,
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const ruleEntries = await this.accessRulesStorage.fetchRules(ruleIds);
|
|
43
|
+
|
|
44
|
+
log.info(() => ({
|
|
45
|
+
msg: "Fetched rules",
|
|
46
|
+
data: {
|
|
47
|
+
count: ruleEntries.length,
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
if (ruleEntries.length !== ruleIds.length) {
|
|
52
|
+
log.warn(() => ({
|
|
53
|
+
msg: "Fetched rules count is not equal to the requested count",
|
|
54
|
+
data: {
|
|
55
|
+
fetchedCount: ruleEntries.length,
|
|
56
|
+
requestedCount: ruleIds.length,
|
|
57
|
+
},
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await this.accessRulesStorage.deleteRules(ruleIds);
|
|
62
|
+
|
|
63
|
+
log.info(() => ({
|
|
64
|
+
msg: "Deleted rules",
|
|
65
|
+
data: {
|
|
66
|
+
count: ruleIds.length,
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
await this.accessRulesStorage.insertRules(ruleEntries);
|
|
71
|
+
|
|
72
|
+
log.info(() => ({
|
|
73
|
+
msg: "Inserted rules",
|
|
74
|
+
data: {
|
|
75
|
+
count: ruleEntries.length,
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
status: ApiEndpointResponseStatus.SUCCESS,
|
|
82
|
+
data: {},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
export { accessRuleMongooseSchema } from "./mongooseRuleSchema.js";
|
|
@@ -0,0 +1,65 @@
|
|
|
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 { AllKeys, Keys } from "@prosopo/common";
|
|
16
|
+
import type { SchemaDefinition } from "mongoose";
|
|
17
|
+
import type { AccessPolicy, PolicyScope } from "#policy/rule.js";
|
|
18
|
+
import type {
|
|
19
|
+
AccessRuleRecord,
|
|
20
|
+
UserAttributesRecord,
|
|
21
|
+
UserIpRecord,
|
|
22
|
+
UserScopeRecord,
|
|
23
|
+
} from "#policy/ruleRecord.js";
|
|
24
|
+
|
|
25
|
+
const userAttributesSchema: SchemaDefinition<UserAttributesRecord> = {
|
|
26
|
+
userId: { type: String, required: false },
|
|
27
|
+
ja4Hash: { type: String, required: false },
|
|
28
|
+
userAgent: { type: String, required: false },
|
|
29
|
+
headersHash: { type: String, required: false },
|
|
30
|
+
headHash: { type: String, required: false },
|
|
31
|
+
coords: { type: String, required: false },
|
|
32
|
+
countryCode: { type: String, required: false },
|
|
33
|
+
} satisfies AllKeys<UserAttributesRecord>;
|
|
34
|
+
|
|
35
|
+
const userIpSchema: SchemaDefinition<UserIpRecord> = {
|
|
36
|
+
ip: { type: String, required: false },
|
|
37
|
+
ipMask: { type: String, required: false },
|
|
38
|
+
} satisfies AllKeys<UserIpRecord>;
|
|
39
|
+
|
|
40
|
+
const userScopeSchema: SchemaDefinition<UserScopeRecord> = {
|
|
41
|
+
...userAttributesSchema,
|
|
42
|
+
...userIpSchema,
|
|
43
|
+
} satisfies Keys<UserScopeRecord>;
|
|
44
|
+
|
|
45
|
+
const policyScopeSchema: SchemaDefinition<PolicyScope> = {
|
|
46
|
+
clientId: { type: String, required: false },
|
|
47
|
+
} satisfies AllKeys<PolicyScope>;
|
|
48
|
+
|
|
49
|
+
const accessPolicySchema: SchemaDefinition<AccessPolicy> = {
|
|
50
|
+
type: { type: String, required: true },
|
|
51
|
+
captchaType: { type: String, required: false },
|
|
52
|
+
description: { type: String, required: false },
|
|
53
|
+
solvedImagesCount: { type: Number, required: false },
|
|
54
|
+
imageThreshold: { type: Number, required: false },
|
|
55
|
+
powDifficulty: { type: Number, required: false },
|
|
56
|
+
unsolvedImagesCount: { type: Number, required: false },
|
|
57
|
+
frictionlessScore: { type: Number, required: false },
|
|
58
|
+
} satisfies AllKeys<AccessPolicy>;
|
|
59
|
+
|
|
60
|
+
export const accessRuleMongooseSchema: SchemaDefinition<AccessRuleRecord> = {
|
|
61
|
+
...accessPolicySchema,
|
|
62
|
+
...policyScopeSchema,
|
|
63
|
+
...userScopeSchema,
|
|
64
|
+
ruleGroupId: { type: String, required: false },
|
|
65
|
+
} satisfies Keys<AccessRuleRecord>;
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
export { createRedisAccessRulesStorage } from "./redisRulesStorage.js";
|
|
16
|
+
|
|
17
|
+
export { accessRulesRedisIndex } from "./redisRuleIndex.js";
|
|
@@ -0,0 +1,103 @@
|
|
|
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 type { FtAggregateWithCursorOptions } from "@redis/search/dist/lib/commands/AGGREGATE_WITHCURSOR.js";
|
|
17
|
+
import type { RedisClientType } from "redis";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import { REDIS_QUERY_DIALECT } from "#policy/redis/reader/redisRulesQuery.js";
|
|
20
|
+
import {
|
|
21
|
+
REDIS_BATCH_SIZE,
|
|
22
|
+
parseRedisRecords,
|
|
23
|
+
} from "#policy/redis/redisClient.js";
|
|
24
|
+
import { ACCESS_RULES_REDIS_INDEX_NAME } from "#policy/redis/redisRuleIndex.js";
|
|
25
|
+
|
|
26
|
+
// aggregation is used for cases when we need to get "unlimited" search results
|
|
27
|
+
export const aggregateRedisKeys = async (
|
|
28
|
+
client: RedisClientType,
|
|
29
|
+
query: string,
|
|
30
|
+
logger: Logger,
|
|
31
|
+
batchHandler?: (keys: string[]) => Promise<void>,
|
|
32
|
+
): Promise<string[]> => {
|
|
33
|
+
const keyField = "__key";
|
|
34
|
+
|
|
35
|
+
const recordSchema = z.object({
|
|
36
|
+
// it's a reserved name for the record key
|
|
37
|
+
[keyField]: z.string(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const foundKeys: string[] = [];
|
|
41
|
+
|
|
42
|
+
const addRecordKeys = async (records: object[]) => {
|
|
43
|
+
const parsedRecords = parseRedisRecords(records, recordSchema, logger);
|
|
44
|
+
|
|
45
|
+
const recordKeys = parsedRecords.map((record) => record[keyField]);
|
|
46
|
+
|
|
47
|
+
if (batchHandler) {
|
|
48
|
+
await batchHandler(recordKeys);
|
|
49
|
+
} else {
|
|
50
|
+
foundKeys.push(...recordKeys);
|
|
51
|
+
|
|
52
|
+
logger.debug(() => ({
|
|
53
|
+
msg: "Processed aggregation batch",
|
|
54
|
+
data: {
|
|
55
|
+
size: recordKeys.length,
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
await executeAggregation(
|
|
62
|
+
client,
|
|
63
|
+
query,
|
|
64
|
+
{
|
|
65
|
+
// #2 is a required option when the 'ismissing()' function is in the query body
|
|
66
|
+
DIALECT: REDIS_QUERY_DIALECT,
|
|
67
|
+
COUNT: REDIS_BATCH_SIZE,
|
|
68
|
+
LOAD: `@${keyField}`,
|
|
69
|
+
},
|
|
70
|
+
addRecordKeys,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return foundKeys;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const executeAggregation = async (
|
|
77
|
+
client: RedisClientType,
|
|
78
|
+
query: string,
|
|
79
|
+
aggregateOptions: FtAggregateWithCursorOptions,
|
|
80
|
+
handleBatch: (records: object[]) => Promise<void>,
|
|
81
|
+
): Promise<void> => {
|
|
82
|
+
const initialReply = await client.ft.aggregateWithCursor(
|
|
83
|
+
ACCESS_RULES_REDIS_INDEX_NAME,
|
|
84
|
+
query,
|
|
85
|
+
aggregateOptions,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
await handleBatch(initialReply.results);
|
|
89
|
+
|
|
90
|
+
let cursor = initialReply.cursor;
|
|
91
|
+
|
|
92
|
+
while (0 !== cursor) {
|
|
93
|
+
const batchReply = await client.ft.cursorRead(
|
|
94
|
+
ACCESS_RULES_REDIS_INDEX_NAME,
|
|
95
|
+
cursor,
|
|
96
|
+
{ COUNT: aggregateOptions.COUNT },
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await handleBatch(batchReply.results);
|
|
100
|
+
|
|
101
|
+
cursor = batchReply.cursor;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
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 { PolicyScope, UserIp, UserScope } from "#policy/rule.js";
|
|
16
|
+
import { userScopeSchema } from "#policy/ruleInput/userScopeInput.js";
|
|
17
|
+
import {
|
|
18
|
+
type AccessRulesFilter,
|
|
19
|
+
FilterScopeMatch,
|
|
20
|
+
} from "#policy/rulesStorage.js";
|
|
21
|
+
|
|
22
|
+
type QueryBuilder = (value: unknown, scope: UserIp) => string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Escapes special characters for Redis TAG field queries.
|
|
26
|
+
* Redis TAG fields treat these characters as special and they must be escaped with a backslash.
|
|
27
|
+
*/
|
|
28
|
+
const escapeTagValue = (value: string): string => {
|
|
29
|
+
// Characters that need escaping in Redis TAG queries
|
|
30
|
+
return value.replace(/([,.<>{}\[\]"':;!@#$%^&*()\-+=~|/\\])/g, "\\$1");
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// #2 is a required option when the 'ismissing()' function is in the query body
|
|
34
|
+
export const REDIS_QUERY_DIALECT = 2;
|
|
35
|
+
|
|
36
|
+
const userIpQueries: Record<keyof UserIp, QueryBuilder> = {
|
|
37
|
+
numericIp: (value, scope) => {
|
|
38
|
+
if (undefined !== value) {
|
|
39
|
+
return `( @numericIp:[${value} ${value}] | ( @numericIpMaskMin:[-inf ${value}] @numericIpMaskMax:[${value} +inf] ) )`;
|
|
40
|
+
}
|
|
41
|
+
// Only emit ismissing(@numericIp) if ranges are also not present
|
|
42
|
+
if (
|
|
43
|
+
scope.numericIpMaskMin === undefined &&
|
|
44
|
+
scope.numericIpMaskMax === undefined
|
|
45
|
+
) {
|
|
46
|
+
return "ismissing(@numericIp) ismissing(@numericIpMaskMin) ismissing(@numericIpMaskMax)";
|
|
47
|
+
}
|
|
48
|
+
// Else, let ranges handle it
|
|
49
|
+
return "";
|
|
50
|
+
},
|
|
51
|
+
numericIpMaskMin: (value, scope) => {
|
|
52
|
+
if (scope.numericIp !== undefined) {
|
|
53
|
+
return ""; // handled by numericIp
|
|
54
|
+
}
|
|
55
|
+
// When all IP fields are undefined, numericIp handler already emits all ismissing clauses
|
|
56
|
+
if (value === undefined && scope.numericIpMaskMax === undefined) {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
return value !== undefined
|
|
60
|
+
? `@numericIpMaskMin:[-inf ${value}]`
|
|
61
|
+
: "ismissing(@numericIpMaskMin)";
|
|
62
|
+
},
|
|
63
|
+
numericIpMaskMax: (value, scope) => {
|
|
64
|
+
if (scope.numericIp !== undefined) {
|
|
65
|
+
return ""; // handled by numericIp
|
|
66
|
+
}
|
|
67
|
+
// When all IP fields are undefined, numericIp handler already emits all ismissing clauses
|
|
68
|
+
if (value === undefined && scope.numericIpMaskMin === undefined) {
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
return value !== undefined
|
|
72
|
+
? `@numericIpMaskMax:[${value} +inf]`
|
|
73
|
+
: "ismissing(@numericIpMaskMax)";
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const getUserScopeQuery = (
|
|
78
|
+
userScope: UserScope,
|
|
79
|
+
FilterScopeMatchType: FilterScopeMatch | undefined,
|
|
80
|
+
matchingFieldsOnly: boolean,
|
|
81
|
+
): string => {
|
|
82
|
+
let scopeEntries = Object.entries(userScope) as Array<
|
|
83
|
+
[keyof UserScope, unknown]
|
|
84
|
+
>;
|
|
85
|
+
let scopeJoinType = " ";
|
|
86
|
+
|
|
87
|
+
// skip fields with undefined values if in greedy mode and set operator to OR
|
|
88
|
+
if (FilterScopeMatchType === FilterScopeMatch.Greedy) {
|
|
89
|
+
scopeEntries = scopeEntries.filter(
|
|
90
|
+
([_, value]) => value !== undefined,
|
|
91
|
+
) as Array<[keyof UserScope, unknown]>;
|
|
92
|
+
scopeJoinType = " | ";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (matchingFieldsOnly) {
|
|
96
|
+
const scopeMap = new Map<keyof UserScope, unknown>(scopeEntries);
|
|
97
|
+
|
|
98
|
+
// If numericIp is explicitly undefined, set both range fields to undefined
|
|
99
|
+
if (scopeMap.has("numericIp") && scopeMap.get("numericIp") === undefined) {
|
|
100
|
+
scopeMap.set("numericIpMaskMin", undefined);
|
|
101
|
+
scopeMap.set("numericIpMaskMax", undefined);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Ensure all expected fields are accounted for
|
|
105
|
+
for (const name of Object.keys(userScopeSchema.shape) as Array<
|
|
106
|
+
keyof UserScope
|
|
107
|
+
>) {
|
|
108
|
+
if (!scopeMap.has(name)) {
|
|
109
|
+
scopeMap.set(name, undefined);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
scopeEntries = [...scopeMap.entries()];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const scopeObj = Object.fromEntries(scopeEntries) as Partial<UserScope>;
|
|
117
|
+
|
|
118
|
+
return scopeEntries
|
|
119
|
+
.map(([scopeFieldName, scopeFieldValue]) =>
|
|
120
|
+
getUserScopeFieldQuery(
|
|
121
|
+
scopeFieldName,
|
|
122
|
+
scopeFieldValue,
|
|
123
|
+
FilterScopeMatchType,
|
|
124
|
+
scopeObj,
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.join(scopeJoinType);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Fields that may contain special characters requiring escaping in Redis TAG queries
|
|
132
|
+
const FIELDS_REQUIRING_ESCAPE: ReadonlySet<keyof UserScope> = new Set([
|
|
133
|
+
"coords",
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
const getUserScopeFieldQuery = (
|
|
137
|
+
fieldName: keyof UserScope,
|
|
138
|
+
fieldValue: unknown,
|
|
139
|
+
scopeMatch: FilterScopeMatch | undefined,
|
|
140
|
+
fullScope: Partial<UserScope>,
|
|
141
|
+
): string => {
|
|
142
|
+
if (fieldName in userIpQueries) {
|
|
143
|
+
const queryBuilder = userIpQueries[fieldName as keyof UserIp];
|
|
144
|
+
|
|
145
|
+
return queryBuilder(fieldValue, fullScope);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (undefined === fieldValue) {
|
|
149
|
+
return `ismissing(@${fieldName})`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const stringValue = String(fieldValue);
|
|
153
|
+
// Only escape fields that may contain special characters (like coords with JSON)
|
|
154
|
+
const queryValue = FIELDS_REQUIRING_ESCAPE.has(fieldName)
|
|
155
|
+
? escapeTagValue(stringValue)
|
|
156
|
+
: stringValue;
|
|
157
|
+
|
|
158
|
+
return `@${fieldName}:{${queryValue}}`;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const getPolicyScopeQuery = (
|
|
162
|
+
policyScope: PolicyScope | undefined,
|
|
163
|
+
scopeMatch: FilterScopeMatch | undefined,
|
|
164
|
+
): string => {
|
|
165
|
+
const clientId = policyScope?.clientId;
|
|
166
|
+
|
|
167
|
+
if ("string" === typeof clientId) {
|
|
168
|
+
return FilterScopeMatch.Exact === scopeMatch
|
|
169
|
+
? `@clientId:{${clientId}}`
|
|
170
|
+
: `( @clientId:{${clientId}} | ismissing(@clientId) )`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return FilterScopeMatch.Exact === scopeMatch ? "ismissing(@clientId)" : "";
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* Search command example:
|
|
178
|
+
*
|
|
179
|
+
* ft.search index:test "( @clientId:{value} | ismissing(@clientId) )
|
|
180
|
+
* (
|
|
181
|
+
* ( @ip:[value] | ( @ipRangeMin:[-inf value] @ipRangeMax:[value +inf] ) ) |
|
|
182
|
+
* @id:{value} | @ja4Fingerprint:{value} | headersFingerprint:{value}"
|
|
183
|
+
* )
|
|
184
|
+
* DIALECT 2 # must have when the ismissing() function in use
|
|
185
|
+
* */
|
|
186
|
+
export const getRulesRedisQuery = (
|
|
187
|
+
filter: AccessRulesFilter,
|
|
188
|
+
matchingFieldsOnly: boolean,
|
|
189
|
+
): string => {
|
|
190
|
+
const { policyScope, userScope } = filter;
|
|
191
|
+
const queryParts = [];
|
|
192
|
+
|
|
193
|
+
if (filter.groupId) {
|
|
194
|
+
queryParts.push(`@groupId:{${filter.groupId}}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const policyScopeQuery = getPolicyScopeQuery(
|
|
198
|
+
policyScope,
|
|
199
|
+
filter.policyScopeMatch,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (policyScopeQuery) {
|
|
203
|
+
queryParts.push(policyScopeQuery);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (userScope && Object.keys(userScope).length > 0) {
|
|
207
|
+
const userScopeFilter = getUserScopeQuery(
|
|
208
|
+
userScope,
|
|
209
|
+
filter.userScopeMatch,
|
|
210
|
+
matchingFieldsOnly,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
queryParts.push(`( ${userScopeFilter} )`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return queryParts.length > 0 ? queryParts.join(" ") : "*";
|
|
217
|
+
};
|