Merge branch 'main' into next-merge

This commit is contained in:
adlerhurst 2023-10-19 10:08:05 +02:00
commit 9a7517dd2c
153 changed files with 3279 additions and 812 deletions

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

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

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

@ -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": "Доставчици на идентичност",

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",

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",

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",

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é",

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à",

View File

@ -1020,6 +1020,8 @@
"LOCKOUT": "ロックアウト",
"COMPLEXITY": "パスワードの複雑さ",
"NOTIFICATIONS": "通知設定",
"SMTP_PROVIDER": "SMTPプロバイダー",
"SMS_PROVIDER": "SMS/電話プロバイダー",
"NOTIFICATIONS_DESC": "SMTPおよびSMS設定",
"MESSAGETEXTS": "メッセージテキスト",
"IDP": "IDプロバイダー",

View File

@ -1021,6 +1021,8 @@
"LOCKOUT": "Забрана на пристап",
"COMPLEXITY": "Сложеност на лозинката",
"NOTIFICATIONS": "Подесувања за известувања",
"SMTP_PROVIDER": "SMTP провајдер",
"SMS_PROVIDER": "СМС/Провајдер на телефон",
"NOTIFICATIONS_DESC": "Подесувања за SMTP и SMS",
"MESSAGETEXTS": "Текстови на пораки",
"IDP": "Identity Providers",

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",

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",

View File

@ -1019,6 +1019,8 @@
"LOCKOUT": "安全锁策略",
"COMPLEXITY": "密码复杂性",
"NOTIFICATIONS": "通知设置",
"SMTP_PROVIDER": "SMTP 提供商",
"SMS_PROVIDER": "短信/电话提供商",
"NOTIFICATIONS_DESC": "SMTP 和 SMS 设置",
"MESSAGETEXTS": "消息文本",
"IDP": "身份提供者",

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1024 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

View File

@ -0,0 +1,62 @@
const notificationPath = `/settings?id=notifications`;
const smtpPath = `/settings?id=smtpprovider`;
const smsPath = `/settings?id=smsprovider`;
beforeEach(() => {
cy.context().as('ctx');
});
describe('instance notifications', () => {
describe('notification settings', () => {
it(`should show notification settings`, () => {
cy.visit(notificationPath);
cy.contains('Notification');
});
});
describe('smtp settings', () => {
it(`should show SMTP provider settings`, () => {
cy.visit(smtpPath);
cy.contains('SMTP Settings');
});
it(`should add SMTP provider settings`, () => {
cy.visit(smtpPath);
cy.get('[formcontrolname="senderAddress"]').clear().type('sender@example.com');
cy.get('[formcontrolname="senderName"]').clear().type('Zitadel');
cy.get('[formcontrolname="hostAndPort"]').clear().type('smtp.mailtrap.io:2525');
cy.get('[formcontrolname="user"]').clear().type('user@example.com');
cy.get('[data-e2e="save-smtp-settings-button"]').click();
cy.shouldConfirmSuccess();
cy.get('[formcontrolname="senderAddress"]').should('have.value', 'sender@example.com');
cy.get('[formcontrolname="senderName"]').should('have.value', 'Zitadel');
cy.get('[formcontrolname="hostAndPort"]').should('have.value', 'smtp.mailtrap.io:2525');
cy.get('[formcontrolname="user"]').should('have.value', 'user@example.com');
});
it(`should add SMTP provider password`, () => {
cy.visit(smtpPath);
cy.get('[data-e2e="add-smtp-password-button"]').click();
cy.get('[data-e2e="notification-setting-password"]').clear().type('dummy@example.com');
cy.get('[data-e2e="save-notification-setting-password-button"]').click();
cy.shouldConfirmSuccess();
});
});
describe('sms settings', () => {
it(`should show SMS provider settings`, () => {
cy.visit(smsPath);
cy.contains('SMS Settings');
});
it(`should add SMS provider`, () => {
cy.visit(smsPath);
cy.get('[data-e2e="new-twilio-button"]').click();
cy.get('[formcontrolname="sid"]').clear().type('test');
cy.get('[formcontrolname="token"]').clear().type('token');
cy.get('[formcontrolname="senderNumber"]').clear().type('2312123132');
cy.get('[data-e2e="save-sms-settings-button"]').click();
cy.shouldConfirmSuccess();
});
});
});

View File

@ -55,12 +55,6 @@ func AssetAPI(externalSecure bool) func(context.Context) string {
}
}
func AssetAPIFromDomain(externalSecure bool, externalPort uint16) func(context.Context) string {
return func(ctx context.Context) string {
return http_util.BuildHTTP(authz.GetInstance(ctx).RequestedDomain(), externalPort, externalSecure) + HandlerPrefix
}
}
type Uploader interface {
UploadAsset(ctx context.Context, info string, asset *command.AssetUpload, commands *command.Commands) error
ObjectName(data authz.CtxData) (string, error)

View File

@ -133,7 +133,7 @@ func GetAllPermissionsFromCtx(ctx context.Context) []string {
func checkOrigin(ctx context.Context, origins []string) error {
origin := grpc.GetGatewayHeader(ctx, http_util.Origin)
if origin == "" {
origin = http_util.OriginFromCtx(ctx)
origin = http_util.OriginHeader(ctx)
if origin == "" {
return nil
}

View File

@ -1,6 +1,7 @@
package http
import (
errorsAs "errors"
"fmt"
"io/ioutil"
"net"
@ -36,6 +37,9 @@ func ValidateDomainHTTP(domain, token, verifier string) error {
return errors.ThrowInternal(err, "HTTP-BH42h", "Errors.Internal")
}
if resp.StatusCode != 200 {
if resp.StatusCode == 404 {
return errors.ThrowNotFound(err, "ORG-F4zhw", "Errors.Org.DomainVerificationHTTPNotFound")
}
return errors.ThrowInternal(err, "HTTP-G2zsw", "Errors.Internal")
}
defer resp.Body.Close()
@ -46,12 +50,21 @@ func ValidateDomainHTTP(domain, token, verifier string) error {
if string(body) == verifier {
return nil
}
return errors.ThrowInvalidArgument(err, "HTTP-GH422", "Errors.Internal")
return errors.ThrowNotFound(err, "ORG-GH422", "Errors.Org.DomainVerificationHTTPNoMatch")
}
func ValidateDomainDNS(domain, verifier string) error {
txtRecords, err := net.LookupTXT(tokenUrlDNS(domain))
if err != nil {
var dnsError *net.DNSError
if errorsAs.As(err, &dnsError) {
if dnsError.IsNotFound {
return errors.ThrowNotFound(err, "ORG-G241f", "Errors.Org.DomainVerificationTXTNotFound")
}
if dnsError.IsTimeout {
return errors.ThrowNotFound(err, "ORG-K563l", "Errors.Org.DomainVerificationTimeout")
}
}
return errors.ThrowInternal(err, "HTTP-Hwsw2", "Errors.Internal")
}
@ -60,7 +73,7 @@ func ValidateDomainDNS(domain, verifier string) error {
return nil
}
}
return errors.ThrowInvalidArgument(err, "HTTP-G241f", "Errors.Internal")
return errors.ThrowNotFound(err, "ORG-G28if", "Errors.Org.DomainVerificationTXTNoMatch")
}
func TokenUrl(domain, token string, checkType CheckType) (string, error) {

View File

@ -45,6 +45,7 @@ type key int
const (
httpHeaders key = iota
remoteAddr
origin
)
func CopyHeadersToContext(h http.Handler) http.Handler {
@ -61,7 +62,7 @@ func HeadersFromCtx(ctx context.Context) (http.Header, bool) {
return headers, ok
}
func OriginFromCtx(ctx context.Context) string {
func OriginHeader(ctx context.Context) string {
headers, ok := ctx.Value(httpHeaders).(http.Header)
if !ok {
return ""
@ -69,6 +70,18 @@ func OriginFromCtx(ctx context.Context) string {
return headers.Get(Origin)
}
func ComposedOrigin(ctx context.Context) string {
o, ok := ctx.Value(origin).(string)
if !ok {
return ""
}
return o
}
func WithComposedOrigin(ctx context.Context, composed string) context.Context {
return context.WithValue(ctx, origin, composed)
}
func RemoteIPFromCtx(ctx context.Context) string {
ctxHeaders, ok := HeadersFromCtx(ctx)
if !ok {

View File

@ -0,0 +1,103 @@
package middleware
import (
"fmt"
"net/http"
"net/url"
"github.com/muhlemmer/httpforwarded"
"github.com/zitadel/logging"
http_util "github.com/zitadel/zitadel/internal/api/http"
)
func OriginHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := composeOrigin(r)
if !http_util.IsOrigin(origin) {
logging.Debugf("extracted origin is not valid: %s", origin)
next.ServeHTTP(w, r)
return
}
next.ServeHTTP(w, r.WithContext(http_util.WithComposedOrigin(r.Context(), origin)))
})
}
func composeOrigin(r *http.Request) string {
if origin, err := originFromForwardedHeader(r); err != nil {
logging.OnError(err).Debug("failed to build origin from forwarded header, trying x-forwarded-* headers")
} else {
return origin
}
if origin, err := originFromXForwardedHeaders(r); err != nil {
logging.OnError(err).Debug("failed to build origin from x-forwarded-* headers, using host header")
} else {
return origin
}
scheme := "https"
if r.TLS == nil {
scheme = "http"
}
return fmt.Sprintf("%s://%s", scheme, r.Host)
}
func originFromForwardedHeader(r *http.Request) (string, error) {
fwd, err := httpforwarded.ParseFromRequest(r)
if err != nil {
return "", err
}
var fwdProto, fwdHost, fwdPort string
if fwdProto = mostRecentValue(fwd, "proto"); fwdProto == "" {
return "", fmt.Errorf("no proto in forwarded header")
}
if fwdHost = mostRecentValue(fwd, "host"); fwdHost == "" {
return "", fmt.Errorf("no host in forwarded header")
}
fwdPort, foundFwdFor := extractPort(mostRecentValue(fwd, "for"))
if !foundFwdFor {
return "", fmt.Errorf("no for in forwarded header")
}
o := fmt.Sprintf("%s://%s", fwdProto, fwdHost)
if fwdPort != "" {
o += ":" + fwdPort
}
return o, nil
}
func originFromXForwardedHeaders(r *http.Request) (string, error) {
scheme := r.Header.Get("X-Forwarded-Proto")
if scheme == "" {
return "", fmt.Errorf("no X-Forwarded-Proto header")
}
host := r.Header.Get("X-Forwarded-Host")
if host == "" {
return "", fmt.Errorf("no X-Forwarded-Host header")
}
return fmt.Sprintf("%s://%s", scheme, host), nil
}
func extractPort(raw string) (string, bool) {
if u, ok := parseURL(raw); ok {
return u.Port(), ok
}
return "", false
}
func parseURL(raw string) (*url.URL, bool) {
if raw == "" {
return nil, false
}
u, err := url.Parse(raw)
return u, err == nil
}
func mostRecentValue(forwarded map[string][]string, key string) string {
if forwarded == nil {
return ""
}
values := forwarded[key]
if len(values) == 0 {
return ""
}
return values[len(values)-1]
}

View File

@ -119,6 +119,7 @@ func (wm *InstanceSMTPConfigWriteModel) Query() *eventstore.SearchQueryBuilder {
AggregateIDs(wm.AggregateID).
EventTypes(
instance.SMTPConfigAddedEventType,
instance.SMTPConfigRemovedEventType,
instance.SMTPConfigChangedEventType,
instance.SMTPConfigPasswordChangedEventType,
instance.InstanceDomainAddedEventType,

View File

@ -213,9 +213,11 @@ func (c *Commands) ValidateOrgDomain(ctx context.Context, orgDomain *domain.OrgD
return writeModelToObjectDetails(&domainWriteModel.WriteModel), nil
}
events = append(events, org.NewDomainVerificationFailedEvent(ctx, orgAgg, orgDomain.Domain))
_, err = c.eventstore.Push(ctx, events...)
logging.LogWithFields("ORG-dhTE", "orgID", orgAgg.ID, "domain", orgDomain.Domain).OnError(err).Error("NewDomainVerificationFailedEvent push failed")
return nil, errors.ThrowInvalidArgument(err, "ORG-GH3s", "Errors.Org.DomainVerificationFailed")
_, errPush := c.eventstore.Push(ctx, events...)
logging.LogWithFields("ORG-dhTE", "orgID", orgAgg.ID, "domain", orgDomain.Domain).OnError(errPush).Error("NewDomainVerificationFailedEvent push failed")
return nil, err
}
func (c *Commands) SetPrimaryOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain) (*domain.ObjectDetails, error) {

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