From 0a28a3e493608792051d98af97c74af7ec76be34 Mon Sep 17 00:00:00 2001 From: "manuel.sowada" Date: Tue, 12 May 2026 07:57:32 +0200 Subject: [PATCH] client online state --- dbcreate.sql | 55 +- globalize.js | 1 + package-lock.json | 281 +----- package.json | 3 +- public/javascript/rbacAPI.js | 21 +- public/styles/default.css | 2 +- public/views/authentications.hbs | 0 public/views/desktop.hbs | 20 + public/views/rbac.hbs | 65 +- server.js | 3 +- src/models/authenticationGroupsModel.js | 9 + src/routes/adminRoutes.js | 23 +- src/services/activeDirectoryManager.js | 1200 +++++++++++++++-------- src/services/authenticationManager.js | 22 +- src/services/rbacManager.js | 264 ++++- src/services/socketManager.js | 4 +- src/sockets/mainSocket.js | 25 +- 17 files changed, 1210 insertions(+), 788 deletions(-) delete mode 100644 public/views/authentications.hbs diff --git a/dbcreate.sql b/dbcreate.sql index 292fc96..c684320 100644 --- a/dbcreate.sql +++ b/dbcreate.sql @@ -552,51 +552,16 @@ GO CREATE OR ALTER VIEW dbo.vPermissionOverview AS -SELECT - p.ID AS Permission_ID, - p.Scope, - p.Resource, - p.Action, - p.Scope + '.' + p.Resource + '.' + p.Action AS PermissionKey, - - r.ID AS Role_ID, - - - -- 🔥 NEU: Anzahl Rollen pro Permission - COUNT(r.ID) OVER (PARTITION BY p.ID) AS RoleCount, - - COUNT(gr.Group_ObjectGUID) AS GroupCount, - COUNT(ar.Authentication_ObjectGUID) AS DirectUserCount, - COUNT(ag.Authentication_ObjectGUID) AS GroupUserCount, - - COUNT( - COALESCE(ar.Authentication_ObjectGUID, ag.Authentication_ObjectGUID) - ) AS TotalUserCount - -FROM dbo.Permission AS p - -INNER JOIN dbo.RolePermissions AS rp - ON rp.Permission_ID = p.ID - -INNER JOIN dbo.Role AS r - ON r.ID = rp.Role_ID - -LEFT JOIN dbo.GroupRoles AS gr - ON gr.Role_ID = r.ID - -LEFT JOIN dbo.AuthenticationRoles AS ar - ON ar.Role_ID = r.ID - -LEFT JOIN dbo.AuthenticationGroups AS ag - ON ag.Group_ObjectGUID = gr.Group_ObjectGUID - -GROUP BY - p.ID, - p.Scope, - p.Resource, - p.Action, - r.ID, - r.Name; +SELECT p.ID AS Permission_ID, p.Scope, p.Resource, p.Action, p.Scope + '.' + p.Resource + '.' + p.Action AS PermissionKey, r.ID AS Role_ID, /* 🔥 NEU: Anzahl Rollen pro Permission*/ COUNT(r.ID) OVER (PARTITION BY p.ID) + AS RoleCount, COUNT(gr.Group_ObjectGUID) AS GroupCount, COUNT(ar.Authentication_ObjectGUID) AS DirectUserCount, COUNT(ag.Authentication_ObjectGUID) AS GroupUserCount, + COUNT(COALESCE (ar.Authentication_ObjectGUID, ag.Authentication_ObjectGUID)) AS TotalUserCount +FROM dbo.Permission AS p FULL JOIN + dbo.RolePermissions AS rp ON rp.Permission_ID = p.ID LEFT JOIN + dbo.Role AS r ON r.ID = rp.Role_ID LEFT JOIN + dbo.GroupRoles AS gr ON gr.Role_ID = r.ID LEFT JOIN + dbo.AuthenticationRoles AS ar ON ar.Role_ID = r.ID LEFT JOIN + dbo.AuthenticationGroups AS ag ON ag.Group_ObjectGUID = gr.Group_ObjectGUID +GROUP BY p.ID, p.Scope, p.Resource, p.Action, r.ID, r.Name; GO diff --git a/globalize.js b/globalize.js index 6fb8995..3bae33b 100644 --- a/globalize.js +++ b/globalize.js @@ -14,6 +14,7 @@ module.exports = { localPath, cache: { startMenuItems: [], + clientsOnline: [] }, runtimeFile: { package: new HotReload(path.join(localPath.root, 'package.json')), diff --git a/package-lock.json b/package-lock.json index 5e4f950..a02b8bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.9", "license": "UNLICENSED", "dependencies": { - "activedirectory2": "^2.2.0", "bcryptjs": "^3.0.2", "body-parser": "^2.2.0", "child_process": "^1.0.2", @@ -21,7 +20,7 @@ "fs-extra": "^11.3.2", "https": "^1.0.0", "jsonwebtoken": "^9.0.2", - "ldapjs": "^3.0.7", + "ldapts": "^8.1.7", "module-alias": "^2.2.3", "multer": "^2.0.2", "net": "^1.0.2", @@ -298,92 +297,6 @@ "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.6.5.tgz", "integrity": "sha512-3zwefSMwHpu8iVUW8YYz227sIv6UFqO31p1Bf1ZH/Vom7CmNyUsXjDBlnNzcuhmOL1XfxZ3nvND42kR23XlbcQ==" }, - "node_modules/@ldapjs/asn1": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-2.0.0.tgz", - "integrity": "sha512-G9+DkEOirNgdPmD0I8nu57ygQJKOOgFEMKknEuQvIHbGLwP3ny1mY+OTUYLCbCaGJP4sox5eYgBJRuSUpnAddA==", - "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md" - }, - "node_modules/@ldapjs/attribute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@ldapjs/attribute/-/attribute-1.0.0.tgz", - "integrity": "sha512-ptMl2d/5xJ0q+RgmnqOi3Zgwk/TMJYG7dYMC0Keko+yZU6n+oFM59MjQOUht5pxJeS4FWrImhu/LebX24vJNRQ==", - "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", - "dependencies": { - "@ldapjs/asn1": "2.0.0", - "@ldapjs/protocol": "^1.2.1", - "process-warning": "^2.1.0" - } - }, - "node_modules/@ldapjs/change": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@ldapjs/change/-/change-1.0.0.tgz", - "integrity": "sha512-EOQNFH1RIku3M1s0OAJOzGfAohuFYXFY4s73wOhRm4KFGhmQQ7MChOh2YtYu9Kwgvuq1B0xKciXVzHCGkB5V+Q==", - "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", - "dependencies": { - "@ldapjs/asn1": "2.0.0", - "@ldapjs/attribute": "1.0.0" - } - }, - "node_modules/@ldapjs/controls": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@ldapjs/controls/-/controls-2.1.0.tgz", - "integrity": "sha512-2pFdD1yRC9V9hXfAWvCCO2RRWK9OdIEcJIos/9cCVP9O4k72BY1bLDQQ4KpUoJnl4y/JoD4iFgM+YWT3IfITWw==", - "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", - "dependencies": { - "@ldapjs/asn1": "^1.2.0", - "@ldapjs/protocol": "^1.2.1" - } - }, - "node_modules/@ldapjs/controls/node_modules/@ldapjs/asn1": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-1.2.0.tgz", - "integrity": "sha512-KX/qQJ2xxzvO2/WOvr1UdQ+8P5dVvuOLk/C9b1bIkXxZss8BaR28njXdPgFCpj5aHaf1t8PmuVnea+N9YG9YMw==", - "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md" - }, - "node_modules/@ldapjs/dn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@ldapjs/dn/-/dn-1.1.0.tgz", - "integrity": "sha512-R72zH5ZeBj/Fujf/yBu78YzpJjJXG46YHFo5E4W1EqfNpo1UsVPqdLrRMXeKIsJT3x9dJVIfR6OpzgINlKpi0A==", - "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", - "dependencies": { - "@ldapjs/asn1": "2.0.0", - "process-warning": "^2.1.0" - } - }, - "node_modules/@ldapjs/filter": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@ldapjs/filter/-/filter-2.1.1.tgz", - "integrity": "sha512-TwPK5eEgNdUO1ABPBUQabcZ+h9heDORE4V9WNZqCtYLKc06+6+UAJ3IAbr0L0bYTnkkWC/JEQD2F+zAFsuikNw==", - "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", - "dependencies": { - "@ldapjs/asn1": "2.0.0", - "@ldapjs/protocol": "^1.2.1", - "process-warning": "^2.1.0" - } - }, - "node_modules/@ldapjs/messages": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ldapjs/messages/-/messages-1.3.0.tgz", - "integrity": "sha512-K7xZpXJ21bj92jS35wtRbdcNrwmxAtPwy4myeh9duy/eR3xQKvikVycbdWVzkYEAVE5Ce520VXNOwCHjomjCZw==", - "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", - "dependencies": { - "@ldapjs/asn1": "^2.0.0", - "@ldapjs/attribute": "^1.0.0", - "@ldapjs/change": "^1.0.0", - "@ldapjs/controls": "^2.1.0", - "@ldapjs/dn": "^1.1.0", - "@ldapjs/filter": "^2.1.1", - "@ldapjs/protocol": "^1.2.1", - "process-warning": "^2.2.0" - } - }, - "node_modules/@ldapjs/protocol": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz", - "integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==", - "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md" - }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -455,11 +368,6 @@ "node": ">=6.5" } }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" - }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -472,40 +380,6 @@ "node": ">= 0.6" } }, - "node_modules/activedirectory2": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/activedirectory2/-/activedirectory2-2.2.0.tgz", - "integrity": "sha512-uGbw74xttFG6hgocU8T1a0oDofLsyTp44BPTn42JN5C2QlyO5kRl2E7ZoUdfpFzV+yxhaQTKI+8QqRB5HONYvA==", - "deprecated": "Decomissioned.", - "dependencies": { - "abstract-logging": "^2.0.0", - "async": "^3.1.0", - "ldapjs": "^2.3.3", - "merge-options": "^2.0.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/activedirectory2/node_modules/ldapjs": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz", - "integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==", - "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", - "dependencies": { - "abstract-logging": "^2.0.0", - "asn1": "^0.2.4", - "assert-plus": "^1.0.0", - "backoff": "^2.5.0", - "ldap-filter": "^0.3.3", - "once": "^1.4.0", - "vasync": "^2.2.0", - "verror": "^1.8.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -541,38 +415,6 @@ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" - }, - "node_modules/backoff": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", - "integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==", - "dependencies": { - "precond": "0.2" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -885,11 +727,6 @@ "node": ">=6.6.0" } }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" - }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -1219,14 +1056,6 @@ "node": ">=22.15.0" } }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "engines": [ - "node >=0.6.0" - ] - }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -1558,14 +1387,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -1660,37 +1481,15 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/ldap-filter": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz", - "integrity": "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==", + "node_modules/ldapts": { + "version": "8.1.7", + "resolved": "https://registry.npmjs.org/ldapts/-/ldapts-8.1.7.tgz", + "integrity": "sha512-TJl6T92eIwMf/OJ0hDfKVa6ISwzo+lqCWCI5Mf//ARlKa3LKQZaSrme/H2rCRBhW0DZCQlrsV+fgoW5YHRNLUw==", "dependencies": { - "assert-plus": "^1.0.0" + "strict-event-emitter-types": "2.0.0" }, "engines": { - "node": ">=0.8" - } - }, - "node_modules/ldapjs": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-3.0.7.tgz", - "integrity": "sha512-1ky+WrN+4CFMuoekUOv7Y1037XWdjKpu0xAPwSP+9KdvmV9PG+qOKlssDV6a+U32apwxdD3is/BZcWOYzN30cg==", - "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", - "dependencies": { - "@ldapjs/asn1": "^2.0.0", - "@ldapjs/attribute": "^1.0.0", - "@ldapjs/change": "^1.0.0", - "@ldapjs/controls": "^2.1.0", - "@ldapjs/dn": "^1.1.0", - "@ldapjs/filter": "^2.1.1", - "@ldapjs/messages": "^1.3.0", - "@ldapjs/protocol": "^1.2.1", - "abstract-logging": "^2.0.1", - "assert-plus": "^1.0.0", - "backoff": "^2.5.0", - "once": "^1.4.0", - "vasync": "^2.2.1", - "verror": "^1.10.1" + "node": ">=20" } }, "node_modules/lodash": { @@ -1769,17 +1568,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-options": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-2.0.0.tgz", - "integrity": "sha512-S7xYIeWHl2ZUKF7SDeBhGg6rfv5bKxVBdk95s/I7wVF8d+hjLSztJ/B271cnUiF6CAFduEQ5Zn3HYwAjT16DlQ==", - "dependencies": { - "is-plain-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -3854,14 +3642,6 @@ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==" }, - "node_modules/precond": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", - "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -3870,11 +3650,6 @@ "node": ">= 0.6.0" } }, - "node_modules/process-warning": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", - "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4402,6 +4177,11 @@ "node": ">=10.0.0" } }, + "node_modules/strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -4629,43 +4409,6 @@ "node": ">= 0.8" } }, - "node_modules/vasync": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz", - "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "verror": "1.10.0" - } - }, - "node_modules/vasync/node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index e86c12e..40963c2 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "license": "UNLICENSED", "licensefile": "license_internal.txt", "dependencies": { - "activedirectory2": "^2.2.0", "bcryptjs": "^3.0.2", "body-parser": "^2.2.0", "child_process": "^1.0.2", @@ -33,7 +32,7 @@ "fs-extra": "^11.3.2", "https": "^1.0.0", "jsonwebtoken": "^9.0.2", - "ldapjs": "^3.0.7", + "ldapts": "^8.1.7", "module-alias": "^2.2.3", "multer": "^2.0.2", "net": "^1.0.2", diff --git a/public/javascript/rbacAPI.js b/public/javascript/rbacAPI.js index 4d315fe..394026f 100644 --- a/public/javascript/rbacAPI.js +++ b/public/javascript/rbacAPI.js @@ -30,11 +30,13 @@ const RBAC = { })) .sort((a, b) => a.sAMAccountName.localeCompare(b.sAMAccountName)), createUser: (data) => api('/api/rbac/auth/create', 'POST', data), + syncUsersFromAD: () => api('/api/rbac/auth/syncFromAD', 'POST', { }), deleteUser: (guid) => api(`/api/rbac/auth/${guid}`, 'DELETE'), // 👥 GROUPS loadGroups: () => api('/api/rbac/group/get', 'POST'), createGroup: (name) => api('/api/rbac/group/create', 'POST', { name }), + syncGroupsFromAD: () => api('/api/rbac/group/syncFromAD', 'POST', { }), deleteGroup: (guid) => api(`/api/rbac/group/${guid}`, 'DELETE'), // 🎭 ROLES @@ -193,7 +195,7 @@ const rbacPermissionsVT = virtualTable({ data: [], rowHeight: 20, buffer: 5, - groupKey: 'Scope', + groupKey: null, rowKey: 'ID', filterConfig: { hideCounter: true, @@ -372,6 +374,13 @@ async function createUser() { loadUsers(); } + +async function syncUsersFromAD() { + const users = await RBAC.syncUsersFromAD(); + sendUserEvent('RBAC', `${users.length} Benutzer aus dem AD synchronisiert`, null, 0); + loadUsers(); +} + async function deleteUser(guid, name) { feedbox({ title: `Benutzer löschen`, @@ -420,6 +429,16 @@ async function deleteGroup(guid, name) { } +async function syncGroupsFromAD() { + const group = await RBAC.syncGroupsFromAD(); + + sendUserEvent('RBAC', `${group.length} Gruppen aus dem AD synchronisiert`, null, 0); + + loadGroups(); + loadUsers(); +} + + ////////////////////////////// // 🎭 ROLE ACTIONS ////////////////////////////// diff --git a/public/styles/default.css b/public/styles/default.css index f3a42ce..960f85f 100644 --- a/public/styles/default.css +++ b/public/styles/default.css @@ -43,7 +43,7 @@ button:not(:disabled).yellowbutton:hover { background:var(--theme-button-yellow- .card.static.row { overflow:hidden; display:flex; flex-direction:row; flex-wrap: wrap;} .card.static { overflow:hidden; display:flex; flex-direction:column; } -.container { width:calc(100% - 20px); margin:10px auto; display:grid; grid-template-columns:100%; gap:12px; min-height:0; overflow:auto; max-height:100%; } +.container:not(.static) { width:calc(100% - 20px); margin:10px auto; display:grid; grid-template-columns:100%; gap:12px; min-height:0; overflow:auto; max-height:100%; } .container:not(.static) * { box-sizing:border-box; } .card { border-width:1px; border-style:solid; border-radius:8px; padding:20px; } diff --git a/public/views/authentications.hbs b/public/views/authentications.hbs deleted file mode 100644 index e69de29..0000000 diff --git a/public/views/desktop.hbs b/public/views/desktop.hbs index 869d6ea..bf56ab6 100644 --- a/public/views/desktop.hbs +++ b/public/views/desktop.hbs @@ -149,4 +149,24 @@ } }); + +/* Checks tab visibility +document.addEventListener( + 'visibilitychange', + () => { + + if (document.hidden) { + + alert('AWAY AT: ' + new Date().toISOString()); + + } else { + + updateActivity(); + } + } +); +*/ + + + diff --git a/public/views/rbac.hbs b/public/views/rbac.hbs index 9c5f6ea..5e96b85 100644 --- a/public/views/rbac.hbs +++ b/public/views/rbac.hbs @@ -29,7 +29,7 @@ section { align-items: center; justify-content: space-between; padding: 5px; - width: 120px; + width: 200px; border: 1px solid #ccc; border-radius: 8px; margin: 0 2px 2px 0; @@ -62,12 +62,21 @@ input { - -
-
- Users -
- + + +
+ +
+ AD Synchronisation: + + + +
+
+ Users + +
+
@@ -88,8 +97,30 @@ input { + + + +
+ + +
+ GRUPPEN WERDEN GELADEN . . . +
+
+ + +
+ + + +
+ ROLLEN WERDEN GELADEN . . . +
+
+ + -
+
.. @@ -115,24 +146,6 @@ input {
- -
- - -
- GRUPPEN WERDEN GELADEN . . . -
-
- - -
- - - -
- ROLLEN WERDEN GELADEN . . . -
-
diff --git a/server.js b/server.js index 71e7ed0..59d5e1e 100644 --- a/server.js +++ b/server.js @@ -57,7 +57,8 @@ const server = https.createServer(httpsOptions, app); const io = new Server(server, { - pingTimeout: 60000, + pingInterval: 1000, // alle 25s Ping senden + pingTimeout: 200, // 20s ohne Pong => disconnect maxHttpBufferSize: 1e8, // 100 MB }); diff --git a/src/models/authenticationGroupsModel.js b/src/models/authenticationGroupsModel.js index 8fc6792..de7ae02 100644 --- a/src/models/authenticationGroupsModel.js +++ b/src/models/authenticationGroupsModel.js @@ -12,6 +12,15 @@ module.exports = (sequelize) => { } }, { tableName: 'AuthenticationGroups', + indexes: [ + { + unique: true, + fields: [ + 'Authentication_ObjectGUID', + 'Group_ObjectGUID' + ] + } + ], timestamps: false }); diff --git a/src/routes/adminRoutes.js b/src/routes/adminRoutes.js index d312e1c..c331f4f 100644 --- a/src/routes/adminRoutes.js +++ b/src/routes/adminRoutes.js @@ -102,7 +102,6 @@ module.exports = { app.post('/api/rbac/auth/get', async (req, res) => { try { - console.log(await service.get('rbacManager').syncAuthByActiveDirectory()); rbacUsers = await service.get('rbacManager').getAuth(); res.json(rbacUsers); } catch (err) { @@ -138,6 +137,17 @@ module.exports = { } }); + app.post('/api/rbac/auth/syncFromAD', async (req, res) => { + try { + rbacUsers = await service.get('rbacManager').syncAuthByActiveDirectory(); + console.log(await service.get('rbacManager').syncAuthenticationGroupsFromAD()); + res.json(rbacUsers); + } catch (err) { + service.get('eventManager').writeLog(req.cookies.ObjectGUID, 4, 'RBAC', err.message); + res.status(500).json({ error: err.message }); + } + }); + app.put('/api/rbac/auth/:id', async (req, res) => { try { await service.get('rbacManager').updateAuth(req.params.id, req.body); @@ -189,6 +199,17 @@ module.exports = { res.status(500).json({ error: err.message }); } }); + + app.post('/api/rbac/group/syncFromAD', async (req, res) => { + try { + console.log(await service.get('rbacManager').syncGroupClosureFromAD()); + rbacGroups = await service.get('rbacManager').syncGroupByActiveDirectory(); + res.json(rbacGroups); + } catch (err) { + service.get('eventManager').writeLog(req.cookies.ObjectGUID, 4, 'RBAC', err); + res.status(500).json({ error: err.message }); + } + }); app.put('/api/rbac/group/:id', async (req, res) => { try { diff --git a/src/services/activeDirectoryManager.js b/src/services/activeDirectoryManager.js index 06760da..dc4a8fa 100644 --- a/src/services/activeDirectoryManager.js +++ b/src/services/activeDirectoryManager.js @@ -1,26 +1,21 @@ -const ActiveDirectory = require('activedirectory2'); +const { Client } = require('ldapts'); class ActiveDirectoryManager { - constructor({ - url, - baseDN, - username, + + constructor({ + url, + baseDN, + username, password, - userAttributes, - groupAttributes, - computerAttributes + userAttributes = [], + groupAttributes = [], + computerAttributes = [] }) { - this.ad = new ActiveDirectory({ - url, - baseDN, - username, - password, - attributes: { - user: userAttributes, - group: groupAttributes, - computer: computerAttributes - } - }); + + this.url = url; + this.baseDN = baseDN; + this.username = username; + this.password = password; this.userAttributes = userAttributes; this.groupAttributes = groupAttributes; @@ -29,250 +24,556 @@ class ActiveDirectoryManager { /** * ----------------------------------------------------- - * INTERNAL GENERIC LDAP SEARCH + * CONNECTION * ----------------------------------------------------- */ - async ldapSearch(options) { - return new Promise((resolve, reject) => { - this.ad.find(options, (err, result) => { - if (err) return reject(err); - resolve(result || {}); - }); + + async createClient() { + + const client = new Client({ + url: this.url, + timeout: 10000, + connectTimeout: 10000 }); + + await client.bind(this.username, this.password); + + return client; } /** * ----------------------------------------------------- - * USER FUNCTIONS + * HELPERS + * ----------------------------------------------------- + */ + + escape(value) { + return String(value).replace(/[*()\\]/g, '\\$&'); + } + + bufferToGuid(buffer) { + + const hex = buffer.toString('hex'); + + return [ + hex.substring(6, 8), + hex.substring(4, 6), + hex.substring(2, 4), + hex.substring(0, 2), + '-', + hex.substring(10, 12), + hex.substring(8, 10), + '-', + hex.substring(14, 16), + hex.substring(12, 14), + '-', + hex.substring(16, 20), + '-', + hex.substring(20) + ].join(''); + } + + normalizeEntry(entry) { + + const obj = {}; + + for (const [key, value] of Object.entries(entry)) { + + let normalized = value; + + // GUID konvertieren + if (key === 'objectGUID' && value instanceof Buffer) { + normalized = this.bufferToGuid(value); + } + + // Arrays mit einem Wert -> String + else if (Array.isArray(value)) { + + if (value.length === 0) { + normalized = null; + } + + else if (value.length === 1) { + normalized = value[0]; + } + } + + obj[key] = normalized; + } + + obj.ObjectSource_ID = 2; + + return obj; + } + + /** + * ----------------------------------------------------- + * GENERIC LDAP SEARCH + * ----------------------------------------------------- + */ + + async ldapSearch({ + baseDN = this.baseDN, + filter, + attributes = [] + }) { + + const client = await this.createClient(); + + try { + + const { searchEntries } = await client.search(baseDN, { + scope: 'sub', + filter, + attributes, + paged: true + }); + + return searchEntries.map(entry => + this.normalizeEntry(entry) + ); + + } finally { + await client.unbind(); + } + } + + /** + * ----------------------------------------------------- + * USERS * ----------------------------------------------------- */ async getUser(username, attributes = this.userAttributes) { - return new Promise((resolve, reject) => { - this.ad.findUser({ attributes }, username, (err, user) => { - if (err) return reject(err); - resolve(user || null); - }); + + const safe = this.escape(username); + + const filter = + `(&(objectCategory=person)(objectClass=user)` + + `(|(sAMAccountName=${safe})` + + `(mail=${safe})` + + `(cn=${safe})))`; + + const users = await this.ldapSearch({ + filter, + attributes + }); + + return users[0] || null; + } + + async getUserDN(username) { + + const user = await this.getUser( + username, + ['distinguishedName'] + ); + + return user?.distinguishedName || null; + } + + async getAllUsers(attributes = this.userAttributes) { + + const filter = + '(&(objectCategory=person)(objectClass=user))'; + + return await this.ldapSearch({ + filter, + attributes }); } - - async getAllUsers(attributes = this.userAttributes) { - const options = { - baseDN: this.ad.baseDN, - filter: '(&(objectClass=user)(objectCategory=person))', - attributes: ['objectGUID' - ,'sAMAccountName' - ,'mail' - ,'givenName' - ,'sn' - ,'employeeID' - ,'title' - ,'department' - ,'streetAddress' - ,'telephoneNumber' - ,'physicalDeliveryOfficeName' - ,'distinguishedName'] - }; - const result = await this.ldapSearch(options); - console.log(result) - return result.users || []; - } - - - async getUserDN(username) { - const user = await this.getUser(username); - return user?.dn || null; - } - async findUsers(query, attributes = this.userAttributes) { - return new Promise((resolve, reject) => { - const filter = `(&(objectClass=user)(|(cn=${query})(sAMAccountName=${query})(mail=${query})(displayName=${query})))`; - this.ad.findUsers({ filter, attributes }, (err, users) => { - if (err) return reject(err); - resolve(users || []); - }); + const safe = this.escape(query); + + const filter = + `(&(objectClass=user)` + + `(|(cn=${safe})` + + `(sAMAccountName=${safe})` + + `(mail=${safe})` + + `(displayName=${safe})))`; + + return await this.ldapSearch({ + filter, + attributes }); } /** * ----------------------------------------------------- - * GROUP FUNCTIONS + * GROUPS * ----------------------------------------------------- */ async getGroup(groupName, attributes = this.groupAttributes) { - return new Promise((resolve, reject) => { - this.ad.findGroup({ attributes }, groupName, (err, group) => { - if (err) return reject(err); - resolve(group || null); - }); - }); - } - async findGroups(query, attributes = this.groupAttributes) { - return new Promise((resolve, reject) => { - const filter = `(&(objectClass=group)(cn=${query}))`; + const safe = this.escape(groupName); - this.ad.findGroups({ filter, attributes }, (err, groups) => { - if (err) return reject(err); - resolve(groups || []); - }); - }); - } + const filter = + `(&(objectClass=group)(cn=${safe}))`; - /** - * ----------------------------------------------------- - * COMPUTER / OU FUNCTIONS 🖥️ - * ----------------------------------------------------- - */ - - /** - * Einzelnen Computer holen - */ - async getComputer(name, attributes = this.computerAttributes) { - return new Promise((resolve, reject) => { - const filter = `(&(objectClass=computer)(|(cn=${name})(dNSHostName=${name})))`; - - this.ad.find({ filter, attributes }, (err, result) => { - if (err) return reject(err); - resolve(result?.other?.[0] || null); - }); - }); - } - - - /** - * Alle Computer - */ - async getComputers(attributes = this.computerAttributes) { - const options = { - baseDN: this.ad.baseDN, - filter: '(objectClass=computer)', - attributes - }; - - const result = await this.ldapSearch(options); - return result.other || []; - } - - - /** - * Alle Computer aus einer OU holen - */ - async getComputersFromOU(ouDn, attributes = this.computerAttributes) { - const options = { - baseDN: ouDn, - filter: '(objectClass=computer)', - attributes - }; - - const result = await this.ldapSearch(options); - return result.other || []; - } - - /** - * Computer suchen (Wildcard möglich) - * Beispiele: "PC-*", "*LAPTOP*", "SRV01" - */ - async findComputers(query, attributes = this.computerAttributes) { - const filter = `(&(objectClass=computer)(|(cn=${query})(dNSHostName=${query})))`; - - const result = await this.ldapSearch({ + const groups = await this.ldapSearch({ filter, attributes }); - return result.other || []; + + return groups[0] || null; } - /** - * ----------------------------------------------------- - * GROUP MEMBERSHIP (DIRECT & RECURSIVE) - * ----------------------------------------------------- - */ + async findGroups(query, attributes = this.groupAttributes) { - async isUserMemberOfDirect(username, groupName) { - return new Promise((resolve, reject) => { - this.ad.isUserMemberOf(username, groupName, (err, isMember) => { - if (err) return reject(err); - resolve(isMember); - }); + const safe = this.escape(query); + + const filter = + `(&(objectClass=group)(cn=${safe}))`; + + return await this.ldapSearch({ + filter, + attributes }); } - async isUserMemberOfRecursive(username, groupName, visited = new Set()) { - const key = groupName.toLowerCase(); - if (visited.has(key)) return false; - visited.add(key); + async getAllGroups({ + attributes = this.groupAttributes, + includeMembers = false, + memberAttributes = this.userAttributes || [ + 'objectGUID', + 'cn', + 'sAMAccountName', + 'mail', + 'distinguishedName' + ] + } = {}) { - const direct = await this.isUserMemberOfDirect(username, groupName); - if (direct) return true; + const finalAttributes = [...new Set([ + ...attributes, + ...(includeMembers ? ['member'] : []) + ])]; - const group = await this.getGroup(groupName); - if (!group || !Array.isArray(group.member)) return false; + const filter = '(objectClass=group)'; - for (const dn of group.member) { - const match = dn.match(/CN=([^,]+)/i); - if (!match) continue; - - const subGroupName = match[1]; - const found = await this.isUserMemberOfRecursive(username, subGroupName, visited); - if (found) return true; - } - - return false; - } - - async getGroupSubgroups(groupName, visited = new Set()) { - const key = groupName.toLowerCase(); - if (visited.has(key)) return []; - - visited.add(key); - - const group = await this.getGroup(groupName); - if (!group || !Array.isArray(group.member)) return []; + const groups = await this.ldapSearch({ + filter, + attributes: finalAttributes + }); const results = []; - for (const memberDN of group.member) { - const match = memberDN.match(/CN=([^,]+)/i); - if (!match) continue; + for (const group of groups) { - const subGroupName = match[1]; - const sub = await this.getGroup(subGroupName).catch(() => null); - if (!sub) continue; + const result = {}; - results.push(sub); - results.push(...await this.getGroupSubgroups(subGroupName, visited)); + // Gruppenattribute übernehmen + for (const attr of attributes) { + result[attr] = group[attr] ?? null; + } + + // Mitglieder laden + if (includeMembers) { + + const rawMembers = Array.isArray(group.member) + ? group.member + : group.member + ? [group.member] + : []; + + const members = []; + + for (const memberDN of rawMembers) { + + const memberResults = await this.ldapSearch({ + baseDN: memberDN, + filter: '(objectClass=*)', + attributes: memberAttributes + }); + + if (memberResults.length > 0) { + + const member = memberResults[0]; + + const normalizedMember = {}; + + for (const attr of memberAttributes) { + normalizedMember[attr] = member[attr] ?? null; + } + + members.push(normalizedMember); + } + } + + result.members = members; + } + + results.push(result); } return results; } - async getGroupRecursive(groupName, visited = new Set()) { + /** + * ----------------------------------------------------- + * COMPUTERS + * ----------------------------------------------------- + */ + + async getComputer(name, attributes = this.computerAttributes) { + + const safe = this.escape(name); + + const filter = + `(&(objectClass=computer)` + + `(|(cn=${safe})(dNSHostName=${safe})))`; + + const results = await this.ldapSearch({ + filter, + attributes + }); + + return results[0] || null; + } + + async getComputers(attributes = this.computerAttributes) { + + return await this.ldapSearch({ + filter: '(objectClass=computer)', + attributes + }); + } + + async getComputersFromOU( + ouDn, + attributes = this.computerAttributes + ) { + + return await this.ldapSearch({ + baseDN: ouDn, + filter: '(objectClass=computer)', + attributes + }); + } + + async findComputers( + query, + attributes = this.computerAttributes + ) { + + const safe = this.escape(query); + + const filter = + `(&(objectClass=computer)` + + `(|(cn=${safe})(dNSHostName=${safe})))`; + + return await this.ldapSearch({ + filter, + attributes + }); + } + + /** + * ----------------------------------------------------- + * GROUP MEMBERSHIP + * ----------------------------------------------------- + */ + + async isUserMemberOfDirect(username, groupName) { + + const user = await this.getUser( + username, + ['distinguishedName'] + ); + + if (!user) { + return false; + } + + const safeGroup = this.escape(groupName); + + const filter = + `(&(objectClass=group)` + + `(cn=${safeGroup})` + + `(member=${user.distinguishedName}))`; + + const results = await this.ldapSearch({ + filter, + attributes: ['cn'] + }); + + return results.length > 0; + } + + async isUserMemberOfRecursive( + username, + groupName, + visited = new Set() + ) { + const key = groupName.toLowerCase(); - if (visited.has(key)) return null; + + if (visited.has(key)) { + return false; + } + + visited.add(key); + + const direct = + await this.isUserMemberOfDirect( + username, + groupName + ); + + if (direct) { + return true; + } + + const group = await this.getGroup( + groupName, + ['member'] + ); + + if (!group || !group.member) { + return false; + } + + const members = Array.isArray(group.member) + ? group.member + : [group.member]; + + for (const dn of members) { + + const match = dn.match(/CN=([^,]+)/i); + + if (!match) { + continue; + } + + const subGroupName = match[1]; + + const found = + await this.isUserMemberOfRecursive( + username, + subGroupName, + visited + ); + + if (found) { + return true; + } + } + + return false; + } + + async getGroupSubgroups( + groupName, + visited = new Set() + ) { + + const key = groupName.toLowerCase(); + + if (visited.has(key)) { + return []; + } + + visited.add(key); + + const group = await this.getGroup( + groupName, + ['member'] + ); + + if (!group || !group.member) { + return []; + } + + const members = Array.isArray(group.member) + ? group.member + : [group.member]; + + const results = []; + + for (const dn of members) { + + const match = dn.match(/CN=([^,]+)/i); + + if (!match) { + continue; + } + + const subGroupName = match[1]; + + const subGroup = await this.getGroup( + subGroupName + ).catch(() => null); + + if (!subGroup) { + continue; + } + + results.push(subGroup); + + results.push( + ...await this.getGroupSubgroups( + subGroupName, + visited + ) + ); + } + + return results; + } + + async getGroupRecursive( + groupName, + visited = new Set() + ) { + + const key = groupName.toLowerCase(); + + if (visited.has(key)) { + return null; + } visited.add(key); const group = await this.getGroup(groupName); - if (!group) return null; + + if (!group) { + return null; + } const result = { ...group, subgroups: [] }; - if (!Array.isArray(group.member)) return result; + if (!group.member) { + return result; + } - for (const memberDN of group.member) { - const match = memberDN.match(/CN=([^,]+)/i); - if (!match) continue; + const members = Array.isArray(group.member) + ? group.member + : [group.member]; + + for (const dn of members) { + + const match = dn.match(/CN=([^,]+)/i); + + if (!match) { + continue; + } const subGroupName = match[1]; - const subTree = await this.getGroupRecursive(subGroupName, visited); - if (subTree) result.subgroups.push(subTree); + + const subTree = + await this.getGroupRecursive( + subGroupName, + visited + ); + + if (subTree) { + result.subgroups.push(subTree); + } } return result; @@ -281,13 +582,10 @@ class ActiveDirectoryManager { module.exports = ActiveDirectoryManager; - - - - -// const ldap = require('ldapjs'); +// const { Client } = require('ldapts'); // class ActiveDirectoryManager { + // constructor({ // url, // baseDN, @@ -297,6 +595,7 @@ module.exports = ActiveDirectoryManager; // groupAttributes = [], // computerAttributes = [] // }) { + // this.url = url; // this.baseDN = baseDN; // this.username = username; @@ -305,50 +604,30 @@ module.exports = ActiveDirectoryManager; // this.userAttributes = userAttributes; // this.groupAttributes = groupAttributes; // this.computerAttributes = computerAttributes; +// } -// this.client = ldap.createClient({ +// /** +// * ----------------------------------------------------- +// * CONNECTION +// * ----------------------------------------------------- +// */ + +// async createClient() { + +// const client = new Client({ // url: this.url, -// reconnect: true, // timeout: 10000, // connectTimeout: 10000 // }); + +// await client.bind(this.username, this.password); + +// return client; // } // /** // * ----------------------------------------------------- -// * CONNECTION HANDLING -// * ----------------------------------------------------- -// */ -// async bind() { -// return new Promise((resolve, reject) => { -// this.client.bind(this.username, this.password, (err) => { -// if (err) return reject(err); -// resolve(); -// }); -// }); -// } - -// async unbind() { -// return new Promise((resolve, reject) => { -// this.client.unbind(err => { -// if (err) return reject(err); -// resolve(); -// }); -// }); -// } - -// async withConnection(fn) { -// try { -// await this.bind(); -// return await fn(); -// } finally { -// await this.unbind(); -// } -// } - -// /** -// * ----------------------------------------------------- -// * INTERNAL HELPERS +// * HELPERS // * ----------------------------------------------------- // */ @@ -356,130 +635,276 @@ module.exports = ActiveDirectoryManager; // return String(value).replace(/[*()\\]/g, '\\$&'); // } -// async ldapSearch({ baseDN = this.baseDN, filter, attributes = [] }) { -// const opts = { -// filter, -// scope: 'sub', -// attributes, -// paged: true -// }; +// bufferToGuid(buffer) { -// return new Promise((resolve, reject) => { -// const results = []; +// const hex = buffer.toString('hex'); -// this.client.search(baseDN, opts, (err, res) => { -// if (err) return reject(err); +// return [ +// hex.substring(6, 8), +// hex.substring(4, 6), +// hex.substring(2, 4), +// hex.substring(0, 2), +// '-', +// hex.substring(10, 12), +// hex.substring(8, 10), +// '-', +// hex.substring(14, 16), +// hex.substring(12, 14), +// '-', +// hex.substring(16, 20), +// '-', +// hex.substring(20) +// ].join(''); +// } -// res.on('searchEntry', (entry) => { -// results.push(entry.object); -// }); +// normalizeEntry(entry) { -// res.on('error', (err) => reject(err)); -// res.on('end', () => resolve(results)); +// const obj = {}; + +// for (const [key, value] of Object.entries(entry)) { + +// let normalized = value; + +// // GUID konvertieren +// if (key === 'objectGUID' && value instanceof Buffer) { +// normalized = this.bufferToGuid(value); +// } + +// // Arrays mit einem Wert -> String +// else if (Array.isArray(value)) { + +// if (value.length === 0) { +// normalized = null; +// } + +// else if (value.length === 1) { +// normalized = value[0]; +// } +// } + +// obj[key] = normalized; +// } +// obj['ObjectSource_ID'] = 2; +// return obj; +// } + +// /** +// * ----------------------------------------------------- +// * GENERIC SEARCH +// * ----------------------------------------------------- +// */ + +// async ldapSearch({ +// baseDN = this.baseDN, +// filter, +// attributes = [] +// }) { + +// const client = await this.createClient(); + +// try { + +// const { searchEntries } = await client.search(baseDN, { +// scope: 'sub', +// filter, +// attributes, +// paged: true // }); -// }); + +// return searchEntries.map(entry => +// this.normalizeEntry(entry) +// ); + +// } finally { +// await client.unbind(); +// } // } // /** // * ----------------------------------------------------- -// * USER FUNCTIONS +// * USERS // * ----------------------------------------------------- // */ // async getUser(username, attributes = this.userAttributes) { + // const safe = this.escape(username); -// const filter = `(&(objectCategory=person)(objectClass=user)(|(sAMAccountName=${safe})(mail=${safe})(cn=${safe}))))`; +// const filter = +// `(&(objectCategory=person)(objectClass=user)` + +// `(|(sAMAccountName=${safe})(mail=${safe})(cn=${safe})))`; -// const res = await this.ldapSearch({ filter, attributes }); -// return res[0] || null; +// const users = await this.ldapSearch({ +// filter, +// attributes +// }); + +// return users[0] || null; // } -// async getUserDN(username) { -// const user = await this.getUser(username); -// return user?.distinguishedName || null; -// } +// async getAllUsers(attributes = [ +// 'objectGUID', +// 'sAMAccountName', +// 'mail', +// 'givenName', +// 'sn', +// 'employeeID', +// 'title', +// 'department', +// 'streetAddress', +// 'telephoneNumber', +// 'physicalDeliveryOfficeName', +// 'distinguishedName' +// ]) { -// async findUsers(query, attributes = this.userAttributes) { -// const safe = this.escape(query); - -// const filter = `(&(objectCategory=person)(objectClass=user)(|(cn=${safe})(sAMAccountName=${safe})(mail=${safe})(displayName=${safe})))`; - -// return await this.ldapSearch({ filter, attributes }); -// } - -// async getAllUsers(attributes = this.userAttributes) { -// const filter = '(&(objectCategory=person)(objectClass=user))'; - -// return await this.ldapSearch({ filter, attributes }); -// } - -// /** -// * ----------------------------------------------------- -// * GROUP FUNCTIONS -// * ----------------------------------------------------- -// */ - -// async getGroup(groupName, attributes = this.groupAttributes) { -// const safe = this.escape(groupName); - -// const filter = `(&(objectClass=group)(cn=${safe}))`; - -// const res = await this.ldapSearch({ filter, attributes }); -// return res[0] || null; -// } - -// async findGroups(query, attributes = this.groupAttributes) { -// const safe = this.escape(query); - -// const filter = `(&(objectClass=group)(cn=${safe}))`; - -// return await this.ldapSearch({ filter, attributes }); -// } - -// async getAllGroups(attributes = this.groupAttributes) { -// const filter = '(objectClass=group)'; - -// return await this.ldapSearch({ filter, attributes }); -// } - -// /** -// * ----------------------------------------------------- -// * COMPUTER / OU FUNCTIONS -// * ----------------------------------------------------- -// */ - -// async getComputer(name, attributes = this.computerAttributes) { -// const safe = this.escape(name); - -// const filter = `(&(objectClass=computer)(|(cn=${safe})(dNSHostName=${safe})))`; - -// const res = await this.ldapSearch({ filter, attributes }); -// return res[0] || null; -// } - -// async getComputers(attributes = this.computerAttributes) { -// const filter = '(objectClass=computer)'; - -// return await this.ldapSearch({ filter, attributes }); -// } - -// async getComputersFromOU(ouDn, attributes = this.computerAttributes) { -// const filter = '(objectClass=computer)'; +// const filter = +// '(&(objectCategory=person)(objectClass=user))'; // return await this.ldapSearch({ -// baseDN: ouDn, // filter, // attributes // }); // } -// async findComputers(query, attributes = this.computerAttributes) { +// async findUsers(query, attributes = this.userAttributes) { + // const safe = this.escape(query); -// const filter = `(&(objectClass=computer)(|(cn=${safe})(dNSHostName=${safe})))`; +// const filter = +// `(&(objectClass=user)` + +// `(|(cn=${safe})` + +// `(sAMAccountName=${safe})` + +// `(mail=${safe})` + +// `(displayName=${safe})))`; -// return await this.ldapSearch({ filter, attributes }); +// return await this.ldapSearch({ +// filter, +// attributes +// }); +// } + +// /** +// * ----------------------------------------------------- +// * GROUPS +// * ----------------------------------------------------- +// */ + +// async getGroup(groupName, attributes = this.groupAttributes) { + +// const safe = this.escape(groupName); + +// const filter = +// `(&(objectClass=group)(cn=${safe}))`; + +// const groups = await this.ldapSearch({ +// filter, +// attributes +// }); + +// return groups[0] || null; +// } + +// /** +// * ----------------------------------------------------- +// * ALL GROUPS +// * ----------------------------------------------------- +// */ + +// /** +// * ----------------------------------------------------- +// * ALL GROUPS +// * ----------------------------------------------------- +// */ + +// async getAllGroups({ attributes = this.groupAttributes, includeMembers = false } = {}) { + +// const finalAttributes = [...new Set([ +// ...attributes, +// ...(includeMembers ? ['member'] : []) +// ])]; + +// const filter = '(objectClass=group)'; + +// const groups = await this.ldapSearch({ +// filter, +// attributes: finalAttributes +// }); + +// return groups.map(group => { + +// const result = {}; + +// // Nur definierte Attribute übernehmen +// for (const attr of attributes) { +// result[attr] = group[attr] ?? null; +// } +// result['ObjectSource_ID'] = 2; + +// // Optional Mitglieder hinzufügen +// if (includeMembers) { + +// const rawMembers = Array.isArray(group.member) +// ? group.member +// : group.member +// ? [group.member] +// : []; + +// result.members = rawMembers.map(memberDN => { + +// const match = memberDN.match(/CN=([^,]+)/i); + +// return { +// distinguishedName: memberDN, +// cn: match ? match[1] : null +// }; +// }); +// } + +// return result; +// }); +// } + +// async findGroups(query, attributes = this.groupAttributes) { + +// const safe = this.escape(query); + +// const filter = +// `(&(objectClass=group)(cn=${safe}))`; + +// return await this.ldapSearch({ +// filter, +// attributes +// }); +// } + +// /** +// * ----------------------------------------------------- +// * COMPUTERS +// * ----------------------------------------------------- +// */ + +// async getComputers(attributes = this.computerAttributes) { + +// return await this.ldapSearch({ +// filter: '(objectClass=computer)', +// attributes +// }); +// } + +// async getComputer(name, attributes = this.computerAttributes) { + +// const safe = this.escape(name); + +// const filter = +// `(&(objectClass=computer)` + +// `(|(cn=${safe})(dNSHostName=${safe})))`; + +// const results = await this.ldapSearch({ +// filter, +// attributes +// }); + +// return results[0] || null; // } // /** @@ -489,100 +914,29 @@ module.exports = ActiveDirectoryManager; // */ // async isUserMemberOfDirect(username, groupName) { -// const user = await this.getUser(username, ['distinguishedName']); -// if (!user) return false; -// const userDN = user.distinguishedName; +// const user = await this.getUser(username, [ +// 'distinguishedName' +// ]); + +// if (!user) { +// return false; +// } + // const safeGroup = this.escape(groupName); -// const filter = `(&(objectClass=group)(cn=${safeGroup})(member=${userDN}))`; +// const filter = +// `(&(objectClass=group)` + +// `(cn=${safeGroup})` + +// `(member=${user.distinguishedName}))`; -// const res = await this.ldapSearch({ filter, attributes: ['cn'] }); -// return res.length > 0; -// } +// const results = await this.ldapSearch({ +// filter, +// attributes: ['cn'] +// }); -// async isUserMemberOfRecursive(username, groupName, visited = new Set()) { -// const key = groupName.toLowerCase(); -// if (visited.has(key)) return false; -// visited.add(key); - -// const direct = await this.isUserMemberOfDirect(username, groupName); -// if (direct) return true; - -// const group = await this.getGroup(groupName, ['member']); -// if (!group || !group.member) return false; - -// const members = Array.isArray(group.member) ? group.member : [group.member]; - -// for (const dn of members) { -// const match = dn.match(/CN=([^,]+)/i); -// if (!match) continue; - -// const subGroup = match[1]; -// const found = await this.isUserMemberOfRecursive(username, subGroup, visited); -// if (found) return true; -// } - -// return false; -// } - -// async getGroupSubgroups(groupName, visited = new Set()) { -// const key = groupName.toLowerCase(); -// if (visited.has(key)) return []; - -// visited.add(key); - -// const group = await this.getGroup(groupName, ['member']); -// if (!group || !group.member) return []; - -// const members = Array.isArray(group.member) ? group.member : [group.member]; - -// const results = []; - -// for (const dn of members) { -// const match = dn.match(/CN=([^,]+)/i); -// if (!match) continue; - -// const subGroupName = match[1]; -// const sub = await this.getGroup(subGroupName).catch(() => null); -// if (!sub) continue; - -// results.push(sub); -// results.push(...await this.getGroupSubgroups(subGroupName, visited)); -// } - -// return results; -// } - -// async getGroupRecursive(groupName, visited = new Set()) { -// const key = groupName.toLowerCase(); -// if (visited.has(key)) return null; - -// visited.add(key); - -// const group = await this.getGroup(groupName); -// if (!group) return null; - -// const result = { -// ...group, -// subgroups: [] -// }; - -// if (!group.member) return result; - -// const members = Array.isArray(group.member) ? group.member : [group.member]; - -// for (const dn of members) { -// const match = dn.match(/CN=([^,]+)/i); -// if (!match) continue; - -// const subGroupName = match[1]; -// const subTree = await this.getGroupRecursive(subGroupName, visited); -// if (subTree) result.subgroups.push(subTree); -// } - -// return result; +// return results.length > 0; // } // } -// module.exports = ActiveDirectoryManager; +// module.exports = ActiveDirectoryManager; \ No newline at end of file diff --git a/src/services/authenticationManager.js b/src/services/authenticationManager.js index 368907e..63837ce 100644 --- a/src/services/authenticationManager.js +++ b/src/services/authenticationManager.js @@ -57,6 +57,10 @@ class AuthenticationManager { return { token: null, levelId: 2, message: 'Falsches Passwort' }; } + if(!user.active) { + return { token: null, levelId: 2, message: 'Benutzer nicht aktiv' }; + } + const token = jwt.sign( { ObjectGUID: user.ObjectGUID, @@ -67,12 +71,26 @@ class AuthenticationManager { ); user.refreshtoken = token; - user.online = true; + await this.setOnline(sAMAccountName); await user.save(); return { token, levelId: 0, message: 'Erfolgreich angemeldet' }; } + + async setOnline(sAMAccountName) { + const user = await this.findUser(sAMAccountName); + user.online = true; + await user.save(); + } + + async setOffline(sAMAccountName) { + const user = await this.findUser(sAMAccountName); + user.online = false; + await user.save(); + } + + // ========================================================= // LOGOUT // ========================================================= @@ -85,8 +103,8 @@ class AuthenticationManager { } user.refreshtoken = null; - user.online = false; await user.save(); + await this.setOffline(); return { token: null, levelId: 0, message: 'Erfolgreich abgemeldet' }; } diff --git a/src/services/rbacManager.js b/src/services/rbacManager.js index f9d1219..3efd05d 100644 --- a/src/services/rbacManager.js +++ b/src/services/rbacManager.js @@ -190,8 +190,9 @@ class RBACManager { return res.redirect('/login'); } - const user = await this.db.get('authentication').findOne( { where: { sAMAccountName } } ); + req.user = {}; + const user = await this.db.get('authentication').findOne( { where: { sAMAccountName } } ); if (!user || !user.active) { return res.redirect('/login'); } @@ -217,6 +218,7 @@ class RBACManager { permissions: normalized, isSuperAdmin }; + next(); } catch (err) { @@ -254,19 +256,17 @@ async syncAuthByActiveDirectory() { const all = await this.service.get('activeDirectoryManager').getAllUsers(); all.forEach(async user => { - user.userAccountControl = user.userAccountControl_ID; - user.distinguishedName = user.distinguishedName; - - await auth.upsert({ - objectGUID: user.ObjectGUID, - ObjectSource_ID: 2, - userAccountControl_ID: user.userAccountControl, - mail: user.mail, - displayName: user.displayName - }); + try { + if(user.objectGUID !== null) { + await auth.upsert({...user, ObjectGUID: user.objectGUID}); + } else { + } + } catch(err) { + throw err; + } }) - // return all; + return all; } @@ -319,6 +319,27 @@ async createGroup(data) { }); } +async syncGroupByActiveDirectory() { + const group = await this.db.get('group'); + const all = await this.service.get('activeDirectoryManager').getAllGroups({ + attributes: ['objectGUID', 'sAMAccountName', 'distinguishedName'], + includeMembers: false + }); + + all.forEach(async adGroup => { + try { + if(adGroup.objectGUID !== null) { + await group.upsert({...adGroup, ObjectGUID: adGroup.objectGUID, Name: adGroup.sAMAccountName}); + } else { + } + } catch(err) { + throw err; + } + }) + + return all; +} + async updateGroup(id, data) { const Group = this.db.get('group'); @@ -367,6 +388,225 @@ async removeUserFromGroup(authId, groupId) { }); } +async syncAuthenticationGroupsFromAD() { + + const AuthGroups = this.db.get('authenticationGroupsModel'); + + const users = await this.service + .get('activeDirectoryManager') + .getAllUsers([ + 'objectGUID', + 'memberOf' + ]); + + const groups = await this.service + .get('activeDirectoryManager') + .getAllGroups({ + attributes: [ + 'objectGUID', + 'distinguishedName' + ] + }); + + /** + * ----------------------------------------------------- + * GROUP MAP (DN -> GUID) + * ----------------------------------------------------- + */ + + const groupMap = new Map(); + + for (const g of groups) { + + if (g.distinguishedName && g.objectGUID) { + groupMap.set( + g.distinguishedName.toLowerCase(), + g.objectGUID + ); + } + } + + /** + * ----------------------------------------------------- + * BUILD RELATIONS + * ----------------------------------------------------- + */ + + const relations = []; + + for (const user of users) { + + if (!user.objectGUID) continue; + + const memberOf = Array.isArray(user.memberOf) + ? user.memberOf + : user.memberOf + ? [user.memberOf] + : []; + + for (const dn of memberOf) { + + const groupGuid = groupMap.get(dn.toLowerCase()); + + if (!groupGuid) continue; + + relations.push({ + Authentication_ObjectGUID: user.objectGUID, + Group_ObjectGUID: groupGuid + }); + } + } + + /** + * ----------------------------------------------------- + * DB SYNC (clean rebuild) + * ----------------------------------------------------- + */ + + // await AuthGroups.destroy({ + // where: {} + // }); + + if (relations.length) { + await AuthGroups.upsert(relations, { + // ignoreDuplicates: true + }); + } + + return relations; +} + + +async syncGroupClosureFromAD() { + + const GroupClosure = this.db.get('groupClosureModel'); + + const groups = await this.service + .get('activeDirectoryManager') + .getAllGroups({ + attributes: [ + 'objectGUID', + 'distinguishedName', + 'member' + ], + includeMembers: false + }); + + /** + * ----------------------------------------------------- + * MAP: DN -> GUID + * ----------------------------------------------------- + */ + + const groupMap = new Map(); + + for (const g of groups) { + if (g.distinguishedName && g.objectGUID) { + groupMap.set( + g.distinguishedName.toLowerCase(), + g.objectGUID + ); + } + } + + /** + * ----------------------------------------------------- + * BUILD DIRECT RELATIONS + * ----------------------------------------------------- + */ + + const edges = []; + + for (const group of groups) { + + if (!group.objectGUID) continue; + + const members = Array.isArray(group.member) + ? group.member + : group.member + ? [group.member] + : []; + + for (const memberDN of members) { + + const childGUID = groupMap.get( + memberDN.toLowerCase() + ); + + if (!childGUID) continue; + + edges.push({ + ParentGroup_ObjectGUID: group.objectGUID, + ChildGroup_ObjectGUID: childGUID, + Depth: 1 + }); + } + } + + /** + * ----------------------------------------------------- + * TRANSITIVE CLOSURE (DFS) + * ----------------------------------------------------- + */ + + const closure = [...edges]; + + const adjacency = new Map(); + + for (const e of edges) { + + if (!adjacency.has(e.ParentGroup_ObjectGUID)) { + adjacency.set(e.ParentGroup_ObjectGUID, []); + } + + adjacency.get(e.ParentGroup_ObjectGUID) + .push(e.ChildGroup_ObjectGUID); + } + + const visitedCache = new Map(); + + const dfs = (root, current, depth) => { + + const children = adjacency.get(current) || []; + + for (const child of children) { + + closure.push({ + ParentGroup_ObjectGUID: root, + ChildGroup_ObjectGUID: child, + Depth: depth + 1 + }); + + dfs(root, child, depth + 1); + } + }; + + for (const group of groups) { + + const root = group.objectGUID; + if (!root) continue; + + dfs(root, root, 0); + } + + /** + * ----------------------------------------------------- + * CLEAN & SAVE + * ----------------------------------------------------- + */ + + // await GroupClosure.destroy({ + // where: {} + // }); + + await GroupClosure.bulkCreate(closure, { + ignoreDuplicates: true + }); + + return closure.length; +} + + // ========================================================= // 🎭 ROLE CRUD // ========================================================= diff --git a/src/services/socketManager.js b/src/services/socketManager.js index 0612fc7..d9d54ad 100644 --- a/src/services/socketManager.js +++ b/src/services/socketManager.js @@ -25,11 +25,11 @@ class SocketManager { userName: sAMAccountName }); - // console.log(`${sAMAccountName} [${objectGuid}] connected to ${namespace}`); + console.log(`${sAMAccountName} [${objectGuid}] connected to ${namespace}`); socket.on('disconnect', () => { clients.delete(objectGuid); - // console.log(`${sAMAccountName} [${objectGuid}] disconnected from ${namespace}`); + console.log(`${sAMAccountName} [${objectGuid}] disconnected from ${namespace}`); }); }); diff --git a/src/sockets/mainSocket.js b/src/sockets/mainSocket.js index bdbe2a3..96ca2c1 100644 --- a/src/sockets/mainSocket.js +++ b/src/sockets/mainSocket.js @@ -1,5 +1,7 @@ const path = require('path'); const fs = require('fs'); +const { cache } = require('@root/globalize.js'); +const { service } = require('@root/server.js'); module.exports = (app, socketManager, namespace, pluginManager, authenticationModel, fileSystemManager, eventManager, activeDirectory) => { @@ -12,9 +14,26 @@ module.exports = (app, socketManager, namespace, pluginManager, authenticationMo // eventManager.write(data.objectGuid, data.levelId, data.pluginName, data.message) }); - // global.json.configuration.onChange(info => { - // console.log(info.delta) - // }); + + + socketManager.io.on('connection', async (socket) => { + if(!socket.handshake.auth.objectGuid) { + return; + } + await service.get('authenticationManager').setOnline(socket.handshake.auth.sAMAccountName); + }); + + socketManager.on(namespace, 'disconnect', (socket, data) => { + if(!socket.handshake.auth.objectGuid) { + return; + } + + setTimeout(async () => { + if(!socketManager.clients.get(namespace).has(socket.handshake.auth.objectGuid)) { + await service.get('authenticationManager').setOffline(socket.handshake.auth.sAMAccountName); + } + }, 8000); + }); mainSocket.on('connection', socket => {