From 00250d2904880f45537de575cd0d16d35b016b62 Mon Sep 17 00:00:00 2001 From: alphaeusmote <41258468-alphaeusmote@users.noreply.replit.com> Date: Wed, 9 Apr 2025 03:41:16 +0000 Subject: [PATCH] Add Active Directory group management functionality Replit-Commit-Author: Agent Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/6f419947-5d2b-4028-a9f5-2456de5a48d3.jpg --- package-lock.json | 215 ++++++++++++++++++++++++++++++ package.json | 1 + replit.nix | 5 + server/routes.ts | 323 ++++++++++++++++++++++++++++++++++++++++++++++ shared/schema.ts | 1 + 5 files changed, 545 insertions(+) create mode 100644 replit.nix diff --git a/package-lock.json b/package-lock.json index 6a74196..c96b995 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,6 +68,7 @@ "input-otp": "^1.2.4", "ioredis": "^5.6.0", "jsonwebtoken": "^9.0.2", + "ldapjs": "^3.0.7", "lucide-react": "^0.453.0", "memorystore": "^1.6.7", "morgan": "^1.10.0", @@ -1514,6 +1515,101 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "license": "MIT" }, + "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", + "license": "MIT" + }, + "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", + "license": "MIT", + "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", + "license": "MIT", + "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", + "license": "MIT", + "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", + "license": "MIT" + }, + "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", + "license": "MIT", + "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", + "license": "MIT", + "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", + "license": "MIT", + "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", + "license": "MIT" + }, "node_modules/@neondatabase/serverless": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-0.10.4.tgz", @@ -3786,6 +3882,12 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "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==", + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3872,6 +3974,15 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -3910,6 +4021,18 @@ "postcss": "^8.1.0" } }, + "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==", + "license": "MIT", + "dependencies": { + "precond": "0.2" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4369,6 +4492,12 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "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==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5546,6 +5675,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "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" + ], + "license": "MIT" + }, "node_modules/fast-equals": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", @@ -6204,6 +6342,29 @@ "safe-buffer": "^5.0.1" } }, + "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", + "license": "MIT", + "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_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -7217,6 +7378,20 @@ "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", "license": "MIT" }, + "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-warning": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", + "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8953,6 +9128,32 @@ "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" + ], + "license": "MIT", + "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" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/vaul": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.1.tgz", @@ -8966,6 +9167,20 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0" } }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", diff --git a/package.json b/package.json index da42777..39b9027 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "input-otp": "^1.2.4", "ioredis": "^5.6.0", "jsonwebtoken": "^9.0.2", + "ldapjs": "^3.0.7", "lucide-react": "^0.453.0", "memorystore": "^1.6.7", "morgan": "^1.10.0", diff --git a/replit.nix b/replit.nix new file mode 100644 index 0000000..404c2dd --- /dev/null +++ b/replit.nix @@ -0,0 +1,5 @@ +{pkgs}: { + deps = [ + pkgs.postgresql + ]; +} diff --git a/server/routes.ts b/server/routes.ts index d3ab166..622a8ba 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -19,6 +19,7 @@ import { requireAdmin, initializeRBAC } from "./authorization"; +import { ldapClient } from "./ldap"; // Extend Express Request to include user property interface Request extends ExpressRequest { @@ -1003,6 +1004,148 @@ export async function registerRoutes(app: Express): Promise { next(error); } }); + + /** + * @swagger + * /api/connections/{connectionId}/ad-groups: + * post: + * summary: Create a new Active Directory group + * tags: [AD Groups] + * security: + * - cookieAuth: [] + * - bearerAuth: [] + * parameters: + * - in: path + * name: connectionId + * required: true + * schema: + * type: integer + * description: LDAP connection ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * - parentDN + * properties: + * name: + * type: string + * description: The name of the group (will be used for cn and sAMAccountName) + * parentDN: + * type: string + * description: The distinguished name of the parent container (OU or domain) + * description: + * type: string + * description: Group description + * groupType: + * type: string + * enum: ['Global', 'DomainLocal', 'Universal'] + * default: 'Global' + * description: Group scope type + * groupCategory: + * type: string + * enum: ['Security', 'Distribution'] + * default: 'Security' + * description: Group category type + * responses: + * 201: + * description: Group created successfully + * 400: + * description: Invalid input + * 401: + * description: Unauthorized + * 403: + * description: Forbidden + * 500: + * description: Server error + */ + app.post("/api/connections/:connectionId/ad-groups", authenticateApiToken, requirePermission(PERMISSIONS.CREATE_AD_GROUPS), async (req: Request, res, next) => { + try { + const connectionId = parseInt(req.params.connectionId); + const { name, parentDN, description, groupType = 'Global', groupCategory = 'Security' } = req.body; + + // Basic validation + if (!name || !parentDN) { + return res.status(400).json({ message: "Group name and parent DN are required" }); + } + + // Get connection + const connection = await storage.getLdapConnection(connectionId); + if (!connection) { + return res.status(404).json({ message: "LDAP connection not found" }); + } + + // Create the DN for the new group + const groupDN = `CN=${name},${parentDN}`; + + // Set the group type value based on type and category + const groupTypeValue = + (groupCategory === 'Security' ? 0x80000000 : 0) | + (groupType === 'Global' ? 0x2 : groupType === 'Universal' ? 0x8 : 0x4); + + // Attributes for the new group + const groupAttributes = { + objectClass: ['top', 'group'], + cn: name, + sAMAccountName: name, + description: description || '', + groupType: groupTypeValue.toString() + }; + + // Create the group in AD + await ldapClient.createEntry(connectionId, groupDN, groupAttributes); + + // Search for the newly created group to get its attributes + const searchResults = await ldapClient.searchGroups(connectionId, `(&(objectClass=group)(cn=${name}))`, [ + 'objectGUID', 'distinguishedName', 'canonicalName', 'cn', 'sAMAccountName', 'description', 'groupType' + ]); + + if (!searchResults || searchResults.length === 0) { + return res.status(500).json({ message: "Group created but could not retrieve details" }); + } + + const adGroup = searchResults[0]; + + // Store in database + const storedGroup = await storage.createAdGroup({ + connectionId, + objectGUID: adGroup.objectGUID, + distinguishedName: adGroup.distinguishedName, + canonicalName: adGroup.canonicalName, + cn: adGroup.cn, + sAMAccountName: adGroup.sAMAccountName, + groupType: groupCategory + ' ' + groupType, + description: adGroup.description, + members: [], // No members initially + adProperties: adGroup // Store all attributes + }); + + // Add audit log + await storage.createAuditLogEntry({ + userId: req.user.id, + connectionId, + action: 'create', + targetId: storedGroup.id.toString(), + details: { + objectType: 'group', + objectName: name, + distinguishedName: groupDN, + groupType: groupCategory + ' ' + groupType + } + }); + + res.status(201).json(storedGroup); + } catch (err) { + console.error("Error creating AD group:", err); + if (err.message && err.message.includes('Entry Already Exists')) { + return res.status(409).json({ message: "Group already exists" }); + } + next(err); + } + }); /** * @swagger @@ -1353,6 +1496,186 @@ export async function registerRoutes(app: Express): Promise { next(error); } }); + + /** + * @swagger + * /api/connections/{connectionId}/ad-computers: + * post: + * summary: Create a new Active Directory computer + * tags: [AD Computers] + * security: + * - cookieAuth: [] + * - bearerAuth: [] + * parameters: + * - in: path + * name: connectionId + * required: true + * schema: + * type: integer + * description: LDAP connection ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * - parentDN + * properties: + * name: + * type: string + * description: The name of the computer (will be used for cn and sAMAccountName) + * parentDN: + * type: string + * description: The distinguished name of the parent container (OU or domain) + * description: + * type: string + * description: Computer description + * dnsHostName: + * type: string + * description: DNS host name of the computer + * operatingSystem: + * type: string + * description: Operating system of the computer + * operatingSystemVersion: + * type: string + * description: Operating system version + * enabled: + * type: boolean + * default: true + * description: Whether the computer account is enabled + * responses: + * 201: + * description: Computer created successfully + * 400: + * description: Invalid input + * 401: + * description: Unauthorized + * 403: + * description: Forbidden + * 500: + * description: Server error + */ + app.post("/api/connections/:connectionId/ad-computers", authenticateApiToken, requirePermission(PERMISSIONS.CREATE_AD_COMPUTERS), async (req: Request, res, next) => { + try { + const connectionId = parseInt(req.params.connectionId); + const { + name, + parentDN, + description, + dnsHostName, + operatingSystem, + operatingSystemVersion, + enabled = true + } = req.body; + + // Basic validation + if (!name || !parentDN) { + return res.status(400).json({ message: "Computer name and parent DN are required" }); + } + + // Get connection + const connection = await storage.getLdapConnection(connectionId); + if (!connection) { + return res.status(404).json({ message: "LDAP connection not found" }); + } + + // Create the DN for the new computer + const computerDN = `CN=${name},${parentDN}`; + + // The sAMAccountName needs to end with $ + const sAMAccountName = name.endsWith('$') ? name : `${name}$`; + + // Create the userAccountControl value + // 4096 = WORKSTATION_TRUST_ACCOUNT + // 2 = ACCOUNTDISABLE (if not enabled) + const userAccountControl = enabled ? 4096 : 4098; + + // Set the dnsHostName if not provided + const actualDnsHostName = dnsHostName || `${name}.${connection.domain}`; + + // Attributes for the new computer + const computerAttributes: { + objectClass: string[]; + cn: string; + sAMAccountName: string; + userAccountControl: string; + description: string; + dNSHostName: string; + operatingSystem?: string; + operatingSystemVersion?: string; + } = { + objectClass: ['top', 'computer'], + cn: name, + sAMAccountName: sAMAccountName, + userAccountControl: userAccountControl.toString(), + description: description || '', + dNSHostName: actualDnsHostName + }; + + // Add operating system information if provided + if (operatingSystem) { + computerAttributes.operatingSystem = operatingSystem; + } + + if (operatingSystemVersion) { + computerAttributes.operatingSystemVersion = operatingSystemVersion; + } + + // Create the computer in AD + await ldapClient.createEntry(connectionId, computerDN, computerAttributes); + + // Search for the newly created computer to get its attributes + const searchResults = await ldapClient.searchComputers(connectionId, `(&(objectClass=computer)(cn=${name}))`, [ + 'objectGUID', 'distinguishedName', 'canonicalName', 'cn', 'sAMAccountName', 'description', + 'dNSHostName', 'operatingSystem', 'operatingSystemVersion', 'userAccountControl' + ]); + + if (!searchResults || searchResults.length === 0) { + return res.status(500).json({ message: "Computer created but could not retrieve details" }); + } + + const adComputer = searchResults[0]; + + // Store in database + const storedComputer = await storage.createAdComputer({ + connectionId, + objectGUID: adComputer.objectGUID, + distinguishedName: adComputer.distinguishedName, + canonicalName: adComputer.canonicalName, + cn: adComputer.cn, + name: adComputer.cn, + sAMAccountName: adComputer.sAMAccountName, + dnsHostName: adComputer.dNSHostName, + operatingSystem: adComputer.operatingSystem, + operatingSystemVersion: adComputer.operatingSystemVersion, + enabled: (parseInt(adComputer.userAccountControl) & 2) === 0, // Check if ACCOUNTDISABLE flag is not set + adProperties: adComputer // Store all attributes + }); + + // Add audit log + await storage.createAuditLogEntry({ + userId: req.user.id, + connectionId, + action: 'create', + targetId: storedComputer.id.toString(), + details: { + distinguishedName: computerDN, + enabled: enabled, + name: name + } + }); + + res.status(201).json(storedComputer); + } catch (err) { + console.error("Error creating AD computer:", err); + if (err.message && err.message.includes('Entry Already Exists')) { + return res.status(409).json({ message: "Computer already exists" }); + } + next(err); + } + }); /** * @swagger diff --git a/shared/schema.ts b/shared/schema.ts index 4247b58..ead01c2 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -198,6 +198,7 @@ export const adComputers = pgTable("ad_computers", { canonicalName: text("canonical_name"), cn: text("cn"), name: text("name").notNull(), + sAMAccountName: text("sam_account_name"), dnsHostName: text("dns_host_name"), operatingSystem: text("operating_system"), operatingSystemVersion: text("operating_system_version"),