]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: delete-ceph-authx
authorPedro Gonzalez Gomez <pegonzal@redhat.com>
Thu, 6 Apr 2023 14:18:41 +0000 (16:18 +0200)
committerPere Diaz Bou <pere-altea@hotmail.com>
Fri, 5 May 2023 16:43:11 +0000 (18:43 +0200)
Fixes: https://tracker.ceph.com/issues/59365
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
(cherry picked from commit 6b5a00fb8e8b9a72d9308a069763dd86e9ecd153)
(cherry picked from commit c40ca918f3ef29b5738fd5e5e252f2df3976a5b5)

src/pybind/mgr/dashboard/controllers/_crud.py
src/pybind/mgr/dashboard/controllers/ceph_users.py
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/openapi.yaml

index 63224928b1bbd1016bde825a9bdb566aae4d41ee..bf86773116fd7638b48454501e170ac1a218165a 100644 (file)
@@ -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,
                           })
index 439a2e093835cec4d2090d04d078f14bd0da45b3..e66aa18fa2e9446066b0fbc964abebd228ec888d 100644 (file)
@@ -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):
index df10e2a97e43548be2f1ac31018db5ce317517d7..678bec079c46eb85493fbf82386ed529d579a970 100644 (file)
@@ -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(() => {
index 7241a08b63894cf03b462b76d94ed160b938065c..75ff5341c08cc6ae061f6fa1a28c425db713c522 100644 (file)
@@ -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) {
index fbd4979ec609ccc96cacff9501f731ee7d004e52..2f8a9bc9e29a13411e71ac4996f2d1aa44a7e627 100644 (file)
@@ -12,4 +12,5 @@ export class CrudMetadata {
   permissions: string[];
   actions: CdTableAction[];
   forms: any;
+  columnKey: string;
 }
index f6db42818772a703df9c868bb730bfd951b4fa77..3424ca1b3c44f145c83c4d4f7742cb16b1af7a1b 100644 (file)
@@ -35,6 +35,15 @@ export class DataGatewayService {
     });
   }
 
+  delete(dataPath: string, key: string): Observable<any> {
+    const { url, version } = this.getUrlAndVersion(dataPath);
+
+    return this.http.delete<any>(`${url}/${key}`, {
+      headers: { Accept: `application/vnd.ceph.api.v${version}+json` },
+      observe: 'response'
+    });
+  }
+
   form(dataPath: string): Observable<JsonFormUISchema> {
     const cacheable = this.getCacheable(dataPath, 'get');
     if (this.cache[cacheable] === undefined) {
index 42c07ff0471be6a9be7f59e237ca615cb451c226..195f3b1374ab3134a348417d0757df91e2c3e3a2 100644 (file)
@@ -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
index 108f940e9ef3cd9b1e333fc5a1a369a32f1e127b..606794bd4d7823aa8a28d272436f6d8af4d84a22 100644 (file)
@@ -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: []