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 ENV TZ Europe/Madrid
ARG DEBIAN_FRONTEND=noninteractive 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 \ 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 update \
&& apt-get install -y --no-install-recommends \
samba-common-bin samba-dsdb-modules\
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& npm -g install pm2 && npm -g install pm2

View File

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

View File

@ -12,8 +12,21 @@ describe('VnUser Sign-in()', () => {
}, },
args: {} args: {}
}; };
const {VnUser, AccessToken} = models; const {VnUser, AccessToken, SignInLog} = models;
describe('when credentials are correct', () => { 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() => { it('should return the token', async() => {
let login = await VnUser.signIn(unauthCtx, 'salesAssistant', 'nightmare'); let login = await VnUser.signIn(unauthCtx, 'salesAssistant', 'nightmare');
let accessToken = await AccessToken.findById(login.token); let accessToken = await AccessToken.findById(login.token);

View File

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

View File

@ -2,17 +2,18 @@
-- --
-- Table structure for table `signInLog` -- Table structure for table `signInLog`
-- Description: log to debug cross-login error
-- --
DROP TABLE IF EXISTS `account`.`signInLog`; DROP TABLE IF EXISTS `account`.`signInLog`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8 */;
CREATE TABLE `account`.`signInLog` ( 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, `userFk` int(10) unsigned DEFAULT NULL,
`creationDate` timestamp NULL DEFAULT current_timestamp(), `creationDate` timestamp NULL DEFAULT current_timestamp(),
`ip` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, `ip` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
PRIMARY KEY (`id`),
KEY `userFk` (`userFk`), KEY `userFk` (`userFk`),
CONSTRAINT `signInLog_ibfk_1` FOREIGN KEY (`userFk`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 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 => { 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,123 @@ 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 bindDn = `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(bindDn, this.adPassword);
Object.assign(this, { Object.assign(this, {
sshClient, adClient,
adClient fullUsersDn: `${this.userDn},${baseDn}`,
bindDn
}); });
}, },
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 let authArgs = [
.split('.') '--URL', `ldaps://${this.adController}`,
.map(part => `dc=${part}`) '--simple-bind-dn', this.bindDn,
.join(','); '--password', this.adPassword
return `cn=Users,${dnBase}`; ];
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 getAdUser(userName) {
const sambaUser = await this.adClient.searchOne(this.fullUsersDn, {
scope: 'sub',
attributes: [
'dn',
'userAccountControl',
'uidNumber',
'accountExpires',
'mail'
],
filter: `(&(objectClass=user)(sAMAccountName=${userName}))`
});
if (sambaUser) {
for (const intProp of ['uidNumber', 'userAccountControl']) {
if (sambaUser[intProp] != null)
sambaUser[intProp] = parseInt(sambaUser[intProp]);
}
}
return sambaUser;
}, },
async syncUser(userName, info, password) { async syncUser(userName, info, password) {
let {sshClient} = this; let sambaUser = await this.getAdUser(userName);
let entry;
let sambaUser = await this.adClient.searchOne(this.usersDn(), {
scope: 'sub',
attributes: ['userAccountControl'],
filter: `(&(objectClass=user)(sAMAccountName=${userName}))`
});
let isEnabled = sambaUser
&& !(sambaUser.userAccountControl & UserAccountControlFlags.ACCOUNTDISABLE);
if (process.env.NODE_ENV === 'test')
return;
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 && shouldSync)
await this.adClient.modify(sambaUser.dn, changes);
}
}, },
/** /**
@ -117,14 +155,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})`; 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

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

View File

@ -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() {
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', { 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,

View File

@ -40,6 +40,11 @@
type="password" type="password"
rule="SambaConfig"> rule="SambaConfig">
</vn-textfield> </vn-textfield>
<vn-textfield
label="User DN (without domain part)"
ng-model="$ctrl.config.userDn"
rule="SambaConfig">
</vn-textfield>
<vn-check <vn-check
label="Verify certificate" label="Verify certificate"
ng-model="$ctrl.config.verifyCert"> ng-model="$ctrl.config.verifyCert">

View File

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