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 1e2d791fe0b..bbbf5b971ad 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 c4351a783b2..6ba9e386e6f 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 5d552a87773..1372db06196 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 b3b6f0f35fb..15790fd2f8e 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 dae12f99a28..4248c5ff2a2 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 8d05bb3b314..c9d7719d6b6 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 c308bc6d94b..4267f6c03b6 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 dd2f6d35e20..f95134494d9 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 c38780ee32d..172aaa747a7 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 fc4f15d2c1e..a46a3c5cc3f 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 361a1e30b66..daeff590c5f 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 e69de29bb2d..b13eb5c0ed3 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 64f5e72bae1..80e29fb8a31 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 c7156140034..c79ed0dfb69 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 a369498cf70..f87e79a0e5d 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 c691085a4aa..118467619de 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 f1895743c79..73d2acd3952 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 e74eff6bc5c..4a2715e4bf0 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 31cefd3a026..ed1bfe09581 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