]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: align RGW role management with Carbon and fix API routing 69393/head
authorSagar Gopale <sagar.gopale@ibm.com>
Wed, 10 Jun 2026 13:29:50 +0000 (18:59 +0530)
committerSagar Gopale <sagar.gopale@ibm.com>
Thu, 18 Jun 2026 13:00:47 +0000 (18:30 +0530)
Fixes: https://tracker.ceph.com/issues/77328
Signed-off-by: Sagar Gopale <sagar.gopale@ibm.com>
19 files changed:
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/accounts.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-role.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-role.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-role.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py

index 9e06597f1713751606c7d2654c39f365c4507ba4..a0ef6965771f394b3da2fe16436608fecba28890 100755 (executable)
@@ -1230,49 +1230,52 @@ class RgwUser(RgwRESTController):
 
 class RGWRoleEndpoints:
     @staticmethod
-    def role_list(_):
+    def role_list(_, account_id: Optional[str] = None):
         rgw_client = RgwClient.admin_instance()
-        roles = rgw_client.list_roles()
+        roles = rgw_client.list_roles(account_id)
         return roles
 
     @staticmethod
-    def role_create(_, role_name: str = '', role_path: str = '', role_assume_policy_doc: str = ''):
+    def role_create(_, role_name: str = '', role_path: str = '', role_assume_policy_doc: str = '',
+                    account_id: Optional[str] = None):
         assert role_name
         assert role_path
         rgw_client = RgwClient.admin_instance()
-        rgw_client.create_role(role_name, role_path, role_assume_policy_doc)
+        rgw_client.create_role(role_name, role_path, role_assume_policy_doc, account_id)
         return f'Role {role_name} created successfully'
 
     @staticmethod
-    def role_update(_, role_name: str, max_session_duration: str):
+    def role_update(_, role_name: str, max_session_duration: str, account_id: Optional[str] = None):
         assert role_name
         assert max_session_duration
         # convert max_session_duration which is in hours to seconds
         max_session_duration = int(float(max_session_duration) * 3600)
         rgw_client = RgwClient.admin_instance()
-        rgw_client.update_role(role_name, str(max_session_duration))
+        rgw_client.update_role(role_name, str(max_session_duration), account_id)
         return f'Role {role_name} updated successfully'
 
     @staticmethod
-    def role_delete(_, role_name: str):
+    def role_delete(_, role_name: str, account_id: Optional[str] = None):
         assert role_name
         rgw_client = RgwClient.admin_instance()
-        rgw_client.delete_role(role_name)
+        rgw_client.delete_role(role_name, account_id)
         return f'Role {role_name} deleted successfully'
 
     @staticmethod
-    def model(role_name: str):
+    def get(_, role_name: str, account_id: Optional[str] = None):
         assert role_name
         rgw_client = RgwClient.admin_instance()
-        role = rgw_client.get_role(role_name)
-        model = {'role_name': '', 'max_session_duration': ''}
-        model['role_name'] = role['RoleName']
-
+        role = rgw_client.get_role(role_name, account_id)
+        model = {'role_name': role['RoleName'], 'max_session_duration': ''}
         # convert maxsessionduration which is in seconds to hours
         if role['MaxSessionDuration']:
             model['max_session_duration'] = role['MaxSessionDuration'] / 3600
         return model
 
+    @staticmethod
+    def model(role_name: str, account_id: Optional[str] = None):
+        return RGWRoleEndpoints.get(None, role_name, account_id)
+
 
 # pylint: disable=C0301
 assume_role_policy_help = (
@@ -1317,7 +1320,7 @@ edit_role_form = Form(path='/edit',
 
 
 @CRUDEndpoint(
-    router=APIRouter('/rgw/roles', Scope.RGW),
+    router=APIRouter('/rgw/accounts/{account_id}/roles', Scope.RGW),
     doc=APIDoc("List of RGW roles", "RGW"),
     actions=[
         TableAction(name='Create', permission='create', icon=Icon.ADD.value,
@@ -1335,6 +1338,12 @@ edit_role_form = Form(path='/edit',
         func=RGWRoleEndpoints.role_list,
         doc=EndpointDoc("List RGW roles")
     ),
+    extra_endpoints=[
+        ('get', CRUDCollectionMethod(
+            func=RGWRoleEndpoints.get,
+            doc=EndpointDoc("Get RGW role")
+        ))
+    ],
     create=CRUDCollectionMethod(
         func=RGWRoleEndpoints.role_create,
         doc=EndpointDoc("Create RGW role")
index 215507045a5d2d065645f38d194f9430db6da387..f58932982fd0ca3669a2f1d666d25d6828671e4c 100644 (file)
@@ -53,7 +53,9 @@ export class AccountsPageHelper extends PageHelper {
 
     this.getFirstTableCell(account.name).should('have.text', account.name);
     this.getTableRow(account.name).within(() => {
-      cy.get('td').eq(this.columnIndex.tenant).should('have.text', account.tenant);
+      cy.get('td')
+        .eq(this.columnIndex.tenant)
+        .should('have.text', account.tenant || '');
       cy.get('td').eq(this.columnIndex.account_id).should('not.be.empty');
       cy.get('td').eq(this.columnIndex.email).should('have.text', account.email);
       cy.get('td').eq(this.columnIndex.max_users).should('have.text', 1000);
index 870317503858a6d907f24c64269dc5dfed02f84b..2026d0dc589bc0dd1b653d682832516f246c740d 100644 (file)
@@ -1,20 +1,36 @@
 import { RolesPageHelper } from './roles.po';
+import { AccountsPageHelper } from './accounts.po';
 
 describe('RGW roles page', () => {
   const roles = new RolesPageHelper();
+  const accounts = new AccountsPageHelper();
+  const accountName = 'roles-test-account';
+  const roleName = 'testRole';
+
+  before(() => {
+    cy.login();
+    accounts.navigateTo('create');
+    accounts.create({ name: accountName, email: 'test@example.com' });
+  });
+
+  after(() => {
+    cy.login();
+    accounts.navigateTo();
+    accounts.delete(accountName, null, null, true, false, false, false);
+  });
 
   beforeEach(() => {
     cy.login();
-    roles.navigateTo();
+    accounts.navigateTo();
+    accounts.getExpandCollapseElement(accountName).click();
+    cy.contains('cds-tab-headers button[role="tab"]', 'Roles').click();
+    // Wait for the roles list to render
+    cy.get('cd-rgw-account-roles-list').should('exist');
   });
 
   describe('Create, Edit & Delete rgw roles', () => {
-    const roleName = 'testRole';
-
-    it('should create rgw roles', () => {
-      roles.navigateTo('create');
+    it('should create rgw role', () => {
       roles.create(roleName, '/', '{}');
-      roles.navigateTo();
       roles.checkExist(roleName, true);
     });
 
@@ -23,7 +39,8 @@ describe('RGW roles page', () => {
     });
 
     it('should delete rgw role', () => {
-      roles.delete(roleName, null, null, true);
+      roles.deleteRole(roleName);
+      roles.checkExist(roleName, false);
     });
   });
 });
index 920a9d1d1106fef8b48f4839e5ba1c281dba283e..611a060236e791aab6eab44619a407400b9a1254 100644 (file)
@@ -1,55 +1,88 @@
 import { PageHelper } from '../page-helper.po';
 
-const pages = {
-  index: { url: '#/rgw/roles', id: 'cd-crud-table' },
-  create: { url: '#/rgw/roles/create', id: 'cd-crud-form' }
-};
-
 export class RolesPageHelper extends PageHelper {
-  pages = pages;
+  pages = {};
 
   columnIndex = {
-    roleName: 2,
-    path: 3,
-    arn: 4,
-    createDate: 5,
-    maxSessionDuration: 6
+    roleName: 1,
+    path: 2,
+    arn: 3,
+    createDate: 4,
+    maxSessionDuration: 5
   };
 
-  @PageHelper.restrictTo(pages.create.url)
   create(name: string, path: string, policyDocument: string) {
-    cy.get('[id$="string_role_name_0"]').type(name);
-    cy.get('[id$="role_assume_policy_doc_2"]').type(policyDocument);
-    cy.get('[id$="role_path_1"]').type(path);
-    cy.get("[aria-label='Create Role']").should('exist').click();
-    cy.get('cd-crud-table').should('exist');
+    cy.get('cd-rgw-account-roles-list cd-table-actions button[aria-label="Create"]')
+      .should('exist')
+      .click();
+    cy.get('cds-modal').should('be.visible');
+    cy.get('#role_name').type(name);
+    cy.get('#role_assume_policy_doc')
+      .clear()
+      .type(policyDocument, { parseSpecialCharSequences: false, delay: 0 });
+    cy.get('#role_path').type(path);
+    cy.get('cds-modal').contains('button', 'Create').click();
+    cy.get('cds-modal').should('not.exist');
   }
 
   edit(name: string, maxSessionDuration: number) {
-    this.navigateEdit(name);
-    cy.get('[id$="max_session_duration_1"]').clear().type(maxSessionDuration.toString());
-    cy.get("[aria-label='Edit Role']").should('exist').click();
-    cy.get('cd-crud-table').should('exist');
+    this.getRolesTableCell(this.columnIndex.roleName, name).click();
+    this.getRolesTableCell(this.columnIndex.roleName, name)
+      .parent('tr')
+      .find('[data-testid="table-action-btn"]')
+      .should('exist')
+      .click();
+    cy.get('cds-overflow-menu-option[aria-label="Edit"]').should('exist').click();
+    cy.get('cds-modal').should('be.visible');
+
+    cy.get('cds-number[formControlName="max_session_duration"] input')
+      .clear()
+      .type(maxSessionDuration.toString());
+    cy.get('cds-modal').contains('button', 'Edit').click();
+    cy.get('cds-modal').should('not.exist');
 
-    this.getTableCell(this.columnIndex.roleName, name)
-      .click()
+    this.getRolesTableCell(this.columnIndex.roleName, name)
       .parent()
-      .find(`[cdstabledata]:nth-child(${this.columnIndex.maxSessionDuration})`)
+      .find(`td:nth-child(${this.columnIndex.maxSessionDuration})`)
       .should(($elements) => {
         const roleName = $elements.map((_, el) => el.textContent).get();
         expect(roleName).to.include(`${maxSessionDuration} hours`);
       });
   }
 
-  @PageHelper.restrictTo(pages.index.url)
-  checkExist(name: string, exist: boolean) {
-    this.getTableCell(this.columnIndex.roleName, name).should(($elements) => {
-      const roleName = $elements.map((_, el) => el.textContent).get();
-      if (exist) {
-        expect(roleName).to.include(name);
-      } else {
-        expect(roleName).to.not.include(name);
-      }
+  deleteRole(name: string) {
+    this.getRolesTableCell(this.columnIndex.roleName, name).click();
+    this.getRolesTableCell(this.columnIndex.roleName, name)
+      .parent('tr')
+      .find('[data-testid="table-action-btn"]')
+      .should('exist')
+      .click();
+    cy.get('cds-overflow-menu-option[aria-label="Delete"]').should('exist').click();
+    cy.get('cds-modal').should('be.visible');
+    cy.get('cds-modal [aria-label="confirmation"]').click({ force: true });
+    cy.get('cds-modal').contains('button', 'Delete Role').click();
+    cy.get('cds-modal').should('not.exist');
+  }
+
+  private getRolesTableCell(columnIndex: number, exactContent: string, partialMatch = false) {
+    cy.get('cd-rgw-account-roles-list').within(() => {
+      cy.get('.cds--search-close').first().click({ force: true });
+      cy.get('.cds--search-input').first().clear({ force: true }).type(exactContent, { delay: 35 });
     });
+    const selector = `tbody tr td:nth-child(${columnIndex})`;
+    if (partialMatch) {
+      return cy.get('cd-rgw-account-roles-list').contains(selector, exactContent);
+    }
+    return cy
+      .get('cd-rgw-account-roles-list')
+      .contains(selector, new RegExp(`^\\s*${exactContent}\\s*$`, 'i'));
+  }
+
+  checkExist(name: string, exist: boolean) {
+    if (exist) {
+      this.getRolesTableCell(this.columnIndex.roleName, name).should('exist');
+    } else {
+      cy.get('cd-rgw-account-roles-list').contains(name).should('not.exist');
+    }
   }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-role.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-role.ts
new file mode 100644 (file)
index 0000000..8522e8f
--- /dev/null
@@ -0,0 +1,22 @@
+export interface RgwRole {
+  RoleId: string;
+  RoleName: string;
+  Path: string;
+  Arn: string;
+  CreateDate: string;
+  MaxSessionDuration: number;
+  AssumeRolePolicyDocument: string;
+}
+
+export interface RgwRoleCreatePayload {
+  role_name: string;
+  role_path: string;
+  role_assume_policy_doc: string;
+  account_id: string;
+}
+
+export interface RgwRoleUpdatePayload {
+  role_name: string;
+  max_session_duration: number;
+  account_id: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.html
new file mode 100644 (file)
index 0000000..11fff87
--- /dev/null
@@ -0,0 +1,136 @@
+<cds-modal size="md" [open]="open" [hasScrollingContent]="false" (overlaySelected)="closeModal()">
+  <cds-modal-header (closeSelect)="closeModal()">
+    <h4 cdsModalHeaderLabel class="cds--type-label-01">{{ accountName }}</h4>
+    <h3 cdsModalHeaderHeading i18n>{{ mode }} role</h3>
+  </cds-modal-header>
+
+  <form class="form" #formDir="ngForm" [formGroup]="form">
+    <div cdsModalContent>
+      <!-- Role Name -->
+      <div class="form-item">
+        <cds-text-label
+          label="Role name"
+          for="role_name"
+          cdRequiredField="Role name"
+          [invalid]="roleName.isInvalid"
+          [invalidText]="roleNameError"
+          >Role name
+          <input
+            cdsText
+            cdValidate
+            #roleName="cdValidate"
+            type="text"
+            id="role_name"
+            name="role_name"
+            formControlName="role_name"
+            placeholder="Placeholder text"
+            [invalid]="roleName.isInvalid"
+            [autofocus]="true"
+            modal-primary-focus
+          />
+        </cds-text-label>
+        <ng-template #roleNameError>
+          @if (form.showError('role_name', formDir, 'required')) {
+          <span class="invalid-feedback" i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+
+      <!-- Path & Assume Role Policy Document (Create only) -->
+      @if (!isEdit) {
+      <!-- Path -->
+      <div class="form-item">
+        <cds-text-label
+          label="Path"
+          for="role_path"
+          cdRequiredField="Path"
+          [invalid]="rolePath.isInvalid"
+          [invalidText]="pathError"
+          >Path
+          <input
+            cdsText
+            cdValidate
+            #rolePath="cdValidate"
+            type="text"
+            id="role_path"
+            name="role_path"
+            formControlName="role_path"
+            placeholder="Placeholder text"
+            [invalid]="rolePath.isInvalid"
+          />
+        </cds-text-label>
+        <ng-template #pathError>
+          @if (form.showError('role_path', formDir, 'required')) {
+          <span class="invalid-feedback" i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+
+      <!-- Assume Role Policy Document -->
+      <div class="form-item">
+        <cds-textarea-label
+          labelInputID="role_assume_policy_doc"
+          [invalid]="policyDoc.isInvalid"
+          [invalidText]="policyError"
+          cdRequiredField="Trust Policy"
+          >Trust policy
+          <textarea
+            cdsTextArea
+            cdValidate
+            #policyDoc="cdValidate"
+            id="role_assume_policy_doc"
+            name="role_assume_policy_doc"
+            formControlName="role_assume_policy_doc"
+            placeholder="Placeholder text"
+            rows="8"
+            cols="200"
+            [invalid]="policyDoc.isInvalid"
+            [invalidText]="policyError"
+          ></textarea>
+        </cds-textarea-label>
+        <ng-template #policyError>
+          @if (form.showError('role_assume_policy_doc', formDir, 'required')) {
+          <span class="invalid-feedback" i18n>This field is required.</span>
+          } @if (form.showError('role_assume_policy_doc', formDir, 'json')) {
+          <span class="invalid-feedback" i18n>Must be a valid JSON.</span>
+          }
+        </ng-template>
+      </div>
+      }
+
+      <!-- Max Session Duration (Edit only) -->
+      @if (isEdit) {
+      <div class="form-item">
+        <cds-number
+          label="Max Session Duration (Hours)"
+          cdValidate
+          #maxSessionDuration="cdValidate"
+          [invalid]="maxSessionDuration.isInvalid"
+          [invalidText]="durationError"
+          formControlName="max_session_duration"
+          min="1"
+          max="12"
+          i18n-label
+        >
+        </cds-number>
+        <ng-template #durationError>
+          @if (form.showError('max_session_duration', formDir, 'required')) {
+          <span class="invalid-feedback" i18n>This field is required.</span>
+          } @if (form.showError('max_session_duration', formDir, 'min')) {
+          <span class="invalid-feedback" i18n>Minimum value is 1 hour.</span>
+          } @if (form.showError('max_session_duration', formDir, 'max')) {
+          <span class="invalid-feedback" i18n>Maximum value is 12 hours.</span>
+          }
+        </ng-template>
+      </div>
+      }
+    </div>
+
+    <cd-form-button-panel
+      (submitActionEvent)="onSubmit()"
+      [form]="form"
+      [submitText]="mode"
+      [modalForm]="true"
+    ></cd-form-button-panel>
+  </form>
+</cds-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.spec.ts
new file mode 100644 (file)
index 0000000..e2f6b2f
--- /dev/null
@@ -0,0 +1,51 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ReactiveFormsModule } from '@angular/forms';
+
+import { RgwAccountRoleFormComponent } from './rgw-account-role-form.component';
+import { SharedModule } from '~/app/shared/shared.module';
+
+describe('RgwAccountRoleFormComponent', () => {
+  let component: RgwAccountRoleFormComponent;
+  let fixture: ComponentFixture<RgwAccountRoleFormComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [HttpClientTestingModule, RouterTestingModule, SharedModule, ReactiveFormsModule],
+      declarations: [RgwAccountRoleFormComponent]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(RgwAccountRoleFormComponent);
+    component = fixture.componentInstance;
+    component.accountId = 'test-account';
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should create form correctly on init', () => {
+    expect(component.form).toBeDefined();
+    expect(component.form.contains('role_name')).toBeTruthy();
+    expect(component.form.contains('role_path')).toBeTruthy();
+    expect(component.form.contains('role_assume_policy_doc')).toBeTruthy();
+  });
+
+  it('should patch value in edit mode', () => {
+    component.isEdit = true;
+    component.roleName = 'test-role';
+    component.role = {
+      RoleName: 'test-role',
+      Path: '/path',
+      MaxSessionDuration: 3 * 3600
+    } as any;
+
+    component.ngOnInit();
+
+    expect(component.form.get('role_name').value).toBe('test-role');
+    expect(component.form.get('max_session_duration').value).toBe(3);
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.ts
new file mode 100644 (file)
index 0000000..8cd266c
--- /dev/null
@@ -0,0 +1,114 @@
+import { Component, Inject, OnInit, Optional } from '@angular/core';
+import { Validators } from '@angular/forms';
+import { BaseModal } from 'carbon-components-angular';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { RgwRoleService } from '~/app/shared/api/rgw-role.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { RgwRole } from '../models/rgw-role';
+
+@Component({
+  selector: 'cd-rgw-account-role-form',
+  templateUrl: './rgw-account-role-form.component.html',
+  styleUrls: ['./rgw-account-role-form.component.scss'],
+  standalone: false
+})
+export class RgwAccountRoleFormComponent extends BaseModal implements OnInit {
+  form: CdFormGroup;
+  mode: string;
+
+  constructor(
+    @Optional() @Inject('accountId') public accountId: string,
+    @Optional() @Inject('accountName') public accountName: string,
+    @Optional() @Inject('roleName') public roleName: string,
+    @Optional() @Inject('isEdit') public isEdit = false,
+    @Optional() @Inject('role') public role: RgwRole = null,
+    private formBuilder: CdFormBuilder,
+    public actionLabels: ActionLabelsI18n,
+    private rgwRoleService: RgwRoleService,
+    private notificationService: NotificationService
+  ) {
+    super();
+  }
+
+  ngOnInit(): void {
+    this.mode = this.isEdit ? this.actionLabels.EDIT : this.actionLabels.CREATE;
+    this.createForm();
+    if (this.isEdit && this.role) {
+      this.form.patchValue({
+        role_name: this.role.RoleName,
+        role_path: this.role.Path || '/',
+        max_session_duration: this.role.MaxSessionDuration ? this.role.MaxSessionDuration / 3600 : 1
+      });
+    }
+  }
+
+  private createForm() {
+    this.form = this.formBuilder.group({
+      role_name: [{ value: '', disabled: this.isEdit }, [Validators.required]],
+      role_path: [{ value: '', disabled: this.isEdit }, [Validators.required]],
+      role_assume_policy_doc: [''],
+      max_session_duration: [1]
+    });
+
+    CdValidators.validateIf(this.form.get('role_assume_policy_doc'), () => !this.isEdit, [
+      Validators.required,
+      CdValidators.json()
+    ]);
+
+    CdValidators.validateIf(this.form.get('max_session_duration'), () => this.isEdit, [
+      Validators.required,
+      Validators.min(1),
+      Validators.max(12)
+    ]);
+  }
+
+  onSubmit() {
+    if (this.form.invalid) {
+      return;
+    }
+
+    const payload = this.form.getRawValue();
+    payload.account_id = this.accountId;
+    if (!this.isEdit) {
+      delete payload.max_session_duration;
+    }
+
+    if (this.isEdit) {
+      this.rgwRoleService
+        .update(this.roleName, {
+          role_name: this.roleName,
+          max_session_duration: payload.max_session_duration,
+          account_id: this.accountId
+        })
+        .subscribe({
+          next: () => {
+            this.notificationService.show(
+              NotificationType.success,
+              $localize`Role updated successfully`
+            );
+            this.closeModal();
+          },
+          error: () => {
+            this.form.setErrors({ cdSubmitButton: true });
+          }
+        });
+    } else {
+      this.rgwRoleService.create(payload).subscribe({
+        next: () => {
+          this.notificationService.show(
+            NotificationType.success,
+            $localize`Role created successfully`
+          );
+          this.closeModal();
+        },
+        error: () => {
+          this.form.setErrors({ cdSubmitButton: true });
+        }
+      });
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.html
new file mode 100644 (file)
index 0000000..209f15a
--- /dev/null
@@ -0,0 +1,14 @@
+<cd-table #table
+          [autoReload]="true"
+          [data]="data$ | async"
+          [columns]="columns"
+          selectionType="single"
+          columnMode="flex"
+          (updateSelection)="updateSelection($event)"
+          identifier="RoleName">
+  <cd-table-actions class="table-actions"
+                    [permission]="permission"
+                    [selection]="selection"
+                    [tableActions]="tableActions">
+  </cd-table-actions>
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.scss
new file mode 100644 (file)
index 0000000..dfca32b
--- /dev/null
@@ -0,0 +1 @@
+/* Scss stylesheet for RGW account roles list */
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.spec.ts
new file mode 100644 (file)
index 0000000..6beb2fa
--- /dev/null
@@ -0,0 +1,71 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { of } from 'rxjs';
+
+import { RgwAccountRolesListComponent } from './rgw-account-roles-list.component';
+import { RgwRoleService } from '~/app/shared/api/rgw-role.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
+
+describe('RgwAccountRolesListComponent', () => {
+  let component: RgwAccountRolesListComponent;
+  let fixture: ComponentFixture<RgwAccountRolesListComponent>;
+  let rgwRoleService: RgwRoleService;
+  let notificationService: NotificationService;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [HttpClientTestingModule, RouterTestingModule, SharedModule],
+      declarations: [RgwAccountRolesListComponent],
+      providers: [
+        {
+          provide: AuthStorageService,
+          useValue: {
+            getPermissions: () => ({ rgw: { create: true, update: true, delete: true } })
+          }
+        }
+      ]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(RgwAccountRolesListComponent);
+    component = fixture.componentInstance;
+    rgwRoleService = TestBed.inject(RgwRoleService);
+    notificationService = TestBed.inject(NotificationService);
+    spyOn(notificationService, 'show');
+    component.accountId = 'test-account';
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should load roles on init', () => {
+    const roles = [{ RoleName: 'test-role' }];
+    spyOn(rgwRoleService, 'list').and.returnValue(of(roles));
+    component.loadRoles();
+    expect(rgwRoleService.list).toHaveBeenCalledWith('test-account');
+    component.data$.subscribe((res) => {
+      expect(res).toEqual(roles);
+    });
+  });
+
+  it('should delete a role and show notification', () => {
+    spyOn(rgwRoleService, 'delete').and.returnValue(of(null));
+    spyOn(component, 'loadRoles');
+    component.selection.selected = [{ RoleName: 'test-role' }];
+    spyOn(TestBed.inject(ModalCdsService), 'show').and.callFake((_componentClass, config) => {
+      config.submitActionObservable().subscribe();
+      return null;
+    });
+
+    component.deleteRole();
+    expect(rgwRoleService.delete).toHaveBeenCalledWith('test-role', 'test-account');
+    expect(notificationService.show).toHaveBeenCalled();
+    expect(component.loadRoles).toHaveBeenCalled();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.ts
new file mode 100644 (file)
index 0000000..af1c13f
--- /dev/null
@@ -0,0 +1,167 @@
+import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
+import { RgwRoleService } from '~/app/shared/api/rgw-role.service';
+import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
+import { RgwAccountRoleFormComponent } from '../rgw-account-role-form/rgw-account-role-form.component';
+import { Observable, Subscriber, of } from 'rxjs';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
+import { DurationPipe } from '~/app/shared/pipes/duration.pipe';
+import { RgwRole } from '../models/rgw-role';
+
+@Component({
+  selector: 'cd-rgw-account-roles-list',
+  templateUrl: './rgw-account-roles-list.component.html',
+  styleUrls: ['./rgw-account-roles-list.component.scss'],
+  standalone: false
+})
+export class RgwAccountRolesListComponent implements OnInit, OnChanges {
+  @Input()
+  accountId: string;
+
+  @Input()
+  accountName: string;
+
+  @ViewChild('table')
+  table: TableComponent;
+
+  columns: CdTableColumn[] = [];
+  data$: Observable<RgwRole[]>;
+  tableActions: CdTableAction[] = [];
+  selection: CdTableSelection = new CdTableSelection();
+  permission: Permission;
+
+  constructor(
+    public actionLabels: ActionLabelsI18n,
+    private rgwRoleService: RgwRoleService,
+    private modalService: ModalCdsService,
+    private authStorageService: AuthStorageService,
+    private cdDatePipe: CdDatePipe,
+    private durationPipe: DurationPipe,
+    private notificationService: NotificationService
+  ) {
+    this.permission = this.authStorageService.getPermissions().rgw;
+  }
+
+  ngOnInit(): void {
+    this.loadRoles();
+    this.columns = [
+      {
+        name: $localize`Role name`,
+        prop: 'RoleName',
+        flexGrow: 2
+      },
+      {
+        name: $localize`Path`,
+        prop: 'Path',
+        flexGrow: 2
+      },
+      {
+        name: $localize`Arn`,
+        prop: 'Arn',
+        flexGrow: 3
+      },
+      {
+        name: $localize`Created at`,
+        prop: 'CreateDate',
+        flexGrow: 2,
+        pipe: this.cdDatePipe
+      },
+      {
+        name: $localize`Max session duration`,
+        prop: 'MaxSessionDuration',
+        flexGrow: 2,
+        pipe: this.durationPipe
+      }
+    ];
+
+    this.tableActions = [
+      {
+        permission: 'create',
+        icon: Icons.add,
+        click: () => this.openRoleForm(false),
+        name: this.actionLabels.CREATE,
+        canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+      },
+      {
+        permission: 'update',
+        icon: Icons.edit,
+        click: () => this.openRoleForm(true),
+        name: this.actionLabels.EDIT
+      },
+      {
+        permission: 'delete',
+        icon: Icons.destroy,
+        click: () => this.deleteRole(),
+        name: this.actionLabels.DELETE,
+        disable: () => !this.selection.hasSelection
+      }
+    ];
+  }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.accountId) {
+      this.loadRoles();
+    }
+  }
+
+  loadRoles(): void {
+    if (!this.accountId) {
+      this.data$ = of([]);
+      return;
+    }
+    this.data$ = this.rgwRoleService.list(this.accountId);
+  }
+
+  updateSelection(selection: CdTableSelection): void {
+    this.selection = selection;
+  }
+
+  openRoleForm(isEdit: boolean): void {
+    const role = isEdit ? this.selection.first() : null;
+    const modalRef = this.modalService.show(RgwAccountRoleFormComponent, {
+      accountId: this.accountId,
+      accountName: this.accountName,
+      roleName: role ? role.RoleName : '',
+      isEdit: isEdit,
+      role: role
+    });
+    modalRef?.close?.subscribe(() => this.loadRoles());
+  }
+
+  deleteRole(): void {
+    const roleName = this.selection.first().RoleName;
+    this.modalService.show(DeleteConfirmationModalComponent, {
+      itemDescription: $localize`Role`,
+      itemNames: [roleName],
+      submitActionObservable: () => {
+        return new Observable((observer: Subscriber<any>) => {
+          this.rgwRoleService.delete(roleName, this.accountId).subscribe({
+            next: () => {
+              this.notificationService.show(
+                NotificationType.success,
+                $localize`Role deleted successfully`
+              );
+              observer.next();
+              observer.complete();
+              this.loadRoles();
+            },
+            error: (err) => {
+              observer.error(err);
+            }
+          });
+        });
+      }
+    });
+  }
+}
index 77a4008580d671075227913b86282f717873f357..f5511853d4447ad82e2ab7af54938eda04d66ff1 100644 (file)
@@ -1,13 +1,27 @@
-<!-- Account quota -->
-<div *ngIf="selection.quota">
-  <legend i18n>Account quota</legend>
-  <cd-table-key-value [data]="quota">
-  </cd-table-key-value>
-</div>
-
-<!-- Bucket quota -->
-<div *ngIf="selection.bucket_quota">
-  <legend i18n>Bucket quota</legend>
-  <cd-table-key-value [data]="bucket_quota">
-  </cd-table-key-value>
-</div>
+@if (selection) {
+<cds-tabs>
+  <cds-tab heading="Quotas" i18n-heading>
+    <div class="row">
+      @if (selection.quota) {
+      <div class="col-sm-6">
+        <legend i18n>Account quota</legend>
+        <cd-table-key-value [data]="quota"> </cd-table-key-value>
+      </div>
+      } @if (selection.bucket_quota) {
+      <div class="col-sm-6">
+        <legend i18n>Bucket quota</legend>
+        <cd-table-key-value [data]="bucket_quota"> </cd-table-key-value>
+      </div>
+      }
+    </div>
+  </cds-tab>
+  <cds-tab heading="Roles" i18n-heading>
+    <div class="mt-4">
+      <cd-rgw-account-roles-list
+        [accountId]="selection.id"
+        [accountName]="selection.name"
+      ></cd-rgw-account-roles-list>
+    </div>
+  </cds-tab>
+</cds-tabs>
+}
index c573b617bf033244f2a478663a654eae8db03719..585f3c25f1272f23d0fcbf0efed724656bfff068 100644 (file)
@@ -120,6 +120,8 @@ import { RgwTopicFormComponent } from './rgw-topic-form/rgw-topic-form.component
 import { RgwBucketNotificationListComponent } from './rgw-bucket-notification-list/rgw-bucket-notification-list.component';
 import { RgwNotificationFormComponent } from './rgw-notification-form/rgw-notification-form.component';
 import { ComponentsModule } from '~/app/shared/components/components.module';
+import { RgwAccountRolesListComponent } from './rgw-account-roles-list/rgw-account-roles-list.component';
+import { RgwAccountRoleFormComponent } from './rgw-account-role-form/rgw-account-role-form.component';
 
 @NgModule({
   imports: [
@@ -229,7 +231,9 @@ import { ComponentsModule } from '~/app/shared/components/components.module';
     RgwTopicDetailsComponent,
     RgwTopicFormComponent,
     RgwBucketNotificationListComponent,
-    RgwNotificationFormComponent
+    RgwNotificationFormComponent,
+    RgwAccountRolesListComponent,
+    RgwAccountRoleFormComponent
   ],
   providers: [TitleCasePipe]
 })
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-role.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-role.service.spec.ts
new file mode 100644 (file)
index 0000000..3ec6598
--- /dev/null
@@ -0,0 +1,59 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { RgwRoleService } from './rgw-role.service';
+
+describe('RgwRoleService', () => {
+  let service: RgwRoleService;
+  let httpTesting: HttpTestingController;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpClientTestingModule]
+    });
+    service = TestBed.inject(RgwRoleService);
+    httpTesting = TestBed.inject(HttpTestingController);
+  });
+
+  afterEach(() => {
+    httpTesting.verify();
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+
+  it('should call list with account_id', () => {
+    service.list('test-account').subscribe();
+    const req = httpTesting.expectOne('api/rgw/accounts/test-account/roles');
+    expect(req.request.method).toBe('GET');
+  });
+
+  it('should call get with account_id', () => {
+    service.get('test-role', 'test-account').subscribe();
+    const req = httpTesting.expectOne('api/rgw/accounts/test-account/roles/test-role');
+    expect(req.request.method).toBe('GET');
+  });
+
+  it('should call create', () => {
+    const payload = { role_name: 'test', account_id: 'test-account' };
+    service.create(payload as any).subscribe();
+    const req = httpTesting.expectOne('api/rgw/accounts/test-account/roles');
+    expect(req.request.method).toBe('POST');
+    expect(req.request.body).toEqual(payload);
+  });
+
+  it('should call update', () => {
+    const payload = { max_session_duration: 2, account_id: 'test-account' };
+    service.update('test-role', payload as any).subscribe();
+    const req = httpTesting.expectOne('api/rgw/accounts/test-account/roles');
+    expect(req.request.method).toBe('PUT');
+    expect(req.request.body).toEqual(payload);
+  });
+
+  it('should call delete with account_id', () => {
+    service.delete('test-role', 'test-account').subscribe();
+    const req = httpTesting.expectOne('api/rgw/accounts/test-account/roles/test-role');
+    expect(req.request.method).toBe('DELETE');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-role.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-role.service.ts
new file mode 100644 (file)
index 0000000..57afbb2
--- /dev/null
@@ -0,0 +1,41 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import {
+  RgwRole,
+  RgwRoleCreatePayload,
+  RgwRoleUpdatePayload
+} from '~/app/ceph/rgw/models/rgw-role';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class RgwRoleService {
+  constructor(private http: HttpClient) {}
+
+  private getUrl(accountId: string): string {
+    return `api/rgw/accounts/${accountId}/roles`;
+  }
+
+  list(accountId: string): Observable<RgwRole[]> {
+    return this.http.get<RgwRole[]>(this.getUrl(accountId));
+  }
+
+  get(roleName: string, accountId: string): Observable<RgwRole> {
+    return this.http.get<RgwRole>(`${this.getUrl(accountId)}/${roleName}`);
+  }
+
+  create(role: RgwRoleCreatePayload): Observable<RgwRole> {
+    const accountId = role.account_id;
+    return this.http.post<RgwRole>(this.getUrl(accountId), role);
+  }
+
+  update(_roleName: string, payload: RgwRoleUpdatePayload): Observable<RgwRole> {
+    const accountId = payload.account_id;
+    return this.http.put<RgwRole>(this.getUrl(accountId), payload);
+  }
+
+  delete(roleName: string, accountId: string): Observable<any> {
+    return this.http.delete(`${this.getUrl(accountId)}/${roleName}`);
+  }
+}
index abb7a7351ff5a1f2aa8c9c58aec6380ce0e22e99..1a46d821cc6ab2778d65561a18cb90eecad28fc7 100644 (file)
@@ -18007,6 +18007,228 @@ paths:
       summary: Enable/Disable RGW Account/Bucket quota
       tags:
       - RgwUserAccounts
+  /api/rgw/accounts/{account_id}/roles:
+    get:
+      parameters:
+      - allowEmptyValue: true
+        in: path
+        name: account_id
+        schema:
+          type: integer
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                type: object
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                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
+    post:
+      parameters:
+      - allowEmptyValue: true
+        in: path
+        name: account_id
+        schema:
+          type: integer
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                role_assume_policy_doc:
+                  default: ''
+                  type: string
+                role_name:
+                  default: ''
+                  type: string
+                role_path:
+                  default: ''
+                  type: string
+              type: object
+      responses:
+        '201':
+          content:
+            application/json:
+              schema:
+                type: object
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                type: object
+          description: Resource created.
+        '202':
+          content:
+            application/json:
+              schema:
+                type: object
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                type: object
+          description: Operation is still executing. Please check the task queue.
+        '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: Create RGW role
+      tags:
+      - RGW
+    put:
+      parameters:
+      - allowEmptyValue: true
+        in: path
+        name: account_id
+        schema:
+          type: integer
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                max_session_duration:
+                  type: string
+                role_name:
+                  type: string
+              required:
+              - role_name
+              - max_session_duration
+              type: object
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                type: object
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                type: object
+          description: Resource updated.
+        '202':
+          content:
+            application/json:
+              schema:
+                type: object
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                type: object
+          description: Operation is still executing. Please check the task queue.
+        '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: Edit RGW role
+      tags:
+      - RGW
+  /api/rgw/accounts/{account_id}/roles/{role_name}:
+    delete:
+      parameters:
+      - in: path
+        name: role_name
+        required: true
+        schema:
+          type: string
+      - allowEmptyValue: true
+        in: path
+        name: account_id
+        schema:
+          type: integer
+      responses:
+        '202':
+          content:
+            application/json:
+              schema:
+                type: object
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                type: object
+          description: Operation is still executing. Please check the task queue.
+        '204':
+          content:
+            application/json:
+              schema:
+                type: object
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                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 RGW role
+      tags:
+      - RGW
+    get:
+      parameters:
+      - in: path
+        name: role_name
+        required: true
+        schema:
+          type: string
+      - allowEmptyValue: true
+        in: path
+        name: account_id
+        schema:
+          type: integer
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                type: object
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                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: Get RGW role
+      tags:
+      - RGW
   /api/rgw/bucket:
     get:
       parameters:
@@ -19857,172 +20079,6 @@ paths:
       - jwt: []
       tags:
       - RgwRealm
-  /api/rgw/roles:
-    get:
-      parameters: []
-      responses:
-        '200':
-          content:
-            application/json:
-              schema:
-                type: object
-            application/vnd.ceph.api.v1.0+json:
-              schema:
-                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
-    post:
-      parameters: []
-      requestBody:
-        content:
-          application/json:
-            schema:
-              properties:
-                role_assume_policy_doc:
-                  default: ''
-                  type: string
-                role_name:
-                  default: ''
-                  type: string
-                role_path:
-                  default: ''
-                  type: string
-              type: object
-      responses:
-        '201':
-          content:
-            application/json:
-              schema:
-                type: object
-            application/vnd.ceph.api.v1.0+json:
-              schema:
-                type: object
-          description: Resource created.
-        '202':
-          content:
-            application/json:
-              schema:
-                type: object
-            application/vnd.ceph.api.v1.0+json:
-              schema:
-                type: object
-          description: Operation is still executing. Please check the task queue.
-        '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: Create RGW role
-      tags:
-      - RGW
-    put:
-      parameters: []
-      requestBody:
-        content:
-          application/json:
-            schema:
-              properties:
-                max_session_duration:
-                  type: string
-                role_name:
-                  type: string
-              required:
-              - role_name
-              - max_session_duration
-              type: object
-      responses:
-        '200':
-          content:
-            application/json:
-              schema:
-                type: object
-            application/vnd.ceph.api.v1.0+json:
-              schema:
-                type: object
-          description: Resource updated.
-        '202':
-          content:
-            application/json:
-              schema:
-                type: object
-            application/vnd.ceph.api.v1.0+json:
-              schema:
-                type: object
-          description: Operation is still executing. Please check the task queue.
-        '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: Edit RGW role
-      tags:
-      - RGW
-  /api/rgw/roles/{role_name}:
-    delete:
-      parameters:
-      - in: path
-        name: role_name
-        required: true
-        schema:
-          type: string
-      responses:
-        '202':
-          content:
-            application/json:
-              schema:
-                type: object
-            application/vnd.ceph.api.v1.0+json:
-              schema:
-                type: object
-          description: Operation is still executing. Please check the task queue.
-        '204':
-          content:
-            application/json:
-              schema:
-                type: object
-            application/vnd.ceph.api.v1.0+json:
-              schema:
-                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 RGW role
-      tags:
-      - RGW
   /api/rgw/site:
     get:
       parameters:
index e4ab27911ec0d7319fd95ec3ab77bff5294d6b34..a36eded72dd873fda8ce11e89cdee4d3fc0eaa53 100755 (executable)
@@ -1020,19 +1020,31 @@ class RgwClient(RestClient):
         except RequestException as e:
             raise DashboardException(msg=str(e), component='rgw')
 
-    def list_roles(self) -> List[Dict[str, Any]]:
+    def list_roles(self, account_id: Optional[str] = None) -> List[Dict[str, Any]]:
         rgw_list_roles_command = ['role', 'list']
+        if account_id:
+            rgw_list_roles_command += ['--account-id', account_id]
         code, roles, err = mgr.send_rgwadmin_command(rgw_list_roles_command)
-        if code < 0:
+        if code != 0:
             logger.warning('Error listing roles with code %d: %s', code, err)
             return []
 
+        if isinstance(roles, dict):
+            roles = roles.get('Roles', [])
+
+        result = []
         for role in roles:
+            if isinstance(role, dict) and 'member' in role:
+                role = role['member']
+            if not isinstance(role, dict):
+                continue
             if 'PermissionPolicies' not in role:
                 role['PermissionPolicies'] = []
-        return roles
+            result.append(role)
+        return result
 
-    def create_role(self, role_name: str, role_path: str, role_assume_policy_doc: str) -> None:
+    def create_role(self, role_name: str, role_path: str, role_assume_policy_doc: str,
+                    account_id: Optional[str] = None) -> None:
         try:
             json.loads(role_assume_policy_doc)
         except:  # noqa: E722
@@ -1065,6 +1077,8 @@ class RgwClient(RestClient):
         rgw_create_role_command = ['role', 'create', '--role-name', role_name, '--path', role_path]
         if role_assume_policy_doc:
             rgw_create_role_command += ['--assume-role-policy-doc', f"{role_assume_policy_doc}"]
+        if account_id:
+            rgw_create_role_command += ['--account-id', account_id]
 
         code, _roles, _err = mgr.send_rgwadmin_command(rgw_create_role_command,
                                                        stdout_as_json=False)
@@ -1084,25 +1098,34 @@ class RgwClient(RestClient):
                                      component='rgw')
         return lifecycle_progress
 
-    def get_role(self, role_name: str):
+    def get_role(self, role_name: str, account_id: Optional[str] = None):
         rgw_get_role_command = ['role', 'get', '--role-name', role_name]
+        if account_id:
+            rgw_get_role_command += ['--account-id', account_id]
         code, role, _err = mgr.send_rgwadmin_command(rgw_get_role_command)
         if code != 0:
             raise DashboardException(msg=f'Error getting role with code {code}: {_err}',
                                      component='rgw')
+        if isinstance(role, dict) and 'role' in role:
+            role = role['role']
         return role
 
-    def update_role(self, role_name: str, max_session_duration: str):
+    def update_role(self, role_name: str, max_session_duration: str,
+                    account_id: Optional[str] = None):
         rgw_update_role_command = ['role', 'update', '--role-name',
                                    role_name, '--max_session_duration', max_session_duration]
+        if account_id:
+            rgw_update_role_command += ['--account-id', account_id]
         code, _, _err = mgr.send_rgwadmin_command(rgw_update_role_command,
                                                   stdout_as_json=False)
         if code != 0:
             raise DashboardException(msg=f'Error updating role with code {code}: {_err}',
                                      component='rgw')
 
-    def delete_role(self, role_name: str) -> None:
+    def delete_role(self, role_name: str, account_id: Optional[str] = None) -> None:
         rgw_delete_role_command = ['role', 'delete', '--role-name', role_name]
+        if account_id:
+            rgw_delete_role_command += ['--account-id', account_id]
         code, _, _err = mgr.send_rgwadmin_command(rgw_delete_role_command,
                                                   stdout_as_json=False)
         if code != 0:
@@ -2734,10 +2757,11 @@ class RgwMultisite:
 
     def get_user_list(self, zoneName=None, realmName=None):
         user_list = []
+        rgw_user_list_cmd = ['user', 'list']
         if zoneName:
-            rgw_user_list_cmd = ['user', 'list', '--rgw-zone', zoneName]
+            rgw_user_list_cmd.extend(['--rgw-zone', zoneName])
         if realmName:
-            rgw_user_list_cmd = ['user', 'list', '--rgw-realm', realmName]
+            rgw_user_list_cmd.extend(['--rgw-realm', realmName])
         try:
             exit_code, out, _ = mgr.send_rgwadmin_command(rgw_user_list_cmd)
             if exit_code > 0: