]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Support minimum password complexity rules 29532/head
authorDziomdziora, El?bieta <elzbieta.dziomdziora@ts.fujitsu.coom>
Wed, 7 Aug 2019 13:49:20 +0000 (15:49 +0200)
committerDziomdziora, Elżbieta <elzbieta.dziomdziora@ts.fujitsu.com>
Fri, 13 Sep 2019 14:16:59 +0000 (16:16 +0200)
Fixes: https://tracker.ceph.com/issues/25232
Signed-off-by: Dziomdziora, Elżbieta <elzbieta.dziomdziora@ts.fujitsu.com>
14 files changed:
qa/tasks/mgr/dashboard/test_user.py
src/pybind/mgr/dashboard/controllers/user.py
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/user-change-password.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/styles.scss
src/pybind/mgr/dashboard/frontend/src/styles/defaults.scss
src/pybind/mgr/dashboard/services/access_control.py

index a72df6f1375081eb6c5d6633938f0b0cd2a8884d..5a3566f105e34dda9bd106901fe4900335f3f3a2 100644 (file)
@@ -25,7 +25,7 @@ class UserTest(DashboardTestCase):
 
     def test_crud_user(self):
         self._create_user(username='user1',
-                          password='mypassword',
+                          password='mypassword10#',
                           name='My Name',
                           email='my@email.com',
                           roles=['administrator'])
@@ -79,7 +79,7 @@ class UserTest(DashboardTestCase):
 
     def test_create_user_already_exists(self):
         self._create_user(username='admin',
-                          password='mypassword',
+                          password='mypassword10#',
                           name='administrator',
                           email='my@email.com',
                           roles=['administrator'])
@@ -89,7 +89,7 @@ class UserTest(DashboardTestCase):
 
     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'])
@@ -141,16 +141,71 @@ class UserTest(DashboardTestCase):
         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')
index 07728c88bf5df4ac99af0416da00105a44a4bee0..a8d63a21adca2b6fe6fde78a4633ccb460bf3cb5 100644 (file)
@@ -8,10 +8,41 @@ from .. import mgr
 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
@@ -52,6 +83,8 @@ class User(RESTController):
         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)
@@ -91,6 +124,7 @@ class User(RESTController):
         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
@@ -118,5 +152,6 @@ class UserChangePassword(BaseController):
             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()
index a752c7686ed5099e0dd698e719319ed5af947d09..2139e702442f5369abdb72b59135f88ce826a69e 100644 (file)
 
         <!-- 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>
 
index 603c19862726f49b18e2fd934b170ecaeaa31503..950f8af0a97187d98f0f10a7ed0c5a6f3fe75102 100644 (file)
@@ -101,6 +101,41 @@ describe('UserFormComponent', () => {
       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');
     });
index 0b300684caafa895edab1a4c2853dafc9af4994d..699b1802b0e4e29c3d2fd4b1fbfadd858cd11b3a 100644 (file)
@@ -12,11 +12,13 @@ import { UserService } from '../../../shared/api/user.service';
 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';
@@ -41,6 +43,10 @@ export class UserFormComponent implements OnInit {
   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,
@@ -52,7 +58,8 @@ export class UserFormComponent implements OnInit {
     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();
@@ -60,6 +67,7 @@ export class UserFormComponent implements OnInit {
   }
 
   createForm() {
+    this.requiredPasswordRulesMessage = this.userChangePasswordService.getPasswordRulesMessage();
     this.userForm = new CdFormGroup(
       {
         username: new FormControl('', {
@@ -67,7 +75,11 @@ export class UserFormComponent implements OnInit {
         }),
         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',
@@ -172,6 +184,14 @@ export class UserFormComponent implements OnInit {
     }
   }
 
+  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');
   }
index c0c134969c46db50fcbfd76b9d4e429e110718ed..502ece1f260f4fc23d84cde603581360f1f8d1fe 100644 (file)
@@ -32,6 +32,9 @@
             <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>
 
index cfd8e0c7120d8f5ffd68628a3e60a17de45afd0e..057f914be135e8258a2824266d10fa88fc77d481 100644 (file)
@@ -65,6 +65,41 @@ describe('UserPasswordFormComponent', () => {
     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({
index e9abf89baf13ea6cc71c9d4a55927221855e87c5..53ed7f3e884fc94e2ce1d690e86c74cdf2390c0a 100644 (file)
@@ -6,12 +6,14 @@ import { I18n } from '@ngx-translate/i18n-polyfill';
 
 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',
@@ -22,6 +24,10 @@ export class UserPasswordFormComponent {
   userForm: CdFormGroup;
   action: string;
   resource: string;
+  requiredPasswordRulesMessage: string;
+  passwordStrengthLevel: string;
+  passwordStrengthDescription: string;
+  icons = Icons;
 
   constructor(
     private i18n: I18n,
@@ -30,7 +36,8 @@ export class UserPasswordFormComponent {
     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');
@@ -38,9 +45,21 @@ export class UserPasswordFormComponent {
   }
 
   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,
           [
@@ -50,6 +69,9 @@ export class UserPasswordFormComponent {
                 this.userForm &&
                 this.userForm.getValue('oldpassword') === this.userForm.getValue('newpassword')
               );
+            }),
+            CdValidators.custom('checkPassword', () => {
+              return this.userForm && this.checkPassword(this.userForm.getValue('newpassword'));
             })
           ]
         ],
@@ -61,6 +83,14 @@ export class UserPasswordFormComponent {
     );
   }
 
+  checkPassword(password: string) {
+    [
+      this.passwordStrengthLevel,
+      this.passwordStrengthDescription
+    ] = this.userChangePasswordService.checkPasswordComplexity(password);
+    return password && this.passwordStrengthLevel === 'passwordStrengthLevel0';
+  }
+
   onSubmit() {
     if (this.userForm.pristine) {
       return;
index 5d9663b3d8cab83853f5a121b764dc7f385a3d94..1a09436c0de3aacb35cbffbf03a902de182ca11e 100644 (file)
@@ -1,5 +1,7 @@
 <ng-template #popoverTpl>
-  <div [innerHtml]="html"></div>
+  <div [class]="class"
+       [innerHtml]="html">
+  </div>
   <ng-content></ng-content>
 </ng-template>
 <i [ngClass]="[icons.questionCircle]"
index 996d78ba8d544bd68184fa1f70530f77e4e98805..227f260c67c1de41d798b892b1c4783e9e290107 100644 (file)
@@ -7,6 +7,9 @@ import { Icons } from '../../../shared/enum/icons.enum';
   styleUrls: ['./helper.component.scss']
 })
 export class HelperComponent {
+  @Input()
+  class: string;
+
   @Input()
   html: any;
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/user-change-password.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/user-change-password.service.ts
new file mode 100644 (file)
index 0000000..8cf8597
--- /dev/null
@@ -0,0 +1,82 @@
+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;
+    }
+  }
+}
index 70ceed0212ff84fe6848a711dbd8de5b27c15d3c..79785c5306254f2dc9a97493dcfa9c98f3d4a13f 100644 (file)
@@ -110,6 +110,9 @@ option {
 .text-monospace {
   font-family: monospace;
 }
+.text-pre {
+  white-space: pre;
+}
 
 /* Buttons */
 .btn-light {
@@ -397,3 +400,33 @@ bfv-messages {
 .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;
+  }
+}
index 288730d040ae53718e81e537e1c4424bfd66f01d..7b323683474ef50cef808b4c8ee9bd900aef07f8 100644 (file)
@@ -22,7 +22,7 @@ $color-bright-yellow: #ffc200 !default;
 $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;
index a4032f8dc47d16ec2c61d221198c8c0831d4ea9c..b00aeef3e39911375ade9e1bb6f7df1a048f77db 100644 (file)
@@ -3,10 +3,13 @@
 # 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
 
@@ -34,6 +37,64 @@ def password_hash(password, salt_password=None):
 _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