feat(saml): implementation of saml for ZITADEL v2 (#3618)

This commit is contained in:
Stefan Benz
2022-09-12 17:18:08 +01:00
committed by GitHub
parent 01a92ba5d9
commit 7a5f7f82cf
134 changed files with 5570 additions and 1293 deletions

View File

@@ -86,6 +86,7 @@ const authConfig: AuthConfig = {
scope: 'openid profile email', // offline_access
responseType: 'code',
oidc: true,
requireHttps: false,
};
@NgModule({

View File

@@ -1,5 +1,13 @@
<div class="cnsl-app-card" [ngClass]="{'add': type === OIDCAppType.ADD,'web': type === OIDCAppType.OIDC_APP_TYPE_WEB,
'useragent': type === OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
'native': type === OIDCAppType.OIDC_APP_TYPE_NATIVE, 'api': isApiApp}">
<div
class="cnsl-app-card"
[ngClass]="{
add: type === OIDCAppType.ADD,
web: type === OIDCAppType.OIDC_APP_TYPE_WEB,
useragent: type === OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
native: type === OIDCAppType.OIDC_APP_TYPE_NATIVE,
api: isApiApp,
saml: type === 'SAML'
}"
>
<ng-content></ng-content>
</div>
</div>

View File

@@ -55,5 +55,11 @@
border: none;
color: #fff;
}
&.saml {
background: linear-gradient(40deg, rgb(110, 56, 124), rgb(88, 37, 103));
border: none;
color: #fff;
}
}
}

View File

@@ -8,7 +8,7 @@ import { OIDCAppType } from 'src/app/proto/generated/zitadel/app_pb';
})
export class AppCardComponent {
@Input() public outline: boolean = false;
@Input() public type: OIDCAppType | undefined = undefined;
@Input() public type: OIDCAppType | 'SAML' | undefined = undefined;
@Input() public isApiApp: boolean = false;
public OIDCAppType: any = OIDCAppType;
}

View File

@@ -2,12 +2,13 @@
<ng-container *ngFor="let type of types">
<input class="app" type="radio" (change)="emitChange()" [value]="type" [(ngModel)]="selected" [id]="type.prefix" />
<label class="cnsl-type-radio-button" [for]="type.prefix">
<div class="cnsl-type-radio-header" [ngStyle]="{'background': type.background}">
<span>{{type.prefix}}</span>
<div class="cnsl-type-radio-header" [ngStyle]="{ background: type.background }">
<span>{{ type.prefix }}</span>
</div>
<p>{{type.titleI18nKey | translate}}</p>
<p class="type-desc cnsl-secondary-text">{{type.descI18nKey | translate}}</p>
<p>{{ type.titleI18nKey | translate }}</p>
<p class="type-desc cnsl-secondary-text">{{ type.descI18nKey | translate }}</p>
<span class="fill-space"></span>
<span class="cnsl-type-protocol state" *ngIf="type.protocol">{{ type.protocol }}</span>
</label>
</ng-container>
</div>
</div>

View File

@@ -60,6 +60,8 @@
box-sizing: border-box;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
position: relative;
overflow: hidden;
span {
margin: 2rem;
@@ -76,5 +78,10 @@
.type-desc {
font-size: 14px;
}
.cnsl-type-protocol {
width: fit-content;
margin: 0.5rem auto;
}
}
}

View File

@@ -1,14 +1,24 @@
<cnsl-create-layout
title="{{ 'APP.PAGES.CREATE_OIDC' | translate }}"
[createSteps]="createSteps"
title="{{ 'APP.PAGES.CREATE' | translate }}"
[createSteps]="
appType?.value?.createType === AppCreateType.OIDC
? appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE
? 4
: 3
: appType?.value?.createType === AppCreateType.API
? 3
: appType?.value?.createType === AppCreateType.SAML
? 3
: 0
"
[currentCreateStep]="currentCreateStep"
(closed)="close()"
>
<h1>{{ 'APP.PAGES.CREATE_OIDC_DESC_TITLE' | translate }}</h1>
<h1>{{ 'APP.PAGES.CREATE_DESC_TITLE' | translate }}</h1>
<mat-progress-bar class="progress-bar" color="primary" *ngIf="loading" mode="indeterminate"></mat-progress-bar>
<mat-checkbox class="proswitch" color="primary" [(ngModel)]="devmode">
{{ 'APP.OIDC.PROSWITCH' | translate }}
{{ 'APP.PROSWITCH' | translate }}
</mat-checkbox>
<mat-horizontal-stepper
@@ -21,16 +31,16 @@
>
<mat-step [stepControl]="firstFormGroup" [editable]="true">
<form [formGroup]="firstFormGroup">
<ng-template matStepLabel>{{ 'APP.OIDC.NAMEANDTYPESECTION' | translate }}</ng-template>
<ng-template matStepLabel>{{ 'APP.NAMEANDTYPESECTION' | translate }}</ng-template>
<p class="step-title">{{ 'APP.OIDC.TITLEFIRST' | translate }}</p>
<p class="step-title">{{ 'APP.TITLEFIRST' | translate }}</p>
<cnsl-form-field class="name-formfield">
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
<input cnslInput cdkFocusInitial formControlName="name" />
<span cnslError *ngIf="name?.errors?.required">{{ 'PROJECT.APP.NAMEREQUIRED' | translate }}</span>
</cnsl-form-field>
<p class="step-title">{{ 'APP.OIDC.TYPETITLE' | translate }}</p>
<p class="step-title">{{ 'APP.TYPETITLE' | translate }}</p>
<cnsl-type-radio [types]="appTypes" (selectedType)="appType?.setValue($event)" [selected]="appType?.value">
</cnsl-type-radio>
@@ -48,9 +58,13 @@
</form>
</mat-step>
<!-- skip for native applications -->
<!-- skip for native OIDC and SAML applications -->
<mat-step
*ngIf="oidcAppRequest.appType !== OIDCAppType.OIDC_APP_TYPE_NATIVE"
*ngIf="
(appType?.value?.createType === AppCreateType.OIDC &&
appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE) ||
appType?.value?.createType === AppCreateType.API
"
[stepControl]="secondFormGroup"
[editable]="true"
>
@@ -85,33 +99,39 @@
<ng-template matStepLabel>{{ 'APP.OIDC.REDIRECTSECTION' | translate }}</ng-template>
<p class="step-title">{{ 'APP.OIDC.REDIRECTTITLE' | translate }}</p>
<p class="step-description cnsl-secondary-text" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
<p
class="step-description cnsl-secondary-text"
*ngIf="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
>
{{ 'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate }}
</p>
<p class="step-description cnsl-secondary-text" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB">
<p class="step-description cnsl-secondary-text" *ngIf="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_WEB">
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
</p>
<cnsl-redirect-uris
class="redirect-section"
[canWrite]="true"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)"
[urisList]="oidcAppRequest.redirectUrisList"
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
(changedUris)="oidcAppRequest.setRedirectUrisList($any($event))"
[urisList]="oidcAppRequest.toObject().redirectUrisList"
[getValues]="requestRedirectValuesSubject$"
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
>
</cnsl-redirect-uris>
<p class="step-title">{{ 'APP.OIDC.POSTREDIRECTTITLE' | translate }}</p>
<p class="step-description cnsl-secondary-text" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
<p
class="step-description cnsl-secondary-text"
*ngIf="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
>
{{ 'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate }}
</p>
<p
class="step-description cnsl-secondary-text"
*ngIf="
oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB ||
oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT
appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_WEB ||
appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT
"
>
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
@@ -120,11 +140,11 @@
<cnsl-redirect-uris
class="redirect-section"
[canWrite]="true"
(changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)"
[urisList]="oidcAppRequest.postLogoutRedirectUrisList"
(changedUris)="oidcAppRequest.setPostLogoutRedirectUrisList($any($event))"
[urisList]="oidcAppRequest.toObject().postLogoutRedirectUrisList"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
>
</cnsl-redirect-uris>
@@ -136,97 +156,173 @@
</div>
</mat-step>
<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>
<span class="cnsl-app-or cnsl-secondary-text">{{ 'APP.SAML.OR' | translate }}</span>
<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="saml-xml"
[ngClass]="{ disabled: !!metadataUrl?.value }"
*ngIf="decodedBase64 && samlAppRequest.toObject().metadataXml && !samlAppRequest.toObject().metadataUrl"
>
<!-- <p class="preview-text cnsl-secondary-text">PREVIEW</p> -->
<ngx-codemirror
[(ngModel)]="decodedBase64"
[options]="{
lineNumbers: true,
theme: 'material',
mode: 'application/xml'
}"
></ngx-codemirror>
</div>
<div class="app-create-actions">
<button mat-stroked-button class="bck-button" matStepperPrevious>{{ 'ACTIONS.BACK' | translate }}</button>
<button mat-raised-button color="primary" matStepperNext [attr.data-e2e]="'continue-button-redirecturis'">
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</div>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{ 'APP.OIDC.OVERVIEWSECTION' | translate }}</ng-template>
<p class="step-title">{{ 'APP.OIDC.OVERVIEWTITLE' | translate }}</p>
<div class="row cnsl-secondary-text">
<span class="left">
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.NAME' | translate }}
</span>
<span class="right">
{{ oidcAppRequest.name }}
{{ name?.value }}
</span>
</div>
<ng-container *ngIf="appType?.value?.createType === AppCreateType.OIDC">
<div class="row cnsl-secondary-text">
<span class="left">
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.TYPE' | translate }}
</span>
<span class="right">
{{ 'APP.OIDC.APPTYPE.' + oidcAppRequest.appType | translate }}
{{ 'APP.OIDC.APPTYPE.' + oidcAppRequest.toObject().appType | translate }}
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.GRANT' | translate }}
</span>
<span class="right" *ngIf="oidcAppRequest.grantTypesList && oidcAppRequest.grantTypesList.length > 0">
[<span *ngFor="let element of oidcAppRequest.grantTypesList; index as i">
<span
class="right"
*ngIf="oidcAppRequest.toObject().grantTypesList && oidcAppRequest.toObject().grantTypesList.length > 0"
>
[<span *ngFor="let element of oidcAppRequest.toObject().grantTypesList; index as i">
{{ 'APP.OIDC.GRANT.' + element | translate }}
{{ i < oidcAppRequest.grantTypesList.length - 1 ? ', ' : '' }} </span
{{ i < oidcAppRequest.toObject().grantTypesList.length - 1 ? ', ' : '' }} </span
>]
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.OIDC.RESPONSETYPE' | translate }}
</span>
<span class="right" *ngIf="oidcAppRequest.responseTypesList && oidcAppRequest.responseTypesList.length > 0">
[<span *ngFor="let element of oidcAppRequest.responseTypesList; index as i">
<span
class="right"
*ngIf="oidcAppRequest.toObject().responseTypesList && oidcAppRequest.toObject().responseTypesList.length > 0"
>
[<span *ngFor="let element of oidcAppRequest.toObject().responseTypesList; index as i">
{{ 'APP.OIDC.RESPONSE.' + element | translate }}
{{ i < oidcAppRequest.responseTypesList.length - 1 ? ', ' : '' }} </span
{{ i < oidcAppRequest.toObject().responseTypesList.length - 1 ? ', ' : '' }} </span
>]
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.AUTHMETHOD' | translate }}
</span>
<span class="right">
<span>
{{ 'APP.OIDC.AUTHMETHOD.' + oidcAppRequest.authMethodType | translate }}
{{ 'APP.OIDC.AUTHMETHOD.' + oidcAppRequest.toObject().authMethodType | translate }}
</span>
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.OIDC.REDIRECT' | translate }}
</span>
<span class="right" *ngIf="oidcAppRequest.redirectUrisList && oidcAppRequest.redirectUrisList.length > 0">
[<span *ngFor="let redirect of oidcAppRequest.redirectUrisList; index as i">
<span
class="right"
*ngIf="oidcAppRequest.toObject().redirectUrisList && oidcAppRequest.toObject().redirectUrisList.length > 0"
>
[<span *ngFor="let redirect of oidcAppRequest.toObject().redirectUrisList; index as i">
{{ redirect }}
{{ i < oidcAppRequest.redirectUrisList.length - 1 ? ', ' : '' }} </span
{{ i < oidcAppRequest.toObject().redirectUrisList.length - 1 ? ', ' : '' }} </span
>]
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}
</span>
<span
class="right"
*ngIf="oidcAppRequest.postLogoutRedirectUrisList && oidcAppRequest.postLogoutRedirectUrisList.length > 0"
*ngIf="
oidcAppRequest.toObject().postLogoutRedirectUrisList &&
oidcAppRequest.toObject().postLogoutRedirectUrisList.length > 0
"
>
[<span *ngFor="let redirect of oidcAppRequest.postLogoutRedirectUrisList; index as i">
[<span *ngFor="let redirect of oidcAppRequest.toObject().postLogoutRedirectUrisList; index as i">
{{ redirect }}
{{ i < oidcAppRequest.postLogoutRedirectUrisList.length - 1 ? ', ' : '' }} </span
{{ i < oidcAppRequest.toObject().postLogoutRedirectUrisList.length - 1 ? ', ' : '' }} </span
>]
</span>
</div>
</ng-container>
<ng-container *ngIf="appType?.value?.createType === AppCreateType.API">
<div class="row cnsl-secondary-text">
<span class="left">
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.AUTHMETHOD' | translate }}
</span>
<span class="right">
<span>
{{ 'APP.API.AUTHMETHOD.' + apiAppRequest.authMethodType | translate }}
{{ 'APP.API.AUTHMETHOD.' + authMethodType?.value | translate }}
</span>
</span>
</div>
</ng-container>
<ng-container *ngIf="appType?.value?.createType === AppCreateType.SAML">
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.SAML.METADATA' | translate }}
</span>
<span class="right">
<span *ngIf="metadataUrl?.value">
{{ metadataUrl?.value }}
</span>
<span *ngIf="samlAppRequest.toObject().metadataXml">
{{ 'APP.SAML.METADATAFROMFILE' | translate }}
</span>
</span>
</div>
@@ -278,15 +374,56 @@
</cnsl-form-field>
</ng-container>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'APP.AUTHMETHOD' | translate }}</cnsl-label>
<mat-select formControlName="authMethodType">
<mat-option *ngFor="let type of authMethodTypes" [value]="type.type">
<span *ngIf="type.oidc">{{ 'APP.OIDC.AUTHMETHOD.' + type.type | translate }}</span>
<span *ngIf="type.api">{{ 'APP.API.AUTHMETHOD.' + type.type | translate }}</span>
</mat-option>
</mat-select>
</cnsl-form-field>
<ng-container *ngIf="formappType?.value?.createType !== AppCreateType.SAML">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'APP.AUTHMETHOD' | translate }}</cnsl-label>
<mat-select formControlName="authMethodType">
<mat-option *ngFor="let type of authMethodTypes" [value]="type.type">
<span *ngIf="type.oidc">{{ 'APP.OIDC.AUTHMETHOD.' + type.type | translate }}</span>
<span *ngIf="type.api">{{ 'APP.API.AUTHMETHOD.' + type.type | translate }}</span>
</mat-option>
</mat-select>
</cnsl-form-field>
</ng-container>
</div>
<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>
<span class="cnsl-app-or cnsl-secondary-text">{{ 'APP.SAML.OR' | 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>
<div
class="saml-xml"
[ngClass]="{ disabled: !!formMetadataUrl?.value }"
*ngIf="decodedBase64 && samlAppRequest.toObject().metadataXml && !samlAppRequest.toObject().metadataUrl"
>
<ngx-codemirror
[(ngModel)]="decodedBase64"
[options]="{
lineNumbers: true,
theme: 'material',
mode: 'application/xml'
}"
></ngx-codemirror>
</div>
</div>
</ng-container>
</div>
<div class="content" *ngIf="formappType?.value?.createType === AppCreateType.OIDC">
@@ -294,22 +431,22 @@
<cnsl-redirect-uris
class="redirect-section"
[canWrite]="true"
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)"
[urisList]="oidcAppRequest.redirectUrisList"
(changedUris)="oidcAppRequest.setRedirectUrisList($any($event))"
[urisList]="oidcAppRequest.toObject().redirectUrisList"
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
[getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
>
</cnsl-redirect-uris>
<cnsl-redirect-uris
class="redirect-section"
[canWrite]="true"
(changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)"
[urisList]="oidcAppRequest.postLogoutRedirectUrisList"
(changedUris)="oidcAppRequest.setPostLogoutRedirectUrisList($any($event))"
[urisList]="oidcAppRequest.toObject().postLogoutRedirectUrisList"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
>
</cnsl-redirect-uris>
</div>

View File

@@ -61,6 +61,10 @@ p.desc {
}
}
.cnsl-app-or {
margin-right: 1rem;
}
.row {
display: flex;
justify-content: space-between;
@@ -72,10 +76,20 @@ p.desc {
}
}
.saml-xml {
margin-top: 2rem;
&.disabled {
opacity: 0.5;
pointer-events: none;
}
}
.app-create-actions {
margin-top: 1rem;
margin-top: 2rem;
display: flex;
align-items: center;
justify-content: space-between;
.bck-button {
margin-right: 1rem;
@@ -102,6 +116,12 @@ p.desc {
flex-basis: 80%;
}
}
.saml {
display: block;
width: 100%;
margin: 0 0.5rem;
}
}
.continue-button {

View File

@@ -5,6 +5,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { Buffer } from 'buffer';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component';
@@ -14,12 +15,14 @@ import {
OIDCAuthMethodType,
OIDCGrantType,
OIDCResponseType,
SAMLConfig,
} from 'src/app/proto/generated/zitadel/app_pb';
import {
AddAPIAppRequest,
AddAPIAppResponse,
AddOIDCAppRequest,
AddOIDCAppResponse,
AddSAMLAppRequest,
} from 'src/app/proto/generated/zitadel/management_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
@@ -35,7 +38,9 @@ import {
PKCE_METHOD,
POST_METHOD,
} from '../authmethods';
import { API_TYPE, AppCreateType, NATIVE_TYPE, RadioItemAppType, USER_AGENT_TYPE, WEB_TYPE } from '../authtypes';
import { API_TYPE, AppCreateType, NATIVE_TYPE, RadioItemAppType, SAML_TYPE, USER_AGENT_TYPE, WEB_TYPE } from '../authtypes';
const MAX_ALLOWED_SIZE = 1 * 1024 * 1024;
@Component({
selector: 'cnsl-app-create',
@@ -49,11 +54,11 @@ export class AppCreateComponent implements OnInit, OnDestroy {
public projectId: string = '';
public loading: boolean = false;
public createSteps: number = 4;
public currentCreateStep: number = 1;
public oidcAppRequest: AddOIDCAppRequest.AsObject = new AddOIDCAppRequest().toObject();
public apiAppRequest: AddAPIAppRequest.AsObject = new AddAPIAppRequest().toObject();
public oidcAppRequest: AddOIDCAppRequest = new AddOIDCAppRequest();
public apiAppRequest: AddAPIAppRequest = new AddAPIAppRequest();
public samlAppRequest: AddSAMLAppRequest = new AddSAMLAppRequest();
public oidcResponseTypes: { type: OIDCResponseType; checked: boolean; disabled: boolean }[] = [
{ type: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE, checked: false, disabled: false },
@@ -66,7 +71,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
OIDCAppType.OIDC_APP_TYPE_NATIVE,
OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
];
public appTypes: any = [WEB_TYPE, NATIVE_TYPE, USER_AGENT_TYPE, API_TYPE];
public appTypes: any = [WEB_TYPE, NATIVE_TYPE, USER_AGENT_TYPE, API_TYPE, SAML_TYPE];
public authMethods: RadioItemAuthType[] = [PKCE_METHOD, CODE_METHOD, PK_JWT_METHOD, POST_METHOD];
@@ -84,8 +89,12 @@ export class AppCreateComponent implements OnInit, OnDestroy {
];
// stepper
firstFormGroup!: UntypedFormGroup;
secondFormGroup!: UntypedFormGroup;
public firstFormGroup!: UntypedFormGroup;
public secondFormGroup!: UntypedFormGroup;
public samlConfigForm!: UntypedFormGroup;
public redirectUrisList: string[] = [];
public postLogoutRedirectUrisList: string[] = [];
// devmode
public form!: UntypedFormGroup;
@@ -121,10 +130,13 @@ export class AppCreateComponent implements OnInit, OnDestroy {
) {
this.form = this.fb.group({
name: ['', [Validators.required]],
responseTypesList: ['', [Validators.required]],
grantTypesList: ['', [Validators.required]],
appType: ['', [Validators.required]],
authMethodType: ['', [Validators.required]],
// apptype OIDC
responseTypesList: ['', []],
grantTypesList: ['', []],
authMethodType: ['', []],
// apptype SAML
metadataUrl: ['', []],
});
this.initForm();
@@ -134,25 +146,30 @@ export class AppCreateComponent implements OnInit, OnDestroy {
appType: [WEB_TYPE, [Validators.required]],
});
this.samlConfigForm = this.fb.group({
metadataUrl: ['', []],
});
this.firstFormGroup.valueChanges.subscribe((value) => {
if (this.firstFormGroup.valid) {
this.oidcAppRequest.name = this.name?.value;
this.apiAppRequest.name = this.name?.value;
this.oidcAppRequest.setName(this.name?.value);
this.apiAppRequest.setName(this.name?.value);
this.samlAppRequest.setName(this.name?.value);
if (this.isStepperOIDC) {
const oidcAppType = (this.appType?.value as RadioItemAppType).oidcAppType;
if (oidcAppType !== undefined) {
this.oidcAppRequest.appType = oidcAppType;
this.oidcAppRequest.setAppType(oidcAppType);
}
switch (this.oidcAppRequest.appType) {
switch (this.appType?.value.oidcAppType) {
case OIDCAppType.OIDC_APP_TYPE_NATIVE:
this.authMethods = [PKCE_METHOD];
// automatically set to PKCE and skip step
this.oidcAppRequest.responseTypesList = [OIDCResponseType.OIDC_RESPONSE_TYPE_CODE];
this.oidcAppRequest.grantTypesList = [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE];
this.oidcAppRequest.authMethodType = OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE;
this.oidcAppRequest.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE]);
this.oidcAppRequest.setGrantTypesList([OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE]);
this.oidcAppRequest.setAuthMethodType(OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE);
break;
case OIDCAppType.OIDC_APP_TYPE_WEB:
@@ -179,19 +196,28 @@ export class AppCreateComponent implements OnInit, OnDestroy {
this.secondFormGroup = this.fb.group({
authMethod: [this.authMethods[0].key, [Validators.required]],
});
this.secondFormGroup.valueChanges.subscribe((form) => {
const partialConfig = getPartialConfigFromAuthMethod(form.authMethod);
if (this.isStepperOIDC && partialConfig && partialConfig.oidc) {
this.oidcAppRequest.responseTypesList = partialConfig.oidc?.responseTypesList ?? [];
this.oidcAppRequest.setResponseTypesList(partialConfig.oidc?.responseTypesList ?? []);
this.oidcAppRequest.grantTypesList = partialConfig.oidc?.grantTypesList ?? [];
this.oidcAppRequest.setGrantTypesList(partialConfig.oidc?.grantTypesList ?? []);
this.oidcAppRequest.authMethodType =
partialConfig.oidc?.authMethodType ?? OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE;
this.oidcAppRequest.setAuthMethodType(
partialConfig.oidc?.authMethodType ?? OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
);
} else if (this.isStepperAPI && partialConfig && partialConfig.api) {
this.apiAppRequest.authMethodType =
partialConfig.api?.authMethodType ?? APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC;
this.apiAppRequest.setAuthMethodType(
partialConfig.api?.authMethodType ?? APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC,
);
}
});
this.samlConfigForm.valueChanges.subscribe((form) => {
if (form.metadataUrl && form.metadataUrl.length > 0) {
this.samlAppRequest.setMetadataUrl(form.metadataUrl);
}
});
}
@@ -225,18 +251,23 @@ export class AppCreateComponent implements OnInit, OnDestroy {
public initForm(): void {
this.form.valueChanges.pipe(takeUntil(this.destroyed$), debounceTime(150)).subscribe(() => {
this.oidcAppRequest.name = this.formname?.value;
this.apiAppRequest.name = this.formname?.value;
this.oidcAppRequest.setName(this.formname?.value);
this.apiAppRequest.setName(this.formname?.value);
this.samlAppRequest.setName(this.formname?.value);
this.oidcAppRequest.responseTypesList = this.formresponseTypesList?.value;
this.oidcAppRequest.grantTypesList = this.formgrantTypesList?.value;
this.oidcAppRequest.setResponseTypesList(this.formresponseTypesList?.value);
this.oidcAppRequest.setGrantTypesList(this.grantTypesList?.value);
this.oidcAppRequest.authMethodType = this.formauthMethodType?.value;
this.apiAppRequest.authMethodType = this.formauthMethodType?.value;
this.oidcAppRequest.setAuthMethodType(this.authMethodType?.value);
this.apiAppRequest.setAuthMethodType(this.authMethodType?.value);
if (this.formMetadataUrl?.value) {
this.samlAppRequest.setMetadataUrl(this.formMetadataUrl?.value);
}
const oidcAppType = (this.formappType?.value as RadioItemAppType).oidcAppType;
if (oidcAppType !== undefined) {
this.oidcAppRequest.appType = oidcAppType;
this.oidcAppRequest.setAppType(oidcAppType);
}
});
@@ -282,17 +313,41 @@ export class AppCreateComponent implements OnInit, OnDestroy {
private async getData({ projectid }: Params): Promise<void> {
this.projectId = projectid;
this.oidcAppRequest.projectId = projectid;
this.apiAppRequest.projectId = projectid;
this.oidcAppRequest.setProjectId(projectid);
this.apiAppRequest.setProjectId(projectid);
this.samlAppRequest.setProjectId(projectid);
}
public close(): void {
this._location.back();
}
public onDropXML(filelist: FileList): void {
const file = filelist.item(0);
this.metadataUrl?.setValue('');
if (file) {
if (file.size > MAX_ALLOWED_SIZE) {
this.toast.showInfo('POLICY.PRIVATELABELING.MAXSIZEEXCEEDED', true);
} else {
const reader = new FileReader();
reader.onload = ((aXML) => {
return (e) => {
const xmlBase64 = e.target?.result;
if (xmlBase64 && typeof xmlBase64 === 'string') {
const cropped = xmlBase64.replace('data:text/xml;base64,', '');
this.samlAppRequest.setMetadataXml(cropped);
}
};
})(file);
reader.readAsDataURL(file);
}
}
}
public createApp(): void {
const appOIDCCheck = this.devmode ? this.isDevOIDC : this.isStepperOIDC;
const appAPICheck = this.devmode ? this.isDevAPI : this.isStepperAPI;
const appSAMLCheck = this.devmode ? this.isDevSAML : this.isStepperSAML;
if (appOIDCCheck) {
this.requestRedirectValuesSubject$.next();
@@ -331,6 +386,19 @@ export class AppCreateComponent implements OnInit, OnDestroy {
this.loading = false;
this.toast.showError(error);
});
} else if (appSAMLCheck) {
this.loading = true;
this.toast.showInfo('APP.TOAST.CREATED', true);
this.mgmtService
.addSAMLApp(this.samlAppRequest)
.then((resp) => {
this.loading = false;
this.router.navigate(['projects', this.projectId, 'apps', resp.appId]);
})
.catch((error) => {
this.loading = false;
this.toast.showError(error);
});
}
}
@@ -381,19 +449,27 @@ export class AppCreateComponent implements OnInit, OnDestroy {
get formname(): AbstractControl | null {
return this.form.get('name');
}
get formresponseTypesList(): AbstractControl | null {
return this.form.get('responseTypesList');
}
get formgrantTypesList(): AbstractControl | null {
get grantTypesList(): AbstractControl | null {
return this.form.get('grantTypesList');
}
get formappType(): AbstractControl | null {
return this.form.get('appType');
}
get formMetadataUrl(): AbstractControl | null {
return this.form.get('metadataUrl');
}
// get formapplicationType(): AbstractControl | null {
// return this.form.get('applicationType');
// }
get formauthMethodType(): AbstractControl | null {
get authMethodType(): AbstractControl | null {
return this.form.get('authMethodType');
}
@@ -409,7 +485,35 @@ export class AppCreateComponent implements OnInit, OnDestroy {
return (this.formappType?.value as RadioItemAppType).createType === AppCreateType.API;
}
get isDevSAML(): boolean {
return (this.formappType?.value as RadioItemAppType).createType === AppCreateType.SAML;
}
get isStepperAPI(): boolean {
return (this.appType?.value as RadioItemAppType).createType === AppCreateType.API;
}
get isStepperSAML(): boolean {
return (this.appType?.value as RadioItemAppType).createType === AppCreateType.SAML;
}
get decodedBase64(): string {
const samlReq = this.samlAppRequest.toObject();
if (samlReq && samlReq.metadataXml && typeof samlReq.metadataXml === 'string') {
return Buffer.from(samlReq.metadataXml, 'base64').toString('ascii');
} else {
return '';
}
}
set decodedBase64(xmlString) {
if (this.samlAppRequest) {
const base64 = Buffer.from(xmlString, 'ascii').toString('base64');
this.samlAppRequest.setMetadataXml(base64);
}
}
public get metadataUrl(): AbstractControl | null {
return this.samlConfigForm.get('metadataUrl');
}
}

View File

@@ -2,7 +2,7 @@
title="{{ app?.name }}"
[hasActions]="isZitadel === false && (['project.app.write:' + projectId, 'project.app.write'] | hasRole | async)"
docLink="https://docs.zitadel.com/docs/guides/basics/projects"
[sub]="app?.oidcConfig ? ('APP.OIDC.APPTYPE.' + app?.oidcConfig?.appType | translate) : 'API'"
[sub]="app?.oidcConfig ? ('APP.OIDC.APPTYPE.' + app?.oidcConfig?.appType | translate) : app?.apiConfig ? 'API' : 'SAML'"
[isActive]="app?.state === AppState.APP_STATE_ACTIVE"
[isInactive]="app?.state === AppState.APP_STATE_INACTIVE"
stateTooltip="{{ 'APP.PAGES.DETAIL.STATE.' + app?.state | translate }}"
@@ -156,6 +156,60 @@
</div>
</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>
<div class="cnsl-saml-config-line">
<span class="cnsl-app-or cnsl-secondary-text">{{ 'APP.SAML.OR' | translate }}</span>
<input
#xmlFileInput
style="display: none"
class="file-input"
type="file"
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>
</div>
<div
class="saml-xml"
[ngClass]="{ disabled: !!metadataUrl?.value }"
*ngIf="decodedBase64 && app && app.samlConfig && !app.samlConfig.metadataUrl"
>
<ngx-codemirror
[(ngModel)]="decodedBase64"
[disabled]="!!metadataUrl?.value"
[options]="{
lineNumbers: true,
theme: 'material',
mode: 'application/xml'
}"
></ngx-codemirror>
</div>
<div class="btn-container">
<button
class="submit-button"
type="submit"
color="primary"
(click)="saveSAMLApp()"
[disabled]="samlForm.invalid || !canWrite"
mat-raised-button
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</cnsl-card>
</ng-container>
<ng-container *ngIf="currentSetting === 'token'">

View File

@@ -158,6 +158,24 @@
margin-left: 1rem;
}
}
.cnsl-saml-config-line {
display: flex;
align-items: center;
.cnsl-app-or {
margin-right: 1rem;
}
}
.saml-xml {
margin-top: 2rem;
&.disabled {
opacity: 0.5;
pointer-events: none;
}
}
}
}

View File

@@ -7,6 +7,7 @@ import { MatCheckboxChange } from '@angular/material/checkbox';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Buffer } from 'buffer';
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
import { Subject, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
@@ -27,11 +28,13 @@ import {
OIDCGrantType,
OIDCResponseType,
OIDCTokenType,
SAMLConfig,
} from 'src/app/proto/generated/zitadel/app_pb';
import {
GetOIDCInformationResponse,
UpdateAPIAppConfigRequest,
UpdateOIDCAppConfigRequest,
UpdateSAMLAppConfigRequest,
} from 'src/app/proto/generated/zitadel/management_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
@@ -52,6 +55,8 @@ import {
} from '../authmethods';
import { AuthMethodDialogComponent } from './auth-method-dialog/auth-method-dialog.component';
const MAX_ALLOWED_SIZE = 1 * 1024 * 1024;
@Component({
selector: 'cnsl-app-detail',
templateUrl: './app-detail.component.html',
@@ -104,6 +109,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public oidcForm!: UntypedFormGroup;
public oidcTokenForm!: UntypedFormGroup;
public apiForm!: UntypedFormGroup;
public samlForm!: UntypedFormGroup;
public redirectUrisList: string[] = [];
public postLogoutRedirectUrisList: string[] = [];
@@ -162,6 +168,11 @@ export class AppDetailComponent implements OnInit, OnDestroy {
authMethodType: [{ value: '', disabled: true }],
});
this.samlForm = this.fb.group({
metadataUrl: [{ value: '', disabled: true }],
metadataXml: [{ value: '', disabled: true }],
});
this.http.get('./assets/environment.json').subscribe((env: any) => {
this.environmentMap = {
issuer: env.issuer,
@@ -290,12 +301,15 @@ export class AppDetailComponent implements OnInit, OnDestroy {
} else {
this.authMethods = this.authMethods.filter((element) => element !== CUSTOM_METHOD);
}
} else if (this.app.samlConfig) {
this.settingsList = [{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' }];
}
if (allowed) {
this.oidcForm.enable();
this.oidcTokenForm.enable();
this.apiForm.enable();
this.samlForm.enable();
}
if (this.app.oidcConfig?.redirectUrisList) {
@@ -370,6 +384,30 @@ export class AppDetailComponent implements OnInit, OnDestroy {
}
}
public onDropXML(filelist: FileList): void {
const file = filelist.item(0);
if (file) {
if (file.size > MAX_ALLOWED_SIZE) {
this.toast.showInfo('POLICY.PRIVATELABELING.MAXSIZEEXCEEDED', true);
} else {
this.metadataUrl?.setValue('');
const reader = new FileReader();
reader.onload = ((aXML) => {
return (e) => {
const xmlBase64 = e.target?.result;
if (xmlBase64 && typeof xmlBase64 === 'string' && this.app?.samlConfig) {
const samlConfig = new SAMLConfig();
const cropped = xmlBase64.replace('data:text/xml;base64,', '');
samlConfig.setMetadataXml(cropped);
this.app.samlConfig.metadataXml = cropped;
}
};
})(file);
reader.readAsDataURL(file);
}
}
}
public authMethodFromPartialConfig(config: { oidc?: OIDCConfig.AsObject; api?: APIConfig.AsObject }): string {
const key = getAuthMethodFromPartialConfig(config);
return key;
@@ -581,6 +619,28 @@ export class AppDetailComponent implements OnInit, OnDestroy {
}
}
public saveSAMLApp(): void {
if (this.samlForm.valid && this.app?.samlConfig) {
const req = new UpdateSAMLAppConfigRequest();
req.setProjectId(this.projectId);
req.setAppId(this.app.id);
if (this.app.samlConfig) {
req.setMetadataUrl(this.app.samlConfig?.metadataUrl);
req.setMetadataXml(this.app.samlConfig?.metadataXml);
}
this.mgmtService
.updateSAMLAppConfig(req)
.then(() => {
this.toast.showInfo('APP.TOAST.APIUPDATED', true);
})
.catch((error) => {
this.toast.showError(error);
});
}
}
public regenerateOIDCClientSecret(): void {
if (this.app) {
this.mgmtService
@@ -693,4 +753,31 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public get clockSkewSeconds(): AbstractControl | null {
return this.oidcTokenForm.get('clockSkewSeconds');
}
public get metadataUrl(): AbstractControl | null {
return this.samlForm.get('metadataUrl');
}
get decodedBase64(): string {
if (
this.app &&
this.app.samlConfig &&
this.app.samlConfig.metadataXml &&
typeof this.app.samlConfig.metadataXml === 'string'
) {
return Buffer.from(this.app?.samlConfig.metadataXml, 'base64').toString('ascii');
} else {
return '';
}
}
set decodedBase64(xmlString: string) {
if (this.app && this.app.samlConfig && this.app.samlConfig.metadataXml) {
const base64 = Buffer.from(xmlString, 'ascii').toString('base64');
if (this.app.samlConfig) {
this.app.samlConfig.metadataXml = base64;
}
}
}
}

View File

@@ -16,6 +16,7 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSliderModule } from '@angular/material/slider';
import { MatStepperModule } from '@angular/material/stepper';
import { MatTooltipModule } from '@angular/material/tooltip';
import { CodemirrorModule } from '@ctrl/ngx-codemirror';
import { TranslateModule } from '@ngx-translate/core';
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
@@ -89,6 +90,7 @@ import { RedirectUrisComponent } from './redirect-uris/redirect-uris.component';
InputModule,
MetaLayoutModule,
MatSliderModule,
CodemirrorModule,
ChangesModule,
InfoSectionModule,
],

View File

@@ -10,6 +10,7 @@ import { OIDCAppType } from 'src/app/proto/generated/zitadel/app_pb';
export enum AppCreateType {
API = 'API',
OIDC = 'OIDC',
SAML = 'SAML',
}
export interface RadioItemAppType {
@@ -20,6 +21,7 @@ export interface RadioItemAppType {
descI18nKey: string;
prefix: string;
background: string;
protocol: 'OIDC' | 'SAML';
}
export const WEB_TYPE: RadioItemAppType = {
@@ -30,6 +32,7 @@ export const WEB_TYPE: RadioItemAppType = {
oidcAppType: OIDCAppType.OIDC_APP_TYPE_WEB,
prefix: 'WEB',
background: 'linear-gradient(40deg, #059669 30%, #047857)',
protocol: 'OIDC',
};
export const USER_AGENT_TYPE: RadioItemAppType = {
@@ -40,6 +43,7 @@ export const USER_AGENT_TYPE: RadioItemAppType = {
oidcAppType: OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
prefix: 'UA',
background: 'linear-gradient(40deg, #dc2626 30%, #db2777)',
protocol: 'OIDC',
};
export const NATIVE_TYPE: RadioItemAppType = {
@@ -50,6 +54,7 @@ export const NATIVE_TYPE: RadioItemAppType = {
oidcAppType: OIDCAppType.OIDC_APP_TYPE_NATIVE,
prefix: 'N',
background: 'linear-gradient(40deg, #306ccc 30%, #4f46e5)',
protocol: 'OIDC',
};
export const API_TYPE: RadioItemAppType = {
@@ -59,4 +64,14 @@ export const API_TYPE: RadioItemAppType = {
createType: AppCreateType.API,
prefix: 'API',
background: 'linear-gradient(40deg, #1f2937, #111827)',
protocol: 'OIDC',
};
export const SAML_TYPE: RadioItemAppType = {
titleI18nKey: 'APP.SAML.SELECTION.TITLE',
descI18nKey: 'APP.SAML.SELECTION.DESCRIPTION',
createType: AppCreateType.SAML,
prefix: 'SAML',
background: 'linear-gradient(40deg,rgb(110, 56, 124), rgb(88, 37, 103))',
protocol: 'SAML',
};

View File

@@ -17,20 +17,27 @@
*ngFor="let app of appsSubject | async"
matTooltip="{{ 'ACTIONS.EDIT' | translate }}"
>
<cnsl-app-card class="grid-card" matRipple [type]="app.oidcConfig?.appType" [isApiApp]="app.apiConfig !== undefined">
<cnsl-app-card
class="grid-card"
matRipple
[type]="app.samlConfig ? 'SAML' : app.oidcConfig?.appType"
[isApiApp]="app.apiConfig !== undefined"
>
{{ app.name.charAt(0) }}
<ng-container *ngIf="app.oidcConfig?.appType !== undefined">
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE" class="las la-mobile"></i>
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_WEB" class="las la-code"></i>
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT" class="las la-code"></i>
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE" class="lab la-openid"></i>
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_WEB" class="lab la-openid"></i>
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT" class="lab la-openid"></i>
<i *ngIf="app.apiConfig" class="las la-robot"></i>
</ng-container>
<span *ngIf="app.samlConfig" class="samlspan">SAML</span>
</cnsl-app-card>
<span class="name">{{ app.name }}</span>
<span *ngIf="app.oidcConfig?.appType !== undefined && app.oidcConfig?.appType !== null" class="type">
{{ 'APP.OIDC.APPTYPE.' + app.oidcConfig?.appType | translate }}</span
>
<span *ngIf="app.apiConfig !== undefined" class="type"> API</span>
<span *ngIf="app.samlConfig !== undefined" class="type"> SAML</span>
</div>
<ng-template cnslHasRole [hasRole]="['project.app.write']">

View File

@@ -49,6 +49,17 @@
border-radius: 0.5rem;
font-weight: 800;
box-sizing: border-box;
i {
font-size: 1.2rem;
}
.samlspan {
font-size: 11px;
font-weight: 800;
letter-spacing: 0.05em;
line-height: 1.2rem;
}
}
.name {

View File

@@ -63,6 +63,8 @@ import {
AddProjectResponse,
AddProjectRoleRequest,
AddProjectRoleResponse,
AddSAMLAppRequest,
AddSAMLAppResponse,
AddSecondFactorToLoginPolicyRequest,
AddSecondFactorToLoginPolicyResponse,
AddUserGrantRequest,
@@ -423,6 +425,8 @@ import {
UpdateProjectResponse,
UpdateProjectRoleRequest,
UpdateProjectRoleResponse,
UpdateSAMLAppConfigRequest,
UpdateSAMLAppConfigResponse,
UpdateUserGrantRequest,
UpdateUserGrantResponse,
UpdateUserNameRequest,
@@ -2268,27 +2272,18 @@ export class ManagementService {
return this.grpcService.mgmt.reactivateProjectGrant(req, null).then((resp) => resp.toObject());
}
public addOIDCApp(app: AddOIDCAppRequest.AsObject): Promise<AddOIDCAppResponse.AsObject> {
const req: AddOIDCAppRequest = new AddOIDCAppRequest();
req.setAuthMethodType(app.authMethodType);
req.setName(app.name);
req.setProjectId(app.projectId);
req.setResponseTypesList(app.responseTypesList);
req.setGrantTypesList(app.grantTypesList);
req.setAppType(app.appType);
req.setPostLogoutRedirectUrisList(app.postLogoutRedirectUrisList);
req.setRedirectUrisList(app.redirectUrisList);
public addOIDCApp(req: AddOIDCAppRequest): Promise<AddOIDCAppResponse.AsObject> {
return this.grpcService.mgmt.addOIDCApp(req, null).then((resp) => resp.toObject());
}
public addAPIApp(app: AddAPIAppRequest.AsObject): Promise<AddAPIAppResponse.AsObject> {
const req: AddAPIAppRequest = new AddAPIAppRequest();
req.setAuthMethodType(app.authMethodType);
req.setName(app.name);
req.setProjectId(app.projectId);
public addAPIApp(req: AddAPIAppRequest): Promise<AddAPIAppResponse.AsObject> {
return this.grpcService.mgmt.addAPIApp(req, null).then((resp) => resp.toObject());
}
public addSAMLApp(req: AddSAMLAppRequest): Promise<AddSAMLAppResponse.AsObject> {
return this.grpcService.mgmt.addSAMLApp(req, null).then((resp) => resp.toObject());
}
public regenerateAPIClientSecret(appId: string, projectId: string): Promise<RegenerateAPIClientSecretResponse.AsObject> {
const req = new RegenerateAPIClientSecretRequest();
req.setAppId(appId);
@@ -2312,6 +2307,10 @@ export class ManagementService {
return this.grpcService.mgmt.updateAPIAppConfig(req, null).then((resp) => resp.toObject());
}
public updateSAMLAppConfig(req: UpdateSAMLAppConfigRequest): Promise<UpdateSAMLAppConfigResponse.AsObject> {
return this.grpcService.mgmt.updateSAMLAppConfig(req, null).then((resp) => resp.toObject());
}
public removeApp(projectId: string, appId: string): Promise<RemoveAppResponse.AsObject> {
const req = new RemoveAppRequest();
req.setAppId(appId);

View File

@@ -1609,9 +1609,9 @@
"TITLE": "Anwendung",
"ID": "ID",
"DESCRIPTION": "Hier kannst Du Deine Applikationen bearbeiten und deren Konfiguration anpassen.",
"CREATE_OIDC": "OIDC-Anwendung",
"CREATE_OIDC_DESC_TITLE": "Gebe die Daten der Anwendung Schritt für Schritt ein.",
"CREATE_OIDC_DESC_SUB": "Es wird automatisch eine empfohlene Konfiguration generiert.",
"CREATE": "Applikation erstellen",
"CREATE_DESC_TITLE": "Gebe die Daten der Anwendung Schritt für Schritt ein.",
"CREATE_DESC_SUB": "Es wird automatisch eine empfohlene Konfiguration generiert.",
"STATE": "Status",
"DATECREATED": "Erstellt",
"DATECHANGED": "Geändert",
@@ -1664,6 +1664,10 @@
"ADDITIONALORIGINSDESC": "Wenn sie zusätzliche Origins definieren wollen, die nicht den Redirect URIs gleichzusetzen sind, können Sie dies hier tun.",
"ORIGINS": "Origins",
"NOTANORIGIN": "Der Angegebene Wert ist kein Origin.",
"PROSWITCH": "Konfigurator überspringen",
"NAMEANDTYPESECTION": "Name und Typ",
"TITLEFIRST": "Name der Applikation.",
"TYPETITLE": "Art der Anwendung",
"OIDC": {
"INFO": {
"ISSUER": "Issuer",
@@ -1672,10 +1676,6 @@
"CURRENT": "Aktuelle Konfiguration",
"TOKENSECTIONTITLE": "AuthToken Optionen",
"REDIRECTSECTIONTITLE": "Weiterleitungseinstellungen",
"PROSWITCH": "Konfigurator überspringen",
"NAMEANDTYPESECTION": "Name und Typ",
"TITLEFIRST": "Gebe zuerst einen Namen ein.",
"TYPETITLE": "Welche Art von Anwendung möchtest Du erstellen?",
"REDIRECTTITLE": "Wohin soll nach dem Log-in weitergeleitet werden?",
"REDIRECTDESCRIPTIONWEB": "Die Weiterleitung muss mit https:// beginnen. http:// ist nur im Entwicklermodus zulässig.",
"REDIRECTDESCRIPTIONNATIVE": "Die Weiterleitung muss mit einem eigenen Protokoll, http://127.0.0.1, http://[::1] oder http://localhost beginnen.",
@@ -1767,6 +1767,18 @@
"1": "Private Key JWT"
}
},
"SAML": {
"SELECTION": {
"TITLE": "SAML",
"DESCRIPTION": "SAML Applikationen"
},
"CONFIGSECTION": "SAML Konfiguration",
"URL": "Url des Metadata Files",
"OR": "oder",
"XML": "Metadata XML hochladen",
"METADATA": "Metadata",
"METADATAFROMFILE": "Metadata aus Datei"
},
"AUTHMETHODS": {
"CODE": {
"TITLE": "Code",

View File

@@ -1609,9 +1609,9 @@
"TITLE": "Application",
"ID": "ID",
"DESCRIPTION": "Here you can edit your application data and it's configuration.",
"CREATE_OIDC": "OIDC Application",
"CREATE_OIDC_DESC_TITLE": "Enter Your Application Details Step by Step",
"CREATE_OIDC_DESC_SUB": "A recommended configuration will be automatically generated.",
"CREATE": "Create application",
"CREATE_DESC_TITLE": "Enter Your Application Details Step by Step",
"CREATE_DESC_SUB": "A recommended configuration will be automatically generated.",
"STATE": "Status",
"DATECREATED": "Created",
"DATECHANGED": "Changed",
@@ -1664,6 +1664,10 @@
"ADDITIONALORIGINSDESC": "If you want to add additional Origins to your app which is not used as a redirect you can do that here.",
"ORIGINS": "Origins",
"NOTANORIGIN": "The entered value is not an origin",
"PROSWITCH": "I'm a pro. Skip this wizard.",
"NAMEANDTYPESECTION": "Name and Type",
"TITLEFIRST": "Name of the application",
"TYPETITLE": "Type of application",
"OIDC": {
"INFO": {
"ISSUER": "Issuer",
@@ -1672,10 +1676,6 @@
"CURRENT": "Current Config",
"TOKENSECTIONTITLE": "AuthToken Options",
"REDIRECTSECTIONTITLE": "Redirect Settings",
"PROSWITCH": "I'm a pro. Skip this wizard.",
"NAMEANDTYPESECTION": "Name and Type",
"TITLEFIRST": "Insert a name first.",
"TYPETITLE": "What type of application do you want to create?",
"REDIRECTTITLE": "Specify the URIs where the login will redirect to.",
"POSTREDIRECTTITLE": "This is the redirect URI after logout.",
"REDIRECTDESCRIPTIONWEB": "Redirect URIs must begin with https://. http:// is only valid with enabled development mode.",
@@ -1767,6 +1767,18 @@
"1": "Private Key JWT"
}
},
"SAML": {
"SELECTION": {
"TITLE": "SAML",
"DESCRIPTION": "SAML Applications"
},
"CONFIGSECTION": "SAML Configuration",
"URL": "Url where Metadata file is located",
"OR": "or",
"XML": "Upload Metadata XML",
"METADATA": "Metadata",
"METADATAFROMFILE": "Metadata from File"
},
"AUTHMETHODS": {
"CODE": {
"TITLE": "Code",

View File

@@ -1609,9 +1609,9 @@
"TITLE": "Applicazione",
"ID": "ID",
"DESCRIPTION": "Qui puoi modificare i dati della tua applicazione e la sua configurazione.",
"CREATE_OIDC": "Applicazione OIDC",
"CREATE_OIDC_DESC_TITLE": "Inserisci i dettagli della tua applicazione passo dopo passo",
"CREATE_OIDC_DESC_SUB": "Una configurazione raccomandata sar\u00e0 generata automaticamente.",
"CREATE": "Crea Applicazione",
"CREATE_DESC_TITLE": "Inserisci i dettagli della tua applicazione passo dopo passo",
"CREATE_DESC_SUB": "Una configurazione raccomandata sar\u00e0 generata automaticamente.",
"STATE": "Stato",
"DATECREATED": "Creato",
"DATECHANGED": "Cambiato",
@@ -1664,6 +1664,10 @@
"ADDITIONALORIGINSDESC": "Se vuoi aggiungere ulteriori Origini alla tua app che non \u00e8 usata come reindirizzamento puoi farlo qui.",
"ORIGINS": "Origini",
"NOTANORIGIN": "Il valore inserito non \u00e8 un'origine",
"PROSWITCH": "Sono un professionista. Salta questo passo.",
"NAMEANDTYPESECTION": "Nome e tipo",
"TITLEFIRST": "Nome dell' applicazione",
"TYPETITLE": "Che tipo di applicazione vuoi creare?",
"OIDC": {
"INFO": {
"ISSUER": "Issuer",
@@ -1672,10 +1676,6 @@
"CURRENT": "Configurazione attuale",
"TOKENSECTIONTITLE": "Opzioni AuthToken",
"REDIRECTSECTIONTITLE": "Impostazioni di reindirizzamento",
"PROSWITCH": "Sono un professionista. Salta questo passo.",
"NAMEANDTYPESECTION": "Nome e tipo",
"TITLEFIRST": "Inserisci un nome.",
"TYPETITLE": "Che tipo di applicazione vuoi creare?",
"REDIRECTTITLE": "Specifica gli URI a cui il login sar\u00e0 reindirizzato.",
"POSTREDIRECTTITLE": "Questo \u00e8 l'URI di reindirizzamento dopo il logout.",
"REDIRECTDESCRIPTIONWEB": "Gli URI di reindirizzamento devono iniziare con https://. http:// \u00e8 valido solo con la modalit\u00e0 di sviluppo abilitata (DEV Mode).",
@@ -1767,6 +1767,18 @@
"1": "Private Key JWT"
}
},
"SAML": {
"SELECTION": {
"TITLE": "SAML",
"DESCRIPTION": "Applicazioni SAMML"
},
"CONFIGSECTION": "Configurazione SAML",
"URL": "URL in cui si trova il file di metadati",
"OR": "o",
"XML": "Carica Metadata XML",
"METADATA": "Metadata",
"METADATAFROMFILE": "Metadati dal file"
},
"AUTHMETHODS": {
"CODE": {
"TITLE": "Code",

View File

@@ -1,4 +1,5 @@
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/xml/xml';
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';