@ozdao/martyrs 0.2.563 → 0.2.565
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/abac-BPl9Bmf9.js +1527 -0
- package/dist/builder.js +51 -39
- package/dist/{common.schema-GFSlNJo7.js → common.schema-DswiUXKB.js} +1 -1
- package/dist/community.server.js +48 -9
- package/dist/core.server.js +6 -4
- package/dist/{crud-C7FSTUes.js → crud-q1ye5IhV.js} +7 -7
- package/dist/events.server.js +3 -3
- package/dist/gallery.server.js +2 -2
- package/dist/inventory.server.js +4 -6
- package/dist/{main-CmjWiDVF.js → main-B9o1iBAZ.js} +1279 -1287
- package/dist/marketplace.server.js +1 -1
- package/dist/martyrs/src/components/Button/Button.vue2.js +33 -42
- package/dist/martyrs/src/components/Button/Button.vue2.js.map +1 -1
- package/dist/martyrs/src/components/EditImages/{EditImages.vue.js → EditImages.vue2.js} +2 -2
- package/dist/martyrs/src/components/EditImages/EditImages.vue2.js.map +1 -0
- package/dist/martyrs/src/components/Feed/Feed.vue.js +1 -1
- package/dist/martyrs/src/components/FieldPhone/FieldPhone.vue.js +1 -1
- package/dist/martyrs/src/components/FieldPhone/FieldPhone.vue.js.map +1 -1
- package/dist/martyrs/src/components/Loader/Loader.vue.js +1 -2
- package/dist/martyrs/src/components/Loader/Loader.vue.js.map +1 -1
- package/dist/martyrs/src/components/Menu/{Menu.vue.js → Menu.vue2.js} +2 -2
- package/dist/martyrs/src/components/Menu/Menu.vue2.js.map +1 -0
- package/dist/martyrs/src/components/Tab/{Tab.vue.js → Tab.vue2.js} +2 -2
- package/dist/martyrs/src/components/Tab/Tab.vue2.js.map +1 -0
- package/dist/martyrs/src/components/Tree/Tree.vue.js +6 -3
- package/dist/martyrs/src/components/Tree/Tree.vue.js.map +1 -1
- package/dist/martyrs/src/modules/auth/auth.client.js +10 -7
- package/dist/martyrs/src/modules/auth/auth.client.js.map +1 -1
- package/dist/martyrs/src/modules/auth/views/components/pages/EnterPassword.vue.js +1 -1
- package/dist/martyrs/src/modules/auth/views/components/pages/Invite.vue.js +1 -1
- package/dist/martyrs/src/modules/auth/views/components/pages/Profile.vue.js +1 -1
- package/dist/martyrs/src/modules/auth/views/components/pages/ProfileBlogposts.vue.js +1 -1
- package/dist/martyrs/src/modules/auth/views/components/pages/ResetPassword.vue.js +1 -1
- package/dist/martyrs/src/modules/auth/views/components/pages/SignIn.vue.js +12 -12
- package/dist/martyrs/src/modules/auth/views/components/pages/SignIn.vue.js.map +1 -1
- package/dist/martyrs/src/modules/auth/views/components/pages/SignUp.vue.js +1 -1
- package/dist/martyrs/src/modules/auth/views/router/auth.router.js +116 -0
- package/dist/martyrs/src/modules/auth/views/router/auth.router.js.map +1 -0
- package/dist/martyrs/src/modules/auth/views/router/users.router.js +180 -0
- package/dist/martyrs/src/modules/auth/views/router/users.router.js.map +1 -0
- package/dist/martyrs/src/modules/backoffice/components/partials/Sidebar.vue.js +3 -3
- package/dist/martyrs/src/modules/backoffice/components/partials/Sidebar.vue.js.map +1 -1
- package/dist/martyrs/src/modules/core/locales/en.js +45 -0
- package/dist/martyrs/src/modules/core/locales/en.js.map +1 -1
- package/dist/martyrs/src/modules/core/locales/ru.js +45 -0
- package/dist/martyrs/src/modules/core/locales/ru.js.map +1 -1
- package/dist/martyrs/src/modules/core/views/classes/i18n.manager.js +9 -0
- package/dist/martyrs/src/modules/core/views/classes/i18n.manager.js.map +1 -1
- package/dist/martyrs/src/modules/core/views/components/sections/{Filters.vue.js → Filters.vue2.js} +2 -2
- package/dist/martyrs/src/modules/core/views/components/sections/Filters.vue2.js.map +1 -0
- package/dist/martyrs/src/modules/core/views/components/sections/SectionPageTitle.vue.js +1 -1
- package/dist/martyrs/src/modules/core/views/mixins/mixins.js +1 -2
- package/dist/martyrs/src/modules/core/views/mixins/mixins.js.map +1 -1
- package/dist/martyrs/src/modules/core/views/router/addRoutes.js +6 -1
- package/dist/martyrs/src/modules/core/views/router/addRoutes.js.map +1 -1
- package/dist/martyrs/src/modules/events/components/pages/EditEvent.vue.js +1 -1
- package/dist/martyrs/src/modules/events/components/pages/Event.vue.js +1 -1
- package/dist/martyrs/src/modules/events/components/pages/EventsBackoffice.vue.js +1 -1
- package/dist/martyrs/src/modules/gallery/components/sections/BackofficeGallery.vue.js +1 -1
- package/dist/martyrs/src/modules/inventory/components/pages/InventoryEdit.vue.js +2 -2
- package/dist/martyrs/src/modules/inventory/components/pages/InventoryEdit.vue.js.map +1 -1
- package/dist/martyrs/src/modules/marketplace/views/components/pages/Marketplace.vue.js +1 -1
- package/dist/martyrs/src/modules/marketplace/views/store/marketplace.js +0 -16
- package/dist/martyrs/src/modules/marketplace/views/store/marketplace.js.map +1 -1
- package/dist/martyrs/src/modules/notifications/components/elements/NotificationBadge.vue.js +4 -4
- package/dist/martyrs/src/modules/notifications/components/elements/NotificationBadge.vue.js.map +1 -1
- package/dist/martyrs/src/modules/notifications/components/pages/Notifications.vue.js +1 -1
- package/dist/martyrs/src/modules/orders/components/elements/FieldSubscribeNewsletter.vue.js +3 -0
- package/dist/martyrs/src/modules/orders/components/elements/FieldSubscribeNewsletter.vue.js.map +1 -1
- package/dist/martyrs/src/modules/orders/components/pages/OrderCreateBackoffice.vue.js +1 -1
- package/dist/martyrs/src/modules/orders/components/pages/Orders.vue.js +1 -1
- package/dist/martyrs/src/modules/orders/components/sections/FormDelivery.vue.js +1 -1
- package/dist/martyrs/src/modules/organizations/components/blocks/CardOrganization.vue.js +1 -1
- package/dist/martyrs/src/modules/organizations/components/blocks/CardOrganization.vue.js.map +1 -1
- package/dist/martyrs/src/modules/organizations/components/pages/Organization.vue.js +1 -1
- package/dist/martyrs/src/modules/organizations/components/pages/OrganizationBackoffice.vue.js +1 -1
- package/dist/martyrs/src/modules/organizations/components/pages/OrganizationEdit.vue.js +2 -2
- package/dist/martyrs/src/modules/organizations/components/pages/OrganizationEdit.vue.js.map +1 -1
- package/dist/martyrs/src/modules/organizations/components/pages/Organizations.vue.js +1 -1
- package/dist/martyrs/src/modules/organizations/components/sections/Organizations.vue.js +1 -1
- package/dist/martyrs/src/modules/products/components/blocks/CardCategory.vue.js +1 -1
- package/dist/martyrs/src/modules/products/components/blocks/CardCategory.vue.js.map +1 -1
- package/dist/martyrs/src/modules/products/components/blocks/CardProduct.vue.js +15 -2
- package/dist/martyrs/src/modules/products/components/blocks/CardProduct.vue.js.map +1 -1
- package/dist/martyrs/src/modules/products/components/pages/Categories.vue.js +9 -6
- package/dist/martyrs/src/modules/products/components/pages/Categories.vue.js.map +1 -1
- package/dist/martyrs/src/modules/products/components/pages/CategoryEdit.vue.js +4 -3
- package/dist/martyrs/src/modules/products/components/pages/CategoryEdit.vue.js.map +1 -1
- package/dist/martyrs/src/modules/products/components/pages/Product.vue.js +11 -2
- package/dist/martyrs/src/modules/products/components/pages/Product.vue.js.map +1 -1
- package/dist/martyrs/src/modules/products/components/pages/ProductEdit.vue.js +2 -2
- package/dist/martyrs/src/modules/products/components/pages/Products.vue.js +2 -2
- package/dist/martyrs/src/modules/products/components/sections/EditVariants.vue.js +1 -1
- package/dist/martyrs/src/modules/products/components/sections/SectionProduct.vue.js +11 -8
- package/dist/martyrs/src/modules/products/components/sections/SectionProduct.vue.js.map +1 -1
- package/dist/martyrs/src/modules/rents/views/components/pages/Gant/GanttToolbar.vue.js +1 -1
- package/dist/martyrs/src/modules/rents/views/components/pages/Rents.vue.js +1 -1
- package/dist/martyrs/src/modules/rents/views/components/pages/RentsEdit.vue.js +210 -60
- package/dist/martyrs/src/modules/rents/views/components/pages/RentsEdit.vue.js.map +1 -1
- package/dist/martyrs/src/modules/spots/components/pages/Map.vue.js +3 -3
- package/dist/martyrs/src/modules/spots/components/pages/Map.vue.js.map +1 -1
- package/dist/martyrs/src/modules/spots/components/pages/SpotEdit.vue.js +1 -1
- package/dist/martyrs.css +1 -1
- package/dist/martyrs.es.js +1 -1
- package/dist/music.server.js +11 -12
- package/dist/node_modules/.pnpm/qrcode@1.5.4/node_modules/qrcode/lib/core/utils.js +1 -1
- package/dist/node_modules/.pnpm/qrcode@1.5.4/node_modules/qrcode/lib/renderer/utils.js +1 -1
- package/dist/notifications.server.js +0 -3
- package/dist/orders.server.js +5 -6
- package/dist/organizations.server.js +9 -10
- package/dist/products.server.js +27 -26
- package/dist/{queryProcessor-CBQgZycY.js → queryProcessor-C_5Iipam.js} +4 -1
- package/dist/rents.server.js +2 -3
- package/dist/spots.server.js +1 -1
- package/dist/style.css +38 -23
- package/dist/{web-cNKIl_cL.js → web-BF3ijvEr.js} +1 -1
- package/package.json +1 -1
- package/src/builder/modes/ssr.rspack.dev.js +4 -3
- package/src/builder/rspack/rspack.config.api.js +15 -4
- package/src/builder/rspack/rspack.config.base.js +3 -3
- package/src/builder/rspack/rspack.config.ssr.client.js +28 -28
- package/src/builder/templates/page.js +2 -2
- package/src/components/Button/Button.vue +50 -37
- package/src/components/FieldPhone/FieldPhone.vue +1 -1
- package/src/components/Loader/Loader.vue +1 -1
- package/src/components/Tree/Tree.vue +6 -3
- package/src/modules/PROCESS.md +0 -0
- package/src/modules/TASKS.MD +17 -0
- package/src/modules/auth/auth.client.js +11 -7
- package/src/modules/auth/views/components/pages/SignIn.vue +1 -1
- package/src/modules/auth/views/router/auth.router.js +94 -0
- package/src/modules/auth/views/router/users.router.js +153 -0
- package/src/modules/backoffice/components/partials/Sidebar.vue +7 -7
- package/src/modules/community/community.server.js +8 -0
- package/src/modules/community/policies/blog.policies.js +55 -0
- package/src/modules/community/routes/blog.routes.js +1 -1
- package/src/modules/community/routes/comments.routes.js +1 -1
- package/src/modules/community/routes/reactions.routes.js +1 -4
- package/src/modules/core/controllers/classes/abac/abac.adapter.express.js +206 -124
- package/src/modules/core/controllers/classes/abac/abac.adapter.ws.js +203 -50
- package/src/modules/core/controllers/classes/abac/abac.core.js +127 -36
- package/src/modules/core/controllers/classes/abac/abac.fields.js +144 -179
- package/src/modules/core/controllers/classes/abac/abac.js +201 -10
- package/src/modules/core/controllers/classes/abac/abac.policies.js +147 -57
- package/src/modules/core/controllers/classes/crud/crud.policies.js +5 -5
- package/src/modules/core/controllers/policies/core.policies.js +5 -2
- package/src/modules/core/controllers/utils/queryProcessor.js +4 -1
- package/src/modules/core/core.server.js +1 -0
- package/src/modules/core/locales/en.js +45 -0
- package/src/modules/core/locales/ru.js +45 -0
- package/src/modules/core/models/schemas/common.schema.js +1 -1
- package/src/modules/core/views/classes/i18n.manager.js +13 -0
- package/src/modules/core/views/components/sections/filters/FilterPrice.vue +81 -0
- package/src/modules/core/views/mixins/mixins.js +1 -2
- package/src/modules/core/views/router/addRoutes.js +6 -1
- package/src/modules/events/routes/events.routes.js +1 -1
- package/src/modules/inventory/components/pages/InventoryEdit.vue +3 -3
- package/src/modules/inventory/policies/inventory.policies.js +1 -1
- package/src/modules/inventory/routes/inventory.routes.js +1 -1
- package/src/modules/marketplace/marketplace.router.js +66 -0
- package/src/modules/marketplace/views/components/layouts/Marketplace.vue +363 -0
- package/src/modules/marketplace/views/components/pages/Catalog.vue +73 -0
- package/src/modules/marketplace/views/store/marketplace.js +0 -16
- package/src/modules/music/controllers/stream.controller.js +1 -1
- package/src/modules/music/music.server.js +1 -1
- package/src/modules/music/policies/music.policies.js +3 -2
- package/src/modules/music/router/library.router.js +26 -0
- package/src/modules/music/router/music.router.js +176 -0
- package/src/modules/notifications/components/elements/NotificationBadge.vue +5 -6
- package/src/modules/notifications/notifications.server.js +1 -3
- package/src/modules/orders/components/elements/FieldSubscribeNewsletter.vue +5 -0
- package/src/modules/orders/orders.server.js +0 -1
- package/src/modules/organizations/components/blocks/CardOrganization.vue +2 -2
- package/src/modules/organizations/components/pages/DepartmentEdit.vue +2 -2
- package/src/modules/organizations/components/pages/OrganizationEdit.vue +2 -2
- package/src/modules/organizations/policies/organizations.policies.js +12 -6
- package/src/modules/organizations/routes/organizations.routes.js +1 -3
- package/src/modules/products/components/blocks/CardCategory.vue +1 -1
- package/src/modules/products/components/blocks/CardProduct.vue +16 -2
- package/src/modules/products/components/pages/Categories.vue +9 -6
- package/src/modules/products/components/pages/CategoryEdit.vue +8 -4
- package/src/modules/products/components/pages/Product.vue +11 -5
- package/src/modules/products/components/sections/SectionProduct.vue +11 -7
- package/src/modules/products/controllers/categories.controller.js +32 -27
- package/src/modules/products/routes/categories.routes.js +1 -1
- package/src/modules/rents/controllers/routes/rents.routes.js +1 -1
- package/src/modules/rents/views/components/pages/RentsEdit.vue +208 -49
- package/src/modules/spots/components/pages/Map.vue +2 -2
- package/dist/abac-DYoheWuc.js +0 -1031
- package/dist/core.abac-DUPBnlk6.js +0 -298
- package/dist/core.logger-C3q8A9dl.js +0 -51
- package/dist/martyrs/src/components/EditImages/EditImages.vue.js.map +0 -1
- package/dist/martyrs/src/components/Menu/Menu.vue.js.map +0 -1
- package/dist/martyrs/src/components/Tab/Tab.vue.js.map +0 -1
- package/dist/martyrs/src/modules/auth/auth.router.js +0 -342
- package/dist/martyrs/src/modules/auth/auth.router.js.map +0 -1
- package/dist/martyrs/src/modules/core/views/components/sections/Filters.vue.js.map +0 -1
- package/src/modules/auth/auth.router.js +0 -262
- package/src/modules/core/controllers/classes/abac/v2/abac-core-fixed.js +0 -313
- package/src/modules/core/controllers/classes/abac/v2/abac-express-fixed.js +0 -276
- package/src/modules/core/controllers/classes/abac/v2/abac-fields-fixed.js +0 -425
- package/src/modules/core/controllers/classes/abac/v2/abac-main-fixed.js +0 -295
- package/src/modules/core/controllers/classes/abac/v2/abac-policies-fixed.js +0 -316
- package/src/modules/core/controllers/classes/abac/v2/abac-ws-fixed.js +0 -237
- package/src/modules/core/controllers/classes/core.abac.js +0 -310
- package/src/modules/core/controllers/classes/core.crud.js +0 -89
- package/src/modules/governance/reactcode/eslint.config.js +0 -28
|
@@ -0,0 +1,1527 @@
|
|
|
1
|
+
import { C as CacheNamespaced } from "./core.cache-DALYFDdy.js";
|
|
2
|
+
import set from "lodash/set.js";
|
|
3
|
+
import get from "lodash/get.js";
|
|
4
|
+
import unset from "lodash/unset.js";
|
|
5
|
+
import cloneDeep from "lodash/cloneDeep.js";
|
|
6
|
+
class Logger {
|
|
7
|
+
constructor(db) {
|
|
8
|
+
this.LogModel = db.log;
|
|
9
|
+
}
|
|
10
|
+
async log(level, message) {
|
|
11
|
+
const logEntry = new this.LogModel({
|
|
12
|
+
level,
|
|
13
|
+
message
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
await logEntry.save();
|
|
17
|
+
console.info(`Logged: ${level} - ${message}`);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error("Logging error:", err);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async info(message) {
|
|
23
|
+
await this.log("info", message);
|
|
24
|
+
}
|
|
25
|
+
async error(message) {
|
|
26
|
+
await this.log("error", message);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const instances = /* @__PURE__ */ new Map();
|
|
30
|
+
class LoggerNamespaced {
|
|
31
|
+
constructor(namespaceOrDb, db) {
|
|
32
|
+
if (!db && namespaceOrDb && typeof namespaceOrDb === "object") {
|
|
33
|
+
const namespace2 = "global";
|
|
34
|
+
if (instances.has(namespace2)) {
|
|
35
|
+
return instances.get(namespace2);
|
|
36
|
+
}
|
|
37
|
+
const instance3 = new Logger(namespaceOrDb);
|
|
38
|
+
instances.set(namespace2, instance3);
|
|
39
|
+
return instance3;
|
|
40
|
+
}
|
|
41
|
+
const namespace = namespaceOrDb;
|
|
42
|
+
if (instances.has(namespace)) {
|
|
43
|
+
return instances.get(namespace);
|
|
44
|
+
}
|
|
45
|
+
const instance2 = new Logger(db);
|
|
46
|
+
instances.set(namespace, instance2);
|
|
47
|
+
return instance2;
|
|
48
|
+
}
|
|
49
|
+
// Статический метод для получения всех namespace'ов
|
|
50
|
+
static getNamespaces() {
|
|
51
|
+
return Array.from(instances.keys());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
class ABACCore {
|
|
55
|
+
constructor(abac) {
|
|
56
|
+
this.abac = abac;
|
|
57
|
+
this.runningPolicies = /* @__PURE__ */ new Map();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Нормализация контекста
|
|
61
|
+
*/
|
|
62
|
+
normalizeContext(input) {
|
|
63
|
+
const context = {
|
|
64
|
+
user: null,
|
|
65
|
+
action: null,
|
|
66
|
+
resource: null,
|
|
67
|
+
currentResource: null,
|
|
68
|
+
resourceId: null,
|
|
69
|
+
// Добавляем ID ресурса для кэша
|
|
70
|
+
data: {},
|
|
71
|
+
req: null,
|
|
72
|
+
socket: null,
|
|
73
|
+
params: {},
|
|
74
|
+
_cache: /* @__PURE__ */ new Map(),
|
|
75
|
+
_abac: this.abac,
|
|
76
|
+
skipFieldPolicies: input.skipFieldPolicies || false
|
|
77
|
+
};
|
|
78
|
+
if (input.req) {
|
|
79
|
+
context.user = input.user || input.req.userId;
|
|
80
|
+
context.data = {
|
|
81
|
+
body: input.req.body || {},
|
|
82
|
+
query: input.req.query || {},
|
|
83
|
+
params: input.req.params || {}
|
|
84
|
+
};
|
|
85
|
+
context.params = input.req.params;
|
|
86
|
+
context.req = input.req;
|
|
87
|
+
context.resourceId = input.req.params?._id || input.req.params?.id || input.req.body?._id || input.req.body?.id;
|
|
88
|
+
} else if (input.socket) {
|
|
89
|
+
context.user = input.user || input.socket.userId;
|
|
90
|
+
context.socket = input.socket;
|
|
91
|
+
context.data = input.data || {};
|
|
92
|
+
context.resourceId = input.data?._id || input.data?.id;
|
|
93
|
+
}
|
|
94
|
+
return Object.assign(context, {
|
|
95
|
+
...input,
|
|
96
|
+
data: context.data
|
|
97
|
+
// Сохраняем структурированную data
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Выполнение политики с кэшированием
|
|
102
|
+
*/
|
|
103
|
+
async executePolicyWithCache(policyName, policyFn, context) {
|
|
104
|
+
const contextCacheKey = `${policyName}_${context.action}`;
|
|
105
|
+
if (context._cache.has(contextCacheKey)) {
|
|
106
|
+
return context._cache.get(contextCacheKey);
|
|
107
|
+
}
|
|
108
|
+
const globalCacheKey = this._buildCacheKey(policyName, context);
|
|
109
|
+
if (this.abac.options.cacheEnabled) {
|
|
110
|
+
const cached = await this.abac.cache.get(globalCacheKey);
|
|
111
|
+
if (cached !== void 0) {
|
|
112
|
+
context._cache.set(contextCacheKey, cached);
|
|
113
|
+
return cached;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const result = await policyFn(context);
|
|
117
|
+
context._cache.set(contextCacheKey, result);
|
|
118
|
+
if (this.abac.options.cacheEnabled) {
|
|
119
|
+
const tags = [
|
|
120
|
+
`user_${context.user}`,
|
|
121
|
+
`resource_${context.resource}`,
|
|
122
|
+
`policy_${policyName}`
|
|
123
|
+
];
|
|
124
|
+
if (context.resourceId) {
|
|
125
|
+
tags.push(`resourceId_${context.resourceId}`);
|
|
126
|
+
}
|
|
127
|
+
await this.abac.cache.setWithTags(globalCacheKey, result, tags);
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Построение ключа кэша с учетом ID ресурса
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
_buildCacheKey(policyName, context) {
|
|
136
|
+
const parts = [
|
|
137
|
+
"policy",
|
|
138
|
+
policyName,
|
|
139
|
+
context.user,
|
|
140
|
+
context.resource,
|
|
141
|
+
context.action
|
|
142
|
+
];
|
|
143
|
+
if (context.resourceId) {
|
|
144
|
+
parts.push(context.resourceId);
|
|
145
|
+
}
|
|
146
|
+
return parts.join("_");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Выполнение политик с ограничением параллельности
|
|
150
|
+
*/
|
|
151
|
+
async executePoliciesLimited(policies, context, stopOnDeny = false) {
|
|
152
|
+
const results = [];
|
|
153
|
+
const limit = this.abac.options.concurrencyLimit;
|
|
154
|
+
const batches = [];
|
|
155
|
+
for (let i = 0; i < policies.length; i += limit) {
|
|
156
|
+
batches.push(policies.slice(i, i + limit));
|
|
157
|
+
}
|
|
158
|
+
for (const batch of batches) {
|
|
159
|
+
const batchPromises = batch.map(async ([name, policy]) => {
|
|
160
|
+
try {
|
|
161
|
+
const policyFn = typeof policy === "function" ? policy : policy.fn;
|
|
162
|
+
const result = await this.executePolicyWithCache(name, policyFn, context);
|
|
163
|
+
return { name, result: this.normalizeResult(result, name) };
|
|
164
|
+
} catch (error) {
|
|
165
|
+
this.abac.logger?.error("Policy execution error", {
|
|
166
|
+
policy: name,
|
|
167
|
+
error: error.message,
|
|
168
|
+
stack: error.stack
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
name,
|
|
172
|
+
result: {
|
|
173
|
+
allow: !this.abac.options.strictMode,
|
|
174
|
+
reason: `POLICY_ERROR_${name.toUpperCase()}`
|
|
175
|
+
},
|
|
176
|
+
error
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
const batchResults = await Promise.all(batchPromises);
|
|
181
|
+
results.push(...batchResults);
|
|
182
|
+
if (stopOnDeny) {
|
|
183
|
+
const shouldStop = batchResults.some((r) => !r.result.allow || r.result.force);
|
|
184
|
+
if (shouldStop) break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return results;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Нормализация результата политики
|
|
191
|
+
*/
|
|
192
|
+
normalizeResult(result, policyName) {
|
|
193
|
+
if (this.abac.options.strictMode && result === void 0) {
|
|
194
|
+
return {
|
|
195
|
+
allow: false,
|
|
196
|
+
force: false,
|
|
197
|
+
reason: `UNDEFINED_IN_STRICT_MODE_${policyName.toUpperCase()}`
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (result && typeof result === "object" && ("allow" in result || "force" in result)) {
|
|
201
|
+
return {
|
|
202
|
+
allow: result.allow !== void 0 ? !!result.allow : true,
|
|
203
|
+
force: !!result.force,
|
|
204
|
+
reason: result.reason || `POLICY_${policyName.toUpperCase()}`
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (result === true) {
|
|
208
|
+
return {
|
|
209
|
+
allow: true,
|
|
210
|
+
force: false,
|
|
211
|
+
reason: `ALLOWED_BY_${policyName.toUpperCase()}`
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (result === false) {
|
|
215
|
+
return {
|
|
216
|
+
allow: false,
|
|
217
|
+
force: false,
|
|
218
|
+
reason: `DENIED_BY_${policyName.toUpperCase()}`
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
allow: !this.abac.options.strictMode,
|
|
223
|
+
force: false,
|
|
224
|
+
reason: `NEUTRAL_${policyName.toUpperCase()}`
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Основной метод проверки доступа
|
|
229
|
+
*/
|
|
230
|
+
async checkAccess(rawContext, customPolicies = {}) {
|
|
231
|
+
const startTime = Date.now();
|
|
232
|
+
const context = this.normalizeContext(rawContext);
|
|
233
|
+
if (context.isServiceRequest) {
|
|
234
|
+
return { allow: true, reason: "SERVICE_REQUEST_ALLOWED" };
|
|
235
|
+
}
|
|
236
|
+
if (!context.user && !context.options?.allowUnauthenticated) {
|
|
237
|
+
return { allow: false, reason: "UNAUTHENTICATED_ACCESS_DENIED" };
|
|
238
|
+
}
|
|
239
|
+
if (!context.currentResource) {
|
|
240
|
+
await this.loadResource(context);
|
|
241
|
+
}
|
|
242
|
+
const result = await this.abac.policies.evaluate(context, customPolicies);
|
|
243
|
+
if (this.abac.options.enableAudit) {
|
|
244
|
+
await this.audit(context, result, Date.now() - startTime);
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Загрузка ресурса
|
|
250
|
+
*/
|
|
251
|
+
async loadResource(context) {
|
|
252
|
+
const resourceModel = this.abac.getResourceModel(context.resource);
|
|
253
|
+
if (!resourceModel) return;
|
|
254
|
+
try {
|
|
255
|
+
let currentResource;
|
|
256
|
+
const id = context.resourceId || context.data?.body?._id || context.data?.params?._id;
|
|
257
|
+
if (id) {
|
|
258
|
+
currentResource = await resourceModel.findById(id);
|
|
259
|
+
} else if (context.data?.body?.url || context.data?.params?.url) {
|
|
260
|
+
const url = context.data.body?.url || context.data.params?.url;
|
|
261
|
+
currentResource = await resourceModel.findOne({ url });
|
|
262
|
+
}
|
|
263
|
+
if (currentResource) {
|
|
264
|
+
context.currentResource = currentResource;
|
|
265
|
+
context.resourceModel = resourceModel;
|
|
266
|
+
context.resourceId = currentResource._id?.toString();
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
this.abac.logger?.error("Resource loading error", {
|
|
270
|
+
resource: context.resource,
|
|
271
|
+
error: error.message
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Структурированный аудит
|
|
277
|
+
*/
|
|
278
|
+
async audit(context, result, duration) {
|
|
279
|
+
try {
|
|
280
|
+
const auditEntry = {
|
|
281
|
+
type: "ACCESS_CHECK",
|
|
282
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
283
|
+
user: context.user,
|
|
284
|
+
resource: context.resource,
|
|
285
|
+
resourceId: context.resourceId,
|
|
286
|
+
action: context.action,
|
|
287
|
+
result: result.allow,
|
|
288
|
+
reason: result.reason,
|
|
289
|
+
duration,
|
|
290
|
+
ip: context.req?.ip,
|
|
291
|
+
userAgent: context.req?.get?.("user-agent"),
|
|
292
|
+
metadata: context.auditMetadata || {}
|
|
293
|
+
};
|
|
294
|
+
await this.abac.logger.info("Access check", auditEntry);
|
|
295
|
+
if (["delete", "admin", "export"].includes(context.action)) {
|
|
296
|
+
await this.abac.logger.warn("Critical action attempt", auditEntry);
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
this.abac.logger?.error("Audit logging error", {
|
|
300
|
+
error: error.message
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
class ABACPolicies {
|
|
306
|
+
constructor(abac) {
|
|
307
|
+
this.abac = abac;
|
|
308
|
+
this.global = /* @__PURE__ */ new Map();
|
|
309
|
+
this.resources = /* @__PURE__ */ new Map();
|
|
310
|
+
this.extensions = /* @__PURE__ */ new Map();
|
|
311
|
+
this.priorities = {
|
|
312
|
+
static: [],
|
|
313
|
+
dynamic: [],
|
|
314
|
+
extensions: []
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Регистрация глобальной политики
|
|
319
|
+
*/
|
|
320
|
+
registerGlobalPolicy(name, policyFn, metadata = {}) {
|
|
321
|
+
if (typeof policyFn !== "function") {
|
|
322
|
+
throw new Error(`Global policy "${name}" must be a function`);
|
|
323
|
+
}
|
|
324
|
+
const policy = {
|
|
325
|
+
fn: policyFn,
|
|
326
|
+
type: metadata.type || "dynamic",
|
|
327
|
+
priority: metadata.priority || 0,
|
|
328
|
+
...metadata
|
|
329
|
+
};
|
|
330
|
+
this.global.set(name, policy);
|
|
331
|
+
this.updatePriorities();
|
|
332
|
+
return this.abac;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Регистрация политики ресурса
|
|
336
|
+
*/
|
|
337
|
+
registerResourcePolicy(resourceName, policyFn, options = {}) {
|
|
338
|
+
if (typeof policyFn !== "function") {
|
|
339
|
+
throw new Error(`Resource policy for "${resourceName}" must be a function`);
|
|
340
|
+
}
|
|
341
|
+
this.resources.set(resourceName, {
|
|
342
|
+
fn: policyFn,
|
|
343
|
+
modelName: options.modelName || options.model || resourceName,
|
|
344
|
+
...options
|
|
345
|
+
});
|
|
346
|
+
return this.abac;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Регистрация расширения
|
|
350
|
+
*/
|
|
351
|
+
registerExtension(moduleName, extensionFn) {
|
|
352
|
+
if (typeof extensionFn !== "function") {
|
|
353
|
+
throw new Error(`Extension "${moduleName}" must be a function`);
|
|
354
|
+
}
|
|
355
|
+
this.extensions.set(moduleName, extensionFn);
|
|
356
|
+
this.updatePriorities();
|
|
357
|
+
return this.abac;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Обновление приоритетов с сортировкой
|
|
361
|
+
*/
|
|
362
|
+
updatePriorities() {
|
|
363
|
+
this.priorities = {
|
|
364
|
+
static: [],
|
|
365
|
+
dynamic: [],
|
|
366
|
+
extensions: []
|
|
367
|
+
};
|
|
368
|
+
for (const [name, policy] of this.global) {
|
|
369
|
+
const type = policy.type || "dynamic";
|
|
370
|
+
this.priorities[type].push([name, policy]);
|
|
371
|
+
}
|
|
372
|
+
this.priorities.static.sort((a, b) => (b[1].priority || 0) - (a[1].priority || 0));
|
|
373
|
+
this.priorities.dynamic.sort((a, b) => (b[1].priority || 0) - (a[1].priority || 0));
|
|
374
|
+
this.priorities.extensions = Array.from(this.extensions.entries());
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Основная логика оценки политик
|
|
378
|
+
*/
|
|
379
|
+
async evaluate(context, customPolicies = {}) {
|
|
380
|
+
const core = this.abac.core;
|
|
381
|
+
const evaluation = {
|
|
382
|
+
hasForceAllow: false,
|
|
383
|
+
hasForceDisallow: false,
|
|
384
|
+
hasDeny: false,
|
|
385
|
+
denyReason: "",
|
|
386
|
+
allowReason: "",
|
|
387
|
+
appliedPolicies: []
|
|
388
|
+
};
|
|
389
|
+
const processResult = (name, result) => {
|
|
390
|
+
evaluation.appliedPolicies.push({ name, result });
|
|
391
|
+
if (result.force) {
|
|
392
|
+
if (result.allow) {
|
|
393
|
+
evaluation.hasForceAllow = true;
|
|
394
|
+
evaluation.allowReason = result.reason;
|
|
395
|
+
} else {
|
|
396
|
+
evaluation.hasForceDisallow = true;
|
|
397
|
+
evaluation.denyReason = result.reason;
|
|
398
|
+
}
|
|
399
|
+
} else if (!result.allow) {
|
|
400
|
+
evaluation.hasDeny = true;
|
|
401
|
+
if (!evaluation.denyReason) {
|
|
402
|
+
evaluation.denyReason = result.reason;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const checkForceFlags = () => {
|
|
407
|
+
if (evaluation.hasForceDisallow) {
|
|
408
|
+
return {
|
|
409
|
+
allow: false,
|
|
410
|
+
reason: evaluation.denyReason || "FORCE_DENIED_BY_POLICY",
|
|
411
|
+
policies: evaluation.appliedPolicies
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (evaluation.hasForceAllow) {
|
|
415
|
+
return {
|
|
416
|
+
allow: true,
|
|
417
|
+
reason: evaluation.allowReason || "FORCE_ALLOWED_BY_POLICY",
|
|
418
|
+
policies: evaluation.appliedPolicies
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
};
|
|
423
|
+
const staticResults = await core.executePoliciesLimited(
|
|
424
|
+
this.priorities.static,
|
|
425
|
+
context,
|
|
426
|
+
true
|
|
427
|
+
// останавливаемся на deny
|
|
428
|
+
);
|
|
429
|
+
for (const { name, result, error } of staticResults) {
|
|
430
|
+
if (!error) {
|
|
431
|
+
processResult(name, result);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
let forceResult = checkForceFlags();
|
|
435
|
+
if (forceResult) return forceResult;
|
|
436
|
+
const dynamicPolicies = [...this.priorities.dynamic];
|
|
437
|
+
for (const [name, fn] of Object.entries(customPolicies)) {
|
|
438
|
+
dynamicPolicies.push([name, { fn, type: "dynamic" }]);
|
|
439
|
+
}
|
|
440
|
+
const dynamicResults = await core.executePoliciesLimited(
|
|
441
|
+
dynamicPolicies,
|
|
442
|
+
context,
|
|
443
|
+
false
|
|
444
|
+
);
|
|
445
|
+
for (const { name, result, error } of dynamicResults) {
|
|
446
|
+
if (!error) {
|
|
447
|
+
processResult(name, result);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
forceResult = checkForceFlags();
|
|
451
|
+
if (forceResult) return forceResult;
|
|
452
|
+
const resourcePolicy = this.resources.get(context.resource);
|
|
453
|
+
if (resourcePolicy) {
|
|
454
|
+
const results = await core.executePoliciesLimited(
|
|
455
|
+
[[`RESOURCE_${context.resource}`, resourcePolicy]],
|
|
456
|
+
context
|
|
457
|
+
);
|
|
458
|
+
if (!results[0].error) {
|
|
459
|
+
processResult(`RESOURCE_${context.resource}`, results[0].result);
|
|
460
|
+
}
|
|
461
|
+
forceResult = checkForceFlags();
|
|
462
|
+
if (forceResult) return forceResult;
|
|
463
|
+
}
|
|
464
|
+
if (evaluation.hasDeny) {
|
|
465
|
+
return {
|
|
466
|
+
allow: false,
|
|
467
|
+
reason: evaluation.denyReason || "DENIED_BY_POLICY",
|
|
468
|
+
policies: evaluation.appliedPolicies
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const extensionResults = await core.executePoliciesLimited(
|
|
472
|
+
this.priorities.extensions,
|
|
473
|
+
context
|
|
474
|
+
);
|
|
475
|
+
for (const { name, result } of extensionResults) {
|
|
476
|
+
processResult(name, result);
|
|
477
|
+
if (result.allow) {
|
|
478
|
+
return {
|
|
479
|
+
allow: true,
|
|
480
|
+
reason: result.reason,
|
|
481
|
+
policies: evaluation.appliedPolicies
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const defaultAllow = !this.abac.options.defaultDeny;
|
|
486
|
+
return {
|
|
487
|
+
allow: defaultAllow,
|
|
488
|
+
reason: defaultAllow ? "DEFAULT_ALLOW" : "DEFAULT_DENY",
|
|
489
|
+
policies: evaluation.appliedPolicies
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Проверка конкретных политик
|
|
494
|
+
*/
|
|
495
|
+
async checkPolicies(rawContext, policyNames = [], customPolicies = {}) {
|
|
496
|
+
const context = this.abac.core.normalizeContext(rawContext);
|
|
497
|
+
const policies = this.getPoliciesByNames(policyNames, customPolicies);
|
|
498
|
+
const results = await this.abac.core.executePoliciesLimited(
|
|
499
|
+
Object.entries(policies),
|
|
500
|
+
context,
|
|
501
|
+
this.abac.options.strictMode
|
|
502
|
+
);
|
|
503
|
+
const evaluation = {
|
|
504
|
+
passed: [],
|
|
505
|
+
failed: [],
|
|
506
|
+
errors: []
|
|
507
|
+
};
|
|
508
|
+
for (const { name, result, error } of results) {
|
|
509
|
+
if (error) {
|
|
510
|
+
evaluation.errors.push({ name, error: error.message });
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (result.force) {
|
|
514
|
+
return {
|
|
515
|
+
allow: result.allow,
|
|
516
|
+
reason: result.reason,
|
|
517
|
+
evaluation
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (result.allow) {
|
|
521
|
+
evaluation.passed.push(name);
|
|
522
|
+
} else {
|
|
523
|
+
evaluation.failed.push({ name, reason: result.reason });
|
|
524
|
+
if (this.abac.options.strictMode) {
|
|
525
|
+
return {
|
|
526
|
+
allow: false,
|
|
527
|
+
reason: result.reason,
|
|
528
|
+
evaluation
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const allPassed = evaluation.failed.length === 0 && evaluation.errors.length === 0;
|
|
534
|
+
return {
|
|
535
|
+
allow: allPassed,
|
|
536
|
+
reason: allPassed ? "POLICIES_PASSED" : `FAILED: ${evaluation.failed[0]?.name}`,
|
|
537
|
+
evaluation
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Получение политик по именам
|
|
542
|
+
*/
|
|
543
|
+
getPoliciesByNames(names, customPolicies = {}) {
|
|
544
|
+
const policies = {};
|
|
545
|
+
for (const name of names) {
|
|
546
|
+
if (this.global.has(name)) {
|
|
547
|
+
policies[name] = this.global.get(name);
|
|
548
|
+
} else if (this.extensions.has(name)) {
|
|
549
|
+
policies[name] = { fn: this.extensions.get(name), type: "extension" };
|
|
550
|
+
} else if (this.resources.has(name)) {
|
|
551
|
+
policies[name] = this.resources.get(name);
|
|
552
|
+
} else {
|
|
553
|
+
this.abac.logger?.warn("Policy not found", { name });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
Object.assign(policies, customPolicies);
|
|
557
|
+
return policies;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
class ABACFields {
|
|
561
|
+
constructor(abac) {
|
|
562
|
+
this.abac = abac;
|
|
563
|
+
this.configs = /* @__PURE__ */ new Map();
|
|
564
|
+
this.compiledPatterns = /* @__PURE__ */ new Map();
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Регистрация field policies
|
|
568
|
+
*/
|
|
569
|
+
registerFieldsPolicy(resourceName, config) {
|
|
570
|
+
const normalized = {};
|
|
571
|
+
for (const [pattern, conf] of Object.entries(config)) {
|
|
572
|
+
const { actions, ...baseSettings } = conf;
|
|
573
|
+
const base = {
|
|
574
|
+
actions: baseSettings.actions || "*",
|
|
575
|
+
access: baseSettings.access || "allow",
|
|
576
|
+
validator: baseSettings.validator || null,
|
|
577
|
+
transform: baseSettings.transform || null,
|
|
578
|
+
rule: baseSettings.rule || "remove",
|
|
579
|
+
force: baseSettings.force || false,
|
|
580
|
+
pattern
|
|
581
|
+
};
|
|
582
|
+
if (actions && typeof actions === "object") {
|
|
583
|
+
normalized[pattern] = {
|
|
584
|
+
base,
|
|
585
|
+
actions: Object.entries(actions).reduce((acc, [action, override]) => {
|
|
586
|
+
acc[action] = { ...base, ...override };
|
|
587
|
+
return acc;
|
|
588
|
+
}, {})
|
|
589
|
+
};
|
|
590
|
+
} else {
|
|
591
|
+
normalized[pattern] = { base };
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
this.configs.set(resourceName, normalized);
|
|
595
|
+
this.compiledPatterns.delete(resourceName);
|
|
596
|
+
return this;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Проверка доступа к полям
|
|
600
|
+
*/
|
|
601
|
+
async checkFields(context, data, action = null) {
|
|
602
|
+
const normalizedContext = this.abac.core.normalizeContext(context);
|
|
603
|
+
if (normalizedContext.skipFieldPolicies) {
|
|
604
|
+
return {
|
|
605
|
+
allowed: data,
|
|
606
|
+
denied: [],
|
|
607
|
+
errors: [],
|
|
608
|
+
transformed: data
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const { resource } = normalizedContext;
|
|
612
|
+
const fieldAction = action || normalizedContext.action;
|
|
613
|
+
const config = normalizedContext.options?.fieldsConfig || this.configs.get(resource);
|
|
614
|
+
if (!config) {
|
|
615
|
+
return { allowed: data, denied: [], errors: [], transformed: data };
|
|
616
|
+
}
|
|
617
|
+
await this._applyExtensions(normalizedContext);
|
|
618
|
+
const result = {
|
|
619
|
+
allowed: this._deepClone(data),
|
|
620
|
+
denied: [],
|
|
621
|
+
errors: [],
|
|
622
|
+
transformed: null
|
|
623
|
+
};
|
|
624
|
+
const rules = this._collectRulesOptimized(data, config, fieldAction, resource);
|
|
625
|
+
const forced = rules.filter((r) => r.rule.force);
|
|
626
|
+
const regular = rules.filter((r) => !r.rule.force);
|
|
627
|
+
const processed = /* @__PURE__ */ new Set();
|
|
628
|
+
for (const { path, value, rule } of [...forced, ...regular]) {
|
|
629
|
+
if (processed.has(path)) continue;
|
|
630
|
+
processed.add(path);
|
|
631
|
+
const hasAccess = await this._checkFieldAccess(
|
|
632
|
+
rule.access,
|
|
633
|
+
normalizedContext,
|
|
634
|
+
path,
|
|
635
|
+
value
|
|
636
|
+
);
|
|
637
|
+
if (!hasAccess) {
|
|
638
|
+
await this._handleDenied(result, path, rule.rule);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if (rule.validator && rule.access !== "optional") {
|
|
642
|
+
const validation = await this._validateField(
|
|
643
|
+
rule.validator,
|
|
644
|
+
value,
|
|
645
|
+
normalizedContext,
|
|
646
|
+
path
|
|
647
|
+
);
|
|
648
|
+
if (!validation.isValid) {
|
|
649
|
+
result.errors.push({ path, errors: validation.errors });
|
|
650
|
+
if (rule.rule === "error") {
|
|
651
|
+
throw new Error(`Validation failed: ${path}`);
|
|
652
|
+
}
|
|
653
|
+
await this._handleDenied(result, path, rule.rule);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
result.transformed = this._deepClone(result.allowed);
|
|
658
|
+
for (const { path, value, rule } of [...forced, ...regular]) {
|
|
659
|
+
if (!rule.transform) continue;
|
|
660
|
+
const isDenied = result.denied.some((d) => d.path === path);
|
|
661
|
+
if (isDenied) continue;
|
|
662
|
+
const transformed = await this._transformField(
|
|
663
|
+
rule.transform,
|
|
664
|
+
value,
|
|
665
|
+
normalizedContext,
|
|
666
|
+
path,
|
|
667
|
+
result.transformed
|
|
668
|
+
);
|
|
669
|
+
set(result.transformed, path, transformed);
|
|
670
|
+
}
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Применение расширений
|
|
675
|
+
* @private
|
|
676
|
+
*/
|
|
677
|
+
async _applyExtensions(context) {
|
|
678
|
+
if (!this.abac.policies || !this.abac.policies.priorities.extensions.length) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
for (const [name, extensionFn] of this.abac.policies.priorities.extensions) {
|
|
682
|
+
try {
|
|
683
|
+
await extensionFn(context);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
this.abac.logger?.error("Extension error", { name, error });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Безопасное клонирование
|
|
691
|
+
* @private
|
|
692
|
+
*/
|
|
693
|
+
_deepClone(obj) {
|
|
694
|
+
if (typeof structuredClone === "function") {
|
|
695
|
+
try {
|
|
696
|
+
return structuredClone(obj);
|
|
697
|
+
} catch (e) {
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return cloneDeep(obj);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Оптимизированный сбор правил
|
|
704
|
+
* @private
|
|
705
|
+
*/
|
|
706
|
+
_collectRulesOptimized(data, config, action, resource) {
|
|
707
|
+
const rules = [];
|
|
708
|
+
const compiledPatterns = this._getCompiledPatterns(resource, config);
|
|
709
|
+
const hasWildcard = compiledPatterns.has("*");
|
|
710
|
+
const dataPaths = this._extractPathsOptimized(data);
|
|
711
|
+
if (hasWildcard) {
|
|
712
|
+
const wildcardConfig = config["*"];
|
|
713
|
+
const rule = this._getRuleForAction(wildcardConfig, action);
|
|
714
|
+
if (this._matchesAction(rule.actions, action)) {
|
|
715
|
+
for (const path of dataPaths) {
|
|
716
|
+
rules.push({
|
|
717
|
+
path,
|
|
718
|
+
value: get(data, path),
|
|
719
|
+
rule
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return rules;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
for (const path of dataPaths) {
|
|
726
|
+
for (const [pattern, matcher] of compiledPatterns) {
|
|
727
|
+
if (pattern === "*") continue;
|
|
728
|
+
if (matcher(path)) {
|
|
729
|
+
const fieldConfig = config[pattern];
|
|
730
|
+
const rule = this._getRuleForAction(fieldConfig, action);
|
|
731
|
+
if (this._matchesAction(rule.actions, action)) {
|
|
732
|
+
rules.push({
|
|
733
|
+
path,
|
|
734
|
+
value: get(data, path),
|
|
735
|
+
rule
|
|
736
|
+
});
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return rules;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Получение скомпилированных паттернов
|
|
746
|
+
* @private
|
|
747
|
+
*/
|
|
748
|
+
_getCompiledPatterns(resource, config) {
|
|
749
|
+
if (!this.compiledPatterns.has(resource)) {
|
|
750
|
+
const compiled = /* @__PURE__ */ new Map();
|
|
751
|
+
for (const pattern of Object.keys(config)) {
|
|
752
|
+
compiled.set(pattern, this._compilePattern(pattern));
|
|
753
|
+
}
|
|
754
|
+
this.compiledPatterns.set(resource, compiled);
|
|
755
|
+
}
|
|
756
|
+
return this.compiledPatterns.get(resource);
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Компиляция паттерна в функцию проверки
|
|
760
|
+
* @private
|
|
761
|
+
*/
|
|
762
|
+
_compilePattern(pattern) {
|
|
763
|
+
if (pattern === "*") return () => true;
|
|
764
|
+
const escaped = pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^.]+").replace(/\[\*\]/g, "\\[\\d+\\]");
|
|
765
|
+
const regex = new RegExp(`^${escaped}$`);
|
|
766
|
+
return (path) => regex.test(path);
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Оптимизированное извлечение путей
|
|
770
|
+
* @private
|
|
771
|
+
*/
|
|
772
|
+
_extractPathsOptimized(obj, prefix = "", paths = []) {
|
|
773
|
+
if (!obj || typeof obj !== "object") return paths;
|
|
774
|
+
if (Array.isArray(obj)) {
|
|
775
|
+
obj.forEach((item, i) => {
|
|
776
|
+
const path = prefix ? `${prefix}[${i}]` : `[${i}]`;
|
|
777
|
+
paths.push(path);
|
|
778
|
+
this._extractPathsOptimized(item, path, paths);
|
|
779
|
+
});
|
|
780
|
+
} else {
|
|
781
|
+
for (const key in obj) {
|
|
782
|
+
if (obj.hasOwnProperty(key)) {
|
|
783
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
784
|
+
paths.push(path);
|
|
785
|
+
if (obj[key] && typeof obj[key] === "object") {
|
|
786
|
+
this._extractPathsOptimized(obj[key], path, paths);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return paths;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Получение правила для действия
|
|
795
|
+
* @private
|
|
796
|
+
*/
|
|
797
|
+
_getRuleForAction(fieldConfig, action) {
|
|
798
|
+
if (fieldConfig.actions && fieldConfig.actions[action]) {
|
|
799
|
+
return fieldConfig.actions[action];
|
|
800
|
+
}
|
|
801
|
+
return fieldConfig.base;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Проверка доступа к полю
|
|
805
|
+
* @private
|
|
806
|
+
*/
|
|
807
|
+
async _checkFieldAccess(access, context, fieldPath, fieldValue) {
|
|
808
|
+
if (access === "allow") return true;
|
|
809
|
+
if (access === "deny") return false;
|
|
810
|
+
if (access === "optional") return true;
|
|
811
|
+
if (typeof access === "function") {
|
|
812
|
+
try {
|
|
813
|
+
return !!await access(context, fieldPath, fieldValue);
|
|
814
|
+
} catch (e) {
|
|
815
|
+
this.abac.logger?.error("Field access check error", {
|
|
816
|
+
field: fieldPath,
|
|
817
|
+
error: e.message
|
|
818
|
+
});
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Валидация поля
|
|
826
|
+
* @private
|
|
827
|
+
*/
|
|
828
|
+
async _validateField(validator, value, context, fieldPath) {
|
|
829
|
+
if (typeof validator === "function") {
|
|
830
|
+
try {
|
|
831
|
+
const result = await validator(value, context, fieldPath);
|
|
832
|
+
if (typeof result === "boolean") {
|
|
833
|
+
return { isValid: result, errors: result ? [] : ["Validation failed"] };
|
|
834
|
+
}
|
|
835
|
+
return result;
|
|
836
|
+
} catch (e) {
|
|
837
|
+
return { isValid: false, errors: [e.message] };
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (validator && validator.validate) {
|
|
841
|
+
return validator.validate(value);
|
|
842
|
+
}
|
|
843
|
+
return { isValid: true, errors: [] };
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Трансформация поля
|
|
847
|
+
* @private
|
|
848
|
+
*/
|
|
849
|
+
async _transformField(transform, value, context, fieldPath, currentData) {
|
|
850
|
+
try {
|
|
851
|
+
return await transform(value, context, fieldPath, currentData);
|
|
852
|
+
} catch (error) {
|
|
853
|
+
this.abac.logger?.error("Field transform error", {
|
|
854
|
+
field: fieldPath,
|
|
855
|
+
error: error.message
|
|
856
|
+
});
|
|
857
|
+
return value;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Обработка отказа
|
|
862
|
+
* @private
|
|
863
|
+
*/
|
|
864
|
+
async _handleDenied(result, path, rule) {
|
|
865
|
+
result.denied.push({ path, reason: rule });
|
|
866
|
+
if (rule === "remove") {
|
|
867
|
+
unset(result.allowed, path);
|
|
868
|
+
} else if (rule === "error") {
|
|
869
|
+
throw new Error(`Access denied: ${path}`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Проверка действия
|
|
874
|
+
* @private
|
|
875
|
+
*/
|
|
876
|
+
_matchesAction(actions, action) {
|
|
877
|
+
if (actions === "*") return true;
|
|
878
|
+
return Array.isArray(actions) ? actions.includes(action) : actions === action;
|
|
879
|
+
}
|
|
880
|
+
// Публичные методы для управления конфигами
|
|
881
|
+
getConfig(resourceName) {
|
|
882
|
+
return this.configs.get(resourceName);
|
|
883
|
+
}
|
|
884
|
+
hasConfig(resourceName) {
|
|
885
|
+
return this.configs.has(resourceName);
|
|
886
|
+
}
|
|
887
|
+
removeConfig(resourceName) {
|
|
888
|
+
this.compiledPatterns.delete(resourceName);
|
|
889
|
+
return this.configs.delete(resourceName);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
class ABACExpressAdapter {
|
|
893
|
+
constructor(abac) {
|
|
894
|
+
this.abac = abac;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Основной middleware - разделен на составные части
|
|
898
|
+
*/
|
|
899
|
+
middleware(resource, action, options = {}) {
|
|
900
|
+
const middlewares = [];
|
|
901
|
+
middlewares.push(this._accessMiddleware(resource, action, options));
|
|
902
|
+
if (options.checkFields) {
|
|
903
|
+
middlewares.push(this._fieldsValidationMiddleware(resource, action, options));
|
|
904
|
+
}
|
|
905
|
+
return middlewares;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Middleware проверки доступа
|
|
909
|
+
* @private
|
|
910
|
+
*/
|
|
911
|
+
_accessMiddleware(resource, action, options = {}) {
|
|
912
|
+
return async (req, res, next) => {
|
|
913
|
+
try {
|
|
914
|
+
const context = this._buildContext(req, resource, action, options);
|
|
915
|
+
const accessResult = await this.abac.checkAccess(context, context.customPolicies);
|
|
916
|
+
if (!accessResult.allow) {
|
|
917
|
+
return this._sendAccessDenied(res, accessResult.reason);
|
|
918
|
+
}
|
|
919
|
+
req.abacContext = context;
|
|
920
|
+
req.abacAccessResult = accessResult;
|
|
921
|
+
next();
|
|
922
|
+
} catch (error) {
|
|
923
|
+
this.abac.logger?.error("ABAC middleware error", {
|
|
924
|
+
resource,
|
|
925
|
+
action,
|
|
926
|
+
error: error.message,
|
|
927
|
+
stack: error.stack
|
|
928
|
+
});
|
|
929
|
+
return this._sendError(res);
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Middleware проверки полей
|
|
935
|
+
* @private
|
|
936
|
+
*/
|
|
937
|
+
_fieldsValidationMiddleware(resource, action, options = {}) {
|
|
938
|
+
return async (req, res, next) => {
|
|
939
|
+
try {
|
|
940
|
+
const context = req.abacContext || this._buildContext(req, resource, action, options);
|
|
941
|
+
const fieldsResult = await this.abac.checkFields(context, req.body, action);
|
|
942
|
+
if (fieldsResult.errors.length > 0 && options.strictFieldsMode) {
|
|
943
|
+
return this._sendFieldError(res, fieldsResult.errors);
|
|
944
|
+
}
|
|
945
|
+
req.body = fieldsResult.allowed;
|
|
946
|
+
req.abacFieldsResult = fieldsResult;
|
|
947
|
+
next();
|
|
948
|
+
} catch (error) {
|
|
949
|
+
this.abac.logger?.error("Fields middleware error", {
|
|
950
|
+
resource,
|
|
951
|
+
action,
|
|
952
|
+
error: error.message
|
|
953
|
+
});
|
|
954
|
+
if (options.passErrors) {
|
|
955
|
+
next(error);
|
|
956
|
+
} else {
|
|
957
|
+
return this._sendError(res);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Построение контекста
|
|
964
|
+
* @private
|
|
965
|
+
*/
|
|
966
|
+
_buildContext(req, resource, action, options = {}) {
|
|
967
|
+
return {
|
|
968
|
+
user: req.userId,
|
|
969
|
+
resource,
|
|
970
|
+
action,
|
|
971
|
+
req,
|
|
972
|
+
// НЕ смешиваем body и query - передаем структурированно
|
|
973
|
+
data: {
|
|
974
|
+
body: req.body || {},
|
|
975
|
+
query: req.query || {},
|
|
976
|
+
params: req.params || {}
|
|
977
|
+
},
|
|
978
|
+
params: req.params,
|
|
979
|
+
customPolicies: options.policies || {},
|
|
980
|
+
options
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Policy middleware
|
|
985
|
+
*/
|
|
986
|
+
policyMiddleware(policyNames = [], customPolicies = {}, options = {}) {
|
|
987
|
+
return async (req, res, next) => {
|
|
988
|
+
try {
|
|
989
|
+
const context = this._buildContext(
|
|
990
|
+
req,
|
|
991
|
+
options.resource || "custom",
|
|
992
|
+
options.action || "access",
|
|
993
|
+
options
|
|
994
|
+
);
|
|
995
|
+
const result = await this.abac.checkPolicies(context, policyNames, customPolicies);
|
|
996
|
+
if (!result.allow) {
|
|
997
|
+
return this._sendPolicyDenied(res, result.reason);
|
|
998
|
+
}
|
|
999
|
+
req.abacPolicyResult = result;
|
|
1000
|
+
next();
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
this.abac.logger?.error("Policy middleware error", {
|
|
1003
|
+
policies: policyNames,
|
|
1004
|
+
error: error.message
|
|
1005
|
+
});
|
|
1006
|
+
return this._sendError(res);
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Fields middleware для проверки/трансформации полей
|
|
1012
|
+
*/
|
|
1013
|
+
fieldsMiddleware(resource, options = {}) {
|
|
1014
|
+
return async (req, res, next) => {
|
|
1015
|
+
try {
|
|
1016
|
+
const action = options.action || this._getActionFromMethod(req.method);
|
|
1017
|
+
const context = this._buildContext(req, resource, action, options);
|
|
1018
|
+
let dataToCheck = this._getDataToCheck(req, res, options);
|
|
1019
|
+
const fieldsResult = await this.abac.checkFields(context, dataToCheck, action);
|
|
1020
|
+
if (fieldsResult.errors.length > 0) {
|
|
1021
|
+
if (options.strictMode) {
|
|
1022
|
+
return this._sendFieldError(res, fieldsResult.errors);
|
|
1023
|
+
}
|
|
1024
|
+
this.abac.logger?.warn("Field validation errors", {
|
|
1025
|
+
resource,
|
|
1026
|
+
action,
|
|
1027
|
+
errors: fieldsResult.errors
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
this._applyFieldsResult(req, res, options, fieldsResult);
|
|
1031
|
+
req.abacFieldsResult = fieldsResult;
|
|
1032
|
+
next();
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
this.abac.logger?.error("Fields middleware error", {
|
|
1035
|
+
resource,
|
|
1036
|
+
error: error.message
|
|
1037
|
+
});
|
|
1038
|
+
if (options.passErrors) {
|
|
1039
|
+
next(error);
|
|
1040
|
+
} else {
|
|
1041
|
+
return this._sendError(res);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Получение action из HTTP метода
|
|
1048
|
+
* @private
|
|
1049
|
+
*/
|
|
1050
|
+
_getActionFromMethod(method) {
|
|
1051
|
+
const methodActionMap = {
|
|
1052
|
+
"GET": "read",
|
|
1053
|
+
"POST": "create",
|
|
1054
|
+
"PUT": "update",
|
|
1055
|
+
"PATCH": "update",
|
|
1056
|
+
"DELETE": "delete"
|
|
1057
|
+
};
|
|
1058
|
+
return methodActionMap[method] || "access";
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Определение данных для проверки
|
|
1062
|
+
* @private
|
|
1063
|
+
*/
|
|
1064
|
+
_getDataToCheck(req, res, options) {
|
|
1065
|
+
if (req.method === "GET" && options.checkQuery) {
|
|
1066
|
+
return req.query;
|
|
1067
|
+
}
|
|
1068
|
+
if (options.checkResponse && res.locals.data) {
|
|
1069
|
+
return res.locals.data;
|
|
1070
|
+
}
|
|
1071
|
+
return req.body;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Применение результатов проверки полей
|
|
1075
|
+
* @private
|
|
1076
|
+
*/
|
|
1077
|
+
_applyFieldsResult(req, res, options, fieldsResult) {
|
|
1078
|
+
if (req.method === "GET" && options.checkQuery) {
|
|
1079
|
+
req.query = fieldsResult.allowed;
|
|
1080
|
+
} else if (options.checkResponse && res.locals.data) {
|
|
1081
|
+
res.locals.data = fieldsResult.transformed || fieldsResult.allowed;
|
|
1082
|
+
} else {
|
|
1083
|
+
req.body = fieldsResult.allowed;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Отправка ошибок - унифицированные методы
|
|
1088
|
+
* @private
|
|
1089
|
+
*/
|
|
1090
|
+
_sendAccessDenied(res, reason) {
|
|
1091
|
+
return res.status(403).json({
|
|
1092
|
+
error: {
|
|
1093
|
+
code: "ACCESS_DENIED",
|
|
1094
|
+
message: reason
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
_sendPolicyDenied(res, reason) {
|
|
1099
|
+
return res.status(403).json({
|
|
1100
|
+
error: {
|
|
1101
|
+
code: "POLICY_DENIED",
|
|
1102
|
+
message: reason
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
_sendFieldError(res, errors) {
|
|
1107
|
+
return res.status(400).json({
|
|
1108
|
+
error: {
|
|
1109
|
+
code: "FIELD_VALIDATION_ERROR",
|
|
1110
|
+
fields: errors
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
_sendError(res) {
|
|
1115
|
+
return res.status(500).json({
|
|
1116
|
+
error: {
|
|
1117
|
+
code: "INTERNAL_ERROR",
|
|
1118
|
+
message: "Access control error"
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
class ABACWebSocketAdapter {
|
|
1124
|
+
constructor(abac) {
|
|
1125
|
+
this.abac = abac;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* WebSocket handler для проверки доступа
|
|
1129
|
+
* @param {string} moduleName - Имя модуля
|
|
1130
|
+
* @param {Object} [options] - Опции
|
|
1131
|
+
* @returns {Function} Handler функция
|
|
1132
|
+
*/
|
|
1133
|
+
handler(moduleName, options = {}) {
|
|
1134
|
+
return async (ws, message) => {
|
|
1135
|
+
try {
|
|
1136
|
+
const { action = "access", resource = moduleName } = options;
|
|
1137
|
+
const context = this._buildContext(ws, resource, action, message, options);
|
|
1138
|
+
const accessResult = await this.abac.checkAccess(context, context.customPolicies);
|
|
1139
|
+
if (!accessResult.allow) {
|
|
1140
|
+
await this._sendError(ws, "ACCESS_DENIED", accessResult.reason);
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
if (options.checkFields && message) {
|
|
1144
|
+
const fieldsResult = await this.abac.checkFields(context, message, action);
|
|
1145
|
+
if (fieldsResult.errors.length > 0 && options.strictFieldsMode) {
|
|
1146
|
+
await this._sendError(ws, "FIELD_VALIDATION_ERROR", "Field validation failed", {
|
|
1147
|
+
fields: fieldsResult.errors
|
|
1148
|
+
});
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
return {
|
|
1152
|
+
allowed: true,
|
|
1153
|
+
data: fieldsResult.transformed || fieldsResult.allowed,
|
|
1154
|
+
fieldsResult
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
return true;
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
this.abac.logger?.error("WebSocket access control error", {
|
|
1160
|
+
module: moduleName,
|
|
1161
|
+
error: error.message,
|
|
1162
|
+
stack: error.stack
|
|
1163
|
+
});
|
|
1164
|
+
await this._sendError(ws, "INTERNAL_ERROR", "Access control error");
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* RPC handler для вызовов через WebSocket
|
|
1171
|
+
* @param {string} module - Модуль
|
|
1172
|
+
* @param {string} method - Метод
|
|
1173
|
+
* @param {Object} [options] - Опции
|
|
1174
|
+
* @returns {Function} RPC handler
|
|
1175
|
+
*/
|
|
1176
|
+
rpcHandler(module, method, options = {}) {
|
|
1177
|
+
return async (params, rpcContext) => {
|
|
1178
|
+
try {
|
|
1179
|
+
const context = this._buildRPCContext(rpcContext, module, method, params, options);
|
|
1180
|
+
const accessResult = await this.abac.checkAccess(context, context.customPolicies);
|
|
1181
|
+
if (!accessResult.allow) {
|
|
1182
|
+
throw new RPCError("ACCESS_DENIED", accessResult.reason);
|
|
1183
|
+
}
|
|
1184
|
+
if (options.checkFields && params) {
|
|
1185
|
+
const fieldsResult = await this.abac.checkFields(context, params, method);
|
|
1186
|
+
if (fieldsResult.errors.length > 0) {
|
|
1187
|
+
if (options.strictFieldsMode) {
|
|
1188
|
+
throw new RPCError(
|
|
1189
|
+
"FIELD_VALIDATION_ERROR",
|
|
1190
|
+
`Fields validation failed: ${fieldsResult.errors.map((e) => e.path).join(", ")}`
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
this.abac.logger?.warn("RPC field validation errors", {
|
|
1194
|
+
module,
|
|
1195
|
+
method,
|
|
1196
|
+
errors: fieldsResult.errors
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
return fieldsResult.transformed || fieldsResult.allowed;
|
|
1200
|
+
}
|
|
1201
|
+
return params;
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
if (error instanceof RPCError) {
|
|
1204
|
+
throw error;
|
|
1205
|
+
}
|
|
1206
|
+
this.abac.logger?.error("RPC access control error", {
|
|
1207
|
+
module,
|
|
1208
|
+
method,
|
|
1209
|
+
error: error.message,
|
|
1210
|
+
stack: error.stack
|
|
1211
|
+
});
|
|
1212
|
+
throw new RPCError("INTERNAL_ERROR", "Access control error");
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Middleware для обработки WebSocket сообщений
|
|
1218
|
+
* @param {Object} [options] - Опции
|
|
1219
|
+
* @returns {Function} Middleware функция
|
|
1220
|
+
*/
|
|
1221
|
+
messageMiddleware(options = {}) {
|
|
1222
|
+
return async (ws, message, next) => {
|
|
1223
|
+
try {
|
|
1224
|
+
const { resource = "message", action = "send" } = options;
|
|
1225
|
+
const context = this._buildContext(ws, resource, action, message, options);
|
|
1226
|
+
const accessResult = await this.abac.checkAccess(context, options.policies || {});
|
|
1227
|
+
if (!accessResult.allow) {
|
|
1228
|
+
await this._sendError(ws, "ACCESS_DENIED", accessResult.reason);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
ws.abacContext = context;
|
|
1232
|
+
ws.abacAccessResult = accessResult;
|
|
1233
|
+
next();
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
this.abac.logger?.error("WebSocket middleware error", {
|
|
1236
|
+
error: error.message
|
|
1237
|
+
});
|
|
1238
|
+
await this._sendError(ws, "INTERNAL_ERROR", "Access control error");
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Построение контекста для WebSocket
|
|
1244
|
+
* @private
|
|
1245
|
+
*/
|
|
1246
|
+
_buildContext(ws, resource, action, data, options) {
|
|
1247
|
+
return {
|
|
1248
|
+
user: ws.userId || ws.user?.id,
|
|
1249
|
+
resource,
|
|
1250
|
+
action,
|
|
1251
|
+
data: data || {},
|
|
1252
|
+
socket: ws,
|
|
1253
|
+
customPolicies: options.policies || {},
|
|
1254
|
+
options,
|
|
1255
|
+
// Дополнительная информация о соединении
|
|
1256
|
+
connectionInfo: {
|
|
1257
|
+
id: ws.id,
|
|
1258
|
+
remoteAddress: ws._socket?.remoteAddress,
|
|
1259
|
+
protocol: ws.protocol,
|
|
1260
|
+
readyState: ws.readyState
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Построение контекста для RPC
|
|
1266
|
+
* @private
|
|
1267
|
+
*/
|
|
1268
|
+
_buildRPCContext(rpcContext, module, method, params, options) {
|
|
1269
|
+
return {
|
|
1270
|
+
user: rpcContext.userId || rpcContext.user?.id,
|
|
1271
|
+
resource: module,
|
|
1272
|
+
action: method,
|
|
1273
|
+
data: params || {},
|
|
1274
|
+
socket: rpcContext.ws,
|
|
1275
|
+
customPolicies: options.policies || {},
|
|
1276
|
+
options,
|
|
1277
|
+
rpcInfo: {
|
|
1278
|
+
id: rpcContext.id,
|
|
1279
|
+
module,
|
|
1280
|
+
method,
|
|
1281
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Отправка ошибки через WebSocket
|
|
1287
|
+
* @private
|
|
1288
|
+
*/
|
|
1289
|
+
async _sendError(ws, code, message, details = {}) {
|
|
1290
|
+
if (ws.readyState !== 1) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
try {
|
|
1294
|
+
const errorMessage = JSON.stringify({
|
|
1295
|
+
type: "error",
|
|
1296
|
+
error: {
|
|
1297
|
+
code,
|
|
1298
|
+
message,
|
|
1299
|
+
...details
|
|
1300
|
+
},
|
|
1301
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1302
|
+
});
|
|
1303
|
+
ws.send(errorMessage);
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
this.abac.logger?.error("Failed to send WebSocket error", {
|
|
1306
|
+
originalError: { code, message },
|
|
1307
|
+
sendError: error.message
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
class RPCError extends Error {
|
|
1313
|
+
constructor(code, message, details = {}) {
|
|
1314
|
+
super(message);
|
|
1315
|
+
this.name = "RPCError";
|
|
1316
|
+
this.code = code;
|
|
1317
|
+
this.details = details;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
class GlobalABAC {
|
|
1321
|
+
constructor(db, options = {}) {
|
|
1322
|
+
this.db = db;
|
|
1323
|
+
this.options = {
|
|
1324
|
+
strictMode: false,
|
|
1325
|
+
defaultDeny: false,
|
|
1326
|
+
serviceKey: process.env.SERVICE_KEY,
|
|
1327
|
+
enableAudit: true,
|
|
1328
|
+
cacheEnabled: true,
|
|
1329
|
+
cacheTTL: 300,
|
|
1330
|
+
concurrencyLimit: 10,
|
|
1331
|
+
...options
|
|
1332
|
+
};
|
|
1333
|
+
this.cache = new CacheNamespaced({ ttlSeconds: this.options.cacheTTL });
|
|
1334
|
+
this.logger = this.options.logger || new LoggerNamespaced(db);
|
|
1335
|
+
this.core = new ABACCore(this);
|
|
1336
|
+
this.policies = new ABACPolicies(this);
|
|
1337
|
+
this.fields = new ABACFields(this);
|
|
1338
|
+
this.express = new ABACExpressAdapter(this);
|
|
1339
|
+
this.websocket = new ABACWebSocketAdapter(this);
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Регистрация глобальной политики
|
|
1343
|
+
* @param {string} name - Имя политики
|
|
1344
|
+
* @param {Function} policyFn - Функция политики (context) => boolean|{allow, force, reason}
|
|
1345
|
+
* @param {Object} [metadata] - Метаданные политики
|
|
1346
|
+
* @param {string} [metadata.type='dynamic'] - Тип (static|dynamic)
|
|
1347
|
+
* @param {number} [metadata.priority=0] - Приоритет
|
|
1348
|
+
* @returns {GlobalABAC}
|
|
1349
|
+
*/
|
|
1350
|
+
registerGlobalPolicy(name, policyFn, metadata = {}) {
|
|
1351
|
+
return this.policies.registerGlobalPolicy(name, policyFn, metadata);
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Регистрация политики для ресурса
|
|
1355
|
+
* @param {string} resourceName - Имя ресурса
|
|
1356
|
+
* @param {Function} policyFn - Функция политики
|
|
1357
|
+
* @param {Object} [options] - Опции
|
|
1358
|
+
* @param {string} [options.modelName] - Имя модели в БД
|
|
1359
|
+
* @returns {GlobalABAC}
|
|
1360
|
+
*/
|
|
1361
|
+
registerResourcePolicy(resourceName, policyFn, options = {}) {
|
|
1362
|
+
return this.policies.registerResourcePolicy(resourceName, policyFn, options);
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Регистрация политики полей
|
|
1366
|
+
* @param {string} resourceName - Имя ресурса
|
|
1367
|
+
* @param {Object} config - Конфигурация полей
|
|
1368
|
+
* @returns {GlobalABAC}
|
|
1369
|
+
*
|
|
1370
|
+
* @example
|
|
1371
|
+
* abac.registerFieldsPolicy('user', {
|
|
1372
|
+
* 'email': { access: 'deny', actions: ['update'] },
|
|
1373
|
+
* 'password': { access: 'deny', actions: '*', rule: 'remove' },
|
|
1374
|
+
* 'profile.*': {
|
|
1375
|
+
* access: async (ctx) => ctx.user === ctx.currentResource?.id,
|
|
1376
|
+
* transform: (value) => sanitize(value)
|
|
1377
|
+
* }
|
|
1378
|
+
* });
|
|
1379
|
+
*/
|
|
1380
|
+
registerFieldsPolicy(resourceName, config) {
|
|
1381
|
+
return this.fields.registerFieldsPolicy(resourceName, config);
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Регистрация расширения контекста
|
|
1385
|
+
* @param {string} moduleName - Имя модуля
|
|
1386
|
+
* @param {Function} extensionFn - Функция расширения (context) => void
|
|
1387
|
+
* @returns {GlobalABAC}
|
|
1388
|
+
*/
|
|
1389
|
+
registerExtension(moduleName, extensionFn) {
|
|
1390
|
+
return this.policies.registerExtension(moduleName, extensionFn);
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Проверка доступа
|
|
1394
|
+
* @param {ABACContext|Object} context - Контекст проверки
|
|
1395
|
+
* @param {Object} [customPolicies] - Дополнительные политики
|
|
1396
|
+
* @returns {Promise<ABACResult>}
|
|
1397
|
+
*/
|
|
1398
|
+
async checkAccess(context, customPolicies = {}) {
|
|
1399
|
+
return this.core.checkAccess(context, customPolicies);
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Проверка доступа к полям
|
|
1403
|
+
* @param {ABACContext|Object} context - Контекст
|
|
1404
|
+
* @param {Object} data - Данные для проверки
|
|
1405
|
+
* @param {string} [action] - Действие
|
|
1406
|
+
* @returns {Promise<FieldsResult>}
|
|
1407
|
+
*/
|
|
1408
|
+
async checkFields(context, data, action = null) {
|
|
1409
|
+
return this.fields.checkFields(context, data, action);
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Проверка конкретных политик
|
|
1413
|
+
* @param {ABACContext|Object} context - Контекст
|
|
1414
|
+
* @param {string[]} policyNames - Имена политик
|
|
1415
|
+
* @param {Object} [customPolicies] - Дополнительные политики
|
|
1416
|
+
* @returns {Promise<ABACResult>}
|
|
1417
|
+
*/
|
|
1418
|
+
async checkPolicies(context, policyNames = [], customPolicies = {}) {
|
|
1419
|
+
return this.policies.checkPolicies(context, policyNames, customPolicies);
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Express middleware для проверки доступа
|
|
1423
|
+
* @param {string} resource - Ресурс
|
|
1424
|
+
* @param {string} action - Действие
|
|
1425
|
+
* @param {Object} [options] - Опции
|
|
1426
|
+
* @returns {Function|Function[]} Middleware функция(и)
|
|
1427
|
+
*/
|
|
1428
|
+
middleware(resource, action, options = {}) {
|
|
1429
|
+
return this.express.middleware(resource, action, options);
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Express middleware для проверки политик
|
|
1433
|
+
* @param {string[]} policyNames - Имена политик
|
|
1434
|
+
* @param {Object} [customPolicies] - Дополнительные политики
|
|
1435
|
+
* @param {Object} [options] - Опции
|
|
1436
|
+
* @returns {Function} Middleware функция
|
|
1437
|
+
*/
|
|
1438
|
+
policyMiddleware(policyNames = [], customPolicies = {}, options = {}) {
|
|
1439
|
+
return this.express.policyMiddleware(policyNames, customPolicies, options);
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Express middleware для проверки полей
|
|
1443
|
+
* @param {string} resource - Ресурс
|
|
1444
|
+
* @param {Object} [options] - Опции
|
|
1445
|
+
* @returns {Function} Middleware функция
|
|
1446
|
+
*/
|
|
1447
|
+
fieldsMiddleware(resource, options = {}) {
|
|
1448
|
+
return this.express.fieldsMiddleware(resource, options);
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* WebSocket handler
|
|
1452
|
+
* @param {string} moduleName - Имя модуля
|
|
1453
|
+
* @param {Object} [options] - Опции
|
|
1454
|
+
* @returns {Function} Handler функция
|
|
1455
|
+
*/
|
|
1456
|
+
wsHandler(moduleName, options = {}) {
|
|
1457
|
+
return this.websocket.handler(moduleName, options);
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Получение модели ресурса из БД
|
|
1461
|
+
* @param {string} resourceName - Имя ресурса
|
|
1462
|
+
* @returns {Object|null} Mongoose модель или null
|
|
1463
|
+
*/
|
|
1464
|
+
getResourceModel(resourceName) {
|
|
1465
|
+
const resourcePolicy = this.policies.resources.get(resourceName);
|
|
1466
|
+
if (!resourcePolicy) return null;
|
|
1467
|
+
const modelName = resourcePolicy.modelName || resourceName;
|
|
1468
|
+
return this.db[modelName] || null;
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Очистка кэша
|
|
1472
|
+
* @param {Object} [options] - Опции очистки
|
|
1473
|
+
* @param {string} [options.user] - Очистить для пользователя
|
|
1474
|
+
* @param {string} [options.resource] - Очистить для ресурса
|
|
1475
|
+
* @param {string} [options.policy] - Очистить для политики
|
|
1476
|
+
*/
|
|
1477
|
+
async clearCache(options = {}) {
|
|
1478
|
+
if (options.user) {
|
|
1479
|
+
await this.cache.invalidateTag(`user_${options.user}`);
|
|
1480
|
+
}
|
|
1481
|
+
if (options.resource) {
|
|
1482
|
+
await this.cache.invalidateTag(`resource_${options.resource}`);
|
|
1483
|
+
}
|
|
1484
|
+
if (options.policy) {
|
|
1485
|
+
await this.cache.invalidateTag(`policy_${options.policy}`);
|
|
1486
|
+
}
|
|
1487
|
+
if (!options.user && !options.resource && !options.policy) {
|
|
1488
|
+
await this.cache.clear();
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Получение статистики
|
|
1493
|
+
* @returns {Object} Статистика системы
|
|
1494
|
+
*/
|
|
1495
|
+
getStats() {
|
|
1496
|
+
return {
|
|
1497
|
+
policies: {
|
|
1498
|
+
global: this.policies.global.size,
|
|
1499
|
+
resources: this.policies.resources.size,
|
|
1500
|
+
extensions: this.policies.extensions.size
|
|
1501
|
+
},
|
|
1502
|
+
fields: {
|
|
1503
|
+
configs: this.fields.configs.size
|
|
1504
|
+
},
|
|
1505
|
+
cache: {
|
|
1506
|
+
size: this.cache.size,
|
|
1507
|
+
hits: this.cache.hits,
|
|
1508
|
+
misses: this.cache.misses
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
let instance = null;
|
|
1514
|
+
const getInstance = (db, options) => {
|
|
1515
|
+
if (!instance) {
|
|
1516
|
+
instance = new GlobalABAC(db, options);
|
|
1517
|
+
}
|
|
1518
|
+
return instance;
|
|
1519
|
+
};
|
|
1520
|
+
const resetInstance = () => {
|
|
1521
|
+
instance = null;
|
|
1522
|
+
};
|
|
1523
|
+
const coreabac = { getInstance, resetInstance };
|
|
1524
|
+
export {
|
|
1525
|
+
LoggerNamespaced as L,
|
|
1526
|
+
coreabac as c
|
|
1527
|
+
};
|