From c080f4bebd5d47baca6f3b7f130c90a0dcf031c8 Mon Sep 17 00:00:00 2001 From: Pedro Gonzalez Gomez Date: Thu, 6 Apr 2023 16:18:41 +0200 Subject: [PATCH] mgr/dashboard: delete-ceph-authx Fixes: https://tracker.ceph.com/issues/59365 Signed-off-by: Pedro Gonzalez Gomez (cherry picked from commit 6b5a00fb8e8b9a72d9308a069763dd86e9ecd153) (cherry picked from commit c40ca918f3ef29b5738fd5e5e252f2df3976a5b5) --- src/pybind/mgr/dashboard/controllers/_crud.py | 24 ++++++++++- .../mgr/dashboard/controllers/ceph_users.py | 26 +++++++++++- .../crud-table/crud-table.component.spec.ts | 4 +- .../crud-table/crud-table.component.ts | 42 ++++++++++++++++++- .../app/shared/models/crud-table-metadata.ts | 1 + .../shared/services/data-gateway.service.ts | 9 ++++ .../shared/services/task-message.service.ts | 7 ++++ src/pybind/mgr/dashboard/openapi.yaml | 35 ++++++++++++++++ 8 files changed, 143 insertions(+), 5 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/_crud.py b/src/pybind/mgr/dashboard/controllers/_crud.py index 63224928b1bbd..bf86773116fd7 100644 --- a/src/pybind/mgr/dashboard/controllers/_crud.py +++ b/src/pybind/mgr/dashboard/controllers/_crud.py @@ -64,7 +64,8 @@ class TableAction(NamedTuple): name: str permission: str icon: str - routerLink: str # redirect to... + routerLink: str = '' # redirect to... + click: str = '' class TableComponent(SerializableClass): @@ -76,6 +77,7 @@ class TableComponent(SerializableClass): class Icon(Enum): add = 'fa fa-plus' + destroy = 'fa fa-times' class Validator(Enum): @@ -272,6 +274,7 @@ class CRUDMeta(SerializableClass): self.permissions = [] self.actions = [] self.forms = [] + self.columnKey = '' self.detail_columns = [] @@ -285,18 +288,20 @@ class CRUDResourceMethod(NamedTuple): doc: EndpointDoc +# pylint: disable=R0902 class CRUDEndpoint: # for testing purposes CRUDClass: Optional[RESTController] = None CRUDClassMetadata: Optional[RESTController] = None - # pylint: disable=R0902 def __init__(self, router: APIRouter, doc: APIDoc, set_column: Optional[Dict[str, Dict[str, str]]] = None, actions: Optional[List[TableAction]] = None, permissions: Optional[List[str]] = None, forms: Optional[List[Form]] = None, + column_key: Optional[str] = None, meta: CRUDMeta = CRUDMeta(), get_all: Optional[CRUDCollectionMethod] = None, create: Optional[CRUDCollectionMethod] = None, + delete: Optional[CRUDCollectionMethod] = None, detail_columns: Optional[List[str]] = None): self.router = router self.doc = doc @@ -306,7 +311,9 @@ class CRUDEndpoint: self.meta = meta self.get_all = get_all self.create = create + self.delete = delete self.permissions = permissions if permissions is not None else [] + self.column_key = column_key if column_key is not None else '' self.detail_columns = detail_columns if detail_columns is not None else [] def __call__(self, cls: Any): @@ -338,6 +345,12 @@ class CRUDEndpoint: def create(self, *args, **kwargs): return outer_self.create.func(self, *args, **kwargs) # type: ignore + if self.delete: + @self.delete.doc + @wraps(self.delete.func) + def delete(self, *args, **kwargs): + return outer_self.delete.func(self, *args, **kwargs) # type: ignore + cls.CRUDClass = CRUDClass def create_meta_class(self, cls): @@ -346,6 +359,7 @@ class CRUDEndpoint: self.generate_actions() self.generate_forms() self.set_permissions() + self.set_column_key() self.get_detail_columns() return serialize(self.__class__.outer_self.meta) @@ -385,6 +399,11 @@ class CRUDEndpoint: def set_permissions(self): if self.__class__.outer_self.permissions: self.outer_self.meta.permissions.extend(self.__class__.outer_self.permissions) + + def set_column_key(self): + if self.__class__.outer_self.column_key: + self.outer_self.meta.columnKey = self.__class__.outer_self.column_key + class_name = self.router.path.replace('/', '') meta_class = type(f'{class_name}_CRUDClassMetadata', (RESTController,), @@ -394,6 +413,7 @@ class CRUDEndpoint: 'generate_actions': generate_actions, 'generate_forms': generate_forms, 'set_permissions': set_permissions, + 'set_column_key': set_column_key, 'get_detail_columns': get_detail_columns, 'outer_self': self, }) diff --git a/src/pybind/mgr/dashboard/controllers/ceph_users.py b/src/pybind/mgr/dashboard/controllers/ceph_users.py index 439a2e093835c..e66aa18fa2e94 100644 --- a/src/pybind/mgr/dashboard/controllers/ceph_users.py +++ b/src/pybind/mgr/dashboard/controllers/ceph_users.py @@ -57,6 +57,23 @@ class CephUserEndpoints: raise DashboardException(msg, code=500) return f"Successfully created user '{user_entity}'" + @staticmethod + def user_delete(_, user_entity: str): + """ + Delete a ceph user and it's defined capabilities. + :param user_entity: Entity to dlelete + """ + logger.debug("Sending command 'auth del' of entity '%s'", user_entity) + try: + CephService.send_command('mon', 'auth del', entity=user_entity) + except SendCommandError as ex: + msg = f'{ex} in command {ex.prefix}' + if ex.errno == -EINVAL: + raise DashboardException(msg, code=400) + raise DashboardException(msg, code=500) + return f"Successfully eleted user '{user_entity}'" + + create_cap_container = ArrayHorizontalContainer('Capabilities', 'capabilities', fields=[ FormField('Entity', 'entity', field_type=str), @@ -81,10 +98,13 @@ create_form = Form(path='/cluster/user/create', set_column={"caps": {"cellTemplate": "badgeDict"}}, actions=[ TableAction(name='create', permission='create', icon=Icon.add.value, - routerLink='/cluster/user/create') + routerLink='/cluster/user/create'), + TableAction(name='Delete', permission='delete', icon=Icon.destroy.value, + click='delete') ], permissions=[Scope.CONFIG_OPT], forms=[create_form], + column_key='entity', get_all=CRUDCollectionMethod( func=CephUserEndpoints.user_list, doc=EndpointDoc("Get Ceph Users") @@ -93,6 +113,10 @@ create_form = Form(path='/cluster/user/create', func=CephUserEndpoints.user_create, doc=EndpointDoc("Create Ceph User") ), + delete=CRUDCollectionMethod( + func=CephUserEndpoints.user_delete, + doc=EndpointDoc("Delete Ceph User") + ), meta=CRUDMeta() ) class CephUser(NamedTuple): diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.spec.ts index df10e2a97e435..678bec079c46e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.spec.ts @@ -7,6 +7,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { NgxDatatableModule } from '@swimlane/ngx-datatable'; import { NgxPipeFunctionModule } from 'ngx-pipe-function'; +import { ToastrModule } from 'ngx-toastr'; import { ComponentsModule } from '~/app/shared/components/components.module'; import { PipesModule } from '~/app/shared/pipes/pipes.module'; @@ -30,7 +31,8 @@ describe('CRUDTableComponent', () => { NgbTooltipModule, RouterTestingModule, NgxPipeFunctionModule, - HttpClientTestingModule + HttpClientTestingModule, + ToastrModule.forRoot() ] }); beforeEach(() => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts index 7241a08b63894..75ff5341c08cc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts @@ -8,8 +8,13 @@ import { CrudMetadata } from '~/app/shared/models/crud-table-metadata'; import { DataGatewayService } from '~/app/shared/services/data-gateway.service'; import { TimerService } from '~/app/shared/services/timer.service'; import { CdTableSelection } from '../../models/cd-table-selection'; +import { FinishedTask } from '../../models/finished-task'; import { Permission, Permissions } from '../../models/permissions'; import { AuthStorageService } from '../../services/auth-storage.service'; +import { TaskWrapperService } from '../../services/task-wrapper.service'; +import { ModalService } from '../../services/modal.service'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { CriticalConfirmationModalComponent } from '../../components/critical-confirmation-modal/critical-confirmation-modal.component'; @Component({ selector: 'cd-crud-table', @@ -32,12 +37,16 @@ export class CRUDTableComponent implements OnInit { selection = new CdTableSelection(); expandedRow: any = null; tabs = {}; + resource: string; + modalRef: NgbModalRef; constructor( private authStorageService: AuthStorageService, private timerService: TimerService, private dataGatewayService: DataGatewayService, - private activatedRoute: ActivatedRoute + private activatedRoute: ActivatedRoute, + private taskWrapper: TaskWrapperService, + private modalService: ModalService ) { this.permissions = this.authStorageService.getPermissions(); } @@ -55,6 +64,9 @@ export class CRUDTableComponent implements OnInit { .subscribe((response: CrudMetadata) => this.processMeta(response)); this.data$ = this.timerService.get(() => this.dataGatewayService.list(resource)); }); + this.activatedRoute.data.subscribe((data: any) => { + this.resource = data.resource; + }); } processMeta(meta: CrudMetadata) { @@ -86,6 +98,34 @@ export class CRUDTableComponent implements OnInit { return !col['isHidden']; }); this.meta = meta; + for (let i = 0; i < this.meta.actions.length; i++) { + if (this.meta.actions[i].click.toString() !== '') { + this.meta.actions[i].click = this[this.meta.actions[i].click.toString()].bind(this); + } + } + } + + delete() { + const selectedKey = this.selection.first()[this.meta.columnKey]; + this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { + itemDescription: $localize`${this.meta.columnKey}`, + itemNames: [selectedKey], + submitAction: () => { + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('crud-component/id', selectedKey), + call: this.dataGatewayService.delete(this.resource, selectedKey) + }) + .subscribe({ + error: () => { + this.modalRef.close(); + }, + complete: () => { + this.modalRef.close(); + } + }); + } + }); } updateSelection(selection: CdTableSelection) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts index fbd4979ec609c..2f8a9bc9e29a1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts @@ -12,4 +12,5 @@ export class CrudMetadata { permissions: string[]; actions: CdTableAction[]; forms: any; + columnKey: string; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts index f6db42818772a..3424ca1b3c44f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts @@ -35,6 +35,15 @@ export class DataGatewayService { }); } + delete(dataPath: string, key: string): Observable { + const { url, version } = this.getUrlAndVersion(dataPath); + + return this.http.delete(`${url}/${key}`, { + headers: { Accept: `application/vnd.ceph.api.v${version}+json` }, + observe: 'response' + }); + } + form(dataPath: string): Observable { const cacheable = this.getCacheable(dataPath, 'get'); if (this.cache[cacheable] === undefined) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 42c07ff0471be..195f3b1374ab3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -343,6 +343,9 @@ export class TaskMessageService { ), 'crud-component': this.newTaskMessage(this.commonOperations.create, (metadata) => this.crudMessage(metadata) + ), + 'crud-component/id': this.newTaskMessage(this.commonOperations.delete, (id) => + this.crudMessageId(id) ) }; @@ -399,6 +402,10 @@ export class TaskMessageService { return $localize`${message}`; } + crudMessageId(id: string) { + return $localize`${id}`; + } + _getTaskTitle(task: Task) { if (task.name && task.name.startsWith('progress/')) { // we don't fill the failure string because, at least for now, all diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 108f940e9ef3c..606794bd4d782 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -2211,6 +2211,41 @@ paths: summary: Create Ceph User tags: - Cluster + /api/cluster/user/{user_entity}: + delete: + description: "\n Delete a ceph user and it's defined capabilities.\n\ + \ :param user_entity: Entity to dlelete\n " + parameters: + - in: path + name: user_entity + required: true + schema: + type: string + responses: + '202': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Operation is still executing. Please check the task queue. + '204': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Resource deleted. + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: Delete Ceph User + tags: + - Cluster /api/cluster_conf: get: parameters: [] -- 2.39.5