Skip to content

Commit 1d01d04

Browse files
committed
mgr/dashboard: Add mTLS support
- enables mTLS support from dashboard - adds unit tests related to mTLS support - can enable mTLS - can disable mTLS - inlcuded refactoring from prev commit Fixes https://tracker.ceph.com/issues/66416 Signed-off-by: Afreen Misbah <[email protected]>
1 parent e0e467a commit 1d01d04

File tree

5 files changed

+280
-36
lines changed

5 files changed

+280
-36
lines changed

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
name="pool"
9797
class="form-select"
9898
formControlName="pool"
99-
(change)="onBlockPoolChange()">
99+
(change)="setNvmeServiceId()">
100100
<option *ngIf="rbdPools === null"
101101
[ngValue]="null"
102102
i18n>Loading...</option>
@@ -131,7 +131,7 @@
131131
class="form-control"
132132
type="text"
133133
formControlName="group"
134-
(change)="onNvmeofGroupChange($event.target.value)">
134+
(change)="setNvmeServiceId()">
135135
</div>
136136
<cd-help-text i18n>
137137
The name of the gateway group.
@@ -1272,6 +1272,125 @@
12721272
i18n>
12731273
Modifying the default settings could lead to a weaker security configuration
12741274
</cd-alert-panel>
1275+
1276+
<!-- NVMe/TCP -->
1277+
<!-- mTLS -->
1278+
<div class="form-group row"
1279+
*ngIf="serviceForm.controls.service_type.value === 'nvmeof'">
1280+
<div class="cd-col-form-offset">
1281+
<div class="custom-control custom-checkbox">
1282+
<input class="custom-control-input"
1283+
id="enable_mtls"
1284+
type="checkbox"
1285+
formControlName="enable_mtls">
1286+
<label class="custom-control-label"
1287+
for="enable_mtls"
1288+
i18n>Encryption</label>
1289+
<cd-help-text i18n>Enables mutual TLS (mTLS) between the client and the gateway server.</cd-help-text>
1290+
</div>
1291+
</div>
1292+
</div>
1293+
1294+
<!-- root_ca_cert -->
1295+
<div *ngIf="serviceForm.controls.enable_auth.value"
1296+
class="form-group row">
1297+
<label class="cd-col-form-label required"
1298+
for="root_ca_cert">
1299+
<span i18n>Root CA certificate</span>
1300+
</label>
1301+
<div class="cd-col-form-input">
1302+
<textarea id="root_ca_cert"
1303+
class="form-control resize-vertical text-monospace text-pre"
1304+
formControlName="root_ca_cert"
1305+
rows="5"></textarea>
1306+
<input type="file"
1307+
(change)="fileUpload($event.target.files, 'root_ca_cert')">
1308+
<span class="invalid-feedback"
1309+
*ngIf="serviceForm.showError('root_ca_cert', frm, 'required')"
1310+
i18n>This field is required.</span>
1311+
</div>
1312+
</div>
1313+
1314+
<!-- client_cert -->
1315+
<div *ngIf="serviceForm.controls.enable_auth.value"
1316+
class="form-group row">
1317+
<label class="cd-col-form-label required"
1318+
for="client_cert">
1319+
<span i18n>Client CA certificate</span>
1320+
</label>
1321+
<div class="cd-col-form-input">
1322+
<textarea id="client_cert"
1323+
class="form-control resize-vertical text-monospace text-pre"
1324+
formControlName="client_cert"
1325+
rows="5"></textarea>
1326+
<input type="file"
1327+
(change)="fileUpload($event.target.files, 'client_cert')">
1328+
<span class="invalid-feedback"
1329+
*ngIf="serviceForm.showError('client_cert', frm, 'required')"
1330+
i18n>This field is required.</span>
1331+
</div>
1332+
</div>
1333+
1334+
<!-- client_key -->
1335+
<div *ngIf="serviceForm.controls.enable_auth.value"
1336+
class="form-group row">
1337+
<label class="cd-col-form-label required"
1338+
for="client_key">
1339+
<span i18n>Client key</span>
1340+
</label>
1341+
<div class="cd-col-form-input">
1342+
<textarea id="client_key"
1343+
class="form-control resize-vertical text-monospace text-pre"
1344+
formControlName="client_key"
1345+
rows="5"></textarea>
1346+
<input type="file"
1347+
(change)="fileUpload($event.target.files, 'client_key')">
1348+
<span class="invalid-feedback"
1349+
*ngIf="serviceForm.showError('client_key', frm, 'required')"
1350+
i18n>This field is required.</span>
1351+
</div>
1352+
</div>
1353+
1354+
<!-- server_cert -->
1355+
<div *ngIf="serviceForm.controls.enable_auth.value"
1356+
class="form-group row">
1357+
<label class="cd-col-form-label required"
1358+
for="server_cert">
1359+
<span i18n>Gateway server certificate</span>
1360+
</label>
1361+
<div class="cd-col-form-input">
1362+
<textarea id="server_cert"
1363+
class="form-control resize-vertical text-monospace text-pre"
1364+
formControlName="server_cert"
1365+
rows="5"></textarea>
1366+
<input type="file"
1367+
(change)="fileUpload($event.target.files, 'server_cert')">
1368+
<span class="invalid-feedback"
1369+
*ngIf="serviceForm.showError('server_cert', frm, 'required')"
1370+
i18n>This field is required.</span>
1371+
</div>
1372+
</div>
1373+
1374+
<!-- server_key -->
1375+
<div *ngIf="serviceForm.controls.enable_auth.value"
1376+
class="form-group row">
1377+
<label class="cd-col-form-label required"
1378+
for="server_key">
1379+
<span i18n>Gateway server key</span>
1380+
</label>
1381+
<div class="cd-col-form-input">
1382+
<textarea id="server_key"
1383+
class="form-control resize-vertical text-monospace text-pre"
1384+
formControlName="server_key"
1385+
rows="5"></textarea>
1386+
<input type="file"
1387+
(change)="fileUpload($event.target.files, 'server_key')">
1388+
<span class="invalid-feedback"
1389+
*ngIf="serviceForm.showError('server_key', frm, 'required')"
1390+
i18n>This field is required.</span>
1391+
</div>
1392+
</div>
1393+
12751394
</div>
12761395

12771396
<div class="modal-footer">

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -469,15 +469,55 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
469469
expect(countEl).toBeNull();
470470
});
471471

472-
it('should submit nvmeof', () => {
472+
it('should not show certs and keys field with mTLS disabled', () => {
473+
formHelper.setValue('ssl', true);
474+
fixture.detectChanges();
475+
const root_ca_cert = fixture.debugElement.query(By.css('#root_ca_cert'));
476+
const client_cert = fixture.debugElement.query(By.css('#client_cert'));
477+
const client_key = fixture.debugElement.query(By.css('#client_key'));
478+
const server_cert = fixture.debugElement.query(By.css('#server_cert'));
479+
const server_key = fixture.debugElement.query(By.css('#server_key'));
480+
expect(root_ca_cert).toBeNull();
481+
expect(client_cert).toBeNull();
482+
expect(client_key).toBeNull();
483+
expect(server_cert).toBeNull();
484+
expect(server_key).toBeNull();
485+
});
486+
487+
it('should submit nvmeof without mTLS', () => {
488+
component.onSubmit();
489+
expect(cephServiceService.create).toHaveBeenCalledWith({
490+
service_type: 'nvmeof',
491+
service_id: 'rbd.default',
492+
placement: {},
493+
unmanaged: false,
494+
pool: 'rbd',
495+
group: 'default',
496+
enable_auth: false
497+
});
498+
});
499+
500+
it('should submit nvmeof with mTLS', () => {
501+
formHelper.setValue('enable_mtls', true);
502+
formHelper.setValue('root_ca_cert', 'root_ca_cert');
503+
formHelper.setValue('client_cert', 'client_cert');
504+
formHelper.setValue('client_key', 'client_key');
505+
formHelper.setValue('server_cert', 'server_cert');
506+
formHelper.setValue('server_key', 'server_key');
473507
component.onSubmit();
474508
expect(cephServiceService.create).toHaveBeenCalledWith({
475509
service_type: 'nvmeof',
476510
service_id: 'rbd.default',
477511
placement: {},
478512
unmanaged: false,
479513
pool: 'rbd',
480-
group: 'default'
514+
group: 'default',
515+
enable_auth: true,
516+
root_ca_cert: 'root_ca_cert',
517+
client_cert: 'client_cert',
518+
client_key: 'client_key',
519+
server_cert: 'server_cert',
520+
server_key: 'server_key'
481521
});
482522
});
483523
});
@@ -721,6 +761,25 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
721761
const groupId = fixture.debugElement.query(By.css('#group')).nativeElement;
722762
expect(groupId.disabled).toBeTruthy();
723763
});
764+
765+
it('should update nvmeof service to disable mTLS', () => {
766+
spyOn(cephServiceService, 'update').and.stub();
767+
component.serviceType = 'nvmeof';
768+
formHelper.setValue('service_type', 'nvmeof');
769+
formHelper.setValue('pool', 'rbd');
770+
formHelper.setValue('group', 'default');
771+
// mTLS disabled
772+
formHelper.setValue('enable_auth', false);
773+
component.onSubmit();
774+
expect(cephServiceService.update).toHaveBeenCalledWith({
775+
service_type: 'nvmeof',
776+
placement: {},
777+
unmanaged: false,
778+
pool: 'rbd',
779+
group: 'default',
780+
enable_auth: false
781+
});
782+
});
724783
});
725784
});
726785
});

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts

Lines changed: 92 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,67 @@ export class ServiceFormComponent extends CdForm implements OnInit {
217217
service_type: 'nvmeof'
218218
})
219219
],
220+
enable_mtls: [false],
221+
root_ca_cert: [
222+
null,
223+
[
224+
CdValidators.composeIf(
225+
{
226+
service_type: 'nvmeof',
227+
enable_mtls: true
228+
},
229+
[Validators.required]
230+
)
231+
]
232+
],
233+
client_cert: [
234+
null,
235+
[
236+
CdValidators.composeIf(
237+
{
238+
service_type: 'nvmeof',
239+
enable_mtls: true
240+
},
241+
[Validators.required]
242+
)
243+
]
244+
],
245+
client_key: [
246+
null,
247+
[
248+
CdValidators.composeIf(
249+
{
250+
service_type: 'nvmeof',
251+
enable_mtls: true
252+
},
253+
[Validators.required]
254+
)
255+
]
256+
],
257+
server_cert: [
258+
null,
259+
[
260+
CdValidators.composeIf(
261+
{
262+
service_type: 'nvmeof',
263+
enable_mtls: true
264+
},
265+
[Validators.required]
266+
)
267+
]
268+
],
269+
server_key: [
270+
null,
271+
[
272+
CdValidators.composeIf(
273+
{
274+
service_type: 'nvmeof',
275+
enable_mtls: true
276+
},
277+
[Validators.required]
278+
)
279+
]
280+
],
220281
// RGW
221282
rgw_frontend_port: [null, [CdValidators.number(false)]],
222283
realm_name: [null],
@@ -633,6 +694,12 @@ export class ServiceFormComponent extends CdForm implements OnInit {
633694
case 'nvmeof':
634695
this.serviceForm.get('pool').setValue(response[0].spec.pool);
635696
this.serviceForm.get('group').setValue(response[0].spec.group);
697+
this.serviceForm.get('enable_auth').setValue(response[0].spec.enable_auth);
698+
this.serviceForm.get('root_ca_cert').setValue(response[0].spec.root_ca_cert);
699+
this.serviceForm.get('client_cert').setValue(response[0].spec.client_cert);
700+
this.serviceForm.get('client_key').setValue(response[0].spec.client_key);
701+
this.serviceForm.get('server_cert').setValue(response[0].spec.server_cert);
702+
this.serviceForm.get('server_key').setValue(response[0].spec.server_key);
636703
break;
637704
case 'rgw':
638705
this.serviceForm
@@ -961,39 +1028,36 @@ export class ServiceFormComponent extends CdForm implements OnInit {
9611028
);
9621029
}
9631030

964-
onNvmeofGroupChange(groupName: string) {
1031+
setNvmeServiceId() {
9651032
const pool = this.serviceForm.get('pool').value;
966-
if (pool) this.serviceForm.get('service_id').setValue(`${pool}.${groupName}`);
967-
else this.serviceForm.get('service_id').setValue(groupName);
968-
}
969-
970-
getDefaultBlockPool(): string {
971-
// returns 'rbd' pool otherwise the first block pool
972-
return (
973-
this.rbdPools?.find((p: Pool) => p.pool_name === 'rbd')?.pool_name ||
974-
this.rbdPools?.[0].pool_name
975-
);
976-
}
977-
978-
setNvmeofServiceIdAndPool(): void {
979-
const defaultBlockPool: string = this.getDefaultBlockPool();
980-
const group: string = this.serviceForm.get('group').value;
981-
if (defaultBlockPool && group) {
982-
this.serviceForm.get('pool').setValue(defaultBlockPool);
983-
this.serviceForm.get('service_id').setValue(`${defaultBlockPool}.${group}`);
1033+
const group = this.serviceForm.get('group').value;
1034+
if (pool && group) {
1035+
this.serviceForm.get('service_id').setValue(`${pool}.${group}`);
1036+
} else if (pool) {
1037+
this.serviceForm.get('service_id').setValue(pool);
1038+
} else if (group) {
1039+
this.serviceForm.get('service_id').setValue(group);
9841040
} else {
9851041
this.serviceForm.get('service_id').setValue(null);
9861042
}
9871043
}
9881044

1045+
setNvmeDefaultPool() {
1046+
const defaultPool =
1047+
this.rbdPools?.find((p: Pool) => p.pool_name === 'rbd')?.pool_name ||
1048+
this.rbdPools?.[0].pool_name;
1049+
this.serviceForm.get('pool').setValue(defaultPool);
1050+
}
1051+
9891052
requiresServiceId(serviceType: string) {
9901053
return ['mds', 'rgw', 'nfs', 'iscsi', 'nvmeof', 'smb', 'ingress'].includes(serviceType);
9911054
}
9921055

9931056
setServiceId(serviceId: string): void {
9941057
const requiresServiceId: boolean = this.requiresServiceId(serviceId);
9951058
if (requiresServiceId && serviceId === 'nvmeof') {
996-
this.setNvmeofServiceIdAndPool();
1059+
this.setNvmeDefaultPool();
1060+
this.setNvmeServiceId();
9971061
} else if (requiresServiceId) {
9981062
this.serviceForm.get('service_id').setValue(null);
9991063
} else {
@@ -1029,18 +1093,6 @@ export class ServiceFormComponent extends CdForm implements OnInit {
10291093
}
10301094
}
10311095

1032-
onBlockPoolChange() {
1033-
const selectedBlockPool = this.serviceForm.get('pool').value;
1034-
const group = this.serviceForm.get('group').value;
1035-
if (selectedBlockPool && group) {
1036-
this.serviceForm.get('service_id').setValue(`${selectedBlockPool}.${group}`);
1037-
} else if (selectedBlockPool) {
1038-
this.serviceForm.get('service_id').setValue(selectedBlockPool);
1039-
} else {
1040-
this.serviceForm.get('service_id').setValue(null);
1041-
}
1042-
}
1043-
10441096
disableForEditing(serviceType: string) {
10451097
const disableForEditKeys = ['service_type', 'service_id'];
10461098
disableForEditKeys.forEach((key) => {
@@ -1138,6 +1190,14 @@ export class ServiceFormComponent extends CdForm implements OnInit {
11381190
case 'nvmeof':
11391191
serviceSpec['pool'] = values['pool'];
11401192
serviceSpec['group'] = values['group'];
1193+
serviceSpec['enable_auth'] = values['enable_mtls'];
1194+
if (values['enable_mtls']) {
1195+
serviceSpec['root_ca_cert'] = values['root_ca_cert'];
1196+
serviceSpec['client_cert'] = values['client_cert'];
1197+
serviceSpec['client_key'] = values['client_key'];
1198+
serviceSpec['server_cert'] = values['server_cert'];
1199+
serviceSpec['server_key'] = values['server_key'];
1200+
}
11411201
break;
11421202
case 'iscsi':
11431203
serviceSpec['pool'] = values['pool'];

0 commit comments

Comments
 (0)