mostly because code is being autogenerated by all the AI stuff using this prefix. it's also used in the stack trace.
540 lines
22 KiB
JavaScript
540 lines
22 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
getConfig,
|
|
setConfig,
|
|
|
|
verifyPassword,
|
|
maybeCreateUser,
|
|
|
|
supports2FA,
|
|
|
|
startSyncer,
|
|
|
|
removePrivateFields,
|
|
|
|
sync
|
|
};
|
|
|
|
const assert = require('node:assert'),
|
|
AuditSource = require('./auditsource.js'),
|
|
BoxError = require('./boxerror.js'),
|
|
constants = require('./constants.js'),
|
|
cron = require('./cron.js'),
|
|
debug = require('debug')('box:externalldap'),
|
|
eventlog = require('./eventlog.js'),
|
|
groups = require('./groups.js'),
|
|
ldap = require('ldapjs'),
|
|
safe = require('safetydance'),
|
|
settings = require('./settings.js'),
|
|
tasks = require('./tasks.js'),
|
|
users = require('./users.js'),
|
|
util = require('node:util');
|
|
|
|
function injectPrivateFields(newConfig, currentConfig) {
|
|
if (newConfig.bindPassword === constants.SECRET_PLACEHOLDER) newConfig.bindPassword = currentConfig.bindPassword;
|
|
}
|
|
|
|
function removePrivateFields(ldapConfig) {
|
|
assert.strictEqual(typeof ldapConfig, 'object');
|
|
if (ldapConfig.bindPassword) ldapConfig.bindPassword = constants.SECRET_PLACEHOLDER;
|
|
return ldapConfig;
|
|
}
|
|
|
|
function translateUser(ldapConfig, ldapUser) {
|
|
assert.strictEqual(typeof ldapConfig, 'object');
|
|
|
|
// RFC: https://datatracker.ietf.org/doc/html/rfc2798
|
|
const user = {
|
|
username: typeof ldapUser[ldapConfig.usernameField] === 'string' ? ldapUser[ldapConfig.usernameField].toLowerCase() : '',
|
|
email: ldapUser.mail || ldapUser.mailPrimaryAddress,
|
|
twoFactorAuthenticationEnabled: !!ldapUser.twoFactorAuthenticationEnabled,
|
|
displayName: ldapUser.displayName || ldapUser.cn // user.giveName + ' ' + user.sn
|
|
};
|
|
|
|
if (!user.username || !user.email || !user.displayName) {
|
|
debug(`[Invalid LDAP user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
|
return null;
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
async function getConfig() {
|
|
const value = await settings.get(settings.EXTERNAL_LDAP_KEY);
|
|
if (value === null) return { provider: 'noop', autoCreate: false };
|
|
|
|
const config = JSON.parse(value);
|
|
if (!config.autoCreate) config.autoCreate = false; // ensure new keys
|
|
return config;
|
|
}
|
|
|
|
async function setConfig(newConfig, auditSource) {
|
|
assert.strictEqual(typeof newConfig, 'object');
|
|
assert(auditSource && typeof auditSource === 'object');
|
|
|
|
if (constants.DEMO) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');
|
|
|
|
const currentConfig = await getConfig();
|
|
|
|
injectPrivateFields(newConfig, currentConfig);
|
|
|
|
const error = await testConfig(newConfig);
|
|
if (error) throw error;
|
|
|
|
await settings.setJson(settings.EXTERNAL_LDAP_KEY, newConfig);
|
|
|
|
if (newConfig.provider === 'noop') {
|
|
await users.resetSources(); // otherwise, the owner could be 'ldap' source and lock themselves out
|
|
await groups.resetSources();
|
|
}
|
|
|
|
await eventlog.add(eventlog.ACTION_EXTERNAL_LDAP_CONFIGURE, auditSource, { oldConfig: removePrivateFields(currentConfig), config: removePrivateFields(newConfig) });
|
|
|
|
await cron.handleExternalLdapChanged(newConfig);
|
|
}
|
|
|
|
function supports2FA(config) {
|
|
return config.provider === 'cloudron';
|
|
}
|
|
|
|
// performs service bind if required
|
|
async function getClient(config, options) {
|
|
assert.strictEqual(typeof config, 'object');
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
// basic validation to not crash
|
|
try { ldap.parseDN(config.baseDn); } catch (e) { throw new BoxError(BoxError.BAD_FIELD, `invalid baseDn ${config.baseDn}: ${e.message}`); }
|
|
try { ldap.parseFilter(config.filter); } catch (e) { throw new BoxError(BoxError.BAD_FIELD, `invalid filter ${config.filter}: ${e.mssage}`); }
|
|
|
|
let client;
|
|
try {
|
|
const ldapConfig = {
|
|
url: config.url,
|
|
tlsOptions: {
|
|
rejectUnauthorized: config.acceptSelfSignedCerts ? false : true
|
|
},
|
|
// https://github.com/ldapjs/node-ldapjs/issues/486
|
|
timeout: 60000,
|
|
connectTimeout: 10000
|
|
};
|
|
|
|
client = ldap.createClient(ldapConfig);
|
|
} catch (e) {
|
|
if (e instanceof ldap.ProtocolError) throw new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid');
|
|
throw new BoxError(BoxError.INTERNAL_ERROR, `Client creation error: ${e.message}`);
|
|
}
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
// ensure we don't just crash
|
|
client.on('error', function (error) { // don't reject, we must have gotten a bind error
|
|
debug('getClient: ExternalLdap client error:', error);
|
|
});
|
|
|
|
// skip bind auth if none exist or if not wanted
|
|
if (!config.bindDn || !options.bind) return resolve(client);
|
|
|
|
client.bind(config.bindDn, config.bindPassword, function (error) {
|
|
if (error instanceof ldap.InvalidCredentialsError) return reject(new BoxError(BoxError.INVALID_CREDENTIALS, 'Incorrect bind password'));
|
|
if (error) return reject(new BoxError(BoxError.EXTERNAL_ERROR, `Bind error: ${error.message}`));
|
|
|
|
resolve(client);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function clientSearch(client, dn, searchOptions) {
|
|
assert.strictEqual(typeof client, 'object');
|
|
assert.strictEqual(typeof dn, 'string');
|
|
assert.strictEqual(typeof searchOptions, 'object');
|
|
|
|
// basic validation to not crash
|
|
try { ldap.parseDN(dn); } catch (e) { throw new BoxError(BoxError.BAD_FIELD, `invalid dn ${dn}: ${e.message}`); }
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
client.search(dn, searchOptions, function (error, result) {
|
|
if (error instanceof ldap.NoSuchObjectError) return reject(new BoxError(BoxError.NOT_FOUND, `dn not found ${dn}`));
|
|
if (error) return reject(new BoxError(BoxError.EXTERNAL_ERROR, `search error: ${error.message}`));
|
|
|
|
const ldapObjects = [];
|
|
|
|
result.on('searchEntry', entry => ldapObjects.push(entry.object));
|
|
result.on('error', error => reject(new BoxError(BoxError.EXTERNAL_ERROR, `search error: ${error.message}`)));
|
|
|
|
result.on('end', function (result) {
|
|
if (result.status !== 0) return reject(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
|
|
|
|
resolve(ldapObjects);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
async function supportsPagination(client) {
|
|
assert.strictEqual(typeof client, 'object');
|
|
|
|
const searchOptions = {
|
|
scope: 'base',
|
|
filter: '(objectClass=*)',
|
|
attributes: ['supportedControl', 'supportedExtension', 'supportedFeature']
|
|
};
|
|
|
|
const result = await clientSearch(client, '', searchOptions);
|
|
const controls = result.supportedControl;
|
|
if (!controls || !Array.isArray(controls)) {
|
|
debug('supportsPagination: no supportedControl attribute returned');
|
|
return false;
|
|
}
|
|
|
|
if (!controls.includes(ldap.PagedResultsControl.OID)) {
|
|
debug('supportsPagination: server does not support pagination. Available controls:', controls);
|
|
return false;
|
|
}
|
|
|
|
debug('supportsPagination: server supports pagination');
|
|
return true;
|
|
}
|
|
|
|
async function ldapGetByDN(config, dn) {
|
|
assert.strictEqual(typeof config, 'object');
|
|
assert.strictEqual(typeof dn, 'string');
|
|
|
|
debug(`ldapGetByDN: Get object at ${dn}`);
|
|
|
|
const client = await getClient(config, { bind: true });
|
|
const paged = await supportsPagination(client);
|
|
const searchOptions = {
|
|
paged,
|
|
scope: 'sub'
|
|
};
|
|
const result = await clientSearch(client, dn, searchOptions);
|
|
client.unbind();
|
|
if (result.length === 0) throw new BoxError(BoxError.NOT_FOUND, `dn ${dn} not found`);
|
|
return result[0];
|
|
}
|
|
|
|
async function ldapUserSearch(config, options) {
|
|
assert.strictEqual(typeof config, 'object');
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
const client = await getClient(config, { bind: true });
|
|
const paged = await supportsPagination(client);
|
|
|
|
const searchOptions = {
|
|
paged,
|
|
filter: ldap.parseFilter(config.filter),
|
|
scope: 'sub'
|
|
};
|
|
|
|
if (options.filter) { // https://github.com/ldapjs/node-ldapjs/blob/master/docs/filters.md
|
|
const extraFilter = ldap.parseFilter(options.filter);
|
|
searchOptions.filter = new ldap.AndFilter({ filters: [ extraFilter, searchOptions.filter ] });
|
|
}
|
|
|
|
const result = await clientSearch(client, config.baseDn, searchOptions);
|
|
client.unbind();
|
|
return result;
|
|
}
|
|
|
|
async function ldapGroupSearch(config, options) {
|
|
assert.strictEqual(typeof config, 'object');
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
const searchOptions = {
|
|
paged: true,
|
|
scope: 'sub' // We may have to make this configurable
|
|
};
|
|
|
|
if (config.groupFilter) searchOptions.filter = ldap.parseFilter(config.groupFilter);
|
|
|
|
if (options.filter) { // https://github.com/ldapjs/node-ldapjs/blob/master/docs/filters.md
|
|
const extraFilter = ldap.parseFilter(options.filter);
|
|
searchOptions.filter = new ldap.AndFilter({ filters: [ extraFilter, searchOptions.filter ] });
|
|
}
|
|
|
|
const client = await getClient(config, { bind: true });
|
|
const result = await clientSearch(client, config.groupBaseDn, searchOptions);
|
|
client.unbind();
|
|
return result;
|
|
}
|
|
|
|
async function testConfig(config) {
|
|
assert.strictEqual(typeof config, 'object');
|
|
|
|
if (config.provider === 'noop') return null;
|
|
|
|
if (!config.url) return new BoxError(BoxError.BAD_FIELD, 'url must not be empty');
|
|
if (!config.url.startsWith('ldap://') && !config.url.startsWith('ldaps://')) return new BoxError(BoxError.BAD_FIELD, 'url is missing ldap:// or ldaps:// prefix');
|
|
if (!config.usernameField) config.usernameField = 'uid';
|
|
|
|
// bindDn may not be a dn!
|
|
if (!config.baseDn) return new BoxError(BoxError.BAD_FIELD, 'basedn must not be empty');
|
|
try { ldap.parseDN(config.baseDn); } catch (e) { throw new BoxError(BoxError.BAD_FIELD, `invalid base ${config.baseDn}: ${e.message}`); }
|
|
|
|
if (!config.filter) return new BoxError(BoxError.BAD_FIELD, 'filter must not be empty');
|
|
try { ldap.parseFilter(config.filter); } catch (e) { return new BoxError(BoxError.BAD_FIELD, `invalid filter ${config.filter}: ${e.message}`); }
|
|
|
|
if ('syncGroups' in config && typeof config.syncGroups !== 'boolean') return new BoxError(BoxError.BAD_FIELD, 'syncGroups must be a boolean');
|
|
if ('acceptSelfSignedCerts' in config && typeof config.acceptSelfSignedCerts !== 'boolean') return new BoxError(BoxError.BAD_FIELD, 'acceptSelfSignedCerts must be a boolean');
|
|
|
|
if (config.syncGroups) {
|
|
if (!config.groupBaseDn) return new BoxError(BoxError.BAD_FIELD, 'groupBaseDn must not be empty');
|
|
try { ldap.parseDN(config.groupBaseDn); } catch (e) { return new BoxError(BoxError.BAD_FIELD, `invalid groupBaseDn ${config.groupBaseDn}: ${e.message}`); }
|
|
|
|
if (!config.groupFilter) return new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty');
|
|
try { ldap.parseFilter(config.groupFilter); } catch (e) { return new BoxError(BoxError.BAD_FIELD, `invalid groupFilter ${config.groupFilter}: ${e.message}`); }
|
|
|
|
if (!config.groupnameField || typeof config.groupnameField !== 'string') return new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty');
|
|
}
|
|
|
|
const [error, client] = await safe(getClient(config, { bind: true }));
|
|
if (error) return error;
|
|
|
|
const opts = {
|
|
filter: config.filter,
|
|
scope: 'sub'
|
|
};
|
|
|
|
const [searchError, ] = await safe(clientSearch(client, config.baseDn, opts));
|
|
client.unbind();
|
|
if (searchError) return searchError;
|
|
|
|
return null;
|
|
}
|
|
|
|
async function maybeCreateUser(identifier) {
|
|
assert.strictEqual(typeof identifier, 'string');
|
|
|
|
const config = await getConfig();
|
|
if (config.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
|
|
if (!config.autoCreate) throw new BoxError(BoxError.BAD_STATE, 'auto create not enabled');
|
|
|
|
const ldapUsers = await ldapUserSearch(config, { filter: `${config.usernameField}=${identifier}` });
|
|
if (ldapUsers.length === 0) throw new BoxError(BoxError.NOT_FOUND, `no users found for filter ${config.usernameField}=${identifier}`);
|
|
if (ldapUsers.length > 1) throw new BoxError(BoxError.CONFLICT, `more than 1 user matches filter ${config.usernameField}=${identifier}`);
|
|
|
|
const user = translateUser(config, ldapUsers[0]);
|
|
if (!user) throw new BoxError(BoxError.BAD_FIELD, 'Failed to translate user');
|
|
|
|
return await users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP);
|
|
}
|
|
|
|
async function verifyPassword(username, password, options) {
|
|
assert.strictEqual(typeof username, 'string');
|
|
assert.strictEqual(typeof password, 'string');
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
const config = await getConfig();
|
|
if (config.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
|
|
|
|
const ldapUsers = await ldapUserSearch(config, { filter: `${config.usernameField}=${username}` });
|
|
if (ldapUsers.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'no such user');
|
|
if (ldapUsers.length > 1) throw new BoxError(BoxError.CONFLICT, 'multiple users found');
|
|
|
|
const client = await getClient(config, { bind: false });
|
|
|
|
let userAuthDn;
|
|
if (!options.skipTotpCheck && supports2FA(config)) {
|
|
// inject totptoken into first attribute. in ldap, '+' is the attribute separator in a RDNS
|
|
const rdns = ldapUsers[0].dn.split(',');
|
|
userAuthDn = `${rdns[0]}+totptoken=${options.totpToken},` + rdns.slice(1).join(',');
|
|
} else {
|
|
userAuthDn = ldapUsers[0].dn;
|
|
}
|
|
|
|
const [error] = await safe(util.promisify(client.bind.bind(client))(userAuthDn, password));
|
|
client.unbind();
|
|
if (error instanceof ldap.InvalidCredentialsError) throw new BoxError(BoxError.INVALID_CREDENTIALS, error.lde_message);
|
|
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, `Bind error: ${error.message}`);
|
|
|
|
const user = translateUser(config, ldapUsers[0]);
|
|
if (!user) throw new BoxError(BoxError.BAD_FIELD, 'could not translate user');
|
|
|
|
return user;
|
|
}
|
|
|
|
async function startSyncer() {
|
|
const config = await getConfig();
|
|
if (config.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
|
|
|
|
const taskId = await tasks.add(tasks.TASK_SYNC_EXTERNAL_LDAP, []);
|
|
safe(tasks.startTask(taskId, {}), { debug }); // background
|
|
return taskId;
|
|
}
|
|
|
|
async function syncUsers(config, progressCallback) {
|
|
assert.strictEqual(typeof config, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const ldapUsers = await ldapUserSearch(config, {});
|
|
|
|
debug(`syncUsers: Found ${ldapUsers.length} users`);
|
|
|
|
let percent = 10;
|
|
const step = 30/(ldapUsers.length+1); // ensure no divide by 0
|
|
|
|
// we ignore all errors here and just log them for now
|
|
for (let i = 0; i < ldapUsers.length; i++) {
|
|
const ldapUser = translateUser(config, ldapUsers[i]);
|
|
if (!ldapUser) continue;
|
|
|
|
percent += step;
|
|
progressCallback({ percent, message: `Syncing... ${ldapUser.username}` });
|
|
|
|
const user = await users.getByUsername(ldapUser.username);
|
|
|
|
if (!user) {
|
|
debug(`syncUsers: [adding user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
|
|
|
const [userAddError] = await safe(users.add(ldapUser.email, { username: ldapUser.username, password: null, displayName: ldapUser.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP));
|
|
if (userAddError) debug('syncUsers: Failed to create user. %j %o', ldapUser, userAddError);
|
|
} else if (user.source !== 'ldap') {
|
|
debug(`syncUsers: [mapping user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
|
|
|
const [userMappingError] = await safe(users.update(user, { email: ldapUser.email, fallbackEmail: ldapUser.email, displayName: ldapUser.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP));
|
|
if (userMappingError) debug('Failed to map user. %j %o', ldapUser, userMappingError);
|
|
} else if (user.email !== ldapUser.email || user.displayName !== ldapUser.displayName) {
|
|
debug(`syncUsers: [updating user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
|
|
|
const [userUpdateError] = await safe(users.update(user, { email: ldapUser.email, fallbackEmail: ldapUser.email, displayName: ldapUser.displayName }, AuditSource.EXTERNAL_LDAP));
|
|
if (userUpdateError) debug('Failed to update user. %j %o', ldapUser, userUpdateError);
|
|
} else {
|
|
// user known and up-to-date
|
|
debug(`syncUsers: [up-to-date user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
|
}
|
|
}
|
|
|
|
debug('syncUsers: done');
|
|
}
|
|
|
|
async function syncGroups(config, progressCallback) {
|
|
assert.strictEqual(typeof config, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
if (!config.syncGroups) {
|
|
debug('syncGroups: Group sync is disabled');
|
|
progressCallback({ percent: 70, message: 'Skipping group sync...' });
|
|
return [];
|
|
}
|
|
|
|
const ldapGroups = await ldapGroupSearch(config, {});
|
|
|
|
debug(`syncGroups: Found ${ldapGroups.length} groups`);
|
|
|
|
let percent = 40;
|
|
const step = 30/(ldapGroups.length+1); // ensure no divide by 0
|
|
|
|
for (const ldapGroup of ldapGroups) {
|
|
let groupName = ldapGroup[config.groupnameField];
|
|
if (!groupName) return;
|
|
if (typeof groupName !== 'string') return; // some servers return empty array for unknown properties :-/
|
|
|
|
groupName = groupName.toLowerCase();
|
|
|
|
percent += step;
|
|
progressCallback({ percent, message: `Syncing... ${groupName}` });
|
|
|
|
const result = await groups.getByName(groupName);
|
|
|
|
if (!result) {
|
|
debug(`syncGroups: [adding group] groupname=${groupName}`);
|
|
const [error] = await safe(groups.add({ name: groupName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP));
|
|
if (error) debug('syncGroups: Failed to create group', groupName, error);
|
|
} else {
|
|
// convert local group to ldap group. 2 reasons:
|
|
// 1. we reset source flag when externalldap is disabled. if we renable, it automatically coverts
|
|
// 2. externalldap connector usually implies user wants to user external users/groups.
|
|
groups.update(result.id, { source: 'ldap' });
|
|
debug(`syncGroups: [up-to-date group] groupname=${groupName}`);
|
|
}
|
|
}
|
|
|
|
debug('syncGroups: sync done');
|
|
}
|
|
|
|
async function syncGroupMembers(config, progressCallback) {
|
|
assert.strictEqual(typeof config, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
if (!config.syncGroups) {
|
|
debug('syncGroupMembers: Group users sync is disabled');
|
|
progressCallback({ percent: 99, message: 'Skipping group users sync...' });
|
|
return [];
|
|
}
|
|
|
|
const allGroups = await groups.list();
|
|
const ldapGroups = allGroups.filter(function (g) { return g.source === 'ldap'; });
|
|
debug(`syncGroupMembers: Found ${ldapGroups.length} groups to sync users`);
|
|
|
|
for (const group of ldapGroups) {
|
|
debug(`syncGroupMembers: Sync users for group ${group.name}`);
|
|
|
|
const result = await ldapGroupSearch(config, {});
|
|
if (!result || result.length === 0) {
|
|
debug(`syncGroupMembers: Unable to find group ${group.name} ignoring for now.`);
|
|
continue;
|
|
}
|
|
|
|
// since our group names are lowercase we cannot use potentially case matching ldap filters
|
|
const found = result.find(function (r) {
|
|
if (!r[config.groupnameField]) return false;
|
|
return r[config.groupnameField].toLowerCase() === group.name;
|
|
});
|
|
|
|
if (!found) {
|
|
debug(`syncGroupMembers: Unable to find group ${group.name} ignoring for now.`);
|
|
continue;
|
|
}
|
|
|
|
let ldapGroupMembers = found.member || found.uniquemember || [];
|
|
|
|
// if only one entry is in the group ldap returns a string, not an array!
|
|
if (typeof ldapGroupMembers === 'string') ldapGroupMembers = [ ldapGroupMembers ];
|
|
|
|
debug(`syncGroupMembers: Group ${group.name} has ${ldapGroupMembers.length} members.`);
|
|
|
|
const userIds = [];
|
|
for (const memberDn of ldapGroupMembers) {
|
|
const [ldapError, result] = await safe(ldapGetByDN(config, memberDn));
|
|
if (ldapError) {
|
|
debug(`syncGroupMembers: Group ${group.name} failed to get ${memberDn}: %o`, ldapError);
|
|
continue;
|
|
}
|
|
|
|
debug(`syncGroupMembers: Group ${group.name} has member object ${memberDn}`);
|
|
|
|
const username = result[config.usernameField]?.toLowerCase();
|
|
if (!username) continue;
|
|
|
|
const [getError, userObject] = await safe(users.getByUsername(username));
|
|
if (getError || !userObject) {
|
|
debug(`syncGroupMembers: Failed to get user by username ${username}. %o`, getError ? getError : 'User not found');
|
|
continue;
|
|
}
|
|
|
|
userIds.push(userObject.id);
|
|
}
|
|
const [setError] = await safe(groups.setMembers(group, userIds, { skipSourceCheck: true }, AuditSource.EXTERNAL_LDAP));
|
|
if (setError) debug(`syncGroupMembers: Failed to set members of group ${group.name}. %o`, setError);
|
|
}
|
|
|
|
debug('syncGroupMembers: done');
|
|
}
|
|
|
|
async function sync(progressCallback) {
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
progressCallback({ percent: 10, message: 'Starting ldap user sync' });
|
|
|
|
const config = await getConfig();
|
|
if (config.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
|
|
|
|
await syncUsers(config, progressCallback);
|
|
await syncGroups(config, progressCallback);
|
|
await syncGroupMembers(config, progressCallback);
|
|
|
|
progressCallback({ percent: 100, message: 'Done' });
|
|
|
|
debug('sync: done');
|
|
}
|