]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Ceph dashboard user management 22758/head
authorRicardo Marques <rimarques@suse.com>
Tue, 12 Jun 2018 14:55:08 +0000 (15:55 +0100)
committerRicardo Marques <rimarques@suse.com>
Wed, 25 Jul 2018 16:51:48 +0000 (17:51 +0100)
Fixes: https://tracker.ceph.com/issues/24446
Signed-off-by: Ricardo Marques <rimarques@suse.com>
28 files changed:
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/logout/logout.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-mode.enum.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-role.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/components.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/permissions.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts

index 3ed80e16c40c8be6f7004f7a2b8d1d11b4048edd..0d44e19ec7d9eb0cc4377b180a7d05cf80cc6307 100644 (file)
@@ -20,6 +20,8 @@ import { RgwDaemonListComponent } from './ceph/rgw/rgw-daemon-list/rgw-daemon-li
 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';
@@ -108,6 +110,9 @@ const routes: Routes = [
   { 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] },
index e96b1b30b8f84546d3641fcb7edd77726fab8b91..f9e395c14514209acb6af775d12b9abcb436e739 100644 (file)
@@ -1,18 +1,28 @@
 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 { }
index 4a8405e2eefad0daa701deb844d4f490a0806e33..36098d9e357ec203578137d9ce58cd2120570b27 100644 (file)
@@ -9,15 +9,12 @@ import { AuthService } from '../../../shared/api/auth.service';
   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']);
     });
   }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-mode.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-mode.enum.ts
new file mode 100644 (file)
index 0000000..8cae7d1
--- /dev/null
@@ -0,0 +1,3 @@
+export enum UserFormMode {
+  editing = 'editing'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-role.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-role.model.ts
new file mode 100644 (file)
index 0000000..6f3ce00
--- /dev/null
@@ -0,0 +1,11 @@
+export class UserFormRoleModel implements SelectBadgesOption {
+  name: string;
+  description: string;
+  selected = false;
+  scopes_permissions: object;
+
+  constructor(name, description) {
+    this.name = name;
+    this.description = description;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html
new file mode 100644 (file)
index 0000000..60dd162
--- /dev/null
@@ -0,0 +1,197 @@
+<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>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts
new file mode 100644 (file)
index 0000000..5dfaa74
--- /dev/null
@@ -0,0 +1,243 @@
+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']);
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts
new file mode 100644 (file)
index 0000000..02ff034
--- /dev/null
@@ -0,0 +1,231 @@
+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();
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.model.ts
new file mode 100644 (file)
index 0000000..cd3b188
--- /dev/null
@@ -0,0 +1,7 @@
+export class UserFormModel {
+  username: string;
+  password: string;
+  name: string;
+  email: string;
+  roles: Array<string>;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.html
new file mode 100644 (file)
index 0000000..f2e902c
--- /dev/null
@@ -0,0 +1,86 @@
+<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>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.spec.ts
new file mode 100644 (file)
index 0000000..8511e46
--- /dev/null
@@ -0,0 +1,30 @@
+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();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts
new file mode 100644 (file)
index 0000000..03be329
--- /dev/null
@@ -0,0 +1,114 @@
+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
+    });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.html
new file mode 100644 (file)
index 0000000..934c003
--- /dev/null
@@ -0,0 +1,18 @@
+<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>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.spec.ts
new file mode 100644 (file)
index 0000000..a4c47b4
--- /dev/null
@@ -0,0 +1,26 @@
+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();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.ts
new file mode 100644 (file)
index 0000000..eacef89
--- /dev/null
@@ -0,0 +1,19 @@
+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() {}
+}
index aaf9bb91fe80a704982b569959e56824fb32cb6b..178eeade9b7ac6c295e6d59c809b6d2baa3a795d 100644 (file)
@@ -8,6 +8,7 @@ import { AppRoutingModule } from '../../app-routing.module';
 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';
@@ -31,7 +32,8 @@ import { TaskManagerComponent } from './task-manager/task-manager.component';
     NavigationComponent,
     NotificationsComponent,
     TaskManagerComponent,
-    DashboardHelpComponent
+    DashboardHelpComponent,
+    AdministrationComponent
   ],
   exports: [NavigationComponent]
 })
index e7cc38ae1aac000927aecc62faf4c50e62eee483..d212e21deea94992f0f63384ae6913942144617c 100644 (file)
       <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>
index 7284d862c18648229440338bc03e8e0b94d36556..9954e346b1a8d73a381e7a2d5afb230567768aac 100644 (file)
@@ -20,12 +20,12 @@ export class AuthService {
       });
   }
 
-  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();
+      }
+    });
   }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.spec.ts
new file mode 100644 (file)
index 0000000..ac057ce
--- /dev/null
@@ -0,0 +1,35 @@
+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');
+  });
+
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.ts
new file mode 100644 (file)
index 0000000..9c828b4
--- /dev/null
@@ -0,0 +1,15 @@
+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');
+  }
+}
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
new file mode 100644 (file)
index 0000000..1e87ca0
--- /dev/null
@@ -0,0 +1,74 @@
+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');
+  });
+
+});
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
new file mode 100644 (file)
index 0000000..2643c7c
--- /dev/null
@@ -0,0 +1,32 @@
+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);
+  }
+}
index 2c6dd9b1766deeda79fdc47cd6f627e9b52a7200..39b2228e08b36ee522163b120a57e359b7f731b7 100644 (file)
@@ -3,5 +3,6 @@ export enum Components {
   cephfs = 'CephFS',
   rbd = 'RBD',
   pool = 'Pool',
-  osd = 'OSD'
+  osd = 'OSD',
+  user = 'User'
 }
index f97652cfac5a9fc0121958049960eb0315db3d6f..e8a50e9e42b9295ee2e0ee9a5e7c1d158c945c0b 100644 (file)
@@ -25,6 +25,7 @@ export class Permissions {
   cephfs: Permission;
   manager: Permission;
   log: Permission;
+  user: Permission;
 
   constructor(serverPermissions: any) {
     this.hosts = new Permission(serverPermissions['hosts']);
@@ -39,5 +40,6 @@ export class Permissions {
     this.cephfs = new Permission(serverPermissions['cephfs']);
     this.manager = new Permission(serverPermissions['manager']);
     this.log = new Permission(serverPermissions['log']);
+    this.user = new Permission(serverPermissions['user']);
   }
 }
index cbcda41209e1a9b0a4c9c8d5f3259bfc4d5191f8..86afaff40fba16a9f60b380a0603acf791bc972b 100644 (file)
@@ -22,6 +22,10 @@ export class AuthStorageService {
     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({}))