commit aa5632c8a07e91fc1f1b8a3259bd6dfb00e7ff38 Author: ohdarling88 Date: Mon Feb 3 14:10:29 2020 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efca3b5 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f99e3b3 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..34adf42 --- /dev/null +++ b/README.md @@ -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 + +create connectionauth + +ldap browse + + +## License + +MIT License diff --git a/config.example.js b/config.example.js new file mode 100644 index 0000000..656c18e --- /dev/null +++ b/config.example.js @@ -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' ], + } + ] +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..8c74a43 --- /dev/null +++ b/index.js @@ -0,0 +1,8 @@ +const server = require("./lib"); + +if (require.main === module) { + server.setupVirtualLDAPServer(require("./config.example")); + server.runVirtualLDAPServer(); +} else { + module.exports = server; +} diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..645a54c --- /dev/null +++ b/lib/config.js @@ -0,0 +1 @@ +module.exports = require("./index").serverConfig; \ No newline at end of file diff --git a/lib/db/db.js b/lib/db/db.js new file mode 100644 index 0000000..084289f --- /dev/null +++ b/lib/db/db.js @@ -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, +}; diff --git a/lib/db/dbinit.sql b/lib/db/dbinit.sql new file mode 100644 index 0000000..9a9763d --- /dev/null +++ b/lib/db/dbinit.sql @@ -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; diff --git a/lib/db/memory.js b/lib/db/memory.js new file mode 100644 index 0000000..5c734ad --- /dev/null +++ b/lib/db/memory.js @@ -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, +}; diff --git a/lib/db/mysql.js b/lib/db/mysql.js new file mode 100644 index 0000000..1f01470 --- /dev/null +++ b/lib/db/mysql.js @@ -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, +}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..2589a4e --- /dev/null +++ b/lib/index.js @@ -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, +}; diff --git a/lib/providers/dingtalk.js b/lib/providers/dingtalk.js new file mode 100644 index 0000000..70f3341 --- /dev/null +++ b/lib/providers/dingtalk.js @@ -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, +}; diff --git a/lib/server.js b/lib/server.js new file mode 100644 index 0000000..c0a1596 --- /dev/null +++ b/lib/server.js @@ -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, +}; diff --git a/lib/utilities/cache.js b/lib/utilities/cache.js new file mode 100644 index 0000000..adf2576 --- /dev/null +++ b/lib/utilities/cache.js @@ -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, +}; diff --git a/lib/utilities/custom_groups.js b/lib/utilities/custom_groups.js new file mode 100644 index 0000000..d387570 --- /dev/null +++ b/lib/utilities/custom_groups.js @@ -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, +}; diff --git a/lib/utilities/ldap.js b/lib/utilities/ldap.js new file mode 100644 index 0000000..ea6d84d --- /dev/null +++ b/lib/utilities/ldap.js @@ -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, +}; diff --git a/lib/utilities/password.js b/lib/utilities/password.js new file mode 100644 index 0000000..e88f90e --- /dev/null +++ b/lib/utilities/password.js @@ -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, +}; diff --git a/lib/utilities/provider.js b/lib/utilities/provider.js new file mode 100644 index 0000000..2750148 --- /dev/null +++ b/lib/utilities/provider.js @@ -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, +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..21a4e0e --- /dev/null +++ b/package-lock.json @@ -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==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9ab37bd --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/screenshots/1-create-connection.png b/screenshots/1-create-connection.png new file mode 100644 index 0000000..1e180cc Binary files /dev/null and b/screenshots/1-create-connection.png differ diff --git a/screenshots/2-base-auth.png b/screenshots/2-base-auth.png new file mode 100644 index 0000000..1cca922 Binary files /dev/null and b/screenshots/2-base-auth.png differ diff --git a/screenshots/3-ldap-browse.png b/screenshots/3-ldap-browse.png new file mode 100644 index 0000000..dde11ee Binary files /dev/null and b/screenshots/3-ldap-browse.png differ