const { Client } = require('ldapts'); class ActiveDirectoryManager { constructor({ url, baseDN, username, password, userAttributes = [], groupAttributes = [], computerAttributes = [] }) { this.url = url; this.baseDN = baseDN; this.username = username; this.password = password; this.userAttributes = userAttributes; this.groupAttributes = groupAttributes; this.computerAttributes = computerAttributes; } /** * ----------------------------------------------------- * CONNECTION * ----------------------------------------------------- */ async createClient() { const client = new Client({ url: this.url, timeout: 10000, connectTimeout: 10000 }); await client.bind(this.username, this.password); return client; } /** * ----------------------------------------------------- * HELPERS * ----------------------------------------------------- */ escape(value) { return String(value).replace(/[*()\\]/g, '\\$&'); } bufferToGuid(buffer) { const hex = buffer.toString('hex'); return [ hex.substring(6, 8), hex.substring(4, 6), hex.substring(2, 4), hex.substring(0, 2), '-', hex.substring(10, 12), hex.substring(8, 10), '-', hex.substring(14, 16), hex.substring(12, 14), '-', hex.substring(16, 20), '-', hex.substring(20) ].join(''); } normalizeEntry(entry) { const obj = {}; for (const [key, value] of Object.entries(entry)) { let normalized = value; // GUID konvertieren if (key === 'objectGUID' && value instanceof Buffer) { normalized = this.bufferToGuid(value); } // Arrays mit einem Wert -> String else if (Array.isArray(value)) { if (value.length === 0) { normalized = null; } else if (value.length === 1) { normalized = value[0]; } } obj[key] = normalized; } obj.ObjectSource_ID = 2; return obj; } /** * ----------------------------------------------------- * GENERIC LDAP SEARCH * ----------------------------------------------------- */ async ldapSearch({ baseDN = this.baseDN, filter, attributes = [] }) { const client = await this.createClient(); try { const { searchEntries } = await client.search(baseDN, { scope: 'sub', filter, attributes, paged: true }); return searchEntries.map(entry => this.normalizeEntry(entry) ); } finally { await client.unbind(); } } /** * ----------------------------------------------------- * USERS * ----------------------------------------------------- */ async getUser(username, attributes = this.userAttributes) { const safe = this.escape(username); const filter = `(&(objectCategory=person)(objectClass=user)` + `(|(sAMAccountName=${safe})` + `(mail=${safe})` + `(cn=${safe})))`; const users = await this.ldapSearch({ filter, attributes }); return users[0] || null; } async getUserDN(username) { const user = await this.getUser( username, ['distinguishedName'] ); return user?.distinguishedName || null; } async getAllUsers(attributes = this.userAttributes) { const filter = '(&(objectCategory=person)(objectClass=user))'; return await this.ldapSearch({ filter, attributes }); } async findUsers(query, attributes = this.userAttributes) { const safe = this.escape(query); const filter = `(&(objectClass=user)` + `(|(cn=${safe})` + `(sAMAccountName=${safe})` + `(mail=${safe})` + `(displayName=${safe})))`; return await this.ldapSearch({ filter, attributes }); } /** * ----------------------------------------------------- * GROUPS * ----------------------------------------------------- */ async getGroup(groupName, attributes = this.groupAttributes) { const safe = this.escape(groupName); const filter = `(&(objectClass=group)(cn=${safe}))`; const groups = await this.ldapSearch({ filter, attributes }); return groups[0] || null; } async findGroups(query, attributes = this.groupAttributes) { const safe = this.escape(query); const filter = `(&(objectClass=group)(cn=${safe}))`; return await this.ldapSearch({ filter, attributes }); } async getAllGroups({ attributes = this.groupAttributes, includeMembers = false, memberAttributes = this.userAttributes || [ 'objectGUID', 'cn', 'sAMAccountName', 'mail', 'distinguishedName' ] } = {}) { const finalAttributes = [...new Set([ ...attributes, ...(includeMembers ? ['member'] : []) ])]; const filter = '(objectClass=group)'; const groups = await this.ldapSearch({ filter, attributes: finalAttributes }); const results = []; for (const group of groups) { const result = {}; // Gruppenattribute übernehmen for (const attr of attributes) { result[attr] = group[attr] ?? null; } // Mitglieder laden if (includeMembers) { const rawMembers = Array.isArray(group.member) ? group.member : group.member ? [group.member] : []; const members = []; for (const memberDN of rawMembers) { const memberResults = await this.ldapSearch({ baseDN: memberDN, filter: '(objectClass=*)', attributes: memberAttributes }); if (memberResults.length > 0) { const member = memberResults[0]; const normalizedMember = {}; for (const attr of memberAttributes) { normalizedMember[attr] = member[attr] ?? null; } members.push(normalizedMember); } } result.members = members; } results.push(result); } return results; } /** * ----------------------------------------------------- * COMPUTERS * ----------------------------------------------------- */ async getComputer(name, attributes = this.computerAttributes) { const safe = this.escape(name); const filter = `(&(objectClass=computer)` + `(|(cn=${safe})(dNSHostName=${safe})))`; const results = await this.ldapSearch({ filter, attributes }); return results[0] || null; } async getComputers(attributes = this.computerAttributes) { return await this.ldapSearch({ filter: '(objectClass=computer)', attributes }); } async getComputersFromOU( ouDn, attributes = this.computerAttributes ) { return await this.ldapSearch({ baseDN: ouDn, filter: '(objectClass=computer)', attributes }); } async findComputers( query, attributes = this.computerAttributes ) { const safe = this.escape(query); const filter = `(&(objectClass=computer)` + `(|(cn=${safe})(dNSHostName=${safe})))`; return await this.ldapSearch({ filter, attributes }); } /** * ----------------------------------------------------- * GROUP MEMBERSHIP * ----------------------------------------------------- */ async isUserMemberOfDirect(username, groupName) { const user = await this.getUser( username, ['distinguishedName'] ); if (!user) { return false; } const safeGroup = this.escape(groupName); const filter = `(&(objectClass=group)` + `(cn=${safeGroup})` + `(member=${user.distinguishedName}))`; const results = await this.ldapSearch({ filter, attributes: ['cn'] }); return results.length > 0; } async isUserMemberOfRecursive( username, groupName, visited = new Set() ) { const key = groupName.toLowerCase(); if (visited.has(key)) { return false; } visited.add(key); const direct = await this.isUserMemberOfDirect( username, groupName ); if (direct) { return true; } const group = await this.getGroup( groupName, ['member'] ); if (!group || !group.member) { return false; } const members = Array.isArray(group.member) ? group.member : [group.member]; for (const dn of members) { const match = dn.match(/CN=([^,]+)/i); if (!match) { continue; } const subGroupName = match[1]; const found = await this.isUserMemberOfRecursive( username, subGroupName, visited ); if (found) { return true; } } return false; } async getGroupSubgroups( groupName, visited = new Set() ) { const key = groupName.toLowerCase(); if (visited.has(key)) { return []; } visited.add(key); const group = await this.getGroup( groupName, ['member'] ); if (!group || !group.member) { return []; } const members = Array.isArray(group.member) ? group.member : [group.member]; const results = []; for (const dn of members) { const match = dn.match(/CN=([^,]+)/i); if (!match) { continue; } const subGroupName = match[1]; const subGroup = await this.getGroup( subGroupName ).catch(() => null); if (!subGroup) { continue; } results.push(subGroup); results.push( ...await this.getGroupSubgroups( subGroupName, visited ) ); } return results; } async getGroupRecursive( groupName, visited = new Set() ) { const key = groupName.toLowerCase(); if (visited.has(key)) { return null; } visited.add(key); const group = await this.getGroup(groupName); if (!group) { return null; } const result = { ...group, subgroups: [] }; if (!group.member) { return result; } const members = Array.isArray(group.member) ? group.member : [group.member]; for (const dn of members) { const match = dn.match(/CN=([^,]+)/i); if (!match) { continue; } const subGroupName = match[1]; const subTree = await this.getGroupRecursive( subGroupName, visited ); if (subTree) { result.subgroups.push(subTree); } } return result; } } module.exports = ActiveDirectoryManager; // const { Client } = require('ldapts'); // class ActiveDirectoryManager { // constructor({ // url, // baseDN, // username, // password, // userAttributes = [], // groupAttributes = [], // computerAttributes = [] // }) { // this.url = url; // this.baseDN = baseDN; // this.username = username; // this.password = password; // this.userAttributes = userAttributes; // this.groupAttributes = groupAttributes; // this.computerAttributes = computerAttributes; // } // /** // * ----------------------------------------------------- // * CONNECTION // * ----------------------------------------------------- // */ // async createClient() { // const client = new Client({ // url: this.url, // timeout: 10000, // connectTimeout: 10000 // }); // await client.bind(this.username, this.password); // return client; // } // /** // * ----------------------------------------------------- // * HELPERS // * ----------------------------------------------------- // */ // escape(value) { // return String(value).replace(/[*()\\]/g, '\\$&'); // } // bufferToGuid(buffer) { // const hex = buffer.toString('hex'); // return [ // hex.substring(6, 8), // hex.substring(4, 6), // hex.substring(2, 4), // hex.substring(0, 2), // '-', // hex.substring(10, 12), // hex.substring(8, 10), // '-', // hex.substring(14, 16), // hex.substring(12, 14), // '-', // hex.substring(16, 20), // '-', // hex.substring(20) // ].join(''); // } // normalizeEntry(entry) { // const obj = {}; // for (const [key, value] of Object.entries(entry)) { // let normalized = value; // // GUID konvertieren // if (key === 'objectGUID' && value instanceof Buffer) { // normalized = this.bufferToGuid(value); // } // // Arrays mit einem Wert -> String // else if (Array.isArray(value)) { // if (value.length === 0) { // normalized = null; // } // else if (value.length === 1) { // normalized = value[0]; // } // } // obj[key] = normalized; // } // obj['ObjectSource_ID'] = 2; // return obj; // } // /** // * ----------------------------------------------------- // * GENERIC SEARCH // * ----------------------------------------------------- // */ // async ldapSearch({ // baseDN = this.baseDN, // filter, // attributes = [] // }) { // const client = await this.createClient(); // try { // const { searchEntries } = await client.search(baseDN, { // scope: 'sub', // filter, // attributes, // paged: true // }); // return searchEntries.map(entry => // this.normalizeEntry(entry) // ); // } finally { // await client.unbind(); // } // } // /** // * ----------------------------------------------------- // * USERS // * ----------------------------------------------------- // */ // async getUser(username, attributes = this.userAttributes) { // const safe = this.escape(username); // const filter = // `(&(objectCategory=person)(objectClass=user)` + // `(|(sAMAccountName=${safe})(mail=${safe})(cn=${safe})))`; // const users = await this.ldapSearch({ // filter, // attributes // }); // return users[0] || null; // } // async getAllUsers(attributes = [ // 'objectGUID', // 'sAMAccountName', // 'mail', // 'givenName', // 'sn', // 'employeeID', // 'title', // 'department', // 'streetAddress', // 'telephoneNumber', // 'physicalDeliveryOfficeName', // 'distinguishedName' // ]) { // const filter = // '(&(objectCategory=person)(objectClass=user))'; // return await this.ldapSearch({ // filter, // attributes // }); // } // async findUsers(query, attributes = this.userAttributes) { // const safe = this.escape(query); // const filter = // `(&(objectClass=user)` + // `(|(cn=${safe})` + // `(sAMAccountName=${safe})` + // `(mail=${safe})` + // `(displayName=${safe})))`; // return await this.ldapSearch({ // filter, // attributes // }); // } // /** // * ----------------------------------------------------- // * GROUPS // * ----------------------------------------------------- // */ // async getGroup(groupName, attributes = this.groupAttributes) { // const safe = this.escape(groupName); // const filter = // `(&(objectClass=group)(cn=${safe}))`; // const groups = await this.ldapSearch({ // filter, // attributes // }); // return groups[0] || null; // } // /** // * ----------------------------------------------------- // * ALL GROUPS // * ----------------------------------------------------- // */ // /** // * ----------------------------------------------------- // * ALL GROUPS // * ----------------------------------------------------- // */ // async getAllGroups({ attributes = this.groupAttributes, includeMembers = false } = {}) { // const finalAttributes = [...new Set([ // ...attributes, // ...(includeMembers ? ['member'] : []) // ])]; // const filter = '(objectClass=group)'; // const groups = await this.ldapSearch({ // filter, // attributes: finalAttributes // }); // return groups.map(group => { // const result = {}; // // Nur definierte Attribute übernehmen // for (const attr of attributes) { // result[attr] = group[attr] ?? null; // } // result['ObjectSource_ID'] = 2; // // Optional Mitglieder hinzufügen // if (includeMembers) { // const rawMembers = Array.isArray(group.member) // ? group.member // : group.member // ? [group.member] // : []; // result.members = rawMembers.map(memberDN => { // const match = memberDN.match(/CN=([^,]+)/i); // return { // distinguishedName: memberDN, // cn: match ? match[1] : null // }; // }); // } // return result; // }); // } // async findGroups(query, attributes = this.groupAttributes) { // const safe = this.escape(query); // const filter = // `(&(objectClass=group)(cn=${safe}))`; // return await this.ldapSearch({ // filter, // attributes // }); // } // /** // * ----------------------------------------------------- // * COMPUTERS // * ----------------------------------------------------- // */ // async getComputers(attributes = this.computerAttributes) { // return await this.ldapSearch({ // filter: '(objectClass=computer)', // attributes // }); // } // async getComputer(name, attributes = this.computerAttributes) { // const safe = this.escape(name); // const filter = // `(&(objectClass=computer)` + // `(|(cn=${safe})(dNSHostName=${safe})))`; // const results = await this.ldapSearch({ // filter, // attributes // }); // return results[0] || null; // } // /** // * ----------------------------------------------------- // * GROUP MEMBERSHIP // * ----------------------------------------------------- // */ // async isUserMemberOfDirect(username, groupName) { // const user = await this.getUser(username, [ // 'distinguishedName' // ]); // if (!user) { // return false; // } // const safeGroup = this.escape(groupName); // const filter = // `(&(objectClass=group)` + // `(cn=${safeGroup})` + // `(member=${user.distinguishedName}))`; // const results = await this.ldapSearch({ // filter, // attributes: ['cn'] // }); // return results.length > 0; // } // } // module.exports = ActiveDirectoryManager;