data-primals-engine 1.6.2 → 1.6.5
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/README.md +56 -911
- package/client/package-lock.json +64 -50
- package/client/package.json +6 -0
- package/client/src/App.scss +29 -0
- package/client/src/Dashboard.jsx +1 -1
- package/client/src/WorkflowEditor.jsx +101 -0
- package/client/src/WorkflowEditor.scss +29 -4
- package/client/src/_variables.scss +3 -0
- package/doc/automation-workflows.md +102 -0
- package/doc/core-concepts.md +33 -0
- package/doc/custom-api-endpoints.md +40 -0
- package/doc/dashboards-kpis-charts.md +49 -0
- package/doc/data-management.md +120 -0
- package/doc/data-models.md +75 -0
- package/doc/index.md +14 -0
- package/doc/roles-permissions.md +43 -0
- package/doc/users.md +30 -0
- package/package.json +7 -5
- package/src/client.js +6 -4
- package/src/core.js +31 -12
- package/src/defaultModels +1628 -0
- package/src/defaultModels.js +14 -0
- package/src/filter.js +110 -42
- package/src/modules/data/data.history.js +5 -1
- package/src/modules/data/data.js +2 -1
- package/src/modules/data/data.operations.js +116 -72
- package/src/modules/data/data.relations.js +1 -1
- package/src/modules/mongodb.js +2 -1
- package/src/modules/swagger.js +25 -5
- package/src/modules/user.js +138 -76
- package/src/modules/workflow.js +187 -177
- package/src/providers.js +1 -1
- package/swagger-en.yml +2308 -472
- package/swagger-fr.yml +487 -3
- package/test/core.test.js +341 -0
- package/test/data.history.integration.test.js +140 -16
- package/test/data.integration.test.js +137 -2
- package/test/user.test.js +201 -280
- package/test/workflow.integration.test.js +8 -0
package/src/modules/user.js
CHANGED
|
@@ -15,7 +15,10 @@ import rateLimit from "express-rate-limit";
|
|
|
15
15
|
import ivm from "isolated-vm";
|
|
16
16
|
import {emailDefaultConfig} from "../constants.js";
|
|
17
17
|
import {safeAssignObject} from "../core.js";
|
|
18
|
+
import {substituteVariables} from "./workflow.js";
|
|
19
|
+
import NodeCache from "node-cache";
|
|
18
20
|
import {Config} from "../config.js";
|
|
21
|
+
import { Event } from '../events.js';
|
|
19
22
|
|
|
20
23
|
export const userInitiator = async (req, res, next) => {
|
|
21
24
|
|
|
@@ -98,127 +101,186 @@ let logger,engine;
|
|
|
98
101
|
export async function onInit(defaultEngine) {
|
|
99
102
|
engine = defaultEngine;
|
|
100
103
|
logger = engine.getComponent(Logger);
|
|
104
|
+
|
|
105
|
+
const invalidatePermissionsCache = (engine, {modelName, insertedDocs, user}) => {
|
|
106
|
+
const modelsToWatch = ['role', 'permission', 'userPermission'];
|
|
107
|
+
|
|
108
|
+
// Le payload contient le nom du modèle qui a été modifié.
|
|
109
|
+
if (modelName && modelsToWatch.includes(modelName)) {
|
|
110
|
+
logger.info(`[Permissions] Invalidating permissions cache due to change in '${modelName}'.`);
|
|
111
|
+
permissionsCache.flushAll();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// On attache notre fonction d'invalidation aux événements système.
|
|
116
|
+
Event.Listen("OnDataAdded", invalidatePermissionsCache, "event", "system");
|
|
117
|
+
Event.Listen("OnDataEdited", invalidatePermissionsCache, "event", "system");
|
|
118
|
+
Event.Listen("OnDataDeleted", invalidatePermissionsCache, "event", "system");
|
|
101
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Cache pour les permissions des utilisateurs.
|
|
122
|
+
* TTL de 10 minutes, vérification toutes les 2 minutes.
|
|
123
|
+
*/
|
|
124
|
+
const permissionsCache = new NodeCache({ stdTTL: 600, checkperiod: 120, useClones: false });
|
|
125
|
+
|
|
102
126
|
/**
|
|
103
127
|
* Calcule et retourne l'ensemble des permissions actives pour un utilisateur.
|
|
104
128
|
* Cette fonction interne est la pierre angulaire de la nouvelle logique de permission.
|
|
105
129
|
* 1. Elle récupère toutes les permissions de base issues des rôles de l'utilisateur.
|
|
106
130
|
* 2. Elle applique ensuite les "exceptions" (ajouts ou retraits de permissions) qui sont valides (non expirées).
|
|
107
131
|
* @param {object} user - L'objet utilisateur pour lequel calculer les permissions.
|
|
108
|
-
* @returns {Promise<
|
|
132
|
+
* @returns {Promise<Map<string, object|null>>} Une Map contenant les noms des permissions actives et leur filtre associé.
|
|
109
133
|
* @private
|
|
110
134
|
*/
|
|
111
135
|
export async function getUserActivePermissions(user, env = null) {
|
|
136
|
+
// Clé de cache unique pour l'utilisateur et l'environnement
|
|
137
|
+
const cacheKey = `${user._user || user.username}:${user._id.toString()}:${env || 'global'}`;
|
|
138
|
+
const cachedPermissions = permissionsCache.get(cacheKey);
|
|
139
|
+
if (cachedPermissions) {
|
|
140
|
+
logger.debug(`[Permissions] Cache hit for user ${user.username}`);
|
|
141
|
+
return cachedPermissions;
|
|
142
|
+
}
|
|
143
|
+
logger.debug(`[Permissions] Cache miss for user ${user.username}. Calculating permissions...`);
|
|
112
144
|
const datasCollection = await getCollectionForUser(user);
|
|
113
145
|
const now = new Date();
|
|
114
|
-
const activePermissions = new
|
|
146
|
+
const activePermissions = new Map();
|
|
115
147
|
|
|
116
148
|
// --- ÉTAPE 1: Récupérer les permissions de base des rôles ---
|
|
117
149
|
if (user.roles && user.roles.length > 0) {
|
|
118
150
|
const roleIds = user.roles.map(id => new ObjectId(id));
|
|
119
151
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{ $
|
|
123
|
-
{
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
{ $
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
rolePermissions.forEach(p =>
|
|
152
|
+
// Étape 1: Récupérer tous les IDs de permission des rôles de l'utilisateur.
|
|
153
|
+
const roles = await datasCollection.find(
|
|
154
|
+
{ _id: { $in: roleIds }, _model: "role", _user: user._user || user.username },
|
|
155
|
+
{ projection: { permissions: 1, _user: 1 } }
|
|
156
|
+
).toArray();
|
|
157
|
+
|
|
158
|
+
// Aplatir tous les tableaux de permissions en un seul et supprimer les doublons.
|
|
159
|
+
const permissionIdStrings = [...new Set(roles.flatMap(role => role.permissions || []))];
|
|
160
|
+
const permissionObjectIds = permissionIdStrings.map(id => new ObjectId(id));
|
|
161
|
+
|
|
162
|
+
// Étape 2: Récupérer tous les documents de permission correspondants en une seule requête.
|
|
163
|
+
const rolePermissions = await datasCollection.find(
|
|
164
|
+
{ _id: { $in: permissionObjectIds }, _model: "permission", _user: user._user || user.username },
|
|
165
|
+
{ projection: { name: 1, filter: 1 } }
|
|
166
|
+
).toArray();
|
|
167
|
+
|
|
168
|
+
rolePermissions.forEach(p => {
|
|
169
|
+
if (p.name) {
|
|
170
|
+
// On stocke l'objet permission complet pour un accès ultérieur
|
|
171
|
+
activePermissions.set(p.name, { filter: p.filter ?? null, details: p });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
137
174
|
}
|
|
138
175
|
|
|
139
|
-
// --- ÉTAPE
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
{
|
|
147
|
-
$or: [ // La permission est soit globale, soit spécifique à l'environnement demandé
|
|
148
|
-
{ env: { $exists: false } },
|
|
149
|
-
{ env: env }
|
|
150
|
-
]
|
|
151
|
-
},
|
|
152
|
-
{ $or: [{ expiresAt: { $exists: false } }, { expiresAt: { $gt: now } }] }
|
|
153
|
-
]
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
$lookup: {
|
|
158
|
-
from: datasCollection.collectionName,
|
|
159
|
-
let: { permissionId: "$permission" },
|
|
160
|
-
pipeline: [
|
|
161
|
-
{
|
|
162
|
-
$match: {
|
|
163
|
-
$expr: {
|
|
164
|
-
$eq: [
|
|
165
|
-
"$_id",
|
|
166
|
-
{ $toObjectId: "$$permissionId" } // Conversion ici
|
|
167
|
-
]
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
{ $project: { name: 1 } }
|
|
172
|
-
],
|
|
173
|
-
as: 'permissionDoc'
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
{ $unwind: '$permissionDoc' }
|
|
177
|
-
]).toArray();
|
|
176
|
+
// --- NOUVELLE ÉTAPE : Pré-charger les détails de toutes les permissions d'exception ---
|
|
177
|
+
// Cela garantit que nous avons les noms de permission même pour les révocations.
|
|
178
|
+
const exceptionPermissionIds = (await datasCollection.distinct("permission", {
|
|
179
|
+
_model: "userPermission",
|
|
180
|
+
user: user._id.toString(),
|
|
181
|
+
_user: user._user || user.username
|
|
182
|
+
})).map(id => new ObjectId(id));
|
|
178
183
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
const exceptionPermsDetails = await datasCollection.find(
|
|
185
|
+
{ _id: { $in: exceptionPermissionIds }, _model: "permission" },
|
|
186
|
+
{ projection: { name: 1, filter: 1 } }
|
|
187
|
+
).toArray();
|
|
183
188
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
189
|
+
const allPermDetailsMap = new Map(exceptionPermsDetails.map(p => [p._id.toString(), p]));
|
|
190
|
+
|
|
191
|
+
// --- ÉTAPE 2: Appliquer les exceptions de permission ---
|
|
192
|
+
const exceptionsQuery = {
|
|
193
|
+
_model: "userPermission",
|
|
194
|
+
user: user._id.toString(),
|
|
195
|
+
_user: user._user || user.username,
|
|
196
|
+
$and: [
|
|
197
|
+
{ $or: [{ expiresAt: { $exists: false } }, { expiresAt: { $gt: now } }] },
|
|
198
|
+
{ $or: [{ env: { $exists: false } }, { env: env }] }
|
|
199
|
+
]
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const exceptions = await datasCollection.find(exceptionsQuery, {
|
|
203
|
+
projection: { permission: 1, isGranted: 1, filter: 1 } // Le filtre sur l'exception
|
|
204
|
+
}).toArray();
|
|
205
|
+
|
|
206
|
+
if (exceptions.length > 0) {
|
|
207
|
+
for (const exception of exceptions) {
|
|
208
|
+
// Utiliser la map pré-chargée qui contient toutes les permissions d'exception
|
|
209
|
+
const permDetails = allPermDetailsMap.get(exception.permission);
|
|
210
|
+
if (!permDetails?.name) continue;
|
|
211
|
+
|
|
212
|
+
if (exception.isGranted) {
|
|
213
|
+
// Priorité 1: Le filtre défini sur l'exception elle-même.
|
|
214
|
+
// Priorité 2: Le filtre défini sur la permission de base.
|
|
215
|
+
const finalFilter = exception.filter ?? permDetails.filter ?? null;
|
|
216
|
+
// On met à jour l'entrée existante ou on en crée une nouvelle
|
|
217
|
+
activePermissions.set(permDetails.name, { filter: finalFilter, details: permDetails });
|
|
218
|
+
} else {
|
|
219
|
+
activePermissions.delete(permDetails.name);
|
|
220
|
+
}
|
|
188
221
|
}
|
|
189
222
|
}
|
|
190
223
|
|
|
191
|
-
|
|
224
|
+
// Mettre le résultat en cache avant de le retourner
|
|
225
|
+
// On ne stocke que le nom et le filtre, pas l'objet 'details' complet
|
|
226
|
+
const finalPermissionsToCache = new Map();
|
|
227
|
+
for (const [name, permData] of activePermissions.entries()) {
|
|
228
|
+
finalPermissionsToCache.set(name, permData.filter);
|
|
229
|
+
}
|
|
230
|
+
permissionsCache.set(cacheKey, finalPermissionsToCache);
|
|
231
|
+
return finalPermissionsToCache;
|
|
192
232
|
}
|
|
193
233
|
|
|
194
234
|
/**
|
|
195
235
|
* Vérifie si un utilisateur possède au moins une des permissions spécifiées.
|
|
196
236
|
* Cette fonction utilise la nouvelle logique basée sur les rôles et les exceptions de permission.
|
|
197
237
|
* @param {string|string[]} permissionNames - Le nom de la permission ou un tableau de noms.
|
|
198
|
-
* @param {object} user - L'objet utilisateur
|
|
199
|
-
* @
|
|
238
|
+
* @param {object} user - L'objet utilisateur.
|
|
239
|
+
* @param {string|null} env - L'environnement de la permission.
|
|
240
|
+
* @param {object|null} req - L'objet requête Express optionnel.
|
|
241
|
+
* @returns {Promise<boolean|object>} - `false` si aucune permission n'est trouvée. Sinon, retourne le filtre de la première permission trouvée (ou `true` si aucun filtre n'est défini).
|
|
200
242
|
*/
|
|
201
|
-
export async function hasPermission(permissionNames, user, env = null) {
|
|
243
|
+
export async function hasPermission(permissionNames, user, env = null, req = null) {
|
|
202
244
|
// Garde la compatibilité pour les utilisateurs non-locaux (ex: système)
|
|
203
245
|
if (!isLocalUser(user)) {
|
|
204
246
|
const userRoles = new Set(user.roles || []);
|
|
205
247
|
const requiredPermissions = Array.isArray(permissionNames) ? permissionNames : [permissionNames];
|
|
206
|
-
|
|
248
|
+
const hasPerm = requiredPermissions.some(p => userRoles.has(p));
|
|
249
|
+
// Pour les non-locaux, on ne gère pas les filtres complexes, on retourne juste true/false.
|
|
250
|
+
return hasPerm ? true : false;
|
|
207
251
|
}
|
|
208
252
|
|
|
209
253
|
try {
|
|
254
|
+
// 1. Obtenir l'ensemble final et à jour des permissions de l'utilisateur
|
|
255
|
+
const activePermissions = await getUserActivePermissions(user, env);
|
|
256
|
+
|
|
210
257
|
const requiredPermissions = Array.isArray(permissionNames) ? permissionNames : [permissionNames];
|
|
211
258
|
// Si aucune permission n'est requise, on autorise
|
|
212
259
|
if (requiredPermissions.length === 0) {
|
|
213
260
|
return true;
|
|
214
261
|
}
|
|
215
262
|
|
|
216
|
-
//
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
263
|
+
// 2. Chercher la première permission correspondante
|
|
264
|
+
for (const pName of requiredPermissions) {
|
|
265
|
+
if (activePermissions.has(pName)) {
|
|
266
|
+
const filter = activePermissions.get(pName);
|
|
267
|
+
// Si un filtre existe, on substitue les variables
|
|
268
|
+
if (filter && typeof filter === 'object' && Object.keys(filter).length > 0) {
|
|
269
|
+
// Le contexte de substitution est enrichi avec des données pertinentes
|
|
270
|
+
const context = {
|
|
271
|
+
user,
|
|
272
|
+
permissionName: pName,
|
|
273
|
+
env,
|
|
274
|
+
now: new Date(),
|
|
275
|
+
req: req ? { query: req.query, body: req.body, params: req.params, ip: req.ip } : null
|
|
276
|
+
};
|
|
277
|
+
return await substituteVariables(filter, context, user);
|
|
278
|
+
}
|
|
279
|
+
return true; // Pas de filtre ou filtre vide, on autorise sans condition supplémentaire
|
|
280
|
+
}
|
|
281
|
+
}
|
|
221
282
|
|
|
283
|
+
return false; // Aucune permission trouvée
|
|
222
284
|
} catch (e) {
|
|
223
285
|
logger.error("Erreur lors de la vérification des permissions :", e);
|
|
224
286
|
return false;
|