Add LDAP query builder feature with UI and API endpoints.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90
This commit is contained in:
alphaeusmote
2025-04-08 20:12:52 +00:00
parent 23d46e6c94
commit 6ed03199b7
6 changed files with 1212 additions and 1 deletions
+82 -1
View File
@@ -6,7 +6,7 @@ import {
buildLdapFilter,
LdapQueryBuilder
} from "./ldap-filter-builder";
import { connectToLdap, searchLdap } from "./ldap";
import { connectToLdap, searchLdap, getLdapAvailableAttributes } from "./ldap";
import { IStorage } from "./storage";
import { InsertLdapQuery, LdapQuery, InsertLdapQueryVersion } from "@shared/schema";
@@ -506,6 +506,87 @@ export function registerLdapQueryBuilderRoutes(router: Router, storage: IStorage
* 404:
* description: Connection not found
*/
/**
* @swagger
* /ldap-queries/attributes:
* get:
* summary: Get available LDAP attributes
* description: Retrieve a list of available attributes for the specified object type from an LDAP connection
* tags: [LDAP Query Builder]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: connectionId
* required: true
* schema:
* type: integer
* description: LDAP Connection ID
* - in: query
* name: targetObject
* required: true
* schema:
* type: string
* enum: [users, groups, computers, ous]
* description: The type of object to get attributes for
* responses:
* 200:
* description: A list of available attributes
* content:
* application/json:
* schema:
* type: array
* items:
* type: string
* 400:
* description: Invalid request parameters
* 404:
* description: Connection not found
*/
router.get("/ldap-queries/attributes", hasPermission("read:ldap_objects"), async (req, res) => {
try {
const connectionId = parseInt(req.query.connectionId as string);
const targetObject = req.query.targetObject as "users" | "groups" | "computers" | "ous";
if (isNaN(connectionId)) {
return res.status(400).json({ error: "Invalid connection ID" });
}
if (!targetObject || !["users", "groups", "computers", "ous"].includes(targetObject)) {
return res.status(400).json({ error: "Invalid target object type" });
}
// Get the connection
const connection = await storage.getLdapConnection(connectionId);
if (!connection) {
return res.status(404).json({ error: "LDAP connection not found" });
}
// Connect to LDAP
try {
const client = await connectToLdap(connection);
// Get available attributes
const attributes = await getLdapAvailableAttributes(client, targetObject);
// Close the connection
client.destroy();
// Return the attributes
res.json(attributes);
} catch (error) {
console.error("Error connecting to LDAP:", error);
return res.status(500).json({
error: "Failed to connect to LDAP server",
details: error instanceof Error ? error.message : String(error)
});
}
} catch (error) {
console.error("Error getting LDAP attributes:", error);
res.status(500).json({ error: "Failed to retrieve LDAP attributes" });
}
});
router.post("/ldap-queries/test", hasPermission("read:ldap_objects"), async (req, res) => {
try {
const { connectionId, targetObject, filter, limit = 100, properties = [] } = req.body;
+128
View File
@@ -128,4 +128,132 @@ export async function getLdapDomainInfo(client: ldapjs.Client): Promise<Record<s
debug("Failed to get LDAP domain info:", error);
throw new Error(`Failed to get LDAP domain info: ${errorMessage}`);
}
}
/**
* Get available attributes for a specific object type from LDAP schema
* This function retrieves attributes that are commonly used for the specified object type
*/
export async function getLdapAvailableAttributes(
client: ldapjs.Client,
targetObject: "users" | "groups" | "computers" | "ous"
): Promise<string[]> {
debug(`Getting available attributes for ${targetObject}`);
// Define common attributes for each object type
const commonAttributes = [
"cn", "name", "displayName", "description", "objectClass", "objectCategory",
"distinguishedName", "whenCreated", "whenChanged", "objectGUID", "objectSid"
];
// Object type specific attributes
const specificAttributes: Record<string, string[]> = {
users: [
"sAMAccountName", "userPrincipalName", "givenName", "sn", "mail",
"telephoneNumber", "mobile", "title", "department", "company",
"manager", "employeeID", "employeeNumber", "memberOf", "userAccountControl",
"pwdLastSet", "lastLogon", "lastLogonTimestamp", "accountExpires", "badPwdCount",
"logonCount", "homeDirectory", "homeDrive", "scriptPath", "profilePath",
"lockoutTime", "country", "st", "l", "streetAddress", "postalCode", "otherTelephone"
],
groups: [
"sAMAccountName", "groupType", "member", "memberOf", "managedBy", "msDS-PrincipalName",
"mail", "info", "groupCategory", "adminCount", "proxyAddresses"
],
computers: [
"sAMAccountName", "operatingSystem", "operatingSystemVersion", "operatingSystemServicePack",
"dNSHostName", "servicePrincipalName", "lastLogonTimestamp", "pwdLastSet",
"userAccountControl", "managedBy", "location", "serialNumber", "msDS-SupportedEncryptionTypes",
"networkAddress", "primaryGroupID"
],
ous: [
"ou", "name", "description", "distinguishedName", "managedBy", "gPLink", "gPOptions",
"msDS-Approx-Immed-Subordinates", "streetAddress", "l", "st", "postalCode", "c"
]
};
try {
// Try to dynamically retrieve schema info for more attributes
// This gets attributes from the schema for the specific object class
let dynamicAttributes: string[] = [];
let objectClass: string;
switch (targetObject) {
case "users":
objectClass = "user";
break;
case "groups":
objectClass = "group";
break;
case "computers":
objectClass = "computer";
break;
case "ous":
objectClass = "organizationalUnit";
break;
}
try {
// Attempt to query the schema - we need to try different base DNs since the schema location varies
let schemaResults: Record<string, any>[] = [];
// Try common locations for the schema (most AD servers will use one of these)
const schemaLocations = [
"CN=Schema,CN=Configuration,DC=domain,DC=com", // Generic example
"CN=Schema,CN=Configuration,DC=ad,DC=example,DC=com",
"CN=Schema,CN=Configuration,DC=example,DC=com",
"CN=Schema,CN=Configuration",
"CN=Schema",
];
let schemaFound = false;
for (const schemaLocation of schemaLocations) {
try {
schemaResults = await searchLdap(client, {
base: schemaLocation,
filter: `(&(objectClass=attributeSchema)(|(attributeSyntax=2.5.5.8)(attributeSyntax=2.5.5.9)(attributeSyntax=2.5.5.12)))`,
scope: "sub",
attributes: ["lDAPDisplayName", "attributeSyntax", "isSingleValued"],
limit: 500
});
if (schemaResults.length > 0) {
schemaFound = true;
debug(`Found schema at ${schemaLocation} with ${schemaResults.length} attributes`);
break;
}
} catch (locationError) {
debug(`Schema not found at ${schemaLocation}: ${locationError instanceof Error ? locationError.message : String(locationError)}`);
// Continue trying other locations
}
}
if (!schemaFound) {
debug("Could not find schema in any of the standard locations");
}
dynamicAttributes = schemaResults.map(attr => attr.lDAPDisplayName);
debug(`Retrieved ${dynamicAttributes.length} attributes from schema`);
} catch (schemaError) {
debug("Failed to retrieve attributes from schema, using predefined list:", schemaError);
// Continue with the predefined attributes
}
// Combine common and specific attributes, then add dynamic attributes
let allAttributes = [...commonAttributes, ...specificAttributes[targetObject]];
// Add any dynamic attributes that aren't already in our list
dynamicAttributes.forEach(attr => {
if (!allAttributes.includes(attr)) {
allAttributes.push(attr);
}
});
// Sort alphabetically
return allAttributes.sort();
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
debug("Failed to get LDAP available attributes:", error);
// Return a default set of attributes instead of throwing
return [...commonAttributes, ...specificAttributes[targetObject]].sort();
}
}