diff --git a/example.env b/example.env index a6bf148d..84f37c3f 100644 --- a/example.env +++ b/example.env @@ -153,6 +153,48 @@ # По умолчанию кластер выключен (off). # VUE_APP_DOCHUB_CLUSTER= on / off +# *********************************************************** +# Пример конфигурирования ролевой модели +# *********************************************************** +# (B) Режим backend +# Если "y", то включается режим работы с ролевой модель. +# VUE_APP_DOCHUB_ROLES_MODEL= y / n +# Указываем путь до манифеста с описанием ролей +# Пример описания: +# roles: +# users: +# - '^kadzo.v2023.data_objects*$' +# - '^ecogroup.berezka.data_objects[a-zA-Z\._0-9]*$' +# uek: +# - '^kadzo\.v2023\.kb_systems[a-zA-Z\._0-9]*$' +# - '^ecogroup.berezka.kb[a-zA-Z\._0-9]*$' +# default: +# - '^kadzo\.v2023\.data_objects*$' +#VUE_APP_DOCHUB_ROLES=file:///workspace/sberauto/roles.yaml + +# Варианты использования: +# documentation/roles.yaml - (F / FB) Относительная ссылка на файл расположенный в папке @/public/ +# https://dochub.info/documentation/roles.yaml - (F / FB) прямая ссылка на внешний http/https ресурс +# gitlab:34:main@roles.yaml - Прямая ссылка на на файл в GitLab репозитории. +# (F) Может использоваться только при указании VUE_APP_DOCHUB_PERSONAL_TOKEN или VUE_APP_DOCHUB_GITLAB_URL + VUE_APP_DOCHUB_APP_ID +# (FB) Может использоваться только при указании VUE_APP_DOCHUB_PERSONAL_TOKEN +# Структура ссылки: +# gitlab - протокол GitLab +# 34 - идентификатор репозитория +# main - бранч +# root.yaml - путь к файлу +# +# file://roles.yaml - (FB) Прямая ссылка на файл в хранилище VUE_APP_DOCHUB_BACKEND_FILE_STORAGE. +# Если VUE_APP_DOCHUB_BACKEND_FILE_STORAGE не задан, то ./public/* +# bitbucket:myproject:myrepo:mybranch@roles.yaml Прямая ссылка на на файл в BitBucket репозитории. +# (F/FB) Может использоваться только при указании VUE_APP_DOCHUB_PERSONAL_TOKEN +# Указываем путь до сервера аутентификации с указанием realms +#VUE_APP_DOCHUB_AUTHORITY_SERVER=https://auth.slsdev.ru/realms/dochub +# Указываем client id, например: dochub +#VUE_APP_DOCHUB_AUTHORITY_CLIENT_ID={CLIENTID} +# {PUBLIC KEY} - указываем public key, смотрим настройки keycloack +#VUE_APP_DOCHUB_AUTH_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----{PUBLIC KEY}-----END PUBLIC KEY----- + # (FB) Включение поддержки HTML тэгов в markdown документах # По умолчанию поддержка выключена (off). # VUE_APP_DOCHUB_MARKDOWN_HTML= on / off diff --git a/oidc-settings.js b/oidc-settings.js new file mode 100644 index 00000000..8c75f4e1 --- /dev/null +++ b/oidc-settings.js @@ -0,0 +1,22 @@ +import {Log, UserManager} from 'oidc-client-ts'; + +Log.setLogger(console); +Log.setLevel(Log.ERROR); + +const url = window.location.origin; + +export const settings = { + authority: process.env.VUE_APP_DOCHUB_AUTHORITY_SERVER, + client_id: process.env.VUE_APP_DOCHUB_AUTHORITY_CLIENT_ID, + redirect_uri: new URL('/login', url), + post_logout_redirect_uri: new URL('/logout', url), + response_type: 'code', + scope: 'openid', + response_mode: 'fragment', + automaticSilentRenew: true +}; + +export { + Log, + UserManager +}; diff --git a/package-lock.json b/package-lock.json index de85c1c2..67007c83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,10 +20,12 @@ "core-js": "3.26.1", "dateformat": "3.0.3", "jsonata": "2.0.3", + "jsrsasign": "10.8.6", "md5": "2.3.0", "mermaid": "10.6.1", "monaco-editor": "0.34.1", "mustache": "4.2.0", + "oidc-client-ts": "2.4.0", "semver": "7.5.4", "stream": "^0.0.2", "swagger-ui": "3.52.5", @@ -9497,6 +9499,11 @@ "node": "*" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", @@ -17667,6 +17674,14 @@ "node": ">=0.6.0" } }, + "node_modules/jsrsasign": { + "version": "10.8.6", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.8.6.tgz", + "integrity": "sha512-bQmbVtsfbgaKBTWCKiDCPlUPbdlRIK/FzSwT3BzIgZl/cU6TqXu6pZJsCI/dJVrZ9Gir5GC4woqw9shH/v7MBw==", + "funding": { + "url": "https://github.com/kjur/jsrsasign#donations" + } + }, "node_modules/just-diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-5.2.0.tgz", @@ -17679,6 +17694,11 @@ "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", "dev": true }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/katex": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/katex/-/katex-0.6.0.tgz", @@ -20863,6 +20883,18 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "node_modules/oidc-client-ts": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz", + "integrity": "sha512-WijhkTrlXK2VvgGoakWJiBdfIsVGz6CFzgjNNqZU1hPKV2kyeEaJgLs7RwuiSp2WhLfWBQuLvr2SxVlZnk3N1w==", + "dependencies": { + "crypto-js": "^4.2.0", + "jwt-decode": "^3.1.2" + }, + "engines": { + "node": ">=12.13.0" + } + }, "node_modules/omggif": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", @@ -38459,6 +38491,11 @@ "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==" }, + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", @@ -44673,6 +44710,11 @@ "verror": "1.10.0" } }, + "jsrsasign": { + "version": "10.8.6", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.8.6.tgz", + "integrity": "sha512-bQmbVtsfbgaKBTWCKiDCPlUPbdlRIK/FzSwT3BzIgZl/cU6TqXu6pZJsCI/dJVrZ9Gir5GC4woqw9shH/v7MBw==" + }, "just-diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-5.2.0.tgz", @@ -44685,6 +44727,11 @@ "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", "dev": true }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "katex": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/katex/-/katex-0.6.0.tgz", @@ -47117,6 +47164,15 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "oidc-client-ts": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz", + "integrity": "sha512-WijhkTrlXK2VvgGoakWJiBdfIsVGz6CFzgjNNqZU1hPKV2kyeEaJgLs7RwuiSp2WhLfWBQuLvr2SxVlZnk3N1w==", + "requires": { + "crypto-js": "^4.2.0", + "jwt-decode": "^3.1.2" + } + }, "omggif": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", diff --git a/package.json b/package.json index 647ac1d1..e689611f 100644 --- a/package.json +++ b/package.json @@ -39,10 +39,12 @@ "core-js": "3.26.1", "dateformat": "3.0.3", "jsonata": "2.0.3", + "jsrsasign": "10.8.6", "md5": "2.3.0", "mermaid": "10.6.1", "monaco-editor": "0.34.1", "mustache": "4.2.0", + "oidc-client-ts": "2.4.0", "semver": "7.5.4", "stream": "0.0.2", "swagger-ui": "3.52.5", diff --git a/public/documentation/docs/manual/entities/rules.md b/public/documentation/docs/manual/entities/rules.md new file mode 100644 index 00000000..c6b62910 --- /dev/null +++ b/public/documentation/docs/manual/entities/rules.md @@ -0,0 +1,35 @@ +# Ролевая модель + +Ролевая модель - позволяет разграничивать доступы к элементам архитектуры. +Управление ролями пользователей происходит на стороне keycloack. + +Для включения работы с ролевой моделью требуется установить следующий флаг: +VUE_APP_DOCHUB_ROLES_MODEL=y + +Описание ролей осуществляется в корневом файле roles.yaml +Пример описания правил доступа: +``` + roles: + users: + - '^kadzo.v2023.data_objects*$' // Правило представляет собой регулярное выражение + - '^ecogroup.berezka.data_objects[a-zA-Z\._0-9]*$' + uek: + - '^kadzo\.v2023\.kb_systems[a-zA-Z\._0-9]*$' + - '^ecogroup.berezka.kb[a-zA-Z\._0-9]*$' + default: + - '^kadzo\.v2023\.data_objects*$' +``` +VUE_APP_DOCHUB_ROLES=file:///workspace/sberauto/roles.yaml + +Если пользователь имеет несколько ролей, то наборы правил объединяются. + +Для работы ролевой модели требуется дополнительно указать в файле .env несколько параметров: + +``` +Указываем путь до сервера аутентификации с указанием realms +VUE_APP_DOCHUB_AUTHORITY_SERVER=https://dochub-server.ru/realms/dochub +Указываем client id, например: dochub +VUE_APP_DOCHUB_AUTHORITY_CLIENT_ID={CLIENTID} +{PUBLIC KEY} - указываем public key, смотрим настройки keycloack +VUE_APP_DOCHUB_AUTH_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----{PUBLIC KEY}-----END PUBLIC KEY----- +``` diff --git a/public/documentation/docs/manual/root.yaml b/public/documentation/docs/manual/root.yaml index 43e7ee67..3b255aa5 100755 --- a/public/documentation/docs/manual/root.yaml +++ b/public/documentation/docs/manual/root.yaml @@ -130,6 +130,11 @@ docs: subjects: - dochub.front source: config/deployment.md + dochub.rules: + location: DocHub/Руководство/Ролевая модель + description: Ролевая модель + type: markdown + source: entities/rules.md dochub.context.source: type: plantuml source: context_source.puml diff --git a/src/backend/controllers/core.mjs b/src/backend/controllers/core.mjs index 26fcc299..bdf74c1d 100644 --- a/src/backend/controllers/core.mjs +++ b/src/backend/controllers/core.mjs @@ -4,18 +4,31 @@ import cache from '../storage/cache.mjs'; import queries from '../../global/jsonata/queries.mjs'; import helpers from './helpers.mjs'; import compression from '../../global/compress/compress.mjs'; +import {getRoles} from '../helpers/jwt.mjs'; +import {DEFAULT_ROLE, getCurrentRuleId, getCurrentRules, isRolesMode} from "../utils/rules.mjs"; const compressor = compression(); // const LOG_TAG = 'controller-core'; - export default (app) => { // Создает ответ на JSONata запрос и при необходимости кэширует ответ - function makeJSONataQueryResponse(res, query, params, subject) { - cache.pullFromCache(app.storage.hash, JSON.stringify({ query, params, subject }), async() => { + function makeJSONataQueryResponse(res, query, params, subject, ruleId) { + let key; + if(isRolesMode()) { + key = { query, params, subject, ruleId }; + } else { + key = { query, params, subject}; + } + cache.pullFromCache(app.storage.hash, JSON.stringify(key), async() => { + let context; + if(isRolesMode()) { + context = ruleId === '' ? app.storage.manifests[DEFAULT_ROLE] : app.storage.manifests[ruleId]; + } else { + context = app.storage.manifest; + } return await datasets(app).parseSource( - app.storage.manifest, + context, query, subject, params @@ -23,6 +36,15 @@ export default (app) => { }, res); } + function checkRulesManifest(ruleName) { + for(let key in app.storage.manifests) { + if(key === ruleName) { + return true; + } + } + return false; + } + // Парсит переданные во внутреннем формате данные function parseRequest(req) { return { @@ -34,15 +56,22 @@ export default (app) => { } // Выполняет произвольные запросы - app.get('/core/storage/jsonata/:query', function(req, res) { + app.get('/core/storage/jsonata/:query', async function(req, res) { if (!helpers.isServiceReady(app, res)) return; + let id; + if(isRolesMode()) { + const roles = getRoles(req.headers); + id = await getCurrentRuleId(roles); + const currentRules = await getCurrentRules(roles); + app.storage = {...app.storage, roles: [...currentRules], roleId: id}; + } const request = parseRequest(req); const query = (request.query.length === 36) && queries.QUERIES[request.query] ? `(${queries.makeQuery(queries.QUERIES[request.query], request.params)})` : request.query; - makeJSONataQueryResponse(res, query, request.params, request.subject); + makeJSONataQueryResponse(res, query, request.params, request.subject, id); }); // Запрос на обновление манифеста @@ -54,8 +83,11 @@ export default (app) => { }); return; } else { + if(isRolesMode()) { + app.storage = {...app.storage, manifests: null} + } const oldHash = app.storage.hash; - storeManager.reloadManifest() + storeManager.reloadManifest(app) .then((storage) => storeManager.applyManifest(app, storage)) .then(() => cache.clearCache(oldHash)) .then(() => res.json({ message: 'success' })); @@ -63,43 +95,74 @@ export default (app) => { }); // Выполняет произвольные запросы - app.get('/core/storage/release-data-profile/:query', function(req, res) { + app.get('/core/storage/release-data-profile/:query', async function(req, res) { if (!helpers.isServiceReady(app, res)) return; const request = parseRequest(req); - cache.pullFromCache(app.storage.hash, JSON.stringify({ path: request.query, params: request.params }), async() => { - if (request.query.startsWith('/')) - return await datasets(app).releaseData(request.query, request.params); - else { - let profile = null; - const params = request.params; - if (request.query.startsWith('{')) - profile = JSON.parse(request.query); - else - profile = JSON.parse(await compressor.decodeBase64(request.query)); - - const ds = datasets(app); - if (profile.$base) { - const path = ds.pathResolver(profile.$base); - if (!path) { - res.status(400).json({ - error: `Error $base location [${profile.$base}]` - }); - return; + + let storageManifest = app.storage.manifest; + let key = { + path: request.query, + params: request.params + }; + + if(isRolesMode()) { + const roles = getRoles(req.headers); + const id = await getCurrentRuleId(roles); + const currentRules = await getCurrentRules(roles); + app.storage = {...app.storage, roles: [...currentRules], roleId: id}; + storageManifest = app.storage.manifests[id]; + key ={ + path: request.query, + params: request.params, + roles: id + }; + } + + cache.pullFromCache(app.storage.hash, JSON.stringify(key), async () => { + if (request.query.startsWith('/')) + return await datasets(app).releaseData(request.query, request.params); + else { + let profile = null; + const params = request.params; + if (request.query.startsWith('{')) + profile = JSON.parse(request.query); + else + profile = JSON.parse(await compressor.decodeBase64(request.query)); + + const ds = datasets(app); + if (profile.$base) { + const path = ds.pathResolver(profile.$base); + if (!path) { + res.status(400).json({ + error: `Error $base location [${profile.$base}]` + }); + return; + } + return await ds.getData(path.context, profile, params, path.baseURI); + } else { + return await ds.getData(storageManifest, profile, params); } - return await ds.getData(path.context, profile, params, path.baseURI); - } else { - return await ds.getData(app.storage.manifest, profile, params); } - } - }, res); + }, res); }); // Возвращает результат работы валидаторов - app.get('/core/storage/problems/', function(req, res) { + app.get('/core/storage/problems/', async function(req, res) { if (!helpers.isServiceReady(app, res)) return; + + if(isRolesMode()) { + const roles = getRoles(req.headers); + const currentRules = await getCurrentRules(roles); + const id = await getCurrentRuleId(roles); + app.storage = {...app.storage, roles: [...currentRules], roleId: id}; + + if (!checkRulesManifest(id)) { + app.new_rules = currentRules; + await storeManager.createNewManifest(app); + } + } res.json(app.storage.problems || []); }); - }; diff --git a/src/backend/controllers/storage.mjs b/src/backend/controllers/storage.mjs index a8c4b085..007288de 100644 --- a/src/backend/controllers/storage.mjs +++ b/src/backend/controllers/storage.mjs @@ -1,11 +1,19 @@ import logger from '../utils/logger.mjs'; import request from '../helpers/request.mjs'; +import {getRoles} from '../helpers/jwt.mjs'; +import {getCurrentRuleId, getCurrentRules} from "../utils/rules.mjs"; const LOG_TAG = 'controller-storage'; export default (app) => { // Проксирует запрос к хранилищу app.get('/core/storage/:hash/*', async function(req, res) { + + const roles = getRoles(req.headers); + const currentRules = await getCurrentRules(roles); + const id = await getCurrentRuleId(roles); + app.storage = {...app.storage, roles: [...currentRules], roleId: id}; + const hash = req.params.hash || '$unknown$'; const url = req.originalUrl.slice(`/core/storage/${hash}/`.length).replace(/\%E2\%86\%90/g, '..'); //const url = decodeURIComponent(req.params.url); @@ -24,5 +32,4 @@ export default (app) => { })); } }); -}; - +}; \ No newline at end of file diff --git a/src/backend/helpers/datasets.mjs b/src/backend/helpers/datasets.mjs index c0491ac9..16f72736 100644 --- a/src/backend/helpers/datasets.mjs +++ b/src/backend/helpers/datasets.mjs @@ -2,16 +2,28 @@ import request from './request.mjs'; import jsonataDriver from '../../global/jsonata/driver.mjs'; import datasetDriver from '../../global/datasets/driver.mjs'; import pathTool from '../../global/manifest/tools/path.mjs'; +import entities from '../entities/entities.mjs'; +import {isRolesMode, DEFAULT_ROLE} from "../utils/rules.mjs"; import md5 from 'md5'; export default function(app) { + + let currentContext; + + if(isRolesMode()) { + currentContext = app.storage.roleId === DEFAULT_ROLE ? app.storage.manifests[DEFAULT_ROLE] : app.storage.manifests[app.storage.roleId]; + entities(currentContext); + } else { + currentContext = app.storage.manifest; + } + const result = Object.assign({}, datasetDriver, { // Возвращаем метаданных об объекте pathResolver(path) { return { - context: app.storage.manifest, - subject: pathTool.get(app.storage.manifest, path), + context: currentContext, + subject: pathTool.get(currentContext, path), baseURI: app.storage.md5Map[md5(path)] }; }, diff --git a/src/backend/helpers/env.mjs b/src/backend/helpers/env.mjs index 9bf6ef5a..a905a4c8 100644 --- a/src/backend/helpers/env.mjs +++ b/src/backend/helpers/env.mjs @@ -21,4 +21,9 @@ global.$listeners = { onFoundLoadingError: process.env.VUE_APP_DOCHUB_BACKEND_EVENT_LOADING_ERRORS_FOUND }; +global.$roles = { + MODE: process.env.VUE_APP_DOCHUB_ROLES_MODEL, + URI: process.env.VUE_APP_DOCHUB_ROLES +} + export default dotenv; diff --git a/src/backend/helpers/jwt.mjs b/src/backend/helpers/jwt.mjs new file mode 100644 index 00000000..1d5a6706 --- /dev/null +++ b/src/backend/helpers/jwt.mjs @@ -0,0 +1,24 @@ +import { KJUR } from "jsrsasign"; + +export function getRoles(headers) { + console.log('headers', headers); + const jwt = headers?.authorization?.slice(7); + + if (!!jwt && typeof jwt === "string" && !jwt.includes('undefined')) { + try { + console.log('headers.authorization', jwt); + console.log('KJUR.jws.JWS.parse(jwt)',KJUR.jws.JWS.parse(jwt)); + if(KJUR.jws.JWS.verifyJWT(jwt, process.env.VUE_APP_DOCHUB_AUTH_PUBLIC_KEY, {alg: ['RS256']})) { + return KJUR.jws.JWS.parse(jwt)?.payloadObj?.realm_access?.roles || []; + } else { + console.warn(`Verification error: jwt: ${jwt}`); + } + } catch (e) { + console.error('Error getting user groups!'); + // eslint-disable-next-line no-console + console.error(e); + return []; + } + } + return []; +} diff --git a/src/backend/helpers/validators.mjs b/src/backend/helpers/validators.mjs index 47cdf1fa..d130cff9 100644 --- a/src/backend/helpers/validators.mjs +++ b/src/backend/helpers/validators.mjs @@ -1,6 +1,7 @@ import validators from '../../global/rules/validators.mjs'; import datasets from './datasets.mjs'; import logger from '../utils/logger.mjs'; +import {isRolesMode} from "../utils/rules.mjs"; const LOG_TAG = 'validators'; @@ -11,6 +12,12 @@ export default function(app) { app.storage.problems.push(validator); }; logger.log('Executing validators..', LOG_TAG); - validators(datasets(app), app.storage.manifest, pushValidator, pushValidator); + + let storageManifest = app.storage.manifest; + if(isRolesMode()) { + storageManifest = app.storage.manifests[app.storage.roleId]; + } + validators(datasets(app), storageManifest, pushValidator, pushValidator); logger.log('Done.', LOG_TAG); + } diff --git a/src/backend/main.mjs b/src/backend/main.mjs index e04f303b..f1c704d7 100644 --- a/src/backend/main.mjs +++ b/src/backend/main.mjs @@ -25,31 +25,33 @@ middlewareAccess(app); // Основной цикл приложения const mainLoop = async function() { // Загружаем манифест - app.listen(serverPort, function(){ + const server = app.listen(serverPort, function(){ logger.log(`DocHub server running on ${serverPort}`, LOG_TAG); }); - storeManager.reloadManifest() - .then(async(storage) => { - await storeManager.applyManifest(app, storage); - // Подключаем драйвер кластера - await middlewareCluster(app, storeManager); + server.setTimeout(500000); - // Подключаем сжатие контента - middlewareCompression(app); + storeManager.reloadManifest(app) + .then(async(storage) => { + await storeManager.applyManifest(app, storage); + // Подключаем драйвер кластера + await middlewareCluster(app, storeManager); - // API ядра - controllerCore(app); + // Подключаем сжатие контента + middlewareCompression(app); - // API сущностей - controllerEntity(app); + // API ядра + controllerCore(app); - // Контроллер доступа к файлам в хранилище - controllerStorage(app); + // API сущностей + controllerEntity(app); - // Статические ресурсы - controllerStatic(app); - }); + // Контроллер доступа к файлам в хранилище + controllerStorage(app); + + // Статические ресурсы + controllerStatic(app); + }); }; mainLoop(); diff --git a/src/backend/middlewares/cluster.mjs b/src/backend/middlewares/cluster.mjs index c00d313c..5813b7f1 100644 --- a/src/backend/middlewares/cluster.mjs +++ b/src/backend/middlewares/cluster.mjs @@ -30,7 +30,7 @@ export default async(app, storeManager) => { if (remoteHash !== app.storage?.hash) { logger.log(`Cluster inconsistency detected. Current hash is [${app.storage?.hash}], remote hash is [${remoteHash}]. Reloading manifest...`, LOG_TAG); storeManager.cleanStorage(app); - storeManager.reloadManifest() + storeManager.reloadManifest(app) .then(async(storage) => { await storeManager.applyManifest(app, storage); logger.log(`Reloading complete. Current hash is [${app.storage.hash}], remote hash is [${remoteHash}].`, LOG_TAG); diff --git a/src/backend/storage/cache.mjs b/src/backend/storage/cache.mjs index bbd804ea..f92b19a9 100644 --- a/src/backend/storage/cache.mjs +++ b/src/backend/storage/cache.mjs @@ -124,6 +124,8 @@ export default Object.assign(prototype, { } if (res) { + console.log('__dirname', __dirname); + console.log('fileName', fileName); if (fileName) { res.setHeader('Content-Type', 'application/json').sendFile(fileName); } else res.status(200).json(result); diff --git a/src/backend/storage/manager.mjs b/src/backend/storage/manager.mjs index 0f7cb689..843fc994 100644 --- a/src/backend/storage/manager.mjs +++ b/src/backend/storage/manager.mjs @@ -6,8 +6,11 @@ import events from '../helpers/events.mjs'; import validators from '../helpers/validators.mjs'; import entities from '../entities/entities.mjs'; import objectHash from 'object-hash'; +import '../helpers/env.mjs'; + import jsonataDriver from '../../global/jsonata/driver.mjs'; import jsonataFunctions from '../../global/jsonata/functions.mjs'; +import {newManifest, loader, isRolesMode, DEFAULT_ROLE} from "../utils/rules.mjs"; const LOG_TAG = 'storage-manager'; @@ -39,29 +42,97 @@ export default { this.cacheFunction = jsonataFunctions(jsonataDriver, storage.functions || {}); return this.cacheFunction; }; - + }, // Стек обработчиков события на обновление манифеста onApplyManifest: [], - reloadManifest: async function() { + createNewManifest: async function(app) { + if(app.new_rules) { + + let mergeRules = []; + const ids = []; + + const {URI} = global.$roles; + const url = new URL(URI); + const defaultUrl = new URL( 'default.yaml', URI); + const defaultRoles = await loader(defaultUrl); + const systemRules = defaultRoles?.roles; + const exclude = defaultRoles?.exclude; + + const manifest = await loader(url); + + for(let rule in app.new_rules) { + for(let nRule in manifest?.roles) { + if(app.new_rules[rule] === nRule) { + mergeRules = mergeRules.concat(manifest?.roles[nRule]); + ids.push(nRule) + } + } + } + + const id = ids.sort((a,b) => {return a.localeCompare(b);}).join(''); + + const filters = systemRules.concat(mergeRules); + app.storage.manifests[id] = newManifest(app.storage.manifests.origin, exclude, filters); + } + }, + reloadManifest: async function(app) { + logger.log('Run full reload manifest', LOG_TAG); // Загрузку начинаем с виртуального манифеста cache.errorClear(); - await manifestParser.clean(); - await manifestParser.startLoad(); - await manifestParser.import('file:///$root$'); - await manifestParser.checkAwaitedPackages(); - await manifestParser.checkLoaded(); - await manifestParser.stopLoad(); + let storageManifest = {}; + let createManifest = async function() { + await manifestParser.clean(); + await manifestParser.startLoad(); + await manifestParser.import('file:///$root$'); + await manifestParser.checkAwaitedPackages(); + await manifestParser.checkLoaded(); + await manifestParser.stopLoad(); + }; + + let createRoleManifest = async function () { + try { + // загружаю основной файл с ролями + const {URI} = global.$roles; + const url = new URL(URI); + const manifest = await loader(url); + // загружаю правила по умолчанию + const defaultUrl = new URL('default.yaml', URI); + const defaultRoles = await loader(defaultUrl); + const systemRules = defaultRoles?.roles; + const exclude = defaultRoles?.exclude; + + for (const role in manifest?.roles) { + const filters = systemRules.concat(manifest?.roles[role]); + storageManifest.manifests[role] = newManifest(storageManifest.manifests.origin, exclude, filters); + } + } catch (e) { + this.registerError(e, e.uri || uri); + } + } + + await createManifest(); + + let baseManifest = manifestParser.manifest; + + if(isRolesMode()) { + storageManifest.manifests = {origin: baseManifest}; + Object.freeze(storageManifest.manifests.origin); + await createRoleManifest(); + baseManifest = storageManifest.manifests[DEFAULT_ROLE]; + } - entities(manifestParser.manifest); + entities(baseManifest); logger.log('Full reload is done', LOG_TAG); const result = { - manifest: manifestParser.manifest, // Сформированный манифест - hash: objectHash(manifestParser.manifest), // HASH состояния для контроля в кластере + manifest: baseManifest, // Сформированный манифест + hash: objectHash(baseManifest), // HASH состояния для контроля в кластере mergeMap: {}, // Карта склейки объектов - md5Map: {}, // Карта путей к ресурсам по md5 пути + md5Map: {}, // Карта путей к ресурсам по md5 пути + manifests: {...storageManifest.manifests}, + roleId: DEFAULT_ROLE, // Ошибки, которые возникли при загрузке манифестов // по умолчанию заполняем ошибками, которые возникли при загрузке problems: Object.keys(cache.errors || {}).map((key) => cache.errors[key]) || [] @@ -85,6 +156,7 @@ export default { applyManifest: async function(app, storage) { app.storage = storage; // Инициализируем данные хранилища this.resetCustomFunctions(storage.manifest); + app.storage.roles = []; validators(app); // Выполняет валидаторы Object.freeze(app.storage); this.onApplyManifest.map((listener) => listener(app)); diff --git a/src/backend/utils/rules.mjs b/src/backend/utils/rules.mjs new file mode 100644 index 00000000..4d9106d2 --- /dev/null +++ b/src/backend/utils/rules.mjs @@ -0,0 +1,99 @@ +import cache from "../storage/cache.mjs"; + +export const DEFAULT_ROLE = 'default'; + +export async function getCurrentRuleId(rules) { + if(rules.length === 0) return 'default'; + + const ids = []; + + const {URI} = global.$roles; + const url = new URL(URI); + const response = await cache.request(url, '/'); + + const manifest = response && (typeof response.data === 'object' + ? response.data + : JSON.parse(response.data)); + + + for(let rule in rules) { + for(let nRule in manifest?.roles) { + if(rules[rule] === nRule) { + ids.push(nRule) + } + } + } + + return ids.sort((a,b) => {return a.localeCompare(b);}).join(''); +} + +export async function getCurrentRules(rules) { + if(rules.length === 0) return []; + + const result = []; + const {URI} = global.$roles; + const url = new URL(URI); + const response = await cache.request(url, '/'); + + const manifest = response && (typeof response.data === 'object' + ? response.data + : JSON.parse(response.data)); + + for(let rule in rules) { + for(let nRule in manifest?.roles) { + if(rules[rule] === nRule) { + result.push(nRule); + } + } + } + return result; +} + +export const newManifest = (obj, exclude, filters)=> Object.entries(obj) + .filter(([key, value]) => (typeof value != 'object' || Array.isArray(value) || matchExclude(key, exclude)) || matchRegex(key, filters)) + .reduce((acc, [key, value]) => { + if(value != null && typeof value === 'object' && !Array.isArray(value)) { + acc[key] = newManifest(value, exclude, filters); + return acc; + } + return Array.isArray(obj) ? [...acc, ...obj] : ({...acc, [key]: obj[key]}); + }, {}); + +export async function loader(uri) { + const response = await cache.request(uri, '/'); + return response && (typeof response.data === 'object' + ? response.data + : JSON.parse(response.data)); +} + +export function isRolesMode() { + const {MODE} = global.$roles; + return (MODE || 'N').toUpperCase() === 'Y'; +} + +function matchRegex(string, filters){ + + const len = filters.length; + + for (let i = 0; i < len; i++) { + if (string.match(filters[i])) { + return true; + } + } + return false; +}; + +function matchExclude(string, exclude) { + const len = exclude.length; + + for (let i = 0; i < len; i++) { + if (string === exclude[i]) { + return true; + } + } + return false; +} + + + + diff --git a/src/frontend/auth/oidc-client.js b/src/frontend/auth/oidc-client.js new file mode 100644 index 00000000..b5fea27b --- /dev/null +++ b/src/frontend/auth/oidc-client.js @@ -0,0 +1,35 @@ +export default { + login() { + window.OidcUserManager.signinRedirect() + .then(() => { + // eslint-disable-next-line no-console + console.log('User logged in'); + }) + .catch(error => { + // eslint-disable-next-line no-console + console.error(error); + }); + }, + logout() { + window.OidcUserManager.signoutRedirect() + .then(() => { + // eslint-disable-next-line no-console + console.log('User logged out'); + }) + .catch(error => { + // eslint-disable-next-line no-console + console.error(error); + }); + }, + async signinCallback() { + if (window.location.hash) { + await window.OidcUserManager.signinCallback(); + window.location.hash = ''; + } else { + window.location = window.origin + '/main'; + } + }, + async getAccessToken() { + return (await window.OidcUserManager.getUser())?.access_token; + } +}; diff --git a/src/frontend/components/Layouts/Header.vue b/src/frontend/components/Layouts/Header.vue index ecaefe14..cdacea7a 100644 --- a/src/frontend/components/Layouts/Header.vue +++ b/src/frontend/components/Layouts/Header.vue @@ -6,6 +6,8 @@ dark v-bind:class="isPrintVersion ? 'print-version' : ''" style="z-index: 99"> +
+
@@ -20,7 +22,13 @@ refresh - +
+
+ {{ + user || 'Login' + }} + + error @@ -43,11 +51,14 @@ +
+