Merge branch 'test' of https://gitea.verdnatura.es/verdnatura/salix into dev
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Alex Moreno 2023-11-20 10:13:02 +01:00
commit e25ca914a0
20 changed files with 239 additions and 140 deletions

View File

@ -1,4 +1,4 @@
FROM debian:bullseye-slim
FROM debian:bookworm-slim
ENV TZ Europe/Madrid
ARG DEBIAN_FRONTEND=noninteractive
@ -25,7 +25,13 @@ RUN apt-get update \
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 update \
&& apt-get install -y --no-install-recommends \
samba-common-bin samba-dsdb-modules\
&& rm -rf /var/lib/apt/lists/* \
&& npm -g install pm2

View File

@ -51,7 +51,7 @@ module.exports = Self => {
}
const validateLogin = await Self.validateLogin(user, password);
await Self.app.models.SignInLog.create({
id: validateLogin.token,
token: validateLogin.token,
userFk: vnUser.id,
ip: ctx.req.ip
});

View File

@ -12,8 +12,21 @@ describe('VnUser Sign-in()', () => {
},
args: {}
};
const {VnUser, AccessToken} = models;
const {VnUser, AccessToken, SignInLog} = models;
describe('when credentials are correct', () => {
it('should return the token if user uses email', async() => {
let login = await VnUser.signIn(unauthCtx, 'salesAssistant@mydomain.com', 'nightmare');
let accessToken = await AccessToken.findById(login.token);
let ctx = {req: {accessToken: accessToken}};
let signInLog = await SignInLog.find({where: {token: accessToken.id}});
expect(signInLog.length).toEqual(1);
expect(signInLog[0].userFk).toEqual(accessToken.userId);
expect(login.token).toBeDefined();
await VnUser.logout(ctx.req.accessToken.id);
});
it('should return the token', async() => {
let login = await VnUser.signIn(unauthCtx, 'salesAssistant', 'nightmare');
let accessToken = await AccessToken.findById(login.token);

View File

@ -124,17 +124,20 @@ module.exports = function(Self) {
return email.send();
});
Self.signInValidate = (user, userToken) => {
const [[key, value]] = Object.entries(Self.userUses(user));
if (userToken[key].toLowerCase().trim() !== value.toLowerCase().trim()) {
console.error('ERROR!!! - Signin with other user', userToken, user);
throw new UserError('Try again');
}
};
Self.validateLogin = async function(user, password) {
const loginInfo = Object.assign({password}, Self.userUses(user));
const token = await Self.login(loginInfo, 'user');
const userToken = await token.user.get();
if (userToken.username.toLowerCase() !== user.toLowerCase()) {
console.error('ERROR!!! - Signin with other user', userToken, user);
throw new UserError('Try again');
}
Self.signInValidate(user, userToken);
try {
await Self.app.models.Account.sync(userToken.name, password);

View File

@ -2,17 +2,18 @@
--
-- Table structure for table `signInLog`
-- Description: log to debug cross-login error
--
DROP TABLE IF EXISTS `account`.`signInLog`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `account`.`signInLog` (
`id` varchar(10) NOT NULL ,
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`token` varchar(255) NOT NULL ,
`userFk` int(10) unsigned DEFAULT NULL,
`creationDate` timestamp NULL DEFAULT current_timestamp(),
`ip` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
PRIMARY KEY (`id`),
KEY `userFk` (`userFk`),
CONSTRAINT `signInLog_ibfk_1` FOREIGN KEY (`userFk`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
);

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 => {
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);
};
};

View File

@ -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;

View File

@ -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 {

View File

@ -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,123 @@ 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 bindDn = `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(bindDn, this.adPassword);
Object.assign(this, {
sshClient,
adClient
adClient,
fullUsersDn: `${this.userDn},${baseDn}`,
bindDn
});
},
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 = []) {
let authArgs = [
'--URL', `ldaps://${this.adController}`,
'--simple-bind-dn', this.bindDn,
'--password', this.adPassword
];
if (!this.verifyCert)
authArgs.push('--option', 'tls verify peer = no_check');
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 && shouldSync)
await this.adClient.modify(sambaUser.dn, changes);
}
},
/**
@ -117,14 +155,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})`;
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));
}
});

View File

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

View File

@ -8,13 +8,20 @@
},
"properties": {
"id": {
"type": "number",
"id": true,
"type": "string"
"description": "Identifier"
},
"token": {
"required": true,
"type": "string",
"description": "Token's user"
},
"creationDate": {
"type": "date"
"type": "date"
},
"userFk": {
"required": true,
"type": "number"
},
"ip": {

View File

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

View File

@ -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!

View File

@ -67,6 +67,15 @@
translate>
Deactivate user
</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-body>
<div class="attributes">
@ -153,6 +162,32 @@
<button response="accept" translate>Change password</button>
</tpl-buttons>
</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-user-summary user="$ctrl.user"></vn-user-summary>
</vn-popup>

View File

@ -120,6 +120,20 @@ class Controller extends Descriptor {
this.vnApp.showSuccess(this.$t(message));
});
}
onSync() {
const params = {force: true};
if (this.shouldSyncPassword)
params.password = this.syncPassword;
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', {

View File

@ -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,

View File

@ -12,7 +12,7 @@
<vn-card class="vn-pa-lg" vn-focus>
<vn-vertical>
<vn-check
label="Enable synchronization"
label="Enable synchronization"
ng-model="watcher.hasData">
</vn-check>
</vn-vertical>
@ -20,28 +20,33 @@
ng-if="watcher.hasData"
class="vn-mt-md">
<vn-textfield
label="AD domain"
label="AD domain"
ng-model="$ctrl.config.adDomain"
rule="SambaConfig">
</vn-textfield>
<vn-textfield
label="Domain controller"
label="Domain controller"
ng-model="$ctrl.config.adController"
rule="SambaConfig">
</vn-textfield>
<vn-textfield
label="AD user"
label="AD user"
ng-model="$ctrl.config.adUser"
rule="SambaConfig">
</vn-textfield>
<vn-textfield
label="AD password"
label="AD password"
ng-model="$ctrl.config.adPassword"
type="password"
rule="SambaConfig">
</vn-textfield>
<vn-textfield
label="User DN (without domain part)"
ng-model="$ctrl.config.userDn"
rule="SambaConfig">
</vn-textfield>
<vn-check
label="Verify certificate"
label="Verify certificate"
ng-model="$ctrl.config.verifyCert">
</vn-check>
</vn-vertical>
@ -63,4 +68,4 @@
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
</form>
</form>

View File

@ -3,6 +3,7 @@ Domain controller: Controlador de dominio
AD domain: Dominio AD
AD user: Usuario AD
AD password: Contraseña AD
User DN (without domain part): DN usuarios (sin la parte del dominio)
Verify certificate: Verificar certificado
Test connection: Probar conexión
Samba connection established!: ¡Conexión con Samba establecida!