feat(console): add-saml-to-idp (#6687) (#6750)

Co-authored-by: Max Peintner <max@caos.ch>
This commit is contained in:
Michal 2023-11-07 10:58:31 +01:00 committed by GitHub
parent b3ff359fc1
commit a50d1408be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 382 additions and 1 deletions

View File

@ -85,6 +85,10 @@
<img class="idp-logo apple light" src="./assets/images/idp/apple.svg" alt="apple" />
Apple
</div>
<div class="idp-table-provider-type" *ngSwitchCase="ProviderType.PROVIDER_TYPE_SAML">
<img class="idp-logo" src="./assets/images/idp/saml-icon.svg" alt="saml" />
SAML SP
</div>
<div class="idp-table-provider-type" *ngSwitchDefault>coming soon</div>
</div>
</td>

View File

@ -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];
}
}
}

View File

@ -196,4 +196,20 @@
<span class="title">Active Directory / LDAP</span>
</div>
</a>
<a
class="item card"
[routerLink]="
serviceType === PolicyComponentServiceType.ADMIN
? ['/instance', 'provider', 'saml', 'create']
: serviceType === PolicyComponentServiceType.MGMT
? ['/org', 'provider', 'saml', 'create']
: []
"
>
<img class="idp-logo" src="./assets/images/idp/saml-icon.svg" alt="oauth" />
<div class="text-container">
<span class="title">SAML SP</span>
</div>
</a>
</div>

View File

@ -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)

View File

@ -0,0 +1,68 @@
<cnsl-create-layout
title="{{ id ? ('IDP.DETAIL.TITLE' | translate) : ('IDP.CREATE.TITLE' | translate) }}"
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<img class="idp-logo" src="./assets/images/idp/saml-icon.svg" alt="saml" />
<h1>{{ 'IDP.CREATE.SAML.TITLE' | translate }}</h1>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<p class="identity-provider-desc cnsl-secondary-text">
{{ !provider ? ('IDP.CREATE.SAML.DESCRIPTION' | translate) : ('IDP.DETAIL.DESCRIPTION' | translate) }}
</p>
<form [formGroup]="form" (ngSubmit)="submitForm()">
<div class="identity-provider-content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.SAML.METADATAXML' | translate }}</cnsl-label>
<input cnslInput formControlName="metadataXml" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.SAML.METADATAURL' | translate }}</cnsl-label>
<input cnslInput formControlName="metadataUrl" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.SAML.BINDING' | translate }}</cnsl-label>
<mat-select formControlName="binding">
<mat-option *ngFor="let binding of bindingValues" [value]="binding">{{ binding }}</mat-option>
</mat-select>
</cnsl-form-field>
<mat-checkbox formControlName="withSignedRequest">{{ 'IDP.SAML.SIGNEDREQUEST' | translate }}</mat-checkbox>
</div>
<div class="identity-provider-optional-h-wrapper">
<h2>{{ 'IDP.OPTIONAL' | translate }}</h2>
<button (click)="showOptional = !showOptional" type="button" mat-icon-button>
<mat-icon *ngIf="showOptional">keyboard_arrow_up</mat-icon
><mat-icon *ngIf="!showOptional">keyboard_arrow_down</mat-icon>
</button>
</div>
<div *ngIf="showOptional">
<cnsl-provider-options
[initialOptions]="provider?.config?.options"
(optionsChanged)="options = $event"
></cnsl-provider-options>
</div>
<div class="identity-provider-create-actions">
<button
color="primary"
mat-raised-button
class="continue-button"
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
</button>
</div>
</form>
</div>
</cnsl-create-layout>

View File

@ -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<ProviderSamlSpComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ProviderSamlSpComponent],
});
fixture = TestBed.createComponent(ProviderSamlSpComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<ManagementService>);
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
this.breadcrumbService.setBreadcrumb([bread]);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
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');
}
}

View File

@ -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]) => {

View File

@ -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,

View File

@ -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<AddSAMLProviderResponse.AsObject> {
return this.grpcService.admin.addSAMLProvider(req, null).then((resp) => resp.toObject());
}
public updateSAMLProvider(req: UpdateSAMLProviderRequest): Promise<UpdateSAMLProviderResponse.AsObject> {
return this.grpcService.admin.updateSAMLProvider(req, null).then((resp) => resp.toObject());
}
public addGitHubEnterpriseServerProvider(
req: AddGitHubEnterpriseServerProviderRequest,
): Promise<AddGitHubEnterpriseServerProviderResponse.AsObject> {

View File

@ -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<AddSAMLProviderResponse.AsObject> {
return this.grpcService.mgmt.addSAMLProvider(req, null).then((resp) => resp.toObject());
}
public updateSAMLProvider(req: UpdateSAMLProviderRequest): Promise<UpdateSAMLProviderResponse.AsObject> {
return this.grpcService.mgmt.updateSAMLProvider(req, null).then((resp) => resp.toObject());
}
public addGitHubEnterpriseServerProvider(
req: AddGitHubEnterpriseServerProviderRequest,
): Promise<AddGitHubEnterpriseServerProviderResponse.AsObject> {

View File

@ -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.",

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
<g fill="#C22E33">
<path d="M7.754 2l.463.41c.343.304.687.607 1.026.915C11.44 5.32 13.3 7.565 14.7 10.149c.072.132.137.268.202.403l.098.203-.108.057-.081-.115-.21-.299-.147-.214c-1.019-1.479-2.04-2.96-3.442-4.145a6.563 6.563 0 00-1.393-.904c-1.014-.485-1.916-.291-2.69.505-.736.757-1.118 1.697-1.463 2.653-.045.123-.092.245-.139.367l-.082.215-.172-.055c.1-.348.192-.698.284-1.049.21-.795.42-1.59.712-2.356.31-.816.702-1.603 1.093-2.39.169-.341.338-.682.5-1.025h.092z"/>
<path d="M8.448 11.822c-1.626.77-5.56 1.564-7.426 1.36C.717 11.576 3.71 4.05 5.18 2.91l-.095.218a4.638 4.638 0 01-.138.303l-.066.129c-.76 1.462-1.519 2.926-1.908 4.53a7.482 7.482 0 00-.228 1.689c-.01 1.34.824 2.252 2.217 2.309.67.027 1.347-.043 2.023-.114.294-.03.587-.061.88-.084.108-.008.214-.021.352-.039l.231-.028z"/>
<path d="M3.825 14.781c-.445.034-.89.068-1.333.108 4.097.39 8.03-.277 11.91-1.644-1.265-2.23-2.97-3.991-4.952-5.522.026.098.084.169.141.239l.048.06c.17.226.348.448.527.67.409.509.818 1.018 1.126 1.578.778 1.42.356 2.648-1.168 3.296-1.002.427-2.097.718-3.18.892-1.03.164-2.075.243-3.119.323z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB