import { RgwUserFormComponent } from './ceph/rgw/rgw-user-form/rgw-user-form.component';
import { RgwUserListComponent } from './ceph/rgw/rgw-user-list/rgw-user-list.component';
import { LoginComponent } from './core/auth/login/login.component';
+import { UserFormComponent } from './core/auth/user-form/user-form.component';
+import { UserListComponent } from './core/auth/user-list/user-list.component';
import { ForbiddenComponent } from './core/forbidden/forbidden.component';
import { NotFoundComponent } from './core/not-found/not-found.component';
import { AuthGuardService } from './shared/services/auth-guard.service';
{ path: 'cephfs', component: CephfsListComponent, canActivate: [AuthGuardService] },
{ path: 'configuration', component: ConfigurationComponent, canActivate: [AuthGuardService] },
{ path: 'mirroring', component: MirroringComponent, canActivate: [AuthGuardService] },
+ { path: 'users', component: UserListComponent, canActivate: [AuthGuardService] },
+ { path: 'users/add', component: UserFormComponent, canActivate: [AuthGuardService] },
+ { path: 'users/edit/:username', component: UserFormComponent, canActivate: [AuthGuardService] },
{ path: '403', component: ForbiddenComponent },
{ path: '404', component: NotFoundComponent },
{ path: 'osd', component: OsdListComponent, canActivate: [AuthGuardService] },
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-import { SharedModule } from '../../shared/shared.module';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+import { BsDropdownModule, PopoverModule, TabsModule } from 'ngx-bootstrap';
+import { SharedModule } from '../../shared/shared.module';
import { LoginComponent } from './login/login.component';
import { LogoutComponent } from './logout/logout.component';
+import { UserFormComponent } from './user-form/user-form.component';
+import { UserListComponent } from './user-list/user-list.component';
@NgModule({
imports: [
+ BsDropdownModule.forRoot(),
CommonModule,
FormsModule,
- SharedModule
+ PopoverModule.forRoot(),
+ ReactiveFormsModule,
+ SharedModule,
+ TabsModule.forRoot(),
+ RouterModule
],
- declarations: [LoginComponent, LogoutComponent],
+ declarations: [LoginComponent, LogoutComponent, UserListComponent, UserFormComponent],
exports: [LogoutComponent]
})
export class AuthModule { }
styleUrls: ['./logout.component.scss']
})
export class LogoutComponent implements OnInit {
+ constructor(private authService: AuthService, private router: Router) {}
- constructor(private authService: AuthService,
- private router: Router) { }
-
- ngOnInit() {
- }
+ ngOnInit() {}
logout() {
- this.authService.logout().then(() => {
+ this.authService.logout(() => {
this.router.navigate(['/login']);
});
}
--- /dev/null
+export enum UserFormMode {
+ editing = 'editing'
+}
--- /dev/null
+export class UserFormRoleModel implements SelectBadgesOption {
+ name: string;
+ description: string;
+ selected = false;
+ scopes_permissions: object;
+
+ constructor(name, description) {
+ this.name = name;
+ this.description = description;
+ }
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item">Administration</li>
+ <li class="breadcrumb-item">
+ <a routerLink="/users">Users</a></li>
+ <li class="breadcrumb-item active"
+ i18n>{mode, select, editing {Edit} other {Add}}</li>
+ </ol>
+</nav>
+
+<div class="col-sm-12 col-lg-6">
+ <form name="userForm"
+ class="form-horizontal"
+ #formDir="ngForm"
+ [formGroup]="userForm"
+ novalidate>
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">
+ <span i18n>{mode, select, editing {Edit} other {Add}}</span> User
+ </h3>
+ </div>
+ <div class="panel-body">
+
+ <!-- Username -->
+ <div class="form-group"
+ [ngClass]="{'has-error': userForm.showError('username', formDir)}">
+ <label i18n
+ class="control-label col-sm-3"
+ for="name">Username
+ <span class="required"
+ *ngIf="mode !== userFormMode.editing"></span>
+ </label>
+ <div class="col-sm-9">
+ <input class="form-control"
+ type="text"
+ placeholder="Username..."
+ id="username"
+ name="username"
+ formControlName="username"
+ autofocus>
+ <span i18n
+ class="help-block"
+ *ngIf="userForm.showError('username', formDir, 'required')">
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ <!-- Password -->
+ <div class="form-group"
+ [ngClass]="{'has-error': userForm.showError('password', formDir)}">
+ <label i18n
+ class="control-label col-sm-3"
+ for="name">Password
+ <span class="required"
+ *ngIf="mode !== userFormMode.editing"></span>
+ </label>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ placeholder="Password..."
+ id="password"
+ name="password"
+ formControlName="password">
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default"
+ cdPasswordButton="password">
+ </button>
+ </span>
+ </div>
+ <span i18n
+ class="help-block"
+ *ngIf="userForm.showError('password', formDir, 'required')">
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ <!-- Confirm password -->
+ <div class="form-group"
+ [ngClass]="{'has-error': userForm.showError('confirmpassword', formDir)}">
+ <label i18n
+ class="control-label col-sm-3"
+ for="name">Confirm password
+ <span class="required"
+ *ngIf="mode !== userFormMode.editing"></span>
+ </label>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ placeholder="Confirm password..."
+ id="confirmpassword"
+ name="confirmpassword"
+ formControlName="confirmpassword">
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default"
+ cdPasswordButton="confirmpassword">
+ </button>
+ </span>
+ </div>
+ <span i18n
+ class="help-block"
+ *ngIf="userForm.showError('confirmpassword', formDir, 'required')">
+ This field is required.
+ </span>
+ <span i18n
+ class="help-block"
+ *ngIf="userForm.showError('confirmpassword', formDir, 'match')">
+ Password confirmation doesn't match the password.
+ </span>
+ </div>
+ </div>
+
+ <!-- Name -->
+ <div class="form-group">
+ <label i18n
+ class="control-label col-sm-3"
+ for="name">Full name
+ </label>
+ <div class="col-sm-9">
+ <input class="form-control"
+ type="text"
+ placeholder="Full name..."
+ id="name"
+ name="name"
+ formControlName="name">
+ </div>
+ </div>
+
+ <!-- Email -->
+ <div class="form-group"
+ [ngClass]="{'has-error': userForm.showError('email', formDir)}">
+ <label i18n
+ class="control-label col-sm-3"
+ for="email">Email
+ </label>
+ <div class="col-sm-9">
+ <input class="form-control"
+ type="email"
+ placeholder="Email..."
+ id="email"
+ name="email"
+ formControlName="email">
+
+ <span i18n
+ class="help-block"
+ *ngIf="userForm.showError('email', formDir, 'email')">
+ Invalid email.
+ </span>
+ </div>
+ </div>
+
+ <!-- Roles -->
+ <label class="col-sm-3 control-label"
+ i18n>Roles
+ </label>
+ <div class="col-sm-9">
+ <span class="form-control no-border full-height">
+ <cd-select-badges
+ [data]="userForm.controls.roles.value"
+ [options]="allRoles"
+ emptyMessage="There are no roles."></cd-select-badges>
+ </span>
+ </div>
+
+ </div>
+ <div class="panel-footer">
+ <div class="button-group text-right">
+ <cd-submit-button [form]="formDir"
+ type="button"
+ (submitAction)="submit()">
+ <span i18n>{mode, select, editing {Update} other {Create}}</span> User
+ </cd-submit-button>
+ <button i18n
+ type="button"
+ class="btn btn-sm btn-default"
+ routerLink="/users">
+ Back
+ </button>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
+
+<ng-template #removeSelfUserReadUpdatePermissionTpl>
+ <p><strong>You are about to remove "user read / update" permissions from your own user.</strong></p>
+ <br>
+ <p>If you continue, you will no longer be able to add or remove roles from any user.</p>
+
+ Are you sure you want to continue?
+</ng-template>
--- /dev/null
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastModule } from 'ng2-toastr';
+import { BsModalService } from 'ngx-bootstrap';
+import { of } from 'rxjs';
+
+import { configureTestBed } from '../../../../testing/unit-test-helper';
+import { RoleService } from '../../../shared/api/role.service';
+import { UserService } from '../../../shared/api/user.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 { NotificationService } from '../../../shared/services/notification.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { UserFormComponent } from './user-form.component';
+import { UserFormModel } from './user-form.model';
+
+describe('UserFormComponent', () => {
+ let component: UserFormComponent;
+ let form: CdFormGroup;
+ let fixture: ComponentFixture<UserFormComponent>;
+ let httpTesting: HttpTestingController;
+ let userService: UserService;
+ let modalService: BsModalService;
+ let router: Router;
+ const setUrl = (url) => Object.defineProperty(router, 'url', { value: url });
+
+ @Component({ selector: 'cd-fake', template: '' })
+ class FakeComponent {}
+
+ const routes: Routes = [
+ { path: 'login', component: FakeComponent },
+ { path: 'users', component: FakeComponent }
+ ];
+
+ configureTestBed({
+ imports: [
+ [RouterTestingModule.withRoutes(routes)],
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ ComponentsModule,
+ ToastModule.forRoot(),
+ SharedModule
+ ],
+ declarations: [UserFormComponent, FakeComponent]
+ }, true);
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserFormComponent);
+ component = fixture.componentInstance;
+ form = component.userForm;
+ httpTesting = TestBed.get(HttpTestingController);
+ userService = TestBed.get(UserService);
+ modalService = TestBed.get(BsModalService);
+ router = TestBed.get(Router);
+ spyOn(router, 'navigate');
+ fixture.detectChanges();
+ const notify = TestBed.get(NotificationService);
+ spyOn(notify, 'show');
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(form).toBeTruthy();
+ });
+
+ describe('create mode', () => {
+ beforeEach(() => {
+ setUrl('/users/add');
+ component.ngOnInit();
+ });
+
+ it('should not disable fields', () => {
+ ['username', 'name', 'password', 'confirmpassword', 'email', 'roles'].forEach((key) =>
+ expect(form.get(key).disabled).toBeFalsy()
+ );
+ });
+
+ it('should validate username required', () => {
+ form.get('username').setValue('');
+ expect(form.get('username').hasError('required')).toBeTruthy();
+ });
+
+ it('should validate password required', () => {
+ ['password', 'confirmpassword'].forEach((key) =>
+ expect(form.get(key).hasError('required')).toBeTruthy()
+ );
+ });
+
+ it('should validate password match', () => {
+ form.get('password').setValue('aaa');
+ form.get('confirmpassword').setValue('bbb');
+ expect(form.get('confirmpassword').hasError('match')).toBeTruthy();
+ form.get('confirmpassword').setValue('aaa');
+ expect(form.get('confirmpassword').valid).toBeTruthy();
+ });
+
+ it('should validate email', () => {
+ form.get('email').setValue('aaa');
+ expect(form.get('email').hasError('email')).toBeTruthy();
+ });
+
+ it('should set mode', () => {
+ expect(component.mode).toBeUndefined();
+ });
+
+ it('should submit', () => {
+ const user: UserFormModel = {
+ username: 'user0',
+ password: 'pass0',
+ name: 'User 0',
+ email: 'user0@email.com',
+ roles: ['administrator']
+ };
+ Object.keys(user).forEach((k) => form.get(k).setValue(user[k]));
+ form.get('confirmpassword').setValue(user.password);
+ component.submit();
+ const userReq = httpTesting.expectOne('api/user');
+ expect(userReq.request.method).toBe('POST');
+ expect(userReq.request.body).toEqual(user);
+ userReq.flush({});
+ expect(router.navigate).toHaveBeenCalledWith(['/users']);
+ });
+ });
+
+ describe('edit mode', () => {
+ const user: UserFormModel = {
+ username: 'user1',
+ password: undefined,
+ name: 'User 1',
+ email: 'user1@email.com',
+ roles: ['administrator']
+ };
+ const roles = [
+ {
+ name: 'administrator',
+ description: 'Administrator',
+ scopes_permissions: {
+ user: ['create', 'delete', 'read', 'update']
+ }
+ },
+ {
+ name: 'read-only',
+ description: 'Read-Only',
+ scopes_permissions: {
+ user: ['read']
+ }
+ },
+ {
+ name: 'user-manager',
+ description: 'User Manager',
+ scopes_permissions: {
+ user: ['create', 'delete', 'read', 'update']
+ }
+ }
+ ];
+
+ beforeEach(() => {
+ spyOn(userService, 'get').and.callFake(() => of(user));
+ spyOn(TestBed.get(RoleService), 'list').and.callFake(() => of(roles));
+ setUrl('/users/edit/user1');
+ component.ngOnInit();
+ const req = httpTesting.expectOne('api/role');
+ expect(req.request.method).toBe('GET');
+ req.flush(roles);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should disable fields if editing', () => {
+ expect(form.get('username').disabled).toBeTruthy();
+ ['name', 'password', 'confirmpassword', 'email', 'roles'].forEach((key) =>
+ expect(form.get(key).disabled).toBeFalsy()
+ );
+ });
+
+ it('should set control values', () => {
+ ['username', 'name', 'email', 'roles'].forEach((key) =>
+ expect(form.getValue(key)).toBe(user[key])
+ );
+ ['password', 'confirmpassword'].forEach((key) => expect(form.getValue(key)).toBeFalsy());
+ });
+
+ it('should set mode', () => {
+ expect(component.mode).toBe('editing');
+ });
+
+ it('should validate password not required', () => {
+ ['password', 'confirmpassword'].forEach((key) => {
+ form.get(key).setValue('');
+ expect(form.get(key).hasError('required')).toBeFalsy();
+ });
+ });
+
+ it('should alert if user is removing needed role permission', () => {
+ spyOn(TestBed.get(AuthStorageService), 'getUsername').and.callFake(() => user.username);
+ let modalBodyTpl = null;
+ spyOn(modalService, 'show').and.callFake((content, config) => {
+ modalBodyTpl = config.initialState.bodyTpl;
+ });
+ form.get('roles').setValue(['read-only']);
+ component.submit();
+ expect(modalBodyTpl).toEqual(component.removeSelfUserReadUpdatePermissionTpl);
+ });
+
+ it('should logout if current user roles have been changed', () => {
+ spyOn(TestBed.get(AuthStorageService), 'getUsername').and.callFake(() => user.username);
+ form.get('roles').setValue(['user-manager']);
+ component.submit();
+ const userReq = httpTesting.expectOne(`api/user/${user.username}`);
+ expect(userReq.request.method).toBe('PUT');
+ userReq.flush({});
+ const authReq = httpTesting.expectOne('api/auth');
+ expect(authReq.request.method).toBe('DELETE');
+ authReq.flush(null);
+ expect(router.navigate).toHaveBeenCalledWith(['/login']);
+ });
+
+ it('should submit', () => {
+ spyOn(TestBed.get(AuthStorageService), 'getUsername').and.callFake(() => user.username);
+ component.submit();
+ const userReq = httpTesting.expectOne(`api/user/${user.username}`);
+ expect(userReq.request.method).toBe('PUT');
+ expect(userReq.request.body).toEqual({
+ username: 'user1',
+ password: '',
+ name: 'User 1',
+ email: 'user1@email.com',
+ roles: ['administrator']
+ });
+ userReq.flush({});
+ expect(router.navigate).toHaveBeenCalledWith(['/users']);
+ });
+ });
+});
--- /dev/null
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import * as _ from 'lodash';
+import { BsModalRef, BsModalService } from 'ngx-bootstrap';
+
+import { AuthService } from '../../../shared/api/auth.service';
+import { RoleService } from '../../../shared/api/role.service';
+import { UserService } from '../../../shared/api/user.service';
+import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
+import { NotificationType } from '../../../shared/enum/notification-type.enum';
+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';
+import { UserFormMode } from './user-form-mode.enum';
+import { UserFormRoleModel } from './user-form-role.model';
+import { UserFormModel } from './user-form.model';
+
+@Component({
+ selector: 'cd-user-form',
+ templateUrl: './user-form.component.html',
+ styleUrls: ['./user-form.component.scss']
+})
+export class UserFormComponent implements OnInit {
+ @ViewChild('removeSelfUserReadUpdatePermissionTpl')
+ removeSelfUserReadUpdatePermissionTpl: TemplateRef<any>;
+
+ modalRef: BsModalRef;
+
+ userForm: CdFormGroup;
+ response: UserFormModel;
+
+ userFormMode = UserFormMode;
+ mode: UserFormMode;
+ allRoles: Array<UserFormRoleModel>;
+
+ constructor(
+ private authService: AuthService,
+ private authStorageService: AuthStorageService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private modalService: BsModalService,
+ private roleService: RoleService,
+ private userService: UserService,
+ private notificationService: NotificationService
+ ) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.userForm = new CdFormGroup(
+ {
+ username: new FormControl('', {
+ validators: [Validators.required]
+ }),
+ name: new FormControl(''),
+ password: new FormControl('', {
+ validators: []
+ }),
+ confirmpassword: new FormControl('', {
+ updateOn: 'blur',
+ validators: []
+ }),
+ email: new FormControl('', {
+ validators: [Validators.email]
+ }),
+ roles: new FormControl([])
+ },
+ {
+ validators: [CdValidators.match('password', 'confirmpassword')]
+ }
+ );
+ }
+
+ ngOnInit() {
+ if (this.router.url.startsWith('/users/edit')) {
+ this.mode = this.userFormMode.editing;
+ }
+ this.roleService.list().subscribe((roles: Array<UserFormRoleModel>) => {
+ this.allRoles = roles;
+ });
+ if (this.mode === this.userFormMode.editing) {
+ this.initEdit();
+ } else {
+ this.initAdd();
+ }
+ }
+
+ initAdd() {
+ ['password', 'confirmpassword'].forEach((controlName) =>
+ this.userForm.get(controlName).setValidators([Validators.required])
+ );
+ }
+
+ initEdit() {
+ ['password', 'confirmpassword'].forEach((controlName) =>
+ this.userForm.get(controlName).setValidators([])
+ );
+ this.disableForEdit();
+ this.route.params.subscribe((params: { username: string }) => {
+ const username = params.username;
+ this.userService.get(username).subscribe((userFormModel: UserFormModel) => {
+ this.response = _.cloneDeep(userFormModel);
+ this.setResponse(userFormModel);
+ });
+ });
+ }
+
+ disableForEdit() {
+ this.userForm.get('username').disable();
+ }
+
+ setResponse(response: UserFormModel) {
+ ['username', 'name', 'email', 'roles'].forEach((key) =>
+ this.userForm.get(key).setValue(response[key])
+ );
+ }
+
+ getRequest(): UserFormModel {
+ const userFormModel = new UserFormModel();
+ ['username', 'password', 'name', 'email', 'roles'].forEach(
+ (key) => (userFormModel[key] = this.userForm.get(key).value)
+ );
+ return userFormModel;
+ }
+
+ createAction() {
+ const userFormModel = this.getRequest();
+ this.userService.create(userFormModel).subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ `User "${userFormModel.username}" has been created.`,
+ 'Create User'
+ );
+ this.router.navigate(['/users']);
+ },
+ () => {
+ this.userForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+
+ editAction() {
+ if (this.isUserRemovingNeededRolePermissions()) {
+ const initialState = {
+ titleText: 'Update user',
+ buttonText: 'Continue',
+ bodyTpl: this.removeSelfUserReadUpdatePermissionTpl,
+ onSubmit: () => {
+ this.modalRef.hide();
+ this.doEditAction();
+ },
+ onCancel: () => {
+ this.userForm.setErrors({ cdSubmitButton: true });
+ this.userForm.get('roles').reset(this.userForm.get('roles').value);
+ }
+ };
+ this.modalRef = this.modalService.show(ConfirmationModalComponent, { initialState });
+ } else {
+ this.doEditAction();
+ }
+ }
+
+ private isCurrentUser(): boolean {
+ return this.authStorageService.getUsername() === this.userForm.getValue('username');
+ }
+
+ private isUserChangingRoles(): boolean {
+ const isCurrentUser = this.isCurrentUser();
+ return (
+ isCurrentUser &&
+ this.response &&
+ !_.isEqual(this.response.roles, this.userForm.getValue('roles'))
+ );
+ }
+
+ private isUserRemovingNeededRolePermissions(): boolean {
+ const isCurrentUser = this.isCurrentUser();
+ return isCurrentUser && !this.hasUserReadUpdatePermissions(this.userForm.getValue('roles'));
+ }
+
+ private hasUserReadUpdatePermissions(roles: Array<string> = []) {
+ for (const role of this.allRoles) {
+ if (roles.indexOf(role.name) !== -1 && role.scopes_permissions['user']) {
+ const userPermissions = role.scopes_permissions['user'];
+ return ['read', 'update'].every((permission) => {
+ return userPermissions.indexOf(permission) !== -1;
+ });
+ }
+ }
+ return false;
+ }
+
+ private doEditAction() {
+ const userFormModel = this.getRequest();
+ this.userService.update(userFormModel).subscribe(
+ () => {
+ if (this.isUserChangingRoles()) {
+ this.authService.logout(() => {
+ this.notificationService.show(
+ NotificationType.info,
+ 'You were automatically logged out because your roles have been changed.'
+ );
+ this.router.navigate(['/login']);
+ });
+ } else {
+ this.notificationService.show(
+ NotificationType.success,
+ `User "${userFormModel.username}" has been updated.`,
+ 'Edit User'
+ );
+ this.router.navigate(['/users']);
+ }
+ },
+ () => {
+ this.userForm.setErrors({ cdSubmitButton: true });
+ }
+ );
+ }
+
+ submit() {
+ if (this.mode === this.userFormMode.editing) {
+ this.editAction();
+ } else {
+ this.createAction();
+ }
+ }
+}
--- /dev/null
+export class UserFormModel {
+ username: string;
+ password: string;
+ name: string;
+ email: string;
+ roles: Array<string>;
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li i18n
+ class="breadcrumb-item">Administration</li>
+ <li i18n
+ class="breadcrumb-item active"
+ aria-current="page">Users</li>
+ </ol>
+</nav>
+
+<cd-table [data]="users"
+ columnMode="flex"
+ [columns]="columns"
+ identifier="username"
+ selectionType="single"
+ (fetchData)="getUsers()"
+ (updateSelection)="updateSelection($event)">
+ <div class="table-actions">
+ <div class="btn-group" dropdown>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ *ngIf="permission.create && (
+ permission.update && !selection.hasSingleSelection ||
+ !permission.update)"
+ routerLink="/users/add">
+ <i class="fa fa-fw fa-plus"></i>
+ <span i18n>Add</span>
+ </button>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ [ngClass]="{'disabled': !selection.hasSelection}"
+ *ngIf="permission.update && (!permission.create || selection.hasSingleSelection)"
+ routerLink="/users/edit/{{ selection.first()?.username }}">
+ <i class="fa fa-fw fa-pencil"></i>
+ <span i18n>Edit</span>
+ </button>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ [ngClass]="{'disabled': !selection.hasSelection}"
+ *ngIf="permission.delete && !permission.create && !permission.update"
+ (click)="deleteUserModal()">
+ <i class="fa fa-fw fa-trash-o"></i>
+ <span i18n>Delete</span>
+ </button>
+ <button type="button"
+ dropdownToggle
+ class="btn btn-sm btn-primary dropdown-toggle dropdown-toggle-split"
+ *ngIf="((permission.create?1:0) + (permission.update?1:0) + (permission.delete?1:0)) > 1">
+ <span class="caret"></span>
+ <span class="sr-only"></span>
+ </button>
+ <ul *dropdownMenu class="dropdown-menu" role="menu">
+ <li role="menuitem"
+ *ngIf="permission.create">
+ <a class="dropdown-item" routerLink="/users/add">
+ <i class="fa fa-fw fa-plus"></i>
+ <span i18n>Add</span>
+ </a>
+ </li>
+ <li role="menuitem"
+ *ngIf="permission.update"
+ [ngClass]="{'disabled': !selection.hasSingleSelection}">
+ <a class="dropdown-item" routerLink="/users/edit/{{ selection.first()?.username}}">
+ <i class="fa fa-fw fa-pencil"></i>
+ <span i18n>Edit</span>
+ </a>
+ </li>
+ <li role="menuitem"
+ *ngIf="permission.delete"
+ [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
+ <a class="dropdown-item" (click)="deleteUserModal()">
+ <i class="fa fa-fw fa-trash-o"></i>
+ <span i18n>Delete</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+</cd-table>
+
+<ng-template #userRolesTpl
+ let-value="value">
+ <span *ngFor="let role of value; last as isLast">
+ {{ role }}{{ !isLast ? ", " : "" }}
+ </span>
+</ng-template>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastModule } from 'ng2-toastr';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { UserListComponent } from './user-list.component';
+
+describe('UserListComponent', () => {
+ let component: UserListComponent;
+ let fixture: ComponentFixture<UserListComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [SharedModule, ToastModule.forRoot(), RouterTestingModule, HttpClientTestingModule],
+ declarations: [UserListComponent]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { BsModalRef, BsModalService } from 'ngx-bootstrap';
+
+import { UserService } from '../../../shared/api/user.service';
+import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component';
+import { EmptyPipe } from '../../../shared/empty.pipe';
+import { NotificationType } from '../../../shared/enum/notification-type.enum';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { Permission } from '../../../shared/models/permissions';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
+import { NotificationService } from '../../../shared/services/notification.service';
+
+@Component({
+ selector: 'cd-user-list',
+ templateUrl: './user-list.component.html',
+ styleUrls: ['./user-list.component.scss']
+})
+export class UserListComponent implements OnInit {
+ @ViewChild('userRolesTpl') userRolesTpl: TemplateRef<any>;
+
+ permission: Permission;
+ columns: CdTableColumn[];
+ users: Array<any>;
+ selection = new CdTableSelection();
+
+ modalRef: BsModalRef;
+
+ constructor(
+ private userService: UserService,
+ private emptyPipe: EmptyPipe,
+ private modalService: BsModalService,
+ private notificationService: NotificationService,
+ private authStorageService: AuthStorageService
+ ) {
+ this.permission = this.authStorageService.getPermissions().user;
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: 'Username',
+ prop: 'username',
+ flexGrow: 1
+ },
+ {
+ name: 'Name',
+ prop: 'name',
+ flexGrow: 1,
+ pipe: this.emptyPipe
+ },
+ {
+ name: 'Email',
+ prop: 'email',
+ flexGrow: 1,
+ pipe: this.emptyPipe
+ },
+ {
+ name: 'Roles',
+ prop: 'roles',
+ flexGrow: 1,
+ cellTemplate: this.userRolesTpl
+ }
+ ];
+ }
+
+ getUsers() {
+ this.userService.list().subscribe((users: Array<any>) => {
+ this.users = users;
+ });
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ deleteUser(username: string) {
+ this.userService.delete(username).subscribe(
+ () => {
+ this.getUsers();
+ this.modalRef.hide();
+ this.notificationService.show(
+ NotificationType.success,
+ `User "${username}" has been deleted.`,
+ 'Delete User'
+ );
+ },
+ () => {
+ this.modalRef.content.stopLoadingSpinner();
+ }
+ );
+ }
+
+ deleteUserModal() {
+ const sessionUsername = this.authStorageService.getUsername();
+ const username = this.selection.first().username;
+ if (sessionUsername === username) {
+ this.notificationService.show(
+ NotificationType.error,
+ `You are currently authenticated with user "${username}".`,
+ 'Cannot Delete User'
+ );
+ return;
+ }
+ this.modalRef = this.modalService.show(DeletionModalComponent);
+ this.modalRef.content.setUp({
+ metaType: 'User',
+ pattern: `${username}`,
+ deletionMethod: () => this.deleteUser(username),
+ modalRef: this.modalRef
+ });
+ }
+}
--- /dev/null
+<div dropdown
+ *ngIf="userPermission.read">
+ <a dropdownToggle
+ class="dropdown-toggle"
+ data-toggle="dropdown">
+ <i class="fa fa-cog"></i>
+ <span class="caret"></span>
+ </a>
+ <ul *dropdownMenu
+ class="dropdown-menu">
+ <li *ngIf="userPermission.read">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/users">Users
+ </a>
+ </li>
+ </ul>
+</div>
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { AdministrationComponent } from './administration.component';
+
+describe('AdministrationComponent', () => {
+ let component: AdministrationComponent;
+ let fixture: ComponentFixture<AdministrationComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [SharedModule],
+ declarations: [AdministrationComponent]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AdministrationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+
+import { Permission } from '../../../shared/models/permissions';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
+
+@Component({
+ selector: 'cd-administration',
+ templateUrl: './administration.component.html',
+ styleUrls: ['./administration.component.scss']
+})
+export class AdministrationComponent implements OnInit {
+ userPermission: Permission;
+
+ constructor(private authStorageService: AuthStorageService) {
+ this.userPermission = this.authStorageService.getPermissions().user;
+ }
+
+ ngOnInit() {}
+}
import { SharedModule } from '../../shared/shared.module';
import { AuthModule } from '../auth/auth.module';
import { AboutComponent } from './about/about.component';
+import { AdministrationComponent } from './administration/administration.component';
import { DashboardHelpComponent } from './dashboard-help/dashboard-help.component';
import { NavigationComponent } from './navigation/navigation.component';
import { NotificationsComponent } from './notifications/notifications.component';
NavigationComponent,
NotificationsComponent,
TaskManagerComponent,
- DashboardHelpComponent
+ DashboardHelpComponent,
+ AdministrationComponent
],
exports: [NavigationComponent]
})
<li>
<cd-dashboard-help class="oa-navbar"></cd-dashboard-help>
</li>
+ <li>
+ <cd-administration class="oa-navbar"></cd-administration>
+ </li>
<li class="tc_logout">
<cd-logout class="oa-navbar"></cd-logout>
</li>
});
}
- logout() {
- return this.http
- .delete('api/auth')
- .toPromise()
- .then(() => {
- this.authStorageService.remove();
- });
+ logout(callback: Function) {
+ return this.http.delete('api/auth').subscribe(() => {
+ this.authStorageService.remove();
+ if (callback) {
+ callback();
+ }
+ });
}
}
--- /dev/null
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '../../../testing/unit-test-helper';
+import { RoleService } from './role.service';
+
+describe('RoleService', () => {
+ let service: RoleService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [RoleService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.get(RoleService);
+ httpTesting = TestBed.get(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne('api/role');
+ expect(req.request.method).toBe('GET');
+ });
+
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { ApiModule } from './api.module';
+
+@Injectable({
+ providedIn: ApiModule
+})
+export class RoleService {
+ constructor(private http: HttpClient) {}
+
+ list() {
+ return this.http.get('api/role');
+ }
+}
--- /dev/null
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '../../../testing/unit-test-helper';
+import { UserFormModel } from '../../core/auth/user-form/user-form.model';
+import { UserService } from './user.service';
+
+describe('UserService', () => {
+ let service: UserService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [UserService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.get(UserService);
+ httpTesting = TestBed.get(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call create', () => {
+ const user = new UserFormModel();
+ user.username = 'user0';
+ user.password = 'pass0';
+ user.name = 'User 0';
+ user.email = 'user0@email.com';
+ user.roles = ['administrator'];
+ service.create(user).subscribe();
+ const req = httpTesting.expectOne('api/user');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual(user);
+ });
+
+ it('should call delete', () => {
+ service.delete('user0').subscribe();
+ const req = httpTesting.expectOne('api/user/user0');
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call update', () => {
+ const user = new UserFormModel();
+ user.username = 'user0';
+ user.password = 'pass0';
+ user.name = 'User 0';
+ user.email = 'user0@email.com';
+ user.roles = ['administrator'];
+ service.update(user).subscribe();
+ const req = httpTesting.expectOne('api/user/user0');
+ expect(req.request.body).toEqual(user);
+ expect(req.request.method).toBe('PUT');
+ });
+
+ it('should call get', () => {
+ service.get('user0').subscribe();
+ const req = httpTesting.expectOne('api/user/user0');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne('api/user');
+ expect(req.request.method).toBe('GET');
+ });
+
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { UserFormModel } from '../../core/auth/user-form/user-form.model';
+import { ApiModule } from './api.module';
+
+@Injectable({
+ providedIn: ApiModule
+})
+export class UserService {
+ constructor(private http: HttpClient) {}
+
+ list() {
+ return this.http.get('api/user');
+ }
+
+ delete(username: string) {
+ return this.http.delete(`api/user/${username}`);
+ }
+
+ get(username: string) {
+ return this.http.get(`api/user/${username}`);
+ }
+
+ create(user: UserFormModel) {
+ return this.http.post(`api/user`, user);
+ }
+
+ update(user: UserFormModel) {
+ return this.http.put(`api/user/${user.username}`, user);
+ }
+}
cephfs = 'CephFS',
rbd = 'RBD',
pool = 'Pool',
- osd = 'OSD'
+ osd = 'OSD',
+ user = 'User'
}
cephfs: Permission;
manager: Permission;
log: Permission;
+ user: Permission;
constructor(serverPermissions: any) {
this.hosts = new Permission(serverPermissions['hosts']);
this.cephfs = new Permission(serverPermissions['cephfs']);
this.manager = new Permission(serverPermissions['manager']);
this.log = new Permission(serverPermissions['log']);
+ this.user = new Permission(serverPermissions['user']);
}
}
return localStorage.getItem('dashboard_username') !== null;
}
+ getUsername() {
+ return localStorage.getItem('dashboard_username');
+ }
+
getPermissions(): Permissions {
return JSON.parse(
localStorage.getItem('dashboard_permissions') || JSON.stringify(new Permissions({}))