]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: support rgw roles updating 55541/head
authorNizamudeen A <nia@redhat.com>
Wed, 18 Oct 2023 06:38:21 +0000 (12:08 +0530)
committerNizamudeen A <nia@redhat.com>
Tue, 19 Mar 2024 15:03:43 +0000 (20:33 +0530)
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>
(cherry picked from commit 5c28d78a45d87d2be6b9ea2961be42689673e252)

14 files changed:
src/pybind/mgr/dashboard/controllers/_crud.py
src/pybind/mgr/dashboard/controllers/ceph_users.py
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/helpers.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/rgw-role-validator.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/crud-table-metadata.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py

index 4a57ac06cc588cad0c3a28fa5e87cd6ca60c20ed..100e5fe4b4d0dd3f584917cafefd2a14c9d2c25b 100644 (file)
@@ -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,
                           })
index e1bdc157091c01d268cf4908d64c97ad5261696d..022f8f36c420b95dd69f2643875aed80ae047f6b 100644 (file)
@@ -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")
index 4dd3872aea5b1241e621e52ff50793a32db3229f..85f2988ee3bd1622cdbe64102d669a5f6c9c6a9e 100644 (file)
@@ -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")
index 597f7d1be881770322930aa33370e790fdcef7c6..80a8b0ec902aa6c53b793a688efcf5edbc8b93a0 100644 (file)
@@ -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);
     });
   });
 });
index b72ca5df9a7ff7ca3e440aac5e2de200dcde5ef0..717655b2f08c35d893a900503a8a9429a69d7e69 100644 (file)
@@ -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) => {
index 5367a2b93224f5c00d0574ba072ac8430b98bbfe..f8149fd7e35b6b8dacfe39b6bcc0619a084ad6d2 100644 (file)
@@ -156,6 +156,13 @@ const routes: Routes = [
         data: {
           breadcrumbs: ActionLabels.CREATE
         }
+      },
+      {
+        path: URLVerbs.EDIT,
+        component: CrudFormComponent,
+        data: {
+          breadcrumbs: ActionLabels.EDIT
+        }
       }
     ]
   },
index e036b754438b4c6314637c5cb3bc5afb42481fce..178f230c931f697fa018294f205eef658944e019 100644 (file)
@@ -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:
index 2d1f520bac6ff1108f028f9f5cb256710ab51824..340ddf7005bc46d2e8937c9818b406b63b756484 100644 (file)
@@ -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
@@ -157,7 +157,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() {
index 37e94f236be967b838f13b857de39f8507e26cb3..76cbbcfb3a2045bc5adb2262a47e0f6a5d439195 100644 (file)
@@ -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 }]
     }),
index 1ea21b71081cc638628f5911fc986a7f2283a1af..aca9a20af09c353fafc445ff3eb67b3f3d03a1cb 100644 (file)
@@ -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 };
index a100f278bea07423201d65310793bd23e35d2962..c994dc964070c20f722b86278c114849189b21c1 100644 (file)
@@ -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 });
+  });
+}
index 5ff77031c4e0858cb643a2f011963b6e496c4197..791a0e469097abb4c3144094a54ecfbf8007c4d7 100644 (file)
@@ -15,4 +15,5 @@ export class CrudMetadata {
   forms: any;
   columnKey: string;
   detail_columns: string[];
+  resource: string;
 }
index 135379b127bc6bf2e2495239e144caf3df4d1583..b7325b9f8b504e08b3a4ba44ab490a845e3f647c 100644 (file)
@@ -10154,7 +10154,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:
index e6507436b0a5972f24c4c1c650e33e99fc167dab..495a2b16ca58f88c83eeb3d38cb1e861cd107b36 100644 (file)
@@ -854,7 +854,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,