diff options
author | Nizamudeen A <nia@redhat.com> | 2023-10-18 08:38:21 +0200 |
---|---|---|
committer | Nizamudeen A <nia@redhat.com> | 2023-11-27 13:43:41 +0100 |
commit | 5c28d78a45d87d2be6b9ea2961be42689673e252 (patch) | |
tree | 3ceebd5b875bb2191b87791c725fde5feb3c7b91 /src/pybind/mgr/dashboard | |
parent | mgr/dashboard: support rgw roles removal (diff) | |
download | ceph-5c28d78a45d87d2be6b9ea2961be42689673e252.tar.xz ceph-5c28d78a45d87d2be6b9ea2961be42689673e252.zip |
mgr/dashboard: support rgw roles updating
Right now only the modification of max_session_duration is supported via
the roles update command. To update, we need to use `policy modify`
command which is not added in this PR. That should be done separately
Refer: https://docs.ceph.com/en/latest/radosgw/role/#update-a-role
Fixes: https://tracker.ceph.com/issues/63230
Signed-off-by: Nizamudeen A <nia@redhat.com>
Diffstat (limited to 'src/pybind/mgr/dashboard')
14 files changed, 237 insertions, 20 deletions
diff --git a/src/pybind/mgr/dashboard/controllers/_crud.py b/src/pybind/mgr/dashboard/controllers/_crud.py index 240a2b5ab8c..d65649cadb4 100644 --- a/src/pybind/mgr/dashboard/controllers/_crud.py +++ b/src/pybind/mgr/dashboard/controllers/_crud.py @@ -104,6 +104,7 @@ class Validator(Enum): RGW_ROLE_NAME = 'rgwRoleName' RGW_ROLE_PATH = 'rgwRolePath' FILE = 'file' + RGW_ROLE_SESSION_DURATION = 'rgwRoleSessionDuration' class FormField(NamedTuple): @@ -224,6 +225,10 @@ class Container: properties[field.key]['title'] = field.name field_ui_schema['key'] = field_key field_ui_schema['readonly'] = field.readonly + if field.readonly: + field_ui_schema['templateOptions'] = { + 'disabled': True + } field_ui_schema['help'] = f'{field.help}' field_ui_schema['validators'] = [i.value for i in field.validators] items.append(field_ui_schema) @@ -307,6 +312,7 @@ class CRUDMeta(SerializableClass): self.forms = [] self.columnKey = '' self.detail_columns = [] + self.resource = '' class CRUDCollectionMethod(NamedTuple): @@ -330,6 +336,7 @@ class CRUDEndpoint: actions: Optional[List[TableAction]] = None, permissions: Optional[List[str]] = None, forms: Optional[List[Form]] = None, column_key: Optional[str] = None, + resource: Optional[str] = None, meta: CRUDMeta = CRUDMeta(), get_all: Optional[CRUDCollectionMethod] = None, create: Optional[CRUDCollectionMethod] = None, delete: Optional[CRUDCollectionMethod] = None, @@ -352,6 +359,7 @@ class CRUDEndpoint: self.detail_columns = detail_columns if detail_columns is not None else [] self.extra_endpoints = extra_endpoints if extra_endpoints is not None else [] self.selection_type = selection_type + self.resource = resource def __call__(self, cls: Any): self.create_crud_class(cls) @@ -415,6 +423,7 @@ class CRUDEndpoint: self.generate_forms(model_key) self.set_permissions() self.set_column_key() + self.set_table_resource() self.get_detail_columns() selection_type = self.__class__.outer_self.selection_type self.__class__.outer_self.meta.table.set_selection_type(selection_type) @@ -468,6 +477,10 @@ class CRUDEndpoint: if self.__class__.outer_self.column_key: self.outer_self.meta.columnKey = self.__class__.outer_self.column_key + def set_table_resource(self): + if self.__class__.outer_self.resource: + self.outer_self.meta.resource = self.__class__.outer_self.resource + class_name = self.router.path.replace('/', '') meta_class = type(f'{class_name}_CRUDClassMetadata', (RESTController,), @@ -478,6 +491,7 @@ class CRUDEndpoint: 'generate_forms': generate_forms, 'set_permissions': set_permissions, 'set_column_key': set_column_key, + 'set_table_resource': set_table_resource, 'get_detail_columns': get_detail_columns, 'outer_self': self, }) diff --git a/src/pybind/mgr/dashboard/controllers/ceph_users.py b/src/pybind/mgr/dashboard/controllers/ceph_users.py index e1bdc157091..022f8f36c42 100644 --- a/src/pybind/mgr/dashboard/controllers/ceph_users.py +++ b/src/pybind/mgr/dashboard/controllers/ceph_users.py @@ -174,7 +174,7 @@ edit_form = Form(path='/cluster/user/edit', TableAction(name='Create', permission='create', icon=Icon.ADD.value, routerLink='/cluster/user/create'), TableAction(name='Edit', permission='update', icon=Icon.EDIT.value, - click='edit'), + click='edit', routerLink='/cluster/user/edit'), TableAction(name='Delete', permission='delete', icon=Icon.DESTROY.value, click='delete', disable=True), TableAction(name='Import', permission='create', icon=Icon.IMPORT.value, @@ -185,6 +185,7 @@ edit_form = Form(path='/cluster/user/edit', permissions=[Scope.CONFIG_OPT], forms=[create_form, edit_form, import_user_form], column_key='entity', + resource='user', get_all=CRUDCollectionMethod( func=CephUserEndpoints.user_list, doc=EndpointDoc("Get Ceph Users") diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index fd376a53f03..bf2d9555317 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: disable=C0302 import json import logging import re @@ -717,6 +718,15 @@ class RGWRoleEndpoints: rgw_client.create_role(role_name, role_path, role_assume_policy_doc) return f'Role {role_name} created successfully' + @staticmethod + def role_update(_, role_name: str, max_session_duration: str): + 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)) + return f'Role {role_name} updated successfully' @staticmethod def role_delete(_, role_name: str): @@ -725,7 +735,18 @@ class RGWRoleEndpoints: rgw_client.delete_role(role_name) return f'Role {role_name} deleted successfully' + @staticmethod + def model(role_name: str): + 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'] + # convert maxsessionduration which is in seconds to hours + if role['MaxSessionDuration']: + model['max_session_duration'] = role['MaxSessionDuration'] / 3600 + return model # pylint: disable=C0301 @@ -735,6 +756,10 @@ assume_role_policy_help = ( 'target="_blank">click here.</a>' ) +max_session_duration_help = ( + 'The maximum session duration (in hours) that you want to set for the specified role.This setting can have a value from 1 hour to 12 hours.' # noqa: E501 +) + create_container = VerticalContainer('Create Role', 'create_role', fields=[ FormField('Role name', 'role_name', validators=[Validator.RGW_ROLE_NAME]), FormField('Path', 'role_path', validators=[Validator.RGW_ROLE_PATH]), @@ -744,23 +769,43 @@ create_container = VerticalContainer('Create Role', 'create_role', fields=[ field_type='textarea', validators=[Validator.JSON]), ]) -create_role_form = Form(path='/rgw/roles/create', + +edit_container = VerticalContainer('Edit Role', 'edit_role', fields=[ + FormField('Role name', 'role_name', readonly=True), + FormField('Max Session Duration', 'max_session_duration', + help=max_session_duration_help, + validators=[Validator.RGW_ROLE_SESSION_DURATION]) +]) + +create_role_form = Form(path='/create', root_container=create_container, task_info=FormTaskInfo("IAM RGW Role '{role_name}' created successfully", ['role_name']), method_type=MethodType.POST.value) +edit_role_form = Form(path='/edit', + root_container=edit_container, + task_info=FormTaskInfo("IAM RGW Role '{role_name}' edited successfully", + ['role_name']), + method_type=MethodType.PUT.value, + model_callback=RGWRoleEndpoints.model) + @CRUDEndpoint( router=APIRouter('/rgw/roles', Scope.RGW), doc=APIDoc("List of RGW roles", "RGW"), actions=[ TableAction(name='Create', permission='create', icon=Icon.ADD.value, + routerLink='/rgw/roles/create'), + TableAction(name='Edit', permission='update', icon=Icon.EDIT.value, + click='edit', routerLink='/rgw/roles/edit'), TableAction(name='Delete', permission='delete', icon=Icon.DESTROY.value, click='delete', disable=True), ], - forms=[create_role_form], - permissions=[Scope.CONFIG_OPT], + forms=[create_role_form, edit_role_form], + column_key='RoleName', + resource='Role', + permissions=[Scope.RGW], get_all=CRUDCollectionMethod( func=RGWRoleEndpoints.role_list, doc=EndpointDoc("List RGW roles") @@ -769,6 +814,10 @@ create_role_form = Form(path='/rgw/roles/create', func=RGWRoleEndpoints.role_create, doc=EndpointDoc("Create RGW role") ), + edit=CRUDCollectionMethod( + func=RGWRoleEndpoints.role_update, + doc=EndpointDoc("Edit RGW role") + ), delete=CRUDCollectionMethod( func=RGWRoleEndpoints.role_delete, doc=EndpointDoc("Delete RGW role") 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 597f7d1be88..80a8b0ec902 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 @@ -9,11 +9,21 @@ describe('RGW roles page', () => { }); describe('Create, Edit & Delete rgw roles', () => { + const roleName = 'testRole'; + it('should create rgw roles', () => { roles.navigateTo('create'); - roles.create('testRole', '/', '{}'); + roles.create(roleName, '/', '{}'); roles.navigateTo(); - roles.checkExist('testRole', true); + roles.checkExist(roleName, true); + }); + + it('should edit rgw role', () => { + roles.edit(roleName, 3); + }); + + it('should delete rgw role', () => { + roles.delete(roleName); }); }); }); 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 b72ca5df9a7..717655b2f08 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 @@ -11,18 +11,36 @@ export class RolesPageHelper extends PageHelper { columnIndex = { roleName: 2, path: 3, - arn: 4 + arn: 4, + createDate: 5, + maxSessionDuration: 6 }; @PageHelper.restrictTo(pages.create.url) create(name: string, path: string, policyDocument: string) { - cy.get('#formly_3_string_role_name_0').type(name); - cy.get('#formly_3_textarea_role_assume_policy_doc_2').type(policyDocument); - cy.get('#formly_3_string_role_path_1').type(path); + 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'); } + 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.getTableCell(this.columnIndex.roleName, name) + .click() + .parent() + .find(`datatable-body-cell: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) => { 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 c16c13a81bd..f2e37af0aa7 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 @@ -156,6 +156,13 @@ const routes: Routes = [ data: { breadcrumbs: ActionLabels.CREATE } + }, + { + path: URLVerbs.EDIT, + component: CrudFormComponent, + data: { + breadcrumbs: ActionLabels.EDIT + } } ] }, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts index e036b754438..178f230c931 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts @@ -24,12 +24,14 @@ export class ContextComponent implements OnInit, OnDestroy { private subs = new Subscription(); private rgwUrlPrefix = '/rgw'; private rgwUserUrlPrefix = '/rgw/user'; + private rgwRoleUrlPrefix = '/rgw/roles'; private rgwBuckerUrlPrefix = '/rgw/bucket'; permissions: Permissions; featureToggleMap$: FeatureTogglesMap$; isRgwRoute = document.location.href.includes(this.rgwUserUrlPrefix) || - document.location.href.includes(this.rgwBuckerUrlPrefix); + document.location.href.includes(this.rgwBuckerUrlPrefix) || + document.location.href.includes(this.rgwRoleUrlPrefix); constructor( private authStorageService: AuthStorageService, @@ -48,9 +50,11 @@ export class ContextComponent implements OnInit, OnDestroy { .pipe(filter((event: Event) => event instanceof NavigationEnd)) .subscribe( () => - (this.isRgwRoute = [this.rgwBuckerUrlPrefix, this.rgwUserUrlPrefix].some((urlPrefix) => - this.router.url.startsWith(urlPrefix) - )) + (this.isRgwRoute = [ + this.rgwBuckerUrlPrefix, + this.rgwUserUrlPrefix, + this.rgwRoleUrlPrefix + ].some((urlPrefix) => this.router.url.startsWith(urlPrefix))) ) ); // Set daemon list polling only when in RGW route: diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts index 750152161c2..6881e373b58 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts @@ -120,7 +120,7 @@ export class CRUDTableComponent implements OnInit { delete() { const selectedKey = this.selection.first()[this.meta.columnKey]; this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { - itemDescription: $localize`${this.meta.columnKey}`, + itemDescription: $localize`${this.meta.resource}`, itemNames: [selectedKey], submitAction: () => { this.taskWrapper @@ -153,7 +153,9 @@ export class CRUDTableComponent implements OnInit { if (this.selection.hasSelection) { key = this.selection.first()[this.meta.columnKey]; } - this.router.navigate(['/cluster/user/edit'], { queryParams: { key: key } }); + + const editAction = this.meta.actions.find((action) => action.name === 'Edit'); + this.router.navigate([editAction.routerLink], { queryParams: { key: key } }); } authExport() { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts index 37e94f236be..76cbbcfb3a2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts @@ -61,7 +61,11 @@ import { CheckedTableFormComponent } from './checked-table-form/checked-table-fo 'Role path must start and finish with a slash "/".' + ' (pattern: (\u002F)|(\u002F[\u0021-\u007E]+\u002F))' }, - { name: 'file_size', message: 'File size must not exceed 4KiB' } + { name: 'file_size', message: 'File size must not exceed 4KiB' }, + { + name: 'rgwRoleSessionDuration', + message: 'This field must be a number and should be a value from 1 hour to 12 hour' + } ], wrappers: [{ name: 'input-wrapper', component: FormlyInputWrapperComponent }] }), diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/helpers.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/helpers.ts index 1ea21b71081..aca9a20af09 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/helpers.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/helpers.ts @@ -3,7 +3,11 @@ import { FormlyFieldConfig } from '@ngx-formly/core'; import { forEach } from 'lodash'; import { formlyAsyncFileValidator } from './validators/file-validator'; import { formlyAsyncJsonValidator } from './validators/json-validator'; -import { formlyRgwRoleNameValidator, formlyRgwRolePath } from './validators/rgw-role-validator'; +import { + formlyFormNumberValidator, + formlyRgwRoleNameValidator, + formlyRgwRolePath +} from './validators/rgw-role-validator'; export function getFieldState(field: FormlyFieldConfig, uiSchema: any[] = undefined) { const formState: any[] = uiSchema || field.options?.formState; @@ -34,6 +38,10 @@ export function setupValidators(field: FormlyFieldConfig, uiSchema: any[]) { validators.push(formlyAsyncFileValidator); break; } + case 'rgwRoleSessionDuration': { + validators.push(formlyFormNumberValidator); + break; + } } }); field.asyncValidators = { validation: validators }; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/rgw-role-validator.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/rgw-role-validator.ts index a100f278bea..c994dc96407 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/rgw-role-validator.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/rgw-role-validator.ts @@ -17,3 +17,12 @@ export function formlyRgwRoleNameValidator(control: AbstractControl): Promise<an resolve({ rgwRoleName: true }); }); } + +export function formlyFormNumberValidator(control: AbstractControl): Promise<any> { + return new Promise((resolve, _reject) => { + if (control.value.match('^[0-9.]+$')) { + if (control.value <= 12 && control.value >= 1) resolve(null); + } + resolve({ rgwRoleSessionDuration: true }); + }); +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts index 140fa5b5f8e..dc33e6236ae 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts @@ -14,4 +14,5 @@ export class CrudMetadata { actions: CdTableAction[]; forms: any; columnKey: string; + resource: string; } diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index d35ea87e15a..453352493d4 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -9596,7 +9596,80 @@ paths: trace. security: - jwt: [] - summary: Create Ceph User + 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/vnd.ceph.api.v1.0+json: + type: object + description: Resource updated. + '202': + content: + application/vnd.ceph.api.v1.0+json: + 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/vnd.ceph.api.v1.0+json: + type: object + description: Operation is still executing. Please check the task queue. + '204': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Resource deleted. + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: Delete RGW role tags: - RGW /api/rgw/site: diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index fe7b8c66898..6358d397976 100644 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -851,7 +851,24 @@ class RgwClient(RestClient): 'Looks like the document has a wrong format.' f' For more information about the format look at {link}') raise DashboardException(msg=msg, component='rgw') - + + def get_role(self, role_name: str): + rgw_get_role_command = ['role', 'get', '--role-name', role_name] + 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') + return role + + def update_role(self, role_name: str, max_session_duration: str): + rgw_update_role_command = ['role', 'update', '--role-name', + role_name, '--max_session_duration', max_session_duration] + 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: rgw_delete_role_command = ['role', 'delete', '--role-name', role_name] code, _, _err = mgr.send_rgwadmin_command(rgw_delete_role_command, |