]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Splitting tenant$user when creating rgw user 40297/head
authorNizamudeen A <nia@redhat.com>
Sat, 16 Jan 2021 14:40:36 +0000 (20:10 +0530)
committerNizamudeen A <nia@redhat.com>
Sun, 21 Mar 2021 18:26:37 +0000 (23:56 +0530)
Fixes: https://tracker.ceph.com/issues/47378
Signed-off-by: Nizamudeen A <nia@redhat.com>
(cherry picked from commit 7f4387d34fd073a3b0d8c828fecdc5df4b498122)

 Conflicts:
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html
    -  Accepted the current changes and pasted the new change
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts
    -  Accepted incoming change and changed const modalRef =
       this.ModalService.show(RgwUserSubuserModalComponent); to const modalRef = this.bsModalService.show(RgwUserSubuserModalComponent);
    -  Made some modification in getUID() function to adapt with octopus
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts
    -  Accepted the current change and pasted the new change. Changed
       the $localize to this.i18n to match the octopus way

src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/users.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/users.po.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-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.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts

index 1b580db7dcfc40e0037f41bbb99a3a98bd54593f..b5f366a090935d2c3f78cb6686b51d50100a1aea 100644 (file)
@@ -2,7 +2,9 @@ import { UsersPageHelper } from './users.po';
 
 describe('RGW users page', () => {
   const users = new UsersPageHelper();
-  const user_name = 'e2e_000user_create_edit_delete';
+  const tenant = 'e2e_000tenant';
+  const user_id = 'e2e_000user_create_edit_delete';
+  const user_name = tenant + '$' + user_id;
 
   beforeEach(() => {
     cy.login();
@@ -19,8 +21,8 @@ describe('RGW users page', () => {
   describe('create, edit & delete user tests', () => {
     it('should create user', () => {
       users.navigateTo('create');
-      users.create(user_name, 'Some Name', 'original@website.com', '1200');
-      users.getFirstTableCell(user_name).should('exist');
+      users.create(tenant, user_id, 'Some Name', 'original@website.com', '1200');
+      users.getFirstTableCell(user_id).should('exist');
     });
 
     it('should edit users full name, email and max buckets', () => {
index 6796ff5be5d523aaa33568d0cfc0548b0cfcd834..7ded71f59a92953553c16959a9255127c689b63d 100644 (file)
@@ -9,10 +9,13 @@ export class UsersPageHelper extends PageHelper {
   pages = pages;
 
   @PageHelper.restrictTo(pages.create.url)
-  create(username: string, fullname: string, email: string, maxbuckets: string) {
-    // Enter in  username
-    cy.get('#uid').type(username);
-
+  create(tenant: string, user_id: string, fullname: string, email: string, maxbuckets: string) {
+    // Enter in user_id
+    cy.get('#user_id').type(user_id);
+    // Show Tenanat
+    cy.get('#show_tenant').click({ force: true });
+    // Enter in tenant
+    cy.get('#tenant').type(tenant);
     // Enter in full name
     cy.get('#display_name').click().type(fullname);
 
@@ -26,7 +29,7 @@ export class UsersPageHelper extends PageHelper {
 
     // Click the create button and wait for user to be made
     cy.contains('button', 'Create User').click();
-    this.getFirstTableCell(username).should('exist');
+    this.getFirstTableCell(tenant + '$' + user_id).should('exist');
   }
 
   @PageHelper.restrictTo(pages.index.url)
@@ -54,26 +57,27 @@ export class UsersPageHelper extends PageHelper {
   }
 
   invalidCreate() {
+    const tenant = '000invalid_tenant';
     const uname = '000invalid_create_user';
     // creating this user in order to check that you can't give two users the same name
     this.navigateTo('create');
-    this.create(uname, 'xxx', 'xxx@xxx', '1');
+    this.create(tenant, uname, 'xxx', 'xxx@xxx', '1');
 
     this.navigateTo('create');
 
     // Username
-    cy.get('#uid')
+    cy.get('#user_id')
       // No username had been entered. Field should be invalid
       .should('have.class', 'ng-invalid')
       // Try to give user already taken name. Should make field invalid.
-      .type(uname)
-      .blur()
-      .should('have.class', 'ng-invalid');
-    cy.contains('#uid + .invalid-feedback', 'The chosen user ID is already in use.');
+      .type(uname);
+    cy.get('#show_tenant').click({ force: true });
+    cy.get('#tenant').type(tenant).should('have.class', 'ng-invalid');
+    cy.contains('#tenant + .invalid-feedback', 'The chosen user ID exists in this tenant.');
 
     // check that username field is marked invalid if username has been cleared off
-    cy.get('#uid').clear().blur().should('have.class', 'ng-invalid');
-    cy.contains('#uid + .invalid-feedback', 'This field is required.');
+    cy.get('#user_id').clear().blur().should('have.class', 'ng-invalid');
+    cy.contains('#user_id + .invalid-feedback', 'This field is required.');
 
     // Full name
     cy.get('#display_name')
@@ -96,14 +100,15 @@ export class UsersPageHelper extends PageHelper {
     cy.contains('#max_buckets + .invalid-feedback', 'The entered value must be >= 1.');
 
     this.navigateTo();
-    this.delete(uname);
+    this.delete(tenant + '$' + uname);
   }
 
   invalidEdit() {
+    const tenant = '000invalid_tenant';
     const uname = '000invalid_edit_user';
     // creating this user to edit for the test
     this.navigateTo('create');
-    this.create(uname, 'xxx', 'xxx@xxx', '50');
+    this.create(tenant, uname, 'xxx', 'xxx@xxx', '50');
 
     this.navigateEdit(name);
 
@@ -129,6 +134,6 @@ export class UsersPageHelper extends PageHelper {
     cy.contains('#max_buckets + .invalid-feedback', 'The entered value must be >= 1.');
 
     this.navigateTo();
-    this.delete(uname);
+    this.delete(tenant + '$' + uname);
   }
 }
index e4080a26362c27bf5351e3af0ab24b769cc0d890..02fe9583ae1ec753cab8a7f3be9e9d901b13de18 100644 (file)
@@ -4,6 +4,16 @@
     <div *ngIf="user">
       <table class="table table-striped table-bordered">
         <tbody>
+          <tr>
+            <td i18n
+                class="bold w-25">Tenant</td>
+            <td class="w-75">{{ user.tenant }}</td>
+          </tr>
+          <tr>
+            <td i18n
+                class="bold w-25">User ID</td>
+            <td class="w-75">{{ user.user_id }}</td>
+          </tr>
           <tr>
             <td i18n
                 class="bold w-25">Username</td>
index 1b29228c38cabc511e98c849b4816d134e654714..93ca7ff68490b0f932e2eff7cd45d6f76b555c4c 100644 (file)
@@ -67,13 +67,13 @@ describe('RgwUserDetailsComponent', () => {
     const detailsTab = fixture.debugElement.nativeElement.querySelectorAll(
       '.table.table-striped.table-bordered tr td'
     );
-    expect(detailsTab[6].textContent).toEqual('System');
-    expect(detailsTab[7].textContent).toEqual('Yes');
+    expect(detailsTab[10].textContent).toEqual('System');
+    expect(detailsTab[11].textContent).toEqual('Yes');
 
     component.selection.system = 'false';
     component.ngOnChanges();
     fixture.detectChanges();
 
-    expect(detailsTab[7].textContent).toEqual('No');
+    expect(detailsTab[11].textContent).toEqual('No');
   });
 });
index f7191fbdd7136c8491b864ca7ccee1961d832e9d..6a07a2cee83ccb3030d38e27c0edb4bd743cecb0 100644 (file)
            class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
 
       <div class="card-body">
-        <!-- Username -->
+        <!-- User ID -->
         <div class="form-group row">
           <label class="cd-col-form-label"
                  [ngClass]="{'required': !editing}"
-                 for="uid"
-                 i18n>Username</label>
+                 for="user_id"
+                 i18n>User ID</label>
           <div class="cd-col-form-input">
-            <input id="uid"
+            <input id="user_id"
                    class="form-control"
                    type="text"
-                   formControlName="uid"
+                   formControlName="user_id"
+                   [readonly]="editing">
+              <span class="invalid-feedback"
+                    *ngIf="userForm.showError('user_id', frm, 'required')"
+                    i18n>This field is required.</span>
+            <span class="invalid-feedback"
+                  *ngIf="userForm.showError('user_id', frm, 'pattern')"
+                  i18n>The value is not valid.</span>
+            <span class="invalid-feedback"
+                  *ngIf="!userForm.getValue('show_tenant') && userForm.showError('user_id', frm, 'notUnique')"
+                  i18n>The chosen user ID is already in use.</span>
+          </div>
+        </div>
+
+          <!-- Show Tenant -->
+          <div class="form-group row">
+            <div class="cd-col-form-offset">
+              <div class="custom-control custom-checkbox">
+                <input class="custom-control-input"
+                       id="show_tenant"
+                       type="checkbox"
+                       (click)="updateFieldsWhenTenanted()"
+                       formControlName="show_tenant"
+                       [readonly]="true">
+                <label class="custom-control-label"
+                       for="show_tenant"
+                       i18n>Show Tenant</label>
+              </div>
+            </div>
+          </div>
+
+        <!-- Tenant -->
+        <div class="form-group row"
+             *ngIf="userForm.getValue('show_tenant')">
+          <label class="cd-col-form-label"
+                 for="tenant"
+                 i18n>Tenant</label>
+          <div class="cd-col-form-input">
+            <input id="tenant"
+                   class="form-control"
+                   type="text"
+                   formControlName="tenant"
                    [readonly]="editing"
                    autofocus>
             <span class="invalid-feedback"
-                  *ngIf="userForm.showError('uid', frm, 'required')"
-                  i18n>This field is required.</span>
+                  *ngIf="userForm.showError('tenant', frm, 'pattern')"
+                  i18n>The value is not valid.</span>
             <span class="invalid-feedback"
-                  *ngIf="userForm.showError('uid', frm, 'notUnique')"
-                  i18n>The chosen user ID is already in use.</span>
+                  *ngIf="userForm.showError('tenant', frm, 'notUnique')"
+                  i18n>The chosen user ID exists in this tenant.</span>
           </div>
         </div>
 
index 4eb8c2783a10eb03fc863fd6959728b2cb2ab8bb..d9b112f76623d6496518c576086249d0fa1261f5 100644 (file)
@@ -156,21 +156,21 @@ describe('RgwUserFormComponent', () => {
 
   describe('username validation', () => {
     it('should validate that username is required', () => {
-      formHelper.expectErrorChange('uid', '', 'required', true);
+      formHelper.expectErrorChange('user_id', '', 'required', true);
     });
 
     it('should validate that username is valid', fakeAsync(() => {
       spyOn(rgwUserService, 'get').and.returnValue(throwError('foo'));
-      formHelper.setValue('uid', 'ab', true);
+      formHelper.setValue('user_id', 'ab', true);
       tick(500);
-      formHelper.expectValid('uid');
+      formHelper.expectValid('user_id');
     }));
 
     it('should validate that username is invalid', fakeAsync(() => {
       spyOn(rgwUserService, 'get').and.returnValue(observableOf({}));
-      formHelper.setValue('uid', 'abc', true);
+      formHelper.setValue('user_id', 'abc', true);
       tick(500);
-      formHelper.expectError('uid', 'notUnique');
+      formHelper.expectError('user_id', 'notUnique');
     }));
   });
 
index e7645a6bb8c304531446c0af9ef18df2771fe81a..c3b2d81302f40184cd922328fda5910b421bbffa 100644 (file)
@@ -48,6 +48,9 @@ export class RgwUserFormComponent implements OnInit {
   subuserLabel: string;
   s3keyLabel: string;
   capabilityLabel: string;
+  usernameExists: boolean;
+  showTenant = false;
+  previousTenant: string = null;
 
   constructor(
     private formBuilder: CdFormBuilder,
@@ -71,10 +74,31 @@ export class RgwUserFormComponent implements OnInit {
   createForm() {
     this.userForm = this.formBuilder.group({
       // General
-      uid: [
+      user_id: [
         null,
-        [Validators.required],
-        this.editing ? [] : [CdValidators.unique(this.rgwUserService.exists, this.rgwUserService)]
+        [Validators.required, Validators.pattern(/^[a-zA-Z0-9!@#%^&*()_-]+$/)],
+        this.editing
+          ? []
+          : [
+              CdValidators.unique(this.rgwUserService.exists, this.rgwUserService, () =>
+                this.userForm.getValue('tenant')
+              )
+            ]
+      ],
+      show_tenant: [this.editing],
+      tenant: [
+        null,
+        [Validators.pattern(/^[a-zA-Z0-9!@#%^&*()_-]+$/)],
+        this.editing
+          ? []
+          : [
+              CdValidators.unique(
+                this.rgwUserService.exists,
+                this.rgwUserService,
+                () => this.userForm.getValue('user_id'),
+                true
+              )
+            ]
       ],
       display_name: [null, [Validators.required]],
       email: [
@@ -242,7 +266,7 @@ export class RgwUserFormComponent implements OnInit {
       this.goToListView();
       return;
     }
-    const uid = this.userForm.getValue('uid');
+    const uid = this.getUID();
     if (this.editing) {
       // Edit
       if (this._isGeneralDirty()) {
@@ -279,6 +303,30 @@ export class RgwUserFormComponent implements OnInit {
     });
   }
 
+  updateFieldsWhenTenanted() {
+    this.showTenant = this.userForm.getValue('show_tenant');
+    if (!this.showTenant) {
+      this.userForm.get('user_id').markAsUntouched();
+      this.userForm.get('tenant').patchValue(this.previousTenant);
+    } else {
+      this.userForm.get('user_id').markAsTouched();
+      this.previousTenant = this.userForm.get('tenant').value;
+      this.userForm.get('tenant').patchValue(null);
+    }
+  }
+
+  getUID(): string {
+    let uid = this.userForm.getValue('user_id');
+    let tenant: any;
+    if (this.userForm !== null) {
+      tenant = this.userForm.getValue('tenant');
+    }
+    if (tenant && tenant.length > 0) {
+      uid = `${this.userForm.getValue('tenant')}$${uid}`;
+    }
+    return uid;
+  }
+
   /**
    * Validate the quota maximum size, e.g. 1096, 1K, 30M or 1.9MiB.
    */
@@ -304,7 +352,7 @@ export class RgwUserFormComponent implements OnInit {
       'full-control': 'full',
       'read-write': 'readwrite'
     };
-    const uid = this.userForm.getValue('uid');
+    const uid = this.getUID();
     const args = {
       subuser: subuser.id,
       access:
@@ -342,9 +390,7 @@ export class RgwUserFormComponent implements OnInit {
   deleteSubuser(index: number) {
     const subuser = this.subusers[index];
     // Create an observable to delete the subuser when the form is submitted.
-    this.submitObservables.push(
-      this.rgwUserService.deleteSubuser(this.userForm.getValue('uid'), subuser.id)
-    );
+    this.submitObservables.push(this.rgwUserService.deleteSubuser(this.getUID(), subuser.id));
     // Remove the associated S3 keys.
     this.s3Keys = this.s3Keys.filter((key) => {
       return key.user !== subuser.id;
@@ -363,7 +409,7 @@ export class RgwUserFormComponent implements OnInit {
    * Add/Update a capability.
    */
   setCapability(cap: RgwUserCapability, index?: number) {
-    const uid = this.userForm.getValue('uid');
+    const uid = this.getUID();
     if (_.isNumber(index)) {
       // Modify
       const oldCap = this.capabilities[index];
@@ -395,7 +441,7 @@ export class RgwUserFormComponent implements OnInit {
     const cap = this.capabilities[index];
     // Create an observable to delete the capability when the form is submitted.
     this.submitObservables.push(
-      this.rgwUserService.deleteCapability(this.userForm.getValue('uid'), cap.type, cap.perm)
+      this.rgwUserService.deleteCapability(this.getUID(), cap.type, cap.perm)
     );
     // Remove the capability to update the UI.
     this.capabilities.splice(index, 1);
@@ -452,9 +498,7 @@ export class RgwUserFormComponent implements OnInit {
   deleteS3Key(index: number) {
     const key = this.s3Keys[index];
     // Create an observable to delete the S3 key when the form is submitted.
-    this.submitObservables.push(
-      this.rgwUserService.deleteS3Key(this.userForm.getValue('uid'), key.access_key)
-    );
+    this.submitObservables.push(this.rgwUserService.deleteS3Key(this.getUID(), key.access_key));
     // Remove the S3 key to update the UI.
     this.s3Keys.splice(index, 1);
     // Mark the form as dirty to be able to submit it.
@@ -466,7 +510,7 @@ export class RgwUserFormComponent implements OnInit {
    * @param {number | undefined} index The subuser to show.
    */
   showSubuserModal(index?: number) {
-    const uid = this.userForm.getValue('uid');
+    const uid = this.getUID();
     const modalRef = this.bsModalService.show(RgwUserSubuserModalComponent);
     if (_.isNumber(index)) {
       // Edit
@@ -587,7 +631,7 @@ export class RgwUserFormComponent implements OnInit {
    */
   private _getCreateArgs() {
     const result = {
-      uid: this.userForm.getValue('uid'),
+      uid: this.getUID(),
       display_name: this.userForm.getValue('display_name'),
       suspended: this.userForm.getValue('suspended'),
       email: '',
@@ -689,7 +733,7 @@ export class RgwUserFormComponent implements OnInit {
   private _getS3KeyUserCandidates() {
     let result = [];
     // Add the current user id.
-    const uid = this.userForm.getValue('uid');
+    const uid = this.getUID();
     if (_.isString(uid) && !_.isEmpty(uid)) {
       result.push(uid);
     }
index b920ecd7d30fdbe3d434d00c7d27aa113715408b..3221b8ac3a851e68df82aa247fb4a667a8b1c8c6 100644 (file)
@@ -55,6 +55,11 @@ export class RgwUserListComponent extends ListWithDetails {
         prop: 'uid',
         flexGrow: 1
       },
+      {
+        name: this.i18n('Tenant'),
+        prop: 'tenant',
+        flexGrow: 1
+      },
       {
         name: this.i18n('Full name'),
         prop: 'display_name',
index 6edfb163690492f27e47cb00d0be6a75c5dccf88..0d302bb90135746a18c39b83e6fb6d216760aacd 100644 (file)
@@ -348,18 +348,30 @@ export class CdValidators {
   static unique(
     serviceFn: existsServiceFn,
     serviceFnThis: any = null,
+    usernameFn?: Function,
+    uidfield = false,
     dueTime = 500
   ): AsyncValidatorFn {
+    let uname: string;
     return (control: AbstractControl): Observable<ValidationErrors | null> => {
       // Exit immediately if user has not interacted with the control yet
       // or the control value is empty.
       if (control.pristine || isEmptyInputValue(control.value)) {
         return observableOf(null);
       }
+      uname = control.value;
+      if (_.isFunction(usernameFn) && usernameFn() !== null && usernameFn() !== '') {
+        if (uidfield) {
+          uname = `${control.value}$${usernameFn()}`;
+        } else {
+          uname = `${usernameFn()}$${control.value}`;
+        }
+      }
+
       // Forgot previous requests if a new one arrives within the specified
       // delay time.
       return observableTimer(dueTime).pipe(
-        switchMapTo(serviceFn.call(serviceFnThis, control.value)),
+        switchMapTo(serviceFn.call(serviceFnThis, uname)),
         map((resp: boolean) => {
           if (!resp) {
             return null;