Skip to content

Commit e37c605

Browse files
authored
Merge pull request ceph#55870 from rhcs-dashboard/cephfs-auth
mgr/dashboard: ceph authenticate user from fs Reviewed-by: Ankush Behl <[email protected]> Reviewed-by: ivoalmeida <NOT@FOUND> Reviewed-by: Nizamudeen A <[email protected]>
2 parents 00760c9 + 19a9f79 commit e37c605

File tree

12 files changed

+440
-1
lines changed

12 files changed

+440
-1
lines changed

src/pybind/mgr/dashboard/controllers/cephfs.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,29 @@ def rename(self, name: str, new_name: str):
101101
component='cephfs')
102102
return f'Volume {name} renamed successfully to {new_name}'
103103

104+
@UpdatePermission
105+
@Endpoint('PUT')
106+
@EndpointDoc("Set Ceph authentication capabilities for the specified user ID in the given path",
107+
parameters={
108+
'fs_name': (str, 'File system name'),
109+
'client_id': (str, 'Cephx user ID'),
110+
'caps': (str, 'Path and given capabilities'),
111+
'root_squash': (str, 'File System Identifier'),
112+
113+
})
114+
def auth(self, fs_name: str, client_id: int, caps: List[str], root_squash: bool):
115+
if root_squash:
116+
caps.insert(2, 'root_squash')
117+
error_code, _, err = mgr.mon_command({'prefix': 'fs authorize',
118+
'filesystem': fs_name,
119+
'entity': client_id,
120+
'caps': caps})
121+
if error_code != 0:
122+
raise DashboardException(
123+
msg=f'Error setting authorization for {client_id} with {caps}: {err}',
124+
component='cephfs')
125+
return f'Updated {client_id} authorization successfully'
126+
104127
def get(self, fs_id):
105128
fs_id = self.fs_id_to_int(fs_id)
106129
return self.fs_status(fs_id)
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<cd-modal [modalRef]="activeModal">
2+
<ng-container i18n="form title"
3+
class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
4+
<ng-container class="modal-content"
5+
*cdFormLoading="loading">
6+
<form name="form"
7+
#formDir="ngForm"
8+
[formGroup]="form">
9+
<div class="modal-body">
10+
11+
<!-- FsName -->
12+
<div class="form-group row">
13+
<label class="cd-col-form-label required"
14+
for="userId"
15+
i18n>Fs name
16+
</label>
17+
<div class="cd-col-form-input">
18+
<input id="fsName"
19+
name="fsName"
20+
type="text"
21+
class="form-control"
22+
formControlName="fsName">
23+
<span class="invalid-feedback"
24+
*ngIf="form.showError('fsName', formDir, 'required')"
25+
i18n>This field is required!</span>
26+
</div>
27+
</div>
28+
29+
<!-- UserId -->
30+
<div class="form-group row">
31+
<label class="cd-col-form-label required"
32+
for="userId"
33+
i18n>User ID
34+
<cd-helper>
35+
You can manage users from
36+
<a routerLink="/ceph-users"
37+
(click)="closeModal()">Ceph Users</a>
38+
page
39+
</cd-helper>
40+
</label>
41+
<div class="cd-col-form-input">
42+
<div class="input-group">
43+
<span class="input-group-text"
44+
for="userId"
45+
i18n>client.
46+
</span>
47+
<input id="userId"
48+
name="userId"
49+
type="text"
50+
class="form-control"
51+
formControlName="userId">
52+
<span class="invalid-feedback"
53+
*ngIf="form.showError('userId', formDir, 'required')"
54+
i18n>This field is required!</span>
55+
</div>
56+
</div>
57+
</div>
58+
59+
<!-- Directory -->
60+
<div class="form-group row">
61+
<label class="cd-col-form-label required"
62+
for="directory"
63+
i18n>Directory
64+
<cd-helper>Path to restrict access to</cd-helper>
65+
</label>
66+
<div class="cd-col-form-input">
67+
<input id="typeahead-http"
68+
i18n
69+
type="text"
70+
class="form-control"
71+
disabled="directoryStore.isLoading"
72+
formControlName="directory"
73+
[ngbTypeahead]="search"
74+
[placeholder]="directoryStore.isLoading ? 'Loading directories' : 'Directory search'"
75+
i18n-placeholder>
76+
<div *ngIf="directoryStore.isLoading">
77+
<i [ngClass]="[icons.spinner, icons.spin, 'mt-2', 'me-2']"></i>
78+
</div>
79+
<span class="invalid-feedback"
80+
*ngIf="form.showError('directory', formDir, 'required')"
81+
i18n>This field is required!</span>
82+
</div>
83+
</div>
84+
85+
<!-- Permissions -->
86+
<div class="form-group row">
87+
<label i18n
88+
class="cd-col-form-label"
89+
for="permissions">Permissons</label>
90+
<div class="cd-col-form-input">
91+
92+
<!-- Read -->
93+
<div class="custom-control custom-checkbox">
94+
<input class="custom-control-input"
95+
id="read"
96+
formControlName="read"
97+
type="checkbox">
98+
<label class="custom-control-label"
99+
for="read"
100+
i18n>Read
101+
</label>
102+
<cd-helper i18n>Read permission is the minimum givable access</cd-helper>
103+
</div>
104+
105+
<!-- Write -->
106+
<div class="custom-control custom-checkbox">
107+
<input class="custom-control-input"
108+
id="write"
109+
formControlName="write"
110+
type="checkbox"
111+
(change)="toggleFormControl()">
112+
<label class="custom-control-label"
113+
for="write"
114+
i18n>Write
115+
</label>
116+
</div>
117+
118+
<!-- Quota -->
119+
<div class="custom-control custom-checkbox">
120+
<input class="custom-control-input"
121+
id="quota"
122+
formControlName="quota"
123+
type="checkbox">
124+
<label class="custom-control-label"
125+
for="quota"
126+
i18n>Quota
127+
</label>
128+
<cd-helper i18n>Permission to set layouts or quotas, write access needed</cd-helper>
129+
</div>
130+
131+
<!-- Snapshot -->
132+
<div class="custom-control custom-checkbox">
133+
<input class="custom-control-input"
134+
id="snapshot"
135+
formControlName="snapshot"
136+
type="checkbox">
137+
<label class="custom-control-label"
138+
for="snapshot"
139+
i18n>Snapshot
140+
</label>
141+
<cd-helper i18n>Permission to create or delete snapshots, write access needed</cd-helper>
142+
</div>
143+
144+
<!-- Root Squash -->
145+
<div class="custom-control custom-checkbox">
146+
<input class="custom-control-input"
147+
id="rootSquash"
148+
formControlName="rootSquash"
149+
type="checkbox">
150+
<label class="custom-control-label"
151+
for="rootSquash"
152+
i18n>Root Squash
153+
</label>
154+
<cd-helper>Safety measure to prevent scenarios such as accidental sudo rm -rf /path</cd-helper>
155+
</div>
156+
</div>
157+
</div>
158+
</div>
159+
<div class="modal-footer">
160+
<cd-form-button-panel (submitActionEvent)="onSubmit()"
161+
[form]="form"
162+
[submitText]="(action | titlecase)"></cd-form-button-panel>
163+
</div>
164+
</form>
165+
</ng-container>
166+
</cd-modal>

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component.scss

Whitespace-only changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { CephfsAuthModalComponent } from './cephfs-auth-modal.component';
4+
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
5+
import { HttpClientTestingModule } from '@angular/common/http/testing';
6+
import { ToastrModule } from 'ngx-toastr';
7+
import { SharedModule } from '~/app/shared/shared.module';
8+
import { ReactiveFormsModule } from '@angular/forms';
9+
10+
describe('CephfsAuthModalComponent', () => {
11+
let component: CephfsAuthModalComponent;
12+
let fixture: ComponentFixture<CephfsAuthModalComponent>;
13+
14+
beforeEach(async () => {
15+
await TestBed.configureTestingModule({
16+
declarations: [CephfsAuthModalComponent],
17+
imports: [HttpClientTestingModule, SharedModule, ReactiveFormsModule, ToastrModule.forRoot()],
18+
providers: [NgbActiveModal]
19+
}).compileComponents();
20+
21+
fixture = TestBed.createComponent(CephfsAuthModalComponent);
22+
component = fixture.componentInstance;
23+
fixture.detectChanges();
24+
});
25+
26+
it('should create', () => {
27+
expect(component).toBeTruthy();
28+
});
29+
});
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { FormControl, Validators } from '@angular/forms';
3+
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
4+
import { OperatorFunction, Observable, of } from 'rxjs';
5+
import { debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs/operators';
6+
import { CephfsService } from '~/app/shared/api/cephfs.service';
7+
import { DirectoryStoreService } from '~/app/shared/api/directory-store.service';
8+
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
9+
import { Icons } from '~/app/shared/enum/icons.enum';
10+
import { CdForm } from '~/app/shared/forms/cd-form';
11+
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
12+
import { FinishedTask } from '~/app/shared/models/finished-task';
13+
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
14+
15+
const DEBOUNCE_TIMER = 300;
16+
17+
@Component({
18+
selector: 'cd-cephfs-auth-modal',
19+
templateUrl: './cephfs-auth-modal.component.html',
20+
styleUrls: ['./cephfs-auth-modal.component.scss']
21+
})
22+
export class CephfsAuthModalComponent extends CdForm implements OnInit {
23+
fsName: string;
24+
id: number;
25+
subvolumeGroup: string;
26+
subvolume: string;
27+
isDefaultSubvolumeGroup = false;
28+
isSubvolume = false;
29+
form: CdFormGroup;
30+
action: string;
31+
resource: string;
32+
icons = Icons;
33+
34+
constructor(
35+
public activeModal: NgbActiveModal,
36+
private actionLabels: ActionLabelsI18n,
37+
public directoryStore: DirectoryStoreService,
38+
private cephfsService: CephfsService,
39+
private taskWrapper: TaskWrapperService
40+
) {
41+
super();
42+
this.action = this.actionLabels.UPDATE;
43+
this.resource = $localize`access`;
44+
}
45+
46+
ngOnInit() {
47+
this.directoryStore.loadDirectories(this.id, '/', 3);
48+
this.createForm();
49+
this.loadingReady();
50+
}
51+
52+
createForm() {
53+
this.form = new CdFormGroup({
54+
fsName: new FormControl(
55+
{ value: this.fsName, disabled: true },
56+
{
57+
validators: [Validators.required]
58+
}
59+
),
60+
directory: new FormControl(undefined, {
61+
updateOn: 'blur',
62+
validators: [Validators.required]
63+
}),
64+
userId: new FormControl(undefined, {
65+
validators: [Validators.required]
66+
}),
67+
read: new FormControl(
68+
{ value: true, disabled: true },
69+
{
70+
validators: [Validators.required]
71+
}
72+
),
73+
write: new FormControl(undefined),
74+
snapshot: new FormControl({ value: false, disabled: true }),
75+
quota: new FormControl({ value: false, disabled: true }),
76+
rootSquash: new FormControl(undefined)
77+
});
78+
}
79+
80+
search: OperatorFunction<string, readonly string[]> = (input: Observable<string>) =>
81+
input.pipe(
82+
debounceTime(DEBOUNCE_TIMER),
83+
distinctUntilChanged(),
84+
switchMap((term) =>
85+
this.directoryStore.search(term, this.id).pipe(
86+
catchError(() => {
87+
return of([]);
88+
})
89+
)
90+
)
91+
);
92+
93+
closeModal() {
94+
this.activeModal.close();
95+
}
96+
97+
onSubmit() {
98+
const clientId: number = this.form.getValue('userId');
99+
const caps: string[] = [this.form.getValue('directory'), this.transformPermissions()];
100+
const rootSquash: boolean = this.form.getValue('rootSquash');
101+
this.taskWrapper
102+
.wrapTaskAroundCall({
103+
task: new FinishedTask('cephfs/auth', {
104+
clientId: clientId
105+
}),
106+
call: this.cephfsService.setAuth(this.fsName, clientId, caps, rootSquash)
107+
})
108+
.subscribe({
109+
error: () => this.form.setErrors({ cdSubmitButton: true }),
110+
complete: () => {
111+
this.activeModal.close();
112+
}
113+
});
114+
}
115+
116+
transformPermissions(): string {
117+
const write = this.form.getValue('write');
118+
const snapshot = this.form.getValue('snapshot');
119+
const quota = this.form.getValue('quota');
120+
return `r${write ? 'w' : ''}${quota ? 'p' : ''}${snapshot ? 's' : ''}`;
121+
}
122+
123+
toggleFormControl() {
124+
const snapshot = this.form.get('snapshot');
125+
const quota = this.form.get('quota');
126+
snapshot.disabled ? snapshot.enable() : snapshot.disable();
127+
quota.disabled ? quota.enable() : quota.disable();
128+
}
129+
}

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
2525
import { CephfsMountDetailsComponent } from '../cephfs-mount-details/cephfs-mount-details.component';
2626
import { map, switchMap } from 'rxjs/operators';
2727
import { HealthService } from '~/app/shared/api/health.service';
28+
import { CephfsAuthModalComponent } from '~/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component';
2829

2930
const BASE_URL = 'cephfs';
3031

@@ -95,6 +96,12 @@ export class CephfsListComponent extends ListWithDetails implements OnInit {
9596
click: () =>
9697
this.router.navigate([this.urlBuilder.getEdit(String(this.selection.first().id))])
9798
},
99+
{
100+
name: this.actionLabels.AUTHORIZE,
101+
permission: 'update',
102+
icon: Icons.edit,
103+
click: () => this.authorizeModal()
104+
},
98105
{
99106
name: this.actionLabels.ATTACH,
100107
permission: 'read',
@@ -187,4 +194,16 @@ export class CephfsListComponent extends ListWithDetails implements OnInit {
187194

188195
return true;
189196
}
197+
198+
authorizeModal() {
199+
const selectedFileSystem = this.selection?.selected?.[0];
200+
this.modalService.show(
201+
CephfsAuthModalComponent,
202+
{
203+
fsName: selectedFileSystem.mdsmap['fs_name'],
204+
id: selectedFileSystem.id
205+
},
206+
{ size: 'lg' }
207+
);
208+
}
190209
}

0 commit comments

Comments
 (0)