Merge branch 'test' of https://gitea.verdnatura.es/verdnatura/salix into dev
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
This commit is contained in:
commit
cb8e9655af
|
@ -29,27 +29,41 @@ module.exports = Self => {
|
|||
let $ = Self.app.models;
|
||||
let token;
|
||||
let usesEmail = user.indexOf('@') !== -1;
|
||||
let userInfo = usesEmail
|
||||
? {email: user}
|
||||
: {username: user};
|
||||
|
||||
let loginInfo = {password};
|
||||
|
||||
if (usesEmail)
|
||||
loginInfo.email = user;
|
||||
else
|
||||
loginInfo.username = user;
|
||||
let loginInfo = Object.assign({password}, userInfo);
|
||||
|
||||
try {
|
||||
token = await $.User.login(loginInfo, 'user');
|
||||
try {
|
||||
let instance = await $.User.findOne({
|
||||
fields: ['username'],
|
||||
where: userInfo
|
||||
});
|
||||
await $.UserAccount.sync(instance.username, password);
|
||||
} catch (err) {
|
||||
if (err.code != 'LOGIN_FAILED' || usesEmail)
|
||||
console.warn(err);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code != 'LOGIN_FAILED')
|
||||
throw err;
|
||||
|
||||
let filter = {where: {name: user}};
|
||||
let instance = await Self.findOne(filter);
|
||||
let where = usesEmail
|
||||
? {email: user}
|
||||
: {name: user};
|
||||
Object.assign(where, {
|
||||
password: md5(password || '')
|
||||
});
|
||||
|
||||
if (!instance || instance.password !== md5(password || ''))
|
||||
throw err;
|
||||
let instance = await Self.findOne({
|
||||
fields: ['name'],
|
||||
where
|
||||
});
|
||||
if (!instance) throw err;
|
||||
|
||||
await $.UserAccount.sync(user, password);
|
||||
await $.UserAccount.sync(instance.name, password);
|
||||
token = await $.User.login(loginInfo, 'user');
|
||||
}
|
||||
|
||||
|
|
|
@ -8,3 +8,15 @@ ALTER TABLE account.ldapConfig DROP COLUMN `filter`;
|
|||
ALTER TABLE account.ldapConfig CHANGE baseDn userDn varchar(255) DEFAULT NULL NULL COMMENT 'The base DN to do the query';
|
||||
ALTER TABLE account.ldapConfig CHANGE host server varchar(255) NOT NULL COMMENT 'The hostname of LDAP server';
|
||||
ALTER TABLE account.ldapConfig MODIFY COLUMN password varchar(255) NOT NULL COMMENT 'The LDAP password';
|
||||
|
||||
-- Updated
|
||||
|
||||
ALTER TABLE account.sambaConfig DROP COLUMN sshUser;
|
||||
ALTER TABLE account.sambaConfig DROP COLUMN sshPassword;
|
||||
ALTER TABLE account.sambaConfig CHANGE host adController varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL NULL COMMENT 'The hosname of domain controller';
|
||||
ALTER TABLE account.sambaConfig MODIFY COLUMN adController varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL NULL COMMENT 'The hosname of domain controller';
|
||||
|
||||
ALTER TABLE account.sambaConfig DROP COLUMN userDn;
|
||||
ALTER TABLE account.sambaConfig ADD adDomain varchar(255) NOT NULL AFTER id;
|
||||
ALTER TABLE account.sambaConfig ADD verifyCert TINYINT UNSIGNED NOT NULL DEFAULT TRUE AFTER adPassword;
|
||||
ALTER TABLE account.sambaConfig MODIFY COLUMN adController varchar(255) NOT NULL COMMENT 'The hosname of domain controller';
|
||||
|
|
|
@ -63,3 +63,4 @@ Loading...: Cargando...
|
|||
No results found: Sin resultados
|
||||
No data: Sin datos
|
||||
Load more: Cargar más
|
||||
Undo changes: Deshacer cambios
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('test', {
|
||||
description: 'Tests connector configuration',
|
||||
http: {
|
||||
path: `/test`,
|
||||
verb: 'GET'
|
||||
}
|
||||
});
|
||||
|
||||
Self.test = async function() {
|
||||
let connector = await Self.getSynchronizer();
|
||||
await connector.test();
|
||||
};
|
||||
};
|
|
@ -1,6 +1,4 @@
|
|||
|
||||
const SyncEngine = require('../../util/sync-engine');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('sync', {
|
||||
description: 'Synchronizes the user with the other user databases',
|
||||
|
@ -11,17 +9,6 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
Self.sync = async function() {
|
||||
let engine = new SyncEngine();
|
||||
await engine.init(Self.app.models);
|
||||
|
||||
let err;
|
||||
try {
|
||||
await engine.syncRoles();
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
await engine.deinit();
|
||||
if (err) throw err;
|
||||
await Self.app.models.AccountConfig.syncRoles();
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
|
||||
const SyncEngine = require('../../util/sync-engine');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('syncAll', {
|
||||
description: 'Synchronizes user database with LDAP and Samba',
|
||||
|
@ -11,26 +9,6 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
Self.syncAll = async function() {
|
||||
let $ = Self.app.models;
|
||||
|
||||
let engine = new SyncEngine();
|
||||
await engine.init($);
|
||||
|
||||
let usersToSync = await engine.getUsers();
|
||||
usersToSync = Array.from(usersToSync.values())
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
for (let user of usersToSync) {
|
||||
try {
|
||||
console.log(`Synchronizing user '${user}'`);
|
||||
await engine.sync(user);
|
||||
console.log(` -> '${user}' sinchronized`);
|
||||
} catch (err) {
|
||||
console.error(` -> '${user}' synchronization error:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
await engine.deinit();
|
||||
await $.RoleInherit.sync();
|
||||
await Self.app.models.AccountConfig.syncUsers();
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12,6 +12,10 @@ module.exports = Self => {
|
|||
arg: 'password',
|
||||
type: 'string',
|
||||
description: 'The password'
|
||||
}, {
|
||||
arg: 'force',
|
||||
type: 'boolean',
|
||||
description: 'Whether to force synchronization'
|
||||
}
|
||||
],
|
||||
http: {
|
||||
|
@ -20,8 +24,8 @@ module.exports = Self => {
|
|||
}
|
||||
});
|
||||
|
||||
Self.syncById = async function(id, password) {
|
||||
Self.syncById = async function(id, password, force) {
|
||||
let user = await Self.app.models.Account.findById(id, {fields: ['name']});
|
||||
await Self.sync(user.name, password);
|
||||
await Self.sync(user.name, password, force);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
|
||||
const SyncEngine = require('../../util/sync-engine');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('sync', {
|
||||
description: 'Synchronizes the user with the other user databases',
|
||||
|
@ -14,6 +12,10 @@ module.exports = Self => {
|
|||
arg: 'password',
|
||||
type: 'string',
|
||||
description: 'The password'
|
||||
}, {
|
||||
arg: 'force',
|
||||
type: 'boolean',
|
||||
description: 'Whether to force synchronization'
|
||||
}
|
||||
],
|
||||
http: {
|
||||
|
@ -22,7 +24,7 @@ module.exports = Self => {
|
|||
}
|
||||
});
|
||||
|
||||
Self.sync = async function(userName, password) {
|
||||
Self.sync = async function(userName, password, force) {
|
||||
let $ = Self.app.models;
|
||||
|
||||
let user = await $.Account.findOne({
|
||||
|
@ -31,21 +33,9 @@ module.exports = Self => {
|
|||
});
|
||||
let isSync = !await $.UserSync.exists(userName);
|
||||
|
||||
if (user && isSync) return;
|
||||
|
||||
let err;
|
||||
let engine = new SyncEngine();
|
||||
await engine.init($);
|
||||
|
||||
try {
|
||||
await engine.sync(userName, password, true);
|
||||
if (!force && isSync && user) return;
|
||||
await $.AccountConfig.syncUser(userName, password);
|
||||
await $.UserSync.destroyById(userName);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
await engine.deinit();
|
||||
if (err) throw err;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
|
||||
const app = require('vn-loopback/server/server');
|
||||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = function(Self, options) {
|
||||
require('../methods/account-synchronizer/test')(Self);
|
||||
|
||||
Self.once('attached', function() {
|
||||
app.models.AccountConfig.addSynchronizer(Self);
|
||||
});
|
||||
|
||||
/**
|
||||
* Mixin for user synchronizers.
|
||||
*
|
||||
* @property {Array<Model>} $
|
||||
* @property {Object} accountConfig
|
||||
* @property {Object} mailConfig
|
||||
*/
|
||||
let Mixin = {
|
||||
/**
|
||||
* Initalizes the synchronizer.
|
||||
*/
|
||||
async init() {},
|
||||
|
||||
/**
|
||||
* Deinitalizes the synchronizer.
|
||||
*/
|
||||
async deinit() {},
|
||||
|
||||
/**
|
||||
* Get users to synchronize.
|
||||
*
|
||||
* @param {Set} usersToSync Set where users are added
|
||||
*/
|
||||
async getUsers(usersToSync) {},
|
||||
|
||||
/**
|
||||
* Synchronizes a user.
|
||||
*
|
||||
* @param {Object} info User information
|
||||
* @param {String} userName The user name
|
||||
* @param {String} password Thepassword
|
||||
*/
|
||||
async syncUser(info, userName, password) {},
|
||||
|
||||
/**
|
||||
* Synchronizes user groups.
|
||||
*
|
||||
* @param {Object} info User information
|
||||
* @param {String} userName The user name
|
||||
*/
|
||||
async syncUserGroups(info, userName) {},
|
||||
|
||||
/**
|
||||
* Synchronizes roles.
|
||||
*/
|
||||
async syncRoles() {},
|
||||
|
||||
/**
|
||||
* Tests synchronizer configuration.
|
||||
*/
|
||||
async test() {
|
||||
try {
|
||||
await this.init();
|
||||
await this.deinit();
|
||||
} catch (e) {
|
||||
let err = new UserError(e.message);
|
||||
err.name = e.name;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (let method in Mixin) {
|
||||
if (!Self.prototype[method])
|
||||
Self.prototype[method] = Mixin[method];
|
||||
}
|
||||
};
|
|
@ -29,6 +29,12 @@
|
|||
"SambaConfig": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"Sip": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"SipConfig": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"UserAccount": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
|
||||
const app = require('vn-loopback/server/server');
|
||||
|
||||
module.exports = Self => {
|
||||
Object.assign(Self, {
|
||||
synchronizers: [],
|
||||
|
||||
addSynchronizer(synchronizer) {
|
||||
this.synchronizers.push(synchronizer);
|
||||
},
|
||||
|
||||
async syncUsers() {
|
||||
let instance = await Self.getInstance();
|
||||
|
||||
let usersToSync = instance.getUsers();
|
||||
usersToSync = Array.from(usersToSync.values())
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
for (let userName of usersToSync) {
|
||||
try {
|
||||
console.log(`Synchronizing user '${userName}'`);
|
||||
await instance.synchronizerSyncUser(userName);
|
||||
console.log(` -> '${userName}' sinchronized`);
|
||||
} catch (err) {
|
||||
console.error(` -> '${userName}' synchronization error:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
await instance.synchronizerDeinit();
|
||||
await Self.syncRoles();
|
||||
},
|
||||
|
||||
async syncUser(userName, password) {
|
||||
let instance = await Self.getInstance();
|
||||
try {
|
||||
await instance.synchronizerSyncUser(userName, password, true);
|
||||
} finally {
|
||||
await instance.synchronizerDeinit();
|
||||
}
|
||||
},
|
||||
|
||||
async syncRoles() {
|
||||
let instance = await Self.getInstance();
|
||||
try {
|
||||
await instance.synchronizerSyncRoles();
|
||||
} finally {
|
||||
await instance.synchronizerDeinit();
|
||||
}
|
||||
},
|
||||
|
||||
async getSynchronizer() {
|
||||
return await Self.findOne();
|
||||
},
|
||||
|
||||
async getInstance() {
|
||||
let instance = await Self.findOne({
|
||||
fields: ['homedir', 'shell', 'idBase']
|
||||
});
|
||||
await instance.synchronizerInit();
|
||||
return instance;
|
||||
}
|
||||
});
|
||||
|
||||
Object.assign(Self.prototype, {
|
||||
async synchronizerInit() {
|
||||
let mailConfig = await app.models.MailConfig.findOne({
|
||||
fields: ['domain']
|
||||
});
|
||||
|
||||
let synchronizers = [];
|
||||
|
||||
for (let Synchronizer of Self.synchronizers) {
|
||||
let synchronizer = await Synchronizer.getSynchronizer();
|
||||
if (!synchronizer) continue;
|
||||
Object.assign(synchronizer, {
|
||||
accountConfig: this
|
||||
});
|
||||
await synchronizer.init();
|
||||
synchronizers.push(synchronizer);
|
||||
}
|
||||
|
||||
Object.assign(this, {
|
||||
synchronizers,
|
||||
domain: mailConfig.domain
|
||||
});
|
||||
},
|
||||
|
||||
async synchronizerDeinit() {
|
||||
for (let synchronizer of this.synchronizers)
|
||||
await synchronizer.deinit();
|
||||
},
|
||||
|
||||
async synchronizerSyncUser(userName, password, syncGroups) {
|
||||
let $ = app.models;
|
||||
|
||||
if (!userName) return;
|
||||
userName = userName.toLowerCase();
|
||||
|
||||
// Skip conflicting users
|
||||
if (['administrator', 'root'].indexOf(userName) >= 0)
|
||||
return;
|
||||
|
||||
let user = await $.Account.findOne({
|
||||
where: {name: userName},
|
||||
fields: [
|
||||
'id',
|
||||
'nickname',
|
||||
'email',
|
||||
'lang',
|
||||
'roleFk',
|
||||
'sync',
|
||||
'active',
|
||||
'created',
|
||||
'bcryptPassword',
|
||||
'updated'
|
||||
],
|
||||
include: {
|
||||
relation: 'roles',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'inherits',
|
||||
scope: {
|
||||
fields: ['name']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let info = {
|
||||
user,
|
||||
hasAccount: false
|
||||
};
|
||||
|
||||
if (user) {
|
||||
let exists = await $.UserAccount.exists(user.id);
|
||||
Object.assign(info, {
|
||||
hasAccount: user.active && exists,
|
||||
corporateMail: `${userName}@${this.domain}`,
|
||||
uidNumber: this.idBase + user.id
|
||||
});
|
||||
}
|
||||
|
||||
let errs = [];
|
||||
|
||||
for (let synchronizer of this.synchronizers) {
|
||||
try {
|
||||
await synchronizer.syncUser(userName, info, password);
|
||||
if (syncGroups)
|
||||
await synchronizer.syncUserGroups(userName, info);
|
||||
} catch (err) {
|
||||
errs.push(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (errs.length) throw errs[0];
|
||||
},
|
||||
|
||||
async synchronizerGetUsers() {
|
||||
let usersToSync = new Set();
|
||||
|
||||
for (let synchronizer of this.synchronizers)
|
||||
await synchronizer.getUsers(usersToSync);
|
||||
|
||||
return usersToSync;
|
||||
},
|
||||
|
||||
async synchronizerSyncRoles() {
|
||||
for (let synchronizer of this.synchronizers)
|
||||
await synchronizer.syncRoles();
|
||||
},
|
||||
|
||||
async syncUser(userName, info, password) {
|
||||
let $ = app.models;
|
||||
let {user} = info;
|
||||
|
||||
if (user && user.active) {
|
||||
let bcryptPassword = password
|
||||
? $.User.hashPassword(password)
|
||||
: user.bcryptPassword;
|
||||
|
||||
await $.Account.upsertWithWhere({id: user.id},
|
||||
{bcryptPassword}
|
||||
);
|
||||
|
||||
let dbUser = {
|
||||
id: user.id,
|
||||
username: userName,
|
||||
email: user.email,
|
||||
created: user.created,
|
||||
updated: user.updated
|
||||
};
|
||||
if (bcryptPassword)
|
||||
dbUser.password = bcryptPassword;
|
||||
|
||||
if (await $.user.exists(user.id))
|
||||
await $.user.replaceById(user.id, dbUser);
|
||||
else
|
||||
await $.user.create(dbUser);
|
||||
} else
|
||||
await $.user.destroyAll({username: userName});
|
||||
},
|
||||
|
||||
async getUsers(usersToSync) {
|
||||
let accounts = await app.models.UserAccount.find({
|
||||
fields: ['id'],
|
||||
include: {
|
||||
relation: 'user',
|
||||
scope: {
|
||||
fields: ['name'],
|
||||
where: {active: true}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (let account of accounts) {
|
||||
let user = account.user();
|
||||
if (!user) continue;
|
||||
usersToSync.add(user.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
|
@ -6,6 +6,9 @@
|
|||
"table": "account.accountConfig"
|
||||
}
|
||||
},
|
||||
"mixins": {
|
||||
"AccountSynchronizer": {}
|
||||
},
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
|
||||
const app = require('vn-loopback/server/server');
|
||||
const ldap = require('../util/ldapjs-extra');
|
||||
const crypto = require('crypto');
|
||||
const nthash = require('smbhash').nthash;
|
||||
|
||||
module.exports = Self => {
|
||||
Self.getSynchronizer = async function() {
|
||||
return await Self.findOne({
|
||||
fields: [
|
||||
'server',
|
||||
'rdn',
|
||||
'password',
|
||||
'userDn',
|
||||
'groupDn'
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
Object.assign(Self.prototype, {
|
||||
async init() {
|
||||
this.client = ldap.createClient({
|
||||
url: this.server
|
||||
});
|
||||
await this.client.bind(this.rdn, this.password);
|
||||
},
|
||||
|
||||
async deinit() {
|
||||
await this.client.unbind();
|
||||
},
|
||||
|
||||
async syncUser(userName, info, password) {
|
||||
let {
|
||||
client,
|
||||
accountConfig
|
||||
} = this;
|
||||
|
||||
let {user} = info;
|
||||
|
||||
let res = await client.search(this.userDn, {
|
||||
scope: 'sub',
|
||||
attributes: ['userPassword', 'sambaNTPassword'],
|
||||
filter: `&(uid=${userName})`
|
||||
});
|
||||
|
||||
let oldUser;
|
||||
await new Promise((resolve, reject) => {
|
||||
res.on('error', reject);
|
||||
res.on('searchEntry', e => oldUser = e.object);
|
||||
res.on('end', resolve);
|
||||
});
|
||||
|
||||
try {
|
||||
let dn = `uid=${userName},${this.userDn}`;
|
||||
await client.del(dn);
|
||||
} catch (e) {
|
||||
if (e.name !== 'NoSuchObjectError') throw e;
|
||||
}
|
||||
|
||||
if (!info.hasAccount) {
|
||||
if (oldUser)
|
||||
console.log(` -> '${userName}' removed from LDAP`);
|
||||
return;
|
||||
}
|
||||
|
||||
let nickname = user.nickname || userName;
|
||||
let nameArgs = nickname.trim().split(' ');
|
||||
let sn = nameArgs.length > 1
|
||||
? nameArgs.splice(1).join(' ')
|
||||
: '-';
|
||||
|
||||
let dn = `uid=${userName},${this.userDn}`;
|
||||
let newEntry = {
|
||||
uid: userName,
|
||||
objectClass: [
|
||||
'inetOrgPerson',
|
||||
'posixAccount',
|
||||
'sambaSamAccount'
|
||||
],
|
||||
cn: nickname,
|
||||
displayName: nickname,
|
||||
givenName: nameArgs[0],
|
||||
sn,
|
||||
mail: info.corporateMail,
|
||||
preferredLanguage: user.lang || 'en',
|
||||
homeDirectory: `${accountConfig.homedir}/${userName}`,
|
||||
loginShell: accountConfig.shell,
|
||||
uidNumber: info.uidNumber,
|
||||
gidNumber: accountConfig.idBase + user.roleFk,
|
||||
sambaSID: '-'
|
||||
};
|
||||
|
||||
if (password) {
|
||||
let salt = crypto
|
||||
.randomBytes(8)
|
||||
.toString('base64');
|
||||
|
||||
let hash = crypto.createHash('sha1');
|
||||
hash.update(password);
|
||||
hash.update(salt, 'binary');
|
||||
let digest = hash.digest('binary');
|
||||
|
||||
let ssha = Buffer
|
||||
.from(digest + salt, 'binary')
|
||||
.toString('base64');
|
||||
|
||||
Object.assign(newEntry, {
|
||||
userPassword: `{SSHA}${ssha}`,
|
||||
sambaNTPassword: nthash(password)
|
||||
});
|
||||
} else if (oldUser) {
|
||||
Object.assign(newEntry, {
|
||||
userPassword: oldUser.userPassword,
|
||||
sambaNTPassword: oldUser.sambaNTPassword
|
||||
});
|
||||
}
|
||||
|
||||
for (let prop in newEntry) {
|
||||
if (newEntry[prop] == null)
|
||||
delete newEntry[prop];
|
||||
}
|
||||
|
||||
await client.add(dn, newEntry);
|
||||
},
|
||||
|
||||
async syncUserGroups(userName, info) {
|
||||
let {client} = this;
|
||||
|
||||
let res = await client.search(this.groupDn, {
|
||||
scope: 'sub',
|
||||
attributes: ['dn'],
|
||||
filter: `&(memberUid=${userName})(objectClass=posixGroup)`
|
||||
});
|
||||
|
||||
let oldGroups = [];
|
||||
await new Promise((resolve, reject) => {
|
||||
res.on('error', reject);
|
||||
res.on('searchEntry', e => oldGroups.push(e.object));
|
||||
res.on('end', resolve);
|
||||
});
|
||||
|
||||
let reqs = [];
|
||||
for (let oldGroup of oldGroups) {
|
||||
let change = new ldap.Change({
|
||||
operation: 'delete',
|
||||
modification: {memberUid: userName}
|
||||
});
|
||||
reqs.push(client.modify(oldGroup.dn, change));
|
||||
}
|
||||
await Promise.all(reqs);
|
||||
|
||||
if (!info.hasAccount) return;
|
||||
|
||||
reqs = [];
|
||||
for (let role of info.user.roles()) {
|
||||
let change = new ldap.Change({
|
||||
operation: 'add',
|
||||
modification: {memberUid: userName}
|
||||
});
|
||||
let roleName = role.inherits().name;
|
||||
let dn = `cn=${roleName},${this.groupDn}`;
|
||||
reqs.push(client.modify(dn, change));
|
||||
}
|
||||
await Promise.all(reqs);
|
||||
},
|
||||
|
||||
async getUsers(usersToSync) {
|
||||
let {client} = this;
|
||||
|
||||
let res = await client.search(this.userDn, {
|
||||
scope: 'sub',
|
||||
attributes: ['uid'],
|
||||
filter: `uid=*`
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
res.on('error', reject);
|
||||
res.on('searchEntry', e => usersToSync.add(e.object.uid));
|
||||
res.on('end', resolve);
|
||||
});
|
||||
},
|
||||
|
||||
async syncRoles() {
|
||||
let $ = app.models;
|
||||
let {
|
||||
client,
|
||||
accountConfig
|
||||
} = this;
|
||||
|
||||
// Delete roles
|
||||
|
||||
let opts = {
|
||||
scope: 'sub',
|
||||
attributes: ['dn'],
|
||||
filter: 'objectClass=posixGroup'
|
||||
};
|
||||
let res = await client.search(this.groupDn, opts);
|
||||
|
||||
let reqs = [];
|
||||
await new Promise((resolve, reject) => {
|
||||
res.on('error', err => {
|
||||
if (err.name === 'NoSuchObjectError')
|
||||
err = new Error(`Object '${this.groupDn}' does not exist`);
|
||||
reject(err);
|
||||
});
|
||||
res.on('searchEntry', e => {
|
||||
reqs.push(client.del(e.object.dn));
|
||||
});
|
||||
res.on('end', resolve);
|
||||
});
|
||||
await Promise.all(reqs);
|
||||
|
||||
// Recreate roles
|
||||
|
||||
let roles = await $.Role.find({
|
||||
fields: ['id', 'name', 'description']
|
||||
});
|
||||
let roleRoles = await $.RoleRole.find({
|
||||
fields: ['role', 'inheritsFrom']
|
||||
});
|
||||
let roleMap = toMap(roleRoles, e => {
|
||||
return {key: e.inheritsFrom, val: e.role};
|
||||
});
|
||||
|
||||
let accounts = await $.UserAccount.find({
|
||||
fields: ['id'],
|
||||
include: {
|
||||
relation: 'user',
|
||||
scope: {
|
||||
fields: ['name', 'roleFk'],
|
||||
where: {active: true}
|
||||
}
|
||||
}
|
||||
});
|
||||
let accountMap = toMap(accounts, e => {
|
||||
let user = e.user();
|
||||
if (!user) return;
|
||||
return {key: user.roleFk, val: user.name};
|
||||
});
|
||||
|
||||
reqs = [];
|
||||
for (let role of roles) {
|
||||
let newEntry = {
|
||||
objectClass: ['top', 'posixGroup'],
|
||||
cn: role.name,
|
||||
description: role.description,
|
||||
gidNumber: accountConfig.idBase + role.id
|
||||
};
|
||||
|
||||
let memberUid = [];
|
||||
for (let subrole of roleMap.get(role.id) || [])
|
||||
memberUid = memberUid.concat(accountMap.get(subrole) || []);
|
||||
|
||||
if (memberUid.length) {
|
||||
memberUid.sort((a, b) => a.localeCompare(b));
|
||||
newEntry.memberUid = memberUid;
|
||||
}
|
||||
|
||||
let dn = `cn=${role.name},${this.groupDn}`;
|
||||
reqs.push(client.add(dn, newEntry));
|
||||
}
|
||||
await Promise.all(reqs);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -6,6 +6,9 @@
|
|||
"table": "account.ldapConfig"
|
||||
}
|
||||
},
|
||||
"mixins": {
|
||||
"AccountSynchronizer": {}
|
||||
},
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
|
||||
const ldap = require('../util/ldapjs-extra');
|
||||
const ssh = require('node-ssh');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.getSynchronizer = async function() {
|
||||
return await Self.findOne({
|
||||
fields: [
|
||||
'host',
|
||||
'adDomain',
|
||||
'adController',
|
||||
'adUser',
|
||||
'adPassword',
|
||||
'verifyCert'
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
Object.assign(Self.prototype, {
|
||||
async init() {
|
||||
let sshClient = new ssh.NodeSSH();
|
||||
await sshClient.connect({
|
||||
host: this.adController,
|
||||
username: this.adUser,
|
||||
password: this.adPassword
|
||||
});
|
||||
|
||||
let adUser = `cn=${this.adUser},${this.usersDn()}`;
|
||||
|
||||
let adClient = ldap.createClient({
|
||||
url: `ldaps://${this.adController}:636`,
|
||||
tlsOptions: {rejectUnauthorized: this.verifyCert}
|
||||
});
|
||||
await adClient.bind(adUser, this.adPassword);
|
||||
|
||||
Object.assign(this, {
|
||||
sshClient,
|
||||
adClient
|
||||
});
|
||||
},
|
||||
|
||||
async deinit() {
|
||||
await this.sshClient.dispose();
|
||||
await this.adClient.unbind();
|
||||
},
|
||||
|
||||
usersDn() {
|
||||
let dnBase = this.adDomain
|
||||
.split('.')
|
||||
.map(part => `dc=${part}`)
|
||||
.join(',');
|
||||
return `cn=Users,${dnBase}`;
|
||||
},
|
||||
|
||||
async syncUser(userName, info, password) {
|
||||
let {sshClient} = this;
|
||||
|
||||
if (info.hasAccount) {
|
||||
try {
|
||||
await sshClient.exec('samba-tool user create', [
|
||||
userName,
|
||||
'--uid-number', `${info.uidNumber}`,
|
||||
'--mail-address', info.corporateMail,
|
||||
'--random-password'
|
||||
]);
|
||||
await sshClient.exec('samba-tool user setexpiry', [
|
||||
userName,
|
||||
'--noexpiry'
|
||||
]);
|
||||
await sshClient.exec('mkhomedir_helper', [
|
||||
userName,
|
||||
'0027'
|
||||
]);
|
||||
} catch (e) {}
|
||||
|
||||
await sshClient.exec('samba-tool user enable', [
|
||||
userName
|
||||
]);
|
||||
|
||||
if (password) {
|
||||
await sshClient.exec('samba-tool user setpassword', [
|
||||
userName,
|
||||
'--newpassword', password
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await sshClient.exec('samba-tool user disable', [
|
||||
userName
|
||||
]);
|
||||
console.log(` -> '${userName}' disabled on Samba`);
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets Samba enabled users.
|
||||
*
|
||||
* Summary of userAccountControl flags:
|
||||
* https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
|
||||
*
|
||||
* @param {Set} usersToSync
|
||||
*/
|
||||
async getUsers(usersToSync) {
|
||||
let {adClient} = this;
|
||||
let usersDn = this.usersDn();
|
||||
|
||||
let opts = {
|
||||
scope: 'sub',
|
||||
attributes: ['sAMAccountName'],
|
||||
filter: '(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))'
|
||||
};
|
||||
let res = await adClient.search(usersDn, opts);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
res.on('error', err => {
|
||||
if (err.name === 'NoSuchObjectError')
|
||||
err = new Error(`Object '${usersDn}' does not exist`);
|
||||
reject(err);
|
||||
});
|
||||
res.on('searchEntry', e => {
|
||||
usersToSync.add(e.object.sAMAccountName);
|
||||
});
|
||||
res.on('end', resolve);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
|
@ -6,20 +6,21 @@
|
|||
"table": "account.sambaConfig"
|
||||
}
|
||||
},
|
||||
"mixins": {
|
||||
"AccountSynchronizer": {}
|
||||
},
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"id": true
|
||||
},
|
||||
"host": {
|
||||
"adDomain": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"sshUser": {
|
||||
"type": "string"
|
||||
},
|
||||
"sshPassword": {
|
||||
"type": "string"
|
||||
"adController": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"adUser": {
|
||||
"type": "string"
|
||||
|
@ -27,8 +28,8 @@
|
|||
"adPassword": {
|
||||
"type": "string"
|
||||
},
|
||||
"userDn": {
|
||||
"type": "string"
|
||||
"verifyCert": {
|
||||
"type": "Boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
const app = require('vn-loopback/server/server');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.getSynchronizer = async function() {
|
||||
return await Self.findOne({fields: ['id']});
|
||||
};
|
||||
|
||||
Object.assign(Self.prototype, {
|
||||
async syncUser(userName, info, password) {
|
||||
if (!info.hasAccount || !password) return;
|
||||
|
||||
await app.models.Account.rawSql('CALL pbx.sip_setPassword(?, ?)',
|
||||
[info.user.id, password]
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "SipConfig",
|
||||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "pbx.sipConfig"
|
||||
}
|
||||
},
|
||||
"mixins": {
|
||||
"AccountSynchronizer": {}
|
||||
},
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "Number",
|
||||
"id": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
module.exports = Self => {
|
||||
Self.validatesUniquenessOf('extension', {
|
||||
message: `The extension must be unique`
|
|
@ -1,52 +0,0 @@
|
|||
/**
|
||||
* Base class for user synchronizators.
|
||||
*
|
||||
* @property {Array<Model>} $
|
||||
* @property {Object} accountConfig
|
||||
* @property {Object} mailConfig
|
||||
*/
|
||||
class SyncConnector {
|
||||
/**
|
||||
* Initalizes the connector.
|
||||
*/
|
||||
async init() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users to synchronize.
|
||||
*
|
||||
* @param {Set} usersToSync Set where users are added
|
||||
*/
|
||||
async getUsers(usersToSync) {}
|
||||
|
||||
/**
|
||||
* Synchronizes a user.
|
||||
*
|
||||
* @param {Object} info User information
|
||||
* @param {String} userName The user name
|
||||
* @param {String} password Thepassword
|
||||
*/
|
||||
async sync(info, userName, password) {}
|
||||
|
||||
/**
|
||||
* Synchronizes user groups.
|
||||
*
|
||||
* @param {User} user Instace of user
|
||||
* @param {String} userName The user name
|
||||
*/
|
||||
async syncGroups(user, userName) {}
|
||||
|
||||
/**
|
||||
* Synchronizes roles.
|
||||
*/
|
||||
async syncRoles() {}
|
||||
|
||||
/**
|
||||
* Deinitalizes the connector.
|
||||
*/
|
||||
async deinit() {}
|
||||
}
|
||||
|
||||
SyncConnector.connectors = [];
|
||||
module.exports = SyncConnector;
|
|
@ -1,57 +0,0 @@
|
|||
|
||||
const SyncConnector = require('./sync-connector');
|
||||
|
||||
class SyncDb extends SyncConnector {
|
||||
async sync(info, userName, password) {
|
||||
let {$} = this;
|
||||
let {user} = info;
|
||||
|
||||
if (user && user.active) {
|
||||
let bcryptPassword = password
|
||||
? $.User.hashPassword(password)
|
||||
: user.bcryptPassword;
|
||||
|
||||
await $.Account.upsertWithWhere({id: user.id},
|
||||
{bcryptPassword}
|
||||
);
|
||||
|
||||
let dbUser = {
|
||||
id: user.id,
|
||||
username: userName,
|
||||
email: user.email,
|
||||
created: user.created,
|
||||
updated: user.updated
|
||||
};
|
||||
if (bcryptPassword)
|
||||
dbUser.password = bcryptPassword;
|
||||
|
||||
if (await $.user.exists(user.id))
|
||||
await $.user.replaceById(user.id, dbUser);
|
||||
else
|
||||
await $.user.create(dbUser);
|
||||
} else
|
||||
await $.user.destroyAll({username: userName});
|
||||
}
|
||||
|
||||
async getUsers(usersToSync) {
|
||||
let accounts = await this.$.UserAccount.find({
|
||||
fields: ['id'],
|
||||
include: {
|
||||
relation: 'user',
|
||||
scope: {
|
||||
fields: ['name'],
|
||||
where: {active: true}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (let account of accounts) {
|
||||
let user = account.user();
|
||||
if (!user) continue;
|
||||
usersToSync.add(user.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SyncConnector.connectors.push(SyncDb);
|
||||
module.exports = SyncDb;
|
|
@ -1,127 +0,0 @@
|
|||
|
||||
const SyncConnector = require('./sync-connector');
|
||||
require('./sync-db');
|
||||
require('./sync-sip');
|
||||
require('./sync-ldap');
|
||||
require('./sync-samba');
|
||||
|
||||
module.exports = class SyncEngine {
|
||||
async init($) {
|
||||
let accountConfig = await $.AccountConfig.findOne({
|
||||
fields: ['homedir', 'shell', 'idBase']
|
||||
});
|
||||
let mailConfig = await $.MailConfig.findOne({
|
||||
fields: ['domain']
|
||||
});
|
||||
|
||||
let connectors = [];
|
||||
|
||||
for (let ConnectorClass of SyncConnector.connectors) {
|
||||
let connector = new ConnectorClass();
|
||||
Object.assign(connector, {
|
||||
engine: this,
|
||||
$,
|
||||
accountConfig,
|
||||
mailConfig
|
||||
});
|
||||
if (!await connector.init()) continue;
|
||||
connectors.push(connector);
|
||||
}
|
||||
|
||||
Object.assign(this, {
|
||||
connectors,
|
||||
$,
|
||||
accountConfig,
|
||||
mailConfig
|
||||
});
|
||||
}
|
||||
|
||||
async deinit() {
|
||||
for (let connector of this.connectors)
|
||||
await connector.deinit();
|
||||
}
|
||||
|
||||
async sync(userName, password, syncGroups) {
|
||||
let {
|
||||
$,
|
||||
accountConfig,
|
||||
mailConfig
|
||||
} = this;
|
||||
|
||||
if (!userName) return;
|
||||
userName = userName.toLowerCase();
|
||||
|
||||
// Skip conflicting users
|
||||
if (['administrator', 'root'].indexOf(userName) >= 0)
|
||||
return;
|
||||
|
||||
let user = await $.Account.findOne({
|
||||
where: {name: userName},
|
||||
fields: [
|
||||
'id',
|
||||
'nickname',
|
||||
'email',
|
||||
'lang',
|
||||
'roleFk',
|
||||
'sync',
|
||||
'active',
|
||||
'created',
|
||||
'bcryptPassword',
|
||||
'updated'
|
||||
],
|
||||
include: {
|
||||
relation: 'roles',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'inherits',
|
||||
scope: {
|
||||
fields: ['name']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let info = {
|
||||
user,
|
||||
hasAccount: false
|
||||
};
|
||||
|
||||
if (user) {
|
||||
let exists = await $.UserAccount.exists(user.id);
|
||||
Object.assign(info, {
|
||||
hasAccount: user.active && exists,
|
||||
corporateMail: `${userName}@${mailConfig.domain}`,
|
||||
uidNumber: accountConfig.idBase + user.id
|
||||
});
|
||||
}
|
||||
|
||||
let errs = [];
|
||||
|
||||
for (let connector of this.connectors) {
|
||||
try {
|
||||
await connector.sync(info, userName, password);
|
||||
if (syncGroups)
|
||||
await connector.syncGroups(info, userName);
|
||||
} catch (err) {
|
||||
errs.push(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (errs.length) throw errs[0];
|
||||
}
|
||||
|
||||
async syncRoles() {
|
||||
for (let connector of this.connectors)
|
||||
await connector.syncRoles();
|
||||
}
|
||||
|
||||
async getUsers() {
|
||||
let usersToSync = new Set();
|
||||
|
||||
for (let connector of this.connectors)
|
||||
await connector.getUsers(usersToSync);
|
||||
|
||||
return usersToSync;
|
||||
}
|
||||
};
|
|
@ -1,292 +0,0 @@
|
|||
|
||||
const SyncConnector = require('./sync-connector');
|
||||
const nthash = require('smbhash').nthash;
|
||||
const ldap = require('./ldapjs-extra');
|
||||
const crypto = require('crypto');
|
||||
|
||||
class SyncLdap extends SyncConnector {
|
||||
async init() {
|
||||
let ldapConfig = await this.$.LdapConfig.findOne({
|
||||
fields: [
|
||||
'server',
|
||||
'rdn',
|
||||
'password',
|
||||
'userDn',
|
||||
'groupDn'
|
||||
]
|
||||
});
|
||||
if (!ldapConfig) return false;
|
||||
|
||||
let client = ldap.createClient({
|
||||
url: ldapConfig.server
|
||||
});
|
||||
await client.bind(ldapConfig.rdn, ldapConfig.password);
|
||||
|
||||
Object.assign(this, {
|
||||
ldapConfig,
|
||||
client
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async deinit() {
|
||||
if (this.client)
|
||||
await this.client.unbind();
|
||||
}
|
||||
|
||||
async sync(info, userName, password) {
|
||||
let {
|
||||
ldapConfig,
|
||||
client,
|
||||
accountConfig
|
||||
} = this;
|
||||
|
||||
let {user} = info;
|
||||
|
||||
let res = await client.search(ldapConfig.userDn, {
|
||||
scope: 'sub',
|
||||
attributes: ['userPassword', 'sambaNTPassword'],
|
||||
filter: `&(uid=${userName})`
|
||||
});
|
||||
|
||||
let oldUser;
|
||||
await new Promise((resolve, reject) => {
|
||||
res.on('error', reject);
|
||||
res.on('searchEntry', e => oldUser = e.object);
|
||||
res.on('end', resolve);
|
||||
});
|
||||
|
||||
try {
|
||||
let dn = `uid=${userName},${ldapConfig.userDn}`;
|
||||
await client.del(dn);
|
||||
} catch (e) {
|
||||
if (e.name !== 'NoSuchObjectError') throw e;
|
||||
}
|
||||
|
||||
if (!info.hasAccount) {
|
||||
if (oldUser)
|
||||
console.log(` -> '${userName}' removed from LDAP`);
|
||||
return;
|
||||
}
|
||||
|
||||
let nickname = user.nickname || userName;
|
||||
let nameArgs = nickname.trim().split(' ');
|
||||
let sn = nameArgs.length > 1
|
||||
? nameArgs.splice(1).join(' ')
|
||||
: '-';
|
||||
|
||||
let dn = `uid=${userName},${ldapConfig.userDn}`;
|
||||
let newEntry = {
|
||||
uid: userName,
|
||||
objectClass: [
|
||||
'inetOrgPerson',
|
||||
'posixAccount',
|
||||
'sambaSamAccount'
|
||||
],
|
||||
cn: nickname,
|
||||
displayName: nickname,
|
||||
givenName: nameArgs[0],
|
||||
sn,
|
||||
mail: info.corporateMail,
|
||||
preferredLanguage: user.lang,
|
||||
homeDirectory: `${accountConfig.homedir}/${userName}`,
|
||||
loginShell: accountConfig.shell,
|
||||
uidNumber: info.uidNumber,
|
||||
gidNumber: accountConfig.idBase + user.roleFk,
|
||||
sambaSID: '-'
|
||||
};
|
||||
|
||||
if (password) {
|
||||
let salt = crypto
|
||||
.randomBytes(8)
|
||||
.toString('base64');
|
||||
|
||||
let hash = crypto.createHash('sha1');
|
||||
hash.update(password);
|
||||
hash.update(salt, 'binary');
|
||||
let digest = hash.digest('binary');
|
||||
|
||||
let ssha = Buffer
|
||||
.from(digest + salt, 'binary')
|
||||
.toString('base64');
|
||||
|
||||
Object.assign(newEntry, {
|
||||
userPassword: `{SSHA}${ssha}`,
|
||||
sambaNTPassword: nthash(password)
|
||||
});
|
||||
} else if (oldUser) {
|
||||
Object.assign(newEntry, {
|
||||
userPassword: oldUser.userPassword,
|
||||
sambaNTPassword: oldUser.sambaNTPassword
|
||||
});
|
||||
}
|
||||
|
||||
for (let prop in newEntry) {
|
||||
if (newEntry[prop] == null)
|
||||
delete newEntry[prop];
|
||||
}
|
||||
|
||||
await client.add(dn, newEntry);
|
||||
}
|
||||
|
||||
async syncGroups(info, userName) {
|
||||
let {
|
||||
ldapConfig,
|
||||
client
|
||||
} = this;
|
||||
|
||||
let res = await client.search(ldapConfig.groupDn, {
|
||||
scope: 'sub',
|
||||
attributes: ['dn'],
|
||||
filter: `&(memberUid=${userName})(objectClass=posixGroup)`
|
||||
});
|
||||
|
||||
let oldGroups = [];
|
||||
await new Promise((resolve, reject) => {
|
||||
res.on('error', reject);
|
||||
res.on('searchEntry', e => oldGroups.push(e.object));
|
||||
res.on('end', resolve);
|
||||
});
|
||||
|
||||
let reqs = [];
|
||||
for (let oldGroup of oldGroups) {
|
||||
let change = new ldap.Change({
|
||||
operation: 'delete',
|
||||
modification: {memberUid: userName}
|
||||
});
|
||||
reqs.push(client.modify(oldGroup.dn, change));
|
||||
}
|
||||
await Promise.all(reqs);
|
||||
|
||||
if (!info.hasAccount) return;
|
||||
|
||||
reqs = [];
|
||||
for (let role of info.user.roles()) {
|
||||
let change = new ldap.Change({
|
||||
operation: 'add',
|
||||
modification: {memberUid: userName}
|
||||
});
|
||||
let roleName = role.inherits().name;
|
||||
let dn = `cn=${roleName},${ldapConfig.groupDn}`;
|
||||
reqs.push(client.modify(dn, change));
|
||||
}
|
||||
await Promise.all(reqs);
|
||||
}
|
||||
|
||||
async getUsers(usersToSync) {
|
||||
let {
|
||||
ldapConfig,
|
||||
client
|
||||
} = this;
|
||||
|
||||
let res = await client.search(ldapConfig.userDn, {
|
||||
scope: 'sub',
|
||||
attributes: ['uid'],
|
||||
filter: `uid=*`
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
res.on('error', reject);
|
||||
res.on('searchEntry', e => usersToSync.add(e.object.uid));
|
||||
res.on('end', resolve);
|
||||
});
|
||||
}
|
||||
|
||||
async syncRoles() {
|
||||
let {
|
||||
$,
|
||||
ldapConfig,
|
||||
client,
|
||||
accountConfig
|
||||
} = this;
|
||||
|
||||
// Delete roles
|
||||
|
||||
let opts = {
|
||||
scope: 'sub',
|
||||
attributes: ['dn'],
|
||||
filter: 'objectClass=posixGroup'
|
||||
};
|
||||
let res = await client.search(ldapConfig.groupDn, opts);
|
||||
|
||||
let reqs = [];
|
||||
await new Promise((resolve, reject) => {
|
||||
res.on('error', err => {
|
||||
if (err.name === 'NoSuchObjectError')
|
||||
err = new Error(`Object '${ldapConfig.groupDn}' does not exist`);
|
||||
reject(err);
|
||||
});
|
||||
res.on('searchEntry', e => {
|
||||
reqs.push(client.del(e.object.dn));
|
||||
});
|
||||
res.on('end', resolve);
|
||||
});
|
||||
await Promise.all(reqs);
|
||||
|
||||
// Recreate roles
|
||||
|
||||
let roles = await $.Role.find({
|
||||
fields: ['id', 'name', 'description']
|
||||
});
|
||||
let roleRoles = await $.RoleRole.find({
|
||||
fields: ['role', 'inheritsFrom']
|
||||
});
|
||||
let roleMap = toMap(roleRoles, e => {
|
||||
return {key: e.inheritsFrom, val: e.role};
|
||||
});
|
||||
|
||||
let accounts = await $.UserAccount.find({
|
||||
fields: ['id'],
|
||||
include: {
|
||||
relation: 'user',
|
||||
scope: {
|
||||
fields: ['name', 'roleFk'],
|
||||
where: {active: true}
|
||||
}
|
||||
}
|
||||
});
|
||||
let accountMap = toMap(accounts, e => {
|
||||
let user = e.user();
|
||||
if (!user) return;
|
||||
return {key: user.roleFk, val: user.name};
|
||||
});
|
||||
|
||||
reqs = [];
|
||||
for (let role of roles) {
|
||||
let newEntry = {
|
||||
objectClass: ['top', 'posixGroup'],
|
||||
cn: role.name,
|
||||
description: role.description,
|
||||
gidNumber: accountConfig.idBase + role.id
|
||||
};
|
||||
|
||||
let memberUid = [];
|
||||
for (let subrole of roleMap.get(role.id) || [])
|
||||
memberUid = memberUid.concat(accountMap.get(subrole) || []);
|
||||
|
||||
if (memberUid.length) {
|
||||
memberUid.sort((a, b) => a.localeCompare(b));
|
||||
newEntry.memberUid = memberUid;
|
||||
}
|
||||
|
||||
let dn = `cn=${role.name},${ldapConfig.groupDn}`;
|
||||
reqs.push(client.add(dn, newEntry));
|
||||
}
|
||||
await Promise.all(reqs);
|
||||
}
|
||||
}
|
||||
|
||||
SyncConnector.connectors.push(SyncLdap);
|
||||
module.exports = SyncLdap;
|
||||
|
||||
function toMap(array, fn) {
|
||||
let map = new Map();
|
||||
for (let item of array) {
|
||||
let keyVal = fn(item);
|
||||
if (!keyVal) continue;
|
||||
let key = keyVal.key;
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
map.get(key).push(keyVal.val);
|
||||
}
|
||||
return map;
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
|
||||
const SyncConnector = require('./sync-connector');
|
||||
const ssh = require('node-ssh');
|
||||
const ldap = require('./ldapjs-extra');
|
||||
|
||||
class SyncSamba extends SyncConnector {
|
||||
async init() {
|
||||
let sambaConfig = await this.$.SambaConfig.findOne({
|
||||
fields: [
|
||||
'host',
|
||||
'sshUser',
|
||||
'sshPassword',
|
||||
'adUser',
|
||||
'adPassword',
|
||||
'userDn'
|
||||
]
|
||||
});
|
||||
if (!sambaConfig) return false;
|
||||
|
||||
let client = new ssh.NodeSSH();
|
||||
await client.connect({
|
||||
host: sambaConfig.host,
|
||||
username: sambaConfig.sshUser,
|
||||
password: sambaConfig.sshPassword
|
||||
});
|
||||
|
||||
let adClient = ldap.createClient({
|
||||
url: `ldaps://${sambaConfig.host}:636`,
|
||||
tlsOptions: {rejectUnauthorized: false}
|
||||
});
|
||||
await adClient.bind(sambaConfig.adUser, sambaConfig.adPassword);
|
||||
|
||||
Object.assign(this, {
|
||||
sambaConfig,
|
||||
client,
|
||||
adClient
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async deinit() {
|
||||
if (!this.client) return;
|
||||
await this.client.dispose();
|
||||
await this.adClient.unbind();
|
||||
}
|
||||
|
||||
async sync(info, userName, password) {
|
||||
let {client} = this;
|
||||
|
||||
if (info.hasAccount) {
|
||||
try {
|
||||
await client.exec('samba-tool user create', [
|
||||
userName,
|
||||
'--uid-number', `${info.uidNumber}`,
|
||||
'--mail-address', info.corporateMail,
|
||||
'--random-password'
|
||||
]);
|
||||
await client.exec('samba-tool user setexpiry', [
|
||||
userName,
|
||||
'--noexpiry'
|
||||
]);
|
||||
await client.exec('mkhomedir_helper', [
|
||||
userName,
|
||||
'0027'
|
||||
]);
|
||||
} catch (e) {}
|
||||
|
||||
await client.exec('samba-tool user enable', [
|
||||
userName
|
||||
]);
|
||||
|
||||
if (password) {
|
||||
await client.exec('samba-tool user setpassword', [
|
||||
userName,
|
||||
'--newpassword', password
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await client.exec('samba-tool user disable', [
|
||||
userName
|
||||
]);
|
||||
console.log(` -> '${userName}' disabled on Samba`);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets enabled users from Samba.
|
||||
*
|
||||
* Summary of userAccountControl flags:
|
||||
* https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
|
||||
*
|
||||
* @param {Set} usersToSync
|
||||
*/
|
||||
async getUsers(usersToSync) {
|
||||
let {
|
||||
sambaConfig,
|
||||
adClient
|
||||
} = this;
|
||||
|
||||
let opts = {
|
||||
scope: 'sub',
|
||||
attributes: ['sAMAccountName'],
|
||||
filter: '(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))'
|
||||
};
|
||||
let res = await adClient.search(sambaConfig.userDn, opts);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
res.on('error', err => {
|
||||
if (err.name === 'NoSuchObjectError')
|
||||
err = new Error(`Object '${sambaConfig.userDn}' does not exist`);
|
||||
reject(err);
|
||||
});
|
||||
res.on('searchEntry', e => {
|
||||
usersToSync.add(e.object.sAMAccountName);
|
||||
});
|
||||
res.on('end', resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SyncConnector.connectors.push(SyncSamba);
|
||||
module.exports = SyncSamba;
|
|
@ -1,15 +0,0 @@
|
|||
|
||||
const SyncConnector = require('./sync-connector');
|
||||
|
||||
class SyncSip extends SyncConnector {
|
||||
async sync(info, userName, password) {
|
||||
if (!info.hasAccount || !password) return;
|
||||
|
||||
await this.$.Account.rawSql('CALL pbx.sip_setPassword(?, ?)',
|
||||
[info.user.id, password]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SyncConnector.connectors.push(SyncSip);
|
||||
module.exports = SyncSip;
|
|
@ -12,7 +12,7 @@
|
|||
<vn-card class="vn-pa-lg" vn-focus>
|
||||
<vn-vertical>
|
||||
<vn-textfield
|
||||
label="Homedir"
|
||||
label="Homedir base"
|
||||
ng-model="$ctrl.config.homedir"
|
||||
rule="AccountConfig">
|
||||
</vn-textfield>
|
||||
|
@ -22,7 +22,7 @@
|
|||
rule="AccountConfig">
|
||||
</vn-textfield>
|
||||
<vn-input-number
|
||||
label="Id base"
|
||||
label="User and role base id"
|
||||
ng-model="$ctrl.config.idBase"
|
||||
rule="AccountConfig">
|
||||
</vn-input-number>
|
||||
|
@ -58,6 +58,21 @@
|
|||
ng-if="watcher.dataChanged()"
|
||||
ng-click="watcher.loadOriginalData()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
ng-if="!watcher.dataChanged()"
|
||||
label="Synchronize all"
|
||||
ng-click="$ctrl.onSynchronizeAll()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
ng-if="!watcher.dataChanged()"
|
||||
label="Synchronize user"
|
||||
ng-click="syncUser.show()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
ng-if="!watcher.dataChanged()"
|
||||
label="Synchronize roles"
|
||||
ng-click="$ctrl.onSynchronizeRoles()">
|
||||
</vn-button>
|
||||
</vn-button-bar>
|
||||
<vn-submit
|
||||
icon="save"
|
||||
|
@ -66,3 +81,25 @@
|
|||
fixed-bottom-right>
|
||||
</vn-submit>
|
||||
</form>
|
||||
<vn-dialog
|
||||
vn-id="syncUser"
|
||||
on-accept="$ctrl.onUserSync()"
|
||||
on-close="$ctrl.onSyncClose()">
|
||||
<tpl-body>
|
||||
<vn-textfield
|
||||
label="Username"
|
||||
ng-model="$ctrl.syncUser"
|
||||
vn-focus>
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
label="Password"
|
||||
ng-model="$ctrl.syncPassword"
|
||||
type="password"
|
||||
info="If password is not specified, just user attributes are synchronized">
|
||||
</vn-textfield>
|
||||
</tpl-body>
|
||||
<tpl-buttons>
|
||||
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
|
||||
<button response="accept" translate>Synchronize</button>
|
||||
</tpl-buttons>
|
||||
</vn-dialog>
|
|
@ -0,0 +1,38 @@
|
|||
import ngModule from '../module';
|
||||
import Section from 'salix/components/section';
|
||||
import UserError from 'core/lib/user-error';
|
||||
|
||||
export default class Controller extends Section {
|
||||
onSynchronizeAll() {
|
||||
this.vnApp.showSuccess(this.$t('Synchronizing in the background'));
|
||||
this.$http.patch(`UserAccounts/syncAll`)
|
||||
.then(() => this.vnApp.showSuccess(this.$t('Users synchronized!')));
|
||||
}
|
||||
|
||||
onUserSync() {
|
||||
if (!this.syncUser)
|
||||
throw new UserError('Please enter the username');
|
||||
|
||||
let params = {
|
||||
password: this.syncPassword,
|
||||
force: true
|
||||
};
|
||||
return this.$http.patch(`UserAccounts/${this.syncUser}/sync`, params)
|
||||
.then(() => this.vnApp.showSuccess(this.$t('User synchronized!')));
|
||||
}
|
||||
|
||||
onSynchronizeRoles() {
|
||||
this.$http.patch(`RoleInherits/sync`)
|
||||
.then(() => this.vnApp.showSuccess(this.$t('Roles synchronized!')));
|
||||
}
|
||||
|
||||
onSyncClose() {
|
||||
this.syncUser = '';
|
||||
this.syncPassword = '';
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.component('vnAccountAccounts', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller
|
||||
});
|
|
@ -1,16 +1,16 @@
|
|||
Host: Host
|
||||
RDN: RDN
|
||||
Base DN: DN base
|
||||
Password should be base64 encoded: La contraseña debe estar codificada en base64
|
||||
Filter: Filtro
|
||||
Group DN: DN grupos
|
||||
Synchronize now: Sincronizar ahora
|
||||
Accounts: Cuentas
|
||||
Homedir base: Directorio base para carpetas de usuario
|
||||
Shell: Intérprete de línea de comandos
|
||||
User and role base id: Id base usuarios y roles
|
||||
Synchronize all: Sincronizar todo
|
||||
Synchronize user: Sincronizar usuario
|
||||
Synchronize roles: Sincronizar roles
|
||||
If password is not specified, just user attributes are synchronized: >-
|
||||
Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario
|
||||
Synchronizing in the background: Sincronizando en segundo plano
|
||||
LDAP users synchronized: Usuarios LDAP sincronizados
|
||||
Users synchronized!: ¡Usuarios sincronizados!
|
||||
Username: Nombre de usuario
|
||||
Synchronize: Sincronizar
|
||||
Please enter the username: Por favor introduce el nombre de usuario
|
||||
User synchronized: Usuario sincronizado
|
||||
User synchronized!: ¡Usuario sincronizado!
|
||||
Roles synchronized!: ¡Roles sincronizados!
|
|
@ -13,8 +13,5 @@ ngModule.component('vnUserBasicData', {
|
|||
controller: Controller,
|
||||
require: {
|
||||
card: '^vnUserCard'
|
||||
},
|
||||
bindings: {
|
||||
user: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -17,4 +17,4 @@ import './aliases';
|
|||
import './roles';
|
||||
import './ldap';
|
||||
import './samba';
|
||||
import './posix';
|
||||
import './accounts';
|
||||
|
|
|
@ -48,21 +48,16 @@
|
|||
</vn-vertical>
|
||||
</vn-card>
|
||||
<vn-button-bar>
|
||||
<vn-button
|
||||
disabled="watcher.dataChanged()"
|
||||
label="Test connection"
|
||||
ng-click="$ctrl.onTestConection()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
label="Undo changes"
|
||||
ng-if="watcher.dataChanged()"
|
||||
ng-click="watcher.loadOriginalData()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
label="Synchronize now"
|
||||
ng-if="watcher.hasData"
|
||||
ng-click="$ctrl.onSynchronizeAll()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
label="Synchronize user"
|
||||
ng-if="watcher.hasData"
|
||||
ng-click="syncUser.show()">
|
||||
</vn-button>
|
||||
</vn-button-bar>
|
||||
<vn-submit
|
||||
icon="save"
|
||||
|
@ -71,25 +66,3 @@
|
|||
fixed-bottom-right>
|
||||
</vn-submit>
|
||||
</form>
|
||||
<vn-dialog
|
||||
vn-id="syncUser"
|
||||
on-accept="$ctrl.onUserSync()"
|
||||
on-close="$ctrl.onPassClose()">
|
||||
<tpl-body>
|
||||
<vn-textfield
|
||||
label="Username"
|
||||
ng-model="$ctrl.syncUser"
|
||||
vn-focus>
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
label="Password"
|
||||
ng-model="$ctrl.syncPassword"
|
||||
type="password"
|
||||
info="If password is not specified, just user attributes are synchronized">
|
||||
</vn-textfield>
|
||||
</tpl-body>
|
||||
<tpl-buttons>
|
||||
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
|
||||
<button response="accept" translate>Synchronize</button>
|
||||
</tpl-buttons>
|
||||
</vn-dialog>
|
||||
|
|
|
@ -1,26 +1,10 @@
|
|||
import ngModule from '../module';
|
||||
import Section from 'salix/components/section';
|
||||
import UserError from 'core/lib/user-error';
|
||||
|
||||
export default class Controller extends Section {
|
||||
onSynchronizeAll() {
|
||||
this.vnApp.showSuccess(this.$t('Synchronizing in the background'));
|
||||
this.$http.patch(`UserAccounts/syncAll`)
|
||||
.then(() => this.vnApp.showSuccess(this.$t('Users synchronized!')));
|
||||
}
|
||||
|
||||
onUserSync() {
|
||||
if (!this.syncUser)
|
||||
throw new UserError('Please enter the username');
|
||||
|
||||
let params = {password: this.syncPassword};
|
||||
return this.$http.patch(`UserAccounts/${this.syncUser}/sync`, params)
|
||||
.then(() => this.vnApp.showSuccess(this.$t('User synchronized!')));
|
||||
}
|
||||
|
||||
onSyncClose() {
|
||||
this.syncUser = '';
|
||||
this.syncPassword = '';
|
||||
onTestConection() {
|
||||
this.$http.get(`LdapConfigs/test`)
|
||||
.then(() => this.vnApp.showSuccess(this.$t('LDAP connection established!')));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,13 +4,5 @@ RDN: RDN
|
|||
User DN: DN usuarios
|
||||
Filter: Filtro
|
||||
Group DN: DN grupos
|
||||
Synchronize now: Sincronizar ahora
|
||||
Synchronize user: Sincronizar usuario
|
||||
If password is not specified, just user attributes are synchronized: >-
|
||||
Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario
|
||||
Synchronizing in the background: Sincronizando en segundo plano
|
||||
Users synchronized!: ¡Usuarios sincronizados!
|
||||
Username: Nombre de usuario
|
||||
Synchronize: Sincronizar
|
||||
Please enter the username: Por favor introduce el nombre de usuario
|
||||
User synchronized!: ¡Usuario sincronizado!
|
||||
Test connection: Probar conexión
|
||||
LDAP connection established!: ¡Conexión con LDAP establecida!
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import ngModule from '../module';
|
||||
import Section from 'salix/components/section';
|
||||
|
||||
export default class Controller extends Section {}
|
||||
|
||||
ngModule.component('vnAccountPosix', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller
|
||||
});
|
|
@ -9,7 +9,7 @@
|
|||
{"state": "account.index", "icon": "face"},
|
||||
{"state": "account.role", "icon": "group"},
|
||||
{"state": "account.alias", "icon": "email"},
|
||||
{"state": "account.posix", "icon": "accessibility"},
|
||||
{"state": "account.accounts", "icon": "accessibility"},
|
||||
{"state": "account.ldap", "icon": "account_tree"},
|
||||
{"state": "account.samba", "icon": "desktop_windows"},
|
||||
{"state": "account.acl", "icon": "check"},
|
||||
|
@ -67,34 +67,22 @@
|
|||
"url": "/basic-data",
|
||||
"state": "account.card.basicData",
|
||||
"component": "vn-user-basic-data",
|
||||
"description": "Basic data",
|
||||
"params": {
|
||||
"user": "$ctrl.user"
|
||||
}
|
||||
"description": "Basic data"
|
||||
}, {
|
||||
"url": "/roles",
|
||||
"state": "account.card.roles",
|
||||
"component": "vn-user-roles",
|
||||
"description": "Inherited roles",
|
||||
"params": {
|
||||
"user": "$ctrl.user"
|
||||
}
|
||||
"description": "Inherited roles"
|
||||
}, {
|
||||
"url": "/mail-forwarding",
|
||||
"state": "account.card.mailForwarding",
|
||||
"component": "vn-user-mail-forwarding",
|
||||
"description": "Mail forwarding",
|
||||
"params": {
|
||||
"user": "$ctrl.user"
|
||||
}
|
||||
"description": "Mail forwarding"
|
||||
}, {
|
||||
"url": "/aliases",
|
||||
"state": "account.card.aliases",
|
||||
"component": "vn-user-aliases",
|
||||
"description": "Mail aliases",
|
||||
"params": {
|
||||
"user": "$ctrl.user"
|
||||
}
|
||||
"description": "Mail aliases"
|
||||
}, {
|
||||
"url": "/role?q",
|
||||
"state": "account.role",
|
||||
|
@ -178,10 +166,10 @@
|
|||
"component": "vn-alias-users",
|
||||
"description": "Users"
|
||||
}, {
|
||||
"url": "/posix",
|
||||
"state": "account.posix",
|
||||
"component": "vn-account-posix",
|
||||
"description": "Posix",
|
||||
"url": "/accounts",
|
||||
"state": "account.accounts",
|
||||
"component": "vn-account-accounts",
|
||||
"description": "Accounts",
|
||||
"acl": ["developer"]
|
||||
}, {
|
||||
"url": "/ldap",
|
||||
|
|
|
@ -20,19 +20,13 @@
|
|||
ng-if="watcher.hasData"
|
||||
class="vn-mt-md">
|
||||
<vn-textfield
|
||||
label="Host"
|
||||
ng-model="$ctrl.config.host"
|
||||
label="AD domain"
|
||||
ng-model="$ctrl.config.adDomain"
|
||||
rule="SambaConfig">
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
label="SSH user"
|
||||
ng-model="$ctrl.config.sshUser"
|
||||
rule="SambaConfig">
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
label="SSH password"
|
||||
ng-model="$ctrl.config.sshPassword"
|
||||
type="password"
|
||||
label="Domain controller"
|
||||
ng-model="$ctrl.config.adController"
|
||||
rule="SambaConfig">
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
|
@ -46,14 +40,18 @@
|
|||
type="password"
|
||||
rule="SambaConfig">
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
label="User DN"
|
||||
ng-model="$ctrl.config.userDn"
|
||||
rule="SambaConfig">
|
||||
</vn-textfield>
|
||||
<vn-check
|
||||
label="Verify certificate"
|
||||
ng-model="$ctrl.config.verifyCert">
|
||||
</vn-check>
|
||||
</vn-vertical>
|
||||
</vn-card>
|
||||
<vn-button-bar>
|
||||
<vn-button
|
||||
disabled="watcher.dataChanged()"
|
||||
label="Test connection"
|
||||
ng-click="$ctrl.onTestConection()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
label="Undo changes"
|
||||
ng-if="watcher.dataChanged()"
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import ngModule from '../module';
|
||||
import Section from 'salix/components/section';
|
||||
|
||||
export default class Controller extends Section {}
|
||||
export default class Controller extends Section {
|
||||
onTestConection() {
|
||||
this.$http.get(`SambaConfigs/test`)
|
||||
.then(() => this.vnApp.showSuccess(this.$t('Samba connection established!')));
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.component('vnAccountSamba', {
|
||||
template: require('./index.html'),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
Enable synchronization: Habilitar sincronización
|
||||
Host: Host
|
||||
SSH user: Usuario SSH
|
||||
SSH password: Contraseña SSH
|
||||
Domain controller: Controlador de dominio
|
||||
AD domain: Dominio AD
|
||||
AD user: Usuario AD
|
||||
AD password: Contraseña AD
|
||||
User DN: DN usuarios
|
||||
Verify certificate: Verificar certificado
|
||||
Test connection: Probar conexión
|
||||
Samba connection established!: ¡Conexión con Samba establecida!
|
||||
|
|
|
@ -187,7 +187,7 @@
|
|||
<vn-invoice-out-descriptor-popover
|
||||
vn-id="invoiceOutDescriptor">
|
||||
</vn-invoice-out-descriptor-popover>
|
||||
<vn-contextmenu vn-id="contextmenu" model="model"
|
||||
<vn-contextmenu vn-id="contextmenu" targets="['vn-data-viewer']" model="model"
|
||||
expr-builder="$ctrl.exprBuilder(param, value)">
|
||||
<slot-menu>
|
||||
<vn-item translate
|
||||
|
|
|
@ -7175,7 +7175,7 @@
|
|||
},
|
||||
"browserify-rsa": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
|
||||
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -7235,7 +7235,7 @@
|
|||
},
|
||||
"buffer": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
|
||||
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
|
||||
"requires": {
|
||||
"base64-js": "^1.0.2",
|
||||
|
@ -7436,7 +7436,7 @@
|
|||
},
|
||||
"camelcase-keys": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
|
||||
"integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -8710,7 +8710,7 @@
|
|||
},
|
||||
"readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -11226,7 +11226,7 @@
|
|||
},
|
||||
"globby": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "http://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
|
||||
"integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -13346,7 +13346,7 @@
|
|||
},
|
||||
"is-obj": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
|
||||
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8="
|
||||
},
|
||||
"is-path-cwd": {
|
||||
|
@ -18776,7 +18776,7 @@
|
|||
},
|
||||
"load-json-file": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
|
||||
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -19719,7 +19719,7 @@
|
|||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
|
||||
},
|
||||
"mem": {
|
||||
|
@ -19744,7 +19744,7 @@
|
|||
},
|
||||
"meow": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
|
||||
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -20193,7 +20193,7 @@
|
|||
},
|
||||
"multipipe": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "http://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz",
|
||||
"resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz",
|
||||
"integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -21106,7 +21106,7 @@
|
|||
},
|
||||
"os-homedir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
|
||||
"dev": true
|
||||
},
|
||||
|
@ -21122,7 +21122,7 @@
|
|||
},
|
||||
"os-tmpdir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
|
||||
"dev": true
|
||||
},
|
||||
|
@ -23078,7 +23078,7 @@
|
|||
},
|
||||
"sha.js": {
|
||||
"version": "2.4.11",
|
||||
"resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -24763,7 +24763,7 @@
|
|||
},
|
||||
"through": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
|
||||
},
|
||||
"through2": {
|
||||
|
@ -25042,7 +25042,7 @@
|
|||
},
|
||||
"tty-browserify": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "http://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
|
||||
"integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
|
||||
"dev": true
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue