From: Tatjana Dehler Date: Fri, 13 Sep 2019 12:08:39 +0000 (+0200) Subject: mgr/dashboard: add password expiration date X-Git-Tag: v15.1.0~489^2~1 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=0221241909e93a8239e256de0cf1efa2bdfe9bbf;p=ceph-ci.git mgr/dashboard: add password expiration date 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 --- diff --git a/doc/mgr/dashboard.rst b/doc/mgr/dashboard.rst index 4944e917e14..ae810bd6415 100644 --- a/doc/mgr/dashboard.rst +++ b/doc/mgr/dashboard.rst @@ -622,7 +622,7 @@ We provide a set of CLI commands to manage user accounts: - *Create User*:: - $ ceph dashboard ac-user-create [--force-password] [] [] [] [] [--enabled] + $ ceph dashboard ac-user-create [--force-password] [] [] [] [] [--enabled] [] - *Delete User*:: diff --git a/qa/tasks/mgr/dashboard/test_auth.py b/qa/tasks/mgr/dashboard/test_auth.py index 0fd5b3080c9..44a8d0d56df 100644 --- a/qa/tasks/mgr/dashboard/test_auth.py +++ b/qa/tasks/mgr/dashboard/test_auth.py @@ -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']) diff --git a/qa/tasks/mgr/dashboard/test_user.py b/qa/tasks/mgr/dashboard/test_user.py index a66228539a8..115e554ec11 100644 --- a/qa/tasks/mgr/dashboard/test_user.py +++ b/qa/tasks/mgr/dashboard/test_user.py @@ -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']) diff --git a/src/pybind/mgr/dashboard/.pylintrc b/src/pybind/mgr/dashboard/.pylintrc index 8efd540f49e..5cec25f4ce4 100644 --- a/src/pybind/mgr/dashboard/.pylintrc +++ b/src/pybind/mgr/dashboard/.pylintrc @@ -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 diff --git a/src/pybind/mgr/dashboard/controllers/auth.py b/src/pybind/mgr/dashboard/controllers/auth.py index 4c4b1a6027c..44128ce3201 100644 --- a/src/pybind/mgr/dashboard/controllers/auth.py +++ b/src/pybind/mgr/dashboard/controllers/auth.py @@ -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' } diff --git a/src/pybind/mgr/dashboard/controllers/settings.py b/src/pybind/mgr/dashboard/controllers/settings.py index a484580ee1c..19e177c61c0 100644 --- a/src/pybind/mgr/dashboard/controllers/settings.py +++ b/src/pybind/mgr/dashboard/controllers/settings.py @@ -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 + } diff --git a/src/pybind/mgr/dashboard/controllers/user.py b/src/pybind/mgr/dashboard/controllers/user.py index 560471957af..645f5cbc2be 100644 --- a/src/pybind/mgr/dashboard/controllers/user.py +++ b/src/pybind/mgr/dashboard/controllers/user.py @@ -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) diff --git a/src/pybind/mgr/dashboard/exceptions.py b/src/pybind/mgr/dashboard/exceptions.py index 5d4badd7ff8..672caeff180 100644 --- a/src/pybind/mgr/dashboard/exceptions.py +++ b/src/pybind/mgr/dashboard/exceptions.py @@ -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 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts index 0e9f4590317..56ce6b74c5a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts @@ -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, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts index 430efa87fa4..70180ea5ebd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts @@ -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(['']); } }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html index 48660e2ab1e..419c0059122 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html @@ -1,7 +1,11 @@ +Loading... +
+ +
+ +
+
+ + + + + This field is required. +
+
+
+