]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: link user to rgw account & add root account user functionality 61362/head
authorNaman Munet <naman.munet@ibm.com>
Tue, 14 Jan 2025 08:21:57 +0000 (13:51 +0530)
committerNaman Munet <naman.munet@ibm.com>
Fri, 28 Mar 2025 10:46:56 +0000 (16:16 +0530)
Fixes: https://tracker.ceph.com/issues/69529
Signed-off-by: Naman Munet <naman.munet@ibm.com>
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user.ts [new file with mode: 0644]
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-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.ts
src/pybind/mgr/dashboard/openapi.yaml

index 746bd1c5d1a37665704dfa7a93668a8624b5c661..e6edd3d5b2f88ab5984e8c6b431d2964cc414687 100755 (executable)
@@ -851,7 +851,8 @@ class RgwUser(RgwRESTController):
     @allow_empty_body
     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):
+               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
@@ -869,13 +870,18 @@ class RgwUser(RgwRESTController):
             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
+        if account_root_user:
+            params['account-root'] = account_root_user
         result = self.proxy(daemon_name, 'PUT', 'user', params)
         result['uid'] = result['full_user_id']
         return result
 
     @allow_empty_body
     def set(self, uid, display_name=None, email=None, max_buckets=None,
-            system=None, suspended=None, daemon_name=None):
+            system=None, suspended=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
@@ -887,6 +893,10 @@ class RgwUser(RgwRESTController):
             params['system'] = system
         if suspended is not None:
             params['suspended'] = suspended
+        if account_id is not None:
+            params['account-id'] = account_id
+        if account_root_user:
+            params['account-root'] = account_root_user
         result = self.proxy(daemon_name, 'POST', 'user', params)
         result['uid'] = result['full_user_id']
         return result
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user.ts
new file mode 100644 (file)
index 0000000..573dd5b
--- /dev/null
@@ -0,0 +1,80 @@
+interface Key {
+  access_key: string;
+  active: boolean;
+  secret_key: string;
+  user: string;
+}
+
+interface SwiftKey {
+  active: boolean;
+  secret_key: string;
+  user: string;
+}
+
+interface Cap {
+  perm: string;
+  type: string;
+}
+
+interface Subuser {
+  id: string;
+  permissions: string;
+}
+
+interface BucketQuota {
+  check_on_raw: boolean;
+  enabled: boolean;
+  max_objects: number;
+  max_size: number;
+  max_size_kb: number;
+}
+
+interface UserQuota {
+  check_on_raw: boolean;
+  enabled: boolean;
+  max_objects: number;
+  max_size: number;
+  max_size_kb: number;
+}
+
+interface Stats {
+  num_objects: number;
+  size: number;
+  size_actual: number;
+  size_utilized: number;
+  size_kb: number;
+  size_kb_actual: number;
+  size_kb_utilized: number;
+}
+
+export interface RgwUser {
+  account_id: string;
+  admin: boolean;
+  bucket_quota: BucketQuota;
+  caps: Cap[];
+  create_date: string;
+  default_placement: string;
+  default_storage_class: string;
+  display_name: string;
+  email: string;
+  full_user_id: string;
+  group_ids: any[];
+  keys: Key[];
+  max_buckets: number;
+  mfa_ids: any[];
+  op_mask: string;
+  path: string;
+  placement_tags: any[];
+  stats: Stats;
+  subusers: Subuser[];
+  suspended: number;
+  swift_keys: SwiftKey[];
+  system: boolean;
+  tags: any[];
+  tenant: string;
+  temp_url_keys: any[];
+  type: string;
+  uid: string;
+  user_id: string;
+  user_quota: UserQuota;
+}
index 055dd54f13f7f16783ce3fb77ff34b1116d4ce7d..e0defa08fb89dde9d1b6aa8b0ccb2c888b0602a4 100644 (file)
       </tbody>
     </table>
 
+    <ng-container *ngIf="selection.account && selection.account?.id">
+      <legend i18n>Account Details</legend>
+      <table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md">
+        <tbody>
+          <tr>
+            <td i18n
+                class="bold w-25">Account ID</td>
+            <td class="w-75">{{ selection.account?.id }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold w-25">Name</td>
+            <td class="w-75">{{ selection.account?.name }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold w-25">Tenant</td>
+            <td class="w-75">{{ selection.account?.tenant || '-'}}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold w-25">User type</td>
+            <td class="w-75"
+                i18n>{{ user?.type === 'root' ? 'Account root user' : 'rgw user' }}</td>
+          </tr>
+        </tbody>
+      </table>
+    </ng-container>
+
     <!-- User quota -->
     <div *ngIf="user.user_quota">
       <legend i18n>User quota</legend>
index 8863576311750b35836f6764f8eb9ea3e6d1afe0..053a30f0c581b632ed7ba6b2518fb900d6010153 100644 (file)
@@ -7,6 +7,49 @@
     <div i18n="form title"
          class="form-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
 
+    @if(accounts.length > 0){
+      <!-- Link Account -->
+      <div class="form-item">
+        <cds-select label="Link Account"
+                    i18n-label
+                    for="link_account"
+                    formControlName="account_id"
+                    [invalid]="userForm.controls.account_id.invalid && userForm.controls.account_id.dirty"
+                    [invalidText]="accountError"
+                    [helperText]="accountsHelper">
+          <option i18n
+                  *ngIf="accounts === null"
+                  [ngValue]="null">Loading...</option>
+          <option i18n
+                  *ngIf="accounts !== null"
+                  [ngValue]="null">-- Select an Account --</option>
+          <option *ngFor="let account of accounts"
+                  [value]="account.id">{{ account.name }} {{account.tenant ? '- '+account.tenant : ''}}</option>
+        </cds-select>
+        <ng-template #accountError>
+          <span class="invalid-feedback"
+                *ngIf="userForm.showError('account_id', frm, 'tenantedAccount')"
+                i18n>Only accounts with the same tenant name can be linked to a tenanted user.</span>
+        </ng-template>
+        <ng-template #accountsHelper>
+          <div i18n>Account membership is permanent. Once added, users cannot be removed from their account.</div>
+          <div i18n>Ownership of all of the user's buckets will be transferred to the account.</div>
+        </ng-template>
+      </div>
+
+      <!-- Account Root user -->
+      <div *ngIf="userForm.getValue('account_id')"
+           class="form-item">
+        <cds-checkbox formControlName="account_root_user"
+                      id="account_root_user"
+                      i18n>Account Root user
+          <cd-help-text>The account root user has full access to all resources and manages the account.
+            It's recommended to use this account for management tasks only and create additional users with specific permissions.
+          </cd-help-text>
+        </cds-checkbox>
+      </div>
+    }
+
     <!-- User ID -->
     <div class="form-item">
       <cds-text-label for="user_id"
       </ng-template>
     </div>
 
-      <!-- Show Tenant -->
-      <div class="form-item">
-        <cds-checkbox formControlName="show_tenant"
-                      id="show_tenant"
-                      [readonly]="true"
-                      (checkedChange)="updateFieldsWhenTenanted()">Show Tenant
-        </cds-checkbox>
-      </div>
+    <!-- Show Tenant -->
+    <div class="form-item">
+      <cds-checkbox formControlName="show_tenant"
+                    id="show_tenant"
+                    [readonly]="true"
+                    (checkedChange)="updateFieldsWhenTenanted()">Show Tenant
+      </cds-checkbox>
+    </div>
 
     <!-- Tenant -->
     <div class="form-item"
index 933c0094823c4c0f05ad4d7277ede50218c66e95..a1d740e6a484141e3abe4508120a02f4e1993453 100644 (file)
@@ -23,6 +23,7 @@ import { FormatterService } from '~/app/shared/services/formatter.service';
 import { RgwRateLimitComponent } from '../rgw-rate-limit/rgw-rate-limit.component';
 import { By } from '@angular/platform-browser';
 import { CheckboxModule, NumberModule, SelectModule } from 'carbon-components-angular';
+import { LoadingStatus } from '~/app/shared/forms/cd-form';
 
 describe('RgwUserFormComponent', () => {
   let component: RgwUserFormComponent;
@@ -185,6 +186,7 @@ describe('RgwUserFormComponent', () => {
 
   describe('max buckets', () => {
     beforeEach(() => {
+      component.loading = LoadingStatus.Ready;
       fixture.detectChanges();
       childComponent = fixture.debugElement.query(By.directive(RgwRateLimitComponent))
         .componentInstance;
@@ -203,7 +205,9 @@ describe('RgwUserFormComponent', () => {
         secret_key: '',
         suspended: false,
         system: false,
-        uid: null
+        uid: null,
+        account_id: '',
+        account_root_user: false
       });
       expect(spyRateLimit).toHaveBeenCalled();
     });
@@ -219,7 +223,8 @@ describe('RgwUserFormComponent', () => {
         email: null,
         max_buckets: -1,
         suspended: false,
-        system: false
+        system: false,
+        account_root_user: false
       });
       expect(spyRateLimit).toHaveBeenCalled();
     });
@@ -238,7 +243,9 @@ describe('RgwUserFormComponent', () => {
         secret_key: '',
         suspended: false,
         system: false,
-        uid: null
+        uid: null,
+        account_id: '',
+        account_root_user: false
       });
       expect(spyRateLimit).toHaveBeenCalled();
     });
@@ -254,7 +261,8 @@ describe('RgwUserFormComponent', () => {
         email: null,
         max_buckets: 0,
         suspended: false,
-        system: false
+        system: false,
+        account_root_user: false
       });
       expect(spyRateLimit).toHaveBeenCalled();
     });
@@ -264,6 +272,7 @@ describe('RgwUserFormComponent', () => {
       formHelper.setValue('max_buckets_mode', 1, true);
       formHelper.setValue('max_buckets', 100, true);
       let spyRateLimit = jest.spyOn(childComponent, 'getRateLimitFormValue');
+
       component.onSubmit();
       expect(rgwUserService.create).toHaveBeenCalledWith({
         access_key: '',
@@ -274,7 +283,9 @@ describe('RgwUserFormComponent', () => {
         secret_key: '',
         suspended: false,
         system: false,
-        uid: null
+        uid: null,
+        account_id: '',
+        account_root_user: false
       });
       expect(spyRateLimit).toHaveBeenCalled();
     });
@@ -291,7 +302,8 @@ describe('RgwUserFormComponent', () => {
         email: null,
         max_buckets: 100,
         suspended: false,
-        system: false
+        system: false,
+        account_root_user: false
       });
       expect(spyRateLimit).toHaveBeenCalled();
     });
@@ -301,6 +313,7 @@ describe('RgwUserFormComponent', () => {
     let notificationService: NotificationService;
 
     beforeEach(() => {
+      component.loading = LoadingStatus.Ready;
       spyOn(TestBed.inject(Router), 'navigate').and.stub();
       notificationService = TestBed.inject(NotificationService);
       spyOn(notificationService, 'show');
@@ -320,7 +333,8 @@ describe('RgwUserFormComponent', () => {
         email: '',
         max_buckets: 1000,
         suspended: false,
-        system: false
+        system: false,
+        account_root_user: false
       });
     });
 
@@ -348,6 +362,9 @@ describe('RgwUserFormComponent', () => {
   });
 
   describe('RgwUserCapabilities', () => {
+    beforeEach(() => {
+      component.loading = LoadingStatus.Ready;
+    });
     it('capability button disabled when all capabilities are added', () => {
       component.editing = true;
       for (const capabilityType of RgwUserCapabilities.getAll()) {
@@ -669,4 +686,49 @@ describe('RgwUserFormComponent', () => {
       expect(modalRef.submitAction.subscribe).toHaveBeenCalled();
     });
   });
+
+  describe('RgwUserAccounts', () => {
+    beforeEach(() => {
+      component.loading = LoadingStatus.Ready;
+      fixture.detectChanges();
+      childComponent = fixture.debugElement.query(By.directive(RgwRateLimitComponent))
+        .componentInstance;
+    });
+    it('create with account id & account root user', () => {
+      spyOn(rgwUserService, 'create');
+      formHelper.setValue('account_id', 'RGW12312312312312312', true);
+      formHelper.setValue('account_root_user', true, true);
+      component.onSubmit();
+      expect(rgwUserService.create).toHaveBeenCalledWith({
+        access_key: '',
+        display_name: null,
+        email: '',
+        generate_key: true,
+        max_buckets: 1000,
+        secret_key: '',
+        suspended: false,
+        system: false,
+        uid: null,
+        account_id: 'RGW12312312312312312',
+        account_root_user: true
+      });
+    });
+
+    it('edit to link account to existing user', () => {
+      spyOn(rgwUserService, 'update');
+      component.editing = true;
+      formHelper.setValue('account_id', 'RGW12312312312312312', true);
+      formHelper.setValue('account_root_user', true, true);
+      component.onSubmit();
+      expect(rgwUserService.update).toHaveBeenCalledWith(null, {
+        display_name: null,
+        email: null,
+        max_buckets: 1000,
+        suspended: false,
+        system: false,
+        account_id: 'RGW12312312312312312',
+        account_root_user: true
+      });
+    });
+  });
 });
index 9cf8741e3539449883695cd6a521c0f893a78c6a..f0783324c2451d3f642854151d6185e4f0a0398d 100644 (file)
@@ -27,6 +27,9 @@ import { RgwUserSwiftKeyModalComponent } from '../rgw-user-swift-key-modal/rgw-u
 import { RgwRateLimitComponent } from '../rgw-rate-limit/rgw-rate-limit.component';
 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';
 
 @Component({
   selector: 'cd-rgw-user-form',
@@ -52,6 +55,7 @@ export class RgwUserFormComponent extends CdForm implements OnInit {
   showTenant = false;
   previousTenant: string = null;
   @ViewChild(RgwRateLimitComponent, { static: false }) rateLimitComponent!: RgwRateLimitComponent;
+  accounts: Account[] = [];
 
   constructor(
     private formBuilder: CdFormBuilder,
@@ -60,7 +64,8 @@ export class RgwUserFormComponent extends CdForm implements OnInit {
     private rgwUserService: RgwUserService,
     private modalService: ModalCdsService,
     private notificationService: NotificationService,
-    public actionLabels: ActionLabelsI18n
+    public actionLabels: ActionLabelsI18n,
+    private rgwUserAccountService: RgwUserAccountsService
   ) {
     super();
     this.resource = $localize`user`;
@@ -110,6 +115,8 @@ export class RgwUserFormComponent extends CdForm implements OnInit {
         [CdValidators.email],
         [CdValidators.unique(this.rgwUserService.emailExists, this.rgwUserService)]
       ],
+      account_id: [null, [this.tenantedAccountValidator.bind(this)]],
+      account_root_user: [false],
       max_buckets_mode: [1],
       max_buckets: [
         1000,
@@ -178,7 +185,6 @@ export class RgwUserFormComponent extends CdForm implements OnInit {
     // Process route parameters.
     this.route.params.subscribe((params: { uid: string }) => {
       if (!params.hasOwnProperty('uid')) {
-        this.loadingReady();
         return;
       }
       const uid = decodeURIComponent(params.uid);
@@ -232,6 +238,13 @@ export class RgwUserFormComponent extends CdForm implements OnInit {
           // Update the form.
           this.userForm.setValue(value);
 
+          if (this.userForm.getValue('account_id')) {
+            this.userForm.get('account_id').disable();
+          } else {
+            this.userForm.get('account_id').setValue(null);
+          }
+          const isAccountRoot: boolean = resp[0]['type'] !== RGW;
+          this.userForm.get('account_root_user').setValue(isAccountRoot);
           // Get the sub users.
           this.subusers = resp[0].subusers;
 
@@ -248,13 +261,54 @@ export class RgwUserFormComponent extends CdForm implements OnInit {
           });
           this.capabilities = resp[0].caps;
           this.uid = this.getUID();
-          this.loadingReady();
         },
         () => {
           this.loadingError();
         }
       );
     });
+    this.rgwUserAccountService.list(true).subscribe(
+      (accounts: Account[]) => {
+        this.accounts = accounts;
+        if (!this.editing) {
+          // needed to disable checkbox on create form load
+          this.userForm.get('account_id').reset();
+        }
+        this.loadingReady();
+      },
+      () => {
+        this.loadingError();
+      }
+    );
+    this.userForm.get('account_id').valueChanges.subscribe((value) => {
+      if (!value) {
+        this.userForm
+          .get('display_name')
+          .setValidators([Validators.pattern(/^[a-zA-Z0-9!@#%^&*()._ -]+$/), Validators.required]);
+        this.userForm.get('display_name').updateValueAndValidity();
+        this.userForm.get('account_root_user').disable();
+      } else {
+        this.userForm
+          .get('display_name')
+          .setValidators([Validators.pattern(/^[\w+=,.@-]+$/), Validators.required]);
+        this.userForm.get('display_name').updateValueAndValidity();
+        this.userForm.get('account_root_user').enable();
+      }
+    });
+  }
+
+  tenantedAccountValidator(control: AbstractControl): ValidationErrors | null {
+    if (this?.userForm?.getValue('tenant') && this.accounts.length > 0) {
+      const index: number = this.accounts.findIndex(
+        (account: Account) => account.id === control.value
+      );
+      if (index !== -1) {
+        return this.userForm.getValue('tenant') !== this.accounts[index].tenant
+          ? { tenantedAccount: true }
+          : null;
+      }
+    }
+    return null;
   }
 
   rateLimitFormInit(rateLimitForm: FormGroup) {
@@ -593,11 +647,18 @@ export class RgwUserFormComponent extends CdForm implements OnInit {
    * @return {Boolean} Returns TRUE if the general user settings have been modified.
    */
   private _isGeneralDirty(): boolean {
-    return ['display_name', 'email', 'max_buckets_mode', 'max_buckets', 'system', 'suspended'].some(
-      (path) => {
-        return this.userForm.get(path).dirty;
-      }
-    );
+    return [
+      'display_name',
+      'email',
+      'max_buckets_mode',
+      'max_buckets',
+      'system',
+      'suspended',
+      'account_id',
+      'account_root_user'
+    ].some((path) => {
+      return this.userForm.get(path).dirty;
+    });
   }
 
   /**
@@ -639,6 +700,8 @@ 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'),
@@ -675,10 +738,20 @@ export class RgwUserFormComponent extends CdForm implements OnInit {
    */
   private _getUpdateArgs() {
     const result: Record<string, any> = {};
-    const keys = ['display_name', 'email', 'max_buckets', 'system', 'suspended'];
+    const keys = [
+      'display_name',
+      'email',
+      'max_buckets',
+      'system',
+      'suspended',
+      'account_root_user'
+    ];
     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');
+    }
     const maxBucketsMode = parseInt(this.userForm.getValue('max_buckets_mode'), 10);
     if (_.includes([-1, 0], maxBucketsMode)) {
       // -1 => Disable bucket creation.
@@ -770,4 +843,8 @@ export class RgwUserFormComponent extends CdForm implements OnInit {
       }
     }
   }
+
+  goToCreateAccountForm() {
+    this.router.navigate(['rgw/accounts/create']);
+  }
 }
index 0bb5726726242df57a002e7e349b505c56aac379..44c0017a9f39da136e6c184394d6777223896058 100644 (file)
   <ng-template #noObjectQuota
                i18n>No Limit</ng-template>
 </ng-template>
+
+<ng-template #accountTmpl
+             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>
+</ng-template>
index f44e8f8c2cd03fc247c9e5826ebea28c04e03bc3..64f5e72bae11722419b79ac16e9dd8214e9a0c91 100644 (file)
@@ -1,6 +1,7 @@
 import { Component, NgZone, OnInit, TemplateRef, ViewChild } from '@angular/core';
 
-import { forkJoin as observableForkJoin, Observable, Subscriber } from 'rxjs';
+import { forkJoin as observableForkJoin, Observable, Subscriber, Subject } from 'rxjs';
+import { RgwUserAccountsService } from '~/app/shared/api/rgw-user-accounts.service';
 
 import { RgwUserService } from '~/app/shared/api/rgw-user.service';
 import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
@@ -18,6 +19,9 @@ import { Permission } from '~/app/shared/models/permissions';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
 import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { Account } from '../models/rgw-user-accounts';
+import { switchMap } from 'rxjs/operators';
+import { RgwUser } from '../models/rgw-user';
 
 const BASE_URL = 'rgw/user';
 
@@ -34,11 +38,15 @@ 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>;
   permission: Permission;
   tableActions: CdTableAction[];
   columns: CdTableColumn[] = [];
   users: object[] = [];
+  userAccounts: Account[];
   selection: CdTableSelection = new CdTableSelection();
+  userDataSubject = new Subject();
   declare staleTimeout: number;
 
   constructor(
@@ -47,7 +55,8 @@ export class RgwUserListComponent extends ListWithDetails implements OnInit {
     private modalService: ModalCdsService,
     private urlBuilder: URLBuilderService,
     public actionLabels: ActionLabelsI18n,
-    protected ngZone: NgZone
+    protected ngZone: NgZone,
+    private rgwUserAccountService: RgwUserAccountsService
   ) {
     super(ngZone);
   }
@@ -65,6 +74,12 @@ export class RgwUserListComponent extends ListWithDetails implements OnInit {
         prop: 'tenant',
         flexGrow: 1
       },
+      {
+        name: $localize`Account name`,
+        prop: 'account.name',
+        flexGrow: 1,
+        cellTemplate: this.accountTmpl
+      },
       {
         name: $localize`Full name`,
         prop: 'display_name',
@@ -105,6 +120,17 @@ export class RgwUserListComponent extends ListWithDetails implements OnInit {
         flexGrow: 0.8
       }
     ];
+    this.userDataSubject
+      .pipe(
+        switchMap((_: object[]) => {
+          return this.rgwUserAccountService.list(true);
+        })
+      )
+      .subscribe((accounts: Account[]) => {
+        this.userAccounts = accounts;
+        this.mapUsersWithAccount();
+      });
+
     const getUserUri = () =>
       this.selection.first() && `${encodeURIComponent(this.selection.first().uid)}`;
     const addAction: CdTableAction = {
@@ -136,6 +162,7 @@ export class RgwUserListComponent extends ListWithDetails implements OnInit {
     this.rgwUserService.list().subscribe(
       (resp: object[]) => {
         this.users = resp;
+        this.userDataSubject.next(resp);
       },
       () => {
         context.error();
@@ -143,6 +170,16 @@ export class RgwUserListComponent extends ListWithDetails implements OnInit {
     );
   }
 
+  mapUsersWithAccount() {
+    this.users = this.users.map((user: RgwUser) => {
+      const account: Account = this.userAccounts.find((acc) => acc.id === user.account_id);
+      return {
+        account: account ? account : { name: '' }, // adding {name: ''} for sorting account name in user list to work
+        ...user
+      };
+    });
+  }
+
   updateSelection(selection: CdTableSelection) {
     this.selection = selection;
   }
index e1e5fab12dfa14c903aece231e85f1a463ccce96..29791d7196d50fee75d441184aee51253a8cd81c 100755 (executable)
@@ -13203,6 +13203,11 @@ paths:
               properties:
                 access_key:
                   type: string
+                account_id:
+                  type: integer
+                account_root_user:
+                  default: false
+                  type: integer
                 daemon_name:
                   type: string
                 display_name:
@@ -13384,6 +13389,11 @@ paths:
           application/json:
             schema:
               properties:
+                account_id:
+                  type: integer
+                account_root_user:
+                  default: false
+                  type: integer
                 daemon_name:
                   type: string
                 display_name: