fix: refs #6432 account sync fixes

This commit is contained in:
Juan Ferrer 2023-11-16 23:07:26 +01:00
parent 9c6b594426
commit 4f5242e3ae
13 changed files with 186 additions and 123 deletions

View File

@ -17,15 +17,19 @@ RUN apt-get update \
# Puppeteer # Puppeteer
RUN apt-get update \ RUN apt-get install -y --no-install-recommends \
&& apt-get install -y --no-install-recommends \
libfontconfig lftp xvfb gconf-service libasound2 libatk1.0-0 libc6 \ libfontconfig lftp xvfb gconf-service libasound2 libatk1.0-0 libc6 \
libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 \ libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 \
libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 \ 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 \ libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \
libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \ libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \
libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \ 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/* \ && rm -rf /var/lib/apt/lists/* \
&& npm -g install pm2 && npm -g install pm2

View File

@ -0,0 +1,2 @@
ALTER TABLE account.sambaConfig
ADD userDn varchar(255) NOT NULL COMMENT 'Base DN for users without domain DN part';

View File

@ -1,3 +1,4 @@
const ForbiddenError = require('vn-loopback/util/forbiddenError');
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('sync', { Self.remoteMethod('sync', {
@ -32,9 +33,13 @@ module.exports = Self => {
const models = Self.app.models; const models = Self.app.models;
const user = await models.VnUser.findOne({ const user = await models.VnUser.findOne({
fields: ['id'], fields: ['id', 'password'],
where: {name: userName} where: {name: userName}
}, myOptions); }, myOptions);
if (user && password && !await user.hasPassword(password))
throw new ForbiddenError('Wrong password');
const isSync = !await models.UserSync.exists(userName, myOptions); const isSync = !await models.UserSync.exists(userName, myOptions);
if (!force && isSync && user) return; if (!force && isSync && user) return;
@ -42,4 +47,3 @@ module.exports = Self => {
await models.UserSync.destroyById(userName, myOptions); await models.UserSync.destroyById(userName, myOptions);
}; };
}; };

View File

@ -5,7 +5,7 @@ const crypto = require('crypto');
const nthash = require('smbhash').nthash; const nthash = require('smbhash').nthash;
module.exports = Self => { module.exports = Self => {
const shouldSync = process.env.NODE_ENV === 'production'; const shouldSync = process.env.NODE_ENV !== 'test';
Self.getSynchronizer = async function() { Self.getSynchronizer = async function() {
return await Self.findOne({ return await Self.findOne({
@ -140,6 +140,7 @@ module.exports = Self => {
try { try {
if (shouldSync) if (shouldSync)
await client.del(dn); await client.del(dn);
// eslint-disable-next-line no-console
console.log(` -> User '${userName}' removed from LDAP`); console.log(` -> User '${userName}' removed from LDAP`);
} catch (e) { } catch (e) {
if (e.name !== 'NoSuchObjectError') throw e; if (e.name !== 'NoSuchObjectError') throw e;

View File

@ -27,8 +27,7 @@ module.exports = Self => {
const [row] = await Self.rawSql( const [row] = await Self.rawSql(
`SELECT COUNT(*) AS nRows `SELECT COUNT(*) AS nRows
FROM mysql.user FROM mysql.user
WHERE User = ? WHERE User = ? AND Host = ?`,
AND Host = ?`,
[mysqlUser, this.userHost] [mysqlUser, this.userHost]
); );
let userExists = row.nRows > 0; let userExists = row.nRows > 0;
@ -38,8 +37,7 @@ module.exports = Self => {
const [row] = await Self.rawSql( const [row] = await Self.rawSql(
`SELECT Priv AS priv `SELECT Priv AS priv
FROM mysql.global_priv FROM mysql.global_priv
WHERE User = ? WHERE User = ? AND Host = ?`,
AND Host = ?`,
[mysqlUser, this.userHost] [mysqlUser, this.userHost]
); );
const priv = row && JSON.parse(row.priv); const priv = row && JSON.parse(row.priv);
@ -88,10 +86,18 @@ module.exports = Self => {
else else
throw err; 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 ?@?', await Self.rawSql('SET DEFAULT ROLE ? FOR ?@?',
[role, mysqlUser, this.userHost]); [role, mysqlUser, this.userHost]);
} else { } else {

View File

@ -1,6 +1,6 @@
const ldap = require('../util/ldapjs-extra'); const ldap = require('../util/ldapjs-extra');
const ssh = require('node-ssh'); const execFile = require('child_process').execFile;
/** /**
* Summary of userAccountControl flags: * Summary of userAccountControl flags:
@ -11,6 +11,8 @@ const UserAccountControlFlags = {
}; };
module.exports = Self => { module.exports = Self => {
const shouldSync = process.env.NODE_ENV !== 'test';
Self.getSynchronizer = async function() { Self.getSynchronizer = async function() {
return await Self.findOne({ return await Self.findOne({
fields: [ fields: [
@ -19,6 +21,7 @@ module.exports = Self => {
'adController', 'adController',
'adUser', 'adUser',
'adPassword', 'adPassword',
'userDn',
'verifyCert' 'verifyCert'
] ]
}); });
@ -26,88 +29,119 @@ module.exports = Self => {
Object.assign(Self.prototype, { Object.assign(Self.prototype, {
async init() { async init() {
let sshClient = new ssh.NodeSSH(); const baseDn = this.adDomain
await sshClient.connect({ .split('.')
host: this.adController, .map(part => `dc=${part}`)
username: this.adUser, .join(',');
password: this.adPassword const ldapUser = `cn=${this.adUser},cn=Users,${baseDn}`;
});
let adUser = `cn=${this.adUser},${this.usersDn()}`; const adClient = ldap.createClient({
let adClient = ldap.createClient({
url: `ldaps://${this.adController}:636`, url: `ldaps://${this.adController}:636`,
tlsOptions: {rejectUnauthorized: this.verifyCert} tlsOptions: {rejectUnauthorized: this.verifyCert}
}); });
await adClient.bind(adUser, this.adPassword); await adClient.bind(ldapUser, this.adPassword);
Object.assign(this, { Object.assign(this, {
sshClient, adClient,
adClient fullUsersDn: `${this.userDn},${baseDn}`
}); });
}, },
async deinit() { async deinit() {
await this.sshClient.dispose();
await this.adClient.unbind(); await this.adClient.unbind();
}, },
usersDn() { async sambaTool(command, args = []) {
let dnBase = this.adDomain const authArgs = [
.split('.') '--URL', `ldap://${this.adController}`,
.map(part => `dc=${part}`) '--username', this.adUser,
.join(','); '--password', this.adPassword
return `cn=Users,${dnBase}`; ];
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) { async getAdUser(userName) {
let {sshClient} = this; const sambaUser = await this.adClient.searchOne(this.fullUsersDn, {
let sambaUser = await this.adClient.searchOne(this.usersDn(), {
scope: 'sub', scope: 'sub',
attributes: ['userAccountControl'], attributes: [
'dn',
'userAccountControl',
'uidNumber',
'accountExpires',
'mail'
],
filter: `(&(objectClass=user)(sAMAccountName=${userName}))` filter: `(&(objectClass=user)(sAMAccountName=${userName}))`
}); });
let isEnabled = sambaUser if (sambaUser) {
&& !(sambaUser.userAccountControl & UserAccountControlFlags.ACCOUNTDISABLE); for (const intProp of ['uidNumber', 'userAccountControl']) {
if (sambaUser[intProp] != null)
if (process.env.NODE_ENV === 'test') sambaUser[intProp] = parseInt(sambaUser[intProp]);
return; }
}
return sambaUser;
},
async syncUser(userName, info, password) {
let sambaUser = await this.getAdUser(userName);
let entry;
if (info.hasAccount) { if (info.hasAccount) {
if (!sambaUser) { if (!sambaUser) {
await sshClient.exec('samba-tool user create', [ await this.sambaTool('user', [
userName, 'create', userName,
'--uid-number', `${info.uidNumber}`, '--userou', this.userDn,
'--mail-address', info.corporateMail,
'--random-password' '--random-password'
]); ]);
await sshClient.exec('samba-tool user setexpiry', [ sambaUser = await this.getAdUser(userName);
userName,
'--noexpiry'
]);
await sshClient.exec('mkhomedir_helper', [
userName,
'0027'
]);
}
if (!isEnabled) {
await sshClient.exec('samba-tool user enable', [
userName
]);
} }
if (password) { if (password) {
await sshClient.exec('samba-tool user setpassword', [ await this.sambaTool('user', [
userName, 'setpassword', userName,
'--newpassword', password '--newpassword', password
]); ]);
} }
} else if (isEnabled) {
await sshClient.exec('samba-tool user disable', [ entry = {
userName 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`); 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) { async getUsers(usersToSync) {
const LDAP_MATCHING_RULE_BIT_AND = '1.2.840.113556.1.4.803'; 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', scope: 'sub',
attributes: ['sAMAccountName'], attributes: ['sAMAccountName'],
filter: `(&(objectClass=user)(${filter}))` filter: `(&(objectClass=user)(${filter}))`
}; };
await this.adClient.searchForeach(this.usersDn(), opts, await this.adClient.searchForeach(this.fullUsersDn, opts,
o => usersToSync.add(o.sAMAccountName)); o => usersToSync.add(o.sAMAccountName));
} }
}); });

View File

@ -28,6 +28,10 @@
"adPassword": { "adPassword": {
"type": "string" "type": "string"
}, },
"userDn": {
"type": "string",
"required": true
},
"verifyCert": { "verifyCert": {
"type": "boolean" "type": "boolean"
} }

View File

@ -12,40 +12,40 @@
<vn-card class="vn-pa-lg" vn-focus> <vn-card class="vn-pa-lg" vn-focus>
<vn-vertical> <vn-vertical>
<vn-textfield <vn-textfield
label="Homedir base" label="Homedir base"
ng-model="$ctrl.config.homedir" ng-model="$ctrl.config.homedir"
rule="AccountConfig"> rule="AccountConfig">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="Shell" label="Shell"
ng-model="$ctrl.config.shell" ng-model="$ctrl.config.shell"
rule="AccountConfig"> rule="AccountConfig">
</vn-textfield> </vn-textfield>
<vn-input-number <vn-input-number
label="User and role base id" label="User and role base id"
ng-model="$ctrl.config.idBase" ng-model="$ctrl.config.idBase"
rule="AccountConfig"> rule="AccountConfig">
</vn-input-number> </vn-input-number>
<vn-horizontal> <vn-horizontal>
<vn-input-number <vn-input-number
label="Min" label="Min"
ng-model="$ctrl.config.min" ng-model="$ctrl.config.min"
rule="AccountConfig"> rule="AccountConfig">
</vn-input-number> </vn-input-number>
<vn-input-number <vn-input-number
label="Max" label="Max"
ng-model="$ctrl.config.max" ng-model="$ctrl.config.max"
rule="AccountConfig"> rule="AccountConfig">
</vn-input-number> </vn-input-number>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-input-number <vn-input-number
label="Warn" label="Warn"
ng-model="$ctrl.config.warn" ng-model="$ctrl.config.warn"
rule="AccountConfig"> rule="AccountConfig">
</vn-input-number> </vn-input-number>
<vn-input-number <vn-input-number
label="Inact" label="Inact"
ng-model="$ctrl.config.inact" ng-model="$ctrl.config.inact"
rule="AccountConfig"> rule="AccountConfig">
</vn-input-number> </vn-input-number>
@ -61,10 +61,6 @@
label="Synchronize all" label="Synchronize all"
ng-click="$ctrl.onSynchronizeAll()"> ng-click="$ctrl.onSynchronizeAll()">
</vn-button> </vn-button>
<vn-button
label="Synchronize user"
ng-click="syncUser.show()">
</vn-button>
<vn-button <vn-button
label="Synchronize roles" label="Synchronize roles"
ng-click="$ctrl.onSynchronizeRoles()"> ng-click="$ctrl.onSynchronizeRoles()">
@ -77,25 +73,3 @@
</vn-button> </vn-button>
</vn-button-bar> </vn-button-bar>
</form> </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>

View File

@ -1,6 +1,5 @@
import ngModule from '../module'; import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
import UserError from 'core/lib/user-error';
export default class Controller extends Section { export default class Controller extends Section {
onSynchronizeAll() { onSynchronizeAll() {
@ -8,27 +7,10 @@ export default class Controller extends Section {
this.$http.patch(`Accounts/syncAll`); 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() { onSynchronizeRoles() {
this.$http.patch(`RoleInherits/sync`) this.$http.patch(`RoleInherits/sync`)
.then(() => this.vnApp.showSuccess(this.$t('Roles synchronized!'))); .then(() => this.vnApp.showSuccess(this.$t('Roles synchronized!')));
} }
onSyncClose() {
this.syncUser = '';
this.syncPassword = '';
}
} }
ngModule.component('vnAccountAccounts', { ngModule.component('vnAccountAccounts', {

View File

@ -3,7 +3,6 @@ Homedir base: Directorio base para carpetas de usuario
Shell: Intérprete de línea de comandos Shell: Intérprete de línea de comandos
User and role base id: Id base usuarios y roles User and role base id: Id base usuarios y roles
Synchronize all: Sincronizar todo Synchronize all: Sincronizar todo
Synchronize user: Sincronizar usuario
Synchronize roles: Sincronizar roles Synchronize roles: Sincronizar roles
If password is not specified, just user attributes are synchronized: >- 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 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 Username: Nombre de usuario
Synchronize: Sincronizar Synchronize: Sincronizar
Please enter the username: Por favor introduce el nombre de usuario Please enter the username: Por favor introduce el nombre de usuario
User synchronized!: ¡Usuario sincronizado!
Roles synchronized!: ¡Roles sincronizados! Roles synchronized!: ¡Roles sincronizados!

View File

@ -67,6 +67,15 @@
translate> translate>
Deactivate user Deactivate user
</vn-item> </vn-item>
<vn-item
ng-if="$ctrl.user.active"
ng-click="syncUser.show()"
name="synchronizeUser"
vn-acl="it"
vn-acl-action="remove"
translate>
Synchronize
</vn-item>
</slot-menu> </slot-menu>
<slot-body> <slot-body>
<div class="attributes"> <div class="attributes">
@ -153,6 +162,32 @@
<button response="accept" translate>Change password</button> <button response="accept" translate>Change password</button>
</tpl-buttons> </tpl-buttons>
</vn-dialog> </vn-dialog>
<vn-dialog
vn-id="syncUser"
on-accept="$ctrl.onSync()"
on-close="$ctrl.onSyncClose()">
<tpl-title ng-translate>
Do you want to synchronize user?
</tpl-title>
<tpl-body>
<vn-check
label="Synchronize password"
ng-model="$ctrl.shouldSyncPassword"
info="If password is not specified, just user attributes are synchronized"
vn-focus>
</vn-check>
<vn-textfield
label="Password"
ng-model="$ctrl.syncPassword"
type="password"
ng-if="$ctrl.shouldSyncPassword">
</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>
<vn-popup vn-id="summary"> <vn-popup vn-id="summary">
<vn-user-summary user="$ctrl.user"></vn-user-summary> <vn-user-summary user="$ctrl.user"></vn-user-summary>
</vn-popup> </vn-popup>

View File

@ -120,6 +120,20 @@ class Controller extends Descriptor {
this.vnApp.showSuccess(this.$t(message)); 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', { ngModule.component('vnUserDescriptor', {

View File

@ -22,6 +22,10 @@ Old password: Contraseña antigua
New password: Nueva contraseña New password: Nueva contraseña
Repeat password: Repetir contraseña Repeat password: Repetir contraseña
Password changed succesfully!: ¡Contraseña modificada correctamente! 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! Role changed succesfully!: ¡Rol modificado correctamente!
Password requirements: > Password requirements: >
La contraseña debe tener al menos {{ length }} caracteres de longitud, La contraseña debe tener al menos {{ length }} caracteres de longitud,