from datetime import datetime, timedelta
-from .helper import DashboardTestCase
+from .helper import DashboardTestCase, JObj, JLeaf
class UserTest(DashboardTestCase):
self._delete('/api/user/user1')
self._ceph_cmd(['dashboard', 'set-user-pwd-expiration-span', '0'])
+
+ def test_validate_password_weak(self):
+ data = self._post('/api/user/validate_password', {
+ 'password': 'mypassword1'
+ })
+ self.assertStatus(200)
+ self.assertSchema(data, JObj(sub_elems={
+ 'valid': JLeaf(bool),
+ 'credits': JLeaf(int),
+ 'valuation': JLeaf(str)
+ }))
+ self.assertTrue(data['valid'])
+ self.assertEqual(data['credits'], 11)
+ self.assertEqual(data['valuation'], 'Weak')
+
+ def test_validate_password_ok(self):
+ data = self._post('/api/user/validate_password', {
+ 'password': 'mypassword1!@'
+ })
+ self.assertStatus(200)
+ self.assertTrue(data['valid'])
+ self.assertEqual(data['credits'], 17)
+ self.assertEqual(data['valuation'], 'OK')
+
+ def test_validate_password_strong(self):
+ data = self._post('/api/user/validate_password', {
+ 'password': 'testpassword0047!@'
+ })
+ self.assertStatus(200)
+ self.assertTrue(data['valid'])
+ self.assertEqual(data['credits'], 22)
+ self.assertEqual(data['valuation'], 'Strong')
+
+ def test_validate_password_very_strong(self):
+ data = self._post('/api/user/validate_password', {
+ 'password': 'testpassword#!$!@$'
+ })
+ self.assertStatus(200)
+ self.assertTrue(data['valid'])
+ self.assertEqual(data['credits'], 30)
+ self.assertEqual(data['valuation'], 'Very strong')
+
+ def test_validate_password_fail(self):
+ data = self._post('/api/user/validate_password', {
+ 'password': 'foo'
+ })
+ self.assertStatus(200)
+ self.assertFalse(data['valid'])
+ self.assertEqual(data['credits'], 0)
+ self.assertEqual(data['valuation'], 'Password is too weak.')
+
+ def test_validate_password_fail_name(self):
+ data = self._post('/api/user/validate_password', {
+ 'password': 'x1zhugo_10',
+ 'username': 'hugo'
+ })
+ self.assertStatus(200)
+ self.assertFalse(data['valid'])
+ self.assertEqual(data['credits'], 0)
+ self.assertEqual(data['valuation'], 'Password cannot contain username.')
+
+ def test_validate_password_fail_oldpwd(self):
+ data = self._post('/api/user/validate_password', {
+ 'password': 'x1zt-st10',
+ 'old_password': 'x1zt-st10'
+ })
+ self.assertStatus(200)
+ self.assertFalse(data['valid'])
+ self.assertEqual(data['credits'], 0)
+ self.assertEqual(data['valuation'], 'Password cannot be the same as the previous one.')
+
+ @DashboardTestCase.RunAs('test', 'test', [{'user': ['read', 'delete']}])
+ def test_validate_password_invalid_permissions(self):
+ self._post('/api/user/validate_password', {
+ 'password': 'foo'
+ })
+ self.assertStatus(403)
+ self.assertError(code='invalid_credentials', component='auth')
def ReadPermission(func): # noqa: N802
+ """
+ :raises PermissionNotValid: If the permission is missing.
+ """
_set_func_permissions(func, Permission.READ)
return func
def CreatePermission(func): # noqa: N802
+ """
+ :raises PermissionNotValid: If the permission is missing.
+ """
_set_func_permissions(func, Permission.CREATE)
return func
def DeletePermission(func): # noqa: N802
+ """
+ :raises PermissionNotValid: If the permission is missing.
+ """
_set_func_permissions(func, Permission.DELETE)
return func
def UpdatePermission(func): # noqa: N802
+ """
+ :raises PermissionNotValid: If the permission is missing.
+ """
_set_func_permissions(func, Permission.UPDATE)
return func
from . import BaseController, ApiController, RESTController, Endpoint
from .. import mgr
from ..exceptions import DashboardException, UserAlreadyExists, \
- UserDoesNotExist, PasswordCheckException, PwdExpirationDateNotValid
+ UserDoesNotExist, PasswordPolicyException, PwdExpirationDateNotValid
from ..security import Scope
-from ..services.access_control import SYSTEM_ROLES, PasswordCheck
+from ..services.access_control import SYSTEM_ROLES, PasswordPolicy
from ..services.auth import JwtManager
-def validate_password_policy(password, username, old_password=None):
- pw_check = PasswordCheck(password, username, old_password)
+def validate_password_policy(password, username=None, old_password=None):
+ """
+ :param password: The password to validate.
+ :param username: The name of the user (optional).
+ :param old_password: The old password (optional).
+ :return: Returns the password complexity credits.
+ :rtype: int
+ :raises DashboardException: If a password policy fails.
+ """
+ pw_policy = PasswordPolicy(password, username, old_password)
try:
- pw_check.check_all()
- except PasswordCheckException as ex:
+ pw_policy.check_all()
+ return pw_policy.complexity_credits
+ except PasswordPolicyException as ex:
raise DashboardException(msg=str(ex),
code='password_policy_validation_failed',
component='user')
return User._user_to_dict(user)
+@ApiController('/user')
+class UserPasswordPolicy(RESTController):
+ @Endpoint('POST')
+ def validate_password(self, password, username=None, old_password=None):
+ """
+ Check if the password meets the password policy.
+ :param password: The password to validate.
+ :param username: The name of the user (optional).
+ :param old_password: The old password (optional).
+ :return: An object with the properties valid, credits and valuation.
+ 'credits' contains the password complexity credits and
+ 'valuation' the textual summary of the validation.
+ """
+ result = {'valid': False, 'credits': 0, 'valuation': None}
+ try:
+ result['credits'] = validate_password_policy(password, username, old_password)
+ if result['credits'] < 15:
+ result['valuation'] = 'Weak'
+ elif result['credits'] < 20:
+ result['valuation'] = 'OK'
+ elif result['credits'] < 25:
+ result['valuation'] = 'Strong'
+ else:
+ result['valuation'] = 'Very strong'
+ result['valid'] = True
+ except DashboardException as ex:
+ result['valuation'] = str(ex)
+ return result
+
+
@ApiController('/user/{username}')
class UserChangePassword(BaseController):
@Endpoint('POST')
pass
-class PasswordCheckException(Exception):
+class PasswordPolicyException(Exception):
pass
for="password">
<ng-container i18n>Password</ng-container>
<cd-helper class="text-pre"
- html="{{ requiredPasswordRulesMessage }}">
+ html="{{ passwordPolicyHelpText }}">
</cd-helper>
</label>
<div class="col-sm-9">
</button>
</span>
</div>
- <div class="passwordStrengthLevel">
- <div class="{{ passwordStrengthLevel }}"
+ <div class="password-strength-level">
+ <div class="{{ passwordStrengthLevelClass }}"
data-toggle="tooltip"
- title="{{ passwordStrengthDescription }}">
+ title="{{ passwordValuation }}">
</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>
+ *ngIf="userForm.showError('password', formDir, 'passwordPolicy')">
+ {{ passwordValuation }}
+ </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 { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
-import { FormControl, Validators } from '@angular/forms';
+import { Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { I18n } from '@ngx-translate/i18n-polyfill';
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 { CdPwdExpirationSettings } from '../../../shared/models/cd-pwd-expiration-settings';
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 { PasswordPolicyService } from '../../../shared/services/password-policy.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: this.i18n('There are no roles.') }, this.i18n);
action: string;
resource: string;
- requiredPasswordRulesMessage: string;
- passwordStrengthLevel: string;
- passwordStrengthDescription: string;
+ passwordPolicyHelpText: string;
+ passwordStrengthLevelClass: string;
+ passwordValuation: string;
icons = Icons;
minDate: Date;
bsConfig = {
private notificationService: NotificationService,
private i18n: I18n,
public actionLabels: ActionLabelsI18n,
- private userChangePasswordService: UserChangePasswordService,
+ private passwordPolicyService: PasswordPolicyService,
+ private formBuilder: CdFormBuilder,
private settingsService: SettingsService
) {
this.resource = this.i18n('user');
}
createForm() {
- this.requiredPasswordRulesMessage = this.userChangePasswordService.getPasswordRulesMessage();
- this.userForm = new CdFormGroup(
+ this.passwordPolicyHelpText = this.passwordPolicyService.getHelpText();
+ this.userForm = this.formBuilder.group(
{
- username: new FormControl('', {
- validators: [Validators.required]
- }),
- name: new FormControl(''),
- password: new FormControl('', {
- validators: [
- CdValidators.custom('checkPassword', () => {
- return this.userForm && this.checkPassword(this.userForm.getValue('password'));
- })
+ username: ['', [Validators.required]],
+ name: [''],
+ password: [
+ '',
+ [],
+ [
+ CdValidators.passwordPolicy(
+ this.userService,
+ () => this.userForm.getValue('username'),
+ (_valid: boolean, credits: number, valuation: string) => {
+ this.passwordStrengthLevelClass = this.passwordPolicyService.mapCreditsToCssClass(
+ credits
+ );
+ this.passwordValuation = _.defaultTo(valuation, '');
+ }
+ )
]
- }),
- confirmpassword: new FormControl('', {
- updateOn: 'blur',
- validators: []
- }),
- pwdExpirationDate: new FormControl(''),
- email: new FormControl('', {
- validators: [Validators.email]
- }),
- roles: new FormControl([]),
- enabled: new FormControl(true, {
- validators: [Validators.required]
- })
+ ],
+ confirmpassword: [''],
+ pwdExpirationDate: [''],
+ email: ['', [CdValidators.email]],
+ roles: [[]],
+ enabled: [true, [Validators.required]]
},
{
validators: [CdValidators.match('password', 'confirmpassword')]
}
}
- checkPassword(password: string) {
- [
- this.passwordStrengthLevel,
- this.passwordStrengthDescription
- ] = this.userChangePasswordService.checkPasswordComplexity(password);
- return password && this.passwordStrengthLevel === 'passwordStrengthLevel0';
- }
-
showExpirationDateField() {
return (
this.userForm.getValue('pwdExpirationDate') > 0 ||
for="newpassword">
<ng-container i18n>New password</ng-container>
<cd-helper class="text-pre"
- html="{{ requiredPasswordRulesMessage }}">
+ html="{{ passwordPolicyHelpText }}">
</cd-helper>
<span class="required"></span>
</label>
</button>
</span>
</div>
- <div class="passwordStrengthLevel">
- <div class="{{ passwordStrengthLevel }}"
+ <div class="password-strength-level">
+ <div class="{{ passwordStrengthLevelClass }}"
data-toggle="tooltip"
- title="{{ passwordStrengthDescription }}">
+ title="{{ passwordValuation }}">
</div>
</div>
<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>
+ *ngIf="userForm.showError('newpassword', frm, 'passwordPolicy')">
+ {{ passwordValuation }}
+ </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 { Router } from '@angular/router';
import { I18n } from '@ngx-translate/i18n-polyfill';
+import * as _ from 'lodash';
import { UserService } from '../../../shared/api/user.service';
import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
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 { PasswordPolicyService } from '../../../shared/services/password-policy.service';
@Component({
selector: 'cd-user-password-form',
userForm: CdFormGroup;
action: string;
resource: string;
- requiredPasswordRulesMessage: string;
- passwordStrengthLevel: string;
- passwordStrengthDescription: string;
+ passwordPolicyHelpText: string;
+ passwordStrengthLevelClass: string;
+ passwordValuation: string;
icons = Icons;
constructor(
private authStorageService: AuthStorageService,
private formBuilder: CdFormBuilder,
private router: Router,
- private userChangePasswordService: UserChangePasswordService
+ private passwordPolicyService: PasswordPolicyService
) {
this.action = this.actionLabels.CHANGE;
this.resource = this.i18n('password');
}
createForm() {
- this.requiredPasswordRulesMessage = this.userChangePasswordService.getPasswordRulesMessage();
+ this.passwordPolicyHelpText = this.passwordPolicyService.getHelpText();
this.userForm = this.formBuilder.group(
{
oldpassword: [
this.userForm &&
this.userForm.getValue('oldpassword') === this.userForm.getValue('newpassword')
);
- }),
- CdValidators.custom('checkPassword', () => {
- return this.userForm && this.checkPassword(this.userForm.getValue('newpassword'));
})
+ ],
+ [
+ CdValidators.passwordPolicy(
+ this.userService,
+ () => this.authStorageService.getUsername(),
+ (_valid: boolean, credits: number, valuation: string) => {
+ this.passwordStrengthLevelClass = this.passwordPolicyService.mapCreditsToCssClass(
+ credits
+ );
+ this.passwordValuation = _.defaultTo(valuation, '');
+ }
+ )
]
],
confirmnewpassword: [null, [Validators.required]]
);
}
- checkPassword(password: string) {
- [
- this.passwordStrengthLevel,
- this.passwordStrengthDescription
- ] = this.userChangePasswordService.checkPasswordComplexity(password);
- return password && this.passwordStrengthLevel === 'passwordStrengthLevel0';
- }
-
onSubmit() {
if (this.userForm.pristine) {
return;
});
expect(req.request.method).toBe('POST');
});
+
+ it('should call validatePassword', () => {
+ service.validatePassword('foo').subscribe();
+ const req = httpTesting.expectOne('api/user/validate_password?password=foo');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call validatePassword (incl. name)', () => {
+ service.validatePassword('foo_bar', 'bar').subscribe();
+ const req = httpTesting.expectOne('api/user/validate_password?password=foo_bar&username=bar');
+ expect(req.request.method).toBe('POST');
+ });
+
+ it('should call validatePassword (incl. old password)', () => {
+ service.validatePassword('foo', null, 'foo').subscribe();
+ const req = httpTesting.expectOne('api/user/validate_password?password=foo&old_password=foo');
+ expect(req.request.method).toBe('POST');
+ });
});
-import { HttpClient } from '@angular/common/http';
+import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { UserFormModel } from '../../core/auth/user-form/user-form.model';
new_password: newPassword
});
}
+
+ validatePassword(password: string, username: string = null, oldPassword: string = null) {
+ let params = new HttpParams();
+ params = params.append('password', password);
+ if (username) {
+ params = params.append('username', username);
+ }
+ if (oldPassword) {
+ params = params.append('old_password', oldPassword);
+ }
+ return this.http.post('api/user/validate_password', null, { params });
+ }
}
);
});
});
+
+ describe('passwordPolicy', () => {
+ let valid: boolean;
+ let callbackCalled: boolean;
+
+ const fakeUserService = {
+ validatePassword: () => {
+ return observableOf({ valid: valid, credits: 17, valuation: 'foo' });
+ }
+ };
+
+ beforeEach(() => {
+ callbackCalled = false;
+ form = new CdFormGroup({
+ x: new FormControl(
+ '',
+ null,
+ CdValidators.passwordPolicy(
+ fakeUserService,
+ () => 'admin',
+ () => {
+ callbackCalled = true;
+ }
+ )
+ )
+ });
+ formHelper = new FormHelper(form);
+ });
+
+ it('should not error because of empty input', () => {
+ expectValid('');
+ expect(callbackCalled).toBeTruthy();
+ });
+
+ it('should not error because password matches the policy', fakeAsync(() => {
+ valid = true;
+ formHelper.setValue('x', 'abc', true);
+ tick(500);
+ formHelper.expectValid('x');
+ }));
+
+ it('should error because password does not match the policy', fakeAsync(() => {
+ valid = false;
+ formHelper.setValue('x', 'xyz', true);
+ tick(500);
+ formHelper.expectError('x', 'passwordPolicy');
+ }));
+
+ it('should call the callback function', fakeAsync(() => {
+ formHelper.setValue('x', 'xyz', true);
+ tick(500);
+ expect(callbackCalled).toBeTruthy();
+ }));
+ });
});
};
};
}
+
+ /**
+ * Asynchronous validator that checks if the password meets the password
+ * policy.
+ * @param userServiceThis The object to be used as the 'this' object
+ * when calling the 'validatePassword' method of the 'UserService'.
+ * @param usernameFn Function to get the username that should be
+ * taken into account.
+ * @param callback Callback function that is called after the validation
+ * has been done.
+ * @return {AsyncValidatorFn} Returns an asynchronous validator function
+ * that returns an error map with the `passwordPolicy` property if the
+ * validation check fails, otherwise `null`.
+ */
+ static passwordPolicy(
+ userServiceThis: any,
+ usernameFn?: Function,
+ callback?: (valid: boolean, credits?: number, valuation?: string) => void
+ ): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<ValidationErrors | null> => {
+ if (control.pristine || control.value === '') {
+ if (_.isFunction(callback)) {
+ callback(true, 0);
+ }
+ return observableOf(null);
+ }
+ let username;
+ if (_.isFunction(usernameFn)) {
+ username = usernameFn();
+ }
+ return observableTimer(500).pipe(
+ switchMapTo(_.invoke(userServiceThis, 'validatePassword', control.value, username)),
+ map((resp: { valid: boolean; credits: number; valuation: string }) => {
+ if (_.isFunction(callback)) {
+ callback(resp.valid, resp.credits, resp.valuation);
+ }
+ if (resp.valid) {
+ return null;
+ } else {
+ return { passwordPolicy: true };
+ }
+ }),
+ take(1)
+ );
+ };
+ }
}
--- /dev/null
+import { Injectable } from '@angular/core';
+
+import { I18n } from '@ngx-translate/i18n-polyfill';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class PasswordPolicyService {
+ constructor(private i18n: I18n) {}
+
+ getHelpText() {
+ 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 sequential 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)'
+ );
+ }
+
+ /**
+ * Helper function to map password policy credits to a CSS class.
+ * @param credits The password policy credits.
+ * @return The name of the CSS class.
+ */
+ mapCreditsToCssClass(credits: number): string {
+ let result = 'very-strong';
+ if (credits < 10) {
+ result = 'too-weak';
+ } else if (credits < 15) {
+ result = 'weak';
+ } else if (credits < 20) {
+ result = 'ok';
+ } else if (credits < 25) {
+ result = 'strong';
+ }
+ return result;
+ }
+}
+++ /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;
- }
- }
-}
}
//Displaying the password strength
-.passwordStrengthLevel {
+.password-strength-level {
flex: 100%;
margin-top: 2px;
- .passwordStrengthLevel1,
- .passwordStrengthLevel2,
- .passwordStrengthLevel3,
- .passwordStrengthLevel4 {
+ .weak,
+ .ok,
+ .strong,
+ .very-strong {
border-radius: 0.25rem;
height: 13px;
}
- .passwordStrengthLevel1 {
+ .weak {
width: 25%;
background: $color-solid-red;
}
- .passwordStrengthLevel2 {
+ .ok {
width: 50%;
background: $color-bright-yellow;
}
- .passwordStrengthLevel3 {
+ .strong {
width: 75%;
background: $color-bright-green;
}
- .passwordStrengthLevel4 {
+ .very-strong {
width: 100%;
background: $color-green;
}
from ..exceptions import RoleAlreadyExists, RoleDoesNotExist, ScopeNotValid, \
PermissionNotValid, RoleIsAssociatedWithUser, \
UserAlreadyExists, UserDoesNotExist, ScopeNotInRole, \
- RoleNotInUser, PasswordCheckException, PwdExpirationDateNotValid
+ RoleNotInUser, PasswordPolicyException, PwdExpirationDateNotValid
logger = logging.getLogger('access_control')
_P = Permission # short alias
-class PasswordCheck(object):
- def __init__(self, password, username, old_password=None):
+class PasswordPolicy(object):
+ def __init__(self, password, username=None, old_password=None):
"""
:param password: The new plain password.
:type password: str
:param username: The name of the user.
- :type username: str
+ :type username: str | None
:param old_password: The old plain password.
:type old_password: str | None
"""
big_letter_credit = 2
special_character_credit = 3
other_character_credit = 5
+ self.complexity_credits = 0
for ch in self.password:
if ch in ascii_uppercase:
self.complexity_credits += big_letter_credit
return self.old_password and self.password == self.old_password
def check_if_contains_username(self):
+ if not self.username:
+ return False
return self._check_if_contains_word(self.password, self.username)
def check_if_contains_forbidden_words(self):
return True
return False
+ def check_password_length(self, min_length=8):
+ return len(self.password) >= min_length
+
def check_all(self):
"""
Perform all password policy checks.
- :raise PasswordCheckException: If a password policy check fails.
+ :raise PasswordPolicyException: If a password policy check fails.
"""
+ if self.check_password_characters() < 10 or not self.check_password_length():
+ raise PasswordPolicyException('Password is too weak.')
if self.check_is_old_password():
- raise PasswordCheckException('Password cannot be the same as the previous one.')
+ raise PasswordPolicyException('Password cannot be the same as the previous one.')
if self.check_if_contains_username():
- raise PasswordCheckException('Password cannot contain username.')
+ raise PasswordPolicyException('Password cannot contain username.')
if self.check_if_contains_forbidden_words():
- raise PasswordCheckException('Password cannot contain keywords.')
+ raise PasswordPolicyException('Password cannot contain keywords.')
if self.check_if_repetetive_characters():
- raise PasswordCheckException('Password cannot contain repetitive characters.')
+ raise PasswordPolicyException('Password cannot contain repetitive characters.')
if self.check_if_sequential_characters():
- raise PasswordCheckException('Password cannot contain sequential characters.')
- if self.check_password_characters() < 10:
- raise PasswordCheckException('Password is too weak.')
+ raise PasswordPolicyException('Password cannot contain sequential characters.')
class Role(object):
try:
if not force_password:
- pw_check = PasswordCheck(password, username)
+ pw_check = PasswordPolicy(password, username)
pw_check.check_all()
user = mgr.ACCESS_CTRL_DB.create_user(username, password, name, email,
enabled, pwd_expiration_date)
- except PasswordCheckException as ex:
+ except PasswordPolicyException as ex:
return -errno.EINVAL, '', str(ex)
except UserAlreadyExists as ex:
return -errno.EEXIST, '', str(ex)
try:
user = mgr.ACCESS_CTRL_DB.get_user(username)
if not force_password:
- pw_check = PasswordCheck(password, user.name)
+ pw_check = PasswordPolicy(password, user.name)
pw_check.check_all()
user.set_password(password)
mgr.ACCESS_CTRL_DB.save()
return 0, json.dumps(user.to_dict()), ''
- except PasswordCheckException as ex:
+ except PasswordPolicyException as ex:
return -errno.EINVAL, '', str(ex)
except UserDoesNotExist as ex:
return -errno.ENOENT, '', str(ex)
from ..security import Scope, Permission
from ..services.access_control import load_access_control_db, \
password_hash, AccessControlDB, \
- SYSTEM_ROLES
+ SYSTEM_ROLES, PasswordPolicy
class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
'roles': ['administrator'],
'enabled': True
})
+
+ def test_password_policy_pw_length(self):
+ pw_policy = PasswordPolicy('foo')
+ self.assertTrue(pw_policy.check_password_length(3))
+
+ def test_password_policy_pw_length_fail(self):
+ pw_policy = PasswordPolicy('bar')
+ self.assertFalse(pw_policy.check_password_length())
+
+ def test_password_policy_credits_too_weak(self):
+ pw_policy = PasswordPolicy('foo')
+ pw_credits = pw_policy.check_password_characters()
+ self.assertEqual(pw_credits, 3)
+
+ def test_password_policy_credits_weak(self):
+ pw_policy = PasswordPolicy('mypassword1')
+ pw_credits = pw_policy.check_password_characters()
+ self.assertEqual(pw_credits, 11)
+
+ def test_password_policy_credits_ok(self):
+ pw_policy = PasswordPolicy('mypassword1!@')
+ pw_credits = pw_policy.check_password_characters()
+ self.assertEqual(pw_credits, 17)
+
+ def test_password_policy_credits_strong(self):
+ pw_policy = PasswordPolicy('testpassword0047!@')
+ pw_credits = pw_policy.check_password_characters()
+ self.assertEqual(pw_credits, 22)
+
+ def test_password_policy_credits_very_strong(self):
+ pw_policy = PasswordPolicy('testpassword#!$!@$')
+ pw_credits = pw_policy.check_password_characters()
+ self.assertEqual(pw_credits, 30)
+
+ def test_password_policy_forbidden_words(self):
+ pw_policy = PasswordPolicy('!@$testdashboard#!$')
+ self.assertTrue(pw_policy.check_if_contains_forbidden_words())
+
+ def test_password_policy_sequential_chars(self):
+ pw_policy = PasswordPolicy('!@$test123#!$')
+ self.assertTrue(pw_policy.check_if_sequential_characters())
+
+ def test_password_policy_repetetive_chars(self):
+ pw_policy = PasswordPolicy('!@$testfooo#!$')
+ self.assertTrue(pw_policy.check_if_repetetive_characters())
+
+ def test_password_policy_contain_username(self):
+ pw_policy = PasswordPolicy('%admin135)', 'admin')
+ self.assertTrue(pw_policy.check_if_contains_username())
+
+ def test_password_policy_is_old_pwd(self):
+ pw_policy = PasswordPolicy('foo', old_password='foo')
+ self.assertTrue(pw_policy.check_is_old_password())