Initial commit
This commit is contained in:
commit
aa5632c8a0
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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' ],
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
const server = require("./lib");
|
||||
|
||||
if (require.main === module) {
|
||||
server.setupVirtualLDAPServer(require("./config.example"));
|
||||
server.runVirtualLDAPServer();
|
||||
} else {
|
||||
module.exports = server;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
module.exports = require("./index").serverConfig;
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 |
Binary file not shown.
After Width: | Height: | Size: 338 KiB |
Binary file not shown.
After Width: | Height: | Size: 410 KiB |
Loading…
Reference in New Issue