From e8b5c39a80b0abb87cc96c79ff848febc3f6a681 Mon Sep 17 00:00:00 2001 From: "manuel.sowada" Date: Sat, 25 Apr 2026 17:56:06 +0000 Subject: [PATCH] auth bugfix + licences --- dbcreate.sql | 17 ++ public/views/authentications.hbs | 0 public/views/desktop.hbs | 3 +- server.js | 25 +-- src/models/integratedStartMenuItems.json | 17 ++ src/models/vaulModel.js | 59 +++++++ src/routes/adminRoutes.js | 46 +++--- src/routes/indexRoutes.js | 5 +- src/routes/loginRoutes.js | 29 ++-- src/services/authenticationManager.js | 29 +++- src/services/vaultifyManager.js | 199 +++++++++++++++++++++++ 11 files changed, 362 insertions(+), 67 deletions(-) create mode 100644 public/views/authentications.hbs create mode 100644 src/models/vaulModel.js create mode 100644 src/services/vaultifyManager.js diff --git a/dbcreate.sql b/dbcreate.sql index 98ee7b5..a514ec3 100644 --- a/dbcreate.sql +++ b/dbcreate.sql @@ -43,12 +43,29 @@ DROP TABLE IF EXISTS dbo.Permission; DROP TABLE IF EXISTS dbo.Plugins; DROP TABLE IF EXISTS dbo.ObjectSource; DROP TABLE IF EXISTS dbo.AuthenticationUAC; +DROP TABLE IF EXISTS dbo.Vault; GO /* ========================================================= CORE TABLES ========================================================= */ +CREATE TABLE Vault ( + ID UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), + + Customer_ID NVARCHAR(128) NOT NULL, -- ehem. tenantId + Feature NVARCHAR(128) NOT NULL, -- z.B. AD_SYNC, DEMO_PLUGIN + + Payload NVARCHAR(MAX) NOT NULL, -- flexible JSON (config, limits etc.) + Signature NVARCHAR(MAX) NOT NULL, -- RSA-Signatur (Base64) + + Active BIT NOT NULL DEFAULT 1, + + ExpiresAt DATETIME NULL, + + CreatedAt DATETIME NOT NULL DEFAULT GETDATE(), + UpdatedAt DATETIME NULL DEFAULT GETDATE() +); CREATE TABLE dbo.ObjectSource ( ID INT IDENTITY(1,1) PRIMARY KEY, diff --git a/public/views/authentications.hbs b/public/views/authentications.hbs new file mode 100644 index 0000000..e69de29 diff --git a/public/views/desktop.hbs b/public/views/desktop.hbs index df00f06..c89df4f 100644 --- a/public/views/desktop.hbs +++ b/public/views/desktop.hbs @@ -56,10 +56,9 @@

  • {{else}} {{#if this.authorized}} -
  • +
  • {{#if this.icon}} - {{else}} {{/if}} {{this.label}} {{#if this.version}}v{{this.version}}{{/if}} diff --git a/server.js b/server.js index fa6c8b5..1943b23 100644 --- a/server.js +++ b/server.js @@ -97,6 +97,7 @@ const server = https.createServer(httpsOptions, app); let FileSystemManager = require(`@services/fileSystemManager.js`); let AuthenticationManager = require(`@services/authenticationManager.js`); let ActiveDirectory = require(`@services/activeDirectoryManager.js`); + let VaultifyManager = require(`@services/vaultifyManager.js`); service.set('socketManager', new SocketManager(io)); await service.get('socketManager').addAsync('/'); @@ -129,10 +130,9 @@ const server = https.createServer(httpsOptions, app); databaseModel.set('permissionModel', require(`@models/permissionModel`)(service.get('sqlManager').getInstance('main'))); databaseModel.set('roleModel', require(`@models/roleModel`)(service.get('sqlManager').getInstance('main'))); databaseModel.set('rolePermissionsModel', require(`@models/rolePermissionsModel`)(service.get('sqlManager').getInstance('main'))); - service.set('authenticationManager', new AuthenticationManager(databaseModel.get('authentication'), app.locals.configuration.integration.token.secret, databaseModel)); - + // service.set('vaultifyManager', new VaultifyManager()); service.set('activeDirectoryManager', new ActiveDirectory(app.locals.configuration.integration.activedirectory)) // everytime last created service! @@ -149,6 +149,8 @@ const server = https.createServer(httpsOptions, app); let helpers = service.get('fileSystemManager').loadAllFiles(`${app.locals.path.public}/helpers`, '.js'); exports.helpers = helpers; + // app.use(service.get('vaultifyManager').createMiddleware()); + app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use(cookieParser()); @@ -214,22 +216,9 @@ const server = https.createServer(httpsOptions, app); //#region Implement routes - require(`${app.locals.path.source}/routes/indexRoutes.js`).route(app, service); - require(`${app.locals.path.source}/routes/loginRoutes.js`).route( - app, - service.get('authenticationManager'), - service.get('socketManager'), - service.get('eventManager') - ); - require(`${app.locals.path.source}/routes/adminRoutes.js`).route( - app, - service.get('authenticationManager'), - service.get('pluginManager'), - service.get('eventManager'), - service.get('socketManager'), - service.get('activeDirectoryManager'), - `${app.locals.path.source}/models/stylesheet.json` - ); + require(`${app.locals.path.source}/routes/loginRoutes.js`).route(app, service); // #1 - no token security! important: first!!! + require(`${app.locals.path.source}/routes/indexRoutes.js`).route(app, service); // #2 - token security enabled at this point + require(`${app.locals.path.source}/routes/adminRoutes.js`).route(app, service); // #3 - token security always enabled //#endregion diff --git a/src/models/integratedStartMenuItems.json b/src/models/integratedStartMenuItems.json index a08493b..d10d140 100644 --- a/src/models/integratedStartMenuItems.json +++ b/src/models/integratedStartMenuItems.json @@ -31,6 +31,23 @@ "action": "Administration" } ] + }, + { + "label": "RBAC", + "description": "Role-Based Access Control ist eine rollenbasierte Zugriffskontrolle, die auf Basis von Rollen und Gruppen, systemweite Berechtigungen vergibt", + "view": "rbac.hbs", + "defaultSize": { + "width": "800px", + "height": "600px" + }, + "icon": "app.png", + "license": "rbac", + "permissions": [ + { + "scope": "SYSTEM", + "action": "Administration" + } + ] } ] } diff --git a/src/models/vaulModel.js b/src/models/vaulModel.js new file mode 100644 index 0000000..1fbe410 --- /dev/null +++ b/src/models/vaulModel.js @@ -0,0 +1,59 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + + const Vault = sequelize.define('Vault', { + + ID: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4 + }, + + Customer_ID: { + type: DataTypes.STRING(128), + allowNull: false + }, + + Feature: { + type: DataTypes.STRING(128), + allowNull: false + }, + + Payload: { + type: DataTypes.TEXT, // NVARCHAR(MAX) + allowNull: false + }, + + Signature: { + type: DataTypes.TEXT, + allowNull: false + }, + + Active: { + type: DataTypes.BOOLEAN, + defaultValue: true + }, + + ExpiresAt: { + type: DataTypes.DATE, + allowNull: true + }, + + CreatedAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW + }, + + UpdatedAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW + } + + }, { + tableName: 'Vault', + timestamps: false + }); + + return Vault; +}; \ No newline at end of file diff --git a/src/routes/adminRoutes.js b/src/routes/adminRoutes.js index ccf7852..50216ab 100644 --- a/src/routes/adminRoutes.js +++ b/src/routes/adminRoutes.js @@ -1,7 +1,6 @@ const { exec } = require('child_process'); const fs = require('fs'); const path = require('path'); -const { service } = require('@root/server.js'); const configurationFile = path.join(require('@root/server.js').path.source, 'models', 'configuration.json'); @@ -10,7 +9,7 @@ const serverInfoFile = path.join(require('@root/server.js').path.root, 'package. module.exports = { - route(app, authenticationManager, pluginManager, eventManager, socketManager, activeDirectoryManager, stylesheetJson) { + route(app, service) { // JSON configuration abrufen app.post('/api/getConfig', (req, res) => { fs.readFile(configurationFile, 'utf8', (err, data) => { @@ -62,7 +61,7 @@ module.exports = { app.post('/api/eventlog/clearlog', (req, res) => { - eventManager.clear(); + service.get('eventManager').clear(); res.status(200).send({ status: 'ok' }) }) @@ -87,10 +86,10 @@ module.exports = { res.status(200).send({ status: 'ok' }); // exec(`kill -9 ${process.pid}`, (error, stdout, stderr) => { // if (error) { - // service.get('eventManager').write(req.cookies.ObjectGUID, 4, null, error.message); + // service.get('service.get('eventManager')').write(req.cookies.ObjectGUID, 4, null, error.message); // return res.status(500).send({ status: 'error', message: error.message }); // } - // service.get('eventManager').write(req.cookies.ObjectGUID, 0, null, `Server neu gestartet`); + // service.get('service.get('eventManager')').write(req.cookies.ObjectGUID, 0, null, `Server neu gestartet`); // res.status(200).send({ status: 'ok' }); // }); }); @@ -100,22 +99,19 @@ module.exports = { const { name, state } = req.body; let result = null; if(state) { - result = await pluginManager.load(name, true); + result = await service.get('pluginManager').load(name, true); } else { - result = await pluginManager.unload(name); + result = await service.get('pluginManager').unload(name); } - console.log(result) - // result = { ...result, authorized: result.metadata.permissions.some(async permission => { await activeDirectoryManager.getGroup(permission) != null && await activeDirectoryManager.isUserMemberOfRecursive(req.cookies.sAMAccountName, permission)}) } - eventManager.write(null, result.levelId, name, result.message); - // socketManager.broadcast('admin', 'plugin_status', result); - socketManager.broadcast('/', 'plugin_status', result); + service.get('eventManager').write(null, result.levelId, name, result.message); + service.get('socketManager').broadcast('/', 'plugin_status', result); res.status(200).json(result); }); app.post('/api/plugins/getAll', async (req, res) => { try { - const plugins = await pluginManager.getStatus(); + const plugins = await service.get('pluginManager').getStatus(); res.status(200).json(plugins); } catch (error) { res.status(500).json({ error: error.message }); @@ -134,12 +130,12 @@ module.exports = { const { name } = req.params; try { const { updates } = req.body; - const result = await pluginManager.update(name, updates); + const result = await service.get('pluginManager').update(name, updates); // result = { ...result, authorized: result.metadata.permissions.some(async permission => { await activeDirectoryManager.getGroup(permission) != null && await activeDirectoryManager.isUserMemberOfRecursive(req.cookies.sAMAccountName, permission)}) } service.get('eventManager').writeLog(req.cookies.ObjectGUID, result.levelId, name, result.message); - // socketManager.broadcast('admin', 'plugin_status', result); - // socketManager.broadcast('/', 'plugin_status', result); + // service.get('socketManager').broadcast('admin', 'plugin_status', result); + // service.get('socketManager').broadcast('/', 'plugin_status', result); res.status(200).json(result); } catch (error) { service.get('eventManager').write(req.cookies.ObjectGUID, 4, name, `Fehler beim Aktualisieren des Plugins: ${error}`); @@ -153,30 +149,30 @@ module.exports = { const result = await service.get('pluginManager').rename(name, newName); // const result = { levelId: 0, pluginName: name, message: `Plugin erstellt` }; - // await pluginManager.create(name); + // await service.get('pluginManager').create(name); // res.status(200).json(result); - eventManager.writeLog(null, result.levelId, name, result.message); - // socketManager.broadcast('admin', 'plugin_status', result); + service.get('eventManager').writeLog(null, result.levelId, name, result.message); + // service.get('socketManager').broadcast('admin', 'plugin_status', result); }); app.post('/api/plugins/:name/create', async (req, res) => { const { name } = req.params; const result = { levelId: 0, pluginName: name, message: `Plugin erstellt` }; - await pluginManager.create(name); + await service.get('pluginManager').create(name); - eventManager.writeLog(null, result.levelId, name, result.message); + service.get('eventManager').writeLog(null, result.levelId, name, result.message); res.status(200).json(result); - // socketManager.broadcast('admin', 'plugin_status', result); + // service.get('socketManager').broadcast('admin', 'plugin_status', result); }); app.post('/admin/plugins/:name/delete', async (req, res) => { const { name } = req.params; - const result = { status: 'delete', pluginName: name, levelId: 0, message: `Plugin ${name} gelöscht` }; //await pluginManager.delete(name); + const result = { status: 'delete', pluginName: name, levelId: 0, message: `Plugin ${name} gelöscht` }; //await service.get('pluginManager').delete(name); res.status(200).json(result); - eventManager.write(null, result.levelId, name, result.message); - socketManager.broadcast('admin', 'plugin_status', result); + service.get('eventManager').write(null, result.levelId, name, result.message); + service.get('socketManager').broadcast('admin', 'plugin_status', result); }); } }; diff --git a/src/routes/indexRoutes.js b/src/routes/indexRoutes.js index 012b641..04dea15 100644 --- a/src/routes/indexRoutes.js +++ b/src/routes/indexRoutes.js @@ -11,8 +11,9 @@ const { doesNotReject } = require('assert'); module.exports = { route: function(app, service) { - app.get('/', service.get('authenticationManager').authenticate(), async (req, res) => { - console.log(req.cookies.ObjectGUID) + app.use(service.get('authenticationManager').authenticate()); + + app.get('/', async (req, res) => { const startMenuItems = await global.startMenuItems(app, req.cookies.ObjectGUID, false); res.render('desktop', { layout: 'default', startMenuItems: startMenuItems }); }); diff --git a/src/routes/loginRoutes.js b/src/routes/loginRoutes.js index 8419e3d..a63eca4 100644 --- a/src/routes/loginRoutes.js +++ b/src/routes/loginRoutes.js @@ -1,14 +1,15 @@ const { verify } = require("jsonwebtoken"); + module.exports = { - route(app, authenticationManager, socketManager, eventManager) { + route(app, service) { app.get(`/login`, (req, res) => { res.render(`login`, { layout: 'default' }); }) app.post('/login', async (req, res) => { const { sAMAccountName, password } = req.body; - const userModel = await authenticationManager.Authentication.findOne({ + const userModel = await service.get('authenticationManager').Authentication.findOne({ where: { sAMAccountName: sAMAccountName }, attributes: ['ObjectGUID'], raw: true }); @@ -28,9 +29,9 @@ module.exports = { sameSite: 'Strict', maxAge: 1000 * 60 * 60 * 24 * 365 }) - const login = await authenticationManager.login(sAMAccountName, password); + const login = await service.get('authenticationManager').login(sAMAccountName, password); - eventManager.writeLog(objectGuid, login.levelId, null, login.message); + service.get('eventManager').writeLog(objectGuid, login.levelId, null, login.message); res.status(login.levelId == 0 ? 200 : 401).json(login); } catch (err) { res.status(500).json(login); @@ -39,7 +40,7 @@ module.exports = { // Geschützte Route - app.get('/me', authenticationManager.authenticate(), (req, res) => { + app.get('/me', service.get('authenticationManager').authenticate(), (req, res) => { res.json(JSON.stringify({ user: { name: req.user @@ -50,28 +51,28 @@ module.exports = { app.post('/checkLoginName', async (req, res) => { const { sAMAccountName } = req.body; - const userExists = await authenticationManager.Authentication.findOne({ where: { sAMAccountName: sAMAccountName } }); + const userExists = await service.get('authenticationManager').Authentication.findOne({ where: { sAMAccountName: sAMAccountName } }); const auth = { objectGuid: userExists != null ? userExists.ObjectGUID : sAMAccountName, sAMAccountName: sAMAccountName }; res.status(userExists ? 200 : 404).json({ exists: userExists != null }); }); app.get('/verifying', async (req, res, next) => { - const verify = await authenticationManager.verifyUserToken(); - eventManager.write(req.user.objectGuid, verify.levelId, null, verify.message); + const verify = await service.get('authenticationManager').verifyUserToken(); + service.get('eventManager').writeLog(req.user.objectGuid, verify.levelId, null, verify.message); next(); }); // Logout - app.post('/logout', authenticationManager.authenticate(), async (req, res) => { - const logout = await authenticationManager.logout(req.user.sAMAccountName); + app.post('/logout', service.get('authenticationManager').authenticate(), async (req, res) => { + const logout = await service.get('authenticationManager').logout(req.user.sAMAccountName); - socketManager.sendTo('/', req.user.objectGuid, 'login_status', { levelId: logout.levelId, message: logout.message } ) - eventManager.write(req.user.objectGuid, logout.levelId, null, logout.message); + // socketManager.sendTo('/', req.user.objectGuid, 'login_status', { levelId: logout.levelId, message: logout.message } ) + service.get('eventManager').writeLog(req.user.objectGuid, logout.levelId, null, logout.message); res.clearCookie('sAMAccountName'); res.clearCookie('ObjectGUID'); - - setTimeout(() => res.render('login', { layout: false, title: app.locals.configuration.server.name }), 3000); + res.render('login', { layout: false, title: app.locals.configuration.server.name }) + // setTimeout(() => res.render('login', { layout: false, title: app.locals.configuration.server.name }), 3000); // res.json({ message: 'Logout erfolgreich' }); }); } diff --git a/src/services/authenticationManager.js b/src/services/authenticationManager.js index 300a273..8b470dd 100644 --- a/src/services/authenticationManager.js +++ b/src/services/authenticationManager.js @@ -135,7 +135,7 @@ async resolvePermissions(objectGuid) { sAMAccountName: user.sAMAccountName }, this.SECRET_KEY, - { expiresIn: '10s' } + { expiresIn: '1y' } ); user.refreshtoken = token; @@ -197,7 +197,17 @@ async resolvePermissions(objectGuid) { authenticate() { return async (req, res, next) => { + try { + + // 🔥 SKIP PUBLIC ROUTES + if ( + req.path.startsWith('/login') || + req.path.startsWith('/public') + ) { + return next(); + } + const sAMAccountName = req.cookies?.sAMAccountName; if (!sAMAccountName) { @@ -206,23 +216,30 @@ async resolvePermissions(objectGuid) { const user = await this.findUser(sAMAccountName); - if (!user || !user.refreshtoken || !user.active) { + if (!user || !user.active) { + return res.redirect('/login'); + } + + let payload; + + try { + payload = jwt.verify(user.refreshtoken, this.SECRET_KEY); + } catch { return res.redirect('/login'); } - // jwt.verify(user.refreshtoken, this.SECRET_KEY); -this.verifyUserToken(sAMAccountName) - // 🔥 LIVE RBAC RESOLUTION (bei JEDEM REQUEST) const rbac = await this.resolvePermissions(user.ObjectGUID); req.user = { ...user.toJSON(), + jwt: payload, groups: rbac.groups, roles: rbac.roles, permissions: rbac.permissions }; -console.log(req.user) + next(); + } catch (err) { console.error(err); return res.redirect('/login'); diff --git a/src/services/vaultifyManager.js b/src/services/vaultifyManager.js new file mode 100644 index 0000000..6e7e569 --- /dev/null +++ b/src/services/vaultifyManager.js @@ -0,0 +1,199 @@ +const crypto = require('crypto'); + +class VaultifyManager { + + constructor({ + vaultModel, + publicKey + }) { + this.Vault = vaultModel; + this.publicKey = publicKey; + + this.cache = new Map(); // feature cache per customer + } + + + createMiddleware() { + + return async (req, res, next) => { + + const customerId = req.user?.Customer_ID; + + if (!customerId) { + return res.status(403).send('No customer'); + } + + await this.loadCustomer(customerId); + + req.vault = { + has: (f) => this.has(customerId, f), + get: (f, p) => this.get(customerId, f, p) + }; + + next(); + }; + } + + // ========================================================= + // LOAD ALL LICENSES FOR CUSTOMER + // ========================================================= + + async loadCustomer(customerId) { + + const records = await this.Vault.findAll({ + where: { + Customer_ID: customerId, + Active: true + } + }); + + const resultMap = new Map(); + + for (const record of records) { + + const valid = this.verify(record); + if (!valid) continue; + + const payload = this.parsePayload(record.Payload); + + resultMap.set(record.Feature, { + payload, + expiresAt: record.ExpiresAt + }); + } + + this.cache.set(customerId, resultMap); + + return { + customerId, + features: [...resultMap.keys()] + }; + } + + // ========================================================= + // VERIFY SIGNATURE + // ========================================================= + + verify(record) { + + try { + const data = { + Customer_ID: record.Customer_ID, + Feature: record.Feature, + Payload: this.parsePayload(record.Payload), + ExpiresAt: record.ExpiresAt + }; + + const verifier = crypto.createVerify('RSA-SHA256'); + + verifier.update(JSON.stringify(data)); + verifier.end(); + + return verifier.verify( + this.publicKey, + record.Signature, + 'base64' + ); + + } catch { + return false; + } + } + + // ========================================================= + // SAFE JSON PARSER + // ========================================================= + + parsePayload(payload) { + try { + return typeof payload === 'string' + ? JSON.parse(payload) + : payload; + } catch { + return {}; + } + } + + // ========================================================= + // FEATURE CHECK + // ========================================================= + + has(customerId, feature) { + + const customer = this.cache.get(customerId); + if (!customer) return false; + + const entry = customer.get(feature); + if (!entry) return false; + + if (entry.expiresAt && new Date(entry.expiresAt) < new Date()) { + return false; + } + + return true; + } + + // ========================================================= + // GET FEATURE CONFIG + // ========================================================= + + get(customerId, feature, path = null) { + + const customer = this.cache.get(customerId); + if (!customer) return undefined; + + const entry = customer.get(feature); + if (!entry) return undefined; + + if (!path) return entry.payload; + + return path + .split('.') + .reduce((obj, key) => obj?.[key], entry.payload); + } + + // ========================================================= + // REFRESH SINGLE FEATURE + // ========================================================= + + async refreshFeature(customerId, feature) { + + const record = await this.Vault.findOne({ + where: { + Customer_ID: customerId, + Feature: feature, + Active: true + } + }); + + if (!record) return false; + + if (!this.verify(record)) return false; + + const customer = this.cache.get(customerId) || new Map(); + + customer.set(feature, { + payload: this.parsePayload(record.Payload), + expiresAt: record.ExpiresAt + }); + + this.cache.set(customerId, customer); + + return true; + } + + // ========================================================= + // STATUS + // ========================================================= + + status(customerId) { + const customer = this.cache.get(customerId); + + return { + customerId, + features: customer ? [...customer.keys()] : [] + }; + } +} + +module.exports = VaultifyManager; \ No newline at end of file