From b3ab1fe0593416d745bfb6a5d4a7fe2ca809cfa0 Mon Sep 17 00:00:00 2001 From: Juan Ferrer Toribio Date: Mon, 2 Nov 2020 19:58:07 +0100 Subject: [PATCH] Sync fixes & improvements --- .../00-role_syncPrivileges.sql | 503 ++++++++++++++++++ db/changes/10240-allSaints/00-sambaConfig.sql | 10 + .../account/back/methods/role-inherit/sync.js | 113 +--- .../back/methods/user-account/sync-all.js | 11 +- .../account/back/methods/user-account/sync.js | 8 +- modules/account/back/models/ldap-config.json | 7 +- modules/account/back/models/samba-config.json | 11 +- modules/account/back/util/sync-connector.js | 5 + modules/account/back/util/sync-engine.js | 38 +- modules/account/back/util/sync-ldap.js | 143 ++++- modules/account/back/util/sync-samba.js | 102 ++-- modules/account/front/ldap/index.html | 22 +- modules/account/front/ldap/index.js | 4 +- modules/account/front/ldap/locale/es.yml | 10 +- modules/account/front/samba/index.html | 33 +- modules/account/front/samba/locale/es.yml | 9 +- 16 files changed, 801 insertions(+), 228 deletions(-) create mode 100644 db/changes/10240-allSaints/00-role_syncPrivileges.sql create mode 100644 db/changes/10240-allSaints/00-sambaConfig.sql diff --git a/db/changes/10240-allSaints/00-role_syncPrivileges.sql b/db/changes/10240-allSaints/00-role_syncPrivileges.sql new file mode 100644 index 000000000..0e3b0d55b --- /dev/null +++ b/db/changes/10240-allSaints/00-role_syncPrivileges.sql @@ -0,0 +1,503 @@ +DROP PROCEDURE IF EXISTS account.role_syncPrivileges; +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`role_syncPrivileges`() +BEGIN +/** + * Synchronizes permissions of MySQL role users based on role hierarchy. + * The computed role users of permission mix will be named according to + * pattern z-[role_name]. + * + * If any@localhost user exists, it will be taken as a template for basic + * attributes. + * + * Warning! This procedure should only be called when MySQL privileges + * are modified. If role hierarchy is modified, you must call the role_sync() + * procedure wich calls this internally. + */ + DECLARE vIsMysql BOOL DEFAULT VERSION() NOT LIKE '%MariaDB%'; + DECLARE vVersion INT DEFAULT SUBSTRING_INDEX(VERSION(), '.', 1); + DECLARE vTplUser VARCHAR(255) DEFAULT 'any'; + DECLARE vTplHost VARCHAR(255) DEFAULT '%'; + DECLARE vRoleHost VARCHAR(255) DEFAULT 'localhost'; + DECLARE vAllHost VARCHAR(255) DEFAULT '%'; + DECLARE vPrefix VARCHAR(2) DEFAULT 'z-'; + DECLARE vPrefixedLike VARCHAR(255); + DECLARE vPassword VARCHAR(255) DEFAULT ''; + + -- Deletes computed role users + + SET vPrefixedLike = CONCAT(vPrefix, '%'); + + IF vIsMysql THEN + DELETE FROM mysql.user + WHERE `User` LIKE vPrefixedLike; + ELSE + DELETE FROM mysql.global_priv + WHERE `User` LIKE vPrefixedLike; + END IF; + + DELETE FROM mysql.db + WHERE `User` LIKE vPrefixedLike; + + DELETE FROM mysql.tables_priv + WHERE `User` LIKE vPrefixedLike; + + DELETE FROM mysql.columns_priv + WHERE `User` LIKE vPrefixedLike; + + DELETE FROM mysql.procs_priv + WHERE `User` LIKE vPrefixedLike; + + DELETE FROM mysql.proxies_priv + WHERE `Proxied_user` LIKE vPrefixedLike; + + -- Temporary tables + + DROP TEMPORARY TABLE IF EXISTS tRole; + CREATE TEMPORARY TABLE tRole + (INDEX (id)) + ENGINE = MEMORY + SELECT + id, + `name` role, + CONCAT(vPrefix, `name`) prefixedRole + FROM role + WHERE hasLogin; + + DROP TEMPORARY TABLE IF EXISTS tRoleInherit; + CREATE TEMPORARY TABLE tRoleInherit + (INDEX (inheritsFrom)) + ENGINE = MEMORY + SELECT + r.prefixedRole, + ri.`name` inheritsFrom + FROM tRole r + JOIN roleRole rr ON rr.role = r.id + JOIN role ri ON ri.id = rr.inheritsFrom; + + -- Recreate role users + + IF vIsMysql THEN + DROP TEMPORARY TABLE IF EXISTS tUser; + CREATE TEMPORARY TABLE tUser + SELECT + r.prefixedRole `User`, + vTplHost `Host`, + IFNULL(t.`authentication_string`, + '') `authentication_string`, + IFNULL(t.`plugin`, + 'mysql_native_password') `plugin`, + IFNULL(IF('' != u.`ssl_type`, + u.`ssl_type`, t.`ssl_type`), + '') `ssl_type`, + IFNULL(IF('' != u.`ssl_cipher`, + u.`ssl_cipher`, t.`ssl_cipher`), + '') `ssl_cipher`, + IFNULL(IF('' != u.`x509_issuer`, + u.`x509_issuer`, t.`x509_issuer`), + '') `x509_issuer`, + IFNULL(IF('' != u.`x509_subject`, + u.`x509_subject`, t.`x509_subject`), + '') `x509_subject`, + IFNULL(IF(0 != u.`max_questions`, + u.`max_questions`, t.`max_questions`), + 0) `max_questions`, + IFNULL(IF(0 != u.`max_updates`, + u.`max_updates`, t.`max_updates`), + 0) `max_updates`, + IFNULL(IF(0 != u.`max_connections`, + u.`max_connections`, t.`max_connections`), + 0) `max_connections`, + IFNULL(IF(0 != u.`max_user_connections`, + u.`max_user_connections`, t.`max_user_connections`), + 0) `max_user_connections` + FROM tRole r + LEFT JOIN mysql.user t + ON t.`User` = vTplUser + AND t.`Host` = vRoleHost + LEFT JOIN mysql.user u + ON u.`User` = r.role + AND u.`Host` = vRoleHost; + + IF vVersion <= 5 THEN + SELECT `Password` INTO vPassword + FROM mysql.user + WHERE `User` = vTplUser + AND `Host` = vRoleHost; + + INSERT INTO mysql.user ( + `User`, + `Host`, + `Password`, + `authentication_string`, + `plugin`, + `ssl_type`, + `ssl_cipher`, + `x509_issuer`, + `x509_subject`, + `max_questions`, + `max_updates`, + `max_connections`, + `max_user_connections` + ) + SELECT + `User`, + `Host`, + vPassword, + `authentication_string`, + `plugin`, + `ssl_type`, + `ssl_cipher`, + `x509_issuer`, + `x509_subject`, + `max_questions`, + `max_updates`, + `max_connections`, + `max_user_connections` + FROM tUser; + ELSE + INSERT INTO mysql.user ( + `User`, + `Host`, + `authentication_string`, + `plugin`, + `ssl_type`, + `ssl_cipher`, + `x509_issuer`, + `x509_subject`, + `max_questions`, + `max_updates`, + `max_connections`, + `max_user_connections` + ) + SELECT + `User`, + `Host`, + `authentication_string`, + `plugin`, + `ssl_type`, + `ssl_cipher`, + `x509_issuer`, + `x509_subject`, + `max_questions`, + `max_updates`, + `max_connections`, + `max_user_connections` + FROM tUser; + END IF; + + DROP TEMPORARY TABLE IF EXISTS tUser; + ELSE + INSERT INTO mysql.global_priv ( + `User`, + `Host`, + `Priv` + ) + SELECT + r.prefixedRole, + vTplHost, + JSON_MERGE_PATCH( + IFNULL(t.`Priv`, '{}'), + IFNULL(u.`Priv`, '{}'), + JSON_OBJECT( + 'mysql_old_password', JSON_VALUE(t.`Priv`, '$.mysql_old_password'), + 'mysql_native_password', JSON_VALUE(t.`Priv`, '$.mysql_native_password'), + 'authentication_string', JSON_VALUE(t.`Priv`, '$.authentication_string') + ) + ) + FROM tRole r + LEFT JOIN mysql.global_priv t + ON t.`User` = vTplUser + AND t.`Host` = vRoleHost + LEFT JOIN mysql.global_priv u + ON u.`User` = r.role + AND u.`Host` = vRoleHost; + END IF; + + INSERT INTO mysql.proxies_priv ( + `User`, + `Host`, + `Proxied_user`, + `Proxied_host`, + `Grantor` + ) + SELECT + '', + vAllHost, + prefixedRole, + vTplHost, + CONCAT(prefixedRole, '@', vTplHost) + FROM tRole; + + -- Copies global privileges + + DROP TEMPORARY TABLE IF EXISTS tUserPriv; + + IF vIsMysql THEN + CREATE TEMPORARY TABLE tUserPriv + (INDEX (prefixedRole)) + ENGINE = MEMORY + SELECT + r.prefixedRole, + MAX(u.`Select_priv`) `Select_priv`, + MAX(u.`Insert_priv`) `Insert_priv`, + MAX(u.`Update_priv`) `Update_priv`, + MAX(u.`Delete_priv`) `Delete_priv`, + MAX(u.`Create_priv`) `Create_priv`, + MAX(u.`Drop_priv`) `Drop_priv`, + MAX(u.`Reload_priv`) `Reload_priv`, + MAX(u.`Shutdown_priv`) `Shutdown_priv`, + MAX(u.`Process_priv`) `Process_priv`, + MAX(u.`File_priv`) `File_priv`, + MAX(u.`Grant_priv`) `Grant_priv`, + MAX(u.`References_priv`) `References_priv`, + MAX(u.`Index_priv`) `Index_priv`, + MAX(u.`Alter_priv`) `Alter_priv`, + MAX(u.`Show_db_priv`) `Show_db_priv`, + MAX(u.`Super_priv`) `Super_priv`, + MAX(u.`Create_tmp_table_priv`) `Create_tmp_table_priv`, + MAX(u.`Lock_tables_priv`) `Lock_tables_priv`, + MAX(u.`Execute_priv`) `Execute_priv`, + MAX(u.`Repl_slave_priv`) `Repl_slave_priv`, + MAX(u.`Repl_client_priv`) `Repl_client_priv`, + MAX(u.`Create_view_priv`) `Create_view_priv`, + MAX(u.`Show_view_priv`) `Show_view_priv`, + MAX(u.`Create_routine_priv`) `Create_routine_priv`, + MAX(u.`Alter_routine_priv`) `Alter_routine_priv`, + MAX(u.`Create_user_priv`) `Create_user_priv`, + MAX(u.`Event_priv`) `Event_priv`, + MAX(u.`Trigger_priv`) `Trigger_priv`, + MAX(u.`Create_tablespace_priv`) `Create_tablespace_priv` + FROM tRoleInherit r + JOIN mysql.user u + ON u.`User` = r.inheritsFrom + AND u.`Host`= vRoleHost + GROUP BY r.prefixedRole; + + UPDATE mysql.user u + JOIN tUserPriv t + ON u.`User` = t.prefixedRole + AND u.`Host` = vTplHost + SET + u.`Select_priv` + = t.`Select_priv`, + u.`Insert_priv` + = t.`Insert_priv`, + u.`Update_priv` + = t.`Update_priv`, + u.`Delete_priv` + = t.`Delete_priv`, + u.`Create_priv` + = t.`Create_priv`, + u.`Drop_priv` + = t.`Drop_priv`, + u.`Reload_priv` + = t.`Reload_priv`, + u.`Shutdown_priv` + = t.`Shutdown_priv`, + u.`Process_priv` + = t.`Process_priv`, + u.`File_priv` + = t.`File_priv`, + u.`Grant_priv` + = t.`Grant_priv`, + u.`References_priv` + = t.`References_priv`, + u.`Index_priv` + = t.`Index_priv`, + u.`Alter_priv` + = t.`Alter_priv`, + u.`Show_db_priv` + = t.`Show_db_priv`, + u.`Super_priv` + = t.`Super_priv`, + u.`Create_tmp_table_priv` + = t.`Create_tmp_table_priv`, + u.`Lock_tables_priv` + = t.`Lock_tables_priv`, + u.`Execute_priv` + = t.`Execute_priv`, + u.`Repl_slave_priv` + = t.`Repl_slave_priv`, + u.`Repl_client_priv` + = t.`Repl_client_priv`, + u.`Create_view_priv` + = t.`Create_view_priv`, + u.`Show_view_priv` + = t.`Show_view_priv`, + u.`Create_routine_priv` + = t.`Create_routine_priv`, + u.`Alter_routine_priv` + = t.`Alter_routine_priv`, + u.`Create_user_priv` + = t.`Create_user_priv`, + u.`Event_priv` + = t.`Event_priv`, + u.`Trigger_priv` + = t.`Trigger_priv`, + u.`Create_tablespace_priv` + = t.`Create_tablespace_priv`; + ELSE + CREATE TEMPORARY TABLE tUserPriv + (INDEX (prefixedRole)) + SELECT + r.prefixedRole, + BIT_OR(JSON_VALUE(p.`Priv`, '$.access')) access + FROM tRoleInherit r + JOIN mysql.global_priv p + ON p.`User` = r.inheritsFrom + AND p.`Host`= vRoleHost + GROUP BY r.prefixedRole; + + UPDATE mysql.global_priv p + JOIN tUserPriv t + ON p.`User` = t.prefixedRole + AND p.`Host` = vTplHost + SET + p.`Priv` = JSON_SET(p.`Priv`, '$.access', t.access); + END IF; + + DROP TEMPORARY TABLE tUserPriv; + + -- Copy schema level privileges + + INSERT INTO mysql.db ( + `User`, + `Host`, + `Db`, + `Select_priv`, + `Insert_priv`, + `Update_priv`, + `Delete_priv`, + `Create_priv`, + `Drop_priv`, + `Grant_priv`, + `References_priv`, + `Index_priv`, + `Alter_priv`, + `Create_tmp_table_priv`, + `Lock_tables_priv`, + `Create_view_priv`, + `Show_view_priv`, + `Create_routine_priv`, + `Alter_routine_priv`, + `Execute_priv`, + `Event_priv`, + `Trigger_priv` + ) + SELECT + r.prefixedRole, + vTplHost, + t.`Db`, + MAX(t.`Select_priv`), + MAX(t.`Insert_priv`), + MAX(t.`Update_priv`), + MAX(t.`Delete_priv`), + MAX(t.`Create_priv`), + MAX(t.`Drop_priv`), + MAX(t.`Grant_priv`), + MAX(t.`References_priv`), + MAX(t.`Index_priv`), + MAX(t.`Alter_priv`), + MAX(t.`Create_tmp_table_priv`), + MAX(t.`Lock_tables_priv`), + MAX(t.`Create_view_priv`), + MAX(t.`Show_view_priv`), + MAX(t.`Create_routine_priv`), + MAX(t.`Alter_routine_priv`), + MAX(t.`Execute_priv`), + MAX(t.`Event_priv`), + MAX(t.`Trigger_priv`) + FROM tRoleInherit r + JOIN mysql.db t + ON t.`User` = r.inheritsFrom + AND t.`Host`= vRoleHost + GROUP BY r.prefixedRole, t.`Db`; + + -- Copy table level privileges + + INSERT INTO mysql.tables_priv ( + `User`, + `Host`, + `Db`, + `Table_name`, + `Grantor`, + `Timestamp`, + `Table_priv`, + `Column_priv` + ) + SELECT + r.prefixedRole, + vTplHost, + t.`Db`, + t.`Table_name`, + t.`Grantor`, + MAX(t.`Timestamp`), + IFNULL(GROUP_CONCAT(NULLIF(t.`Table_priv`, '')), ''), + IFNULL(GROUP_CONCAT(NULLIF(t.`Column_priv`, '')), '') + FROM tRoleInherit r + JOIN mysql.tables_priv t + ON t.`User` = r.inheritsFrom + AND t.`Host`= vRoleHost + GROUP BY r.prefixedRole, t.`Db`, t.`Table_name`; + + -- Copy column level privileges + + INSERT INTO mysql.columns_priv ( + `User`, + `Host`, + `Db`, + `Table_name`, + `Column_name`, + `Timestamp`, + `Column_priv` + ) + SELECT + r.prefixedRole, + vTplHost, + t.`Db`, + t.`Table_name`, + t.`Column_name`, + MAX(t.`Timestamp`), + IFNULL(GROUP_CONCAT(NULLIF(t.`Column_priv`, '')), '') + FROM tRoleInherit r + JOIN mysql.columns_priv t + ON t.`User` = r.inheritsFrom + AND t.`Host`= vRoleHost + GROUP BY r.prefixedRole, t.`Db`, t.`Table_name`, t.`Column_name`; + + -- Copy routine privileges + + INSERT IGNORE INTO mysql.procs_priv ( + `User`, + `Host`, + `Db`, + `Routine_name`, + `Routine_type`, + `Grantor`, + `Timestamp`, + `Proc_priv` + ) + SELECT + r.prefixedRole, + vTplHost, + t.`Db`, + t.`Routine_name`, + t.`Routine_type`, + t.`Grantor`, + t.`Timestamp`, + t.`Proc_priv` + FROM tRoleInherit r + JOIN mysql.procs_priv t + ON t.`User` = r.inheritsFrom + AND t.`Host`= vRoleHost; + + -- Free memory + + DROP TEMPORARY TABLE + tRole, + tRoleInherit; + + FLUSH PRIVILEGES; +END$$ +DELIMITER ; diff --git a/db/changes/10240-allSaints/00-sambaConfig.sql b/db/changes/10240-allSaints/00-sambaConfig.sql new file mode 100644 index 000000000..e92d62cff --- /dev/null +++ b/db/changes/10240-allSaints/00-sambaConfig.sql @@ -0,0 +1,10 @@ +ALTER TABLE account.sambaConfig ADD adUser VARCHAR(255) DEFAULT NULL NULL COMMENT 'Active directory user'; +ALTER TABLE account.sambaConfig ADD adPassword varchar(255) DEFAULT NULL NULL COMMENT 'Active directory password'; +ALTER TABLE account.sambaConfig ADD userDn varchar(255) DEFAULT NULL NULL COMMENT 'The base DN for users'; +ALTER TABLE account.sambaConfig DROP COLUMN uidBase; +ALTER TABLE account.sambaConfig CHANGE sshPass sshPassword varchar(255) DEFAULT NULL NULL COMMENT 'The SSH password'; + +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'; diff --git a/modules/account/back/methods/role-inherit/sync.js b/modules/account/back/methods/role-inherit/sync.js index 44a9a7932..b84e4c2fe 100644 --- a/modules/account/back/methods/role-inherit/sync.js +++ b/modules/account/back/methods/role-inherit/sync.js @@ -1,4 +1,5 @@ -const ldap = require('../../util/ldapjs-extra'); + +const SyncEngine = require('../../util/sync-engine'); module.exports = Self => { Self.remoteMethod('sync', { @@ -10,119 +11,17 @@ module.exports = Self => { }); Self.sync = async function() { - let $ = Self.app.models; - - let ldapConfig = await $.LdapConfig.findOne({ - fields: ['host', 'rdn', 'password', 'groupDn'] - }); - let accountConfig = await $.AccountConfig.findOne({ - fields: ['idBase'] - }); - - if (!ldapConfig) return; - - // Connect - - let client = ldap.createClient({ - url: `ldap://${ldapConfig.host}:389` - }); - - let ldapPassword = Buffer - .from(ldapConfig.password, 'base64') - .toString('ascii'); - await client.bind(ldapConfig.rdn, ldapPassword); + let engine = new SyncEngine(); + await engine.init(Self.app.models); let err; try { - // 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'] - }); - 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, - gidNumber: accountConfig.idBase + role.id - }; - - let memberUid = []; - for (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); + await engine.syncRoles(); } catch (e) { err = e; } - await client.unbind(); + await engine.deinit(); if (err) throw err; }; }; - -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; -} diff --git a/modules/account/back/methods/user-account/sync-all.js b/modules/account/back/methods/user-account/sync-all.js index 3c9d0c778..737099ce8 100644 --- a/modules/account/back/methods/user-account/sync-all.js +++ b/modules/account/back/methods/user-account/sync-all.js @@ -13,25 +13,24 @@ module.exports = Self => { Self.syncAll = async function() { let $ = Self.app.models; - let se = new SyncEngine(); - await se.init($); + let engine = new SyncEngine(); + await engine.init($); - let usersToSync = await se.getUsers(); + 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 se.sync(user); + await engine.sync(user); console.log(` -> '${user}' sinchronized`); } catch (err) { console.error(` -> '${user}' synchronization error:`, err.message); } } - await se.deinit(); - + await engine.deinit(); await $.RoleInherit.sync(); }; }; diff --git a/modules/account/back/methods/user-account/sync.js b/modules/account/back/methods/user-account/sync.js index eff316c92..f16b2c052 100644 --- a/modules/account/back/methods/user-account/sync.js +++ b/modules/account/back/methods/user-account/sync.js @@ -34,17 +34,17 @@ module.exports = Self => { if (user && isSync) return; let err; - let se = new SyncEngine(); - await se.init($); + let engine = new SyncEngine(); + await engine.init($); try { - await se.sync(userName, password, true); + await engine.sync(userName, password, true); await $.UserSync.destroyById(userName); } catch (e) { err = e; } - await se.deinit(); + await engine.deinit(); if (err) throw err; }; }; diff --git a/modules/account/back/models/ldap-config.json b/modules/account/back/models/ldap-config.json index e3061d651..f7d3ab08b 100644 --- a/modules/account/back/models/ldap-config.json +++ b/modules/account/back/models/ldap-config.json @@ -11,7 +11,7 @@ "type": "number", "id": true }, - "host": { + "server": { "type": "string", "required": true }, @@ -23,10 +23,7 @@ "type": "string", "required": true }, - "baseDn": { - "type": "string" - }, - "filter": { + "userDn": { "type": "string" }, "groupDn": { diff --git a/modules/account/back/models/samba-config.json b/modules/account/back/models/samba-config.json index ffbcce4eb..d729ca111 100644 --- a/modules/account/back/models/samba-config.json +++ b/modules/account/back/models/samba-config.json @@ -18,7 +18,16 @@ "sshUser": { "type": "string" }, - "sshPass": { + "sshPassword": { + "type": "string" + }, + "adUser": { + "type": "string" + }, + "adPassword": { + "type": "string" + }, + "userDn": { "type": "string" } } diff --git a/modules/account/back/util/sync-connector.js b/modules/account/back/util/sync-connector.js index 4b8c9ed6a..db7d230be 100644 --- a/modules/account/back/util/sync-connector.js +++ b/modules/account/back/util/sync-connector.js @@ -37,6 +37,11 @@ class SyncConnector { */ async syncGroups(user, userName) {} + /** + * Synchronizes roles. + */ + async syncRoles() {} + /** * Deinitalizes the connector. */ diff --git a/modules/account/back/util/sync-engine.js b/modules/account/back/util/sync-engine.js index 5ca9cac65..6fd256c98 100644 --- a/modules/account/back/util/sync-engine.js +++ b/modules/account/back/util/sync-engine.js @@ -19,8 +19,10 @@ module.exports = class SyncEngine { for (let ConnectorClass of SyncConnector.connectors) { let connector = new ConnectorClass(); Object.assign(connector, { - se: this, - $ + engine: this, + $, + accountConfig, + mailConfig }); if (!await connector.init()) continue; connectors.push(connector); @@ -80,27 +82,20 @@ module.exports = class SyncEngine { } }); - let extraParams; - let hasAccount = false; - - if (user) { - hasAccount = user.active - && await $.UserAccount.exists(user.id); - - extraParams = { - corporateMail: `${userName}@${mailConfig.domain}`, - uidNumber: accountConfig.idBase + user.id - }; - } - let info = { user, - extraParams, - hasAccount, - accountConfig, - mailConfig + 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) { @@ -116,6 +111,11 @@ module.exports = class SyncEngine { if (errs.length) throw errs[0]; } + async syncRoles() { + for (let connector of this.connectors) + await connector.syncRoles(); + } + async getUsers() { let usersToSync = new Set(); diff --git a/modules/account/back/util/sync-ldap.js b/modules/account/back/util/sync-ldap.js index d72520ed6..3f98633d9 100644 --- a/modules/account/back/util/sync-ldap.js +++ b/modules/account/back/util/sync-ldap.js @@ -7,18 +7,20 @@ const crypto = require('crypto'); class SyncLdap extends SyncConnector { async init() { let ldapConfig = await this.$.LdapConfig.findOne({ - fields: ['host', 'rdn', 'password', 'baseDn', 'groupDn'] + fields: [ + 'server', + 'rdn', + 'password', + 'userDn', + 'groupDn' + ] }); if (!ldapConfig) return false; let client = ldap.createClient({ - url: `ldap://${ldapConfig.host}:389` + url: ldapConfig.server }); - - let ldapPassword = Buffer - .from(ldapConfig.password, 'base64') - .toString('ascii'); - await client.bind(ldapConfig.rdn, ldapPassword); + await client.bind(ldapConfig.rdn, ldapConfig.password); Object.assign(this, { ldapConfig, @@ -36,16 +38,12 @@ class SyncLdap extends SyncConnector { let { ldapConfig, client, + accountConfig } = this; - let { - user, - hasAccount, - extraParams, - accountConfig - } = info; + let {user} = info; - let res = await client.search(ldapConfig.baseDn, { + let res = await client.search(ldapConfig.userDn, { scope: 'sub', attributes: ['userPassword', 'sambaNTPassword'], filter: `&(uid=${userName})` @@ -59,13 +57,13 @@ class SyncLdap extends SyncConnector { }); try { - let dn = `uid=${userName},${ldapConfig.baseDn}`; + let dn = `uid=${userName},${ldapConfig.userDn}`; await client.del(dn); } catch (e) { if (e.name !== 'NoSuchObjectError') throw e; } - if (!hasAccount) { + if (!info.hasAccount) { if (oldUser) console.log(` -> '${userName}' removed from LDAP`); return; @@ -77,7 +75,7 @@ class SyncLdap extends SyncConnector { ? nameArgs.splice(1).join(' ') : '-'; - let dn = `uid=${userName},${ldapConfig.baseDn}`; + let dn = `uid=${userName},${ldapConfig.userDn}`; let newEntry = { uid: userName, objectClass: [ @@ -89,11 +87,11 @@ class SyncLdap extends SyncConnector { displayName: nickname, givenName: nameArgs[0], sn, - mail: extraParams.corporateMail, + mail: info.corporateMail, preferredLanguage: user.lang, homeDirectory: `${accountConfig.homedir}/${userName}`, loginShell: accountConfig.shell, - uidNumber: extraParams.uidNumber, + uidNumber: info.uidNumber, gidNumber: accountConfig.idBase + user.roleFk, sambaSID: '-' }; @@ -137,11 +135,6 @@ class SyncLdap extends SyncConnector { client } = this; - let { - user, - hasAccount - } = info; - let res = await client.search(ldapConfig.groupDn, { scope: 'sub', attributes: ['dn'], @@ -165,10 +158,10 @@ class SyncLdap extends SyncConnector { } await Promise.all(reqs); - if (!hasAccount) return; + if (!info.hasAccount) return; reqs = []; - for (let role of user.roles()) { + for (let role of info.user.roles()) { let change = new ldap.Change({ operation: 'add', modification: {memberUid: userName} @@ -186,7 +179,7 @@ class SyncLdap extends SyncConnector { client } = this; - let res = await client.search(ldapConfig.baseDn, { + let res = await client.search(ldapConfig.userDn, { scope: 'sub', attributes: ['uid'], filter: `uid=*` @@ -198,7 +191,103 @@ class SyncLdap extends SyncConnector { 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'] + }); + 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}; + }); + + console.log; + + reqs = []; + for (let role of roles) { + let newEntry = { + objectClass: ['top', 'posixGroup'], + cn: role.name, + 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; +} diff --git a/modules/account/back/util/sync-samba.js b/modules/account/back/util/sync-samba.js index c6ef556e5..6e5ef9d5a 100644 --- a/modules/account/back/util/sync-samba.js +++ b/modules/account/back/util/sync-samba.js @@ -1,60 +1,71 @@ 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', 'sshPass'] + fields: [ + 'host', + 'sshUser', + 'sshPassword', + 'adUser', + 'adPassword', + 'userDn' + ] }); if (!sambaConfig) return false; - let sshPassword = Buffer - .from(sambaConfig.sshPass, 'base64') - .toString('ascii'); - let client = new ssh.NodeSSH(); await client.connect({ host: sambaConfig.host, username: sambaConfig.sshUser, - password: sshPassword + password: sambaConfig.sshPassword + }); + + let adClient = ldap.createClient({ + url: `ldaps://${sambaConfig.host}:636`, + tlsOptions: {rejectUnauthorized: false} }); Object.assign(this, { sambaConfig, - client + client, + adClient }); return true; } async deinit() { - if (this.client) - await this.client.dispose(); + if (!this.client) return; + await this.client.dispose(); + await this.adClient.unbind(); } async sync(info, userName, password) { - let { - client - } = this; + let {client} = this; - let { - hasAccount, - extraParams - } = info; - - if (hasAccount) { + if (info.hasAccount) { try { await client.exec('samba-tool user create', [ userName, - '--uid-number', `${extraParams.uidNumber}`, - '--mail-address', extraParams.corporateMail, + '--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 setexpiry', [ - userName, - '--noexpiry' + await client.exec('samba-tool user enable', [ + userName ]); if (password) { @@ -62,15 +73,7 @@ class SyncSamba extends SyncConnector { userName, '--newpassword', password ]); - await client.exec('samba-tool user enable', [ - userName - ]); } - - await client.exec('mkhomedir_helper', [ - userName, - '0027' - ]); } else { try { await client.exec('samba-tool user disable', [ @@ -81,11 +84,40 @@ class SyncSamba extends SyncConnector { } } + /** + * 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 {client} = this; - let res = await client.execCommand('samba-tool user list'); - let users = res.stdout.split('\n'); - for (let user of users) usersToSync.add(user.trim()); + let { + sambaConfig, + adClient + } = this; + + await adClient.bind(sambaConfig.adUser, sambaConfig.adPassword); + + 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); + }); } } diff --git a/modules/account/front/ldap/index.html b/modules/account/front/ldap/index.html index 5c9ec7625..2ba1ceda6 100644 --- a/modules/account/front/ldap/index.html +++ b/modules/account/front/ldap/index.html @@ -11,9 +11,17 @@ class="vn-w-md"> + + + + @@ -25,18 +33,12 @@ - - this.vnApp.showSuccess(this.$t('LDAP users synchronized'))); + .then(() => this.vnApp.showSuccess(this.$t('Users synchronized!'))); } onUserSync() { @@ -15,7 +15,7 @@ export default class Controller extends Section { let params = {password: this.syncPassword}; return this.$http.patch(`UserAccounts/${this.syncUser}/sync`, params) - .then(() => this.vnApp.showSuccess(this.$t('User synchronized'))); + .then(() => this.vnApp.showSuccess(this.$t('User synchronized!'))); } onSyncClose() { diff --git a/modules/account/front/ldap/locale/es.yml b/modules/account/front/ldap/locale/es.yml index e7481699f..56fe623e8 100644 --- a/modules/account/front/ldap/locale/es.yml +++ b/modules/account/front/ldap/locale/es.yml @@ -1,7 +1,7 @@ -Host: Host +Enable synchronization: Habilitar sincronización +Server: Servidor RDN: RDN -Base DN: DN base -Password should be base64 encoded: La contraseña debe estar codificada en base64 +User DN: DN usuarios Filter: Filtro Group DN: DN grupos Synchronize now: Sincronizar ahora @@ -9,8 +9,8 @@ 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 -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! diff --git a/modules/account/front/samba/index.html b/modules/account/front/samba/index.html index 57461cb14..9b66cdc2a 100644 --- a/modules/account/front/samba/index.html +++ b/modules/account/front/samba/index.html @@ -11,24 +11,47 @@ class="vn-w-md"> + + + + + + + + + + diff --git a/modules/account/front/samba/locale/es.yml b/modules/account/front/samba/locale/es.yml index 7cfc4c744..a036bb3cc 100644 --- a/modules/account/front/samba/locale/es.yml +++ b/modules/account/front/samba/locale/es.yml @@ -1,2 +1,7 @@ -SSH host: Host SSH -Password should be base64 encoded: La contraseña debe estar codificada en base64 +Enable synchronization: Habilitar sincronización +Host: Host +SSH user: Usuario SSH +SSH password: Contraseña SSH +AD user: Usuario AD +AD password: Contraseña AD +User DN: DN usuarios