import jwt
-from .helper import DashboardTestCase
+from .helper import DashboardTestCase, JObj, JLeaf
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):
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()
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')
return {
'token': token,
'username': username,
- 'permissions': user_perms
+ 'permissions': user_perms,
+ 'sso': mgr.SSO_DB.protocol == 'saml2'
}
logger.debug('Login failed')
return {
'username': user.username,
'permissions': user.permissions_dict(),
+ 'sso': mgr.SSO_DB.protocol == 'saml2'
}
logger.debug("AMT: user info changed after token was"
import cherrypy
-from . import ApiController, RESTController
+from . import BaseController, ApiController, RESTController, Endpoint
from .. import mgr
from ..exceptions import DashboardException, UserAlreadyExists, \
UserDoesNotExist
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()
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';
},
loadChildren: './ceph/rgw/rgw.module#RoutedRgwModule'
},
- // Dashboard Settings
+ // User/Role Management
{
path: 'user-management',
canActivate: [AuthGuardService],
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',
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({
SsoNotFoundComponent,
UserTabsComponent,
UserListComponent,
- UserFormComponent
+ UserFormComponent,
+ UserPasswordFormComponent
]
})
export class AuthModule {}
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(['']);
}
});
--- /dev/null
+<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>
--- /dev/null
+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']);
+ });
+});
--- /dev/null
+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 });
+ }
+ );
+ }
+}
<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()">
styleUrls: ['./identity.component.scss']
})
export class IdentityComponent implements OnInit {
+ sso: boolean;
username: string;
icons = Icons;
ngOnInit() {
this.username = this.authStorageService.getUsername();
+ this.sso = this.authStorageService.isSSO();
}
logout() {
.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);
});
}
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');
+ });
});
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
+ });
+ }
}
/* Non-standard actions */
COPY = 'Copy',
CLONE = 'Clone',
+ UPDATE = 'Update',
/* Read-only */
SHOW = 'Show',
UNPROTECT: string;
RECREATE: string;
EXPIRE: string;
+ CHANGE: string;
constructor(private i18n: I18n) {
/* Create a new item */
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');
SHOWED: string;
TRASHED: string;
UNPROTECTED: string;
+ CHANGE: string;
RECREATED: string;
EXPIRED: string;
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');
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 {
username: string;
token: string;
permissions: object;
+ sso: boolean;
}
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);
+ });
});
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() {
localStorage.getItem('dashboard_permissions') || JSON.stringify(new Permissions({}))
);
}
+
+ isSSO() {
+ return localStorage.getItem('sso') === 'true';
+ }
}
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()
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)