Files
radixOS/src/services/activeDirectoryManager.js
2026-05-12 08:06:46 +02:00

942 lines
19 KiB
JavaScript

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;