]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: rgw role listing
authorPere Diaz Bou <pdiazbou@redhat.com>
Mon, 23 Jan 2023 11:34:12 +0000 (12:34 +0100)
committerPere Diaz Bou <pdiazbou@redhat.com>
Tue, 14 Feb 2023 09:10:00 +0000 (10:10 +0100)
Listing is performed using the radosgw-admin command api we have with
the mgr for now until the S3 api is fixed: https://tracker.ceph.com/issues/58547.

This commit fixes and issue with regards to the _crud.py controller
where redefining `CRUDClassMetadata` caused the users table and the
roles table to share columns. We fixed this by creating
CRUDClassMetadata dynamically for each endpoint.

The issue described above is linked to an issue with NamedTuple were
default nested lists are not a great move because it can cause
unexpected issues when 2 or more classes are created. Moreover,
NamedTuples are read-only making initialization even harder without
factory methods as with dataclasses. Therefore, let's move to the good
old __init__ :).

Signed-off-by: Pere Diaz Bou <pdiazbou@redhat.com>
Fixes: https://tracker.ceph.com/issues/58699
13 files changed:
src/pybind/mgr/dashboard/controllers/_crud.py
src/pybind/mgr/dashboard/controllers/ceph_users.py
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py

index 94b1a3146d7a2d482e41ce6331cae25356ca73f4..0ac299db6e4674f6948e2976c348c74b0d349c7c 100644 (file)
@@ -18,9 +18,21 @@ def isnamedtuple(o):
     return isinstance(o, tuple) and hasattr(o, '_asdict') and hasattr(o, '_fields')
 
 
+class SerializableClass:
+    def __iter__(self):
+        for attr in self.__dict__:
+            if not attr.startswith("__"):
+                yield attr, getattr(self, attr)
+
+    def __contains__(self, value):
+        return value in self.__dict__
+
+    def __len__(self):
+        return len(self.__dict__)
+
+
 def serialize(o, expected_type=None):
     # pylint: disable=R1705,W1116
-    print(o, expected_type)
     if isnamedtuple(o):
         hints = get_type_hints(o)
         return {k: serialize(v, hints[k]) for k, v in zip(o._fields, o)}
@@ -30,6 +42,8 @@ def serialize(o, expected_type=None):
         # NOTE: we could add a metadata value in a list to indentify tuples and,
         # sets if we wanted but for now let's go for lists.
         return [serialize(i) for i in o]
+    elif isinstance(o, SerializableClass):
+        return {serialize(k): serialize(v) for k, v in o}
     elif isinstance(o, (Iterator, Generator)):
         return [serialize(i) for i in o]
     elif expected_type and isclass(expected_type) and issubclass(expected_type, SecretStr):
@@ -43,6 +57,7 @@ class TableColumn(NamedTuple):
     cellTemplate: str = ''
     isHidden: bool = False
     filterable: bool = True
+    flexGrow: int = 1
 
 
 class TableAction(NamedTuple):
@@ -52,10 +67,11 @@ class TableAction(NamedTuple):
     routerLink: str  # redirect to...
 
 
-class TableComponent(NamedTuple):
-    columns: List[TableColumn] = []
-    columnMode: str = 'flex'
-    toolHeader: bool = True
+class TableComponent(SerializableClass):
+    def __init__(self) -> None:
+        self.columns: List[TableColumn] = []
+        self.columnMode: str = 'flex'
+        self.toolHeader: bool = True
 
 
 class Icon(Enum):
@@ -269,11 +285,12 @@ class Form:
         return container_schema
 
 
-class CRUDMeta(NamedTuple):
-    table: TableComponent = TableComponent()
-    permissions: List[str] = []
-    actions: List[Dict[str, Any]] = []
-    forms: List[Dict[str, Any]] = []
+class CRUDMeta(SerializableClass):
+    def __init__(self):
+        self.table = TableComponent()
+        self.permissions = []
+        self.actions = []
+        self.forms = []
 
 
 class CRUDCollectionMethod(NamedTuple):
@@ -286,23 +303,39 @@ class CRUDResourceMethod(NamedTuple):
     doc: EndpointDoc
 
 
-class CRUDEndpoint(NamedTuple):
-    router: APIRouter
-    doc: APIDoc
-    set_column: Optional[Dict[str, Dict[str, str]]] = None
-    actions: List[TableAction] = []
-    permissions: List[str] = []
-    forms: List[Form] = []
-    meta: CRUDMeta = CRUDMeta()
-    get_all: Optional[CRUDCollectionMethod] = None
-    create: Optional[CRUDCollectionMethod] = None
-
+class CRUDEndpoint:
     # for testing purposes
     CRUDClass: Optional[RESTController] = None
     CRUDClassMetadata: Optional[RESTController] = None
-    # ---------------------
 
-    def __call__(self, cls: NamedTuple):
+    # 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,
+                 meta: CRUDMeta = CRUDMeta(), get_all: Optional[CRUDCollectionMethod] = None,
+                 create: Optional[CRUDCollectionMethod] = None):
+        self.router = router
+        self.doc = doc
+        self.set_column = set_column
+        if actions:
+            self.actions = actions
+        else:
+            self.actions = []
+
+        if forms:
+            self.forms = forms
+        else:
+            self.forms = []
+        self.meta = meta
+        self.get_all = get_all
+        self.create = create
+        if permissions:
+            self.permissions = permissions
+        else:
+            self.permissions = []
+
+    def __call__(self, cls: Any):
         self.create_crud_class(cls)
 
         self.meta.table.columns.extend(TableColumn(prop=field) for field in cls._fields)
@@ -334,42 +367,55 @@ class CRUDEndpoint(NamedTuple):
         cls.CRUDClass = CRUDClass
 
     def create_meta_class(self, cls):
-        outer_self: CRUDEndpoint = self
-
-        @UIRouter(self.router.path, self.router.security_scope)
-        class CRUDClassMetadata(RESTController):
-            def list(self):
-                self.update_columns()
-                self.generate_actions()
-                self.generate_forms()
-                self.set_permissions()
-                return serialize(outer_self.meta)
-
-            def update_columns(self):
-                if outer_self.set_column:
-                    for i, column in enumerate(outer_self.meta.table.columns):
-                        if column.prop in dict(outer_self.set_column):
-                            new_template = outer_self.set_column[column.prop]["cellTemplate"]
-                            new_column = TableColumn(column.prop,
-                                                     new_template,
-                                                     column.isHidden,
-                                                     column.filterable)
-                            outer_self.meta.table.columns[i] = new_column
-
-            def generate_actions(self):
-                outer_self.meta.actions.clear()
-
-                for action in outer_self.actions:
-                    outer_self.meta.actions.append(action._asdict())
-
-            def generate_forms(self):
-                outer_self.meta.forms.clear()
-
-                for form in outer_self.forms:
-                    outer_self.meta.forms.append(form.to_dict())
-
-            def set_permissions(self):
-                if outer_self.permissions:
-                    outer_self.meta.permissions.extend(outer_self.permissions)
-
-        cls.CRUDClassMetadata = CRUDClassMetadata
+        def _list(self):
+            self.update_columns()
+            self.generate_actions()
+            self.generate_forms()
+            self.set_permissions()
+            return serialize(self.__class__.outer_self.meta)
+
+        def update_columns(self):
+            if self.__class__.outer_self.set_column:
+                for i, column in enumerate(self.__class__.outer_self.meta.table.columns):
+                    if column.prop in dict(self.__class__.outer_self.set_column):
+                        prop = self.__class__.outer_self.set_column[column.prop]
+                        new_template = ""
+                        if "cellTemplate" in prop:
+                            new_template = prop["cellTemplate"]
+                        hidden = prop['isHidden'] if 'isHidden' in prop else False
+                        flex_grow = prop['flexGrow'] if 'flexGrow' in prop else column.flexGrow
+                        new_column = TableColumn(column.prop,
+                                                 new_template,
+                                                 hidden,
+                                                 column.filterable,
+                                                 flex_grow)
+                        self.__class__.outer_self.meta.table.columns[i] = new_column
+
+        def generate_actions(self):
+            self.__class__.outer_self.meta.actions.clear()
+
+            for action in self.__class__.outer_self.actions:
+                self.__class__.outer_self.meta.actions.append(action._asdict())
+
+        def generate_forms(self):
+            self.__class__.outer_self.meta.forms.clear()
+
+            for form in self.__class__.outer_self.forms:
+                self.__class__.outer_self.meta.forms.append(form.to_dict())
+
+        def set_permissions(self):
+            if self.__class__.outer_self.permissions:
+                self.outer_self.meta.permissions.extend(self.__class__.outer_self.permissions)
+        class_name = self.router.path.replace('/', '')
+        meta_class = type(f'{class_name}_CRUDClassMetadata',
+                          (RESTController,),
+                          {
+                              'list': _list,
+                              'update_columns': update_columns,
+                              'generate_actions': generate_actions,
+                              'generate_forms': generate_forms,
+                              'set_permissions': set_permissions,
+                              'outer_self': self,
+                          })
+        UIRouter(self.router.path, self.router.security_scope)(meta_class)
+        cls.CRUDClassMetadata = meta_class
index 0c65ce1a912d4889964010e3b7fe0eccfc9ca7ac..3562f33a227122c28d0c7f7158e0ec6f9ffd386a 100644 (file)
@@ -6,7 +6,8 @@ from ..exceptions import DashboardException
 from ..security import Scope
 from ..services.ceph_service import CephService, SendCommandError
 from . import APIDoc, APIRouter, CRUDCollectionMethod, CRUDEndpoint, EndpointDoc, SecretStr
-from ._crud import ArrayHorizontalContainer, Form, FormField, Icon, TableAction, VerticalContainer
+from ._crud import ArrayHorizontalContainer, CRUDMeta, Form, FormField, Icon, \
+    TableAction, VerticalContainer
 
 logger = logging.getLogger("controllers.ceph_users")
 
@@ -92,7 +93,8 @@ create_form = Form(path='/cluster/user/create',
     create=CRUDCollectionMethod(
         func=CephUserEndpoints.user_create,
         doc=EndpointDoc("Create Ceph User")
-    )
+    ),
+    meta=CRUDMeta()
 )
 class CephUser(NamedTuple):
     entity: str
index f120b4a5ee3e1744403584ab2ac73976d6c70081..710a4980f7cd3b08bd08250e20fd8b871ac9995a 100644 (file)
@@ -2,6 +2,7 @@
 
 import json
 import logging
+from typing import Any, Dict, List, NamedTuple, Optional, Union
 
 import cherrypy
 
@@ -12,15 +13,12 @@ from ..services.auth import AuthManager, JwtManager
 from ..services.ceph_service import CephService
 from ..services.rgw_client import NoRgwDaemonsException, RgwClient
 from ..tools import json_str_to_object, str_to_bool
-from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
-    ReadPermission, RESTController, UIRouter, allow_empty_body
+from . import APIDoc, APIRouter, BaseController, CRUDCollectionMethod, \
+    CRUDEndpoint, Endpoint, EndpointDoc, ReadPermission, RESTController, \
+    UIRouter, allow_empty_body
+from ._crud import CRUDMeta
 from ._version import APIVersion
 
-try:
-    from typing import Any, Dict, List, Optional, Union
-except ImportError:  # pragma: no cover
-    pass  # Just for type checking
-
 logger = logging.getLogger("controllers.rgw")
 
 RGW_SCHEMA = {
@@ -607,3 +605,37 @@ class RgwUser(RgwRESTController):
             'subuser': subuser,
             'purge-keys': purge_keys
         }, json_response=False)
+
+
+class RGWRoleEndpoints:
+    @staticmethod
+    def role_list(_):
+        rgw_client = RgwClient.admin_instance()
+        roles = rgw_client.list_roles()
+        return roles
+
+
+@CRUDEndpoint(
+    router=APIRouter('/rgw/user/roles', Scope.RGW),
+    doc=APIDoc("List of RGW roles", "RGW"),
+    actions=[],
+    permissions=[Scope.CONFIG_OPT],
+    get_all=CRUDCollectionMethod(
+        func=RGWRoleEndpoints.role_list,
+        doc=EndpointDoc("List RGW roles")
+    ),
+    set_column={
+        "CreateDate": {'cellTemplate': 'date'},
+        "MaxSessionDuration": {'cellTemplate': 'duration'},
+        "AssumeRolePolicyDocument": {'isHidden': True}
+    },
+    meta=CRUDMeta()
+)
+class RgwUserRole(NamedTuple):
+    RoleId: int
+    RoleName: str
+    Path: str
+    Arn: str
+    CreateDate: str
+    MaxSessionDuration: int
+    AssumeRolePolicyDocument: str
index 6c6d7677ec70d2948f362eabb61f3595278bd90e..8f50e4abcb241077c3a62ac71bcbd41b658a742a 100644 (file)
@@ -1,3 +1,5 @@
+<cd-rgw-user-tabs></cd-rgw-user-tabs>
+
 <cd-table #table
           [autoReload]="false"
           [data]="users"
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.html
new file mode 100644 (file)
index 0000000..2130d3c
--- /dev/null
@@ -0,0 +1,18 @@
+<ul class="nav nav-tabs">
+  <li class="nav-item">
+    <a class="nav-link"
+       routerLink="/rgw/user"
+       routerLinkActive="active"
+       ariaCurrentWhenActive="page"
+       [routerLinkActiveOptions]="{exact: true}"
+       i18n>Users</a>
+  </li>
+  <li class="nav-item">
+    <a class="nav-link"
+       routerLink="/rgw/user/roles"
+       routerLinkActive="active"
+       ariaCurrentWhenActive="page"
+       [routerLinkActiveOptions]="{exact: true}"
+       i18n>Roles</a>
+  </li>
+</ul>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.spec.ts
new file mode 100644 (file)
index 0000000..3b81e71
--- /dev/null
@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwUserTabsComponent } from './rgw-user-tabs.component';
+
+describe('RgwUserTabsComponent', () => {
+  let component: RgwUserTabsComponent;
+  let fixture: ComponentFixture<RgwUserTabsComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [RgwUserTabsComponent]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwUserTabsComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-tabs/rgw-user-tabs.component.ts
new file mode 100644 (file)
index 0000000..95625ca
--- /dev/null
@@ -0,0 +1,8 @@
+import { Component } from '@angular/core';
+
+@Component({
+  selector: 'cd-rgw-user-tabs',
+  templateUrl: './rgw-user-tabs.component.html',
+  styleUrls: ['./rgw-user-tabs.component.scss']
+})
+export class RgwUserTabsComponent {}
index 7ecaddddfef08f3946443fcbadea8ddaf9e4604f..edffa8e856b756a7c9a96df8f3d9102474bcfe70 100644 (file)
@@ -7,6 +7,7 @@ import { NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
 import { NgxPipeFunctionModule } from 'ngx-pipe-function';
 
 import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
+import { CRUDTableComponent } from '~/app/shared/datatable/crud-table/crud-table.component';
 import { SharedModule } from '~/app/shared/shared.module';
 import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
 import { RgwBucketDetailsComponent } from './rgw-bucket-details/rgw-bucket-details.component';
@@ -22,6 +23,7 @@ import { RgwUserListComponent } from './rgw-user-list/rgw-user-list.component';
 import { RgwUserS3KeyModalComponent } from './rgw-user-s3-key-modal/rgw-user-s3-key-modal.component';
 import { RgwUserSubuserModalComponent } from './rgw-user-subuser-modal/rgw-user-subuser-modal.component';
 import { RgwUserSwiftKeyModalComponent } from './rgw-user-swift-key-modal/rgw-user-swift-key-modal.component';
+import { RgwUserTabsComponent } from './rgw-user-tabs/rgw-user-tabs.component';
 
 @NgModule({
   imports: [
@@ -58,7 +60,8 @@ import { RgwUserSwiftKeyModalComponent } from './rgw-user-swift-key-modal/rgw-us
     RgwUserS3KeyModalComponent,
     RgwUserCapabilityModalComponent,
     RgwUserSubuserModalComponent,
-    RgwConfigModalComponent
+    RgwConfigModalComponent,
+    RgwUserTabsComponent
   ]
 })
 export class RgwModule {}
@@ -82,6 +85,24 @@ const routes: Routes = [
         path: `${URLVerbs.EDIT}/:uid`,
         component: RgwUserFormComponent,
         data: { breadcrumbs: ActionLabels.EDIT }
+      },
+      {
+        path: 'roles',
+        component: CRUDTableComponent,
+        data: {
+          breadcrumbs: 'Roles',
+          resource: 'api.rgw.user.roles@1.0',
+          tabs: [
+            {
+              name: 'Users',
+              url: '/rgw/user'
+            },
+            {
+              name: 'Roles',
+              url: '/rgw/user/roles'
+            }
+          ]
+        }
       }
     ]
   },
index 87a3c586b4a8544ceb9a560f86832db905b3a57b..b9b4ae62b34ddf84c508c59a5e1e2e422d98c8b8 100644 (file)
@@ -1,3 +1,16 @@
+<ul class="nav nav-tabs"
+    *ngIf="tabs">
+  <li class="nav-item"
+      *ngFor="let tab of tabs; keyvalue">
+    <a class="nav-link"
+       [routerLink]="tab.url"
+       routerLinkActive="active"
+       ariaCurrentWhenActive="page"
+       [routerLinkActiveOptions]="{exact: true}"
+       i18n>{{tab.name}}</a>
+  </li>
+</ul>
+
 <ng-container *ngIf="meta">
   <cd-table
       [data]="data$ | async"
     <ng-container *ngIf="!isLast">&nbsp;</ng-container>
   </span>
 </ng-template>
+
+<ng-template #dateTpl
+             let-value="value">
+  <span>{{ value | cdDate }}</span>
+</ng-template>
+
+<ng-template #durationTpl
+             let-value="value">
+  <span>{{ value | duration }}</span>
+</ng-template>
index 7e82c523e78d91e3119f2b0f96809c457ad61572..96346ea7a1b7e8f567b51e99df706ae82669ba2a 100644 (file)
@@ -19,6 +19,10 @@ import { AuthStorageService } from '../../services/auth-storage.service';
 export class CRUDTableComponent implements OnInit {
   @ViewChild('badgeDictTpl')
   public badgeDictTpl: TemplateRef<any>;
+  @ViewChild('dateTpl')
+  public dateTpl: TemplateRef<any>;
+  @ViewChild('durationTpl')
+  public durationTpl: TemplateRef<any>;
 
   data$: Observable<any>;
   meta$: Observable<CrudMetadata>;
@@ -26,6 +30,7 @@ export class CRUDTableComponent implements OnInit {
   permissions: Permissions;
   permission: Permission;
   selection = new CdTableSelection();
+  tabs = {};
 
   constructor(
     private authStorageService: AuthStorageService,
@@ -43,9 +48,10 @@ export class CRUDTableComponent implements OnInit {
     */
     this.activatedRoute.data.subscribe((data: any) => {
       const resource: string = data.resource;
+      this.tabs = data.tabs;
       this.dataGatewayService
         .list(`ui-${resource}`)
-        .subscribe((response: any) => this.processMeta(response));
+        .subscribe((response: CrudMetadata) => this.processMeta(response));
       this.data$ = this.timerService.get(() => this.dataGatewayService.list(resource));
     });
   }
@@ -62,15 +68,22 @@ export class CRUDTableComponent implements OnInit {
           ''
         );
     this.permission = this.permissions[toCamelCase(meta.permissions[0])];
-    this.meta = meta;
     const templates = {
-      badgeDict: this.badgeDictTpl
+      badgeDict: this.badgeDictTpl,
+      date: this.dateTpl,
+      duration: this.durationTpl
     };
-    this.meta.table.columns.forEach((element, index) => {
+    meta.table.columns.forEach((element, index) => {
       if (element['cellTemplate'] !== undefined) {
-        this.meta.table.columns[index]['cellTemplate'] =
-          templates[element['cellTemplate'] as string];
+        meta.table.columns[index]['cellTemplate'] = templates[element['cellTemplate'] as string];
       }
     });
+    // isHidden flag does not work as expected somehow so the best ways to enforce isHidden is
+    // to filter the columns manually instead of letting isHidden flag inside table.component to
+    // work.
+    meta.table.columns = meta.table.columns.filter((col: any) => {
+      return !col['isHidden'];
+    });
+    this.meta = meta;
   }
 }
index d607cfb066884749355ab26e9f0366930f5eb799..16143a8794cb9c05b256853dcef85965db7c52fe 100644 (file)
@@ -8269,6 +8269,29 @@ paths:
       - jwt: []
       tags:
       - RgwUser
+  /api/rgw/user/roles:
+    get:
+      parameters: []
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: OK
+        '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: List RGW roles
+      tags:
+      - RGW
   /api/rgw/user/{uid}:
     delete:
       parameters:
@@ -10787,6 +10810,8 @@ tags:
   name: Prometheus
 - description: Prometheus Notifications Management API
   name: PrometheusNotifications
+- description: List of RGW roles
+  name: RGW
 - description: RBD Management API
   name: Rbd
 - description: RBD Mirroring Management API
index 5ef32044d6c6f6a7493d7791f69851d482e8d520..bf65dd2efe46153f7e5d6f7d9da1182a4fc785d5 100644 (file)
@@ -787,6 +787,15 @@ class RgwClient(RestClient):
         except RequestException as e:
             raise DashboardException(msg=str(e), component='rgw')
 
+    def list_roles(self) -> List[Dict[str, Any]]:
+        rgw_list_roles_command = ['role', 'list']
+        code, roles, err = mgr.send_rgwadmin_command(rgw_list_roles_command)
+        if code < 0:
+            logger.warning('Error listing roles with code %d: %s', code, err)
+            return []
+
+        return roles
+
     def perform_validations(self, retention_period_days, retention_period_years, mode):
         try:
             retention_period_days = int(retention_period_days) if retention_period_days else 0