def test_crud_user(self):
self._create_user(username='user1',
- password='mypassword',
+ password='mypassword10#',
name='My Name',
email='my@email.com',
roles=['administrator'])
def test_create_user_already_exists(self):
self._create_user(username='admin',
- password='mypassword',
+ password='mypassword10#',
name='administrator',
email='my@email.com',
roles=['administrator'])
def test_create_user_invalid_role(self):
self._create_user(username='user1',
- password='mypassword',
+ password='mypassword10#',
name='My Name',
email='my@email.com',
roles=['invalid-role'])
self.assertStatus(400)
self.assertError(code='invalid_old_password', component='user')
+ def test_change_password_as_old_password(self):
+ self.create_user('test1', 'mypassword10#', ['read-only'])
+ self.login('test1', 'mypassword10#')
+ self._post('/api/user/test1/change_password', {
+ 'old_password': 'mypassword10#',
+ 'new_password': 'mypassword10#'
+ })
+ self.assertStatus(400)
+ self.assertError(code='the_same_as_old_password', component='user')
+ self.delete_user('test1')
+
+ def test_change_password_contains_username(self):
+ self.create_user('test1', 'mypassword10#', ['read-only'])
+ self.login('test1', 'mypassword10#')
+ self._post('/api/user/test1/change_password', {
+ 'old_password': 'mypassword10#',
+ 'new_password': 'mypasstest1@#'
+ })
+ self.assertStatus(400)
+ self.assertError(code='contains_username', component='user')
+ self.delete_user('test1')
+
+ def test_change_password_contains_forbidden_words(self):
+ self.create_user('test1', 'mypassword10#', ['read-only'])
+ self.login('test1', 'mypassword10#')
+ self._post('/api/user/test1/change_password', {
+ 'old_password': 'mypassword10#',
+ 'new_password': 'mypassOSD01'
+ })
+ self.assertStatus(400)
+ self.assertError(code='contains_forbidden_words', component='user')
+ self.delete_user('test1')
+
+ def test_change_password_contains_sequential_characters(self):
+ self.create_user('test1', 'mypassword10#', ['read-only'])
+ self.login('test1', 'mypassword10#')
+ self._post('/api/user/test1/change_password', {
+ 'old_password': 'mypassword10#',
+ 'new_password': 'mypass123456!@$'
+ })
+ self.assertStatus(400)
+ self.assertError(code='contains_sequential_characters', component='user')
+ self.delete_user('test1')
+
+ def test_change_password_contains_repetetive_characters(self):
+ self.create_user('test1', 'mypassword10#', ['read-only'])
+ self.login('test1', 'mypassword10#')
+ self._post('/api/user/test1/change_password', {
+ 'old_password': 'mypassword10#',
+ 'new_password': 'aaaaA1@!#'
+ })
+ self.assertStatus(400)
+ self.assertError(code='contains_repetetive_characters', component='user')
+ self.delete_user('test1')
+
def test_change_password(self):
- self.create_user('test1', 'test1', ['read-only'])
- self.login('test1', 'test1')
+ self.create_user('test1', 'mypassword10#', ['read-only'])
+ self.login('test1', 'mypassword10#')
self._post('/api/user/test1/change_password', {
- 'old_password': 'test1',
- 'new_password': 'foo'
+ 'old_password': 'mypassword10#',
+ 'new_password': 'newpassword01#'
})
self.assertStatus(200)
self.logout()
- self._post('/api/auth', {'username': 'test1', 'password': 'test1'})
+ self._post('/api/auth', {'username': 'test1', 'password': 'mypassword10#'})
self.assertStatus(400)
self.assertError(code='invalid_credentials', component='auth')
self.delete_user('test1')
from ..exceptions import DashboardException, UserAlreadyExists, \
UserDoesNotExist
from ..security import Scope
-from ..services.access_control import SYSTEM_ROLES
+from ..services.access_control import SYSTEM_ROLES, PasswordCheck
from ..services.auth import JwtManager
+def check_password_complexity(password, username, old_password=None):
+ password_complexity = PasswordCheck(password, username, old_password)
+ if password_complexity.check_if_as_the_old_password():
+ raise DashboardException(msg='Password cannot be the\
+ same as the previous one.',
+ code='not-strong-enough-password',
+ component='user')
+ if password_complexity.check_if_contains_username():
+ raise DashboardException(msg='Password cannot contain username.',
+ code='not-strong-enough-password',
+ component='user')
+ if password_complexity.check_if_contains_forbidden_words():
+ raise DashboardException(msg='Password cannot contain keywords.',
+ code='not-strong-enough-password',
+ component='user')
+ if password_complexity.check_if_repetetive_characters():
+ raise DashboardException(msg='Password cannot contain repetitive\
+ characters.',
+ code='not-strong-enough-password',
+ component='user')
+ if password_complexity.check_if_sequential_characters():
+ raise DashboardException(msg='Password cannot contain sequential\
+ characters.',
+ code='not-strong-enough-password',
+ component='user')
+ if password_complexity.check_password_characters() < 10:
+ raise DashboardException(msg='Password is too weak.',
+ code='not-strong-enough-password',
+ component='user')
+
+
@ApiController('/user', Scope.USER)
class User(RESTController):
@staticmethod
user_roles = None
if roles:
user_roles = User._get_user_roles(roles)
+ if password:
+ check_password_complexity(password, username)
try:
user = mgr.ACCESS_CTRL_DB.create_user(username, password, name,
email, enabled)
if roles:
user_roles = User._get_user_roles(roles)
if password:
+ check_password_complexity(password, username)
user.set_password(password)
user.name = name
user.email = email
raise DashboardException(msg='Invalid old password',
code='invalid_old_password',
component='user')
+ check_password_complexity(new_password, username, old_password)
user.set_password(new_password)
mgr.ACCESS_CTRL_DB.save()
<!-- Password -->
<div class="form-group row">
- <label i18n
- class="col-form-label col-sm-3"
- for="password">Password</label>
+ <label class="col-form-label col-sm-3"
+ for="password">
+ <ng-container i18n>Password</ng-container>
+ <cd-helper class="text-pre"
+ html="{{ requiredPasswordRulesMessage }}">
+ </cd-helper>
+ </label>
<div class="col-sm-9">
<div class="input-group">
<input class="form-control"
</button>
</span>
</div>
+ <div class="passwordStrengthLevel">
+ <div class="{{ passwordStrengthLevel }}"
+ data-toggle="tooltip"
+ title="{{ passwordStrengthDescription }}">
+ </div>
+ </div>
<span class="invalid-feedback"
*ngIf="userForm.showError('password', formDir, 'required')"
i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('password', formDir, 'checkPassword')"
+ i18n>Too weak</span>
</div>
</div>
formHelper.expectValidChange('confirmpassword', 'aaa');
});
+ it('should validate password strength very strong', () => {
+ formHelper.setValue('password', 'testpassword#!$!@$');
+ component.checkPassword('testpassword#!$!@$');
+ expect(component.passwordStrengthDescription).toBe('Very strong');
+ expect(component.passwordStrengthLevel).toBe('passwordStrengthLevel4');
+ });
+
+ it('should validate password strength strong', () => {
+ formHelper.setValue('password', 'testpassword0047!@');
+ component.checkPassword('testpassword0047!@');
+ expect(component.passwordStrengthDescription).toBe('Strong');
+ expect(component.passwordStrengthLevel).toBe('passwordStrengthLevel3');
+ });
+
+ it('should validate password strength ok ', () => {
+ formHelper.setValue('password', 'mypassword1!@');
+ component.checkPassword('mypassword1!@');
+ expect(component.passwordStrengthDescription).toBe('OK');
+ expect(component.passwordStrengthLevel).toBe('passwordStrengthLevel2');
+ });
+
+ it('should validate password strength weak', () => {
+ formHelper.setValue('password', 'mypassword1');
+ component.checkPassword('mypassword1');
+ expect(component.passwordStrengthDescription).toBe('Weak');
+ expect(component.passwordStrengthLevel).toBe('passwordStrengthLevel1');
+ });
+
+ it('should validate password strength too weak', () => {
+ formHelper.setValue('password', 'bar0');
+ component.checkPassword('bar0');
+ expect(component.passwordStrengthDescription).toBe('Too weak');
+ expect(component.passwordStrengthLevel).toBe('passwordStrengthLevel0');
+ });
+
it('should validate email', () => {
formHelper.expectErrorChange('email', 'aaa', 'email');
});
import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
import { SelectMessages } from '../../../shared/components/select/select-messages.model';
import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
+import { Icons } from '../../../shared/enum/icons.enum';
import { NotificationType } from '../../../shared/enum/notification-type.enum';
import { CdFormGroup } from '../../../shared/forms/cd-form-group';
import { CdValidators } from '../../../shared/forms/cd-validators';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { NotificationService } from '../../../shared/services/notification.service';
+import { UserChangePasswordService } from '../../../shared/services/user-change-password.service';
import { UserFormMode } from './user-form-mode.enum';
import { UserFormRoleModel } from './user-form-role.model';
import { UserFormModel } from './user-form.model';
messages = new SelectMessages({ empty: 'There are no roles.' }, this.i18n);
action: string;
resource: string;
+ requiredPasswordRulesMessage: string;
+ passwordStrengthLevel: string;
+ passwordStrengthDescription: string;
+ icons = Icons;
constructor(
private authService: AuthService,
private userService: UserService,
private notificationService: NotificationService,
private i18n: I18n,
- public actionLabels: ActionLabelsI18n
+ public actionLabels: ActionLabelsI18n,
+ private userChangePasswordService: UserChangePasswordService
) {
this.resource = this.i18n('user');
this.createForm();
}
createForm() {
+ this.requiredPasswordRulesMessage = this.userChangePasswordService.getPasswordRulesMessage();
this.userForm = new CdFormGroup(
{
username: new FormControl('', {
}),
name: new FormControl(''),
password: new FormControl('', {
- validators: []
+ validators: [
+ CdValidators.custom('checkPassword', () => {
+ return this.userForm && this.checkPassword(this.userForm.getValue('password'));
+ })
+ ]
}),
confirmpassword: new FormControl('', {
updateOn: 'blur',
}
}
+ checkPassword(password: string) {
+ [
+ this.passwordStrengthLevel,
+ this.passwordStrengthDescription
+ ] = this.userChangePasswordService.checkPasswordComplexity(password);
+ return password && this.passwordStrengthLevel === 'passwordStrengthLevel0';
+ }
+
public isCurrentUser(): boolean {
return this.authStorageService.getUsername() === this.userForm.getValue('username');
}
<span class="invalid-feedback"
*ngIf="userForm.showError('oldpassword', frm, 'required')"
i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('oldpassword', frm, 'notmatch')"
+ i18n>The old and new passwords must be different.</span>
</div>
</div>
<div class="form-group row">
<label class="col-form-label col-sm-3"
for="newpassword">
- <ng-container i18n>New password</ng-container>
+ <ng-container i18n>New password</ng-container>
+ <cd-helper class="text-pre"
+ html="{{ requiredPasswordRulesMessage }}">
+ </cd-helper>
<span class="required"></span>
</label>
<div class="col-sm-9">
autocomplete="new-password"
formControlName="newpassword">
<span class="input-group-append">
- <button class="btn btn-light"
+ <button type="button"
+ class="btn btn-light"
cdPasswordButton="newpassword">
</button>
</span>
</div>
+ <div class="passwordStrengthLevel">
+ <div class="{{ passwordStrengthLevel }}"
+ data-toggle="tooltip"
+ title="{{ passwordStrengthDescription }}">
+ </div>
+ </div>
<span class="invalid-feedback"
*ngIf="userForm.showError('newpassword', frm, 'required')"
i18n>This field is required.</span>
<span class="invalid-feedback"
*ngIf="userForm.showError('newpassword', frm, 'notmatch')"
i18n>The old and new passwords must be different.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('newpassword', frm, 'checkPassword')"
+ i18n>Too weak</span>
</div>
</div>
formHelper.expectValidChange('confirmnewpassword', 'aaa');
});
+ it('should validate password strength very strong', () => {
+ formHelper.setValue('newpassword', 'testpassword#!$!@$');
+ component.checkPassword('testpassword#!$!@$');
+ expect(component.passwordStrengthDescription).toBe('Very strong');
+ expect(component.passwordStrengthLevel).toBe('passwordStrengthLevel4');
+ });
+
+ it('should validate password strength strong', () => {
+ formHelper.setValue('newpassword', 'testpassword0047!@');
+ component.checkPassword('testpassword0047!@');
+ expect(component.passwordStrengthDescription).toBe('Strong');
+ expect(component.passwordStrengthLevel).toBe('passwordStrengthLevel3');
+ });
+
+ it('should validate password strength ok ', () => {
+ formHelper.setValue('newpassword', 'mypassword1!@');
+ component.checkPassword('mypassword1!@');
+ expect(component.passwordStrengthDescription).toBe('OK');
+ expect(component.passwordStrengthLevel).toBe('passwordStrengthLevel2');
+ });
+
+ it('should validate password strength weak', () => {
+ formHelper.setValue('newpassword', 'mypassword1');
+ component.checkPassword('mypassword1');
+ expect(component.passwordStrengthDescription).toBe('Weak');
+ expect(component.passwordStrengthLevel).toBe('passwordStrengthLevel1');
+ });
+
+ it('should validate password strength too weak', () => {
+ formHelper.setValue('newpassword', 'abc0');
+ component.checkPassword('abc0');
+ expect(component.passwordStrengthDescription).toBe('Too weak');
+ expect(component.passwordStrengthLevel).toBe('passwordStrengthLevel0');
+ });
+
it('should submit', () => {
spyOn(authStorageService, 'getUsername').and.returnValue('xyz');
formHelper.setMultipleValues({
import { UserService } from '../../../shared/api/user.service';
import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
+import { Icons } from '../../../shared/enum/icons.enum';
import { NotificationType } from '../../../shared/enum/notification-type.enum';
import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
import { CdFormGroup } from '../../../shared/forms/cd-form-group';
import { CdValidators } from '../../../shared/forms/cd-validators';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { NotificationService } from '../../../shared/services/notification.service';
+import { UserChangePasswordService } from '../../../shared/services/user-change-password.service';
@Component({
selector: 'cd-user-password-form',
userForm: CdFormGroup;
action: string;
resource: string;
+ requiredPasswordRulesMessage: string;
+ passwordStrengthLevel: string;
+ passwordStrengthDescription: string;
+ icons = Icons;
constructor(
private i18n: I18n,
private userService: UserService,
private authStorageService: AuthStorageService,
private formBuilder: CdFormBuilder,
- private router: Router
+ private router: Router,
+ private userChangePasswordService: UserChangePasswordService
) {
this.action = this.actionLabels.CHANGE;
this.resource = this.i18n('password');
}
createForm() {
+ this.requiredPasswordRulesMessage = this.userChangePasswordService.getPasswordRulesMessage();
this.userForm = this.formBuilder.group(
{
- oldpassword: [null, [Validators.required]],
+ oldpassword: [
+ null,
+ [
+ Validators.required,
+ CdValidators.custom('notmatch', () => {
+ return (
+ this.userForm &&
+ this.userForm.getValue('newpassword') === this.userForm.getValue('oldpassword')
+ );
+ })
+ ]
+ ],
newpassword: [
null,
[
this.userForm &&
this.userForm.getValue('oldpassword') === this.userForm.getValue('newpassword')
);
+ }),
+ CdValidators.custom('checkPassword', () => {
+ return this.userForm && this.checkPassword(this.userForm.getValue('newpassword'));
})
]
],
);
}
+ checkPassword(password: string) {
+ [
+ this.passwordStrengthLevel,
+ this.passwordStrengthDescription
+ ] = this.userChangePasswordService.checkPasswordComplexity(password);
+ return password && this.passwordStrengthLevel === 'passwordStrengthLevel0';
+ }
+
onSubmit() {
if (this.userForm.pristine) {
return;
<ng-template #popoverTpl>
- <div [innerHtml]="html"></div>
+ <div [class]="class"
+ [innerHtml]="html">
+ </div>
<ng-content></ng-content>
</ng-template>
<i [ngClass]="[icons.questionCircle]"
styleUrls: ['./helper.component.scss']
})
export class HelperComponent {
+ @Input()
+ class: string;
+
@Input()
html: any;
--- /dev/null
+import { Injectable } from '@angular/core';
+
+import { I18n } from '@ngx-translate/i18n-polyfill';
+import * as _ from 'lodash';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UserChangePasswordService {
+ requiredPasswordRulesMessage: string;
+ passwordStrengthLevel: string;
+ passwordStrengthDescription: string;
+
+ constructor(private i18n: I18n) {}
+ getPasswordRulesMessage() {
+ return this.i18n(
+ 'Required rules for password complexity:\n\
+ - must contain at least 8 characters\n\
+ - cannot contain username\n\
+ - cannot contain any keyword used in Ceph\n\
+ - cannot contain any repetitive characters e.g. "aaa"\n\
+ - cannot contain any sequencial characters e.g. "abc"\n\
+ - must consist of characters from the following groups:\n\
+ * alphabetic a-z, A-Z\n\
+ * numbers 0-9\n\
+ * special chars: !"#$%& \'()*+,-./:;<=>?@[\\]^_`{{|}}~\n\
+ * any other characters (signs)'
+ );
+ }
+
+ checkPasswordComplexity(password): [string, string] {
+ this.passwordStrengthLevel = 'passwordStrengthLevel0';
+ this.passwordStrengthDescription = '';
+ const credits = this.checkPasswordComplexityLetters(password);
+ if (credits) {
+ if (password.length < 8 || credits < 10) {
+ this.passwordStrengthLevel = 'passwordStrengthLevel0';
+ this.passwordStrengthDescription = this.i18n('Too weak');
+ } else {
+ if (credits < 15) {
+ this.passwordStrengthLevel = 'passwordStrengthLevel1';
+ this.passwordStrengthDescription = this.i18n('Weak');
+ } else {
+ if (credits < 20) {
+ this.passwordStrengthLevel = 'passwordStrengthLevel2';
+ this.passwordStrengthDescription = this.i18n('OK');
+ } else {
+ if (credits < 25) {
+ this.passwordStrengthLevel = 'passwordStrengthLevel3';
+ this.passwordStrengthDescription = this.i18n('Strong');
+ } else {
+ this.passwordStrengthLevel = 'passwordStrengthLevel4';
+ this.passwordStrengthDescription = this.i18n('Very strong');
+ }
+ }
+ }
+ }
+ }
+ return [this.passwordStrengthLevel, this.passwordStrengthDescription];
+ }
+
+ private checkPasswordComplexityLetters(password): number {
+ if (_.isString(password)) {
+ const digitsNumber = password.replace(/[^0-9]/g, '').length;
+ const smallLettersNumber = password.replace(/[^a-z]/g, '').length;
+ const bigLettersNumber = password.replace(/[^A-Z]/g, '').length;
+ const punctuationNumber = password.replace(/[^!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/g, '').length;
+ const othersCharactersNumber =
+ password.length -
+ (digitsNumber + smallLettersNumber + bigLettersNumber + punctuationNumber);
+ return (
+ digitsNumber +
+ smallLettersNumber +
+ bigLettersNumber * 2 +
+ punctuationNumber * 3 +
+ othersCharactersNumber * 5
+ );
+ } else {
+ return 0;
+ }
+ }
+}
.text-monospace {
font-family: monospace;
}
+.text-pre {
+ white-space: pre;
+}
/* Buttons */
.btn-light {
.form-group.has-error .invalid-feedback {
display: block;
}
+
+//Displaying the password strength
+.passwordStrengthLevel {
+ flex: 100%;
+ margin-top: 2px;
+ .passwordStrengthLevel1,
+ .passwordStrengthLevel2,
+ .passwordStrengthLevel3,
+ .passwordStrengthLevel4 {
+ border-radius: 0.25rem;
+ height: 13px;
+ }
+
+ .passwordStrengthLevel1 {
+ width: 25%;
+ background: $color-solid-red;
+ }
+ .passwordStrengthLevel2 {
+ width: 50%;
+ background: $color-bright-yellow;
+ }
+ .passwordStrengthLevel3 {
+ width: 75%;
+ background: $color-bright-green;
+ }
+ .passwordStrengthLevel4 {
+ width: 100%;
+ background: $color-green;
+ }
+}
$color-light-yellow: #fff3cd !default;
$color-bright-green: #00bb00 !default;
-// $color-green: #71843f !default;
+$color-green: #245e03 !default;
$color-blue: #288cea !default;
$color-light-blue: #d1ecf1 !default;
# pylint: disable=too-many-branches, too-many-locals, too-many-statements
from __future__ import absolute_import
+from string import punctuation, ascii_lowercase, digits, ascii_uppercase
+
import errno
import json
import threading
import time
+import re
import bcrypt
_P = Permission # short alias
+class PasswordCheck(object):
+ def __init__(self, password, username, old_password=None):
+ self.password = password
+ self.username = username
+ self.old_password = old_password
+ self.forbidden_words = ['osd', 'host', 'dashboard', 'pool',
+ 'block', 'nfs', 'ceph', 'monitors',
+ 'gateway', 'logs', 'crush', 'maps']
+ self.complexity_credits = 0
+
+ @staticmethod
+ def _check_if_contains_word(password, word):
+ return re.compile('(?:{0})'.format(word),
+ flags=re.IGNORECASE).search(password)
+
+ def check_password_characters(self):
+ digit_credit = 1
+ small_letter_credit = 1
+ big_letter_credit = 2
+ special_character_credit = 3
+ other_character_credit = 5
+ for _ in self.password:
+ if _ in ascii_uppercase:
+ self.complexity_credits += big_letter_credit
+ elif _ in ascii_lowercase:
+ self.complexity_credits += small_letter_credit
+ elif _ in digits:
+ self.complexity_credits += digit_credit
+ elif _ in punctuation:
+ self.complexity_credits += special_character_credit
+ else:
+ self.complexity_credits += other_character_credit
+ return self.complexity_credits
+
+ def check_if_as_the_old_password(self):
+ return self.old_password and self.password == self.old_password
+
+ def check_if_contains_username(self):
+ return self._check_if_contains_word(self.password, self.username)
+
+ def check_if_contains_forbidden_words(self):
+ return self._check_if_contains_word(self.password,
+ '|'.join(self.forbidden_words))
+
+ def check_if_sequential_characters(self):
+ for _ in range(1, len(self.password)-1):
+ if ord(self.password[_-1])+1 == ord(self.password[_])\
+ == ord(self.password[_+1])-1:
+ return True
+ return False
+
+ def check_if_repetetive_characters(self):
+ for _ in range(1, len(self.password)-1):
+ if self.password[_-1] == self.password[_] == self.password[_+1]:
+ return True
+ return False
+
+
class Role(object):
def __init__(self, name, description=None, scope_permissions=None):
self.name = name