@prosopo/user-access-policy 3.5.19 → 3.5.28
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/CHANGELOG.md +84 -0
- package/dist/.export.js +21 -0
- package/dist/api/.export.js +11 -0
- package/dist/api/delete/.export.js +1 -0
- package/dist/api/{deleteAllRulesEndpoint.js → delete/deleteAllRules.js} +10 -9
- package/dist/api/delete/deleteRuleGroups.js +52 -0
- package/dist/api/delete/deleteRules.js +43 -0
- package/dist/api/read/.export.js +1 -0
- package/dist/api/read/fetchRules.js +43 -0
- package/dist/api/read/findRuleIds.js +50 -0
- package/dist/api/read/getMissingIds.js +41 -0
- package/dist/api/ruleApiRoutes.js +131 -0
- package/dist/api/rulesApiClient.js +93 -0
- package/dist/api/write/.export.js +1 -0
- package/dist/api/write/insertRules.js +102 -0
- package/dist/api/write/rehashRules.js +57 -0
- package/dist/cjs/.export.cjs +21 -0
- package/dist/cjs/api/.export.cjs +11 -0
- package/dist/cjs/api/delete/.export.cjs +1 -0
- package/dist/cjs/api/{deleteAllRulesEndpoint.cjs → delete/deleteAllRules.cjs} +9 -8
- package/dist/cjs/api/delete/deleteRuleGroups.cjs +52 -0
- package/dist/cjs/api/delete/deleteRules.cjs +43 -0
- package/dist/cjs/api/read/.export.cjs +1 -0
- package/dist/cjs/api/read/fetchRules.cjs +43 -0
- package/dist/cjs/api/read/findRuleIds.cjs +50 -0
- package/dist/cjs/api/read/getMissingIds.cjs +41 -0
- package/dist/cjs/api/ruleApiRoutes.cjs +131 -0
- package/dist/cjs/api/rulesApiClient.cjs +93 -0
- package/dist/cjs/api/write/.export.cjs +1 -0
- package/dist/cjs/api/write/insertRules.cjs +102 -0
- package/dist/cjs/api/write/rehashRules.cjs +57 -0
- package/dist/cjs/mongoose/.export.cjs +4 -0
- package/dist/cjs/mongoose/mongooseRuleSchema.cjs +36 -0
- package/dist/cjs/redis/.export.cjs +6 -0
- package/dist/cjs/redis/reader/redisAggregate.cjs +60 -0
- package/dist/cjs/redis/reader/redisRulesQuery.cjs +99 -0
- package/dist/cjs/redis/reader/redisRulesReader.cjs +230 -0
- package/dist/cjs/redis/redisClient.cjs +67 -0
- package/dist/cjs/redis/redisRuleIndex.cjs +50 -0
- package/dist/cjs/redis/redisRulesStorage.cjs +22 -9
- package/dist/cjs/redis/redisRulesWriter.cjs +91 -64
- package/dist/cjs/rule.cjs +8 -0
- package/dist/cjs/ruleInput/.export.cjs +9 -0
- package/dist/cjs/ruleInput/policyInput.cjs +25 -0
- package/dist/cjs/ruleInput/ruleInput.cjs +50 -0
- package/dist/cjs/ruleInput/userScopeInput.cjs +55 -0
- package/dist/cjs/ruleRecord.cjs +23 -0
- package/dist/cjs/rulesStorage.cjs +8 -0
- package/dist/cjs/transformRule.cjs +77 -0
- package/dist/mongoose/.export.js +4 -0
- package/dist/mongoose/mongooseRuleSchema.js +36 -0
- package/dist/redis/.export.js +6 -0
- package/dist/redis/reader/redisAggregate.js +60 -0
- package/dist/redis/reader/redisRulesQuery.js +99 -0
- package/dist/redis/reader/redisRulesReader.js +213 -0
- package/dist/redis/redisClient.js +67 -0
- package/dist/redis/redisRuleIndex.js +50 -0
- package/dist/redis/redisRulesStorage.js +23 -10
- package/dist/redis/redisRulesWriter.js +91 -64
- package/dist/rule.js +8 -0
- package/dist/ruleInput/.export.js +9 -0
- package/dist/ruleInput/policyInput.js +25 -0
- package/dist/ruleInput/ruleInput.js +50 -0
- package/dist/ruleInput/userScopeInput.js +55 -0
- package/dist/ruleRecord.js +23 -0
- package/dist/rulesStorage.js +8 -0
- package/dist/transformRule.js +77 -0
- package/entries.ts +20 -0
- package/package.json +34 -18
- package/vite.cjs.config.ts +4 -1
- package/vite.esm.config.ts +6 -1
- package/dist/accessPolicy.js +0 -80
- package/dist/accessPolicyResolver.js +0 -31
- package/dist/accessRules.js +0 -11
- package/dist/api/accessRuleApiRoutes.js +0 -79
- package/dist/api/accessRulesApiClient.js +0 -38
- package/dist/api/deleteRulesEndpoint.js +0 -34
- package/dist/api/insertRulesEndpoint.js +0 -62
- package/dist/cjs/accessPolicy.cjs +0 -80
- package/dist/cjs/accessPolicyResolver.cjs +0 -31
- package/dist/cjs/accessRules.cjs +0 -11
- package/dist/cjs/api/accessRuleApiRoutes.cjs +0 -79
- package/dist/cjs/api/accessRulesApiClient.cjs +0 -38
- package/dist/cjs/api/deleteRulesEndpoint.cjs +0 -34
- package/dist/cjs/api/insertRulesEndpoint.cjs +0 -62
- package/dist/cjs/index.cjs +0 -31
- package/dist/cjs/redis/redisRulesIndex.cjs +0 -138
- package/dist/cjs/redis/redisRulesReader.cjs +0 -142
- package/dist/cjs/util.cjs +0 -5
- package/dist/index.js +0 -32
- package/dist/redis/redisRulesIndex.js +0 -138
- package/dist/redis/redisRulesReader.js +0 -125
- package/dist/util.js +0 -5
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const search = require("@redis/search");
|
|
4
|
+
const transformRule = require("../transformRule.cjs");
|
|
5
|
+
const userIpRedisSchema = {
|
|
6
|
+
numericIpMaskMin: { type: search.SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
|
|
7
|
+
numericIpMaskMax: { type: search.SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
|
|
8
|
+
numericIp: { type: search.SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true }
|
|
9
|
+
};
|
|
10
|
+
const userAttributesRedisSchema = {
|
|
11
|
+
userId: { type: search.SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
|
|
12
|
+
ja4Hash: { type: search.SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
|
|
13
|
+
headersHash: { type: search.SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
|
|
14
|
+
userAgentHash: { type: search.SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true }
|
|
15
|
+
};
|
|
16
|
+
const userScopeRedisSchema = {
|
|
17
|
+
...userAttributesRedisSchema,
|
|
18
|
+
...userIpRedisSchema
|
|
19
|
+
};
|
|
20
|
+
const policyScopeRedisSchema = {
|
|
21
|
+
clientId: {
|
|
22
|
+
type: search.SCHEMA_FIELD_TYPE.TAG,
|
|
23
|
+
INDEXMISSING: true
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const accessRuleRedisSchema = {
|
|
27
|
+
...policyScopeRedisSchema,
|
|
28
|
+
...userScopeRedisSchema,
|
|
29
|
+
groupId: { type: search.SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true }
|
|
30
|
+
};
|
|
31
|
+
const ACCESS_RULES_REDIS_INDEX_NAME = "index:user-access-rules";
|
|
32
|
+
const ACCESS_RULE_REDIS_KEY_PREFIX = "uar:";
|
|
33
|
+
const accessRulesRedisIndex = {
|
|
34
|
+
name: ACCESS_RULES_REDIS_INDEX_NAME,
|
|
35
|
+
schema: accessRuleRedisSchema,
|
|
36
|
+
options: {
|
|
37
|
+
ON: "HASH",
|
|
38
|
+
PREFIX: [ACCESS_RULE_REDIS_KEY_PREFIX]
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const getAccessRuleRedisKey = (rule) => ACCESS_RULE_REDIS_KEY_PREFIX + transformRule.makeAccessRuleHash(rule);
|
|
42
|
+
exports.ACCESS_RULES_REDIS_INDEX_NAME = ACCESS_RULES_REDIS_INDEX_NAME;
|
|
43
|
+
exports.ACCESS_RULE_REDIS_KEY_PREFIX = ACCESS_RULE_REDIS_KEY_PREFIX;
|
|
44
|
+
exports.accessRuleRedisSchema = accessRuleRedisSchema;
|
|
45
|
+
exports.accessRulesRedisIndex = accessRulesRedisIndex;
|
|
46
|
+
exports.getAccessRuleRedisKey = getAccessRuleRedisKey;
|
|
47
|
+
exports.policyScopeRedisSchema = policyScopeRedisSchema;
|
|
48
|
+
exports.userAttributesRedisSchema = userAttributesRedisSchema;
|
|
49
|
+
exports.userIpRedisSchema = userIpRedisSchema;
|
|
50
|
+
exports.userScopeRedisSchema = userScopeRedisSchema;
|
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const redisRulesReader = require("./redisRulesReader.cjs");
|
|
3
|
+
const redisRulesReader = require("./reader/redisRulesReader.cjs");
|
|
4
4
|
const redisRulesWriter = require("./redisRulesWriter.cjs");
|
|
5
5
|
const createRedisAccessRulesStorage = (connection, logger) => {
|
|
6
|
-
const storage =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const storage = composeStorage(
|
|
7
|
+
new redisRulesReader.DummyRedisRulesReader(logger),
|
|
8
|
+
new redisRulesWriter.DummyRedisRulesWriter(logger)
|
|
9
|
+
);
|
|
10
10
|
connection.getClient().then((client) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const realStorage = composeStorage(
|
|
12
|
+
new redisRulesReader.RedisRulesReader(client, logger),
|
|
13
|
+
new redisRulesWriter.RedisRulesWriter(client, logger)
|
|
14
|
+
);
|
|
15
|
+
Object.assign(storage, realStorage);
|
|
15
16
|
logger.info(() => ({
|
|
16
17
|
msg: "RedisAccessRules storage got a ready Redis client"
|
|
17
18
|
}));
|
|
18
19
|
});
|
|
19
20
|
return storage;
|
|
20
21
|
};
|
|
22
|
+
const composeStorage = (reader, writer) => ({
|
|
23
|
+
// reader
|
|
24
|
+
fetchRules: reader.fetchRules.bind(reader),
|
|
25
|
+
getMissingRuleIds: reader.getMissingRuleIds.bind(reader),
|
|
26
|
+
findRules: reader.findRules.bind(reader),
|
|
27
|
+
findRuleIds: reader.findRuleIds.bind(reader),
|
|
28
|
+
fetchAllRuleIds: reader.fetchAllRuleIds.bind(reader),
|
|
29
|
+
// writer
|
|
30
|
+
insertRules: writer.insertRules.bind(writer),
|
|
31
|
+
deleteRules: writer.deleteRules.bind(writer),
|
|
32
|
+
deleteAllRules: writer.deleteAllRules.bind(writer)
|
|
33
|
+
});
|
|
21
34
|
exports.createRedisAccessRulesStorage = createRedisAccessRulesStorage;
|
|
@@ -1,73 +1,100 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
const common = require("@prosopo/common");
|
|
4
|
+
const redisClient = require("./redisClient.cjs");
|
|
5
|
+
const redisRuleIndex = require("./redisRuleIndex.cjs");
|
|
6
|
+
class RedisRulesWriter {
|
|
7
|
+
constructor(client, logger) {
|
|
8
|
+
this.client = client;
|
|
9
|
+
this.logger = logger;
|
|
10
|
+
}
|
|
11
|
+
async insertRules(ruleEntries) {
|
|
12
|
+
const entryBatches = common.chunkIntoBatches(ruleEntries, redisClient.REDIS_BATCH_SIZE);
|
|
13
|
+
const keyBatches = await common.executeBatchesSequentially(
|
|
14
|
+
entryBatches,
|
|
15
|
+
async (entriesBatch) => this.insertRuleEntries(entriesBatch)
|
|
16
|
+
);
|
|
17
|
+
return keyBatches.flatMap(
|
|
18
|
+
(ruleKey) => ruleKey.slice(redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX.length)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
async deleteRules(ruleIds) {
|
|
22
|
+
const ruleKeys = ruleIds.map(
|
|
23
|
+
(ruleId) => redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX + ruleId
|
|
24
|
+
);
|
|
25
|
+
const keyBatches = common.chunkIntoBatches(ruleKeys, redisClient.REDIS_BATCH_SIZE);
|
|
26
|
+
await common.executeBatchesSequentially(keyBatches, async (keysBatch) => {
|
|
27
|
+
const queries = this.client.multi();
|
|
28
|
+
for (const ruleKey of keysBatch) {
|
|
29
|
+
queries.del(ruleKey);
|
|
30
|
+
}
|
|
31
|
+
await queries.exec();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async deleteAllRules() {
|
|
35
|
+
let cursor = "0";
|
|
36
|
+
let total = 0;
|
|
37
|
+
do {
|
|
38
|
+
const reply = await this.client.scan(cursor, {
|
|
39
|
+
MATCH: `${redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX}*`,
|
|
40
|
+
COUNT: redisClient.REDIS_BATCH_SIZE
|
|
41
|
+
});
|
|
42
|
+
const ids = reply.keys.map(
|
|
43
|
+
(key) => key.slice(redisRuleIndex.ACCESS_RULE_REDIS_KEY_PREFIX.length)
|
|
44
|
+
);
|
|
45
|
+
await this.deleteRules(ids);
|
|
46
|
+
total += ids.length;
|
|
47
|
+
cursor = reply.cursor;
|
|
48
|
+
} while ("0" !== cursor);
|
|
49
|
+
return total;
|
|
50
|
+
}
|
|
51
|
+
async insertRuleEntries(ruleEntries) {
|
|
52
|
+
const queries = this.client.multi();
|
|
53
|
+
const ruleKeys = ruleEntries.map((ruleEntry) => {
|
|
54
|
+
const { rule, expiresUnixTimestamp } = ruleEntry;
|
|
55
|
+
const ruleKey = redisRuleIndex.getAccessRuleRedisKey(rule);
|
|
10
56
|
const ruleValue = getRedisRuleValue(rule);
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
if (expiryDate.getUTCFullYear() === 1970) {
|
|
15
|
-
await client.expireAt(ruleKey, expirationTimestamp);
|
|
16
|
-
} else {
|
|
17
|
-
const timestampInSeconds = Math.floor(expirationTimestamp / 1e3);
|
|
18
|
-
await client.expireAt(ruleKey, timestampInSeconds);
|
|
19
|
-
}
|
|
57
|
+
queries.hSet(ruleKey, ruleValue);
|
|
58
|
+
if (expiresUnixTimestamp) {
|
|
59
|
+
queries.expireAt(ruleKey, expiresUnixTimestamp);
|
|
20
60
|
}
|
|
21
61
|
return ruleKey;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return await client.del(keys);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
const getDummyRedisRulesWriter = (logger) => {
|
|
32
|
-
return {
|
|
33
|
-
insertRule: async (rule, expirationTimestamp) => {
|
|
34
|
-
logger.info(() => ({
|
|
35
|
-
msg: "Dummy insertRule() has no effect (redis is not ready)",
|
|
36
|
-
data: {
|
|
37
|
-
rule
|
|
38
|
-
}
|
|
39
|
-
}));
|
|
40
|
-
return "";
|
|
41
|
-
},
|
|
42
|
-
deleteRules: async (ruleIds) => {
|
|
43
|
-
logger.info(() => ({
|
|
44
|
-
msg: "Dummy deleteRules() has no effect (redis is not ready)",
|
|
45
|
-
data: {
|
|
46
|
-
ruleIds
|
|
47
|
-
}
|
|
48
|
-
}));
|
|
49
|
-
},
|
|
50
|
-
deleteAllRules: async () => {
|
|
51
|
-
logger.info(() => ({
|
|
52
|
-
msg: "Dummy deleteAllRules() has no effect (redis is not ready)"
|
|
53
|
-
}));
|
|
54
|
-
return 0;
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
};
|
|
58
|
-
const getRedisRuleKey = (rule) => redisRulesIndex.redisRuleKeyPrefix + crypto.createHash(redisRuleContentHashAlgorithm).update(
|
|
59
|
-
JSON.stringify(
|
|
60
|
-
rule,
|
|
61
|
-
(key, value) => (
|
|
62
|
-
// JSON.stringify can't handle BigInt itself: throws "Do not know how to serialize a BigInt"
|
|
63
|
-
"bigint" === typeof value ? value.toString() : value
|
|
64
|
-
)
|
|
65
|
-
)
|
|
66
|
-
).digest("hex");
|
|
62
|
+
});
|
|
63
|
+
await queries.exec();
|
|
64
|
+
return ruleKeys;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
67
|
const getRedisRuleValue = (rule) => Object.fromEntries(
|
|
68
68
|
Object.entries(rule).map(([key, value]) => [key, String(value)])
|
|
69
69
|
);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
class DummyRedisRulesWriter {
|
|
71
|
+
constructor(logger) {
|
|
72
|
+
this.logger = logger;
|
|
73
|
+
}
|
|
74
|
+
async insertRules(ruleEntries) {
|
|
75
|
+
this.logger.info(() => ({
|
|
76
|
+
msg: "Dummy insertRules() has no effect (redis is not ready)",
|
|
77
|
+
data: {
|
|
78
|
+
ruleEntries
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
async deleteRules(ruleIds) {
|
|
84
|
+
this.logger.info(() => ({
|
|
85
|
+
msg: "Dummy deleteRules() has no effect (redis is not ready)",
|
|
86
|
+
data: {
|
|
87
|
+
ruleIds
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
async deleteAllRules() {
|
|
92
|
+
this.logger.info(() => ({
|
|
93
|
+
msg: "Dummy deleteAllRules() has no effect (redis is not ready)"
|
|
94
|
+
}));
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.DummyRedisRulesWriter = DummyRedisRulesWriter;
|
|
99
|
+
exports.RedisRulesWriter = RedisRulesWriter;
|
|
73
100
|
exports.getRedisRuleValue = getRedisRuleValue;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
var AccessPolicyType = /* @__PURE__ */ ((AccessPolicyType2) => {
|
|
4
|
+
AccessPolicyType2["Block"] = "block";
|
|
5
|
+
AccessPolicyType2["Restrict"] = "restrict";
|
|
6
|
+
return AccessPolicyType2;
|
|
7
|
+
})(AccessPolicyType || {});
|
|
8
|
+
exports.AccessPolicyType = AccessPolicyType;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const ruleInput = require("./ruleInput.cjs");
|
|
4
|
+
const policyInput = require("./policyInput.cjs");
|
|
5
|
+
const userScopeInput = require("./userScopeInput.cjs");
|
|
6
|
+
exports.accessRuleInput = ruleInput.accessRuleInput;
|
|
7
|
+
exports.accessPolicyInput = policyInput.accessPolicyInput;
|
|
8
|
+
exports.policyScopeInput = policyInput.policyScopeInput;
|
|
9
|
+
exports.userScopeInput = userScopeInput.userScopeInput;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const types = require("@prosopo/types");
|
|
4
|
+
const zod = require("zod");
|
|
5
|
+
const rule = require("../rule.cjs");
|
|
6
|
+
const accessPolicyInput = zod.z.object({
|
|
7
|
+
type: zod.z.nativeEnum(rule.AccessPolicyType),
|
|
8
|
+
captchaType: types.CaptchaTypeSchema.optional(),
|
|
9
|
+
description: zod.z.coerce.string().optional(),
|
|
10
|
+
// Redis stores values as strings, so coerce is needed to parse properly
|
|
11
|
+
solvedImagesCount: zod.z.coerce.number().optional(),
|
|
12
|
+
// the percentage of image panels that must be solved per image CAPTCHA
|
|
13
|
+
imageThreshold: zod.z.coerce.number().optional(),
|
|
14
|
+
// the Proof-of-Work difficulty level
|
|
15
|
+
powDifficulty: zod.z.coerce.number().optional(),
|
|
16
|
+
// the number of unsolved image CAPTCHA challenges to serve
|
|
17
|
+
unsolvedImagesCount: zod.z.coerce.number().optional(),
|
|
18
|
+
// used to increase the user's score
|
|
19
|
+
frictionlessScore: zod.z.coerce.number().optional()
|
|
20
|
+
});
|
|
21
|
+
const policyScopeInput = zod.z.object({
|
|
22
|
+
clientId: zod.z.coerce.string().optional()
|
|
23
|
+
});
|
|
24
|
+
exports.accessPolicyInput = accessPolicyInput;
|
|
25
|
+
exports.policyScopeInput = policyScopeInput;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const zod = require("zod");
|
|
4
|
+
const rulesStorage = require("../rulesStorage.cjs");
|
|
5
|
+
const policyInput = require("./policyInput.cjs");
|
|
6
|
+
const userScopeInput = require("./userScopeInput.cjs");
|
|
7
|
+
const ruleGroupInput = zod.z.object({
|
|
8
|
+
groupId: zod.z.coerce.string().optional(),
|
|
9
|
+
ruleGroupId: zod.z.coerce.string().optional()
|
|
10
|
+
}).transform((ruleGroupInput2) => {
|
|
11
|
+
const { ruleGroupId, ...ruleGroup } = ruleGroupInput2;
|
|
12
|
+
if ("string" === typeof ruleGroupId) {
|
|
13
|
+
ruleGroup.groupId = ruleGroupId;
|
|
14
|
+
}
|
|
15
|
+
return ruleGroup;
|
|
16
|
+
});
|
|
17
|
+
const accessRuleInput = zod.z.object({
|
|
18
|
+
...policyInput.accessPolicyInput.shape,
|
|
19
|
+
...policyInput.policyScopeInput.shape
|
|
20
|
+
}).and(userScopeInput.userScopeInput).and(ruleGroupInput).transform((ruleInput) => ruleInput);
|
|
21
|
+
const ruleEntryInput = zod.z.object({
|
|
22
|
+
rule: accessRuleInput,
|
|
23
|
+
expiresUnixTimestamp: zod.z.coerce.number().optional()
|
|
24
|
+
});
|
|
25
|
+
const accessRulesFilterInput = zod.z.object({
|
|
26
|
+
policyScope: policyInput.policyScopeInput.optional(),
|
|
27
|
+
policyScopes: zod.z.array(policyInput.policyScopeInput).optional(),
|
|
28
|
+
policyScopeMatch: zod.z.nativeEnum(rulesStorage.FilterScopeMatch).default(rulesStorage.FilterScopeMatch.Exact),
|
|
29
|
+
userScope: userScopeInput.userScopeInput.optional(),
|
|
30
|
+
userScopeMatch: zod.z.nativeEnum(rulesStorage.FilterScopeMatch).default(rulesStorage.FilterScopeMatch.Exact),
|
|
31
|
+
groupId: zod.z.string().optional()
|
|
32
|
+
});
|
|
33
|
+
const getAccessRuleFiltersFromInput = (filterInput) => {
|
|
34
|
+
const { policyScopes, policyScope, ...filterBase } = filterInput;
|
|
35
|
+
const allPolicyScopes = policyScopes || [];
|
|
36
|
+
if (policyScope) {
|
|
37
|
+
allPolicyScopes.push(policyScope);
|
|
38
|
+
}
|
|
39
|
+
if (allPolicyScopes.length > 0) {
|
|
40
|
+
return allPolicyScopes.map((policyScope2) => ({
|
|
41
|
+
...filterBase,
|
|
42
|
+
policyScope: policyScope2
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
return [filterBase];
|
|
46
|
+
};
|
|
47
|
+
exports.accessRuleInput = accessRuleInput;
|
|
48
|
+
exports.accessRulesFilterInput = accessRulesFilterInput;
|
|
49
|
+
exports.getAccessRuleFiltersFromInput = getAccessRuleFiltersFromInput;
|
|
50
|
+
exports.ruleEntryInput = ruleEntryInput;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const util = require("@prosopo/util");
|
|
5
|
+
const ipAddress = require("ip-address");
|
|
6
|
+
const zod = require("zod");
|
|
7
|
+
const userAttributesSchema = zod.z.object({
|
|
8
|
+
// coerce is used for safety, as e.g., incoming userId can be digital
|
|
9
|
+
userId: zod.z.coerce.string().optional(),
|
|
10
|
+
ja4Hash: zod.z.coerce.string().optional(),
|
|
11
|
+
headersHash: zod.z.coerce.string().optional(),
|
|
12
|
+
userAgentHash: zod.z.coerce.string().optional()
|
|
13
|
+
});
|
|
14
|
+
const userAttributesInput = zod.z.object({
|
|
15
|
+
...userAttributesSchema.shape,
|
|
16
|
+
userAgent: zod.z.coerce.string().optional()
|
|
17
|
+
}).transform((userAttributesInput2) => {
|
|
18
|
+
const { userAgent, ...userScope } = userAttributesInput2;
|
|
19
|
+
if ("string" === typeof userAgent) {
|
|
20
|
+
userScope.userAgentHash = hashUserAgent(userAgent);
|
|
21
|
+
}
|
|
22
|
+
return userScope;
|
|
23
|
+
});
|
|
24
|
+
const hashUserAgent = (userAgent) => crypto.createHash("sha256").update(userAgent).digest("hex");
|
|
25
|
+
const userIpSchema = zod.z.object({
|
|
26
|
+
numericIp: zod.z.coerce.bigint().optional(),
|
|
27
|
+
numericIpMaskMin: zod.z.coerce.bigint().optional(),
|
|
28
|
+
numericIpMaskMax: zod.z.coerce.bigint().optional()
|
|
29
|
+
});
|
|
30
|
+
const userIpInput = zod.z.object({
|
|
31
|
+
...userIpSchema.shape,
|
|
32
|
+
ip: zod.z.string().optional(),
|
|
33
|
+
ipMask: zod.z.string().optional()
|
|
34
|
+
}).transform((userIpInput2) => {
|
|
35
|
+
const { ip, ipMask, ...numericUserIp } = userIpInput2;
|
|
36
|
+
if ("string" === typeof ip) {
|
|
37
|
+
numericUserIp.numericIp = util.getIPAddress(ip).bigInt();
|
|
38
|
+
}
|
|
39
|
+
if ("string" === typeof ipMask) {
|
|
40
|
+
const ipObject = new ipAddress.Address4(ipMask);
|
|
41
|
+
numericUserIp.numericIpMaskMin = ipObject.startAddress().bigInt();
|
|
42
|
+
numericUserIp.numericIpMaskMax = ipObject.endAddress().bigInt();
|
|
43
|
+
}
|
|
44
|
+
return numericUserIp;
|
|
45
|
+
});
|
|
46
|
+
const userScopeSchema = zod.z.object({
|
|
47
|
+
...userIpSchema.shape,
|
|
48
|
+
...userAttributesSchema.shape
|
|
49
|
+
});
|
|
50
|
+
const userScopeInput = zod.z.object({}).and(userIpInput).and(userAttributesInput).transform(
|
|
51
|
+
// transform is used for type safety only - plain "satisfies ZodType<x>" doesn't work after ".and()"
|
|
52
|
+
(userScopeInput2) => userScopeInput2
|
|
53
|
+
);
|
|
54
|
+
exports.userScopeInput = userScopeInput;
|
|
55
|
+
exports.userScopeSchema = userScopeSchema;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const userAttributesRecordFields = [
|
|
4
|
+
"userId",
|
|
5
|
+
"ja4Hash",
|
|
6
|
+
"headersHash",
|
|
7
|
+
"userAgent"
|
|
8
|
+
];
|
|
9
|
+
const userIpRecordFields = [
|
|
10
|
+
"ip",
|
|
11
|
+
"ipMask"
|
|
12
|
+
];
|
|
13
|
+
const userScopeRecordFields = [
|
|
14
|
+
...userAttributesRecordFields,
|
|
15
|
+
...userIpRecordFields
|
|
16
|
+
];
|
|
17
|
+
const getUserScopeRecordFromAccessRuleRecord = (ruleRecord) => Object.fromEntries(
|
|
18
|
+
userScopeRecordFields.map((field) => [field, ruleRecord[field]]).filter(([, value]) => value !== void 0)
|
|
19
|
+
);
|
|
20
|
+
exports.getUserScopeRecordFromAccessRuleRecord = getUserScopeRecordFromAccessRuleRecord;
|
|
21
|
+
exports.userAttributesRecordFields = userAttributesRecordFields;
|
|
22
|
+
exports.userIpRecordFields = userIpRecordFields;
|
|
23
|
+
exports.userScopeRecordFields = userScopeRecordFields;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
var FilterScopeMatch = /* @__PURE__ */ ((FilterScopeMatch2) => {
|
|
4
|
+
FilterScopeMatch2["Exact"] = "exact";
|
|
5
|
+
FilterScopeMatch2["Greedy"] = "greedy";
|
|
6
|
+
return FilterScopeMatch2;
|
|
7
|
+
})(FilterScopeMatch || {});
|
|
8
|
+
exports.FilterScopeMatch = FilterScopeMatch;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const cidrCalc = require("cidr-calc");
|
|
5
|
+
const ipAddress = require("ip-address");
|
|
6
|
+
const zod = require("zod");
|
|
7
|
+
const policyInput = require("./ruleInput/policyInput.cjs");
|
|
8
|
+
const ruleInput = require("./ruleInput/ruleInput.cjs");
|
|
9
|
+
const userScopeInput = require("./ruleInput/userScopeInput.cjs");
|
|
10
|
+
const RULE_HASH_ALGORITHM = "md5";
|
|
11
|
+
const makeAccessRuleHash = (rule) => {
|
|
12
|
+
const valueProperties = Object.entries(rule).filter(
|
|
13
|
+
([key, value]) => "undefined" !== typeof value
|
|
14
|
+
);
|
|
15
|
+
const orderedProperties = valueProperties.sort();
|
|
16
|
+
const objectToHash = Object.fromEntries(orderedProperties);
|
|
17
|
+
return hashObject(objectToHash, RULE_HASH_ALGORITHM);
|
|
18
|
+
};
|
|
19
|
+
const transformAccessRuleRecordIntoRule = (ruleRecord) => (
|
|
20
|
+
// accessRuleInput does all the record field transformations
|
|
21
|
+
ruleInput.accessRuleInput.parse(ruleRecord)
|
|
22
|
+
);
|
|
23
|
+
const transformAccessRuleIntoRecord = (rule) => accessRuleToRecordScheme.parse(rule);
|
|
24
|
+
const accessRuleToRecordScheme = zod.z.object({
|
|
25
|
+
...policyInput.accessPolicyInput.shape,
|
|
26
|
+
...policyInput.policyScopeInput.shape,
|
|
27
|
+
...userScopeInput.userScopeSchema.shape,
|
|
28
|
+
groupId: zod.z.coerce.string().optional()
|
|
29
|
+
}).transform((ruleInput2) => {
|
|
30
|
+
const {
|
|
31
|
+
groupId,
|
|
32
|
+
numericIp,
|
|
33
|
+
numericIpMaskMin,
|
|
34
|
+
numericIpMaskMax,
|
|
35
|
+
userAgentHash,
|
|
36
|
+
...rule
|
|
37
|
+
} = ruleInput2;
|
|
38
|
+
const record = rule;
|
|
39
|
+
if ("string" === typeof groupId) {
|
|
40
|
+
record.ruleGroupId = groupId;
|
|
41
|
+
}
|
|
42
|
+
if ("string" === typeof userAgentHash) {
|
|
43
|
+
record.userAgent = userAgentHash;
|
|
44
|
+
}
|
|
45
|
+
if ("bigint" === typeof numericIp) {
|
|
46
|
+
record.ip = getStringIpFromNumeric(numericIp);
|
|
47
|
+
}
|
|
48
|
+
if ("bigint" === typeof numericIpMaskMin && "bigint" === typeof numericIpMaskMax) {
|
|
49
|
+
record.ipMask = getCidrFromNumericIpRange(
|
|
50
|
+
numericIpMaskMin,
|
|
51
|
+
numericIpMaskMax
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return record;
|
|
55
|
+
});
|
|
56
|
+
const hashObject = (object, algorithm) => crypto.createHash(algorithm).update(
|
|
57
|
+
JSON.stringify(
|
|
58
|
+
object,
|
|
59
|
+
(key, value) => (
|
|
60
|
+
// JSON.stringify can't handle BigInt itself: throws "Do not know how to serialize a BigInt"
|
|
61
|
+
"bigint" === typeof value ? value.toString() : value
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
).digest("hex");
|
|
65
|
+
const getStringIpFromNumeric = (numericIp) => ipAddress.Address4.fromInteger(Number(numericIp)).address;
|
|
66
|
+
const getCidrFromNumericIpRange = (startIp, endIp) => {
|
|
67
|
+
const ipRange = new cidrCalc.IpRange(
|
|
68
|
+
cidrCalc.IpAddress.of(getStringIpFromNumeric(startIp)),
|
|
69
|
+
cidrCalc.IpAddress.of(getStringIpFromNumeric(endIp))
|
|
70
|
+
);
|
|
71
|
+
const cidr = ipRange.toCidrs()[0];
|
|
72
|
+
return cidr ? `${cidr.prefix.toString()}/${cidr.prefixLen}` : void 0;
|
|
73
|
+
};
|
|
74
|
+
exports.getCidrFromNumericIpRange = getCidrFromNumericIpRange;
|
|
75
|
+
exports.makeAccessRuleHash = makeAccessRuleHash;
|
|
76
|
+
exports.transformAccessRuleIntoRecord = transformAccessRuleIntoRecord;
|
|
77
|
+
exports.transformAccessRuleRecordIntoRule = transformAccessRuleRecordIntoRule;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const userAttributesSchema = {
|
|
2
|
+
userId: { type: String, required: false },
|
|
3
|
+
ja4Hash: { type: String, required: false },
|
|
4
|
+
userAgent: { type: String, required: false },
|
|
5
|
+
headersHash: { type: String, required: false }
|
|
6
|
+
};
|
|
7
|
+
const userIpSchema = {
|
|
8
|
+
ip: { type: String, required: false },
|
|
9
|
+
ipMask: { type: String, required: false }
|
|
10
|
+
};
|
|
11
|
+
const userScopeSchema = {
|
|
12
|
+
...userAttributesSchema,
|
|
13
|
+
...userIpSchema
|
|
14
|
+
};
|
|
15
|
+
const policyScopeSchema = {
|
|
16
|
+
clientId: { type: String, required: false }
|
|
17
|
+
};
|
|
18
|
+
const accessPolicySchema = {
|
|
19
|
+
type: { type: String, required: true },
|
|
20
|
+
captchaType: { type: String, required: false },
|
|
21
|
+
description: { type: String, required: false },
|
|
22
|
+
solvedImagesCount: { type: Number, required: false },
|
|
23
|
+
imageThreshold: { type: Number, required: false },
|
|
24
|
+
powDifficulty: { type: Number, required: false },
|
|
25
|
+
unsolvedImagesCount: { type: Number, required: false },
|
|
26
|
+
frictionlessScore: { type: Number, required: false }
|
|
27
|
+
};
|
|
28
|
+
const accessRuleMongooseSchema = {
|
|
29
|
+
...accessPolicySchema,
|
|
30
|
+
...policyScopeSchema,
|
|
31
|
+
...userScopeSchema,
|
|
32
|
+
ruleGroupId: { type: String, required: false }
|
|
33
|
+
};
|
|
34
|
+
export {
|
|
35
|
+
accessRuleMongooseSchema
|
|
36
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { REDIS_QUERY_DIALECT } from "./redisRulesQuery.js";
|
|
3
|
+
import { parseRedisRecords, REDIS_BATCH_SIZE } from "../redisClient.js";
|
|
4
|
+
import { ACCESS_RULES_REDIS_INDEX_NAME } from "../redisRuleIndex.js";
|
|
5
|
+
const aggregateRedisKeys = async (client, query, logger, batchHandler) => {
|
|
6
|
+
const keyField = "__key";
|
|
7
|
+
const recordSchema = z.object({
|
|
8
|
+
// it's a reserved name for the record key
|
|
9
|
+
[keyField]: z.string()
|
|
10
|
+
});
|
|
11
|
+
const foundKeys = [];
|
|
12
|
+
const addRecordKeys = async (records) => {
|
|
13
|
+
const parsedRecords = parseRedisRecords(records, recordSchema, logger);
|
|
14
|
+
const recordKeys = parsedRecords.map((record) => record[keyField]);
|
|
15
|
+
if (batchHandler) {
|
|
16
|
+
await batchHandler(recordKeys);
|
|
17
|
+
} else {
|
|
18
|
+
foundKeys.push(...recordKeys);
|
|
19
|
+
logger.debug(() => ({
|
|
20
|
+
msg: "Processed aggregation batch",
|
|
21
|
+
data: {
|
|
22
|
+
size: recordKeys.length
|
|
23
|
+
}
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
await executeAggregation(
|
|
28
|
+
client,
|
|
29
|
+
query,
|
|
30
|
+
{
|
|
31
|
+
// #2 is a required option when the 'ismissing()' function is in the query body
|
|
32
|
+
DIALECT: REDIS_QUERY_DIALECT,
|
|
33
|
+
COUNT: REDIS_BATCH_SIZE,
|
|
34
|
+
LOAD: `@${keyField}`
|
|
35
|
+
},
|
|
36
|
+
addRecordKeys
|
|
37
|
+
);
|
|
38
|
+
return foundKeys;
|
|
39
|
+
};
|
|
40
|
+
const executeAggregation = async (client, query, aggregateOptions, handleBatch) => {
|
|
41
|
+
const initialReply = await client.ft.aggregateWithCursor(
|
|
42
|
+
ACCESS_RULES_REDIS_INDEX_NAME,
|
|
43
|
+
query,
|
|
44
|
+
aggregateOptions
|
|
45
|
+
);
|
|
46
|
+
await handleBatch(initialReply.results);
|
|
47
|
+
let cursor = initialReply.cursor;
|
|
48
|
+
while (0 !== cursor) {
|
|
49
|
+
const batchReply = await client.ft.cursorRead(
|
|
50
|
+
ACCESS_RULES_REDIS_INDEX_NAME,
|
|
51
|
+
cursor,
|
|
52
|
+
{ COUNT: aggregateOptions.COUNT }
|
|
53
|
+
);
|
|
54
|
+
await handleBatch(batchReply.results);
|
|
55
|
+
cursor = batchReply.cursor;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
export {
|
|
59
|
+
aggregateRedisKeys
|
|
60
|
+
};
|