Add support for querying Active Directory sites and subnets

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/38d85472-a91b-4a98-977a-cd9ab8f43196.jpg
This commit is contained in:
alphaeusmote
2025-04-09 18:49:54 +00:00
7 changed files with 1423 additions and 4 deletions
+21
View File
@@ -51,6 +51,7 @@
"@types/passport-jwt": "^4.0.1",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"@types/uuid": "^10.0.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@@ -88,6 +89,7 @@
"swagger-ui-express": "^5.0.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"vaul": "^1.1.0",
"wouter": "^3.3.5",
"ws": "^8.18.0",
@@ -3852,6 +3854,12 @@
"@types/serve-static": "*"
}
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
@@ -9110,6 +9118,19 @@
"node": ">= 0.4.0"
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/validator": {
"version": "13.15.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz",
+2
View File
@@ -53,6 +53,7 @@
"@types/passport-jwt": "^4.0.1",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"@types/uuid": "^10.0.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@@ -90,6 +91,7 @@
"swagger-ui-express": "^5.0.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"vaul": "^1.1.0",
"wouter": "^3.3.5",
"ws": "^8.18.0",
+88
View File
@@ -152,6 +152,94 @@ class LdapClient extends EventEmitter {
return this.searchUsers(connectionId, filter, attributes || defaultAttributes);
}
async searchSites(connectionId: number, filter = '(objectClass=site)', attributes?: string[]): Promise<any[]> {
const defaultAttributes = ['cn', 'distinguishedName', 'description', 'location', 'managedBy'];
// Sites are typically stored in the Configuration naming context
const client = this.getClient(connectionId);
if (!client) throw new Error('LDAP connection not established');
const connection = await storage.getLdapConnection(connectionId);
if (!connection) throw new Error('LDAP connection not found');
// Sites are located in the Configuration container
const baseDN = 'CN=Sites,CN=Configuration,' + this.getDomainDN(connection);
const searchAttributes = attributes?.length ? attributes : defaultAttributes;
return new Promise((resolve, reject) => {
const results: any[] = [];
client.search(baseDN, {
filter,
scope: 'sub',
attributes: searchAttributes
}, (err, res) => {
if (err) {
reject(err);
return;
}
res.on('searchEntry', (entry) => {
results.push(entry.object);
});
res.on('error', (err) => {
reject(err);
});
res.on('end', (result) => {
resolve(results);
});
});
});
}
async searchSubnets(connectionId: number, filter = '(objectClass=subnet)', attributes?: string[]): Promise<any[]> {
const defaultAttributes = ['cn', 'distinguishedName', 'description', 'location', 'siteObject', 'managedBy'];
// Subnets are typically stored in the Configuration naming context
const client = this.getClient(connectionId);
if (!client) throw new Error('LDAP connection not established');
const connection = await storage.getLdapConnection(connectionId);
if (!connection) throw new Error('LDAP connection not found');
// Subnets are located in the Configuration container
const baseDN = 'CN=Subnets,CN=Sites,CN=Configuration,' + this.getDomainDN(connection);
const searchAttributes = attributes?.length ? attributes : defaultAttributes;
return new Promise((resolve, reject) => {
const results: any[] = [];
client.search(baseDN, {
filter,
scope: 'sub',
attributes: searchAttributes
}, (err, res) => {
if (err) {
reject(err);
return;
}
res.on('searchEntry', (entry) => {
results.push(entry.object);
});
res.on('error', (err) => {
reject(err);
});
res.on('end', (result) => {
resolve(results);
});
});
});
}
// Helper function to extract domain DN from connection
getDomainDN(connection: LdapConnection): string {
const domainParts = connection.domain.split('.');
return domainParts.map(part => `DC=${part}`).join(',');
}
async createEntry(connectionId: number, dn: string, attributes: any): Promise<boolean> {
const client = this.getClient(connectionId);
if (!client) throw new Error('LDAP connection not established');
+1080 -1
View File
File diff suppressed because it is too large Load Diff
+153 -2
View File
@@ -3,11 +3,11 @@ import {
LdapConnection, InsertLdapConnection,
AdUser, InsertAdUser, AdGroup, InsertAdGroup,
AdOrgUnit, InsertAdOrgUnit, AdComputer, InsertAdComputer,
AdDomain, InsertAdDomain, Role, ApiQuery,
AdDomain, InsertAdDomain, AdSite, InsertAdSite, AdSubnet, InsertAdSubnet, Role, ApiQuery,
LdapFilter, InsertLdapFilter, LdapFilterRevision, InsertLdapFilterRevision,
LdapAttribute, InsertLdapAttribute, AuditLog, InsertAuditLog,
users, apiTokens, ldapConnections, adUsers, adGroups, adOrgUnits, adComputers, adDomains,
roles, ldapFilters, ldapFilterRevisions, ldapAttributes, auditLogs
adSites, adSubnets, roles, ldapFilters, ldapFilterRevisions, ldapAttributes, auditLogs
} from "@shared/schema";
import session from "express-session";
import createMemoryStore from "memorystore";
@@ -121,6 +121,24 @@ export interface IStorage {
deleteAdDomain(id: number): Promise<boolean>;
listAdDomains(connectionId: number, query?: any): Promise<AdDomain[]>;
// AD Sites
getAdSite(id: number): Promise<AdSite | undefined>;
getAdSiteByObjectGUID(connectionId: number, objectGUID: string): Promise<AdSite | undefined>;
createAdSite(site: InsertAdSite): Promise<AdSite>;
updateAdSite(id: number, site: Partial<AdSite>): Promise<AdSite | undefined>;
updateAdSiteByObjectGUID(connectionId: number, objectGUID: string, site: Partial<AdSite>): Promise<AdSite | undefined>;
deleteAdSite(id: number): Promise<boolean>;
listAdSites(connectionId: number, query?: any): Promise<AdSite[]>;
// AD Subnets
getAdSubnet(id: number): Promise<AdSubnet | undefined>;
getAdSubnetByObjectGUID(connectionId: number, objectGUID: string): Promise<AdSubnet | undefined>;
createAdSubnet(subnet: InsertAdSubnet): Promise<AdSubnet>;
updateAdSubnet(id: number, subnet: Partial<AdSubnet>): Promise<AdSubnet | undefined>;
updateAdSubnetByObjectGUID(connectionId: number, objectGUID: string, subnet: Partial<AdSubnet>): Promise<AdSubnet | undefined>;
deleteAdSubnet(id: number): Promise<boolean>;
listAdSubnets(connectionId: number, query?: any): Promise<AdSubnet[]>;
// Audit logging
createAuditLogEntry(entry: InsertAuditLog): Promise<AuditLog>;
getAuditLogs(connectionId?: number, userId?: number, page?: number, pageSize?: number): Promise<{ data: AuditLog[], metadata: { currentPage: number, totalPages: number, totalRecords: number, nextPage: number | null, prevPage: number | null } }>;
@@ -548,6 +566,12 @@ export class DatabaseStorage implements IStorage {
case 'domain':
results = await this.listAdDomains(connectionId);
break;
case 'site':
results = await this.listAdSites(connectionId);
break;
case 'subnet':
results = await this.listAdSubnets(connectionId);
break;
default:
throw new Error(`Unsupported object class: ${objectClass}`);
}
@@ -651,6 +675,11 @@ export class DatabaseStorage implements IStorage {
if (selectedFields && selectedFields.length > 0) {
const result = users.map(user => {
const filtered: Partial<AdUser> = { id: user.id };
// Always include managedBy field regardless of selection
filtered.managedBy = user.managedBy;
// Add selected fields
selectedFields.forEach(field => {
if (field in user) {
filtered[field as keyof AdUser] = user[field as keyof AdUser];
@@ -765,6 +794,10 @@ export class DatabaseStorage implements IStorage {
if (selectedFields && selectedFields.length > 0) {
const result = groups.map(group => {
const filtered: Partial<AdGroup> = { id: group.id };
// Always include managedBy field regardless of selection
filtered.managedBy = group.managedBy;
selectedFields.forEach(field => {
if (field in group) {
filtered[field as keyof AdGroup] = group[field as keyof AdGroup];
@@ -845,6 +878,10 @@ export class DatabaseStorage implements IStorage {
return orgUnits.map(ou => {
const result: any = { id: ou.id };
// Always include managedBy field regardless of selection
result.managedBy = ou.managedBy;
properties.forEach((prop: string) => {
if ((ou as any)[prop] !== undefined) {
result[prop] = (ou as any)[prop];
@@ -913,6 +950,10 @@ export class DatabaseStorage implements IStorage {
return computers.map(computer => {
const result: any = { id: computer.id };
// Always include managedBy field regardless of selection
result.managedBy = computer.managedBy;
properties.forEach((prop: string) => {
if ((computer as any)[prop] !== undefined) {
result[prop] = (computer as any)[prop];
@@ -968,6 +1009,116 @@ export class DatabaseStorage implements IStorage {
return adDomainsQuery;
}
// AD Sites
async getAdSite(id: number): Promise<AdSite | undefined> {
const result = await db.select().from(adSites).where(eq(adSites.id, id));
return result.length > 0 ? result[0] : undefined;
}
async getAdSiteByObjectGUID(connectionId: number, objectGUID: string): Promise<AdSite | undefined> {
const result = await db.select().from(adSites).where(
and(
eq(adSites.connectionId, connectionId),
eq(adSites.objectGUID, objectGUID)
)
);
return result.length > 0 ? result[0] : undefined;
}
async createAdSite(site: InsertAdSite): Promise<AdSite> {
const result = await db.insert(adSites).values(site).returning();
return result[0];
}
async updateAdSite(id: number, site: Partial<AdSite>): Promise<AdSite | undefined> {
const result = await db.update(adSites).set(site).where(eq(adSites.id, id)).returning();
return result.length > 0 ? result[0] : undefined;
}
async updateAdSiteByObjectGUID(connectionId: number, objectGUID: string, site: Partial<AdSite>): Promise<AdSite | undefined> {
const result = await db.update(adSites)
.set(site)
.where(
and(
eq(adSites.connectionId, connectionId),
eq(adSites.objectGUID, objectGUID)
)
)
.returning();
return result.length > 0 ? result[0] : undefined;
}
async deleteAdSite(id: number): Promise<boolean> {
const result = await db.delete(adSites).where(eq(adSites.id, id)).returning({ id: adSites.id });
return result.length > 0;
}
async listAdSites(connectionId: number, query?: any): Promise<AdSite[]> {
let adSitesQuery = db.select().from(adSites).where(eq(adSites.connectionId, connectionId));
// Apply query options (filtering, pagination, etc.)
if (query) {
adSitesQuery = applyQueryOptions(adSitesQuery, query, adSites);
}
return adSitesQuery;
}
// AD Subnets
async getAdSubnet(id: number): Promise<AdSubnet | undefined> {
const result = await db.select().from(adSubnets).where(eq(adSubnets.id, id));
return result.length > 0 ? result[0] : undefined;
}
async getAdSubnetByObjectGUID(connectionId: number, objectGUID: string): Promise<AdSubnet | undefined> {
const result = await db.select().from(adSubnets).where(
and(
eq(adSubnets.connectionId, connectionId),
eq(adSubnets.objectGUID, objectGUID)
)
);
return result.length > 0 ? result[0] : undefined;
}
async createAdSubnet(subnet: InsertAdSubnet): Promise<AdSubnet> {
const result = await db.insert(adSubnets).values(subnet).returning();
return result[0];
}
async updateAdSubnet(id: number, subnet: Partial<AdSubnet>): Promise<AdSubnet | undefined> {
const result = await db.update(adSubnets).set(subnet).where(eq(adSubnets.id, id)).returning();
return result.length > 0 ? result[0] : undefined;
}
async updateAdSubnetByObjectGUID(connectionId: number, objectGUID: string, subnet: Partial<AdSubnet>): Promise<AdSubnet | undefined> {
const result = await db.update(adSubnets)
.set(subnet)
.where(
and(
eq(adSubnets.connectionId, connectionId),
eq(adSubnets.objectGUID, objectGUID)
)
)
.returning();
return result.length > 0 ? result[0] : undefined;
}
async deleteAdSubnet(id: number): Promise<boolean> {
const result = await db.delete(adSubnets).where(eq(adSubnets.id, id)).returning({ id: adSubnets.id });
return result.length > 0;
}
async listAdSubnets(connectionId: number, query?: any): Promise<AdSubnet[]> {
let adSubnetsQuery = db.select().from(adSubnets).where(eq(adSubnets.connectionId, connectionId));
// Apply query options (filtering, pagination, etc.)
if (query) {
adSubnetsQuery = applyQueryOptions(adSubnetsQuery, query, adSubnets);
}
return adSubnetsQuery;
}
// Audit logging
async createAuditLogEntry(entry: InsertAuditLog): Promise<AuditLog> {
debug(`Creating audit log entry: ${JSON.stringify(entry)}`);
+39
View File
@@ -105,6 +105,7 @@ const swaggerOptions = {
enabled: { type: "boolean" },
lastLogon: { type: "string", format: "date-time" },
memberOf: { type: "array", items: { type: "string" } },
managedBy: { type: "string", description: "Distinguished name of the user or group managing this object" },
adProperties: { type: "object" },
objectType: { type: "string", enum: ["user"] },
},
@@ -122,6 +123,7 @@ const swaggerOptions = {
groupType: { type: "string" },
description: { type: "string" },
members: { type: "array", items: { type: "string" } },
managedBy: { type: "string", description: "Distinguished name of the user or group managing this object" },
adProperties: { type: "object" },
objectType: { type: "string", enum: ["group"] },
},
@@ -137,6 +139,7 @@ const swaggerOptions = {
cn: { type: "string" },
name: { type: "string" },
description: { type: "string" },
managedBy: { type: "string", description: "Distinguished name of the user or group managing this object" },
adProperties: { type: "object" },
objectType: { type: "string", enum: ["organizationalUnit"] },
},
@@ -156,6 +159,7 @@ const swaggerOptions = {
operatingSystemVersion: { type: "string" },
lastLogon: { type: "string", format: "date-time" },
enabled: { type: "boolean" },
managedBy: { type: "string", description: "Distinguished name of the user or group managing this object" },
adProperties: { type: "object" },
objectType: { type: "string", enum: ["computer"] },
},
@@ -177,6 +181,41 @@ const swaggerOptions = {
objectType: { type: "string", enum: ["domain"] },
},
},
AdSite: {
type: "object",
properties: {
id: { type: "integer" },
connectionId: { type: "integer" },
objectGUID: { type: "string" },
distinguishedName: { type: "string" },
cn: { type: "string" },
name: { type: "string" },
description: { type: "string" },
location: { type: "string" },
managedBy: { type: "string", description: "Distinguished name of the user or group managing this site" },
adProperties: { type: "object" },
objectType: { type: "string", enum: ["site"] },
},
},
AdSubnet: {
type: "object",
properties: {
id: { type: "integer" },
connectionId: { type: "integer" },
objectGUID: { type: "string" },
distinguishedName: { type: "string" },
cn: { type: "string" },
name: { type: "string" },
description: { type: "string" },
siteObject: { type: "string", description: "Distinguished name of the site this subnet belongs to" },
location: { type: "string" },
networkAddress: { type: "string" },
networkMask: { type: "string" },
managedBy: { type: "string", description: "Distinguished name of the user or group managing this subnet" },
adProperties: { type: "object" },
objectType: { type: "string", enum: ["subnet"] },
},
},
LdapAttribute: {
type: "object",
properties: {
+40 -1
View File
@@ -226,6 +226,39 @@ export const adDomains = pgTable("ad_domains", {
adProperties: jsonb("ad_properties"),
});
// AD Sites table for Sites and Services
export const adSites = pgTable("ad_sites", {
id: serial("id").primaryKey(),
connectionId: integer("connection_id").notNull(),
objectGUID: text("object_guid").notNull(),
distinguishedName: text("distinguished_name").notNull(),
cn: text("cn"),
name: text("name").notNull(),
description: text("description"),
location: text("location"),
managedBy: text("managed_by"),
adProperties: jsonb("ad_properties"),
objectType: text("object_type").default("site").notNull(),
});
// AD Subnets table for Sites and Services
export const adSubnets = pgTable("ad_subnets", {
id: serial("id").primaryKey(),
connectionId: integer("connection_id").notNull(),
objectGUID: text("object_guid").notNull(),
distinguishedName: text("distinguished_name").notNull(),
cn: text("cn"),
name: text("name").notNull(),
description: text("description"),
siteObject: text("site_object"),
location: text("location"),
networkAddress: text("network_address"),
networkMask: text("network_mask"),
managedBy: text("managed_by"),
adProperties: jsonb("ad_properties"),
objectType: text("object_type").default("subnet").notNull(),
});
// Define relations between tables
export const rolesRelations = relations(roles, ({ many }) => ({
permissions: many(rolePermissions),
@@ -270,6 +303,8 @@ export const insertAdGroupSchema = createInsertSchema(adGroups).omit({ id: true
export const insertAdOrgUnitSchema = createInsertSchema(adOrgUnits).omit({ id: true });
export const insertAdComputerSchema = createInsertSchema(adComputers).omit({ id: true });
export const insertAdDomainSchema = createInsertSchema(adDomains).omit({ id: true });
export const insertAdSiteSchema = createInsertSchema(adSites).omit({ id: true });
export const insertAdSubnetSchema = createInsertSchema(adSubnets).omit({ id: true });
// Login schema
export const loginSchema = z.object({
@@ -339,6 +374,10 @@ export type AdComputer = typeof adComputers.$inferSelect;
export type InsertAdComputer = z.infer<typeof insertAdComputerSchema>;
export type AdDomain = typeof adDomains.$inferSelect;
export type InsertAdDomain = z.infer<typeof insertAdDomainSchema>;
export type AdSite = typeof adSites.$inferSelect;
export type InsertAdSite = z.infer<typeof insertAdSiteSchema>;
export type AdSubnet = typeof adSubnets.$inferSelect;
export type InsertAdSubnet = z.infer<typeof insertAdSubnetSchema>;
export type Login = z.infer<typeof loginSchema>;
export type ApiQuery = z.infer<typeof apiQuerySchema>;
export type MoveComputer = z.infer<typeof moveComputerSchema>;
@@ -348,7 +387,7 @@ export type RemoveFromGroup = z.infer<typeof removeFromGroupSchema>;
// LDAP Query Builder schemas
export const ldapFilterObjectClasses = ["user", "group", "organizationalUnit", "computer", "domain"] as const;
export const ldapFilterObjectClasses = ["user", "group", "organizationalUnit", "computer", "domain", "site", "subnet"] as const;
// LDAP Filter schema
export const ldapFilters = pgTable("ldap_filters", {