node-ldap/lib/server.js

247 lines
7.1 KiB
JavaScript

const ldap = require('ldapjs');
const log = require('log').get('server');
const CronJob = require('cron').CronJob;
const {
ldap: ldapConfig,
} = require('./config');
const { parseDN, parseFilter } = ldap;
const {
validateUserPassword,
storeUserPassword,
} = require('./utilities/password');
const {
getBaseEntries,
getRootDN,
getOrganizationBaseDN,
validateAdminPassword,
validateAdminPermission,
} = require('./utilities/ldap');
const {
createProvider,
getProviderLDAPEntries,
reloadEntriesFromProvider,
} = require('./utilities/provider');
const {
getLDAPCustomGroupEntries,
} = require('./utilities/custom_groups');
const {
getDBRecordForUserId,
saveDBRecordForUserId,
} = require('./db/db');
let server = ldap.createServer();
// equalsDN for comparing dn in case-insensitive
function equalsDN(a, b) {
a = parseDN((a instanceof String ? a : a.toString()).toLowerCase());
b = parseDN((b instanceof String ? b : b.toString()).toLowerCase());
return a.equals(b);
}
function getPersonMatchedDN(reqDN) {
const personFilter = parseFilter('(objectclass=inetOrgPerson)');
const [ matchedUser ] = getProviderLDAPEntries().filter(entry => {
return equalsDN(reqDN, entry.dn) && personFilter.matches(entry.attributes);
});
return matchedUser;
}
function authorize(req, res, next) {
log.debug('===> authorize info');
log.debug('req type', req.constructor.name);
log.debug('reqDN', (req.baseObject || '').toString());
log.debug('bindDN', req.connection.ldap.bindDN.toString());
log.debug('====');
const bindDN = req.connection.ldap.bindDN;
const rootDN = parseDN(getRootDN());
// person can search itself
if (req instanceof ldap.SearchRequest) {
if (equalsDN(bindDN, req.baseObject)) {
return next();
}
}
// root admin can do everything
if (equalsDN(bindDN.parent(), rootDN)) {
return next();
}
return next(new ldap.InsufficientAccessRightsError());
}
// DSE
server.search('', authorize, function(req, res, next) {
var baseObject = {
dn: '',
structuralObjectClass: 'OpenLDAProotDSE',
configContext: 'cn=config',
attributes: {
objectclass: ['top', 'OpenLDAProotDSE'],
namingContexts: [ getRootDN() ],
supportedLDAPVersion: ['3'],
subschemaSubentry:['cn=Subschema']
}
};
log.info("scope "+req.scope+" filter "+req.filter+" baseObject "+req.baseObject);
if('base' == req.scope
&& '(objectclass=*)' == req.filter.toString()
&& req.baseObject == ''){
res.send(baseObject);
}
//log.info('scope: ' + req.scope);
//log.info('filter: ' + req.filter.toString());
//log.info('attributes: ' + req.attributes);
res.end();
return next();
});
server.search('cn=Subschema', authorize, function(req, res, next) {
var schema = {
dn: 'cn=Subschema',
attributes: {
objectclass: ['top', 'subentry', 'subschema', 'extensibleObject'],
cn: ['Subschema']
}
};
res.send(schema);
res.end();
return next();
});
server.search(getRootDN(), authorize, (req, res, next) => {
log.info("search req scope " + req.scope + " filter " + req.filter + " baseObject " + req.baseObject);
// console.log('sizeLimit', req.sizeLimit, 'timeLimit', req.timeLimit);
const reqDN = parseDN(req.baseObject.toString().toLowerCase());
const comparators = {
'one': (objDN) => {
return equalsDN(reqDN, objDN.parent());
},
'sub': (objDN) => {
return equalsDN(reqDN, objDN) || reqDN.parentOf(objDN);
},
'base': (objDN) => {
return equalsDN(reqDN, objDN);
},
}
const comparator = comparators[req.scope];
if (comparator) {
[
...getBaseEntries(),
...getProviderLDAPEntries(),
...getLDAPCustomGroupEntries(),
].filter(entry => {
return comparator(parseDN(entry.dn.toLowerCase())) && req.filter.matches(entry.attributes);
}).forEach(entry => {
log.debug('send entry', entry.dn);
res.send(entry);
});
}
res.end();
return next();
});
server.modify(getOrganizationBaseDN(), authorize, async (req, res, next) => {
// DN: uid=zhangsan@alibaba-inc.com, ou=HR, o=LongBridge, dc=longbridge-inc, dc=com
// changes:
// operation: replace
// modification: {"type":"userpassword","vals":["{SMD5}jFCfdWsVs6GF/1CO2+2ZBwIevCudAH7v"]}
const reqDN = parseDN(req.dn.toString().toLowerCase());
const username = req.connection.ldap.bindDN.rdns[0].attrs.cn.value;
log.debug('modify req', reqDN.toString());
if (!validateAdminPermission(username, 'canModifyEntry')) {
return next(new ldap.InsufficientAccessRightsError());
}
const matchedUser = getPersonMatchedDN(reqDN);
// console.log('modify user', matchedUser);
if (matchedUser) {
// console.log('DN: ' + req.dn.toString());
// console.log('changes:');
for (let i = 0; i < req.changes.length; ++i) {
const c = req.changes[i];
// console.log(' operation: ' + c.operation);
// console.log(' modification: ' + c.modification.toString());
if (c.operation === 'replace' && c.modification.type === 'userpassword') {
let newpass = c.modification.vals[0];
if (newpass.substr(0, 9) !== '{SSHA256}') {
newpass = storeUserPassword(newpass);
}
await saveDBRecordForUserId(matchedUser.attributes.uid, {
password: newpass,
});
// console.log('modify user', matchedUser.attributes.uid, 'pass to', newpass);
}
if (c.operation === 'replace' && c.modification.type === 'otpsecret') {
let newsecret = c.modification.vals[0];
// console.log('modify user', matchedUser.dn, 'otp secret to', newsecret);
await saveDBRecordForUserId(matchedUser.attributes.uid, {
otpsecret: newsecret,
});
}
}
}
res.end();
return next();
});
server.bind(getRootDN(), async (req, res, next) => {
const reqDN = parseDN(req.dn.toString().toLowerCase());
log.debug('bind req', reqDN.toString())
// console.log('bind PW: ' + req.credentials);
if (parseDN(getRootDN().toLowerCase()).equals(reqDN.parent())) {
// admins
const username = reqDN.rdns[0].attrs.cn.value;
if (validateAdminPassword(username, req.credentials)) {
res.end();
return next();
} else {
return next(new ldap.InvalidCredentialsError());
}
} else if (parseDN(getOrganizationBaseDN().toLowerCase()).parentOf(reqDN)) {
// users
const matchedUser = getPersonMatchedDN(reqDN);
if (matchedUser) {
const record = await getDBRecordForUserId(matchedUser.attributes.uid);
if (validateUserPassword(record.password || '123456', req.credentials)) {
res.end();
return next();
} else {
log.debug('password failed');
return next(new ldap.InvalidCredentialsError());
}
} else {
log.debug('user not found');
return next(new ldap.InvalidCredentialsError());
}
}
return next(new ldap.InsufficientAccessRightsError());
});
function runVirtualLDAPServer() {
server.listen(ldapConfig.listenPort, async function() {
await createProvider();
log.info('virtual-ldap listening at ' + server.url);
});
// reload data from server every hour
new CronJob('0 0 * * * *', () => {
reloadEntriesFromProvider();
}, null, true, 'Asia/Shanghai').start();
}
module.exports = {
runVirtualLDAPServer,
};