From 84c3d9f9ba1c1e163bf4732586b23f3460d21ed0 Mon Sep 17 00:00:00 2001 From: "manuel.sowada" Date: Sat, 25 Apr 2026 13:31:02 +0000 Subject: [PATCH] add rbac + db-create script --- dbcreate.sql | 397 +++++++++++++++++++++++ public/views/eventlog.hbs | 10 - server.js | 26 +- src/models/authenticationGroupsModel.js | 19 ++ src/models/authenticationModel.js | 4 - src/models/authenticationRolesModel.js | 19 ++ src/models/eventlogView.js | 5 +- src/models/groupClosureModel.js | 20 ++ src/models/groupModel.js | 18 + src/models/groupRolesModel.js | 19 ++ src/models/integratedStartMenuItems.json | 35 +- src/models/notifyTrayObjectsModel.js | 5 + src/models/objectSourceModel.js | 21 ++ src/models/permissionModel.js | 20 ++ src/models/roleModel.js | 19 ++ src/models/rolePermissionsModel.js | 19 ++ src/routes/indexRoutes.js | 9 +- src/services/authenticationManager.js | 356 ++++++++------------ src/services/notifyTrayManager.js | 11 - src/services/pluginManager.js | 38 ++- utils.js | 174 +++++++--- 21 files changed, 908 insertions(+), 336 deletions(-) create mode 100644 dbcreate.sql create mode 100644 src/models/authenticationGroupsModel.js create mode 100644 src/models/authenticationRolesModel.js create mode 100644 src/models/groupClosureModel.js create mode 100644 src/models/groupModel.js create mode 100644 src/models/groupRolesModel.js create mode 100644 src/models/objectSourceModel.js create mode 100644 src/models/permissionModel.js create mode 100644 src/models/roleModel.js create mode 100644 src/models/rolePermissionsModel.js diff --git a/dbcreate.sql b/dbcreate.sql new file mode 100644 index 0000000..98ee7b5 --- /dev/null +++ b/dbcreate.sql @@ -0,0 +1,397 @@ + +/* ========================================================= + DATABASE +========================================================= */ + +IF DB_ID('Radix_OS') IS NULL +BEGIN + CREATE DATABASE Radix_OS; +END +GO + +USE Radix_OS; +GO + + +/* ========================================================= + CLEAN RESET +========================================================= */ + +DROP VIEW IF EXISTS dbo.vAuthenticationEffectivePermissions; +DROP VIEW IF EXISTS dbo.vAuthenticationRoles; +DROP VIEW IF EXISTS dbo.vAuthenticationGroups; +DROP VIEW IF EXISTS dbo.vGroupHierarchy; +DROP VIEW IF EXISTS dbo.vAuthentications; +DROP VIEW IF EXISTS dbo.vEventLog; +DROP VIEW IF EXISTS dbo.vNotifyTray; + +DROP TABLE IF EXISTS dbo.AuthenticationRoles; +DROP TABLE IF EXISTS dbo.AuthenticationGroups; +DROP TABLE IF EXISTS dbo.GroupRoles; +DROP TABLE IF EXISTS dbo.RolePermissions; +DROP TABLE IF EXISTS dbo.GroupClosure; + +DROP TABLE IF EXISTS dbo.NotifyTray; +DROP TABLE IF EXISTS dbo.NotifyTrayObjects; +DROP TABLE IF EXISTS dbo.EventLog; +DROP TABLE IF EXISTS dbo.EventLevels; + +DROP TABLE IF EXISTS dbo.Authentication; +DROP TABLE IF EXISTS dbo.[Group]; +DROP TABLE IF EXISTS dbo.[Role]; +DROP TABLE IF EXISTS dbo.Permission; +DROP TABLE IF EXISTS dbo.Plugins; +DROP TABLE IF EXISTS dbo.ObjectSource; +DROP TABLE IF EXISTS dbo.AuthenticationUAC; +GO + + +/* ========================================================= + CORE TABLES +========================================================= */ + +CREATE TABLE dbo.ObjectSource ( + ID INT IDENTITY(1,1) PRIMARY KEY, + Name VARCHAR(100) NOT NULL UNIQUE +); + +CREATE TABLE dbo.AuthenticationUAC ( + ID INT PRIMARY KEY, + AttributeName NVARCHAR(100), + AttributeOriginal VARCHAR(255) +); + +CREATE TABLE dbo.[Role] ( + ID INT IDENTITY(1,1) PRIMARY KEY, + Name NVARCHAR(255) UNIQUE, + Description NVARCHAR(MAX), + RoleType VARCHAR(50) +); + +CREATE TABLE dbo.Permission ( + ID INT IDENTITY(1,1) PRIMARY KEY, + Scope VARCHAR(100), + Resource VARCHAR(100), + Action VARCHAR(100), + CONSTRAINT UQ_Permission UNIQUE (Scope, Resource, Action) +); + +CREATE TABLE dbo.Plugins ( + Name VARCHAR(50) PRIMARY KEY, + Active BIT, + Version VARCHAR(25) +); + + +/* ========================================================= + AUTHENTICATION +========================================================= */ + +CREATE TABLE dbo.Authentication ( + ObjectGUID UNIQUEIDENTIFIER PRIMARY KEY, + + sAMAccountName VARCHAR(255), + mail VARCHAR(255), + givenName VARCHAR(255), + sn VARCHAR(255), + + employeeID VARCHAR(255), + title VARCHAR(255), + department VARCHAR(255), + streetAddress VARCHAR(255), + + userAccountControl_ID INT, + + telephoneNumber VARCHAR(255), + physicalDeliveryOfficeName VARCHAR(255), + distinguishedName VARCHAR(MAX), + + password VARCHAR(MAX), + refreshtoken VARCHAR(MAX), + + active BIT, + online BIT, + + ObjectSource_ID INT, + FOREIGN KEY (ObjectSource_ID) REFERENCES dbo.ObjectSource(ID) +); + +CREATE TABLE dbo.[Group] ( + ObjectGUID UNIQUEIDENTIFIER PRIMARY KEY, + Name VARCHAR(255), + ObjectSource_ID INT, + distinguishedName VARCHAR(MAX), + FOREIGN KEY (ObjectSource_ID) REFERENCES dbo.ObjectSource(ID) +); + + +/* ========================================================= + GROUP CLOSURE +========================================================= */ + +CREATE TABLE dbo.GroupClosure ( + ParentGroup_ObjectGUID UNIQUEIDENTIFIER, + ChildGroup_ObjectGUID UNIQUEIDENTIFIER, + Depth INT, + PRIMARY KEY (ParentGroup_ObjectGUID, ChildGroup_ObjectGUID) +); + + +/* ========================================================= + RBAC +========================================================= */ + +CREATE TABLE dbo.AuthenticationRoles ( + Authentication_ObjectGUID UNIQUEIDENTIFIER, + Role_ID INT, + PRIMARY KEY (Authentication_ObjectGUID, Role_ID) +); + +CREATE TABLE dbo.AuthenticationGroups ( + Authentication_ObjectGUID UNIQUEIDENTIFIER, + Group_ObjectGUID UNIQUEIDENTIFIER, + PRIMARY KEY (Authentication_ObjectGUID, Group_ObjectGUID) +); + +CREATE TABLE dbo.GroupRoles ( + Group_ObjectGUID UNIQUEIDENTIFIER, + Role_ID INT, + PRIMARY KEY (Group_ObjectGUID, Role_ID) +); + +CREATE TABLE dbo.RolePermissions ( + Role_ID INT, + Permission_ID INT, + PRIMARY KEY (Role_ID, Permission_ID) +); + + +/* ========================================================= + EVENT SYSTEM +========================================================= */ + +CREATE TABLE dbo.EventLevels ( + ID INT PRIMARY KEY, + LevelName VARCHAR(50), + DisplayName VARCHAR(150), + Priority INT +); + +CREATE TABLE dbo.EventLog ( + ID INT IDENTITY(1,1) PRIMARY KEY, + Message VARCHAR(MAX), + Trace VARCHAR(MAX), + Level_ID INT, + PluginName VARCHAR(50), + Date DATETIME2, + ObjectGUID UNIQUEIDENTIFIER +); + + +/* ========================================================= + NOTIFY SYSTEM +========================================================= */ + +CREATE TABLE dbo.NotifyTrayObjects ( + ID INT IDENTITY(1,1) PRIMARY KEY, + PluginName VARCHAR(50), + Message VARCHAR(MAX), + JSON VARCHAR(MAX), + ActionRequired BIT DEFAULT 0, + CreatedAt DATETIME2, + ExpiresAt DATETIME2 +); + +CREATE TABLE dbo.NotifyTray ( + ID INT IDENTITY(1,1) PRIMARY KEY, + ObjectGUID UNIQUEIDENTIFIER, + NotifyTrayObject_ID INT, + SeenAt DATETIME2 +); + + +/* ========================================================= + SECURITY VIEWS +========================================================= */ + +CREATE VIEW dbo.vAuthenticationRoles AS +SELECT a.ObjectGUID, r.ID Role_ID, r.Name, 'DIRECT' Source +FROM dbo.Authentication a +JOIN dbo.AuthenticationRoles ar ON ar.Authentication_ObjectGUID = a.ObjectGUID +JOIN dbo.[Role] r ON r.ID = ar.Role_ID + +UNION ALL + +SELECT a.ObjectGUID, r.ID, r.Name, 'GROUP' +FROM dbo.Authentication a +JOIN dbo.AuthenticationGroups ag ON ag.Authentication_ObjectGUID = a.ObjectGUID +JOIN dbo.GroupRoles gr ON gr.Group_ObjectGUID = ag.Group_ObjectGUID +JOIN dbo.[Role] r ON r.ID = gr.Role_ID; + + +CREATE VIEW dbo.vAuthenticationEffectivePermissions AS +SELECT DISTINCT + a.ObjectGUID, + p.Scope, + p.Resource, + p.Action, + CONCAT(p.Scope,'.',p.Resource,'.',p.Action) PermissionKey +FROM dbo.Authentication a +JOIN dbo.vAuthenticationRoles r ON r.ObjectGUID = a.ObjectGUID +JOIN dbo.RolePermissions rp ON rp.Role_ID = r.Role_ID +JOIN dbo.Permission p ON p.ID = rp.Permission_ID; + + +/* ========================================================= + FIXED vEventLog (SEQUELIZE MATCH + SYSTEM FIX) +========================================================= */ + +CREATE OR ALTER VIEW dbo.vEventLog +AS +SELECT + e.ID, + e.Message, + e.Trace, + e.Date, + + e.Level_ID, + el.LevelName, + el.DisplayName AS LevelDisplayName, + el.Priority AS LevelPriority, + + e.PluginName, + + COALESCE(a.sn + ' ' + a.givenName, 'SYSTEM') AS ClearTextUser, + + a.sn AS Surname, + a.givenName, + + e.ObjectGUID, + + a.sAMAccountName, + a.mail, + a.department, + + a.telephoneNumber AS Phone, + a.physicalDeliveryOfficeName AS Office, + a.streetAddress AS Adress, + + COALESCE(a.ObjectSource_ID, 1) AS ObjectSource_ID, + os.Name AS ObjectSourceName + +FROM dbo.EventLog e +LEFT JOIN dbo.Authentication a ON a.ObjectGUID = e.ObjectGUID +LEFT JOIN dbo.EventLevels el ON el.ID = e.Level_ID +LEFT JOIN dbo.ObjectSource os ON os.ID = COALESCE(a.ObjectSource_ID, 1); +GO + + +/* ========================================================= + AUTH VIEW +========================================================= */ + +CREATE VIEW dbo.vAuthentications AS +SELECT a.*, os.Name AS ObjectSource +FROM dbo.Authentication a +LEFT JOIN dbo.ObjectSource os ON os.ID = a.ObjectSource_ID; + + +/* ========================================================= + GROUP VIEW +========================================================= */ + +CREATE VIEW dbo.vGroupHierarchy AS +SELECT * FROM dbo.GroupClosure; + + +/* ========================================================= + NOTIFY VIEWS +========================================================= */ +CREATE VIEW vNotifyTray AS +SELECT + n.ID, + n.ObjectGUID, + n.SeenAt, + + a.sAMAccountName, + a.givenName, + a.sn, + a.mail, + a.active, + a.online, + + nto.PluginName, + nto.JSON, + nto.ActionRequired, + nto.CreatedAt, + nto.Message + +FROM NotifyTray n +LEFT JOIN Authentication a ON a.ObjectGUID = n.ObjectGUID +LEFT JOIN NotifyTrayObjects nto ON n.ID = n.NotifyTrayObject_ID + +GO + +/* ========================================================= + SEED DATA +========================================================= */ + +INSERT INTO dbo.ObjectSource VALUES ('LOCAL'),('AD'); + +INSERT INTO dbo.EventLevels VALUES +(-1,'test','Test',5), +(0,'success','Success',4), +(1,'log','Log',3), +(2,'warn','Warn',2), +(4,'error','Error',1), +(8,'throw_exception','Exception',0); + +INSERT INTO dbo.Plugins VALUES ('SYSTEM',1,'1.0.0'); + +INSERT INTO dbo.[Role] (Name,Description,RoleType) +VALUES ('ADMIN','System Administrator','SYSTEM'); + +INSERT INTO dbo.Permission (Scope,Resource,Action) +VALUES ('SYSTEM','ALL','ALL'); + + +INSERT INTO dbo.RolePermissions +SELECT r.ID, p.ID +FROM dbo.[Role] r +JOIN dbo.Permission p ON p.Scope='SYSTEM' +WHERE r.Name='ADMIN'; + + +/* ========================================================= + ADMIN USER +========================================================= */ + +INSERT INTO dbo.Authentication ( + ObjectGUID, + sAMAccountName, + mail, + givenName, + sn, + active, + online, + ObjectSource_ID +) +SELECT + '00000000-0000-0000-0000-000000000001', + 'admin', + 'admin@local', + 'System', + 'Admin', + 1, + 0, + ID +FROM dbo.ObjectSource +WHERE Name='LOCAL'; + + +INSERT INTO dbo.AuthenticationRoles +SELECT + '00000000-0000-0000-0000-000000000001', + ID +FROM dbo.[Role] +WHERE Name='ADMIN'; \ No newline at end of file diff --git a/public/views/eventlog.hbs b/public/views/eventlog.hbs index deffb0c..2d91c28 100644 --- a/public/views/eventlog.hbs +++ b/public/views/eventlog.hbs @@ -83,16 +83,6 @@ Plugin: 'dropdown', Level: 'dropdown', User: 'dropdown' - }, - checkboxFilter:{ - column:'Level_ID', - rules:[ - { label:'Erfolgreich', test:v => parseInt(v) === 0 }, - { label:'Information', test:v => parseInt(v) === 1 }, - { label:'Warnung', test:v => parseInt(v) === 2 }, - { label:'Fehler', test:v => parseInt(v) === 4 }, - { label:'Absturz', test:v => parseInt(v) === 8 }, - ] } }, customRender: (row, tr) => { diff --git a/server.js b/server.js index e507254..fa6c8b5 100644 --- a/server.js +++ b/server.js @@ -118,7 +118,21 @@ const server = https.createServer(httpsOptions, app); databaseModel.set('authentication', require(`@models/authenticationModel`)(service.get('sqlManager').getInstance('main'))); service.set('fileSystemManager', new FileSystemManager()); - service.set('authenticationManager', new AuthenticationManager(databaseModel.get('authentication'), app.locals.configuration.integration.token.secret)); + + + databaseModel.set('authenticationGroupsModel', require(`@models/authenticationGroupsModel`)(service.get('sqlManager').getInstance('main'))); + databaseModel.set('authenticationRolesModel', require(`@models/authenticationRolesModel`)(service.get('sqlManager').getInstance('main'))); + databaseModel.set('groupClosureModel', require(`@models/groupClosureModel`)(service.get('sqlManager').getInstance('main'))); + databaseModel.set('groupModel', require(`@models/groupModel`)(service.get('sqlManager').getInstance('main'))); + databaseModel.set('groupRolesModel', require(`@models/groupRolesModel`)(service.get('sqlManager').getInstance('main'))); + databaseModel.set('objectSourceModel', require(`@models/objectSourceModel`)(service.get('sqlManager').getInstance('main'))); + 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('activeDirectoryManager', new ActiveDirectory(app.locals.configuration.integration.activedirectory)) // everytime last created service! @@ -154,7 +168,7 @@ const server = https.createServer(httpsOptions, app); }) //#endregion - + //#region App config values app.set('view engine', '.hbs'); app.set('views', [ @@ -164,7 +178,6 @@ const server = https.createServer(httpsOptions, app); app.set('trust proxy', true) //#endregion - //#region Error exception handling app.on('uncaughtException', (err) => service.get('eventManager').write(null, 8, null, err )); process.on('uncaughtException', (err) => service.get('eventManager').write(null, 8, null, err )); @@ -188,12 +201,7 @@ const server = https.createServer(httpsOptions, app); // Loading plugins const plugins = await service.get('pluginManager').loadAll() - // const pluginsLoaded = { - // levelId: plugins.some(plugin => plugin.levelId > 0) ? 2 : 0, - // message: plugins.map(plugin => `${plugin.pluginName} v${plugin.metadata.version} ${plugin.message}`).join('
') - // } - // service.get('eventManager').write(null, pluginsLoaded.levelId, null, pluginsLoaded.message); - + plugins.forEach(plugin => { service.get('eventManager').write(null, plugin.levelId, null, `${plugin.pluginName} v${plugin.metadata.version} ${plugin.message}`); }); diff --git a/src/models/authenticationGroupsModel.js b/src/models/authenticationGroupsModel.js new file mode 100644 index 0000000..8fc6792 --- /dev/null +++ b/src/models/authenticationGroupsModel.js @@ -0,0 +1,19 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const AuthenticationGroups = sequelize.define('AuthenticationGroups', { + Authentication_ObjectGUID: { + type: DataTypes.UUID, + primaryKey: true + }, + Group_ObjectGUID: { + type: DataTypes.UUID, + primaryKey: true + } + }, { + tableName: 'AuthenticationGroups', + timestamps: false + }); + + return AuthenticationGroups; +}; \ No newline at end of file diff --git a/src/models/authenticationModel.js b/src/models/authenticationModel.js index d4cb72f..7101aff 100644 --- a/src/models/authenticationModel.js +++ b/src/models/authenticationModel.js @@ -44,10 +44,6 @@ module.exports = (sequelize) => { type: DataTypes.STRING, allowNull: true, }, - authenticationType_ID: { - type: DataTypes.INTEGER, - allowNull: true, - }, telephoneNumber: { type: DataTypes.STRING, allowNull: true, diff --git a/src/models/authenticationRolesModel.js b/src/models/authenticationRolesModel.js new file mode 100644 index 0000000..68c2e4a --- /dev/null +++ b/src/models/authenticationRolesModel.js @@ -0,0 +1,19 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const AuthenticationRoles = sequelize.define('AuthenticationRoles', { + Authentication_ObjectGUID: { + type: DataTypes.UUID, + primaryKey: true + }, + Role_ID: { + type: DataTypes.INTEGER, + primaryKey: true + } + }, { + tableName: 'AuthenticationRoles', + timestamps: false + }); + + return AuthenticationRoles; +}; \ No newline at end of file diff --git a/src/models/eventlogView.js b/src/models/eventlogView.js index dca16ec..03d7452 100644 --- a/src/models/eventlogView.js +++ b/src/models/eventlogView.js @@ -10,7 +10,6 @@ module.exports = (sequelize) => { Date: { type: DataTypes.DATE }, department: { type: DataTypes.STRING }, ClearTextUser: { type: DataTypes.STRING }, - ObjectTypDisplayName: { type: DataTypes.STRING }, Level_ID: { type: DataTypes.INTEGER }, LevelName: { type: DataTypes.STRING }, LevelPriority: { type: DataTypes.INTEGER }, @@ -22,8 +21,8 @@ module.exports = (sequelize) => { Phone: { type: DataTypes.STRING }, Office: { type: DataTypes.STRING }, Adress: { type: DataTypes.STRING }, - authenticationType_ID: { type: DataTypes.INTEGER }, - TypeName: { type: DataTypes.STRING } + ObjectSource_ID: { type: DataTypes.INTEGER }, + ObjectSourceName: { type: DataTypes.STRING }, }, { tableName: 'vEventLog', // dein SQL-View timestamps: false, diff --git a/src/models/groupClosureModel.js b/src/models/groupClosureModel.js new file mode 100644 index 0000000..687d9dc --- /dev/null +++ b/src/models/groupClosureModel.js @@ -0,0 +1,20 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const GroupClosure = sequelize.define('GroupClosure', { + ParentGroup_ObjectGUID: { + type: DataTypes.UUID, + primaryKey: true + }, + ChildGroup_ObjectGUID: { + type: DataTypes.UUID, + primaryKey: true + }, + Depth: DataTypes.INTEGER + }, { + tableName: 'GroupClosure', + timestamps: false + }); + + return GroupClosure; +}; \ No newline at end of file diff --git a/src/models/groupModel.js b/src/models/groupModel.js new file mode 100644 index 0000000..411f574 --- /dev/null +++ b/src/models/groupModel.js @@ -0,0 +1,18 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const Group = sequelize.define('Group', { + ObjectGUID: { + type: DataTypes.UUID, + primaryKey: true + }, + Name: DataTypes.STRING(255), + ObjectSource_ID: DataTypes.INTEGER, + distinguishedName: DataTypes.TEXT + }, { + tableName: 'Group', + timestamps: false + }); + + return Group; +}; \ No newline at end of file diff --git a/src/models/groupRolesModel.js b/src/models/groupRolesModel.js new file mode 100644 index 0000000..85d62e2 --- /dev/null +++ b/src/models/groupRolesModel.js @@ -0,0 +1,19 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const GroupRoles = sequelize.define('GroupRoles', { + Group_ObjectGUID: { + type: DataTypes.UUID, + primaryKey: true + }, + Role_ID: { + type: DataTypes.INTEGER, + primaryKey: true + } + }, { + tableName: 'GroupRoles', + timestamps: false + }); + + return GroupRoles; +}; \ No newline at end of file diff --git a/src/models/integratedStartMenuItems.json b/src/models/integratedStartMenuItems.json index d6a120f..a08493b 100644 --- a/src/models/integratedStartMenuItems.json +++ b/src/models/integratedStartMenuItems.json @@ -11,7 +11,10 @@ "view": "styleconfig", "icon": "brush.png", "permissions": [ - "Administration" + { + "scope": "SYSTEM", + "action": "Administration" + } ] }, { @@ -23,7 +26,10 @@ "height": "900px" }, "permissions": [ - "Administration" + { + "scope": "SYSTEM", + "action": "Administration" + } ] } ] @@ -45,7 +51,10 @@ }, "icon": "eventlog.ico", "permissions": [ - "Administration" + { + "scope": "SYSTEM", + "action": "Administration" + } ] } ] @@ -67,7 +76,10 @@ }, "icon": "plugins.png", "permissions": [ - "Administration" + { + "scope": "SYSTEM", + "action": "Administration" + } ] } ] @@ -89,7 +101,10 @@ }, "icon": "serverinfo.png", "permissions": [ - "Administration" + { + "scope": "SYSTEM", + "action": "Administration" + } ] } ] @@ -111,7 +126,10 @@ }, "icon": "app.png", "permissions": [ - "*" + { + "scope": "SYSTEM", + "action": "Default_Access" + } ] } ] @@ -129,7 +147,10 @@ "view": "help", "icon": "help.png", "permissions": [ - "*" + { + "scope": "SYSTEM", + "action": "Default_Access" + } ] } ] diff --git a/src/models/notifyTrayObjectsModel.js b/src/models/notifyTrayObjectsModel.js index aa59517..c786856 100644 --- a/src/models/notifyTrayObjectsModel.js +++ b/src/models/notifyTrayObjectsModel.js @@ -29,6 +29,11 @@ module.exports = (sequelize) => { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW, + }, + ExpiresAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, } }, { tableName: 'NotifyTrayObjects', diff --git a/src/models/objectSourceModel.js b/src/models/objectSourceModel.js new file mode 100644 index 0000000..0dd4d91 --- /dev/null +++ b/src/models/objectSourceModel.js @@ -0,0 +1,21 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const ObjectSource = sequelize.define('ObjectSource', { + ID: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + Name: { + type: DataTypes.STRING(100), + allowNull: false, + unique: true + } + }, { + tableName: 'ObjectSource', + timestamps: false + }); + + return ObjectSource; +}; \ No newline at end of file diff --git a/src/models/permissionModel.js b/src/models/permissionModel.js new file mode 100644 index 0000000..ad26319 --- /dev/null +++ b/src/models/permissionModel.js @@ -0,0 +1,20 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const Permission = sequelize.define('Permission', { + ID: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + + Scope: DataTypes.STRING(100), + Resource: DataTypes.STRING(100), + Action: DataTypes.STRING(100) + }, { + tableName: 'Permission', + timestamps: false + }); + + return Permission; +}; \ No newline at end of file diff --git a/src/models/roleModel.js b/src/models/roleModel.js new file mode 100644 index 0000000..4c52178 --- /dev/null +++ b/src/models/roleModel.js @@ -0,0 +1,19 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const Role = sequelize.define('Role', { + ID: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + Name: DataTypes.STRING(255), + Description: DataTypes.TEXT, + RoleType: DataTypes.STRING(50) + }, { + tableName: 'Role', + timestamps: false + }); + + return Role; +}; \ No newline at end of file diff --git a/src/models/rolePermissionsModel.js b/src/models/rolePermissionsModel.js new file mode 100644 index 0000000..4dfe500 --- /dev/null +++ b/src/models/rolePermissionsModel.js @@ -0,0 +1,19 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const RolePermissions = sequelize.define('RolePermissions', { + Role_ID: { + type: DataTypes.INTEGER, + primaryKey: true + }, + Permission_ID: { + type: DataTypes.INTEGER, + primaryKey: true + } + }, { + tableName: 'RolePermissions', + timestamps: false + }); + + return RolePermissions; +}; \ No newline at end of file diff --git a/src/routes/indexRoutes.js b/src/routes/indexRoutes.js index 45dab4c..012b641 100644 --- a/src/routes/indexRoutes.js +++ b/src/routes/indexRoutes.js @@ -12,11 +12,8 @@ const { doesNotReject } = require('assert'); module.exports = { route: function(app, service) { app.get('/', service.get('authenticationManager').authenticate(), async (req, res) => { - const startMenuItems = await global.startMenuItems( - app, - req.cookies.sAMAccountName, - service - ); + console.log(req.cookies.ObjectGUID) + const startMenuItems = await global.startMenuItems(app, req.cookies.ObjectGUID, false); res.render('desktop', { layout: 'default', startMenuItems: startMenuItems }); }); @@ -54,7 +51,7 @@ module.exports = { res.status(204).send(); }) - app.post('/api/Plugins/loadScripts', service.get('authenticationManager').authenticate(), async(req, res) => { + app.post('/api/Plugins/loadScripts', async(req, res) => { const scripts = service.get('pluginManager').getStatus().map(plugin => { const exists = service.get('fileSystemManager').exists(path.join(plugin.pluginPath, 'public', 'javascript', 'main.js')) diff --git a/src/services/authenticationManager.js b/src/services/authenticationManager.js index 62d2e32..300a273 100644 --- a/src/services/authenticationManager.js +++ b/src/services/authenticationManager.js @@ -1,31 +1,27 @@ const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); -const { fn, col, where } = require('sequelize'); -/** - * Authentication class for login method, token validation and password setting - */ class AuthenticationManager { - constructor(model, secretKey) { + constructor(model, secretKey, databaseModel) { this.Authentication = model; this.SECRET_KEY = secretKey; + this.databaseModel = databaseModel; } - /** - * Helper: Case-insensitive User Lookup - */ + // ========================================================= + // USER + // ========================================================= + async findUser(sAMAccountName) { - return await this.Authentication.findOne({ - where: where( - fn('LOWER', col('sAMAccountName')), - sAMAccountName.toLowerCase() - ) + return this.Authentication.findOne({ + where: { sAMAccountName } }); } - /** - * Set or reset password of user - */ + // ========================================================= + // PASSWORD + // ========================================================= + async setPassword(sAMAccountName, password) { const user = await this.findUser(sAMAccountName); @@ -33,16 +29,88 @@ class AuthenticationManager { return { token: null, levelId: 2, message: 'Unbekannter User' }; } - const hashedPassword = await bcrypt.hash(password, 10); - user.password = hashedPassword; + user.password = await bcrypt.hash(password, 10); await user.save(); return { token: null, levelId: 0, message: 'Passwort gesetzt' }; } - /** - * Login - */ + // ========================================================= + // RBAC RESOLVER (LIVE - WICHTIG!) + // ========================================================= + +async resolvePermissions(objectGuid) { + const AuthenticationGroups = this.databaseModel.get('authenticationGroupsModel'); + const GroupClosure = this.databaseModel.get('groupClosureModel'); + const AuthenticationRoles = this.databaseModel.get('authenticationRolesModel'); + const GroupRoles = this.databaseModel.get('groupRolesModel'); + const RolePermissions = this.databaseModel.get('rolePermissionsModel'); + const Permission = this.databaseModel.get('permissionModel'); + + // 1. USER GROUPS + const userGroups = await AuthenticationGroups.findAll({ + where: { Authentication_ObjectGUID: objectGuid } + }); + + const directGroupIds = userGroups.map(g => g.Group_ObjectGUID); + + // 2. NESTED GROUPS + let allGroupIds = [...directGroupIds]; + + if (directGroupIds.length) { + const closure = await GroupClosure.findAll({ + where: { ParentGroup_ObjectGUID: directGroupIds } + }); + + allGroupIds.push(...closure.map(c => c.ChildGroup_ObjectGUID)); + } + + allGroupIds = [...new Set(allGroupIds)]; + + // 3. ROLES + const userRoles = await AuthenticationRoles.findAll({ + where: { Authentication_ObjectGUID: objectGuid } + }); + + const groupRoles = await GroupRoles.findAll({ + where: { Group_ObjectGUID: allGroupIds } + }); + + const roleIds = [ + ...new Set([ + ...userRoles.map(r => r.Role_ID), + ...groupRoles.map(r => r.Role_ID) + ]) + ]; + + // 4. PERMISSIONS + const rolePerms = await RolePermissions.findAll({ + where: { Role_ID: roleIds } + }); + + const permissionIds = rolePerms.map(r => r.Permission_ID); + + const permissions = await Permission.findAll({ + where: { ID: permissionIds } + }); + + // 🔥 HIER DIE ÄNDERUNG + return { + groups: allGroupIds, + roles: roleIds, + permissions: permissions.map(p => ({ + id: p.ID, + scope: p.Scope, + resource: p.Resource, + action: p.Action + })) + }; +} + + // ========================================================= + // LOGIN (minimal change) + // ========================================================= + async login(sAMAccountName, password) { const user = await this.findUser(sAMAccountName); @@ -55,20 +123,20 @@ class AuthenticationManager { return { token: null, levelId: 1, message: 'Benutzer nicht registriert' }; } - const passwordMatch = await bcrypt.compare(password, user.password); + const ok = await bcrypt.compare(password, user.password); - if (!passwordMatch) { + if (!ok) { return { token: null, levelId: 2, message: 'Falsches Passwort' }; } - const payload = { - sAMAccountName: user.sAMAccountName, - mail: user.mail, - givenName: user.givenName, - sn: user.sn - }; - - const token = jwt.sign(payload, this.SECRET_KEY, { expiresIn: '100y' }); + const token = jwt.sign( + { + ObjectGUID: user.ObjectGUID, + sAMAccountName: user.sAMAccountName + }, + this.SECRET_KEY, + { expiresIn: '10s' } + ); user.refreshtoken = token; user.online = true; @@ -81,9 +149,10 @@ class AuthenticationManager { }; } - /** - * Logout - */ + // ========================================================= + // LOGOUT + // ========================================================= + async logout(sAMAccountName) { const user = await this.findUser(sAMAccountName); @@ -98,14 +167,15 @@ class AuthenticationManager { return { token: null, levelId: 0, message: 'Erfolgreich abgemeldet' }; } - /** - * Token prüfen - */ + // ========================================================= + // VERIFY TOKEN (unchanged) + // ========================================================= + async verifyUserToken(sAMAccountName) { const user = await this.findUser(sAMAccountName); if (!user || !user.refreshtoken) { - return { valid: false, levelId: 1, message: 'Kein gültiger Token' }; + return { valid: false, levelId: 1 }; } try { @@ -113,42 +183,45 @@ class AuthenticationManager { return { valid: true, - payload, user, - levelId: 0, - message: 'User verifiziert' + payload }; } catch { - return { - valid: false, - levelId: 4, - message: 'Ungültiger Token' - }; + return { valid: false, levelId: 4 }; } } - /** - * Middleware - */ + // ========================================================= + // 🔥 AUTH MIDDLEWARE (HIER PASSIERT DIE MAGIE) + // ========================================================= + authenticate() { return async (req, res, next) => { try { const sAMAccountName = req.cookies?.sAMAccountName; - const objectGUID = req.cookies?.ObjectGUID; - if (!sAMAccountName || !objectGUID) { + if (!sAMAccountName) { return res.redirect('/login'); } const user = await this.findUser(sAMAccountName); - if (!user || !user.refreshtoken || user.active === false) { + if (!user || !user.refreshtoken || !user.active) { return res.redirect('/login'); } - jwt.verify(user.refreshtoken, this.SECRET_KEY); + // 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; + req.user = { + ...user.toJSON(), + groups: rbac.groups, + roles: rbac.roles, + permissions: rbac.permissions + }; +console.log(req.user) next(); } catch (err) { console.error(err); @@ -158,177 +231,4 @@ class AuthenticationManager { } } -module.exports = AuthenticationManager; - -// const jwt = require('jsonwebtoken'); -// const bcrypt = require('bcryptjs'); - -// let { levelId, message } = ''; - -// /** -// * Authentication class for login method, token validation and password setting -// */ -// class AuthenticationManager { -// /** -// * -// * @param {object} model - Use the authentication database model for interact with the database -// * @param {string} secretKey - Defines the server secret for token validation -// */ -// constructor(model, secretKey, eventManager) { -// this.eventManager = eventManager; - -// // if (!model) throw new Error('Sequelize Model wird benötigt'); -// // if (!secretKey) throw new Error('Secret Key wird benötigt'); - -// this.Authentication = model; -// this.SECRET_KEY = secretKey; -// } - -// /** -// * Set or reset password of user -// * @param {string} sAMAccountName - Windows account name -// * @param {string} password - Set the new password -// */ -// async setPassword(sAMAccountName, password) { -// const user = await this.Authentication.findOne({ where: { sAMAccountName } }); -// if (!user) { -// // this.eventManager.write(null, 2, 0, { aboveLevel: 1 }, `User nicht gefunden`); -// levelId = 2; -// message = `Unbekannter User` -// return {token: null, levelId: levelId }; -// // throw new Error(`User ${sAMAccountName} nicht gefunden`); -// } -// // if (user.password) throw new Error('Passwort bereits gesetzt'); - -// const hashedPassword = await bcrypt.hash(password, 10); -// user.password = hashedPassword; -// await user.save(); -// } - -// /** -// * Login mit Speicherung des Tokens in der Datenbank -// */ -// async login(sAMAccountName, password) { -// const user = await this.Authentication.findOne({ where: { sAMAccountName } }); - -// if (!user) { -// //this.eventManager.write(null, 2, null, null, `User ${sAMAccountName} nicht geufunden`) -// levelId = 2; -// message = `Unbekannter Benutzer`; -// return { token: null, levelId: levelId, message: message }; -// // throw new Error('Unkown user'); -// } -// if (!user.password) { -// this.setPassword(sAMAccountName, password); -// // this.eventManager.write(user.ObjectGUID, 2, null, null, 'User registration initialized') -// levelId = 1; -// message = `Benutzer nicht registiert`; -// return { token: null, levelId: levelId, message: message }; -// // throw new Error('User not registered'); -// } - -// const passwordMatch = await bcrypt.compare(password, user.password); -// if (!passwordMatch) { -// // this.eventManager.write(user.ObjectGUID, 2, null, null, 'Password doesn\'t match'); -// levelId = 2; -// message = `Falsches Passwort`; -// return { token: null, levelId: levelId, message: message }; -// // throw new Error('Wrong password'); -// } - -// // Token erzeugen -// const payload = { -// sAMAccountName: user.sAMAccountName, -// mail: user.mail, -// givenName: user.givenName, -// sn: user.sn -// }; - -// const token = jwt.sign(payload, this.SECRET_KEY, { expiresIn: '100y' }); -// // Token in DB speichern -// user.refreshtoken = token; -// user.online = true; -// await user.save(); - -// // this.eventManager.write(user.ObjectGUID, 1, null, null, 'Erfolgreich angemeldet'); -// levelId = 0; -// message = `Erfolgreich angemeldet`; -// return { token: token, levelId: levelId, message: message }; -// } - -// /** -// * Logout löscht Token aus der DB -// */ -// async logout(sAMAccountName) { -// const user = await this.Authentication.findOne({ where: { sAMAccountName } }); -// if (user) { -// user.refreshtoken = null; -// user.online = false; -// await user.save(); -// levelId = 0; -// message = `Erfolgreich abgemeldet`; -// return { token: null, levelId: levelId, message: message }; -// } -// } - -// /** -// * Token-Prüfung (über DB) -// */ -// async verifyUserToken(sAMAccountName) { -// const user = await this.Authentication.findOne({ where: { sAMAccountName } }); -// if (!user || !user.refreshtoken) { -// levelId = 1, -// message = `Kein gültiger Token`; -// // throw new Error('Kein gespeicherter Token gefunden'); -// } - -// try { -// const payload = jwt.verify(user.refreshtoken, this.SECRET_KEY); -// levelId = 0; -// message = `User verifiziert`; -// return { valid: true, payload, user, levelId: levelId, message: message } -// } catch { -// levelId = 4; -// message = `Ungültiger Token`; -// return { valid: false, payload, user, levelId: levelId, message: message } -// } -// } - -// /** -// * Express Middleware – prüft Token direkt aus DB anhand sAMAccountNamec -// */ -// authenticate() { -// return async (req, res, next) => { -// try { -// const sAMAccountName = req.cookies?.sAMAccountName; -// const objectGUID = req.cookies?.ObjectGUID; -// if (!sAMAccountName || !objectGUID) { -// return res.redirect('/login'); -// // return res.status(401).json({ message: 'Kein Benutzer-Cookie gefunden' }); -// } - -// const user = await this.Authentication.findOne({ where: { sAMAccountName } }); -// if (!user || !user.refreshtoken) { -// return res.redirect('/login'); -// // return res.status(401).json({ message: 'Benutzer oder Token nicht gefunden' }); -// } - -// if (user.active === false) { -// return res.redirect('/login'); -// // return res.status(401).json({ message: 'Benutzer ist nicht aktiv' }); -// } - -// // Token aus DB prüfen -// const payload = jwt.verify(user.refreshtoken, this.SECRET_KEY); -// req.user = user; -// next(); -// } catch (err) { -// console.error(err); -// return res.redirect('/login'); -// // res.status(401).json({ message: 'Authentifizierung fehlgeschlagen' }); -// } -// }; -// } -// } - -// module.exports = AuthenticationManager; +module.exports = AuthenticationManager; \ No newline at end of file diff --git a/src/services/notifyTrayManager.js b/src/services/notifyTrayManager.js index 7eee9c2..809005f 100644 --- a/src/services/notifyTrayManager.js +++ b/src/services/notifyTrayManager.js @@ -61,17 +61,6 @@ class notifyTrayManager { ObjectGUID: objectGuid, SeenAt: null }, - // include: [{ - // model: this.objects, - // as: 'NotificationObject', - // required: true, // join zwingend - // where: { - // [this.objects.sequelize.Op.Or]: [ - // { ExpiresAt: null }, - // { ExpiresAt: { [this.objects.sequelize.Op.gt]: new Date() } } - // ] - // } - // }], order: [[ 'SeenAt', 'ASC']] }); } diff --git a/src/services/pluginManager.js b/src/services/pluginManager.js index 69197bd..8767983 100644 --- a/src/services/pluginManager.js +++ b/src/services/pluginManager.js @@ -91,7 +91,7 @@ class PluginManager { await this.Plugin.upsert({ Name: name, - Active: withActivate ? true : (await this.Plugin.findOne({ where: { Name: meta.live.name }})).Active, + Active: !withActivate ? meta.live.active : false, Version: meta.live.version }); @@ -287,27 +287,45 @@ class PluginManager { } } - __pluginTemplate(name, options = { }) { + __pluginTemplate(name, options = {}) { return { name, description: options.description || 'Beschreibung hier einfügen', - version: options.version || `1.${new Date().getFullYear().toString().slice(-2)}.${new Date().getMonth() + 1}.${new Date().getDate()}`, + + version: options.version || + `1.${new Date().getFullYear().toString().slice(-2)}.${new Date().getMonth() + 1}.${new Date().getDate()}`, + + // ========================= + // MENU (RBAC READY) + // ========================= menu: { + label: name, + items: [ + { label: name, - items:[ + view: "index", + defaultSize: { width: '800px', height: '600px' }, + icon: "../../images/app.png", + + // ========================= + // RBAC PERMISSIONS + // ========================= + permissions: [ { - label: name, - view: "index", - defaultSize: { width: '800px', height: '600px' }, - icon: "../../images/app.png", - permissions: ["*"] + scope: name, // Plugin Scope (default = plugin name) + action: "Default_Access", + resource: "MenuItem" } ] + } + ] }, + config: options.config || {}, + active: true + }; } - } __setPermissions(pluginPath) { if (!this.filePermissions.user && !this.filePermissions.group) return; diff --git a/utils.js b/utils.js index 5a08e12..4fa4ec5 100644 --- a/utils.js +++ b/utils.js @@ -23,14 +23,22 @@ global.json = { startMenuItems: new HotReload(path.join(global.path.source, 'models', 'integratedStartMenuItems.json')) } - -module.exports = startMenuItems = async function (app, sAMAccountName) { +module.exports = startMenuItems = async function (app, objectGuid, debug = false) { function safeClone(obj) { return JSON.parse(JSON.stringify(obj)); } - const integratedStartmenuItems = safeClone(service.get('fileSystemManager').loadJSON(global.json.startMenuItems.filePath)); + const log = (...args) => { + if (debug) console.log('[RBAC DEBUG]', ...args); + }; + + // ========================= + // Load menu sources + // ========================= + const integratedStartmenuItems = + service.get('fileSystemManager') + .loadJSON(global.json.startMenuItems.filePath) || []; const plugins = service .get('pluginManager') @@ -40,59 +48,123 @@ module.exports = startMenuItems = async function (app, sAMAccountName) { section: 'Plugin' })); - let getAllPlugins = [...plugins, ...integratedStartmenuItems]; + let allPlugins = [...plugins, ...integratedStartmenuItems]; - for (const plugin of getAllPlugins) { + // ========================= + // Load user permissions + // ========================= + const authManager = service.get('authenticationManager'); - plugin.menu.items = await Promise.all( - (plugin.menu.items || []).map(async item => { + const userPermissions = + (await authManager.resolvePermissions(objectGuid)) + ?.permissions || []; - const authorized = - item.label === 'hr' || - item.permissions.includes('Administration') - ? global.json.configuration.live.administration.some( - name => name.toLowerCase() === sAMAccountName.toLowerCase() - ) - : item.permissions.includes('*') || - ( - await Promise.all( - item.permissions.map(async permission => - (await service.get('activeDirectoryManager').getGroup(permission)) && - (await service.get('activeDirectoryManager').isUserMemberOfRecursive( - sAMAccountName, - permission - )) - ) - ) - ).some(Boolean); + const normalizedPermissions = userPermissions.map(p => ({ + scope: p.scope, + action: p.action, + resource: p.resource || null + })); - return { - ...safeClone(item), - authorized - }; - }) - ); + log('USER OBJECTGUID:', objectGuid); + log('PERMISSIONS:', normalizedPermissions); + + // ========================= + // SUPER ADMIN CHECK + // ========================= + const isSuperAdmin = normalizedPermissions.some(p => + p.scope === 'SYSTEM' && + p.resource === 'ALL' && + p.action === 'ALL' + ); + + log('SUPER ADMIN:', isSuperAdmin); + + // ========================= + // BUILD MENU + // ========================= + for (const plugin of allPlugins) { + + plugin.menu.items = (plugin.menu.items || []).map(item => { + + const resource = item.label; + const requiredPermissions = item.permissions || []; + + const debugTrace = []; + + const authorized = isSuperAdmin + ? (debugTrace.push('SUPERADMIN OVERRIDE'), true) + : normalizedPermissions.some(userPerm => { + + return requiredPermissions.some(required => { + + const scopeMatch = + userPerm.scope === required.scope; + + const actionMatch = + userPerm.action === 'ALL' || + userPerm.action === required.action || + required.action === 'ALL'; + + const resourceMatch = + !userPerm.resource || + userPerm.resource === 'ALL' || + userPerm.resource === resource; + + const result = scopeMatch && actionMatch && resourceMatch; + + if (debug) { + debugTrace.push({ + userPerm, + required, + scopeMatch, + actionMatch, + resourceMatch, + result + }); + } + + return result; + }); + }); + + if (debug) { + log(`\n--- MENU ITEM: ${item.label} ---`); + log('AUTHORIZED:', authorized); + log('TRACE:', debugTrace); + } + + return { + ...safeClone(item), + authorized + }; + }); plugin.onlyAdministration = - plugin.menu.items.every(item => !item.authorized) && - !global.json.configuration.live.administration.includes(sAMAccountName); + plugin.menu.items.every(i => !i.authorized); + + if (debug) { + log(`PLUGIN: ${plugin.name}`); + log('VISIBLE:', !plugin.onlyAdministration); + } } - getAllPlugins = getAllPlugins - .filter(plugin => !plugin.onlyAdministration) - .filter(plugin => plugin.active); - - app.locals.startMenuItems = getAllPlugins; - - return [...getAllPlugins]; + // ========================= + // FILTER FINAL MENU + // ========================= + allPlugins = allPlugins + .filter(p => !p.onlyAdministration) + .filter(p => p.active); + app.locals.startMenuItems = allPlugins; + return allPlugins; }; -// module.exports = startMenuItems = async function(app, sAMAccountName) { -// function safeClone(obj) { -// return JSON.parse(JSON.stringify(obj)); -// } -// delete integratedStartmenuItems; -// integratedStartmenuItems = safeClone(json.startMenuItems.live); +// module.exports = startMenuItems = async function (app, sAMAccountName) { + +// function safeClone(obj) { +// return JSON.parse(JSON.stringify(obj)); +// } + +// const integratedStartmenuItems = safeClone(service.get('fileSystemManager').loadJSON(global.json.startMenuItems.filePath)); // const plugins = service // .get('pluginManager') @@ -112,13 +184,18 @@ module.exports = startMenuItems = async function (app, sAMAccountName) { // const authorized = // item.label === 'hr' || // item.permissions.includes('Administration') -// ? global.json.configuration.live.administration.some(name => name.toLowerCase() === sAMAccountName.toLowerCase()) +// ? global.json.configuration.live.administration.some( +// name => name.toLowerCase() === sAMAccountName.toLowerCase() +// ) // : item.permissions.includes('*') || // ( // await Promise.all( // item.permissions.map(async permission => // (await service.get('activeDirectoryManager').getGroup(permission)) && -// (await service.get('activeDirectoryManager').isUserMemberOfRecursive(sAMAccountName, permission)) +// (await service.get('activeDirectoryManager').isUserMemberOfRecursive( +// sAMAccountName, +// permission +// )) // ) // ) // ).some(Boolean); @@ -145,6 +222,7 @@ module.exports = startMenuItems = async function (app, sAMAccountName) { // }; + /** * Convert date into custom dateformat * @param {any} date - Valid date as datetype or string