@cocreate/authorize 1.16.2 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -1,934 +1,893 @@
1
- (function (root, factory) {
2
- if (typeof define === "function" && define.amd) {
3
- define(["@cocreate/crud-client", "@cocreate/utils"], function (
4
- crud,
5
- { getValueFromObject, dotNotationToObject }
6
- ) {
7
- return factory(true, crud, {
8
- getValueFromObject,
9
- dotNotationToObject
10
- });
11
- });
12
- } else if (typeof module === "object" && module.exports) {
13
- const {
14
- getValueFromObject,
15
- dotNotationToObject
16
- } = require("@cocreate/utils");
17
- module.exports = class CoCreateAuthorize {
18
- constructor(crud) {
19
- return factory(false, crud, {
20
- getValueFromObject,
21
- dotNotationToObject
22
- });
23
- }
24
- };
25
- } else {
26
- root.returnExports = factory(
27
- true,
28
- root["@cocreate/crud-client"],
29
- root["@cocreate/utils"]
30
- );
31
- }
32
- })(
33
- typeof self !== "undefined" ? self : this,
34
- function (isBrowser, crud, { getValueFromObject, dotNotationToObject }) {
35
- const organizations = {};
36
-
37
- if (isBrowser) {
38
- crud.listen("object.update", function (data) {
39
- updateAuthorization(data);
40
- });
41
-
42
- crud.listen("object.delete", function (data) {
43
- deleteAuthorization(data);
44
- });
45
- } else {
46
- process.on("crud-event", async (data) => {
47
- updateAuthorization(data);
48
- });
49
- }
50
-
51
- /**
52
- * Updates the cached authorization for a specific key within an organization.
53
- *
54
- * This function checks if the `array` is `"keys"` and if an authorization object is provided.
55
- * If the conditions are met, it fetches the latest authorization details using the `readAuthorization`
56
- * function and updates the cache for the given `organization_id` and `key`.
57
- *
58
- * @param {object} data - The data object containing:
59
- * - `array`: The type of collection being updated (e.g., "keys").
60
- * - `object`: The object array containing the authorization details.
61
- * - `organization_id`: The ID of the organization the authorization belongs to.
62
- * @returns {Promise<void>} Resolves when the update is complete.
63
- */
64
- async function updateAuthorization(data) {
65
- const { array, object, organization_id } = data;
66
-
67
- // Ensure we're working with "keys" and an authorization object is provided
68
- if (array === "keys" && object) {
69
- let authorization = object[0];
70
-
71
- // Check if the authorization key exists in the cache
72
- if (
73
- authorization &&
74
- authorization.key &&
75
- organizations[organization_id] &&
76
- organizations[organization_id][authorization.key]
77
- ) {
78
- // Fetch the latest authorization details
79
- let newAuthorization = await readAuthorization(
80
- authorization.key,
81
- data
82
- );
83
-
84
- // Update the cache with the new authorization details
85
- organizations[organization_id][authorization.key] =
86
- newAuthorization;
87
- }
88
- }
89
- }
90
-
91
- /**
92
- * Deletes the cached authorization for a specific key within an organization.
93
- *
94
- * This function checks if the `array` is `"keys"` and if an authorization object is provided.
95
- * If the conditions are met, it removes the cached authorization for the given
96
- * `organization_id` and `key` from the `organizations` cache.
97
- *
98
- * @param {object} data - The data object containing:
99
- * - `array`: The type of collection being deleted (e.g., "keys").
100
- * - `object`: The object array containing the authorization details.
101
- * - `organization_id`: The ID of the organization the authorization belongs to.
102
- * @returns {Promise<void>} Resolves when the deletion is complete.
103
- */
104
- async function deleteAuthorization(data) {
105
- const { array, object, organization_id } = data;
106
-
107
- // Ensure we're working with "keys" and an authorization object is provided
108
- if (array === "keys" && object) {
109
- let authorization = object[0];
110
-
111
- // Check if the authorization key exists in the cache
112
- if (
113
- authorization &&
114
- authorization.key &&
115
- organizations[organization_id] &&
116
- organizations[organization_id][authorization.key]
117
- ) {
118
- // Remove the cached authorization for the given key
119
- delete organizations[organization_id][authorization.key];
120
- }
121
- }
122
- }
123
-
124
- /**
125
- * Retrieves the cached authorization for a specific key within an organization.
126
- *
127
- * If the authorization is not cached, this function fetches the authorization
128
- * details using the `readAuthorization` function and stores it in the cache.
129
- *
130
- * @param {string} key - The authorization key (e.g., user ID or API key).
131
- * @param {object} data - The data object containing:
132
- * - `organization_id`: The ID of the organization the authorization belongs to.
133
- * @returns {Promise<object>} A promise that resolves to the authorization details.
134
- */
135
- async function getAuthorization(key, data) {
136
- let organization_id = data.organization_id;
137
-
138
- // Initialize the cache for the organization if it doesn't exist
139
- if (!organizations[organization_id])
140
- organizations[organization_id] = {};
141
-
142
- // If the authorization for the key is not cached, fetch and cache it
143
- if (!organizations[organization_id][key])
144
- organizations[organization_id][key] = readAuthorization(
145
- key,
146
- data
147
- );
148
-
149
- // Await the authorization (handles the promise stored during the initial fetch)
150
- organizations[organization_id][key] = await organizations[
151
- organization_id
152
- ][key];
153
-
154
- return organizations[organization_id][key];
155
- }
156
-
157
- /**
158
- * Reads authorization details for a given key (user ID or API key) and organization.
159
- *
160
- * This function constructs a request to retrieve authorization details from the database.
161
- * If the `key` is provided, it queries for the specific authorization key.
162
- * If no `key` is provided, it queries for the default authorization settings.
163
- *
164
- * Additionally, it resolves roles associated with the authorization and merges role permissions
165
- * into the final authorization object.
166
- *
167
- * @param {string} key - The authorization key (e.g., user ID or API key).
168
- * @param {object} data - The data object containing request details, such as the `organization_id` and `host`.
169
- * @returns {Promise<object>} A promise that resolves to the authorization object or an error message.
170
- */
171
- async function readAuthorization(key, data) {
172
- try {
173
- // Extract and validate the organization_id
174
- let organization_id = data.organization_id;
175
- if (!organization_id)
176
- return { error: "An organization_id is required" };
177
-
178
- // Construct the initial request for authorization data
179
- let request = {
180
- method: "object.read", // CRUD method to read the object
181
- host: data.host, // Host associated with the request
182
- database: organization_id, // Database scoped to the organization
183
- array: "keys", // Collection or array to query
184
- organization_id, // Organization context for the request
185
- $filter: {
186
- query: {} // Query filters to apply
187
- }
188
- };
189
-
190
- // Add query filters based on the key or default authorization
191
- if (key) request.$filter.query.key = key;
192
- else request.$filter.query.default = true;
193
-
194
- // Send the request to the CRUD layer
195
- let authorization = await crud.send(request);
196
-
197
- // If authorization data exists, process the roles and permissions
198
- if (
199
- authorization &&
200
- authorization.object &&
201
- authorization.object[0]
202
- ) {
203
- authorization = authorization.object[0];
204
-
205
- // Ensure the 'arrays' field exists to prevent undefined references
206
- if (!authorization.arrays) {
207
- authorization.arrays = {};
208
- }
209
-
210
- // Process roles associated with the authorization
211
- if (authorization && authorization.roles) {
212
- const role_ids = [];
213
- authorization.roles.forEach((_id) => {
214
- if (_id) role_ids.push({ _id });
215
- });
216
-
217
- // Prepare a new request to fetch role details
218
- delete request.$filter;
219
- delete request.isFilter;
220
-
221
- request.object = role_ids;
222
-
223
- // Retrieve and merge role permissions into the authorization object
224
- let roles = await crud.send(request);
225
- roles = roles.object;
226
-
227
- for (let role of roles) {
228
- authorization = dotNotationToObject(
229
- authorization,
230
- role
231
- );
232
- }
233
- }
234
- return authorization; // Return the final authorization object
235
- } else {
236
- // Return an empty object if no authorization is found
237
- return {};
238
- }
239
- } catch (error) {
240
- // Handle and log any errors during the process
241
- console.log("authorization Error", error);
242
- return { error };
243
- }
244
- }
245
-
246
- /**
247
- * Validates the authorization of a user or an API key to perform an action.
248
- *
249
- * The function first checks authorization using the provided `user_id`.
250
- * If that fails or returns an error, it checks the authorization using the `apikey`
251
- * present in the `data` object.
252
- *
253
- * @param {object} data - The data object containing request details, such as the `apikey` and `organization_id`.
254
- * @param {string} user_id - The user ID for authorization validation.
255
- * @returns {Promise<object|boolean>} A promise that resolves to the authorization result:
256
- * - `false` if unauthorized.
257
- * - An object with authorization details if authorized.
258
- */
259
- async function check(data, user_id) {
260
- let authorization = false;
261
-
262
- // Attempt to check authorization with the user ID if provided
263
- if (user_id) {
264
- authorization = await checkAuthorization({
265
- key: user_id,
266
- data
267
- });
268
- }
269
-
270
- // Fallback to checking authorization with the API key if user ID check fails
271
- if (!authorization || authorization.error) {
272
- authorization = await checkAuthorization({
273
- key: data.apikey,
274
- data
275
- });
276
- }
277
-
278
- return authorization;
279
- }
280
-
281
- /**
282
- * Checks whether a given key (user ID or API key) is authorized to perform the specified action.
283
- *
284
- * The function validates the presence of required fields in the `data` object (`organization_id`, `method`),
285
- * fetches authorization details, and performs checks such as:
286
- * - Organization ID matching.
287
- * - Host-level restrictions.
288
- * - Admin privileges.
289
- * - Defined actions and their permissions.
290
- *
291
- * @param {object} params - The parameters for the function.
292
- * @param {string} params.key - The key used for authorization (user ID or API key).
293
- * @param {object} params.data - The data object containing details like `organization_id`, `host`, and `method`.
294
- * @returns {Promise<object|boolean>} A promise that resolves to:
295
- * - An object with an error message if validation fails.
296
- * - `false` if authorization checks fail.
297
- * - `true` or an object with sanitized `data` if authorized.
298
- */
299
- async function checkAuthorization({ key, data }) {
300
- // Validate required fields
301
- if (!data.organization_id)
302
- return { error: "organization_id is required" };
303
- if (!data.method) return { error: "method is required" };
304
-
305
- // Fetch authorization details using the provided key
306
- let authorized = await getAuthorization(key, data);
307
-
308
- // If no authorization details or an error is returned, propagate the error or deny access
309
- if (!authorized || authorized.error) return authorized;
310
-
311
- // Ensure the organization ID matches
312
- if (authorized.organization_id !== data.organization_id)
313
- return false;
314
-
315
- // Validate host restrictions if specified
316
- if (authorized.host && authorized.host.length) {
317
- if (
318
- !authorized.host ||
319
- (!authorized.host.includes(data.host) &&
320
- !authorized.host.includes("*"))
321
- )
322
- return false;
323
- }
324
-
325
- // Allow access if the user has admin privileges
326
- if (authorized.admin === "true" || authorized.admin === true)
327
- return true;
328
-
329
- // Ensure actions are defined for the authorization
330
- if (!authorized.actions)
331
- return {
332
- error: "Authorization does not have any actions defined"
333
- };
334
-
335
- // Check if the requested method is allowed under the defined actions
336
- let status = await checkMethod(
337
- data,
338
- authorized.actions,
339
- data.method
340
- );
341
-
342
- // If the method check fails, deny access
343
- if (!status) {
344
- return false;
345
- }
346
-
347
- // Return the sanitized and authorized data if all checks pass
348
- return { authorized: data };
349
- }
350
-
351
- /**
352
- * Validates whether a method is permitted based on the authorization rules.
353
- *
354
- * This function checks for an exact match of the `method` in the `authorized` actions.
355
- * If no exact match is found, it checks progressively for partial matches using dot notation (e.g., "read.user").
356
- *
357
- * @param {object} data - The data object containing request details.
358
- * @param {object} authorized - The authorization rules object mapping methods to permissions.
359
- * @param {string} method - The method to validate (e.g., "read.user").
360
- * @returns {Promise<boolean>} A promise that resolves to `true` if the method is authorized, `false` otherwise.
361
- */
362
- async function checkMethod(data, authorized, method) {
363
- // Check for an exact match of the method in the authorization rules
364
- if (authorized[method]) {
365
- return await checkMethodMatch(data, authorized[method], method);
366
- }
367
- // Check for partial matches using dot notation (e.g., "read.user")
368
- else if (method.includes(".")) {
369
- let match = "";
370
- let splitMethod = method.split(".");
371
-
372
- for (let i = 0; i < splitMethod.length; i++) {
373
- if (!match) match = splitMethod[i];
374
- else match += "." + splitMethod[i];
375
-
376
- // If a partial match is found, validate it
377
- if (authorized[match]) {
378
- return await checkMethodMatch(
379
- data,
380
- authorized[match],
381
- match
382
- );
383
- }
384
- // If the last segment has no match, deny access
385
- else if (i === splitMethod.length - 1) return false;
386
- }
387
- }
388
- // Deny access if no match is found
389
- else return false;
390
- }
391
-
392
- /**
393
- * Checks whether a given method matches the authorized permissions and
394
- * optionally sanitizes the provided data based on inclusion and exclusion rules.
395
- *
396
- * This function supports various types of authorization configurations, including:
397
- * - Booleans: Directly allow (`true`) or deny (`false`).
398
- * - Strings: Match the authorization string with the method or default to allow.
399
- * - Numbers: Treat any non-zero number as `true` (authorized).
400
- * - Arrays: Recursively check multiple authorization rules.
401
- * - Objects: Evaluate nested authorization rules and apply inclusion/exclusion logic.
402
- *
403
- * The function also aggregates all inclusion and exclusion rules before sanitizing
404
- * the data, ensuring consistent prioritization logic is applied holistically.
405
- *
406
- * @param {object} data - The data object containing the method and other fields to process.
407
- * @param {boolean | string | number | object | Array} authorized - The authorization rules.
408
- * @param {string} match - The method string to match against the authorization rules.
409
- * @returns {Promise<boolean>} A promise that resolves to `true` if the method is authorized,
410
- * `false` otherwise.
411
- *
412
- * @example
413
- * // Example usage:
414
- * const data = { method: "read", read: { field1: "value1", field2: "value2" } };
415
- * const authorized = { read: [{ field1: true }, { field2: false }] };
416
- * const match = "read";
417
- *
418
- * checkMethodMatch(data, authorized, match).then((isAuthorized) => {
419
- * console.log(isAuthorized); // true
420
- * console.log(data); // { method: "read", read: { field1: "value1" } }
421
- * });
422
- */
423
- async function checkMethodMatch(data, authorized, match) {
424
- if (typeof authorized === "boolean") {
425
- return authorized;
426
- } else if (typeof authorized === "string") {
427
- if (authorized === "false") return false;
428
- else if (authorized === "true") return true;
429
- else if (authorized === match) return true;
430
- else return true;
431
- } else if (typeof authorized === "number") {
432
- return !!authorized;
433
- } else {
434
- let status = false;
435
- let newmatch = data.method.replace(match + ".", "");
436
-
437
- // Aggregate raw keys
438
- let rawKeys = [];
439
-
440
- if (Array.isArray(authorized)) {
441
- for (let i = 0; i < authorized.length; i++) {
442
- status = await checkMethodMatch(
443
- data,
444
- authorized[i],
445
- newmatch
446
- );
447
- }
448
- } else if (typeof authorized === "object") {
449
- for (const key of Object.keys(authorized)) {
450
- if (key.includes("$")) {
451
- if (
452
- [
453
- "$storage",
454
- "$database",
455
- "$array",
456
- "$index"
457
- ].includes(key)
458
- ) {
459
- let opStatus = await checkMethodOperators(
460
- data,
461
- key,
462
- authorized[key]
463
- );
464
- if (opStatus === true || opStatus === false)
465
- status = opStatus;
466
- } else if (key === "$keys") {
467
- let type = data.method.split(".")[0];
468
- if (data[type]) {
469
- rawKeys.push(authorized[key]); // Collect raw authorization keys
470
- }
471
- } else {
472
- let isFilter = applyFilter(
473
- data,
474
- authorized[key],
475
- key
476
- );
477
- console.log("isFilter", isFilter);
478
- }
479
- }
480
- }
481
- }
482
-
483
- // Parse all raw keys together to get inclusion/exclusion
484
- let { inclusion, exclusion } = parsePermissions(rawKeys);
485
-
486
- // Apply sanitization if there are inclusion or exclusion rules
487
- if (inclusion || exclusion) {
488
- let type = data.method.split(".")[0]; // Extract type from method
489
- if (data[type]) {
490
- data[type] = sanitizeData(
491
- data[type],
492
- inclusion,
493
- exclusion
494
- );
495
- status = true;
496
- }
497
- }
498
-
499
- if (newmatch) {
500
- if (!status && authorized[newmatch]) {
501
- status = await checkMethodMatch(
502
- data,
503
- authorized[newmatch],
504
- newmatch
505
- );
506
- }
507
- if (!status && authorized["*"]) {
508
- status = await checkMethodMatch(
509
- data,
510
- authorized["*"],
511
- newmatch
512
- );
513
- }
514
- }
515
-
516
- return status;
517
- }
518
- }
519
-
520
- /**
521
- * Checks method operators to validate authorization based on dynamic data types and keys.
522
- *
523
- * @param {Object} data - The input data object containing various properties.
524
- * @param {string} key - A string key to determine the type of operation,
525
- * starting with `$` for dynamic type-based checks.
526
- * @param {string} authorization - The authorization identifier, which could be a static value
527
- * or a dynamic reference like `$user_id`.
528
- *
529
- * @returns {boolean|undefined} - Returns `true` if authorized, `false` if unauthorized,
530
- * or `undefined` if no validation applies.
531
- *
532
- * This function:
533
- * - Dynamically resolves `authorization` when `$user_id` is used and `data.socket` contains user information.
534
- * - Handles dynamic key-based operations using a `$` prefix (e.g., `$type`).
535
- * - Validates authorization for strings, arrays, and objects in `data[type]`:
536
- * - For strings: Directly validates using `checkAuthorizationData`.
537
- * - For arrays: Validates each item. Returns `false` if any item fails validation.
538
- * If all items are valid, returns `true`. Returns `undefined` if the array is empty.
539
- * - For objects: Validates based on the `name` property of the object.
540
- *
541
- * Example Usage:
542
- * ```
543
- * const data = {
544
- * socket: { user_id: "123" },
545
- * roles: ["admin", "editor"]
546
- * };
547
- * const result = checkMethodOperators(data, "$roles", "$user_id");
548
- * ```
549
- */
550
- async function checkMethodOperators(data, key, authorization) {
551
- try {
552
- // Adjust authorization dynamically if `$user_id` is provided and user info exists in the socket
553
- if (authorization === "$user_id" && data.socket) {
554
- authorization = data.socket.user_id || data.user_id;
555
- }
556
-
557
- // Check if the key starts with `$`, indicating a dynamic type-based operation
558
- if (key.startsWith("$")) {
559
- let type = key.substring(1); // Extract the type by removing `$`
560
-
561
- // If the specified type does not exist in data, return undefined
562
- if (!data[type]) {
563
- return undefined;
564
- }
565
-
566
- // Handle cases where the type is a string
567
- if (typeof data[type] === "string") {
568
- return checkAuthorizationData(
569
- data,
570
- authorization,
571
- data[type]
572
- );
573
- }
574
- // Handle cases where the type is an array
575
- else if (Array.isArray(data[type])) {
576
- if (!data[type].length) return undefined; // Return undefined for empty arrays
577
-
578
- // ToDo: Current strategy ensures all items must match; adjust as needed
579
- let allAuthorized = true;
580
- for (let i = 0; i < data[type].length; i++) {
581
- const itemData =
582
- typeof data[type][i] === "object"
583
- ? data[type][i].name // Extract name if the item is an object
584
- : data[type][i];
585
- const authResult = checkAuthorizationData(
586
- data,
587
- authorization,
588
- itemData
589
- );
590
- if (authResult === false) {
591
- return false; // Return false if any item fails validation
592
- } else if (authResult === undefined) {
593
- allAuthorized = undefined; // Mark as undefined if any validation is inconclusive
594
- }
595
- }
596
- return allAuthorized;
597
- }
598
- // Handle cases where the type is an object
599
- else if (typeof data[type] === "object") {
600
- return checkAuthorizationData(
601
- data,
602
- authorization,
603
- data[type].name
604
- );
605
- }
606
- }
607
-
608
- // Return undefined if no conditions match
609
- return undefined;
610
- } catch (e) {
611
- // Log the error and return undefined in case of an exception
612
- console.log(e);
613
- return undefined;
614
- }
615
- }
616
-
617
- /**
618
- * Checks if the provided key is authorized based on the authorization rules.
619
- *
620
- * @param {Object} data - The data object to be used in filtering.
621
- * @param {string|Array|Object} authorization - The authorization rules, which can be a string, array, or object.
622
- * @param {string} key - The key to be checked against the authorization rules.
623
- * @returns {boolean|undefined} - Returns true if authorized, false if explicitly unauthorized, and undefined if no rule matches.
624
- */
625
- function checkAuthorizationData(data, authorization, key) {
626
- // If the authorization is a string, check if it matches the key.
627
- if (typeof authorization === "string") {
628
- if (key === authorization)
629
- return true; // Key matches the authorization string.
630
- else return undefined; // No match, return undefined.
631
- }
632
- // If the authorization is an array, check if the key is included in the array.
633
- else if (Array.isArray(authorization)) {
634
- if (authorization.includes(key))
635
- return true; // Key is included in the array.
636
- else return undefined; // Key not found, return undefined.
637
- }
638
- // If the authorization is an object, handle various object-based rules.
639
- else if (typeof authorization === "object") {
640
- // If the authorization[key] is an object, apply filters recursively.
641
- if (typeof authorization[key] === "object") {
642
- for (const queryKey of Object.keys(authorization[key])) {
643
- return applyFilter(data, authorization[key], queryKey); // Apply the filter logic.
644
- }
645
- }
646
- // If authorization[key] is explicitly false or "false", return false.
647
- else if (
648
- authorization[key] === false ||
649
- authorization[key] === "false"
650
- )
651
- return false;
652
- // If authorization[key] exists and is truthy, return true.
653
- else if (authorization[key]) return true;
654
- // If none of the above conditions match, return undefined.
655
- else return undefined;
656
- }
657
- }
658
-
659
- function applyFilter(data, authorization, authorizationKey) {
660
- let keyParts = authorizationKey.split(".");
661
- let operator = keyParts.pop();
662
- let key = keyParts.join(".");
663
- if (
664
- [
665
- "$eq",
666
- "$ne",
667
- "$lt",
668
- "$lte",
669
- "$gt",
670
- "$gte",
671
- "$in",
672
- "$nin",
673
- "$or",
674
- "$and",
675
- "$not",
676
- "$nor",
677
- "$exists",
678
- "$type",
679
- "$mod",
680
- "$regex",
681
- "$text",
682
- "$where",
683
- "$all",
684
- "$elemMatch",
685
- "$size"
686
- ].includes(operator)
687
- ) {
688
- if (!data.$filter) data.$filter = { query: {} };
689
- else if (!data.$filter.query) data.$filter.query = {};
690
-
691
- if (
692
- authorization[authorizationKey] === "$user_id" &&
693
- data.socket
694
- ) {
695
- authorization[authorizationKey] =
696
- data.socket.user_id || data.user_id;
697
- }
698
-
699
- if (typeof authorization === "string")
700
- data.$filter.query[key] = {
701
- [operator]: authorization
702
- };
703
- else
704
- data.$filter.query[key] = {
705
- [operator]: authorization[authorizationKey]
706
- };
707
-
708
- return true;
709
- }
710
- }
711
-
712
- /**
713
- * Parses permissions into inclusion and exclusion arrays.
714
- *
715
- * @param {string | object | Array<string | object>} authorization - The authorization to parse.
716
- * @returns {object} An object containing 'inclusion' and 'exclusion' arrays.
717
- */
718
- function parsePermissions(authorization) {
719
- // Initialize local arrays for inclusion and exclusion
720
- const inclusion = [];
721
- const exclusion = [];
722
-
723
- // Helper function to process a single permission entry
724
- function processPermission(entry) {
725
- if (typeof entry === "string") {
726
- // Treat string as inclusion
727
- inclusion.push(entry);
728
- } else if (typeof entry === "object" && entry !== null) {
729
- for (const [key, value] of Object.entries(entry)) {
730
- if (value === true) {
731
- inclusion.push(key);
732
- } else if (value === false) {
733
- exclusion.push(key);
734
- }
735
- // Ignore entries where value is neither true nor false
736
- }
737
- }
738
- // Ignore entries that are neither string nor object
739
- }
740
-
741
- // Normalize the input into an array of permission entries
742
- let permissionEntries = [];
743
-
744
- if (Array.isArray(authorization)) {
745
- permissionEntries = authorization;
746
- } else if (
747
- typeof authorization === "string" ||
748
- (typeof authorization === "object" && authorization !== null)
749
- ) {
750
- permissionEntries = [authorization];
751
- }
752
- // If authorization is undefined or null, treat as empty
753
-
754
- // Process each permission entry
755
- permissionEntries.forEach(processPermission);
756
-
757
- // Apply prioritization logic
758
- if (inclusion.length > 0) {
759
- // If any inclusion exists, disregard exclusions
760
- return { inclusion, exclusion: null };
761
- } else if (exclusion.length > 0) {
762
- // If only exclusions exist, disregard inclusions
763
- return { inclusion: null, exclusion };
764
- } else {
765
- // If neither exists, set both to null
766
- return { inclusion: null, exclusion: null };
767
- }
768
- }
769
-
770
- /**
771
- * Sanitizes data based on parsed permissions.
772
- *
773
- * @param {object | Array} data - The data object or array to sanitize.
774
- * @param {Array} inclusion - The parsed inclusion array.
775
- * @param {Array} exclusion - The parsed exclusion array.
776
- * @returns {object | Array} The sanitized data object or array.
777
- */
778
- function sanitizeData(data, inclusion, exclusion) {
779
- // If both inclusion and exclusion are null, return the data as is
780
- if (inclusion === null && exclusion === null) {
781
- return data;
782
- }
783
-
784
- if (Array.isArray(data)) {
785
- return data.map((item) =>
786
- sanitizeData(item, inclusion, exclusion)
787
- );
788
- } else if (typeof data === "object" && data !== null) {
789
- let sanitized = {};
790
-
791
- if (inclusion) {
792
- // Include only specified fields (supports nested fields)
793
- for (let i = 0; i < inclusion.length; i++) {
794
- let inclusionData = generateDotNotation(
795
- data,
796
- inclusion[i]
797
- );
798
- sanitized = { ...sanitized, ...inclusionData };
799
- }
800
- return dotNotationToObject(sanitized);
801
- } else if (exclusion) {
802
- for (let i = 0; i < exclusion.length; i++) {
803
- let exclusionData = generateDotNotation(
804
- data,
805
- exclusion[i],
806
- true
807
- );
808
- sanitized = { ...sanitized, ...exclusionData };
809
- }
810
- return dotNotationToObject(sanitized, data);
811
- }
812
- }
813
-
814
- return data;
815
- }
816
-
817
- /**
818
- * Generates an object with dot notation keys from the given data and path.
819
- *
820
- * @param {Object} data - The source data object to extract values from.
821
- * @param {string} path - A dot-separated string representing the path to access data.
822
- * Supports array notation with `[]` (e.g., `users[].name`).
823
- * @param {boolean} isEclusion - If true, the result will exclude the specified path by setting its value to `undefined`.
824
- * If false, the result will include the value at the specified path.
825
- *
826
- * @returns {Object} - An object with dot notation keys representing the path in the data.
827
- * Example: `{ "user.name": "John" }`
828
- *
829
- * @throws {Error} - Throws an error if the `path` parameter is not a string.
830
- *
831
- * The function supports:
832
- * - Nested object traversal based on the `path`.
833
- * - Array indexing when the path includes `[]` to handle elements dynamically.
834
- *
835
- * Key Details:
836
- * - The `processKeys` function handles recursive traversal of the data structure.
837
- * - If `isEclusion` is true, it marks the path with `undefined` to represent exclusion.
838
- * - If the path points to an array (e.g., `users[].name`), it iterates through array elements,
839
- * appending index-based keys (e.g., `users[0].name`, `users[1].name`).
840
- *
841
- * Example Usage:
842
- * ```
843
- * const data = {
844
- * users: [
845
- * { name: "John", age: 30 },
846
- * { name: "Jane", age: 25 }
847
- * ]
848
- * };
849
- *
850
- * const result = generateDotNotation(data, "users[].name", false);
851
- * // Output: { "users[0].name": "John", "users[1].name": "Jane" }
852
- * ```
853
- */
854
- function generateDotNotation(data, path, isEclusion) {
855
- if (typeof path !== "string")
856
- throw new Error("Path must be a string");
857
-
858
- let result = {};
859
-
860
- function processKeys(dataItem, keys, prefix = "", result) {
861
- if (dataItem === undefined) return;
862
-
863
- if (!keys.length) {
864
- if (isEclusion) {
865
- result[prefix] = undefined;
866
- } else {
867
- result[prefix] = dataItem;
868
- }
869
- return;
870
- }
871
-
872
- let key = keys.shift();
873
- let isArray = key.endsWith("[]");
874
- if (isArray) key = key.slice(0, -2); // Remove [] from key
875
-
876
- // Construct prefix
877
- prefix = prefix ? `${prefix}.${key}` : key;
878
-
879
- const currentData = getValueFromObject(dataItem, key); // Using your existing function
880
-
881
- if (isArray && Array.isArray(currentData)) {
882
- currentData.forEach((item, index) => {
883
- const newPrefix = `${prefix}[${index}]`;
884
- processKeys(item, [...keys], newPrefix, result);
885
- });
886
- } else if (!isArray && currentData !== undefined) {
887
- processKeys(currentData, keys, prefix, result);
888
- }
889
- }
890
-
891
- if (data && path.trim()) {
892
- const keys = path.split(".");
893
- processKeys(data, keys, "", result);
894
- }
895
-
896
- return result;
897
- }
898
-
899
- async function checkFilter(authorized, data, apikey, unauthorize) {
900
- if (data.object.$filter && data.object.$filter.query) {
901
- let key;
902
- if (data.object.$filter.type == "object") key = "_id";
903
- else if (data.object.$filter.type == "array") key = "name";
904
- if (key) {
905
- for (let value of authorized[apikey]) {
906
- if (value[key]) value = value[key];
907
- if (!data.object.$filter.query.$or)
908
- data.object.$filter.query.$or = [];
909
- if (unauthorize)
910
- data.object.$filter.query.$or.push({
911
- [key]: { $ne: value }
912
- });
913
- else
914
- data.object.$filter.query.$or.push({
915
- [key]: value
916
- });
917
- }
918
- if (!unauthorize) return true;
919
- }
920
- }
921
- }
922
-
923
- function deleteKey(data, path) {
924
- if (!data || !path) return;
925
- if (path.includes("._id")) path = path.replace("._id", "");
926
- data = dotNotationToObject({ [path]: undefined }, data);
927
- return data;
928
- }
929
-
930
- return {
931
- check
932
- };
933
- }
934
- );
1
+ /********************************************************************************
2
+ * Copyright (C) 2023 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ ********************************************************************************/
17
+
18
+ import { getValueFromObject, dotNotationToObject } from "@cocreate/utils";
19
+ import crud from "@cocreate/crud-server";
20
+
21
+ // ==========================================
22
+ // Module-Level Sandbox State (ESM Singleton)
23
+ // ==========================================
24
+ const organizations = {};
25
+
26
+ // ==========================================
27
+ // Event Listeners for Cache Invalidation
28
+ // ==========================================
29
+ if (typeof window !== "undefined") {
30
+ if (crud && typeof crud.listen === "function") {
31
+ crud.listen("object.update", function (data) {
32
+ updateAuthorization(data);
33
+ });
34
+
35
+ crud.listen("object.delete", function (data) {
36
+ deleteAuthorization(data);
37
+ });
38
+ }
39
+ } else {
40
+ if (typeof process !== "undefined") {
41
+ process.on("crud-event", async (data) => {
42
+ // Safely route deletes vs updates to maintain precise cache sync
43
+ if (data.method === "object.delete") {
44
+ deleteAuthorization(data);
45
+ } else {
46
+ updateAuthorization(data);
47
+ }
48
+ });
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Updates the cached authorization for a specific key within an organization.
54
+ *
55
+ * This function checks if the `array` is `"keys"` and if an authorization object is provided.
56
+ * If the conditions are met, it fetches the latest authorization details using the `readAuthorization`
57
+ * function and updates the cache for the given `organization_id` and `key`.
58
+ *
59
+ * @param {object} data - The data object containing:
60
+ * - `array`: The type of collection being updated (e.g., "keys").
61
+ * - `object`: The object array containing the authorization details.
62
+ * - `organization_id`: The ID of the organization the authorization belongs to.
63
+ * @returns {Promise<void>} Resolves when the update is complete.
64
+ */
65
+ export async function updateAuthorization(data) {
66
+ const { array, object, organization_id } = data;
67
+
68
+ // Ensure we're working with "keys" and an authorization object is provided
69
+ if (array === "keys" && object) {
70
+ let authorization = object[0];
71
+
72
+ // Check if the authorization key exists in the cache
73
+ if (
74
+ authorization &&
75
+ authorization.key &&
76
+ organizations[organization_id] &&
77
+ organizations[organization_id][authorization.key]
78
+ ) {
79
+ // Fetch the latest authorization details
80
+ let newAuthorization = await readAuthorization(
81
+ authorization.key,
82
+ data
83
+ );
84
+
85
+ // Update the cache with the new authorization details
86
+ organizations[organization_id][authorization.key] =
87
+ newAuthorization;
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Deletes the cached authorization for a specific key within an organization.
94
+ *
95
+ * This function checks if the `array` is `"keys"` and if an authorization object is provided.
96
+ * If the conditions are met, it removes the cached authorization for the given
97
+ * `organization_id` and `key` from the `organizations` cache.
98
+ *
99
+ * @param {object} data - The data object containing:
100
+ * - `array`: The type of collection being deleted (e.g., "keys").
101
+ * - `object`: The object array containing the authorization details.
102
+ * - `organization_id`: The ID of the organization the authorization belongs to.
103
+ * @returns {Promise<void>} Resolves when the deletion is complete.
104
+ */
105
+ export async function deleteAuthorization(data) {
106
+ const { array, object, organization_id } = data;
107
+
108
+ // Ensure we're working with "keys" and an authorization object is provided
109
+ if (array === "keys" && object) {
110
+ let authorization = object[0];
111
+
112
+ // Check if the authorization key exists in the cache
113
+ if (
114
+ authorization &&
115
+ authorization.key &&
116
+ organizations[organization_id] &&
117
+ organizations[organization_id][authorization.key]
118
+ ) {
119
+ // Remove the cached authorization for the given key
120
+ delete organizations[organization_id][authorization.key];
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Retrieves the cached authorization for a specific key within an organization.
127
+ *
128
+ * If the authorization is not cached, this function fetches the authorization
129
+ * details using the `readAuthorization` function and stores it in the cache.
130
+ *
131
+ * @param {string} key - The authorization key (e.g., user ID or API key).
132
+ * @param {object} data - The data object containing:
133
+ * - `organization_id`: The ID of the organization the authorization belongs to.
134
+ * @returns {Promise<object>} A promise that resolves to the authorization details.
135
+ */
136
+ export async function getAuthorization(key, data) {
137
+ let organization_id = data.organization_id;
138
+
139
+ // Initialize the cache for the organization if it doesn't exist
140
+ if (!organizations[organization_id])
141
+ organizations[organization_id] = {};
142
+
143
+ // If the authorization for the key is not cached, fetch and cache it
144
+ if (!organizations[organization_id][key])
145
+ organizations[organization_id][key] = readAuthorization(
146
+ key,
147
+ data
148
+ );
149
+
150
+ // Await the authorization (handles the promise stored during the initial fetch)
151
+ organizations[organization_id][key] = await organizations[
152
+ organization_id
153
+ ][key];
154
+
155
+ return organizations[organization_id][key];
156
+ }
157
+
158
+ /**
159
+ * Reads authorization details for a given key (user ID or API key) and organization.
160
+ *
161
+ * This function constructs a request to retrieve authorization details from the database.
162
+ * If the `key` is provided, it queries for the specific authorization key.
163
+ * If no `key` is provided, it queries for the default authorization settings.
164
+ *
165
+ * Additionally, it resolves roles associated with the authorization and merges role permissions
166
+ * into the final authorization object.
167
+ *
168
+ * @param {string} key - The authorization key (e.g., user ID or API key).
169
+ * @param {object} data - The data object containing request details, such as the `organization_id` and `host`.
170
+ * @returns {Promise<object>} A promise that resolves to the authorization object or an error message.
171
+ */
172
+ export async function readAuthorization(key, data) {
173
+ try {
174
+ // Extract and validate the organization_id
175
+ let organization_id = data.organization_id;
176
+ if (!organization_id)
177
+ return { error: "An organization_id is required" };
178
+
179
+ // Construct the initial request for authorization data
180
+ let request = {
181
+ method: "object.read", // CRUD method to read the object
182
+ host: data.host, // Host associated with the request
183
+ database: organization_id, // Database scoped to the organization
184
+ array: "keys", // Collection or array to query
185
+ organization_id, // Organization context for the request
186
+ $filter: {
187
+ query: {}, // Query filters to apply
188
+ limit: 2 // Limit to 2 results to fetch both default and specific key if needed
189
+ }
190
+ };
191
+
192
+ // Always fetch the default authorization and, if provided, the specific key
193
+ request.$filter.query.$or = [{ default: true }];
194
+ if (key) request.$filter.query.$or.push({ key: key });
195
+
196
+ // Send the request to the CRUD layer
197
+ let authRes = await crud.send(request);
198
+
199
+ // If authorization data exists, merge default + key (key overrides default)
200
+ if (authRes && authRes.object && authRes.object.length) {
201
+ const objs = authRes.object;
202
+ const defaultDoc = objs.find((o) => o && o.default) || {};
203
+ const keyDoc = key ? objs.find((o) => o && o.key === key) : undefined;
204
+
205
+ // Expand dot-notation fields and merge: defaults then key-specific (key overrides)
206
+ const expandedDefault = dotNotationToObject({}, defaultDoc || {});
207
+ const expandedKey = keyDoc ? dotNotationToObject({}, keyDoc) : {};
208
+ let authorization = dotNotationToObject(expandedDefault, expandedKey);
209
+
210
+ // Merge roles uniquely (default roles + key roles)
211
+ const defaultRoles = defaultDoc.roles || [];
212
+ const keyRoles = (keyDoc && keyDoc.roles) || [];
213
+ authorization.roles = Array.from(new Set([...defaultRoles, ...keyRoles]));
214
+
215
+ // Ensure the 'arrays' field exists to prevent undefined references
216
+ if (!authorization.arrays) authorization.arrays = {};
217
+
218
+ // Process roles associated with the authorization (if any)
219
+ if (authorization && authorization.roles && authorization.roles.length) {
220
+ const role_ids = [];
221
+ authorization.roles.forEach((_id) => {
222
+ if (_id) role_ids.push({ _id });
223
+ });
224
+
225
+ // Prepare a new request to fetch role details
226
+ delete request.$filter;
227
+ delete request.isFilter;
228
+
229
+ request.object = role_ids;
230
+
231
+ // Retrieve and merge role permissions into the authorization object
232
+ let roles = await crud.send(request);
233
+ roles = roles.object;
234
+
235
+ for (let role of roles) {
236
+ authorization = dotNotationToObject(authorization, role);
237
+ }
238
+ }
239
+
240
+ return authorization; // Return the final merged authorization object
241
+ } else {
242
+ // Return an empty object if no authorization is found
243
+ return {};
244
+ }
245
+ } catch (error) {
246
+ // Handle and log any errors during the process
247
+ console.log("authorization Error", error);
248
+ return { error };
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Validates the authorization of a user or an API key to perform an action.
254
+ *
255
+ * The function first checks authorization using the provided `user_id`.
256
+ * If that fails or returns an error, it checks the authorization using the `apikey`
257
+ * present in the `data` object.
258
+ *
259
+ * @param {object} data - The data object containing request details, such as the `apikey` and `organization_id`.
260
+ * @param {string} user_id - The user ID for authorization validation.
261
+ * @returns {Promise<object|boolean>} A promise that resolves to the authorization result:
262
+ * - `false` if unauthorized.
263
+ * - An object with authorization details if authorized.
264
+ */
265
+ export async function check(data, user_id) {
266
+ let authorization = false;
267
+
268
+ // Attempt to check authorization with the user ID if provided
269
+ if (user_id) {
270
+ authorization = await checkAuthorization({
271
+ key: user_id,
272
+ data
273
+ });
274
+ }
275
+
276
+ // Fallback to checking authorization with the API key if user ID check fails
277
+ if (!authorization || authorization.error) {
278
+ authorization = await checkAuthorization({
279
+ key: data.apikey,
280
+ data
281
+ });
282
+ }
283
+
284
+ return authorization;
285
+ }
286
+
287
+ /**
288
+ * Checks whether a given key (user ID or API key) is authorized to perform the specified action.
289
+ *
290
+ * The function validates the presence of required fields in the `data` object (`organization_id`, `method`),
291
+ * fetches authorization details, and performs checks such as:
292
+ * - Organization ID matching.
293
+ * - Host-level restrictions.
294
+ * - Admin privileges.
295
+ * - Defined actions and their permissions.
296
+ *
297
+ * @param {object} params - The parameters for the function.
298
+ * @param {string} params.key - The key used for authorization (user ID or API key).
299
+ * @param {object} params.data - The data object containing details like `organization_id`, `host`, and `method`.
300
+ * @returns {Promise<object|boolean>} A promise that resolves to:
301
+ * - An object with an error message if validation fails.
302
+ * - `false` if authorization checks fail.
303
+ * - `true` or an object with sanitized `data` if authorized.
304
+ */
305
+ export async function checkAuthorization({ key, data }) {
306
+ // Validate required fields
307
+ if (!data.organization_id)
308
+ return { error: "organization_id is required" };
309
+ if (!data.method) return { error: "method is required" };
310
+
311
+ // Fetch authorization details using the provided key
312
+ let authorized = await getAuthorization(key, data);
313
+
314
+ // If no authorization details or an error is returned, propagate the error or deny access
315
+ if (!authorized || authorized.error) return authorized;
316
+
317
+ // Ensure the organization ID matches
318
+ if (authorized.organization_id !== data.organization_id)
319
+ return false;
320
+
321
+ // Validate host restrictions if specified
322
+ if (authorized.host && authorized.host.length) {
323
+ if (
324
+ !authorized.host ||
325
+ (!authorized.host.includes(data.host) &&
326
+ !authorized.host.includes("*"))
327
+ )
328
+ return false;
329
+ }
330
+
331
+ // Allow access if the user has admin privileges
332
+ if (authorized.admin === "true" || authorized.admin === true)
333
+ return true;
334
+
335
+ // Ensure actions are defined for the authorization
336
+ if (!authorized.actions)
337
+ return {
338
+ error: "Authorization does not have any actions defined"
339
+ };
340
+
341
+ // Check if the requested method is allowed under the defined actions
342
+ let status = await checkMethod(
343
+ data,
344
+ authorized.actions,
345
+ data.method
346
+ );
347
+
348
+ // If the method check fails, deny access
349
+ if (!status) {
350
+ return false;
351
+ }
352
+
353
+ // Return the sanitized and authorized data if all checks pass
354
+ return { authorized: data };
355
+ }
356
+
357
+ /**
358
+ * Validates whether a method is permitted based on the authorization rules.
359
+ *
360
+ * This function checks for an exact match of the `method` in the `authorized` actions.
361
+ * If no exact match is found, it checks progressively for partial matches using dot notation (e.g., "read.user").
362
+ *
363
+ * @param {object} data - The data object containing request details.
364
+ * @param {object} authorized - The authorization rules object mapping methods to permissions.
365
+ * @param {string} method - The method to validate (e.g., "read.user").
366
+ * @returns {Promise<boolean>} A promise that resolves to `true` if the method is authorized, `false` otherwise.
367
+ */
368
+ export async function checkMethod(data, authorized, method) {
369
+ // Check for an exact match of the method in the authorization rules
370
+ if (authorized[method]) {
371
+ return await checkMethodMatch(data, authorized[method], method);
372
+ }
373
+ // Check for partial matches using dot notation (e.g., "read.user")
374
+ else if (method.includes(".")) {
375
+ let match = "";
376
+ let splitMethod = method.split(".");
377
+
378
+ for (let i = 0; i < splitMethod.length; i++) {
379
+ if (!match) match = splitMethod[i];
380
+ else match += "." + splitMethod[i];
381
+
382
+ // If a partial match is found, validate it
383
+ if (authorized[match]) {
384
+ return await checkMethodMatch(
385
+ data,
386
+ authorized[match],
387
+ match
388
+ );
389
+ }
390
+ // If the last segment has no match, deny access
391
+ else if (i === splitMethod.length - 1) return false;
392
+ }
393
+ }
394
+ // Deny access if no match is found
395
+ else return false;
396
+ }
397
+
398
+ /**
399
+ * Checks whether a given method matches the authorized permissions and
400
+ * optionally sanitizes the provided data based on inclusion and exclusion rules.
401
+ *
402
+ * This function supports various types of authorization configurations, including:
403
+ * - Booleans: Directly allow (`true`) or deny (`false`).
404
+ * - Strings: Match the authorization string with the method or default to allow.
405
+ * - Numbers: Treat any non-zero number as `true` (authorized).
406
+ * - Arrays: Recursively check multiple authorization rules.
407
+ * - Objects: Evaluate nested authorization rules and apply inclusion/exclusion logic.
408
+ *
409
+ * The function also aggregates all inclusion and exclusion rules before sanitizing
410
+ * the data, ensuring consistent prioritization logic is applied holistically.
411
+ *
412
+ * @param {object} data - The data object containing the method and other fields to process.
413
+ * @param {boolean | string | number | object | Array} authorized - The authorization rules.
414
+ * @param {string} match - The method string to match against the authorization rules.
415
+ * @returns {Promise<boolean>} A promise that resolves to `true` if the method is authorized,
416
+ * `false` otherwise.
417
+ */
418
+ export async function checkMethodMatch(data, authorized, match) {
419
+ if (typeof authorized === "boolean") {
420
+ return authorized;
421
+ } else if (typeof authorized === "string") {
422
+ if (authorized === "false") return false;
423
+ else if (authorized === "true") return true;
424
+ else if (authorized === match) return true;
425
+ else return true;
426
+ } else if (typeof authorized === "number") {
427
+ return !!authorized;
428
+ } else {
429
+ let status = false;
430
+ let newmatch = data.method.replace(match + ".", "");
431
+
432
+ // Aggregate raw keys
433
+ let rawKeys = [];
434
+
435
+ if (Array.isArray(authorized)) {
436
+ for (let i = 0; i < authorized.length; i++) {
437
+ status = await checkMethodMatch(
438
+ data,
439
+ authorized[i],
440
+ newmatch
441
+ );
442
+ }
443
+ } else if (typeof authorized === "object") {
444
+ for (const key of Object.keys(authorized)) {
445
+ if (key.includes("$")) {
446
+ if (
447
+ [
448
+ "$storage",
449
+ "$database",
450
+ "$array",
451
+ "$index"
452
+ ].includes(key)
453
+ ) {
454
+ let opStatus = await checkMethodOperators(
455
+ data,
456
+ key,
457
+ authorized[key]
458
+ );
459
+ if (opStatus === true || opStatus === false)
460
+ status = opStatus;
461
+ } else if (key === "$keys") {
462
+ let type = data.method.split(".")[0];
463
+ if (data[type]) {
464
+ rawKeys.push(authorized[key]); // Collect raw authorization keys
465
+ }
466
+ } else {
467
+ let isFilter = applyFilter(
468
+ data,
469
+ authorized[key],
470
+ key
471
+ );
472
+ console.log("isFilter", isFilter);
473
+ }
474
+ }
475
+ }
476
+ }
477
+
478
+ // Parse all raw keys together to get inclusion/exclusion
479
+ let { inclusion, exclusion } = parsePermissions(rawKeys);
480
+
481
+ // Apply sanitization if there are inclusion or exclusion rules
482
+ if (inclusion || exclusion) {
483
+ let type = data.method.split(".")[0]; // Extract type from method
484
+ if (data[type]) {
485
+ data[type] = sanitizeData(
486
+ data[type],
487
+ inclusion,
488
+ exclusion
489
+ );
490
+ status = true;
491
+ }
492
+ }
493
+
494
+ if (newmatch) {
495
+ if (!status && authorized[newmatch]) {
496
+ status = await checkMethodMatch(
497
+ data,
498
+ authorized[newmatch],
499
+ newmatch
500
+ );
501
+ }
502
+ if (!status && authorized["*"]) {
503
+ status = await checkMethodMatch(
504
+ data,
505
+ authorized["*"],
506
+ newmatch
507
+ );
508
+ }
509
+ }
510
+
511
+ return status;
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Checks method operators to validate authorization based on dynamic data types and keys.
517
+ *
518
+ * @param {Object} data - The input data object containing various properties.
519
+ * @param {string} key - A string key to determine the type of operation,
520
+ * starting with `$` for dynamic type-based checks.
521
+ * @param {string} authorization - The authorization identifier, which could be a static value
522
+ * or a dynamic reference like `$user_id`.
523
+ *
524
+ * @returns {boolean|undefined} - Returns `true` if authorized, `false` if unauthorized,
525
+ * or `undefined` if no validation applies.
526
+ */
527
+ export async function checkMethodOperators(data, key, authorization) {
528
+ try {
529
+ // Adjust authorization dynamically if `$user_id` is provided and user info exists in the socket
530
+ if (authorization === "$user_id" && data.socket) {
531
+ authorization = data.socket.user_id || data.user_id;
532
+ }
533
+
534
+ // Check if the key starts with `$`, indicating a dynamic type-based operation
535
+ if (key.startsWith("$")) {
536
+ let type = key.substring(1); // Extract the type by removing `$`
537
+
538
+ // If the specified type does not exist in data, return undefined
539
+ if (!data[type]) {
540
+ return undefined;
541
+ }
542
+
543
+ // Handle cases where the type is a string
544
+ if (typeof data[type] === "string") {
545
+ return checkAuthorizationData(
546
+ data,
547
+ authorization,
548
+ data[type]
549
+ );
550
+ }
551
+ // Handle cases where the type is an array
552
+ else if (Array.isArray(data[type])) {
553
+ if (!data[type].length) return undefined; // Return undefined for empty arrays
554
+
555
+ // ToDo: Current strategy ensures all items must match; adjust as needed
556
+ let allAuthorized = true;
557
+ for (let i = 0; i < data[type].length; i++) {
558
+ const itemData =
559
+ typeof data[type][i] === "object"
560
+ ? data[type][i].name // Extract name if the item is an object
561
+ : data[type][i];
562
+ const authResult = checkAuthorizationData(
563
+ data,
564
+ authorization,
565
+ itemData
566
+ );
567
+ if (authResult === false) {
568
+ return false; // Return false if any item fails validation
569
+ } else if (authResult === undefined) {
570
+ allAuthorized = undefined; // Mark as undefined if any validation is inconclusive
571
+ }
572
+ }
573
+ return allAuthorized;
574
+ }
575
+ // Handle cases where the type is an object
576
+ else if (typeof data[type] === "object") {
577
+ return checkAuthorizationData(
578
+ data,
579
+ authorization,
580
+ data[type].name
581
+ );
582
+ }
583
+ }
584
+
585
+ // Return undefined if no conditions match
586
+ return undefined;
587
+ } catch (e) {
588
+ // Log the error and return undefined in case of an exception
589
+ console.log(e);
590
+ return undefined;
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Checks if the provided key is authorized based on the authorization rules.
596
+ *
597
+ * @param {Object} data - The data object to be used in filtering.
598
+ * @param {string|Array|Object} authorization - The authorization rules, which can be a string, array, or object.
599
+ * @param {string} key - The key to be checked against the authorization rules.
600
+ * @returns {boolean|undefined} - Returns true if authorized, false if explicitly unauthorized, and undefined if no rule matches.
601
+ */
602
+ export function checkAuthorizationData(data, authorization, key) {
603
+ // If the authorization is a string, check if it matches the key.
604
+ if (typeof authorization === "string") {
605
+ if (key === authorization)
606
+ return true; // Key matches the authorization string.
607
+ else return undefined; // No match, return undefined.
608
+ }
609
+ // If the authorization is an array, check if the key is included in the array.
610
+ else if (Array.isArray(authorization)) {
611
+ if (authorization.includes(key))
612
+ return true; // Key is included in the array.
613
+ else return undefined; // Key not found, return undefined.
614
+ }
615
+ // If the authorization is an object, handle various object-based rules.
616
+ else if (typeof authorization === "object") {
617
+ // If the authorization[key] is an object, apply filters recursively.
618
+ if (typeof authorization[key] === "object") {
619
+ for (const queryKey of Object.keys(authorization[key])) {
620
+ return applyFilter(data, authorization[key], queryKey); // Apply the filter logic.
621
+ }
622
+ }
623
+ // If authorization[key] is explicitly false or "false", return false.
624
+ else if (
625
+ authorization[key] === false ||
626
+ authorization[key] === "false"
627
+ )
628
+ return false;
629
+ // If authorization[key] exists and is truthy, return true.
630
+ else if (authorization[key]) return true;
631
+ // If none of the above conditions match, return undefined.
632
+ else return undefined;
633
+ }
634
+ }
635
+
636
+ export function applyFilter(data, authorization, authorizationKey) {
637
+ let keyParts = authorizationKey.split(".");
638
+ let operator = keyParts.pop();
639
+ let key = keyParts.join(".");
640
+ if (
641
+ [
642
+ "$eq",
643
+ "$ne",
644
+ "$lt",
645
+ "$lte",
646
+ "$gt",
647
+ "$gte",
648
+ "$in",
649
+ "$nin",
650
+ "$or",
651
+ "$and",
652
+ "$not",
653
+ "$nor",
654
+ "$exists",
655
+ "$type",
656
+ "$mod",
657
+ "$regex",
658
+ "$text",
659
+ "$where",
660
+ "$all",
661
+ "$elemMatch",
662
+ "$size"
663
+ ].includes(operator)
664
+ ) {
665
+ if (!data.$filter) data.$filter = { query: {} };
666
+ else if (!data.$filter.query) data.$filter.query = {};
667
+
668
+ if (
669
+ authorization[authorizationKey] === "$user_id" &&
670
+ data.socket
671
+ ) {
672
+ authorization[authorizationKey] =
673
+ data.socket.user_id || data.user_id;
674
+ }
675
+
676
+ if (typeof authorization === "string")
677
+ data.$filter.query[key] = {
678
+ [operator]: authorization
679
+ };
680
+ else
681
+ data.$filter.query[key] = {
682
+ [operator]: authorization[authorizationKey]
683
+ };
684
+
685
+ return true;
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Parses permissions into inclusion and exclusion arrays.
691
+ *
692
+ * @param {string | object | Array<string | object>} authorization - The authorization to parse.
693
+ * @returns {object} An object containing 'inclusion' and 'exclusion' arrays.
694
+ */
695
+ export function parsePermissions(authorization) {
696
+ // Initialize local arrays for inclusion and exclusion
697
+ const inclusion = [];
698
+ const exclusion = [];
699
+
700
+ // Helper function to process a single permission entry
701
+ function processPermission(entry) {
702
+ if (typeof entry === "string") {
703
+ // Treat string as inclusion
704
+ inclusion.push(entry);
705
+ } else if (typeof entry === "object" && entry !== null) {
706
+ for (const [key, value] of Object.entries(entry)) {
707
+ if (value === true) {
708
+ inclusion.push(key);
709
+ } else if (value === false) {
710
+ exclusion.push(key);
711
+ }
712
+ // Ignore entries where value is neither true nor false
713
+ }
714
+ }
715
+ // Ignore entries that are neither string nor object
716
+ }
717
+
718
+ // Normalize the input into an array of permission entries
719
+ let permissionEntries = [];
720
+
721
+ if (Array.isArray(authorization)) {
722
+ permissionEntries = authorization;
723
+ } else if (
724
+ typeof authorization === "string" ||
725
+ (typeof authorization === "object" && authorization !== null)
726
+ ) {
727
+ permissionEntries = [authorization];
728
+ }
729
+ // If authorization is undefined or null, treat as empty
730
+
731
+ // Process each permission entry
732
+ permissionEntries.forEach(processPermission);
733
+
734
+ // Apply prioritization logic
735
+ if (inclusion.length > 0) {
736
+ // If any inclusion exists, disregard exclusions
737
+ return { inclusion, exclusion: null };
738
+ } else if (exclusion.length > 0) {
739
+ // If only exclusions exist, disregard inclusions
740
+ return { inclusion: null, exclusion };
741
+ } else {
742
+ // If neither exists, set both to null
743
+ return { inclusion: null, exclusion: null };
744
+ }
745
+ }
746
+
747
+ /**
748
+ * Sanitizes data based on parsed permissions.
749
+ *
750
+ * @param {object | Array} data - The data object or array to sanitize.
751
+ * @param {Array} inclusion - The parsed inclusion array.
752
+ * @param {Array} exclusion - The parsed exclusion array.
753
+ * @returns {object | Array} The sanitized data object or array.
754
+ */
755
+ export function sanitizeData(data, inclusion, exclusion) {
756
+ // If both inclusion and exclusion are null, return the data as is
757
+ if (inclusion === null && exclusion === null) {
758
+ return data;
759
+ }
760
+
761
+ if (Array.isArray(data)) {
762
+ return data.map((item) =>
763
+ sanitizeData(item, inclusion, exclusion)
764
+ );
765
+ } else if (typeof data === "object" && data !== null) {
766
+ let sanitized = {};
767
+
768
+ if (inclusion) {
769
+ // Include only specified fields (supports nested fields)
770
+ for (let i = 0; i < inclusion.length; i++) {
771
+ let inclusionData = generateDotNotation(
772
+ data,
773
+ inclusion[i]
774
+ );
775
+ sanitized = { ...sanitized, ...inclusionData };
776
+ }
777
+ return dotNotationToObject(sanitized);
778
+ } else if (exclusion) {
779
+ for (let i = 0; i < exclusion.length; i++) {
780
+ let exclusionData = generateDotNotation(
781
+ data,
782
+ exclusion[i],
783
+ true
784
+ );
785
+ sanitized = { ...sanitized, ...exclusionData };
786
+ }
787
+ return dotNotationToObject(sanitized, data);
788
+ }
789
+ }
790
+
791
+ return data;
792
+ }
793
+
794
+ /**
795
+ * Generates an object with dot notation keys from the given data and path.
796
+ */
797
+ export function generateDotNotation(data, path, isEclusion) {
798
+ if (typeof path !== "string")
799
+ throw new Error("Path must be a string");
800
+
801
+ let result = {};
802
+
803
+ function processKeys(dataItem, keys, prefix = "", result) {
804
+ if (dataItem === undefined) return;
805
+
806
+ if (!keys.length) {
807
+ if (isEclusion) {
808
+ result[prefix] = undefined;
809
+ } else {
810
+ result[prefix] = dataItem;
811
+ }
812
+ return;
813
+ }
814
+
815
+ let key = keys.shift();
816
+ let isArray = key.endsWith("[]");
817
+ if (isArray) key = key.slice(0, -2); // Remove [] from key
818
+
819
+ // Construct prefix
820
+ prefix = prefix ? `${prefix}.${key}` : key;
821
+
822
+ const currentData = getValueFromObject(dataItem, key); // Using your existing function
823
+
824
+ if (isArray && Array.isArray(currentData)) {
825
+ currentData.forEach((item, index) => {
826
+ const newPrefix = `${prefix}[${index}]`;
827
+ processKeys(item, [...keys], newPrefix, result);
828
+ });
829
+ } else if (!isArray && currentData !== undefined) {
830
+ processKeys(currentData, keys, prefix, result);
831
+ }
832
+ }
833
+
834
+ if (data && path.trim()) {
835
+ const keys = path.split(".");
836
+ processKeys(data, keys, "", result);
837
+ }
838
+
839
+ return result;
840
+ }
841
+
842
+ export async function checkFilter(authorized, data, apikey, unauthorize) {
843
+ if (data.object.$filter && data.object.$filter.query) {
844
+ let key;
845
+ if (data.object.$filter.type == "object") key = "_id";
846
+ else if (data.object.$filter.type == "array") key = "name";
847
+ if (key) {
848
+ for (let value of authorized[apikey]) {
849
+ if (value[key]) value = value[key];
850
+ if (!data.object.$filter.query.$or)
851
+ data.object.$filter.query.$or = [];
852
+ if (unauthorize)
853
+ data.object.$filter.query.$or.push({
854
+ [key]: { $ne: value }
855
+ });
856
+ else
857
+ data.object.$filter.query.$or.push({
858
+ [key]: value
859
+ });
860
+ }
861
+ if (!unauthorize) return true;
862
+ }
863
+ }
864
+ }
865
+
866
+ export function deleteKey(data, path) {
867
+ if (!data || !path) return;
868
+ if (path.includes("._id")) path = path.replace("._id", "");
869
+ data = dotNotationToObject({ [path]: undefined }, data);
870
+ return data;
871
+ }
872
+
873
+ // ==========================================
874
+ // Bound API Assembly Object
875
+ // ==========================================
876
+ export default {
877
+ check,
878
+ updateAuthorization,
879
+ deleteAuthorization,
880
+ getAuthorization,
881
+ readAuthorization,
882
+ checkAuthorization,
883
+ checkMethod,
884
+ checkMethodMatch,
885
+ checkMethodOperators,
886
+ checkAuthorizationData,
887
+ applyFilter,
888
+ parsePermissions,
889
+ sanitizeData,
890
+ generateDotNotation,
891
+ checkFilter,
892
+ deleteKey
893
+ };