From 1d3e33140b8f694e75a14f6530ec54712b984179 Mon Sep 17 00:00:00 2001 From: Sagar Gopale Date: Wed, 10 Jun 2026 18:59:50 +0530 Subject: [PATCH] mgr/dashboard: align RGW role management with Carbon and fix API routing Fixes: https://tracker.ceph.com/issues/77328 Signed-off-by: Sagar Gopale --- src/pybind/mgr/dashboard/controllers/rgw.py | 37 +- .../frontend/cypress/e2e/rgw/accounts.po.ts | 4 +- .../cypress/e2e/rgw/roles.e2e-spec.ts | 31 +- .../frontend/cypress/e2e/rgw/roles.po.ts | 99 +++-- .../src/app/ceph/rgw/models/rgw-role.ts | 22 + .../rgw-account-role-form.component.html | 136 ++++++ .../rgw-account-role-form.component.scss | 0 .../rgw-account-role-form.component.spec.ts | 51 +++ .../rgw-account-role-form.component.ts | 114 +++++ .../rgw-account-roles-list.component.html | 14 + .../rgw-account-roles-list.component.scss | 1 + .../rgw-account-roles-list.component.spec.ts | 71 ++++ .../rgw-account-roles-list.component.ts | 167 ++++++++ .../rgw-user-accounts-details.component.html | 40 +- .../frontend/src/app/ceph/rgw/rgw.module.ts | 6 +- .../app/shared/api/rgw-role.service.spec.ts | 59 +++ .../src/app/shared/api/rgw-role.service.ts | 41 ++ src/pybind/mgr/dashboard/openapi.yaml | 388 ++++++++++-------- .../mgr/dashboard/services/rgw_client.py | 42 +- 19 files changed, 1079 insertions(+), 244 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-role.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-role.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-role.service.ts diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 9e06597f171..a0ef6965771 100755 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -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") diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/accounts.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/accounts.po.ts index 215507045a5..f58932982fd 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/accounts.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/accounts.po.ts @@ -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); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts index 87031750385..2026d0dc589 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts @@ -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); }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts index 920a9d1d110..611a060236e 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts @@ -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 index 00000000000..8522e8f1775 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-role.ts @@ -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 index 00000000000..11fff876688 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.html @@ -0,0 +1,136 @@ + + +

{{ accountName }}

+

{{ mode }} role

+
+ +
+
+ +
+ Role name + + + + @if (form.showError('role_name', formDir, 'required')) { + This field is required. + } + +
+ + + @if (!isEdit) { + +
+ Path + + + + @if (form.showError('role_path', formDir, 'required')) { + This field is required. + } + +
+ + +
+ Trust policy + + + + @if (form.showError('role_assume_policy_doc', formDir, 'required')) { + This field is required. + } @if (form.showError('role_assume_policy_doc', formDir, 'json')) { + Must be a valid JSON. + } + +
+ } + + + @if (isEdit) { +
+ + + + @if (form.showError('max_session_duration', formDir, 'required')) { + This field is required. + } @if (form.showError('max_session_duration', formDir, 'min')) { + Minimum value is 1 hour. + } @if (form.showError('max_session_duration', formDir, 'max')) { + Maximum value is 12 hours. + } + +
+ } +
+ + +
+
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 index 00000000000..e69de29bb2d 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 index 00000000000..e2f6b2fabb1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.spec.ts @@ -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; + + 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 index 00000000000..8cd266cdf46 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-role-form/rgw-account-role-form.component.ts @@ -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 index 00000000000..209f15a6daa --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.html @@ -0,0 +1,14 @@ + + + + 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 index 00000000000..dfca32bf3c2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.scss @@ -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 index 00000000000..6beb2fa5ccb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.spec.ts @@ -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; + 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 index 00000000000..af1c13f0a4b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-account-roles-list/rgw-account-roles-list.component.ts @@ -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; + 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) => { + 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); + } + }); + }); + } + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.html index 77a4008580d..f5511853d44 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.html @@ -1,13 +1,27 @@ - -
- Account quota - - -
- - -
- Bucket quota - - -
+@if (selection) { + + +
+ @if (selection.quota) { +
+ Account quota + +
+ } @if (selection.bucket_quota) { +
+ Bucket quota + +
+ } +
+
+ +
+ +
+
+
+} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index c573b617bf0..585f3c25f12 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -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 index 00000000000..3ec6598338d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-role.service.spec.ts @@ -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 index 00000000000..57afbb253e8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-role.service.ts @@ -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 { + return this.http.get(this.getUrl(accountId)); + } + + get(roleName: string, accountId: string): Observable { + return this.http.get(`${this.getUrl(accountId)}/${roleName}`); + } + + create(role: RgwRoleCreatePayload): Observable { + const accountId = role.account_id; + return this.http.post(this.getUrl(accountId), role); + } + + update(_roleName: string, payload: RgwRoleUpdatePayload): Observable { + const accountId = payload.account_id; + return this.http.put(this.getUrl(accountId), payload); + } + + delete(roleName: string, accountId: string): Observable { + return this.http.delete(`${this.getUrl(accountId)}/${roleName}`); + } +} diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index abb7a7351ff..1a46d821cc6 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -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: diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index e4ab27911ec..a36eded72dd 100755 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -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: -- 2.47.3