]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add password expiration date
authorTatjana Dehler <tdehler@suse.com>
Fri, 13 Sep 2019 12:08:39 +0000 (14:08 +0200)
committerTatjana Dehler <tdehler@suse.com>
Fri, 13 Dec 2019 07:53:05 +0000 (08:53 +0100)
Add a 'pwd_expiration_date' field to the User object to be able
to set password expiration date per user.

There are two options to set a password expiration date for a
user:

1. Set the 'USER_PWD_EXPIRATION_SPAN' setting. If defined
   the expiration date of the password will be calculated
   automatically based on the given value. It will also be
   updated automatically when the user changes the password.
2. Set an expiration date by setting the 'pwdExpirationDate'
   field when creating a user.

Add two more settings in addition: USER_PWD_EXPIRATION_WARNING_1
and USER_PWD_EXPIRATION_WARNING_2. These two settings are
defining the amount of days to notify the user that his password
will expiration soon.

It's not possible to set the 'pwd_expiration_date' field to a
date in the past.

If the password of a user is already expired he is no longer
allowed to log into the dashboard.

Fixes: https://tracker.ceph.com/issues/40329
Signed-off-by: Tatjana Dehler <tdehler@suse.com>
23 files changed:
doc/mgr/dashboard.rst
qa/tasks/mgr/dashboard/test_auth.py
qa/tasks/mgr/dashboard/test_user.py
src/pybind/mgr/dashboard/.pylintrc
src/pybind/mgr/dashboard/controllers/auth.py
src/pybind/mgr/dashboard/controllers/settings.py
src/pybind/mgr/dashboard/controllers/user.py
src/pybind/mgr/dashboard/exceptions.py
src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts
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-form/user-form.model.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-expiration-settings.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts
src/pybind/mgr/dashboard/services/access_control.py
src/pybind/mgr/dashboard/settings.py
src/pybind/mgr/dashboard/tests/test_access_control.py

index 4944e917e14775d2cd0dcdd1ff139aeb4b561576..ae810bd6415a62c57ad006c4b26abf0407f86e61 100644 (file)
@@ -622,7 +622,7 @@ We provide a set of CLI commands to manage user accounts:
 
 - *Create User*::
 
-  $ ceph dashboard ac-user-create [--force-password] <username> [<password>] [<rolename>] [<name>] [<email>] [--enabled]
+  $ ceph dashboard ac-user-create [--force-password] <username> [<password>] [<rolename>] [<name>] [<email>] [--enabled] [<pwd_expiration_date>]
 
 - *Delete User*::
 
index 0fd5b3080c98fbb47a15727b8d1f86e80403d536..44a8d0d56dfde109b6fe379b6beacd0b3ed8aa2f 100644 (file)
@@ -44,7 +44,8 @@ class AuthTest(DashboardTestCase):
             'token': JLeaf(str),
             'username': JLeaf(str),
             'permissions': JObj(sub_elems={}, allow_unknown=True),
-            'sso': JLeaf(bool)
+            'sso': JLeaf(bool),
+            'pwdExpirationDate': JLeaf(int, none=True)
         }, allow_unknown=False))
         self._validate_jwt_token(data['token'], "admin", data['permissions'])
 
index a66228539a8942b6c48ae16f7c729367cab7e946..115e554ec11db46c26b95c398cf2725c16fa842c 100644 (file)
@@ -4,12 +4,15 @@ from __future__ import absolute_import
 
 import time
 
+from datetime import datetime, timedelta
+
 from .helper import DashboardTestCase
 
 
 class UserTest(DashboardTestCase):
     @classmethod
-    def _create_user(cls, username=None, password=None, name=None, email=None, roles=None, enabled=True):
+    def _create_user(cls, username=None, password=None, name=None, email=None, roles=None,
+                     enabled=True, pwd_expiration_date=None):
         data = {}
         if username:
             data['username'] = username
@@ -21,13 +24,16 @@ class UserTest(DashboardTestCase):
             data['email'] = email
         if roles:
             data['roles'] = roles
+        if pwd_expiration_date:
+            data['pwdExpirationDate'] = pwd_expiration_date
         data['enabled'] = enabled
         cls._post("/api/user", data)
 
     @classmethod
-    def _reset_login_to_admin(cls, username):
+    def _reset_login_to_admin(cls, username=None):
         cls.logout()
-        cls.delete_user(username)
+        if username:
+            cls.delete_user(username)
         cls.login('admin', 'admin')
 
     def test_crud_user(self):
@@ -47,7 +53,8 @@ class UserTest(DashboardTestCase):
             'email': 'my@email.com',
             'roles': ['administrator'],
             'lastUpdate': user['lastUpdate'],
-            'enabled': True
+            'enabled': True,
+            'pwdExpirationDate': None
         })
 
         self._put('/api/user/user1', {
@@ -63,7 +70,8 @@ class UserTest(DashboardTestCase):
             'email': 'mynew@email.com',
             'roles': ['block-manager'],
             'lastUpdate': user['lastUpdate'],
-            'enabled': True
+            'enabled': True,
+            'pwdExpirationDate': None
         })
 
         self._delete('/api/user/user1')
@@ -92,7 +100,8 @@ class UserTest(DashboardTestCase):
             'email': 'klara@musterfrau.com',
             'roles': ['administrator'],
             'lastUpdate': user['lastUpdate'],
-            'enabled': False
+            'enabled': False,
+            'pwdExpirationDate': None
         })
 
         self._delete('/api/user/klara')
@@ -110,7 +119,8 @@ class UserTest(DashboardTestCase):
             'email': None,
             'roles': ['administrator'],
             'lastUpdate': user['lastUpdate'],
-            'enabled': True
+            'enabled': True,
+            'pwdExpirationDate': None
         }])
 
     def test_create_user_already_exists(self):
@@ -290,3 +300,92 @@ class UserTest(DashboardTestCase):
                                           'test4', 'bar'])
         self.assertNotEqual(exitcode, 0)
         self.delete_user('test4')
+
+    def test_create_user_with_pwd_expiration_date(self):
+        future_date = datetime.utcnow() + timedelta(days=10)
+        future_date = int(time.mktime(future_date.timetuple()))
+
+        self._create_user(username='user1',
+                          password='mypassword10#',
+                          name='My Name',
+                          email='my@email.com',
+                          roles=['administrator'],
+                          pwd_expiration_date=future_date)
+        self.assertStatus(201)
+        user = self.jsonBody()
+
+        self._get('/api/user/user1')
+        self.assertStatus(200)
+        self.assertJsonBody({
+            'username': 'user1',
+            'name': 'My Name',
+            'email': 'my@email.com',
+            'roles': ['administrator'],
+            'lastUpdate': user['lastUpdate'],
+            'enabled': True,
+            'pwdExpirationDate': future_date
+        })
+        self._delete('/api/user/user1')
+
+    def test_create_with_pwd_expiration_date_not_valid(self):
+        past_date = datetime.utcnow() - timedelta(days=10)
+        past_date = int(time.mktime(past_date.timetuple()))
+
+        self._create_user(username='user1',
+                          password='mypassword10#',
+                          name='My Name',
+                          email='my@email.com',
+                          roles=['administrator'],
+                          pwd_expiration_date=past_date)
+        self.assertStatus(400)
+        self.assertError(code='pwd_past_expiration_date', component='user')
+
+    def test_create_with_default_expiration_date(self):
+        future_date_1 = datetime.utcnow() + timedelta(days=10)
+        future_date_1 = int(time.mktime(future_date_1.timetuple()))
+        future_date_2 = datetime.utcnow() + timedelta(days=11)
+        future_date_2 = int(time.mktime(future_date_2.timetuple()))
+
+        self._ceph_cmd(['dashboard', 'set-user-pwd-expiration-span', '10'])
+        self._create_user(username='user1',
+                          password='mypassword10#',
+                          name='My Name',
+                          email='my@email.com',
+                          roles=['administrator'])
+        self.assertStatus(201)
+
+        user = self._get('/api/user/user1')
+        self.assertStatus(200)
+        self.assertIsNotNone(user['pwdExpirationDate'])
+        self.assertGreater(user['pwdExpirationDate'], future_date_1)
+        self.assertLess(user['pwdExpirationDate'], future_date_2)
+
+        self._delete('/api/user/user1')
+        self._ceph_cmd(['dashboard', 'set-user-pwd-expiration-span', '0'])
+
+    def test_pwd_expiration_date_update(self):
+        self._ceph_cmd(['dashboard', 'set-user-pwd-expiration-span', '10'])
+        self._create_user(username='user1',
+                          password='mypassword10#',
+                          name='My Name',
+                          email='my@email.com',
+                          roles=['administrator'])
+        self.assertStatus(201)
+
+        user_1 = self._get('/api/user/user1')
+        self.assertStatus(200)
+
+        self.login('user1', 'mypassword10#')
+        self._post('/api/user/user1/change_password', {
+            'old_password': 'mypassword10#',
+            'new_password': 'newpassword01#'
+        })
+        self.assertStatus(200)
+        self._reset_login_to_admin()
+
+        user_2 = self._get('/api/user/user1')
+        self.assertStatus(200)
+        self.assertLess(user_1['pwdExpirationDate'], user_2['pwdExpirationDate'])
+
+        self._delete('/api/user/user1')
+        self._ceph_cmd(['dashboard', 'set-user-pwd-expiration-span', '0'])
index 8efd540f49e530f2a4f596e89dfa8aa81715dda1..5cec25f4ce4069da324bf983980fdb43a2f12799 100644 (file)
@@ -502,7 +502,7 @@ valid-metaclass-classmethod-first-arg=mcs
 max-args=5
 
 # Maximum number of attributes for a class (see R0902).
-max-attributes=7
+max-attributes=10
 
 # Maximum number of boolean expressions in a if statement
 max-bool-expr=5
index 4c4b1a6027ce55357a4f8162d6d03458277493d7..44128ce3201a128f4c2c0cc523a060f05c6d4ffd 100644 (file)
@@ -20,7 +20,12 @@ class Auth(RESTController):
     """
 
     def create(self, username, password):
-        user_perms = AuthManager.authenticate(username, password)
+        user_data = AuthManager.authenticate(username, password)
+        user_perms, pwd_expiration_date = None, None
+        if user_data:
+            user_perms = user_data.get('permissions')
+            pwd_expiration_date = user_data.get('pwdExpirationDate')
+
         if user_perms is not None:
             logger.debug('Login successful')
             token = JwtManager.gen_token(username)
@@ -30,6 +35,7 @@ class Auth(RESTController):
                 'token': token,
                 'username': username,
                 'permissions': user_perms,
+                'pwdExpirationDate': pwd_expiration_date,
                 'sso': mgr.SSO_DB.protocol == 'saml2'
             }
 
index a484580ee1c1a65887b5bc795851469e217361b4..19e177c61c0c37cb832d0fc0c13bfd185edf9dcb 100644 (file)
@@ -4,7 +4,7 @@ from contextlib import contextmanager
 
 import cherrypy
 
-from . import ApiController, RESTController
+from . import ApiController, RESTController, UiApiController
 from ..settings import Settings as SettingsModule, Options
 from ..security import Scope
 
@@ -66,3 +66,13 @@ class Settings(RESTController):
         with self._attribute_handler(kwargs) as data:
             for name, value in data.items():
                 setattr(SettingsModule, self._to_native(name), value)
+
+
+@UiApiController('/standard_settings')
+class StandardSettings(RESTController):
+    def list(self):
+        return {
+            'user_pwd_expiration_span': SettingsModule.USER_PWD_EXPIRATION_SPAN,
+            'user_pwd_expiration_warning_1': SettingsModule.USER_PWD_EXPIRATION_WARNING_1,
+            'user_pwd_expiration_warning_2': SettingsModule.USER_PWD_EXPIRATION_WARNING_2
+        }
index 560471957aff761803142631fbee7aa5b9671ce0..645f5cbc2be016e0f9f7b647e60faa7be4f0ca0b 100644 (file)
@@ -1,12 +1,16 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
+from datetime import datetime
+
+import time
+
 import cherrypy
 
 from . import BaseController, ApiController, RESTController, Endpoint
 from .. import mgr
 from ..exceptions import DashboardException, UserAlreadyExists, \
-    UserDoesNotExist, PasswordCheckException
+    UserDoesNotExist, PasswordCheckException, PwdExpirationDateNotValid
 from ..security import Scope
 from ..services.access_control import SYSTEM_ROLES, PasswordCheck
 from ..services.auth import JwtManager
@@ -54,7 +58,7 @@ class User(RESTController):
         return User._user_to_dict(user)
 
     def create(self, username=None, password=None, name=None, email=None,
-               roles=None, enabled=True):
+               roles=None, enabled=True, pwdExpirationDate=None):
         if not username:
             raise DashboardException(msg='Username is required',
                                      code='username_required',
@@ -66,11 +70,17 @@ class User(RESTController):
             validate_password_policy(password, username)
         try:
             user = mgr.ACCESS_CTRL_DB.create_user(username, password, name,
-                                                  email, enabled)
+                                                  email, enabled, pwdExpirationDate)
         except UserAlreadyExists:
             raise DashboardException(msg='Username already exists',
                                      code='username_already_exists',
                                      component='user')
+        except PwdExpirationDateNotValid:
+            raise DashboardException(msg='Password expiration date must not be in '
+                                         'the past',
+                                     code='pwd_past_expiration_date',
+                                     component='user')
+
         if user_roles:
             user.set_roles(user_roles)
         mgr.ACCESS_CTRL_DB.save()
@@ -89,7 +99,7 @@ class User(RESTController):
         mgr.ACCESS_CTRL_DB.save()
 
     def set(self, username, password=None, name=None, email=None, roles=None,
-            enabled=None):
+            enabled=None, pwdExpirationDate=None):
         if JwtManager.get_username() == username and enabled is False:
             raise DashboardException(msg='You are not allowed to disable your user',
                                      code='cannot_disable_current_user',
@@ -105,10 +115,16 @@ class User(RESTController):
         if password:
             validate_password_policy(password, username)
             user.set_password(password)
+        if pwdExpirationDate and \
+           (pwdExpirationDate < int(time.mktime(datetime.utcnow().timetuple()))):
+            raise DashboardException(
+                msg='Password expiration date must not be in the past',
+                code='pwd_past_expiration_date', component='user')
         user.name = name
         user.email = email
         if enabled is not None:
             user.enabled = enabled
+        user.pwd_expiration_date = pwdExpirationDate
         user.set_roles(user_roles)
         mgr.ACCESS_CTRL_DB.save()
         return User._user_to_dict(user)
index 5d4badd7ff83508a517f9367c7578dcb61378a90..672caeff18044b13496dedc145fddd640619ac79 100644 (file)
@@ -103,6 +103,12 @@ class RoleNotInUser(Exception):
             .format(rolename, username))
 
 
+class PwdExpirationDateNotValid(Exception):
+    def __init__(self):
+        super(PwdExpirationDateNotValid, self).__init__(
+            "The password expiration date must not be in the past")
+
+
 class GrafanaError(Exception):
     pass
 
index 0e9f45903171dbfecfa74cacb815eba7e51aea0d..56ce6b74c5a5571dc41c59b594652120771b9921 100644 (file)
@@ -5,6 +5,7 @@ import { RouterModule, Routes } from '@angular/router';
 
 import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
 import { ButtonsModule } from 'ngx-bootstrap/buttons';
+import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
 import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
 import { PopoverModule } from 'ngx-bootstrap/popover';
 import { TabsModule } from 'ngx-bootstrap/tabs';
@@ -32,7 +33,8 @@ import { UserTabsComponent } from './user-tabs/user-tabs.component';
     SharedModule,
     TabsModule.forRoot(),
     RouterModule,
-    NgBootstrapFormValidationModule
+    NgBootstrapFormValidationModule,
+    BsDatepickerModule.forRoot()
   ],
   declarations: [
     LoginComponent,
index 430efa87fa4e7a5cae43236d5937e6d218c4c4bf..70180ea5ebd68ed387d9f5ae9290abcaf99c2e1a 100644 (file)
@@ -54,7 +54,13 @@ export class LoginComponent implements OnInit {
             window.location.replace(login.login_url);
           }
         } else {
-          this.authStorageService.set(login.username, token, login.permissions, login.sso);
+          this.authStorageService.set(
+            login.username,
+            token,
+            login.permissions,
+            login.sso,
+            login.pwdExpirationDate
+          );
           this.router.navigate(['']);
         }
       });
index 48660e2ab1e6ef3e4cd9b27da39b04464f12d1f8..419c00591223eb9d67f411a5ffff68c68eefbd39 100644 (file)
@@ -1,7 +1,11 @@
+<cd-loading-panel *ngIf="!pwdExpirationSettings"
+                  i18n>Loading...</cd-loading-panel>
+
 <div class="col-sm-12 col-lg-6">
   <form name="userForm"
         #formDir="ngForm"
         [formGroup]="userForm"
+        *ngIf="pwdExpirationSettings"
         novalidate>
     <div class="card">
       <div i18n="form title|Example: Create Pool@@formTitle"
           </div>
         </div>
 
+        <!-- Password expiration date -->
+        <div class="form-group row"
+             *ngIf="showExpirationDateField()">
+          <label class="col-form-label col-sm-3"
+                 for="pwdExpirationDate">
+            <ng-container i18n>Password expiration date</ng-container>
+            <span class="required"
+                  *ngIf="pwdExpirationSettings.pwdExpirationSpan > 0"></span>
+          </label>
+          <div class="col-sm-9">
+            <div class="input-group">
+              <input type="text"
+                     class="form-control"
+                     i18n-placeholder
+                     placeholder="Password expiration date..."
+                     [bsConfig]="bsConfig"
+                     [minDate]="minDate"
+                     bsDatepicker
+                     id="pwdExpirationDate"
+                     name="pwdExpirationDate"
+                     formControlName="pwdExpirationDate">
+              <span class="input-group-append">
+                <button type="button"
+                        class="btn btn-light"
+                        (click)="clearExpirationDate()">
+                  <i class="icon-prepend {{ icons.destroy }}"></i>
+                </button>
+              </span>
+              <span class="invalid-feedback"
+                    *ngIf="userForm.showError('pwdExpirationDate', formDir, 'required')"
+                    i18n>This field is required.</span>
+            </div>
+          </div>
+        </div>
+
         <!-- Name -->
         <div class="form-group row">
           <label i18n
index 950f8af0a97187d98f0f10a7ed0c5a6f3fe75102..3a0c785b476c7de670dd7e976db4efaa64a2323e 100644 (file)
@@ -6,12 +6,14 @@ import { Router, Routes } from '@angular/router';
 import { RouterTestingModule } from '@angular/router/testing';
 
 import { ButtonsModule } from 'ngx-bootstrap/buttons';
+import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
 import { BsModalService } from 'ngx-bootstrap/modal';
 import { ToastrModule } from 'ngx-toastr';
 import { of } from 'rxjs';
 
 import { configureTestBed, FormHelper, i18nProviders } from '../../../../testing/unit-test-helper';
 import { RoleService } from '../../../shared/api/role.service';
+import { SettingsService } from '../../../shared/api/settings.service';
 import { UserService } from '../../../shared/api/user.service';
 import { ComponentsModule } from '../../../shared/components/components.module';
 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
@@ -50,7 +52,8 @@ describe('UserFormComponent', () => {
         ComponentsModule,
         ToastrModule.forRoot(),
         SharedModule,
-        ButtonsModule.forRoot()
+        ButtonsModule.forRoot(),
+        BsDatepickerModule.forRoot()
       ],
       declarations: [UserFormComponent, FakeComponent],
       providers: i18nProviders
@@ -85,9 +88,15 @@ describe('UserFormComponent', () => {
     });
 
     it('should not disable fields', () => {
-      ['username', 'name', 'password', 'confirmpassword', 'email', 'roles'].forEach((key) =>
-        expect(form.get(key).disabled).toBeFalsy()
-      );
+      [
+        'username',
+        'name',
+        'password',
+        'confirmpassword',
+        'email',
+        'roles',
+        'pwdExpirationDate'
+      ].forEach((key) => expect(form.get(key).disabled).toBeFalsy());
     });
 
     it('should validate username required', () => {
@@ -151,7 +160,8 @@ describe('UserFormComponent', () => {
         name: 'User 0',
         email: 'user0@email.com',
         roles: ['administrator'],
-        enabled: true
+        enabled: true,
+        pwdExpirationDate: undefined
       };
       formHelper.setMultipleValues(user);
       formHelper.setValue('confirmpassword', user.password);
@@ -171,7 +181,8 @@ describe('UserFormComponent', () => {
       name: 'User 1',
       email: 'user1@email.com',
       roles: ['administrator'],
-      enabled: true
+      enabled: true,
+      pwdExpirationDate: undefined
     };
     const roles = [
       {
@@ -196,15 +207,24 @@ describe('UserFormComponent', () => {
         }
       }
     ];
+    const pwdExpirationSettings = {
+      user_pwd_expiration_warning_1: 10,
+      user_pwd_expiration_warning_2: 5,
+      user_pwd_expiration_span: 90
+    };
 
     beforeEach(() => {
       spyOn(userService, 'get').and.callFake(() => of(user));
       spyOn(TestBed.get(RoleService), 'list').and.callFake(() => of(roles));
       setUrl('/user-management/users/edit/user1');
+      spyOn(TestBed.get(SettingsService), 'pwdExpirationSettings').and.callFake(() =>
+        of(pwdExpirationSettings)
+      );
       component.ngOnInit();
       const req = httpTesting.expectOne('api/role');
       expect(req.request.method).toBe('GET');
       req.flush(roles);
+      httpTesting.expectOne('ui-api/standard_settings');
     });
 
     afterEach(() => {
index f571f1dda20ba982439de5d13e7d9f6a80abbcc8..c7bdfe722faadbf5b8c7b9465e8383071fa46d8a 100644 (file)
@@ -5,9 +5,11 @@ import { ActivatedRoute, Router } from '@angular/router';
 import { I18n } from '@ngx-translate/i18n-polyfill';
 import * as _ from 'lodash';
 import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
+import { forkJoin as observableForkJoin } from 'rxjs';
 
 import { AuthService } from '../../../shared/api/auth.service';
 import { RoleService } from '../../../shared/api/role.service';
+import { SettingsService } from '../../../shared/api/settings.service';
 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';
@@ -16,6 +18,7 @@ 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 { 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';
@@ -47,6 +50,12 @@ export class UserFormComponent implements OnInit {
   passwordStrengthLevel: string;
   passwordStrengthDescription: string;
   icons = Icons;
+  minDate: Date;
+  bsConfig = {
+    dateInputFormat: 'YYYY-MM-DD',
+    containerClass: 'theme-default'
+  };
+  pwdExpirationSettings: CdPwdExpirationSettings;
 
   constructor(
     private authService: AuthService,
@@ -59,7 +68,8 @@ export class UserFormComponent implements OnInit {
     private notificationService: NotificationService,
     private i18n: I18n,
     public actionLabels: ActionLabelsI18n,
-    private userChangePasswordService: UserChangePasswordService
+    private userChangePasswordService: UserChangePasswordService,
+    private settingsService: SettingsService
   ) {
     this.resource = this.i18n('user');
     this.createForm();
@@ -85,6 +95,7 @@ export class UserFormComponent implements OnInit {
           updateOn: 'blur',
           validators: []
         }),
+        pwdExpirationDate: new FormControl(''),
         email: new FormControl('', {
           validators: [Validators.email]
         }),
@@ -106,16 +117,32 @@ export class UserFormComponent implements OnInit {
     } else {
       this.action = this.actionLabels.CREATE;
     }
+    this.minDate = new Date();
 
-    this.roleService.list().subscribe((roles: Array<UserFormRoleModel>) => {
-      this.allRoles = _.map(roles, (role) => {
-        role.enabled = true;
-        return role;
-      });
-    });
-    if (this.mode === this.userFormMode.editing) {
-      this.initEdit();
-    }
+    const observables = [this.roleService.list(), this.settingsService.pwdExpirationSettings()];
+    observableForkJoin(observables).subscribe(
+      (result: [UserFormRoleModel[], CdPwdExpirationSettings]) => {
+        this.allRoles = _.map(result[0], (role) => {
+          role.enabled = true;
+          return role;
+        });
+        this.pwdExpirationSettings = new CdPwdExpirationSettings(result[1]);
+
+        if (this.mode === this.userFormMode.editing) {
+          this.initEdit();
+        } else {
+          if (this.pwdExpirationSettings.pwdExpirationSpan > 0) {
+            const pwdExpirationDateField = this.userForm.get('pwdExpirationDate');
+            const expirationDate = new Date();
+            expirationDate.setDate(
+              this.minDate.getDate() + this.pwdExpirationSettings.pwdExpirationSpan
+            );
+            pwdExpirationDateField.setValue(expirationDate);
+            pwdExpirationDateField.setValidators([Validators.required]);
+          }
+        }
+      }
+    );
   }
 
   initEdit() {
@@ -137,6 +164,10 @@ export class UserFormComponent implements OnInit {
     ['username', 'name', 'email', 'roles', 'enabled'].forEach((key) =>
       this.userForm.get(key).setValue(response[key])
     );
+    const expirationDate = response['pwdExpirationDate'];
+    if (expirationDate) {
+      this.userForm.get('pwdExpirationDate').setValue(new Date(expirationDate * 1000));
+    }
   }
 
   getRequest(): UserFormModel {
@@ -144,6 +175,16 @@ export class UserFormComponent implements OnInit {
     ['username', 'password', 'name', 'email', 'roles', 'enabled'].forEach(
       (key) => (userFormModel[key] = this.userForm.get(key).value)
     );
+    const expirationDate = this.userForm.get('pwdExpirationDate').value;
+    if (expirationDate) {
+      if (
+        this.mode !== this.userFormMode.editing ||
+        this.response.pwdExpirationDate !== Number(expirationDate) / 1000
+      ) {
+        expirationDate.setHours(23, 59, 59);
+      }
+      userFormModel['pwdExpirationDate'] = Number(expirationDate) / 1000;
+    }
     return userFormModel;
   }
 
@@ -192,6 +233,14 @@ export class UserFormComponent implements OnInit {
     return password && this.passwordStrengthLevel === 'passwordStrengthLevel0';
   }
 
+  showExpirationDateField() {
+    return (
+      this.userForm.getValue('pwdExpirationDate') > 0 ||
+      this.userForm.touched ||
+      this.pwdExpirationSettings.pwdExpirationSpan > 0
+    );
+  }
+
   public isCurrentUser(): boolean {
     return this.authStorageService.getUsername() === this.userForm.getValue('username');
   }
@@ -247,6 +296,10 @@ export class UserFormComponent implements OnInit {
     );
   }
 
+  clearExpirationDate() {
+    this.userForm.get('pwdExpirationDate').setValue(undefined);
+  }
+
   submit() {
     if (this.mode === this.userFormMode.editing) {
       this.editAction();
index c3e3126a56cf9dc3aafb871b84074e3174aca25d..4989905800d86d4ab4344d16b9a68e0e64eb24dd 100644 (file)
@@ -1,6 +1,7 @@
 export class UserFormModel {
   username: string;
   password: string;
+  pwdExpirationDate: number;
   name: string;
   email: string;
   roles: Array<string>;
index 8b1f1127e076d0429c9962ae2762c76d07ca981d..c9f4a0c8ec4646360f4560509d033b6eef6c9f66 100644 (file)
@@ -13,6 +13,7 @@ import { CdTableAction } from '../../../shared/models/cd-table-action';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { Permission } from '../../../shared/models/permissions';
+import { CdDatePipe } from '../../../shared/pipes/cd-date.pipe';
 import { EmptyPipe } from '../../../shared/pipes/empty.pipe';
 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
 import { NotificationService } from '../../../shared/services/notification.service';
@@ -48,6 +49,7 @@ export class UserListComponent implements OnInit {
     private authStorageService: AuthStorageService,
     private i18n: I18n,
     private urlBuilder: URLBuilderService,
+    private cdDatePipe: CdDatePipe,
     public actionLabels: ActionLabelsI18n
   ) {
     this.permission = this.authStorageService.getPermissions().user;
@@ -103,12 +105,23 @@ export class UserListComponent implements OnInit {
         prop: 'enabled',
         flexGrow: 1,
         cellTransformation: CellTemplate.checkIcon
+      },
+      {
+        name: this.i18n('Password expiration date'),
+        prop: 'pwdExpirationDate',
+        flexGrow: 1,
+        pipe: this.cdDatePipe
       }
     ];
   }
 
   getUsers() {
     this.userService.list().subscribe((users: Array<any>) => {
+      users.forEach((user) => {
+        if (user['pwdExpirationDate'] && user['pwdExpirationDate'] > 0) {
+          user['pwdExpirationDate'] = user['pwdExpirationDate'] * 1000;
+        }
+      });
       this.users = users;
     });
   }
index 8ab1fb7d432611712fa70ac2b94a8586be1367b2..7172ccba926a9754530e06c3632ff17cf2f91677 100644 (file)
@@ -27,7 +27,13 @@ export class AuthService {
   login(credentials: Credentials): Observable<LoginResponse> {
     return this.http.post('api/auth', credentials).pipe(
       tap((resp: LoginResponse) => {
-        this.authStorageService.set(resp.username, resp.token, resp.permissions, resp.sso);
+        this.authStorageService.set(
+          resp.username,
+          resp.token,
+          resp.permissions,
+          resp.sso,
+          resp.pwdExpirationDate
+        );
       })
     );
   }
index 9403b5769858208bd2d390ce4620cdc0f0e406c5..75b88c61d266aa771ab6e6a0176dff7c65838546 100644 (file)
@@ -1,7 +1,9 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 
-import * as _ from 'lodash';
+import { Observable } from 'rxjs';
+
+import { CdPwdExpirationSettings } from '../models/cd-pwd-expiration-settings';
 import { ApiModule } from './api.module';
 
 @Injectable({
@@ -47,4 +49,8 @@ export class SettingsService {
   validateGrafanaDashboardUrl(uid) {
     return this.http.get(`api/grafana/validation/${uid}`);
   }
+
+  pwdExpirationSettings(): Observable<CdPwdExpirationSettings> {
+    return this.http.get<CdPwdExpirationSettings>('ui-api/standard_settings');
+  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-expiration-settings.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-expiration-settings.ts
new file mode 100644 (file)
index 0000000..eaa1454
--- /dev/null
@@ -0,0 +1,11 @@
+export class CdPwdExpirationSettings {
+  pwdExpirationSpan = 0;
+  pwdExpirationWarning1: number;
+  pwdExpirationWarning2: number;
+
+  constructor(data) {
+    this.pwdExpirationSpan = data.user_pwd_expiration_span;
+    this.pwdExpirationWarning1 = data.user_pwd_expiration_warning_1;
+    this.pwdExpirationWarning2 = data.user_pwd_expiration_warning_2;
+  }
+}
index 0df52d567522f055f4ef18c67011c2f636c0755f..d9f1797a74c46965cf3f32435e1e4449a91a4fa5 100644 (file)
@@ -2,5 +2,6 @@ export class LoginResponse {
   username: string;
   token: string;
   permissions: object;
+  pwdExpirationDate: number;
   sso: boolean;
 }
index 3c618050540e8080d45350488ce5c7d90703f94b..dabcc5675871e60493e69b48edc5e07345b55220 100644 (file)
@@ -8,16 +8,24 @@ import { Permissions } from '../models/permissions';
 export class AuthStorageService {
   constructor() {}
 
-  set(username: string, token: string, permissions: object = {}, sso = false) {
+  set(
+    username: string,
+    token: string,
+    permissions: object = {},
+    sso = false,
+    pwdExpirationDate: number = null
+  ) {
     localStorage.setItem('dashboard_username', username);
     localStorage.setItem('access_token', token);
     localStorage.setItem('dashboard_permissions', JSON.stringify(new Permissions(permissions)));
+    localStorage.setItem('user_pwd_expiration_date', String(pwdExpirationDate));
     localStorage.setItem('sso', String(sso));
   }
 
   remove() {
     localStorage.removeItem('access_token');
     localStorage.removeItem('dashboard_username');
+    localStorage.removeItem('user_pwd_expiration_data');
   }
 
   getToken(): string {
@@ -38,6 +46,10 @@ export class AuthStorageService {
     );
   }
 
+  getPwdExpirationDate(): number {
+    return Number(localStorage.getItem('user_pwd_expiration_date'));
+  }
+
   isSSO() {
     return localStorage.getItem('sso') === 'true';
   }
index e3d1b2e67719064c886c7eee39e6e4f3d5a8aac7..b8f7c6d29ec8b5dd478ecc7d7e3d690c39f258a5 100644 (file)
@@ -12,16 +12,19 @@ import threading
 import time
 import re
 
+from datetime import datetime, timedelta
+
 import bcrypt
 
 from mgr_module import CLIReadCommand, CLIWriteCommand
 
 from .. import mgr
 from ..security import Scope, Permission
+from ..settings import Settings
 from ..exceptions import RoleAlreadyExists, RoleDoesNotExist, ScopeNotValid, \
                          PermissionNotValid, RoleIsAssociatedWithUser, \
                          UserAlreadyExists, UserDoesNotExist, ScopeNotInRole, \
-                         RoleNotInUser, PasswordCheckException
+                         RoleNotInUser, PasswordCheckException, PwdExpirationDateNotValid
 
 
 logger = logging.getLogger('access_control')
@@ -265,7 +268,7 @@ SYSTEM_ROLES = {
 
 class User(object):
     def __init__(self, username, password, name=None, email=None, roles=None,
-                 last_update=None, enabled=True):
+                 last_update=None, enabled=True, pwd_expiration_date=None):
         self.username = username
         self.password = password
         self.name = name
@@ -279,10 +282,21 @@ class User(object):
         else:
             self.last_update = last_update
         self._enabled = enabled
+        self.pwd_expiration_date = pwd_expiration_date
+        if self.pwd_expiration_date is None:
+            self.refresh_pwd_expiration_date()
 
     def refresh_last_update(self):
         self.last_update = int(time.time())
 
+    def refresh_pwd_expiration_date(self):
+        if Settings.USER_PWD_EXPIRATION_SPAN > 0:
+            expiration_date = datetime.utcnow() + timedelta(
+                days=Settings.USER_PWD_EXPIRATION_SPAN)
+            self.pwd_expiration_date = int(time.mktime(expiration_date.timetuple()))
+        else:
+            self.pwd_expiration_date = None
+
     @property
     def enabled(self):
         return self._enabled
@@ -298,6 +312,7 @@ class User(object):
     def set_password_hash(self, hashed_password):
         self.password = hashed_password
         self.refresh_last_update()
+        self.refresh_pwd_expiration_date()
 
     def compare_password(self, password):
         """
@@ -310,6 +325,12 @@ class User(object):
         pass_hash = password_hash(password, salt_password=self.password)
         return pass_hash == self.password
 
+    def is_pwd_expired(self):
+        if self.pwd_expiration_date:
+            current_time = int(time.mktime(datetime.utcnow().timetuple()))
+            return self.pwd_expiration_date < current_time
+        return False
+
     def set_roles(self, roles):
         self.roles = set(roles)
         self.refresh_last_update()
@@ -351,14 +372,15 @@ class User(object):
             'name': self.name,
             'email': self.email,
             'lastUpdate': self.last_update,
-            'enabled': self.enabled
+            'enabled': self.enabled,
+            'pwdExpirationDate': self.pwd_expiration_date
         }
 
     @classmethod
     def from_dict(cls, u_dict, roles):
         return User(u_dict['username'], u_dict['password'], u_dict['name'],
                     u_dict['email'], {roles[r] for r in u_dict['roles']},
-                    u_dict['lastUpdate'], u_dict['enabled'])
+                    u_dict['lastUpdate'], u_dict['enabled'], u_dict['pwdExpirationDate'])
 
 
 class AccessControlDB(object):
@@ -398,12 +420,16 @@ class AccessControlDB(object):
 
             del self.roles[name]
 
-    def create_user(self, username, password, name, email, enabled=True):
+    def create_user(self, username, password, name, email, enabled=True, pwd_expiration_date=None):
         logger.debug("creating user: username=%s", username)
         with self.lock:
             if username in self.users:
                 raise UserAlreadyExists(username)
-            user = User(username, password_hash(password), name, email, enabled=enabled)
+            if pwd_expiration_date and \
+               (pwd_expiration_date < int(time.mktime(datetime.utcnow().timetuple()))):
+                raise PwdExpirationDateNotValid()
+            user = User(username, password_hash(password), name, email, enabled=enabled,
+                        pwd_expiration_date=pwd_expiration_date)
             self.users[username] = user
             return user
 
@@ -467,6 +493,7 @@ class AccessControlDB(object):
 
                 for user, _ in v1_db['users'].items():
                     v1_db['users'][user]['enabled'] = True
+                    v1_db['users'][user]['pwdExpirationDate'] = None
 
                 self.roles = {rn: Role.from_dict(r) for rn, r in v1_db.get('roles', {}).items()}
                 self.users = {un: User.from_dict(u, dict(self.roles, **SYSTEM_ROLES))
@@ -644,10 +671,12 @@ def ac_user_show_cmd(_, username=None):
                  'name=name,type=CephString,req=false '
                  'name=email,type=CephString,req=false '
                  'name=enabled,type=CephBool,req=false '
-                 'name=force_password,type=CephBool,req=false',
+                 'name=force_password,type=CephBool,req=false '
+                 'name=pwd_expiration_date,type=CephInt,req=false',
                  'Create a user')
 def ac_user_create_cmd(_, username, password=None, rolename=None, name=None,
-                       email=None, enabled=True, force_password=False):
+                       email=None, enabled=True, force_password=False,
+                       pwd_expiration_date=None):
     try:
         role = mgr.ACCESS_CTRL_DB.get_role(rolename) if rolename else None
     except RoleDoesNotExist as ex:
@@ -659,7 +688,8 @@ def ac_user_create_cmd(_, username, password=None, rolename=None, name=None,
         if not force_password:
             pw_check = PasswordCheck(password, username)
             pw_check.check_all()
-        user = mgr.ACCESS_CTRL_DB.create_user(username, password, name, email, enabled)
+        user = mgr.ACCESS_CTRL_DB.create_user(username, password, name, email,
+                                              enabled, pwd_expiration_date)
     except PasswordCheckException as ex:
         return -errno.EINVAL, '', str(ex)
     except UserAlreadyExists as ex:
@@ -850,8 +880,10 @@ class LocalAuthenticator(object):
         try:
             user = mgr.ACCESS_CTRL_DB.get_user(username)
             if user.password:
-                if user.enabled and user.compare_password(password):
-                    return user.permissions_dict()
+                if user.enabled and user.compare_password(password) \
+                   and not user.is_pwd_expired():
+                    return {'permissions': user.permissions_dict(),
+                            'pwdExpirationDate': user.pwd_expiration_date}
         except UserDoesNotExist:
             logger.debug("User '%s' does not exist", username)
         return None
index bda0dda04133d29a2e31cd91eaefa02578d72e59..9e1a6a95bc74b6e45510b2982fdd685a2e3b4f73 100644 (file)
@@ -52,6 +52,16 @@ class Options(object):
     # iSCSI management settings
     ISCSI_API_SSL_VERIFICATION = (True, bool)
 
+    # user management settings
+    # Time span of user passwords to expire in days.
+    # The default value is '0' which means that user passwords are
+    # never going to expire.
+    USER_PWD_EXPIRATION_SPAN = (0, int)
+    # warning levels to notify the user that the password is going
+    # to expire soon
+    USER_PWD_EXPIRATION_WARNING_1 = (10, int)
+    USER_PWD_EXPIRATION_WARNING_2 = (5, int)
+
     @staticmethod
     def has_default_value(name):
         return getattr(Settings, name, None) is None or \
index 337d5cb53d85440e4ad527e19d0629d26a0c5360..79caa07c4e921a9104d14dab2a69904d1df73bca 100644 (file)
@@ -7,6 +7,8 @@ import json
 import time
 import unittest
 
+from datetime import datetime, timedelta
+
 from . import CmdException, CLICommandTestMixin
 from .. import mgr
 from ..security import Scope, Permission
@@ -70,7 +72,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
 
     def validate_persistent_user(self, username, roles, password=None,
                                  name=None, email=None, last_update=None,
-                                 enabled=True):
+                                 enabled=True, pwdExpirationDate=None):
         db = self.load_persistent_db()
         self.assertIn('users', db)
         self.assertIn(username, db['users'])
@@ -84,6 +86,8 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             self.assertEqual(db['users'][username]['email'], email)
         if last_update:
             self.assertEqual(db['users'][username]['lastUpdate'], last_update)
+        if pwdExpirationDate:
+            self.assertEqual(db['users'][username]['pwdExpirationDate'], pwdExpirationDate)
         self.assertEqual(db['users'][username]['enabled'], enabled)
 
     def validate_persistent_no_user(self, username):
@@ -271,17 +275,20 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
         self.assertEqual(str(ctx.exception),
                          "Cannot update system role 'read-only'")
 
-    def test_create_user(self, username='admin', rolename=None, enabled=True):
+    def test_create_user(self, username='admin', rolename=None, enabled=True,
+                         pwdExpirationDate=None):
         user = self.exec_cmd('ac-user-create', username=username,
                              rolename=rolename, password='admin',
                              name='{} User'.format(username),
                              email='{}@user.com'.format(username),
-                             enabled=enabled, force_password=True)
+                             enabled=enabled, force_password=True,
+                             pwd_expiration_date=pwdExpirationDate)
 
         pass_hash = password_hash('admin', user['password'])
         self.assertDictEqual(user, {
             'username': username,
             'password': pass_hash,
+            'pwdExpirationDate': pwdExpirationDate,
             'lastUpdate': user['lastUpdate'],
             'name': '{} User'.format(username),
             'email': '{}@user.com'.format(username),
@@ -297,6 +304,11 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
     def test_create_disabled_user(self):
         self.test_create_user(enabled=False)
 
+    def test_create_user_pwd_expiration_date(self):
+        expiration_date = datetime.utcnow() + timedelta(days=10)
+        expiration_date = int(time.mktime(expiration_date.timetuple()))
+        self.test_create_user(pwdExpirationDate=expiration_date)
+
     def test_create_user_with_role(self):
         self.test_add_role_scope_perms()
         self.test_create_user(rolename='test_role')
@@ -490,6 +502,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'username': 'admin',
             'lastUpdate': user['lastUpdate'],
             'password': pass_hash,
+            'pwdExpirationDate': None,
             'name': 'admin User',
             'email': 'admin@user.com',
             'roles': ['block-manager', 'pool-manager'],
@@ -531,6 +544,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
         self.assertDictEqual(user, {
             'username': 'admin',
             'password': pass_hash,
+            'pwdExpirationDate': None,
             'name': 'Admin Name',
             'email': 'admin@admin.com',
             'lastUpdate': user['lastUpdate'],
@@ -557,6 +571,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
         self.assertDictEqual(user, {
             'username': 'admin',
             'password': pass_hash,
+            'pwdExpirationDate': None,
             'name': 'admin User',
             'email': 'admin@user.com',
             'lastUpdate': user['lastUpdate'],
@@ -584,6 +599,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
         self.assertDictEqual(user, {
             'username': 'admin',
             'password': pass_hash,
+            'pwdExpirationDate': None,
             'name': 'admin User',
             'email': 'admin@user.com',
             'lastUpdate': user['lastUpdate'],
@@ -620,6 +636,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
         self.assertDictEqual(user, {
             'username': 'admin',
             'password': pass_hash,
+            'pwdExpirationDate': None,
             'name': None,
             'email': None,
             'lastUpdate': user['lastUpdate'],
@@ -638,6 +655,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
         self.assertDictEqual(user, {
             'username': 'admin',
             'password': pass_hash,
+            'pwdExpirationDate': None,
             'name': 'admin User',
             'email': 'admin@user.com',
             'lastUpdate': user['lastUpdate'],
@@ -692,6 +710,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'lastUpdate': user['lastUpdate'],
             'password':
                 "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
+            'pwdExpirationDate': None,
             'name': 'admin User',
             'email': 'admin@user.com',
             'roles': ['block-manager', 'test_role'],
@@ -700,7 +719,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
 
     def test_load_v2(self):
         """
-        The `enabled` attribute of a user has been added in v2
+        The `enabled` and `pwdExpirationDate` attributes of a user have been added in v2
         """
         self.CONFIG_KEY_DICT['accessdb_v1'] = '''
             {{
@@ -709,6 +728,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
                         "username": "admin",
                         "password":
                 "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
+                        "pwdExpirationDate": null,
                         "roles": ["block-manager", "test_role"],
                         "name": "admin User",
                         "email": "admin@user.com",
@@ -747,6 +767,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'lastUpdate': user['lastUpdate'],
             'password':
                 "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
+            'pwdExpirationDate': None,
             'name': 'admin User',
             'email': 'admin@user.com',
             'roles': ['block-manager', 'test_role'],
@@ -764,6 +785,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'lastUpdate': user['lastUpdate'],
             'password':
                 "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
+            'pwdExpirationDate': None,
             'name': None,
             'email': None,
             'roles': ['administrator'],