]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Allow users to change their password on the UI
authorVolker Theile <vtheile@suse.com>
Tue, 9 Jul 2019 07:57:44 +0000 (09:57 +0200)
committerVolker Theile <vtheile@suse.com>
Wed, 17 Jul 2019 09:00:58 +0000 (11:00 +0200)
Fixes: https://tracker.ceph.com/issues/40248
Signed-off-by: Volker Theile <vtheile@suse.com>
22 files changed:
qa/tasks/mgr/dashboard/test_auth.py
qa/tasks/mgr/dashboard/test_user.py
src/pybind/mgr/dashboard/controllers/auth.py
src/pybind/mgr/dashboard/controllers/user.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/login.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts
src/pybind/mgr/dashboard/services/access_control.py

index 0de3f278120757eefc4a2ccf04de87ce0691e346..e58112dfe99e2df4f2b990ca1e8d9d6aaaee7f72 100644 (file)
@@ -6,7 +6,7 @@ import time
 
 import jwt
 
-from .helper import DashboardTestCase
+from .helper import DashboardTestCase, JObj, JLeaf
 
 
 class AuthTest(DashboardTestCase):
@@ -40,6 +40,12 @@ class AuthTest(DashboardTestCase):
         self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
         self.assertStatus(201)
         data = self.jsonBody()
+        self.assertSchema(data, JObj(sub_elems={
+            'token': JLeaf(str),
+            'username': JLeaf(str),
+            'permissions': JObj(sub_elems={}, allow_unknown=True),
+            'sso': JLeaf(bool)
+        }, allow_unknown=False))
         self._validate_jwt_token(data['token'], "admin", data['permissions'])
 
     def test_login_invalid(self):
@@ -136,3 +142,25 @@ class AuthTest(DashboardTestCase):
         self._get("/api/host")
         self.assertStatus(200)
         self.delete_user("user")
+
+    def test_check_token(self):
+        self.login("admin", "admin")
+        self._post("/api/auth/check", {"token": self.jsonBody()["token"]})
+        self.assertStatus(200)
+        data = self.jsonBody()
+        self.assertSchema(data, JObj(sub_elems={
+            "username": JLeaf(str),
+            "permissions": JObj(sub_elems={}, allow_unknown=True),
+            "sso": JLeaf(bool)
+        }, allow_unknown=False))
+        self.logout()
+
+    def test_check_wo_token(self):
+        self.login("admin", "admin")
+        self._post("/api/auth/check", {"token": ""})
+        self.assertStatus(200)
+        data = self.jsonBody()
+        self.assertSchema(data, JObj(sub_elems={
+            "login_url": JLeaf(str)
+        }, allow_unknown=False))
+        self.logout()
index 7af3442d422f0667863923c1df386a2eee88d5c7..34a41118211e1a1cd6a0b17b554ad9e91d331967 100644 (file)
@@ -113,3 +113,34 @@ class UserTest(DashboardTestCase):
         self.assertStatus(400)
         self.assertError(code='role_does_not_exist',
                          component='user')
+
+    def test_change_password_from_other_user(self):
+        self._post('/api/user/test2/change_password', {
+            'old_password': 'abc',
+            'new_password': 'xyz'
+        })
+        self.assertStatus(400)
+        self.assertError(code='invalid_user_context', component='user')
+
+    def test_change_password_old_not_match(self):
+        self._post('/api/user/admin/change_password', {
+            'old_password': 'foo',
+            'new_password': 'bar'
+        })
+        self.assertStatus(400)
+        self.assertError(code='invalid_old_password', component='user')
+
+    def test_change_password(self):
+        self.create_user('test1', 'test1', ['read-only'])
+        self.login('test1', 'test1')
+        self._post('/api/user/test1/change_password', {
+            'old_password': 'test1',
+            'new_password': 'foo'
+        })
+        self.assertStatus(200)
+        self.logout()
+        self._post('/api/auth', {'username': 'test1', 'password': 'test1'})
+        self.assertStatus(400)
+        self.assertError(code='invalid_credentials', component='auth')
+        self.delete_user('test1')
+        self.login('admin', 'admin')
index 7d145adcfedde2781d47314a8a9dc16e2f76390b..824b42b50dbc0b99f6df7c0d14849343517b3239 100644 (file)
@@ -28,7 +28,8 @@ class Auth(RESTController):
             return {
                 'token': token,
                 'username': username,
-                'permissions': user_perms
+                'permissions': user_perms,
+                'sso': mgr.SSO_DB.protocol == 'saml2'
             }
 
         logger.debug('Login failed')
@@ -64,6 +65,7 @@ class Auth(RESTController):
                         return {
                             'username': user.username,
                             'permissions': user.permissions_dict(),
+                            'sso': mgr.SSO_DB.protocol == 'saml2'
                         }
 
                     logger.debug("AMT: user info changed after token was"
index d99dead317075a392c5c53dc524c5100463236ad..587d9bd324bc407c3250e3098fce5fb41dfe888f 100644 (file)
@@ -3,7 +3,7 @@ from __future__ import absolute_import
 
 import cherrypy
 
-from . import ApiController, RESTController
+from . import BaseController, ApiController, RESTController, Endpoint
 from .. import mgr
 from ..exceptions import DashboardException, UserAlreadyExists, \
     UserDoesNotExist
@@ -89,3 +89,24 @@ class User(RESTController):
         user.set_roles(user_roles)
         mgr.ACCESS_CTRL_DB.save()
         return User._user_to_dict(user)
+
+
+@ApiController('/user/{username}')
+class UserChangePassword(BaseController):
+    @Endpoint('POST')
+    def change_password(self, username, old_password, new_password):
+        session_username = JwtManager.get_username()
+        if username != session_username:
+            raise DashboardException(msg='Invalid user context',
+                                     code='invalid_user_context',
+                                     component='user')
+        try:
+            user = mgr.ACCESS_CTRL_DB.get_user(session_username)
+        except UserDoesNotExist:
+            raise cherrypy.HTTPError(404)
+        if not user.compare_password(old_password):
+            raise DashboardException(msg='Invalid old password',
+                                     code='invalid_old_password',
+                                     component='user')
+        user.set_password(new_password)
+        mgr.ACCESS_CTRL_DB.save()
index 73dbc4088f50f11d144a6c982c3000a7486ccf30..e38235fcf294bc058cbcf8856ecc7ee0695ac767 100644 (file)
@@ -23,6 +23,7 @@ import { NfsListComponent } from './ceph/nfs/nfs-list/nfs-list.component';
 import { PerformanceCounterComponent } from './ceph/performance-counter/performance-counter/performance-counter.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 { NotFoundComponent } from './core/not-found/not-found.component';
 import { ActionLabels, URLVerbs } from './shared/constants/app.constants';
@@ -209,7 +210,7 @@ const routes: Routes = [
     },
     loadChildren: './ceph/rgw/rgw.module#RoutedRgwModule'
   },
-  // Dashboard Settings
+  // User/Role Management
   {
     path: 'user-management',
     canActivate: [AuthGuardService],
@@ -217,6 +218,20 @@ const routes: Routes = [
     data: { breadcrumbs: 'User management', path: null },
     loadChildren: './core/auth/auth.module#RoutedAuthModule'
   },
+  // User Profile
+  {
+    path: 'user-profile',
+    canActivate: [AuthGuardService],
+    canActivateChild: [AuthGuardService],
+    data: { breadcrumbs: 'User profile', path: null },
+    children: [
+      {
+        path: URLVerbs.EDIT,
+        component: UserPasswordFormComponent,
+        data: { breadcrumbs: ActionLabels.EDIT }
+      }
+    ]
+  },
   // NFS
   {
     path: 'nfs/501/:message',
index f9ecf551cbbd704e8403bd297e4bfdb9403bee64..b26a370b39bcfdffe976e9aa8af82c106e548208 100644 (file)
@@ -17,6 +17,7 @@ import { RoleListComponent } from './role-list/role-list.component';
 import { SsoNotFoundComponent } from './sso/sso-not-found/sso-not-found.component';
 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 { UserTabsComponent } from './user-tabs/user-tabs.component';
 
 @NgModule({
@@ -39,7 +40,8 @@ import { UserTabsComponent } from './user-tabs/user-tabs.component';
     SsoNotFoundComponent,
     UserTabsComponent,
     UserListComponent,
-    UserFormComponent
+    UserFormComponent,
+    UserPasswordFormComponent
   ]
 })
 export class AuthModule {}
index 108663be985fd358398525c8670b8398bb779de3..521ab305d2bc38493e53490efea6c706d2a6bf93 100644 (file)
@@ -48,7 +48,7 @@ export class LoginComponent implements OnInit {
             window.location.replace(login.login_url);
           }
         } else {
-          this.authStorageService.set(login.username, token, login.permissions);
+          this.authStorageService.set(login.username, token, login.permissions, login.sso);
           this.router.navigate(['']);
         }
       });
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
new file mode 100644 (file)
index 0000000..c0c1349
--- /dev/null
@@ -0,0 +1,110 @@
+<div class="col-sm-12 col-lg-6">
+  <form #frm="ngForm"
+        [formGroup]="userForm"
+        novalidate>
+    <div class="card">
+      <div i18n="form title|Example: Create Pool@@formTitle"
+           class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+
+      <div class="card-body">
+        <!-- Old password -->
+        <div class="form-group row">
+          <label class="col-form-label col-sm-3"
+                 for="oldpassword">
+            <ng-container i18n>Old password</ng-container>
+            <span class="required"></span>
+          </label>
+          <div class="col-sm-9">
+            <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-light"
+                        cdPasswordButton="oldpassword">
+                </button>
+              </span>
+            </div>
+            <span class="invalid-feedback"
+                  *ngIf="userForm.showError('oldpassword', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+
+        <!-- New password -->
+        <div class="form-group row">
+          <label class="col-form-label col-sm-3"
+                 for="newpassword">
+            <ng-container i18n>New password</ng-container>
+            <span class="required"></span>
+          </label>
+          <div class="col-sm-9">
+            <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 class="btn btn-light"
+                        cdPasswordButton="newpassword">
+                </button>
+              </span>
+            </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>
+          </div>
+        </div>
+
+        <!-- Confirm new password -->
+        <div class="form-group row">
+          <label class="col-form-label col-sm-3"
+                 for="confirmnewpassword">
+            <ng-container i18n>Confirm new password</ng-container>
+            <span class="required"></span>
+          </label>
+          <div class="col-sm-9">
+            <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-light"
+                        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>
+        </div>
+      </div>
+
+      <div class="card-footer">
+        <div class="button-group text-right">
+          <cd-submit-button (submitAction)="onSubmit()"
+                            [form]="userForm"
+                            i18n="form action button|Example: Create Pool@@formActionButton">
+            {{ action | titlecase }} {{ resource | upperFirst }}
+          </cd-submit-button>
+        </div>
+      </div>
+    </div>
+  </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.spec.ts
new file mode 100644 (file)
index 0000000..cfd8e0c
--- /dev/null
@@ -0,0 +1,85 @@
+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 { 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 { UserPasswordFormComponent } from './user-password-form.component';
+
+describe('UserPasswordFormComponent', () => {
+  let component: UserPasswordFormComponent;
+  let fixture: ComponentFixture<UserPasswordFormComponent>;
+  let form: CdFormGroup;
+  let formHelper: FormHelper;
+  let httpTesting: HttpTestingController;
+  let router: Router;
+  let authStorageService: AuthStorageService;
+
+  configureTestBed(
+    {
+      imports: [
+        HttpClientTestingModule,
+        RouterTestingModule,
+        ReactiveFormsModule,
+        ComponentsModule,
+        ToastrModule.forRoot(),
+        SharedModule
+      ],
+      declarations: [UserPasswordFormComponent],
+      providers: i18nProviders
+    },
+    true
+  );
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(UserPasswordFormComponent);
+    component = fixture.componentInstance;
+    form = component.userForm;
+    httpTesting = TestBed.get(HttpTestingController);
+    router = TestBed.get(Router);
+    authStorageService = TestBed.get(AuthStorageService);
+    spyOn(router, 'navigate');
+    fixture.detectChanges();
+    formHelper = new FormHelper(form);
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should validate old password required', () => {
+    formHelper.expectErrorChange('oldpassword', '', 'required');
+    formHelper.expectValidChange('oldpassword', 'foo');
+  });
+
+  it('should validate password match', () => {
+    formHelper.setValue('newpassword', 'aaa');
+    formHelper.expectErrorChange('confirmnewpassword', 'bbb', 'match');
+    formHelper.expectValidChange('confirmnewpassword', 'aaa');
+  });
+
+  it('should submit', () => {
+    spyOn(authStorageService, 'getUsername').and.returnValue('xyz');
+    formHelper.setMultipleValues({
+      oldpassword: 'foo',
+      newpassword: 'bar'
+    });
+    formHelper.setValue('confirmnewpassword', 'bar', true);
+    component.onSubmit();
+    const request = httpTesting.expectOne('api/user/xyz/change_password');
+    expect(request.request.method).toBe('POST');
+    expect(request.request.body).toEqual({
+      old_password: 'foo',
+      new_password: 'bar'
+    });
+    request.flush({});
+    expect(router.navigate).toHaveBeenCalledWith(['/logout']);
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.ts
new file mode 100644 (file)
index 0000000..e9abf89
--- /dev/null
@@ -0,0 +1,88 @@
+import { Component } from '@angular/core';
+import { Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+
+import { I18n } from '@ngx-translate/i18n-polyfill';
+
+import { UserService } from '../../../shared/api/user.service';
+import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
+import { NotificationType } from '../../../shared/enum/notification-type.enum';
+import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
+import { CdFormGroup } from '../../../shared/forms/cd-form-group';
+import { CdValidators } from '../../../shared/forms/cd-validators';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
+import { NotificationService } from '../../../shared/services/notification.service';
+
+@Component({
+  selector: 'cd-user-password-form',
+  templateUrl: './user-password-form.component.html',
+  styleUrls: ['./user-password-form.component.scss']
+})
+export class UserPasswordFormComponent {
+  userForm: CdFormGroup;
+  action: string;
+  resource: string;
+
+  constructor(
+    private i18n: I18n,
+    public actionLabels: ActionLabelsI18n,
+    private notificationService: NotificationService,
+    private userService: UserService,
+    private authStorageService: AuthStorageService,
+    private formBuilder: CdFormBuilder,
+    private router: Router
+  ) {
+    this.action = this.actionLabels.CHANGE;
+    this.resource = this.i18n('password');
+    this.createForm();
+  }
+
+  createForm() {
+    this.userForm = this.formBuilder.group(
+      {
+        oldpassword: [null, [Validators.required]],
+        newpassword: [
+          null,
+          [
+            Validators.required,
+            CdValidators.custom('notmatch', () => {
+              return (
+                this.userForm &&
+                this.userForm.getValue('oldpassword') === this.userForm.getValue('newpassword')
+              );
+            })
+          ]
+        ],
+        confirmnewpassword: [null, [Validators.required]]
+      },
+      {
+        validators: [CdValidators.match('newpassword', 'confirmnewpassword')]
+      }
+    );
+  }
+
+  onSubmit() {
+    if (this.userForm.pristine) {
+      return;
+    }
+    const username = this.authStorageService.getUsername();
+    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.userForm.setErrors({ cdSubmitButton: true });
+      }
+    );
+  }
+}
index 7b1ca8a33f30b9618a8986f8394884367a2a1e48..629a635e81151dbf5fc5f10db5570078b77b1621 100644 (file)
       <strong>{{ username }}</strong></a>
     </li>
     <li class="dropdown-divider"></li>
+    <li *ngIf="!sso"
+        role="menuitem">
+      <a class="dropdown-item"
+         routerLink="/user-profile/edit">
+        <i [ngClass]="[icons.lock]"></i>
+        <span i18n>Change password</span>
+      </a>
+    </li>
     <li role="menuitem">
       <a class="dropdown-item"
          (click)="logout()">
index 54d1a277f0a8c13fe4976c9c7a2b427e2f786100..6d4843d3d24acef748987ee059246e36a703b3a3 100644 (file)
@@ -10,6 +10,7 @@ import { AuthStorageService } from '../../../shared/services/auth-storage.servic
   styleUrls: ['./identity.component.scss']
 })
 export class IdentityComponent implements OnInit {
+  sso: boolean;
   username: string;
   icons = Icons;
 
@@ -17,6 +18,7 @@ export class IdentityComponent implements OnInit {
 
   ngOnInit() {
     this.username = this.authStorageService.getUsername();
+    this.sso = this.authStorageService.isSSO();
   }
 
   logout() {
index fc940081f2e99c2ed830cc2b42f99d1ea97b5113..18382f61d12a953e29f8ccf4ef3a6a52acc5de2d 100644 (file)
@@ -26,7 +26,7 @@ export class AuthService {
       .post('api/auth', credentials)
       .toPromise()
       .then((resp: LoginResponse) => {
-        this.authStorageService.set(resp.username, resp.token, resp.permissions);
+        this.authStorageService.set(resp.username, resp.token, resp.permissions, resp.sso);
       });
   }
 
index e7a15b8a00b03f9b69b1d6301f105649ec6d5c79..90a54cf4e1b4356ffda1d210091e18bacda4dbcf 100644 (file)
@@ -70,4 +70,14 @@ describe('UserService', () => {
     const req = httpTesting.expectOne('api/user');
     expect(req.request.method).toBe('GET');
   });
+
+  it('should call changePassword', () => {
+    service.changePassword('user0', 'foo', 'bar').subscribe();
+    const req = httpTesting.expectOne('api/user/user0/change_password');
+    expect(req.request.body).toEqual({
+      old_password: 'foo',
+      new_password: 'bar'
+    });
+    expect(req.request.method).toBe('POST');
+  });
 });
index 2643c7c35004b4cbf610378bfb2c11235ed25131..8e593875b452e34bdd40c113d752e7aa3ab3b973 100644 (file)
@@ -29,4 +29,14 @@ export class UserService {
   update(user: UserFormModel) {
     return this.http.put(`api/user/${user.username}`, user);
   }
+
+  changePassword(username, oldPassword, newPassword) {
+    // Note, the specified user MUST be logged in to be able to change
+    // the password. The backend ensures that the password of another
+    // user can not be changed, otherwise an error will be thrown.
+    return this.http.post(`api/user/${username}/change_password`, {
+      old_password: oldPassword,
+      new_password: newPassword
+    });
+  }
 }
index e3ca443d26144a7f7e3cbabb1183c3d043ec4de5..438f9fc5cb31047b5624287038008e7e46297dac 100644 (file)
@@ -58,6 +58,7 @@ export enum ActionLabels {
   /* Non-standard actions */
   COPY = 'Copy',
   CLONE = 'Clone',
+  UPDATE = 'Update',
 
   /* Read-only */
   SHOW = 'Show',
@@ -101,6 +102,7 @@ export class ActionLabelsI18n {
   UNPROTECT: string;
   RECREATE: string;
   EXPIRE: string;
+  CHANGE: string;
 
   constructor(private i18n: I18n) {
     /* Create a new item */
@@ -139,6 +141,7 @@ export class ActionLabelsI18n {
     this.SHOW = this.i18n('Show');
     this.TRASH = this.i18n('Move to Trash');
     this.UNPROTECT = this.i18n('Unprotect');
+    this.CHANGE = this.i18n('Change');
 
     /* Prometheus wording */
     this.RECREATE = this.i18n('Recreate');
@@ -178,6 +181,7 @@ export class SucceededActionLabelsI18n {
   SHOWED: string;
   TRASHED: string;
   UNPROTECTED: string;
+  CHANGE: string;
   RECREATED: string;
   EXPIRED: string;
 
@@ -218,6 +222,7 @@ export class SucceededActionLabelsI18n {
     this.SHOWED = this.i18n('Showed');
     this.TRASHED = this.i18n('Moved to Trash');
     this.UNPROTECTED = this.i18n('Unprotected');
+    this.CHANGE = this.i18n('Change');
 
     /* Prometheus wording */
     this.RECREATED = this.i18n('Recreated');
index 50fd21c220c5fd64ce42c7937e5c4fef2df09cdb..fd4c01e31c244bb5954cb810fc20c5f61384d7f5 100644 (file)
@@ -243,6 +243,9 @@ export class CdValidators {
     return (control: AbstractControl): { [key: string]: any } => {
       const ctrl1 = control.get(path1);
       const ctrl2 = control.get(path2);
+      if (!ctrl1 || !ctrl2) {
+        return null;
+      }
       if (ctrl1.value !== ctrl2.value) {
         ctrl2.setErrors({ match: true });
       } else {
index 4e8c5d17f88fcea1c157009c32fd54df0ec92eea..0df52d567522f055f4ef18c67011c2f636c0755f 100644 (file)
@@ -2,4 +2,5 @@ export class LoginResponse {
   username: string;
   token: string;
   permissions: object;
+  sso: boolean;
 }
index e9726a47e10cb2e2e833fc7532bbdd5bf30528e9..67c093de6ec6e9fbb7533f122a43221bb9c06f48 100644 (file)
@@ -32,4 +32,16 @@ describe('AuthStorageService', () => {
     service.remove();
     expect(service.isLoggedIn()).toBe(false);
   });
+
+  it('should be SSO', () => {
+    service.set(username, '', {}, true);
+    expect(localStorage.getItem('sso')).toBe('true');
+    expect(service.isSSO()).toBe(true);
+  });
+
+  it('should not be SSO', () => {
+    service.set(username, '');
+    expect(localStorage.getItem('sso')).toBe('false');
+    expect(service.isSSO()).toBe(false);
+  });
 });
index cab5bc813ca36109eef46f20452da89b8e3d6603..3c618050540e8080d45350488ce5c7d90703f94b 100644 (file)
@@ -8,10 +8,11 @@ import { Permissions } from '../models/permissions';
 export class AuthStorageService {
   constructor() {}
 
-  set(username: string, token: string, permissions: object = {}) {
+  set(username: string, token: string, permissions: object = {}, sso = false) {
     localStorage.setItem('dashboard_username', username);
     localStorage.setItem('access_token', token);
     localStorage.setItem('dashboard_permissions', JSON.stringify(new Permissions(permissions)));
+    localStorage.setItem('sso', String(sso));
   }
 
   remove() {
@@ -36,4 +37,8 @@ export class AuthStorageService {
       localStorage.getItem('dashboard_permissions') || JSON.stringify(new Permissions({}))
     );
   }
+
+  isSSO() {
+    return localStorage.getItem('sso') === 'true';
+  }
 }
index 4d1669b6afb0b65dd6db064a79b6b8c906fbd3e8..2fb873479c1119c8af2d6300b93c80bad6d1d9ee 100644 (file)
@@ -189,6 +189,17 @@ class User(object):
         self.password = password_hash(password)
         self.refreshLastUpdate()
 
+    def compare_password(self, password):
+        """
+        Compare the specified password with the user password.
+        :param password: The plain password to check.
+        :type password: str
+        :return: `True` if the passwords are equal, otherwise `False`.
+        :rtype: bool
+        """
+        pass_hash = password_hash(password, salt_password=self.password)
+        return pass_hash == self.password
+
     def set_roles(self, roles):
         self.roles = set(roles)
         self.refreshLastUpdate()
@@ -650,8 +661,7 @@ class LocalAuthenticator(object):
         try:
             user = mgr.ACCESS_CTRL_DB.get_user(username)
             if user.password:
-                pass_hash = password_hash(password, user.password)
-                if pass_hash == user.password:
+                if user.compare_password(password):
                     return user.permissions_dict()
         except UserDoesNotExist:
             logger.debug("User '%s' does not exist", username)