From: Volker Theile Date: Tue, 9 Jul 2019 07:57:44 +0000 (+0200) Subject: mgr/dashboard: Allow users to change their password on the UI X-Git-Tag: v15.1.0~2176^2~1 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=ee80139fa095821d5c26c8f41021e78a2e994aa2;p=ceph.git mgr/dashboard: Allow users to change their password on the UI Fixes: https://tracker.ceph.com/issues/40248 Signed-off-by: Volker Theile --- diff --git a/qa/tasks/mgr/dashboard/test_auth.py b/qa/tasks/mgr/dashboard/test_auth.py index 0de3f2781207..e58112dfe99e 100644 --- a/qa/tasks/mgr/dashboard/test_auth.py +++ b/qa/tasks/mgr/dashboard/test_auth.py @@ -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() diff --git a/qa/tasks/mgr/dashboard/test_user.py b/qa/tasks/mgr/dashboard/test_user.py index 7af3442d422f..34a41118211e 100644 --- a/qa/tasks/mgr/dashboard/test_user.py +++ b/qa/tasks/mgr/dashboard/test_user.py @@ -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') diff --git a/src/pybind/mgr/dashboard/controllers/auth.py b/src/pybind/mgr/dashboard/controllers/auth.py index 7d145adcfedd..824b42b50dbc 100644 --- a/src/pybind/mgr/dashboard/controllers/auth.py +++ b/src/pybind/mgr/dashboard/controllers/auth.py @@ -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" diff --git a/src/pybind/mgr/dashboard/controllers/user.py b/src/pybind/mgr/dashboard/controllers/user.py index d99dead31707..587d9bd324bc 100644 --- a/src/pybind/mgr/dashboard/controllers/user.py +++ b/src/pybind/mgr/dashboard/controllers/user.py @@ -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() diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 73dbc4088f50..e38235fcf294 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -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', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts index f9ecf551cbbd..b26a370b39bc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts @@ -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 {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts index 108663be985f..521ab305d2bc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts @@ -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 index 000000000000..c0c134969c46 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.html @@ -0,0 +1,110 @@ +
+
+
+
{{ action | titlecase }} {{ resource | upperFirst }}
+ +
+ +
+ +
+
+ + + + +
+ This field is required. +
+
+ + +
+ +
+
+ + + + +
+ This field is required. + The old and new passwords must be different. +
+
+ + +
+ +
+
+ + + + +
+ This field is required. + Password confirmation doesn't match the new password. +
+
+
+ + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-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 index 000000000000..e69de29bb2d1 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 index 000000000000..cfd8e0c7120d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.spec.ts @@ -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; + 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 index 000000000000..e9abf89baf13 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-password-form/user-password-form.component.ts @@ -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 }); + } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.html index 7b1ca8a33f30..629a635e8115 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.html @@ -19,6 +19,14 @@ {{ username }} +
  • + + + Change password + +
  • diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.ts index 54d1a277f0a8..6d4843d3d24a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/identity/identity.component.ts @@ -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() { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts index fc940081f2e9..18382f61d12a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts @@ -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); }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.spec.ts index e7a15b8a00b0..90a54cf4e1b4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.spec.ts @@ -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'); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.ts index 2643c7c35004..8e593875b452 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.ts @@ -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 + }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts index e3ca443d2614..438f9fc5cb31 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts @@ -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'); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts index 50fd21c220c5..fd4c01e31c24 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts @@ -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 { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts index 4e8c5d17f88f..0df52d567522 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts @@ -2,4 +2,5 @@ export class LoginResponse { username: string; token: string; permissions: object; + sso: boolean; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts index e9726a47e10c..67c093de6ec6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts @@ -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); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts index cab5bc813ca3..3c618050540e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts @@ -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'; + } } diff --git a/src/pybind/mgr/dashboard/services/access_control.py b/src/pybind/mgr/dashboard/services/access_control.py index 4d1669b6afb0..2fb873479c11 100644 --- a/src/pybind/mgr/dashboard/services/access_control.py +++ b/src/pybind/mgr/dashboard/services/access_control.py @@ -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)