From 19a9f79b5f218d8cb757579bf8c0f5d610dde3a8 Mon Sep 17 00:00:00 2001 From: Pedro Gonzalez Gomez Date: Fri, 1 Mar 2024 13:43:12 +0100 Subject: [PATCH] mgr/dashboard: ceph authenticate user from fs Fixes: https://tracker.ceph.com/issues/64660 Signed-off-by: Pedro Gonzalez Gomez --- .../mgr/dashboard/controllers/cephfs.py | 23 +++ .../cephfs-auth-modal.component.html | 166 ++++++++++++++++++ .../cephfs-auth-modal.component.scss | 0 .../cephfs-auth-modal.component.spec.ts | 29 +++ .../cephfs-auth-modal.component.ts | 129 ++++++++++++++ .../cephfs-list/cephfs-list.component.ts | 19 ++ .../cephfs-tabs/cephfs-tabs.component.html | 1 + .../src/app/ceph/cephfs/cephfs.module.ts | 4 +- .../src/app/shared/api/cephfs.service.ts | 9 + .../src/app/shared/constants/app.constants.ts | 2 + .../shared/services/task-message.service.ts | 7 + src/pybind/mgr/dashboard/openapi.yaml | 52 ++++++ 12 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.ts diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index e1e271b69dcc1..6410a73785ecb 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -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 index 0000000000000..290504bf3a868 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.html @@ -0,0 +1,166 @@ + + {{ action | titlecase }} {{ resource | upperFirst }} + +
+ + +
+
+
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 index 0000000000000..e69de29bb2d1d 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 index 0000000000000..0f0ab89cbcc82 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.spec.ts @@ -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; + + 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 index 0000000000000..211f2c662c3e0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.ts @@ -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 = (input: Observable) => + 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(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts index 0943ed82574ea..2957401d86aae 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts @@ -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' } + ); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html index a840692ed7673..4581cc0b3c92c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html @@ -29,6 +29,7 @@ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts index 14481d8382241..78081faa1fecd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts @@ -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 {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts index ab43343f9edd4..2d49de37c08a5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts @@ -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 + }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts index 876e22bbc1b61..185c778bc1bdf 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts @@ -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`; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 4fbcc09d09027..9aa9f02af6163 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -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}'`; } diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index c59532c7243d6..f3d4f3607f333 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -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: -- 2.39.5