Skip to content

Commit 5c28d78

Browse files
committed
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 <[email protected]>
1 parent 2e06ab0 commit 5c28d78

File tree

14 files changed

+237
-20
lines changed

14 files changed

+237
-20
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: 52 additions & 3 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
@@ -717,6 +718,15 @@ def role_create(_, role_name: str = '', role_path: str = '', role_assume_policy_
717718
rgw_client.create_role(role_name, role_path, role_assume_policy_doc)
718719
return f'Role {role_name} created successfully'
719720

721+
@staticmethod
722+
def role_update(_, role_name: str, max_session_duration: str):
723+
assert role_name
724+
assert max_session_duration
725+
# convert max_session_duration which is in hours to seconds
726+
max_session_duration = int(float(max_session_duration) * 3600)
727+
rgw_client = RgwClient.admin_instance()
728+
rgw_client.update_role(role_name, str(max_session_duration))
729+
return f'Role {role_name} updated successfully'
720730

721731
@staticmethod
722732
def role_delete(_, role_name: str):
@@ -725,7 +735,18 @@ def role_delete(_, role_name: str):
725735
rgw_client.delete_role(role_name)
726736
return f'Role {role_name} deleted successfully'
727737

738+
@staticmethod
739+
def model(role_name: str):
740+
assert role_name
741+
rgw_client = RgwClient.admin_instance()
742+
role = rgw_client.get_role(role_name)
743+
model = {'role_name': '', 'max_session_duration': ''}
744+
model['role_name'] = role['RoleName']
728745

746+
# convert maxsessionduration which is in seconds to hours
747+
if role['MaxSessionDuration']:
748+
model['max_session_duration'] = role['MaxSessionDuration'] / 3600
749+
return model
729750

730751

731752
# pylint: disable=C0301
@@ -735,6 +756,10 @@ def role_delete(_, role_name: str):
735756
'target="_blank">click here.</a>'
736757
)
737758

759+
max_session_duration_help = (
760+
'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
761+
)
762+
738763
create_container = VerticalContainer('Create Role', 'create_role', fields=[
739764
FormField('Role name', 'role_name', validators=[Validator.RGW_ROLE_NAME]),
740765
FormField('Path', 'role_path', validators=[Validator.RGW_ROLE_PATH]),
@@ -744,23 +769,43 @@ def role_delete(_, role_name: str):
744769
field_type='textarea',
745770
validators=[Validator.JSON]),
746771
])
747-
create_role_form = Form(path='/rgw/roles/create',
772+
773+
edit_container = VerticalContainer('Edit Role', 'edit_role', fields=[
774+
FormField('Role name', 'role_name', readonly=True),
775+
FormField('Max Session Duration', 'max_session_duration',
776+
help=max_session_duration_help,
777+
validators=[Validator.RGW_ROLE_SESSION_DURATION])
778+
])
779+
780+
create_role_form = Form(path='/create',
748781
root_container=create_container,
749782
task_info=FormTaskInfo("IAM RGW Role '{role_name}' created successfully",
750783
['role_name']),
751784
method_type=MethodType.POST.value)
752785

786+
edit_role_form = Form(path='/edit',
787+
root_container=edit_container,
788+
task_info=FormTaskInfo("IAM RGW Role '{role_name}' edited successfully",
789+
['role_name']),
790+
method_type=MethodType.PUT.value,
791+
model_callback=RGWRoleEndpoints.model)
792+
753793

754794
@CRUDEndpoint(
755795
router=APIRouter('/rgw/roles', Scope.RGW),
756796
doc=APIDoc("List of RGW roles", "RGW"),
757797
actions=[
758798
TableAction(name='Create', permission='create', icon=Icon.ADD.value,
799+
routerLink='/rgw/roles/create'),
800+
TableAction(name='Edit', permission='update', icon=Icon.EDIT.value,
801+
click='edit', routerLink='/rgw/roles/edit'),
759802
TableAction(name='Delete', permission='delete', icon=Icon.DESTROY.value,
760803
click='delete', disable=True),
761804
],
762-
forms=[create_role_form],
763-
permissions=[Scope.CONFIG_OPT],
805+
forms=[create_role_form, edit_role_form],
806+
column_key='RoleName',
807+
resource='Role',
808+
permissions=[Scope.RGW],
764809
get_all=CRUDCollectionMethod(
765810
func=RGWRoleEndpoints.role_list,
766811
doc=EndpointDoc("List RGW roles")
@@ -769,6 +814,10 @@ def role_delete(_, role_name: str):
769814
func=RGWRoleEndpoints.role_create,
770815
doc=EndpointDoc("Create RGW role")
771816
),
817+
edit=CRUDCollectionMethod(
818+
func=RGWRoleEndpoints.role_update,
819+
doc=EndpointDoc("Edit RGW role")
820+
),
772821
delete=CRUDCollectionMethod(
773822
func=RGWRoleEndpoints.role_delete,
774823
doc=EndpointDoc("Delete RGW role")

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
@@ -156,6 +156,13 @@ const routes: Routes = [
156156
data: {
157157
breadcrumbs: ActionLabels.CREATE
158158
}
159+
},
160+
{
161+
path: URLVerbs.EDIT,
162+
component: CrudFormComponent,
163+
data: {
164+
breadcrumbs: ActionLabels.EDIT
165+
}
159166
}
160167
]
161168
},

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)