Merge branch 'main' into next-merge

This commit is contained in:
adlerhurst
2023-10-19 10:11:02 +02:00
60 changed files with 434 additions and 84 deletions

View File

@@ -105,13 +105,21 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
// check if username is email style or else append @<orgname>.<custom-domain> // check if username is email style or else append @<orgname>.<custom-domain>
//this way we have the same value as before changing `UserLoginMustBeDomain` to false //this way we have the same value as before changing `UserLoginMustBeDomain` to false
if !mig.instanceSetup.DomainPolicy.UserLoginMustBeDomain && !strings.Contains(mig.instanceSetup.Org.Human.Username, "@") { if !mig.instanceSetup.DomainPolicy.UserLoginMustBeDomain && !strings.Contains(mig.instanceSetup.Org.Human.Username, "@") {
mig.instanceSetup.Org.Human.Username = mig.instanceSetup.Org.Human.Username + "@" + domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain) orgDomain, err := domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain)
if err != nil {
return err
}
mig.instanceSetup.Org.Human.Username = mig.instanceSetup.Org.Human.Username + "@" + orgDomain
} }
mig.instanceSetup.Org.Human.Email.Address = mig.instanceSetup.Org.Human.Email.Address.Normalize() mig.instanceSetup.Org.Human.Email.Address = mig.instanceSetup.Org.Human.Email.Address.Normalize()
if mig.instanceSetup.Org.Human.Email.Address == "" { if mig.instanceSetup.Org.Human.Email.Address == "" {
mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username) mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username)
if !strings.Contains(string(mig.instanceSetup.Org.Human.Email.Address), "@") { if !strings.Contains(string(mig.instanceSetup.Org.Human.Email.Address), "@") {
mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username + "@" + domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain)) orgDomain, err := domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain)
if err != nil {
return err
}
mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username + "@" + orgDomain)
} }
} }

View File

@@ -1,5 +1,5 @@
<div class="main-container"> <div class="main-container">
<ng-container *ngIf="(authService.user | async) || {} as user"> <ng-container *ngIf="(authService.userSubject | async) || {} as user">
<cnsl-header <cnsl-header
*ngIf="user && user !== {}" *ngIf="user && user !== {}"
[org]="org" [org]="org"

View File

@@ -225,9 +225,11 @@ export class AppComponent implements OnDestroy {
}); });
this.isDarkTheme = this.themeService.isDarkTheme; this.isDarkTheme = this.themeService.isDarkTheme;
this.isDarkTheme.subscribe((dark) => this.onSetTheme(dark ? 'dark-theme' : 'light-theme')); this.isDarkTheme
.pipe(takeUntil(this.destroy$))
.subscribe((dark) => this.onSetTheme(dark ? 'dark-theme' : 'light-theme'));
this.translate.onLangChange.subscribe((language: LangChangeEvent) => { this.translate.onLangChange.pipe(takeUntil(this.destroy$)).subscribe((language: LangChangeEvent) => {
this.document.documentElement.lang = language.lang; this.document.documentElement.lang = language.lang;
this.language = language.lang; this.language = language.lang;
}); });
@@ -271,7 +273,7 @@ export class AppComponent implements OnDestroy {
this.translate.addLangs(supportedLanguages); this.translate.addLangs(supportedLanguages);
this.translate.setDefaultLang(fallbackLanguage); this.translate.setDefaultLang(fallbackLanguage);
this.authService.user.subscribe((userprofile) => { this.authService.userSubject.pipe(takeUntil(this.destroy$)).subscribe((userprofile) => {
if (userprofile) { if (userprofile) {
const cropped = navigator.language.split('-')[0] ?? fallbackLanguage; const cropped = navigator.language.split('-')[0] ?? fallbackLanguage;
const fallbackLang = cropped.match(supportedLanguagesRegexp) ? cropped : fallbackLanguage; const fallbackLang = cropped.match(supportedLanguagesRegexp) ? cropped : fallbackLanguage;

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs'; import { map, Observable, take } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { GrpcAuthService } from '../services/grpc-auth.service'; import { GrpcAuthService } from '../services/grpc-auth.service';
@@ -19,11 +18,13 @@ export class UserGuard {
state: RouterStateSnapshot, state: RouterStateSnapshot,
): Observable<boolean> | Promise<boolean> | boolean { ): Observable<boolean> | Promise<boolean> | boolean {
return this.authService.user.pipe( return this.authService.user.pipe(
map((user) => user?.id !== route.params['id']), take(1),
tap((isNotMe) => { map((user) => {
if (!isNotMe) { const isMe = user?.id === route.params['id'];
if (isMe) {
this.router.navigate(['/users', 'me']); this.router.navigate(['/users', 'me']);
} }
return !isMe;
}), }),
); );
} }

View File

@@ -2,7 +2,7 @@
title="{{ 'APP.PAGES.CREATE' | translate }}" title="{{ 'APP.PAGES.CREATE' | translate }}"
class="app-create-wrapper" class="app-create-wrapper"
[createSteps]=" [createSteps]="
!devmode !pro
? appType?.value?.createType === AppCreateType.OIDC ? appType?.value?.createType === AppCreateType.OIDC
? appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE ? appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE
? 4 ? 4
@@ -20,13 +20,13 @@
<h1>{{ 'APP.PAGES.CREATE_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-progress-bar class="progress-bar" color="primary" *ngIf="loading" mode="indeterminate"></mat-progress-bar>
<mat-checkbox class="proswitch" color="primary" [(ngModel)]="devmode"> <mat-checkbox class="proswitch" color="primary" [(ngModel)]="pro">
{{ 'APP.PROSWITCH' | translate }} {{ 'APP.PROSWITCH' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-horizontal-stepper <mat-horizontal-stepper
class="stepper {{ 'last-edited-step-' + stepper.selectedIndex }}" class="stepper {{ 'last-edited-step-' + stepper.selectedIndex }}"
*ngIf="!devmode" *ngIf="!pro"
linear linear
#stepper #stepper
labelPosition="bottom" labelPosition="bottom"
@@ -109,6 +109,15 @@
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }} {{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
</p> </p>
<mat-slide-toggle
color="primary"
[(ngModel)]="devMode"
[ngModelOptions]="{ standalone: true }"
matTooltip="{{ 'APP.OIDC.DEVMODEDESC' | translate }}"
>
{{ 'APP.OIDC.DEVMODE' | translate }}
</mat-slide-toggle>
<cnsl-redirect-uris <cnsl-redirect-uris
class="redirect-section" class="redirect-section"
[disabled]="false" [disabled]="false"
@@ -116,6 +125,7 @@
[(ngModel)]="redirectUris" [(ngModel)]="redirectUris"
[getValues]="requestRedirectValuesSubject$" [getValues]="requestRedirectValuesSubject$"
title="{{ 'APP.OIDC.REDIRECT' | translate }}" title="{{ 'APP.OIDC.REDIRECT' | translate }}"
[devMode]="devMode"
data-e2e="redirect-uris" data-e2e="redirect-uris"
> >
</cnsl-redirect-uris> </cnsl-redirect-uris>
@@ -144,6 +154,7 @@
[getValues]="requestRedirectValuesSubject$" [getValues]="requestRedirectValuesSubject$"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}" title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE" [isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
[devMode]="devMode"
data-e2e="postlogout-uris" data-e2e="postlogout-uris"
> >
</cnsl-redirect-uris> </cnsl-redirect-uris>
@@ -299,6 +310,19 @@
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="appType?.value?.createType === AppCreateType.OIDC">
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.OIDC.DEVMODE' | translate }}
</span>
<span class="right">
<span>
{{ devMode ? ('APP.OIDC.DEVMODE_ENABLED' | translate) : ('APP.OIDC.DEVMODE_DISABLED' | translate) }}
</span>
</span>
</div>
</ng-container>
<ng-container *ngIf="appType?.value?.createType === AppCreateType.API"> <ng-container *ngIf="appType?.value?.createType === AppCreateType.API">
<div class="row"> <div class="row">
<span class="left cnsl-secondary-text"> <span class="left cnsl-secondary-text">
@@ -342,7 +366,7 @@
</ng-template> </ng-template>
</mat-horizontal-stepper> </mat-horizontal-stepper>
<div *ngIf="devmode" class="dev"> <div *ngIf="pro" class="dev">
<form [formGroup]="form" (ngSubmit)="createApp()" data-e2e="create-app-wizzard-3"> <form [formGroup]="form" (ngSubmit)="createApp()" data-e2e="create-app-wizzard-3">
<div class="content"> <div class="content">
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">
@@ -439,6 +463,14 @@
" "
> >
<div class="formfield full-width"> <div class="formfield full-width">
<mat-slide-toggle
color="primary"
[(ngModel)]="devMode"
[ngModelOptions]="{ standalone: true }"
matTooltip="{{ 'APP.OIDC.DEVMODEDESC' | translate }}"
>
{{ 'APP.OIDC.DEVMODE' | translate }}
</mat-slide-toggle>
<cnsl-redirect-uris <cnsl-redirect-uris
class="redirect-section" class="redirect-section"
[disabled]="false" [disabled]="false"

View File

@@ -28,6 +28,7 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { MatLegacySlideToggleChange } from '@angular/material/legacy-slide-toggle';
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component'; import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
import { import {
BASIC_AUTH_METHOD, BASIC_AUTH_METHOD,
@@ -51,7 +52,7 @@ const MAX_ALLOWED_SIZE = 1 * 1024 * 1024;
export class AppCreateComponent implements OnInit, OnDestroy { export class AppCreateComponent implements OnInit, OnDestroy {
private subscription: Subscription = new Subscription(); private subscription: Subscription = new Subscription();
private destroyed$: Subject<void> = new Subject(); private destroyed$: Subject<void> = new Subject();
public devmode: boolean = false; public pro: boolean = false;
public projectId: string = ''; public projectId: string = '';
public loading: boolean = false; public loading: boolean = false;
@@ -239,7 +240,16 @@ export class AppCreateComponent implements OnInit, OnDestroy {
this.oidcAppRequest.setPostLogoutRedirectUrisList(value); this.oidcAppRequest.setPostLogoutRedirectUrisList(value);
} }
public get devMode() {
return this.oidcAppRequest.toObject().devMode;
}
public set devMode(value: boolean) {
this.oidcAppRequest.setDevMode(value);
}
public ngOnInit(): void { public ngOnInit(): void {
this.devMode = false;
this.subscription = this.route.params.subscribe((params) => this.getData(params)); this.subscription = this.route.params.subscribe((params) => this.getData(params));
const projectId = this.route.snapshot.paramMap.get('projectid'); const projectId = this.route.snapshot.paramMap.get('projectid');
@@ -362,9 +372,9 @@ export class AppCreateComponent implements OnInit, OnDestroy {
} }
public createApp(): void { public createApp(): void {
const appOIDCCheck = this.devmode ? this.isDevOIDC : this.isStepperOIDC; const appOIDCCheck = this.pro ? this.isDevOIDC : this.isStepperOIDC;
const appAPICheck = this.devmode ? this.isDevAPI : this.isStepperAPI; const appAPICheck = this.pro ? this.isDevAPI : this.isStepperAPI;
const appSAMLCheck = this.devmode ? this.isDevSAML : this.isStepperSAML; const appSAMLCheck = this.pro ? this.isDevSAML : this.isStepperSAML;
if (appOIDCCheck) { if (appOIDCCheck) {
this.requestRedirectValuesSubject$.next(); this.requestRedirectValuesSubject$.next();

View File

@@ -427,7 +427,7 @@
<div class="app-info-row"> <div class="app-info-row">
<div class="app-info-wrapper" *ngFor="let wellKnownV of wellKnownMap$ | async | keyvalue"> <div class="app-info-wrapper" *ngFor="let wellKnownV of wellKnownMap$ | async | keyvalue">
<ng-container *ngIf="wellKnownV.key.endsWith('endpoint')"> <ng-container *ngIf="wellKnownV.key.endsWith('endpoint') || wellKnownV.key.toString() === 'jwks_uri'">
<p class="app-info-row-title cnsl-secondary-text">{{ wellKnownV.key }}</p> <p class="app-info-row-title cnsl-secondary-text">{{ wellKnownV.key }}</p>
<div class="app-copy-row"> <div class="app-copy-row">
<div *ngIf="wellKnownV.value" class="environment"> <div *ngIf="wellKnownV.value" class="environment">

View File

@@ -3,6 +3,7 @@ import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
import { BehaviorSubject, from, lastValueFrom, Observable } from 'rxjs'; import { BehaviorSubject, from, lastValueFrom, Observable } from 'rxjs';
import { StatehandlerService } from './statehandler/statehandler.service'; import { StatehandlerService } from './statehandler/statehandler.service';
import { ToastService } from './toast.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -15,6 +16,7 @@ export class AuthenticationService {
constructor( constructor(
private oauthService: OAuthService, private oauthService: OAuthService,
private statehandler: StatehandlerService, private statehandler: StatehandlerService,
private toast: ToastService,
) {} ) {}
public initConfig(data: AuthConfig): void { public initConfig(data: AuthConfig): void {
@@ -39,7 +41,10 @@ export class AuthenticationService {
} }
this.oauthService.configure(this.authConfig); this.oauthService.configure(this.authConfig);
this.oauthService.strictDiscoveryDocumentValidation = false; this.oauthService.strictDiscoveryDocumentValidation = false;
await this.oauthService.loadDiscoveryDocumentAndTryLogin(); await this.oauthService.loadDiscoveryDocumentAndTryLogin().catch((error) => {
this.toast.showError(error, false, false);
});
this._authenticated = this.oauthService.hasValidAccessToken(); this._authenticated = this.oauthService.hasValidAccessToken();
if (!this.oauthService.hasValidIdToken() || !this.authenticated || partialConfig || force) { if (!this.oauthService.hasValidIdToken() || !this.authenticated || partialConfig || force) {
const newState = await lastValueFrom(this.statehandler.createState()); const newState = await lastValueFrom(this.statehandler.createState());

View File

@@ -1,19 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort'; import { SortDirection } from '@angular/material/sort';
import { OAuthService } from 'angular-oauth2-oidc'; import { OAuthService } from 'angular-oauth2-oidc';
import { BehaviorSubject, from, merge, Observable, of, Subject } from 'rxjs'; import { BehaviorSubject, forkJoin, from, Observable, of, Subject } from 'rxjs';
import { import { catchError, distinctUntilChanged, filter, finalize, map, switchMap, timeout, withLatestFrom } from 'rxjs/operators';
catchError,
distinctUntilChanged,
filter,
finalize,
map,
mergeMap,
switchMap,
take,
timeout,
withLatestFrom,
} from 'rxjs/operators';
import { import {
AddMyAuthFactorOTPEmailRequest, AddMyAuthFactorOTPEmailRequest,
@@ -184,25 +173,21 @@ export class GrpcAuthService {
}, },
}); });
this.user = merge( this.user = forkJoin([
of(this.oauthService.getAccessToken()).pipe(filter((token) => (token ? true : false))), of(this.oauthService.getAccessToken()),
this.oauthService.events.pipe( this.oauthService.events.pipe(
filter((e) => e.type === 'token_received'), filter((e) => e.type === 'token_received'),
timeout(this.oauthService.waitForTokenInMsec || 0), timeout(this.oauthService.waitForTokenInMsec || 0),
catchError((_) => of(null)), // timeout is not an error catchError((_) => of(null)), // timeout is not an error
map((_) => this.oauthService.getAccessToken()), map((_) => this.oauthService.getAccessToken()),
), ),
).pipe( ]).pipe(
take(1), filter((token) => (token ? true : false)),
mergeMap(() => { distinctUntilChanged(),
switchMap(() => {
return from( return from(
this.getMyUser().then((resp) => { this.getMyUser().then((resp) => {
const user = resp.user; return resp.user;
if (user) {
return user;
} else {
return undefined;
}
}), }),
); );
}), }),

View File

@@ -1977,6 +1977,8 @@
"CLIENTSECRET_DESCRIPTION": "Пазете клиентската си тайна на сигурно място, тъй като тя ще изчезне, след като диалоговият прозорец бъде затворен.", "CLIENTSECRET_DESCRIPTION": "Пазете клиентската си тайна на сигурно място, тъй като тя ще изчезне, след като диалоговият прозорец бъде затворен.",
"REGENERATESECRET": "Повторно генериране на клиентска тайна", "REGENERATESECRET": "Повторно генериране на клиентска тайна",
"DEVMODE": "Режим на разработка", "DEVMODE": "Режим на разработка",
"DEVMODE_ENABLED": "Активиран",
"DEVMODE_DISABLED": "Деактивиран",
"DEVMODEDESC": "Внимание: При активиран режим на разработка URI адресите за пренасочване няма да бъдат валидирани.", "DEVMODEDESC": "Внимание: При активиран режим на разработка URI адресите за пренасочване няма да бъдат валидирани.",
"SKIPNATIVEAPPSUCCESSPAGE": "Пропуснете страницата за успешно влизане", "SKIPNATIVEAPPSUCCESSPAGE": "Пропуснете страницата за успешно влизане",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Пропуснете страницата за успех след влизане в това родно приложение.", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Пропуснете страницата за успех след влизане в това родно приложение.",

View File

@@ -1986,6 +1986,8 @@
"CLIENTSECRET_DESCRIPTION": "Verwahre das Client Secret an einem sicheren Ort, da es nicht mehr angezeigt werden kann, sobald der Dialog geschlossen wird.", "CLIENTSECRET_DESCRIPTION": "Verwahre das Client Secret an einem sicheren Ort, da es nicht mehr angezeigt werden kann, sobald der Dialog geschlossen wird.",
"REGENERATESECRET": "Client Secret neu generieren", "REGENERATESECRET": "Client Secret neu generieren",
"DEVMODE": "Entwicklermodus", "DEVMODE": "Entwicklermodus",
"DEVMODE_ENABLED": "Aktiviert",
"DEVMODE_DISABLED": "Deaktiviert",
"DEVMODEDESC": "Bei eingeschaltetem Entwicklermodus werden die Weiterleitungs-URIs im OIDC-Flow nicht validiert.", "DEVMODEDESC": "Bei eingeschaltetem Entwicklermodus werden die Weiterleitungs-URIs im OIDC-Flow nicht validiert.",
"SKIPNATIVEAPPSUCCESSPAGE": "Login Erfolgseite überspringen", "SKIPNATIVEAPPSUCCESSPAGE": "Login Erfolgseite überspringen",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Erfolgseite nach dem Login für diese Native Applikation überspringen.", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Erfolgseite nach dem Login für diese Native Applikation überspringen.",

View File

@@ -1995,6 +1995,8 @@
"CLIENTSECRET_DESCRIPTION": "Keep your client secret at a safe place as it will disappear once the dialog is closed.", "CLIENTSECRET_DESCRIPTION": "Keep your client secret at a safe place as it will disappear once the dialog is closed.",
"REGENERATESECRET": "Regenerate Client Secret", "REGENERATESECRET": "Regenerate Client Secret",
"DEVMODE": "Development Mode", "DEVMODE": "Development Mode",
"DEVMODE_ENABLED": "Enabled",
"DEVMODE_DISABLED": "Disabled",
"DEVMODEDESC": "Beware: With development mode enabled redirect URIs will not be validated.", "DEVMODEDESC": "Beware: With development mode enabled redirect URIs will not be validated.",
"SKIPNATIVEAPPSUCCESSPAGE": "Skip Login Success Page", "SKIPNATIVEAPPSUCCESSPAGE": "Skip Login Success Page",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Skip the success page after a login for this native app.", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Skip the success page after a login for this native app.",

View File

@@ -1983,6 +1983,8 @@
"CLIENTSECRET_DESCRIPTION": "Mantén tu secreto de cliente en un lugar seguro puesto que desaparecerá una vez que se cierre el diálogo.", "CLIENTSECRET_DESCRIPTION": "Mantén tu secreto de cliente en un lugar seguro puesto que desaparecerá una vez que se cierre el diálogo.",
"REGENERATESECRET": "Regenerar secreto de cliente", "REGENERATESECRET": "Regenerar secreto de cliente",
"DEVMODE": "Modo Desarrollo", "DEVMODE": "Modo Desarrollo",
"DEVMODE_ENABLED": "Activado",
"DEVMODE_DISABLED": "Desactivado",
"DEVMODEDESC": "Cuidado: Si el modo de desarrollo está activado las URIs de redirección no serán validadas.", "DEVMODEDESC": "Cuidado: Si el modo de desarrollo está activado las URIs de redirección no serán validadas.",
"SKIPNATIVEAPPSUCCESSPAGE": "Saltar página de inicio de sesión con éxito", "SKIPNATIVEAPPSUCCESSPAGE": "Saltar página de inicio de sesión con éxito",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Sáltate la página de éxito después de iniciar sesión en esta app nativa.", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Sáltate la página de éxito después de iniciar sesión en esta app nativa.",

View File

@@ -1987,6 +1987,8 @@
"CLIENTSECRET_DESCRIPTION": "Conservez votre secret client dans un endroit sûr car il disparaîtra une fois la boîte de dialogue fermée.", "CLIENTSECRET_DESCRIPTION": "Conservez votre secret client dans un endroit sûr car il disparaîtra une fois la boîte de dialogue fermée.",
"REGENERATESECRET": "Régénérer le secret du client", "REGENERATESECRET": "Régénérer le secret du client",
"DEVMODE": "Mode développement", "DEVMODE": "Mode développement",
"DEVMODE_ENABLED": "Activé",
"DEVMODE_DISABLED": "Désactivé",
"DEVMODEDESC": "Attention", "DEVMODEDESC": "Attention",
"SKIPNATIVEAPPSUCCESSPAGE": "Sauter la page de succès de connexion", "SKIPNATIVEAPPSUCCESSPAGE": "Sauter la page de succès de connexion",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Sauter la page de succès après la connexion pour cette application native", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Sauter la page de succès après la connexion pour cette application native",

View File

@@ -1987,6 +1987,8 @@
"CLIENTSECRET_DESCRIPTION": "Salvate il Client Secret in un luogo sicuro, perch\u00e9 non sarà più disponibile dopo aver chiuso la finestra di dialogo", "CLIENTSECRET_DESCRIPTION": "Salvate il Client Secret in un luogo sicuro, perch\u00e9 non sarà più disponibile dopo aver chiuso la finestra di dialogo",
"REGENERATESECRET": "Rigenera il Client Secret", "REGENERATESECRET": "Rigenera il Client Secret",
"DEVMODE": "Modalit\u00e0 di sviluppo (DEV Mode)", "DEVMODE": "Modalit\u00e0 di sviluppo (DEV Mode)",
"DEVMODE_ENABLED": "Attivato",
"DEVMODE_DISABLED": "Disattivato",
"DEVMODEDESC": "Attenzione: Con la modalit\u00e0 di sviluppo abilitata, gli URI di reindirizzamento non saranno convalidati.", "DEVMODEDESC": "Attenzione: Con la modalit\u00e0 di sviluppo abilitata, gli URI di reindirizzamento non saranno convalidati.",
"SKIPNATIVEAPPSUCCESSPAGE": "Salta la pagina di successo dopo il login", "SKIPNATIVEAPPSUCCESSPAGE": "Salta la pagina di successo dopo il login",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Salta la pagina di successo dopo il login per questa applicazione nativa", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Salta la pagina di successo dopo il login per questa applicazione nativa",

View File

@@ -1978,6 +1978,8 @@
"CLIENTSECRET_DESCRIPTION": "クライアントシークレットは、ダイアログを閉じると消えてしまうので、安全な場所に保管してください。", "CLIENTSECRET_DESCRIPTION": "クライアントシークレットは、ダイアログを閉じると消えてしまうので、安全な場所に保管してください。",
"REGENERATESECRET": "クライアントシークレットを再生成する", "REGENERATESECRET": "クライアントシークレットを再生成する",
"DEVMODE": "開発モード", "DEVMODE": "開発モード",
"DEVMODE_ENABLED": "アクティブ化された",
"DEVMODE_DISABLED": "無効化されました",
"DEVMODEDESC": "注意開発モードを有効にすると、URIが認証されません。", "DEVMODEDESC": "注意開発モードを有効にすると、URIが認証されません。",
"SKIPNATIVEAPPSUCCESSPAGE": "ログイン後に成功ページをスキップする", "SKIPNATIVEAPPSUCCESSPAGE": "ログイン後に成功ページをスキップする",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "このネイティブアプリのログイン後に成功ページをスキップする", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "このネイティブアプリのログイン後に成功ページをスキップする",

View File

@@ -1984,6 +1984,8 @@
"CLIENTSECRET_DESCRIPTION": "Чувајте ја вашата клиентска тајна на безбедно место, бидејќи ќе исчезне откако ќе се затвори дијалогот.", "CLIENTSECRET_DESCRIPTION": "Чувајте ја вашата клиентска тајна на безбедно место, бидејќи ќе исчезне откако ќе се затвори дијалогот.",
"REGENERATESECRET": "Генерирај нова клиентска тајна", "REGENERATESECRET": "Генерирај нова клиентска тајна",
"DEVMODE": "Development mode", "DEVMODE": "Development mode",
"DEVMODE_ENABLED": "Активиран",
"DEVMODE_DISABLED": "Деактивирано",
"DEVMODEDESC": "Внимавајте: Со овозможен Development mode, URIs за пренасочување нема да бидат валидирани.", "DEVMODEDESC": "Внимавајте: Со овозможен Development mode, URIs за пренасочување нема да бидат валидирани.",
"SKIPNATIVEAPPSUCCESSPAGE": "Прескокни страница за успешна најава", "SKIPNATIVEAPPSUCCESSPAGE": "Прескокни страница за успешна најава",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Прескокнете ја страницата за успешна најава за оваа нативна апликација.", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Прескокнете ја страницата за успешна најава за оваа нативна апликација.",

View File

@@ -1987,6 +1987,8 @@
"CLIENTSECRET_DESCRIPTION": "Trzymaj swój sekret klienta w bezpiecznym miejscu, ponieważ zniknie on po zamknięciu okna dialogowego.", "CLIENTSECRET_DESCRIPTION": "Trzymaj swój sekret klienta w bezpiecznym miejscu, ponieważ zniknie on po zamknięciu okna dialogowego.",
"REGENERATESECRET": "Odtwórz sekret klienta", "REGENERATESECRET": "Odtwórz sekret klienta",
"DEVMODE": "Tryb rozwoju", "DEVMODE": "Tryb rozwoju",
"DEVMODE_ENABLED": "Aktywowany",
"DEVMODE_DISABLED": "Dezaktywowane",
"DEVMODEDESC": "Uwaga: przy włączonym trybie rozwoju adresy URI przekierowania nie będą sprawdzane.", "DEVMODEDESC": "Uwaga: przy włączonym trybie rozwoju adresy URI przekierowania nie będą sprawdzane.",
"SKIPNATIVEAPPSUCCESSPAGE": "Pomiń stronę sukcesu po zalogowaniu", "SKIPNATIVEAPPSUCCESSPAGE": "Pomiń stronę sukcesu po zalogowaniu",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Pomiń stronę sukcesu po zalogowaniu dla tej Natywny aplikację", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Pomiń stronę sukcesu po zalogowaniu dla tej Natywny aplikację",

View File

@@ -1982,6 +1982,8 @@
"CLIENTSECRET_DESCRIPTION": "Mantenha o segredo do cliente em um local seguro, pois ele desaparecerá assim que o diálogo for fechado.", "CLIENTSECRET_DESCRIPTION": "Mantenha o segredo do cliente em um local seguro, pois ele desaparecerá assim que o diálogo for fechado.",
"REGENERATESECRET": "Regenerar Segredo do Cliente", "REGENERATESECRET": "Regenerar Segredo do Cliente",
"DEVMODE": "Modo de Desenvolvimento", "DEVMODE": "Modo de Desenvolvimento",
"DEVMODE_ENABLED": "Ativado",
"DEVMODE_DISABLED": "Desativado",
"DEVMODEDESC": "Atenção: Com o modo de desenvolvimento habilitado, as URIs de redirecionamento não serão validadas.", "DEVMODEDESC": "Atenção: Com o modo de desenvolvimento habilitado, as URIs de redirecionamento não serão validadas.",
"SKIPNATIVEAPPSUCCESSPAGE": "Pular Página de Sucesso de Login", "SKIPNATIVEAPPSUCCESSPAGE": "Pular Página de Sucesso de Login",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Pule a página de sucesso após o login para este aplicativo nativo.", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Pule a página de sucesso após o login para este aplicativo nativo.",

View File

@@ -1986,6 +1986,8 @@
"CLIENTSECRET_DESCRIPTION": "将您的客户保密在一个安全的地方,因为一旦对话框关闭,便无法再次查看。", "CLIENTSECRET_DESCRIPTION": "将您的客户保密在一个安全的地方,因为一旦对话框关闭,便无法再次查看。",
"REGENERATESECRET": "重新生成客户端密钥", "REGENERATESECRET": "重新生成客户端密钥",
"DEVMODE": "开发模式", "DEVMODE": "开发模式",
"DEVMODE_ENABLED": "活性",
"DEVMODE_DISABLED": "已停用",
"DEVMODEDESC": "注意:启用开发模式的重定向 URI 将不会被验证。", "DEVMODEDESC": "注意:启用开发模式的重定向 URI 将不会被验证。",
"SKIPNATIVEAPPSUCCESSPAGE": "登录后跳过成功页面", "SKIPNATIVEAPPSUCCESSPAGE": "登录后跳过成功页面",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "登录后跳过本机应用的成功页面", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "登录后跳过本机应用的成功页面",

View File

@@ -0,0 +1,69 @@
---
title: Configure Keycloak as an Identity Provider in ZITADEL
sidebar_label: Keycloak generic OIDC
id: keycloak
---
import GeneralConfigDescription from './_general_config_description.mdx';
import Intro from './_intro.mdx';
import CustomLoginPolicy from './_custom_login_policy.mdx';
import IDPsOverview from './_idps_overview.mdx';
import GenericOIDC from './_generic_oidc.mdx';
import Activate from './_activate.mdx';
import TestSetup from './_test_setup.mdx';
<Intro provider="Keycloak"/>
## Keycloak Configuration
### Register a new client
1. Login to your Keycloak account and go to the clients list: <$KEYCLOAK-DOMAIN/auth/admin/$REALM/console/#/$REALM/clients>
2. Click on "Create Client"
3. Choose OpenID Connect as Client Type and give your client an ID
4. Enable Client authentication and the standard flow and direct access grants as authentication flow
5. Add the valid redirect URIs
- {your-domain}/ui/login/login/externalidp/callback
- Example redirect url for the domain `https://acme-gzoe4x.zitadel.cloud` would look like this: `https://acme-gzoe4x.zitadel.cloud/ui/login/login/externalidp/callback`
6. Go to the credentials tab and copy the secret
![Add new OIDC Client in Keycloak](/img/guides/keycloak_add_client.png)
![Get Client Secret](/img/guides/keycloak_client_secret.png)
## ZITADEL configuration
### Add custom login policy
<CustomLoginPolicy/>
### Go to the IdP providers overview
<IDPsOverview templates="Generic OIDC"/>
### Create a new generic OIDC provider
<GenericOIDC
name=": e.g. Keycloak"
issuer=": The domain where your Keycloak can be reached with the path /auth/realms/$REALM, Example: https://lemur-0.cloud-iam.com/auth/realms/acme"
clientid=": Client id from the client previously created in your Keycloak account"
/>
<GeneralConfigDescription provider_account="Keycloak account" />
![Keycloak Provider](/img/guides/zitadel_keycloak_create_provider.png)
### Activate IdP
<Activate/>
![Activate the Keycloak Provider](/img/guides/zitadel_activate_keycloak.png)
## Test the setup
<TestSetup loginscreen="your Keycloak login"/>
![Keycloak Button](/img/guides/zitadel_login_keycloak.png)
![Keycloak Login](/img/guides/keycloak_login.png)

View File

@@ -20,7 +20,7 @@ This means that the users and also their authorizations will be managed within Z
An organization is the ZITADEL resource which contains users, projects, applications, policies and so on. An organization is the ZITADEL resource which contains users, projects, applications, policies and so on.
In an organization projects and users are managed by the organization. In an organization projects and users are managed by the organization.
You need at least one organization for your own company in our case "The Timing Company". You need at least one organization for your own company in our case "The Timing Company".
As next step grate an organization for each of your costumers. Your next step is to create an organization for each of your customers.
## Project ## Project
@@ -39,7 +39,7 @@ You can configure `check roles on authentication` on the project, if you want to
To give a customer permissions to a project, a project grant to the customers organization is needed (search the granted organization by its domain). To give a customer permissions to a project, a project grant to the customers organization is needed (search the granted organization by its domain).
It is also possible to delegate only specific roles of the project to a certain customer. It is also possible to delegate only specific roles of the project to a certain customer.
As soon as a project grant exists, the customer will see the project in the granted projects section of his organization and will be able to authorize his own users to the given project. As soon as a project grant exists, the customer will see the project in the granted projects section of their organization and will be able to authorize their own users to the given project.
## Authorizations ## Authorizations

View File

@@ -6,14 +6,14 @@ title: Technical Advisory 10002
Version: TBD Version: TBD
Date: Calendar week 40/41 Date: Calendar week 44
## Description ## Description
Since Angular Material v15 many of the UI components have been refactored Since Angular Material v15 many of the UI components have been refactored
to be based on the official Material Design Components for Web (MDC). to be based on the official Material Design Components for Web (MDC).
These refactored components do not support dynamic styling, so in order to keep the library up-to-date, These refactored components do not support dynamic styling, so in order to keep the library up-to-date,
the console UI will loose its dynamic theming capability. the console UI will lose its dynamic theming capability.
## Statement ## Statement
@@ -23,7 +23,7 @@ As soon as the release version is published, we will include the version here.
## Mitigation ## Mitigation
If you need users to have your branding settings If you need users to have your branding settings
(background-, button-, link and text coloring), you should implemement your (background-, button-, link and text coloring), you should implement your
own user facing UI yourself and not use ZITADELs console UI. Assets like your logo and icons will still be used. own user facing UI yourself and not use ZITADELs console UI. Assets like your logo and icons will still be used.
## Impact ## Impact

View File

@@ -0,0 +1,46 @@
---
title: Technical Advisory 10003
---
## Date and Version
Version: 2.38.0
Date: Calendar week 41
## Description
When users are redirected to the ZITADEL Login-UI without any organizational context, they're currently presented a login screen,
based on the instance settings, e.g. available IDPs and possible login mechanisms. If the user will then register himself,
by the registration form or through an IDP, the user will always be created on the default organization.
This behaviour led to confusion, e.g. when activating IDPs on default org would not show up in the Login-UI, because they would still be loaded from the instance settings.
To improve this, we're introducing the following change:
If users are redirected to the Login-UI without any organizational context, they will be presented a login screen based on the settings of the default organization (incl. IDPs).
:::note
If the registration (and also authentication) needs to occur on a specified organization, apps can already
specify this by providing [an organization scope](https://zitadel.com/docs/apis/openidoauth/scopes#reserved-scopes).
:::
## Statement
This change was tracked in the following PR:
[feat(login): use default org for login without provided org context](https://github.com/zitadel/zitadel/pull/6625), which was released in Version [2.38.0](https://github.com/zitadel/zitadel/releases/tag/v2.38.0)
## Mitigation
There's no action needed on your side currently as existing instances are not affected directly and IAM_OWNER can activate the flag at their own pace.
## Impact
Once this update has been released and deployed, newly created instances will always use the default organization and its settings as default context for the login.
Already existing instances will still use the instance settings by default and can switch to the new default by ["Activating the 'LoginDefaultOrg' feature"](https://zitadel.com/docs/apis/resources/admin/admin-service-activate-feature-login-default-org) through the Admin API.
**This change is irreversible!**
:::note
Regardless of the change:
If a known username is entered on the first screen, the login switches its context to the organization of that user and settings will be updated to that organization as well.
:::

View File

@@ -0,0 +1,37 @@
---
title: Technical Advisory 10004
---
## Date and Version
Version: 2.39.0
Date: 2023-10-14
## Description
Due to storage optimisations ZITADEL changes the behaviour of sequences.
This change improves command (create, update, delete) performance of ZITADEL.
Sequences are no longer unique inside an instance.
From now on sequences are upcounting per aggregate id.
For example sequences of newly created users begin at 1.
Existing sequences remain untouched.
## Statement
This change is tracked in the following PR: [new eventstore framework](https://github.com/zitadel/zitadel/issues/5358).
As soon as the release version is published, we will include the version here.
## Mitigation
If you use the ListEvents API to scrape events use the creation date instead of the sequence.
If you use sequences on a list of objects it's no longer garanteed to have unique sequences across the list.
Therefore it's recommended to use the change data of the objects instead.
## Impact
Once this update has been released and deployed, sequences are no longer unique inside an instance.
ZITADEL will increase parallel write capabilities, because there is no global sequence to track anymore.
Editor service does not respond the different services of ZITADEL anymore, it returns zitadel.
As we are switching to resource based API's there is no need for this field anymore.

View File

@@ -0,0 +1,33 @@
---
title: Technical Advisory 10005
---
## Date and Version
Version: 2.39.0
Date: Calendar week 41/42 2023
## Description
Migrating to version >= 2.39 from < 2.39 will cause down time during setup starts and the new version is started.
This is caused by storage optimisations which replace the `eventstore.events` database table with the new `eventstore.events2` table.
All existing events are migrated during the execution of the `zitadel setup` command.
New events will be inserted into the new `eventstore.events2` table. The old table `evetstore.events` is renamed to `eventstore.events_old` and will be dropped in a future release of ZITADEL.
## Statement
This change is tracked in the following PR: [new eventstore framework](https://github.com/zitadel/zitadel/issues/5358).
As soon as the release version is published, we will include the version here.
## Mitigation
If you use this table for TRIGGERS or Change data capture please check the new table definition and change your code to use `eventstore.events2`.
## Impact
Once the setup step renamed the table. Old versions of ZITADEL are not able to read/write events.
:::note
If the upgrade fails make sure to rename `eventstore.events_old` to `eventstore.events`. This change enables older ZITADEL versions to work properly.
:::

View File

@@ -68,7 +68,55 @@ We understand that these advisories may include breaking changes, and we aim to
ZITADEL hosted Login-UI is not affected by this change. ZITADEL hosted Login-UI is not affected by this change.
</td> </td>
<td>TBD</td> <td>TBD</td>
<td>Calendar week 40/41</td> <td>Calendar week 44</td>
</tr>
<tr>
<td>
<a href="./advisory/a10003">A-10003</a>
</td>
<td>Login-UI - Default Context</td>
<td>Breaking Behaviour Change</td>
<td>
When users are redirected to the ZITADEL Login-UI without any organizational context,
they're currently presented a login screen, based on the instance settings,
e.g. available IDPs and possible login mechanisms. If the user will then register himself,
by the registration form or through an IDP, the user will always be created on the default organization.
With the introduced change, the settings will no longer be loaded from the instance, but rather the default organization directly.
</td>
<td>2.38.0</td>
<td>Calendar week 41</td>
</tr>
<tr>
<td>
<a href="./advisory/a10004">A-10004</a>
</td>
<td>Sequence uniquenes</td>
<td>Breaking Behaviour Change</td>
<td>
Due to storage optimisations ZITADEL changes the behaviour of sequences.
This change improves command (create, update, delete) performance of ZITADEL.
Sequences are no longer unique inside an instance.
From now on sequences are upcounting per aggregate id.
For example sequences of newly created users begin at 1.
Existing sequences remain untouched.
</td>
<td>2.39.0</td>
<td>2023-10-14</td>
</tr>
<tr>
<td>
<a href="./advisory/a10005">A-10005</a>
</td>
<td>Expected downtime during upgrade</td>
<td>Expected downtime during upgrade</td>
<td>
Migrating to versions &gt;= 2.39 from &lt; 2.39 will cause down time during setup starts and the new version is started.
This is caused by storage optimisations which replace the `eventstore.events` database table with the new `eventstore.events2` table.
All existing events are migrated during the execution of the `zitadel setup` command.
New events will be inserted into the new `eventstore.events2` table. The old table `evetstore.events` is renamed to `eventstore.events_old` and will be dropped in a future release of ZITADEL.
</td>
<td>2.39.0</td>
<td>Calendar week 41/42 2023</td>
</tr> </tr>
</table> </table>

View File

@@ -244,6 +244,7 @@ module.exports = {
"guides/integrate/identity-providers/openldap", "guides/integrate/identity-providers/openldap",
"guides/integrate/identity-providers/migrate", "guides/integrate/identity-providers/migrate",
"guides/integrate/identity-providers/okta", "guides/integrate/identity-providers/okta",
"guides/integrate/identity-providers/keycloak",
], ],
}, },
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@@ -66,7 +66,11 @@ func (s *Server) ListOrgs(ctx context.Context, req *admin_pb.ListOrgsRequest) (*
} }
func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (*admin_pb.SetUpOrgResponse, error) { func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (*admin_pb.SetUpOrgResponse, error) {
userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, domain.NewIAMDomainName(req.Org.Name, authz.GetInstance(ctx).RequestedDomain())) orgDomain, err := domain.NewIAMDomainName(req.Org.Name, authz.GetInstance(ctx).RequestedDomain())
if err != nil {
return nil, err
}
userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, orgDomain)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -72,7 +72,11 @@ func (s *Server) ListOrgChanges(ctx context.Context, req *mgmt_pb.ListOrgChanges
} }
func (s *Server) AddOrg(ctx context.Context, req *mgmt_pb.AddOrgRequest) (*mgmt_pb.AddOrgResponse, error) { func (s *Server) AddOrg(ctx context.Context, req *mgmt_pb.AddOrgRequest) (*mgmt_pb.AddOrgResponse, error) {
userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, domain.NewIAMDomainName(req.Name, authz.GetInstance(ctx).RequestedDomain()), "") orgDomain, err := domain.NewIAMDomainName(req.Name, authz.GetInstance(ctx).RequestedDomain())
if err != nil {
return nil, err
}
userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, orgDomain, "")
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -79,7 +79,8 @@ func createInstancePbToAddHuman(req *system_pb.CreateInstanceRequest_Human, defa
// check if default username is email style or else append @<orgname>.<custom-domain> // check if default username is email style or else append @<orgname>.<custom-domain>
// this way we have the same value as before changing `UserLoginMustBeDomain` to false // this way we have the same value as before changing `UserLoginMustBeDomain` to false
if !userLoginMustBeDomain && !strings.Contains(user.Username, "@") { if !userLoginMustBeDomain && !strings.Contains(user.Username, "@") {
user.Username = user.Username + "@" + domain.NewIAMDomainName(org, externalDomain) orgDomain, _ := domain.NewIAMDomainName(org, externalDomain)
user.Username = user.Username + "@" + orgDomain
} }
if req.UserName != "" { if req.UserName != "" {
user.Username = req.UserName user.Username = req.UserName
@@ -185,7 +186,8 @@ func AddInstancePbToSetupInstance(req *system_pb.AddInstanceRequest, defaultInst
// check if default username is email style or else append @<orgname>.<custom-domain> // check if default username is email style or else append @<orgname>.<custom-domain>
// this way we have the same value as before changing `UserLoginMustBeDomain` to false // this way we have the same value as before changing `UserLoginMustBeDomain` to false
if !instance.DomainPolicy.UserLoginMustBeDomain && !strings.Contains(instance.Org.Human.Username, "@") { if !instance.DomainPolicy.UserLoginMustBeDomain && !strings.Contains(instance.Org.Human.Username, "@") {
instance.Org.Human.Username = instance.Org.Human.Username + "@" + domain.NewIAMDomainName(instance.Org.Name, externalDomain) orgDomain, _ := domain.NewIAMDomainName(instance.Org.Name, externalDomain)
instance.Org.Human.Username = instance.Org.Human.Username + "@" + orgDomain
} }
if req.OwnerPassword != nil { if req.OwnerPassword != nil {
instance.Org.Human.Password = req.OwnerPassword.Password instance.Org.Human.Password = req.OwnerPassword.Password

View File

@@ -9,7 +9,7 @@ import (
) )
func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) { func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) {
details, err := s.command.AddHumanOTPSMS(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner) details, err := s.command.AddHumanOTPSMS(ctx, req.GetUserId(), authz.GetCtxData(ctx).OrgID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -18,7 +18,7 @@ func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*us
} }
func (s *Server) RemoveOTPSMS(ctx context.Context, req *user.RemoveOTPSMSRequest) (*user.RemoveOTPSMSResponse, error) { func (s *Server) RemoveOTPSMS(ctx context.Context, req *user.RemoveOTPSMSRequest) (*user.RemoveOTPSMSResponse, error) {
objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner) objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.GetUserId(), authz.GetCtxData(ctx).OrgID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -26,7 +26,7 @@ func (s *Server) RemoveOTPSMS(ctx context.Context, req *user.RemoveOTPSMSRequest
} }
func (s *Server) AddOTPEmail(ctx context.Context, req *user.AddOTPEmailRequest) (*user.AddOTPEmailResponse, error) { func (s *Server) AddOTPEmail(ctx context.Context, req *user.AddOTPEmailRequest) (*user.AddOTPEmailResponse, error) {
details, err := s.command.AddHumanOTPEmail(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner) details, err := s.command.AddHumanOTPEmail(ctx, req.GetUserId(), authz.GetCtxData(ctx).OrgID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -35,7 +35,7 @@ func (s *Server) AddOTPEmail(ctx context.Context, req *user.AddOTPEmailRequest)
} }
func (s *Server) RemoveOTPEmail(ctx context.Context, req *user.RemoveOTPEmailRequest) (*user.RemoveOTPEmailResponse, error) { func (s *Server) RemoveOTPEmail(ctx context.Context, req *user.RemoveOTPEmailRequest) (*user.RemoveOTPEmailResponse, error) {
objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner) objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.GetUserId(), authz.GetCtxData(ctx).OrgID)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -15,7 +15,7 @@ import (
func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) { func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) {
var ( var (
resourceOwner = authz.GetCtxData(ctx).ResourceOwner resourceOwner = authz.GetCtxData(ctx).OrgID
authenticator = passkeyAuthenticatorToDomain(req.GetAuthenticator()) authenticator = passkeyAuthenticatorToDomain(req.GetAuthenticator())
) )
if code := req.GetCode(); code != nil { if code := req.GetCode(); code != nil {
@@ -65,7 +65,7 @@ func passkeyRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails,
} }
func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) { func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) {
resourceOwner := authz.GetCtxData(ctx).ResourceOwner resourceOwner := authz.GetCtxData(ctx).OrgID
pkc, err := req.GetPublicKeyCredential().MarshalJSON() pkc, err := req.GetPublicKeyCredential().MarshalJSON()
if err != nil { if err != nil {
return nil, caos_errs.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal") return nil, caos_errs.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal")
@@ -80,7 +80,7 @@ func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.Verify
} }
func (s *Server) CreatePasskeyRegistrationLink(ctx context.Context, req *user.CreatePasskeyRegistrationLinkRequest) (resp *user.CreatePasskeyRegistrationLinkResponse, err error) { func (s *Server) CreatePasskeyRegistrationLink(ctx context.Context, req *user.CreatePasskeyRegistrationLinkRequest) (resp *user.CreatePasskeyRegistrationLinkResponse, err error) {
resourceOwner := authz.GetCtxData(ctx).ResourceOwner resourceOwner := authz.GetCtxData(ctx).OrgID
switch medium := req.Medium.(type) { switch medium := req.Medium.(type) {
case nil: case nil:

View File

@@ -48,7 +48,7 @@ func notificationTypeToDomain(notificationType user.NotificationType) domain.Not
} }
func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest) (_ *user.SetPasswordResponse, err error) { func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest) (_ *user.SetPasswordResponse, err error) {
var resourceOwner = authz.GetCtxData(ctx).ResourceOwner var resourceOwner = authz.GetCtxData(ctx).OrgID
var details *domain.ObjectDetails var details *domain.ObjectDetails
switch v := req.GetVerification().(type) { switch v := req.GetVerification().(type) {

View File

@@ -11,7 +11,7 @@ import (
func (s *Server) RegisterTOTP(ctx context.Context, req *user.RegisterTOTPRequest) (*user.RegisterTOTPResponse, error) { func (s *Server) RegisterTOTP(ctx context.Context, req *user.RegisterTOTPRequest) (*user.RegisterTOTPResponse, error) {
return totpDetailsToPb( return totpDetailsToPb(
s.command.AddUserTOTP(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner), s.command.AddUserTOTP(ctx, req.GetUserId(), authz.GetCtxData(ctx).OrgID),
) )
} }
@@ -28,7 +28,7 @@ func totpDetailsToPb(totp *domain.TOTP, err error) (*user.RegisterTOTPResponse,
} }
func (s *Server) VerifyTOTPRegistration(ctx context.Context, req *user.VerifyTOTPRegistrationRequest) (*user.VerifyTOTPRegistrationResponse, error) { func (s *Server) VerifyTOTPRegistration(ctx context.Context, req *user.VerifyTOTPRegistrationRequest) (*user.VerifyTOTPRegistrationResponse, error) {
objectDetails, err := s.command.CheckUserTOTP(ctx, req.GetUserId(), req.GetCode(), authz.GetCtxData(ctx).ResourceOwner) objectDetails, err := s.command.CheckUserTOTP(ctx, req.GetUserId(), req.GetCode(), authz.GetCtxData(ctx).OrgID)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -12,7 +12,7 @@ import (
func (s *Server) RegisterU2F(ctx context.Context, req *user.RegisterU2FRequest) (*user.RegisterU2FResponse, error) { func (s *Server) RegisterU2F(ctx context.Context, req *user.RegisterU2FRequest) (*user.RegisterU2FResponse, error) {
return u2fRegistrationDetailsToPb( return u2fRegistrationDetailsToPb(
s.command.RegisterUserU2F(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner, req.GetDomain()), s.command.RegisterUserU2F(ctx, req.GetUserId(), authz.GetCtxData(ctx).OrgID, req.GetDomain()),
) )
} }
@@ -29,7 +29,7 @@ func u2fRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err
} }
func (s *Server) VerifyU2FRegistration(ctx context.Context, req *user.VerifyU2FRegistrationRequest) (*user.VerifyU2FRegistrationResponse, error) { func (s *Server) VerifyU2FRegistration(ctx context.Context, req *user.VerifyU2FRegistrationRequest) (*user.VerifyU2FRegistrationResponse, error) {
resourceOwner := authz.GetCtxData(ctx).ResourceOwner resourceOwner := authz.GetCtxData(ctx).OrgID
pkc, err := req.GetPublicKeyCredential().MarshalJSON() pkc, err := req.GetPublicKeyCredential().MarshalJSON()
if err != nil { if err != nil {
return nil, caos_errs.ThrowInternal(err, "USERv2-IeTh4", "Errors.Internal") return nil, caos_errs.ThrowInternal(err, "USERv2-IeTh4", "Errors.Internal")

View File

@@ -161,7 +161,11 @@ func (l *Login) Handler() http.Handler {
} }
func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string) ([]string, error) { func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string) ([]string, error) {
loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+domain.NewIAMDomainName(orgName, authz.GetInstance(ctx).RequestedDomain()), query.TextEndsWithIgnoreCase) orgDomain, err := domain.NewIAMDomainName(orgName, authz.GetInstance(ctx).RequestedDomain())
if err != nil {
return nil, err
}
loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -238,7 +238,10 @@ func AddOrgCommand(ctx context.Context, a *org.Aggregate, name string, userIDs .
if name = strings.TrimSpace(name); name == "" { if name = strings.TrimSpace(name); name == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument") return nil, errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument")
} }
defaultDomain := domain.NewIAMDomainName(name, authz.GetInstance(ctx).RequestedDomain()) defaultDomain, err := domain.NewIAMDomainName(name, authz.GetInstance(ctx).RequestedDomain())
if err != nil {
return nil, err
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{ return []eventstore.Command{
org.NewOrgAddedEvent(ctx, &a.Aggregate, name), org.NewOrgAddedEvent(ctx, &a.Aggregate, name),

View File

@@ -309,13 +309,16 @@ func (c *Commands) changeDefaultDomain(ctx context.Context, orgID, newName strin
return nil, err return nil, err
} }
iamDomain := authz.GetInstance(ctx).RequestedDomain() iamDomain := authz.GetInstance(ctx).RequestedDomain()
defaultDomain := domain.NewIAMDomainName(orgDomains.OrgName, iamDomain) defaultDomain, _ := domain.NewIAMDomainName(orgDomains.OrgName, iamDomain)
isPrimary := defaultDomain == orgDomains.PrimaryDomain isPrimary := defaultDomain == orgDomains.PrimaryDomain
orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel) orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel)
for _, orgDomain := range orgDomains.Domains { for _, orgDomain := range orgDomains.Domains {
if orgDomain.State == domain.OrgDomainStateActive { if orgDomain.State == domain.OrgDomainStateActive {
if orgDomain.Domain == defaultDomain { if orgDomain.Domain == defaultDomain {
newDefaultDomain := domain.NewIAMDomainName(newName, iamDomain) newDefaultDomain, err := domain.NewIAMDomainName(newName, iamDomain)
if err != nil {
return nil, err
}
events := []eventstore.Command{ events := []eventstore.Command{
org.NewDomainAddedEvent(ctx, orgAgg, newDefaultDomain), org.NewDomainAddedEvent(ctx, orgAgg, newDefaultDomain),
org.NewDomainVerifiedEvent(ctx, orgAgg, newDefaultDomain), org.NewDomainVerifiedEvent(ctx, orgAgg, newDefaultDomain),
@@ -338,7 +341,7 @@ func (c *Commands) removeCustomDomains(ctx context.Context, orgID string) ([]eve
return nil, err return nil, err
} }
hasDefault := false hasDefault := false
defaultDomain := domain.NewIAMDomainName(orgDomains.OrgName, authz.GetInstance(ctx).RequestedDomain()) defaultDomain, _ := domain.NewIAMDomainName(orgDomains.OrgName, authz.GetInstance(ctx).RequestedDomain())
isPrimary := defaultDomain == orgDomains.PrimaryDomain isPrimary := defaultDomain == orgDomains.PrimaryDomain
orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel) orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel)
events := make([]eventstore.Command, 0, len(orgDomains.Domains)) events := make([]eventstore.Command, 0, len(orgDomains.Domains))

View File

@@ -25,7 +25,8 @@ func (o *Org) IsValid() bool {
} }
func (o *Org) AddIAMDomain(iamDomain string) { func (o *Org) AddIAMDomain(iamDomain string) {
o.Domains = append(o.Domains, &OrgDomain{Domain: NewIAMDomainName(o.Name, iamDomain), Verified: true, Primary: true}) orgDomain, _ := NewIAMDomainName(o.Name, iamDomain)
o.Domains = append(o.Domains, &OrgDomain{Domain: orgDomain, Verified: true, Primary: true})
} }
type OrgState int32 type OrgState int32

View File

@@ -6,6 +6,7 @@ import (
http_util "github.com/zitadel/zitadel/internal/api/http" http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/eventstore/v1/models"
) )
@@ -32,15 +33,18 @@ func (domain *OrgDomain) GenerateVerificationCode(codeGenerator crypto.Generator
return validationCode, nil return validationCode, nil
} }
func NewIAMDomainName(orgName, iamDomain string) string { func NewIAMDomainName(orgName, iamDomain string) (string, error) {
// Reference: label domain requirements https://www.nic.ad.jp/timeline/en/20th/appendix1.html // Reference: label domain requirements https://www.nic.ad.jp/timeline/en/20th/appendix1.html
// Replaces spaces in org name with hyphens // Replaces spaces in org name with hyphens
label := strings.ReplaceAll(orgName, " ", "-") label := strings.ReplaceAll(orgName, " ", "-")
// The label must only contains alphanumeric characters and hyphens // The label must only contains alphanumeric characters and hyphens
// Invalid characters are replaced with and empty space // Invalid characters are replaced with and empty space but as #6471,
label = string(regexp.MustCompile(`[^a-zA-Z0-9-]`).ReplaceAll([]byte(label), []byte(""))) // as these domains are not used to host ZITADEL, but only for user names,
// the characters shouldn't matter that much so we'll accept unicode
// characters, accented characters (\p{L}\p{M}), numbers and hyphens.
label = string(regexp.MustCompile(`[^\p{L}\p{M}0-9-]`).ReplaceAll([]byte(label), []byte("")))
// The label cannot exceed 63 characters // The label cannot exceed 63 characters
if len(label) > 63 { if len(label) > 63 {
@@ -64,7 +68,12 @@ func NewIAMDomainName(orgName, iamDomain string) string {
label = label[:len(label)-1] label = label[:len(label)-1]
} }
return strings.ToLower(label + "." + iamDomain) // Empty string should be invalid
if len(label) > 0 {
return strings.ToLower(label + "." + iamDomain), nil
}
return "", errors.ThrowInvalidArgument(nil, "ORG-RrfXY", "Errors.Org.Domain.EmptyString")
} }
type OrgDomainValidationType int32 type OrgDomainValidationType int32

View File

@@ -41,10 +41,10 @@ func TestNewIAMDomainName(t *testing.T) {
{ {
name: "replace invalid characters [^a-zA-Z0-9-] with empty spaces", name: "replace invalid characters [^a-zA-Z0-9-] with empty spaces",
args: args{ args: args{
orgName: "mí Örg name?", orgName: "mí >**name?",
iamDomain: "localhost", iamDomain: "localhost",
}, },
result: "m-rg-name.localhost", result: "mí-name.localhost",
}, },
{ {
name: "label created from org name size is not greater than 63 chars", name: "label created from org name size is not greater than 63 chars",
@@ -78,10 +78,18 @@ func TestNewIAMDomainName(t *testing.T) {
}, },
result: "my-super-long-organization-name-with-many-many-many-characters.localhost", result: "my-super-long-organization-name-with-many-many-many-characters.localhost",
}, },
{
name: "string full with invalid characters returns empty",
args: args{
orgName: "*¿=@^[])",
iamDomain: "localhost",
},
result: "",
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
domain := NewIAMDomainName(tt.args.orgName, tt.args.iamDomain) domain, _ := NewIAMDomainName(tt.args.orgName, tt.args.iamDomain)
if tt.result != domain { if tt.result != domain {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, domain) t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, domain)
} }

View File

@@ -46,5 +46,6 @@ func (o *Org) GetPrimaryDomain() *OrgDomain {
} }
func (o *Org) AddIAMDomain(iamDomain string) { func (o *Org) AddIAMDomain(iamDomain string) {
o.Domains = append(o.Domains, &OrgDomain{Domain: domain.NewIAMDomainName(o.Name, iamDomain), Verified: true, Primary: true}) orgDomain, _ := domain.NewIAMDomainName(o.Name, iamDomain)
o.Domains = append(o.Domains, &OrgDomain{Domain: orgDomain, Verified: true, Primary: true})
} }

View File

@@ -204,6 +204,7 @@ Errors:
Domain: Domain:
AlreadyExists: Домейнът вече съществува AlreadyExists: Домейнът вече съществува
InvalidCharacter: "Само буквено-цифрови знаци, . " InvalidCharacter: "Само буквено-цифрови знаци, . "
EmptyString: Невалидни нецифрови и азбучни знаци бяха заменени с празни интервали и полученият домейн е празен низ
IDP: IDP:
InvalidSearchQuery: Невалидна заявка за търсене InvalidSearchQuery: Невалидна заявка за търсене
ClientIDMissing: Липсва ClientID ClientIDMissing: Липсва ClientID

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: Domäne existiert bereits AlreadyExists: Domäne existiert bereits
InvalidCharacter: Nur alphanumerische Zeichen, . und - sind für eine Domäne erlaubt InvalidCharacter: Nur alphanumerische Zeichen, . und - sind für eine Domäne erlaubt
EmptyString: Ungültige nicht numerische und alphabetische Zeichen wurden durch Leerzeichen ersetzt und die resultierende Domäne ist eine leere Zeichenfolge
IDP: IDP:
InvalidSearchQuery: Ungültiger Suchparameter InvalidSearchQuery: Ungültiger Suchparameter
ClientIDMissing: ClientID fehlt ClientIDMissing: ClientID fehlt

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: Domain already exists AlreadyExists: Domain already exists
InvalidCharacter: Only alphanumeric characters, . and - are allowed for a domain InvalidCharacter: Only alphanumeric characters, . and - are allowed for a domain
EmptyString: Invalid non numeric and alphabetical characters were replaced with empty spaces and resulting domain is an empty string
IDP: IDP:
InvalidSearchQuery: Invalid search query InvalidSearchQuery: Invalid search query
ClientIDMissing: ClientID missing ClientIDMissing: ClientID missing

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: El dominio ya existe AlreadyExists: El dominio ya existe
InvalidCharacter: Solo caracteres alfanuméricos, . y - se permiten para un dominio InvalidCharacter: Solo caracteres alfanuméricos, . y - se permiten para un dominio
EmptyString: Los caracteres alfabéticos y no numéricos no válidos se reemplazaron con espacios vacíos y el dominio resultante es una cadena vacía
IDP: IDP:
InvalidSearchQuery: Consulta de búsqueda no válida InvalidSearchQuery: Consulta de búsqueda no válida
ClientIDMissing: Falta ClientID ClientIDMissing: Falta ClientID

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: Le domaine existe déjà AlreadyExists: Le domaine existe déjà
InvalidCharacter: Seuls les caractères alphanumériques, . et - sont autorisés pour un domaine InvalidCharacter: Seuls les caractères alphanumériques, . et - sont autorisés pour un domaine
EmptyString: Les caractères non numériques et alphabétiques non valides ont été remplacés par des espaces vides et le domaine résultant est une chaîne vide
IDP: IDP:
InvalidSearchQuery: Paramètre de recherche non valide InvalidSearchQuery: Paramètre de recherche non valide
ClientIDMissing: ID client manquant ClientIDMissing: ID client manquant

View File

@@ -201,6 +201,8 @@ Errors:
IdpIsNotOIDC: La configurazione IDP non è di tipo oidc IdpIsNotOIDC: La configurazione IDP non è di tipo oidc
Domain: Domain:
AlreadyExists: Il dominio già esistente AlreadyExists: Il dominio già esistente
InvalidCharacter: Solo caratteri alfanumerici, . e - sono consentiti per un dominio
EmptyString: I caratteri non numerici e alfabetici non validi sono stati sostituiti con spazi vuoti e il dominio risultante è una stringa vuota
IDP: IDP:
InvalidSearchQuery: Parametro di ricerca non valido InvalidSearchQuery: Parametro di ricerca non valido
InvalidCharacter: Per un dominio sono ammessi solo caratteri alfanumerici, . e - InvalidCharacter: Per un dominio sono ammessi solo caratteri alfanumerici, . e -

View File

@@ -194,6 +194,7 @@ Errors:
Domain: Domain:
AlreadyExists: ドメインはすでに存在します AlreadyExists: ドメインはすでに存在します
InvalidCharacter: ドメインは英数字、'.'、'-'のみ使用可能です。 InvalidCharacter: ドメインは英数字、'.'、'-'のみ使用可能です。
EmptyString: 無効な数字およびアルファベット以外の文字は空のスペースに置き換えられ、結果のドメインは空の文字列になります
IDP: IDP:
InvalidSearchQuery: 無効な検索クエリです InvalidSearchQuery: 無効な検索クエリです
ClientIDMissing: クライアントIDがありません ClientIDMissing: クライアントIDがありません

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: Доменот веќе постои AlreadyExists: Доменот веќе постои
InvalidCharacter: Дозволени се само алфанумерички знаци, . и - се дозволени за домен InvalidCharacter: Дозволени се само алфанумерички знаци, . и - се дозволени за домен
EmptyString: Неважечките ненумерички и азбучни знаци се заменети со празни места и добиениот домен е празна низа
IDP: IDP:
InvalidSearchQuery: Невалидно пребарување InvalidSearchQuery: Невалидно пребарување
ClientID Missing: ClientID недостасува ClientID Missing: ClientID недостасува

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: Domena już istnieje AlreadyExists: Domena już istnieje
InvalidCharacter: Tylko znaki alfanumeryczne, . i - są dozwolone dla domeny InvalidCharacter: Tylko znaki alfanumeryczne, . i - są dozwolone dla domeny
EmptyString: Nieprawidłowe znaki inne niż numeryczne i alfabetyczne zostały zastąpione pustymi spacjami, a wynikowa domena jest pustym ciągiem znaków
IDP: IDP:
InvalidSearchQuery: Nieprawidłowe zapytanie wyszukiwania InvalidSearchQuery: Nieprawidłowe zapytanie wyszukiwania
ClientIDMissing: Brak ClientID ClientIDMissing: Brak ClientID

View File

@@ -200,6 +200,7 @@ Errors:
Domain: Domain:
AlreadyExists: Domínio já existe AlreadyExists: Domínio já existe
InvalidCharacter: Apenas caracteres alfanuméricos, . e - são permitidos para um domínio InvalidCharacter: Apenas caracteres alfanuméricos, . e - são permitidos para um domínio
EmptyString: Caracteres não numéricos e alfabéticos inválidos foram substituídos por espaços vazios e o domínio resultante é uma string vazia
IDP: IDP:
InvalidSearchQuery: Consulta de pesquisa inválida InvalidSearchQuery: Consulta de pesquisa inválida
ClientIDMissing: ClientID ausente ClientIDMissing: ClientID ausente

View File

@@ -202,6 +202,7 @@ Errors:
Domain: Domain:
AlreadyExists: 域名已存在 AlreadyExists: 域名已存在
InvalidCharacter: 只有字母数字字符,.和 - 允许用于域名中 InvalidCharacter: 只有字母数字字符,.和 - 允许用于域名中
EmptyString: 无效的非数字和字母字符被替换为空格,结果域是空字符串
IDP: IDP:
InvalidSearchQuery: 无效的搜索查询 InvalidSearchQuery: 无效的搜索查询
ClientIDMissing: 客户端 ID 丢失 ClientIDMissing: 客户端 ID 丢失