@prosopo/user-access-policy 3.4.0 → 3.5.19

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 CHANGED
@@ -1,5 +1,247 @@
1
1
  # @prosopo/user-access-policy
2
2
 
3
+ ## 3.5.19
4
+ ### Patch Changes
5
+
6
+ - @prosopo/api@3.1.24
7
+ - @prosopo/api-route@2.6.28
8
+ - @prosopo/common@3.1.20
9
+ - @prosopo/redis-client@1.0.5
10
+ - @prosopo/types@3.5.3
11
+ - @prosopo/util@3.1.5
12
+
13
+ ## 3.5.18
14
+ ### Patch Changes
15
+
16
+ - 5659b24: Release 3.4.4
17
+ - Updated dependencies [f912439]
18
+ - Updated dependencies [5659b24]
19
+ - @prosopo/common@3.1.19
20
+ - @prosopo/redis-client@1.0.4
21
+ - @prosopo/api-route@2.6.27
22
+ - @prosopo/types@3.5.2
23
+ - @prosopo/util@3.1.4
24
+ - @prosopo/api@3.1.23
25
+
26
+ ## 3.5.17
27
+ ### Patch Changes
28
+
29
+ - 50c4120: Release 3.4.3
30
+ - Updated dependencies [52cd544]
31
+ - Updated dependencies [b117ba3]
32
+ - Updated dependencies [50c4120]
33
+ - @prosopo/types@3.5.1
34
+ - @prosopo/redis-client@1.0.3
35
+ - @prosopo/api-route@2.6.26
36
+ - @prosopo/common@3.1.18
37
+ - @prosopo/util@3.1.3
38
+ - @prosopo/api@3.1.22
39
+
40
+ ## 3.5.16
41
+ ### Patch Changes
42
+
43
+ - 618703f: Release 3.4.2
44
+ - Updated dependencies [618703f]
45
+ - Updated dependencies [e20ad6b]
46
+ - @prosopo/redis-client@1.0.2
47
+ - @prosopo/api-route@2.6.25
48
+ - @prosopo/common@3.1.17
49
+ - @prosopo/types@3.5.0
50
+ - @prosopo/util@3.1.2
51
+ - @prosopo/api@3.1.21
52
+
53
+ ## 3.5.15
54
+ ### Patch Changes
55
+
56
+ - 11303d9: feat/pluggable-redis
57
+ - 11303d9: Release 3.4.0
58
+ - 18cb28b: Release 3.4.1
59
+ - 11303d9: feat/pluggable-redis
60
+ - Updated dependencies [11303d9]
61
+ - Updated dependencies [11303d9]
62
+ - Updated dependencies [18cb28b]
63
+ - Updated dependencies [11303d9]
64
+ - @prosopo/redis-client@1.0.1
65
+ - @prosopo/api-route@2.6.24
66
+ - @prosopo/common@3.1.16
67
+ - @prosopo/types@3.4.1
68
+ - @prosopo/util@3.1.1
69
+ - @prosopo/api@3.1.20
70
+
71
+ ## 3.5.14
72
+ ### Patch Changes
73
+
74
+ - f3f7aec: Release 3.4.0
75
+ - Updated dependencies [f3f7aec]
76
+ - Updated dependencies [6768f14]
77
+ - @prosopo/api-route@2.6.23
78
+ - @prosopo/common@3.1.15
79
+ - @prosopo/types@3.4.0
80
+ - @prosopo/util@3.1.0
81
+ - @prosopo/config@3.1.15
82
+
83
+ ## 3.5.13
84
+ ### Patch Changes
85
+
86
+ - Release 3.3.1
87
+ - 0824221: Release 3.2.4
88
+ - Updated dependencies [97edf3f]
89
+ - Updated dependencies
90
+ - Updated dependencies [0824221]
91
+ - @prosopo/types@3.3.0
92
+ - @prosopo/api-route@2.6.22
93
+ - @prosopo/common@3.1.14
94
+ - @prosopo/util@3.0.17
95
+ - @prosopo/config@3.1.14
96
+
97
+ ## 3.5.12
98
+ ### Patch Changes
99
+
100
+ - 008d112: Release 3.3.0
101
+ - Updated dependencies [509be28]
102
+ - Updated dependencies [008d112]
103
+ - @prosopo/types@3.2.1
104
+ - @prosopo/api-route@2.6.21
105
+ - @prosopo/common@3.1.13
106
+ - @prosopo/util@3.0.16
107
+ - @prosopo/config@3.1.13
108
+
109
+ ## 3.5.11
110
+ ### Patch Changes
111
+
112
+ - 0824221: Release 3.2.4
113
+ - Updated dependencies [cf48565]
114
+ - Updated dependencies [0824221]
115
+ - @prosopo/types@3.2.0
116
+ - @prosopo/api-route@2.6.20
117
+ - @prosopo/common@3.1.12
118
+ - @prosopo/util@3.0.15
119
+ - @prosopo/config@3.1.12
120
+
121
+ ## 3.5.10
122
+ ### Patch Changes
123
+
124
+ - 1a23649: Release 3.2.3
125
+ - Updated dependencies [0d1a33e]
126
+ - Updated dependencies [0d1a33e]
127
+ - Updated dependencies [1a23649]
128
+ - @prosopo/types@3.1.4
129
+ - @prosopo/api-route@2.6.19
130
+ - @prosopo/common@3.1.11
131
+ - @prosopo/util@3.0.14
132
+ - @prosopo/config@3.1.11
133
+
134
+ ## 3.5.9
135
+ ### Patch Changes
136
+
137
+ - 657a827: Release 3.2.2
138
+ - Updated dependencies [657a827]
139
+ - @prosopo/api-route@2.6.18
140
+ - @prosopo/common@3.1.10
141
+ - @prosopo/types@3.1.3
142
+ - @prosopo/util@3.0.13
143
+ - @prosopo/config@3.1.10
144
+
145
+ ## 3.5.8
146
+ ### Patch Changes
147
+
148
+ - 4440947: fix type-only tsc compilation
149
+ - 7bdaca6: Release 3.2.1
150
+ - Updated dependencies [4440947]
151
+ - Updated dependencies [7bdaca6]
152
+ - Updated dependencies [809b984]
153
+ - Updated dependencies [1249ce0]
154
+ - Updated dependencies [809b984]
155
+ - @prosopo/api-route@2.6.17
156
+ - @prosopo/common@3.1.9
157
+ - @prosopo/types@3.1.2
158
+ - @prosopo/util@3.0.12
159
+ - @prosopo/config@3.1.9
160
+
161
+ ## 3.5.7
162
+ ### Patch Changes
163
+
164
+ - 6fe8570: Release 3.2.0
165
+ - Updated dependencies [1f980c4]
166
+ - Updated dependencies [6fe8570]
167
+ - @prosopo/types@3.1.1
168
+ - @prosopo/api-route@2.6.16
169
+ - @prosopo/common@3.1.8
170
+ - @prosopo/util@3.0.11
171
+ - @prosopo/config@3.1.8
172
+
173
+ ## 3.5.6
174
+ ### Patch Changes
175
+
176
+ - f304be9: Release 3.1.13
177
+ - Updated dependencies [f304be9]
178
+ - Updated dependencies [8bdc7f0]
179
+ - @prosopo/api-route@2.6.15
180
+ - @prosopo/common@3.1.7
181
+ - @prosopo/types@3.1.0
182
+ - @prosopo/util@3.0.10
183
+ - @prosopo/config@3.1.7
184
+
185
+ ## 3.5.5
186
+ ### Patch Changes
187
+
188
+ - a07db04: Release 3.1.12
189
+ - Updated dependencies [9eed772]
190
+ - Updated dependencies [ebb0168]
191
+ - @prosopo/config@3.1.6
192
+ - @prosopo/util@3.0.9
193
+ - @prosopo/api-route@2.6.14
194
+ - @prosopo/common@3.1.6
195
+ - @prosopo/types@3.0.10
196
+
197
+ ## 3.5.4
198
+ ### Patch Changes
199
+
200
+ - 553025d: Index
201
+
202
+ ## 3.5.3
203
+ ### Patch Changes
204
+
205
+ - 6960643: lint detect missing and unneccessary imports
206
+ - Updated dependencies [6960643]
207
+ - @prosopo/api-route@2.6.13
208
+ - @prosopo/common@3.1.5
209
+ - @prosopo/types@3.0.9
210
+ - @prosopo/util@3.0.8
211
+
212
+ ## 3.5.2
213
+ ### Patch Changes
214
+
215
+ - Updated dependencies [30e7d4d]
216
+ - @prosopo/config@3.1.5
217
+ - @prosopo/api-route@2.6.12
218
+ - @prosopo/common@3.1.4
219
+ - @prosopo/types@3.0.8
220
+ - @prosopo/util@3.0.7
221
+
222
+ ## 3.5.1
223
+ ### Patch Changes
224
+
225
+ - 1f3a02f: Release 3.1.8
226
+
227
+ ## 3.5.0
228
+ ### Minor Changes
229
+
230
+ - e0628d9: Make sure rules don't leak between IPs
231
+
232
+ ## 3.4.1
233
+ ### Patch Changes
234
+
235
+ - a49b538: Extra tests
236
+ - e090e2f: More tests
237
+ - Updated dependencies [44ffda2]
238
+ - Updated dependencies [a49b538]
239
+ - @prosopo/config@3.1.4
240
+ - @prosopo/common@3.1.3
241
+ - @prosopo/api-route@2.6.11
242
+ - @prosopo/types@3.0.7
243
+ - @prosopo/util@3.0.6
244
+
3
245
  ## 3.4.0
4
246
  ### Minor Changes
5
247
 
@@ -0,0 +1,38 @@
1
+ import { ApiClient } from "@prosopo/api";
2
+ import { accessRuleApiPaths } from "./accessRuleApiRoutes.js";
3
+ class AccessRulesApiClient extends ApiClient {
4
+ insertMany(toInsert, timestamp, signature) {
5
+ return this.post(accessRuleApiPaths.INSERT_MANY, toInsert, {
6
+ headers: {
7
+ "Prosopo-Site-Key": this.account,
8
+ timestamp,
9
+ signature
10
+ }
11
+ });
12
+ }
13
+ deleteMany(toDelete, timestamp, signature) {
14
+ return this.post(accessRuleApiPaths.DELETE_MANY, toDelete, {
15
+ headers: {
16
+ "Prosopo-Site-Key": this.account,
17
+ timestamp,
18
+ signature
19
+ }
20
+ });
21
+ }
22
+ deleteAll(timestamp, signature) {
23
+ return this.post(
24
+ accessRuleApiPaths.DELETE_ALL,
25
+ {},
26
+ {
27
+ headers: {
28
+ "Prosopo-Site-Key": this.account,
29
+ timestamp,
30
+ signature
31
+ }
32
+ }
33
+ );
34
+ }
35
+ }
36
+ export {
37
+ AccessRulesApiClient
38
+ };
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const api = require("@prosopo/api");
4
+ const accessRuleApiRoutes = require("./accessRuleApiRoutes.cjs");
5
+ class AccessRulesApiClient extends api.ApiClient {
6
+ insertMany(toInsert, timestamp, signature) {
7
+ return this.post(accessRuleApiRoutes.accessRuleApiPaths.INSERT_MANY, toInsert, {
8
+ headers: {
9
+ "Prosopo-Site-Key": this.account,
10
+ timestamp,
11
+ signature
12
+ }
13
+ });
14
+ }
15
+ deleteMany(toDelete, timestamp, signature) {
16
+ return this.post(accessRuleApiRoutes.accessRuleApiPaths.DELETE_MANY, toDelete, {
17
+ headers: {
18
+ "Prosopo-Site-Key": this.account,
19
+ timestamp,
20
+ signature
21
+ }
22
+ });
23
+ }
24
+ deleteAll(timestamp, signature) {
25
+ return this.post(
26
+ accessRuleApiRoutes.accessRuleApiPaths.DELETE_ALL,
27
+ {},
28
+ {
29
+ headers: {
30
+ "Prosopo-Site-Key": this.account,
31
+ timestamp,
32
+ signature
33
+ }
34
+ }
35
+ );
36
+ }
37
+ }
38
+ exports.AccessRulesApiClient = AccessRulesApiClient;
@@ -2,12 +2,14 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const accessPolicy = require("./accessPolicy.cjs");
4
4
  const accessPolicyResolver = require("./accessPolicyResolver.cjs");
5
+ const accessRules = require("./accessRules.cjs");
5
6
  const accessRuleApiRoutes = require("./api/accessRuleApiRoutes.cjs");
6
7
  const deleteAllRulesEndpoint = require("./api/deleteAllRulesEndpoint.cjs");
7
8
  const deleteRulesEndpoint = require("./api/deleteRulesEndpoint.cjs");
8
9
  const insertRulesEndpoint = require("./api/insertRulesEndpoint.cjs");
9
- const redisAccessRules = require("./redis/redisAccessRules.cjs");
10
- const redisAccessRulesIndex = require("./redis/redisAccessRulesIndex.cjs");
10
+ const redisRulesStorage = require("./redis/redisRulesStorage.cjs");
11
+ const redisRulesIndex = require("./redis/redisRulesIndex.cjs");
12
+ const accessRulesApiClient = require("./api/accessRulesApiClient.cjs");
11
13
  const createApiRuleRoutesProvider = (rulesStorage) => {
12
14
  return new accessRuleApiRoutes.AccessRuleApiRoutes(rulesStorage);
13
15
  };
@@ -17,11 +19,13 @@ exports.accessRuleSchemaExtended = accessPolicy.accessRuleSchemaExtended;
17
19
  exports.policyScopeSchema = accessPolicy.policyScopeSchema;
18
20
  exports.userScopeInputSchema = accessPolicy.userScopeInputSchema;
19
21
  exports.ScopeMatch = accessPolicyResolver.ScopeMatch;
22
+ exports.accessRuleSchema = accessRules.accessRuleSchema;
20
23
  exports.accessRuleApiPaths = accessRuleApiRoutes.accessRuleApiPaths;
21
24
  exports.getExpressApiRuleRateLimits = accessRuleApiRoutes.getExpressApiRuleRateLimits;
22
25
  exports.deleteAllRulesEndpointSchema = deleteAllRulesEndpoint.deleteAllRulesEndpointSchema;
23
26
  exports.deleteRulesEndpointSchema = deleteRulesEndpoint.deleteRulesEndpointSchema;
24
27
  exports.insertRulesEndpointSchema = insertRulesEndpoint.insertRulesEndpointSchema;
25
- exports.createRedisAccessRulesStorage = redisAccessRules.createRedisAccessRulesStorage;
26
- exports.createRedisAccessRulesIndex = redisAccessRulesIndex.createRedisAccessRulesIndex;
28
+ exports.createRedisAccessRulesStorage = redisRulesStorage.createRedisAccessRulesStorage;
29
+ exports.redisAccessRulesIndex = redisRulesIndex.redisAccessRulesIndex;
30
+ exports.AccessRulesApiClient = accessRulesApiClient.AccessRulesApiClient;
27
31
  exports.createApiRuleRoutesProvider = createApiRuleRoutesProvider;
@@ -1,15 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const crypto = require("node:crypto");
4
3
  const search = require("@redis/search");
4
+ const accessPolicy = require("../accessPolicy.cjs");
5
5
  const accessPolicyResolver = require("../accessPolicyResolver.cjs");
6
- const redisIndex = require("./redisIndex.cjs");
7
- const accessRulesRedisIndexName = "index:user-access-rules";
8
- const accessRuleRedisKeyPrefix = "uar:";
9
- const accessRuleContentHashAlgorithm = "md5";
10
- const DEFAULT_SEARCH_LIMIT = 1e3;
11
- const accessRulesIndex = {
12
- name: accessRulesRedisIndexName,
6
+ const redisRulesIndexName = "index:user-access-rules";
7
+ const redisRuleKeyPrefix = "uar:";
8
+ const redisAccessRulesIndex = {
9
+ name: redisRulesIndexName,
13
10
  /**
14
11
  * Note on the field type decision
15
12
  *
@@ -24,8 +21,8 @@ const accessRulesIndex = {
24
21
  // necessary to make possible use of the ismissing() function on this field in the search
25
22
  INDEXMISSING: true
26
23
  },
27
- numericIpMaskMin: search.SCHEMA_FIELD_TYPE.NUMERIC,
28
- numericIpMaskMax: search.SCHEMA_FIELD_TYPE.NUMERIC,
24
+ numericIpMaskMin: { type: search.SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
25
+ numericIpMaskMax: { type: search.SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
29
26
  userId: { type: search.SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
30
27
  numericIp: { type: search.SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
31
28
  ja4Hash: { type: search.SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
@@ -35,43 +32,53 @@ const accessRulesIndex = {
35
32
  // the satisfy statement is to guarantee that the keys are right
36
33
  options: {
37
34
  ON: "HASH",
38
- PREFIX: [accessRuleRedisKeyPrefix]
35
+ PREFIX: [redisRuleKeyPrefix]
39
36
  }
40
37
  };
41
- const createRedisAccessRulesIndex = async (client, indexName) => {
42
- if (indexName) {
43
- accessRulesIndex.name = indexName;
44
- }
45
- return redisIndex.createRedisIndex(client, accessRulesIndex);
46
- };
47
38
  const numericIndexFields = [
48
39
  "numericIp",
49
40
  "numericIpMaskMin",
50
41
  "numericIpMaskMax"
51
42
  ];
52
43
  const greedyFieldComparisons = {
53
- numericIp: (value) => `( @numericIp:[${value}] | ( @numericIpMaskMin:[-inf ${value}] @numericIpMaskMax:[${value} +inf] ) )`
44
+ numericIp: (value, scope) => {
45
+ if (value !== void 0) {
46
+ return `( @numericIp:[${value}] | ( @numericIpMaskMin:[-inf ${value}] @numericIpMaskMax:[${value} +inf] ) )`;
47
+ }
48
+ if (scope.numericIpMaskMin === void 0 && scope.numericIpMaskMax === void 0) {
49
+ return "ismissing(@numericIp) ismissing(@numericIpMaskMin) ismissing(@numericIpMaskMax)";
50
+ }
51
+ return "";
52
+ },
53
+ numericIpMaskMin: (value, scope) => {
54
+ if (scope.numericIp !== void 0) {
55
+ return "";
56
+ }
57
+ return value !== void 0 ? `@numericIpMaskMin:[-inf ${value}]` : "ismissing(@numericIpMaskMin)";
58
+ },
59
+ numericIpMaskMax: (value, scope) => {
60
+ if (scope.numericIp !== void 0) {
61
+ return "";
62
+ }
63
+ return value !== void 0 ? `@numericIpMaskMax:[${value} +inf]` : "ismissing(@numericIpMaskMax)";
64
+ }
54
65
  };
55
- const accessRulesRedisSearchOptions = {
66
+ const redisRulesSearchOptions = {
56
67
  // #2 is a required option when the 'ismissing()' function is in the query body
57
68
  DIALECT: 2
58
69
  };
59
- const accessRulesRedisDeleteOptions = {
60
- // #2 is a required option when the 'ismissing()' function is in the query body
61
- DIALECT: 2,
62
- LIMIT: {
63
- from: 0,
64
- size: DEFAULT_SEARCH_LIMIT
65
- }
66
- };
67
- const getRedisAccessRulesQuery = (filter) => {
70
+ const getRedisRulesQuery = (filter, matchingFieldsOnly) => {
68
71
  const { policyScope, userScope } = filter;
69
72
  const policyScopeFilter = getPolicyScopeQuery(
70
73
  policyScope,
71
74
  filter.policyScopeMatch
72
75
  );
73
76
  if (userScope && Object.keys(userScope).length > 0) {
74
- const userScopeFilter = getUserScopeQuery(userScope, filter.userScopeMatch);
77
+ const userScopeFilter = getUserScopeQuery(
78
+ userScope,
79
+ filter.userScopeMatch,
80
+ matchingFieldsOnly
81
+ );
75
82
  return `${policyScopeFilter} ( ${userScopeFilter} )`;
76
83
  }
77
84
  return policyScopeFilter ? policyScopeFilter : "*";
@@ -83,7 +90,7 @@ const getPolicyScopeQuery = (policyScope, scopeMatchType) => {
83
90
  }
84
91
  return accessPolicyResolver.ScopeMatch.Exact === scopeMatchType ? "ismissing(@clientId)" : "";
85
92
  };
86
- const getUserScopeQuery = (userScope, scopeMatchType) => {
93
+ const getUserScopeQuery = (userScope, scopeMatchType, matchingFieldsOnly) => {
87
94
  let scopeEntries = Object.entries(userScope);
88
95
  let scopeJoinType = " ";
89
96
  if (scopeMatchType === accessPolicyResolver.ScopeMatch.Greedy) {
@@ -92,39 +99,40 @@ const getUserScopeQuery = (userScope, scopeMatchType) => {
92
99
  );
93
100
  scopeJoinType = " | ";
94
101
  }
102
+ if (matchingFieldsOnly) {
103
+ const scopeMap = new Map(scopeEntries);
104
+ if (scopeMap.has("numericIp") && scopeMap.get("numericIp") === void 0) {
105
+ scopeMap.set("numericIpMaskMin", void 0);
106
+ scopeMap.set("numericIpMaskMax", void 0);
107
+ }
108
+ for (const name of Object.keys(accessPolicy.userScopeSchema.shape)) {
109
+ if (!scopeMap.has(name)) {
110
+ scopeMap.set(name, void 0);
111
+ }
112
+ }
113
+ scopeEntries = [...scopeMap.entries()];
114
+ }
115
+ const scopeObj = Object.fromEntries(scopeEntries);
95
116
  return scopeEntries.map(
96
- ([scopeFieldName, scopeFieldValue]) => getUserScopeFieldQuery(scopeFieldName, scopeFieldValue, scopeMatchType)
97
- ).join(scopeJoinType);
117
+ ([scopeFieldName, scopeFieldValue]) => getUserScopeFieldQuery(
118
+ scopeFieldName,
119
+ scopeFieldValue,
120
+ scopeMatchType,
121
+ scopeObj
122
+ )
123
+ ).filter(Boolean).join(scopeJoinType);
98
124
  };
99
- const getUserScopeFieldQuery = (fieldName, fieldValue, matchType) => {
100
- if (
101
- //ScopeMatch.Greedy === matchType &&
102
- "function" === typeof greedyFieldComparisons[fieldName]
103
- ) {
104
- return greedyFieldComparisons[fieldName](fieldValue);
125
+ const getUserScopeFieldQuery = (fieldName, fieldValue, matchType, fullScope) => {
126
+ if ("function" === typeof greedyFieldComparisons[fieldName]) {
127
+ return greedyFieldComparisons[fieldName](fieldValue, fullScope);
105
128
  }
106
129
  if (fieldValue === void 0) {
107
130
  return `ismissing(@${fieldName})`;
108
131
  }
109
132
  return numericIndexFields.includes(fieldName) ? `@${fieldName}:[${fieldValue}]` : `@${fieldName}:{${fieldValue}}`;
110
133
  };
111
- const getRedisAccessRuleKey = (rule) => accessRuleRedisKeyPrefix + crypto.createHash(accessRuleContentHashAlgorithm).update(
112
- JSON.stringify(
113
- rule,
114
- (key, value) => (
115
- // JSON.stringify can't handle BigInt itself: throws "Do not know how to serialize a BigInt"
116
- "bigint" === typeof value ? value.toString() : value
117
- )
118
- )
119
- ).digest("hex");
120
- const getRedisAccessRuleValue = (rule) => Object.fromEntries(
121
- Object.entries(rule).map(([key, value]) => [key, String(value)])
122
- );
123
- exports.accessRuleRedisKeyPrefix = accessRuleRedisKeyPrefix;
124
- exports.accessRulesRedisDeleteOptions = accessRulesRedisDeleteOptions;
125
- exports.accessRulesRedisIndexName = accessRulesRedisIndexName;
126
- exports.accessRulesRedisSearchOptions = accessRulesRedisSearchOptions;
127
- exports.createRedisAccessRulesIndex = createRedisAccessRulesIndex;
128
- exports.getRedisAccessRuleKey = getRedisAccessRuleKey;
129
- exports.getRedisAccessRuleValue = getRedisAccessRuleValue;
130
- exports.getRedisAccessRulesQuery = getRedisAccessRulesQuery;
134
+ exports.getRedisRulesQuery = getRedisRulesQuery;
135
+ exports.redisAccessRulesIndex = redisAccessRulesIndex;
136
+ exports.redisRuleKeyPrefix = redisRuleKeyPrefix;
137
+ exports.redisRulesIndexName = redisRulesIndexName;
138
+ exports.redisRulesSearchOptions = redisRulesSearchOptions;
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const util = require("node:util");
4
4
  const accessRules = require("../accessRules.cjs");
5
- const redisAccessRulesIndex = require("./redisAccessRulesIndex.cjs");
5
+ const redisRulesIndex = require("./redisRulesIndex.cjs");
6
6
  function _interopNamespaceDefault(e) {
7
7
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
8
8
  if (e) {
@@ -20,19 +20,19 @@ function _interopNamespaceDefault(e) {
20
20
  return Object.freeze(n);
21
21
  }
22
22
  const util__namespace = /* @__PURE__ */ _interopNamespaceDefault(util);
23
- const createRedisAccessRulesReader = (client, logger) => {
23
+ const createRedisRulesReader = (client, logger) => {
24
24
  return {
25
- findRules: async (filter, skipEmptyUserScopes = true) => {
26
- const query = redisAccessRulesIndex.getRedisAccessRulesQuery(filter);
25
+ findRules: async (filter, matchingFieldsOnly = false, skipEmptyUserScopes = true) => {
26
+ const query = redisRulesIndex.getRedisRulesQuery(filter, matchingFieldsOnly);
27
27
  if (skipEmptyUserScopes && query === "ismissing(@clientId)") {
28
28
  return [];
29
29
  }
30
30
  let searchReply;
31
31
  try {
32
32
  searchReply = await client.ft.search(
33
- redisAccessRulesIndex.accessRulesRedisIndexName,
33
+ redisRulesIndex.redisRulesIndexName,
34
34
  query,
35
- redisAccessRulesIndex.accessRulesRedisSearchOptions
35
+ redisRulesIndex.redisRulesSearchOptions
36
36
  );
37
37
  if (searchReply.total > 0) {
38
38
  logger.debug(() => ({
@@ -67,16 +67,16 @@ const createRedisAccessRulesReader = (client, logger) => {
67
67
  }));
68
68
  return [];
69
69
  }
70
- return extractAccessRulesFromSearchReply(searchReply, logger);
70
+ return extractRulesFromSearchReply(searchReply, logger);
71
71
  },
72
- findRuleIds: async (filter) => {
73
- const query = redisAccessRulesIndex.getRedisAccessRulesQuery(filter);
72
+ findRuleIds: async (filter, matchingFieldsOnly = false) => {
73
+ const query = redisRulesIndex.getRedisRulesQuery(filter, matchingFieldsOnly);
74
74
  let searchReply;
75
75
  try {
76
76
  searchReply = await client.ft.searchNoContent(
77
- redisAccessRulesIndex.accessRulesRedisIndexName,
77
+ redisRulesIndex.redisRulesIndexName,
78
78
  query,
79
- redisAccessRulesIndex.accessRulesRedisSearchOptions
79
+ redisRulesIndex.redisRulesSearchOptions
80
80
  );
81
81
  } catch (e) {
82
82
  logger.error(() => ({
@@ -100,38 +100,29 @@ const createRedisAccessRulesReader = (client, logger) => {
100
100
  }
101
101
  };
102
102
  };
103
- const createRedisAccessRulesWriter = (client) => {
103
+ const getDummyRedisRulesReader = (logger) => {
104
104
  return {
105
- insertRule: async (rule, expirationTimestamp) => {
106
- const ruleKey = redisAccessRulesIndex.getRedisAccessRuleKey(rule);
107
- const ruleValue = redisAccessRulesIndex.getRedisAccessRuleValue(rule);
108
- await client.hSet(ruleKey, ruleValue);
109
- if (expirationTimestamp) {
110
- const expiryDate = new Date(expirationTimestamp);
111
- if (expiryDate.getUTCFullYear() === 1970) {
112
- await client.expireAt(ruleKey, expirationTimestamp);
113
- } else {
114
- const timestampInSeconds = Math.floor(expirationTimestamp / 1e3);
115
- await client.expireAt(ruleKey, timestampInSeconds);
105
+ findRules: async (filter, matchingFieldsOnly = false, skipEmptyUserScopes = true) => {
106
+ logger.info(() => ({
107
+ msg: "Dummy findRules() has no effect (redis is not ready)",
108
+ data: {
109
+ filter
116
110
  }
117
- }
118
- return ruleKey;
111
+ }));
112
+ return [];
119
113
  },
120
- deleteRules: async (ruleIds) => void await client.del(ruleIds),
121
- deleteAllRules: async () => {
122
- const keys = await client.keys(`${redisAccessRulesIndex.accessRuleRedisKeyPrefix}*`);
123
- if (keys.length === 0) return 0;
124
- return await client.del(keys);
114
+ findRuleIds: async (filter, matchingFieldsOnly = false) => {
115
+ logger.info(() => ({
116
+ msg: "Dummy findRuleIds() has no effect (redis is not ready)",
117
+ data: {
118
+ filter
119
+ }
120
+ }));
121
+ return [];
125
122
  }
126
123
  };
127
124
  };
128
- const createRedisAccessRulesStorage = (client, logger) => {
129
- return {
130
- ...createRedisAccessRulesReader(client, logger),
131
- ...createRedisAccessRulesWriter(client)
132
- };
133
- };
134
- const extractAccessRulesFromSearchReply = (searchReply, logger) => {
125
+ const extractRulesFromSearchReply = (searchReply, logger) => {
135
126
  const accessRules$1 = [];
136
127
  searchReply.documents.map(({ id, value: document }) => {
137
128
  const parsedDocument = accessRules.accessRuleSchema.safeParse(document);
@@ -147,6 +138,5 @@ const extractAccessRulesFromSearchReply = (searchReply, logger) => {
147
138
  });
148
139
  return accessRules$1;
149
140
  };
150
- exports.createRedisAccessRulesReader = createRedisAccessRulesReader;
151
- exports.createRedisAccessRulesStorage = createRedisAccessRulesStorage;
152
- exports.createRedisAccessRulesWriter = createRedisAccessRulesWriter;
141
+ exports.createRedisRulesReader = createRedisRulesReader;
142
+ exports.getDummyRedisRulesReader = getDummyRedisRulesReader;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const redisRulesReader = require("./redisRulesReader.cjs");
4
+ const redisRulesWriter = require("./redisRulesWriter.cjs");
5
+ const createRedisAccessRulesStorage = (connection, logger) => {
6
+ const storage = {
7
+ ...redisRulesReader.getDummyRedisRulesReader(logger),
8
+ ...redisRulesWriter.getDummyRedisRulesWriter(logger)
9
+ };
10
+ connection.getClient().then((client) => {
11
+ Object.assign(storage, {
12
+ ...redisRulesReader.createRedisRulesReader(client, logger),
13
+ ...redisRulesWriter.createRedisRulesWriter(client)
14
+ });
15
+ logger.info(() => ({
16
+ msg: "RedisAccessRules storage got a ready Redis client"
17
+ }));
18
+ });
19
+ return storage;
20
+ };
21
+ exports.createRedisAccessRulesStorage = createRedisAccessRulesStorage;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const crypto = require("node:crypto");
4
+ const redisRulesIndex = require("./redisRulesIndex.cjs");
5
+ const redisRuleContentHashAlgorithm = "md5";
6
+ const createRedisRulesWriter = (client) => {
7
+ return {
8
+ insertRule: async (rule, expirationTimestamp) => {
9
+ const ruleKey = getRedisRuleKey(rule);
10
+ const ruleValue = getRedisRuleValue(rule);
11
+ await client.hSet(ruleKey, ruleValue);
12
+ if (expirationTimestamp) {
13
+ const expiryDate = new Date(expirationTimestamp);
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
+ }
20
+ }
21
+ return ruleKey;
22
+ },
23
+ deleteRules: async (ruleIds) => void await client.del(ruleIds),
24
+ deleteAllRules: async () => {
25
+ const keys = await client.keys(`${redisRulesIndex.redisRuleKeyPrefix}*`);
26
+ if (keys.length === 0) return 0;
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");
67
+ const getRedisRuleValue = (rule) => Object.fromEntries(
68
+ Object.entries(rule).map(([key, value]) => [key, String(value)])
69
+ );
70
+ exports.createRedisRulesWriter = createRedisRulesWriter;
71
+ exports.getDummyRedisRulesWriter = getDummyRedisRulesWriter;
72
+ exports.getRedisRuleKey = getRedisRuleKey;
73
+ exports.getRedisRuleValue = getRedisRuleValue;
package/dist/index.js CHANGED
@@ -1,28 +1,32 @@
1
1
  import { AccessPolicyType, accessPolicySchema, accessRuleSchemaExtended, policyScopeSchema, userScopeInputSchema } from "./accessPolicy.js";
2
2
  import { ScopeMatch } from "./accessPolicyResolver.js";
3
+ import { accessRuleSchema } from "./accessRules.js";
3
4
  import { AccessRuleApiRoutes } from "./api/accessRuleApiRoutes.js";
4
5
  import { accessRuleApiPaths, getExpressApiRuleRateLimits } from "./api/accessRuleApiRoutes.js";
5
6
  import { deleteAllRulesEndpointSchema } from "./api/deleteAllRulesEndpoint.js";
6
7
  import { deleteRulesEndpointSchema } from "./api/deleteRulesEndpoint.js";
7
8
  import { insertRulesEndpointSchema } from "./api/insertRulesEndpoint.js";
8
- import { createRedisAccessRulesStorage } from "./redis/redisAccessRules.js";
9
- import { createRedisAccessRulesIndex } from "./redis/redisAccessRulesIndex.js";
9
+ import { createRedisAccessRulesStorage } from "./redis/redisRulesStorage.js";
10
+ import { redisAccessRulesIndex } from "./redis/redisRulesIndex.js";
11
+ import { AccessRulesApiClient } from "./api/accessRulesApiClient.js";
10
12
  const createApiRuleRoutesProvider = (rulesStorage) => {
11
13
  return new AccessRuleApiRoutes(rulesStorage);
12
14
  };
13
15
  export {
14
16
  AccessPolicyType,
17
+ AccessRulesApiClient,
15
18
  ScopeMatch,
16
19
  accessPolicySchema,
17
20
  accessRuleApiPaths,
21
+ accessRuleSchema,
18
22
  accessRuleSchemaExtended,
19
23
  createApiRuleRoutesProvider,
20
- createRedisAccessRulesIndex,
21
24
  createRedisAccessRulesStorage,
22
25
  deleteAllRulesEndpointSchema,
23
26
  deleteRulesEndpointSchema,
24
27
  getExpressApiRuleRateLimits,
25
28
  insertRulesEndpointSchema,
26
29
  policyScopeSchema,
30
+ redisAccessRulesIndex,
27
31
  userScopeInputSchema
28
32
  };
@@ -1,13 +1,10 @@
1
- import crypto from "node:crypto";
2
1
  import { SCHEMA_FIELD_TYPE } from "@redis/search";
2
+ import { userScopeSchema } from "../accessPolicy.js";
3
3
  import { ScopeMatch } from "../accessPolicyResolver.js";
4
- import { createRedisIndex } from "./redisIndex.js";
5
- const accessRulesRedisIndexName = "index:user-access-rules";
6
- const accessRuleRedisKeyPrefix = "uar:";
7
- const accessRuleContentHashAlgorithm = "md5";
8
- const DEFAULT_SEARCH_LIMIT = 1e3;
9
- const accessRulesIndex = {
10
- name: accessRulesRedisIndexName,
4
+ const redisRulesIndexName = "index:user-access-rules";
5
+ const redisRuleKeyPrefix = "uar:";
6
+ const redisAccessRulesIndex = {
7
+ name: redisRulesIndexName,
11
8
  /**
12
9
  * Note on the field type decision
13
10
  *
@@ -22,8 +19,8 @@ const accessRulesIndex = {
22
19
  // necessary to make possible use of the ismissing() function on this field in the search
23
20
  INDEXMISSING: true
24
21
  },
25
- numericIpMaskMin: SCHEMA_FIELD_TYPE.NUMERIC,
26
- numericIpMaskMax: SCHEMA_FIELD_TYPE.NUMERIC,
22
+ numericIpMaskMin: { type: SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
23
+ numericIpMaskMax: { type: SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
27
24
  userId: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
28
25
  numericIp: { type: SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
29
26
  ja4Hash: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
@@ -33,43 +30,53 @@ const accessRulesIndex = {
33
30
  // the satisfy statement is to guarantee that the keys are right
34
31
  options: {
35
32
  ON: "HASH",
36
- PREFIX: [accessRuleRedisKeyPrefix]
33
+ PREFIX: [redisRuleKeyPrefix]
37
34
  }
38
35
  };
39
- const createRedisAccessRulesIndex = async (client, indexName) => {
40
- if (indexName) {
41
- accessRulesIndex.name = indexName;
42
- }
43
- return createRedisIndex(client, accessRulesIndex);
44
- };
45
36
  const numericIndexFields = [
46
37
  "numericIp",
47
38
  "numericIpMaskMin",
48
39
  "numericIpMaskMax"
49
40
  ];
50
41
  const greedyFieldComparisons = {
51
- numericIp: (value) => `( @numericIp:[${value}] | ( @numericIpMaskMin:[-inf ${value}] @numericIpMaskMax:[${value} +inf] ) )`
42
+ numericIp: (value, scope) => {
43
+ if (value !== void 0) {
44
+ return `( @numericIp:[${value}] | ( @numericIpMaskMin:[-inf ${value}] @numericIpMaskMax:[${value} +inf] ) )`;
45
+ }
46
+ if (scope.numericIpMaskMin === void 0 && scope.numericIpMaskMax === void 0) {
47
+ return "ismissing(@numericIp) ismissing(@numericIpMaskMin) ismissing(@numericIpMaskMax)";
48
+ }
49
+ return "";
50
+ },
51
+ numericIpMaskMin: (value, scope) => {
52
+ if (scope.numericIp !== void 0) {
53
+ return "";
54
+ }
55
+ return value !== void 0 ? `@numericIpMaskMin:[-inf ${value}]` : "ismissing(@numericIpMaskMin)";
56
+ },
57
+ numericIpMaskMax: (value, scope) => {
58
+ if (scope.numericIp !== void 0) {
59
+ return "";
60
+ }
61
+ return value !== void 0 ? `@numericIpMaskMax:[${value} +inf]` : "ismissing(@numericIpMaskMax)";
62
+ }
52
63
  };
53
- const accessRulesRedisSearchOptions = {
64
+ const redisRulesSearchOptions = {
54
65
  // #2 is a required option when the 'ismissing()' function is in the query body
55
66
  DIALECT: 2
56
67
  };
57
- const accessRulesRedisDeleteOptions = {
58
- // #2 is a required option when the 'ismissing()' function is in the query body
59
- DIALECT: 2,
60
- LIMIT: {
61
- from: 0,
62
- size: DEFAULT_SEARCH_LIMIT
63
- }
64
- };
65
- const getRedisAccessRulesQuery = (filter) => {
68
+ const getRedisRulesQuery = (filter, matchingFieldsOnly) => {
66
69
  const { policyScope, userScope } = filter;
67
70
  const policyScopeFilter = getPolicyScopeQuery(
68
71
  policyScope,
69
72
  filter.policyScopeMatch
70
73
  );
71
74
  if (userScope && Object.keys(userScope).length > 0) {
72
- const userScopeFilter = getUserScopeQuery(userScope, filter.userScopeMatch);
75
+ const userScopeFilter = getUserScopeQuery(
76
+ userScope,
77
+ filter.userScopeMatch,
78
+ matchingFieldsOnly
79
+ );
73
80
  return `${policyScopeFilter} ( ${userScopeFilter} )`;
74
81
  }
75
82
  return policyScopeFilter ? policyScopeFilter : "*";
@@ -81,7 +88,7 @@ const getPolicyScopeQuery = (policyScope, scopeMatchType) => {
81
88
  }
82
89
  return ScopeMatch.Exact === scopeMatchType ? "ismissing(@clientId)" : "";
83
90
  };
84
- const getUserScopeQuery = (userScope, scopeMatchType) => {
91
+ const getUserScopeQuery = (userScope, scopeMatchType, matchingFieldsOnly) => {
85
92
  let scopeEntries = Object.entries(userScope);
86
93
  let scopeJoinType = " ";
87
94
  if (scopeMatchType === ScopeMatch.Greedy) {
@@ -90,41 +97,42 @@ const getUserScopeQuery = (userScope, scopeMatchType) => {
90
97
  );
91
98
  scopeJoinType = " | ";
92
99
  }
100
+ if (matchingFieldsOnly) {
101
+ const scopeMap = new Map(scopeEntries);
102
+ if (scopeMap.has("numericIp") && scopeMap.get("numericIp") === void 0) {
103
+ scopeMap.set("numericIpMaskMin", void 0);
104
+ scopeMap.set("numericIpMaskMax", void 0);
105
+ }
106
+ for (const name of Object.keys(userScopeSchema.shape)) {
107
+ if (!scopeMap.has(name)) {
108
+ scopeMap.set(name, void 0);
109
+ }
110
+ }
111
+ scopeEntries = [...scopeMap.entries()];
112
+ }
113
+ const scopeObj = Object.fromEntries(scopeEntries);
93
114
  return scopeEntries.map(
94
- ([scopeFieldName, scopeFieldValue]) => getUserScopeFieldQuery(scopeFieldName, scopeFieldValue, scopeMatchType)
95
- ).join(scopeJoinType);
115
+ ([scopeFieldName, scopeFieldValue]) => getUserScopeFieldQuery(
116
+ scopeFieldName,
117
+ scopeFieldValue,
118
+ scopeMatchType,
119
+ scopeObj
120
+ )
121
+ ).filter(Boolean).join(scopeJoinType);
96
122
  };
97
- const getUserScopeFieldQuery = (fieldName, fieldValue, matchType) => {
98
- if (
99
- //ScopeMatch.Greedy === matchType &&
100
- "function" === typeof greedyFieldComparisons[fieldName]
101
- ) {
102
- return greedyFieldComparisons[fieldName](fieldValue);
123
+ const getUserScopeFieldQuery = (fieldName, fieldValue, matchType, fullScope) => {
124
+ if ("function" === typeof greedyFieldComparisons[fieldName]) {
125
+ return greedyFieldComparisons[fieldName](fieldValue, fullScope);
103
126
  }
104
127
  if (fieldValue === void 0) {
105
128
  return `ismissing(@${fieldName})`;
106
129
  }
107
130
  return numericIndexFields.includes(fieldName) ? `@${fieldName}:[${fieldValue}]` : `@${fieldName}:{${fieldValue}}`;
108
131
  };
109
- const getRedisAccessRuleKey = (rule) => accessRuleRedisKeyPrefix + crypto.createHash(accessRuleContentHashAlgorithm).update(
110
- JSON.stringify(
111
- rule,
112
- (key, value) => (
113
- // JSON.stringify can't handle BigInt itself: throws "Do not know how to serialize a BigInt"
114
- "bigint" === typeof value ? value.toString() : value
115
- )
116
- )
117
- ).digest("hex");
118
- const getRedisAccessRuleValue = (rule) => Object.fromEntries(
119
- Object.entries(rule).map(([key, value]) => [key, String(value)])
120
- );
121
132
  export {
122
- accessRuleRedisKeyPrefix,
123
- accessRulesRedisDeleteOptions,
124
- accessRulesRedisIndexName,
125
- accessRulesRedisSearchOptions,
126
- createRedisAccessRulesIndex,
127
- getRedisAccessRuleKey,
128
- getRedisAccessRuleValue,
129
- getRedisAccessRulesQuery
133
+ getRedisRulesQuery,
134
+ redisAccessRulesIndex,
135
+ redisRuleKeyPrefix,
136
+ redisRulesIndexName,
137
+ redisRulesSearchOptions
130
138
  };
@@ -1,19 +1,19 @@
1
1
  import * as util from "node:util";
2
2
  import { accessRuleSchema } from "../accessRules.js";
3
- import { getRedisAccessRulesQuery, accessRulesRedisIndexName, accessRulesRedisSearchOptions, accessRuleRedisKeyPrefix, getRedisAccessRuleKey, getRedisAccessRuleValue } from "./redisAccessRulesIndex.js";
4
- const createRedisAccessRulesReader = (client, logger) => {
3
+ import { getRedisRulesQuery, redisRulesIndexName, redisRulesSearchOptions } from "./redisRulesIndex.js";
4
+ const createRedisRulesReader = (client, logger) => {
5
5
  return {
6
- findRules: async (filter, skipEmptyUserScopes = true) => {
7
- const query = getRedisAccessRulesQuery(filter);
6
+ findRules: async (filter, matchingFieldsOnly = false, skipEmptyUserScopes = true) => {
7
+ const query = getRedisRulesQuery(filter, matchingFieldsOnly);
8
8
  if (skipEmptyUserScopes && query === "ismissing(@clientId)") {
9
9
  return [];
10
10
  }
11
11
  let searchReply;
12
12
  try {
13
13
  searchReply = await client.ft.search(
14
- accessRulesRedisIndexName,
14
+ redisRulesIndexName,
15
15
  query,
16
- accessRulesRedisSearchOptions
16
+ redisRulesSearchOptions
17
17
  );
18
18
  if (searchReply.total > 0) {
19
19
  logger.debug(() => ({
@@ -48,16 +48,16 @@ const createRedisAccessRulesReader = (client, logger) => {
48
48
  }));
49
49
  return [];
50
50
  }
51
- return extractAccessRulesFromSearchReply(searchReply, logger);
51
+ return extractRulesFromSearchReply(searchReply, logger);
52
52
  },
53
- findRuleIds: async (filter) => {
54
- const query = getRedisAccessRulesQuery(filter);
53
+ findRuleIds: async (filter, matchingFieldsOnly = false) => {
54
+ const query = getRedisRulesQuery(filter, matchingFieldsOnly);
55
55
  let searchReply;
56
56
  try {
57
57
  searchReply = await client.ft.searchNoContent(
58
- accessRulesRedisIndexName,
58
+ redisRulesIndexName,
59
59
  query,
60
- accessRulesRedisSearchOptions
60
+ redisRulesSearchOptions
61
61
  );
62
62
  } catch (e) {
63
63
  logger.error(() => ({
@@ -81,38 +81,29 @@ const createRedisAccessRulesReader = (client, logger) => {
81
81
  }
82
82
  };
83
83
  };
84
- const createRedisAccessRulesWriter = (client) => {
84
+ const getDummyRedisRulesReader = (logger) => {
85
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);
86
+ findRules: async (filter, matchingFieldsOnly = false, skipEmptyUserScopes = true) => {
87
+ logger.info(() => ({
88
+ msg: "Dummy findRules() has no effect (redis is not ready)",
89
+ data: {
90
+ filter
97
91
  }
98
- }
99
- return ruleKey;
92
+ }));
93
+ return [];
100
94
  },
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);
95
+ findRuleIds: async (filter, matchingFieldsOnly = false) => {
96
+ logger.info(() => ({
97
+ msg: "Dummy findRuleIds() has no effect (redis is not ready)",
98
+ data: {
99
+ filter
100
+ }
101
+ }));
102
+ return [];
106
103
  }
107
104
  };
108
105
  };
109
- const createRedisAccessRulesStorage = (client, logger) => {
110
- return {
111
- ...createRedisAccessRulesReader(client, logger),
112
- ...createRedisAccessRulesWriter(client)
113
- };
114
- };
115
- const extractAccessRulesFromSearchReply = (searchReply, logger) => {
106
+ const extractRulesFromSearchReply = (searchReply, logger) => {
116
107
  const accessRules = [];
117
108
  searchReply.documents.map(({ id, value: document }) => {
118
109
  const parsedDocument = accessRuleSchema.safeParse(document);
@@ -129,7 +120,6 @@ const extractAccessRulesFromSearchReply = (searchReply, logger) => {
129
120
  return accessRules;
130
121
  };
131
122
  export {
132
- createRedisAccessRulesReader,
133
- createRedisAccessRulesStorage,
134
- createRedisAccessRulesWriter
123
+ createRedisRulesReader,
124
+ getDummyRedisRulesReader
135
125
  };
@@ -0,0 +1,21 @@
1
+ import { getDummyRedisRulesReader, createRedisRulesReader } from "./redisRulesReader.js";
2
+ import { getDummyRedisRulesWriter, createRedisRulesWriter } from "./redisRulesWriter.js";
3
+ const createRedisAccessRulesStorage = (connection, logger) => {
4
+ const storage = {
5
+ ...getDummyRedisRulesReader(logger),
6
+ ...getDummyRedisRulesWriter(logger)
7
+ };
8
+ connection.getClient().then((client) => {
9
+ Object.assign(storage, {
10
+ ...createRedisRulesReader(client, logger),
11
+ ...createRedisRulesWriter(client)
12
+ });
13
+ logger.info(() => ({
14
+ msg: "RedisAccessRules storage got a ready Redis client"
15
+ }));
16
+ });
17
+ return storage;
18
+ };
19
+ export {
20
+ createRedisAccessRulesStorage
21
+ };
@@ -0,0 +1,73 @@
1
+ import crypto from "node:crypto";
2
+ import { redisRuleKeyPrefix } from "./redisRulesIndex.js";
3
+ const redisRuleContentHashAlgorithm = "md5";
4
+ const createRedisRulesWriter = (client) => {
5
+ return {
6
+ insertRule: async (rule, expirationTimestamp) => {
7
+ const ruleKey = getRedisRuleKey(rule);
8
+ const ruleValue = getRedisRuleValue(rule);
9
+ await client.hSet(ruleKey, ruleValue);
10
+ if (expirationTimestamp) {
11
+ const expiryDate = new Date(expirationTimestamp);
12
+ if (expiryDate.getUTCFullYear() === 1970) {
13
+ await client.expireAt(ruleKey, expirationTimestamp);
14
+ } else {
15
+ const timestampInSeconds = Math.floor(expirationTimestamp / 1e3);
16
+ await client.expireAt(ruleKey, timestampInSeconds);
17
+ }
18
+ }
19
+ return ruleKey;
20
+ },
21
+ deleteRules: async (ruleIds) => void await client.del(ruleIds),
22
+ deleteAllRules: async () => {
23
+ const keys = await client.keys(`${redisRuleKeyPrefix}*`);
24
+ if (keys.length === 0) return 0;
25
+ return await client.del(keys);
26
+ }
27
+ };
28
+ };
29
+ const getDummyRedisRulesWriter = (logger) => {
30
+ return {
31
+ insertRule: async (rule, expirationTimestamp) => {
32
+ logger.info(() => ({
33
+ msg: "Dummy insertRule() has no effect (redis is not ready)",
34
+ data: {
35
+ rule
36
+ }
37
+ }));
38
+ return "";
39
+ },
40
+ deleteRules: async (ruleIds) => {
41
+ logger.info(() => ({
42
+ msg: "Dummy deleteRules() has no effect (redis is not ready)",
43
+ data: {
44
+ ruleIds
45
+ }
46
+ }));
47
+ },
48
+ deleteAllRules: async () => {
49
+ logger.info(() => ({
50
+ msg: "Dummy deleteAllRules() has no effect (redis is not ready)"
51
+ }));
52
+ return 0;
53
+ }
54
+ };
55
+ };
56
+ const getRedisRuleKey = (rule) => redisRuleKeyPrefix + crypto.createHash(redisRuleContentHashAlgorithm).update(
57
+ JSON.stringify(
58
+ rule,
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 getRedisRuleValue = (rule) => Object.fromEntries(
66
+ Object.entries(rule).map(([key, value]) => [key, String(value)])
67
+ );
68
+ export {
69
+ createRedisRulesWriter,
70
+ getDummyRedisRulesWriter,
71
+ getRedisRuleKey,
72
+ getRedisRuleValue
73
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prosopo/user-access-policy",
3
- "version": "3.4.0",
3
+ "version": "3.5.19",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -23,23 +23,28 @@
23
23
  "build": "NODE_ENV=${NODE_ENV:-development}; vite build --config vite.esm.config.ts --mode $NODE_ENV",
24
24
  "build:tsc": "tsc --build --verbose",
25
25
  "build:cjs": "NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV",
26
- "typecheck": "tsc --build --declaration --emitDeclarationOnly",
27
- "test": "NODE_ENV=${NODE_ENV:-test}; npx vitest run --config ./vite.test.config.ts"
26
+ "typecheck": "tsc --project tsconfig.types.json",
27
+ "test:unit": "NODE_ENV=${NODE_ENV:-test}; TEST_TYPE=unit npx vitest run --config ./vite.test.config.ts",
28
+ "test:integration": "NODE_ENV=${NODE_ENV:-test}; NX_PARALLEL=1 TEST_TYPE=integration npx vitest run --config ./vite.test.config.ts",
29
+ "test": "npm run test:unit && npm run test:integration"
28
30
  },
29
31
  "dependencies": {
30
- "@prosopo/api-route": "2.6.10",
31
- "@prosopo/common": "3.1.2",
32
- "@prosopo/types": "3.0.6",
33
- "@prosopo/util": "3.0.5",
34
- "axios": "1.10.0",
35
- "esbuild": "0.25.6",
32
+ "@prosopo/api": "3.1.24",
33
+ "@prosopo/api-route": "2.6.28",
34
+ "@prosopo/common": "3.1.20",
35
+ "@prosopo/redis-client": "1.0.5",
36
+ "@prosopo/types": "3.5.3",
37
+ "@prosopo/util": "3.1.5",
38
+ "@redis/search": "5.0.0",
39
+ "dotenv": "16.4.5",
36
40
  "ip-address": "10.0.1",
37
41
  "redis": "5.0.0",
38
- "zod": "3.23.8",
39
- "@prosopo/config": "3.1.3",
40
- "webpack-dev-server": "5.2.2"
42
+ "zod": "3.23.8"
41
43
  },
42
44
  "devDependencies": {
45
+ "@prosopo/config": "3.1.20",
46
+ "@prosopo/util-crypto": "13.5.22",
47
+ "@types/node": "22.10.2",
43
48
  "vite": "6.3.5",
44
49
  "vitest": "3.0.9",
45
50
  "yargs": "17.7.2"
@@ -1,22 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const crypto = require("node:crypto");
4
- const redisIndexHashesRecordKey = "_index_hashes";
5
- const redisIndexHashAlgorithm = "sha256";
6
- const createRedisIndex = async (client, index) => {
7
- const indexHash = createIndexHash(index);
8
- const existingIndexes = await client.ft._LIST();
9
- if (existingIndexes.includes(index.name)) {
10
- const existingIndexHash = await fetchIndexHash(client, index.name);
11
- if (indexHash === existingIndexHash) {
12
- return;
13
- }
14
- await client.ft.dropIndex(index.name);
15
- }
16
- await client.ft.create(index.name, index.schema, index.options);
17
- await saveIndexHash(client, index.name, indexHash);
18
- };
19
- const createIndexHash = (index) => crypto.createHash(redisIndexHashAlgorithm).update(JSON.stringify(index)).digest("hex");
20
- const fetchIndexHash = async (client, indexName) => client.hGet(redisIndexHashesRecordKey, indexName);
21
- const saveIndexHash = async (client, indexName, indexHash) => client.hSet(redisIndexHashesRecordKey, indexName, indexHash);
22
- exports.createRedisIndex = createRedisIndex;
@@ -1,22 +0,0 @@
1
- import crypto from "node:crypto";
2
- const redisIndexHashesRecordKey = "_index_hashes";
3
- const redisIndexHashAlgorithm = "sha256";
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;
11
- }
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
- };
17
- const createIndexHash = (index) => crypto.createHash(redisIndexHashAlgorithm).update(JSON.stringify(index)).digest("hex");
18
- const fetchIndexHash = async (client, indexName) => client.hGet(redisIndexHashesRecordKey, indexName);
19
- const saveIndexHash = async (client, indexName, indexHash) => client.hSet(redisIndexHashesRecordKey, indexName, indexHash);
20
- export {
21
- createRedisIndex
22
- };