Skip to content

Commit f64ad44

Browse files
authored
Merge pull request ceph#54068 from rhcs-dashboard/rgw-roles-update
mgr/dashboard: add support for editing and deleting rgw roles Reviewed-by: Pedro Gonzalez Gomez <[email protected]> Reviewed-by: Aashish Sharma <[email protected]>
2 parents b393ea0 + 5c28d78 commit f64ad44

File tree

14 files changed

+261
-21
lines changed

14 files changed

+261
-21
lines changed

src/pybind/mgr/dashboard/controllers/_crud.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ class Validator(Enum):
104104
RGW_ROLE_NAME = 'rgwRoleName'
105105
RGW_ROLE_PATH = 'rgwRolePath'
106106
FILE = 'file'
107+
RGW_ROLE_SESSION_DURATION = 'rgwRoleSessionDuration'
107108

108109

109110
class FormField(NamedTuple):
@@ -224,6 +225,10 @@ def to_dict(self, key=''):
224225
properties[field.key]['title'] = field.name
225226
field_ui_schema['key'] = field_key
226227
field_ui_schema['readonly'] = field.readonly
228+
if field.readonly:
229+
field_ui_schema['templateOptions'] = {
230+
'disabled': True
231+
}
227232
field_ui_schema['help'] = f'{field.help}'
228233
field_ui_schema['validators'] = [i.value for i in field.validators]
229234
items.append(field_ui_schema)
@@ -307,6 +312,7 @@ def __init__(self):
307312
self.forms = []
308313
self.columnKey = ''
309314
self.detail_columns = []
315+
self.resource = ''
310316

311317

312318
class CRUDCollectionMethod(NamedTuple):
@@ -330,6 +336,7 @@ def __init__(self, router: APIRouter, doc: APIDoc,
330336
actions: Optional[List[TableAction]] = None,
331337
permissions: Optional[List[str]] = None, forms: Optional[List[Form]] = None,
332338
column_key: Optional[str] = None,
339+
resource: Optional[str] = None,
333340
meta: CRUDMeta = CRUDMeta(), get_all: Optional[CRUDCollectionMethod] = None,
334341
create: Optional[CRUDCollectionMethod] = None,
335342
delete: Optional[CRUDCollectionMethod] = None,
@@ -352,6 +359,7 @@ def __init__(self, router: APIRouter, doc: APIDoc,
352359
self.detail_columns = detail_columns if detail_columns is not None else []
353360
self.extra_endpoints = extra_endpoints if extra_endpoints is not None else []
354361
self.selection_type = selection_type
362+
self.resource = resource
355363

356364
def __call__(self, cls: Any):
357365
self.create_crud_class(cls)
@@ -415,6 +423,7 @@ def _list(self, model_key: str = ''):
415423
self.generate_forms(model_key)
416424
self.set_permissions()
417425
self.set_column_key()
426+
self.set_table_resource()
418427
self.get_detail_columns()
419428
selection_type = self.__class__.outer_self.selection_type
420429
self.__class__.outer_self.meta.table.set_selection_type(selection_type)
@@ -468,6 +477,10 @@ def set_column_key(self):
468477
if self.__class__.outer_self.column_key:
469478
self.outer_self.meta.columnKey = self.__class__.outer_self.column_key
470479

480+
def set_table_resource(self):
481+
if self.__class__.outer_self.resource:
482+
self.outer_self.meta.resource = self.__class__.outer_self.resource
483+
471484
class_name = self.router.path.replace('/', '')
472485
meta_class = type(f'{class_name}_CRUDClassMetadata',
473486
(RESTController,),
@@ -478,6 +491,7 @@ def set_column_key(self):
478491
'generate_forms': generate_forms,
479492
'set_permissions': set_permissions,
480493
'set_column_key': set_column_key,
494+
'set_table_resource': set_table_resource,
481495
'get_detail_columns': get_detail_columns,
482496
'outer_self': self,
483497
})

src/pybind/mgr/dashboard/controllers/ceph_users.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def model(user_entity: str):
174174
TableAction(name='Create', permission='create', icon=Icon.ADD.value,
175175
routerLink='/cluster/user/create'),
176176
TableAction(name='Edit', permission='update', icon=Icon.EDIT.value,
177-
click='edit'),
177+
click='edit', routerLink='/cluster/user/edit'),
178178
TableAction(name='Delete', permission='delete', icon=Icon.DESTROY.value,
179179
click='delete', disable=True),
180180
TableAction(name='Import', permission='create', icon=Icon.IMPORT.value,
@@ -185,6 +185,7 @@ def model(user_entity: str):
185185
permissions=[Scope.CONFIG_OPT],
186186
forms=[create_form, edit_form, import_user_form],
187187
column_key='entity',
188+
resource='user',
188189
get_all=CRUDCollectionMethod(
189190
func=CephUserEndpoints.user_list,
190191
doc=EndpointDoc("Get Ceph Users")

src/pybind/mgr/dashboard/controllers/rgw.py

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22

3+
# pylint: disable=C0302
34
import json
45
import logging
56
import re
@@ -726,6 +727,36 @@ def role_create(_, role_name: str = '', role_path: str = '', role_assume_policy_
726727
rgw_client.create_role(role_name, role_path, role_assume_policy_doc)
727728
return f'Role {role_name} created successfully'
728729

730+
@staticmethod
731+
def role_update(_, role_name: str, max_session_duration: str):
732+
assert role_name
733+
assert max_session_duration
734+
# convert max_session_duration which is in hours to seconds
735+
max_session_duration = int(float(max_session_duration) * 3600)
736+
rgw_client = RgwClient.admin_instance()
737+
rgw_client.update_role(role_name, str(max_session_duration))
738+
return f'Role {role_name} updated successfully'
739+
740+
@staticmethod
741+
def role_delete(_, role_name: str):
742+
assert role_name
743+
rgw_client = RgwClient.admin_instance()
744+
rgw_client.delete_role(role_name)
745+
return f'Role {role_name} deleted successfully'
746+
747+
@staticmethod
748+
def model(role_name: str):
749+
assert role_name
750+
rgw_client = RgwClient.admin_instance()
751+
role = rgw_client.get_role(role_name)
752+
model = {'role_name': '', 'max_session_duration': ''}
753+
model['role_name'] = role['RoleName']
754+
755+
# convert maxsessionduration which is in seconds to hours
756+
if role['MaxSessionDuration']:
757+
model['max_session_duration'] = role['MaxSessionDuration'] / 3600
758+
return model
759+
729760

730761
# pylint: disable=C0301
731762
assume_role_policy_help = (
@@ -734,6 +765,10 @@ def role_create(_, role_name: str = '', role_path: str = '', role_assume_policy_
734765
'target="_blank">click here.</a>'
735766
)
736767

768+
max_session_duration_help = (
769+
'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
770+
)
771+
737772
create_container = VerticalContainer('Create Role', 'create_role', fields=[
738773
FormField('Role name', 'role_name', validators=[Validator.RGW_ROLE_NAME]),
739774
FormField('Path', 'role_path', validators=[Validator.RGW_ROLE_PATH]),
@@ -743,29 +778,58 @@ def role_create(_, role_name: str = '', role_path: str = '', role_assume_policy_
743778
field_type='textarea',
744779
validators=[Validator.JSON]),
745780
])
746-
create_role_form = Form(path='/rgw/roles/create',
781+
782+
edit_container = VerticalContainer('Edit Role', 'edit_role', fields=[
783+
FormField('Role name', 'role_name', readonly=True),
784+
FormField('Max Session Duration', 'max_session_duration',
785+
help=max_session_duration_help,
786+
validators=[Validator.RGW_ROLE_SESSION_DURATION])
787+
])
788+
789+
create_role_form = Form(path='/create',
747790
root_container=create_container,
748791
task_info=FormTaskInfo("IAM RGW Role '{role_name}' created successfully",
749792
['role_name']),
750793
method_type=MethodType.POST.value)
751794

795+
edit_role_form = Form(path='/edit',
796+
root_container=edit_container,
797+
task_info=FormTaskInfo("IAM RGW Role '{role_name}' edited successfully",
798+
['role_name']),
799+
method_type=MethodType.PUT.value,
800+
model_callback=RGWRoleEndpoints.model)
801+
752802

753803
@CRUDEndpoint(
754804
router=APIRouter('/rgw/roles', Scope.RGW),
755805
doc=APIDoc("List of RGW roles", "RGW"),
756806
actions=[
757807
TableAction(name='Create', permission='create', icon=Icon.ADD.value,
758-
routerLink='/rgw/roles/create')
808+
routerLink='/rgw/roles/create'),
809+
TableAction(name='Edit', permission='update', icon=Icon.EDIT.value,
810+
click='edit', routerLink='/rgw/roles/edit'),
811+
TableAction(name='Delete', permission='delete', icon=Icon.DESTROY.value,
812+
click='delete', disable=True),
759813
],
760-
forms=[create_role_form],
761-
permissions=[Scope.CONFIG_OPT],
814+
forms=[create_role_form, edit_role_form],
815+
column_key='RoleName',
816+
resource='Role',
817+
permissions=[Scope.RGW],
762818
get_all=CRUDCollectionMethod(
763819
func=RGWRoleEndpoints.role_list,
764820
doc=EndpointDoc("List RGW roles")
765821
),
766822
create=CRUDCollectionMethod(
767823
func=RGWRoleEndpoints.role_create,
768-
doc=EndpointDoc("Create Ceph User")
824+
doc=EndpointDoc("Create RGW role")
825+
),
826+
edit=CRUDCollectionMethod(
827+
func=RGWRoleEndpoints.role_update,
828+
doc=EndpointDoc("Edit RGW role")
829+
),
830+
delete=CRUDCollectionMethod(
831+
func=RGWRoleEndpoints.role_delete,
832+
doc=EndpointDoc("Delete RGW role")
769833
),
770834
set_column={
771835
"CreateDate": {'cellTemplate': 'date'},

src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,21 @@ describe('RGW roles page', () => {
99
});
1010

1111
describe('Create, Edit & Delete rgw roles', () => {
12+
const roleName = 'testRole';
13+
1214
it('should create rgw roles', () => {
1315
roles.navigateTo('create');
14-
roles.create('testRole', '/', '{}');
16+
roles.create(roleName, '/', '{}');
1517
roles.navigateTo();
16-
roles.checkExist('testRole', true);
18+
roles.checkExist(roleName, true);
19+
});
20+
21+
it('should edit rgw role', () => {
22+
roles.edit(roleName, 3);
23+
});
24+
25+
it('should delete rgw role', () => {
26+
roles.delete(roleName);
1727
});
1828
});
1929
});

src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,36 @@ export class RolesPageHelper extends PageHelper {
1111
columnIndex = {
1212
roleName: 2,
1313
path: 3,
14-
arn: 4
14+
arn: 4,
15+
createDate: 5,
16+
maxSessionDuration: 6
1517
};
1618

1719
@PageHelper.restrictTo(pages.create.url)
1820
create(name: string, path: string, policyDocument: string) {
19-
cy.get('#formly_3_string_role_name_0').type(name);
20-
cy.get('#formly_3_textarea_role_assume_policy_doc_2').type(policyDocument);
21-
cy.get('#formly_3_string_role_path_1').type(path);
21+
cy.get('[id$="string_role_name_0"]').type(name);
22+
cy.get('[id$="role_assume_policy_doc_2"]').type(policyDocument);
23+
cy.get('[id$="role_path_1"]').type(path);
2224
cy.get("[aria-label='Create Role']").should('exist').click();
2325
cy.get('cd-crud-table').should('exist');
2426
}
2527

28+
edit(name: string, maxSessionDuration: number) {
29+
this.navigateEdit(name);
30+
cy.get('[id$="max_session_duration_1"]').clear().type(maxSessionDuration.toString());
31+
cy.get("[aria-label='Edit Role']").should('exist').click();
32+
cy.get('cd-crud-table').should('exist');
33+
34+
this.getTableCell(this.columnIndex.roleName, name)
35+
.click()
36+
.parent()
37+
.find(`datatable-body-cell:nth-child(${this.columnIndex.maxSessionDuration})`)
38+
.should(($elements) => {
39+
const roleName = $elements.map((_, el) => el.textContent).get();
40+
expect(roleName).to.include(`${maxSessionDuration} hours`);
41+
});
42+
}
43+
2644
@PageHelper.restrictTo(pages.index.url)
2745
checkExist(name: string, exist: boolean) {
2846
this.getTableCell(this.columnIndex.roleName, name).should(($elements) => {

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ const routes: Routes = [
158158
data: {
159159
breadcrumbs: ActionLabels.CREATE
160160
}
161+
},
162+
{
163+
path: URLVerbs.EDIT,
164+
component: CrudFormComponent,
165+
data: {
166+
breadcrumbs: ActionLabels.EDIT
167+
}
161168
}
162169
]
163170
},

src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ export class ContextComponent implements OnInit, OnDestroy {
2424
private subs = new Subscription();
2525
private rgwUrlPrefix = '/rgw';
2626
private rgwUserUrlPrefix = '/rgw/user';
27+
private rgwRoleUrlPrefix = '/rgw/roles';
2728
private rgwBuckerUrlPrefix = '/rgw/bucket';
2829
permissions: Permissions;
2930
featureToggleMap$: FeatureTogglesMap$;
3031
isRgwRoute =
3132
document.location.href.includes(this.rgwUserUrlPrefix) ||
32-
document.location.href.includes(this.rgwBuckerUrlPrefix);
33+
document.location.href.includes(this.rgwBuckerUrlPrefix) ||
34+
document.location.href.includes(this.rgwRoleUrlPrefix);
3335

3436
constructor(
3537
private authStorageService: AuthStorageService,
@@ -48,9 +50,11 @@ export class ContextComponent implements OnInit, OnDestroy {
4850
.pipe(filter((event: Event) => event instanceof NavigationEnd))
4951
.subscribe(
5052
() =>
51-
(this.isRgwRoute = [this.rgwBuckerUrlPrefix, this.rgwUserUrlPrefix].some((urlPrefix) =>
52-
this.router.url.startsWith(urlPrefix)
53-
))
53+
(this.isRgwRoute = [
54+
this.rgwBuckerUrlPrefix,
55+
this.rgwUserUrlPrefix,
56+
this.rgwRoleUrlPrefix
57+
].some((urlPrefix) => this.router.url.startsWith(urlPrefix)))
5458
)
5559
);
5660
// Set daemon list polling only when in RGW route:

src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export class CRUDTableComponent implements OnInit {
120120
delete() {
121121
const selectedKey = this.selection.first()[this.meta.columnKey];
122122
this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
123-
itemDescription: $localize`${this.meta.columnKey}`,
123+
itemDescription: $localize`${this.meta.resource}`,
124124
itemNames: [selectedKey],
125125
submitAction: () => {
126126
this.taskWrapper
@@ -153,7 +153,9 @@ export class CRUDTableComponent implements OnInit {
153153
if (this.selection.hasSelection) {
154154
key = this.selection.first()[this.meta.columnKey];
155155
}
156-
this.router.navigate(['/cluster/user/edit'], { queryParams: { key: key } });
156+
157+
const editAction = this.meta.actions.find((action) => action.name === 'Edit');
158+
this.router.navigate([editAction.routerLink], { queryParams: { key: key } });
157159
}
158160

159161
authExport() {

src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ import { CheckedTableFormComponent } from './checked-table-form/checked-table-fo
6161
'Role path must start and finish with a slash "/".' +
6262
' (pattern: (\u002F)|(\u002F[\u0021-\u007E]+\u002F))'
6363
},
64-
{ name: 'file_size', message: 'File size must not exceed 4KiB' }
64+
{ name: 'file_size', message: 'File size must not exceed 4KiB' },
65+
{
66+
name: 'rgwRoleSessionDuration',
67+
message: 'This field must be a number and should be a value from 1 hour to 12 hour'
68+
}
6569
],
6670
wrappers: [{ name: 'input-wrapper', component: FormlyInputWrapperComponent }]
6771
}),

src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/helpers.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { FormlyFieldConfig } from '@ngx-formly/core';
33
import { forEach } from 'lodash';
44
import { formlyAsyncFileValidator } from './validators/file-validator';
55
import { formlyAsyncJsonValidator } from './validators/json-validator';
6-
import { formlyRgwRoleNameValidator, formlyRgwRolePath } from './validators/rgw-role-validator';
6+
import {
7+
formlyFormNumberValidator,
8+
formlyRgwRoleNameValidator,
9+
formlyRgwRolePath
10+
} from './validators/rgw-role-validator';
711

812
export function getFieldState(field: FormlyFieldConfig, uiSchema: any[] = undefined) {
913
const formState: any[] = uiSchema || field.options?.formState;
@@ -34,6 +38,10 @@ export function setupValidators(field: FormlyFieldConfig, uiSchema: any[]) {
3438
validators.push(formlyAsyncFileValidator);
3539
break;
3640
}
41+
case 'rgwRoleSessionDuration': {
42+
validators.push(formlyFormNumberValidator);
43+
break;
44+
}
3745
}
3846
});
3947
field.asyncValidators = { validation: validators };

0 commit comments

Comments
 (0)