From: Volker Theile Date: Wed, 15 Jan 2020 09:07:58 +0000 (+0100) Subject: mgr/dashboard: Clone an existing user role X-Git-Tag: v15.1.0~119^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=944490f34f0bea26cd29eaa1886e697f223bd962;p=ceph.git mgr/dashboard: Clone an existing user role Fixes: https://tracker.ceph.com/issues/43603 Signed-off-by: Volker Theile --- diff --git a/qa/tasks/mgr/dashboard/test_role.py b/qa/tasks/mgr/dashboard/test_role.py index 6b0e35b2441..dbfaea9e4f2 100644 --- a/qa/tasks/mgr/dashboard/test_role.py +++ b/qa/tasks/mgr/dashboard/test_role.py @@ -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') diff --git a/src/pybind/mgr/dashboard/controllers/role.py b/src/pybind/mgr/dashboard/controllers/role.py index f87eff7bc3c..6bf616d08ef 100644 --- a/src/pybind/mgr/dashboard/controllers/role.py +++ b/src/pybind/mgr/dashboard/controllers/role.py @@ -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): diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts index 799cf250397..4b8ac38c708 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts @@ -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': { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts index 9f5efd5a0df..c4bc066254b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts @@ -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'] + }) + ); + }); + } + } + }); + } } 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 index d3290157340..c6ea8a009b5 100644 --- 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 @@ -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) => { 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 index 974ee5afb84..08c1097115d 100644 --- 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 @@ -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); }