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