From 6e9db91d1ca614e1414b7af27a8abdcd86d05c01 Mon Sep 17 00:00:00 2001 From: Naman Munet Date: Tue, 22 Jul 2025 22:38:42 +0530 Subject: [PATCH] mgr/dashboard: user accounts enhancements fixes: https://tracker.ceph.com/issues/72072 PR covers: 1) Displaying account name instead of account id in bucket list page & bucket edit form for account owned buckets 2) non-root account user can now be assigned with managed policies with which they can perform operations 3) The root user indication shifted next to username in users list rather than on Account Name with a new icon. Signed-off-by: Naman Munet --- src/pybind/mgr/dashboard/controllers/rgw.py | 149 ++++++++++++++---- .../mgr/dashboard/frontend/package-lock.json | 8 +- .../mgr/dashboard/frontend/package.json | 2 +- .../rgw-bucket-form.component.ts | 11 +- .../rgw-bucket-list.component.ts | 5 +- .../rgw-user-details.component.html | 7 + .../rgw-user-details.component.spec.ts | 8 +- .../rgw-user-details.component.ts | 10 ++ .../rgw-user-form.component.html | 27 +++- .../rgw-user-form.component.spec.ts | 24 +-- .../rgw-user-form/rgw-user-form.component.ts | 80 ++++++++-- .../rgw-user-list.component.html | 18 ++- .../rgw-user-list.component.scss | 5 + .../rgw-user-list/rgw-user-list.component.ts | 11 +- .../frontend/src/app/ceph/rgw/rgw.module.ts | 6 +- .../src/app/ceph/rgw/utils/constants.ts | 10 ++ .../src/app/shared/api/rgw-user.service.ts | 6 + .../src/app/shared/enum/icons.enum.ts | 1 + src/pybind/mgr/dashboard/openapi.yaml | 6 + src/pybind/mgr/dashboard/services/rgw_iam.py | 40 +++++ 20 files changed, 345 insertions(+), 89 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 1e2d791fe0b73..bbbf5b971adba 100755 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -546,6 +546,41 @@ class RgwBucket(RgwRESTController): bucket_name = '{}:{}'.format(tenant, bucket_name) return bucket_name + def map_bucket_owners(self, result, daemon_name): + """ + Replace bucket owner IDs with account names for a list of buckets. + + :param result: List of bucket dicts with 'owner' keys. + :param daemon_name: RGW daemon identifier. + :return: Modified result with owner names instead of IDs. + """ + # Get unique owner IDs from buckets + owner_ids = {bucket['owner'] for bucket in result} + + # Get available account IDs + valid_accounts = set(RgwAccounts().get_accounts()) + + # Determine which owner IDs are valid and need querying + query_ids = owner_ids & valid_accounts + + # Fetch account names for valid owner IDs + id_to_name = {} + for owner_id in query_ids: + try: + account = self.proxy(daemon_name, 'GET', 'account', {'id': owner_id}) + if 'name' in account: + id_to_name[owner_id] = account['name'] + except RequestException: + continue + + # Replace owner IDs with names in the bucket list + for bucket in result: + owner_id = bucket.get('owner') + if owner_id in id_to_name: + bucket['owner'] = id_to_name[owner_id] + + return result + @RESTController.MethodMap(version=APIVersion(1, 1)) # type: ignore def list(self, stats: bool = False, daemon_name: Optional[str] = None, uid: Optional[str] = None) -> List[Union[str, Dict[str, Any]]]: @@ -553,9 +588,9 @@ class RgwBucket(RgwRESTController): if uid and uid.strip(): query_params = f'{query_params}&uid={uid.strip()}' result = self.proxy(daemon_name, 'GET', 'bucket{}'.format(query_params)) - - if stats: + if str_to_bool(stats): result = [self._append_bid(bucket) for bucket in result] + result = self.map_bucket_owners(result, daemon_name) return result @@ -900,6 +935,9 @@ class RgwUser(RgwRESTController): if not self._keys_allowed(): del result['keys'] del result['swift_keys'] + if result.get('account_id') not in (None, '') and result.get('type') != 'root': + rgwAccounts = RgwAccounts() + result['managed_user_policies'] = rgwAccounts.list_managed_policy(uid) result['uid'] = result['full_user_id'] return result @@ -918,53 +956,94 @@ class RgwUser(RgwRESTController): def create(self, uid, display_name, email=None, max_buckets=None, system=None, suspended=None, generate_key=None, access_key=None, secret_key=None, daemon_name=None, account_id: Optional[str] = None, - account_root_user: Optional[bool] = False): - params = {'uid': uid} - if display_name is not None: - params['display-name'] = display_name - if email is not None: - params['email'] = email - if max_buckets is not None: - params['max-buckets'] = max_buckets - if system is not None: - params['system'] = system - if suspended is not None: - params['suspended'] = suspended - if generate_key is not None: - params['generate-key'] = generate_key - if access_key is not None: - params['access-key'] = access_key - if secret_key is not None: - params['secret-key'] = secret_key - if account_id is not None: - params['account-id'] = account_id + account_root_user: Optional[bool] = False, + account_policies: Optional[str] = None): + """Create a new RGW user.""" + + params = {'uid': uid, 'display-name': display_name} + + # Add optional parameters + optional_params = { + 'email': email, + 'max-buckets': max_buckets, + 'system': system, + 'suspended': suspended, + 'generate-key': generate_key, + 'access-key': access_key, + 'secret-key': secret_key, + 'account-id': account_id + } + + # Add only non-None parameters + for key, value in optional_params.items(): + if value is not None: + params[key] = value + + # Handle boolean parameter separately if account_root_user: params['account-root'] = account_root_user + + # Make the API request result = self.proxy(daemon_name, 'PUT', 'user', params) result['uid'] = result['full_user_id'] + + # Process account policies + if account_policies is not None: + self._process_account_policies(uid, account_policies) + return result + def _process_account_policies(self, uid, account_policies): + """Process account policies for a user.""" + rgw_accounts = RgwAccounts() + # Parse the policies JSON if it's a string + if isinstance(account_policies, str): + account_policies = json.loads(account_policies) + + # Attach policies + for policy_arn in account_policies.get('attach', []): + rgw_accounts.attach_managed_policy(uid, policy_arn) + + # Detach policies + for policy_arn in account_policies.get('detach', []): + rgw_accounts.detach_managed_policy(uid, policy_arn) + @allow_empty_body def set(self, uid, display_name=None, email=None, max_buckets=None, system=None, suspended=None, daemon_name=None, account_id: Optional[str] = None, - account_root_user: Optional[bool] = False): + account_root_user: Optional[bool] = False, + account_policies: Optional[str] = None): + """Update an existing RGW user.""" + params = {'uid': uid} - if display_name is not None: - params['display-name'] = display_name - if email is not None: - params['email'] = email - if max_buckets is not None: - params['max-buckets'] = max_buckets - if system is not None: - params['system'] = system - if suspended is not None: - params['suspended'] = suspended - if account_id is not None: - params['account-id'] = account_id + + # Add optional parameters + optional_params = { + 'display-name': display_name, + 'email': email, + 'max-buckets': max_buckets, + 'system': system, + 'suspended': suspended, + 'account-id': account_id + } + + # Add only non-None parameters + for key, value in optional_params.items(): + if value is not None: + params[key] = value + + # Handle boolean parameter separately if account_root_user: params['account-root'] = account_root_user + + # Make the API request result = self.proxy(daemon_name, 'POST', 'user', params) result['uid'] = result['full_user_id'] + + # Process account policies + if account_policies is not None: + self._process_account_policies(uid, account_policies) + return result def delete(self, uid, daemon_name=None): diff --git a/src/pybind/mgr/dashboard/frontend/package-lock.json b/src/pybind/mgr/dashboard/frontend/package-lock.json index c4351a783b244..6ba9e386e6fd3 100644 --- a/src/pybind/mgr/dashboard/frontend/package-lock.json +++ b/src/pybind/mgr/dashboard/frontend/package-lock.json @@ -19,7 +19,7 @@ "@angular/platform-browser-dynamic": "18.2.11", "@angular/router": "18.2.11", "@carbon/charts-angular": "1.23.9", - "@carbon/icons": "11.41.0", + "@carbon/icons": "11.63.0", "@carbon/styles": "1.83.0", "@ibm/plex": "6.4.0", "@ng-bootstrap/ng-bootstrap": "17.0.1", @@ -3947,9 +3947,9 @@ "license": "Apache-2.0" }, "node_modules/@carbon/icons": { - "version": "11.41.0", - "resolved": "https://registry.npmjs.org/@carbon/icons/-/icons-11.41.0.tgz", - "integrity": "sha512-9RGaOnihPQx74yBQ0UnEr9JJ+e2aa/J+tmTG/sZ203q2hfoeMF2PqipwOhNS1fqCnyW1zvsYQNydUsNIDzCqaA==", + "version": "11.63.0", + "resolved": "https://registry.npmjs.org/@carbon/icons/-/icons-11.63.0.tgz", + "integrity": "sha512-J5sGbamMMBbQPcdX9ImzEIoa7l2DyNiZYu9ScKtY3dJ6lKeG6GJUFQYH5/vcpuyj02tizVe6rSgWcH210+OOqw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/src/pybind/mgr/dashboard/frontend/package.json b/src/pybind/mgr/dashboard/frontend/package.json index 5d552a87773b3..1372db06196de 100644 --- a/src/pybind/mgr/dashboard/frontend/package.json +++ b/src/pybind/mgr/dashboard/frontend/package.json @@ -52,7 +52,7 @@ "@angular/platform-browser-dynamic": "18.2.11", "@angular/router": "18.2.11", "@carbon/charts-angular": "1.23.9", - "@carbon/icons": "11.41.0", + "@carbon/icons": "11.63.0", "@carbon/styles": "1.83.0", "@ibm/plex": "6.4.0", "@ng-bootstrap/ng-bootstrap": "17.0.1", diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts index b3b6f0f35fb1e..15790fd2f8e4f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts @@ -231,7 +231,12 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC } // Process route parameters. - this.route.params.subscribe((params: { bid: string }) => { + this.route.params.subscribe((params: { bid: string; owner: string }) => { + let bucketOwner = ''; + if (params.hasOwnProperty('owner')) { + // only used for showing bucket owned by account + bucketOwner = decodeURIComponent(params.owner); + } if (params.hasOwnProperty('bid')) { const bid = decodeURIComponent(params.bid); promises['getBid'] = this.rgwBucketService.get(bid); @@ -299,13 +304,13 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC // creating dummy user object to show the account owner // here value['owner] is the account user id const user = Object.assign( - { uid: value['owner'] }, + { uid: bucketOwner }, ownersList.find((owner: RgwUser) => owner.uid === AppConstants.defaultUser) ); this.accountUsers.push(user); this.bucketForm.get('isAccountOwner').setValue(true); this.bucketForm.get('isAccountOwner').disable(); - this.bucketForm.get('accountUser').setValue(value['owner']); + this.bucketForm.get('accountUser').setValue(bucketOwner); this.bucketForm.get('accountUser').disable(); } this.isVersioningAlreadyEnabled = this.isVersioningEnabled; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts index dae12f99a2889..4248c5ff2a2f0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts @@ -110,7 +110,10 @@ export class RgwBucketListComponent extends ListWithDetails implements OnInit, O } ]; const getBucketUri = () => - this.selection.first() && `${encodeURIComponent(this.selection.first().bid)}`; + this.selection.first() && + `${encodeURIComponent(this.selection.first().bid)}/${encodeURIComponent( + this.selection.first().owner + )}`; const addAction: CdTableAction = { permission: 'create', icon: Icons.add, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html index 8d05bb3b314bb..c9d7719d6b6f4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html @@ -64,6 +64,13 @@ class="bold">Maximum buckets {{ user.max_buckets | map: maxBucketsMap }} + @if (user.type === 'rgw' && selection.account?.id){ + + Managed policies + {{ extractPolicyNamesFromArns(user.managed_user_policies) }} + + } Subusers diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts index c308bc6d94b16..4267f6c03b67c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts @@ -56,7 +56,9 @@ describe('RgwUserDetailsComponent', () => { system: 'true', keys: [], swift_keys: [], - mfa_ids: ['testMFA1', 'testMFA2'] + mfa_ids: ['testMFA1', 'testMFA2'], + type: 'rgw', + account: { id: 'RGW12345678901234567' } }; component.ngOnChanges(); @@ -65,8 +67,8 @@ describe('RgwUserDetailsComponent', () => { const detailsTab = fixture.debugElement.nativeElement.querySelectorAll( '.cds--data-table--sort.cds--data-table--no-border tr td' ); - expect(detailsTab[14].textContent).toEqual('MFAs(Id)'); - expect(detailsTab[15].textContent).toEqual('testMFA1, testMFA2'); + expect(detailsTab[16].textContent).toEqual('MFAs(Id)'); + expect(detailsTab[17].textContent).toEqual('testMFA1, testMFA2'); }); it('should test updateKeysSelection', () => { component.selection = { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts index dd2f6d35e2096..f95134494d991 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts @@ -136,4 +136,14 @@ export class RgwUserDetailsComponent implements OnChanges, OnInit { break; } } + + extractPolicyNamesFromArns(arnList: string[]) { + if (!arnList || arnList.length === 0) { + return '-'; + } + return arnList + .map((arn) => arn.trim().split('/').pop()) + .filter(Boolean) + .join(', '); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html index c38780ee32d59..172aaa747a784 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html @@ -23,7 +23,7 @@ [ngValue]="null">Loading... + [value]="''">-- Select an Account -- @@ -208,6 +208,31 @@ + @if(userForm.getValue('account_id') && !userForm.getValue('account_root_user')) { + +
+
+ Managed policies + + + +
+
+ } +
{ secret_key: '', suspended: false, system: false, - uid: null, - account_id: '', - account_root_user: false + uid: null }); expect(spyRateLimit).toHaveBeenCalled(); }); @@ -248,8 +246,7 @@ describe('RgwUserFormComponent', () => { email: null, max_buckets: -1, suspended: false, - system: false, - account_root_user: false + system: false }); expect(spyRateLimit).toHaveBeenCalled(); }); @@ -268,9 +265,7 @@ describe('RgwUserFormComponent', () => { secret_key: '', suspended: false, system: false, - uid: null, - account_id: '', - account_root_user: false + uid: null }); expect(spyRateLimit).toHaveBeenCalled(); }); @@ -286,8 +281,7 @@ describe('RgwUserFormComponent', () => { email: null, max_buckets: 0, suspended: false, - system: false, - account_root_user: false + system: false }); expect(spyRateLimit).toHaveBeenCalled(); }); @@ -308,9 +302,7 @@ describe('RgwUserFormComponent', () => { secret_key: '', suspended: false, system: false, - uid: null, - account_id: '', - account_root_user: false + uid: null }); expect(spyRateLimit).toHaveBeenCalled(); }); @@ -327,8 +319,7 @@ describe('RgwUserFormComponent', () => { email: null, max_buckets: 100, suspended: false, - system: false, - account_root_user: false + system: false }); expect(spyRateLimit).toHaveBeenCalled(); }); @@ -358,8 +349,7 @@ describe('RgwUserFormComponent', () => { email: '', max_buckets: 1000, suspended: false, - system: false, - account_root_user: false + system: false }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts index fc4f15d2c1e19..a46a3c5cc3f00 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts @@ -29,7 +29,8 @@ import { RgwRateLimitConfig } from '../models/rgw-rate-limit'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { RgwUserAccountsService } from '~/app/shared/api/rgw-user-accounts.service'; import { Account } from '../models/rgw-user-accounts'; -import { RGW } from '../utils/constants'; +import { ManagedPolicyArnMap, ManagedPolicyName, RGW } from '../utils/constants'; +import { ComboBoxItem } from '~/app/shared/models/combo-box.model'; @Component({ selector: 'cd-rgw-user-form', @@ -56,6 +57,19 @@ export class RgwUserFormComponent extends CdForm implements OnInit { previousTenant: string = null; @ViewChild(RgwRateLimitComponent, { static: false }) rateLimitComponent!: RgwRateLimitComponent; accounts: Account[] = []; + initialUserPolicies: string[] = []; + managedPolicies: ComboBoxItem[] = [ + { + content: ManagedPolicyName.AmazonS3FullAccess, + name: ManagedPolicyArnMap[ManagedPolicyName.AmazonS3FullAccess], + selected: false + }, + { + content: ManagedPolicyName.AmazonS3ReadOnlyAccess, + name: ManagedPolicyArnMap[ManagedPolicyName.AmazonS3ReadOnlyAccess], + selected: false + } + ]; constructor( private formBuilder: CdFormBuilder, @@ -117,6 +131,7 @@ export class RgwUserFormComponent extends CdForm implements OnInit { ], account_id: [null, [this.tenantedAccountValidator.bind(this)]], account_root_user: [false], + account_policies: [[]], max_buckets_mode: [1], max_buckets: [ 1000, @@ -261,6 +276,15 @@ export class RgwUserFormComponent extends CdForm implements OnInit { }); this.capabilities = resp[0].caps; this.uid = this.getUID(); + this.initialUserPolicies = resp[0].managed_user_policies ?? []; + + this.managedPolicies.forEach((policy) => { + policy.selected = this.initialUserPolicies.includes(policy.name); + }); + + // Optionally, update form control with selected items + const selectedItems = this.managedPolicies.filter((p) => p.selected).map((p) => p.name); + this.userForm.get('account_policies')?.setValue(selectedItems); }, () => { this.loadingError(); @@ -297,6 +321,12 @@ export class RgwUserFormComponent extends CdForm implements OnInit { }); } + multiSelector(event: ComboBoxItem[]) { + this.managedPolicies.forEach((policy) => { + policy.selected = !!event.find((selected) => selected.name === policy.name); + }); + } + tenantedAccountValidator(control: AbstractControl): ValidationErrors | null { if (this?.userForm?.getValue('tenant') && this.accounts.length > 0) { const index: number = this.accounts.findIndex( @@ -655,7 +685,8 @@ export class RgwUserFormComponent extends CdForm implements OnInit { 'system', 'suspended', 'account_id', - 'account_root_user' + 'account_root_user', + 'account_policies' ].some((path) => { return this.userForm.get(path).dirty; }); @@ -700,8 +731,6 @@ export class RgwUserFormComponent extends CdForm implements OnInit { private _getCreateArgs() { const result = { uid: this.getUID(), - account_id: this.userForm.getValue('account_id') ? this.userForm.getValue('account_id') : '', - account_root_user: this.userForm.getValue('account_root_user'), display_name: this.userForm.getValue('display_name'), system: this.userForm.getValue('system'), suspended: this.userForm.getValue('suspended'), @@ -729,6 +758,16 @@ export class RgwUserFormComponent extends CdForm implements OnInit { // 0 => Unlimited bucket creation. _.merge(result, { max_buckets: maxBucketsMode }); } + if (this.userForm.getValue('account_id')) { + _.merge(result, { + account_id: this.userForm.getValue('account_id'), + account_root_user: this.userForm.getValue('account_root_user') + }); + } + const accountPolicies = this._getAccountManagedPolicies(); + if (this.userForm.getValue('account_id') && !this.userForm.getValue('account_root_user')) { + _.merge(result, { account_policies: accountPolicies }); + } return result; } @@ -738,19 +777,13 @@ export class RgwUserFormComponent extends CdForm implements OnInit { */ private _getUpdateArgs() { const result: Record = {}; - const keys = [ - 'display_name', - 'email', - 'max_buckets', - 'system', - 'suspended', - 'account_root_user' - ]; + const keys = ['display_name', 'email', 'max_buckets', 'system', 'suspended']; for (const key of keys) { result[key] = this.userForm.getValue(key); } if (this.userForm.getValue('account_id')) { result['account_id'] = this.userForm.getValue('account_id'); + result['account_root_user'] = this.userForm.getValue('account_root_user'); } const maxBucketsMode = parseInt(this.userForm.getValue('max_buckets_mode'), 10); if (_.includes([-1, 0], maxBucketsMode)) { @@ -758,6 +791,10 @@ export class RgwUserFormComponent extends CdForm implements OnInit { // 0 => Unlimited bucket creation. result['max_buckets'] = maxBucketsMode; } + const accountPolicies = this._getAccountManagedPolicies(); + if (this.userForm.getValue('account_id') && !this.userForm.getValue('account_root_user')) { + result['account_policies'] = accountPolicies; + } return result; } @@ -831,6 +868,25 @@ export class RgwUserFormComponent extends CdForm implements OnInit { return result; } + /** + * Get the account managed policies to attach/detach. + * @returns {Object} Returns an object with attach and detach arrays. + */ + private _getAccountManagedPolicies() { + const selectedPolicies = this.managedPolicies.filter((p) => p.selected).map((p) => p.name); + + const initialPolicies = this.initialUserPolicies; + const toAttach = selectedPolicies.filter((p) => !initialPolicies.includes(p)); + const toDetach = initialPolicies.filter((p) => !selectedPolicies.includes(p)); + + const payload = { + attach: toAttach, + detach: toDetach + }; + + return payload; + } + onMaxBucketsModeChange(mode: string) { if (mode === '1') { // If 'Custom' mode is selected, then ensure that the form field diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html index 361a1e30b6622..daeff590c5ffd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html @@ -45,10 +45,18 @@ i18n>No Limit - - {{row.account?.name}} +
+ {{ row.uid }} + @if (row.type === 'root') { + + + + } +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.scss index e69de29bb2d1d..b13eb5c0ed345 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.scss @@ -0,0 +1,5 @@ +@use '@carbon/layout'; + +.account-root-icon { + margin-left: layout.$spacing-03; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts index 64f5e72bae117..80e29fb8a318c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts @@ -38,8 +38,8 @@ export class RgwUserListComponent extends ListWithDetails implements OnInit { userSizeTpl: TemplateRef; @ViewChild('userObjectTpl', { static: true }) userObjectTpl: TemplateRef; - @ViewChild('accountTmpl', { static: true }) - public accountTmpl: TemplateRef; + @ViewChild('usernameTpl', { static: true }) + usernameTpl: TemplateRef; permission: Permission; tableActions: CdTableAction[]; columns: CdTableColumn[] = []; @@ -48,6 +48,7 @@ export class RgwUserListComponent extends ListWithDetails implements OnInit { selection: CdTableSelection = new CdTableSelection(); userDataSubject = new Subject(); declare staleTimeout: number; + icons = Icons; constructor( private authStorageService: AuthStorageService, @@ -67,7 +68,8 @@ export class RgwUserListComponent extends ListWithDetails implements OnInit { { name: $localize`Username`, prop: 'uid', - flexGrow: 1 + flexGrow: 1, + cellTemplate: this.usernameTpl }, { name: $localize`Tenant`, @@ -77,8 +79,7 @@ export class RgwUserListComponent extends ListWithDetails implements OnInit { { name: $localize`Account name`, prop: 'account.name', - flexGrow: 1, - cellTemplate: this.accountTmpl + flexGrow: 1 }, { name: $localize`Full name`, 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 c7156140034dd..c79ed0dfb694e 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 @@ -97,6 +97,7 @@ import ArrowDownIcon from '@carbon/icons/es/arrow--down/16'; import ProgressBarRoundIcon from '@carbon/icons/es/progress-bar--round/32'; import ToolsIcon from '@carbon/icons/es/tools/32'; import ParentChild from '@carbon/icons/es/parent-child/20'; +import UserAccessLocked from '@carbon/icons/es/user--access-locked/16'; import { CephSharedModule } from '../shared/ceph-shared.module'; import { RgwUserAccountsComponent } from './rgw-user-accounts/rgw-user-accounts.component'; @@ -234,7 +235,8 @@ export class RgwModule { ArrowDownIcon, ProgressBarRoundIcon, ToolsIcon, - ParentChild + ParentChild, + UserAccessLocked ]); } } @@ -332,7 +334,7 @@ const routes: Routes = [ data: { breadcrumbs: ActionLabels.CREATE } }, { - path: `${URLVerbs.EDIT}/:bid`, + path: `${URLVerbs.EDIT}/:bid/:owner`, component: RgwBucketFormComponent, data: { breadcrumbs: ActionLabels.EDIT } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/utils/constants.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/utils/constants.ts index a369498cf70ba..f87e79a0e5d24 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/utils/constants.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/utils/constants.ts @@ -1 +1,11 @@ export const RGW = 'rgw'; + +export enum ManagedPolicyName { + AmazonS3FullAccess = 'AmazonS3FullAccess', + AmazonS3ReadOnlyAccess = 'AmazonS3ReadOnlyAccess' +} + +export const ManagedPolicyArnMap: Record = { + [ManagedPolicyName.AmazonS3FullAccess]: 'arn:aws:iam::aws:policy/AmazonS3FullAccess', + [ManagedPolicyName.AmazonS3ReadOnlyAccess]: 'arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess' +}; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts index c691085a4aab6..118467619de09 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts @@ -69,6 +69,9 @@ export class RgwUserService { create(args: Record) { return this.rgwDaemonService.request((params: HttpParams) => { _.keys(args).forEach((key) => { + if (typeof args[key] === 'object') { + args[key] = JSON.stringify(args[key]); + } params = params.append(key, args[key]); }); return this.http.post(this.url, null, { params: params }); @@ -78,6 +81,9 @@ export class RgwUserService { update(uid: string, args: Record) { return this.rgwDaemonService.request((params: HttpParams) => { _.keys(args).forEach((key) => { + if (typeof args[key] === 'object') { + args[key] = JSON.stringify(args[key]); + } params = params.append(key, args[key]); }); return this.http.put(`${this.url}/${uid}`, null, { params: params }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts index f1895743c7945..73d2acd39524c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts @@ -84,6 +84,7 @@ export enum Icons { parentChild = 'parent-child', dataTable = 'data-table', idea = 'idea', + userAccessLocked = 'user--access-locked', // User access locked /* Icons for special effect */ size16 = '16', size20 = '20', diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index e74eff6bc5c35..4a2715e4bf020 100755 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -13999,6 +13999,7 @@ paths: tags: - RgwUser post: + description: Create a new RGW user. parameters: [] requestBody: content: @@ -14009,6 +14010,8 @@ paths: type: string account_id: type: integer + account_policies: + type: integer account_root_user: default: false type: integer @@ -14182,6 +14185,7 @@ paths: tags: - RgwUser put: + description: Update an existing RGW user. parameters: - in: path name: uid @@ -14195,6 +14199,8 @@ paths: properties: account_id: type: integer + account_policies: + type: integer account_root_user: default: false type: integer diff --git a/src/pybind/mgr/dashboard/services/rgw_iam.py b/src/pybind/mgr/dashboard/services/rgw_iam.py index 31cefd3a026ca..ed1bfe09581d2 100644 --- a/src/pybind/mgr/dashboard/services/rgw_iam.py +++ b/src/pybind/mgr/dashboard/services/rgw_iam.py @@ -42,3 +42,43 @@ class RgwAccounts: '--account-id', account_id] return cls.send_rgw_cmd(set_quota_status_cmd) + + @classmethod + def attach_managed_policy(cls, userId, policy_arn): + radosgw_attach_managed_policies = ['user', 'policy', 'attach', + '--uid', userId, '--policy-arn', policy_arn] + try: + exit_code, _, err = mgr.send_rgwadmin_command(radosgw_attach_managed_policies, + stdout_as_json=False) + if exit_code > 0: + raise DashboardException(e=err, msg='Unable to attach managed policies', + http_status_code=500, component='rgw') + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + @classmethod + def detach_managed_policy(cls, userId, policy_arn): + radosgw_detach_managed_policy = ['user', 'policy', 'detach', + '--uid', userId, '--policy-arn', policy_arn] + try: + exit_code, _, err = mgr.send_rgwadmin_command(radosgw_detach_managed_policy, + stdout_as_json=False) + if exit_code > 0: + raise DashboardException(e=err, msg='Unable to detach managed policies', + http_status_code=500, component='rgw') + + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + @classmethod + def list_managed_policy(cls, userId): + radosgw_list_managed_policies = ['user', 'policy', 'list', 'attached', + '--uid', userId] + try: + exit_code, out, err = mgr.send_rgwadmin_command(radosgw_list_managed_policies) + if exit_code > 0: + raise DashboardException(e=err, msg='Unable to get managed policies', + http_status_code=500, component='rgw') + return out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') -- 2.39.5