From 4f5242e3ae30d0e9b2ef35d7368ce0aab1052595 Mon Sep 17 00:00:00 2001 From: Juan Ferrer Toribio Date: Thu, 16 Nov 2023 23:07:26 +0100 Subject: [PATCH] fix: refs #6432 account sync fixes --- Dockerfile | 10 +- db/changes/234604/00-sambaConfigUserDn.sql | 2 + modules/account/back/methods/account/sync.js | 8 +- modules/account/back/models/ldap-config.js | 3 +- modules/account/back/models/role-config.js | 20 ++- modules/account/back/models/samba-config.js | 149 +++++++++++------- modules/account/back/models/samba-config.json | 4 + modules/account/front/accounts/index.html | 40 +---- modules/account/front/accounts/index.js | 18 --- modules/account/front/accounts/locale/es.yml | 2 - modules/account/front/descriptor/index.html | 35 ++++ modules/account/front/descriptor/index.js | 14 ++ .../account/front/descriptor/locale/es.yml | 4 + 13 files changed, 186 insertions(+), 123 deletions(-) create mode 100644 db/changes/234604/00-sambaConfigUserDn.sql diff --git a/Dockerfile b/Dockerfile index e1173ad73..814a5e1d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,15 +17,19 @@ RUN apt-get update \ # Puppeteer -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ +RUN apt-get install -y --no-install-recommends \ libfontconfig lftp xvfb gconf-service libasound2 libatk1.0-0 libc6 \ libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 \ libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 \ libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \ libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \ libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \ - fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \ + fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget + +# Extra dependencies + +RUN apt-get install -y --no-install-recommends \ + samba-common-bin \ && rm -rf /var/lib/apt/lists/* \ && npm -g install pm2 diff --git a/db/changes/234604/00-sambaConfigUserDn.sql b/db/changes/234604/00-sambaConfigUserDn.sql new file mode 100644 index 000000000..cacb30e97 --- /dev/null +++ b/db/changes/234604/00-sambaConfigUserDn.sql @@ -0,0 +1,2 @@ +ALTER TABLE account.sambaConfig + ADD userDn varchar(255) NOT NULL COMMENT 'Base DN for users without domain DN part'; diff --git a/modules/account/back/methods/account/sync.js b/modules/account/back/methods/account/sync.js index a5befc22c..0eab0ef63 100644 --- a/modules/account/back/methods/account/sync.js +++ b/modules/account/back/methods/account/sync.js @@ -1,3 +1,4 @@ +const ForbiddenError = require('vn-loopback/util/forbiddenError'); module.exports = Self => { Self.remoteMethod('sync', { @@ -32,9 +33,13 @@ module.exports = Self => { const models = Self.app.models; const user = await models.VnUser.findOne({ - fields: ['id'], + fields: ['id', 'password'], where: {name: userName} }, myOptions); + + if (user && password && !await user.hasPassword(password)) + throw new ForbiddenError('Wrong password'); + const isSync = !await models.UserSync.exists(userName, myOptions); if (!force && isSync && user) return; @@ -42,4 +47,3 @@ module.exports = Self => { await models.UserSync.destroyById(userName, myOptions); }; }; - diff --git a/modules/account/back/models/ldap-config.js b/modules/account/back/models/ldap-config.js index f9ae7562b..9dcc4136d 100644 --- a/modules/account/back/models/ldap-config.js +++ b/modules/account/back/models/ldap-config.js @@ -5,7 +5,7 @@ const crypto = require('crypto'); const nthash = require('smbhash').nthash; module.exports = Self => { - const shouldSync = process.env.NODE_ENV === 'production'; + const shouldSync = process.env.NODE_ENV !== 'test'; Self.getSynchronizer = async function() { return await Self.findOne({ @@ -140,6 +140,7 @@ module.exports = Self => { try { if (shouldSync) await client.del(dn); + // eslint-disable-next-line no-console console.log(` -> User '${userName}' removed from LDAP`); } catch (e) { if (e.name !== 'NoSuchObjectError') throw e; diff --git a/modules/account/back/models/role-config.js b/modules/account/back/models/role-config.js index ba7bf9d52..b90ef75fb 100644 --- a/modules/account/back/models/role-config.js +++ b/modules/account/back/models/role-config.js @@ -27,8 +27,7 @@ module.exports = Self => { const [row] = await Self.rawSql( `SELECT COUNT(*) AS nRows FROM mysql.user - WHERE User = ? - AND Host = ?`, + WHERE User = ? AND Host = ?`, [mysqlUser, this.userHost] ); let userExists = row.nRows > 0; @@ -38,8 +37,7 @@ module.exports = Self => { const [row] = await Self.rawSql( `SELECT Priv AS priv FROM mysql.global_priv - WHERE User = ? - AND Host = ?`, + WHERE User = ? AND Host = ?`, [mysqlUser, this.userHost] ); const priv = row && JSON.parse(row.priv); @@ -88,10 +86,18 @@ module.exports = Self => { else throw err; } - await Self.rawSql('GRANT ? TO ?@?', - [role, mysqlUser, this.userHost]); - if (role) { + const [row] = await Self.rawSql( + `SELECT COUNT(*) AS nRows + FROM mysql.user + WHERE User = ? AND Host = ''`, + [role] + ); + const roleExists = row.nRows > 0; + + if (roleExists) { + await Self.rawSql('GRANT ? TO ?@?', + [role, mysqlUser, this.userHost]); await Self.rawSql('SET DEFAULT ROLE ? FOR ?@?', [role, mysqlUser, this.userHost]); } else { diff --git a/modules/account/back/models/samba-config.js b/modules/account/back/models/samba-config.js index 168b5ffb4..1abac489c 100644 --- a/modules/account/back/models/samba-config.js +++ b/modules/account/back/models/samba-config.js @@ -1,6 +1,6 @@ const ldap = require('../util/ldapjs-extra'); -const ssh = require('node-ssh'); +const execFile = require('child_process').execFile; /** * Summary of userAccountControl flags: @@ -11,6 +11,8 @@ const UserAccountControlFlags = { }; module.exports = Self => { + const shouldSync = process.env.NODE_ENV !== 'test'; + Self.getSynchronizer = async function() { return await Self.findOne({ fields: [ @@ -19,6 +21,7 @@ module.exports = Self => { 'adController', 'adUser', 'adPassword', + 'userDn', 'verifyCert' ] }); @@ -26,88 +29,119 @@ module.exports = Self => { Object.assign(Self.prototype, { async init() { - let sshClient = new ssh.NodeSSH(); - await sshClient.connect({ - host: this.adController, - username: this.adUser, - password: this.adPassword - }); + const baseDn = this.adDomain + .split('.') + .map(part => `dc=${part}`) + .join(','); + const ldapUser = `cn=${this.adUser},cn=Users,${baseDn}`; - let adUser = `cn=${this.adUser},${this.usersDn()}`; - - let adClient = ldap.createClient({ + const adClient = ldap.createClient({ url: `ldaps://${this.adController}:636`, tlsOptions: {rejectUnauthorized: this.verifyCert} }); - await adClient.bind(adUser, this.adPassword); - + await adClient.bind(ldapUser, this.adPassword); Object.assign(this, { - sshClient, - adClient + adClient, + fullUsersDn: `${this.userDn},${baseDn}` }); }, 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 sambaTool(command, args = []) { + const authArgs = [ + '--URL', `ldap://${this.adController}`, + '--username', this.adUser, + '--password', this.adPassword + ]; + const allArgs = [command].concat( + args, authArgs + ); + + if (!shouldSync) return; + return await new Promise((resolve, reject) => { + execFile('samba-tool', allArgs, (err, stdout, stderr) => { + if (err) + reject(err); + else + resolve({stdout, stderr}); + }); + }); }, - async syncUser(userName, info, password) { - let {sshClient} = this; - - let sambaUser = await this.adClient.searchOne(this.usersDn(), { + async getAdUser(userName) { + const sambaUser = await this.adClient.searchOne(this.fullUsersDn, { scope: 'sub', - attributes: ['userAccountControl'], + attributes: [ + 'dn', + 'userAccountControl', + 'uidNumber', + 'accountExpires', + 'mail' + ], filter: `(&(objectClass=user)(sAMAccountName=${userName}))` }); - let isEnabled = sambaUser - && !(sambaUser.userAccountControl & UserAccountControlFlags.ACCOUNTDISABLE); - - if (process.env.NODE_ENV === 'test') - return; + if (sambaUser) { + for (const intProp of ['uidNumber', 'userAccountControl']) { + if (sambaUser[intProp] != null) + sambaUser[intProp] = parseInt(sambaUser[intProp]); + } + } + return sambaUser; + }, + + async syncUser(userName, info, password) { + let sambaUser = await this.getAdUser(userName); + let entry; if (info.hasAccount) { if (!sambaUser) { - await sshClient.exec('samba-tool user create', [ - userName, - '--uid-number', `${info.uidNumber}`, - '--mail-address', info.corporateMail, + await this.sambaTool('user', [ + 'create', userName, + '--userou', this.userDn, '--random-password' ]); - await sshClient.exec('samba-tool user setexpiry', [ - userName, - '--noexpiry' - ]); - await sshClient.exec('mkhomedir_helper', [ - userName, - '0027' - ]); - } - if (!isEnabled) { - await sshClient.exec('samba-tool user enable', [ - userName - ]); + sambaUser = await this.getAdUser(userName); } if (password) { - await sshClient.exec('samba-tool user setpassword', [ - userName, + await this.sambaTool('user', [ + 'setpassword', userName, '--newpassword', password ]); } - } else if (isEnabled) { - await sshClient.exec('samba-tool user disable', [ - userName - ]); + + entry = { + userAccountControl: sambaUser.userAccountControl + & ~UserAccountControlFlags.ACCOUNTDISABLE, + uidNumber: info.uidNumber, + accountExpires: 0, + mail: info.corporateMail + }; + } else if (sambaUser) { + entry = { + userAccountControl: sambaUser.userAccountControl + | UserAccountControlFlags.ACCOUNTDISABLE + }; + // eslint-disable-next-line no-console console.log(` -> User '${userName}' disabled on Samba`); } + + if (sambaUser && entry) { + const changes = []; + for (const prop in entry) { + if (sambaUser[prop] == entry[prop]) continue; + changes.push(new ldap.Change({ + operation: 'replace', + modification: { + [prop]: entry[prop] + } + })); + } + if (changes.length) + await this.adClient.modify(sambaUser.dn, changes); + } }, /** @@ -117,14 +151,15 @@ module.exports = Self => { */ async getUsers(usersToSync) { const LDAP_MATCHING_RULE_BIT_AND = '1.2.840.113556.1.4.803'; - let filter = `!(userAccountControl:${LDAP_MATCHING_RULE_BIT_AND}:=${UserAccountControlFlags.ACCOUNTDISABLE})`; + // eslint-disable-next-line max-len + const filter = `!(userAccountControl:${LDAP_MATCHING_RULE_BIT_AND}:=${UserAccountControlFlags.ACCOUNTDISABLE})`; - let opts = { + const opts = { scope: 'sub', attributes: ['sAMAccountName'], filter: `(&(objectClass=user)(${filter}))` }; - await this.adClient.searchForeach(this.usersDn(), opts, + await this.adClient.searchForeach(this.fullUsersDn, opts, o => usersToSync.add(o.sAMAccountName)); } }); diff --git a/modules/account/back/models/samba-config.json b/modules/account/back/models/samba-config.json index 732c9b071..28cbb2689 100644 --- a/modules/account/back/models/samba-config.json +++ b/modules/account/back/models/samba-config.json @@ -28,6 +28,10 @@ "adPassword": { "type": "string" }, + "userDn": { + "type": "string", + "required": true + }, "verifyCert": { "type": "boolean" } diff --git a/modules/account/front/accounts/index.html b/modules/account/front/accounts/index.html index 6941bb15b..6847e68d1 100644 --- a/modules/account/front/accounts/index.html +++ b/modules/account/front/accounts/index.html @@ -12,40 +12,40 @@ @@ -61,10 +61,6 @@ label="Synchronize all" ng-click="$ctrl.onSynchronizeAll()"> - - @@ -77,25 +73,3 @@ - - - - - - - - - - - - diff --git a/modules/account/front/accounts/index.js b/modules/account/front/accounts/index.js index 0e78ab8d6..ab19126a1 100644 --- a/modules/account/front/accounts/index.js +++ b/modules/account/front/accounts/index.js @@ -1,6 +1,5 @@ import ngModule from '../module'; import Section from 'salix/components/section'; -import UserError from 'core/lib/user-error'; export default class Controller extends Section { onSynchronizeAll() { @@ -8,27 +7,10 @@ export default class Controller extends Section { this.$http.patch(`Accounts/syncAll`); } - onUserSync() { - if (!this.syncUser) - throw new UserError('Please enter the username'); - - let params = { - password: this.syncPassword, - force: true - }; - return this.$http.patch(`Accounts/${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', { diff --git a/modules/account/front/accounts/locale/es.yml b/modules/account/front/accounts/locale/es.yml index 9a6bb5073..614ade3eb 100644 --- a/modules/account/front/accounts/locale/es.yml +++ b/modules/account/front/accounts/locale/es.yml @@ -3,7 +3,6 @@ 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 @@ -12,5 +11,4 @@ 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! Roles synchronized!: ¡Roles sincronizados! diff --git a/modules/account/front/descriptor/index.html b/modules/account/front/descriptor/index.html index 94497aaa9..b0a70edd1 100644 --- a/modules/account/front/descriptor/index.html +++ b/modules/account/front/descriptor/index.html @@ -67,6 +67,15 @@ translate> Deactivate user + + Synchronize +
@@ -153,6 +162,32 @@ + + + Do you want to synchronize user? + + + + + + + + + + + + diff --git a/modules/account/front/descriptor/index.js b/modules/account/front/descriptor/index.js index 786870d36..c327b0836 100644 --- a/modules/account/front/descriptor/index.js +++ b/modules/account/front/descriptor/index.js @@ -120,6 +120,20 @@ class Controller extends Descriptor { this.vnApp.showSuccess(this.$t(message)); }); } + + onSync() { + let params = { + password: this.syncPassword, + force: true + }; + return this.$http.patch(`Accounts/${this.user.name}/sync`, params) + .then(() => this.vnApp.showSuccess(this.$t('User synchronized!'))); + } + + onSyncClose() { + this.shouldSyncPassword = false; + this.syncPassword = undefined; + } } ngModule.component('vnUserDescriptor', { diff --git a/modules/account/front/descriptor/locale/es.yml b/modules/account/front/descriptor/locale/es.yml index 5e8242819..98ced7694 100644 --- a/modules/account/front/descriptor/locale/es.yml +++ b/modules/account/front/descriptor/locale/es.yml @@ -22,6 +22,10 @@ Old password: Contraseña antigua New password: Nueva contraseña Repeat password: Repetir contraseña Password changed succesfully!: ¡Contraseña modificada correctamente! +Synchronize: Sincronizar +Do you want to synchronize user?: ¿Quieres sincronizar el usuario? +Synchronize password: Sincronizar contraseña +User synchronized!: ¡Usuario sincronizado! Role changed succesfully!: ¡Rol modificado correctamente! Password requirements: > La contraseña debe tener al menos {{ length }} caracteres de longitud,