From 9e2d0ce81e120ff2fda1b031e9ec86c762a94556 Mon Sep 17 00:00:00 2001 From: alphaeusmote <41258468-alphaeusmote@users.noreply.replit.com> Date: Wed, 9 Apr 2025 11:38:35 +0000 Subject: [PATCH] Add ability to manage the 'managedBy' attribute for AD objects 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/2b2008cc-a510-433c-9379-89ffac9ce6a0.jpg --- server/ldap.ts | 69 +++++++++++ server/routes.ts | 283 +++++++++++++++++++++++++++++++++++++++++++++- server/storage.ts | 108 ++++++++++++++++++ shared/schema.ts | 12 ++ 4 files changed, 471 insertions(+), 1 deletion(-) diff --git a/server/ldap.ts b/server/ldap.ts index cbacdc8..9fbdd2f 100644 --- a/server/ldap.ts +++ b/server/ldap.ts @@ -196,6 +196,75 @@ class LdapClient extends EventEmitter { }); }); } + + // Get the managed by attribute for an object + async getManagedBy(connectionId: number, dn: string): Promise { + const client = this.getClient(connectionId); + if (!client) throw new Error('LDAP connection not established'); + + return new Promise((resolve, reject) => { + client.search(dn, { + scope: 'base', + attributes: ['managedBy'] + }, (err, res) => { + if (err) { + reject(err); + return; + } + + let managedBy: string | null = null; + + res.on('searchEntry', (entry) => { + const attrs = entry.attributes; + for (const attr of attrs) { + if (attr.type === 'managedBy' && attr.vals && attr.vals.length > 0) { + managedBy = attr.vals[0].toString(); + } + } + }); + + res.on('error', (err) => { + reject(err); + }); + + res.on('end', (result) => { + if (result.status !== 0) { + reject(new Error(`LDAP search error: ${result.errorMessage}`)); + return; + } + resolve(managedBy); + }); + }); + }); + } + + // Set the managed by attribute for an object + async setManagedBy(connectionId: number, dn: string, managerDn: string | null): Promise { + const client = this.getClient(connectionId); + if (!client) throw new Error('LDAP connection not established'); + + const changes = []; + + if (managerDn === null) { + // Remove the managedBy attribute + changes.push(new ldap.Change({ + operation: 'delete', + modification: { + managedBy: [] + } + })); + } else { + // Add or replace the managedBy attribute + changes.push(new ldap.Change({ + operation: 'replace', + modification: { + managedBy: managerDn + } + })); + } + + return this.updateEntry(connectionId, dn, changes); + } } export const ldapClient = new LdapClient(); diff --git a/server/routes.ts b/server/routes.ts index 622a8ba..2de0a49 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -9,7 +9,8 @@ import { moveComputerSchema, moveUserSchema, addToGroupSchema, - removeFromGroupSchema + removeFromGroupSchema, + updateManagedBySchema } from "@shared/schema"; import { ZodError } from "zod"; import rateLimit from "express-rate-limit"; @@ -3083,6 +3084,286 @@ export async function registerRoutes(app: Express): Promise { } }); + /** + * @swagger + * /api/connections/{connectionId}/managed-by: + * get: + * summary: Get the 'managedBy' attribute for an AD object + * tags: [AD Objects] + * security: + * - bearerAuth: [] + * - cookieAuth: [] + * parameters: + * - name: connectionId + * in: path + * required: true + * schema: + * type: integer + * - name: objectGUID + * in: query + * required: true + * schema: + * type: string + * - name: objectType + * in: query + * required: true + * schema: + * type: string + * enum: [user, group, computer, organizationalUnit] + * responses: + * 200: + * description: The managedBy attribute + * content: + * application/json: + * schema: + * type: object + * properties: + * managedBy: + * type: string + * nullable: true + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 404: + * description: Object not found + */ + app.get("/api/connections/:connectionId/managed-by", authenticateApiToken, async (req, res, next) => { + try { + const connectionId = parseInt(req.params.connectionId); + const connection = await storage.getLdapConnection(connectionId); + + if (!connection) { + return res.status(404).json({ message: "LDAP connection not found" }); + } + + const objectGUID = req.query.objectGUID as string; + const objectType = req.query.objectType as string; + + if (!objectGUID || !objectType) { + return res.status(400).json({ message: "objectGUID and objectType are required query parameters" }); + } + + if (!['user', 'group', 'computer', 'organizationalUnit'].includes(objectType)) { + return res.status(400).json({ message: "objectType must be one of: user, group, computer, organizationalUnit" }); + } + + // Get the object distinguished name + let objectDN; + switch (objectType) { + case 'user': + const adUser = await storage.getAdUserByObjectGUID(connectionId, objectGUID); + if (!adUser) { + return res.status(404).json({ message: "User not found" }); + } + objectDN = adUser.distinguishedName; + break; + case 'group': + const adGroup = await storage.getAdGroupByObjectGUID(connectionId, objectGUID); + if (!adGroup) { + return res.status(404).json({ message: "Group not found" }); + } + objectDN = adGroup.distinguishedName; + break; + case 'computer': + const adComputer = await storage.getAdComputerByObjectGUID(connectionId, objectGUID); + if (!adComputer) { + return res.status(404).json({ message: "Computer not found" }); + } + objectDN = adComputer.distinguishedName; + break; + case 'organizationalUnit': + const adOU = await storage.getAdOrgUnitByObjectGUID(connectionId, objectGUID); + if (!adOU) { + return res.status(404).json({ message: "Organizational Unit not found" }); + } + objectDN = adOU.distinguishedName; + break; + } + + // Connect to LDAP + try { + await ldapClient.connect(connection); + } catch (error) { + console.error("LDAP connection error:", error); + return res.status(500).json({ message: "Failed to connect to LDAP server", error: error.message }); + } + + try { + // Get the managedBy attribute + const managedBy = await ldapClient.getManagedBy(connectionId, objectDN); + + return res.status(200).json({ managedBy }); + } catch (error) { + console.error("Error getting managedBy attribute:", error); + return res.status(500).json({ message: "Failed to get managedBy attribute", error: error.message }); + } + } catch (error) { + next(error); + } + }); + + /** + * @swagger + * /api/connections/{connectionId}/managed-by: + * post: + * summary: Set the 'managedBy' attribute for an AD object + * tags: [AD Objects] + * security: + * - bearerAuth: [] + * - cookieAuth: [] + * parameters: + * - name: connectionId + * in: path + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - objectGUID + * - objectType + * properties: + * objectGUID: + * type: string + * managerDistinguishedName: + * type: string + * nullable: true + * objectType: + * type: string + * enum: [user, group, computer, organizationalUnit] + * responses: + * 200: + * description: ManagedBy attribute updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 404: + * description: Object not found + */ + app.post("/api/connections/:connectionId/managed-by", authenticateApiToken, async (req, res, next) => { + try { + // Parse and validate request body + try { + updateManagedBySchema.parse(req.body); + } catch (err) { + if (err instanceof ZodError) { + return handleZodError(err, res); + } + throw err; + } + + const connectionId = parseInt(req.params.connectionId); + const connection = await storage.getLdapConnection(connectionId); + + if (!connection) { + return res.status(404).json({ message: "LDAP connection not found" }); + } + + const { objectGUID, managerDistinguishedName, objectType } = req.body; + + // Get the object distinguished name + let objectDN; + let objectName; + switch (objectType) { + case 'user': + const adUser = await storage.getAdUserByObjectGUID(connectionId, objectGUID); + if (!adUser) { + return res.status(404).json({ message: "User not found" }); + } + objectDN = adUser.distinguishedName; + objectName = adUser.displayName || adUser.sAMAccountName; + break; + case 'group': + const adGroup = await storage.getAdGroupByObjectGUID(connectionId, objectGUID); + if (!adGroup) { + return res.status(404).json({ message: "Group not found" }); + } + objectDN = adGroup.distinguishedName; + objectName = adGroup.sAMAccountName; + break; + case 'computer': + const adComputer = await storage.getAdComputerByObjectGUID(connectionId, objectGUID); + if (!adComputer) { + return res.status(404).json({ message: "Computer not found" }); + } + objectDN = adComputer.distinguishedName; + objectName = adComputer.name; + break; + case 'organizationalUnit': + const adOU = await storage.getAdOrgUnitByObjectGUID(connectionId, objectGUID); + if (!adOU) { + return res.status(404).json({ message: "Organizational Unit not found" }); + } + objectDN = adOU.distinguishedName; + objectName = adOU.name; + break; + } + + // Connect to LDAP + try { + await ldapClient.connect(connection); + } catch (error) { + console.error("LDAP connection error:", error); + return res.status(500).json({ message: "Failed to connect to LDAP server", error: error.message }); + } + + try { + // Set the managedBy attribute + await ldapClient.setManagedBy(connectionId, objectDN, managerDistinguishedName); + + // Update the database record + const updateData = { managedBy: managerDistinguishedName }; + switch (objectType) { + case 'user': + await storage.updateAdUserByObjectGUID(connectionId, objectGUID, updateData); + break; + case 'group': + await storage.updateAdGroupByObjectGUID(connectionId, objectGUID, updateData); + break; + case 'computer': + await storage.updateAdComputerByObjectGUID(connectionId, objectGUID, updateData); + break; + case 'organizationalUnit': + await storage.updateAdOrgUnitByObjectGUID(connectionId, objectGUID, updateData); + break; + } + + // Log the action + await storage.createAuditLogEntry({ + action: `update_managed_by:${objectType}`, + targetId: objectGUID, + details: { + objectDN, + managerDN: managerDistinguishedName + }, + userId: req.user?.id, + connectionId + }); + + return res.status(200).json({ + success: true, + message: `ManagedBy attribute successfully updated for ${objectName}` + }); + } catch (error) { + console.error("Error updating managedBy attribute:", error); + return res.status(500).json({ message: "Failed to update managedBy attribute", error: error.message }); + } + } catch (error) { + next(error); + } + }); + /** * @swagger * /api/audit-logs: diff --git a/server/storage.ts b/server/storage.ts index 5669271..f727ef4 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -80,29 +80,37 @@ export interface IStorage { // AD Users getAdUser(id: number): Promise; + getAdUserByObjectGUID(connectionId: number, objectGUID: string): Promise; createAdUser(user: InsertAdUser): Promise; updateAdUser(id: number, user: Partial): Promise; + updateAdUserByObjectGUID(connectionId: number, objectGUID: string, user: Partial): Promise; deleteAdUser(id: number): Promise; listAdUsers(connectionId: number, query?: any): Promise; // AD Groups getAdGroup(id: number): Promise; + getAdGroupByObjectGUID(connectionId: number, objectGUID: string): Promise; createAdGroup(group: InsertAdGroup): Promise; updateAdGroup(id: number, group: Partial): Promise; + updateAdGroupByObjectGUID(connectionId: number, objectGUID: string, group: Partial): Promise; deleteAdGroup(id: number): Promise; listAdGroups(connectionId: number, query?: any): Promise; // AD Organizational Units getAdOrgUnit(id: number): Promise; + getAdOrgUnitByObjectGUID(connectionId: number, objectGUID: string): Promise; createAdOrgUnit(ou: InsertAdOrgUnit): Promise; updateAdOrgUnit(id: number, ou: Partial): Promise; + updateAdOrgUnitByObjectGUID(connectionId: number, objectGUID: string, ou: Partial): Promise; deleteAdOrgUnit(id: number): Promise; listAdOrgUnits(connectionId: number, query?: any): Promise; // AD Computers getAdComputer(id: number): Promise; + getAdComputerByObjectGUID(connectionId: number, objectGUID: string): Promise; createAdComputer(computer: InsertAdComputer): Promise; updateAdComputer(id: number, computer: Partial): Promise; + updateAdComputerByObjectGUID(connectionId: number, objectGUID: string, computer: Partial): Promise; deleteAdComputer(id: number): Promise; listAdComputers(connectionId: number, query?: any): Promise; @@ -558,6 +566,18 @@ export class DatabaseStorage implements IStorage { const result = await db.select().from(adUsers).where(eq(adUsers.id, id)); return result.length > 0 ? result[0] : undefined; } + + async getAdUserByObjectGUID(connectionId: number, objectGUID: string): Promise { + const result = await db.select() + .from(adUsers) + .where( + and( + eq(adUsers.connectionId, connectionId), + eq(adUsers.objectGUID, objectGUID) + ) + ); + return result.length > 0 ? result[0] : undefined; + } async createAdUser(user: InsertAdUser): Promise { const result = await db.insert(adUsers).values(user).returning(); @@ -568,6 +588,19 @@ export class DatabaseStorage implements IStorage { const result = await db.update(adUsers).set(userData).where(eq(adUsers.id, id)).returning(); return result.length > 0 ? result[0] : undefined; } + + async updateAdUserByObjectGUID(connectionId: number, objectGUID: string, userData: Partial): Promise { + const result = await db.update(adUsers) + .set(userData) + .where( + and( + eq(adUsers.connectionId, connectionId), + eq(adUsers.objectGUID, objectGUID) + ) + ) + .returning(); + return result.length > 0 ? result[0] : undefined; + } async deleteAdUser(id: number): Promise { const result = await db.delete(adUsers).where(eq(adUsers.id, id)).returning({ id: adUsers.id }); @@ -647,6 +680,18 @@ export class DatabaseStorage implements IStorage { const result = await db.select().from(adGroups).where(eq(adGroups.id, id)); return result.length > 0 ? result[0] : undefined; } + + async getAdGroupByObjectGUID(connectionId: number, objectGUID: string): Promise { + const result = await db.select() + .from(adGroups) + .where( + and( + eq(adGroups.connectionId, connectionId), + eq(adGroups.objectGUID, objectGUID) + ) + ); + return result.length > 0 ? result[0] : undefined; + } async createAdGroup(group: InsertAdGroup): Promise { const result = await db.insert(adGroups).values(group).returning(); @@ -657,6 +702,19 @@ export class DatabaseStorage implements IStorage { const result = await db.update(adGroups).set(groupData).where(eq(adGroups.id, id)).returning(); return result.length > 0 ? result[0] : undefined; } + + async updateAdGroupByObjectGUID(connectionId: number, objectGUID: string, groupData: Partial): Promise { + const result = await db.update(adGroups) + .set(groupData) + .where( + and( + eq(adGroups.connectionId, connectionId), + eq(adGroups.objectGUID, objectGUID) + ) + ) + .returning(); + return result.length > 0 ? result[0] : undefined; + } async deleteAdGroup(id: number): Promise { const result = await db.delete(adGroups).where(eq(adGroups.id, id)).returning({ id: adGroups.id }); @@ -736,6 +794,18 @@ export class DatabaseStorage implements IStorage { const result = await db.select().from(adOrgUnits).where(eq(adOrgUnits.id, id)); return result.length > 0 ? result[0] : undefined; } + + async getAdOrgUnitByObjectGUID(connectionId: number, objectGUID: string): Promise { + const result = await db.select() + .from(adOrgUnits) + .where( + and( + eq(adOrgUnits.connectionId, connectionId), + eq(adOrgUnits.objectGUID, objectGUID) + ) + ); + return result.length > 0 ? result[0] : undefined; + } async createAdOrgUnit(ou: InsertAdOrgUnit): Promise { const result = await db.insert(adOrgUnits).values(ou).returning(); @@ -746,6 +816,19 @@ export class DatabaseStorage implements IStorage { const result = await db.update(adOrgUnits).set(ouData).where(eq(adOrgUnits.id, id)).returning(); return result.length > 0 ? result[0] : undefined; } + + async updateAdOrgUnitByObjectGUID(connectionId: number, objectGUID: string, ouData: Partial): Promise { + const result = await db.update(adOrgUnits) + .set(ouData) + .where( + and( + eq(adOrgUnits.connectionId, connectionId), + eq(adOrgUnits.objectGUID, objectGUID) + ) + ) + .returning(); + return result.length > 0 ? result[0] : undefined; + } async deleteAdOrgUnit(id: number): Promise { const result = await db.delete(adOrgUnits).where(eq(adOrgUnits.id, id)).returning({ id: adOrgUnits.id }); @@ -779,6 +862,18 @@ export class DatabaseStorage implements IStorage { const result = await db.select().from(adComputers).where(eq(adComputers.id, id)); return result.length > 0 ? result[0] : undefined; } + + async getAdComputerByObjectGUID(connectionId: number, objectGUID: string): Promise { + const result = await db.select() + .from(adComputers) + .where( + and( + eq(adComputers.connectionId, connectionId), + eq(adComputers.objectGUID, objectGUID) + ) + ); + return result.length > 0 ? result[0] : undefined; + } async createAdComputer(computer: InsertAdComputer): Promise { const result = await db.insert(adComputers).values(computer).returning(); @@ -789,6 +884,19 @@ export class DatabaseStorage implements IStorage { const result = await db.update(adComputers).set(computerData).where(eq(adComputers.id, id)).returning(); return result.length > 0 ? result[0] : undefined; } + + async updateAdComputerByObjectGUID(connectionId: number, objectGUID: string, computerData: Partial): Promise { + const result = await db.update(adComputers) + .set(computerData) + .where( + and( + eq(adComputers.connectionId, connectionId), + eq(adComputers.objectGUID, objectGUID) + ) + ) + .returning(); + return result.length > 0 ? result[0] : undefined; + } async deleteAdComputer(id: number): Promise { const result = await db.delete(adComputers).where(eq(adComputers.id, id)).returning({ id: adComputers.id }); diff --git a/shared/schema.ts b/shared/schema.ts index ead01c2..2a35a47 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -160,6 +160,7 @@ export const adUsers = pgTable("ad_users", { email: text("email"), enabled: boolean("enabled").default(true), lastLogon: timestamp("last_logon"), + managedBy: text("managed_by"), memberOf: jsonb("member_of"), adProperties: jsonb("ad_properties"), }); @@ -174,6 +175,7 @@ export const adGroups = pgTable("ad_groups", { sAMAccountName: text("sam_account_name").notNull(), groupType: text("group_type"), description: text("description"), + managedBy: text("managed_by"), members: jsonb("members"), adProperties: jsonb("ad_properties"), }); @@ -187,6 +189,7 @@ export const adOrgUnits = pgTable("ad_org_units", { cn: text("cn"), name: text("name").notNull(), description: text("description"), + managedBy: text("managed_by"), adProperties: jsonb("ad_properties"), }); @@ -204,6 +207,7 @@ export const adComputers = pgTable("ad_computers", { operatingSystemVersion: text("operating_system_version"), lastLogon: timestamp("last_logon"), enabled: boolean("enabled").default(true), + managedBy: text("managed_by"), memberOf: jsonb("member_of"), adProperties: jsonb("ad_properties"), }); @@ -307,6 +311,13 @@ export const removeFromGroupSchema = z.object({ objectType: z.enum(["user", "computer"]), }); +// ManagedBy operation schema +export const updateManagedBySchema = z.object({ + objectGUID: z.string().min(1, "Object GUID is required"), + managerDistinguishedName: z.string().nullable(), + objectType: z.enum(["user", "group", "computer", "organizationalUnit"]), +}); + // Export types export type Role = typeof roles.$inferSelect; export type InsertRole = z.infer; @@ -339,6 +350,7 @@ export type MoveComputer = z.infer; export type MoveUser = z.infer; export type AddToGroup = z.infer; export type RemoveFromGroup = z.infer; +export type UpdateManagedBy = z.infer; // LDAP Query Builder schemas export const ldapFilterObjectClasses = ["user", "group", "organizationalUnit", "computer", "domain"] as const;