Skip to content

Commit 6174c65

Browse files
authored
Merge pull request ceph#59379 from rhcs-dashboard/oauth2-proxy-ui
mgr/dashboard: add service management for oauth2-proxy Reviewed-by: afreen23 <NOT@FOUND> Reviewed-by: Ankush Behl <[email protected]> Reviewed-by: Redouane Kachach <[email protected]>
2 parents bc3bfe0 + e3953d3 commit 6174c65

File tree

6 files changed

+278
-4
lines changed

6 files changed

+278
-4
lines changed

src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/services.po.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ export class ServicesPageHelper extends PageHelper {
100100
}
101101
break;
102102

103+
case 'oauth2-proxy':
104+
cy.get('#https_address').type('localhost:8443');
105+
cy.get('#provider_display_name').type('provider');
106+
cy.get('#client_id').type('foo');
107+
cy.get('#client_secret').type('bar');
108+
cy.get('#oidc_issuer_url').type('http://127.0.0.0:8080/realms/ceph');
109+
break;
110+
103111
default:
104112
cy.get('#service_id').type('test');
105113
unmanaged

src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/05-services.e2e-spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,14 @@ describe('Services page', () => {
4040

4141
services.deleteService('smb.testsmb');
4242
});
43+
44+
it('should create and delete an oauth2-proxy service', () => {
45+
services.navigateTo('create');
46+
services.addService('oauth2-proxy');
47+
48+
services.checkExist('oauth2-proxy', true);
49+
50+
services.deleteService('oauth2-proxy');
51+
});
4352
});
4453
});

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

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616
Click here</a> to create a new Realm/Zone Group/Zone
1717
</cd-alert-panel>
1818

19+
<cd-alert-panel *ngIf="serviceForm.controls.service_type.value === 'oauth2-proxy'"
20+
type="info"
21+
spacingClass="mb-3"
22+
i18n>
23+
Authentication must be enabled in an active `mgtm-gateway` service to enable Single Sign-On(SSO) with `oauth2-proxy`
24+
</cd-alert-panel>
25+
1926
<!-- Service type -->
2027
<div class="form-group row">
2128
<label class="cd-col-form-label required"
@@ -907,8 +914,146 @@
907914
</div>
908915
</fieldset>
909916
</ng-container>
910-
<!-- RGW, Ingress & iSCSI -->
911-
<ng-container *ngIf="!serviceForm.controls.unmanaged.value && ['rgw', 'iscsi', 'ingress'].includes(serviceForm.controls.service_type.value)">
917+
918+
<!-- oauth2-proxy -->
919+
<ng-container *ngIf="serviceForm.controls.service_type.value === 'oauth2-proxy'">
920+
<!-- provider_display_name -->
921+
<div class="form-group row">
922+
<label class="cd-col-form-label required"
923+
for="provider_display_name">
924+
<span i18n>Provider display name</span>
925+
</label>
926+
<div class="cd-col-form-input">
927+
<input id="provider_display_name"
928+
class="form-control"
929+
type="text"
930+
formControlName="provider_display_name"
931+
placeholder="My OIDC Provider"
932+
i18n-placeholder>
933+
<cd-help-text i18n>The display name for the identity provider (IdP) in the UI.</cd-help-text>
934+
<span class="invalid-feedback"
935+
*ngIf="serviceForm.showError('provider_display_name', frm, 'required')"
936+
i18n>This field is required.</span>
937+
</div>
938+
</div>
939+
<!-- client_id -->
940+
<div class="form-group row">
941+
<label class="cd-col-form-label required"
942+
for="client_id">
943+
<span i18n>Client ID</span>
944+
</label>
945+
<div class="cd-col-form-input">
946+
<input id="client_id"
947+
class="form-control"
948+
type="text"
949+
formControlName="client_id"
950+
placeholder="oauth2-client">
951+
<cd-help-text i18n>The client ID for authenticating with the IdP.</cd-help-text>
952+
<span class="invalid-feedback"
953+
*ngIf="serviceForm.showError('client_id', frm, 'required')"
954+
i18n>This field is required.</span>
955+
</div>
956+
</div>
957+
<!-- client_secret -->
958+
<div class="form-group row">
959+
<label class="cd-col-form-label required"
960+
for="client_secret">
961+
<span i18n>Client secret</span>
962+
</label>
963+
<div class="cd-col-form-input">
964+
<div class="input-group">
965+
<input id="client_secret"
966+
class="form-control"
967+
type="password"
968+
formControlName="client_secret">
969+
<span class="input-group-append">
970+
<button type="button"
971+
class="btn btn-light"
972+
cdPasswordButton="client_secret">
973+
</button>
974+
<cd-copy-2-clipboard-button source="client_secret">
975+
</cd-copy-2-clipboard-button>
976+
</span>
977+
</div>
978+
<cd-help-text i18n>The client secret for authenticating with the IdP.</cd-help-text>
979+
<span class="invalid-feedback"
980+
*ngIf="serviceForm.showError('client_secret', frm, 'required')"
981+
i18n>This field is required.</span>
982+
</div>
983+
</div>
984+
<!-- oidc_issuer_url -->
985+
<div class="form-group row">
986+
<label class="cd-col-form-label required"
987+
for="oidc_issuer_url">
988+
<span i18n>OIDC Issuer URL</span>
989+
</label>
990+
<div class="cd-col-form-input">
991+
<input id="oidc_issuer_url"
992+
class="form-control"
993+
type="text"
994+
formControlName="oidc_issuer_url"
995+
placeholder="https://<IdPs-domain>/realms/<realm-name>">
996+
<cd-help-text i18n>The URL of the OpenID Connect (OIDC) issuer.</cd-help-text>
997+
<span class="invalid-feedback"
998+
*ngIf="serviceForm.showError('oidc_issuer_url', frm, 'required')"
999+
i18n>This field is required.</span>
1000+
<span class="invalid-feedback"
1001+
*ngIf="serviceForm.showError('oidc_issuer_url', frm, 'validUrl')"
1002+
i18n>Invalid url.</span>
1003+
</div>
1004+
</div>
1005+
<!-- https_address -->
1006+
<div class="form-group row">
1007+
<label class="cd-col-form-label"
1008+
for="https_address">
1009+
<span i18n>Https address</span>
1010+
</label>
1011+
<div class="cd-col-form-input">
1012+
<input id="https_address"
1013+
class="form-control"
1014+
type="text"
1015+
formControlName="https_address"
1016+
placeholder="0.0.0.0:4180">
1017+
<cd-help-text i18n>The address for HTTPS connections as [IP|Hostname]:port.</cd-help-text>
1018+
<span class="invalid-feedback"
1019+
*ngIf="serviceForm.showError('https_address', frm, 'invalidAddress')"
1020+
i18n>Format must be [IP|Hostname]:port and the port between 0 and 65535</span>
1021+
</div>
1022+
</div>
1023+
<!-- redirect_url -->
1024+
<div class="form-group row">
1025+
<label class="cd-col-form-label"
1026+
for="redirect_url">
1027+
<span i18n>Redirect URL</span>
1028+
</label>
1029+
<div class="cd-col-form-input">
1030+
<input id="redirect_url"
1031+
class="form-control"
1032+
type="text"
1033+
formControlName="redirect_url"
1034+
placeholder="https://<IP|Hostname>:4180/oauth2/callback">
1035+
<cd-help-text i18n>The URL the oauth2-proxy service will redirect to after a successful login.</cd-help-text>
1036+
</div>
1037+
</div>
1038+
<!-- Allowlist_domains -->
1039+
<div class="form-group row">
1040+
<label class="cd-col-form-label"
1041+
for="allowlist_domains">
1042+
<span i18n>Allowlist domains</span>
1043+
</label>
1044+
<div class="cd-col-form-input">
1045+
<input id="allowlist_domains"
1046+
class="form-control"
1047+
type="text"
1048+
formControlName="allowlist_domains"
1049+
placeholder="domain1.com,192.168.100.1:8080">
1050+
<cd-help-text i18n>Comma separated list of domains to be allowed to redirect to, used for login or logout.</cd-help-text>
1051+
</div>
1052+
</div>
1053+
</ng-container>
1054+
1055+
<!-- RGW, Ingress, iSCSI & Oauth2-proxy -->
1056+
<ng-container *ngIf="!serviceForm.controls.unmanaged.value && ['rgw', 'iscsi', 'ingress', 'oauth2-proxy'].includes(serviceForm.controls.service_type.value)">
9121057
<!-- ssl -->
9131058
<div class="form-group row">
9141059
<div class="cd-col-form-offset">

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

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
4949
readonly SNMP_ENGINE_ID_PATTERN = /^[0-9A-Fa-f]{10,64}/g;
5050
readonly INGRESS_SUPPORTED_SERVICE_TYPES = ['rgw', 'nfs'];
5151
readonly SMB_CONFIG_URI_PATTERN = /^(http:|https:|rados:|rados:mon-config-key:)/;
52+
readonly OAUTH2_ISSUER_URL_PATTERN = /^(https?:\/\/)?([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+)(:[0-9]{1,5})?(\/.*)?$/;
5253
@ViewChild(NgbTypeahead, { static: false })
5354
typeahead: NgbTypeahead;
5455

@@ -328,6 +329,14 @@ export class ServiceFormComponent extends CdForm implements OnInit {
328329
ssl: true
329330
},
330331
[Validators.required, CdValidators.pemCert()]
332+
),
333+
CdValidators.composeIf(
334+
{
335+
service_type: 'oauth2-proxy',
336+
unmanaged: false,
337+
ssl: true
338+
},
339+
[Validators.required, CdValidators.sslCert()]
331340
)
332341
]
333342
],
@@ -341,6 +350,14 @@ export class ServiceFormComponent extends CdForm implements OnInit {
341350
ssl: true
342351
},
343352
[Validators.required, CdValidators.sslPrivKey()]
353+
),
354+
CdValidators.composeIf(
355+
{
356+
service_type: 'oauth2-proxy',
357+
unmanaged: false,
358+
ssl: true
359+
},
360+
[Validators.required, CdValidators.sslPrivKey()]
344361
)
345362
]
346363
],
@@ -425,7 +442,49 @@ export class ServiceFormComponent extends CdForm implements OnInit {
425442
]
426443
],
427444
grafana_port: [null, [CdValidators.number(false)]],
428-
grafana_admin_password: [null]
445+
grafana_admin_password: [null],
446+
// oauth2-proxy
447+
provider_display_name: [
448+
'My OIDC provider',
449+
[
450+
CdValidators.requiredIf({
451+
service_type: 'oauth2-proxy'
452+
})
453+
]
454+
],
455+
client_id: [
456+
null,
457+
[
458+
CdValidators.requiredIf({
459+
service_type: 'oauth2-proxy'
460+
})
461+
]
462+
],
463+
client_secret: [
464+
null,
465+
[
466+
CdValidators.requiredIf({
467+
service_type: 'oauth2-proxy'
468+
})
469+
]
470+
],
471+
oidc_issuer_url: [
472+
null,
473+
[
474+
CdValidators.requiredIf({
475+
service_type: 'oauth2-proxy'
476+
}),
477+
CdValidators.custom('validUrl', (url: string) => {
478+
if (_.isEmpty(url)) {
479+
return false;
480+
}
481+
return !this.OAUTH2_ISSUER_URL_PATTERN.test(url);
482+
})
483+
]
484+
],
485+
https_address: [null, [CdValidators.oauthAddressTest()]],
486+
redirect_url: [null],
487+
allowlist_domains: [null]
429488
});
430489
}
431490

@@ -622,6 +681,23 @@ export class ServiceFormComponent extends CdForm implements OnInit {
622681
.get('grafana_admin_password')
623682
.setValue(response[0].spec.initial_admin_password);
624683
break;
684+
case 'oauth2-proxy':
685+
const oauth2SpecKeys = [
686+
'https_address',
687+
'provider_display_name',
688+
'client_id',
689+
'client_secret',
690+
'oidc_issuer_url',
691+
'redirect_url',
692+
'allowlist_domains'
693+
];
694+
oauth2SpecKeys.forEach((key) => {
695+
this.serviceForm.get(key).setValue(response[0].spec[key]);
696+
});
697+
if (response[0].spec?.ssl) {
698+
this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
699+
this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
700+
}
625701
}
626702
});
627703
}
@@ -686,6 +762,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
686762
case 'jaeger-collector':
687763
case 'jaeger-query':
688764
case 'smb':
765+
case 'oauth2-proxy':
689766
this.serviceForm.get('count').setValue(1);
690767
break;
691768
default:
@@ -1019,9 +1096,22 @@ export class ServiceFormComponent extends CdForm implements OnInit {
10191096
case 'grafana':
10201097
serviceSpec['port'] = values['grafana_port'];
10211098
serviceSpec['initial_admin_password'] = values['grafana_admin_password'];
1099+
break;
1100+
case 'oauth2-proxy':
1101+
serviceSpec['provider_display_name'] = values['provider_display_name']?.trim();
1102+
serviceSpec['client_id'] = values['client_id']?.trim();
1103+
serviceSpec['client_secret'] = values['client_secret']?.trim();
1104+
serviceSpec['oidc_issuer_url'] = values['oidc_issuer_url']?.trim();
1105+
serviceSpec['https_address'] = values['https_address']?.trim();
1106+
serviceSpec['redirect_url'] = values['redirect_url']?.trim();
1107+
serviceSpec['allowlist_domains'] = values['allowlist_domains']?.trim().split(',');
1108+
if (values['ssl']) {
1109+
serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
1110+
serviceSpec['ssl_key'] = values['ssl_key']?.trim();
1111+
}
1112+
break;
10221113
}
10231114
}
1024-
10251115
this.taskWrapperService
10261116
.wrapTaskAroundCall({
10271117
task: new FinishedTask(taskUrl, {

src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,4 +666,21 @@ export class CdValidators {
666666
}
667667
};
668668
}
669+
670+
static oauthAddressTest(): ValidatorFn {
671+
const OAUTH2_HTTPS_ADDRESS_PATTERN = /^((\d{1,3}\.){3}\d{1,3}|([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+)/;
672+
return (control: AbstractControl): Record<string, boolean> | null => {
673+
if (!control.value) {
674+
return null;
675+
}
676+
677+
if (!control.value.includes(':')) {
678+
return { invalidAddress: true };
679+
}
680+
const [address, port] = control.value.split(':');
681+
const addressTest = OAUTH2_HTTPS_ADDRESS_PATTERN.test(address);
682+
const portTest = Number(port) >= 0 && Number(port) <= 65535;
683+
return { invalidAddress: !(addressTest && portTest) };
684+
};
685+
}
669686
}

src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ export interface CephServiceAdditionalSpec {
4747
custom_dns: string[];
4848
join_sources: string[];
4949
include_ceph_users: string[];
50+
https_address: string;
51+
provider_display_name: string;
52+
client_id: string;
53+
client_secret: string;
54+
oidc_issuer_url: string;
5055
}
5156

5257
export interface CephServicePlacement {

0 commit comments

Comments
 (0)