mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-03 12:52:16 +00:00
feat(saml): option to create minimal SAML metadata file (#6671)
* feat: initial look and feel * feat: initial textarea * feat: app details and i18n * fix: add @peintnermax suggestions * fix: detail component move code to valueChanges.subscribe and clear inputs if metadataurl set --------- Co-authored-by: Max Peintner <[email protected]> Co-authored-by: Elio Bischof <[email protected]> Co-authored-by: Fabi <[email protected]> Co-authored-by: Tim Möhlmann <[email protected]>
This commit is contained in:
co-authored by
Max Peintner
Elio Bischof
Fabi
Tim Möhlmann
parent
b51ad53e5a
commit
9d77dcb467
@@ -170,26 +170,56 @@
|
||||
<mat-step *ngIf="appType?.value?.createType === AppCreateType.SAML" [editable]="true">
|
||||
<ng-template matStepLabel>{{ 'APP.SAML.CONFIGSECTION' | translate }}</ng-template>
|
||||
|
||||
<form [formGroup]="samlConfigForm">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'APP.SAML.URL' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="metadataUrl" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
<h3 class="title cnsl-secondary-text">
|
||||
{{ 'APP.SAML.CHOOSEMETADATASOURCE' | translate }}
|
||||
</h3>
|
||||
|
||||
<span class="cnsl-app-or cnsl-secondary-text">{{ 'APP.SAML.OR' | translate }}</span>
|
||||
<div class="cnsl-secondary-text metadata-option">
|
||||
<span class="saml-config-option">{{ 'APP.SAML.METADATAOPT1' | translate }}</span>
|
||||
<form [formGroup]="samlConfigForm">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label></cnsl-label>
|
||||
<input cnslInput formControlName="metadataUrl" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<input
|
||||
#xmlFileInput
|
||||
style="display: none"
|
||||
class="file-input"
|
||||
type="file"
|
||||
accept="text/xml,application/xml"
|
||||
(change)="onDropXML($any($event.target).files)"
|
||||
/>
|
||||
<button type="button" mat-stroked-button (click)="$event.preventDefault(); xmlFileInput.click()">
|
||||
{{ 'APP.SAML.XML' | translate }}
|
||||
</button>
|
||||
<div class="cnsl-secondary-text metadata-option">
|
||||
<span class="saml-config-option">{{ 'APP.SAML.METADATAOPT2' | translate }}</span>
|
||||
|
||||
<form>
|
||||
<input
|
||||
#xmlFileInput
|
||||
style="display: none"
|
||||
class="file-input"
|
||||
type="file"
|
||||
accept="text/xml,application/xml"
|
||||
(change)="onDropXML($any($event.target).files)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="upload-button"
|
||||
mat-stroked-button
|
||||
(click)="$event.preventDefault(); xmlFileInput.click()"
|
||||
>
|
||||
{{ 'APP.SAML.UPLOAD' | translate }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="cnsl-secondary-text metadata-option">
|
||||
<span class="saml-config-option">{{ 'APP.SAML.METADATAOPT3' | translate }}</span>
|
||||
<form [formGroup]="samlConfigForm">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'APP.SAML.ENTITYID' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="entityId" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'APP.SAML.ACSURL' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="acsURL" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="saml-xml"
|
||||
@@ -199,6 +229,7 @@
|
||||
<!-- <p class="preview-text cnsl-secondary-text">PREVIEW</p> -->
|
||||
<ngx-codemirror
|
||||
[(ngModel)]="decodedBase64"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[options]="{
|
||||
lineNumbers: true,
|
||||
theme: 'material',
|
||||
@@ -419,31 +450,64 @@
|
||||
<div class="content">
|
||||
<ng-container *ngIf="formappType?.value?.createType === AppCreateType.SAML">
|
||||
<div class="saml">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'APP.SAML.URL' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="metadataUrl" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
<h3 class="title cnsl-secondary-text">
|
||||
{{ 'APP.SAML.CHOOSEMETADATASOURCE' | translate }}
|
||||
</h3>
|
||||
<div class="cnsl-secondary-text metadata-option">
|
||||
<span class="saml-config-option">{{ 'APP.SAML.METADATAOPT1' | translate }}</span>
|
||||
<form [formGroup]="samlConfigForm">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label></cnsl-label>
|
||||
<input cnslInput formControlName="metadataUrl" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<span class="cnsl-app-or cnsl-secondary-text">{{ 'APP.SAML.OR' | translate }}</span>
|
||||
<div class="cnsl-secondary-text metadata-option">
|
||||
<span class="saml-config-option">{{ 'APP.SAML.METADATAOPT2' | translate }}</span>
|
||||
|
||||
<input
|
||||
#xmlFileInput
|
||||
style="display: none"
|
||||
class="file-input"
|
||||
type="file"
|
||||
(change)="onDropXML($any($event.target).files)"
|
||||
/>
|
||||
<button type="button" mat-stroked-button (click)="$event.preventDefault(); xmlFileInput.click()">
|
||||
{{ 'APP.SAML.XML' | translate }}
|
||||
</button>
|
||||
<form>
|
||||
<input
|
||||
#xmlFileInput
|
||||
style="display: none"
|
||||
class="file-input"
|
||||
type="file"
|
||||
accept="text/xml,application/xml"
|
||||
(change)="onDropXML($any($event.target).files)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="upload-button"
|
||||
mat-stroked-button
|
||||
(click)="$event.preventDefault(); xmlFileInput.click()"
|
||||
>
|
||||
{{ 'APP.SAML.UPLOAD' | translate }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="cnsl-secondary-text metadata-option">
|
||||
<span class="saml-config-option">{{ 'APP.SAML.METADATAOPT3' | translate }}</span>
|
||||
<form [formGroup]="samlConfigForm">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'APP.SAML.ENTITYID' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="entityId" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'APP.SAML.ACSURL' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="acsURL" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="saml-xml"
|
||||
[ngClass]="{ disabled: !!formMetadataUrl?.value }"
|
||||
[ngClass]="{ disabled: !!metadataUrl?.value }"
|
||||
*ngIf="decodedBase64 && samlAppRequest.toObject().metadataXml && !samlAppRequest.toObject().metadataUrl"
|
||||
>
|
||||
<ngx-codemirror
|
||||
[(ngModel)]="decodedBase64"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[options]="{
|
||||
lineNumbers: true,
|
||||
theme: 'material',
|
||||
|
||||
@@ -129,6 +129,10 @@ p.desc {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0 0.5rem;
|
||||
|
||||
.formfield {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,3 +142,20 @@ p.desc {
|
||||
padding: 0.5rem 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.metadata-option {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.upload-button {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.saml-config-option {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,8 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.samlConfigForm = this.fb.group({
|
||||
metadataUrl: ['', []],
|
||||
entityId: ['', []],
|
||||
acsURL: ['', []],
|
||||
});
|
||||
|
||||
this.firstFormGroup.valueChanges.subscribe((value) => {
|
||||
@@ -218,9 +220,24 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.samlConfigForm.valueChanges.subscribe((form) => {
|
||||
let minimalMetadata =
|
||||
this.entityId?.value && this.acsURL?.value
|
||||
? `<?xml version="1.0"?>
|
||||
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="${this.entityId?.value}">
|
||||
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${this.acsURL?.value}" index="0"/>
|
||||
</md:SPSSODescriptor>
|
||||
</md:EntityDescriptor>`
|
||||
: '';
|
||||
|
||||
if (form.metadataUrl && form.metadataUrl.length > 0) {
|
||||
this.samlAppRequest.setMetadataUrl(form.metadataUrl);
|
||||
}
|
||||
|
||||
if (this.samlAppRequest) {
|
||||
const base64 = Buffer.from(minimalMetadata, 'utf-8').toString('base64');
|
||||
this.samlAppRequest.setMetadataXml(base64);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -352,6 +369,8 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
public onDropXML(filelist: FileList): void {
|
||||
const file = filelist.item(0);
|
||||
this.metadataUrl?.setValue('');
|
||||
this.entityId?.setValue('');
|
||||
this.acsURL?.setValue('');
|
||||
if (file) {
|
||||
if (file.size > MAX_ALLOWED_SIZE) {
|
||||
this.toast.showInfo('POLICY.PRIVATELABELING.MAXSIZEEXCEEDED', true);
|
||||
@@ -547,4 +566,12 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
public get metadataUrl(): AbstractControl | null {
|
||||
return this.samlConfigForm.get('metadataUrl');
|
||||
}
|
||||
|
||||
public get entityId(): AbstractControl | null {
|
||||
return this.samlConfigForm.get('entityId');
|
||||
}
|
||||
|
||||
public get acsURL(): AbstractControl | null {
|
||||
return this.samlConfigForm.get('acsURL');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,17 +157,22 @@
|
||||
</form>
|
||||
</cnsl-card>
|
||||
|
||||
<cnsl-card *ngIf="samlForm && app?.samlConfig">
|
||||
<form [formGroup]="samlForm">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'APP.SAML.URL' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="metadataUrl" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
<cnsl-card *ngIf="samlForm && app?.samlConfig" title="{{ 'APP.SAML.CONFIGSECTION' | translate }}">
|
||||
<h3 class="title cnsl-secondary-text">
|
||||
{{ 'APP.SAML.CHOOSEMETADATASOURCE' | translate }}
|
||||
</h3>
|
||||
|
||||
<div class="cnsl-saml-config-line">
|
||||
<span class="cnsl-app-or cnsl-secondary-text">{{ 'APP.SAML.OR' | translate }}</span>
|
||||
<div class="metadata-option">
|
||||
<span class="saml-config-option">{{ 'APP.SAML.METADATAOPT1' | translate }}</span>
|
||||
<form [formGroup]="samlForm">
|
||||
<cnsl-form-field class="formfield">
|
||||
<input cnslInput formControlName="metadataUrl" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="metadata-option">
|
||||
<span class="saml-config-option">{{ 'APP.SAML.METADATAOPT2' | translate }}</span>
|
||||
<input
|
||||
#xmlFileInput
|
||||
style="display: none"
|
||||
@@ -176,11 +181,25 @@
|
||||
accept="text/xml,application/xml"
|
||||
(change)="onDropXML($any($event.target).files)"
|
||||
/>
|
||||
<button mat-stroked-button (click)="$event.preventDefault(); xmlFileInput.click()">
|
||||
{{ 'APP.SAML.XML' | translate }}
|
||||
<button class="upload-button" mat-stroked-button (click)="$event.preventDefault(); xmlFileInput.click()">
|
||||
{{ 'APP.SAML.UPLOAD' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="metadata-option">
|
||||
<span class="saml-config-option">{{ 'APP.SAML.METADATAOPT3' | translate }}</span>
|
||||
<form [formGroup]="samlForm">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'APP.SAML.ENTITYID' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="entityId" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'APP.SAML.ACSURL' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="acsURL" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="saml-xml"
|
||||
[ngClass]="{ disabled: !!metadataUrl?.value }"
|
||||
|
||||
@@ -10,6 +10,27 @@
|
||||
$button-disabled-text-color: map-get($foreground, disabled-button);
|
||||
$divider-color: map-get($foreground, dividers);
|
||||
|
||||
.title {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metadata-option {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.upload-button {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.saml-config-option {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-zitadel-warning {
|
||||
font-size: 14px;
|
||||
color: $warn-color;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
||||
import { MatLegacyCheckboxChange as MatCheckboxChange } from '@angular/material/legacy-checkbox';
|
||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
||||
@@ -183,8 +183,33 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.samlForm = this.fb.group({
|
||||
metadataUrl: [{ value: '', disabled: true }],
|
||||
entityId: ['', []],
|
||||
acsURL: ['', []],
|
||||
metadataXml: [{ value: '', disabled: true }],
|
||||
});
|
||||
|
||||
this.samlForm.valueChanges.subscribe((form) => {
|
||||
let minimalMetadata =
|
||||
this.entityId?.value && this.acsURL?.value
|
||||
? `<?xml version="1.0"?>
|
||||
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="${this.entityId?.value}">
|
||||
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${this.acsURL?.value}" index="0"/>
|
||||
</md:SPSSODescriptor>
|
||||
</md:EntityDescriptor>`
|
||||
: '';
|
||||
|
||||
if (this.metadataUrl && this.metadataUrl.value.length > 0) {
|
||||
if (this.app && this.app.samlConfig && this.app.samlConfig.metadataXml) {
|
||||
this.app.samlConfig.metadataXml = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.app && this.app.samlConfig && this.app.samlConfig.metadataXml && minimalMetadata) {
|
||||
const base64 = Buffer.from(minimalMetadata, 'utf-8').toString('base64');
|
||||
this.app.samlConfig.metadataXml = base64;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public formatClockSkewLabel(seconds: number): string {
|
||||
@@ -406,6 +431,8 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
this.toast.showInfo('POLICY.PRIVATELABELING.MAXSIZEEXCEEDED', true);
|
||||
} else {
|
||||
this.metadataUrl?.setValue('');
|
||||
this.entityId?.setValue('');
|
||||
this.acsURL?.setValue('');
|
||||
const reader = new FileReader();
|
||||
reader.onload = ((aXML) => {
|
||||
return (e) => {
|
||||
@@ -779,6 +806,14 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
return this.samlForm.get('metadataUrl');
|
||||
}
|
||||
|
||||
public get entityId(): AbstractControl | null {
|
||||
return this.samlForm.get('entityId');
|
||||
}
|
||||
|
||||
public get acsURL(): AbstractControl | null {
|
||||
return this.samlForm.get('acsURL');
|
||||
}
|
||||
|
||||
get decodedBase64(): string {
|
||||
if (
|
||||
this.app &&
|
||||
|
||||
Reference in New Issue
Block a user