Initial commit

This commit is contained in:
ohdarling88 2020-02-03 14:10:29 +08:00
commit aa5632c8a0
23 changed files with 2160 additions and 0 deletions

62
.gitignore vendored Normal file
View File

@ -0,0 +1,62 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
cache
/config.js
.DS_Store

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2020 ohdarling88
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

139
README.md Normal file
View File

@ -0,0 +1,139 @@
# Virtual-LDAP
Virtual-LDAP is a service used to bridge any other account services to the LDAP protocol. With Virtual-LDAP, you can use existing account services (such as DingTalk) as an authorization service for many open source projects.
Virtual-LDAP has a provider architecture, so you can extend it with a custom provider to support any other account service, such as a database-based account service.
Virtual-LDAP is not a complete implementation of LDAP and currently only supports partial binding, search, and modification requests. All directory groups and users will be obtained from the provider.
The database used for Virtual-LDAP is used to store user passwords. Passwords will be hashed using SHA256 plus a salt value.
## Configuration
Virtual-LDAP using JavaScript to configure all settings, include DN, admins, database, provider and custom groups.
For every configuration items, see example config file below.
```javascript
module.exports = {
ldap: {
// LDAP serve port, it is a insecure port, please connect with ldap://
listenPort: 1389,
// Base DN will be o=Example,dc=example,dc=com
// Groups base DN will be ou=Groups,o=Example,dc=example,dc=com
// Users base DN will be ou=People,o=Example,dc=example,dc=com
rootDN: 'dc=example,dc=com',
organization: 'Example',
// Admins who can search or modify directory
admins: [
{
// Bind DN will be cn=keycloak,dc=example,dc=com
commonName: 'keycloak',
password: 'keycloak',
// Can this admin modify user's password
canModifyEntry: true,
},
{
commonName: 'jenkins',
password: 'jenkins',
canModifyEntry: false,
},
]
},
// Database for storing users' password
database: {
type: 'mysql',
host: '127.0.0.1',
port: '23306',
user: 'root',
password: '123456',
database: 'vldap',
},
// Provider for providen account service
provider: {
name: 'dingtalk',
appKey: '__APPKEY__',
appSecret: '__APPSECRET__',
},
// Custom groups, base DN will be ou=CustomGroups,ou=Groups,o=Example,dc=example,dc=com
customGroups: [
{
// DN will be ou=Jenkins Admins,ou=CustomGroups,ou=Groups,o=Example,dc=example,dc=com
name: 'Jenkins Admins',
// User with these mails will be added to the group
members: [ 'jenkins@example.com' ],
}
]
}
```
## LDAP DN
For Virtual-LDAP using the above configuration file, the commonly used DNs are as follows.
**Root DN**
`dc=example,dc=com`
**Search Directory Bind DN**
`cn=keycloak,dc=example,dc=com`
**Groups Base DN**
`ou=Groups,o=Example,dc=example,dc=com`
**Users Base DN**
`ou=People,o=Example,dc=example,dc=com`
**Custom Groups Base DN**
`ou=CustomGroups,ou=Groups,o=Example,dc=example,dc=com`
**Jenkins Admins DN**
`ou=Jenkins Admins,ou=CustomGroups,ou=Groups,o=Example,dc=example,dc=com`
**Typical User DN**
`mail=user@example.com,ou=People,o=Example,dc=example,dc=com`
## Run Virtual-LDAP
Virtual-LDAP can run from source or run as a service in another project.
### Run from source
```bash
git clone https://github.com/ohdarling/virtual-ldap
cd virtual-ldap
npm start
```
### Run as a service
```javascript
const server = require('virtual-ldap');
server.setupVirtualLDAPServer(require("./config"));
server.runVirtualLDAPServer();
```
## Testing with ApacheDirectoryStudio
<img src="screenshots/1-create-connection.png" alt="create connection" width="624" /><img src="screenshots/2-base-auth.png" alt="auth" width="624" />
<img src="screenshots/3-ldap-browse.png" alt="ldap browse" width="878" />
## License
MIT License

50
config.example.js Normal file
View File

@ -0,0 +1,50 @@
module.exports = {
ldap: {
// LDAP serve port, it is a insecure port, please connect with ldap://
listenPort: 1389,
// Base DN will be o=Example,dc=example,dc=com
// Groups base DN will be ou=Groups,o=Example,dc=example,dc=com
// Users base DN will be ou=People,o=Example,dc=example,dc=com
rootDN: 'dc=example,dc=com',
organization: 'Example',
// Admins who can search or modify directory
admins: [
{
// Bind DN will be cn=keycloak,dc=example,dc=com
commonName: 'keycloak',
password: 'keycloak',
// Can this admin modify user's password
canModifyEntry: true,
},
{
commonName: 'jenkins',
password: 'jenkins',
canModifyEntry: false,
},
]
},
// Database for storing users' password
database: {
type: 'mysql',
host: '127.0.0.1',
port: '23306',
user: 'root',
password: '123456',
database: 'vldap',
},
// Provider for providen account service
provider: {
name: 'dingtalk',
appKey: '__APPKEY__',
appSecret: '__APPSECRET__',
},
// Custom groups, base DN will be ou=CustomGroups,ou=Groups,o=Example,dc=example,dc=com
customGroups: [
{
// DN will be ou=Jenkins Admins,ou=CustomGroups,ou=Groups,o=Example,dc=example,dc=com
name: 'Jenkins Admins',
// User with these mails will be added to the group
members: [ 'jenkins@example.com' ],
}
]
}

8
index.js Normal file
View File

@ -0,0 +1,8 @@
const server = require("./lib");
if (require.main === module) {
server.setupVirtualLDAPServer(require("./config.example"));
server.runVirtualLDAPServer();
} else {
module.exports = server;
}

1
lib/config.js Normal file
View File

@ -0,0 +1 @@
module.exports = require("./index").serverConfig;

38
lib/db/db.js Normal file
View File

@ -0,0 +1,38 @@
const {
type: dbType
} = require('../config').database;
const {
dbSelect,
dbInsert,
dbUpdate,
} = require('./' + dbType);
const TABLE_USER_CREDENTIALS = 'user_credentials';
async function getDBRecordForUserId(uid) {
const [ record ] = await dbSelect(TABLE_USER_CREDENTIALS, { uid });
if (record) {
return record;
}
return {
userid: uid,
password: '123456',
};
}
async function saveDBRecordForUserId(uid, data) {
const [ record ] = await await dbSelect(TABLE_USER_CREDENTIALS, { uid });
if (record) {
await dbUpdate(TABLE_USER_CREDENTIALS, data, { uid });
} else {
await dbInsert(TABLE_USER_CREDENTIALS, Object.assign({ uid }, data));
}
}
module.exports = {
getDBRecordForUserId,
saveDBRecordForUserId,
};

17
lib/db/dbinit.sql Normal file
View File

@ -0,0 +1,17 @@
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user_credentials
-- ----------------------------
DROP TABLE IF EXISTS `user_credentials`;
CREATE TABLE `user_credentials` (
`uid` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`otpsecret` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SET FOREIGN_KEY_CHECKS = 1;

19
lib/db/memory.js Normal file
View File

@ -0,0 +1,19 @@
const table = {};
async function dbSelect(table, { uid }) {
return [ table[uid] ];
}
async function dbUpdate(table, params, { uid }) {
table[uid] = Object.assign({}, table[uid] || {}, params);
}
async function dbInsert(table, params) {
table[params.uid] = params;
}
module.exports = {
dbSelect,
dbUpdate,
dbInsert,
};

72
lib/db/mysql.js Normal file
View File

@ -0,0 +1,72 @@
const mysql = require('mysql2');
const {
database: dbConfig,
} = require('../config');
let dbConnected = false;
let dbPool = null;
async function connect() {
if (!dbConnected) {
const opts = Object.assign({}, dbConfig);
delete opts.type;
dbPool = mysql.createPool(opts).promise();
dbConnected = true;
}
}
async function dbQuery(sql, params) {
connect();
const [ rows, fields ] = await dbPool.query(sql, params);
return rows;
}
async function dbSelect(table, params) {
const fields = Object.keys(params);
const values = fields.map(k => params[k]);
const sql = `SELECT * FROM ${table} WHERE ${fields.map(k => `${k}=?`).join(' AND ')}`;
return dbQuery(sql, values);
}
async function dbUpdate(table, params, conditions) {
const fields = Object.keys(params);
const values = fields.map(k => params[k]);
let conditionSQL = '';
if (conditions) {
const cfields = Object.keys(conditions);
const cvalues = cfields.map(k => conditions[k]);
conditionSQL = ` WHERE ${cfields.map(k => `${k}=?`).join(' AND ')}`;
values.push(...cvalues);
}
const sql = `UPDATE ${table} SET ${fields.map(k => `${k}=?`).join(', ')} ${conditionSQL}`;
return dbQuery(sql, values);
}
async function dbInsert(table, params) {
const fields = Object.keys(params);
const values = fields.map(k => params[k]);
const sql = `INSERT INTO ${table} (${fields.join(', ')}) VALUES (${fields.map(k => '?').join(', ')})`;
return dbQuery(sql, values);
}
async function dbDelete(table, conditions) {
const values = [];
let conditionSQL = '';
if (conditions) {
const cfields = Object.keys(conditions);
const cvalues = fields.map(k => conditions[k]);
conditionSQL = ` WHERE ${cfields.map(k => `${k}=?`).join(' AND ')}`;
values.push(...cvalues);
}
const sql = `DELETE FROM ${table} ${conditionSQL}`
return dbQuery(sql, values);
}
module.exports = {
dbQuery,
dbSelect,
dbUpdate,
dbInsert,
dbDelete,
};

17
lib/index.js Normal file
View File

@ -0,0 +1,17 @@
require('log-node')();
const serverConfig = {};
function setupVirtualLDAPServer(config) {
Object.assign(serverConfig, config);
}
function runVirtualLDAPServer() {
require("./server").runVirtualLDAPServer();
}
module.exports = {
serverConfig,
setupVirtualLDAPServer,
runVirtualLDAPServer,
};

292
lib/providers/dingtalk.js Normal file
View File

@ -0,0 +1,292 @@
const axios = require('axios');
const log = require('log').get('dingtalk-provider');
const {
makeGroupEntry,
makePersonEntry,
makeOrganizationUnitEntry,
addMemberToGroup,
} = require('../utilities/ldap');
const {
saveCacheToFile,
loadCacheFromFile,
} = require('../utilities/cache');
let appKey = '';
let appSecret = '';
let accessToken = '';
let allLDAPUsers = [];
let allLDAPOrgUnits = [];
let allLDAPGroups = [];
let allLDAPEntries = [];
function api(path) {
return `https://oapi.dingtalk.com/${path}?access_token=${accessToken}`;
}
function parseName(name) {
// 如果名字没有空格,以中文处理,第一个字为姓,其他为名
// 如果有空格,以英文处理,最后一个单词为姓,其他为名
let givenName = name.substr(1);
let sn = name.substr(0, 1);
if (name.indexOf(' ') > 0) {
const parts = name.split(' ');
sn = parts.pop();
givenName = parts.join(' ');
}
return { givenName, sn };
}
async function ddGet(path, params) {
const apiUrl = path.substr(0, 8) === 'https://' ? path : api(path);
const ret = await axios(apiUrl, { params }).catch(e => {
log.error("Dingtalk API error", e);
return null;
});
if (ret && ret.data) {
if (ret.data.errcode != 0) {
log.error('Dingtalk API error', ret.data);
return null;
} else {
return ret.data;
}
} else {
log.error('Dingtalk API error', ret);
}
return null;
}
async function getToken() {
const token = await ddGet(`https://oapi.dingtalk.com/gettoken?appkey=${appKey}&appsecret=${appSecret}`);
if (token && token.access_token) {
accessToken = token.access_token;
} else {
log.error('Get access token failed', token);
}
}
/*
{
'100560627': {
ext: '{"faceCount":"92"}',
createDeptGroup: true,
name: 'Product & Dev / 产品技术',
id: 100560627,
autoAddUser: true,
parentid: 111865024,
dn: 'ou=Product & Dev / 产品技术, ou=全员, o=LongBridge, dc=longbridge-inc, dc=com'
},
*/
async function fetchAllDepartments() {
let allDeps = loadCacheFromFile('dingtalk_groups.json');
if (!allDeps) {
const deps = await ddGet('department/list', {
fetch_child: true,
id: 1,
});
if (!deps) {
return [];
}
log.info('Got', deps.department.length, 'departments');
const depsMap = {};
deps.department.forEach(d => {
d.name = d.name.replace(/ \/ /g, ' - ').replace(/\//g, '&').trim();
depsMap[d.id] = d;
});
allDeps = Object.values(depsMap);
saveCacheToFile('dingtalk_groups.json', allDeps);
}
const depsMap = {};
allDeps.forEach(d => { depsMap[d.id] = d; });
allDeps.forEach(d => {
let obj = d;
let dn = [ `ou=${obj.name}` ];
while (obj.parentid !== 1) {
obj = depsMap[obj.parentid];
dn.push(`ou=${obj.name}`);
}
d.dn = dn.join(',');
});
return allDeps;
}
/*
{
unionid: 'DLB1ru0iiZ6rB8z4juaJwWAiEiE',
openId: 'DLB1ru0iiZ6rB8z4juaJwWAiEiE',
remark: '',
userid: '0156594846753096123',
isBoss: false,
hiredDate: 1539532800000,
tel: '',
department: [ 109334341 ],
workPlace: '杭州',
email: '',
order: 177917621779460500,
isLeader: false,
mobile: '18815286506',
active: true,
isAdmin: false,
avatar: 'https://static-legacy.dingtalk.com/media/lADPACOG819UTLzNAu7NAu4_750_750.jpg',
isHide: false,
orgEmail: 'ke.xu@longbridge.sg',
jobnumber: '0049',
name: '徐克',
extattr: {},
stateCode: '86',
position: 'iOS开发工程师'
}
*/
async function fetchDepartmentUsers(department) {
log.info('get users for department', department);
const userlist = [];
let hasMore = true;
let offset = 0;
while (hasMore) {
const users = await ddGet('user/listbypage', {
department_id: department.id,
offset,
size: 100,
order: 'entry_asc',
});
userlist.push(...users.userlist);
hasMore = users.hasMore;
}
userlist.forEach(u => {
u.firstDepartment = department;
});
return userlist;
}
async function fetchAllUsers(departments) {
let allUsers = loadCacheFromFile('dingtalk_users.json');
if (!allUsers && departments.length > 0) {
allUsers = [];
for (let i = 0; i < departments.length; ++i) {
allUsers.push(...(await fetchDepartmentUsers(departments[i])));
}
saveCacheToFile('dingtalk_users.json', allUsers);
}
return allUsers;
}
async function setupProvider(config) {
appKey = config.appKey;
appSecret = config.appSecret;
await reloadFromDingtalkServer();
}
async function reloadFromDingtalkServer() {
await getToken();
// 获取所有部门
let allDepartments = await fetchAllDepartments();
// 映射到 organizationalUnit
const allDepartmentsMap = {};
allLDAPOrgUnits = allDepartments.map(d => {
allDepartmentsMap[d.id] = d;
return makeOrganizationUnitEntry(d.dn, d.name, {
groupid: d.id,
});
});
// 映射到 groupOfNames
const allLDAPGroupsMap = [];
allLDAPGroups = allDepartments.map(d => {
const g = makeGroupEntry(d.dn, d.name, [], {
groupid: d.id,
});
allLDAPGroupsMap[d.id] = g;
return g;
});
Object.values(allDepartmentsMap).forEach(dep => {
if (dep.parentid != 1) {
const parentDep = allDepartmentsMap[dep.parentid];
addMemberToGroup(allLDAPGroupsMap[dep.id], allLDAPGroupsMap[parentDep.id]);
}
})
// 按部门获取所有员工
const allUsers = await fetchAllUsers(allDepartments);
const allUsersMap = {};
allLDAPUsers = allUsers.filter(u => {
if (!allUsersMap[u.userid]) {
allUsersMap[u.userid] = 1;
return u.active;
}
return false;
}).map(u => {
const mail = (u.orgEmail || u.email).toLowerCase();
const dn = `mail=${mail},${u.firstDepartment.dn}`;
const { givenName, sn } = parseName(u.name);
// 映射到 iNetOrgPerson
const personEntry = makePersonEntry(dn, {
uid: u.userid,
title: u.position,
mobileTelephoneNumber: u.mobile,
cn: u.name,
givenName,
sn,
mail,
avatarurl: u.avatar,
});
// 将用户加到组里
u.department.forEach(depId => {
let parentDep = allDepartmentsMap[depId];
// allLDAPGroupsMap[parentDep.id].attributes.member.push(personEntry.dn);
while (parentDep && parentDep.id !== 1) {
addMemberToGroup(personEntry, allLDAPGroupsMap[parentDep.id]);
// console.log('add member', personEntry.attributes.cn, 'to', allLDAPGroupsMap[parentDep.id].attributes.cn);
parentDep = allDepartmentsMap[parentDep.parentid];
}
})
return personEntry;
});
allLDAPEntries = [].concat(allLDAPGroups, allLDAPOrgUnits, allLDAPUsers);
}
function getAllLDAPEntries() {
return allLDAPEntries;
}
function reloadEntriesFromProvider() {
log.info('Reload entries from Dingtalk');
reloadFromDingtalkServer();
}
// if (0) {
// (async function() {
// await setupProvider(require('../config').provider);
// log.info(getAllLDAPEntries());
// })();
// setTimeout(() => {}, 0);
// }
module.exports = {
setupProvider,
getAllLDAPEntries,
reloadEntriesFromProvider,
};

246
lib/server.js Normal file
View File

@ -0,0 +1,246 @@
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,
};

47
lib/utilities/cache.js Normal file
View File

@ -0,0 +1,47 @@
const path = require('path');
const fs = require('fs');
const log = require('log').get('cache');
function getCachePath(name) {
const folder = path.join(__dirname, '..', '..', 'cache');
if (!fs.existsSync(folder)) {
fs.mkdirSync(folder);
}
const p = path.resolve(path.join(folder, name));
return p
}
function saveCacheToFile(name, obj, expires = 3600) {
const p = getCachePath(name);
const data = {
expires: new Date((new Date() * 1 + expires * 1000)).toISOString(),
data: obj,
};
fs.writeFileSync(p, JSON.stringify(data), { encoding: 'utf8' });
}
function loadCacheFromFile(name) {
const p = getCachePath(name);
let data = null;
if (fs.existsSync(p)) {
const content = fs.readFileSync(p, { encoding: 'utf8' });
try {
const obj = JSON.parse(content);
if (obj && obj.data && obj.expires) {
const now = new Date() * 1;
const expires = new Date(obj.expires) * 1;
if (now < expires) {
data = obj.data;
}
}
} catch (e) {
log.warn('Invalid cache for', name);
}
}
return data;
}
module.exports = {
saveCacheToFile,
loadCacheFromFile,
};

View File

@ -0,0 +1,44 @@
const {
parseFilter,
} = require('ldapjs');
const {
getProviderLDAPEntries,
} = require('./provider');
const {
makeGroupEntry,
addMemberToGroup,
} = require('./ldap');
const {
customGroups,
} = require('../config');
function getLDAPCustomGroupEntries() {
if (!customGroups) {
return [];
}
const baseGroupEntry = makeGroupEntry('ou=CustomGroups', 'CustomGroups', []);
const allEntries = getProviderLDAPEntries();
const groupEntries = customGroups.map(g => {
const groupEntry = makeGroupEntry(`ou=${g.name},ou=CustomGroups`, g.name, []);
addMemberToGroup(groupEntry, baseGroupEntry);
const members = allEntries.filter(o => {
return g.members.indexOf(o.attributes.mail) >= 0;
});
members.forEach(p => {
addMemberToGroup(p, groupEntry);
});
return groupEntry;
});
return [].concat(baseGroupEntry, groupEntries);
}
module.exports = {
getLDAPCustomGroupEntries,
};

225
lib/utilities/ldap.js Normal file
View File

@ -0,0 +1,225 @@
const {
ldap: ldapConfig,
} = require('../config');
let cachedBaseEntries = null;
function getRootDN() {
return ldapConfig.rootDN;
}
function makeDN(...parts) {
return parts.join(',');
}
function getOrganizationBaseDN() {
return makeDN('o=' + ldapConfig.organization, getRootDN());
}
function getPeopleBaseDN() {
const dn = makeDN('ou=People', getOrganizationBaseDN());
return dn;
}
function getGroupsBaseDN() {
const dn = makeDN('ou=Groups', getOrganizationBaseDN());
return dn;
}
function makeGroupEntryDN(...prefix) {
const dn = makeDN(...prefix, getGroupsBaseDN());
return dn;
}
function makeOrganizationUnitEntryDN(...prefix) {
const dn = makeDN(...prefix, getPeopleBaseDN());
return dn;
}
function makeOrganizationUnitEntry(dn, name, attrs = {}) {
// dn: d.dn,
// attributes: {
// objectclass: ['organizationalUnit', 'top'],
// ou: d.name,
// }
const generatedDN = makeOrganizationUnitEntryDN(dn);
const entry = {
dn: generatedDN,
attributes: Object.assign({
objectclass: ['organizationalUnit', 'top'],
ou: name,
entryDN: dn,
}, attrs),
};
return entry;
}
function makePersonEntry(dn, attrs) {
// u.ldap = {
// dn: `mail=${mail}, ${depDN}`,
// attributes: {
// objectclass: ['inetOrgPerson', 'organizationalPerson', 'person', 'top'],
// uid: u.userid,
// title: u.position,
// mobileTelephoneNumber: u.mobile,
// cn: u.name,
// givenName: u.name.substr(1),
// sn: u.name.substr(0, 1),
// mail,
// userPassword: '123456',
// }
// };
const generatedDN = makeOrganizationUnitEntryDN(dn);
const entry = {
dn: generatedDN,
attributes: Object.assign({
objectclass: ['inetOrgPerson', 'organizationalPerson', 'person', 'top'],
userPassword: '********',
// otpSecret: 'abcd',
memberOf: [],
entryDN: dn,
}, attrs)
};
return entry;
}
function makeGroupEntry(dn, name, members, attrs = {}) {
// dn: d.dn,
// attributes: {
// objectclass: ['groupOfNames', 'top'],
// ou: d.name,
// }
const generatedDN = makeGroupEntryDN(dn);
const entry = {
dn: generatedDN,
attributes: Object.assign({
objectclass: ['groupOfNames', 'top'],
cn: name,
ou: name,
member: members,
memberOf: [],
entryDN: dn,
}, attrs),
};
return entry;
}
function getOrganizationEntry() {
const entry = {
dn: getOrganizationBaseDN(),
attributes: {
objectclass: ['organization', 'top'],
ou: ldapConfig.organization,
},
};
return entry;
}
function getPeopleBaseEntry() {
const entry = {
dn: getPeopleBaseDN(),
attributes: {
objectclass: ['organizationalUnit', 'top'],
ou: 'People',
},
};
return entry;
}
function getGroupsBaseEntry() {
const entry = {
dn: getGroupsBaseDN(),
attributes: {
objectclass: ['organizationalUnit', 'top'],
ou: 'Groups',
},
};
return entry;
}
function makeAdminEntry(attrs) {
// {dn: 'cn=admin,'+rootDN, attributes: { objectclass: ['simpleSecurityObject', 'organizationalRole'], hasSubordinates: ['FALSE'] } },
const entry = {
dn: [ `cn=${attrs.commonName}`, getRootDN() ].join(','),
attributes: { objectclass: ['simpleSecurityObject', 'organizationalRole'],
hasSubordinates: ['FALSE'] },
};
return entry;
}
function getAdminEntries() {
return ldapConfig.admins.map(cfg => {
return makeAdminEntry(cfg);
});
}
function getBaseEntries() {
if (!cachedBaseEntries) {
const rootDN = getRootDN();
const rootEntry = {
dn: rootDN,
attributes: {
objectclass: ['dcObject', 'organization', 'top'],
dc: rootDN.split(',')[0].split('=')[1].trim(),
o: ldapConfig.organization,
hasSubordinates: ['TRUE'],
}
};
cachedBaseEntries = [
rootEntry,
getOrganizationEntry(),
getPeopleBaseEntry(),
getGroupsBaseEntry(),
...getAdminEntries(),
];
}
return cachedBaseEntries;
}
function validateAdminPassword(username, password) {
const [ user ] = ldapConfig.admins.filter(u => {
return u.commonName == username;
});
if (user && user.password === password) {
return true;
}
return false;
}
function validateAdminPermission(username, permission) {
const [ user ] = ldapConfig.admins.filter(u => {
return u.commonName == username;
});
if (user && user[permission]) {
return true;
}
return false;
}
function addMemberToGroup(memberEntry, groupEntry) {
if (groupEntry.attributes.member.indexOf(memberEntry.dn) < 0) {
groupEntry.attributes.member.push(memberEntry.dn);
}
if (memberEntry.attributes.memberOf.indexOf(groupEntry.dn) < 0) {
memberEntry.attributes.memberOf.push(groupEntry.dn);
}
}
module.exports = {
getRootDN,
getOrganizationBaseDN,
getAdminEntries,
getBaseEntries,
makeGroupEntry,
makeOrganizationUnitEntry,
makePersonEntry,
validateAdminPassword,
validateAdminPermission,
addMemberToGroup,
};

45
lib/utilities/password.js Normal file
View File

@ -0,0 +1,45 @@
const crypto = require('crypto');
function validateUserPassword(secret, input) {
if (secret.substr(0, 9) === '{SSHA256}') {
secret = secret.substr(9);
const secbin = Buffer.from(secret, 'base64');
const userpw = secbin.subarray(0, secbin.length - 8).toString('hex');
const salt = secbin.subarray(secbin.length - 8);
const hash = crypto.createHash('sha256');
hash.update(input);
hash.update(salt);
const inputpw = hash.digest('hex');
// console.log('userpw', userpw);
// console.log('inputpw', inputpw);
return inputpw === userpw;
} else {
return secret === input;
}
}
function storeUserPassword(input) {
const hash = crypto.createHash('sha256');
hash.update(input);
const salt = Buffer.alloc(8);
for (let i = 0; i < 8; ++i) {
const b = Math.floor(Math.random() * 256);
// console.log('salt byte', i, b);
salt.writeUInt8(b, i);
}
// console.log('salt', salt.toString('hex'));
hash.update(salt);
const digest = hash.digest();
const hashbin = Buffer.concat([ digest, salt ]);
// console.log('pwhash', digest.toString('hex'));
digest.write(salt.toString('hex'), 'hex');
// console.log('pwhash /w salt', hashbin.toString('hex'));
const pw = '{SSHA256}' + hashbin.toString('base64');
return pw;
}
module.exports = {
validateUserPassword,
storeUserPassword,
};

39
lib/utilities/provider.js Normal file
View File

@ -0,0 +1,39 @@
const {
provider: providerConfig,
} = require('../config');
const log = require('log').get('provider');
let provider = null;
async function createProvider() {
if (!provider) {
const name = providerConfig.name;
log.info('Setting up provider', name);
provider = require('../providers/' + name);
await provider.setupProvider(providerConfig);
log.info('Done');
}
return provider;
};
function getProviderLDAPEntries() {
if (provider) {
return provider.getAllLDAPEntries();
}
return [];
}
function reloadEntriesFromProvider() {
if (provider) {
log.info('Reload entries from provider');
provider.reloadEntriesFromProvider();
}
}
module.exports = {
createProvider,
getProviderLDAPEntries,
reloadEntriesFromProvider,
};

741
package-lock.json generated Normal file
View File

@ -0,0 +1,741 @@
{
"name": "virtual-ldap",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@otplib/core": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz",
"integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA=="
},
"@otplib/plugin-crypto": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz",
"integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==",
"requires": {
"@otplib/core": "^12.0.1"
}
},
"@otplib/plugin-thirty-two": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz",
"integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==",
"requires": {
"@otplib/core": "^12.0.1",
"thirty-two": "^1.0.2"
}
},
"@otplib/preset-default": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz",
"integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==",
"requires": {
"@otplib/core": "^12.0.1",
"@otplib/plugin-crypto": "^12.0.1",
"@otplib/plugin-thirty-two": "^12.0.1"
}
},
"@otplib/preset-v11": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz",
"integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==",
"requires": {
"@otplib/core": "^12.0.1",
"@otplib/plugin-crypto": "^12.0.1",
"@otplib/plugin-thirty-two": "^12.0.1"
}
},
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"ansicolors": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz",
"integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk="
},
"axios": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"requires": {
"follow-redirects": "1.5.10"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"cardinal": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz",
"integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=",
"requires": {
"ansicolors": "~0.3.2",
"redeyed": "~2.1.0"
}
},
"cli-color": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.4.0.tgz",
"integrity": "sha512-xu6RvQqqrWEo6MPR1eixqGPywhYBHRs653F9jfXB2Hx4jdM/3WxiNE1vppRmxtMIfl16SFYTpYlrnqH/HsK/2w==",
"requires": {
"ansi-regex": "^2.1.1",
"d": "1",
"es5-ext": "^0.10.46",
"es6-iterator": "^2.0.3",
"memoizee": "^0.4.14",
"timers-ext": "^0.1.5"
}
},
"cli-sprintf-format": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cli-sprintf-format/-/cli-sprintf-format-1.1.0.tgz",
"integrity": "sha512-t3LcCdPvrypZovStadWdRS4a186gsq9aoHJYTIer55VY20YdVjGVHDV4uPWcWCXTw1tPjfwlRGE7zKMWJ663Sw==",
"requires": {
"cli-color": "^1.3",
"es5-ext": "^0.10.46",
"sprintf-kit": "2",
"supports-color": "^5.5"
},
"dependencies": {
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"optional": true
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cron": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/cron/-/cron-1.8.2.tgz",
"integrity": "sha512-Gk2c4y6xKEO8FSAUTklqtfSr7oTq0CiPQeLBG5Fl0qoXpZyMcj1SG59YL+hqq04bu6/IuEA7lMkYDAplQNKkyg==",
"requires": {
"moment-timezone": "^0.5.x"
}
},
"d": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
"integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
"requires": {
"es5-ext": "^0.10.50",
"type": "^1.0.1"
}
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"denque": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
"integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ=="
},
"duration": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/duration/-/duration-0.2.2.tgz",
"integrity": "sha512-06kgtea+bGreF5eKYgI/36A6pLXggY7oR4p1pq4SmdFBn1ReOL5D8RhG64VrqfTTKNucqqtBAwEj8aB88mcqrg==",
"requires": {
"d": "1",
"es5-ext": "~0.10.46"
}
},
"es5-ext": {
"version": "0.10.53",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
"integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
"requires": {
"es6-iterator": "~2.0.3",
"es6-symbol": "~3.1.3",
"next-tick": "~1.0.0"
}
},
"es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
"requires": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"es6-symbol": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
"integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
"requires": {
"d": "^1.0.1",
"ext": "^1.1.2"
}
},
"es6-weak-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz",
"integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==",
"requires": {
"d": "1",
"es5-ext": "^0.10.46",
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.1"
}
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
"requires": {
"d": "1",
"es5-ext": "~0.10.14"
}
},
"ext": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
"integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==",
"requires": {
"type": "^2.0.0"
},
"dependencies": {
"type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz",
"integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow=="
}
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
}
},
"generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"requires": {
"is-property": "^1.0.2"
}
},
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"optional": true,
"requires": {
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "2 || 3",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"has-ansi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-3.0.0.tgz",
"integrity": "sha1-Ngd+8dFfMzSEqn+neihgbxxlWzc=",
"requires": {
"ansi-regex": "^3.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
}
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"iconv-lite": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.1.tgz",
"integrity": "sha512-ONHr16SQvKZNSqjQT9gy5z24Jw+uqfO02/ngBSBoqChZ+W8qXX7GPRa1RoUnzGADw8K63R1BXUMzarCVQBpY8Q==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"optional": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"optional": true
},
"is-promise": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
"integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o="
},
"is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ="
},
"ldapjs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.2.tgz",
"integrity": "sha1-VE/3Ayt7g8aPBwEyjZKXqmlDQPk=",
"requires": {
"asn1": "0.2.3",
"assert-plus": "^1.0.0",
"backoff": "^2.5.0",
"bunyan": "^1.8.3",
"dashdash": "^1.14.0",
"dtrace-provider": "~0.8",
"ldap-filter": "0.2.2",
"once": "^1.4.0",
"vasync": "^1.6.4",
"verror": "^1.8.1"
},
"dependencies": {
"asn1": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"backoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
"integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=",
"requires": {
"precond": "0.2"
}
},
"bunyan": {
"version": "1.8.12",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
"integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
"requires": {
"dtrace-provider": "~0.8",
"moment": "^2.10.6",
"mv": "~2",
"safe-json-stringify": "~1"
}
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"requires": {
"assert-plus": "^1.0.0"
}
},
"dtrace-provider": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
"integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
"optional": true,
"requires": {
"nan": "^2.14.0"
}
},
"extsprintf": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz",
"integrity": "sha1-WtlGwi9bMrp/jNdCZxHG6KP8JSk="
},
"ldap-filter": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
"integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
"requires": {
"assert-plus": "0.1.5"
},
"dependencies": {
"assert-plus": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
}
}
},
"nan": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
"optional": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"vasync": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
"integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
"requires": {
"verror": "1.6.0"
},
"dependencies": {
"verror": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
"integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
"requires": {
"extsprintf": "1.2.0"
}
}
}
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"requires": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
}
}
}
},
"log": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/log/-/log-6.0.0.tgz",
"integrity": "sha512-sxChESNYJ/EcQv8C7xpmxhtTOngoXuMEqGDAkhXBEmt3MAzM3SM/TmIBOqnMEVdrOv1+VgZoYbo6U2GemQiU4g==",
"requires": {
"d": "^1.0.0",
"duration": "^0.2.2",
"es5-ext": "^0.10.49",
"event-emitter": "^0.3.5",
"sprintf-kit": "^2.0.0",
"type": "^1.0.1"
}
},
"log-node": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/log-node/-/log-node-7.0.0.tgz",
"integrity": "sha512-/P5eDVV2AXVmXq3TTKOsAyb3Xcb/iVBuxmWW8HisgmiYKuah05/je7jbbSfVdT9UPxGLANbGsbthDTVHMi/3Eg==",
"requires": {
"cli-color": "^1.4.0",
"cli-sprintf-format": "^1.1.0",
"d": "^1.0.0",
"es5-ext": "^0.10.49",
"has-ansi": "^3.0.0",
"sprintf-kit": "^2.0.0",
"supports-color": "^6.1.0"
}
},
"long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"requires": {
"yallist": "^3.0.2"
}
},
"lru-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
"integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=",
"requires": {
"es5-ext": "~0.10.2"
}
},
"memoizee": {
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz",
"integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==",
"requires": {
"d": "1",
"es5-ext": "^0.10.45",
"es6-weak-map": "^2.0.2",
"event-emitter": "^0.3.5",
"is-promise": "^2.1",
"lru-queue": "0.1",
"next-tick": "1",
"timers-ext": "^0.1.5"
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"optional": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"optional": true,
"requires": {
"minimist": "0.0.8"
}
},
"moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"moment-timezone": {
"version": "0.5.27",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.27.tgz",
"integrity": "sha512-EIKQs7h5sAsjhPCqN6ggx6cEbs94GK050254TIJySD1bzoM5JTYDwAU1IoVOeTOL6Gm27kYJ51/uuvq1kIlrbw==",
"requires": {
"moment": ">= 2.9.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"mv": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
"optional": true,
"requires": {
"mkdirp": "~0.5.1",
"ncp": "~2.0.0",
"rimraf": "~2.4.0"
}
},
"mysql2": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.1.0.tgz",
"integrity": "sha512-9kGVyi930rG2KaHrz3sHwtc6K+GY9d8wWk1XRSYxQiunvGcn4DwuZxOwmK11ftuhhwrYDwGx9Ta4VBwznJn36A==",
"requires": {
"cardinal": "^2.1.1",
"denque": "^1.4.1",
"generate-function": "^2.3.1",
"iconv-lite": "^0.5.0",
"long": "^4.0.0",
"lru-cache": "^5.1.1",
"named-placeholders": "^1.1.2",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.1"
}
},
"named-placeholders": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz",
"integrity": "sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA==",
"requires": {
"lru-cache": "^4.1.3"
},
"dependencies": {
"lru-cache": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
"requires": {
"pseudomap": "^1.0.2",
"yallist": "^2.1.2"
}
},
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
}
}
},
"ncp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
"optional": true
},
"next-tick": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
"once": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/once/-/once-1.3.1.tgz",
"integrity": "sha1-8/Pk2lt9J7XHMpae4+Z+cpRXsx8=",
"optional": true,
"requires": {
"wrappy": "1"
}
},
"otplib": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz",
"integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==",
"requires": {
"@otplib/core": "^12.0.1",
"@otplib/preset-default": "^12.0.1",
"@otplib/preset-v11": "^12.0.1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"optional": true
},
"precond": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
},
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
},
"redeyed": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz",
"integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=",
"requires": {
"esprima": "~4.0.0"
}
},
"rimraf": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
"optional": true,
"requires": {
"glob": "^6.0.1"
}
},
"safe-json-stringify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
"integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"seq-queue": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
"integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4="
},
"sprintf-kit": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/sprintf-kit/-/sprintf-kit-2.0.0.tgz",
"integrity": "sha512-/0d2YTn8ZFVpIPAU230S9ZLF8WDkSSRWvh/UOLM7zzvkCchum1TtouRgyV8OfgOaYilSGU4lSSqzwBXJVlAwUw==",
"requires": {
"es5-ext": "^0.10.46"
}
},
"sqlstring": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
"integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A="
},
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
"integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
"requires": {
"has-flag": "^3.0.0"
}
},
"thirty-two": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz",
"integrity": "sha1-TKL//AKlEpDSdEueP1V2k8prYno="
},
"timers-ext": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz",
"integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==",
"requires": {
"es5-ext": "~0.10.46",
"next-tick": "1"
}
},
"type": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}
}
}

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "virtual-ldap",
"version": "0.1.0",
"author": {
"name": "ohdarling88",
"url": "https://xujiwei.com"
},
"main": "index.js",
"files": [
"index.js",
"lib/**/*.js",
"config.example.js"
],
"keywords": [
"ldap",
"dingtalk",
"keycloak",
"钉钉",
"enterprise"
],
"repository": {
"type": "git",
"url": "https://github.com/ohdarling/virtual-ldap"
},
"license": "MIT",
"scripts": {
"start": "LOG_TIME=abs LOG_LEVEL=info node index.js"
},
"dependencies": {
"axios": "^0.19.2",
"cron": "^1.8.2",
"ldapjs": "^1.0.2",
"log": "^6.0.0",
"log-node": "^7.0.0",
"mysql2": "^2.1.0",
"otplib": "^12.0.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

BIN
screenshots/2-base-auth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB