Merge branch 'main' into next-merge
@ -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
|
||||
|
||||
|
12
README.md
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -33,6 +33,7 @@
|
||||
class="ok-button"
|
||||
color="primary"
|
||||
(click)="closeDialogWithRequest()"
|
||||
data-e2e="save-sms-settings-button"
|
||||
>
|
||||
<span>{{ 'ACTIONS.SAVE' | translate }}</span>
|
||||
</button>
|
@ -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 {
|
@ -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>
|
@ -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;
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 {}
|
@ -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>
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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');
|
||||
}
|
@ -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 {}
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
],
|
||||
|
@ -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'],
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -1013,6 +1013,8 @@
|
||||
"LOCKOUT": "Блокиране",
|
||||
"COMPLEXITY": "Сложност на паролата",
|
||||
"NOTIFICATIONS": "Настройки за известията",
|
||||
"SMTP_PROVIDER": "SMTP доставчик",
|
||||
"SMS_PROVIDER": "Доставчик на SMS/телефон",
|
||||
"NOTIFICATIONS_DESC": "Настройки за SMTP и SMS",
|
||||
"MESSAGETEXTS": "Текстове на съобщения",
|
||||
"IDP": "Доставчици на идентичност",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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é",
|
||||
|
@ -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à",
|
||||
|
@ -1020,6 +1020,8 @@
|
||||
"LOCKOUT": "ロックアウト",
|
||||
"COMPLEXITY": "パスワードの複雑さ",
|
||||
"NOTIFICATIONS": "通知設定",
|
||||
"SMTP_PROVIDER": "SMTPプロバイダー",
|
||||
"SMS_PROVIDER": "SMS/電話プロバイダー",
|
||||
"NOTIFICATIONS_DESC": "SMTPおよびSMS設定",
|
||||
"MESSAGETEXTS": "メッセージテキスト",
|
||||
"IDP": "IDプロバイダー",
|
||||
|
@ -1021,6 +1021,8 @@
|
||||
"LOCKOUT": "Забрана на пристап",
|
||||
"COMPLEXITY": "Сложеност на лозинката",
|
||||
"NOTIFICATIONS": "Подесувања за известувања",
|
||||
"SMTP_PROVIDER": "SMTP провајдер",
|
||||
"SMS_PROVIDER": "СМС/Провајдер на телефон",
|
||||
"NOTIFICATIONS_DESC": "Подесувања за SMTP и SMS",
|
||||
"MESSAGETEXTS": "Текстови на пораки",
|
||||
"IDP": "Identity Providers",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -1019,6 +1019,8 @@
|
||||
"LOCKOUT": "安全锁策略",
|
||||
"COMPLEXITY": "密码复杂性",
|
||||
"NOTIFICATIONS": "通知设置",
|
||||
"SMTP_PROVIDER": "SMTP 提供商",
|
||||
"SMS_PROVIDER": "短信/电话提供商",
|
||||
"NOTIFICATIONS_DESC": "SMTP 和 SMS 设置",
|
||||
"MESSAGETEXTS": "消息文本",
|
||||
"IDP": "身份提供者",
|
||||
|
@ -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
|
||||
|
||||
|
306
docs/docs/guides/migrate/sources/keycloak.md
Normal 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, let’s 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.
|
@ -125,6 +125,7 @@ module.exports = {
|
||||
items: [
|
||||
"guides/migrate/sources/zitadel",
|
||||
"guides/migrate/sources/auth0",
|
||||
"guides/migrate/sources/keycloak",
|
||||
]
|
||||
},
|
||||
]
|
||||
|
BIN
docs/static/img/guides/console/smtp.png
vendored
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 50 KiB |
BIN
docs/static/img/guides/console/twilio.png
vendored
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 39 KiB |
BIN
docs/static/img/guides/migrate/keycloak-01.png
vendored
Normal file
After Width: | Height: | Size: 144 KiB |
BIN
docs/static/img/guides/migrate/keycloak-02.png
vendored
Normal file
After Width: | Height: | Size: 197 KiB |
BIN
docs/static/img/guides/migrate/keycloak-03.png
vendored
Normal file
After Width: | Height: | Size: 221 KiB |
BIN
docs/static/img/guides/migrate/keycloak-04.png
vendored
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
docs/static/img/guides/migrate/keycloak-05.png
vendored
Normal file
After Width: | Height: | Size: 151 KiB |
BIN
docs/static/img/guides/migrate/keycloak-06.png
vendored
Normal file
After Width: | Height: | Size: 242 KiB |
BIN
docs/static/img/guides/migrate/keycloak-07.png
vendored
Normal file
After Width: | Height: | Size: 193 KiB |
BIN
docs/static/img/guides/migrate/keycloak-08.png
vendored
Normal file
After Width: | Height: | Size: 195 KiB |
BIN
docs/static/img/guides/migrate/keycloak-09.png
vendored
Normal file
After Width: | Height: | Size: 213 KiB |
BIN
docs/static/img/guides/migrate/keycloak-10.png
vendored
Normal file
After Width: | Height: | Size: 249 KiB |
BIN
docs/static/img/guides/migrate/keycloak-11.png
vendored
Normal file
After Width: | Height: | Size: 143 KiB |
BIN
docs/static/img/guides/migrate/keycloak-12.png
vendored
Normal file
After Width: | Height: | Size: 156 KiB |
BIN
docs/static/img/guides/migrate/keycloak-13.png
vendored
Normal file
After Width: | Height: | Size: 205 KiB |
BIN
docs/static/img/guides/migrate/keycloak-14.png
vendored
Normal file
After Width: | Height: | Size: 171 KiB |
BIN
docs/static/img/guides/migrate/keycloak-15.png
vendored
Normal file
After Width: | Height: | Size: 170 KiB |
BIN
docs/static/img/guides/migrate/keycloak-16.png
vendored
Normal file
After Width: | Height: | Size: 177 KiB |
BIN
docs/static/img/guides/migrate/keycloak-17.png
vendored
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
docs/static/img/guides/migrate/keycloak-18.png
vendored
Normal file
After Width: | Height: | Size: 220 KiB |
BIN
docs/static/img/guides/migrate/keycloak-19.png
vendored
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
docs/static/img/guides/migrate/keycloak-20.png
vendored
Normal file
After Width: | Height: | Size: 147 KiB |
BIN
docs/static/img/guides/migrate/keycloak-21.png
vendored
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
docs/static/img/guides/migrate/keycloak-22.png
vendored
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
docs/static/img/guides/migrate/keycloak-23.png
vendored
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
docs/static/img/guides/migrate/keycloak-24.png
vendored
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
docs/static/img/guides/migrate/keycloak-25.png
vendored
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
docs/static/img/guides/migrate/keycloak-26.png
vendored
Normal file
After Width: | Height: | Size: 104 KiB |
BIN
docs/static/img/guides/migrate/keycloak-27.png
vendored
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
docs/static/img/guides/migrate/keycloak-28.png
vendored
Normal file
After Width: | Height: | Size: 256 KiB |
BIN
docs/static/img/guides/migrate/keycloak-29.png
vendored
Normal file
After Width: | Height: | Size: 239 KiB |
BIN
docs/static/img/guides/migrate/keycloak-30.png
vendored
Normal file
After Width: | Height: | Size: 313 KiB |
BIN
docs/static/img/guides/migrate/keycloak-31.png
vendored
Normal file
After Width: | Height: | Size: 341 KiB |
BIN
docs/static/img/guides/migrate/keycloak-32.png
vendored
Normal file
After Width: | Height: | Size: 222 KiB |
BIN
docs/static/img/guides/migrate/keycloak-33.png
vendored
Normal file
After Width: | Height: | Size: 204 KiB |
BIN
docs/static/img/guides/migrate/keycloak-34.png
vendored
Normal file
After Width: | Height: | Size: 223 KiB |
BIN
docs/static/img/guides/migrate/keycloak-35.png
vendored
Normal file
After Width: | Height: | Size: 270 KiB |
BIN
docs/static/img/guides/migrate/keycloak-36.png
vendored
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
docs/static/img/guides/migrate/keycloak-37.png
vendored
Normal file
After Width: | Height: | Size: 849 KiB |
BIN
docs/static/img/guides/migrate/keycloak-38.png
vendored
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
docs/static/img/guides/migrate/keycloak-39.png
vendored
Normal file
After Width: | Height: | Size: 124 KiB |
BIN
docs/static/img/guides/migrate/keycloak-40.png
vendored
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
docs/static/img/guides/migrate/keycloak-41.png
vendored
Normal file
After Width: | Height: | Size: 159 KiB |
BIN
docs/static/img/guides/migrate/keycloak-42.png
vendored
Normal file
After Width: | Height: | Size: 317 KiB |
BIN
docs/static/img/guides/migrate/keycloak-43.png
vendored
Normal file
After Width: | Height: | Size: 345 KiB |
BIN
docs/static/img/guides/migrate/keycloak-44.png
vendored
Normal file
After Width: | Height: | Size: 264 KiB |
BIN
docs/static/img/guides/migrate/keycloak-45.png
vendored
Normal file
After Width: | Height: | Size: 1024 KiB |
BIN
docs/static/img/guides/migrate/keycloak-46.png
vendored
Normal file
After Width: | Height: | Size: 421 KiB |
BIN
docs/static/img/guides/migrate/keycloak-47.png
vendored
Normal file
After Width: | Height: | Size: 280 KiB |
62
e2e/cypress/e2e/instance/settings/notifications.cy.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
103
internal/api/http/middleware/origin_interceptor.go
Normal 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]
|
||||
}
|
@ -119,6 +119,7 @@ func (wm *InstanceSMTPConfigWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(
|
||||
instance.SMTPConfigAddedEventType,
|
||||
instance.SMTPConfigRemovedEventType,
|
||||
instance.SMTPConfigChangedEventType,
|
||||
instance.SMTPConfigPasswordChangedEventType,
|
||||
instance.InstanceDomainAddedEventType,
|
||||
|
@ -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) {
|
||||
|