]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: add service management for oauth2-proxy
authorPedro Gonzalez Gomez <pegonzal@redhat.com>
Wed, 21 Aug 2024 11:20:53 +0000 (13:20 +0200)
committerPedro Gonzalez Gomez <pegonzal@redhat.com>
Sun, 15 Sep 2024 22:21:00 +0000 (00:21 +0200)
Fixes: https://tracker.ceph.com/issues/67651
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/services.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/05-services.e2e-spec.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/forms/cd-validators.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts

index 4cb5223d46ac2ab77b764d3a0120521d36a2e6bd..c546d5cc513a5aa4373bc1c03a0295a8fe634bfb 100644 (file)
@@ -100,6 +100,14 @@ export class ServicesPageHelper extends PageHelper {
           }
           break;
 
+        case 'oauth2-proxy':
+          cy.get('#https_address').type('localhost:8443');
+          cy.get('#provider_display_name').type('provider');
+          cy.get('#client_id').type('foo');
+          cy.get('#client_secret').type('bar');
+          cy.get('#oidc_issuer_url').type('http://127.0.0.0:8080/realms/ceph');
+          break;
+
         default:
           cy.get('#service_id').type('test');
           unmanaged
index 0f30542f793c0207b1c01dbf035f09e8b90663e0..6eb14315fdbd0eba0412be9743c621cef54a5117 100644 (file)
@@ -40,5 +40,14 @@ describe('Services page', () => {
 
       services.deleteService('smb.testsmb');
     });
+
+    it('should create and delete an oauth2-proxy service', () => {
+      services.navigateTo('create');
+      services.addService('oauth2-proxy');
+
+      services.checkExist('oauth2-proxy', true);
+
+      services.deleteService('oauth2-proxy');
+    });
   });
 });
index 586ca57209fe9fbafda3300c4d1e88e7ac88d7d0..91271ed78d755c8ed8794e1aec447dfdd9b8deb7 100644 (file)
              Click here</a> to create a new Realm/Zone Group/Zone
         </cd-alert-panel>
 
+        <cd-alert-panel *ngIf="serviceForm.controls.service_type.value === 'oauth2-proxy'"
+                        type="info"
+                        spacingClass="mb-3"
+                        i18n>
+          Authentication must be enabled in an active `mgtm-gateway` service to enable Single Sign-On(SSO) with `oauth2-proxy`
+        </cd-alert-panel>
+
         <!-- Service type -->
         <div class="form-group row">
           <label class="cd-col-form-label required"
             </div>
           </fieldset>
         </ng-container>
-        <!-- RGW, Ingress & iSCSI -->
-        <ng-container *ngIf="!serviceForm.controls.unmanaged.value && ['rgw', 'iscsi', 'ingress'].includes(serviceForm.controls.service_type.value)">
+
+        <!-- oauth2-proxy -->
+        <ng-container *ngIf="serviceForm.controls.service_type.value === 'oauth2-proxy'">
+          <!-- provider_display_name -->
+          <div class="form-group row">
+            <label class="cd-col-form-label required"
+                   for="provider_display_name">
+              <span i18n>Provider display name</span>
+            </label>
+            <div class="cd-col-form-input">
+              <input id="provider_display_name"
+                     class="form-control"
+                     type="text"
+                     formControlName="provider_display_name"
+                     placeholder="My OIDC Provider"
+                     i18n-placeholder>
+              <cd-help-text i18n>The display name for the identity provider (IdP) in the UI.</cd-help-text>
+              <span class="invalid-feedback"
+                    *ngIf="serviceForm.showError('provider_display_name', frm, 'required')"
+                    i18n>This field is required.</span>
+            </div>
+          </div>
+          <!-- client_id -->
+          <div class="form-group row">
+            <label class="cd-col-form-label required"
+                   for="client_id">
+              <span i18n>Client ID</span>
+            </label>
+            <div class="cd-col-form-input">
+              <input id="client_id"
+                     class="form-control"
+                     type="text"
+                     formControlName="client_id"
+                     placeholder="oauth2-client">
+              <cd-help-text i18n>The client ID for authenticating with the IdP.</cd-help-text>
+              <span class="invalid-feedback"
+                    *ngIf="serviceForm.showError('client_id', frm, 'required')"
+                    i18n>This field is required.</span>
+            </div>
+          </div>
+          <!-- client_secret -->
+          <div class="form-group row">
+            <label class="cd-col-form-label required"
+                   for="client_secret">
+              <span i18n>Client secret</span>
+            </label>
+            <div class="cd-col-form-input">
+              <div class="input-group">
+                <input id="client_secret"
+                       class="form-control"
+                       type="password"
+                       formControlName="client_secret">
+                <span class="input-group-append">
+                  <button type="button"
+                          class="btn btn-light"
+                          cdPasswordButton="client_secret">
+                  </button>
+                  <cd-copy-2-clipboard-button source="client_secret">
+                  </cd-copy-2-clipboard-button>
+                </span>
+              </div>
+              <cd-help-text i18n>The client secret for authenticating with the IdP.</cd-help-text>
+              <span class="invalid-feedback"
+                    *ngIf="serviceForm.showError('client_secret', frm, 'required')"
+                    i18n>This field is required.</span>
+            </div>
+          </div>
+          <!-- oidc_issuer_url -->
+          <div class="form-group row">
+            <label class="cd-col-form-label required"
+                   for="oidc_issuer_url">
+              <span i18n>OIDC Issuer URL</span>
+            </label>
+            <div class="cd-col-form-input">
+              <input id="oidc_issuer_url"
+                     class="form-control"
+                     type="text"
+                     formControlName="oidc_issuer_url"
+                     placeholder="https://<IdPs-domain>/realms/<realm-name>">
+              <cd-help-text i18n>The URL of the OpenID Connect (OIDC) issuer.</cd-help-text>
+              <span class="invalid-feedback"
+                    *ngIf="serviceForm.showError('oidc_issuer_url', frm, 'required')"
+                    i18n>This field is required.</span>
+              <span class="invalid-feedback"
+                    *ngIf="serviceForm.showError('oidc_issuer_url', frm, 'validUrl')"
+                    i18n>Invalid url.</span>
+            </div>
+          </div>
+          <!-- https_address -->
+          <div class="form-group row">
+            <label class="cd-col-form-label"
+                   for="https_address">
+              <span i18n>Https address</span>
+            </label>
+            <div class="cd-col-form-input">
+              <input id="https_address"
+                     class="form-control"
+                     type="text"
+                     formControlName="https_address"
+                     placeholder="0.0.0.0:4180">
+              <cd-help-text i18n>The address for HTTPS connections as [IP|Hostname]:port.</cd-help-text>
+              <span class="invalid-feedback"
+                    *ngIf="serviceForm.showError('https_address', frm, 'invalidAddress')"
+                    i18n>Format must be [IP|Hostname]:port and the port between 0 and 65535</span>
+            </div>
+          </div>
+          <!-- redirect_url -->
+          <div class="form-group row">
+            <label class="cd-col-form-label"
+                   for="redirect_url">
+              <span i18n>Redirect URL</span>
+            </label>
+            <div class="cd-col-form-input">
+              <input id="redirect_url"
+                     class="form-control"
+                     type="text"
+                     formControlName="redirect_url"
+                     placeholder="https://<IP|Hostname>:4180/oauth2/callback">
+              <cd-help-text i18n>The URL the oauth2-proxy service will redirect to after a successful login.</cd-help-text>
+            </div>
+          </div>
+          <!-- Allowlist_domains -->
+          <div class="form-group row">
+            <label class="cd-col-form-label"
+                   for="allowlist_domains">
+              <span i18n>Allowlist domains</span>
+            </label>
+            <div class="cd-col-form-input">
+              <input id="allowlist_domains"
+                     class="form-control"
+                     type="text"
+                     formControlName="allowlist_domains"
+                     placeholder="domain1.com,192.168.100.1:8080">
+              <cd-help-text i18n>Comma separated list of domains to be allowed to redirect to, used for login or logout.</cd-help-text>
+            </div>
+          </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 -->
           <div class="form-group row">
             <div class="cd-col-form-offset">
index bada177f735b46de7da27250c3c2185d62622ed8..878ab179b188af42a2909ce95e48e5b6054031c3 100644 (file)
@@ -49,6 +49,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
   readonly SNMP_ENGINE_ID_PATTERN = /^[0-9A-Fa-f]{10,64}/g;
   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})?(\/.*)?$/;
   @ViewChild(NgbTypeahead, { static: false })
   typeahead: NgbTypeahead;
 
@@ -328,6 +329,14 @@ export class ServiceFormComponent extends CdForm implements OnInit {
               ssl: true
             },
             [Validators.required, CdValidators.pemCert()]
+          ),
+          CdValidators.composeIf(
+            {
+              service_type: 'oauth2-proxy',
+              unmanaged: false,
+              ssl: true
+            },
+            [Validators.required, CdValidators.sslCert()]
           )
         ]
       ],
@@ -341,6 +350,14 @@ export class ServiceFormComponent extends CdForm implements OnInit {
               ssl: true
             },
             [Validators.required, CdValidators.sslPrivKey()]
+          ),
+          CdValidators.composeIf(
+            {
+              service_type: 'oauth2-proxy',
+              unmanaged: false,
+              ssl: true
+            },
+            [Validators.required, CdValidators.sslPrivKey()]
           )
         ]
       ],
@@ -425,7 +442,49 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         ]
       ],
       grafana_port: [null, [CdValidators.number(false)]],
-      grafana_admin_password: [null]
+      grafana_admin_password: [null],
+      // oauth2-proxy
+      provider_display_name: [
+        'My OIDC provider',
+        [
+          CdValidators.requiredIf({
+            service_type: 'oauth2-proxy'
+          })
+        ]
+      ],
+      client_id: [
+        null,
+        [
+          CdValidators.requiredIf({
+            service_type: 'oauth2-proxy'
+          })
+        ]
+      ],
+      client_secret: [
+        null,
+        [
+          CdValidators.requiredIf({
+            service_type: 'oauth2-proxy'
+          })
+        ]
+      ],
+      oidc_issuer_url: [
+        null,
+        [
+          CdValidators.requiredIf({
+            service_type: 'oauth2-proxy'
+          }),
+          CdValidators.custom('validUrl', (url: string) => {
+            if (_.isEmpty(url)) {
+              return false;
+            }
+            return !this.OAUTH2_ISSUER_URL_PATTERN.test(url);
+          })
+        ]
+      ],
+      https_address: [null, [CdValidators.oauthAddressTest()]],
+      redirect_url: [null],
+      allowlist_domains: [null]
     });
   }
 
@@ -622,6 +681,23 @@ export class ServiceFormComponent extends CdForm implements OnInit {
                 .get('grafana_admin_password')
                 .setValue(response[0].spec.initial_admin_password);
               break;
+            case 'oauth2-proxy':
+              const oauth2SpecKeys = [
+                'https_address',
+                'provider_display_name',
+                'client_id',
+                'client_secret',
+                'oidc_issuer_url',
+                'redirect_url',
+                'allowlist_domains'
+              ];
+              oauth2SpecKeys.forEach((key) => {
+                this.serviceForm.get(key).setValue(response[0].spec[key]);
+              });
+              if (response[0].spec?.ssl) {
+                this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
+                this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
+              }
           }
         });
     }
@@ -686,6 +762,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
       case 'jaeger-collector':
       case 'jaeger-query':
       case 'smb':
+      case 'oauth2-proxy':
         this.serviceForm.get('count').setValue(1);
         break;
       default:
@@ -1019,9 +1096,22 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         case 'grafana':
           serviceSpec['port'] = values['grafana_port'];
           serviceSpec['initial_admin_password'] = values['grafana_admin_password'];
+          break;
+        case 'oauth2-proxy':
+          serviceSpec['provider_display_name'] = values['provider_display_name']?.trim();
+          serviceSpec['client_id'] = values['client_id']?.trim();
+          serviceSpec['client_secret'] = values['client_secret']?.trim();
+          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(',');
+          if (values['ssl']) {
+            serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
+            serviceSpec['ssl_key'] = values['ssl_key']?.trim();
+          }
+          break;
       }
     }
-
     this.taskWrapperService
       .wrapTaskAroundCall({
         task: new FinishedTask(taskUrl, {
index c72b0b89e1295f4440aa03eca02953ae94f566ce..15f166f4a2506576397eeabb1792fc8960e0ce58 100644 (file)
@@ -666,4 +666,21 @@ export class CdValidators {
       }
     };
   }
+
+  static oauthAddressTest(): ValidatorFn {
+    const OAUTH2_HTTPS_ADDRESS_PATTERN = /^((\d{1,3}\.){3}\d{1,3}|([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+)/;
+    return (control: AbstractControl): Record<string, boolean> | null => {
+      if (!control.value) {
+        return null;
+      }
+
+      if (!control.value.includes(':')) {
+        return { invalidAddress: true };
+      }
+      const [address, port] = control.value.split(':');
+      const addressTest = OAUTH2_HTTPS_ADDRESS_PATTERN.test(address);
+      const portTest = Number(port) >= 0 && Number(port) <= 65535;
+      return { invalidAddress: !(addressTest && portTest) };
+    };
+  }
 }
index 8f2d3faca7191f149556ed1403985d4d9b563c01..434c263582ad324c4ef7a99c491e8ee3a02e7535 100644 (file)
@@ -47,6 +47,11 @@ export interface CephServiceAdditionalSpec {
   custom_dns: string[];
   join_sources: string[];
   include_ceph_users: string[];
+  https_address: string;
+  provider_display_name: string;
+  client_id: string;
+  client_secret: string;
+  oidc_issuer_url: string;
 }
 
 export interface CephServicePlacement {