]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: user accounts enhancements
authorNaman Munet <naman.munet@ibm.com>
Tue, 22 Jul 2025 17:08:42 +0000 (22:38 +0530)
committerNaman Munet <naman.munet@ibm.com>
Wed, 6 Aug 2025 07:20:48 +0000 (12:50 +0530)
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 <naman.munet@ibm.com>
(cherry picked from commit 6e9db91d1ca614e1414b7af27a8abdcd86d05c01)

 Conflicts:
src/pybind/mgr/dashboard/frontend/package-lock.json
src/pybind/mgr/dashboard/frontend/package.json
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts

20 files changed:
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/package-lock.json
src/pybind/mgr/dashboard/frontend/package.json
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/utils/constants.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_iam.py

index 7a45907dd73b19abceed423aded0860c6781e3a5..acce94cd425fa1901d6e8b766e63297efac8e9d2 100755 (executable)
@@ -545,6 +545,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]]]:
@@ -552,9 +587,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
 
@@ -899,6 +934,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
 
@@ -917,53 +955,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):
index 7aa78eceb473479f811f684b8f6d8f916decb5ba..0535d109d491c23ba3d9a6801f8ede6839ce706a 100644 (file)
@@ -18,7 +18,7 @@
         "@angular/platform-browser": "18.2.11",
         "@angular/platform-browser-dynamic": "18.2.11",
         "@angular/router": "18.2.11",
-        "@carbon/icons": "11.41.0",
+        "@carbon/icons": "11.63.0",
         "@carbon/styles": "1.57.0",
         "@ibm/plex": "6.4.0",
         "@ng-bootstrap/ng-bootstrap": "17.0.1",
       "integrity": "sha512-YXed2JUSCGddp3UnY5OffR3W8Pl+dy9a+vfUtYhSLH9TbIEBR6EvYIfvruFMhA8JIVMCUClUqgyMQXM5oMFQ0g=="
     },
     "node_modules/@carbon/icons": {
-      "version": "11.41.0",
-      "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,
       "dependencies": {
         "@ibm/telemetry-js": "^1.5.0"
index 6e816f768550405cb01ecb560bd2e601321da180..4bfcb26ee6f60b5aae73615255e73049b9d2e4b1 100644 (file)
@@ -51,7 +51,7 @@
     "@angular/platform-browser": "18.2.11",
     "@angular/platform-browser-dynamic": "18.2.11",
     "@angular/router": "18.2.11",
-    "@carbon/icons": "11.41.0",
+    "@carbon/icons": "11.63.0",
     "@carbon/styles": "1.57.0",
     "@ibm/plex": "6.4.0",
     "@ng-bootstrap/ng-bootstrap": "17.0.1",
index b3b6f0f35fb1ecd8df1e7bbb952941b30dc5c458..15790fd2f8e4f456f729964c8f6e22486b8dc950 100644 (file)
@@ -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;
index dae12f99a288962afb91f1d822fbca9343f45621..4248c5ff2a2f0061c202e9319760f16b524be655 100644 (file)
@@ -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,
index 8d05bb3b314bb495cf99b7266319e975c26b0202..c9d7719d6b6f478cdc3170a3332b71d0651cee28 100644 (file)
               class="bold">Maximum buckets</td>
           <td>{{ user.max_buckets | map: maxBucketsMap }}</td>
         </tr>
+        @if (user.type === 'rgw' && selection.account?.id){
+        <tr>
+          <td i18n
+              class="bold">Managed policies</td>
+          <td i18n>{{ extractPolicyNamesFromArns(user.managed_user_policies) }}</td>
+        </tr>
+        }
         <tr *ngIf="user.subusers && user.subusers.length">
           <td i18n
               class="bold">Subusers</td>
index c308bc6d94b161011dc42be3790c14aef05dc6f6..4267f6c03b67ca6bebb3729fdffdfc0864d406dd 100644 (file)
@@ -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 = {
index dd2f6d35e20962d2787e458a7905202eb0b55222..f95134494d9912c2c2c3f0ed72b0086d4cc700e0 100644 (file)
@@ -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(', ');
+  }
 }
index 58a6fee7eedd1235e450d3beb6ba2269af9a6209..eb9832198948d10a6a8e136f273e4efe2589785a 100644 (file)
@@ -23,7 +23,7 @@
                   [ngValue]="null">Loading...</option>
           <option i18n
                   *ngIf="accounts !== null"
-                  [ngValue]="null">-- Select an Account --</option>
+                  [value]="''">-- Select an Account --</option>
           <option *ngFor="let account of accounts"
                   [value]="account.id">{{ account.name }} {{account.tenant ? '- '+account.tenant : ''}}</option>
         </cds-select>
       </cds-checkbox>
     </div>
 
+    @if(userForm.getValue('account_id') && !userForm.getValue('account_root_user')) {
+    <!-- Managed policies -->
+    <fieldset>
+      <div class="form-item">
+        <legend i18n
+                class="cd-header">Managed policies</legend>
+        <cds-combo-box label="Policies"
+                       type="multi"
+                       selectionFeedback="top-after-reopen"
+                       formControlName="account_policies"
+                       id="account_policies"
+                       placeholder="Select managed policies..."
+                       i18n-placeholder
+                       [appendInline]="true"
+                       [items]="managedPolicies"
+                       (selected)="multiSelector($event)"
+                       itemValueKey="name"
+                       i18n-label
+                       i18n>
+          <cds-dropdown-list></cds-dropdown-list>
+        </cds-combo-box>
+      </div>
+    </fieldset>
+    }
+
     <!-- S3 key -->
     <fieldset *ngIf="!editing">
       <legend i18n
index efdf125ad9ecbf62fb45dee60cf2e9d7ec9adfd1..915bd38e9ed95b5a240791173715aae1d8ab3912 100644 (file)
@@ -230,9 +230,7 @@ describe('RgwUserFormComponent', () => {
         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
       });
     });
 
index fc4f15d2c1e19d6e9820307c308d10bdc935de3c..a46a3c5cc3f00abe4b7366973b7c674f53d5c913 100644 (file)
@@ -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<string, any> = {};
-    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
index 361a1e30b66223f4fb92bf9aacdf3fca77c4e198..daeff590c5ffd8ba7eaeccf97c5aff398489a208 100644 (file)
                i18n>No Limit</ng-template>
 </ng-template>
 
-<ng-template #accountTmpl
+<ng-template #usernameTpl
              let-row="data.row">
-  <cds-tooltip [description]="row.account?.name ? (row.type === 'root' ? 'Account root user' :'') : ''"
-               [align]="'top'"
-               i18n-description
-               i18n>{{row.account?.name}}</cds-tooltip>
+  <div cdsRow>
+    <span i18n>{{ row.uid }}</span>
+    @if (row.type === 'root') {
+    <cds-tooltip [description]="'Account root user'"
+                 [align]="'top'"
+                 i18n-description>
+      <svg [cdsIcon]="icons.userAccessLocked"
+           [size]="icons.size16"
+           class="account-root-icon"></svg>
+    </cds-tooltip>
+    }
+  </div>
 </ng-template>
index 64f5e72bae11722419b79ac16e9dd8214e9a0c91..80e29fb8a318c6cd8630999d438b2624153329bc 100644 (file)
@@ -38,8 +38,8 @@ export class RgwUserListComponent extends ListWithDetails implements OnInit {
   userSizeTpl: TemplateRef<any>;
   @ViewChild('userObjectTpl', { static: true })
   userObjectTpl: TemplateRef<any>;
-  @ViewChild('accountTmpl', { static: true })
-  public accountTmpl: TemplateRef<any>;
+  @ViewChild('usernameTpl', { static: true })
+  usernameTpl: TemplateRef<any>;
   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`,
index c6c237fa5c60e2e97662f94254ef781671ff8214..440f751ec953bc8e431231783b7b8a3d484ba337 100644 (file)
@@ -83,8 +83,22 @@ import {
   TooltipModule,
   ComboBoxModule,
   ToggletipModule,
-  LayoutModule
+  LayoutModule,
+  IconService
 } from 'carbon-components-angular';
+import EditIcon from '@carbon/icons/es/edit/16';
+import ScalesIcon from '@carbon/icons/es/scales/20';
+import UserIcon from '@carbon/icons/es/user/16';
+import CubeIcon from '@carbon/icons/es/cube/20';
+import ShareIcon from '@carbon/icons/es/share/16';
+import ViewIcon from '@carbon/icons/es/view/16';
+import PasswordIcon from '@carbon/icons/es/password/16';
+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';
 import { RgwUserAccountsFormComponent } from './rgw-user-accounts-form/rgw-user-accounts-form.component';
@@ -208,7 +222,24 @@ import { RgwNotificationFormComponent } from './rgw-notification-form/rgw-notifi
   ],
   providers: [TitleCasePipe]
 })
-export class RgwModule {}
+export class RgwModule {
+  constructor(private iconService: IconService) {
+    this.iconService.registerAll([
+      EditIcon,
+      ScalesIcon,
+      CubeIcon,
+      UserIcon,
+      ShareIcon,
+      ViewIcon,
+      PasswordIcon,
+      ArrowDownIcon,
+      ProgressBarRoundIcon,
+      ToolsIcon,
+      ParentChild,
+      UserAccessLocked
+    ]);
+  }
+}
 
 const routes: Routes = [
   {
@@ -303,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 }
       }
index a369498cf70babe843961b020b99af13852f8ca7..f87e79a0e5d240115e38cf2562f4507466e196ba 100644 (file)
@@ -1 +1,11 @@
 export const RGW = 'rgw';
+
+export enum ManagedPolicyName {
+  AmazonS3FullAccess = 'AmazonS3FullAccess',
+  AmazonS3ReadOnlyAccess = 'AmazonS3ReadOnlyAccess'
+}
+
+export const ManagedPolicyArnMap: Record<ManagedPolicyName, string> = {
+  [ManagedPolicyName.AmazonS3FullAccess]: 'arn:aws:iam::aws:policy/AmazonS3FullAccess',
+  [ManagedPolicyName.AmazonS3ReadOnlyAccess]: 'arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess'
+};
index c691085a4aab6cfd52b7af294ccaece23605499c..118467619de09c771553526f8b9962ab41d4c33b 100644 (file)
@@ -69,6 +69,9 @@ export class RgwUserService {
   create(args: Record<string, any>) {
     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<string, any>) {
     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 });
index 74a474730d63bad58cd0f62868dae20deffc9390..1938e325378b62222ea71caeb5b6253e189ee3a8 100644 (file)
@@ -85,7 +85,7 @@ export enum Icons {
   calendar = 'fa fa-calendar',
   externalUrl = 'fa fa-external-link', // links to external page
   nfsExport = 'fa fa-server', // NFS export
-
+  userAccessLocked = 'user--access-locked', // User access locked
   /* Icons for special effect */
   large = 'fa fa-lg', // icon becomes 33% larger
   large2x = 'fa fa-2x', // icon becomes 50% larger
index 75adda059a917458f7b58878be941197b45df3b3..16d1e6ab51a150ae16034bac73be7475a84c2555 100755 (executable)
@@ -13994,6 +13994,7 @@ paths:
       tags:
       - RgwUser
     post:
+      description: Create a new RGW user.
       parameters: []
       requestBody:
         content:
@@ -14004,6 +14005,8 @@ paths:
                   type: string
                 account_id:
                   type: integer
+                account_policies:
+                  type: integer
                 account_root_user:
                   default: false
                   type: integer
@@ -14177,6 +14180,7 @@ paths:
       tags:
       - RgwUser
     put:
+      description: Update an existing RGW user.
       parameters:
       - in: path
         name: uid
@@ -14190,6 +14194,8 @@ paths:
               properties:
                 account_id:
                   type: integer
+                account_policies:
+                  type: integer
                 account_root_user:
                   default: false
                   type: integer
index 31cefd3a026cac4e4f719bfed384ac383fb75a15..ed1bfe09581d27003f0054490ec5db6d5812f2da 100644 (file)
@@ -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')