diff --git a/cmd/setup/03.go b/cmd/setup/03.go index 4de513be40..4a93888f6b 100644 --- a/cmd/setup/03.go +++ b/cmd/setup/03.go @@ -105,13 +105,21 @@ func (mig *FirstInstance) Execute(ctx context.Context) error { // check if username is email style or else append @. //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, "@") { - 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() if mig.instanceSetup.Org.Human.Email.Address == "" { mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username) 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) } } diff --git a/console/src/app/app.component.html b/console/src/app/app.component.html index 18c5c72501..5b31b33dc4 100644 --- a/console/src/app/app.component.html +++ b/console/src/app/app.component.html @@ -1,5 +1,5 @@
- + 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.language = language.lang; }); @@ -271,7 +273,7 @@ export class AppComponent implements OnDestroy { this.translate.addLangs(supportedLanguages); this.translate.setDefaultLang(fallbackLanguage); - this.authService.user.subscribe((userprofile) => { + this.authService.userSubject.pipe(takeUntil(this.destroy$)).subscribe((userprofile) => { if (userprofile) { const cropped = navigator.language.split('-')[0] ?? fallbackLanguage; const fallbackLang = cropped.match(supportedLanguagesRegexp) ? cropped : fallbackLanguage; diff --git a/console/src/app/guards/user.guard.ts b/console/src/app/guards/user.guard.ts index c57b896ece..b4527753fe 100644 --- a/console/src/app/guards/user.guard.ts +++ b/console/src/app/guards/user.guard.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { Observable } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; +import { map, Observable, take } from 'rxjs'; import { GrpcAuthService } from '../services/grpc-auth.service'; @@ -19,11 +18,13 @@ export class UserGuard { state: RouterStateSnapshot, ): Observable | Promise | boolean { return this.authService.user.pipe( - map((user) => user?.id !== route.params['id']), - tap((isNotMe) => { - if (!isNotMe) { + take(1), + map((user) => { + const isMe = user?.id === route.params['id']; + if (isMe) { this.router.navigate(['/users', 'me']); } + return !isMe; }), ); } diff --git a/console/src/app/pages/projects/apps/app-create/app-create.component.html b/console/src/app/pages/projects/apps/app-create/app-create.component.html index a487adcc1e..1603c8f271 100644 --- a/console/src/app/pages/projects/apps/app-create/app-create.component.html +++ b/console/src/app/pages/projects/apps/app-create/app-create.component.html @@ -2,7 +2,7 @@ title="{{ 'APP.PAGES.CREATE' | translate }}" class="app-create-wrapper" [createSteps]=" - !devmode + !pro ? appType?.value?.createType === AppCreateType.OIDC ? appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE ? 4 @@ -20,13 +20,13 @@

{{ 'APP.PAGES.CREATE_DESC_TITLE' | translate }}

- + {{ 'APP.PROSWITCH' | translate }} + + {{ 'APP.OIDC.DEVMODE' | translate }} + + @@ -144,6 +154,7 @@ [getValues]="requestRedirectValuesSubject$" title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}" [isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE" + [devMode]="devMode" data-e2e="postlogout-uris" > @@ -299,6 +310,19 @@
+ +
+ + {{ 'APP.OIDC.DEVMODE' | translate }} + + + + {{ devMode ? ('APP.OIDC.DEVMODE_ENABLED' | translate) : ('APP.OIDC.DEVMODE_DISABLED' | translate) }} + + +
+
+
@@ -342,7 +366,7 @@ -
+
@@ -439,6 +463,14 @@ " >
+ + {{ 'APP.OIDC.DEVMODE' | translate }} + = new Subject(); - public devmode: boolean = false; + public pro: boolean = false; public projectId: string = ''; public loading: boolean = false; @@ -239,7 +240,16 @@ export class AppCreateComponent implements OnInit, OnDestroy { this.oidcAppRequest.setPostLogoutRedirectUrisList(value); } + public get devMode() { + return this.oidcAppRequest.toObject().devMode; + } + + public set devMode(value: boolean) { + this.oidcAppRequest.setDevMode(value); + } + public ngOnInit(): void { + this.devMode = false; this.subscription = this.route.params.subscribe((params) => this.getData(params)); const projectId = this.route.snapshot.paramMap.get('projectid'); @@ -362,9 +372,9 @@ export class AppCreateComponent implements OnInit, OnDestroy { } public createApp(): void { - const appOIDCCheck = this.devmode ? this.isDevOIDC : this.isStepperOIDC; - const appAPICheck = this.devmode ? this.isDevAPI : this.isStepperAPI; - const appSAMLCheck = this.devmode ? this.isDevSAML : this.isStepperSAML; + const appOIDCCheck = this.pro ? this.isDevOIDC : this.isStepperOIDC; + const appAPICheck = this.pro ? this.isDevAPI : this.isStepperAPI; + const appSAMLCheck = this.pro ? this.isDevSAML : this.isStepperSAML; if (appOIDCCheck) { this.requestRedirectValuesSubject$.next(); diff --git a/console/src/app/pages/projects/apps/app-detail/app-detail.component.html b/console/src/app/pages/projects/apps/app-detail/app-detail.component.html index 486090f6e1..18eff90897 100644 --- a/console/src/app/pages/projects/apps/app-detail/app-detail.component.html +++ b/console/src/app/pages/projects/apps/app-detail/app-detail.component.html @@ -427,7 +427,7 @@
- +

{{ wellKnownV.key }}

diff --git a/console/src/app/services/authentication.service.ts b/console/src/app/services/authentication.service.ts index c53047c047..314b9edb7e 100644 --- a/console/src/app/services/authentication.service.ts +++ b/console/src/app/services/authentication.service.ts @@ -3,6 +3,7 @@ import { AuthConfig, OAuthService } from 'angular-oauth2-oidc'; import { BehaviorSubject, from, lastValueFrom, Observable } from 'rxjs'; import { StatehandlerService } from './statehandler/statehandler.service'; +import { ToastService } from './toast.service'; @Injectable({ providedIn: 'root', @@ -15,6 +16,7 @@ export class AuthenticationService { constructor( private oauthService: OAuthService, private statehandler: StatehandlerService, + private toast: ToastService, ) {} public initConfig(data: AuthConfig): void { @@ -39,7 +41,10 @@ export class AuthenticationService { } this.oauthService.configure(this.authConfig); 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(); if (!this.oauthService.hasValidIdToken() || !this.authenticated || partialConfig || force) { const newState = await lastValueFrom(this.statehandler.createState()); diff --git a/console/src/app/services/grpc-auth.service.ts b/console/src/app/services/grpc-auth.service.ts index 18fc7f928d..dbb2d6dd5f 100644 --- a/console/src/app/services/grpc-auth.service.ts +++ b/console/src/app/services/grpc-auth.service.ts @@ -1,19 +1,8 @@ import { Injectable } from '@angular/core'; import { SortDirection } from '@angular/material/sort'; import { OAuthService } from 'angular-oauth2-oidc'; -import { BehaviorSubject, from, merge, Observable, of, Subject } from 'rxjs'; -import { - catchError, - distinctUntilChanged, - filter, - finalize, - map, - mergeMap, - switchMap, - take, - timeout, - withLatestFrom, -} from 'rxjs/operators'; +import { BehaviorSubject, forkJoin, from, Observable, of, Subject } from 'rxjs'; +import { catchError, distinctUntilChanged, filter, finalize, map, switchMap, timeout, withLatestFrom } from 'rxjs/operators'; import { AddMyAuthFactorOTPEmailRequest, @@ -184,25 +173,21 @@ export class GrpcAuthService { }, }); - this.user = merge( - of(this.oauthService.getAccessToken()).pipe(filter((token) => (token ? true : false))), + this.user = forkJoin([ + of(this.oauthService.getAccessToken()), this.oauthService.events.pipe( filter((e) => e.type === 'token_received'), timeout(this.oauthService.waitForTokenInMsec || 0), catchError((_) => of(null)), // timeout is not an error map((_) => this.oauthService.getAccessToken()), ), - ).pipe( - take(1), - mergeMap(() => { + ]).pipe( + filter((token) => (token ? true : false)), + distinctUntilChanged(), + switchMap(() => { return from( this.getMyUser().then((resp) => { - const user = resp.user; - if (user) { - return user; - } else { - return undefined; - } + return resp.user; }), ); }), diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 4137e7538e..2bb72ca7d6 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -1977,6 +1977,8 @@ "CLIENTSECRET_DESCRIPTION": "Пазете клиентската си тайна на сигурно място, тъй като тя ще изчезне, след като диалоговият прозорец бъде затворен.", "REGENERATESECRET": "Повторно генериране на клиентска тайна", "DEVMODE": "Режим на разработка", + "DEVMODE_ENABLED": "Активиран", + "DEVMODE_DISABLED": "Деактивиран", "DEVMODEDESC": "Внимание: При активиран режим на разработка URI адресите за пренасочване няма да бъдат валидирани.", "SKIPNATIVEAPPSUCCESSPAGE": "Пропуснете страницата за успешно влизане", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Пропуснете страницата за успех след влизане в това родно приложение.", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 37cbb671a5..9ae13c827c 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -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.", "REGENERATESECRET": "Client Secret neu generieren", "DEVMODE": "Entwicklermodus", + "DEVMODE_ENABLED": "Aktiviert", + "DEVMODE_DISABLED": "Deaktiviert", "DEVMODEDESC": "Bei eingeschaltetem Entwicklermodus werden die Weiterleitungs-URIs im OIDC-Flow nicht validiert.", "SKIPNATIVEAPPSUCCESSPAGE": "Login Erfolgseite überspringen", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Erfolgseite nach dem Login für diese Native Applikation überspringen.", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 135b62c82c..25ff13be26 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1995,6 +1995,8 @@ "CLIENTSECRET_DESCRIPTION": "Keep your client secret at a safe place as it will disappear once the dialog is closed.", "REGENERATESECRET": "Regenerate Client Secret", "DEVMODE": "Development Mode", + "DEVMODE_ENABLED": "Enabled", + "DEVMODE_DISABLED": "Disabled", "DEVMODEDESC": "Beware: With development mode enabled redirect URIs will not be validated.", "SKIPNATIVEAPPSUCCESSPAGE": "Skip Login Success Page", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Skip the success page after a login for this native app.", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index e2f8f054c5..34c0c6de46 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -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.", "REGENERATESECRET": "Regenerar secreto de cliente", "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.", "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.", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index ec44f4855a..373feb32a2 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -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.", "REGENERATESECRET": "Régénérer le secret du client", "DEVMODE": "Mode développement", + "DEVMODE_ENABLED": "Activé", + "DEVMODE_DISABLED": "Désactivé", "DEVMODEDESC": "Attention", "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", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index fa0f76745e..cae3b1cf13 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -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", "REGENERATESECRET": "Rigenera il Client Secret", "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.", "SKIPNATIVEAPPSUCCESSPAGE": "Salta la pagina di successo dopo il login", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Salta la pagina di successo dopo il login per questa applicazione nativa", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index d33e84ce30..651d00a263 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -1978,6 +1978,8 @@ "CLIENTSECRET_DESCRIPTION": "クライアントシークレットは、ダイアログを閉じると消えてしまうので、安全な場所に保管してください。", "REGENERATESECRET": "クライアントシークレットを再生成する", "DEVMODE": "開発モード", + "DEVMODE_ENABLED": "アクティブ化された", + "DEVMODE_DISABLED": "無効化されました", "DEVMODEDESC": "注意:開発モードを有効にすると、URIが認証されません。", "SKIPNATIVEAPPSUCCESSPAGE": "ログイン後に成功ページをスキップする", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "このネイティブアプリのログイン後に成功ページをスキップする", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 51ea7b4e20..cad724be75 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -1984,6 +1984,8 @@ "CLIENTSECRET_DESCRIPTION": "Чувајте ја вашата клиентска тајна на безбедно место, бидејќи ќе исчезне откако ќе се затвори дијалогот.", "REGENERATESECRET": "Генерирај нова клиентска тајна", "DEVMODE": "Development mode", + "DEVMODE_ENABLED": "Активиран", + "DEVMODE_DISABLED": "Деактивирано", "DEVMODEDESC": "Внимавајте: Со овозможен Development mode, URIs за пренасочување нема да бидат валидирани.", "SKIPNATIVEAPPSUCCESSPAGE": "Прескокни страница за успешна најава", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Прескокнете ја страницата за успешна најава за оваа нативна апликација.", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 5aca4919f3..a6ef90e0e8 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -1987,6 +1987,8 @@ "CLIENTSECRET_DESCRIPTION": "Trzymaj swój sekret klienta w bezpiecznym miejscu, ponieważ zniknie on po zamknięciu okna dialogowego.", "REGENERATESECRET": "Odtwórz sekret klienta", "DEVMODE": "Tryb rozwoju", + "DEVMODE_ENABLED": "Aktywowany", + "DEVMODE_DISABLED": "Dezaktywowane", "DEVMODEDESC": "Uwaga: przy włączonym trybie rozwoju adresy URI przekierowania nie będą sprawdzane.", "SKIPNATIVEAPPSUCCESSPAGE": "Pomiń stronę sukcesu po zalogowaniu", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Pomiń stronę sukcesu po zalogowaniu dla tej Natywny aplikację", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index b98860e7de..fb03b6c71a 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -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.", "REGENERATESECRET": "Regenerar Segredo do Cliente", "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.", "SKIPNATIVEAPPSUCCESSPAGE": "Pular Página de Sucesso de Login", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Pule a página de sucesso após o login para este aplicativo nativo.", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index ffd818cb3b..4192baed91 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1986,6 +1986,8 @@ "CLIENTSECRET_DESCRIPTION": "将您的客户保密在一个安全的地方,因为一旦对话框关闭,便无法再次查看。", "REGENERATESECRET": "重新生成客户端密钥", "DEVMODE": "开发模式", + "DEVMODE_ENABLED": "活性", + "DEVMODE_DISABLED": "已停用", "DEVMODEDESC": "注意:启用开发模式的重定向 URI 将不会被验证。", "SKIPNATIVEAPPSUCCESSPAGE": "登录后跳过成功页面", "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "登录后跳过本机应用的成功页面", diff --git a/docs/docs/guides/integrate/identity-providers/keycloak.mdx b/docs/docs/guides/integrate/identity-providers/keycloak.mdx new file mode 100644 index 0000000000..d6e2aa4e40 --- /dev/null +++ b/docs/docs/guides/integrate/identity-providers/keycloak.mdx @@ -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'; + + + +## 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 + + + +### Go to the IdP providers overview + + + +### Create a new generic OIDC provider + + + + + + +![Keycloak Provider](/img/guides/zitadel_keycloak_create_provider.png) + +### Activate IdP + + + +![Activate the Keycloak Provider](/img/guides/zitadel_activate_keycloak.png) + +## Test the setup + + + + +![Keycloak Button](/img/guides/zitadel_login_keycloak.png) + +![Keycloak Login](/img/guides/keycloak_login.png) diff --git a/docs/docs/guides/solution-scenarios/saas.md b/docs/docs/guides/solution-scenarios/saas.md index 6744074a7d..c864298d5b 100644 --- a/docs/docs/guides/solution-scenarios/saas.md +++ b/docs/docs/guides/solution-scenarios/saas.md @@ -3,7 +3,7 @@ title: Set up a SaaS Product with Authentication and Authorization using ZITADEL sidebar_label: Software-as-a-Service --- -This is an example architecture for a typical SaaS product. +This is an example architecture for a typical SaaS product. To illustrate it, a fictional organization and project is used. ## Example Case @@ -17,10 +17,10 @@ This means that the users and also their authorizations will be managed within Z ## Organization -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. 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 @@ -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). 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 @@ -69,4 +69,4 @@ The last possibility is to show the private labeling of the project organization For this the Allow User Resource Owner Setting should be set. :::note More about [Private Labeling](/guides/manage/customize/branding) -::: \ No newline at end of file +::: diff --git a/docs/docs/support/advisory/a10002.md b/docs/docs/support/advisory/a10002.md index 6d1e4dc4fc..7c63fb7dc9 100644 --- a/docs/docs/support/advisory/a10002.md +++ b/docs/docs/support/advisory/a10002.md @@ -6,14 +6,14 @@ title: Technical Advisory 10002 Version: TBD -Date: Calendar week 40/41 +Date: Calendar week 44 ## Description Since Angular Material v15 many of the UI components have been refactored 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, -the console UI will loose its dynamic theming capability. +the console UI will lose its dynamic theming capability. ## Statement @@ -23,7 +23,7 @@ As soon as the release version is published, we will include the version here. ## Mitigation 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. ## Impact diff --git a/docs/docs/support/advisory/a10003.md b/docs/docs/support/advisory/a10003.md new file mode 100644 index 0000000000..d3a5d868d2 --- /dev/null +++ b/docs/docs/support/advisory/a10003.md @@ -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. +::: \ No newline at end of file diff --git a/docs/docs/support/advisory/a10004.md b/docs/docs/support/advisory/a10004.md new file mode 100644 index 0000000000..787c65aeba --- /dev/null +++ b/docs/docs/support/advisory/a10004.md @@ -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. diff --git a/docs/docs/support/advisory/a10005.md b/docs/docs/support/advisory/a10005.md new file mode 100644 index 0000000000..d414167c9f --- /dev/null +++ b/docs/docs/support/advisory/a10005.md @@ -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. +::: \ No newline at end of file diff --git a/docs/docs/support/technical_advisory.mdx b/docs/docs/support/technical_advisory.mdx index 520dd3991c..deb87fb3ac 100644 --- a/docs/docs/support/technical_advisory.mdx +++ b/docs/docs/support/technical_advisory.mdx @@ -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. TBD - Calendar week 40/41 + Calendar week 44 + + + + A-10003 + + Login-UI - Default Context + Breaking Behaviour Change + + 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. + + 2.38.0 + Calendar week 41 + + + + A-10004 + + Sequence uniquenes + Breaking Behaviour Change + + 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. + + 2.39.0 + 2023-10-14 + + + + A-10005 + + Expected downtime during upgrade + Expected downtime during upgrade + + Migrating to versions >= 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. + + 2.39.0 + Calendar week 41/42 2023 diff --git a/docs/sidebars.js b/docs/sidebars.js index a7513f905e..1c82d82852 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -244,6 +244,7 @@ module.exports = { "guides/integrate/identity-providers/openldap", "guides/integrate/identity-providers/migrate", "guides/integrate/identity-providers/okta", + "guides/integrate/identity-providers/keycloak", ], }, { diff --git a/docs/static/img/guides/keycloak_add_client.png b/docs/static/img/guides/keycloak_add_client.png new file mode 100644 index 0000000000..83c8832e2c Binary files /dev/null and b/docs/static/img/guides/keycloak_add_client.png differ diff --git a/docs/static/img/guides/keycloak_client_secret.png b/docs/static/img/guides/keycloak_client_secret.png new file mode 100644 index 0000000000..a110510420 Binary files /dev/null and b/docs/static/img/guides/keycloak_client_secret.png differ diff --git a/docs/static/img/guides/keycloak_login.png b/docs/static/img/guides/keycloak_login.png new file mode 100644 index 0000000000..05459b7d84 Binary files /dev/null and b/docs/static/img/guides/keycloak_login.png differ diff --git a/docs/static/img/guides/zitadel_activate_keycloak.png b/docs/static/img/guides/zitadel_activate_keycloak.png new file mode 100644 index 0000000000..af170087ca Binary files /dev/null and b/docs/static/img/guides/zitadel_activate_keycloak.png differ diff --git a/docs/static/img/guides/zitadel_keycloak_create_provider.png b/docs/static/img/guides/zitadel_keycloak_create_provider.png new file mode 100644 index 0000000000..2f3d322ac6 Binary files /dev/null and b/docs/static/img/guides/zitadel_keycloak_create_provider.png differ diff --git a/docs/static/img/guides/zitadel_login_keycloak.png b/docs/static/img/guides/zitadel_login_keycloak.png new file mode 100644 index 0000000000..a52a1da03d Binary files /dev/null and b/docs/static/img/guides/zitadel_login_keycloak.png differ diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index 9edd6f123c..80cb76571d 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -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) { - 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 { return nil, err } diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index 1627530e6c..df8e76c0e2 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -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) { - 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 { return nil, err } diff --git a/internal/api/grpc/system/instance_converter.go b/internal/api/grpc/system/instance_converter.go index 271dc1520d..4eea0a18f7 100644 --- a/internal/api/grpc/system/instance_converter.go +++ b/internal/api/grpc/system/instance_converter.go @@ -79,7 +79,8 @@ func createInstancePbToAddHuman(req *system_pb.CreateInstanceRequest_Human, defa // check if default username is email style or else append @. // this way we have the same value as before changing `UserLoginMustBeDomain` to false 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 != "" { 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 @. // this way we have the same value as before changing `UserLoginMustBeDomain` to false 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 { instance.Org.Human.Password = req.OwnerPassword.Password diff --git a/internal/api/grpc/user/v2/otp.go b/internal/api/grpc/user/v2/otp.go index 9e239fdfc4..0171e1a653 100644 --- a/internal/api/grpc/user/v2/otp.go +++ b/internal/api/grpc/user/v2/otp.go @@ -9,7 +9,7 @@ import ( ) 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 { 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) { - 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 { 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) { - 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 { 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) { - 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 { return nil, err } diff --git a/internal/api/grpc/user/v2/passkey.go b/internal/api/grpc/user/v2/passkey.go index 97a853bfdd..659ef62388 100644 --- a/internal/api/grpc/user/v2/passkey.go +++ b/internal/api/grpc/user/v2/passkey.go @@ -15,7 +15,7 @@ import ( func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) { var ( - resourceOwner = authz.GetCtxData(ctx).ResourceOwner + resourceOwner = authz.GetCtxData(ctx).OrgID authenticator = passkeyAuthenticatorToDomain(req.GetAuthenticator()) ) 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) { - resourceOwner := authz.GetCtxData(ctx).ResourceOwner + resourceOwner := authz.GetCtxData(ctx).OrgID pkc, err := req.GetPublicKeyCredential().MarshalJSON() if err != nil { 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) { - resourceOwner := authz.GetCtxData(ctx).ResourceOwner + resourceOwner := authz.GetCtxData(ctx).OrgID switch medium := req.Medium.(type) { case nil: diff --git a/internal/api/grpc/user/v2/password.go b/internal/api/grpc/user/v2/password.go index c0837d799f..1bd1604058 100644 --- a/internal/api/grpc/user/v2/password.go +++ b/internal/api/grpc/user/v2/password.go @@ -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) { - var resourceOwner = authz.GetCtxData(ctx).ResourceOwner + var resourceOwner = authz.GetCtxData(ctx).OrgID var details *domain.ObjectDetails switch v := req.GetVerification().(type) { diff --git a/internal/api/grpc/user/v2/totp.go b/internal/api/grpc/user/v2/totp.go index e2ab157104..ab7ec03583 100644 --- a/internal/api/grpc/user/v2/totp.go +++ b/internal/api/grpc/user/v2/totp.go @@ -11,7 +11,7 @@ import ( func (s *Server) RegisterTOTP(ctx context.Context, req *user.RegisterTOTPRequest) (*user.RegisterTOTPResponse, error) { 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) { - 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 { return nil, err } diff --git a/internal/api/grpc/user/v2/u2f.go b/internal/api/grpc/user/v2/u2f.go index 8a935fe361..077f2346ef 100644 --- a/internal/api/grpc/user/v2/u2f.go +++ b/internal/api/grpc/user/v2/u2f.go @@ -12,7 +12,7 @@ import ( func (s *Server) RegisterU2F(ctx context.Context, req *user.RegisterU2FRequest) (*user.RegisterU2FResponse, error) { 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) { - resourceOwner := authz.GetCtxData(ctx).ResourceOwner + resourceOwner := authz.GetCtxData(ctx).OrgID pkc, err := req.GetPublicKeyCredential().MarshalJSON() if err != nil { return nil, caos_errs.ThrowInternal(err, "USERv2-IeTh4", "Errors.Internal") diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index 06a7787b59..fcda0252d6 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -161,7 +161,11 @@ func (l *Login) Handler() http.Handler { } 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 { return nil, err } diff --git a/internal/command/org.go b/internal/command/org.go index 1ba999e97c..72a97b6f01 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -238,7 +238,10 @@ func AddOrgCommand(ctx context.Context, a *org.Aggregate, name string, userIDs . if name = strings.TrimSpace(name); name == "" { 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 []eventstore.Command{ org.NewOrgAddedEvent(ctx, &a.Aggregate, name), diff --git a/internal/command/org_domain.go b/internal/command/org_domain.go index aae1b14b68..6ae30baeb2 100644 --- a/internal/command/org_domain.go +++ b/internal/command/org_domain.go @@ -309,13 +309,16 @@ func (c *Commands) changeDefaultDomain(ctx context.Context, orgID, newName strin return nil, err } iamDomain := authz.GetInstance(ctx).RequestedDomain() - defaultDomain := domain.NewIAMDomainName(orgDomains.OrgName, iamDomain) + defaultDomain, _ := domain.NewIAMDomainName(orgDomains.OrgName, iamDomain) isPrimary := defaultDomain == orgDomains.PrimaryDomain orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel) for _, orgDomain := range orgDomains.Domains { if orgDomain.State == domain.OrgDomainStateActive { if orgDomain.Domain == defaultDomain { - newDefaultDomain := domain.NewIAMDomainName(newName, iamDomain) + newDefaultDomain, err := domain.NewIAMDomainName(newName, iamDomain) + if err != nil { + return nil, err + } events := []eventstore.Command{ org.NewDomainAddedEvent(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 } hasDefault := false - defaultDomain := domain.NewIAMDomainName(orgDomains.OrgName, authz.GetInstance(ctx).RequestedDomain()) + defaultDomain, _ := domain.NewIAMDomainName(orgDomains.OrgName, authz.GetInstance(ctx).RequestedDomain()) isPrimary := defaultDomain == orgDomains.PrimaryDomain orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel) events := make([]eventstore.Command, 0, len(orgDomains.Domains)) diff --git a/internal/domain/org.go b/internal/domain/org.go index c5375e83cd..0e87d20a43 100644 --- a/internal/domain/org.go +++ b/internal/domain/org.go @@ -25,7 +25,8 @@ func (o *Org) IsValid() bool { } 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 diff --git a/internal/domain/org_domain.go b/internal/domain/org_domain.go index ad39e36795..bbd2e664e5 100644 --- a/internal/domain/org_domain.go +++ b/internal/domain/org_domain.go @@ -6,6 +6,7 @@ import ( http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) @@ -32,15 +33,18 @@ func (domain *OrgDomain) GenerateVerificationCode(codeGenerator crypto.Generator 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 // Replaces spaces in org name with hyphens label := strings.ReplaceAll(orgName, " ", "-") // The label must only contains alphanumeric characters and hyphens - // Invalid characters are replaced with and empty space - label = string(regexp.MustCompile(`[^a-zA-Z0-9-]`).ReplaceAll([]byte(label), []byte(""))) + // Invalid characters are replaced with and empty space but as #6471, + // 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 if len(label) > 63 { @@ -64,7 +68,12 @@ func NewIAMDomainName(orgName, iamDomain string) string { 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 diff --git a/internal/domain/org_domain_test.go b/internal/domain/org_domain_test.go index 8dd77ca328..e19138e10a 100644 --- a/internal/domain/org_domain_test.go +++ b/internal/domain/org_domain_test.go @@ -41,10 +41,10 @@ func TestNewIAMDomainName(t *testing.T) { { name: "replace invalid characters [^a-zA-Z0-9-] with empty spaces", args: args{ - orgName: "mí Örg name?", + orgName: "mí >**name?", iamDomain: "localhost", }, - result: "m-rg-name.localhost", + result: "mí-name.localhost", }, { 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", }, + { + name: "string full with invalid characters returns empty", + args: args{ + orgName: "*¿=@^[])", + iamDomain: "localhost", + }, + result: "", + }, } for _, tt := range tests { 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 { t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, domain) } diff --git a/internal/org/model/org.go b/internal/org/model/org.go index 4267506b58..50ceda8c85 100644 --- a/internal/org/model/org.go +++ b/internal/org/model/org.go @@ -46,5 +46,6 @@ func (o *Org) GetPrimaryDomain() *OrgDomain { } 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}) } diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index ea8f06409f..b516fcadb6 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -204,6 +204,7 @@ Errors: Domain: AlreadyExists: Домейнът вече съществува InvalidCharacter: "Само буквено-цифрови знаци, . " + EmptyString: Невалидни нецифрови и азбучни знаци бяха заменени с празни интервали и полученият домейн е празен низ IDP: InvalidSearchQuery: Невалидна заявка за търсене ClientIDMissing: Липсва ClientID diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 6bf7115477..7610d69401 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -202,6 +202,7 @@ Errors: Domain: AlreadyExists: Domäne existiert bereits 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: InvalidSearchQuery: Ungültiger Suchparameter ClientIDMissing: ClientID fehlt diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 03a7e16b7c..0c694f463e 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -202,6 +202,7 @@ Errors: Domain: AlreadyExists: Domain already exists 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: InvalidSearchQuery: Invalid search query ClientIDMissing: ClientID missing diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index a6783a1706..9776a61f23 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -202,6 +202,7 @@ Errors: Domain: AlreadyExists: El dominio ya existe 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: InvalidSearchQuery: Consulta de búsqueda no válida ClientIDMissing: Falta ClientID diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 2e9592fe31..b72a5ba1f3 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -202,6 +202,7 @@ Errors: Domain: AlreadyExists: Le domaine existe déjà 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: InvalidSearchQuery: Paramètre de recherche non valide ClientIDMissing: ID client manquant diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 9a3e48af74..43891039ea 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -201,6 +201,8 @@ Errors: IdpIsNotOIDC: La configurazione IDP non è di tipo oidc Domain: 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: InvalidSearchQuery: Parametro di ricerca non valido InvalidCharacter: Per un dominio sono ammessi solo caratteri alfanumerici, . e - diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 3e2960dc27..72a7aa9d32 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -194,6 +194,7 @@ Errors: Domain: AlreadyExists: ドメインはすでに存在します InvalidCharacter: ドメインは英数字、'.'、'-'のみ使用可能です。 + EmptyString: 無効な数字およびアルファベット以外の文字は空のスペースに置き換えられ、結果のドメインは空の文字列になります IDP: InvalidSearchQuery: 無効な検索クエリです ClientIDMissing: クライアントIDがありません diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index f613eebb2f..ae27d818b2 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -202,6 +202,7 @@ Errors: Domain: AlreadyExists: Доменот веќе постои InvalidCharacter: Дозволени се само алфанумерички знаци, . и - се дозволени за домен + EmptyString: Неважечките ненумерички и азбучни знаци се заменети со празни места и добиениот домен е празна низа IDP: InvalidSearchQuery: Невалидно пребарување ClientID Missing: ClientID недостасува diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 2e0c3a8244..9e7a55ef0f 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -202,6 +202,7 @@ Errors: Domain: AlreadyExists: Domena już istnieje 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: InvalidSearchQuery: Nieprawidłowe zapytanie wyszukiwania ClientIDMissing: Brak ClientID diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 7017e12ccf..71448e1a89 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -200,6 +200,7 @@ Errors: Domain: AlreadyExists: Domínio já existe 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: InvalidSearchQuery: Consulta de pesquisa inválida ClientIDMissing: ClientID ausente diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 27d719a828..526e67f055 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -202,6 +202,7 @@ Errors: Domain: AlreadyExists: 域名已存在 InvalidCharacter: 只有字母数字字符,.和 - 允许用于域名中 + EmptyString: 无效的非数字和字母字符被替换为空格,结果域是空字符串 IDP: InvalidSearchQuery: 无效的搜索查询 ClientIDMissing: 客户端 ID 丢失