From a50d1408beeface10a712bb8551aebe92d8b6233 Mon Sep 17 00:00:00 2001 From: Michal <55792593+misko626@users.noreply.github.com> Date: Tue, 7 Nov 2023 10:58:31 +0100 Subject: [PATCH] feat(console): add-saml-to-idp (#6687) (#6750) Co-authored-by: Max Peintner --- .../idp-table/idp-table.component.html | 4 + .../modules/idp-table/idp-table.component.ts | 2 + .../idp-settings/idp-settings.component.html | 16 ++ .../provider-jwt/provider-jwt.component.ts | 1 - .../provider-saml-sp.component.html | 68 ++++++ .../provider-saml-sp.component.scss | 0 .../provider-saml-sp.component.spec.ts | 21 ++ .../provider-saml-sp.component.ts | 224 ++++++++++++++++++ .../providers/providers-routing.module.ts | 2 + .../app/modules/providers/providers.module.ts | 2 + console/src/app/services/admin.service.ts | 12 + console/src/app/services/mgmt.service.ts | 12 + console/src/assets/i18n/en.json | 10 + console/src/assets/images/idp/saml-icon.svg | 9 + 14 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html create mode 100644 console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.scss create mode 100644 console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.spec.ts create mode 100644 console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.ts create mode 100644 console/src/assets/images/idp/saml-icon.svg diff --git a/console/src/app/modules/idp-table/idp-table.component.html b/console/src/app/modules/idp-table/idp-table.component.html index 0ced33e751..5d9514789b 100644 --- a/console/src/app/modules/idp-table/idp-table.component.html +++ b/console/src/app/modules/idp-table/idp-table.component.html @@ -85,6 +85,10 @@ Apple +
+ + SAML SP +
coming soon
diff --git a/console/src/app/modules/idp-table/idp-table.component.ts b/console/src/app/modules/idp-table/idp-table.component.ts index c28b4dd49a..4517afece4 100644 --- a/console/src/app/modules/idp-table/idp-table.component.ts +++ b/console/src/app/modules/idp-table/idp-table.component.ts @@ -263,6 +263,8 @@ export class IdpTableComponent implements OnInit, OnDestroy { return [row.owner === IDPOwnerType.IDP_OWNER_TYPE_SYSTEM ? '/instance' : '/org', 'provider', 'github', row.id]; case ProviderType.PROVIDER_TYPE_APPLE: return [row.owner === IDPOwnerType.IDP_OWNER_TYPE_SYSTEM ? '/instance' : '/org', 'provider', 'apple', row.id]; + case ProviderType.PROVIDER_TYPE_SAML: + return [row.owner === IDPOwnerType.IDP_OWNER_TYPE_SYSTEM ? '/instance' : '/org', 'provider', 'saml', row.id]; } } } diff --git a/console/src/app/modules/policies/idp-settings/idp-settings.component.html b/console/src/app/modules/policies/idp-settings/idp-settings.component.html index 9125008c51..b01403dbbb 100644 --- a/console/src/app/modules/policies/idp-settings/idp-settings.component.html +++ b/console/src/app/modules/policies/idp-settings/idp-settings.component.html @@ -196,4 +196,20 @@ Active Directory / LDAP + + + +
+ SAML SP +
+
diff --git a/console/src/app/modules/providers/provider-jwt/provider-jwt.component.ts b/console/src/app/modules/providers/provider-jwt/provider-jwt.component.ts index 33ae22567e..28258ed384 100644 --- a/console/src/app/modules/providers/provider-jwt/provider-jwt.component.ts +++ b/console/src/app/modules/providers/provider-jwt/provider-jwt.component.ts @@ -146,7 +146,6 @@ export class ProviderJWTComponent { req.setJwtEndpoint(this.jwtEndpoint?.value); req.setKeysEndpoint(this.keysEndpoint?.value); req.setProviderOptions(this.options); - this.loading = true; this.service .addJWTProvider(req) diff --git a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html new file mode 100644 index 0000000000..ebf26ed303 --- /dev/null +++ b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html @@ -0,0 +1,68 @@ + +
+
+ +

{{ 'IDP.CREATE.SAML.TITLE' | translate }}

+ +
+ +

+ {{ !provider ? ('IDP.CREATE.SAML.DESCRIPTION' | translate) : ('IDP.DETAIL.DESCRIPTION' | translate) }} +

+ +
+
+ + {{ 'IDP.NAME' | translate }} + + + + {{ 'IDP.SAML.METADATAXML' | translate }} + + + + {{ 'IDP.SAML.METADATAURL' | translate }} + + + + {{ 'IDP.SAML.BINDING' | translate }} + + {{ binding }} + + + + {{ 'IDP.SAML.SIGNEDREQUEST' | translate }} +
+ +
+

{{ 'IDP.OPTIONAL' | translate }}

+ +
+
+ +
+ +
+ +
+
+
+
diff --git a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.scss b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.spec.ts b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.spec.ts new file mode 100644 index 0000000000..bb8d4cc08f --- /dev/null +++ b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProviderSamlSpComponent } from './provider-saml-sp.component'; + +describe('ProviderSamlSpComponent', () => { + let component: ProviderSamlSpComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ProviderSamlSpComponent], + }); + fixture = TestBed.createComponent(ProviderSamlSpComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.ts b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.ts new file mode 100644 index 0000000000..f5be954d20 --- /dev/null +++ b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.ts @@ -0,0 +1,224 @@ +import { Component, Injector, Type } from '@angular/core'; +import { Location } from '@angular/common'; +import { Options, Provider } from '../../../proto/generated/zitadel/idp_pb'; +import { AbstractControl, FormGroup, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; +import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum'; +import { ManagementService } from '../../../services/mgmt.service'; +import { AdminService } from '../../../services/admin.service'; +import { ToastService } from '../../../services/toast.service'; +import { GrpcAuthService } from '../../../services/grpc-auth.service'; +import { take } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; +import { Breadcrumb, BreadcrumbService, BreadcrumbType } from '../../../services/breadcrumb.service'; +import { requiredValidator } from '../../form-field/validators/validators'; +import { + AddSAMLProviderRequest as AdminAddSAMLProviderRequest, + GetProviderByIDRequest as AdminGetProviderByIDRequest, + UpdateSAMLProviderRequest as AdminUpdateSAMLProviderRequest, +} from 'src/app/proto/generated/zitadel/admin_pb'; +import { + AddSAMLProviderRequest as MgmtAddSAMLProviderRequest, + GetProviderByIDRequest as MgmtGetProviderByIDRequest, + UpdateSAMLProviderRequest as MgmtUpdateSAMLProviderRequest, +} from 'src/app/proto/generated/zitadel/management_pb'; +import * as zitadel_idp_pb from '../../../proto/generated/zitadel/idp_pb'; + +@Component({ + selector: 'cnsl-provider-saml-sp', + templateUrl: './provider-saml-sp.component.html', + styleUrls: ['./provider-saml-sp.component.scss'], +}) +export class ProviderSamlSpComponent { + public id: string | null = ''; + public loading: boolean = false; + public provider?: Provider.AsObject; + public form!: FormGroup; + public showOptional: boolean = false; + public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true); + public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT; + private service!: ManagementService | AdminService; + + bindingValues: string[] = Object.keys(zitadel_idp_pb.SAMLBinding); + + constructor( + private _location: Location, + private toast: ToastService, + private authService: GrpcAuthService, + private route: ActivatedRoute, + private injector: Injector, + private breadcrumbService: BreadcrumbService, + ) { + this._buildBreadcrumbs(); + this._initializeForm(); + this._checkFormPermissions(); + } + + private _initializeForm(): void { + this.form = new UntypedFormGroup({ + name: new UntypedFormControl('', [requiredValidator]), + metadataXml: new UntypedFormControl('', [requiredValidator]), + metadataUrl: new UntypedFormControl('', [requiredValidator]), + binding: new UntypedFormControl(this.bindingValues[0], [requiredValidator]), + withSignedRequest: new UntypedFormControl(true, [requiredValidator]), + }); + } + + private _checkFormPermissions(): void { + this.authService + .isAllowed( + this.serviceType === PolicyComponentServiceType.ADMIN + ? ['iam.idp.write'] + : this.serviceType === PolicyComponentServiceType.MGMT + ? ['org.idp.write'] + : [], + ) + .pipe(take(1)) + .subscribe((allowed) => { + if (allowed) { + this.form.enable(); + } else { + this.form.disable(); + } + }); + } + + private _buildBreadcrumbs(): void { + this.route.data.pipe(take(1)).subscribe((data) => { + this.serviceType = data['serviceType']; + switch (this.serviceType) { + case PolicyComponentServiceType.MGMT: + this.service = this.injector.get(ManagementService as Type); + + const bread: Breadcrumb = { + type: BreadcrumbType.ORG, + routerLink: ['/org'], + }; + + this.breadcrumbService.setBreadcrumb([bread]); + break; + case PolicyComponentServiceType.ADMIN: + this.service = this.injector.get(AdminService as Type); + + const iamBread = new Breadcrumb({ + type: BreadcrumbType.ORG, + name: 'Instance', + routerLink: ['/instance'], + }); + this.breadcrumbService.setBreadcrumb([iamBread]); + break; + } + + this.id = this.route.snapshot.paramMap.get('id'); + if (this.id) { + this.getData(this.id); + } + }); + } + + public updateSAMLProvider(): void { + if (this.provider) { + const req = + this.serviceType === PolicyComponentServiceType.MGMT + ? new MgmtUpdateSAMLProviderRequest() + : new AdminUpdateSAMLProviderRequest(); + req.setId(this.provider.id); + req.setName(this.name?.value); + req.setMetadataUrl(this.metadataUrl?.value); + req.setMetadataXml(this.metadataXml?.value); + // @ts-ignore + req.setBinding(zitadel_idp_pb.SAMLBinding[`${this.biding?.value}`]); + req.setProviderOptions(this.options); + + this.loading = true; + this.service + .updateSAMLProvider(req) + .then((idp) => { + setTimeout(() => { + this.loading = false; + this.close(); + }, 2000); + }) + .catch((error) => { + this.toast.showError(error); + this.loading = false; + }); + } + } + + public addSAMLProvider(): void { + const req = + this.serviceType === PolicyComponentServiceType.MGMT + ? new MgmtAddSAMLProviderRequest() + : new AdminAddSAMLProviderRequest(); + req.setName(this.name?.value); + req.setMetadataUrl(this.metadataUrl?.value); + req.setMetadataXml(this.metadataXml?.value); + req.setProviderOptions(this.options); + // @ts-ignore + req.setBinding(zitadel_idp_pb.SAMLBinding[`${this.biding?.value}`]); + req.setWithSignedRequest(this.withSignedRequest?.value); + this.loading = true; + this.service + .addSAMLProvider(req) + .then((idp) => { + setTimeout(() => { + this.loading = false; + this.close(); + }, 2000); + }) + .catch((error) => { + this.toast.showError(error); + this.loading = false; + }); + } + + public submitForm(): void { + this.provider ? this.updateSAMLProvider() : this.addSAMLProvider(); + } + + private getData(id: string): void { + const req = + this.serviceType === PolicyComponentServiceType.ADMIN + ? new AdminGetProviderByIDRequest() + : new MgmtGetProviderByIDRequest(); + req.setId(id); + this.service + .getProviderByID(req) + .then((resp) => { + this.provider = resp.idp; + this.loading = false; + if (this.provider?.config?.saml) { + this.form.patchValue(this.provider.config.saml); + this.name?.setValue(this.provider.name); + } + }) + .catch((error) => { + this.toast.showError(error); + this.loading = false; + }); + } + + close(): void { + this._location.back(); + } + + private get name(): AbstractControl | null { + return this.form.get('name'); + } + + private get metadataXml(): AbstractControl | null { + return this.form.get('metadataXml'); + } + + private get metadataUrl(): AbstractControl | null { + return this.form.get('metadataUrl'); + } + + private get biding(): AbstractControl | null { + return this.form.get('binding'); + } + + private get withSignedRequest(): AbstractControl | null { + return this.form.get('withSignedRequest'); + } +} diff --git a/console/src/app/modules/providers/providers-routing.module.ts b/console/src/app/modules/providers/providers-routing.module.ts index 224a620a99..cb00d66515 100644 --- a/console/src/app/modules/providers/providers-routing.module.ts +++ b/console/src/app/modules/providers/providers-routing.module.ts @@ -13,6 +13,7 @@ import { ProviderJWTComponent } from './provider-jwt/provider-jwt.component'; import { ProviderLDAPComponent } from './provider-ldap/provider-ldap.component'; import { ProviderOAuthComponent } from './provider-oauth/provider-oauth.component'; import { ProviderOIDCComponent } from './provider-oidc/provider-oidc.component'; +import { ProviderSamlSpComponent } from './provider-saml-sp/provider-saml-sp.component'; const typeMap = { [ProviderType.PROVIDER_TYPE_AZURE_AD]: { path: 'azure-ad', component: ProviderAzureADComponent }, @@ -29,6 +30,7 @@ const typeMap = { [ProviderType.PROVIDER_TYPE_OIDC]: { path: 'oidc', component: ProviderOIDCComponent }, [ProviderType.PROVIDER_TYPE_LDAP]: { path: 'ldap', component: ProviderLDAPComponent }, [ProviderType.PROVIDER_TYPE_APPLE]: { path: 'apple', component: ProviderAppleComponent }, + [ProviderType.PROVIDER_TYPE_SAML]: { path: 'saml', component: ProviderSamlSpComponent }, }; const routes: Routes = Object.entries(typeMap).map(([key, value]) => { diff --git a/console/src/app/modules/providers/providers.module.ts b/console/src/app/modules/providers/providers.module.ts index 5af3fee6a8..c3fd43a82f 100644 --- a/console/src/app/modules/providers/providers.module.ts +++ b/console/src/app/modules/providers/providers.module.ts @@ -29,6 +29,7 @@ import { ProviderLDAPComponent } from './provider-ldap/provider-ldap.component'; import { ProviderOAuthComponent } from './provider-oauth/provider-oauth.component'; import { ProviderOIDCComponent } from './provider-oidc/provider-oidc.component'; import { ProvidersRoutingModule } from './providers-routing.module'; +import { ProviderSamlSpComponent } from './provider-saml-sp/provider-saml-sp.component'; @NgModule({ declarations: [ @@ -45,6 +46,7 @@ import { ProvidersRoutingModule } from './providers-routing.module'; ProviderOAuthComponent, ProviderLDAPComponent, ProviderAppleComponent, + ProviderSamlSpComponent, ], imports: [ ProvidersRoutingModule, diff --git a/console/src/app/services/admin.service.ts b/console/src/app/services/admin.service.ts index e2ef755527..7ee37d555c 100644 --- a/console/src/app/services/admin.service.ts +++ b/console/src/app/services/admin.service.ts @@ -40,6 +40,8 @@ import { AddNotificationPolicyResponse, AddOIDCSettingsRequest, AddOIDCSettingsResponse, + AddSAMLProviderRequest, + AddSAMLProviderResponse, AddSecondFactorToLoginPolicyRequest, AddSecondFactorToLoginPolicyResponse, AddSMSProviderTwilioRequest, @@ -262,6 +264,8 @@ import { UpdatePasswordComplexityPolicyResponse, UpdatePrivacyPolicyRequest, UpdatePrivacyPolicyResponse, + UpdateSAMLProviderRequest, + UpdateSAMLProviderResponse, UpdateSecretGeneratorRequest, UpdateSecretGeneratorResponse, UpdateSMSProviderTwilioRequest, @@ -1170,6 +1174,14 @@ export class AdminService { return this.grpcService.admin.updateJWTProvider(req, null).then((resp) => resp.toObject()); } + public addSAMLProvider(req: AddSAMLProviderRequest): Promise { + return this.grpcService.admin.addSAMLProvider(req, null).then((resp) => resp.toObject()); + } + + public updateSAMLProvider(req: UpdateSAMLProviderRequest): Promise { + return this.grpcService.admin.updateSAMLProvider(req, null).then((resp) => resp.toObject()); + } + public addGitHubEnterpriseServerProvider( req: AddGitHubEnterpriseServerProviderRequest, ): Promise { diff --git a/console/src/app/services/mgmt.service.ts b/console/src/app/services/mgmt.service.ts index f2389a25e9..aaf9398b9e 100644 --- a/console/src/app/services/mgmt.service.ts +++ b/console/src/app/services/mgmt.service.ts @@ -83,6 +83,8 @@ import { AddProjectRoleResponse, AddSAMLAppRequest, AddSAMLAppResponse, + AddSAMLProviderRequest, + AddSAMLProviderResponse, AddSecondFactorToLoginPolicyRequest, AddSecondFactorToLoginPolicyResponse, AddUserGrantRequest, @@ -511,6 +513,8 @@ import { UpdateProjectRoleResponse, UpdateSAMLAppConfigRequest, UpdateSAMLAppConfigResponse, + UpdateSAMLProviderRequest, + UpdateSAMLProviderResponse, UpdateUserGrantRequest, UpdateUserGrantResponse, UpdateUserNameRequest, @@ -1037,6 +1041,14 @@ export class ManagementService { return this.grpcService.mgmt.updateJWTProvider(req, null).then((resp) => resp.toObject()); } + public addSAMLProvider(req: AddSAMLProviderRequest): Promise { + return this.grpcService.mgmt.addSAMLProvider(req, null).then((resp) => resp.toObject()); + } + + public updateSAMLProvider(req: UpdateSAMLProviderRequest): Promise { + return this.grpcService.mgmt.updateSAMLProvider(req, null).then((resp) => resp.toObject()); + } + public addGitHubEnterpriseServerProvider( req: AddGitHubEnterpriseServerProviderRequest, ): Promise { diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 5f6df4d41a..7c65ff26bf 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1726,6 +1726,10 @@ "APPLE": { "TITLE": "Sign in with Apple", "DESCRIPTION": "Enter the credentials for your Apple Provider" + }, + "SAML": { + "TITLE": "Sign in with Saml SP", + "DESCRIPTION": "Enter the credentials for your SAML Provider" } }, "DETAIL": { @@ -1847,6 +1851,12 @@ "UPLOADPRIVATEKEY": "Upload Private Key", "KEYMAXSIZEEXCEEDED": "Maximum size of 5kB exceeded." }, + "SAML": { + "METADATAXML": "Metadata Xml", + "METADATAURL": "Metadata URL", + "BINDING": "Binding", + "SIGNEDREQUEST": "Signed Request" + }, "TOAST": { "SAVED": "Successfully saved.", "REACTIVATED": "Idp reactivated.", diff --git a/console/src/assets/images/idp/saml-icon.svg b/console/src/assets/images/idp/saml-icon.svg new file mode 100644 index 0000000000..8150414908 --- /dev/null +++ b/console/src/assets/images/idp/saml-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file