]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add service management for mgmt-gateway 58618/head
authorPedro Gonzalez Gomez <pegonzal@redhat.com>
Tue, 16 Jul 2024 07:53:19 +0000 (09:53 +0200)
committerPedro Gonzalez Gomez <pegonzal@redhat.com>
Mon, 16 Sep 2024 11:44:48 +0000 (13:44 +0200)
Fixes: https://tracker.ceph.com/issues/66963
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/05-services.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
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.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts

index 6eb14315fdbd0eba0412be9743c621cef54a5117..a3160625067cc4a751a9647d439f3a588da90358 100644 (file)
@@ -49,5 +49,14 @@ describe('Services page', () => {
 
       services.deleteService('oauth2-proxy');
     });
+
+    it('should create and delete a mgmt-gateway service', () => {
+      services.navigateTo('create');
+      services.addService('mgmt-gateway');
+
+      services.checkExist('mgmt-gateway', true);
+
+      services.deleteService('mgmt-gateway');
+    });
   });
 });
index 394337ec09c4242ec4b76478622091cdc43135ba..37ba8e0c1df6a5f04cbca5a3666993ebda77e9eb 100644 (file)
@@ -3,6 +3,8 @@ import { NgModule } from '@angular/core';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { RouterModule } from '@angular/router';
 
+import { ComboBoxModule, DropdownModule, CheckboxModule } from 'carbon-components-angular';
+
 import { TreeModule } from '@circlon/angular-tree-component';
 import {
   NgbActiveModal,
@@ -87,7 +89,10 @@ import { MultiClusterDetailsComponent } from './multi-cluster/multi-cluster-deta
     NgbDropdownModule,
     NgxPipeFunctionModule,
     NgbProgressbarModule,
-    DashboardV3Module
+    DashboardV3Module,
+    ComboBoxModule,
+    DropdownModule,
+    CheckboxModule
   ],
   declarations: [
     HostsComponent,
index 91271ed78d755c8ed8794e1aec447dfdd9b8deb7..4e31c133f7846356473450143ea7822ab8c486a9 100644 (file)
                         i18n>
           Authentication must be enabled in an active `mgtm-gateway` service to enable Single Sign-On(SSO) with `oauth2-proxy`
         </cd-alert-panel>
+        <cd-alert-panel *ngIf="serviceForm.controls.service_type.value === 'mgmt-gateway'"
+                        type="info"
+                        spacingClass="mb-3"
+                        i18n>
+          With an active mgmt-gateway service, the dashboard will continue to be served on {{currentURL}}:{{port}} and all other services will be accessible from {{currentURL}}:{{port}}/service_name
+        </cd-alert-panel>
 
         <!-- Service type -->
         <div class="form-group row">
           </div>
         </ng-container>
 
-        <!-- RGW, Ingress, iSCSI & Oauth2-proxy -->
-        <ng-container *ngIf="!serviceForm.controls.unmanaged.value && ['rgw', 'iscsi', 'ingress', 'oauth2-proxy'].includes(serviceForm.controls.service_type.value)">
-          <!-- ssl -->
+        <ng-container *ngIf="!serviceForm.controls.unmanaged.value && ['mgmt-gateway'].includes(serviceForm.controls.service_type.value)">
+          <!-- port -->
           <div class="form-group row">
-            <div class="cd-col-form-offset">
-              <div class="custom-control custom-checkbox">
-                <input class="custom-control-input"
-                       id="ssl"
-                       type="checkbox"
-                       formControlName="ssl">
-                <label class="custom-control-label"
-                       for="ssl"
-                       i18n>SSL</label>
-              </div>
+            <label i18n
+                   class="cd-col-form-label"
+                   for="port">Port</label>
+            <div class="cd-col-form-input">
+              <input id="port"
+                     class="form-control"
+                     type="number"
+                     formControlName="port"
+                     min="1"
+                     max="65535">
+              <span class="invalid-feedback"
+                    *ngIf="serviceForm.showError('port', frm, 'pattern')"
+                    i18n>The entered value needs to be a number.</span>
+              <span class="invalid-feedback"
+                    *ngIf="serviceForm.showError('port', frm, 'min')"
+                    i18n>The value must be at least 1.</span>
+              <span class="invalid-feedback"
+                    *ngIf="serviceForm.showError('port', frm, 'max')"
+                    i18n>The value cannot exceed 65535.</span>
             </div>
           </div>
-
+          <!-- enable_auth -->
+          <div class="form-item">
+            <fieldset>
+              <label class="cds--label"
+                     for="pools"
+                     i18n>Authentication</label>
+                <cds-checkbox i18n-label
+                              id="enable_auth"
+                              name="enable_auth"
+                              formControlName="enable_auth">
+                Enable
+                <cd-help-text i18n>
+                  Allows to enable authentication through an external Identity Provider (IdP) using Single Sign-On (SSO)
+                </cd-help-text>
+              </cds-checkbox>
+            </fieldset>
+          </div>
+          <!-- ssl_protocols -->
+          <div class="form-item">
+            <cds-combo-box type="multi"
+                           label="SSL protocols"
+                           selectionFeedback="top-after-reopen"
+                           for="ssl_protocols"
+                           name="ssl_protocols"
+                           formControlName="ssl_protocols"
+                           id="ssl_protocols"
+                           placeholder="Select protocols..."
+                           [appendInline]="true"
+                           [items]="sslProtocolsItems"
+                           i18n-placeholder
+                           i18n>
+              <cds-dropdown-list></cds-dropdown-list>
+            </cds-combo-box>
+          </div>
+          <!-- ssl_ciphers -->
+          <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="ssl_ciphers">
+            <span i18n>SSL ciphers</span>
+          </label>
+          <div class="cd-col-form-input">
+            <div class="input-group">
+              <input id="ssl_ciphers"
+                     class="form-control"
+                     type="text"
+                     formControlName="ssl_ciphers"
+                     placeholder="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256">
+            </div>
+            <cd-help-text i18n>Default cipher list used: <a href="https://ssl-config.mozilla.org/#server=nginx"
+                                                            target="_blank">https://ssl-config.mozilla.org/#server=nginx</a></cd-help-text>
+            <span class="invalid-feedback"
+                  *ngIf="serviceForm.showError('ssl_ciphers', frm, 'invalidPattern')"
+                  i18n>Invalid cipher suite. Each cipher must be separated by '-' and each cipher suite must be separated by ':'</span>
+          </div>
+        </div>
+        </ng-container>
+        <!-- RGW, Ingress, iSCSI, Oauth2-proxy & mgmt-gateway -->
+        <ng-container *ngIf="!serviceForm.controls.unmanaged.value && ['rgw', 'iscsi', 'ingress', 'oauth2-proxy', 'mgmt-gateway'].includes(serviceForm.controls.service_type.value)">
+          <!-- ssl -->
+          <ng-container *ngIf="!['mgmt-gateway'].includes(serviceForm.controls.service_type.value)">
+            <div class="form-group row">
+              <div class="cd-col-form-offset">
+                <div class="custom-control custom-checkbox">
+                  <input class="custom-control-input"
+                         id="ssl"
+                         type="checkbox"
+                         formControlName="ssl">
+                  <label class="custom-control-label"
+                         for="ssl"
+                         i18n>SSL</label>
+                </div>
+              </div>
+            </div>
+          </ng-container>
           <!-- ssl_cert -->
-          <div *ngIf="serviceForm.controls.ssl.value"
+          <div *ngIf="serviceForm.controls.ssl.value || ['mgmt-gateway'].includes(serviceForm.controls.service_type.value)"
                class="form-group row">
             <label class="cd-col-form-label"
                    for="ssl_cert">
           </div>
 
           <!-- ssl_key -->
-          <div *ngIf="serviceForm.controls.ssl.value && !(['rgw', 'ingress'].includes(serviceForm.controls.service_type.value))"
+          <div *ngIf="(serviceForm.controls.ssl.value && !(['rgw', 'ingress'].includes(serviceForm.controls.service_type.value))) || ['mgmt-gateway'].includes(serviceForm.controls.service_type.value)"
                class="form-group row">
             <label class="cd-col-form-label"
                    for="ssl_key">
             </div>
           </div>
         </ng-container>
+
+      <cd-alert-panel *ngIf="serviceForm.controls.service_type.value === 'mgmt-gateway' && showMgmtGatewayMessage"
+                      type="warning"
+                      spacingClass="mb-3"
+                      i18n>
+        Modifying the default settings could lead to a weaker security configuration
+      </cd-alert-panel>
       </div>
 
       <div class="modal-footer">
index 878ab179b188af42a2909ce95e48e5b6054031c3..ac4578cbf22f7a650de1f9729865cdee92dd4fda 100644 (file)
@@ -4,6 +4,7 @@ import { AbstractControl, UntypedFormControl, Validators } from '@angular/forms'
 import { ActivatedRoute, Router } from '@angular/router';
 
 import { NgbActiveModal, NgbModalRef, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
+import { ListItem } from 'carbon-components-angular';
 import _ from 'lodash';
 import { forkJoin, merge, Observable, Subject, Subscription } from 'rxjs';
 import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
@@ -24,7 +25,9 @@ import { SelectOption } from '~/app/shared/components/select/select-option.model
 import {
   ActionLabelsI18n,
   TimerServiceInterval,
-  URLVerbs
+  URLVerbs,
+  SSL_PROTOCOLS,
+  SSL_CIPHERS
 } from '~/app/shared/constants/app.constants';
 import { CdForm } from '~/app/shared/forms/cd-form';
 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
@@ -50,6 +53,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
   readonly INGRESS_SUPPORTED_SERVICE_TYPES = ['rgw', 'nfs'];
   readonly SMB_CONFIG_URI_PATTERN = /^(http:|https:|rados:|rados:mon-config-key:)/;
   readonly OAUTH2_ISSUER_URL_PATTERN = /^(https?:\/\/)?([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+)(:[0-9]{1,5})?(\/.*)?$/;
+  readonly SSL_CIPHERS_PATTERN = /^[a-zA-Z0-9\-:]+$/;
+  readonly DEFAULT_SSL_PROTOCOL_ITEM = [{ content: 'TLSv1.3', selected: true }];
   @ViewChild(NgbTypeahead, { static: false })
   typeahead: NgbTypeahead;
 
@@ -90,6 +95,17 @@ export class ServiceFormComponent extends CdForm implements OnInit {
   zonegroupNames: string[];
   zoneNames: string[];
   smbFeaturesList = ['domain'];
+  currentURL: string;
+  port: number = 443;
+  sslProtocolsItems: Array<ListItem> = Object.values(SSL_PROTOCOLS).map((protocol) => ({
+    content: protocol,
+    selected: true
+  }));
+  sslCiphersItems: Array<ListItem> = Object.values(SSL_CIPHERS).map((cipher) => ({
+    content: cipher,
+    selected: false
+  }));
+  showMgmtGatewayMessage: boolean = false;
 
   constructor(
     public actionLabels: ActionLabelsI18n,
@@ -301,6 +317,18 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         ]
       ],
       virtual_interface_networks: [null],
+      ssl_protocols: [this.DEFAULT_SSL_PROTOCOL_ITEM],
+      ssl_ciphers: [
+        null,
+        [
+          CdValidators.custom('invalidPattern', (ciphers: string) => {
+            if (_.isEmpty(ciphers)) {
+              return false;
+            }
+            return !this.SSL_CIPHERS_PATTERN.test(ciphers);
+          })
+        ]
+      ],
       // RGW, Ingress & iSCSI
       ssl: [false],
       ssl_cert: [
@@ -337,6 +365,14 @@ export class ServiceFormComponent extends CdForm implements OnInit {
               ssl: true
             },
             [Validators.required, CdValidators.sslCert()]
+          ),
+          CdValidators.composeIf(
+            {
+              service_type: 'mgmt-gateway',
+              unmanaged: false,
+              ssl: false
+            },
+            [CdValidators.sslCert()]
           )
         ]
       ],
@@ -358,9 +394,20 @@ export class ServiceFormComponent extends CdForm implements OnInit {
               ssl: true
             },
             [Validators.required, CdValidators.sslPrivKey()]
+          ),
+          CdValidators.composeIf(
+            {
+              service_type: 'mgmt-gateway',
+              unmanaged: false,
+              ssl: false
+            },
+            [CdValidators.sslPrivKey()]
           )
         ]
       ],
+      // mgmt-gateway
+      enable_auth: [null],
+      port: [443, [CdValidators.number(false)]],
       // snmp-gateway
       snmp_version: [
         null,
@@ -620,6 +667,36 @@ export class ServiceFormComponent extends CdForm implements OnInit {
                 this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
               }
               break;
+            case 'mgmt-gateway':
+              let hrefSplitted = window.location.href.split(':');
+              this.currentURL = hrefSplitted[0] + hrefSplitted[1];
+              this.port = response[0].spec?.port;
+
+              if (response[0].spec?.ssl_protocols) {
+                let selectedValues: Array<ListItem> = [];
+                for (const value of response[0].spec.ssl_protocols) {
+                  selectedValues.push({ content: value, selected: true });
+                }
+                this.serviceForm.get('ssl_protocols').setValue(selectedValues);
+              }
+              if (response[0].spec?.ssl_ciphers) {
+                this.serviceForm
+                  .get('ssl_ciphers')
+                  .setValue(response[0].spec?.ssl_ciphers.join(':'));
+              }
+              if (response[0].spec?.ssl_cert) {
+                this.serviceForm.get('ssl_cert').setValue(response[0].spec.ssl_certificate);
+              }
+              if (response[0].spec?.ssl_key) {
+                this.serviceForm.get('ssl_key').setValue(response[0].spec.ssl_certificate_key);
+              }
+              if (response[0].spec?.enable_auth) {
+                this.serviceForm.get('enable_auth').setValue(response[0].spec.enable_auth);
+              }
+              if (response[0].spec?.port) {
+                this.serviceForm.get('port').setValue(response[0].spec.port);
+              }
+              break;
             case 'smb':
               const smbSpecKeys = [
                 'cluster_id',
@@ -701,6 +778,35 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           }
         });
     }
+    this.detectChanges();
+  }
+
+  detectChanges(): void {
+    const service_type = this.serviceForm.get('service_type');
+    if (service_type) {
+      service_type.valueChanges.subscribe((value) => {
+        if (value === 'mgmt-gateway') {
+          const port = this.serviceForm.get('port');
+          if (port) {
+            port.valueChanges.subscribe((_) => {
+              this.showMgmtGatewayMessage = true;
+            });
+          }
+          const ssl_protocols = this.serviceForm.get('ssl_protocols');
+          if (ssl_protocols) {
+            ssl_protocols.valueChanges.subscribe((_) => {
+              this.showMgmtGatewayMessage = true;
+            });
+          }
+          const ssl_ciphers = this.serviceForm.get('ssl_ciphers');
+          if (ssl_ciphers) {
+            ssl_ciphers.valueChanges.subscribe((_) => {
+              this.showMgmtGatewayMessage = true;
+            });
+          }
+        }
+      });
+    }
   }
 
   getDefaultsEntitiesForRgw(
@@ -763,6 +869,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
       case 'jaeger-query':
       case 'smb':
       case 'oauth2-proxy':
+      case 'mgmt-gateway':
         this.serviceForm.get('count').setValue(1);
         break;
       default:
@@ -894,6 +1001,14 @@ export class ServiceFormComponent extends CdForm implements OnInit {
     if (selectedServiceType === 'rgw') {
       this.setRgwFields();
     }
+    if (selectedServiceType === 'mgmt-gateway') {
+      let hrefSplitted = window.location.href.split(':');
+      this.currentURL = hrefSplitted[0] + hrefSplitted[1];
+      // mgmt-gateway lacks HA for now
+      this.serviceForm.get('count').disable();
+    } else {
+      this.serviceForm.get('count').enable();
+    }
   }
 
   onPlacementChange(selected: string) {
@@ -1093,6 +1208,23 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           }
           serviceSpec['virtual_interface_networks'] = values['virtual_interface_networks'];
           break;
+        case 'mgmt-gateway':
+          serviceSpec['ssl_certificate'] = values['ssl_cert']?.trim();
+          serviceSpec['ssl_certificate_key'] = values['ssl_key']?.trim();
+          serviceSpec['enable_auth'] = values['enable_auth'];
+          serviceSpec['port'] = values['port'];
+          if (serviceSpec['port'] === (443 || 80)) {
+            // omit port default values due to issues with redirect_url on the backend
+            delete serviceSpec['port'];
+          }
+          serviceSpec['ssl_protocols'] = [];
+          if (values['ssl_protocols'] != this.DEFAULT_SSL_PROTOCOL_ITEM) {
+            for (const key of Object.keys(values['ssl_protocols'])) {
+              serviceSpec['ssl_protocols'].push(values['ssl_protocols'][key]['content']);
+            }
+          }
+          serviceSpec['ssl_ciphers'] = values['ssl_ciphers']?.trim().split(':');
+          break;
         case 'grafana':
           serviceSpec['port'] = values['grafana_port'];
           serviceSpec['initial_admin_password'] = values['grafana_admin_password'];
@@ -1104,7 +1236,11 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           serviceSpec['oidc_issuer_url'] = values['oidc_issuer_url']?.trim();
           serviceSpec['https_address'] = values['https_address']?.trim();
           serviceSpec['redirect_url'] = values['redirect_url']?.trim();
-          serviceSpec['allowlist_domains'] = values['allowlist_domains']?.trim().split(',');
+          serviceSpec['allowlist_domains'] = values['allowlist_domains']
+            .split(',')
+            .map((domain: string) => {
+              return domain.trim();
+            });
           if (values['ssl']) {
             serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
             serviceSpec['ssl_key'] = values['ssl_key']?.trim();
index 96bb439bec3204fbc02776b34b55d36571fcf21b..bf7cb6b9567f3977f5324c30c5b91ed79d7ca458 100644 (file)
@@ -352,3 +352,19 @@ export class TimerServiceInterval {
     this.TIMER_SERVICE_PERIOD = 5000;
   }
 }
+
+export const SSL_PROTOCOLS = ['TLSv1.2', 'TLSv1.3'];
+
+export const SSL_CIPHERS = [
+  'ECDHE',
+  'ECDSA',
+  'AES128',
+  'GCM',
+  'SHA256',
+  'RSA',
+  'AES256',
+  'SHA384',
+  'CHACHA20',
+  'POLY1305',
+  'DHE'
+];
index 434c263582ad324c4ef7a99c491e8ee3a02e7535..172cfd28018e09f54ac4b50d05ef06b822077c43 100644 (file)
@@ -35,7 +35,11 @@ export interface CephServiceAdditionalSpec {
   rgw_frontend_ssl_certificate: string;
   ssl: boolean;
   ssl_cert: string;
+  ssl_certificate: string;
   ssl_key: string;
+  ssl_certificate_key: string;
+  ssl_protocols: string[];
+  ssl_ciphers: string[];
   port: number;
   initial_admin_password: string;
   rgw_realm: string;
@@ -52,6 +56,7 @@ export interface CephServiceAdditionalSpec {
   client_id: string;
   client_secret: string;
   oidc_issuer_url: string;
+  enable_auth: boolean;
 }
 
 export interface CephServicePlacement {