]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: ceph authenticate user from fs
authorPedro Gonzalez Gomez <pegonzal@redhat.com>
Fri, 1 Mar 2024 12:43:12 +0000 (13:43 +0100)
committerPedro Gonzalez Gomez <pegonzal@redhat.com>
Tue, 12 Mar 2024 14:14:44 +0000 (15:14 +0100)
Fixes: https://tracker.ceph.com/issues/64660
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
12 files changed:
src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/openapi.yaml

index e1e271b69dcc18a7a80e7de96e6ba04cf74ddecb..6410a73785ecb84bad94b594c5add27bab29f3f1 100644 (file)
@@ -101,6 +101,29 @@ class CephFS(RESTController):
                 component='cephfs')
         return f'Volume {name} renamed successfully to {new_name}'
 
+    @UpdatePermission
+    @Endpoint('PUT')
+    @EndpointDoc("Set Ceph authentication capabilities for the specified user ID in the given path",
+                 parameters={
+                     'fs_name': (str, 'File system name'),
+                     'client_id': (str, 'Cephx user ID'),
+                     'caps': (str, 'Path and given capabilities'),
+                     'root_squash': (str, 'File System Identifier'),
+
+                 })
+    def auth(self, fs_name: str, client_id: int, caps: List[str], root_squash: bool):
+        if root_squash:
+            caps.insert(2, 'root_squash')
+        error_code, _, err = mgr.mon_command({'prefix': 'fs authorize',
+                                              'filesystem': fs_name,
+                                              'entity': client_id,
+                                              'caps': caps})
+        if error_code != 0:
+            raise DashboardException(
+                msg=f'Error setting authorization for {client_id} with {caps}: {err}',
+                component='cephfs')
+        return f'Updated {client_id} authorization successfully'
+
     def get(self, fs_id):
         fs_id = self.fs_id_to_int(fs_id)
         return self.fs_status(fs_id)
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.html
new file mode 100644 (file)
index 0000000..290504b
--- /dev/null
@@ -0,0 +1,166 @@
+<cd-modal [modalRef]="activeModal">
+  <ng-container i18n="form title"
+                class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+  <ng-container class="modal-content"
+                *cdFormLoading="loading">
+    <form name="form"
+          #formDir="ngForm"
+          [formGroup]="form">
+      <div class="modal-body">
+
+        <!-- FsName -->
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="userId"
+                 i18n>Fs name
+          </label>
+          <div class="cd-col-form-input">
+            <input id="fsName"
+                   name="fsName"
+                   type="text"
+                   class="form-control"
+                   formControlName="fsName">
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('fsName', formDir, 'required')"
+                  i18n>This field is required!</span>
+          </div>
+        </div>
+
+        <!-- UserId -->
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="userId"
+                 i18n>User ID
+            <cd-helper>
+              You can manage users from
+              <a routerLink="/ceph-users"
+                 (click)="closeModal()">Ceph Users</a>
+              page
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <div class="input-group">
+              <span class="input-group-text"
+                    for="userId"
+                    i18n>client.
+              </span>
+              <input id="userId"
+                     name="userId"
+                     type="text"
+                     class="form-control"
+                     formControlName="userId">
+              <span class="invalid-feedback"
+                    *ngIf="form.showError('userId', formDir, 'required')"
+                    i18n>This field is required!</span>
+            </div>
+          </div>
+        </div>
+
+        <!-- Directory -->
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="directory"
+                 i18n>Directory
+            <cd-helper>Path to restrict access to</cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <input id="typeahead-http"
+                   i18n
+                   type="text"
+                   class="form-control"
+                   disabled="directoryStore.isLoading"
+                   formControlName="directory"
+                   [ngbTypeahead]="search"
+                   [placeholder]="directoryStore.isLoading ? 'Loading directories' : 'Directory search'"
+                   i18n-placeholder>
+            <div *ngIf="directoryStore.isLoading">
+              <i [ngClass]="[icons.spinner, icons.spin, 'mt-2', 'me-2']"></i>
+            </div>
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('directory', formDir, 'required')"
+                  i18n>This field is required!</span>
+          </div>
+        </div>
+
+        <!-- Permissions -->
+        <div class="form-group row">
+          <label i18n
+                 class="cd-col-form-label"
+                 for="permissions">Permissons</label>
+          <div class="cd-col-form-input">
+
+            <!-- Read -->
+            <div class="custom-control custom-checkbox">
+              <input class="custom-control-input"
+                     id="read"
+                     formControlName="read"
+                     type="checkbox">
+              <label class="custom-control-label"
+                     for="read"
+                     i18n>Read
+              </label>
+              <cd-helper i18n>Read permission is the minimum givable access</cd-helper>
+            </div>
+
+            <!-- Write -->
+            <div class="custom-control custom-checkbox">
+              <input class="custom-control-input"
+                     id="write"
+                     formControlName="write"
+                     type="checkbox"
+                     (change)="toggleFormControl()">
+              <label class="custom-control-label"
+                     for="write"
+                     i18n>Write
+              </label>
+            </div>
+
+            <!-- Quota -->
+            <div class="custom-control custom-checkbox">
+              <input class="custom-control-input"
+                     id="quota"
+                     formControlName="quota"
+                     type="checkbox">
+              <label class="custom-control-label"
+                     for="quota"
+                     i18n>Quota
+              </label>
+              <cd-helper i18n>Permission to set layouts or quotas, write access needed</cd-helper>
+            </div>
+
+            <!-- Snapshot -->
+            <div class="custom-control custom-checkbox">
+              <input class="custom-control-input"
+                     id="snapshot"
+                     formControlName="snapshot"
+                     type="checkbox">
+              <label class="custom-control-label"
+                     for="snapshot"
+                     i18n>Snapshot
+              </label>
+              <cd-helper i18n>Permission to create or delete snapshots, write access needed</cd-helper>
+            </div>
+
+            <!-- Root Squash -->
+            <div class="custom-control custom-checkbox">
+              <input class="custom-control-input"
+                     id="rootSquash"
+                     formControlName="rootSquash"
+                     type="checkbox">
+              <label class="custom-control-label"
+                     for="rootSquash"
+                     i18n>Root Squash
+              </label>
+              <cd-helper>Safety measure to prevent scenarios such as accidental sudo rm -rf /path</cd-helper>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="modal-footer">
+        <cd-form-button-panel (submitActionEvent)="onSubmit()"
+                              [form]="form"
+                              [submitText]="(action | titlecase)"></cd-form-button-panel>
+      </div>
+    </form>
+  </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.spec.ts
new file mode 100644 (file)
index 0000000..0f0ab89
--- /dev/null
@@ -0,0 +1,29 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CephfsAuthModalComponent } from './cephfs-auth-modal.component';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ReactiveFormsModule } from '@angular/forms';
+
+describe('CephfsAuthModalComponent', () => {
+  let component: CephfsAuthModalComponent;
+  let fixture: ComponentFixture<CephfsAuthModalComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [CephfsAuthModalComponent],
+      imports: [HttpClientTestingModule, SharedModule, ReactiveFormsModule, ToastrModule.forRoot()],
+      providers: [NgbActiveModal]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(CephfsAuthModalComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.ts
new file mode 100644 (file)
index 0000000..211f2c6
--- /dev/null
@@ -0,0 +1,129 @@
+import { Component, OnInit } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { OperatorFunction, Observable, of } from 'rxjs';
+import { debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs/operators';
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { DirectoryStoreService } from '~/app/shared/api/directory-store.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+const DEBOUNCE_TIMER = 300;
+
+@Component({
+  selector: 'cd-cephfs-auth-modal',
+  templateUrl: './cephfs-auth-modal.component.html',
+  styleUrls: ['./cephfs-auth-modal.component.scss']
+})
+export class CephfsAuthModalComponent extends CdForm implements OnInit {
+  fsName: string;
+  id: number;
+  subvolumeGroup: string;
+  subvolume: string;
+  isDefaultSubvolumeGroup = false;
+  isSubvolume = false;
+  form: CdFormGroup;
+  action: string;
+  resource: string;
+  icons = Icons;
+
+  constructor(
+    public activeModal: NgbActiveModal,
+    private actionLabels: ActionLabelsI18n,
+    public directoryStore: DirectoryStoreService,
+    private cephfsService: CephfsService,
+    private taskWrapper: TaskWrapperService
+  ) {
+    super();
+    this.action = this.actionLabels.UPDATE;
+    this.resource = $localize`access`;
+  }
+
+  ngOnInit() {
+    this.directoryStore.loadDirectories(this.id, '/', 3);
+    this.createForm();
+    this.loadingReady();
+  }
+
+  createForm() {
+    this.form = new CdFormGroup({
+      fsName: new FormControl(
+        { value: this.fsName, disabled: true },
+        {
+          validators: [Validators.required]
+        }
+      ),
+      directory: new FormControl(undefined, {
+        updateOn: 'blur',
+        validators: [Validators.required]
+      }),
+      userId: new FormControl(undefined, {
+        validators: [Validators.required]
+      }),
+      read: new FormControl(
+        { value: true, disabled: true },
+        {
+          validators: [Validators.required]
+        }
+      ),
+      write: new FormControl(undefined),
+      snapshot: new FormControl({ value: false, disabled: true }),
+      quota: new FormControl({ value: false, disabled: true }),
+      rootSquash: new FormControl(undefined)
+    });
+  }
+
+  search: OperatorFunction<string, readonly string[]> = (input: Observable<string>) =>
+    input.pipe(
+      debounceTime(DEBOUNCE_TIMER),
+      distinctUntilChanged(),
+      switchMap((term) =>
+        this.directoryStore.search(term, this.id).pipe(
+          catchError(() => {
+            return of([]);
+          })
+        )
+      )
+    );
+
+  closeModal() {
+    this.activeModal.close();
+  }
+
+  onSubmit() {
+    const clientId: number = this.form.getValue('userId');
+    const caps: string[] = [this.form.getValue('directory'), this.transformPermissions()];
+    const rootSquash: boolean = this.form.getValue('rootSquash');
+    this.taskWrapper
+      .wrapTaskAroundCall({
+        task: new FinishedTask('cephfs/auth', {
+          clientId: clientId
+        }),
+        call: this.cephfsService.setAuth(this.fsName, clientId, caps, rootSquash)
+      })
+      .subscribe({
+        error: () => this.form.setErrors({ cdSubmitButton: true }),
+        complete: () => {
+          this.activeModal.close();
+        }
+      });
+  }
+
+  transformPermissions(): string {
+    const write = this.form.getValue('write');
+    const snapshot = this.form.getValue('snapshot');
+    const quota = this.form.getValue('quota');
+    return `r${write ? 'w' : ''}${quota ? 'p' : ''}${snapshot ? 's' : ''}`;
+  }
+
+  toggleFormControl() {
+    const snapshot = this.form.get('snapshot');
+    const quota = this.form.get('quota');
+    snapshot.disabled ? snapshot.enable() : snapshot.disable();
+    quota.disabled ? quota.enable() : quota.disable();
+  }
+}
index 0943ed82574eaf526863055c2391b902988c9656..2957401d86aae18a966b7f5d23bf0f93454cf708 100644 (file)
@@ -25,6 +25,7 @@ import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 import { CephfsMountDetailsComponent } from '../cephfs-mount-details/cephfs-mount-details.component';
 import { map, switchMap } from 'rxjs/operators';
 import { HealthService } from '~/app/shared/api/health.service';
+import { CephfsAuthModalComponent } from '~/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component';
 
 const BASE_URL = 'cephfs';
 
@@ -95,6 +96,12 @@ export class CephfsListComponent extends ListWithDetails implements OnInit {
         click: () =>
           this.router.navigate([this.urlBuilder.getEdit(String(this.selection.first().id))])
       },
+      {
+        name: this.actionLabels.AUTHORIZE,
+        permission: 'update',
+        icon: Icons.edit,
+        click: () => this.authorizeModal()
+      },
       {
         name: this.actionLabels.ATTACH,
         permission: 'read',
@@ -187,4 +194,16 @@ export class CephfsListComponent extends ListWithDetails implements OnInit {
 
     return true;
   }
+
+  authorizeModal() {
+    const selectedFileSystem = this.selection?.selected?.[0];
+    this.modalService.show(
+      CephfsAuthModalComponent,
+      {
+        fsName: selectedFileSystem.mdsmap['fs_name'],
+        id: selectedFileSystem.id
+      },
+      { size: 'lg' }
+    );
+  }
 }
index a840692ed7673dc29ab216fee9984711061a2198..4581cc0b3c92c4766d97605f4e74d297475ce49c 100644 (file)
@@ -29,6 +29,7 @@
         <cd-cephfs-subvolume-list
           [fsName]="selection.mdsmap.fs_name"
           [pools]="details.pools"
+          [id]="id"
         ></cd-cephfs-subvolume-list>
       </ng-template>
     </ng-container>
index 14481d8382241cdd60c3f903af6434ba534dd602..78081faa1fecd0d303c8296e0f5849312dd854bc 100644 (file)
@@ -31,6 +31,7 @@ import { DataTableModule } from '../../shared/datatable/datatable.module';
 import { CephfsSubvolumeSnapshotsFormComponent } from './cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component';
 import { CephfsSnapshotscheduleFormComponent } from './cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component';
 import { CephfsMountDetailsComponent } from './cephfs-mount-details/cephfs-mount-details.component';
+import { CephfsAuthModalComponent } from './cephfs-auth-modal/cephfs-auth-modal.component';
 
 @NgModule({
   imports: [
@@ -66,7 +67,8 @@ import { CephfsMountDetailsComponent } from './cephfs-mount-details/cephfs-mount
     CephfsSnapshotscheduleListComponent,
     CephfsSnapshotscheduleFormComponent,
     CephfsSubvolumeSnapshotsFormComponent,
-    CephfsMountDetailsComponent
+    CephfsMountDetailsComponent,
+    CephfsAuthModalComponent
   ]
 })
 export class CephfsModule {}
index ab43343f9edd4ffea0c6e82476b72cabb18ef2bc..2d49de37c08a529b3ae54b44f6a0be7648b355ad 100644 (file)
@@ -108,4 +108,13 @@ export class CephfsService {
       observe: 'response'
     });
   }
+
+  setAuth(fsName: string, clientId: number, caps: string[], rootSquash: boolean) {
+    return this.http.put(`${this.baseURL}/auth`, {
+      fs_name: fsName,
+      client_id: `client.${clientId}`,
+      caps: caps,
+      root_squash: rootSquash
+    });
+  }
 }
index 876e22bbc1b61c10934e77b02f8e53ef7515751d..185c778bc1bdfd15a346116cb5371c15541c9ad5 100644 (file)
@@ -147,6 +147,7 @@ export class ActionLabelsI18n {
   CONNECT: string;
   DISCONNECT: string;
   RECONNECT: string;
+  AUTHORIZE: string;
 
   constructor() {
     /* Create a new item */
@@ -206,6 +207,7 @@ export class ActionLabelsI18n {
     this.FLAGS = $localize`Flags`;
     this.ENTER_MAINTENANCE = $localize`Enter Maintenance`;
     this.EXIT_MAINTENANCE = $localize`Exit Maintenance`;
+    this.AUTHORIZE = $localize`Authorize`;
 
     this.START_DRAIN = $localize`Start Drain`;
     this.STOP_DRAIN = $localize`Stop Drain`;
index 4fbcc09d090274876d93376afaac75292de5fcbe..9aa9f02af6163785c9f0114335c1e6b7105ec84b 100644 (file)
@@ -369,6 +369,9 @@ export class TaskMessageService {
     'cephfs/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
       this.volume(metadata)
     ),
+    'cephfs/auth': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+      this.auth(metadata)
+    ),
     'cephfs/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
       this.volume(metadata)
     ),
@@ -475,6 +478,10 @@ export class TaskMessageService {
     return $localize`'${metadata.volumeName}'`;
   }
 
+  auth(metadata: any) {
+    return $localize`client.${metadata.clientId} authorization successfully`;
+  }
+
   subvolume(metadata: any) {
     return $localize`subvolume '${metadata.subVolumeName}'`;
   }
index c59532c7243d66d3c5ba4c30e2e75c8ad776e61c..f3d4f3607f333da82b487be2c6acd89952c2b818 100644 (file)
@@ -1681,6 +1681,58 @@ paths:
       - jwt: []
       tags:
       - Cephfs
+  /api/cephfs/auth:
+    put:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                caps:
+                  description: Path and given capabilities
+                  type: string
+                client_id:
+                  description: Cephx user ID
+                  type: string
+                fs_name:
+                  description: File system name
+                  type: string
+                root_squash:
+                  description: File System Identifier
+                  type: string
+              required:
+              - fs_name
+              - client_id
+              - caps
+              - root_squash
+              type: object
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource updated.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Set Ceph authentication capabilities for the specified user ID in the
+        given path
+      tags:
+      - Cephfs
   /api/cephfs/remove/{name}:
     delete:
       parameters: