@prosopo/user-access-policy 3.2.1 → 3.3.1
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 +28 -0
- package/dist/accessPolicy.js +72 -57
- package/dist/accessPolicyResolver.js +62 -36
- package/dist/accessRules.js +9 -6
- package/dist/api/accessRuleApiRoutes.js +73 -50
- package/dist/api/deleteAllRulesEndpoint.js +22 -19
- package/dist/api/deleteRulesEndpoint.js +30 -27
- package/dist/api/insertRulesEndpoint.js +57 -57
- package/dist/cjs/accessPolicy.cjs +4 -3
- package/dist/cjs/redis/redisAccessRules.cjs +21 -15
- package/dist/cjs/redis/redisAccessRulesIndex.cjs +7 -2
- package/dist/index.js +27 -14
- package/dist/redis/redisAccessRules.js +128 -105
- package/dist/redis/redisAccessRulesIndex.js +95 -68
- package/dist/redis/redisIndex.js +15 -16
- package/dist/util.js +4 -2
- package/package.json +15 -10
- package/vite.cjs.config.ts +7 -6
- package/vite.esm.config.ts +20 -0
- package/vite.test.config.ts +16 -21
- package/dist/accessPolicy.d.ts +0 -169
- package/dist/accessPolicy.d.ts.map +0 -1
- package/dist/accessPolicy.js.map +0 -1
- package/dist/accessPolicyResolver.d.ts +0 -115
- package/dist/accessPolicyResolver.d.ts.map +0 -1
- package/dist/accessPolicyResolver.js.map +0 -1
- package/dist/accessRules.d.ts +0 -16
- package/dist/accessRules.d.ts.map +0 -1
- package/dist/accessRules.js.map +0 -1
- package/dist/api/accessRuleApiRoutes.d.ts +0 -27
- package/dist/api/accessRuleApiRoutes.d.ts.map +0 -1
- package/dist/api/accessRuleApiRoutes.js.map +0 -1
- package/dist/api/deleteAllRulesEndpoint.d.ts +0 -12
- package/dist/api/deleteAllRulesEndpoint.d.ts.map +0 -1
- package/dist/api/deleteAllRulesEndpoint.js.map +0 -1
- package/dist/api/deleteRulesEndpoint.d.ts +0 -116
- package/dist/api/deleteRulesEndpoint.d.ts.map +0 -1
- package/dist/api/deleteRulesEndpoint.js.map +0 -1
- package/dist/api/insertRulesEndpoint.d.ts +0 -22
- package/dist/api/insertRulesEndpoint.d.ts.map +0 -1
- package/dist/api/insertRulesEndpoint.js.map +0 -1
- package/dist/index.d.ts +0 -15
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/redis/redisAccessRules.d.ts +0 -7
- package/dist/redis/redisAccessRules.d.ts.map +0 -1
- package/dist/redis/redisAccessRules.js.map +0 -1
- package/dist/redis/redisAccessRulesIndex.d.ts +0 -12
- package/dist/redis/redisAccessRulesIndex.d.ts.map +0 -1
- package/dist/redis/redisAccessRulesIndex.js.map +0 -1
- package/dist/redis/redisIndex.d.ts +0 -9
- package/dist/redis/redisIndex.d.ts.map +0 -1
- package/dist/redis/redisIndex.js.map +0 -1
- package/dist/tests/accessPolicy.test.d.ts +0 -2
- package/dist/tests/accessPolicy.test.d.ts.map +0 -1
- package/dist/tests/accessPolicy.test.js +0 -27
- package/dist/tests/accessPolicy.test.js.map +0 -1
- package/dist/tests/redis/redisAccessRules.test.d.ts +0 -2
- package/dist/tests/redis/redisAccessRules.test.d.ts.map +0 -1
- package/dist/tests/redis/redisAccessRules.test.js +0 -400
- package/dist/tests/redis/redisAccessRules.test.js.map +0 -1
- package/dist/tests/redis/redisIndex.test.d.ts +0 -2
- package/dist/tests/redis/redisIndex.test.d.ts.map +0 -1
- package/dist/tests/redis/redisIndex.test.js +0 -84
- package/dist/tests/redis/redisIndex.test.js.map +0 -1
- package/dist/tests/redis/testRedisClient.d.ts +0 -3
- package/dist/tests/redis/testRedisClient.d.ts.map +0 -1
- package/dist/tests/redis/testRedisClient.js +0 -8
- package/dist/tests/redis/testRedisClient.js.map +0 -1
- package/dist/tests/testLogger.d.ts +0 -4
- package/dist/tests/testLogger.d.ts.map +0 -1
- package/dist/tests/testLogger.js +0 -22
- package/dist/tests/testLogger.js.map +0 -1
- package/dist/util.d.ts +0 -2
- package/dist/util.d.ts.map +0 -1
- package/dist/util.js.map +0 -1
- package/vite.config.ts +0 -39
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const types = require("@prosopo/types");
|
|
4
|
+
const util = require("@prosopo/util");
|
|
4
5
|
const ipAddress = require("ip-address");
|
|
5
6
|
const zod = require("zod");
|
|
6
|
-
const util = require("./util.cjs");
|
|
7
|
+
const util$1 = require("./util.cjs");
|
|
7
8
|
var AccessPolicyType = /* @__PURE__ */ ((AccessPolicyType2) => {
|
|
8
9
|
AccessPolicyType2["Block"] = "block";
|
|
9
10
|
AccessPolicyType2["Restrict"] = "restrict";
|
|
@@ -49,7 +50,7 @@ const userScopeInputSchema = userScopeSchema.extend({
|
|
|
49
50
|
}).transform((inputUserScope) => {
|
|
50
51
|
const { ip, ipMask, userAgent, ...userScope } = inputUserScope;
|
|
51
52
|
if ("string" === typeof ip) {
|
|
52
|
-
userScope.numericIp =
|
|
53
|
+
userScope.numericIp = util.getIPAddress(ip).bigInt();
|
|
53
54
|
}
|
|
54
55
|
if ("string" === typeof ipMask) {
|
|
55
56
|
const ipObject = new ipAddress.Address4(ipMask);
|
|
@@ -57,7 +58,7 @@ const userScopeInputSchema = userScopeSchema.extend({
|
|
|
57
58
|
userScope.numericIpMaskMax = ipObject.endAddress().bigInt();
|
|
58
59
|
}
|
|
59
60
|
if ("string" === typeof userAgent) {
|
|
60
|
-
userScope.userAgentHash = util.hashUserAgent(userAgent);
|
|
61
|
+
userScope.userAgentHash = util$1.hashUserAgent(userAgent);
|
|
61
62
|
}
|
|
62
63
|
return userScope;
|
|
63
64
|
});
|
|
@@ -22,8 +22,11 @@ function _interopNamespaceDefault(e) {
|
|
|
22
22
|
const util__namespace = /* @__PURE__ */ _interopNamespaceDefault(util);
|
|
23
23
|
const createRedisAccessRulesReader = (client, logger) => {
|
|
24
24
|
return {
|
|
25
|
-
findRules: async (filter) => {
|
|
25
|
+
findRules: async (filter, skipEmptyUserScopes = true) => {
|
|
26
26
|
const query = redisAccessRulesIndex.getRedisAccessRulesQuery(filter);
|
|
27
|
+
if (skipEmptyUserScopes && query === "ismissing(@clientId)") {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
27
30
|
let searchReply;
|
|
28
31
|
try {
|
|
29
32
|
searchReply = await client.ft.search(
|
|
@@ -31,19 +34,21 @@ const createRedisAccessRulesReader = (client, logger) => {
|
|
|
31
34
|
query,
|
|
32
35
|
redisAccessRulesIndex.accessRulesRedisSearchOptions
|
|
33
36
|
);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
if (searchReply.total > 0) {
|
|
38
|
+
logger.debug(() => ({
|
|
39
|
+
msg: "Executed search query",
|
|
40
|
+
data: {
|
|
41
|
+
inspect: util__namespace.inspect(
|
|
42
|
+
{
|
|
43
|
+
filter,
|
|
44
|
+
searchReply,
|
|
45
|
+
query
|
|
46
|
+
},
|
|
47
|
+
{ depth: null }
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
47
52
|
} catch (e) {
|
|
48
53
|
logger.error(() => ({
|
|
49
54
|
err: e,
|
|
@@ -115,7 +120,8 @@ const createRedisAccessRulesWriter = (client) => {
|
|
|
115
120
|
deleteRules: async (ruleIds) => void await client.del(ruleIds),
|
|
116
121
|
deleteAllRules: async () => {
|
|
117
122
|
const keys = await client.keys(`${redisAccessRulesIndex.accessRuleRedisKeyPrefix}*`);
|
|
118
|
-
|
|
123
|
+
if (keys.length === 0) return 0;
|
|
124
|
+
return await client.del(keys);
|
|
119
125
|
}
|
|
120
126
|
};
|
|
121
127
|
};
|
|
@@ -7,7 +7,7 @@ const redisIndex = require("./redisIndex.cjs");
|
|
|
7
7
|
const accessRulesRedisIndexName = "index:user-access-rules";
|
|
8
8
|
const accessRuleRedisKeyPrefix = "uar:";
|
|
9
9
|
const accessRuleContentHashAlgorithm = "md5";
|
|
10
|
-
const DEFAULT_SEARCH_LIMIT =
|
|
10
|
+
const DEFAULT_SEARCH_LIMIT = 1e3;
|
|
11
11
|
const accessRulesIndex = {
|
|
12
12
|
name: accessRulesRedisIndexName,
|
|
13
13
|
/**
|
|
@@ -35,7 +35,7 @@ const accessRulesIndex = {
|
|
|
35
35
|
// the satisfy statement is to guarantee that the keys are right
|
|
36
36
|
options: {
|
|
37
37
|
ON: "HASH",
|
|
38
|
-
PREFIX: accessRuleRedisKeyPrefix
|
|
38
|
+
PREFIX: [accessRuleRedisKeyPrefix]
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
const createRedisAccessRulesIndex = async (client) => redisIndex.createRedisIndex(client, accessRulesIndex);
|
|
@@ -48,6 +48,10 @@ const greedyFieldComparisons = {
|
|
|
48
48
|
numericIp: (value) => `( @numericIp:[${value}] | ( @numericIpMaskMin:[-inf ${value}] @numericIpMaskMax:[${value} +inf] ) )`
|
|
49
49
|
};
|
|
50
50
|
const accessRulesRedisSearchOptions = {
|
|
51
|
+
// #2 is a required option when the 'ismissing()' function is in the query body
|
|
52
|
+
DIALECT: 2
|
|
53
|
+
};
|
|
54
|
+
const accessRulesRedisDeleteOptions = {
|
|
51
55
|
// #2 is a required option when the 'ismissing()' function is in the query body
|
|
52
56
|
DIALECT: 2,
|
|
53
57
|
LIMIT: {
|
|
@@ -100,6 +104,7 @@ const getRedisAccessRuleValue = (rule) => Object.fromEntries(
|
|
|
100
104
|
Object.entries(rule).map(([key, value]) => [key, String(value)])
|
|
101
105
|
);
|
|
102
106
|
exports.accessRuleRedisKeyPrefix = accessRuleRedisKeyPrefix;
|
|
107
|
+
exports.accessRulesRedisDeleteOptions = accessRulesRedisDeleteOptions;
|
|
103
108
|
exports.accessRulesRedisIndexName = accessRulesRedisIndexName;
|
|
104
109
|
exports.accessRulesRedisSearchOptions = accessRulesRedisSearchOptions;
|
|
105
110
|
exports.createRedisAccessRulesIndex = createRedisAccessRulesIndex;
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
|
-
import { AccessPolicyType, accessPolicySchema, policyScopeSchema, } from "
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { deleteAllRulesEndpointSchema } from "
|
|
6
|
-
import { deleteRulesEndpointSchema
|
|
7
|
-
import { insertRulesEndpointSchema
|
|
8
|
-
import { createRedisAccessRulesStorage } from "
|
|
9
|
-
import { createRedisAccessRulesIndex } from "
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { AccessPolicyType, accessPolicySchema, policyScopeSchema, userScopeInputSchema } from "./accessPolicy.js";
|
|
2
|
+
import { ScopeMatch, createAccessPolicyResolver } from "./accessPolicyResolver.js";
|
|
3
|
+
import { AccessRuleApiRoutes } from "./api/accessRuleApiRoutes.js";
|
|
4
|
+
import { accessRuleApiPaths, getExpressApiRuleRateLimits } from "./api/accessRuleApiRoutes.js";
|
|
5
|
+
import { deleteAllRulesEndpointSchema } from "./api/deleteAllRulesEndpoint.js";
|
|
6
|
+
import { deleteRulesEndpointSchema } from "./api/deleteRulesEndpoint.js";
|
|
7
|
+
import { insertRulesEndpointSchema } from "./api/insertRulesEndpoint.js";
|
|
8
|
+
import { createRedisAccessRulesStorage } from "./redis/redisAccessRules.js";
|
|
9
|
+
import { createRedisAccessRulesIndex } from "./redis/redisAccessRulesIndex.js";
|
|
10
|
+
const createApiRuleRoutesProvider = (rulesStorage) => {
|
|
11
|
+
return new AccessRuleApiRoutes(rulesStorage);
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
AccessPolicyType,
|
|
15
|
+
ScopeMatch,
|
|
16
|
+
accessPolicySchema,
|
|
17
|
+
accessRuleApiPaths,
|
|
18
|
+
createAccessPolicyResolver,
|
|
19
|
+
createApiRuleRoutesProvider,
|
|
20
|
+
createRedisAccessRulesIndex,
|
|
21
|
+
createRedisAccessRulesStorage,
|
|
22
|
+
deleteAllRulesEndpointSchema,
|
|
23
|
+
deleteRulesEndpointSchema,
|
|
24
|
+
getExpressApiRuleRateLimits,
|
|
25
|
+
insertRulesEndpointSchema,
|
|
26
|
+
policyScopeSchema,
|
|
27
|
+
userScopeInputSchema
|
|
13
28
|
};
|
|
14
|
-
export { createAccessPolicyResolver, AccessPolicyType, ScopeMatch, createRedisAccessRulesIndex, createRedisAccessRulesStorage, accessRuleApiPaths, accessPolicySchema, policyScopeSchema, insertRulesEndpointSchema, deleteAllRulesEndpointSchema, deleteRulesEndpointSchema, getExpressApiRuleRateLimits, userScopeInputSchema, };
|
|
15
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,112 +1,135 @@
|
|
|
1
1
|
import * as util from "node:util";
|
|
2
|
-
import { accessRuleSchema
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
2
|
+
import { accessRuleSchema } from "../accessRules.js";
|
|
3
|
+
import { getRedisAccessRulesQuery, accessRulesRedisIndexName, accessRulesRedisSearchOptions, accessRuleRedisKeyPrefix, getRedisAccessRuleKey, getRedisAccessRuleValue } from "./redisAccessRulesIndex.js";
|
|
4
|
+
const createRedisAccessRulesReader = (client, logger) => {
|
|
5
|
+
return {
|
|
6
|
+
findRules: async (filter, skipEmptyUserScopes = true) => {
|
|
7
|
+
const query = getRedisAccessRulesQuery(filter);
|
|
8
|
+
if (skipEmptyUserScopes && query === "ismissing(@clientId)") {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
let searchReply;
|
|
12
|
+
try {
|
|
13
|
+
searchReply = await client.ft.search(
|
|
14
|
+
accessRulesRedisIndexName,
|
|
15
|
+
query,
|
|
16
|
+
accessRulesRedisSearchOptions
|
|
17
|
+
);
|
|
18
|
+
if (searchReply.total > 0) {
|
|
19
|
+
logger.debug(() => ({
|
|
20
|
+
msg: "Executed search query",
|
|
21
|
+
data: {
|
|
22
|
+
inspect: util.inspect(
|
|
23
|
+
{
|
|
24
|
+
filter,
|
|
25
|
+
searchReply,
|
|
26
|
+
query
|
|
27
|
+
},
|
|
28
|
+
{ depth: null }
|
|
29
|
+
)
|
|
21
30
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
logger.error(() => ({
|
|
35
|
+
err: e,
|
|
36
|
+
data: {
|
|
37
|
+
inspect: util.inspect(
|
|
38
|
+
{
|
|
39
|
+
query,
|
|
40
|
+
filter
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
depth: null
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
},
|
|
47
|
+
msg: "failed to execute search query"
|
|
48
|
+
}));
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return extractAccessRulesFromSearchReply(searchReply, logger);
|
|
52
|
+
},
|
|
53
|
+
findRuleIds: async (filter) => {
|
|
54
|
+
const query = getRedisAccessRulesQuery(filter);
|
|
55
|
+
let searchReply;
|
|
56
|
+
try {
|
|
57
|
+
searchReply = await client.ft.searchNoContent(
|
|
58
|
+
accessRulesRedisIndexName,
|
|
59
|
+
query,
|
|
60
|
+
accessRulesRedisSearchOptions
|
|
61
|
+
);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
logger.error(() => ({
|
|
64
|
+
err: e,
|
|
65
|
+
data: {
|
|
66
|
+
inspect: util.inspect(
|
|
67
|
+
{
|
|
68
|
+
query,
|
|
69
|
+
filter
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
depth: null
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
},
|
|
76
|
+
msg: "Failed to execute search query for rule IDs"
|
|
77
|
+
}));
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
return searchReply.documents;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
63
83
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
const createRedisAccessRulesWriter = (client) => {
|
|
85
|
+
return {
|
|
86
|
+
insertRule: async (rule, expirationTimestamp) => {
|
|
87
|
+
const ruleKey = getRedisAccessRuleKey(rule);
|
|
88
|
+
const ruleValue = getRedisAccessRuleValue(rule);
|
|
89
|
+
await client.hSet(ruleKey, ruleValue);
|
|
90
|
+
if (expirationTimestamp) {
|
|
91
|
+
const expiryDate = new Date(expirationTimestamp);
|
|
92
|
+
if (expiryDate.getUTCFullYear() === 1970) {
|
|
93
|
+
await client.expireAt(ruleKey, expirationTimestamp);
|
|
94
|
+
} else {
|
|
95
|
+
const timestampInSeconds = Math.floor(expirationTimestamp / 1e3);
|
|
96
|
+
await client.expireAt(ruleKey, timestampInSeconds);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return ruleKey;
|
|
100
|
+
},
|
|
101
|
+
deleteRules: async (ruleIds) => void await client.del(ruleIds),
|
|
102
|
+
deleteAllRules: async () => {
|
|
103
|
+
const keys = await client.keys(`${accessRuleRedisKeyPrefix}*`);
|
|
104
|
+
if (keys.length === 0) return 0;
|
|
105
|
+
return await client.del(keys);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
88
108
|
};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
const createRedisAccessRulesStorage = (client, logger) => {
|
|
110
|
+
return {
|
|
111
|
+
...createRedisAccessRulesReader(client, logger),
|
|
112
|
+
...createRedisAccessRulesWriter(client)
|
|
113
|
+
};
|
|
94
114
|
};
|
|
95
115
|
const extractAccessRulesFromSearchReply = (searchReply, logger) => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
const accessRules = [];
|
|
117
|
+
searchReply.documents.map(({ id, value: document }) => {
|
|
118
|
+
const parsedDocument = accessRuleSchema.safeParse(document);
|
|
119
|
+
if (parsedDocument.success) {
|
|
120
|
+
accessRules.push(parsedDocument.data);
|
|
121
|
+
} else {
|
|
122
|
+
logger.debug(() => ({
|
|
123
|
+
msg: "Failed to parse access rule from search reply",
|
|
124
|
+
id,
|
|
125
|
+
error: parsedDocument.error
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return accessRules;
|
|
130
|
+
};
|
|
131
|
+
export {
|
|
132
|
+
createRedisAccessRulesReader,
|
|
133
|
+
createRedisAccessRulesStorage,
|
|
134
|
+
createRedisAccessRulesWriter
|
|
111
135
|
};
|
|
112
|
-
//# sourceMappingURL=redisAccessRules.js.map
|
|
@@ -1,86 +1,113 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { SCHEMA_FIELD_TYPE } from "@redis/search";
|
|
3
|
-
import { ScopeMatch } from "
|
|
4
|
-
import { createRedisIndex } from "
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { ScopeMatch } from "../accessPolicyResolver.js";
|
|
4
|
+
import { createRedisIndex } from "./redisIndex.js";
|
|
5
|
+
const accessRulesRedisIndexName = "index:user-access-rules";
|
|
6
|
+
const accessRuleRedisKeyPrefix = "uar:";
|
|
7
7
|
const accessRuleContentHashAlgorithm = "md5";
|
|
8
|
-
const DEFAULT_SEARCH_LIMIT =
|
|
8
|
+
const DEFAULT_SEARCH_LIMIT = 1e3;
|
|
9
9
|
const accessRulesIndex = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
options: {
|
|
25
|
-
ON: "HASH",
|
|
26
|
-
PREFIX: accessRuleRedisKeyPrefix,
|
|
10
|
+
name: accessRulesRedisIndexName,
|
|
11
|
+
/**
|
|
12
|
+
* Note on the field type decision
|
|
13
|
+
*
|
|
14
|
+
* TAG is designed for the exact value matching
|
|
15
|
+
* TEXT is designed for the word-based and pattern matching
|
|
16
|
+
*
|
|
17
|
+
* For our goal TAG fits perfectly and, more performant
|
|
18
|
+
*/
|
|
19
|
+
schema: {
|
|
20
|
+
clientId: {
|
|
21
|
+
type: SCHEMA_FIELD_TYPE.TAG,
|
|
22
|
+
// necessary to make possible use of the ismissing() function on this field in the search
|
|
23
|
+
INDEXMISSING: true
|
|
27
24
|
},
|
|
25
|
+
numericIpMaskMin: SCHEMA_FIELD_TYPE.NUMERIC,
|
|
26
|
+
numericIpMaskMax: SCHEMA_FIELD_TYPE.NUMERIC,
|
|
27
|
+
userId: SCHEMA_FIELD_TYPE.TAG,
|
|
28
|
+
numericIp: { type: SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
|
|
29
|
+
ja4Hash: SCHEMA_FIELD_TYPE.TAG,
|
|
30
|
+
headersHash: SCHEMA_FIELD_TYPE.TAG,
|
|
31
|
+
userAgentHash: SCHEMA_FIELD_TYPE.TAG
|
|
32
|
+
},
|
|
33
|
+
// the satisfy statement is to guarantee that the keys are right
|
|
34
|
+
options: {
|
|
35
|
+
ON: "HASH",
|
|
36
|
+
PREFIX: [accessRuleRedisKeyPrefix]
|
|
37
|
+
}
|
|
28
38
|
};
|
|
29
|
-
|
|
39
|
+
const createRedisAccessRulesIndex = async (client) => createRedisIndex(client, accessRulesIndex);
|
|
30
40
|
const numericIndexFields = [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
"numericIp",
|
|
42
|
+
"numericIpMaskMin",
|
|
43
|
+
"numericIpMaskMax"
|
|
34
44
|
];
|
|
35
45
|
const greedyFieldComparisons = {
|
|
36
|
-
|
|
46
|
+
numericIp: (value) => `( @numericIp:[${value}] | ( @numericIpMaskMin:[-inf ${value}] @numericIpMaskMax:[${value} +inf] ) )`
|
|
37
47
|
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
from: 0,
|
|
42
|
-
size: DEFAULT_SEARCH_LIMIT,
|
|
43
|
-
},
|
|
48
|
+
const accessRulesRedisSearchOptions = {
|
|
49
|
+
// #2 is a required option when the 'ismissing()' function is in the query body
|
|
50
|
+
DIALECT: 2
|
|
44
51
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const accessRulesRedisDeleteOptions = {
|
|
53
|
+
// #2 is a required option when the 'ismissing()' function is in the query body
|
|
54
|
+
DIALECT: 2,
|
|
55
|
+
LIMIT: {
|
|
56
|
+
from: 0,
|
|
57
|
+
size: DEFAULT_SEARCH_LIMIT
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const getRedisAccessRulesQuery = (filter) => {
|
|
61
|
+
const { policyScope, userScope } = filter;
|
|
62
|
+
const policyScopeFilter = getPolicyScopeQuery(
|
|
63
|
+
policyScope,
|
|
64
|
+
filter.policyScopeMatch
|
|
65
|
+
);
|
|
66
|
+
if (userScope && Object.keys(userScope).length > 0) {
|
|
67
|
+
const userScopeFilter = getUserScopeQuery(userScope, filter.userScopeMatch);
|
|
68
|
+
return `${policyScopeFilter} ( ${userScopeFilter} )`;
|
|
69
|
+
}
|
|
70
|
+
return policyScopeFilter ? policyScopeFilter : "*";
|
|
53
71
|
};
|
|
54
72
|
const getPolicyScopeQuery = (policyScope, scopeMatchType) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
return ScopeMatch.Exact === scopeMatchType ? "ismissing(@clientId)" : "";
|
|
73
|
+
const clientId = policyScope?.clientId;
|
|
74
|
+
if ("string" === typeof clientId) {
|
|
75
|
+
return ScopeMatch.Exact === scopeMatchType ? `@clientId:{${clientId}}` : `( @clientId:{${clientId}} | ismissing(@clientId) )`;
|
|
76
|
+
}
|
|
77
|
+
return ScopeMatch.Exact === scopeMatchType ? "ismissing(@clientId)" : "";
|
|
62
78
|
};
|
|
63
79
|
const getUserScopeQuery = (userScope, scopeMatchType) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.join(scopeJoinType);
|
|
80
|
+
const scopeEntries = Object.entries(userScope).filter(([_, value]) => value !== void 0);
|
|
81
|
+
const scopeJoinType = ScopeMatch.Exact === scopeMatchType ? " " : " | ";
|
|
82
|
+
return scopeEntries.map(
|
|
83
|
+
([scopeFieldName, scopeFieldValue]) => getUserScopeFieldQuery(scopeFieldName, scopeFieldValue, scopeMatchType)
|
|
84
|
+
).join(scopeJoinType);
|
|
70
85
|
};
|
|
71
86
|
const getUserScopeFieldQuery = (fieldName, fieldValue, matchType) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
if (ScopeMatch.Greedy === matchType && "function" === typeof greedyFieldComparisons[fieldName]) {
|
|
88
|
+
return greedyFieldComparisons[fieldName](fieldValue);
|
|
89
|
+
}
|
|
90
|
+
return numericIndexFields.includes(fieldName) ? `@${fieldName}:[${fieldValue}]` : `@${fieldName}:{${fieldValue}}`;
|
|
91
|
+
};
|
|
92
|
+
const getRedisAccessRuleKey = (rule) => accessRuleRedisKeyPrefix + crypto.createHash(accessRuleContentHashAlgorithm).update(
|
|
93
|
+
JSON.stringify(
|
|
94
|
+
rule,
|
|
95
|
+
(key, value) => (
|
|
96
|
+
// JSON.stringify can't handle BigInt itself: throws "Do not know how to serialize a BigInt"
|
|
97
|
+
"bigint" === typeof value ? value.toString() : value
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
).digest("hex");
|
|
101
|
+
const getRedisAccessRuleValue = (rule) => Object.fromEntries(
|
|
102
|
+
Object.entries(rule).map(([key, value]) => [key, String(value)])
|
|
103
|
+
);
|
|
104
|
+
export {
|
|
105
|
+
accessRuleRedisKeyPrefix,
|
|
106
|
+
accessRulesRedisDeleteOptions,
|
|
107
|
+
accessRulesRedisIndexName,
|
|
108
|
+
accessRulesRedisSearchOptions,
|
|
109
|
+
createRedisAccessRulesIndex,
|
|
110
|
+
getRedisAccessRuleKey,
|
|
111
|
+
getRedisAccessRuleValue,
|
|
112
|
+
getRedisAccessRulesQuery
|
|
79
113
|
};
|
|
80
|
-
export const getRedisAccessRuleKey = (rule) => accessRuleRedisKeyPrefix +
|
|
81
|
-
crypto
|
|
82
|
-
.createHash(accessRuleContentHashAlgorithm)
|
|
83
|
-
.update(JSON.stringify(rule, (key, value) => "bigint" === typeof value ? value.toString() : value))
|
|
84
|
-
.digest("hex");
|
|
85
|
-
export const getRedisAccessRuleValue = (rule) => Object.fromEntries(Object.entries(rule).map(([key, value]) => [key, String(value)]));
|
|
86
|
-
//# sourceMappingURL=redisAccessRulesIndex.js.map
|
package/dist/redis/redisIndex.js
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
const redisIndexHashesRecordKey = "_index_hashes";
|
|
3
3
|
const redisIndexHashAlgorithm = "sha256";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
await client.ft.dropIndex(index.name);
|
|
4
|
+
const createRedisIndex = async (client, index) => {
|
|
5
|
+
const indexHash = createIndexHash(index);
|
|
6
|
+
const existingIndexes = await client.ft._LIST();
|
|
7
|
+
if (existingIndexes.includes(index.name)) {
|
|
8
|
+
const existingIndexHash = await fetchIndexHash(client, index.name);
|
|
9
|
+
if (indexHash === existingIndexHash) {
|
|
10
|
+
return;
|
|
13
11
|
}
|
|
14
|
-
await client.ft.
|
|
15
|
-
|
|
12
|
+
await client.ft.dropIndex(index.name);
|
|
13
|
+
}
|
|
14
|
+
await client.ft.create(index.name, index.schema, index.options);
|
|
15
|
+
await saveIndexHash(client, index.name, indexHash);
|
|
16
16
|
};
|
|
17
|
-
const createIndexHash = (index) => crypto
|
|
18
|
-
.createHash(redisIndexHashAlgorithm)
|
|
19
|
-
.update(JSON.stringify(index))
|
|
20
|
-
.digest("hex");
|
|
17
|
+
const createIndexHash = (index) => crypto.createHash(redisIndexHashAlgorithm).update(JSON.stringify(index)).digest("hex");
|
|
21
18
|
const fetchIndexHash = async (client, indexName) => client.hGet(redisIndexHashesRecordKey, indexName);
|
|
22
19
|
const saveIndexHash = async (client, indexName, indexHash) => client.hSet(redisIndexHashesRecordKey, indexName, indexHash);
|
|
23
|
-
|
|
20
|
+
export {
|
|
21
|
+
createRedisIndex
|
|
22
|
+
};
|
package/dist/util.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
const hashUserAgent = (userAgent) => crypto.createHash("sha256").update(userAgent).digest("hex");
|
|
3
|
+
export {
|
|
4
|
+
hashUserAgent
|
|
5
|
+
};
|