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:
Miguel Cabrerizo
2023-10-25 20:39:28 +03:00
committed by GitHub
co-authored by Max Peintner Elio Bischof Fabi Tim Möhlmann
parent b51ad53e5a
commit 9d77dcb467
17 changed files with 349 additions and 83 deletions
@@ -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 &&