merge 'main' into next

This commit is contained in:
Silvan 2023-10-19 11:29:44 +02:00 committed by GitHub
commit 259faba3f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
193 changed files with 3844 additions and 918 deletions

View File

@ -52,7 +52,7 @@ jobs:
-
uses: bufbuild/buf-breaking-action@v1
with:
against: "https://github.com/${{ github.repository }}.git#branch=main"
against: "https://github.com/${{ github.repository }}.git#branch=${{ github.base_ref }}"
console:
if: ${{ github.event_name == 'pull_request' }}

View File

@ -101,7 +101,8 @@ Please make sure you cover your changes with tests before marking a Pull Request
- [ ] Integration tests against the gRPC server ensure that probable good and bad read and write permissions are tested.
- [ ] Integration tests against the gRPC server ensure that the API is easily usable despite eventual consistency.
- [ ] Integration tests against the gRPC server ensure that all probable login and registration flows are covered."
- [ ] Integration tests ensure that certain commands send expected notifications.
- [ ] Integration tests ensure that certain commands emit expected events that trigger notifications.
- [ ] Integration tests ensure that certain events trigger expected notifications.
## Contribute

View File

@ -34,10 +34,10 @@ Do you look for a user management that's quickly set up like Auth0 and open sour
Do you have a project that requires multi-tenant user management with self-service for your customers?
Look no further — ZITADEL combines the ease of Auth0 with the versatility of Keycloak.
Look no further — ZITADEL is the identity infrastructure, simplified for you.
We provide you with a wide range of out-of-the-box features to accelerate your project.
Multi-tenancy with branding customization, secure login, self-service, OpenID Connect, OAuth2.x, SAML2, LDAP, Passwordless with FIDO2 (including Passkeys), OTP, U2F, and an unlimited audit trail is there for you, ready to use.
Multi-tenancy with branding customization, secure login, self-service, OpenID Connect, OAuth2.x, SAML2, LDAP, Passkeys / FIDO2, OTP, U2F, and an unlimited audit trail is there for you, ready to use.
With ZITADEL you can rely on a hardened and extensible turnkey solution to solve all of your authentication and authorization needs.
@ -67,7 +67,7 @@ See all guides [here](https://zitadel.com/docs/self-hosting/deploy/overview)
If you want to experience a hands-free ZITADEL, you should use [ZITADEL Cloud](https://zitadel.cloud).
It is free for up to 25'000 authenticated requests and provides you all the features that make ZITADEL great.
ZITADEL Cloud comes with a free tier and provides you all the features that you find in the open source version.
Learn more about the [pay-as-you-go pricing](https://zitadel.com/pricing).
### Example applications
@ -84,6 +84,7 @@ We built ZITADEL with a complex multi-tenancy architecture in mind and provide t
Yet it offers everything you need for a customer identity ([CIAM](https://zitadel.com/docs/guides/solution-scenarios/b2c)) use case.
- [API-first approach](https://zitadel.com/docs/apis/introduction)
- [Multi-tenancy](https://zitadel.com/docs/guides/solution-scenarios/b2b) authentication and access management
- Strong audit trail thanks to [event sourcing](https://zitadel.com/docs/concepts/eventstore/overview) as storage pattern
- [Actions](https://zitadel.com/docs/apis/actions/introduction) to react on events with custom code and extended ZITADEL for you needs
- [Branding](https://zitadel.com/docs/guides/manage/customize/branding) for a uniform user experience across multiple organizations
@ -94,12 +95,15 @@ Yet it offers everything you need for a customer identity ([CIAM](https://zitade
Authentication
- Single Sign On (SSO)
- Passwordless with FIDO2 support (Including Passkeys)
- Passkeys support (FIDO2 / WebAuthN)
- Username / Password
- Multifactor authentication with OTP, U2F, Email OTP, SMS OTP
- LDAP
- External enterprise identity providers and social logins
- [Device authorization](https://zitadel.com/docs/guides/solution-scenarios/device-authorization)
- [OpenID Connect certified](https://openid.net/certification/#OPs) => [OIDC Endpoints](https://zitadel.com/docs/apis/openidoauth/endpoints)
- [SAML 2.0](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) => [SAML Endpoints](https://zitadel.com/docs/apis/saml/endpoints)
- [Custom sessions](https://zitadel.com/docs/guides/integrate/login-ui/username-password) if you need to go beyond OIDC or SAML
- [Machine-to-machine](https://zitadel.com/docs/guides/integrate/serviceusers) with JWT profile, Personal Access Tokens (PAT), and Client Credentials
Multi-Tenancy

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>
//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)
}
}

View File

@ -235,7 +235,6 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
commands,
queries,
eventstoreClient,
assets.AssetAPIFromDomain(config.ExternalSecure, config.ExternalPort),
config.Login.DefaultOTPEmailURLV2,
config.SystemDefaults.Notifications.FileSystemPath,
keys.User,
@ -311,6 +310,8 @@ func startAPIs(
authZRepo,
queries,
}
// always set the origin in the context if available in the http headers, no matter for what protocol
router.Use(middleware.OriginHandler)
verifier := internal_authz.Start(repo, http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers)
tlsConfig, err := config.TLS.Config()
if err != nil {
@ -444,7 +445,6 @@ func startAPIs(
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcProvider, config.ExternalSecure)); err != nil {
return err
}
// handle grpc at last to be able to handle the root, because grpc and gateway require a lot of different prefixes
apis.RouteGRPC()
return nil

View File

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

View File

@ -225,9 +225,11 @@ export class AppComponent implements OnDestroy {
});
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.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;

View File

@ -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<boolean> | Promise<boolean> | 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;
}),
);
}

View File

@ -26,6 +26,7 @@
color="primary"
name="hasUppercase"
ngDefaultControl
data-e2e="notification-policy-checkbox"
[(ngModel)]="notificationData.passwordChange"
[disabled]="(['policy.write'] | hasRole | async) === false"
>
@ -43,6 +44,7 @@
color="primary"
type="submit"
mat-raised-button
data-e2e="save-notification-policy-button"
>
{{ 'ACTIONS.SAVE' | translate }}
</button>

View File

@ -1,24 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotificationSettingsComponent } from './notification-settings.component';
describe('NotificationSettingsComponent', () => {
let component: NotificationSettingsComponent;
let fixture: ComponentFixture<NotificationSettingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [NotificationSettingsComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NotificationSettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -33,6 +33,7 @@
class="ok-button"
color="primary"
(click)="closeDialogWithRequest()"
data-e2e="save-sms-settings-button"
>
<span>{{ 'ACTIONS.SAVE' | translate }}</span>
</button>

View File

@ -14,7 +14,6 @@ import {
import { SMSProvider, TwilioConfig } from 'src/app/proto/generated/zitadel/settings_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
import { PasswordDialogComponent } from '../password-dialog/password-dialog.component';
enum SMSProviderType {

View File

@ -0,0 +1,56 @@
<h2>{{ 'SETTING.SMS.TITLE' | translate }}</h2>
<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="smsProvidersLoading" color="primary"></mat-spinner>
</div>
<div class="sms-providers">
<cnsl-card class="sms-card" [nomargin]="true">
<div class="sms-provider">
<h4 class="title">Twilio</h4>
<span
*ngIf="twilio"
class="state"
[ngClass]="{
active: twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE,
inactive: twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE
}"
>{{ 'SETTING.SMS.SMSPROVIDERSTATE.' + twilio.state | translate }}</span
>
<span class="fill-space"></span>
<button
*ngIf="twilio && twilio.id"
[disabled]="(['iam.write'] | hasRole | async) === false"
mat-stroked-button
(click)="toggleSMSProviderState(twilio.id)"
>
<span *ngIf="twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE">{{
'ACTIONS.DEACTIVATE' | translate
}}</span>
<span *ngIf="twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE">{{
'ACTIONS.ACTIVATE' | translate
}}</span>
</button>
<button
*ngIf="twilio && twilio.id"
color="warn"
[disabled]="(['iam.write'] | hasRole | async) === false"
mat-icon-button
(click)="removeSMSProvider(twilio.id)"
data-e2e="remove-sms-provider-button"
>
<i class="las la-trash"></i>
</button>
<button
[disabled]="(['iam.write'] | hasRole | async) === false"
mat-icon-button
(click)="editSMSProvider()"
data-e2e="new-twilio-button"
>
<i class="las la-pen"></i>
</button>
</div>
</cnsl-card>
</div>

View File

@ -2,35 +2,6 @@
margin: 0.5rem 0;
}
.smtp-form-field,
.info-section-warn {
max-width: 400px;
display: block;
}
.info-section-warn {
margin-bottom: 0.5rem;
}
.smtp-checkbox {
max-width: 400px;
display: block;
margin: 1rem 0;
}
.set-password-btn {
margin-bottom: 1rem;
}
.general-btn-container {
display: flex;
justify-content: flex-start;
.save-button {
display: block;
}
}
.sms-providers {
display: flex;
align-items: center;

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotificationSMSProviderComponent } from './notification-sms-provider.component';
describe('NotificationSMSProviderComponent', () => {
let component: NotificationSMSProviderComponent;
let fixture: ComponentFixture<NotificationSMSProviderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [NotificationSMSProviderComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NotificationSMSProviderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,140 @@
import { Component, Input } from '@angular/core';
import { AddSMSProviderTwilioRequest, UpdateSMSProviderTwilioRequest } from 'src/app/proto/generated/zitadel/admin_pb';
import { SMSProvider, SMSProviderConfigState } from 'src/app/proto/generated/zitadel/settings_pb';
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
import { InfoSectionType } from '../../info-section/info-section.component';
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
@Component({
selector: 'cnsl-notification-sms-provider',
templateUrl: './notification-sms-provider.component.html',
styleUrls: ['./notification-sms-provider.component.scss'],
})
export class NotificationSMSProviderComponent {
@Input() public serviceType!: PolicyComponentServiceType;
public smsProviders: SMSProvider.AsObject[] = [];
public smsProvidersLoading: boolean = false;
public SMSProviderConfigState: any = SMSProviderConfigState;
public InfoSectionType: any = InfoSectionType;
constructor(
private service: AdminService,
private dialog: MatDialog,
private toast: ToastService,
) {}
private fetchData(): void {
this.smsProvidersLoading = true;
this.service
.listSMSProviders()
.then((smsProviders) => {
this.smsProvidersLoading = false;
if (smsProviders.resultList) {
this.smsProviders = smsProviders.resultList;
}
})
.catch((error) => {
this.smsProvidersLoading = false;
this.toast.showError(error);
});
}
public editSMSProvider(): void {
const dialogRef = this.dialog.open(DialogAddSMSProviderComponent, {
width: '400px',
data: {
smsProviders: this.smsProviders,
},
});
dialogRef.afterClosed().subscribe((req: AddSMSProviderTwilioRequest | UpdateSMSProviderTwilioRequest) => {
if (req) {
if (!!this.twilio) {
this.service
.updateSMSProviderTwilio(req as UpdateSMSProviderTwilioRequest)
.then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
} else {
this.service
.addSMSProviderTwilio(req as AddSMSProviderTwilioRequest)
.then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
}
}
});
}
public toggleSMSProviderState(id: string): void {
const provider = this.smsProviders.find((p) => p.id === id);
if (provider) {
if (provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE) {
this.service
.deactivateSMSProvider(id)
.then(() => {
this.toast.showInfo('SETTING.SMS.DEACTIVATED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
} else if (provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE) {
this.service
.activateSMSProvider(id)
.then(() => {
this.toast.showInfo('SETTING.SMS.ACTIVATED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
}
}
}
public removeSMSProvider(id: string): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'SETTING.SMS.REMOVEPROVIDER',
descriptionKey: 'SETTING.SMS.REMOVEPROVIDER_DESC',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.service
.removeSMSProvider(id)
.then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.REMOVED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
public get twilio(): SMSProvider.AsObject | undefined {
return this.smsProviders.find((p) => p.twilio);
}
}

View File

@ -15,11 +15,10 @@ import { InfoSectionModule } from '../../info-section/info-section.module';
import { InputModule } from '../../input/input.module';
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
import { NotificationSettingsComponent } from './notification-settings.component';
import { PasswordDialogComponent } from './password-dialog/password-dialog.component';
import { NotificationSMSProviderComponent } from './notification-sms-provider.component';
@NgModule({
declarations: [NotificationSettingsComponent, DialogAddSMSProviderComponent, PasswordDialogComponent],
declarations: [NotificationSMSProviderComponent, DialogAddSMSProviderComponent],
imports: [
CommonModule,
CardModule,
@ -38,6 +37,6 @@ import { PasswordDialogComponent } from './password-dialog/password-dialog.compo
MatSelectModule,
TranslateModule,
],
exports: [NotificationSettingsComponent],
exports: [NotificationSMSProviderComponent],
})
export class NotificationSettingsModule {}
export class NotificationSMSProviderModule {}

View File

@ -4,7 +4,13 @@
<div mat-dialog-content>
<cnsl-form-field class="formfield">
<cnsl-label>{{ data.i18nLabel | translate }}</cnsl-label>
<input cnslInput [(ngModel)]="password" type="password" autocomplete="new-password" />
<input
cnslInput
[(ngModel)]="password"
type="password"
autocomplete="new-password"
data-e2e="notification-setting-password"
/>
</cnsl-form-field>
</div>
<div mat-dialog-actions class="action">
@ -19,6 +25,7 @@
mat-raised-button
class="ok-button"
(click)="closeDialog(password)"
data-e2e="save-notification-setting-password-button"
>
{{ 'ACTIONS.OK' | translate }}
</button>

View File

@ -1,7 +1,7 @@
<h2>{{ 'SETTING.SMTP.TITLE' | translate }}</h2>
<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="smtpLoading || smsProvidersLoading" color="primary"></mat-spinner>
<mat-spinner diameter="30" *ngIf="smtpLoading" color="primary"></mat-spinner>
</div>
<cnsl-info-section
@ -48,6 +48,7 @@
(click)="setSMTPPassword()"
type="button"
mat-stroked-button
data-e2e="add-smtp-password-button"
>
{{ 'SETTING.SMTP.SETPASSWORD' | translate }}
</button>
@ -60,55 +61,9 @@
color="primary"
type="submit"
mat-raised-button
data-e2e="save-smtp-settings-button"
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
<br />
<h2>{{ 'SETTING.SMS.TITLE' | translate }}</h2>
<div class="sms-providers">
<cnsl-card class="sms-card" [nomargin]="true">
<div class="sms-provider">
<h4 class="title">Twilio</h4>
<span
*ngIf="twilio"
class="state"
[ngClass]="{
active: twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE,
inactive: twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE
}"
>{{ 'SETTING.SMS.SMSPROVIDERSTATE.' + twilio.state | translate }}</span
>
<span class="fill-space"></span>
<button
*ngIf="twilio && twilio.id"
[disabled]="(['iam.write'] | hasRole | async) === false"
mat-stroked-button
(click)="toggleSMSProviderState(twilio.id)"
>
<span *ngIf="twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE">{{
'ACTIONS.DEACTIVATE' | translate
}}</span>
<span *ngIf="twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE">{{
'ACTIONS.ACTIVATE' | translate
}}</span>
</button>
<button
*ngIf="twilio && twilio.id"
color="warn"
[disabled]="(['iam.write'] | hasRole | async) === false"
mat-icon-button
(click)="removeSMSProvider(twilio.id)"
>
<i class="las la-trash"></i>
</button>
<button [disabled]="(['iam.write'] | hasRole | async) === false" mat-icon-button (click)="editSMSProvider()">
<i class="las la-pen"></i>
</button>
</div>
</cnsl-card>
</div>

View File

@ -0,0 +1,32 @@
.spinner-wr {
margin: 0.5rem 0;
}
.smtp-form-field,
.info-section-warn {
max-width: 400px;
display: block;
}
.info-section-warn {
margin-bottom: 0.5rem;
}
.smtp-checkbox {
max-width: 400px;
display: block;
margin: 1rem 0;
}
.set-password-btn {
margin-bottom: 1rem;
}
.general-btn-container {
display: flex;
justify-content: flex-start;
.save-button {
display: block;
}
}

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotificationSMTPProviderComponent } from './notification-smtp-provider.component';
describe('NotificationSMTPProviderComponent', () => {
let component: NotificationSMTPProviderComponent;
let fixture: ComponentFixture<NotificationSMTPProviderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [NotificationSMTPProviderComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NotificationSMTPProviderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -3,45 +3,33 @@ import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import { take } from 'rxjs';
import {
AddSMSProviderTwilioRequest,
AddSMTPConfigRequest,
AddSMTPConfigResponse,
UpdateSMSProviderTwilioRequest,
UpdateSMTPConfigPasswordRequest,
UpdateSMTPConfigRequest,
UpdateSMTPConfigResponse,
} from 'src/app/proto/generated/zitadel/admin_pb';
import { DebugNotificationProvider, SMSProvider, SMSProviderConfigState } from 'src/app/proto/generated/zitadel/settings_pb';
import { AdminService } from 'src/app/services/admin.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { InfoSectionType } from '../../info-section/info-section.component';
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
import { PasswordDialogComponent } from '../notification-sms-provider/password-dialog/password-dialog.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
import { PasswordDialogComponent } from './password-dialog/password-dialog.component';
@Component({
selector: 'cnsl-notification-settings',
templateUrl: './notification-settings.component.html',
styleUrls: ['./notification-settings.component.scss'],
selector: 'cnsl-notification-smtp-provider',
templateUrl: './notification-smtp-provider.component.html',
styleUrls: ['./notification-smtp-provider.component.scss'],
})
export class NotificationSettingsComponent implements OnInit {
export class NotificationSMTPProviderComponent implements OnInit {
@Input() public serviceType!: PolicyComponentServiceType;
public smsProviders: SMSProvider.AsObject[] = [];
public logNotificationProvider!: DebugNotificationProvider.AsObject;
public fileNotificationProvider!: DebugNotificationProvider.AsObject;
public smtpLoading: boolean = false;
public smsProvidersLoading: boolean = false;
public logProviderLoading: boolean = false;
public fileProviderLoading: boolean = false;
public form!: UntypedFormGroup;
public SMSProviderConfigState: any = SMSProviderConfigState;
public InfoSectionType: any = InfoSectionType;
public hasSMTPConfig: boolean = false;
@ -96,46 +84,6 @@ export class NotificationSettingsComponent implements OnInit {
this.hasSMTPConfig = false;
}
});
this.smsProvidersLoading = true;
this.service
.listSMSProviders()
.then((smsProviders) => {
this.smsProvidersLoading = false;
if (smsProviders.resultList) {
this.smsProviders = smsProviders.resultList;
}
})
.catch((error) => {
this.smsProvidersLoading = false;
this.toast.showError(error);
});
this.logProviderLoading = true;
this.service
.getLogNotificationProvider()
.then((logNotificationProvider) => {
this.logProviderLoading = false;
if (logNotificationProvider.provider) {
this.logNotificationProvider = logNotificationProvider.provider;
}
})
.catch(() => {
this.logProviderLoading = false;
});
this.fileProviderLoading = true;
this.service
.getFileSystemNotificationProvider()
.then((fileNotificationProvider) => {
this.fileProviderLoading = false;
if (fileNotificationProvider.provider) {
this.fileNotificationProvider = fileNotificationProvider.provider;
}
})
.catch(() => {
this.fileProviderLoading = false;
});
}
private updateData(): Promise<UpdateSMTPConfigResponse.AsObject | AddSMTPConfigResponse> {
@ -175,41 +123,6 @@ export class NotificationSettingsComponent implements OnInit {
});
}
public editSMSProvider(): void {
const dialogRef = this.dialog.open(DialogAddSMSProviderComponent, {
width: '400px',
data: {
smsProviders: this.smsProviders,
},
});
dialogRef.afterClosed().subscribe((req: AddSMSProviderTwilioRequest | UpdateSMSProviderTwilioRequest) => {
if (req) {
if (!!this.twilio) {
this.service
.updateSMSProviderTwilio(req as UpdateSMSProviderTwilioRequest)
.then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
} else {
this.service
.addSMSProviderTwilio(req as AddSMSProviderTwilioRequest)
.then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
}
}
});
}
public setSMTPPassword(): void {
const dialogRef = this.dialog.open(PasswordDialogComponent, {
width: '400px',
@ -236,63 +149,6 @@ export class NotificationSettingsComponent implements OnInit {
});
}
public toggleSMSProviderState(id: string): void {
const provider = this.smsProviders.find((p) => p.id === id);
if (provider) {
if (provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE) {
this.service
.deactivateSMSProvider(id)
.then(() => {
this.toast.showInfo('SETTING.SMS.DEACTIVATED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
} else if (provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE) {
this.service
.activateSMSProvider(id)
.then(() => {
this.toast.showInfo('SETTING.SMS.ACTIVATED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
}
}
}
public removeSMSProvider(id: string): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'SETTING.SMS.REMOVEPROVIDER',
descriptionKey: 'SETTING.SMS.REMOVEPROVIDER_DESC',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.service
.removeSMSProvider(id)
.then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.REMOVED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
public get twilio(): SMSProvider.AsObject | undefined {
return this.smsProviders.find((p) => p.twilio);
}
public get senderAddress(): AbstractControl | null {
return this.form.get('senderAddress');
}

View File

@ -0,0 +1,42 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
import { TranslateModule } from '@ngx-translate/core';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { CardModule } from '../../card/card.module';
import { FormFieldModule } from '../../form-field/form-field.module';
import { InfoSectionModule } from '../../info-section/info-section.module';
import { InputModule } from '../../input/input.module';
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
import { PasswordDialogComponent } from '../notification-sms-provider/password-dialog/password-dialog.component';
import { NotificationSMTPProviderComponent } from './notification-smtp-provider.component';
@NgModule({
declarations: [NotificationSMTPProviderComponent, PasswordDialogComponent],
imports: [
CommonModule,
CardModule,
InfoSectionModule,
FormsModule,
ReactiveFormsModule,
HasRolePipeModule,
MatButtonModule,
MatCheckboxModule,
InputModule,
MatIconModule,
FormFieldModule,
WarnDialogModule,
MatSelectModule,
MatProgressSpinnerModule,
MatSelectModule,
TranslateModule,
],
exports: [NotificationSMTPProviderComponent],
})
export class NotificationSMTPProviderModule {}

View File

@ -51,7 +51,7 @@ export const NOTIFICATION_GROUP: SettingLinks = {
i18nTitle: 'SETTINGS.GROUPS.NOTIFICATIONS',
i18nDesc: 'SETTINGS.LIST.NOTIFICATIONS_DESC',
iamRouterLink: ['/settings'],
queryParams: { id: 'notifications' },
queryParams: { id: 'smtpprovider' },
iamWithRole: ['iam.policy.read'],
icon: 'las la-bell',
color: 'red',

View File

@ -27,19 +27,18 @@
<ng-container *ngIf="currentSetting === 'idp'">
<cnsl-idp-settings [serviceType]="serviceType"></cnsl-idp-settings>
</ng-container>
<ng-container *ngIf="currentSetting === 'notifications' && serviceType === PolicyComponentServiceType.ADMIN">
<ng-container *ngIf="currentSetting === 'notifications'">
<cnsl-notification-policy [serviceType]="serviceType"></cnsl-notification-policy>
<cnsl-notification-settings [serviceType]="serviceType"></cnsl-notification-settings>
</ng-container>
<ng-container *ngIf="currentSetting === 'notifications' && serviceType === PolicyComponentServiceType.MGMT">
<cnsl-notification-policy [serviceType]="serviceType"></cnsl-notification-policy
></ng-container>
<ng-container *ngIf="currentSetting === 'smtpprovider' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-notification-smtp-provider [serviceType]="serviceType"></cnsl-notification-smtp-provider>
</ng-container>
<ng-container *ngIf="currentSetting === 'smsprovider' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-notification-sms-provider [serviceType]="serviceType"></cnsl-notification-sms-provider>
</ng-container>
<ng-container *ngIf="currentSetting === 'oidc' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-oidc-configuration></cnsl-oidc-configuration>
</ng-container>
<ng-container *ngIf="currentSetting === 'secrets' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-secret-generator></cnsl-secret-generator>
</ng-container>

View File

@ -13,7 +13,8 @@ import { LoginPolicyModule } from '../policies/login-policy/login-policy.module'
import { LoginTextsPolicyModule } from '../policies/login-texts/login-texts.module';
import { MessageTextsPolicyModule } from '../policies/message-texts/message-texts.module';
import { NotificationPolicyModule } from '../policies/notification-policy/notification-policy.module';
import { NotificationSettingsModule } from '../policies/notification-settings/notification-settings.module';
import { NotificationSMSProviderModule } from '../policies/notification-sms-provider/notification-sms-provider.module';
import { NotificationSMTPProviderModule } from '../policies/notification-smtp-provider/notification-smtp-provider.module';
import { OIDCConfigurationModule } from '../policies/oidc-configuration/oidc-configuration.module';
import { PasswordComplexityPolicyModule } from '../policies/password-complexity-policy/password-complexity-policy.module';
import { PasswordLockoutPolicyModule } from '../policies/password-lockout-policy/password-lockout-policy.module';
@ -46,7 +47,8 @@ import { SettingsListComponent } from './settings-list.component';
DomainPolicyModule,
TranslateModule,
HasRolePipeModule,
NotificationSettingsModule,
NotificationSMTPProviderModule,
NotificationSMSProviderModule,
OIDCConfigurationModule,
SecretGeneratorModule,
],

View File

@ -98,15 +98,25 @@ export const NOTIFICATIONS: SidenavSetting = {
groupI18nKey: 'SETTINGS.GROUPS.NOTIFICATIONS',
requiredRoles: {
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
[PolicyComponentServiceType.MGMT]: ['policy.read'],
},
};
export const NOTIFICATION_POLICY: SidenavSetting = {
id: 'notifications',
i18nKey: 'SETTINGS.LIST.NOTIFICATIONS',
export const SMTP_PROVIDER: SidenavSetting = {
id: 'smtpprovider',
i18nKey: 'SETTINGS.LIST.SMTP_PROVIDER',
groupI18nKey: 'SETTINGS.GROUPS.NOTIFICATIONS',
requiredRoles: {
[PolicyComponentServiceType.MGMT]: ['policy.read'],
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
},
};
export const SMS_PROVIDER: SidenavSetting = {
id: 'smsprovider',
i18nKey: 'SETTINGS.LIST.SMS_PROVIDER',
groupI18nKey: 'SETTINGS.GROUPS.NOTIFICATIONS',
requiredRoles: {
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
},
};

View File

@ -21,6 +21,8 @@ import {
PRIVACYPOLICY,
SECRETS,
SECURITY,
SMS_PROVIDER,
SMTP_PROVIDER,
} from '../../modules/settings-list/settings';
@Component({
@ -36,6 +38,8 @@ export class InstanceSettingsComponent implements OnInit, OnDestroy {
// notifications
// { showWarn: true, ...NOTIFICATIONS },
NOTIFICATIONS,
SMTP_PROVIDER,
SMS_PROVIDER,
// login
LOGIN,
IDP,
@ -80,7 +84,10 @@ export class InstanceSettingsComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.settingsList = this.authService.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.admin);
this.settingsList = this.authService.isAllowedMapper(
this.defaultSettingsList,
(setting) => setting.requiredRoles.admin || [],
);
}
ngOnDestroy(): void {

View File

@ -15,7 +15,7 @@ import {
LOGIN,
LOGINTEXTS,
MESSAGETEXTS,
NOTIFICATION_POLICY,
NOTIFICATIONS,
PRIVACYPOLICY,
VERIFIED_DOMAINS,
} from '../../modules/settings-list/settings';
@ -34,7 +34,7 @@ export class OrgSettingsComponent implements OnInit {
IDP,
COMPLEXITY,
LOCKOUT,
NOTIFICATION_POLICY,
NOTIFICATIONS,
VERIFIED_DOMAINS,
DOMAIN,
BRANDING,
@ -68,7 +68,7 @@ export class OrgSettingsComponent implements OnInit {
ngOnInit(): void {
this.settingsList = this.authService
.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.mgmt)
.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.mgmt || [])
.pipe(take(1));
}
}

View File

@ -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 @@
<h1>{{ 'APP.PAGES.CREATE_DESC_TITLE' | translate }}</h1>
<mat-progress-bar class="progress-bar" color="primary" *ngIf="loading" mode="indeterminate"></mat-progress-bar>
<mat-checkbox class="proswitch" color="primary" [(ngModel)]="devmode">
<mat-checkbox class="proswitch" color="primary" [(ngModel)]="pro">
{{ 'APP.PROSWITCH' | translate }}
</mat-checkbox>
<mat-horizontal-stepper
class="stepper {{ 'last-edited-step-' + stepper.selectedIndex }}"
*ngIf="!devmode"
*ngIf="!pro"
linear
#stepper
labelPosition="bottom"
@ -109,6 +109,15 @@
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
</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
class="redirect-section"
[disabled]="false"
@ -116,6 +125,7 @@
[(ngModel)]="redirectUris"
[getValues]="requestRedirectValuesSubject$"
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
[devMode]="devMode"
data-e2e="redirect-uris"
>
</cnsl-redirect-uris>
@ -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"
>
</cnsl-redirect-uris>
@ -299,6 +310,19 @@
</div>
</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">
<div class="row">
<span class="left cnsl-secondary-text">
@ -342,7 +366,7 @@
</ng-template>
</mat-horizontal-stepper>
<div *ngIf="devmode" class="dev">
<div *ngIf="pro" class="dev">
<form [formGroup]="form" (ngSubmit)="createApp()" data-e2e="create-app-wizzard-3">
<div class="content">
<cnsl-form-field class="formfield">
@ -439,6 +463,14 @@
"
>
<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
class="redirect-section"
[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 { 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 {
BASIC_AUTH_METHOD,
@ -51,7 +52,7 @@ const MAX_ALLOWED_SIZE = 1 * 1024 * 1024;
export class AppCreateComponent implements OnInit, OnDestroy {
private subscription: Subscription = new Subscription();
private destroyed$: Subject<void> = 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();

View File

@ -427,7 +427,7 @@
<div class="app-info-row">
<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>
<div class="app-copy-row">
<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 { 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());

View File

@ -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;
}),
);
}),

View File

@ -71,7 +71,7 @@ export const ONBOARDING_EVENTS: OnboardingActions[] = [
eventType: 'instance.smtp.config.added',
oneof: ['instance.smtp.config.added', 'instance.smtp.config.changed'],
link: ['/settings'],
fragment: 'notifications',
fragment: 'smtpprovider',
iconClasses: 'las la-envelope',
darkcolor: yellowdark,
lightcolor: yellowlight,

View File

@ -1013,6 +1013,8 @@
"LOCKOUT": "Блокиране",
"COMPLEXITY": "Сложност на паролата",
"NOTIFICATIONS": "Настройки за известията",
"SMTP_PROVIDER": "SMTP доставчик",
"SMS_PROVIDER": "Доставчик на SMS/телефон",
"NOTIFICATIONS_DESC": "Настройки за SMTP и SMS",
"MESSAGETEXTS": "Текстове на съобщения",
"IDP": "Доставчици на идентичност",
@ -1975,6 +1977,8 @@
"CLIENTSECRET_DESCRIPTION": "Пазете клиентската си тайна на сигурно място, тъй като тя ще изчезне, след като диалоговият прозорец бъде затворен.",
"REGENERATESECRET": "Повторно генериране на клиентска тайна",
"DEVMODE": "Режим на разработка",
"DEVMODE_ENABLED": "Активиран",
"DEVMODE_DISABLED": "Деактивиран",
"DEVMODEDESC": "Внимание: При активиран режим на разработка URI адресите за пренасочване няма да бъдат валидирани.",
"SKIPNATIVEAPPSUCCESSPAGE": "Пропуснете страницата за успешно влизане",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Пропуснете страницата за успех след влизане в това родно приложение.",

View File

@ -1019,6 +1019,8 @@
"LOCKOUT": "Sperrmechanismen",
"COMPLEXITY": "Passwordkomplexität",
"NOTIFICATIONS": "Benachrichtigungseinstellungen",
"SMTP_PROVIDER": "SMTP-Anbieter",
"SMS_PROVIDER": "SMS / Telefon Anbieter",
"NOTIFICATIONS_DESC": "SMTP und SMS Einstellungen",
"MESSAGETEXTS": "Benachrichtigungstexte",
"IDP": "Identitätsanbieter",
@ -1984,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.",

View File

@ -1019,7 +1019,9 @@
"LOGIN": "Login Behavior and Security",
"LOCKOUT": "Lockout",
"COMPLEXITY": "Password complexity",
"NOTIFICATIONS": "Notification settings",
"NOTIFICATIONS": "Notifications",
"SMTP_PROVIDER": "SMTP Provider",
"SMS_PROVIDER": "SMS/Phone Provider",
"NOTIFICATIONS_DESC": "SMTP and SMS Settings",
"MESSAGETEXTS": "Message Texts",
"IDP": "Identity Providers",
@ -1993,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.",

View File

@ -1020,6 +1020,8 @@
"LOCKOUT": "Bloqueo",
"COMPLEXITY": "Complejidad de contraseña",
"NOTIFICATIONS": "Ajustes de notificación",
"SMTP_PROVIDER": "Proveedor SMTP",
"SMS_PROVIDER": "Proveedor SMS/Teléfono",
"NOTIFICATIONS_DESC": "Ajustes SMTP y SMS",
"MESSAGETEXTS": "Mensajes de texto",
"IDP": "Proveedores de identidad",
@ -1981,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.",

View File

@ -1019,6 +1019,8 @@
"LOCKOUT": "Verrouillage",
"COMPLEXITY": "Complexité du mot de passe",
"NOTIFICATIONS": "Paramètres de notification",
"SMTP_PROVIDER": "Fournisseur SMTP",
"SMS_PROVIDER": "SMS/Téléphone Fournisseur",
"NOTIFICATIONS_DESC": "Paramètres SMTP et SMS",
"MESSAGETEXTS": "Textes des messages",
"IDP": "Fournisseurs d'identité",
@ -1985,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",

View File

@ -1019,6 +1019,8 @@
"LOCKOUT": "Meccanismi di bloccaggio",
"COMPLEXITY": "Complessità della password",
"NOTIFICATIONS": "Impostazioni di notifica",
"SMTP_PROVIDER": "Fornitore SMTP",
"SMS_PROVIDER": "Fornitore di servizi SMS/telefonici",
"NOTIFICATIONS_DESC": "Impostazioni SMTP e SMS",
"MESSAGETEXTS": "Testi di notifica",
"IDP": "Fornitori di identità",
@ -1985,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",

View File

@ -1020,6 +1020,8 @@
"LOCKOUT": "ロックアウト",
"COMPLEXITY": "パスワードの複雑さ",
"NOTIFICATIONS": "通知設定",
"SMTP_PROVIDER": "SMTPプロバイダー",
"SMS_PROVIDER": "SMS/電話プロバイダー",
"NOTIFICATIONS_DESC": "SMTPおよびSMS設定",
"MESSAGETEXTS": "メッセージテキスト",
"IDP": "IDプロバイダー",
@ -1976,6 +1978,8 @@
"CLIENTSECRET_DESCRIPTION": "クライアントシークレットは、ダイアログを閉じると消えてしまうので、安全な場所に保管してください。",
"REGENERATESECRET": "クライアントシークレットを再生成する",
"DEVMODE": "開発モード",
"DEVMODE_ENABLED": "アクティブ化された",
"DEVMODE_DISABLED": "無効化されました",
"DEVMODEDESC": "注意開発モードを有効にすると、URIが認証されません。",
"SKIPNATIVEAPPSUCCESSPAGE": "ログイン後に成功ページをスキップする",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "このネイティブアプリのログイン後に成功ページをスキップする",

View File

@ -1021,6 +1021,8 @@
"LOCKOUT": "Забрана на пристап",
"COMPLEXITY": "Сложеност на лозинката",
"NOTIFICATIONS": "Подесувања за известувања",
"SMTP_PROVIDER": "SMTP провајдер",
"SMS_PROVIDER": "СМС/Провајдер на телефон",
"NOTIFICATIONS_DESC": "Подесувања за SMTP и SMS",
"MESSAGETEXTS": "Текстови на пораки",
"IDP": "Identity Providers",
@ -1982,6 +1984,8 @@
"CLIENTSECRET_DESCRIPTION": "Чувајте ја вашата клиентска тајна на безбедно место, бидејќи ќе исчезне откако ќе се затвори дијалогот.",
"REGENERATESECRET": "Генерирај нова клиентска тајна",
"DEVMODE": "Development mode",
"DEVMODE_ENABLED": "Активиран",
"DEVMODE_DISABLED": "Деактивирано",
"DEVMODEDESC": "Внимавајте: Со овозможен Development mode, URIs за пренасочување нема да бидат валидирани.",
"SKIPNATIVEAPPSUCCESSPAGE": "Прескокни страница за успешна најава",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Прескокнете ја страницата за успешна најава за оваа нативна апликација.",

View File

@ -1019,6 +1019,8 @@
"LOCKOUT": "Blokada",
"COMPLEXITY": "Złożoność hasła",
"NOTIFICATIONS": "Ustawienia powiadomień",
"SMTP_PROVIDER": "Dostawca SMTP",
"SMS_PROVIDER": "Dostawca SMS-ów/telefonów",
"NOTIFICATIONS_DESC": "Ustawienia SMTP i SMS",
"MESSAGETEXTS": "Teksty wiadomości",
"IDP": "Dostawcy tożsamości",
@ -1985,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ę",

View File

@ -1021,6 +1021,8 @@
"LOCKOUT": "Bloqueio",
"COMPLEXITY": "Complexidade de Senha",
"NOTIFICATIONS": "Configurações de Notificação",
"SMTP_PROVIDER": "Provedor SMTP",
"SMS_PROVIDER": "Provedor de SMS/Telefone",
"NOTIFICATIONS_DESC": "Configurações de SMTP e SMS",
"MESSAGETEXTS": "Textos de Mensagem",
"IDP": "Provedores de Identidade",
@ -1980,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.",

View File

@ -1019,6 +1019,8 @@
"LOCKOUT": "安全锁策略",
"COMPLEXITY": "密码复杂性",
"NOTIFICATIONS": "通知设置",
"SMTP_PROVIDER": "SMTP 提供商",
"SMS_PROVIDER": "短信/电话提供商",
"NOTIFICATIONS_DESC": "SMTP 和 SMS 设置",
"MESSAGETEXTS": "消息文本",
"IDP": "身份提供者",
@ -1984,6 +1986,8 @@
"CLIENTSECRET_DESCRIPTION": "将您的客户保密在一个安全的地方,因为一旦对话框关闭,便无法再次查看。",
"REGENERATESECRET": "重新生成客户端密钥",
"DEVMODE": "开发模式",
"DEVMODE_ENABLED": "活性",
"DEVMODE_DISABLED": "已停用",
"DEVMODEDESC": "注意:启用开发模式的重定向 URI 将不会被验证。",
"SKIPNATIVEAPPSUCCESSPAGE": "登录后跳过成功页面",
"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

@ -9,7 +9,7 @@ import OrgDescription from "../../../concepts/structure/_org_description.mdx";
import Column from "../../../../src/components/column";
An Organization is where your projects and users live. Looking at a B2B use case, an organization represents a business partner who typically has its own branding and has different access settings like additional federated login providers.
Users from one organization are seperated from others.
Users from one organization are separated from others.
## Create a new organization
@ -23,7 +23,7 @@ If you choose your logged in user as organization manager, a membership for the
alt="Select Organization"
/>
If you want to enable your customers to create their organization by themselves, we provide a creation form for a organization. `<https://$CUSTOM-DOMAIN/ui/login/register/org`
If you want to enable your customers to create their organization by themselves, we provide a creation form for an organization. `<https://$CUSTOM-DOMAIN/ui/login/register/org`
The customer needs to fill in the form with the organization name and the contact details.
<img
@ -34,8 +34,8 @@ The customer needs to fill in the form with the organization name and the contac
## How ZITADEL handles usernames
If you domain setting "user loginname must contain orgdomain" is disabled. Your username will be unique withing the whole instance.
At the moment the username only allowes e-mail formatted input. (This will be changed soon)
If your domain setting "user loginname must contain orgdomain" is disabled, your username will be unique within the whole instance.
At the moment the username only allows e-mail formatted input. (This will be changed soon)
### User Loginname must contain orgdomain
@ -46,9 +46,9 @@ Those loginnames consist of the format `{username}@{domainname}.{zitadeldomain}`
If your user had the username `john.doe`, the generated loginname would be `john.doe@acme.zitadel.cloud`.
This also means that only one user with the username `john.doe` can exist in your organization called `ACME`.
If you verify your domain name or add additional domains, ZITADEL will generate those additional logonames for you.
If you verify your domain name or add additional domains, ZITADEL will generate those additional login names for you.
If the organization would own the domain `acme.ch` and verify it, then the resulting loginname would be `john.doe@acme.ch` in addition to the already generated `john.doe@acme.zitadel.cloud`.
The user can now use either logonname to authenticate with your application.
The user can now use either login name to authenticate with your application.
> Note: You can set this setting on your instance as well as your organizations. All available usernames are shown on the top of the user pages.
@ -59,12 +59,14 @@ Users that you create within your organization will be suffixed with this domain
You can improve the user experience, by suffixing users with a domain name that is in your control.
If the "validate org domains" settings in the [Domain Settings](./instance-settings#domain-settings) is set to true, you have to prove the ownership of your domain, by DNS or HTTP challenge.
If the settings is set to false, the created domain will automatically be set to verifed.
If the setting is set to false, the created domain will automatically be set to verifed.
An organization can have multiple domain names, but only one domain can be primary.
The primary domain defines which login name ZITADEL displays to the user, and what information gets asserted in access_tokens (`preferred_username`).
Please note that domain verification also removes the logonname from all users, who might have used this combination in the global organization (ie. users not belonging to a specific organization). Relating to our example with acme.ch: If a user coyote exists in the global organization with the logonname coyote@acme.ch, then after verification of acme.ch, this logonname will be replaced with `coyote@{randomvalue.tld}`. ZITADEL will notify users affected by this change.
Please note that domain verification also removes the login name from all users, who might have used this combination in the global organization (ie. users not belonging to a specific organization).
Relating to our example with acme.ch: If a user coyote exists in the global organization with the login name coyote@acme.ch, then after verification of acme.ch, this login name will be replaced with `coyote@{randomvalue.tld}`.
ZITADEL will notify users affected by this change.
## Verify your domain name

View File

@ -0,0 +1,306 @@
---
title: Migrate from Keycloak
sidebar_label: From Keycloak
---
## Migrating from Keycloak to ZITADEL
This guide will use [Docker installation](https://www.docker.com/) to run Keycloak and ZITADEL. However, both Keycloak and ZITADEL offer different installation methods. As a result, this guide won't include any required production tuning or security hardening for either system. However, it's advised you follow [recommended guidelines](https://zitadel.com/docs/guides/manage/self-hosted/production) before putting those systems into production. You can skip setting up Keycloak and ZITADEL if you already have running instances.
## Set up Keycloak
### Run Keycloak
To begin setting up Keycloak, you need to refer to the official [Keycloak Docker image](https://www.keycloak.org/getting-started/getting-started-docker). You'll use it to run a development version of the Keycloak server on your local machine:
```bash
docker run -d -p 8081:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:22.0.1 start-dev
```
In a few seconds, Keycloak will be available at [http://localhost:8081](http://localhost:8081). Access the **Administration Console** via the username `admin` and password `admin`:
<img src="/docs/img/guides/migrate/keycloak-01.png" alt="Migrating users from Keycloak to ZITADEL"/>
<img src="/docs/img/guides/migrate/keycloak-02.png" alt="Migrating users from Keycloak to ZITADEL"/>
### Create a realm in Keycloak
In order to configure Keycloak as the identity provider for your application, you need to create a new realm. This will allow users and authentication resources to be isolated from any other Keycloak usage. Click on the sidebar drop-down menu and select **Create Realm**. Then input the desired realm name and click **Create**:
<img src="/docs/img/guides/migrate/keycloak-03.png" alt="Migrating users from Keycloak to ZITADEL"/>
<img src="/docs/img/guides/migrate/keycloak-04.png" alt="Migrating users from Keycloak to ZITADEL"/>
<img src="/docs/img/guides/migrate/keycloak-05.png" alt="Migrating users from Keycloak to ZITADEL"/>
### Create user in Keycloak
The last thing you need to do in Keycloak is to create at least one new user. This user will be able to log into your application.
On the menu on the left, select **Users**, and click **Add user**. Fill in the username, email, and first and last names, and mark the email as verified. Click on **Create** to create a new user:
<img src="/docs/img/guides/migrate/keycloak-11.png" alt="Migrating users from Keycloak to ZITADEL"/>
<img src="/docs/img/guides/migrate/keycloak-12.png" alt="Migrating users from Keycloak to ZITADEL"/>
<img src="/docs/img/guides/migrate/keycloak-13.png" alt="Migrating users from Keycloak to ZITADEL"/>
Now you should attach a password to this user. Select the **Credentials** tab and click **Set password**.
<img src="/docs/img/guides/migrate/keycloak-14.png" alt="Migrating users from Keycloak to ZITADEL"/>
On the new modal panel, input the desired password and select **Save**.
<img src="/docs/img/guides/migrate/keycloak-15.png" alt="Migrating users from Keycloak to ZITADEL"/>
<img src="/docs/img/guides/migrate/keycloak-16.png" alt="Migrating users from Keycloak to ZITADEL"/>
### Export Keycloak users
Keycloak provides an [export](https://www.keycloak.org/server/importExport) functionality that allows user information to be extracted into JSON files. While it's intended to be used in another Keycloak instance, you can manipulate it to export users to a different user management system.
For example, in order to generate the export files with Keycloak, you will need to enter the Docker container, run the export command, and copy it outside the container:
```bash
# Recover the Container ID for Keycloak
docker ps
# Run the export command inside the Keycloak container
# use the container ID of Keycloak
docker exec <keycloak container ID> /opt/keycloak/bin/kc.sh export --dir /tmp
# copy generated files from docker container to local machine
docker cp <keycloak container ID>:/tmp/my-realm-users-0.json .
```
## Set up ZITADEL
After creating a sample application that connects to Keycloak, you need to set up ZITADEL in order to migrate the application and users from Keycloak to ZITADEL. For this, ZITADEL offers a [Docker Compose](https://zitadel.com/docs/self-hosting/deploy/compose) installation guide. Follow the instructions under the [Docker compose](https://zitadel.com/docs/self-hosting/deploy/compose#docker-compose) section to run a ZITADEL instance locally.
Next, the application will be available at [http://localhost:8080/ui/console/](http://localhost:8080/ui/console/).
<img src="/docs/img/guides/migrate/keycloak-22.png" alt="Migrating users from Keycloak to ZITADEL"/>
Now you can access the console with the following default credentials:
* **Username**: `zitadel-admin@zitadel.localhost`
* **Password**: `Password1!`
## Import Keycloak users into ZITADEL
As explained in this [ZITADEL user migration guide](https://zitadel.com/docs/guides/migrate/users), you can import users individually or in bulk. Since we are looking at importing a single user from Keycloak, migrating that individual user to ZITADEL can be done with the [ImportHumanUser](https://zitadel.com/docs/apis/resources/mgmt/management-service-import-human-user) endpoint.
> With this endpoint, an email will only be sent to the user if the email is marked as not verified or if there's no password set.
### Create a service user to consume ZITADEL API
But first of all, in order to use this ZITADEL API, you need to create a [service user](https://zitadel.com/docs/guides/integrate/serviceusers#exercise-create-a-service-user).
Go to the **Users** menu and select the **Service Users** tab. And click the **+ New** button.
<img src="/docs/img/guides/migrate/keycloak-39.png" alt="Migrating users from Keycloak to ZITADEL"/>
Fill in the details of the service user and click **Create**.
<img src="/docs/img/guides/migrate/keycloak-40.png" alt="Migrating users from Keycloak to ZITADEL"/>
Your service user is now created and listed.
<img src="/docs/img/guides/migrate/keycloak-41.png" alt="Migrating users from Keycloak to ZITADEL"/>
### Provide 'Org Owner' permissions to the service user
This service user needs to have elevated permissions in order to import users. For this example, you should make the service user an organization owner as explained in [this guide](https://zitadel.com/docs/guides/integrate/access-zitadel-apis#add-org_owner-to-service-user).
Let's change the permissions as follows:
Click on the button shown in the image below:
<img src="/docs/img/guides/migrate/keycloak-42.png" alt="Migrating users from Keycloak to ZITADEL"/>
Next, select your service user that you created and select the **Org Owner** checkbox to assign the permissions of an organization owner to the service user.
<img src="/docs/img/guides/migrate/keycloak-43.png" alt="Migrating users from Keycloak to ZITADEL"/>
### Generate an access token for the service user
In order for the service user to access the API, they must be able to authenticate themselves. To authenticate the user, you can use either [JWT with Private Key](/docs/guides/integrate/serviceusers#authenticating-a-service-user) flow (recommended for production) or [Personal Access Tokens](/docs/guides/integrate/pat)(PAT). In this guide, we will choose the latter.
Go to **Users** -> **Service Users** again and click on the service user, then select **Personal Access Tokens** on the left and click the **+ New** button. Copy the generated personal access token to use it later. Click **Close** after copying the PAT.
<img src="/docs/img/guides/migrate/keycloak-44.png" alt="Migrating users from Keycloak to ZITADEL"/>
### Import user to ZITADEL via ZITADEL API
if your Keycloak Realm has a single user, your `my-realm-users-0.json` file, into which you exported your Keycloak user previously, will look like this:
```js
{
"realm" : "my-realm",
"users" : [ {
"id" : "826731b2-bf17-4bd9-b45c-6a26c76ddaae",
"createdTimestamp" : 1693887631918,
"username" : "test-user",
"enabled" : true,
"totp" : false,
"emailVerified" : true,
"firstName" : "John",
"lastName" : "Doe",
"email" : "test-user@mail.com",
"credentials" : [ {
"id" : "c3f3759e-9d8a-4628-aad9-09e66f28a4e2",
"type" : "password",
"userLabel" : "My password",
"createdDate" : 1693888572700,
"secretData" : "{\"value\":\"ng6oDRung/pBLayd5ro7IU3mL/p86pg3WvQNQc+N1Eg=\",\"salt\":\"RaXjs4RiUKgJGkX6kp277w==\",\"additionalParameters\":{}}",
"credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
} ],
"disableableCredentialTypes" : [ ],
"requiredActions" : [ ],
"realmRoles" : [ "default-roles-my-realm" ],
"notBefore" : 0,
"groups" : [ ]
} ]
}
```
Now, you need to transform the JSON to the ZITADEL data format by adhering to the ZITADEL API [specification](https://zitadel.com/docs/apis/resources/mgmt/management-service-import-human-user) to import a user. The minimal format would be as shown below:
```js
{
"userName": "test-user",
"profile": {
"firstName": "John",
"lastName": "Doe"
},
"email": {
"email": "test-user@mail.com",
"isEmailVerified": true
},
"hashedPassword": {
"value": "$pbkdf2-sha256$27500$RaXjs4RiUKgJGkX6kp277w==$ng6oDRung/pBLayd5ro7IU3mL/p86pg3WvQNQc+N1Eg="
}
}
```
Next, you must install [`zitadel-tools`](https://github.com/zitadel/zitadel-tools/tree/main), which is a utility toolset designed to facilitate various interactions with the ZITADEL platform, mainly with tasks related to authentication, authorization, and data migration. We will be using the `migrate` command:
Purpose: Assists users in transforming exported data from other identity providers to be compatible with Zitadel's import schema.
Supported Providers: Currently, migrations from Auth0 and Keycloak are supported.
Usage: Users can get a list of available sub-commands and flags with the --help flag.
Install `zitadel-tools` using the command below. Ensure you have Go already installed on your machine.
```bash
go install github.com/zitadel/zitadel-tools@main
```
Now you can run the migration tool for Keycloak as explained in this [guide](https://github.com/zitadel/zitadel-tools/blob/main/cmd/migration/keycloak/readme.md). Let's go through the steps:
The Keycloak migration tool facilitates the transfer of data to ZITADEL by creating a JSON file tailored to serve as the body for an import request to the ZITADEL API. Note that it's essential that an organization already exists within ZITADEL/
To perform the migration, you'll need:
- The organization ID (--org)
- A realm.json file (in our case, `my-realm-users-0.json`) that houses your exported Keycloak realm with user details (--realm).
- Output path via --output (default: ./importBody.json)
- Timeout duration for the data import request using --timeout (default: 30 minutes)
- Pretty printing the output JSON with --multiline.
Execute with:
```bash
zitadel-tools migrate keycloak --org=<organisation id> --realm=./realm.json --output=./importBody.json --timeout=1h --multiline
```
Example:
```bash
zitadel-tools migrate keycloak --org=233868910057750531 --realm=./my-realm-users-0.json --output=./importBody.json --timeout=1h --multiline
```
Ensure `my-realm-users-0.json` is in the same directory for the tool to process it, or provide the path to the file.
`importBody.json` will now contain the transformed data as shown below:
```bash
{
"dataOrgs": {
"orgs": [
{
"orgId": "233868910057750531",
"humanUsers": [
{
"userId": "826731b2-bf17-4bd9-b45c-6a26c76ddaae",
"user": {
"userName": "test-user",
"profile": {
"firstName": "John",
"lastName": "Doe"
},
"email": {
"email": "test-user@mail.com",
"isEmailVerified": true
},
"hashedPassword": {
"value": "$pbkdf2-sha256$27500$RaXjs4RiUKgJGkX6kp277w==$ng6oDRung/pBLayd5ro7IU3mL/p86pg3WvQNQc+N1Eg="
}
}
}
]
}
]
},
"timeout": "1h0m0s"
}
```
Now copy the following portion to a separate file and name the file `zitadel-users-file.json`.
```bash
"userId": "826731b2-bf17-4bd9-b45c-6a26c76ddaae",
"user": {
"userName": "test-user",
"profile": {
"firstName": "John",
"lastName": "Doe"
},
"email": {
"email": "test-user@mail.com",
"isEmailVerified": true
},
"hashedPassword": {
"value": "$pbkdf2-sha256$27500$RaXjs4RiUKgJGkX6kp277w==$ng6oDRung/pBLayd5ro7IU3mL/p86pg3WvQNQc+N1Eg="
}
}
```
Now that we have the user details in the required JSON format, lets call the ZITADEL API to add the user.
Run the following cURL command to invoke the API and don't forget to replace `<service user access token>` with the service user's personal access token:
```bash
curl --request POST \
--url http://localhost:8080/management/v1/users/human/_import \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <service user access token>' \
--data @zitadel-users-file.json
```
A successful response would be as shown below:
<img src="/docs/img/guides/migrate/keycloak-46.png" alt="Migrating users from Keycloak to ZITADEL"/>
> Note that the previous request imports a single user. If you're using ZITADEL Cloud and have a large number of users, you may hit its rate limit or may need to pay the excess number of API requests. If you experience this, reach out to the [ZITADEL support team](https://zitadel.com/contact), as they can provide an alternative migration tools to move a large number of users.
Now you have imported the Keycloak user into ZITADEL. To view your user go to [http://localhost:8080/ui/console/users](http://localhost:8080/ui/console/users) (or go to the **Users** tab to see the users).
<img src="/docs/img/guides/migrate/keycloak-47.png" alt="Migrating users from Keycloak to ZITADEL"/>
You can now view the Keycloak user's details in ZITADEL. You can see that the password is available too.

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.
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

View File

@ -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

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.
</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>
</table>

View File

@ -125,6 +125,7 @@ module.exports = {
items: [
"guides/migrate/sources/zitadel",
"guides/migrate/sources/auth0",
"guides/migrate/sources/keycloak",
]
},
]
@ -243,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",
],
},
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 39 KiB

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: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Some files were not shown because too many files have changed in this diff Show More