@flowerforce/flowerbase 1.8.4-beta.1 → 1.8.4-beta.2
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/dist/monitoring/utils.d.ts +1 -1
- package/dist/services/mongodb-atlas/index.js +11 -11
- package/dist/utils/context/helpers.d.ts +3 -3
- package/dist/utils/roles/helpers.d.ts +1 -0
- package/dist/utils/roles/helpers.d.ts.map +1 -1
- package/dist/utils/roles/helpers.js +77 -8
- package/dist/utils/roles/machines/utils.d.ts +2 -0
- package/dist/utils/roles/machines/utils.d.ts.map +1 -1
- package/dist/utils/roles/machines/utils.js +33 -1
- package/package.json +1 -1
- package/src/services/mongodb-atlas/index.ts +150 -150
- package/src/utils/__tests__/evaluateExpression.test.ts +89 -0
- package/src/utils/__tests__/getWinningRole.test.ts +17 -0
- package/src/utils/roles/helpers.ts +107 -14
- package/src/utils/roles/machines/utils.ts +34 -0
|
@@ -77,7 +77,7 @@ export declare const getErrorDetails: (error: unknown) => {
|
|
|
77
77
|
};
|
|
78
78
|
export declare const pickHeaders: (headers: FastifyRequest["headers"]) => unknown;
|
|
79
79
|
export declare const createEventStore: (maxAgeMs: number, maxEvents: number) => EventStore;
|
|
80
|
-
export declare const classifyRequest: (url: string) => "function" | "
|
|
80
|
+
export declare const classifyRequest: (url: string) => "function" | "api" | "auth" | "http_endpoint" | "trigger" | "http";
|
|
81
81
|
export declare const createEventId: () => string;
|
|
82
82
|
export declare const buildRulesMeta: (meta?: MonitMeta) => {
|
|
83
83
|
collection: string;
|
|
@@ -490,7 +490,7 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
490
490
|
emitMongoEvent('findOne');
|
|
491
491
|
return null;
|
|
492
492
|
}
|
|
493
|
-
const winningRole = (0, utils_2.
|
|
493
|
+
const winningRole = yield (0, utils_2.getWinningRoleAsync)(result, user, roles);
|
|
494
494
|
logDebug('findOne winningRole', {
|
|
495
495
|
collection: collName,
|
|
496
496
|
winningRoleName: (_a = winningRole === null || winningRole === void 0 ? void 0 : winningRole.name) !== null && _a !== void 0 ? _a : null,
|
|
@@ -546,7 +546,7 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
546
546
|
const formattedQuery = (0, utils_3.getFormattedQuery)(filters, query, user);
|
|
547
547
|
// Retrieve the document to check permissions before deleting
|
|
548
548
|
const result = yield collection.findOne(buildAndQuery(formattedQuery));
|
|
549
|
-
const winningRole = (0, utils_2.
|
|
549
|
+
const winningRole = yield (0, utils_2.getWinningRoleAsync)(result, user, roles);
|
|
550
550
|
logDebug('delete winningRole', {
|
|
551
551
|
collection: collName,
|
|
552
552
|
userId: getUserId(user),
|
|
@@ -600,7 +600,7 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
600
600
|
try {
|
|
601
601
|
if (!run_as_system) {
|
|
602
602
|
(0, utils_3.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.CREATE);
|
|
603
|
-
const winningRole = (0, utils_2.
|
|
603
|
+
const winningRole = yield (0, utils_2.getWinningRoleAsync)(data, user, roles);
|
|
604
604
|
const { status, document } = winningRole
|
|
605
605
|
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
606
606
|
type: 'insert',
|
|
@@ -673,7 +673,7 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
673
673
|
}
|
|
674
674
|
throw new Error('Update not permitted');
|
|
675
675
|
}
|
|
676
|
-
const winningRole = (0, utils_2.
|
|
676
|
+
const winningRole = yield (0, utils_2.getWinningRoleAsync)(result, user, roles);
|
|
677
677
|
// Check if the update data contains MongoDB update operators (e.g., $set, $inc)
|
|
678
678
|
const updatedPaths = getUpdatedPaths(normalizedData);
|
|
679
679
|
const docToCheck = applyDocumentUpdateOperators(result, normalizedData);
|
|
@@ -756,7 +756,7 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
756
756
|
: [applyDocumentUpdateOperators(currentDoc, normalizedData)];
|
|
757
757
|
docToCheck = computedDoc;
|
|
758
758
|
}
|
|
759
|
-
const winningRole = (0, utils_2.
|
|
759
|
+
const winningRole = yield (0, utils_2.getWinningRoleAsync)(docToCheck, user, roles);
|
|
760
760
|
const { status, document } = winningRole
|
|
761
761
|
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
762
762
|
type: validationType,
|
|
@@ -776,7 +776,7 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
776
776
|
emitMongoEvent('findOneAndUpdate');
|
|
777
777
|
return updateResult;
|
|
778
778
|
}
|
|
779
|
-
const readRole = (0, utils_2.
|
|
779
|
+
const readRole = yield (0, utils_2.getWinningRoleAsync)(updateResult, user, roles);
|
|
780
780
|
const readResult = readRole
|
|
781
781
|
? yield (0, machines_1.checkValidation)(readRole, {
|
|
782
782
|
type: 'read',
|
|
@@ -844,7 +844,7 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
844
844
|
const response = yield originalToArray();
|
|
845
845
|
const filteredResponse = yield Promise.all(response.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
|
|
846
846
|
var _a;
|
|
847
|
-
const winningRole = (0, utils_2.
|
|
847
|
+
const winningRole = yield (0, utils_2.getWinningRoleAsync)(currentDoc, user, roles);
|
|
848
848
|
logDebug('find winningRole', {
|
|
849
849
|
collection: collName,
|
|
850
850
|
userId: getUserId(user),
|
|
@@ -976,7 +976,7 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
976
976
|
const isValidChange = (change) => __awaiter(void 0, void 0, void 0, function* () {
|
|
977
977
|
const { fullDocument, updateDescription } = change;
|
|
978
978
|
const hasFullDocument = !!fullDocument;
|
|
979
|
-
const winningRole = (0, utils_2.
|
|
979
|
+
const winningRole = yield (0, utils_2.getWinningRoleAsync)(fullDocument, user, roles);
|
|
980
980
|
const fullDocumentValidation = winningRole
|
|
981
981
|
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
982
982
|
type: 'read',
|
|
@@ -1088,7 +1088,7 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
1088
1088
|
(0, utils_3.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.CREATE);
|
|
1089
1089
|
// Validate each document against user's roles
|
|
1090
1090
|
const filteredItems = yield Promise.all(documents.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1091
|
-
const winningRole = (0, utils_2.
|
|
1091
|
+
const winningRole = yield (0, utils_2.getWinningRoleAsync)(currentDoc, user, roles);
|
|
1092
1092
|
const { status, document } = winningRole
|
|
1093
1093
|
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
1094
1094
|
type: 'insert',
|
|
@@ -1133,7 +1133,7 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
1133
1133
|
const updatedPaths = getUpdatedPaths(normalizedData);
|
|
1134
1134
|
const docsToCheck = result.map((currentDoc) => applyDocumentUpdateOperators(currentDoc, normalizedData));
|
|
1135
1135
|
const filteredItems = yield Promise.all(docsToCheck.map((currentDoc, index) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1136
|
-
const winningRole = (0, utils_2.
|
|
1136
|
+
const winningRole = yield (0, utils_2.getWinningRoleAsync)(currentDoc, user, roles);
|
|
1137
1137
|
const { status, document } = winningRole
|
|
1138
1138
|
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
1139
1139
|
type: 'write',
|
|
@@ -1187,7 +1187,7 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
1187
1187
|
const data = yield collection.find({ $and: formattedQuery }).toArray();
|
|
1188
1188
|
// Filter and validate each document based on user's roles
|
|
1189
1189
|
const filteredItems = yield Promise.all(data.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1190
|
-
const winningRole = (0, utils_2.
|
|
1190
|
+
const winningRole = yield (0, utils_2.getWinningRoleAsync)(currentDoc, user, roles);
|
|
1191
1191
|
const { status, document } = winningRole
|
|
1192
1192
|
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
1193
1193
|
type: 'delete',
|
|
@@ -35,11 +35,11 @@ export declare const generateContextData: ({ user, services, app, rules, current
|
|
|
35
35
|
request: {
|
|
36
36
|
remoteIPAddress: string | undefined;
|
|
37
37
|
id?: string | undefined;
|
|
38
|
-
host?: string | undefined;
|
|
39
|
-
method?: string | undefined;
|
|
40
|
-
url?: string | undefined;
|
|
41
38
|
ips?: string[];
|
|
39
|
+
host?: string | undefined;
|
|
42
40
|
hostname?: string | undefined;
|
|
41
|
+
url?: string | undefined;
|
|
42
|
+
method?: string | undefined;
|
|
43
43
|
ip?: string | undefined;
|
|
44
44
|
};
|
|
45
45
|
user: unknown;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PermissionExpression } from './interface';
|
|
2
2
|
import { MachineContext } from './machines/interface';
|
|
3
|
+
export declare const evaluateExpandedExpression: (expression: unknown, params: MachineContext["params"], user?: MachineContext["user"]) => Promise<boolean>;
|
|
3
4
|
export declare const evaluateExpression: (params: MachineContext["params"], expression?: PermissionExpression, user?: MachineContext["user"]) => Promise<boolean>;
|
|
4
5
|
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/utils/roles/helpers.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/utils/roles/helpers.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AA+DrD,eAAO,MAAM,0BAA0B,GACrC,YAAY,OAAO,EACnB,QAAQ,cAAc,CAAC,QAAQ,CAAC,EAChC,OAAO,cAAc,CAAC,MAAM,CAAC,KAC5B,OAAO,CAAC,OAAO,CAwDjB,CAAA;AAED,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,cAAc,CAAC,QAAQ,CAAC,EAChC,aAAa,oBAAoB,EACjC,OAAO,cAAc,CAAC,MAAM,CAAC,KAC5B,OAAO,CAAC,OAAO,CAMjB,CAAA"}
|
|
@@ -12,13 +12,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.evaluateExpression = void 0;
|
|
15
|
+
exports.evaluateExpression = exports.evaluateExpandedExpression = void 0;
|
|
16
16
|
const services_1 = require("../../services");
|
|
17
17
|
const state_1 = require("../../state");
|
|
18
18
|
const context_1 = require("../context");
|
|
19
19
|
const rules_1 = require("../rules");
|
|
20
20
|
const utils_1 = __importDefault(require("../rules-matcher/utils"));
|
|
21
21
|
const functionsConditions = ['%%true', '%%false'];
|
|
22
|
+
const andConditions = ['$and', '%and'];
|
|
23
|
+
const orConditions = ['$or', '%or'];
|
|
22
24
|
const normalizeUserRole = (user) => {
|
|
23
25
|
if (!user)
|
|
24
26
|
return user;
|
|
@@ -34,17 +36,84 @@ const normalizeUserRole = (user) => {
|
|
|
34
36
|
? Object.assign(Object.assign({}, candidate), { role: customRole })
|
|
35
37
|
: user;
|
|
36
38
|
};
|
|
39
|
+
const buildEvaluationContext = (params, user) => {
|
|
40
|
+
var _a, _b, _c;
|
|
41
|
+
const normalizedUser = normalizeUserRole(user);
|
|
42
|
+
return Object.assign(Object.assign(Object.assign({}, ((_a = params.expansions) !== null && _a !== void 0 ? _a : {})), ((_b = params.cursor) !== null && _b !== void 0 ? _b : {})), { '%%root': params.cursor, '%%prevRoot': (_c = params.expansions) === null || _c === void 0 ? void 0 : _c['%%prevRoot'], '%%user': normalizedUser, '%%true': true, '%%false': false });
|
|
43
|
+
};
|
|
44
|
+
const getFunctionCondition = (expression) => {
|
|
45
|
+
if (!expression || typeof expression !== 'object' || Array.isArray(expression)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const entries = Object.entries(expression);
|
|
49
|
+
if (entries.length !== 1) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const [key, value] = entries[0];
|
|
53
|
+
if (!functionsConditions.includes(key)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return Object.prototype.hasOwnProperty.call(value, '%function')
|
|
60
|
+
? [key, value]
|
|
61
|
+
: null;
|
|
62
|
+
};
|
|
63
|
+
const evaluateExpandedExpression = (expression, params, user) => __awaiter(void 0, void 0, void 0, function* () {
|
|
64
|
+
if (typeof expression === 'boolean') {
|
|
65
|
+
return expression;
|
|
66
|
+
}
|
|
67
|
+
if (!expression || typeof expression !== 'object') {
|
|
68
|
+
return Boolean(expression);
|
|
69
|
+
}
|
|
70
|
+
const block = expression;
|
|
71
|
+
const functionCondition = getFunctionCondition(block);
|
|
72
|
+
if (functionCondition) {
|
|
73
|
+
return evaluateComplexExpression(functionCondition, params, user);
|
|
74
|
+
}
|
|
75
|
+
const andKey = andConditions.find((key) => Object.prototype.hasOwnProperty.call(block, key));
|
|
76
|
+
if (andKey) {
|
|
77
|
+
const conditions = Array.isArray(block[andKey]) ? block[andKey] : [];
|
|
78
|
+
if (!conditions.length)
|
|
79
|
+
return true;
|
|
80
|
+
for (const condition of conditions) {
|
|
81
|
+
if (!(yield (0, exports.evaluateExpandedExpression)(condition, params, user))) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
const orKey = orConditions.find((key) => Object.prototype.hasOwnProperty.call(block, key));
|
|
88
|
+
if (orKey) {
|
|
89
|
+
const conditions = Array.isArray(block[orKey]) ? block[orKey] : [];
|
|
90
|
+
if (!conditions.length)
|
|
91
|
+
return true;
|
|
92
|
+
for (const condition of conditions) {
|
|
93
|
+
if (yield (0, exports.evaluateExpandedExpression)(condition, params, user)) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const keys = Object.keys(block);
|
|
100
|
+
if (keys.length > 1) {
|
|
101
|
+
for (const key of keys) {
|
|
102
|
+
if (!(yield (0, exports.evaluateExpandedExpression)({ [key]: block[key] }, params, user))) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return utils_1.default.checkRule(block, buildEvaluationContext(params, user), {});
|
|
109
|
+
});
|
|
110
|
+
exports.evaluateExpandedExpression = evaluateExpandedExpression;
|
|
37
111
|
const evaluateExpression = (params, expression, user) => __awaiter(void 0, void 0, void 0, function* () {
|
|
38
|
-
var _a;
|
|
39
112
|
if (!expression || typeof expression === 'boolean')
|
|
40
113
|
return !!expression;
|
|
41
|
-
const
|
|
42
|
-
const value = Object.assign(Object.assign(Object.assign({}, params.expansions), params.cursor), { '%%root': params.cursor, '%%prevRoot': (_a = params.expansions) === null || _a === void 0 ? void 0 : _a['%%prevRoot'], '%%user': normalizedUser, '%%true': true, '%%false': false });
|
|
114
|
+
const value = buildEvaluationContext(params, user);
|
|
43
115
|
const conditions = (0, rules_1.expandQuery)(expression, value);
|
|
44
|
-
|
|
45
|
-
return complexCondition
|
|
46
|
-
? yield evaluateComplexExpression(complexCondition, params, normalizedUser)
|
|
47
|
-
: utils_1.default.checkRule(conditions, value, {});
|
|
116
|
+
return (0, exports.evaluateExpandedExpression)(conditions, params, user);
|
|
48
117
|
});
|
|
49
118
|
exports.evaluateExpression = evaluateExpression;
|
|
50
119
|
const evaluateComplexExpression = (condition, params, user) => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -12,6 +12,7 @@ import { LogMachineInfoParams } from './interface';
|
|
|
12
12
|
* @returns {Role | null} - Returns the first role that matches the `apply_when` condition, or `null` if none match.
|
|
13
13
|
*/
|
|
14
14
|
export declare const getWinningRole: (document: OptionalId<Document> | null, user: User, roles?: Role[]) => Role | null;
|
|
15
|
+
export declare const getWinningRoleAsync: (document: OptionalId<Document> | null, user: User, roles?: Role[]) => Promise<Role | null>;
|
|
15
16
|
/**
|
|
16
17
|
* Checks if the `apply_when` condition is valid for the given user and document.
|
|
17
18
|
*
|
|
@@ -22,6 +23,7 @@ export declare const getWinningRole: (document: OptionalId<Document> | null, use
|
|
|
22
23
|
* @returns {boolean} - Returns `true` if at least one valid rule is found, otherwise `false`.
|
|
23
24
|
*/
|
|
24
25
|
export declare const checkApplyWhen: (apply_when: Role["apply_when"], user: User, document: OptionalId<Document> | null) => boolean;
|
|
26
|
+
export declare const checkApplyWhenAsync: (apply_when: Role["apply_when"], user: User, document: OptionalId<Document> | null) => Promise<boolean>;
|
|
25
27
|
/**
|
|
26
28
|
* Logs machine step information if logging is enabled.
|
|
27
29
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../src/utils/roles/machines/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../src/utils/roles/machines/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAA;AAIzC,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AACnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAElD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GACzB,UAAU,UAAU,CAAC,QAAQ,CAAC,GAAG,IAAI,EACrC,MAAM,IAAI,EACV,QAAO,IAAI,EAAO,KACjB,IAAI,GAAG,IAQT,CAAA;AAED,eAAO,MAAM,mBAAmB,GAC9B,UAAU,UAAU,CAAC,QAAQ,CAAC,GAAG,IAAI,EACrC,MAAM,IAAI,EACV,QAAO,IAAI,EAAO,KACjB,OAAO,CAAC,IAAI,GAAG,IAAI,CAQrB,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GACzB,YAAY,IAAI,CAAC,YAAY,CAAC,EAC9B,MAAM,IAAI,EACV,UAAU,UAAU,CAAC,QAAQ,CAAC,GAAG,IAAI,YAQtC,CAAA;AAED,eAAO,MAAM,mBAAmB,GAC9B,YAAY,IAAI,CAAC,YAAY,CAAC,EAC9B,MAAM,IAAI,EACV,UAAU,UAAU,CAAC,QAAQ,CAAC,GAAG,IAAI,qBActC,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,GAAI,sCAK5B,oBAAoB,SAEtB,CAAA"}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
2
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.logMachineInfo = exports.checkApplyWhen = exports.getWinningRole = void 0;
|
|
12
|
+
exports.logMachineInfo = exports.checkApplyWhenAsync = exports.checkApplyWhen = exports.getWinningRoleAsync = exports.getWinningRole = void 0;
|
|
4
13
|
const utils_1 = require("../../../services/mongodb-atlas/utils");
|
|
14
|
+
const helpers_1 = require("../helpers");
|
|
5
15
|
/**
|
|
6
16
|
* Determines the first applicable role for a given user and document.
|
|
7
17
|
*
|
|
@@ -22,6 +32,17 @@ const getWinningRole = (document, user, roles = []) => {
|
|
|
22
32
|
return null;
|
|
23
33
|
};
|
|
24
34
|
exports.getWinningRole = getWinningRole;
|
|
35
|
+
const getWinningRoleAsync = (document_1, user_1, ...args_1) => __awaiter(void 0, [document_1, user_1, ...args_1], void 0, function* (document, user, roles = []) {
|
|
36
|
+
if (!roles.length)
|
|
37
|
+
return null;
|
|
38
|
+
for (const role of roles) {
|
|
39
|
+
if (yield (0, exports.checkApplyWhenAsync)(role.apply_when, user, document)) {
|
|
40
|
+
return role;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
});
|
|
45
|
+
exports.getWinningRoleAsync = getWinningRoleAsync;
|
|
25
46
|
/**
|
|
26
47
|
* Checks if the `apply_when` condition is valid for the given user and document.
|
|
27
48
|
*
|
|
@@ -40,6 +61,17 @@ const checkApplyWhen = (apply_when, user, document) => {
|
|
|
40
61
|
return !!validRule.length;
|
|
41
62
|
};
|
|
42
63
|
exports.checkApplyWhen = checkApplyWhen;
|
|
64
|
+
const checkApplyWhenAsync = (apply_when, user, document) => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
|
+
return (0, helpers_1.evaluateExpression)({
|
|
66
|
+
type: 'read',
|
|
67
|
+
roles: [],
|
|
68
|
+
cursor: document,
|
|
69
|
+
expansions: {
|
|
70
|
+
'%%prevRoot': undefined
|
|
71
|
+
}
|
|
72
|
+
}, apply_when, user);
|
|
73
|
+
});
|
|
74
|
+
exports.checkApplyWhenAsync = checkApplyWhenAsync;
|
|
43
75
|
/**
|
|
44
76
|
* Logs machine step information if logging is enabled.
|
|
45
77
|
*
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { Rules } from '../../features/rules/interface'
|
|
20
20
|
import { buildRulesMeta } from '../../monitoring/utils'
|
|
21
21
|
import { checkValidation } from '../../utils/roles/machines'
|
|
22
|
-
import {
|
|
22
|
+
import { getWinningRoleAsync } from '../../utils/roles/machines/utils'
|
|
23
23
|
import { emitServiceEvent } from '../monitoring'
|
|
24
24
|
import { CHANGESTREAM } from '../../constants'
|
|
25
25
|
import {
|
|
@@ -561,9 +561,9 @@ const getOperators: GetOperatorsFunction = (
|
|
|
561
561
|
const resolvedOptions =
|
|
562
562
|
projection || normalizedOptions
|
|
563
563
|
? {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
564
|
+
...(normalizedOptions ?? {}),
|
|
565
|
+
...(projection ? { projection } : {})
|
|
566
|
+
}
|
|
567
567
|
: undefined
|
|
568
568
|
const resolvedQuery = query ?? {}
|
|
569
569
|
if (!run_as_system) {
|
|
@@ -604,7 +604,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
604
604
|
return null
|
|
605
605
|
}
|
|
606
606
|
|
|
607
|
-
const winningRole =
|
|
607
|
+
const winningRole = await getWinningRoleAsync(result, user, roles)
|
|
608
608
|
|
|
609
609
|
logDebug('findOne winningRole', {
|
|
610
610
|
collection: collName,
|
|
@@ -613,15 +613,15 @@ const getOperators: GetOperatorsFunction = (
|
|
|
613
613
|
})
|
|
614
614
|
const { status, document } = winningRole
|
|
615
615
|
? await checkValidation(
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
616
|
+
winningRole,
|
|
617
|
+
{
|
|
618
|
+
type: 'read',
|
|
619
|
+
roles,
|
|
620
|
+
cursor: result,
|
|
621
|
+
expansions: getValidationExpansions(result)
|
|
622
|
+
},
|
|
623
|
+
user
|
|
624
|
+
)
|
|
625
625
|
: fallbackAccess(result)
|
|
626
626
|
|
|
627
627
|
// Return validated document or empty object if not permitted
|
|
@@ -669,7 +669,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
669
669
|
|
|
670
670
|
// Retrieve the document to check permissions before deleting
|
|
671
671
|
const result = await collection.findOne(buildAndQuery(formattedQuery))
|
|
672
|
-
const winningRole =
|
|
672
|
+
const winningRole = await getWinningRoleAsync(result, user, roles)
|
|
673
673
|
|
|
674
674
|
logDebug('delete winningRole', {
|
|
675
675
|
collection: collName,
|
|
@@ -678,15 +678,15 @@ const getOperators: GetOperatorsFunction = (
|
|
|
678
678
|
})
|
|
679
679
|
const { status } = winningRole
|
|
680
680
|
? await checkValidation(
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
681
|
+
winningRole,
|
|
682
|
+
{
|
|
683
|
+
type: 'delete',
|
|
684
|
+
roles,
|
|
685
|
+
cursor: result,
|
|
686
|
+
expansions: getValidationExpansions(result)
|
|
687
|
+
},
|
|
688
|
+
user
|
|
689
|
+
)
|
|
690
690
|
: fallbackAccess(result)
|
|
691
691
|
|
|
692
692
|
if (!status) {
|
|
@@ -733,19 +733,19 @@ const getOperators: GetOperatorsFunction = (
|
|
|
733
733
|
collection.collectionName,
|
|
734
734
|
CRUD_OPERATIONS.CREATE
|
|
735
735
|
)
|
|
736
|
-
const winningRole =
|
|
736
|
+
const winningRole = await getWinningRoleAsync(data, user, roles)
|
|
737
737
|
|
|
738
738
|
const { status, document } = winningRole
|
|
739
739
|
? await checkValidation(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
740
|
+
winningRole,
|
|
741
|
+
{
|
|
742
|
+
type: 'insert',
|
|
743
|
+
roles,
|
|
744
|
+
cursor: data,
|
|
745
|
+
expansions: getValidationExpansions()
|
|
746
|
+
},
|
|
747
|
+
user
|
|
748
|
+
)
|
|
749
749
|
: fallbackAccess(data)
|
|
750
750
|
|
|
751
751
|
if (!status || !isEqual(data, document)) {
|
|
@@ -823,7 +823,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
823
823
|
throw new Error('Update not permitted')
|
|
824
824
|
}
|
|
825
825
|
|
|
826
|
-
const winningRole =
|
|
826
|
+
const winningRole = await getWinningRoleAsync(result, user, roles)
|
|
827
827
|
|
|
828
828
|
// Check if the update data contains MongoDB update operators (e.g., $set, $inc)
|
|
829
829
|
const updatedPaths = getUpdatedPaths(normalizedData)
|
|
@@ -831,15 +831,15 @@ const getOperators: GetOperatorsFunction = (
|
|
|
831
831
|
// Validate update permissions
|
|
832
832
|
const { status, document } = winningRole
|
|
833
833
|
? await checkValidation(
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
834
|
+
winningRole,
|
|
835
|
+
{
|
|
836
|
+
type: 'write',
|
|
837
|
+
roles,
|
|
838
|
+
cursor: docToCheck,
|
|
839
|
+
expansions: getValidationExpansions(result)
|
|
840
|
+
},
|
|
841
|
+
user
|
|
842
|
+
)
|
|
843
843
|
: fallbackAccess(docToCheck)
|
|
844
844
|
// Ensure no unauthorized changes are made
|
|
845
845
|
const areDocumentsEqual = areUpdatedFieldsAllowed(
|
|
@@ -918,31 +918,31 @@ const getOperators: GetOperatorsFunction = (
|
|
|
918
918
|
} else {
|
|
919
919
|
const [computedDoc] = Array.isArray(normalizedData)
|
|
920
920
|
? await collection
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
921
|
+
.aggregate([
|
|
922
|
+
{ $match: buildAndQuery(safeQuery) },
|
|
923
|
+
{ $limit: 1 },
|
|
924
|
+
...normalizedData
|
|
925
|
+
])
|
|
926
|
+
.toArray()
|
|
927
927
|
: [applyDocumentUpdateOperators(currentDoc, normalizedData as Document)]
|
|
928
928
|
docToCheck = computedDoc
|
|
929
929
|
}
|
|
930
930
|
|
|
931
|
-
const winningRole =
|
|
931
|
+
const winningRole = await getWinningRoleAsync(docToCheck, user, roles)
|
|
932
932
|
|
|
933
933
|
const { status, document } = winningRole
|
|
934
934
|
? await checkValidation(
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
935
|
+
winningRole,
|
|
936
|
+
{
|
|
937
|
+
type: validationType,
|
|
938
|
+
roles,
|
|
939
|
+
cursor: docToCheck,
|
|
940
|
+
expansions: getValidationExpansions(
|
|
941
|
+
validationType === 'insert' ? undefined : currentDoc
|
|
942
|
+
)
|
|
943
|
+
},
|
|
944
|
+
user
|
|
945
|
+
)
|
|
946
946
|
: fallbackAccess(docToCheck)
|
|
947
947
|
|
|
948
948
|
const areDocumentsEqual = areUpdatedFieldsAllowed(
|
|
@@ -956,28 +956,28 @@ const getOperators: GetOperatorsFunction = (
|
|
|
956
956
|
|
|
957
957
|
const updateResult = normalizedOptions
|
|
958
958
|
? await collection.findOneAndUpdate(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
959
|
+
buildAndQuery(safeQuery),
|
|
960
|
+
normalizedData,
|
|
961
|
+
normalizedOptions
|
|
962
|
+
)
|
|
963
963
|
: await collection.findOneAndUpdate(buildAndQuery(safeQuery), normalizedData)
|
|
964
964
|
if (!updateResult) {
|
|
965
965
|
emitMongoEvent('findOneAndUpdate')
|
|
966
966
|
return updateResult
|
|
967
967
|
}
|
|
968
968
|
|
|
969
|
-
const readRole =
|
|
969
|
+
const readRole = await getWinningRoleAsync(updateResult, user, roles)
|
|
970
970
|
const readResult = readRole
|
|
971
971
|
? await checkValidation(
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
972
|
+
readRole,
|
|
973
|
+
{
|
|
974
|
+
type: 'read',
|
|
975
|
+
roles,
|
|
976
|
+
cursor: updateResult,
|
|
977
|
+
expansions: getValidationExpansions(updateResult)
|
|
978
|
+
},
|
|
979
|
+
user
|
|
980
|
+
)
|
|
981
981
|
: fallbackAccess(updateResult)
|
|
982
982
|
|
|
983
983
|
const sanitizedDoc = readResult.status
|
|
@@ -1026,9 +1026,9 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1026
1026
|
const resolvedOptions =
|
|
1027
1027
|
projection || normalizedOptions
|
|
1028
1028
|
? {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1029
|
+
...(normalizedOptions ?? {}),
|
|
1030
|
+
...(projection ? { projection } : {})
|
|
1031
|
+
}
|
|
1032
1032
|
: undefined
|
|
1033
1033
|
if (!run_as_system) {
|
|
1034
1034
|
checkDenyOperation(
|
|
@@ -1053,7 +1053,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1053
1053
|
|
|
1054
1054
|
const filteredResponse = await Promise.all(
|
|
1055
1055
|
response.map(async (currentDoc) => {
|
|
1056
|
-
const winningRole =
|
|
1056
|
+
const winningRole = await getWinningRoleAsync(currentDoc, user, roles)
|
|
1057
1057
|
|
|
1058
1058
|
logDebug('find winningRole', {
|
|
1059
1059
|
collection: collName,
|
|
@@ -1063,15 +1063,15 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1063
1063
|
})
|
|
1064
1064
|
const { status, document } = winningRole
|
|
1065
1065
|
? await checkValidation(
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1066
|
+
winningRole,
|
|
1067
|
+
{
|
|
1068
|
+
type: 'read',
|
|
1069
|
+
roles,
|
|
1070
|
+
cursor: currentDoc,
|
|
1071
|
+
expansions: getValidationExpansions(currentDoc)
|
|
1072
|
+
},
|
|
1073
|
+
user
|
|
1074
|
+
)
|
|
1075
1075
|
: fallbackAccess(currentDoc)
|
|
1076
1076
|
|
|
1077
1077
|
return status ? document : undefined
|
|
@@ -1190,19 +1190,19 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1190
1190
|
const allowDeleteBypass = watchPipelineRequestsDelete(requestedPipeline)
|
|
1191
1191
|
const firstStep = watchFormattedQuery.length
|
|
1192
1192
|
? {
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
$and: watchFormattedQuery
|
|
1198
|
-
},
|
|
1199
|
-
{ operationType: 'delete' }
|
|
1200
|
-
]
|
|
1201
|
-
}
|
|
1202
|
-
: {
|
|
1193
|
+
$match: allowDeleteBypass
|
|
1194
|
+
? {
|
|
1195
|
+
$or: [
|
|
1196
|
+
{
|
|
1203
1197
|
$and: watchFormattedQuery
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1198
|
+
},
|
|
1199
|
+
{ operationType: 'delete' }
|
|
1200
|
+
]
|
|
1201
|
+
}
|
|
1202
|
+
: {
|
|
1203
|
+
$and: watchFormattedQuery
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
1206
|
: undefined
|
|
1207
1207
|
|
|
1208
1208
|
const formattedPipeline = [firstStep, ...requestedPipeline].filter(
|
|
@@ -1221,33 +1221,33 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1221
1221
|
const isValidChange = async (change: Document) => {
|
|
1222
1222
|
const { fullDocument, updateDescription } = change
|
|
1223
1223
|
const hasFullDocument = !!fullDocument
|
|
1224
|
-
const winningRole =
|
|
1224
|
+
const winningRole = await getWinningRoleAsync(fullDocument, user, roles)
|
|
1225
1225
|
|
|
1226
1226
|
const fullDocumentValidation = winningRole
|
|
1227
1227
|
? await checkValidation(
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1228
|
+
winningRole,
|
|
1229
|
+
{
|
|
1230
|
+
type: 'read',
|
|
1231
|
+
roles,
|
|
1232
|
+
cursor: fullDocument,
|
|
1233
|
+
expansions: getValidationExpansions(fullDocument)
|
|
1234
|
+
},
|
|
1235
|
+
user
|
|
1236
|
+
)
|
|
1237
1237
|
: fallbackAccess(fullDocument)
|
|
1238
1238
|
const { status, document } = fullDocumentValidation
|
|
1239
1239
|
|
|
1240
1240
|
const { status: updatedFieldsStatus, document: updatedFields } = winningRole
|
|
1241
1241
|
? await checkValidation(
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1242
|
+
winningRole,
|
|
1243
|
+
{
|
|
1244
|
+
type: 'read',
|
|
1245
|
+
roles,
|
|
1246
|
+
cursor: updateDescription?.updatedFields,
|
|
1247
|
+
expansions: getValidationExpansions(fullDocument)
|
|
1248
|
+
},
|
|
1249
|
+
user
|
|
1250
|
+
)
|
|
1251
1251
|
: fallbackAccess(updateDescription?.updatedFields)
|
|
1252
1252
|
|
|
1253
1253
|
return {
|
|
@@ -1389,19 +1389,19 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1389
1389
|
// Validate each document against user's roles
|
|
1390
1390
|
const filteredItems = await Promise.all(
|
|
1391
1391
|
documents.map(async (currentDoc) => {
|
|
1392
|
-
const winningRole =
|
|
1392
|
+
const winningRole = await getWinningRoleAsync(currentDoc, user, roles)
|
|
1393
1393
|
|
|
1394
1394
|
const { status, document } = winningRole
|
|
1395
1395
|
? await checkValidation(
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1396
|
+
winningRole,
|
|
1397
|
+
{
|
|
1398
|
+
type: 'insert',
|
|
1399
|
+
roles,
|
|
1400
|
+
cursor: currentDoc,
|
|
1401
|
+
expansions: getValidationExpansions()
|
|
1402
|
+
},
|
|
1403
|
+
user
|
|
1404
|
+
)
|
|
1405
1405
|
: fallbackAccess(currentDoc)
|
|
1406
1406
|
|
|
1407
1407
|
return status ? document : undefined
|
|
@@ -1453,19 +1453,19 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1453
1453
|
|
|
1454
1454
|
const filteredItems = await Promise.all(
|
|
1455
1455
|
docsToCheck.map(async (currentDoc, index) => {
|
|
1456
|
-
const winningRole =
|
|
1456
|
+
const winningRole = await getWinningRoleAsync(currentDoc, user, roles)
|
|
1457
1457
|
|
|
1458
1458
|
const { status, document } = winningRole
|
|
1459
1459
|
? await checkValidation(
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1460
|
+
winningRole,
|
|
1461
|
+
{
|
|
1462
|
+
type: 'write',
|
|
1463
|
+
roles,
|
|
1464
|
+
cursor: currentDoc,
|
|
1465
|
+
expansions: getValidationExpansions(result[index])
|
|
1466
|
+
},
|
|
1467
|
+
user
|
|
1468
|
+
)
|
|
1469
1469
|
: fallbackAccess(currentDoc)
|
|
1470
1470
|
|
|
1471
1471
|
return status ? document : undefined
|
|
@@ -1529,19 +1529,19 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1529
1529
|
// Filter and validate each document based on user's roles
|
|
1530
1530
|
const filteredItems = await Promise.all(
|
|
1531
1531
|
data.map(async (currentDoc) => {
|
|
1532
|
-
const winningRole =
|
|
1532
|
+
const winningRole = await getWinningRoleAsync(currentDoc, user, roles)
|
|
1533
1533
|
|
|
1534
1534
|
const { status, document } = winningRole
|
|
1535
1535
|
? await checkValidation(
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1536
|
+
winningRole,
|
|
1537
|
+
{
|
|
1538
|
+
type: 'delete',
|
|
1539
|
+
roles,
|
|
1540
|
+
cursor: currentDoc,
|
|
1541
|
+
expansions: getValidationExpansions(currentDoc)
|
|
1542
|
+
},
|
|
1543
|
+
user
|
|
1544
|
+
)
|
|
1545
1545
|
: fallbackAccess(currentDoc)
|
|
1546
1546
|
|
|
1547
1547
|
return status ? document : undefined
|
|
@@ -1,7 +1,48 @@
|
|
|
1
1
|
import { evaluateExpression } from '../roles/helpers'
|
|
2
2
|
import { Params } from '../roles/interface'
|
|
3
|
+
import { GenerateContext } from '../context'
|
|
4
|
+
import { StateManager } from '../../state'
|
|
5
|
+
|
|
6
|
+
jest.mock('../context', () => ({
|
|
7
|
+
GenerateContext: jest.fn()
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
jest.mock('../../state', () => ({
|
|
11
|
+
StateManager: {
|
|
12
|
+
select: jest.fn()
|
|
13
|
+
}
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
jest.mock('../../services', () => ({
|
|
17
|
+
services: {}
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
const mockedGenerateContext = jest.mocked(GenerateContext)
|
|
21
|
+
const mockedSelect = jest.mocked(StateManager.select)
|
|
3
22
|
|
|
4
23
|
describe('evaluateExpression', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks()
|
|
26
|
+
mockedSelect.mockImplementation(((key: string) => {
|
|
27
|
+
switch (key) {
|
|
28
|
+
case 'functions':
|
|
29
|
+
return {
|
|
30
|
+
checkAccess: {
|
|
31
|
+
name: 'checkAccess'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
case 'app':
|
|
35
|
+
return {
|
|
36
|
+
id: 'app-id'
|
|
37
|
+
}
|
|
38
|
+
case 'rules':
|
|
39
|
+
return {}
|
|
40
|
+
default:
|
|
41
|
+
return undefined
|
|
42
|
+
}
|
|
43
|
+
}) as never)
|
|
44
|
+
})
|
|
45
|
+
|
|
5
46
|
it('supports insert-only write expressions that rely on %%prevRoot', async () => {
|
|
6
47
|
const expression = {
|
|
7
48
|
'%%prevRoot': {
|
|
@@ -30,4 +71,52 @@ describe('evaluateExpression', () => {
|
|
|
30
71
|
await expect(evaluateExpression(insertParams, expression)).resolves.toBe(true)
|
|
31
72
|
await expect(evaluateExpression(readParams, expression)).resolves.toBe(false)
|
|
32
73
|
})
|
|
74
|
+
|
|
75
|
+
it('supports nested %function conditions inside %or/%and expressions', async () => {
|
|
76
|
+
mockedGenerateContext.mockResolvedValue(true)
|
|
77
|
+
|
|
78
|
+
const expression = {
|
|
79
|
+
'%or': [
|
|
80
|
+
{
|
|
81
|
+
'%%root.company': 'company-1'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
'%and': [
|
|
85
|
+
{
|
|
86
|
+
'%%user.custom_data.type': 'customer'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
'%%true': {
|
|
90
|
+
'%function': {
|
|
91
|
+
name: 'checkAccess',
|
|
92
|
+
arguments: ['%%root.company']
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const params = {
|
|
102
|
+
type: 'read',
|
|
103
|
+
cursor: { company: 'company-2' },
|
|
104
|
+
expansions: {},
|
|
105
|
+
roles: []
|
|
106
|
+
} as Params
|
|
107
|
+
|
|
108
|
+
const user = {
|
|
109
|
+
custom_data: {
|
|
110
|
+
type: 'customer'
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await expect(evaluateExpression(params, expression, user as never)).resolves.toBe(true)
|
|
115
|
+
expect(mockedGenerateContext).toHaveBeenCalledWith(
|
|
116
|
+
expect.objectContaining({
|
|
117
|
+
args: ['company-2'],
|
|
118
|
+
functionName: 'checkAccess'
|
|
119
|
+
})
|
|
120
|
+
)
|
|
121
|
+
})
|
|
33
122
|
})
|
|
@@ -70,4 +70,21 @@ describe('getWinningRole', () => {
|
|
|
70
70
|
const result = getWinningRole(null, mockUser, mockRoles)
|
|
71
71
|
expect(result).toEqual(mockRoles[0])
|
|
72
72
|
})
|
|
73
|
+
|
|
74
|
+
it('should return the first matching role asynchronously', async () => {
|
|
75
|
+
const mockCheckApplyWhenAsync = jest
|
|
76
|
+
.spyOn(Utils, 'checkApplyWhenAsync')
|
|
77
|
+
.mockResolvedValueOnce(true)
|
|
78
|
+
|
|
79
|
+
await expect(Utils.getWinningRoleAsync(mockDocument, mockUser, mockRoles)).resolves.toEqual(
|
|
80
|
+
mockRoles[0]
|
|
81
|
+
)
|
|
82
|
+
expect(mockCheckApplyWhenAsync).toHaveBeenCalledWith(
|
|
83
|
+
mockRoles[0].apply_when,
|
|
84
|
+
mockUser,
|
|
85
|
+
mockDocument
|
|
86
|
+
)
|
|
87
|
+
expect(mockCheckApplyWhenAsync).toHaveBeenCalledTimes(1)
|
|
88
|
+
mockCheckApplyWhenAsync.mockReset()
|
|
89
|
+
})
|
|
73
90
|
})
|
|
@@ -7,6 +7,8 @@ import { PermissionExpression } from './interface'
|
|
|
7
7
|
import { MachineContext } from './machines/interface'
|
|
8
8
|
|
|
9
9
|
const functionsConditions = ['%%true', '%%false']
|
|
10
|
+
const andConditions = ['$and', '%and']
|
|
11
|
+
const orConditions = ['$or', '%or']
|
|
10
12
|
|
|
11
13
|
const normalizeUserRole = (user?: MachineContext['user']) => {
|
|
12
14
|
if (!user) return user
|
|
@@ -22,30 +24,121 @@ const normalizeUserRole = (user?: MachineContext['user']) => {
|
|
|
22
24
|
: user
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
const buildEvaluationContext = (
|
|
26
28
|
params: MachineContext['params'],
|
|
27
|
-
expression?: PermissionExpression,
|
|
28
29
|
user?: MachineContext['user']
|
|
29
|
-
)
|
|
30
|
-
if (!expression || typeof expression === 'boolean') return !!expression
|
|
30
|
+
) => {
|
|
31
31
|
const normalizedUser = normalizeUserRole(user)
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
...params.expansions,
|
|
35
|
-
...params.cursor,
|
|
33
|
+
return {
|
|
34
|
+
...(params.expansions ?? {}),
|
|
35
|
+
...(params.cursor ?? {}),
|
|
36
36
|
'%%root': params.cursor,
|
|
37
37
|
'%%prevRoot': params.expansions?.['%%prevRoot'],
|
|
38
38
|
'%%user': normalizedUser,
|
|
39
39
|
'%%true': true,
|
|
40
40
|
'%%false': false
|
|
41
41
|
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const getFunctionCondition = (
|
|
45
|
+
expression: unknown
|
|
46
|
+
): [string, Record<string, any>] | null => {
|
|
47
|
+
if (!expression || typeof expression !== 'object' || Array.isArray(expression)) {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const entries = Object.entries(expression as Record<string, unknown>)
|
|
52
|
+
if (entries.length !== 1) {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const [key, value] = entries[0]
|
|
57
|
+
if (!functionsConditions.includes(key)) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Object.prototype.hasOwnProperty.call(value, '%function')
|
|
66
|
+
? [key, value as Record<string, any>]
|
|
67
|
+
: null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const evaluateExpandedExpression = async (
|
|
71
|
+
expression: unknown,
|
|
72
|
+
params: MachineContext['params'],
|
|
73
|
+
user?: MachineContext['user']
|
|
74
|
+
): Promise<boolean> => {
|
|
75
|
+
if (typeof expression === 'boolean') {
|
|
76
|
+
return expression
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!expression || typeof expression !== 'object') {
|
|
80
|
+
return Boolean(expression)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const block = expression as Record<string, unknown>
|
|
84
|
+
const functionCondition = getFunctionCondition(block)
|
|
85
|
+
|
|
86
|
+
if (functionCondition) {
|
|
87
|
+
return evaluateComplexExpression(functionCondition, params, user)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const andKey = andConditions.find((key) => Object.prototype.hasOwnProperty.call(block, key))
|
|
91
|
+
if (andKey) {
|
|
92
|
+
const conditions = Array.isArray(block[andKey]) ? (block[andKey] as unknown[]) : []
|
|
93
|
+
if (!conditions.length) return true
|
|
94
|
+
|
|
95
|
+
for (const condition of conditions) {
|
|
96
|
+
if (!(await evaluateExpandedExpression(condition, params, user))) {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const orKey = orConditions.find((key) => Object.prototype.hasOwnProperty.call(block, key))
|
|
105
|
+
if (orKey) {
|
|
106
|
+
const conditions = Array.isArray(block[orKey]) ? (block[orKey] as unknown[]) : []
|
|
107
|
+
if (!conditions.length) return true
|
|
108
|
+
|
|
109
|
+
for (const condition of conditions) {
|
|
110
|
+
if (await evaluateExpandedExpression(condition, params, user)) {
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const keys = Object.keys(block)
|
|
119
|
+
if (keys.length > 1) {
|
|
120
|
+
for (const key of keys) {
|
|
121
|
+
if (!(await evaluateExpandedExpression({ [key]: block[key] }, params, user))) {
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return rulesMatcherUtils.checkRule(block as never, buildEvaluationContext(params, user), {})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const evaluateExpression = async (
|
|
133
|
+
params: MachineContext['params'],
|
|
134
|
+
expression?: PermissionExpression,
|
|
135
|
+
user?: MachineContext['user']
|
|
136
|
+
): Promise<boolean> => {
|
|
137
|
+
if (!expression || typeof expression === 'boolean') return !!expression
|
|
138
|
+
|
|
139
|
+
const value = buildEvaluationContext(params, user)
|
|
42
140
|
const conditions = expandQuery(expression, value)
|
|
43
|
-
|
|
44
|
-
([key]) => functionsConditions.includes(key)
|
|
45
|
-
)
|
|
46
|
-
return complexCondition
|
|
47
|
-
? await evaluateComplexExpression(complexCondition, params, normalizedUser)
|
|
48
|
-
: rulesMatcherUtils.checkRule(conditions, value, {})
|
|
141
|
+
return evaluateExpandedExpression(conditions, params, user)
|
|
49
142
|
}
|
|
50
143
|
|
|
51
144
|
const evaluateComplexExpression = async (
|
|
@@ -74,7 +167,7 @@ const evaluateComplexExpression = async (
|
|
|
74
167
|
const expandedArguments =
|
|
75
168
|
fnArguments && fnArguments.length
|
|
76
169
|
? ((expandQuery({ args: fnArguments }, expansionContext) as { args: unknown[] })
|
|
77
|
-
|
|
170
|
+
.args ?? [])
|
|
78
171
|
: [params.cursor]
|
|
79
172
|
|
|
80
173
|
const response = await GenerateContext({
|
|
@@ -2,6 +2,7 @@ import { Document, OptionalId } from 'mongodb'
|
|
|
2
2
|
import { User } from '../../../auth/dtos'
|
|
3
3
|
import { Filter } from '../../../features/rules/interface'
|
|
4
4
|
import { getValidRule } from '../../../services/mongodb-atlas/utils'
|
|
5
|
+
import { evaluateExpression } from '../helpers'
|
|
5
6
|
import { Role } from '../interface'
|
|
6
7
|
import { LogMachineInfoParams } from './interface'
|
|
7
8
|
|
|
@@ -28,6 +29,20 @@ export const getWinningRole = (
|
|
|
28
29
|
return null
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
export const getWinningRoleAsync = async (
|
|
33
|
+
document: OptionalId<Document> | null,
|
|
34
|
+
user: User,
|
|
35
|
+
roles: Role[] = []
|
|
36
|
+
): Promise<Role | null> => {
|
|
37
|
+
if (!roles.length) return null
|
|
38
|
+
for (const role of roles) {
|
|
39
|
+
if (await checkApplyWhenAsync(role.apply_when, user, document)) {
|
|
40
|
+
return role
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
/**
|
|
32
47
|
* Checks if the `apply_when` condition is valid for the given user and document.
|
|
33
48
|
*
|
|
@@ -50,6 +65,25 @@ export const checkApplyWhen = (
|
|
|
50
65
|
return !!validRule.length
|
|
51
66
|
}
|
|
52
67
|
|
|
68
|
+
export const checkApplyWhenAsync = async (
|
|
69
|
+
apply_when: Role['apply_when'],
|
|
70
|
+
user: User,
|
|
71
|
+
document: OptionalId<Document> | null
|
|
72
|
+
) => {
|
|
73
|
+
return evaluateExpression(
|
|
74
|
+
{
|
|
75
|
+
type: 'read',
|
|
76
|
+
roles: [],
|
|
77
|
+
cursor: document,
|
|
78
|
+
expansions: {
|
|
79
|
+
'%%prevRoot': undefined
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
apply_when,
|
|
83
|
+
user
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
53
87
|
/**
|
|
54
88
|
* Logs machine step information if logging is enabled.
|
|
55
89
|
*
|