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 = (
@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,
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")
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);
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);
});
});
it('should delete rgw role', () => {
- roles.delete(roleName, null, null, true);
+ roles.deleteRole(roleName);
+ roles.checkExist(roleName, false);
});
});
});
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');
+ }
}
}
--- /dev/null
+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;
+}
--- /dev/null
+<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>
--- /dev/null
+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);
+ });
+});
--- /dev/null
+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 });
+ }
+ });
+ }
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+/* Scss stylesheet for RGW account roles list */
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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);
+ }
+ });
+ });
+ }
+ });
+ }
+}
-<!-- 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>
+}
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: [
RgwTopicDetailsComponent,
RgwTopicFormComponent,
RgwBucketNotificationListComponent,
- RgwNotificationFormComponent
+ RgwNotificationFormComponent,
+ RgwAccountRolesListComponent,
+ RgwAccountRoleFormComponent
],
providers: [TitleCasePipe]
})
--- /dev/null
+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');
+ });
+});
--- /dev/null
+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}`);
+ }
+}
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:
- 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:
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
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)
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:
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: