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
This commit is contained in:
alphaeusmote
2025-04-09 11:38:35 +00:00
parent 00250d2904
commit 9e2d0ce81e
4 changed files with 471 additions and 1 deletions
+69
View File
@@ -196,6 +196,75 @@ class LdapClient extends EventEmitter {
});
});
}
// Get the managed by attribute for an object
async getManagedBy(connectionId: number, dn: string): Promise<string | null> {
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<boolean> {
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();
+282 -1
View File
@@ -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<Server> {
}
});
/**
* @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:
+108
View File
@@ -80,29 +80,37 @@ export interface IStorage {
// AD Users
getAdUser(id: number): Promise<AdUser | undefined>;
getAdUserByObjectGUID(connectionId: number, objectGUID: string): Promise<AdUser | undefined>;
createAdUser(user: InsertAdUser): Promise<AdUser>;
updateAdUser(id: number, user: Partial<AdUser>): Promise<AdUser | undefined>;
updateAdUserByObjectGUID(connectionId: number, objectGUID: string, user: Partial<AdUser>): Promise<AdUser | undefined>;
deleteAdUser(id: number): Promise<boolean>;
listAdUsers(connectionId: number, query?: any): Promise<AdUser[]>;
// AD Groups
getAdGroup(id: number): Promise<AdGroup | undefined>;
getAdGroupByObjectGUID(connectionId: number, objectGUID: string): Promise<AdGroup | undefined>;
createAdGroup(group: InsertAdGroup): Promise<AdGroup>;
updateAdGroup(id: number, group: Partial<AdGroup>): Promise<AdGroup | undefined>;
updateAdGroupByObjectGUID(connectionId: number, objectGUID: string, group: Partial<AdGroup>): Promise<AdGroup | undefined>;
deleteAdGroup(id: number): Promise<boolean>;
listAdGroups(connectionId: number, query?: any): Promise<AdGroup[]>;
// AD Organizational Units
getAdOrgUnit(id: number): Promise<AdOrgUnit | undefined>;
getAdOrgUnitByObjectGUID(connectionId: number, objectGUID: string): Promise<AdOrgUnit | undefined>;
createAdOrgUnit(ou: InsertAdOrgUnit): Promise<AdOrgUnit>;
updateAdOrgUnit(id: number, ou: Partial<AdOrgUnit>): Promise<AdOrgUnit | undefined>;
updateAdOrgUnitByObjectGUID(connectionId: number, objectGUID: string, ou: Partial<AdOrgUnit>): Promise<AdOrgUnit | undefined>;
deleteAdOrgUnit(id: number): Promise<boolean>;
listAdOrgUnits(connectionId: number, query?: any): Promise<AdOrgUnit[]>;
// AD Computers
getAdComputer(id: number): Promise<AdComputer | undefined>;
getAdComputerByObjectGUID(connectionId: number, objectGUID: string): Promise<AdComputer | undefined>;
createAdComputer(computer: InsertAdComputer): Promise<AdComputer>;
updateAdComputer(id: number, computer: Partial<AdComputer>): Promise<AdComputer | undefined>;
updateAdComputerByObjectGUID(connectionId: number, objectGUID: string, computer: Partial<AdComputer>): Promise<AdComputer | undefined>;
deleteAdComputer(id: number): Promise<boolean>;
listAdComputers(connectionId: number, query?: any): Promise<AdComputer[]>;
@@ -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<AdUser | undefined> {
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<AdUser> {
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<AdUser>): Promise<AdUser | undefined> {
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<boolean> {
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<AdGroup | undefined> {
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<AdGroup> {
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<AdGroup>): Promise<AdGroup | undefined> {
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<boolean> {
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<AdOrgUnit | undefined> {
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<AdOrgUnit> {
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<AdOrgUnit>): Promise<AdOrgUnit | undefined> {
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<boolean> {
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<AdComputer | undefined> {
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<AdComputer> {
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<AdComputer>): Promise<AdComputer | undefined> {
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<boolean> {
const result = await db.delete(adComputers).where(eq(adComputers.id, id)).returning({ id: adComputers.id });
+12
View File
@@ -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<typeof insertRoleSchema>;
@@ -339,6 +350,7 @@ export type MoveComputer = z.infer<typeof moveComputerSchema>;
export type MoveUser = z.infer<typeof moveUserSchema>;
export type AddToGroup = z.infer<typeof addToGroupSchema>;
export type RemoveFromGroup = z.infer<typeof removeFromGroupSchema>;
export type UpdateManagedBy = z.infer<typeof updateManagedBySchema>;
// LDAP Query Builder schemas
export const ldapFilterObjectClasses = ["user", "group", "organizationalUnit", "computer", "domain"] as const;