Files
ProxmoxAAS-LDAP/src/ldap.js
Arthur Lu 3ce798aced improve ldap return values,
fix addUserToGroup and delUser logic with user-group interaction
2023-11-30 04:28:49 +00:00

296 lines
8.4 KiB
JavaScript

import ldap from "ldapjs";
export default class LDAP {
#client = null;
#basedn = null;
#peopledn = null;
#groupsdn = null;
constructor (url, basedn) {
const opts = {
url
};
this.#client = new LDAPJS_CLIENT_ASYNC_WRAPPER(opts);
this.#basedn = basedn;
this.#peopledn = `ou=people,${basedn}`;
this.#groupsdn = `ou=groups,${basedn}`;
}
async addUser (bind, uid, attrs) {
const bindResult = await this.#client.bind(bind.dn, bind.password);
if (!bindResult.ok) {
return bindResult;
}
const userDN = `uid=${uid},${this.#peopledn}`;
const entry = {
objectClass: "inetOrgPerson",
cn: attrs.cn,
sn: attrs.sn,
uid,
userPassword: attrs.userPassword
};
const addResult = await this.#client.add(userDN, entry);
return { op: `add ${uid}`, ok: addResult.ok, error: addResult.error };
}
async getUser (bind, uid) {
const bindResult = await this.#client.bind(bind.dn, bind.password);
if (!bindResult.ok) {
return bindResult;
}
return await this.#client.search(`uid=${uid},${this.#peopledn}`, {});
}
async modUser (bind, uid, newAttrs) {
const bindResult = await this.#client.bind(bind.dn, bind.password);
if (!bindResult.ok) {
return bindResult;
}
const subops = [bindResult];
for (const attr of ["cn", "sn", "userPassword"]) {
if (attr in newAttrs) {
const change = new ldap.Change({
operation: "replace",
modification: {
type: attr,
values: [newAttrs[attr]]
}
});
subops.push(await this.#client.modify(`uid=${uid},${this.#peopledn}`, change));
}
}
return { op: `modify ${uid}`, ok: !subops.some((e) => !e.ok), error: subops.find((e) => !e.ok) || null, subops };
}
async delUser (bind, uid) {
const bindResult = await this.#client.bind(bind.dn, bind.password);
if (!bindResult.ok) {
return bindResult;
}
const userDN = `uid=${uid},${this.#peopledn}`;
const delResult = await this.#client.del(userDN);
const groups = await this.#client.search(this.#groupsdn, {
scope: "one",
filter: `(member=uid=${uid},${this.#peopledn})`
});
if (!groups.ok) {
return { op: `del ${uid}`, ok: groups.ok, error: groups.error, subops: [bindResult, delResult, groups]}
}
const groupsubops = [];
for (const element of groups.entries) {
let change = null;
if (element.attributes.member.length === 1) {
change = new ldap.Change({
operation: "replace",
modification: {
type: "member",
values: [""]
}
});
}
else {
change = new ldap.Change({
operation: "delete",
modification: {
type: "member",
values: [`uid=${uid},${this.#peopledn}`]
}
});
}
const delResult = await this.#client.modify(element.dn, change);
groupsubops.push(delResult);
}
return { op: `del ${uid}`, ok: delResult.ok, error: delResult.error, subops: [bindResult, delResult, groups].concat(groupsubops) };
}
async addGroup (bind, gid, attrs) {
const bindResult = await this.#client.bind(bind.dn, bind.password);
if (!bindResult.ok) {
return bindResult;
}
const groupDN = `cn=${gid},${this.#groupsdn}`;
const entry = {
objectClass: "groupOfNames",
member: attrs && attrs.member ? attrs.member : "",
cn: gid
};
const addResult = await this.#client.add(groupDN, entry);
return { op: `add ${gid}`, ok: addResult.ok, error: addResult.error, subops: [bindResult, addResult] };
}
async getGroup (bind, gid) {
const bindResult = await this.#client.bind(bind.dn, bind.password);
if (!bindResult.ok) {
return bindResult;
}
return await this.#client.search(`cn=${gid},${this.#groupsdn}`, {});
}
async delGroup (bind, gid) {
const bindResult = await this.#client.bind(bind.dn, bind.password);
if (!bindResult.ok) {
return bindResult;
}
const groupDN = `cn=${gid},${this.#groupsdn}`;
const delResult = await this.#client.del(groupDN);
return { op: `del ${gid}`, ok: delResult.ok, error: delResult.error, subops: [bindResult, delResult] };
}
async addUserToGroup (bind, uid, gid) {
const bindResult = await this.#client.bind(bind.dn, bind.password);
if (!bindResult.ok) {
return bindResult;
}
const checkGroupEntry = await this.#client.search(`cn=${gid},${this.#groupsdn}`, {});
if (checkGroupEntry.ok) {
// add the user
const change = new ldap.Change({
operation: "add",
modification: {
type: "member",
values: [`uid=${uid},${this.#peopledn}`]
}
});
const addResult = await this.#client.modify(`cn=${gid},${this.#groupsdn}`, change);
if (!addResult.ok) {
return addResult;
}
// check if there is a blank entry in the group
const groupEntry = checkGroupEntry.entries[0];
if (groupEntry.attributes.member.includes("")) {
// delete the blank user
const change = new ldap.Change({
operation: "delete",
modification: {
type: "member",
values: [""]
}
});
const fixResult = await this.#client.modify(`cn=${gid},${this.#groupsdn}`, change);
return { op: `add ${uid} to ${gid}`, ok: addResult.ok && fixResult.ok, error: addResult.error ? addResult.error : fixResult.error, subops: [bindResult, addResult, fixResult] };
}
return { op: `add ${uid} to ${gid}`, ok: true, error: null, subops: [bindResult, addResult] };
}
else {
return { op: `add ${uid} to ${gid}`, ok: false, error: `${gid} does not exist`, subops: [bindResult] };
}
}
async delUserFromGroup (bind, uid, gid) {
const bindResult = await this.#client.bind(bind.dn, bind.password);
if (!bindResult.ok) {
return bindResult;
}
const change = new ldap.Change({
operation: "delete",
modification: {
type: "member",
values: [`uid=${uid},${this.#peopledn}`]
}
});
const delResult = await this.#client.modify(`cn=${gid},${this.#groupsdn}`, change);
return { op: `del ${uid} from ${gid}`, ok: delResult.ok, error: delResult.error, subops: [bindResult, delResult] };
}
async search (base, opts) {
return await this.#client.search(base, opts);
}
}
class LDAPJS_CLIENT_ASYNC_WRAPPER {
#client = null;
constructor (opts) {
this.#client = ldap.createClient(opts);
this.#client.on("error", (err) => {
console.error(`An error occured:\n${err}`);
});
this.#client.on("connectError", (err) => {
console.error(`Unable to connect to ${opts.url}:\n${err}`);
});
}
bind (dn, password) {
return new Promise((resolve) => {
this.#client.bind(dn, password, (err) => {
if (err) {
resolve({ op: `bind ${dn}`, ok: false, error: err });
}
else {
resolve({ op: `bind ${dn}`, ok: true, error: null });
}
});
});
}
add (dn, entry) {
return new Promise((resolve) => {
this.#client.add(dn, entry, (err) => {
if (err) {
resolve({ op: `add ${dn}`, ok: false, error: err });
}
else {
resolve({ op: `add ${dn}`, ok: true, error: null });
}
});
});
}
search (base, options) {
return new Promise((resolve) => {
this.#client.search(base, options, (err, res) => {
if (err) {
return resolve({ op: `search ${base}`, ok: false, error: err });
}
const results = { op: `search ${base}`, ok: false, error: null, status: 1, message: "", entries: [] };
res.on("searchRequest", (searchRequest) => { });
res.on("searchEntry", (entry) => {
const attributes = {};
for (const element of entry.pojo.attributes) {
attributes[element.type] = element.values;
}
results.entries.push({ dn: entry.pojo.objectName, attributes });
});
res.on("searchReference", (referral) => { });
res.on("error", (error) => {
results.ok = error.status === 0;
results.status = error.status;
results.message = error.message;
resolve(results);
});
res.on("end", (result) => {
results.ok = result.status === 0;
results.status = result.status;
results.message = result.message;
resolve(results);
});
});
});
}
modify (name, changes) {
return new Promise((resolve) => {
this.#client.modify(name, changes, (err) => {
if (err) {
resolve({ op: `modify ${name} ${changes.operation} ${changes.modification.type}`, ok: false, error: err });
}
else {
resolve({ op: `modify ${name} ${changes.operation} ${changes.modification.type}`, ok: true, error: null });
}
});
});
}
del (dn) {
return new Promise((resolve) => {
this.#client.del(dn, (err) => {
if (err) {
resolve({ op: `del ${dn}`, ok: false, error: err });
}
else {
resolve({ op: `del ${dn}`, ok: true, error: null });
}
});
});
}
}