From b37079a6fcbe79161d40b3d88e503fd41ac34a17 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Mon, 13 Jan 2020 10:06:25 +0100 Subject: [PATCH] mgr/dashboard: Enforce password change upon first login - Part 2 Introduce the following: - A new layout component for the login pages. - A new route called /login-change-password. - A guard that checks if a user must change the password (ChangePasswordGuardService). If this is true, redirect to /login-change-password. - Added LoginPasswordFormComponent (extends UserPasswordFormComponent) for the password form but (looks similar the login page). Fixes: tracker.ceph.com/issues/24655 Signed-off-by: Volker Theile --- doc/mgr/dashboard.rst | 3 + qa/tasks/mgr/dashboard/test_user.py | 2 +- .../mgr/dashboard/controllers/settings.py | 39 +++++- .../frontend/src/app/app-routing.module.ts | 22 +++- .../frontend/src/app/core/auth/auth.module.ts | 8 +- .../login-password-form.component.html | 102 +++++++++++++++ .../login-password-form.component.scss | 27 ++++ .../login-password-form.component.spec.ts | 81 ++++++++++++ .../login-password-form.component.ts | 55 ++++++++ .../app/core/auth/login/login.component.html | 121 +++++++----------- .../app/core/auth/login/login.component.scss | 53 +++----- .../app/core/auth/login/login.component.ts | 19 ++- .../auth/user-form/user-form.component.html | 7 +- .../user-form/user-form.component.spec.ts | 13 +- .../auth/user-form/user-form.component.ts | 2 +- .../user-password-form.component.html | 4 +- .../user-password-form.component.spec.ts | 2 + .../user-password-form.component.ts | 39 +++--- .../user-password-login-form.component.html | 85 ------------ .../user-password-login-form.component.scss | 0 ...user-password-login-form.component.spec.ts | 40 ------ .../user-password-login-form.component.ts | 10 -- .../frontend/src/app/core/core.module.ts | 4 +- .../login-layout/login-layout.component.html | 24 ++++ .../login-layout/login-layout.component.scss | 31 +++++ .../login-layout.component.spec.ts | 37 ++++++ .../login-layout/login-layout.component.ts | 8 ++ .../app/shared/api/settings.service.spec.ts | 9 +- .../src/app/shared/api/settings.service.ts | 5 +- ...-expiration-notification.component.spec.ts | 2 +- .../pwd-expiration-notification.component.ts | 2 +- .../submit-button.component.html | 1 + .../submit-button/submit-button.component.ts | 11 +- .../models/cd-pwd-expiration-settings.ts | 8 +- .../shared/models/cd-pwd-policy-settings.ts | 23 ++++ .../services/api-interceptor.service.ts | 6 +- .../app/shared/services/auth-guard.service.ts | 2 +- .../change-password-guard.service.spec.ts | 64 +++++++++ .../services/change-password-guard.service.ts | 36 ++++++ .../services/password-policy.service.spec.ts | 108 ++++++++-------- .../services/password-policy.service.ts | 92 ++++++------- .../app/shared/services/summary.service.ts | 2 +- .../mgr/dashboard/frontend/src/styles.scss | 8 ++ .../frontend/src/styles/defaults.scss | 1 + .../mgr/dashboard/services/access_control.py | 1 + .../dashboard/tests/test_access_control.py | 11 ++ 46 files changed, 801 insertions(+), 429 deletions(-) create mode 100755 src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.html create mode 100755 src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.scss create mode 100755 src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.spec.ts create mode 100755 src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.ts delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.html delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.scss delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.spec.ts delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-policy-settings.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.ts diff --git a/doc/mgr/dashboard.rst b/doc/mgr/dashboard.rst index 8ee2c548bd30a..9e3a3e045501f 100644 --- a/doc/mgr/dashboard.rst +++ b/doc/mgr/dashboard.rst @@ -676,6 +676,9 @@ Ceph Dashboard supports managing multiple user accounts. Each user account consists of a username, a password (stored in encrypted form using ``bcrypt``), an optional name, and an optional email address. +If a new user is created via Web UI, it is possible to set an option that this +user must assign a new password when they log in for the first time. + User accounts are stored in MON's configuration database, and are globally shared across all ceph-mgr instances. diff --git a/qa/tasks/mgr/dashboard/test_user.py b/qa/tasks/mgr/dashboard/test_user.py index 2230be20e7212..ac7517cccd1c9 100644 --- a/qa/tasks/mgr/dashboard/test_user.py +++ b/qa/tasks/mgr/dashboard/test_user.py @@ -418,7 +418,7 @@ class UserTest(DashboardTestCase): self._delete('/api/user/user1') self._ceph_cmd(['dashboard', 'set-user-pwd-expiration-span', '0']) - def test_pwd_update_required(self): + def test_pwd_update_required(self): self._create_user(username='user1', password='mypassword10#', name='My Name', diff --git a/src/pybind/mgr/dashboard/controllers/settings.py b/src/pybind/mgr/dashboard/controllers/settings.py index 4fb63a379a873..9cde5e9ec12d4 100644 --- a/src/pybind/mgr/dashboard/controllers/settings.py +++ b/src/pybind/mgr/dashboard/controllers/settings.py @@ -21,8 +21,10 @@ class Settings(RESTController): :rtype: str|dict[str, str] """ if isinstance(name, dict): - result = {self._to_native(key): value - for key, value in name.items()} + result = { + self._to_native(key): value + for key, value in name.items() + } else: result = self._to_native(name) @@ -90,8 +92,35 @@ class Settings(RESTController): @UiApiController('/standard_settings') class StandardSettings(RESTController): def list(self): + """ + Get various Dashboard related settings. + :return: Returns a dictionary containing various Dashboard + settings. + :rtype: dict + """ 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 + '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, + 'pwd_policy_enabled': + SettingsModule.PWD_POLICY_ENABLED, + 'pwd_policy_min_length': + SettingsModule.PWD_POLICY_MIN_LENGTH, + 'pwd_policy_check_length_enabled': + SettingsModule.PWD_POLICY_CHECK_LENGTH_ENABLED, + 'pwd_policy_check_oldpwd_enabled': + SettingsModule.PWD_POLICY_CHECK_OLDPWD_ENABLED, + 'pwd_policy_check_username_enabled': + SettingsModule.PWD_POLICY_CHECK_USERNAME_ENABLED, + 'pwd_policy_check_exclusion_list_enabled': + SettingsModule.PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED, + 'pwd_policy_check_repetitive_chars_enabled': + SettingsModule.PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED, + 'pwd_policy_check_sequential_chars_enabled': + SettingsModule.PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED, + 'pwd_policy_check_complexity_enabled': + SettingsModule.PWD_POLICY_CHECK_COMPLEXITY_ENABLED } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index f766613ad3baa..1b94b3d9be877 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -24,16 +24,19 @@ import { Nfs501Component } from './ceph/nfs/nfs-501/nfs-501.component'; import { NfsFormComponent } from './ceph/nfs/nfs-form/nfs-form.component'; import { NfsListComponent } from './ceph/nfs/nfs-list/nfs-list.component'; import { PerformanceCounterComponent } from './ceph/performance-counter/performance-counter/performance-counter.component'; +import { LoginPasswordFormComponent } from './core/auth/login-password-form/login-password-form.component'; import { LoginComponent } from './core/auth/login/login.component'; import { SsoNotFoundComponent } from './core/auth/sso/sso-not-found/sso-not-found.component'; import { UserPasswordFormComponent } from './core/auth/user-password-form/user-password-form.component'; import { ForbiddenComponent } from './core/forbidden/forbidden.component'; import { BlankLayoutComponent } from './core/layouts/blank-layout/blank-layout.component'; +import { LoginLayoutComponent } from './core/layouts/login-layout/login-layout.component'; import { WorkbenchLayoutComponent } from './core/layouts/workbench-layout/workbench-layout.component'; import { NotFoundComponent } from './core/not-found/not-found.component'; import { ActionLabels, URLVerbs } from './shared/constants/app.constants'; import { BreadcrumbsResolver, IBreadcrumb } from './shared/models/breadcrumbs'; import { AuthGuardService } from './shared/services/auth-guard.service'; +import { ChangePasswordGuardService } from './shared/services/change-password-guard.service'; import { FeatureTogglesGuardService } from './shared/services/feature-toggles-guard.service'; import { ModuleStatusGuardService } from './shared/services/module-status-guard.service'; import { NoSsoGuardService } from './shared/services/no-sso-guard.service'; @@ -74,8 +77,8 @@ const routes: Routes = [ { path: '', component: WorkbenchLayoutComponent, - canActivate: [AuthGuardService], - canActivateChild: [AuthGuardService], + canActivate: [AuthGuardService, ChangePasswordGuardService], + canActivateChild: [AuthGuardService, ChangePasswordGuardService], children: [ { path: 'dashboard', component: DashboardComponent }, // Cluster @@ -280,6 +283,19 @@ const routes: Routes = [ } ] }, + { + path: '', + component: LoginLayoutComponent, + children: [ + { path: 'login', component: LoginComponent }, + { + path: 'login-change-password', + component: LoginPasswordFormComponent, + canActivate: [NoSsoGuardService] + }, + { path: 'logout', children: [] } + ] + }, { path: '', component: BlankLayoutComponent, @@ -287,8 +303,6 @@ const routes: Routes = [ // Single Sign-On (SSO) { path: 'sso/404', component: SsoNotFoundComponent }, // System - { path: 'login', component: LoginComponent }, - { path: 'logout', children: [] }, { path: '403', component: ForbiddenComponent }, { path: '404', component: NotFoundComponent }, { path: '**', redirectTo: '/404' } 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 43d56c9bad5eb..9171e24e453aa 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 @@ -6,12 +6,12 @@ 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'; import { ActionLabels, URLVerbs } from '../../shared/constants/app.constants'; import { SharedModule } from '../../shared/shared.module'; +import { LoginPasswordFormComponent } from './login-password-form/login-password-form.component'; import { LoginComponent } from './login/login.component'; import { RoleDetailsComponent } from './role-details/role-details.component'; import { RoleFormComponent } from './role-form/role-form.component'; @@ -20,12 +20,10 @@ import { SsoNotFoundComponent } from './sso/sso-not-found/sso-not-found.componen import { UserFormComponent } from './user-form/user-form.component'; import { UserListComponent } from './user-list/user-list.component'; import { UserPasswordFormComponent } from './user-password-form/user-password-form.component'; -import { UserPasswordLoginFormComponent } from './user-password-login-form/user-password-login-form.component'; import { UserTabsComponent } from './user-tabs/user-tabs.component'; @NgModule({ imports: [ - BsDropdownModule.forRoot(), ButtonsModule.forRoot(), CommonModule, FormsModule, @@ -39,6 +37,7 @@ import { UserTabsComponent } from './user-tabs/user-tabs.component'; ], declarations: [ LoginComponent, + LoginPasswordFormComponent, RoleDetailsComponent, RoleFormComponent, RoleListComponent, @@ -46,8 +45,7 @@ import { UserTabsComponent } from './user-tabs/user-tabs.component'; UserTabsComponent, UserListComponent, UserFormComponent, - UserPasswordFormComponent, - UserPasswordLoginFormComponent + UserPasswordFormComponent ] }) export class AuthModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.html new file mode 100755 index 0000000000000..d98e16c50c7e3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.html @@ -0,0 +1,102 @@ +
+

Please set a new password.

+

You will be redirected to the login page afterwards.

+
+ + +
+
+ + + + +
+ This field is required. + The old and new passwords must be different. +
+ + +
+
+ + + + +
+
+
+
+
+ This field is required. + The old and new passwords must be different. + + {{ passwordValuation }} + +
+ + +
+
+ + + + +
+ This field is required. + Password confirmation doesn't match the new password. +
+
+ +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.scss new file mode 100755 index 0000000000000..0feabe5940853 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.scss @@ -0,0 +1,27 @@ +@import 'defaults'; + +::ng-deep cd-login-password-form { + h4 { + margin: 0 0 30px 0; + } + + .btn-password, + .btn-password:focus, + .form-control, + .form-control:focus { + color: $color-password-toggle-text; + background-color: $color-password-toggle-bg; + } + + .form-control::placeholder { + color: $color-password-toggle-placeholder-text; + } + + .btn-password:focus { + outline-color: $color-password-toggle-focus; + } + + button.btn:not(:first-child) { + margin-left: 5px; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.spec.ts new file mode 100755 index 0000000000000..cf21e749ad70f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.spec.ts @@ -0,0 +1,81 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { ToastrModule } from 'ngx-toastr'; + +import { configureTestBed, FormHelper, i18nProviders } from '../../../../testing/unit-test-helper'; +import { AuthService } from '../../../shared/api/auth.service'; +import { ComponentsModule } from '../../../shared/components/components.module'; +import { CdFormGroup } from '../../../shared/forms/cd-form-group'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { SharedModule } from '../../../shared/shared.module'; +import { LoginPasswordFormComponent } from './login-password-form.component'; + +describe('LoginPasswordFormComponent', () => { + let component: LoginPasswordFormComponent; + let fixture: ComponentFixture; + let form: CdFormGroup; + let formHelper: FormHelper; + let httpTesting: HttpTestingController; + let router: Router; + let authStorageService: AuthStorageService; + let authService: AuthService; + + configureTestBed( + { + imports: [ + HttpClientTestingModule, + RouterTestingModule, + ReactiveFormsModule, + ComponentsModule, + ToastrModule.forRoot(), + SharedModule + ], + declarations: [LoginPasswordFormComponent], + providers: i18nProviders + }, + true + ); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginPasswordFormComponent); + component = fixture.componentInstance; + httpTesting = TestBed.get(HttpTestingController); + router = TestBed.get(Router); + authStorageService = TestBed.get(AuthStorageService); + authService = TestBed.get(AuthService); + spyOn(router, 'navigate'); + fixture.detectChanges(); + form = component.userForm; + formHelper = new FormHelper(form); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should submit', () => { + spyOn(component, 'onPasswordChange').and.callThrough(); + spyOn(authService, 'logout'); + spyOn(authStorageService, 'getUsername').and.returnValue('test1'); + formHelper.setMultipleValues({ + oldpassword: 'foo', + newpassword: 'bar' + }); + formHelper.setValue('confirmnewpassword', 'bar', true); + component.onSubmit(); + const request = httpTesting.expectOne('api/user/test1/change_password'); + request.flush({}); + expect(component.onPasswordChange).toHaveBeenCalled(); + expect(authService.logout).toHaveBeenCalled(); + }); + + it('should cancel', () => { + spyOn(authService, 'logout'); + component.onCancel(); + expect(authService.logout).toHaveBeenCalled(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.ts new file mode 100755 index 0000000000000..e952faf38d88b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; + +import { AuthService } from '../../../shared/api/auth.service'; +import { UserService } from '../../../shared/api/user.service'; +import { ActionLabelsI18n } from '../../../shared/constants/app.constants'; +import { CdFormBuilder } from '../../../shared/forms/cd-form-builder'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { NotificationService } from '../../../shared/services/notification.service'; +import { PasswordPolicyService } from '../../../shared/services/password-policy.service'; +import { UserPasswordFormComponent } from '../user-password-form/user-password-form.component'; + +@Component({ + selector: 'cd-login-password-form', + templateUrl: './login-password-form.component.html', + styleUrls: ['./login-password-form.component.scss'] +}) +export class LoginPasswordFormComponent extends UserPasswordFormComponent { + constructor( + public i18n: I18n, + public actionLabels: ActionLabelsI18n, + public notificationService: NotificationService, + public userService: UserService, + public authStorageService: AuthStorageService, + public formBuilder: CdFormBuilder, + public router: Router, + public passwordPolicyService: PasswordPolicyService, + public authService: AuthService + ) { + super( + i18n, + actionLabels, + notificationService, + userService, + authStorageService, + formBuilder, + router, + passwordPolicyService + ); + } + + onPasswordChange() { + // Logout here because changing the password will change the + // session token which will finally lead to a 401 when calling + // the REST API the next time. The API HTTP inteceptor will + // then also redirect to the login page immediately. + this.authService.logout(); + } + + onCancel() { + this.authService.logout(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html index ab42a8ad83d03..b8a0a629a63b9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html @@ -1,79 +1,52 @@ - diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss index e088fe0b04326..7535c8c84ff6c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss @@ -1,47 +1,24 @@ @import 'defaults'; -::ng-deep .login { - height: 100vh; - color: $color-login-row-text; - background-color: $color-login-row-bg; - - header { - position: absolute; - width: 100vw; - - .navbar { - padding: 1rem 2rem; - - .dropdown-menu { - margin-top: 0.2rem; - - li a { - &:hover { - background-color: $color-brand-teal; - } - } - } - } +::ng-deep cd-login { + h1 { + margin: 0 0 30px 0; } - section { - display: inline-flex; - width: 100vw; - min-height: 100vh; - - h1 { - margin: 0 0 30px 0; - } + .btn-password, + .btn-password:focus, + .form-control, + .form-control:focus { + color: $color-password-toggle-text; + background-color: $color-password-toggle-bg; + } - .btn-password, - .form-control { - color: $color-password-toggle-text; - background-color: $color-password-toggle-bg; - } + .form-control::placeholder { + color: $color-password-toggle-placeholder-text; + } - .btn-password:focus { - outline-color: $color-password-toggle-focus; - } + .btn-password:focus { + outline-color: $color-password-toggle-focus; } } 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 de8a44ddfea67..9286e31d30f10 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 @@ -5,7 +5,6 @@ import { BsModalService } from 'ngx-bootstrap/modal'; import { AuthService } from '../../../shared/api/auth.service'; import { Credentials } from '../../../shared/models/credentials'; -import { LoginResponse } from '../../../shared/models/login-response'; import { AuthStorageService } from '../../../shared/services/auth-storage.service'; @Component({ @@ -16,7 +15,6 @@ import { AuthStorageService } from '../../../shared/services/auth-storage.servic export class LoginComponent implements OnInit { model = new Credentials(); isLoginActive = false; - pwdUpdateRequired = false; constructor( private authService: AuthService, @@ -27,7 +25,6 @@ export class LoginComponent implements OnInit { ngOnInit() { if (this.authStorageService.isLoggedIn()) { - this.pwdUpdateRequired = this.authStorageService.getPwdUpdateRequired(); this.router.navigate(['']); } else { // Make sure all open modal dialogs are closed. This might be @@ -52,7 +49,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(['']); } }); @@ -60,12 +63,8 @@ export class LoginComponent implements OnInit { } login() { - this.authService.login(this.model).subscribe((resp: LoginResponse) => { - if (resp.pwdUpdateRequired) { - this.pwdUpdateRequired = true; - } else { - this.router.navigate(['']); - } + this.authService.login(this.model).subscribe(() => { + 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 da786a20db375..cee4438001041 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 @@ -102,6 +102,7 @@ i18n>Password confirmation doesn't match the password. +
@@ -215,10 +216,10 @@
- +
-
+ *ngIf="!isCurrentUser() && !authStorageService.isSSO()"> +
{ } } ]; - 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) + spyOn(TestBed.get(SettingsService), 'getStandardSettings').and.callFake(() => + of({ + user_pwd_expiration_warning_1: 10, + user_pwd_expiration_warning_2: 5, + user_pwd_expiration_span: 90 + }) ); component.ngOnInit(); const req = httpTesting.expectOne('api/role'); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts index 096e2e40a9bf9..75e14e330361f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts @@ -124,7 +124,7 @@ export class UserFormComponent implements OnInit { } this.minDate = new Date(); - const observables = [this.roleService.list(), this.settingsService.pwdExpirationSettings()]; + const observables = [this.roleService.list(), this.settingsService.getStandardSettings()]; observableForkJoin(observables).subscribe( (result: [UserFormRoleModel[], CdPwdExpirationSettings]) => { this.allRoles = _.map(result[0], (role) => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.html index e513447b580e2..08d1c1c75e571 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.html @@ -19,7 +19,7 @@ placeholder="Old password..." id="oldpassword" formControlName="oldpassword" - autocomplete="off" + autocomplete="new-password" autofocus> - -
- This field is required. - The old and new passwords must be different. -
-
-
- - - - -
-
-
-
-
- This field is required. - The old and new passwords must be different. - Too weak -
-
-
- - - - -
- This field is required. - Password confirmation doesn't match the new password. -
- - - diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.scss deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.spec.ts deleted file mode 100644 index a5607adebae04..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ToastrModule } from 'ngx-toastr'; - -import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; -import { SharedModule } from '../../../shared/shared.module'; -import { UserPasswordLoginFormComponent } from './user-password-login-form.component'; - -describe('UserPasswordLoginFormComponent', () => { - let component: UserPasswordLoginFormComponent; - let fixture: ComponentFixture; - - configureTestBed( - { - imports: [ - HttpClientTestingModule, - RouterTestingModule, - ReactiveFormsModule, - ToastrModule.forRoot(), - SharedModule - ], - declarations: [UserPasswordLoginFormComponent], - providers: i18nProviders - }, - true - ); - - beforeEach(() => { - fixture = TestBed.createComponent(UserPasswordLoginFormComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.ts deleted file mode 100644 index 1b1ecfead3227..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core'; - -import { UserPasswordFormComponent } from '../user-password-form/user-password-form.component'; - -@Component({ - selector: 'cd-user-password-login-form', - templateUrl: './user-password-login-form.component.html', - styleUrls: ['./user-password-login-form.component.scss'] -}) -export class UserPasswordLoginFormComponent extends UserPasswordFormComponent {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts index b452185fd124b..c220fa6dedf16 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts @@ -7,6 +7,7 @@ import { BlockUIModule } from 'ng-block-ui'; import { SharedModule } from '../shared/shared.module'; import { ForbiddenComponent } from './forbidden/forbidden.component'; import { BlankLayoutComponent } from './layouts/blank-layout/blank-layout.component'; +import { LoginLayoutComponent } from './layouts/login-layout/login-layout.component'; import { WorkbenchLayoutComponent } from './layouts/workbench-layout/workbench-layout.component'; import { NavigationModule } from './navigation/navigation.module'; import { NotFoundComponent } from './not-found/not-found.component'; @@ -18,7 +19,8 @@ import { NotFoundComponent } from './not-found/not-found.component'; NotFoundComponent, ForbiddenComponent, WorkbenchLayoutComponent, - BlankLayoutComponent + BlankLayoutComponent, + LoginLayoutComponent ] }) export class CoreModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.html new file mode 100644 index 0000000000000..f223ea0af95e8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.html @@ -0,0 +1,24 @@ + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.scss new file mode 100644 index 0000000000000..c88a7664bc9cc --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.scss @@ -0,0 +1,31 @@ +@import 'defaults'; + +::ng-deep .login { + color: $color-login-row-text; + background-color: $color-login-row-bg; + + header { + position: absolute; + width: 100vw; + + .navbar { + padding: 1rem 2rem; + + .dropdown-menu { + margin-top: 0.2rem; + + li a { + &:hover { + background-color: $color-brand-teal; + } + } + } + } + } + + section { + display: inline-flex; + width: 100vw; + min-height: 100vh; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.spec.ts new file mode 100644 index 0000000000000..942f8eee5d2b2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.spec.ts @@ -0,0 +1,37 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; + +import { RouterTestingModule } from '@angular/router/testing'; +import { SharedModule } from '../../../shared/shared.module'; +import { LoginLayoutComponent } from './login-layout.component'; + +describe('LoginLayoutComponent', () => { + let component: LoginLayoutComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [LoginLayoutComponent], + imports: [ + BsDropdownModule.forRoot(), + BsDatepickerModule.forRoot(), + HttpClientTestingModule, + RouterTestingModule, + SharedModule + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginLayoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.ts new file mode 100644 index 0000000000000..ef40fcf885655 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'cd-login-layout', + templateUrl: './login-layout.component.html', + styleUrls: ['./login-layout.component.scss'] +}) +export class LoginLayoutComponent {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.spec.ts index 7764ba063be80..86b8c06e503a2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.spec.ts @@ -145,6 +145,13 @@ describe('SettingsService', () => { it('should return the specified settings (2)', () => { service.getValues(['abc', 'xyz']).subscribe(); - httpTesting.expectOne('api/settings?names=abc,xyz'); + const req = httpTesting.expectOne('api/settings?names=abc,xyz'); + expect(req.request.method).toBe('GET'); + }); + + it('should return standard settings', () => { + service.getStandardSettings().subscribe(); + const req = httpTesting.expectOne('ui-api/standard_settings'); + expect(req.request.method).toBe('GET'); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.ts index 3dd4d3964423c..05a46caec610a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.ts @@ -5,7 +5,6 @@ import * as _ from 'lodash'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { CdPwdExpirationSettings } from '../models/cd-pwd-expiration-settings'; import { ApiModule } from './api.module'; class SettingResponse { @@ -74,7 +73,7 @@ export class SettingsService { return this.http.get(`api/grafana/validation/${uid}`); } - pwdExpirationSettings(): Observable { - return this.http.get('ui-api/standard_settings'); + getStandardSettings(): Observable<{ [key: string]: any }> { + return this.http.get('ui-api/standard_settings'); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.spec.ts index c756074f1ea3b..3273a4e84f23d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.spec.ts @@ -40,7 +40,7 @@ describe('PwdExpirationNotificationComponent', () => { authStorageService = TestBed.get(AuthStorageService); settingsService = TestBed.get(SettingsService); spyOn(authStorageService, 'getPwdExpirationDate').and.returnValue(1645488000); - spyOn(settingsService, 'pwdExpirationSettings').and.returnValue( + spyOn(settingsService, 'getStandardSettings').and.returnValue( observableOf({ user_pwd_expiration_warning_1: 10, user_pwd_expiration_warning_2: 5, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.ts index 676fe5148e83b..0955f52e93657 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.ts @@ -20,7 +20,7 @@ export class PwdExpirationNotificationComponent implements OnInit { ) {} ngOnInit() { - this.settingsService.pwdExpirationSettings().subscribe((pwdExpirationSettings) => { + this.settingsService.getStandardSettings().subscribe((pwdExpirationSettings) => { this.pwdExpirationSettings = new CdPwdExpirationSettings(pwdExpirationSettings); const pwdExpirationDate = this.authStorageService.getPwdExpirationDate(); if (pwdExpirationDate) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.html index 4a517f39ef78e..03c92acfd8148 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.html @@ -1,5 +1,6 @@