mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-06 13:27:45 +00:00
feat: add additional origins on applications (#1691)
* feat: add additional origins on applications * app additional redirects * chore(deps-dev): bump @angular/cli from 11.2.8 to 11.2.11 in /console (#1706) * fix: show org with regex (#1688) * fix: flag mapping (#1699) * chore(deps-dev): bump @angular/cli from 11.2.8 to 11.2.11 in /console Bumps [@angular/cli](https://github.com/angular/angular-cli) from 11.2.8 to 11.2.11. - [Release notes](https://github.com/angular/angular-cli/releases) - [Commits](https://github.com/angular/angular-cli/compare/v11.2.8...v11.2.11) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Max Peintner <max@caos.ch> Co-authored-by: Silvan <silvan.reusser@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump stylelint from 13.10.0 to 13.13.1 in /console (#1703) * fix: show org with regex (#1688) * fix: flag mapping (#1699) * chore(deps-dev): bump stylelint from 13.10.0 to 13.13.1 in /console Bumps [stylelint](https://github.com/stylelint/stylelint) from 13.10.0 to 13.13.1. - [Release notes](https://github.com/stylelint/stylelint/releases) - [Changelog](https://github.com/stylelint/stylelint/blob/master/CHANGELOG.md) - [Commits](https://github.com/stylelint/stylelint/compare/13.10.0...13.13.1) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Max Peintner <max@caos.ch> Co-authored-by: Silvan <silvan.reusser@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump @types/node from 14.14.37 to 15.0.1 in /console (#1702) * fix: show org with regex (#1688) * fix: flag mapping (#1699) * chore(deps-dev): bump @types/node from 14.14.37 to 15.0.1 in /console Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.14.37 to 15.0.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Max Peintner <max@caos.ch> Co-authored-by: Silvan <silvan.reusser@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump ts-protoc-gen from 0.14.0 to 0.15.0 in /console (#1701) * fix: show org with regex (#1688) * fix: flag mapping (#1699) * chore(deps): bump ts-protoc-gen from 0.14.0 to 0.15.0 in /console Bumps [ts-protoc-gen](https://github.com/improbable-eng/ts-protoc-gen) from 0.14.0 to 0.15.0. - [Release notes](https://github.com/improbable-eng/ts-protoc-gen/releases) - [Changelog](https://github.com/improbable-eng/ts-protoc-gen/blob/master/CHANGELOG.md) - [Commits](https://github.com/improbable-eng/ts-protoc-gen/compare/0.14.0...0.15.0) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Max Peintner <max@caos.ch> Co-authored-by: Silvan <silvan.reusser@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump @types/jasmine from 3.6.9 to 3.6.10 in /console (#1682) Bumps [@types/jasmine](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jasmine) from 3.6.9 to 3.6.10. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jasmine) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump @types/google-protobuf in /console (#1681) Bumps [@types/google-protobuf](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/google-protobuf) from 3.7.4 to 3.15.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/google-protobuf) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump grpc from 1.24.5 to 1.24.7 in /console (#1666) Bumps [grpc](https://github.com/grpc/grpc-node) from 1.24.5 to 1.24.7. - [Release notes](https://github.com/grpc/grpc-node/releases) - [Commits](https://github.com/grpc/grpc-node/compare/grpc@1.24.5...grpc@1.24.7) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * lock * chore(deps-dev): bump @angular/language-service from 11.2.9 to 11.2.12 in /console (#1704) * fix: show org with regex (#1688) * fix: flag mapping (#1699) * chore(deps-dev): bump @angular/language-service in /console Bumps [@angular/language-service](https://github.com/angular/angular/tree/HEAD/packages/language-service) from 11.2.9 to 11.2.12. - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/master/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/11.2.12/packages/language-service) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Max Peintner <max@caos.ch> Co-authored-by: Silvan <silvan.reusser@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * package lock * downgrade grpc * downgrade protobuf types * revert npm packs 🥸 Co-authored-by: Max Peintner <max@caos.ch> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Silvan <silvan.reusser@gmail.com>
This commit is contained in:
parent
18ed6633be
commit
2e8fa82261
@ -69,4 +69,4 @@
|
||||
"tslint": "~6.1.3",
|
||||
"typescript": "^4.0.7"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
<form class="form" (ngSubmit)="add(originInput)">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ title }}</cnsl-label>
|
||||
|
||||
<input #originInput cnslInput [placeholder]="placeholder" [formControl]="redirectControl">
|
||||
</cnsl-form-field>
|
||||
<button matTooltip="{{'ACTIONS.ADD' | translate}}" type="submit" mat-icon-button
|
||||
[disabled]="redirectControl.invalid || !canWrite">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="uri-list">
|
||||
<div *ngFor="let uri of urisList" class="uri-line">
|
||||
<span class="uri"
|
||||
[ngClass]="{'green': uri?.startsWith('https://'), 'red': !uri?.startsWith('https://')}">{{uri}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<!-- TODO add regex later -->
|
||||
<!-- <i *ngIf="!(uri | origin)" class="las la-exclamation red" [matTooltip]="'APP.NOTANORIGIN' | translate"></i> -->
|
||||
|
||||
<button matTooltip="{{'ACTIONS.DELETE' | translate}}" mat-icon-button (click)="remove(uri)" class="icon-button">
|
||||
<mat-icon class="icon">cancel</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p *ngIf="redirectControl.value && redirectControl.invalid" class="error">
|
||||
{{'APP.OIDC.REDIRECTNOTVALID' | translate}}</p>
|
@ -0,0 +1,62 @@
|
||||
.form {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
min-width: 320px;
|
||||
|
||||
.formfield {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-bottom: 14px;
|
||||
margin-right: -.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.uri-list {
|
||||
margin: 0 .5rem;
|
||||
width: 100%;
|
||||
|
||||
.uri-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.uri {
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
i.green {
|
||||
font-size: 1rem;
|
||||
line-height: 35px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
i.red {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
.icon {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
&:not(:hover) {
|
||||
color: var(--grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 13px;
|
||||
color: #f44336;
|
||||
margin: 0 .5rem 1.5rem .5rem;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdditionalOriginsComponent } from './additional-origins.component';
|
||||
|
||||
describe('AdditionalOriginsComponent', () => {
|
||||
let component: AdditionalOriginsComponent;
|
||||
let fixture: ComponentFixture<AdditionalOriginsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AdditionalOriginsComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdditionalOriginsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,56 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-additional-origins',
|
||||
templateUrl: './additional-origins.component.html',
|
||||
styleUrls: ['./additional-origins.component.scss'],
|
||||
})
|
||||
export class AdditionalOriginsComponent implements OnInit, OnDestroy {
|
||||
@Input() title: string = '';
|
||||
@Input() canWrite: boolean = false;
|
||||
@Input() public urisList: string[] = [];
|
||||
@Input() public redirectControl: FormControl = new FormControl({ value: '', disabled: true });
|
||||
@Input() public changedUris: EventEmitter<string[]> = new EventEmitter();
|
||||
@Input() public getValues: Observable<void> = new Observable();
|
||||
public placeholder: string = '<scheme> "://" <hostname> [ ":" <port> ]';
|
||||
|
||||
@ViewChild('originInput') input!: any;
|
||||
private sub: Subscription = new Subscription();
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.canWrite) {
|
||||
this.redirectControl.enable();
|
||||
}
|
||||
|
||||
this.sub = this.getValues.subscribe(() => {
|
||||
this.add(this.input.nativeElement);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
|
||||
public add(input: any): void {
|
||||
if (this.redirectControl.valid) {
|
||||
if (input.value !== '' && input.value !== ' ' && input.value !== '/') {
|
||||
this.urisList.push(input.value);
|
||||
}
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public remove(redirect: any): void {
|
||||
const index = this.urisList.indexOf(redirect);
|
||||
|
||||
if (index >= 0) {
|
||||
this.urisList.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -118,7 +118,6 @@
|
||||
{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}
|
||||
</cnsl-info-section>
|
||||
|
||||
<div style="margin: .5rem" class="divider"></div>
|
||||
<cnsl-redirect-uris *ngIf="appType?.value !== undefined" class="redirect-section" [canWrite]="canWrite"
|
||||
[devMode]="devMode?.value" [getValues]="requestRedirectValuesSubject$"
|
||||
(changedUris)="redirectUrisList = $event" [urisList]="redirectUrisList"
|
||||
@ -132,6 +131,27 @@
|
||||
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
|
||||
[isNative]="appType?.value == OIDCAppType.OIDC_APP_TYPE_NATIVE">
|
||||
</cnsl-redirect-uris>
|
||||
|
||||
<div style="margin: .5rem" class="divider"></div>
|
||||
|
||||
<div class="additional-origins">
|
||||
<p class="title">{{'APP.ADDITIONALORIGINS' | translate}}
|
||||
<button mat-icon-button (click)="showAdditionalOrigins = !showAdditionalOrigins"
|
||||
matTooltip="{{(showAdditionalOrigins ? 'ACTIONS.HIDE' : 'ACTIONS.SHOW') | translate}}">
|
||||
<mat-icon *ngIf="!showAdditionalOrigins">expand_more</mat-icon>
|
||||
<mat-icon *ngIf="showAdditionalOrigins">expand_less</mat-icon>
|
||||
</button>
|
||||
</p>
|
||||
<ng-container *ngIf="showAdditionalOrigins">
|
||||
<p class="desc">{{'APP.ADDITIONALORIGINSDESC' | translate}}</p>
|
||||
<cnsl-additional-origins *ngIf="appType?.value !== undefined" class="input" [canWrite]="canWrite"
|
||||
[getValues]="requestRedirectValuesSubject$" (changedUris)="additionalOriginsList = $event"
|
||||
[urisList]="additionalOriginsList" title="{{ 'APP.ORIGINS' | translate }}">
|
||||
</cnsl-additional-origins>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div style="margin: .5rem" class="divider"></div>
|
||||
</div>
|
||||
|
||||
<app-auth-method-radio *ngIf="authMethods && initialAuthMethod && (app?.oidcConfig || app?.apiConfig)"
|
||||
|
@ -155,6 +155,26 @@
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
|
||||
.additional-origins {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0 .5rem;
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: var(--grey);
|
||||
font-size: 14px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.formfield {
|
||||
flex: 1 1 30%;
|
||||
margin: 0 .5rem;
|
||||
|
@ -61,6 +61,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
public errorMessage: string = '';
|
||||
public removable: boolean = true;
|
||||
public addOnBlur: boolean = true;
|
||||
public showAdditionalOrigins: boolean = false;
|
||||
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
|
||||
|
||||
public authMethods: RadioItemAuthType[] = [];
|
||||
@ -102,6 +103,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
public redirectUrisList: string[] = [];
|
||||
public postLogoutRedirectUrisList: string[] = [];
|
||||
public additionalOriginsList: string[] = [];
|
||||
|
||||
public isZitadel: boolean = false;
|
||||
public docs!: GetOIDCInformationResponse.AsObject;
|
||||
@ -252,6 +254,10 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
if (this.app.oidcConfig?.postLogoutRedirectUrisList) {
|
||||
this.postLogoutRedirectUrisList = this.app.oidcConfig.postLogoutRedirectUrisList;
|
||||
}
|
||||
if (this.app.oidcConfig?.additionalOriginsList) {
|
||||
this.additionalOriginsList = this.app.oidcConfig.additionalOriginsList;
|
||||
}
|
||||
|
||||
if (this.app.oidcConfig?.clockSkew) {
|
||||
const inSecs = this.app.oidcConfig?.clockSkew.seconds +
|
||||
this.app.oidcConfig?.clockSkew.nanos / 100000;
|
||||
@ -445,6 +451,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
this.app.oidcConfig.authMethodType = this.authMethodType?.value;
|
||||
this.app.oidcConfig.redirectUrisList = this.redirectUrisList;
|
||||
this.app.oidcConfig.postLogoutRedirectUrisList = this.postLogoutRedirectUrisList;
|
||||
this.app.oidcConfig.additionalOriginsList = this.additionalOriginsList;
|
||||
this.app.oidcConfig.devMode = this.devMode?.value;
|
||||
this.app.oidcConfig.accessTokenType = this.accessTokenType?.value;
|
||||
this.app.oidcConfig.accessTokenRoleAssertion = this.accessTokenRoleAssertion?.value;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
@ -22,25 +23,28 @@ import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
|
||||
import { AppRadioModule } from 'src/app/modules/app-radio/app-radio.module';
|
||||
import { CardModule } from 'src/app/modules/card/card.module';
|
||||
import { ChangesModule } from 'src/app/modules/changes/changes.module';
|
||||
import { ClientKeysModule } from 'src/app/modules/client-keys/client-keys.module';
|
||||
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
|
||||
import { InputModule } from 'src/app/modules/input/input.module';
|
||||
import { LinksModule } from 'src/app/modules/links/links.module';
|
||||
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
|
||||
import { OriginPipeModule } from 'src/app/pipes/origin-pipe/origin-pipe.module';
|
||||
import { RedirectPipeModule } from 'src/app/pipes/redirect-pipe/redirect-pipe.module';
|
||||
|
||||
import { AdditionalOriginsComponent } from './additional-origins/additional-origins.component';
|
||||
import { AppCreateComponent } from './app-create/app-create.component';
|
||||
import { AppDetailComponent } from './app-detail/app-detail.component';
|
||||
import { AppSecretDialogComponent } from './app-secret-dialog/app-secret-dialog.component';
|
||||
import { AppsRoutingModule } from './apps-routing.module';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
import { RedirectUrisComponent } from './redirect-uris/redirect-uris.component';
|
||||
import { LinksModule } from 'src/app/modules/links/links.module';
|
||||
import { RedirectPipeModule } from 'src/app/pipes/redirect-pipe/redirect-pipe.module';
|
||||
import { ClientKeysModule } from 'src/app/modules/client-keys/client-keys.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppCreateComponent,
|
||||
AppDetailComponent,
|
||||
AppSecretDialogComponent,
|
||||
RedirectUrisComponent,
|
||||
AdditionalOriginsComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -51,6 +55,7 @@ import { ClientKeysModule } from 'src/app/modules/client-keys/client-keys.module
|
||||
AppsRoutingModule,
|
||||
FormsModule,
|
||||
TranslateModule,
|
||||
OriginPipeModule,
|
||||
ReactiveFormsModule,
|
||||
HasRoleModule,
|
||||
MatMenuModule,
|
||||
|
18
console/src/app/pipes/origin-pipe/origin-pipe.module.ts
Normal file
18
console/src/app/pipes/origin-pipe/origin-pipe.module.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { OriginPipe } from './origin.pipe';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
OriginPipe,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
],
|
||||
exports: [
|
||||
OriginPipe,
|
||||
],
|
||||
})
|
||||
export class OriginPipeModule { }
|
10
console/src/app/pipes/origin-pipe/origin.pipe.ts
Normal file
10
console/src/app/pipes/origin-pipe/origin.pipe.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'origin',
|
||||
})
|
||||
export class OriginPipe implements PipeTransform {
|
||||
public transform(value: string): boolean {
|
||||
return new RegExp(/^((https?:\/\/).*?([\w\d-]*\.[\w\d]+))($|\/.*$)/gm).test(value);
|
||||
}
|
||||
}
|
@ -104,6 +104,8 @@
|
||||
}
|
||||
},
|
||||
"ACTIONS": {
|
||||
"SHOW":"Aufklappen",
|
||||
"HIDE":"Zuklappen",
|
||||
"SAVE": "Speichern",
|
||||
"SAVENOW": "Speichern",
|
||||
"NEW": "Neu",
|
||||
@ -1056,6 +1058,10 @@
|
||||
"AUTHMETHOD": "Authentifizierungsmethode",
|
||||
"AUTHMETHODSECTION": "Authentifizierungsmethode",
|
||||
"GRANT": "Berechtigungstypen",
|
||||
"ADDITIONALORIGINS":"Zusätzliche Origins",
|
||||
"ADDITIONALORIGINSDESC":"Wenn sie zusätzliche Origins definieren wollen, die nicht den Redirect URIs gleichzusätzen sind, können Sie dies hier tun.",
|
||||
"ORIGINS":"Origins",
|
||||
"NOTANORIGIN":"Der Angegebene Wert ist kein Origin.",
|
||||
"OIDC": {
|
||||
"INFO": {
|
||||
"ISSUER": "Issuer",
|
||||
|
@ -104,6 +104,8 @@
|
||||
}
|
||||
},
|
||||
"ACTIONS": {
|
||||
"SHOW":"Show",
|
||||
"HIDE":"Hide",
|
||||
"SAVE": "Save",
|
||||
"SAVENOW": "Save now",
|
||||
"NEW": "New",
|
||||
@ -1057,6 +1059,10 @@
|
||||
"AUTHMETHOD": "Authentication Method",
|
||||
"AUTHMETHODSECTION": "Authentication Method",
|
||||
"GRANT": "Grant Types",
|
||||
"ADDITIONALORIGINS":"Additional Origins",
|
||||
"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",
|
||||
"OIDC": {
|
||||
"INFO": {
|
||||
"ISSUER": "Issuer",
|
||||
|
@ -84,6 +84,8 @@ title: zitadel/app.proto
|
||||
| id_token_role_assertion | bool | - | |
|
||||
| id_token_userinfo_assertion | bool | - | |
|
||||
| clock_skew | google.protobuf.Duration | - | |
|
||||
| additional_origins | repeated string | - | |
|
||||
| allowed_origins | repeated string | - | |
|
||||
|
||||
|
||||
|
||||
|
@ -2074,6 +2074,7 @@ Change OIDC identity provider configuration of the organisation
|
||||
| id_token_role_assertion | bool | - | |
|
||||
| id_token_userinfo_assertion | bool | - | |
|
||||
| clock_skew | google.protobuf.Duration | - | duration.lte.seconds: 5<br /> duration.lte.nanos: 0<br /> duration.gte.seconds: 0<br /> duration.gte.nanos: 0<br /> |
|
||||
| additional_origins | repeated string | - | |
|
||||
|
||||
|
||||
|
||||
@ -5194,6 +5195,7 @@ This is an empty request
|
||||
| id_token_role_assertion | bool | - | |
|
||||
| id_token_userinfo_assertion | bool | - | |
|
||||
| clock_skew | google.protobuf.Duration | - | duration.lte.seconds: 5<br /> duration.lte.nanos: 0<br /> duration.gte.seconds: 0<br /> duration.gte.nanos: 0<br /> |
|
||||
| additional_origins | repeated string | - | |
|
||||
|
||||
|
||||
|
||||
|
@ -52,6 +52,7 @@ func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) *domain.OIDCApp {
|
||||
IDTokenRoleAssertion: req.IdTokenRoleAssertion,
|
||||
IDTokenUserinfoAssertion: req.IdTokenUserinfoAssertion,
|
||||
ClockSkew: req.ClockSkew.AsDuration(),
|
||||
AdditionalOrigins: req.AdditionalOrigins,
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,6 +91,7 @@ func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest)
|
||||
IDTokenRoleAssertion: app.IdTokenRoleAssertion,
|
||||
IDTokenUserinfoAssertion: app.IdTokenUserinfoAssertion,
|
||||
ClockSkew: app.ClockSkew.AsDuration(),
|
||||
AdditionalOrigins: app.AdditionalOrigins,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,6 +55,8 @@ func AppOIDCConfigToPb(app *proj_model.ApplicationView) *app_pb.App_OidcConfig {
|
||||
IdTokenRoleAssertion: app.IDTokenRoleAssertion,
|
||||
IdTokenUserinfoAssertion: app.IDTokenUserinfoAssertion,
|
||||
ClockSkew: durationpb.New(app.ClockSkew),
|
||||
AdditionalOrigins: app.AdditionalOrigins,
|
||||
AllowedOrigins: app.OriginAllowList,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -21,3 +21,12 @@ func IsOriginAllowed(allowList []string, origin string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//IsOrigin checks if provided string is an origin (scheme://hostname[:port]) without path, query or fragment
|
||||
func IsOrigin(rawOrigin string) bool {
|
||||
parsedUrl, err := url.Parse(rawOrigin)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return parsedUrl.Scheme != "" && parsedUrl.Host != "" && parsedUrl.Path == "" && len(parsedUrl.Query()) == 0 && parsedUrl.Fragment == ""
|
||||
}
|
||||
|
@ -81,7 +81,8 @@ func (c *Commands) addOIDCApplication(ctx context.Context, projectAgg *eventstor
|
||||
oidcApp.AccessTokenRoleAssertion,
|
||||
oidcApp.IDTokenRoleAssertion,
|
||||
oidcApp.IDTokenUserinfoAssertion,
|
||||
oidcApp.ClockSkew))
|
||||
oidcApp.ClockSkew,
|
||||
oidcApp.AdditionalOrigins))
|
||||
|
||||
return events, stringPw, nil
|
||||
}
|
||||
@ -115,7 +116,8 @@ func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCA
|
||||
oidc.AccessTokenRoleAssertion,
|
||||
oidc.IDTokenRoleAssertion,
|
||||
oidc.IDTokenUserinfoAssertion,
|
||||
oidc.ClockSkew)
|
||||
oidc.ClockSkew,
|
||||
oidc.AdditionalOrigins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ type OIDCApplicationWriteModel struct {
|
||||
IDTokenUserinfoAssertion bool
|
||||
ClockSkew time.Duration
|
||||
State domain.AppState
|
||||
AdditionalOrigins []string
|
||||
}
|
||||
|
||||
func NewOIDCApplicationWriteModelWithAppID(projectID, appID, resourceOwner string) *OIDCApplicationWriteModel {
|
||||
@ -151,6 +152,7 @@ func (wm *OIDCApplicationWriteModel) appendAddOIDCEvent(e *project.OIDCConfigAdd
|
||||
wm.IDTokenRoleAssertion = e.IDTokenRoleAssertion
|
||||
wm.IDTokenUserinfoAssertion = e.IDTokenUserinfoAssertion
|
||||
wm.ClockSkew = e.ClockSkew
|
||||
wm.AdditionalOrigins = e.AdditionalOrigins
|
||||
}
|
||||
|
||||
func (wm *OIDCApplicationWriteModel) appendChangeOIDCEvent(e *project.OIDCConfigChangedEvent) {
|
||||
@ -193,6 +195,9 @@ func (wm *OIDCApplicationWriteModel) appendChangeOIDCEvent(e *project.OIDCConfig
|
||||
if e.ClockSkew != nil {
|
||||
wm.ClockSkew = *e.ClockSkew
|
||||
}
|
||||
if e.AdditionalOrigins != nil {
|
||||
wm.AdditionalOrigins = *e.AdditionalOrigins
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *OIDCApplicationWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
@ -229,6 +234,7 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent(
|
||||
idTokenRoleAssertion,
|
||||
idTokenUserinfoAssertion bool,
|
||||
clockSkew time.Duration,
|
||||
additionalOrigins []string,
|
||||
) (*project.OIDCConfigChangedEvent, bool, error) {
|
||||
changes := make([]project.OIDCConfigChanges, 0)
|
||||
var err error
|
||||
@ -272,6 +278,9 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent(
|
||||
if wm.ClockSkew != clockSkew {
|
||||
changes = append(changes, project.ChangeClockSkew(clockSkew))
|
||||
}
|
||||
if !reflect.DeepEqual(wm.AdditionalOrigins, additionalOrigins) {
|
||||
changes = append(changes, project.ChangeAdditionalOrigins(additionalOrigins))
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
@ -151,7 +151,8 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
time.Second*1),
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"}),
|
||||
),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(project.NewAddApplicationUniqueConstraint("app", "project1")),
|
||||
@ -180,6 +181,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
|
||||
IDTokenRoleAssertion: true,
|
||||
IDTokenUserinfoAssertion: true,
|
||||
ClockSkew: time.Second * 1,
|
||||
AdditionalOrigins: []string{"https://sub.test.ch"},
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
@ -206,6 +208,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
|
||||
IDTokenRoleAssertion: true,
|
||||
IDTokenUserinfoAssertion: true,
|
||||
ClockSkew: time.Second * 1,
|
||||
AdditionalOrigins: []string{"https://sub.test.ch"},
|
||||
State: domain.AppStateActive,
|
||||
Compliance: &domain.Compliance{},
|
||||
},
|
||||
@ -382,7 +385,8 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
time.Second*1),
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"}),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -408,6 +412,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
|
||||
IDTokenRoleAssertion: true,
|
||||
IDTokenUserinfoAssertion: true,
|
||||
ClockSkew: time.Second * 1,
|
||||
AdditionalOrigins: []string{"https://sub.test.ch"},
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
@ -451,7 +456,8 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
time.Second*1),
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"}),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
@ -487,6 +493,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
|
||||
IDTokenRoleAssertion: false,
|
||||
IDTokenUserinfoAssertion: false,
|
||||
ClockSkew: time.Second * 2,
|
||||
AdditionalOrigins: []string{"https://sub.test.ch"},
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
@ -512,6 +519,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
|
||||
IDTokenRoleAssertion: false,
|
||||
IDTokenUserinfoAssertion: false,
|
||||
ClockSkew: time.Second * 2,
|
||||
AdditionalOrigins: []string{"https://sub.test.ch"},
|
||||
Compliance: &domain.Compliance{},
|
||||
State: domain.AppStateActive,
|
||||
},
|
||||
@ -645,7 +653,8 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
time.Second*1),
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"}),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
@ -695,6 +704,7 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
|
||||
IDTokenRoleAssertion: true,
|
||||
IDTokenUserinfoAssertion: true,
|
||||
ClockSkew: time.Second * 1,
|
||||
AdditionalOrigins: []string{"https://sub.test.ch"},
|
||||
State: domain.AppStateActive,
|
||||
},
|
||||
},
|
||||
|
@ -51,6 +51,7 @@ func oidcWriteModelToOIDCConfig(writeModel *OIDCApplicationWriteModel) *domain.O
|
||||
IDTokenRoleAssertion: writeModel.IDTokenRoleAssertion,
|
||||
IDTokenUserinfoAssertion: writeModel.IDTokenUserinfoAssertion,
|
||||
ClockSkew: writeModel.ClockSkew,
|
||||
AdditionalOrigins: writeModel.AdditionalOrigins,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
http_util "github.com/caos/zitadel/internal/api/http"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
@ -43,6 +44,7 @@ type OIDCApp struct {
|
||||
IDTokenRoleAssertion bool
|
||||
IDTokenUserinfoAssertion bool
|
||||
ClockSkew time.Duration
|
||||
AdditionalOrigins []string
|
||||
|
||||
State AppState
|
||||
}
|
||||
@ -119,7 +121,7 @@ const (
|
||||
)
|
||||
|
||||
func (a *OIDCApp) IsValid() bool {
|
||||
if a.ClockSkew > time.Second*5 || a.ClockSkew < time.Second*0 {
|
||||
if a.ClockSkew > time.Second*5 || a.ClockSkew < time.Second*0 || !a.OriginsValid() {
|
||||
return false
|
||||
}
|
||||
grantTypes := a.getRequiredGrantTypes()
|
||||
@ -135,6 +137,15 @@ func (a *OIDCApp) IsValid() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *OIDCApp) OriginsValid() bool {
|
||||
for _, origin := range a.AdditionalOrigins {
|
||||
if !http_util.IsOrigin(origin) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *OIDCApp) getRequiredGrantTypes() []OIDCGrantType {
|
||||
grantTypes := make([]OIDCGrantType, 0)
|
||||
implicit := false
|
||||
|
@ -1,9 +1,10 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
func TestApplicationValid(t *testing.T) {
|
||||
@ -160,6 +161,20 @@ func TestApplicationValid(t *testing.T) {
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "invalid oidc application: invalid origin",
|
||||
args: args{
|
||||
app: &OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
AppID: "AppID",
|
||||
AppName: "Name",
|
||||
ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode, OIDCResponseTypeIDToken, OIDCResponseTypeIDTokenToken},
|
||||
GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode, OIDCGrantTypeImplicit},
|
||||
AdditionalOrigins: []string{"https://test.com/test"},
|
||||
},
|
||||
},
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -30,6 +30,7 @@ type ApplicationView struct {
|
||||
ComplianceProblems []string
|
||||
DevMode bool
|
||||
OriginAllowList []string
|
||||
AdditionalOrigins []string
|
||||
AccessTokenType OIDCTokenType
|
||||
IDTokenRoleAssertion bool
|
||||
AccessTokenRoleAssertion bool
|
||||
|
@ -45,6 +45,7 @@ type ApplicationView struct {
|
||||
ComplianceProblems pq.StringArray `json:"-" gorm:"column:compliance_problems"`
|
||||
DevMode bool `json:"devMode" gorm:"column:dev_mode"`
|
||||
OriginAllowList pq.StringArray `json:"-" gorm:"column:origin_allow_list"`
|
||||
AdditionalOrigins pq.StringArray `json:"additionalOrigins" gorm:"column:additional_origins"`
|
||||
AccessTokenType int32 `json:"accessTokenType" gorm:"column:access_token_type"`
|
||||
AccessTokenRoleAssertion bool `json:"accessTokenRoleAssertion" gorm:"column:access_token_role_assertion"`
|
||||
IDTokenRoleAssertion bool `json:"idTokenRoleAssertion" gorm:"column:id_token_role_assertion"`
|
||||
@ -79,6 +80,7 @@ func ApplicationViewToModel(app *ApplicationView) *model.ApplicationView {
|
||||
ComplianceProblems: app.ComplianceProblems,
|
||||
DevMode: app.DevMode,
|
||||
OriginAllowList: app.OriginAllowList,
|
||||
AdditionalOrigins: app.AdditionalOrigins,
|
||||
AccessTokenType: model.OIDCTokenType(app.AccessTokenType),
|
||||
AccessTokenRoleAssertion: app.AccessTokenRoleAssertion,
|
||||
IDTokenRoleAssertion: app.IDTokenRoleAssertion,
|
||||
@ -213,6 +215,11 @@ func (a *ApplicationView) setOriginAllowList() error {
|
||||
allowList = append(allowList, origin)
|
||||
}
|
||||
}
|
||||
for _, origin := range a.AdditionalOrigins {
|
||||
if !http_util.IsOriginAllowed(allowList, origin) {
|
||||
allowList = append(allowList, origin)
|
||||
}
|
||||
}
|
||||
a.OriginAllowList = allowList
|
||||
return nil
|
||||
}
|
||||
|
@ -3,12 +3,12 @@ package project
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/repository"
|
||||
)
|
||||
|
||||
@ -39,6 +39,7 @@ type OIDCConfigAddedEvent struct {
|
||||
IDTokenRoleAssertion bool `json:"idTokenRoleAssertion,omitempty"`
|
||||
IDTokenUserinfoAssertion bool `json:"idTokenUserinfoAssertion,omitempty"`
|
||||
ClockSkew time.Duration `json:"clockSkew,omitempty"`
|
||||
AdditionalOrigins []string `json:"additionalOrigins,omitempty"`
|
||||
}
|
||||
|
||||
func (e *OIDCConfigAddedEvent) Data() interface{} {
|
||||
@ -68,6 +69,7 @@ func NewOIDCConfigAddedEvent(
|
||||
idTokenRoleAssertion bool,
|
||||
idTokenUserinfoAssertion bool,
|
||||
clockSkew time.Duration,
|
||||
additionalOrigins []string,
|
||||
) *OIDCConfigAddedEvent {
|
||||
return &OIDCConfigAddedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
@ -91,6 +93,7 @@ func NewOIDCConfigAddedEvent(
|
||||
IDTokenRoleAssertion: idTokenRoleAssertion,
|
||||
IDTokenUserinfoAssertion: idTokenUserinfoAssertion,
|
||||
ClockSkew: clockSkew,
|
||||
AdditionalOrigins: additionalOrigins,
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,6 +127,7 @@ type OIDCConfigChangedEvent struct {
|
||||
IDTokenRoleAssertion *bool `json:"idTokenRoleAssertion,omitempty"`
|
||||
IDTokenUserinfoAssertion *bool `json:"idTokenUserinfoAssertion,omitempty"`
|
||||
ClockSkew *time.Duration `json:"clockSkew,omitempty"`
|
||||
AdditionalOrigins *[]string `json:"additionalOrigins,omitempty"`
|
||||
}
|
||||
|
||||
func (e *OIDCConfigChangedEvent) Data() interface{} {
|
||||
@ -238,6 +242,12 @@ func ChangeClockSkew(clockSkew time.Duration) func(event *OIDCConfigChangedEvent
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeAdditionalOrigins(additionalOrigins []string) func(event *OIDCConfigChangedEvent) {
|
||||
return func(e *OIDCConfigChangedEvent) {
|
||||
e.AdditionalOrigins = &additionalOrigins
|
||||
}
|
||||
}
|
||||
|
||||
func OIDCConfigChangedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
|
||||
e := &OIDCConfigChangedEvent{
|
||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||
|
3
migrations/cockroach/V1.43__additional_origins.sql
Normal file
3
migrations/cockroach/V1.43__additional_origins.sql
Normal file
@ -0,0 +1,3 @@
|
||||
ALTER TABLE auth.applications ADD COLUMN additional_origins TEXT ARRAY;
|
||||
ALTER TABLE authz.applications ADD COLUMN additional_origins TEXT ARRAY;
|
||||
ALTER TABLE management.applications ADD COLUMN additional_origins TEXT ARRAY;
|
@ -65,7 +65,7 @@ message AppNameQuery {
|
||||
message OIDCConfig {
|
||||
repeated string redirect_uris = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "[\"console.zitadel.ch/authorized\"]";
|
||||
example: "[\"https://console.zitadel.ch/auth/callback\"]";
|
||||
description: "Callback URI of the authorization request where the code or tokens will be sent to";
|
||||
}
|
||||
];
|
||||
@ -103,7 +103,7 @@ message OIDCConfig {
|
||||
];
|
||||
repeated string post_logout_redirect_uris = 8 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "[\"console.zitadel.ch/logout\"]";
|
||||
example: "[\"https://console.zitadel.ch/logout\"]";
|
||||
description: "ZITADEL will redirect to this link after a successful logout";
|
||||
}
|
||||
];
|
||||
@ -154,6 +154,18 @@ message OIDCConfig {
|
||||
// max: "5s";
|
||||
}
|
||||
];
|
||||
repeated string additional_origins = 18 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "[\"https://console.zitadel.ch/auth/callback\"]";
|
||||
description: "additional origins (other than the redirect_uris) from where the api can be used";
|
||||
}
|
||||
];
|
||||
repeated string allowed_origins = 19 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "[\"https://console.zitadel.ch/auth/callback\"]";
|
||||
description: "all allowed origins from where the api can be used";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
enum OIDCResponseType {
|
||||
|
@ -2953,6 +2953,7 @@ message AddOIDCAppRequest {
|
||||
bool id_token_role_assertion = 13;
|
||||
bool id_token_userinfo_assertion = 14;
|
||||
google.protobuf.Duration clock_skew = 15 [(validate.rules).duration = {gte: {}, lte: {seconds: 5}}];
|
||||
repeated string additional_origins = 16;
|
||||
}
|
||||
|
||||
message AddOIDCAppResponse {
|
||||
@ -3023,6 +3024,7 @@ message UpdateOIDCAppConfigRequest {
|
||||
bool id_token_role_assertion = 12;
|
||||
bool id_token_userinfo_assertion = 13;
|
||||
google.protobuf.Duration clock_skew = 14 [(validate.rules).duration = {gte: {}, lte: {seconds: 5}}];
|
||||
repeated string additional_origins = 15;
|
||||
}
|
||||
|
||||
message UpdateOIDCAppConfigResponse {
|
||||
|
Loading…
x
Reference in New Issue
Block a user