]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Clone an existing user role 32653/head
authorVolker Theile <vtheile@suse.com>
Wed, 15 Jan 2020 09:07:58 +0000 (10:07 +0100)
committerVolker Theile <vtheile@suse.com>
Tue, 21 Jan 2020 13:51:56 +0000 (14:51 +0100)
Fixes: https://tracker.ceph.com/issues/43603
Signed-off-by: Volker Theile <vtheile@suse.com>
qa/tasks/mgr/dashboard/test_role.py
src/pybind/mgr/dashboard/controllers/role.py
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.ts

index 6b0e35b244134e45bb5d46065ded20997fa633b9..dbfaea9e4f2d4308ae3326ad1082193611d158e2 100644 (file)
@@ -138,3 +138,8 @@ class RoleTest(DashboardTestCase):
         self.assertStatus(400)
         self.assertError(code='cannot_update_system_role',
                          component='role')
+
+    def test_clone_role(self):
+        self._post('/api/role/read-only/clone', {'new_name': 'foo'})
+        self.assertStatus(201)
+        self._delete('/api/role/foo')
index f87eff7bc3c55aff2c8464fe8a08d2d1a89e81b0..6bf616d08ef12cd1cd9ba3011e80c17bf044e24f 100644 (file)
@@ -3,7 +3,8 @@ from __future__ import absolute_import
 
 import cherrypy
 
-from . import ApiController, RESTController, UiApiController
+from . import ApiController, RESTController, UiApiController,\
+    CreatePermission
 from .. import mgr
 from ..exceptions import RoleDoesNotExist, DashboardException,\
     RoleIsAssociatedWithUser, RoleAlreadyExists
@@ -42,12 +43,14 @@ class Role(RESTController):
                     role.set_scope_permissions(scope, permissions)
 
     def list(self):
+        # type: () -> list
         roles = dict(mgr.ACCESS_CTRL_DB.roles)
         roles.update(SYSTEM_ROLES)
         roles = sorted(roles.values(), key=lambda role: role.name)
         return [Role._role_to_dict(r) for r in roles]
 
-    def get(self, name):
+    @staticmethod
+    def _get(name):
         role = SYSTEM_ROLES.get(name)
         if not role:
             try:
@@ -56,7 +59,12 @@ class Role(RESTController):
                 raise cherrypy.HTTPError(404)
         return Role._role_to_dict(role)
 
-    def create(self, name=None, description=None, scopes_permissions=None):
+    def get(self, name):
+        # type: (str) -> dict
+        return Role._get(name)
+
+    @staticmethod
+    def _create(name=None, description=None, scopes_permissions=None):
         if not name:
             raise DashboardException(msg='Name is required',
                                      code='name_required',
@@ -72,7 +80,12 @@ class Role(RESTController):
         mgr.ACCESS_CTRL_DB.save()
         return Role._role_to_dict(role)
 
+    def create(self, name=None, description=None, scopes_permissions=None):
+        # type: (str, str, dict) -> dict
+        return Role._create(name, description, scopes_permissions)
+
     def set(self, name, description=None, scopes_permissions=None):
+        # type: (str, str, dict) -> dict
         try:
             role = mgr.ACCESS_CTRL_DB.get_role(name)
         except RoleDoesNotExist:
@@ -89,6 +102,7 @@ class Role(RESTController):
         return Role._role_to_dict(role)
 
     def delete(self, name):
+        # type: (str) -> None
         try:
             mgr.ACCESS_CTRL_DB.delete_role(name)
         except RoleDoesNotExist:
@@ -103,6 +117,14 @@ class Role(RESTController):
                                      component='role')
         mgr.ACCESS_CTRL_DB.save()
 
+    @RESTController.Resource('POST', status=201)
+    @CreatePermission
+    def clone(self, name, new_name):
+        # type: (str, str) -> dict
+        role = Role._get(name)
+        return Role._create(new_name, role.get('description'),
+                            role.get('scopes_permissions'))
+
 
 @UiApiController('/scope', SecurityScope.USER)
 class Scope(RESTController):
index 799cf2503976002b7c5fb70862fcbf491d19984b..4b8ac38c70840f492e5b5d2884751061e8087cc6 100644 (file)
@@ -50,19 +50,19 @@ describe('RoleListComponent', () => {
 
     expect(tableActions).toEqual({
       'create,update,delete': {
-        actions: ['Create', 'Edit', 'Delete'],
+        actions: ['Create', 'Clone', 'Edit', 'Delete'],
         primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
       },
       'create,update': {
-        actions: ['Create', 'Edit'],
+        actions: ['Create', 'Clone', 'Edit'],
         primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
       },
       'create,delete': {
-        actions: ['Create', 'Delete'],
+        actions: ['Create', 'Clone', 'Delete'],
         primary: { multiple: 'Create', executing: 'Delete', single: 'Delete', no: 'Create' }
       },
       create: {
-        actions: ['Create'],
+        actions: ['Create', 'Clone'],
         primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
       },
       'update,delete': {
index 9f5efd5a0dff1a05d345b86e23620ac57718c6f6..c4bc066254b6eb90604be5006a6f520645170b92 100644 (file)
@@ -7,6 +7,7 @@ import { forkJoin } from 'rxjs';
 import { RoleService } from '../../../shared/api/role.service';
 import { ScopeService } from '../../../shared/api/scope.service';
 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { FormModalComponent } from '../../../shared/components/form-modal/form-modal.component';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { CellTemplate } from '../../../shared/enum/cell-template.enum';
 import { Icons } from '../../../shared/enum/icons.enum';
@@ -56,6 +57,13 @@ export class RoleListComponent implements OnInit {
       routerLink: () => this.urlBuilder.getCreate(),
       name: this.actionLabels.CREATE
     };
+    const cloneAction: CdTableAction = {
+      permission: 'create',
+      icon: Icons.clone,
+      name: this.actionLabels.CLONE,
+      disable: () => !this.selection.hasSingleSelection,
+      click: () => this.cloneRole()
+    };
     const editAction: CdTableAction = {
       permission: 'update',
       icon: Icons.edit,
@@ -71,7 +79,7 @@ export class RoleListComponent implements OnInit {
       click: () => this.deleteRoleModal(),
       name: this.actionLabels.DELETE
     };
-    this.tableActions = [addAction, editAction, deleteAction];
+    this.tableActions = [addAction, cloneAction, editAction, deleteAction];
   }
 
   ngOnInit() {
@@ -136,4 +144,35 @@ export class RoleListComponent implements OnInit {
       }
     });
   }
+
+  cloneRole() {
+    const name = this.selection.first().name;
+    this.modalRef = this.modalService.show(FormModalComponent, {
+      initialState: {
+        fields: [
+          {
+            type: 'text',
+            name: 'newName',
+            value: `${name}_clone`,
+            label: this.i18n('New name'),
+            required: true
+          }
+        ],
+        titleText: this.i18n('Clone Role'),
+        submitButtonText: this.i18n('Clone Role'),
+        onSubmit: (values) => {
+          this.roleService.clone(name, values['newName']).subscribe(() => {
+            this.getRoles();
+            this.notificationService.show(
+              NotificationType.success,
+              this.i18n(`Cloned role '{{dst_name}}' from '{{src_name}}'`, {
+                src_name: name,
+                dst_name: values['newName']
+              })
+            );
+          });
+        }
+      }
+    });
+  }
 }
index d3290157340e4705d838d7dc544b9a8dd6711277..c6ea8a009b5943edde693c024d2ffe7866ed9d25 100644 (file)
@@ -44,6 +44,12 @@ describe('RoleService', () => {
     expect(req.request.method).toBe('GET');
   });
 
+  it('should call clone', () => {
+    service.clone('foo', 'bar').subscribe();
+    const req = httpTesting.expectOne('api/role/foo/clone?new_name=bar');
+    expect(req.request.method).toBe('POST');
+  });
+
   it('should check if role name exists', () => {
     let exists: boolean;
     service.exists('role1').subscribe((res: boolean) => {
index 974ee5afb841de28c78db5e02924ceb328a59f9f..08c1097115d4690842c28155f125181dafcec5c3 100644 (file)
@@ -1,4 +1,4 @@
-import { HttpClient } from '@angular/common/http';
+import { HttpClient, HttpParams } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 
 import { Observable, of as observableOf } from 'rxjs';
@@ -17,11 +17,11 @@ export class RoleService {
     return this.http.get('api/role');
   }
 
-  delete(role: string) {
-    return this.http.delete(`api/role/${role}`);
+  delete(name: string) {
+    return this.http.delete(`api/role/${name}`);
   }
 
-  get(name) {
+  get(name: string) {
     return this.http.get(`api/role/${name}`);
   }
 
@@ -29,6 +29,12 @@ export class RoleService {
     return this.http.post(`api/role`, role);
   }
 
+  clone(name: string, newName: string) {
+    let params = new HttpParams();
+    params = params.append('new_name', newName);
+    return this.http.post(`api/role/${name}/clone`, null, { params });
+  }
+
   update(role: RoleFormModel) {
     return this.http.put(`api/role/${role.name}`, role);
   }