]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Enforce password change upon first login - Part 2 32680/head
authorVolker Theile <vtheile@suse.com>
Mon, 13 Jan 2020 09:06:25 +0000 (10:06 +0100)
committerVolker Theile <vtheile@suse.com>
Wed, 19 Feb 2020 14:54:42 +0000 (15:54 +0100)
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 <vtheile@suse.com>
46 files changed:
doc/mgr/dashboard.rst
qa/tasks/mgr/dashboard/test_user.py
src/pybind/mgr/dashboard/controllers/settings.py
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.html [new file with mode: 0755]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.scss [new file with mode: 0755]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.spec.ts [new file with mode: 0755]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login-password-form/login-password-form.component.ts [new file with mode: 0755]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss
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-password-form/user-password-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/login-layout/login-layout.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/settings.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/pwd-expiration-notification/pwd-expiration-notification.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-expiration-settings.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-policy-settings.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/password-policy.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/password-policy.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.ts
src/pybind/mgr/dashboard/frontend/src/styles.scss
src/pybind/mgr/dashboard/frontend/src/styles/defaults.scss
src/pybind/mgr/dashboard/services/access_control.py
src/pybind/mgr/dashboard/tests/test_access_control.py

index 8ee2c548bd30a0bbdb61d816abe29474c45c1638..9e3a3e045501f1b962a35c55d311776b6a28535b 100644 (file)
@@ -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.
 
index 2230be20e72124659552be38647f73540a98b01a..ac7517cccd1c91b0781797f6da0eed8c827f8e37 100644 (file)
@@ -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',
index 4fb63a379a8731d3c4e19ffd3ebfa4979e0fec5d..9cde5e9ec12d4cb491e3752450b1fe093a55f062 100644 (file)
@@ -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
         }
index f766613ad3baa480c06a9fc8a5d662de8cc6af90..1b94b3d9be877ed296c0112a473a47bfcfa878b1 100644 (file)
@@ -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' }
index 43d56c9bad5eb703df588765a8a580db12dbafcb..9171e24e453aa22cfd8a53f1fccdfbf0ff8900f7 100644 (file)
@@ -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 (executable)
index 0000000..d98e16c
--- /dev/null
@@ -0,0 +1,102 @@
+<div>
+  <h1 i18n>Please set a new password.</h1>
+  <h4 i18n>You will be redirected to the login page afterwards.</h4>
+  <form #frm="ngForm"
+        [formGroup]="userForm"
+        novalidate>
+
+    <!-- Old password -->
+    <div class="form-group has-feedback">
+      <div class="input-group">
+        <input class="form-control"
+               type="password"
+               placeholder="Old password..."
+               id="oldpassword"
+               formControlName="oldpassword"
+               autocomplete="new-password"
+               autofocus>
+        <span class="input-group-append">
+          <button class="btn btn-outline-light btn-password"
+                  cdPasswordButton="oldpassword">
+          </button>
+        </span>
+      </div>
+      <span class="invalid-feedback"
+            *ngIf="userForm.showError('oldpassword', frm, 'required')"
+            i18n>This field is required.</span>
+      <span class="invalid-feedback"
+            *ngIf="userForm.showError('oldpassword', frm, 'notmatch')"
+            i18n>The old and new passwords must be different.</span>
+    </div>
+
+    <!-- New password -->
+    <div class="form-group has-feedback">
+      <div class="input-group">
+        <input class="form-control"
+               type="password"
+               placeholder="New password..."
+               id="newpassword"
+               autocomplete="new-password"
+               formControlName="newpassword">
+        <span class="input-group-append">
+          <button type="button"
+                  class="btn btn-outline-light btn-password"
+                  cdPasswordButton="newpassword">
+          </button>
+        </span>
+      </div>
+      <div class="password-strength-level">
+        <div class="{{ passwordStrengthLevelClass }}"
+             data-toggle="tooltip"
+             title="{{ passwordValuation }}">
+        </div>
+      </div>
+      <span class="invalid-feedback"
+            *ngIf="userForm.showError('newpassword', frm, 'required')"
+            i18n>This field is required.</span>
+      <span class="invalid-feedback"
+            *ngIf="userForm.showError('newpassword', frm, 'notmatch')"
+            i18n>The old and new passwords must be different.</span>
+      <span class="invalid-feedback"
+            *ngIf="userForm.showError('newpassword', frm, 'passwordPolicy')">
+        {{ passwordValuation }}
+      </span>
+    </div>
+
+    <!-- Confirm new password -->
+    <div class="form-group has-feedback">
+      <div class="input-group">
+        <input class="form-control"
+               type="password"
+               autocomplete="new-password"
+               placeholder="Confirm new password..."
+               id="confirmnewpassword"
+               formControlName="confirmnewpassword">
+        <span class="input-group-append">
+          <button class="btn btn-outline-light btn-password"
+                  cdPasswordButton="confirmnewpassword">
+          </button>
+        </span>
+      </div>
+      <span class="invalid-feedback"
+            *ngIf="userForm.showError('confirmnewpassword', frm, 'required')"
+            i18n>This field is required.</span>
+      <span class="invalid-feedback"
+            *ngIf="userForm.showError('confirmnewpassword', frm, 'match')"
+            i18n>Password confirmation doesn't match the new password.</span>
+    </div>
+  </form>
+  <div class="form-footer">
+    <cd-submit-button class="full-width"
+                      btnClass="btn-block"
+                      (submitAction)="onSubmit()"
+                      [form]="userForm"
+                      i18n="form action button|Example: Create Pool@@formActionButton">
+      {{ action | titlecase }} {{ resource | upperFirst }}
+    </cd-submit-button>
+    <button class="btn btn-light"
+            (click)="onCancel()">
+      <ng-container i18n>Cancel</ng-container>
+    </button>
+  </div>
+</div>
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 (executable)
index 0000000..0feabe5
--- /dev/null
@@ -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 (executable)
index 0000000..cf21e74
--- /dev/null
@@ -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<LoginPasswordFormComponent>;
+  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 (executable)
index 0000000..e952faf
--- /dev/null
@@ -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();
+  }
+}
index ab42a8ad83d03baa0b7834d191a98f057779008c..b8a0a629a63b9c3b7bccd1cb7f4c8ce04c17e636 100644 (file)
@@ -1,79 +1,52 @@
-<div class="login"
-     *ngIf="isLoginActive || pwdUpdateRequired">
-  <header>
-    <nav class="navbar">
-      <a class="navbar-brand"></a>
-      <div class="form-inline">
-        <cd-language-selector></cd-language-selector>
-      </div>
-    </nav>
-  </header>
-
-
-  <section>
-    <div class="container">
-      <div class="row full-height vertical-align">
-        <div class="col-sm-6 d-none d-sm-block">
-          <img src="assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png"
-               alt="Ceph"
-               class="float-right img-fluid">
-        </div>
-
-        <div class="col-12 col-sm-6 col-xl-5">
-          <h1 i18n="The welcome message on the login page">Welcome to Ceph!</h1>
-          <form name="loginForm"
-                (ngSubmit)="login()"
-                #loginForm="ngForm"
-                *ngIf="!pwdUpdateRequired"
-                novalidate>
+<div *ngIf="isLoginActive">
+  <h1 i18n="The welcome message on the login page">Welcome to Ceph!</h1>
+  <form name="loginForm"
+        (ngSubmit)="login()"
+        #loginForm="ngForm"
+        novalidate>
 
-            <!-- Username -->
-            <div class="form-group has-feedback">
-              <input name="username"
-                     [(ngModel)]="model.username"
-                     #username="ngModel"
-                     type="text"
-                     placeholder="Enter your username..."
-                     class="form-control"
-                     required
-                     autofocus>
-              <div class="invalid-feedback"
-                   *ngIf="(loginForm.submitted || username.dirty) && username.invalid"
-                   i18n>Username is required</div>
-            </div>
-
-            <!-- Password -->
-            <div class="form-group has-feedback">
-              <div class="input-group">
-                <input id="password"
-                       name="password"
-                       [(ngModel)]="model.password"
-                       #password="ngModel"
-                       type="password"
-                       placeholder="Enter your password..."
-                       class="form-control"
-                       required>
-                <span class="input-group-append">
-                  <button type="button"
-                          class="btn btn-outline-light btn-password"
-                          cdPasswordButton="password">
-                  </button>
-                </span>
-              </div>
-              <div class="invalid-feedback"
-                   *ngIf="(loginForm.submitted || password.dirty) && password.invalid"
-                   i18n>Password is required</div>
-            </div>
+    <!-- Username -->
+    <div class="form-group has-feedback">
+      <input name="username"
+             [(ngModel)]="model.username"
+             #username="ngModel"
+             type="text"
+             placeholder="Enter your username..."
+             class="form-control"
+             required
+             autofocus>
+      <div class="invalid-feedback"
+           *ngIf="(loginForm.submitted || username.dirty) && username.invalid"
+           i18n>Username is required</div>
+    </div>
 
-            <input type="submit"
-                   class="btn btn-secondary btn-block"
-                   [disabled]="loginForm.invalid"
-                   value="Login"
-                   i18n-value>
-          </form>
-          <cd-user-password-login-form *ngIf="pwdUpdateRequired"></cd-user-password-login-form>
-        </div>
+    <!-- Password -->
+    <div class="form-group has-feedback">
+      <div class="input-group">
+        <input id="password"
+               name="password"
+               [(ngModel)]="model.password"
+               #password="ngModel"
+               type="password"
+               placeholder="Enter your password..."
+               class="form-control"
+               required>
+        <span class="input-group-append">
+          <button type="button"
+                  class="btn btn-outline-light btn-password"
+                  cdPasswordButton="password">
+          </button>
+        </span>
       </div>
+      <div class="invalid-feedback"
+           *ngIf="(loginForm.submitted || password.dirty) && password.invalid"
+           i18n>Password is required</div>
     </div>
-  </section>
+
+    <input type="submit"
+           class="btn btn-secondary btn-block"
+           [disabled]="loginForm.invalid"
+           value="Login"
+           i18n-value>
+  </form>
 </div>
index e088fe0b04326ba1616ec30ea967896ddcf8aca9..7535c8c84ff6c5d3734eee0520c70f899302b0cd 100644 (file)
@@ -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;
   }
 }
 
index de8a44ddfea67a7b4265ee653de6c05ee8181493..9286e31d30f107ea11a64662cdd9111cbacbda0b 100644 (file)
@@ -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(['']);
     });
   }
 }
index da786a20db3753c3bdcaf182dac752d01a61135d..cee4438001041eea5a623eb41501c7c023f8bdc3 100644 (file)
                   i18n>Password confirmation doesn't match the password.</span>
           </div>
         </div>
+
         <!-- Password expiration date -->
         <div class="form-group row"
              *ngIf="!authStorageService.isSSO()">
           </div>
         </div>
 
-        <!-- Change Password -->
+        <!-- Force change password -->
         <div class="form-group row"
-             *ngIf="!isCurrentUser()">
-          <div class="offset-sm-3 col-sm-9">
+             *ngIf="!isCurrentUser() && !authStorageService.isSSO()">
+          <div class="cd-col-form-offset">
             <div class="custom-control custom-checkbox">
               <input type="checkbox"
                      class="custom-control-input"
index 87ba10d690c6e646b06075eaa57fa31680b9c344..9574504f0fd2c30f3c62cfbc8762513bd9efcdea 100644 (file)
@@ -176,18 +176,17 @@ 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)
+      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');
index 096e2e40a9bf97adf12eecab2cd8e2f4dd8ce005..75e14e330361fcee56d3b0ef2aad1a03297723f3 100644 (file)
@@ -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) => {
index e513447b580e2b4954af4185d5af20b42eaf68f3..08d1c1c75e571aa05811ea3553ba11e14e612adb 100644 (file)
@@ -19,7 +19,7 @@
                      placeholder="Old password..."
                      id="oldpassword"
                      formControlName="oldpassword"
-                     autocomplete="off"
+                     autocomplete="new-password"
                      autofocus>
               <span class="input-group-append">
                 <button class="btn btn-light"
@@ -90,7 +90,7 @@
             <div class="input-group">
               <input class="form-control"
                      type="password"
-                     autocomplete="off"
+                     autocomplete="new-password"
                      placeholder="Confirm new password..."
                      id="confirmnewpassword"
                      formControlName="confirmnewpassword">
index cfd8e0c7120d8f5ffd68628a3e60a17de45afd0e..3ac5f040fb0944f66d14cfac580409a0ae25b550 100644 (file)
@@ -66,6 +66,7 @@ describe('UserPasswordFormComponent', () => {
   });
 
   it('should submit', () => {
+    spyOn(component, 'onPasswordChange').and.callThrough();
     spyOn(authStorageService, 'getUsername').and.returnValue('xyz');
     formHelper.setMultipleValues({
       oldpassword: 'foo',
@@ -80,6 +81,7 @@ describe('UserPasswordFormComponent', () => {
       new_password: 'bar'
     });
     request.flush({});
+    expect(component.onPasswordChange).toHaveBeenCalled();
     expect(router.navigate).toHaveBeenCalledWith(['/logout']);
   });
 });
index ae24f9e99ec6c2e7a87b08817665dde817c0aca4..e77d9d86bf6fdf2d395196aba3c9fb6853da55bb 100644 (file)
@@ -31,14 +31,14 @@ export class UserPasswordFormComponent {
   icons = Icons;
 
   constructor(
-    private i18n: I18n,
+    public i18n: I18n,
     public actionLabels: ActionLabelsI18n,
-    private notificationService: NotificationService,
-    private userService: UserService,
-    private authStorageService: AuthStorageService,
-    private formBuilder: CdFormBuilder,
-    private router: Router,
-    private passwordPolicyService: PasswordPolicyService
+    public notificationService: NotificationService,
+    public userService: UserService,
+    public authStorageService: AuthStorageService,
+    public formBuilder: CdFormBuilder,
+    public router: Router,
+    public passwordPolicyService: PasswordPolicyService
   ) {
     this.action = this.actionLabels.CHANGE;
     this.resource = this.i18n('password');
@@ -103,20 +103,23 @@ export class UserPasswordFormComponent {
     const oldPassword = this.userForm.getValue('oldpassword');
     const newPassword = this.userForm.getValue('newpassword');
     this.userService.changePassword(username, oldPassword, newPassword).subscribe(
-      () => {
-        this.notificationService.show(
-          NotificationType.success,
-          this.i18n('Updated user password"')
-        );
-        // Theoretically it is not necessary to navigate to '/logout' because
-        // the auth token gets invalid after changing the password in the
-        // backend, thus the user would be automatically logged out after the
-        // next periodically API request is executed.
-        this.router.navigate(['/logout']);
-      },
+      () => this.onPasswordChange(),
       () => {
         this.userForm.setErrors({ cdSubmitButton: true });
       }
     );
   }
+
+  /**
+   * The function that is called after the password has been changed.
+   * Override this in derived classes to change the behaviour.
+   */
+  onPasswordChange() {
+    this.notificationService.show(NotificationType.success, this.i18n('Updated user password"'));
+    // Theoretically it is not necessary to navigate to '/logout' because
+    // the auth token gets invalid after changing the password in the
+    // backend, thus the user would be automatically logged out after the
+    // next periodically API request is executed.
+    this.router.navigate(['/logout']);
+  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-login-form/user-password-login-form.component.html
deleted file mode 100644 (file)
index b74107c..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-<form #frm="ngForm"
-      [formGroup]="userForm"
-      (ngSubmit)="onSubmit()"
-      novalidate>
-  <div class="form-group has-feedback">
-    <div class="input-group">
-      <input class="form-control"
-             type="password"
-             placeholder="Old password..."
-             id="oldpassword"
-             formControlName="oldpassword"
-             autocomplete="off"
-             autofocus>
-      <span class="input-group-append">
-        <button class="btn btn-outline-light btn-password"
-                cdPasswordButton="oldpassword">
-        </button>
-      </span>
-    </div>
-    <span class="invalid-feedback"
-          *ngIf="userForm.showError('oldpassword', frm, 'required')"
-          i18n>This field is required.</span>
-    <span class="invalid-feedback"
-          *ngIf="userForm.showError('oldpassword', frm, 'notmatch')"
-          i18n>The old and new passwords must be different.</span>
-  </div>
-  <div class="form-group has-feedback">
-    <div class="input-group">
-      <input class="form-control"
-             type="password"
-             placeholder="Password..."
-             id="newpassword"
-             autocomplete="new-password"
-             formControlName="newpassword">
-      <span class="input-group-append">
-        <button type="button"
-                class="btn btn-outline-light btn-password"
-                cdPasswordButton="newpassword">
-        </button>
-      </span>
-    </div>
-    <div class="passwordStrengthLevel">
-      <div class="{{ passwordStrengthLevel }}"
-           data-toggle="tooltip"
-           title="{{ passwordStrengthDescription }}">
-      </div>
-    </div>
-    <span class="invalid-feedback"
-          *ngIf="userForm.showError('newpassword', frm, 'required')"
-          i18n>This field is required.</span>
-    <span class="invalid-feedback"
-          *ngIf="userForm.showError('newpassword', frm, 'notmatch')"
-          i18n>The old and new passwords must be different.</span>
-    <span class="invalid-feedback"
-          *ngIf="userForm.showError('newpassword', frm, 'checkPassword')"
-          i18n>Too weak</span>
-  </div>
-  <div class="form-group has-feedback">
-    <div class="input-group">
-      <input class="form-control"
-             type="password"
-             autocomplete="off"
-             placeholder="Confirm new password..."
-             id="confirmnewpassword"
-             formControlName="confirmnewpassword">
-      <span class="input-group-append">
-        <button class="btn btn-outline-light btn-password"
-                cdPasswordButton="confirmnewpassword">
-        </button>
-      </span>
-    </div>
-    <span class="invalid-feedback"
-          *ngIf="userForm.showError('confirmnewpassword', frm, 'required')"
-          i18n>This field is required.</span>
-    <span class="invalid-feedback"
-          *ngIf="userForm.showError('confirmnewpassword', frm, 'match')"
-          i18n>Password confirmation doesn't match the new password.</span>
-  </div>
-
-  <input type="submit"
-         class="btn btn-secondary btn-block"
-         [disabled]="userForm.invalid"
-         value="Change password"
-         i18n-value>
-</form>
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 (file)
index e69de29..0000000
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 (file)
index a5607ad..0000000
+++ /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<UserPasswordLoginFormComponent>;
-
-  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 (file)
index 1b1ecfe..0000000
+++ /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 {}
index b452185fd124b5c565b04ec8e08615cef2a4b82f..c220fa6dedf1690925709f2f84dd1ab0da0a6f92 100644 (file)
@@ -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 (file)
index 0000000..f223ea0
--- /dev/null
@@ -0,0 +1,24 @@
+<div class="login full-height">
+  <header>
+    <nav class="navbar">
+      <a class="navbar-brand"></a>
+      <div class="form-inline">
+        <cd-language-selector></cd-language-selector>
+      </div>
+    </nav>
+  </header>
+  <section>
+    <div class="container">
+      <div class="row full-height vertical-align">
+        <div class="col-sm-6 d-none d-sm-block">
+          <img src="assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png"
+               alt="Ceph"
+               class="float-right img-fluid">
+        </div>
+        <div class="col-12 col-sm-6 col-xl-5">
+          <router-outlet></router-outlet>
+        </div>
+      </div>
+    </div>
+  </section>
+</div>
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 (file)
index 0000000..c88a766
--- /dev/null
@@ -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 (file)
index 0000000..942f8ee
--- /dev/null
@@ -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<LoginLayoutComponent>;
+
+  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 (file)
index 0000000..ef40fcf
--- /dev/null
@@ -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 {}
index 7764ba063be80071131e80f9abcd579d1e18a9ae..86b8c06e503a25c667400c95729d555d21c969fd 100644 (file)
@@ -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');
   });
 });
index 3dd4d3964423cd50a2437c8adb61945fb79b2724..05a46caec610aa0ae701e7984f1d05da7a877225 100644 (file)
@@ -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<CdPwdExpirationSettings> {
-    return this.http.get<CdPwdExpirationSettings>('ui-api/standard_settings');
+  getStandardSettings(): Observable<{ [key: string]: any }> {
+    return this.http.get('ui-api/standard_settings');
   }
 }
index c756074f1ea3b669aba0d63392c14231dbdacc20..3273a4e84f23d05ac36b39749c829f0bbc6a83a5 100644 (file)
@@ -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,
index 676fe5148e83b3132d504e7f9f98f720b61677d0..0955f52e93657ed8b61bbe33a05b439052542752 100644 (file)
@@ -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) {
index 4a517f39ef78efb229fbf46c1e118697c3ebd32e..03c92acfd8148a25f42e94d989cad20629b1c6a3 100644 (file)
@@ -1,5 +1,6 @@
 <button [type]="type"
         class="btn btn-secondary tc_submitButton"
+        [ngClass]="btnClass"
         [disabled]="loading || disabled"
         (click)="submit($event)">
   <ng-content></ng-content>
index 5d62b892dd6acdf57600137dc660c1ad2839dc2b..1ef6cb2ffb5160cfe50ea88e217d26b212380970 100644 (file)
@@ -30,13 +30,20 @@ import * as _ from 'lodash';
 export class SubmitButtonComponent implements OnInit {
   @Input()
   form: FormGroup | NgForm;
+
   @Input()
   type = 'submit';
-  @Output()
-  submitAction = new EventEmitter();
+
   @Input()
   disabled = false;
 
+  // A CSS class string to apply to the button's main element.
+  @Input()
+  btnClass: string;
+
+  @Output()
+  submitAction = new EventEmitter();
+
   loading = false;
   icons = Icons;
 
index 1df472d660dc293d157102ff17fa7b87d2138de8..53b9d14fddb2366749a9a26924f84261823ce1c8 100644 (file)
@@ -3,9 +3,9 @@ export class CdPwdExpirationSettings {
   pwdExpirationWarning1: number;
   pwdExpirationWarning2: number;
 
-  constructor(data: any) {
-    this.pwdExpirationSpan = data.user_pwd_expiration_span;
-    this.pwdExpirationWarning1 = data.user_pwd_expiration_warning_1;
-    this.pwdExpirationWarning2 = data.user_pwd_expiration_warning_2;
+  constructor(settings: { [key: string]: any }) {
+    this.pwdExpirationSpan = settings.user_pwd_expiration_span;
+    this.pwdExpirationWarning1 = settings.user_pwd_expiration_warning_1;
+    this.pwdExpirationWarning2 = settings.user_pwd_expiration_warning_2;
   }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-policy-settings.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-pwd-policy-settings.ts
new file mode 100644 (file)
index 0000000..fef570f
--- /dev/null
@@ -0,0 +1,23 @@
+export class CdPwdPolicySettings {
+  pwdPolicyEnabled: boolean;
+  pwdPolicyMinLength: number;
+  pwdPolicyCheckLengthEnabled: boolean;
+  pwdPolicyCheckOldpwdEnabled: boolean;
+  pwdPolicyCheckUsernameEnabled: boolean;
+  pwdPolicyCheckExclusionListEnabled: boolean;
+  pwdPolicyCheckRepetitiveCharsEnabled: boolean;
+  pwdPolicyCheckSequentialCharsEnabled: boolean;
+  pwdPolicyCheckComplexityEnabled: boolean;
+
+  constructor(settings: { [key: string]: any }) {
+    this.pwdPolicyEnabled = settings.pwd_policy_enabled;
+    this.pwdPolicyMinLength = settings.pwd_policy_min_length;
+    this.pwdPolicyCheckLengthEnabled = settings.pwd_policy_check_length_enabled;
+    this.pwdPolicyCheckOldpwdEnabled = settings.pwd_policy_check_oldpwd_enabled;
+    this.pwdPolicyCheckUsernameEnabled = settings.pwd_policy_check_username_enabled;
+    this.pwdPolicyCheckExclusionListEnabled = settings.pwd_policy_check_exclusion_list_enabled;
+    this.pwdPolicyCheckRepetitiveCharsEnabled = settings.pwd_policy_check_repetitive_chars_enabled;
+    this.pwdPolicyCheckSequentialCharsEnabled = settings.pwd_policy_check_sequential_chars_enabled;
+    this.pwdPolicyCheckComplexityEnabled = settings.pwd_policy_check_complexity_enabled;
+  }
+}
index 0d46ed1236858edea02b3aa5419f60607416a240..c65f8c051882729ca601da4c9da20e9b25a89d7d 100644 (file)
@@ -61,11 +61,7 @@ export class ApiInterceptorService implements HttpInterceptor {
               this.router.navigate(['/login']);
               break;
             case 403:
-              if (this.authStorageService.getPwdUpdateRequired()) {
-                this.router.navigate(['/login']);
-              } else {
-                this.router.navigate(['/403']);
-              }
+              this.router.navigate(['/403']);
               break;
             default:
               timeoutId = this.prepareNotification(resp);
index 48a367ff99545cdde72c2ac2482697cb5327f2a1..7e11d9a2d033804488e687e5504b884ede4bfb0d 100644 (file)
@@ -10,7 +10,7 @@ export class AuthGuardService implements CanActivate, CanActivateChild {
   constructor(private router: Router, private authStorageService: AuthStorageService) {}
 
   canActivate() {
-    if (this.authStorageService.isLoggedIn() && !this.authStorageService.getPwdUpdateRequired()) {
+    if (this.authStorageService.isLoggedIn()) {
       return true;
     }
     this.router.navigate(['/login']);
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.spec.ts
new file mode 100644 (file)
index 0000000..0a446b6
--- /dev/null
@@ -0,0 +1,64 @@
+import { Component, NgZone } from '@angular/core';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '../../../testing/unit-test-helper';
+import { AuthStorageService } from './auth-storage.service';
+import { ChangePasswordGuardService } from './change-password-guard.service';
+
+describe('ChangePasswordGuardService', () => {
+  let service: ChangePasswordGuardService;
+  let authStorageService: AuthStorageService;
+  let ngZone: NgZone;
+
+  @Component({ selector: 'cd-login-password-form', template: '' })
+  class LoginPasswordFormComponent {}
+
+  const routes: Routes = [{ path: 'login-change-password', component: LoginPasswordFormComponent }];
+
+  configureTestBed({
+    imports: [RouterTestingModule.withRoutes(routes)],
+    providers: [ChangePasswordGuardService, AuthStorageService],
+    declarations: [LoginPasswordFormComponent]
+  });
+
+  beforeEach(() => {
+    service = TestBed.get(ChangePasswordGuardService);
+    authStorageService = TestBed.get(AuthStorageService);
+    ngZone = TestBed.get(NgZone);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+
+  it('should do nothing (not logged in)', () => {
+    spyOn(authStorageService, 'isLoggedIn').and.returnValue(false);
+    expect(service.canActivate()).toBeTruthy();
+  });
+
+  it('should do nothing (SSO enabled)', () => {
+    spyOn(authStorageService, 'isLoggedIn').and.returnValue(true);
+    spyOn(authStorageService, 'isSSO').and.returnValue(true);
+    expect(service.canActivate()).toBeTruthy();
+  });
+
+  it('should do nothing (no update pwd required)', () => {
+    spyOn(authStorageService, 'isLoggedIn').and.returnValue(true);
+    spyOn(authStorageService, 'getPwdUpdateRequired').and.returnValue(false);
+    expect(service.canActivate()).toBeTruthy();
+  });
+
+  it('should redirect to change password page', fakeAsync(() => {
+    spyOn(authStorageService, 'isLoggedIn').and.returnValue(true);
+    spyOn(authStorageService, 'isSSO').and.returnValue(false);
+    spyOn(authStorageService, 'getPwdUpdateRequired').and.returnValue(true);
+    const router = TestBed.get(Router);
+    ngZone.run(() => {
+      expect(service.canActivate()).toBeFalsy();
+    });
+    tick();
+    expect(router.url).toBe('/login-change-password');
+  }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/change-password-guard.service.ts
new file mode 100644 (file)
index 0000000..7a3332b
--- /dev/null
@@ -0,0 +1,36 @@
+import { Injectable } from '@angular/core';
+import { CanActivate, CanActivateChild, Router } from '@angular/router';
+
+import { AuthStorageService } from './auth-storage.service';
+
+/**
+ * This service guard checks if a user must be redirected to a special
+ * page at '/login-change-password' to set a new password.
+ */
+@Injectable({
+  providedIn: 'root'
+})
+export class ChangePasswordGuardService implements CanActivate, CanActivateChild {
+  constructor(private router: Router, private authStorageService: AuthStorageService) {}
+
+  canActivate() {
+    // Redirect to '/login-change-password' when the following constraints
+    // are fulfilled:
+    // - The user must be logged in.
+    // - SSO must be disabled.
+    // - The flag 'User must change password at next logon' must be set.
+    if (
+      this.authStorageService.isLoggedIn() &&
+      !this.authStorageService.isSSO() &&
+      this.authStorageService.getPwdUpdateRequired()
+    ) {
+      this.router.navigate(['/login-change-password']);
+      return false;
+    }
+    return true;
+  }
+
+  canActivateChild(): boolean {
+    return this.canActivate();
+  }
+}
index 2275c0bef5ec79a69b733a06b8bde469862a1fcf..23d0e6651190b98d2aba218d83a55120bd27c107 100644 (file)
@@ -49,9 +49,9 @@ describe('PasswordPolicyService', () => {
 
   it('should not get help text', () => {
     let helpText = '';
-    spyOn(settingsService, 'getValues').and.returnValue(
+    spyOn(settingsService, 'getStandardSettings').and.returnValue(
       observableOf({
-        PWD_POLICY_ENABLED: false
+        pwd_policy_enabled: false
       })
     );
     service.getHelpText().subscribe((text) => (helpText = text));
@@ -61,14 +61,17 @@ describe('PasswordPolicyService', () => {
   it('should get help text chk_length', () => {
     let helpText = '';
     const expectedHelpText = helpTextHelper.get('chk_length');
-    spyOn(settingsService, 'getValues').and.returnValue(
+    spyOn(settingsService, 'getStandardSettings').and.returnValue(
       observableOf({
-        PWD_POLICY_ENABLED: true,
-        PWD_POLICY_MIN_LENGTH: 10,
-        PWD_POLICY_CHECK_LENGTH_ENABLED: true,
-        PWD_POLICY_CHECK_OLDPWD_ENABLED: false,
-        PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED: false,
-        PWD_POLICY_CHECK_COMPLEXITY_ENABLED: false
+        user_pwd_expiration_warning_1: 10,
+        user_pwd_expiration_warning_2: 5,
+        user_pwd_expiration_span: 90,
+        pwd_policy_enabled: true,
+        pwd_policy_min_length: 10,
+        pwd_policy_check_length_enabled: true,
+        pwd_policy_check_oldpwd_enabled: false,
+        pwd_policy_check_sequential_chars_enabled: false,
+        pwd_policy_check_complexity_enabled: false
       })
     );
     service.getHelpText().subscribe((text) => (helpText = text));
@@ -78,13 +81,13 @@ describe('PasswordPolicyService', () => {
   it('should get help text chk_oldpwd', () => {
     let helpText = '';
     const expectedHelpText = helpTextHelper.get('chk_oldpwd');
-    spyOn(settingsService, 'getValues').and.returnValue(
+    spyOn(settingsService, 'getStandardSettings').and.returnValue(
       observableOf({
-        PWD_POLICY_ENABLED: true,
-        PWD_POLICY_CHECK_OLDPWD_ENABLED: true,
-        PWD_POLICY_CHECK_USERNAME_ENABLED: false,
-        PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: false,
-        PWD_POLICY_CHECK_COMPLEXITY_ENABLED: false
+        pwd_policy_enabled: true,
+        pwd_policy_check_oldpwd_enabled: true,
+        pwd_policy_check_username_enabled: false,
+        pwd_policy_check_exclusion_list_enabled: false,
+        pwd_policy_check_complexity_enabled: false
       })
     );
     service.getHelpText().subscribe((text) => (helpText = text));
@@ -94,12 +97,12 @@ describe('PasswordPolicyService', () => {
   it('should get help text chk_username', () => {
     let helpText = '';
     const expectedHelpText = helpTextHelper.get('chk_username');
-    spyOn(settingsService, 'getValues').and.returnValue(
+    spyOn(settingsService, 'getStandardSettings').and.returnValue(
       observableOf({
-        PWD_POLICY_ENABLED: true,
-        PWD_POLICY_CHECK_OLDPWD_ENABLED: false,
-        PWD_POLICY_CHECK_USERNAME_ENABLED: true,
-        PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: false
+        pwd_policy_enabled: true,
+        pwd_policy_check_oldpwd_enabled: false,
+        pwd_policy_check_username_enabled: true,
+        pwd_policy_check_exclusion_list_enabled: false
       })
     );
     service.getHelpText().subscribe((text) => (helpText = text));
@@ -109,12 +112,12 @@ describe('PasswordPolicyService', () => {
   it('should get help text chk_exclusion_list', () => {
     let helpText = '';
     const expectedHelpText = helpTextHelper.get('chk_exclusion_list');
-    spyOn(settingsService, 'getValues').and.returnValue(
+    spyOn(settingsService, 'getStandardSettings').and.returnValue(
       observableOf({
-        PWD_POLICY_ENABLED: true,
-        PWD_POLICY_CHECK_USERNAME_ENABLED: false,
-        PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: true,
-        PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED: false
+        pwd_policy_enabled: true,
+        pwd_policy_check_username_enabled: false,
+        pwd_policy_check_exclusion_list_enabled: true,
+        pwd_policy_check_repetitive_chars_enabled: false
       })
     );
     service.getHelpText().subscribe((text) => (helpText = text));
@@ -124,14 +127,15 @@ describe('PasswordPolicyService', () => {
   it('should get help text chk_repetitive', () => {
     let helpText = '';
     const expectedHelpText = helpTextHelper.get('chk_repetitive');
-    spyOn(settingsService, 'getValues').and.returnValue(
+    spyOn(settingsService, 'getStandardSettings').and.returnValue(
       observableOf({
-        PWD_POLICY_ENABLED: true,
-        PWD_POLICY_CHECK_OLDPWD_ENABLED: false,
-        PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: false,
-        PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED: true,
-        PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED: false,
-        PWD_POLICY_CHECK_COMPLEXITY_ENABLED: false
+        user_pwd_expiration_warning_1: 10,
+        pwd_policy_enabled: true,
+        pwd_policy_check_oldpwd_enabled: false,
+        pwd_policy_check_exclusion_list_enabled: false,
+        pwd_policy_check_repetitive_chars_enabled: true,
+        pwd_policy_check_sequential_chars_enabled: false,
+        pwd_policy_check_complexity_enabled: false
       })
     );
     service.getHelpText().subscribe((text) => (helpText = text));
@@ -141,17 +145,17 @@ describe('PasswordPolicyService', () => {
   it('should get help text chk_sequential', () => {
     let helpText = '';
     const expectedHelpText = helpTextHelper.get('chk_sequential');
-    spyOn(settingsService, 'getValues').and.returnValue(
+    spyOn(settingsService, 'getStandardSettings').and.returnValue(
       observableOf({
-        PWD_POLICY_ENABLED: true,
-        PWD_POLICY_MIN_LENGTH: 8,
-        PWD_POLICY_CHECK_LENGTH_ENABLED: false,
-        PWD_POLICY_CHECK_OLDPWD_ENABLED: false,
-        PWD_POLICY_CHECK_USERNAME_ENABLED: false,
-        PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: false,
-        PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED: false,
-        PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED: true,
-        PWD_POLICY_CHECK_COMPLEXITY_ENABLED: false
+        pwd_policy_enabled: true,
+        pwd_policy_min_length: 8,
+        pwd_policy_check_length_enabled: false,
+        pwd_policy_check_oldpwd_enabled: false,
+        pwd_policy_check_username_enabled: false,
+        pwd_policy_check_exclusion_list_enabled: false,
+        pwd_policy_check_repetitive_chars_enabled: false,
+        pwd_policy_check_sequential_chars_enabled: true,
+        pwd_policy_check_complexity_enabled: false
       })
     );
     service.getHelpText().subscribe((text) => (helpText = text));
@@ -161,17 +165,17 @@ describe('PasswordPolicyService', () => {
   it('should get help text chk_complexity', () => {
     let helpText = '';
     const expectedHelpText = helpTextHelper.get('chk_complexity');
-    spyOn(settingsService, 'getValues').and.returnValue(
+    spyOn(settingsService, 'getStandardSettings').and.returnValue(
       observableOf({
-        PWD_POLICY_ENABLED: true,
-        PWD_POLICY_MIN_LENGTH: 8,
-        PWD_POLICY_CHECK_LENGTH_ENABLED: false,
-        PWD_POLICY_CHECK_OLDPWD_ENABLED: false,
-        PWD_POLICY_CHECK_USERNAME_ENABLED: false,
-        PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: false,
-        PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED: false,
-        PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED: false,
-        PWD_POLICY_CHECK_COMPLEXITY_ENABLED: true
+        pwd_policy_enabled: true,
+        pwd_policy_min_length: 8,
+        pwd_policy_check_length_enabled: false,
+        pwd_policy_check_oldpwd_enabled: false,
+        pwd_policy_check_username_enabled: false,
+        pwd_policy_check_exclusion_list_enabled: false,
+        pwd_policy_check_repetitive_chars_enabled: false,
+        pwd_policy_check_sequential_chars_enabled: false,
+        pwd_policy_check_complexity_enabled: true
       })
     );
     service.getHelpText().subscribe((text) => (helpText = text));
index 160b3eb63688d1ed90d09949aeff174ea8c139e4..936b19d8293e978f87366d6cf6d7b72b505d7f6b 100644 (file)
@@ -1,10 +1,12 @@
 import { Injectable } from '@angular/core';
 
 import { I18n } from '@ngx-translate/i18n-polyfill';
+import * as _ from 'lodash';
 import { Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
 
 import { SettingsService } from '../api/settings.service';
+import { CdPwdPolicySettings } from '../models/cd-pwd-policy-settings';
 
 @Injectable({
   providedIn: 'root'
@@ -13,60 +15,42 @@ export class PasswordPolicyService {
   constructor(private i18n: I18n, private settingsService: SettingsService) {}
 
   getHelpText(): Observable<string> {
-    return this.settingsService
-      .getValues([
-        'PWD_POLICY_ENABLED',
-        'PWD_POLICY_MIN_LENGTH',
-        'PWD_POLICY_CHECK_LENGTH_ENABLED',
-        'PWD_POLICY_CHECK_OLDPWD_ENABLED',
-        'PWD_POLICY_CHECK_USERNAME_ENABLED',
-        'PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED',
-        'PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED',
-        'PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED',
-        'PWD_POLICY_CHECK_COMPLEXITY_ENABLED'
-      ])
-      .pipe(
-        map((resp: Object[]) => {
-          let helpText: string[] = [];
-          if (resp['PWD_POLICY_ENABLED']) {
-            helpText.push(this.i18n('Required rules for passwords:'));
-            const i18nHelp: { [key: string]: string } = {
-              PWD_POLICY_CHECK_LENGTH_ENABLED: this.i18n(
-                'Must contain at least {{length}} characters',
-                {
-                  length: resp['PWD_POLICY_MIN_LENGTH']
-                }
-              ),
-              PWD_POLICY_CHECK_OLDPWD_ENABLED: this.i18n(
-                'Must not be the same as the previous one'
-              ),
-              PWD_POLICY_CHECK_USERNAME_ENABLED: this.i18n('Cannot contain the username'),
-              PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: this.i18n(
-                'Cannot contain any configured keyword'
-              ),
-              PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED: this.i18n(
-                'Cannot contain any repetitive characters e.g. "aaa"'
-              ),
-              PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED: this.i18n(
-                'Cannot contain any sequential characters e.g. "abc"'
-              ),
-              PWD_POLICY_CHECK_COMPLEXITY_ENABLED: this.i18n(
-                'Must consist of characters from the following groups:\n' +
-                  '  * Alphabetic a-z, A-Z\n' +
-                  '  * Numbers 0-9\n' +
-                  '  * Special chars: !"#$%& \'()*+,-./:;<=>?@[\\]^_`{{|}}~\n' +
-                  '  * Any other characters (signs)'
-              )
-            };
-            helpText = helpText.concat(
-              Object.keys(i18nHelp)
-                .filter((key) => resp[key])
-                .map((key) => '- ' + i18nHelp[key])
-            );
-          }
-          return helpText.join('\n');
-        })
-      );
+    return this.settingsService.getStandardSettings().pipe(
+      map((resp: { [key: string]: any }) => {
+        const settings = new CdPwdPolicySettings(resp);
+        let helpText: string[] = [];
+        if (settings.pwdPolicyEnabled) {
+          helpText.push(this.i18n('Required rules for passwords:'));
+          const i18nHelp: { [key: string]: string } = {
+            pwdPolicyCheckLengthEnabled: this.i18n('Must contain at least {{length}} characters', {
+              length: settings.pwdPolicyMinLength
+            }),
+            pwdPolicyCheckOldpwdEnabled: this.i18n('Must not be the same as the previous one'),
+            pwdPolicyCheckUsernameEnabled: this.i18n('Cannot contain the username'),
+            pwdPolicyCheckExclusionListEnabled: this.i18n('Cannot contain any configured keyword'),
+            pwdPolicyCheckRepetitiveCharsEnabled: this.i18n(
+              'Cannot contain any repetitive characters e.g. "aaa"'
+            ),
+            pwdPolicyCheckSequentialCharsEnabled: this.i18n(
+              'Cannot contain any sequential characters e.g. "abc"'
+            ),
+            pwdPolicyCheckComplexityEnabled: this.i18n(
+              'Must consist of characters from the following groups:\n' +
+                '  * Alphabetic a-z, A-Z\n' +
+                '  * Numbers 0-9\n' +
+                '  * Special chars: !"#$%& \'()*+,-./:;<=>?@[\\]^_`{{|}}~\n' +
+                '  * Any other characters (signs)'
+            )
+          };
+          helpText = helpText.concat(
+            _.keys(i18nHelp)
+              .filter((key) => _.get(settings, key))
+              .map((key) => '- ' + _.get(i18nHelp, key))
+          );
+        }
+        return helpText.join('\n');
+      })
+    );
   }
 
   /**
index 1d090d96d8dce823455bdcae583c92ad349040d5..dc199b3f2e7cf37aa643bdec042cfa67d761b021 100644 (file)
@@ -36,7 +36,7 @@ export class SummaryService {
   }
 
   refresh() {
-    if (this.router.url !== '/login') {
+    if (!_.includes(['/login', '/login-change-password'], this.router.url)) {
       this.http.get('api/summary').subscribe((data) => {
         this.summaryDataSource.next(data);
       });
index 90c44e92f4c32f5f1f33b11d80fb2dc918e72bc9..420f01efb58804e5e65b6036f299734f5d61bde6 100644 (file)
@@ -83,6 +83,9 @@ option {
 .full-height {
   height: 100vh;
 }
+.full-width {
+  width: 100vw;
+}
 .vertical-align {
   display: flex;
   align-items: center;
@@ -287,6 +290,11 @@ a {
   padding-left: 4px;
 }
 
+.form-footer {
+  width: 100%;
+  display: flex;
+}
+
 .form-control {
   display: table-cell;
 
index 04540abc5c60f6f9db7fa71aff5b5fe0d450b448..7865cb29a044e129ae6483de7ccb76f52ecb1073 100644 (file)
@@ -81,6 +81,7 @@ $color-required-text: $color-pink !default;
 $color-login-row-text: $color-solid-white !default;
 $color-login-row-bg: $color-secondary !default;
 $color-password-toggle-text: $color-solid-white !default;
+$color-password-toggle-placeholder-text: $color-blue-gray !default;
 $color-password-toggle-bg: $color-solid-gray !default;
 $color-password-toggle-focus: $color-primary !default;
 // $color-login-active-row-bg: $color-light-yellow !default;
index 86bbf8ae3fa3bb22339b03d842cd9aab955dd9f0..6b45b5f9394f4ea1effc5565058bd9a3632a8e39 100644 (file)
@@ -528,6 +528,7 @@ class AccessControlDB(object):
                 for user, _ in v1_db['users'].items():
                     v1_db['users'][user]['enabled'] = True
                     v1_db['users'][user]['pwdExpirationDate'] = None
+                    v1_db['users'][user]['pwdUpdateRequired'] = False
 
                 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))
index f69cdd57a4229f2c3da730f349390d7264d85d0f..684d7a84fd77e85a96be5faed3b7c9a81c19e88d 100644 (file)
@@ -290,6 +290,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'username': username,
             'password': pass_hash,
             'pwdExpirationDate': pwdExpirationDate,
+            'pwdUpdateRequired': False,
             'lastUpdate': user['lastUpdate'],
             'name': '{} User'.format(username),
             'email': '{}@user.com'.format(username),
@@ -504,6 +505,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'lastUpdate': user['lastUpdate'],
             'password': pass_hash,
             'pwdExpirationDate': None,
+            'pwdUpdateRequired': False,
             'name': 'admin User',
             'email': 'admin@user.com',
             'roles': ['block-manager', 'pool-manager'],
@@ -546,6 +548,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'username': 'admin',
             'password': pass_hash,
             'pwdExpirationDate': None,
+            'pwdUpdateRequired': False,
             'name': 'Admin Name',
             'email': 'admin@admin.com',
             'lastUpdate': user['lastUpdate'],
@@ -573,6 +576,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'username': 'admin',
             'password': pass_hash,
             'pwdExpirationDate': None,
+            'pwdUpdateRequired': False,
             'name': 'admin User',
             'email': 'admin@user.com',
             'lastUpdate': user['lastUpdate'],
@@ -601,6 +605,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'username': 'admin',
             'password': pass_hash,
             'pwdExpirationDate': None,
+            'pwdUpdateRequired': False,
             'name': 'admin User',
             'email': 'admin@user.com',
             'lastUpdate': user['lastUpdate'],
@@ -638,6 +643,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'username': 'admin',
             'password': pass_hash,
             'pwdExpirationDate': None,
+            'pwdUpdateRequired': False,
             'name': None,
             'email': None,
             'lastUpdate': user['lastUpdate'],
@@ -657,6 +663,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'username': 'admin',
             'password': pass_hash,
             'pwdExpirationDate': None,
+            'pwdUpdateRequired': False,
             'name': 'admin User',
             'email': 'admin@user.com',
             'lastUpdate': user['lastUpdate'],
@@ -712,6 +719,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'password':
                 "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
             'pwdExpirationDate': None,
+            'pwdUpdateRequired': False,
             'name': 'admin User',
             'email': 'admin@user.com',
             'roles': ['block-manager', 'test_role'],
@@ -727,6 +735,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
                         "password":
                 "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
                         "pwdExpirationDate": null,
+                        "pwdUpdateRequired": false,
                         "roles": ["block-manager", "test_role"],
                         "name": "admin User",
                         "email": "admin@user.com",
@@ -766,6 +775,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'password':
                 "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
             'pwdExpirationDate': None,
+            'pwdUpdateRequired': False,
             'name': 'admin User',
             'email': 'admin@user.com',
             'roles': ['block-manager', 'test_role'],
@@ -784,6 +794,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
             'password':
                 "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
             'pwdExpirationDate': None,
+            'pwdUpdateRequired': False,
             'name': None,
             'email': None,
             'roles': ['administrator'],