update
This commit is contained in:
parent
0620bb1d26
commit
be6654cdc4
|
@ -0,0 +1,5 @@
|
|||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWD=123456
|
||||
DB_NAME=casdoor
|
|
@ -0,0 +1,25 @@
|
|||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir : __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
|
@ -1,62 +1,36 @@
|
|||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.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
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.env
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
139
README.md
139
README.md
|
@ -1,139 +0,0 @@
|
|||
# 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
|
|
@ -1,50 +0,0 @@
|
|||
module.exports = {
|
||||
ldap: {
|
||||
// LDAP serve port, it is a insecure port, please connect with ldap://
|
||||
listenPort: 1389,
|
||||
// Base DN will be o=Example,dc=example,dc=com
|
||||
// Groups base DN will be ou=Groups,o=Example,dc=example,dc=com
|
||||
// Users base DN will be ou=People,o=Example,dc=example,dc=com
|
||||
rootDN: 'dc=example,dc=com',
|
||||
organization: 'Example',
|
||||
// Admins who can search or modify directory
|
||||
admins: [
|
||||
{
|
||||
// Bind DN will be cn=keycloak,dc=example,dc=com
|
||||
commonName: 'keycloak',
|
||||
password: 'keycloak',
|
||||
// Can this admin modify user's password
|
||||
canModifyEntry: true,
|
||||
},
|
||||
{
|
||||
commonName: 'jenkins',
|
||||
password: 'jenkins',
|
||||
canModifyEntry: false,
|
||||
},
|
||||
]
|
||||
},
|
||||
// Database for storing users' password
|
||||
database: {
|
||||
type: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
port: '23306',
|
||||
user: 'root',
|
||||
password: '123456',
|
||||
database: 'vldap',
|
||||
},
|
||||
// Provider for providen account service
|
||||
provider: {
|
||||
name: 'dingtalk',
|
||||
appKey: '__APPKEY__',
|
||||
appSecret: '__APPSECRET__',
|
||||
},
|
||||
// Custom groups, base DN will be ou=CustomGroups,ou=Groups,o=Example,dc=example,dc=com
|
||||
customGroups: [
|
||||
{
|
||||
// DN will be ou=Jenkins Admins,ou=CustomGroups,ou=Groups,o=Example,dc=example,dc=com
|
||||
name: 'Jenkins Admins',
|
||||
// User with these mails will be added to the group
|
||||
members: [ 'jenkins@example.com' ],
|
||||
}
|
||||
]
|
||||
}
|
8
index.js
8
index.js
|
@ -1,8 +0,0 @@
|
|||
const server = require("./lib");
|
||||
|
||||
if (require.main === module) {
|
||||
server.setupVirtualLDAPServer(require("./config.example"));
|
||||
server.runVirtualLDAPServer();
|
||||
} else {
|
||||
module.exports = server;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
module.exports = require("./index").serverConfig;
|
38
lib/db/db.js
38
lib/db/db.js
|
@ -1,38 +0,0 @@
|
|||
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,
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
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;
|
|
@ -1,19 +0,0 @@
|
|||
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,
|
||||
};
|
|
@ -1,72 +0,0 @@
|
|||
const mysql = require('mysql2');
|
||||
|
||||
const {
|
||||
database: dbConfig,
|
||||
} = require('../config');
|
||||
|
||||
let dbConnected = false;
|
||||
let dbPool = null;
|
||||
|
||||
async function connect() {
|
||||
if (!dbConnected) {
|
||||
const opts = Object.assign({}, dbConfig);
|
||||
delete opts.type;
|
||||
dbPool = mysql.createPool(opts).promise();
|
||||
dbConnected = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function dbQuery(sql, params) {
|
||||
connect();
|
||||
const [ rows, fields ] = await dbPool.query(sql, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function dbSelect(table, params) {
|
||||
const fields = Object.keys(params);
|
||||
const values = fields.map(k => params[k]);
|
||||
const sql = `SELECT * FROM ${table} WHERE ${fields.map(k => `${k}=?`).join(' AND ')}`;
|
||||
return dbQuery(sql, values);
|
||||
}
|
||||
|
||||
async function dbUpdate(table, params, conditions) {
|
||||
const fields = Object.keys(params);
|
||||
const values = fields.map(k => params[k]);
|
||||
let conditionSQL = '';
|
||||
if (conditions) {
|
||||
const cfields = Object.keys(conditions);
|
||||
const cvalues = cfields.map(k => conditions[k]);
|
||||
conditionSQL = ` WHERE ${cfields.map(k => `${k}=?`).join(' AND ')}`;
|
||||
values.push(...cvalues);
|
||||
}
|
||||
const sql = `UPDATE ${table} SET ${fields.map(k => `${k}=?`).join(', ')} ${conditionSQL}`;
|
||||
return dbQuery(sql, values);
|
||||
}
|
||||
|
||||
async function dbInsert(table, params) {
|
||||
const fields = Object.keys(params);
|
||||
const values = fields.map(k => params[k]);
|
||||
const sql = `INSERT INTO ${table} (${fields.join(', ')}) VALUES (${fields.map(k => '?').join(', ')})`;
|
||||
return dbQuery(sql, values);
|
||||
}
|
||||
|
||||
async function dbDelete(table, conditions) {
|
||||
const values = [];
|
||||
let conditionSQL = '';
|
||||
if (conditions) {
|
||||
const cfields = Object.keys(conditions);
|
||||
const cvalues = fields.map(k => conditions[k]);
|
||||
conditionSQL = ` WHERE ${cfields.map(k => `${k}=?`).join(' AND ')}`;
|
||||
values.push(...cvalues);
|
||||
}
|
||||
const sql = `DELETE FROM ${table} ${conditionSQL}`
|
||||
return dbQuery(sql, values);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dbQuery,
|
||||
dbSelect,
|
||||
dbUpdate,
|
||||
dbInsert,
|
||||
dbDelete,
|
||||
};
|
17
lib/index.js
17
lib/index.js
|
@ -1,17 +0,0 @@
|
|||
require('log-node')();
|
||||
|
||||
const serverConfig = {};
|
||||
|
||||
function setupVirtualLDAPServer(config) {
|
||||
Object.assign(serverConfig, config);
|
||||
}
|
||||
|
||||
function runVirtualLDAPServer() {
|
||||
require("./server").runVirtualLDAPServer();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
serverConfig,
|
||||
setupVirtualLDAPServer,
|
||||
runVirtualLDAPServer,
|
||||
};
|
|
@ -1,315 +0,0 @@
|
|||
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 = {
|
||||
'1': {
|
||||
name: 'Staff',
|
||||
id: 1,
|
||||
parentid: null,
|
||||
},
|
||||
};
|
||||
deps.department.forEach(d => {
|
||||
d.name = d.name.replace(/ \/ /g, ' - ').replace(/\//g, '&').trim();
|
||||
depsMap[d.id] = d;
|
||||
});
|
||||
|
||||
allDeps = Object.values(depsMap);
|
||||
const allDepNames = {};
|
||||
allDeps.forEach(v => {
|
||||
let name = v.name;
|
||||
let idx = 2;
|
||||
while (allDepNames[name]) {
|
||||
name = v.name + idx;
|
||||
idx++;
|
||||
}
|
||||
allDepNames[name] = 1;
|
||||
v.name = name;
|
||||
})
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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;
|
||||
}).filter(u => {
|
||||
if (!(u.orgEmail || u.email)) {
|
||||
log.warn('Incorrect user missing email', u);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).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) {
|
||||
addMemberToGroup(personEntry, allLDAPGroupsMap[parentDep.id]);
|
||||
// console.log('add member', personEntry.attributes.cn, 'to', allLDAPGroupsMap[parentDep.id].attributes.cn);
|
||||
parentDep = allDepartmentsMap[parentDep.parentid];
|
||||
}
|
||||
})
|
||||
|
||||
return personEntry;
|
||||
});
|
||||
|
||||
allLDAPEntries = [].concat(allLDAPGroups, allLDAPOrgUnits, allLDAPUsers);
|
||||
}
|
||||
|
||||
function getAllLDAPEntries() {
|
||||
return allLDAPEntries;
|
||||
}
|
||||
|
||||
function reloadEntriesFromProvider() {
|
||||
log.info('Reload entries from Dingtalk');
|
||||
reloadFromDingtalkServer();
|
||||
}
|
||||
|
||||
|
||||
// if (0) {
|
||||
// (async function() {
|
||||
// await setupProvider(require('../config').provider);
|
||||
// log.info(getAllLDAPEntries());
|
||||
// })();
|
||||
// setTimeout(() => {}, 0);
|
||||
// }
|
||||
|
||||
module.exports = {
|
||||
setupProvider,
|
||||
getAllLDAPEntries,
|
||||
reloadEntriesFromProvider,
|
||||
};
|
246
lib/server.js
246
lib/server.js
|
@ -1,246 +0,0 @@
|
|||
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 (record && validateUserPassword(record.password, 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,
|
||||
};
|
|
@ -1,47 +0,0 @@
|
|||
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,
|
||||
};
|
|
@ -1,44 +0,0 @@
|
|||
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,
|
||||
};
|
|
@ -1,225 +0,0 @@
|
|||
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,
|
||||
};
|
|
@ -1,45 +0,0 @@
|
|||
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,
|
||||
};
|
|
@ -1,39 +0,0 @@
|
|||
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,5 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src"
|
||||
}
|
File diff suppressed because it is too large
Load Diff
106
package.json
106
package.json
|
@ -1,39 +1,79 @@
|
|||
{
|
||||
"name": "virtual-ldap",
|
||||
"version": "0.2.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",
|
||||
"name": "server",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"start": "LOG_TIME=abs LOG_LEVEL=info node index.js"
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \\\"src/**/*.ts\\\" \\\"../test/**/*.ts\\\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ../test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.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",
|
||||
"minimist": ">=0.2.1"
|
||||
"@nestjs/common": "^9.0.0",
|
||||
"@nestjs/config": "^2.2.0",
|
||||
"@nestjs/core": "^9.0.0",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/platform-express": "^9.0.0",
|
||||
"@nestjs/typeorm": "^9.0.1",
|
||||
"@types/ldapjs": "^2.2.4",
|
||||
"ldapjs": "^2.3.3",
|
||||
"mysql2": "^2.3.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.2.0",
|
||||
"typeorm": "^0.3.10",
|
||||
"typeorm-naming-strategies": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.0",
|
||||
"@nestjs/testing": "^9.0.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jest": "28.1.8",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "28.1.3",
|
||||
"prettier": "^2.3.2",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "28.0.8",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "4.1.0",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
exports.__esModule = true;
|
||||
exports.AppController = void 0;
|
||||
var common_1 = require("@nestjs/common");
|
||||
var AppController = /** @class */ (function () {
|
||||
function AppController(appService) {
|
||||
this.appService = appService;
|
||||
}
|
||||
AppController.prototype.getHello = function () {
|
||||
return this.appService.getHello();
|
||||
};
|
||||
__decorate([
|
||||
(0, common_1.Get)()
|
||||
], AppController.prototype, "getHello");
|
||||
AppController = __decorate([
|
||||
(0, common_1.Controller)()
|
||||
], AppController);
|
||||
return AppController;
|
||||
}());
|
||||
exports.AppController = AppController;
|
|
@ -0,0 +1,64 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
exports.__esModule = true;
|
||||
var testing_1 = require("@nestjs/testing");
|
||||
var app_controller_1 = require("./app.controller");
|
||||
var app_service_1 = require("./app.service");
|
||||
describe('AppController', function () {
|
||||
var appController;
|
||||
beforeEach(function () { return __awaiter(void 0, void 0, void 0, function () {
|
||||
var app;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, testing_1.Test.createTestingModule({
|
||||
controllers: [app_controller_1.AppController],
|
||||
providers: [app_service_1.AppService]
|
||||
}).compile()];
|
||||
case 1:
|
||||
app = _a.sent();
|
||||
appController = app.get(app_controller_1.AppController);
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
}); });
|
||||
describe('root', function () {
|
||||
it('should return "Hello World!"', function () {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
exports.__esModule = true;
|
||||
exports.AppModule = void 0;
|
||||
var common_1 = require("@nestjs/common");
|
||||
var app_controller_1 = require("./app.controller");
|
||||
var app_service_1 = require("./app.service");
|
||||
var AppModule = /** @class */ (function () {
|
||||
function AppModule() {
|
||||
}
|
||||
AppModule = __decorate([
|
||||
(0, common_1.Module)({
|
||||
imports: [],
|
||||
controllers: [app_controller_1.AppController],
|
||||
providers: [app_service_1.AppService]
|
||||
})
|
||||
], AppModule);
|
||||
return AppModule;
|
||||
}());
|
||||
exports.AppModule = AppModule;
|
|
@ -0,0 +1,22 @@
|
|||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
exports.__esModule = true;
|
||||
exports.AppService = void 0;
|
||||
var common_1 = require("@nestjs/common");
|
||||
var AppService = /** @class */ (function () {
|
||||
function AppService() {
|
||||
}
|
||||
AppService.prototype.getHello = function () {
|
||||
return 'Hello World!';
|
||||
};
|
||||
AppService = __decorate([
|
||||
(0, common_1.Injectable)()
|
||||
], AppService);
|
||||
return AppService;
|
||||
}());
|
||||
exports.AppService = AppService;
|
|
@ -0,0 +1,57 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
exports.__esModule = true;
|
||||
var core_1 = require("@nestjs/core");
|
||||
var app_module_1 = require("./app.module");
|
||||
function bootstrap() {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var app;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, core_1.NestFactory.create(app_module_1.AppModule)];
|
||||
case 1:
|
||||
app = _a.sent();
|
||||
return [4 /*yield*/, app.listen(3000)];
|
||||
case 2:
|
||||
_a.sent();
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
bootstrap();
|
|
@ -0,0 +1,67 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
exports.__esModule = true;
|
||||
var testing_1 = require("@nestjs/testing");
|
||||
var request = require("supertest");
|
||||
var app_module_1 = require("./../src/app.module");
|
||||
describe('AppController (e2e)', function () {
|
||||
var app;
|
||||
beforeEach(function () { return __awaiter(void 0, void 0, void 0, function () {
|
||||
var moduleFixture;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, testing_1.Test.createTestingModule({
|
||||
imports: [app_module_1.AppModule]
|
||||
}).compile()];
|
||||
case 1:
|
||||
moduleFixture = _a.sent();
|
||||
app = moduleFixture.createNestApplication();
|
||||
return [4 /*yield*/, app.init()];
|
||||
case 2:
|
||||
_a.sent();
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
}); });
|
||||
it('/ (GET)', function () {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
import {Module} from '@nestjs/common';
|
||||
import {LdapModule} from './ldap/ldap.module';
|
||||
import {UserModule} from './user/user.module';
|
||||
import {TypeOrmModule} from "@nestjs/typeorm";
|
||||
import {ConfigModule, ConfigService} from "@nestjs/config";
|
||||
import {SnakeNamingStrategy} from 'typeorm-naming-strategies';
|
||||
|
||||
|
||||
const DataModule = TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
type: 'mysql',
|
||||
host: configService.get('DB_HOST'),
|
||||
port: +configService.get('DB_PORT'),
|
||||
username: configService.get('DB_USER'),
|
||||
password: configService.get('DB_PASSWD'),
|
||||
database: configService.get('DB_NAME'),
|
||||
entities: [],
|
||||
synchronize: false,
|
||||
autoLoadEntities: true,
|
||||
namingStrategy: new SnakeNamingStrategy(),
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
});
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule.forRoot(), DataModule, LdapModule, UserModule],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
export const ldapConfig = {
|
||||
// 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: [
|
||||
{
|
||||
commonName: 'admin',
|
||||
password: '123456',
|
||||
canModifyEntry: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import {createServer, InsufficientAccessRightsError, parseDN, parseFilter, SearchRequest} from "ldapjs";
|
||||
import {getRootDN} from "../utils/ldap";
|
||||
import {LdapService} from "./ldap.service";
|
||||
|
||||
export 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);
|
||||
}
|
||||
|
||||
export function getPersonMatchedDN(reqDN) {
|
||||
const personFilter = parseFilter('(objectclass=inetOrgPerson)');
|
||||
return {};
|
||||
}
|
||||
|
||||
function authorize(req, res, next) {
|
||||
console.debug('===> authorize info');
|
||||
console.debug('req type', req.constructor.name);
|
||||
console.debug('reqDN', (req.baseObject || '').toString());
|
||||
console.debug('bindDN', req.connection.ldap.bindDN.toString());
|
||||
console.debug('====');
|
||||
const bindDN = req.connection.ldap.bindDN;
|
||||
const rootDN = parseDN(getRootDN());
|
||||
|
||||
// person can search itself
|
||||
if (req instanceof SearchRequest) {
|
||||
if (equalsDN(bindDN, req.baseObject)) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
// root admin can do everything
|
||||
if (equalsDN(bindDN.parent(), rootDN)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return next(new InsufficientAccessRightsError());
|
||||
}
|
||||
|
||||
export const ldapServer = createServer();
|
||||
const registerMap = [];
|
||||
|
||||
export function ldapRegister(t: LdapService) {
|
||||
for (const item of registerMap) {
|
||||
if (item.type === 'search') {
|
||||
ldapServer.search(item.hook, authorize, function (req, res, next) {
|
||||
item.descriptor.value.apply(t, [req, res, next]);
|
||||
})
|
||||
}
|
||||
if (item.type === 'bind') {
|
||||
ldapServer.bind(item.hook, function (req, res, next) {
|
||||
item.descriptor.value.apply(t, [req, res, next]);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export function Search(ditHook: string) {
|
||||
console.log("search hook " + ditHook);
|
||||
return function (target: LdapService, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
registerMap.push({
|
||||
type: 'search', hook: ditHook, descriptor: descriptor
|
||||
});
|
||||
return descriptor;
|
||||
// ldapServer.search(ditHook, authorize, async (req, res, next) => {
|
||||
// await descriptor.value(req, res, next);
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
export function Bind(ditHook: string) {
|
||||
return function (target: LdapService, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
registerMap.push({
|
||||
type: 'bind', hook: ditHook, descriptor: descriptor
|
||||
});
|
||||
return descriptor
|
||||
// ldapServer.bind(ditHook, async (req, res, next) => {
|
||||
// await descriptor.value(req, res, next);
|
||||
// });
|
||||
};
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import {Module} from '@nestjs/common';
|
||||
import {LdapService} from "./ldap.service";
|
||||
import {UserModule} from "../user/user.module";
|
||||
|
||||
@Module({
|
||||
imports: [UserModule],
|
||||
providers: [LdapService]
|
||||
})
|
||||
export class LdapModule {
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
import {Injectable} from "@nestjs/common";
|
||||
import ldap, {Server, createServer, parseDN, InsufficientAccessRightsError} from "ldapjs";
|
||||
import {Bind, equalsDN, getPersonMatchedDN, ldapRegister, ldapServer, Search} from "./function";
|
||||
import {getBaseEntries, getOrganizationBaseDN, getRootDN, validateAdminPassword} from "../utils/ldap";
|
||||
import {UserService} from "../user/user.service";
|
||||
|
||||
@Injectable()
|
||||
export class LdapService {
|
||||
public readonly server: Server = ldapServer;
|
||||
|
||||
constructor(private userService: UserService) {
|
||||
|
||||
}
|
||||
|
||||
public run(port: number) {
|
||||
ldapRegister(this);
|
||||
this.server.listen(port);
|
||||
}
|
||||
|
||||
@Search('')
|
||||
private searchDSE(req, res, next) {
|
||||
const baseObject = {
|
||||
dn: '',
|
||||
structuralObjectClass: 'OpenLDAProotDSE',
|
||||
configContext: 'cn=config',
|
||||
attributes: {
|
||||
objectclass: ['top', 'OpenLDAProotDSE'],
|
||||
namingContexts: [getRootDN()],
|
||||
supportedLDAPVersion: ['3'],
|
||||
subschemaSubentry: ['cn=Subschema']
|
||||
}
|
||||
};
|
||||
console.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();
|
||||
}
|
||||
|
||||
@Search('cn=Subschema')
|
||||
private searchSchema(req, res, next) {
|
||||
var schema = {
|
||||
dn: 'cn=Subschema',
|
||||
attributes: {
|
||||
objectclass: ['top', 'subentry', 'subschema', 'extensibleObject'],
|
||||
cn: ['Subschema']
|
||||
}
|
||||
};
|
||||
res.send(schema);
|
||||
res.end();
|
||||
return next();
|
||||
}
|
||||
|
||||
@Search(getRootDN())
|
||||
private async search(req, res, next) {
|
||||
console.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) => {
|
||||
// console.log(reqDN.toString())
|
||||
// console.log(objDN.toString())
|
||||
return equalsDN(reqDN, objDN) || reqDN.parentOf(objDN);
|
||||
},
|
||||
'base': (objDN) => {
|
||||
return equalsDN(reqDN, objDN);
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
const comparator = comparators[req.scope];
|
||||
if (comparator) {
|
||||
const entries = await this.userService.fetchAllUserEntries();
|
||||
[
|
||||
...getBaseEntries(),
|
||||
...entries,
|
||||
// ...getLDAPCustomGroupEntries(),
|
||||
].filter(entry => {
|
||||
console.log(entry.dn)
|
||||
return comparator(parseDN(entry.dn.toLowerCase())) && req.filter.matches(entry.attributes);
|
||||
}).forEach(entry => {
|
||||
console.debug('send entry', entry.dn);
|
||||
res.send(entry);
|
||||
});
|
||||
}
|
||||
|
||||
res.end();
|
||||
return next();
|
||||
}
|
||||
|
||||
@Bind(getRootDN())
|
||||
async bind(req, res, next) {
|
||||
const reqDN = parseDN(req.dn.toString().toLowerCase());
|
||||
console.debug('bind req', reqDN)
|
||||
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 cn = reqDN['rdns'][0].attrs.cn.value;
|
||||
const matchedUser = await this.userService.findOne(cn)
|
||||
console.log(matchedUser)
|
||||
// if (matchedUser) {
|
||||
// const record = await getDBRecordForUserId(matchedUser.attributes.uid);
|
||||
// if (record && validateUserPassword(record.password, req.credentials)) {
|
||||
// res.end();
|
||||
// return next();
|
||||
// } else {
|
||||
// console.debug('password failed');
|
||||
// return next(new ldap.InvalidCredentialsError());
|
||||
// }
|
||||
// } else {
|
||||
// console.debug('user not found');
|
||||
// return next(new ldap.InvalidCredentialsError());
|
||||
// }
|
||||
}
|
||||
|
||||
return next(new InsufficientAccessRightsError());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import {LdapService} from "./ldap/ldap.service";
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
const ldap = app.get(LdapService)
|
||||
ldap.run(1389)
|
||||
}
|
||||
bootstrap();
|
|
@ -0,0 +1 @@
|
|||
export class CreateUserDto {}
|
|
@ -0,0 +1,4 @@
|
|||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
|
||||
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
|
@ -0,0 +1,14 @@
|
|||
import {Column, Entity, PrimaryColumn} from "typeorm";
|
||||
|
||||
@Entity('user')
|
||||
export class User {
|
||||
@PrimaryColumn('varchar')
|
||||
id:string;
|
||||
@PrimaryColumn('varchar')
|
||||
name: string;
|
||||
@Column('varchar')
|
||||
owner: string;
|
||||
@Column('varchar')
|
||||
password:string;
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import {Module} from '@nestjs/common';
|
||||
import {UserService} from './user.service';
|
||||
import {User} from "./entities/user.entity";
|
||||
import {TypeOrmModule} from "@nestjs/typeorm";
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
providers: [UserService],
|
||||
exports: [UserService]
|
||||
})
|
||||
export class UserModule {
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import {Injectable} from '@nestjs/common';
|
||||
import {CreateUserDto} from './dto/create-user.dto';
|
||||
import {UpdateUserDto} from './dto/update-user.dto';
|
||||
import {InjectRepository} from "@nestjs/typeorm";
|
||||
import {User} from "./entities/user.entity";
|
||||
import {Repository} from "typeorm";
|
||||
import {makePersonEntry} from "../utils/ldap";
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(@InjectRepository(User) private userRepository: Repository<User>) {
|
||||
}
|
||||
|
||||
create(createUserDto: CreateUserDto) {
|
||||
return 'This action adds a new user';
|
||||
}
|
||||
|
||||
findAll() {
|
||||
return `This action returns all user`;
|
||||
}
|
||||
|
||||
findOne(cn: string) {
|
||||
return this.userRepository.findOneBy({name: cn});
|
||||
}
|
||||
|
||||
async fetchAllUserEntries() {
|
||||
const users = await this.userRepository.find();
|
||||
const entries = [];
|
||||
for (let user of users) {
|
||||
const dn = `cn=${user.name}`;
|
||||
// const dn = `mail=${mail},${u.firstDepartment.dn}`;
|
||||
// 映射到 iNetOrgPerson
|
||||
const personEntry = makePersonEntry(dn, {
|
||||
uid: user.id,
|
||||
title: "",
|
||||
mobileTelephoneNumber: "",
|
||||
cn: user.name,
|
||||
givenName: "",
|
||||
sn: "",
|
||||
mail: "",
|
||||
avatarurl: "",
|
||||
});
|
||||
|
||||
entries.push(personEntry);
|
||||
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
update(id: number, updateUserDto: UpdateUserDto) {
|
||||
return `This action updates a #${id} user`;
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
return `This action removes a #${id} user`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
import {ldapConfig} from "../ldap.config";
|
||||
|
||||
export function getRootDN() {
|
||||
return ldapConfig.rootDN;
|
||||
}
|
||||
|
||||
export function makeDN(...parts) {
|
||||
return parts.filter((value) => {
|
||||
return value != ""
|
||||
}).join(',');
|
||||
}
|
||||
|
||||
export function getOrganizationBaseDN() {
|
||||
return makeDN('o=' + ldapConfig.organization, getRootDN());
|
||||
}
|
||||
|
||||
export function getPeopleBaseDN() {
|
||||
return makeDN('ou=People', getOrganizationBaseDN());
|
||||
}
|
||||
|
||||
export function getGroupsBaseDN() {
|
||||
return makeDN('ou=Groups', getOrganizationBaseDN());
|
||||
}
|
||||
|
||||
export function makeGroupEntryDN(...prefix) {
|
||||
return makeDN(...prefix, getGroupsBaseDN());
|
||||
}
|
||||
|
||||
export function makeOrganizationUnitEntryDN(...prefix) {
|
||||
return makeDN(...prefix, getPeopleBaseDN());
|
||||
}
|
||||
|
||||
export function makeOrganizationUnitEntry(dn, name, attrs = {}) {
|
||||
// dn: d.dn,
|
||||
// attributes: {
|
||||
// objectclass: ['organizationalUnit', 'top'],
|
||||
// ou: d.name,
|
||||
// }
|
||||
const generatedDN = makeOrganizationUnitEntryDN(dn);
|
||||
return {
|
||||
dn: generatedDN,
|
||||
attributes: Object.assign({
|
||||
objectclass: ['organizationalUnit', 'top'],
|
||||
ou: name,
|
||||
entryDN: dn,
|
||||
}, attrs),
|
||||
};
|
||||
}
|
||||
|
||||
export 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 generatedDN = makeDN(dn, getRootDN());
|
||||
return {
|
||||
dn: generatedDN,
|
||||
attributes: Object.assign({
|
||||
objectclass: ['inetOrgPerson', 'organizationalPerson', 'person', 'top'],
|
||||
userPassword: '********',
|
||||
// otpSecret: 'abcd',
|
||||
memberOf: [],
|
||||
entryDN: dn,
|
||||
}, attrs)
|
||||
};
|
||||
}
|
||||
|
||||
export function makeGroupEntry(dn, name, members, attrs = {}) {
|
||||
// dn: d.dn,
|
||||
// attributes: {
|
||||
// objectclass: ['groupOfNames', 'top'],
|
||||
// ou: d.name,
|
||||
// }
|
||||
const generatedDN = makeGroupEntryDN(dn);
|
||||
return {
|
||||
dn: generatedDN,
|
||||
attributes: Object.assign({
|
||||
objectclass: ['groupOfNames', 'top'],
|
||||
cn: name,
|
||||
ou: name,
|
||||
member: members,
|
||||
memberOf: [],
|
||||
entryDN: dn,
|
||||
}, attrs),
|
||||
};
|
||||
}
|
||||
|
||||
export function getOrganizationEntry() {
|
||||
return {
|
||||
dn: getOrganizationBaseDN(),
|
||||
attributes: {
|
||||
objectclass: ['organization', 'top'],
|
||||
ou: ldapConfig.organization,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getPeopleBaseEntry() {
|
||||
return {
|
||||
dn: getPeopleBaseDN(),
|
||||
attributes: {
|
||||
objectclass: ['organizationalUnit', 'top'],
|
||||
ou: 'People',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getGroupsBaseEntry() {
|
||||
return {
|
||||
dn: getGroupsBaseDN(),
|
||||
attributes: {
|
||||
objectclass: ['organizationalUnit', 'top'],
|
||||
ou: 'Groups',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function makeAdminEntry(attrs) {
|
||||
// {dn: 'cn=admin,'+rootDN, attributes: { objectclass: ['simpleSecurityObject', 'organizationalRole'], hasSubordinates: ['FALSE'] } },
|
||||
return {
|
||||
dn: [`cn=${attrs.commonName}`, getRootDN()].join(','),
|
||||
attributes: {
|
||||
objectclass: ['simpleSecurityObject', 'organizationalRole'],
|
||||
hasSubordinates: ['FALSE']
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getAdminEntries() {
|
||||
return ldapConfig.admins.map(cfg => {
|
||||
return makeAdminEntry(cfg);
|
||||
});
|
||||
}
|
||||
|
||||
export function getBaseEntries() {
|
||||
|
||||
const rootDN = getRootDN();
|
||||
const rootEntry = {
|
||||
dn: rootDN,
|
||||
attributes: {
|
||||
objectclass: ['dcObject', 'organization', 'top'],
|
||||
dc: rootDN.split(',')[0].split('=')[1].trim(),
|
||||
o: ldapConfig.organization,
|
||||
hasSubordinates: ['TRUE'],
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
rootEntry,
|
||||
getOrganizationEntry(),
|
||||
getPeopleBaseEntry(),
|
||||
getGroupsBaseEntry(),
|
||||
...getAdminEntries(),
|
||||
];
|
||||
}
|
||||
|
||||
export function validateAdminPassword(username, password) {
|
||||
const [user] = ldapConfig.admins.filter(u => {
|
||||
return u.commonName == username;
|
||||
});
|
||||
return user && user.password === password;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function validateAdminPermission(username, permission) {
|
||||
const [user] = ldapConfig.admins.filter(u => {
|
||||
return u.commonName == username;
|
||||
});
|
||||
return !!(user && user[permission]);
|
||||
|
||||
|
||||
}
|
||||
|
||||
export 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules",
|
||||
"test", "dist", "**/*spec.ts"]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2017",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue