]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add mTLS support 59805/head
authorAfreen Misbah <afreen23.git@gmail.com>
Sat, 14 Sep 2024 16:58:44 +0000 (22:28 +0530)
committerAfreen Misbah <afreen23.git@gmail.com>
Mon, 16 Sep 2024 17:07:35 +0000 (22:37 +0530)
- 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 <afreen23.git@gmail.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts

index 641d25a8dc29cce78a140e811c4c917141640a76..46d28f74e6b3bd13e0574af535928c1978903a40 100644 (file)
@@ -96,7 +96,7 @@
                     name="pool"
                     class="form-select"
                     formControlName="pool"
-                    (change)="onBlockPoolChange()">
+                    (change)="setNvmeServiceId()">
               <option *ngIf="rbdPools === null"
                       [ngValue]="null"
                       i18n>Loading...</option>
                      class="form-control"
                      type="text"
                      formControlName="group"
-                     (change)="onNvmeofGroupChange($event.target.value)">
+                     (change)="setNvmeServiceId()">
             </div>
             <cd-help-text i18n>
               The name of the gateway group.
                       i18n>
         Modifying the default settings could lead to a weaker security configuration
       </cd-alert-panel>
+
+        <!-- NVMe/TCP -->
+        <!-- mTLS -->
+        <div class="form-group row"
+             *ngIf="serviceForm.controls.service_type.value === 'nvmeof'">
+          <div class="cd-col-form-offset">
+            <div class="custom-control custom-checkbox">
+              <input class="custom-control-input"
+                     id="enable_mtls"
+                     type="checkbox"
+                     formControlName="enable_mtls">
+              <label class="custom-control-label"
+                     for="enable_mtls"
+                     i18n>Encryption</label>
+              <cd-help-text i18n>Enables mutual TLS (mTLS) between the client and the gateway server.</cd-help-text>
+            </div>
+          </div>
+        </div>
+
+        <!-- root_ca_cert -->
+        <div *ngIf="serviceForm.controls.enable_auth.value"
+             class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="root_ca_cert">
+            <span i18n>Root CA certificate</span>
+          </label>
+          <div class="cd-col-form-input">
+            <textarea id="root_ca_cert"
+                      class="form-control resize-vertical text-monospace text-pre"
+                      formControlName="root_ca_cert"
+                      rows="5"></textarea>
+            <input type="file"
+                   (change)="fileUpload($event.target.files, 'root_ca_cert')">
+            <span class="invalid-feedback"
+                  *ngIf="serviceForm.showError('root_ca_cert', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+
+        <!-- client_cert -->
+        <div *ngIf="serviceForm.controls.enable_auth.value"
+             class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="client_cert">
+            <span i18n>Client CA certificate</span>
+          </label>
+          <div class="cd-col-form-input">
+            <textarea id="client_cert"
+                      class="form-control resize-vertical text-monospace text-pre"
+                      formControlName="client_cert"
+                      rows="5"></textarea>
+            <input type="file"
+                   (change)="fileUpload($event.target.files, 'client_cert')">
+            <span class="invalid-feedback"
+                  *ngIf="serviceForm.showError('client_cert', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+
+        <!-- client_key -->
+        <div *ngIf="serviceForm.controls.enable_auth.value"
+             class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="client_key">
+            <span i18n>Client key</span>
+          </label>
+          <div class="cd-col-form-input">
+            <textarea id="client_key"
+                      class="form-control resize-vertical text-monospace text-pre"
+                      formControlName="client_key"
+                      rows="5"></textarea>
+            <input type="file"
+                   (change)="fileUpload($event.target.files, 'client_key')">
+            <span class="invalid-feedback"
+                  *ngIf="serviceForm.showError('client_key', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+
+        <!-- server_cert -->
+        <div *ngIf="serviceForm.controls.enable_auth.value"
+             class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="server_cert">
+            <span i18n>Gateway server certificate</span>
+          </label>
+          <div class="cd-col-form-input">
+            <textarea id="server_cert"
+                      class="form-control resize-vertical text-monospace text-pre"
+                      formControlName="server_cert"
+                      rows="5"></textarea>
+            <input type="file"
+                   (change)="fileUpload($event.target.files, 'server_cert')">
+            <span class="invalid-feedback"
+                  *ngIf="serviceForm.showError('server_cert', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+
+        <!-- server_key -->
+        <div *ngIf="serviceForm.controls.enable_auth.value"
+             class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="server_key">
+            <span i18n>Gateway server key</span>
+          </label>
+          <div class="cd-col-form-input">
+            <textarea id="server_key"
+                      class="form-control resize-vertical text-monospace text-pre"
+                      formControlName="server_key"
+                      rows="5"></textarea>
+            <input type="file"
+                   (change)="fileUpload($event.target.files, 'server_key')">
+            <span class="invalid-feedback"
+                  *ngIf="serviceForm.showError('server_key', frm, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+
       </div>
 
       <div class="modal-footer">
index 0e931d485d0fe8710ce0a53a1785b521708514fd..cbecc908e87d4cc22e9234119398282a307f5887 100644 (file)
@@ -469,7 +469,41 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
         expect(countEl).toBeNull();
       });
 
-      it('should submit nvmeof', () => {
+      it('should not show certs and keys field with mTLS disabled', () => {
+        formHelper.setValue('ssl', true);
+        fixture.detectChanges();
+        const root_ca_cert = fixture.debugElement.query(By.css('#root_ca_cert'));
+        const client_cert = fixture.debugElement.query(By.css('#client_cert'));
+        const client_key = fixture.debugElement.query(By.css('#client_key'));
+        const server_cert = fixture.debugElement.query(By.css('#server_cert'));
+        const server_key = fixture.debugElement.query(By.css('#server_key'));
+        expect(root_ca_cert).toBeNull();
+        expect(client_cert).toBeNull();
+        expect(client_key).toBeNull();
+        expect(server_cert).toBeNull();
+        expect(server_key).toBeNull();
+      });
+
+      it('should submit nvmeof without mTLS', () => {
+        component.onSubmit();
+        expect(cephServiceService.create).toHaveBeenCalledWith({
+          service_type: 'nvmeof',
+          service_id: 'rbd.default',
+          placement: {},
+          unmanaged: false,
+          pool: 'rbd',
+          group: 'default',
+          enable_auth: false
+        });
+      });
+
+      it('should submit nvmeof with mTLS', () => {
+        formHelper.setValue('enable_mtls', true);
+        formHelper.setValue('root_ca_cert', 'root_ca_cert');
+        formHelper.setValue('client_cert', 'client_cert');
+        formHelper.setValue('client_key', 'client_key');
+        formHelper.setValue('server_cert', 'server_cert');
+        formHelper.setValue('server_key', 'server_key');
         component.onSubmit();
         expect(cephServiceService.create).toHaveBeenCalledWith({
           service_type: 'nvmeof',
@@ -477,7 +511,13 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
           placement: {},
           unmanaged: false,
           pool: 'rbd',
-          group: 'default'
+          group: 'default',
+          enable_auth: true,
+          root_ca_cert: 'root_ca_cert',
+          client_cert: 'client_cert',
+          client_key: 'client_key',
+          server_cert: 'server_cert',
+          server_key: 'server_key'
         });
       });
     });
@@ -721,6 +761,25 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
         const groupId = fixture.debugElement.query(By.css('#group')).nativeElement;
         expect(groupId.disabled).toBeTruthy();
       });
+
+      it('should update nvmeof service to disable mTLS', () => {
+        spyOn(cephServiceService, 'update').and.stub();
+        component.serviceType = 'nvmeof';
+        formHelper.setValue('service_type', 'nvmeof');
+        formHelper.setValue('pool', 'rbd');
+        formHelper.setValue('group', 'default');
+        // mTLS disabled
+        formHelper.setValue('enable_auth', false);
+        component.onSubmit();
+        expect(cephServiceService.update).toHaveBeenCalledWith({
+          service_type: 'nvmeof',
+          placement: {},
+          unmanaged: false,
+          pool: 'rbd',
+          group: 'default',
+          enable_auth: false
+        });
+      });
     });
   });
 });
index 27477a845f79204857841edfd68fc6fa5378efe9..aa50aeab0b4a5895c7b2cb8c52d45c62cdc15f98 100644 (file)
@@ -217,6 +217,67 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           service_type: 'nvmeof'
         })
       ],
+      enable_mtls: [false],
+      root_ca_cert: [
+        null,
+        [
+          CdValidators.composeIf(
+            {
+              service_type: 'nvmeof',
+              enable_mtls: true
+            },
+            [Validators.required]
+          )
+        ]
+      ],
+      client_cert: [
+        null,
+        [
+          CdValidators.composeIf(
+            {
+              service_type: 'nvmeof',
+              enable_mtls: true
+            },
+            [Validators.required]
+          )
+        ]
+      ],
+      client_key: [
+        null,
+        [
+          CdValidators.composeIf(
+            {
+              service_type: 'nvmeof',
+              enable_mtls: true
+            },
+            [Validators.required]
+          )
+        ]
+      ],
+      server_cert: [
+        null,
+        [
+          CdValidators.composeIf(
+            {
+              service_type: 'nvmeof',
+              enable_mtls: true
+            },
+            [Validators.required]
+          )
+        ]
+      ],
+      server_key: [
+        null,
+        [
+          CdValidators.composeIf(
+            {
+              service_type: 'nvmeof',
+              enable_mtls: true
+            },
+            [Validators.required]
+          )
+        ]
+      ],
       // RGW
       rgw_frontend_port: [null, [CdValidators.number(false)]],
       realm_name: [null],
@@ -633,6 +694,12 @@ export class ServiceFormComponent extends CdForm implements OnInit {
             case 'nvmeof':
               this.serviceForm.get('pool').setValue(response[0].spec.pool);
               this.serviceForm.get('group').setValue(response[0].spec.group);
+              this.serviceForm.get('enable_auth').setValue(response[0].spec.enable_auth);
+              this.serviceForm.get('root_ca_cert').setValue(response[0].spec.root_ca_cert);
+              this.serviceForm.get('client_cert').setValue(response[0].spec.client_cert);
+              this.serviceForm.get('client_key').setValue(response[0].spec.client_key);
+              this.serviceForm.get('server_cert').setValue(response[0].spec.server_cert);
+              this.serviceForm.get('server_key').setValue(response[0].spec.server_key);
               break;
             case 'rgw':
               this.serviceForm
@@ -961,31 +1028,27 @@ export class ServiceFormComponent extends CdForm implements OnInit {
     );
   }
 
-  onNvmeofGroupChange(groupName: string) {
+  setNvmeServiceId() {
     const pool = this.serviceForm.get('pool').value;
-    if (pool) this.serviceForm.get('service_id').setValue(`${pool}.${groupName}`);
-    else this.serviceForm.get('service_id').setValue(groupName);
-  }
-
-  getDefaultBlockPool(): string {
-    // returns 'rbd' pool otherwise the first block pool
-    return (
-      this.rbdPools?.find((p: Pool) => p.pool_name === 'rbd')?.pool_name ||
-      this.rbdPools?.[0].pool_name
-    );
-  }
-
-  setNvmeofServiceIdAndPool(): void {
-    const defaultBlockPool: string = this.getDefaultBlockPool();
-    const group: string = this.serviceForm.get('group').value;
-    if (defaultBlockPool && group) {
-      this.serviceForm.get('pool').setValue(defaultBlockPool);
-      this.serviceForm.get('service_id').setValue(`${defaultBlockPool}.${group}`);
+    const group = this.serviceForm.get('group').value;
+    if (pool && group) {
+      this.serviceForm.get('service_id').setValue(`${pool}.${group}`);
+    } else if (pool) {
+      this.serviceForm.get('service_id').setValue(pool);
+    } else if (group) {
+      this.serviceForm.get('service_id').setValue(group);
     } else {
       this.serviceForm.get('service_id').setValue(null);
     }
   }
 
+  setNvmeDefaultPool() {
+    const defaultPool =
+      this.rbdPools?.find((p: Pool) => p.pool_name === 'rbd')?.pool_name ||
+      this.rbdPools?.[0].pool_name;
+    this.serviceForm.get('pool').setValue(defaultPool);
+  }
+
   requiresServiceId(serviceType: string) {
     return ['mds', 'rgw', 'nfs', 'iscsi', 'nvmeof', 'smb', 'ingress'].includes(serviceType);
   }
@@ -993,7 +1056,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
   setServiceId(serviceId: string): void {
     const requiresServiceId: boolean = this.requiresServiceId(serviceId);
     if (requiresServiceId && serviceId === 'nvmeof') {
-      this.setNvmeofServiceIdAndPool();
+      this.setNvmeDefaultPool();
+      this.setNvmeServiceId();
     } else if (requiresServiceId) {
       this.serviceForm.get('service_id').setValue(null);
     } else {
@@ -1029,18 +1093,6 @@ export class ServiceFormComponent extends CdForm implements OnInit {
     }
   }
 
-  onBlockPoolChange() {
-    const selectedBlockPool = this.serviceForm.get('pool').value;
-    const group = this.serviceForm.get('group').value;
-    if (selectedBlockPool && group) {
-      this.serviceForm.get('service_id').setValue(`${selectedBlockPool}.${group}`);
-    } else if (selectedBlockPool) {
-      this.serviceForm.get('service_id').setValue(selectedBlockPool);
-    } else {
-      this.serviceForm.get('service_id').setValue(null);
-    }
-  }
-
   disableForEditing(serviceType: string) {
     const disableForEditKeys = ['service_type', 'service_id'];
     disableForEditKeys.forEach((key) => {
@@ -1138,6 +1190,14 @@ export class ServiceFormComponent extends CdForm implements OnInit {
       case 'nvmeof':
         serviceSpec['pool'] = values['pool'];
         serviceSpec['group'] = values['group'];
+        serviceSpec['enable_auth'] = values['enable_mtls'];
+        if (values['enable_mtls']) {
+          serviceSpec['root_ca_cert'] = values['root_ca_cert'];
+          serviceSpec['client_cert'] = values['client_cert'];
+          serviceSpec['client_key'] = values['client_key'];
+          serviceSpec['server_cert'] = values['server_cert'];
+          serviceSpec['server_key'] = values['server_key'];
+        }
         break;
       case 'iscsi':
         serviceSpec['pool'] = values['pool'];
index f30f954b5b65210c36f34b8718be135012d38792..40a0ae365a5f2b60dcf7e3cf63cf0ac02fdf3ef9 100644 (file)
@@ -123,6 +123,7 @@ describe('PoolDetailsComponent', () => {
         expectedChange(
           {
             poolDetails: {
+              application_metadata: ['rbd'],
               pg_num: 256,
               pg_num_target: 256,
               pg_placement_num: 256,
index 172cfd28018e09f54ac4b50d05ef06b822077c43..f07e85a07951a966fec9d53dca86da6012d3ad2e 100644 (file)
@@ -32,6 +32,11 @@ export interface CephServiceAdditionalSpec {
   virtual_interface_networks: string[];
   pool: string;
   group: string;
+  root_ca_cert: string;
+  client_cert: string;
+  client_key: string;
+  server_cert: string;
+  server_key: string;
   rgw_frontend_ssl_certificate: string;
   ssl: boolean;
   ssl_cert: string;