merge 'main' into next
2
.github/workflows/lint.yml
vendored
@ -52,7 +52,7 @@ jobs:
|
||||
-
|
||||
uses: bufbuild/buf-breaking-action@v1
|
||||
with:
|
||||
against: "https://github.com/${{ github.repository }}.git#branch=main"
|
||||
against: "https://github.com/${{ github.repository }}.git#branch=${{ github.base_ref }}"
|
||||
|
||||
console:
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
@ -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
|
||||
|
@ -105,13 +105,21 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
|
||||
// check if username is email style or else append @<orgname>.<custom-domain>
|
||||
//this way we have the same value as before changing `UserLoginMustBeDomain` to false
|
||||
if !mig.instanceSetup.DomainPolicy.UserLoginMustBeDomain && !strings.Contains(mig.instanceSetup.Org.Human.Username, "@") {
|
||||
mig.instanceSetup.Org.Human.Username = mig.instanceSetup.Org.Human.Username + "@" + domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain)
|
||||
orgDomain, err := domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mig.instanceSetup.Org.Human.Username = mig.instanceSetup.Org.Human.Username + "@" + orgDomain
|
||||
}
|
||||
mig.instanceSetup.Org.Human.Email.Address = mig.instanceSetup.Org.Human.Email.Address.Normalize()
|
||||
if mig.instanceSetup.Org.Human.Email.Address == "" {
|
||||
mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username)
|
||||
if !strings.Contains(string(mig.instanceSetup.Org.Human.Email.Address), "@") {
|
||||
mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username + "@" + domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain))
|
||||
orgDomain, err := domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username + "@" + orgDomain)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="main-container">
|
||||
<ng-container *ngIf="(authService.user | async) || {} as user">
|
||||
<ng-container *ngIf="(authService.userSubject | async) || {} as user">
|
||||
<cnsl-header
|
||||
*ngIf="user && user !== {}"
|
||||
[org]="org"
|
||||
|
@ -225,9 +225,11 @@ export class AppComponent implements OnDestroy {
|
||||
});
|
||||
|
||||
this.isDarkTheme = this.themeService.isDarkTheme;
|
||||
this.isDarkTheme.subscribe((dark) => this.onSetTheme(dark ? 'dark-theme' : 'light-theme'));
|
||||
this.isDarkTheme
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((dark) => this.onSetTheme(dark ? 'dark-theme' : 'light-theme'));
|
||||
|
||||
this.translate.onLangChange.subscribe((language: LangChangeEvent) => {
|
||||
this.translate.onLangChange.pipe(takeUntil(this.destroy$)).subscribe((language: LangChangeEvent) => {
|
||||
this.document.documentElement.lang = language.lang;
|
||||
this.language = language.lang;
|
||||
});
|
||||
@ -271,7 +273,7 @@ export class AppComponent implements OnDestroy {
|
||||
this.translate.addLangs(supportedLanguages);
|
||||
this.translate.setDefaultLang(fallbackLanguage);
|
||||
|
||||
this.authService.user.subscribe((userprofile) => {
|
||||
this.authService.userSubject.pipe(takeUntil(this.destroy$)).subscribe((userprofile) => {
|
||||
if (userprofile) {
|
||||
const cropped = navigator.language.split('-')[0] ?? fallbackLanguage;
|
||||
const fallbackLang = cropped.match(supportedLanguagesRegexp) ? cropped : fallbackLanguage;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { map, Observable, take } from 'rxjs';
|
||||
|
||||
import { GrpcAuthService } from '../services/grpc-auth.service';
|
||||
|
||||
@ -19,11 +18,13 @@ export class UserGuard {
|
||||
state: RouterStateSnapshot,
|
||||
): Observable<boolean> | Promise<boolean> | boolean {
|
||||
return this.authService.user.pipe(
|
||||
map((user) => user?.id !== route.params['id']),
|
||||
tap((isNotMe) => {
|
||||
if (!isNotMe) {
|
||||
take(1),
|
||||
map((user) => {
|
||||
const isMe = user?.id === route.params['id'];
|
||||
if (isMe) {
|
||||
this.router.navigate(['/users', 'me']);
|
||||
}
|
||||
return !isMe;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
title="{{ 'APP.PAGES.CREATE' | translate }}"
|
||||
class="app-create-wrapper"
|
||||
[createSteps]="
|
||||
!devmode
|
||||
!pro
|
||||
? appType?.value?.createType === AppCreateType.OIDC
|
||||
? appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE
|
||||
? 4
|
||||
@ -20,13 +20,13 @@
|
||||
<h1>{{ 'APP.PAGES.CREATE_DESC_TITLE' | translate }}</h1>
|
||||
<mat-progress-bar class="progress-bar" color="primary" *ngIf="loading" mode="indeterminate"></mat-progress-bar>
|
||||
|
||||
<mat-checkbox class="proswitch" color="primary" [(ngModel)]="devmode">
|
||||
<mat-checkbox class="proswitch" color="primary" [(ngModel)]="pro">
|
||||
{{ 'APP.PROSWITCH' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
<mat-horizontal-stepper
|
||||
class="stepper {{ 'last-edited-step-' + stepper.selectedIndex }}"
|
||||
*ngIf="!devmode"
|
||||
*ngIf="!pro"
|
||||
linear
|
||||
#stepper
|
||||
labelPosition="bottom"
|
||||
@ -109,6 +109,15 @@
|
||||
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
|
||||
</p>
|
||||
|
||||
<mat-slide-toggle
|
||||
color="primary"
|
||||
[(ngModel)]="devMode"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
matTooltip="{{ 'APP.OIDC.DEVMODEDESC' | translate }}"
|
||||
>
|
||||
{{ 'APP.OIDC.DEVMODE' | translate }}
|
||||
</mat-slide-toggle>
|
||||
|
||||
<cnsl-redirect-uris
|
||||
class="redirect-section"
|
||||
[disabled]="false"
|
||||
@ -116,6 +125,7 @@
|
||||
[(ngModel)]="redirectUris"
|
||||
[getValues]="requestRedirectValuesSubject$"
|
||||
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
|
||||
[devMode]="devMode"
|
||||
data-e2e="redirect-uris"
|
||||
>
|
||||
</cnsl-redirect-uris>
|
||||
@ -144,6 +154,7 @@
|
||||
[getValues]="requestRedirectValuesSubject$"
|
||||
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
|
||||
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||
[devMode]="devMode"
|
||||
data-e2e="postlogout-uris"
|
||||
>
|
||||
</cnsl-redirect-uris>
|
||||
@ -299,6 +310,19 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="appType?.value?.createType === AppCreateType.OIDC">
|
||||
<div class="row">
|
||||
<span class="left cnsl-secondary-text">
|
||||
{{ 'APP.OIDC.DEVMODE' | translate }}
|
||||
</span>
|
||||
<span class="right">
|
||||
<span>
|
||||
{{ devMode ? ('APP.OIDC.DEVMODE_ENABLED' | translate) : ('APP.OIDC.DEVMODE_DISABLED' | translate) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="appType?.value?.createType === AppCreateType.API">
|
||||
<div class="row">
|
||||
<span class="left cnsl-secondary-text">
|
||||
@ -342,7 +366,7 @@
|
||||
</ng-template>
|
||||
</mat-horizontal-stepper>
|
||||
|
||||
<div *ngIf="devmode" class="dev">
|
||||
<div *ngIf="pro" class="dev">
|
||||
<form [formGroup]="form" (ngSubmit)="createApp()" data-e2e="create-app-wizzard-3">
|
||||
<div class="content">
|
||||
<cnsl-form-field class="formfield">
|
||||
@ -439,6 +463,14 @@
|
||||
"
|
||||
>
|
||||
<div class="formfield full-width">
|
||||
<mat-slide-toggle
|
||||
color="primary"
|
||||
[(ngModel)]="devMode"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
matTooltip="{{ 'APP.OIDC.DEVMODEDESC' | translate }}"
|
||||
>
|
||||
{{ 'APP.OIDC.DEVMODE' | translate }}
|
||||
</mat-slide-toggle>
|
||||
<cnsl-redirect-uris
|
||||
class="redirect-section"
|
||||
[disabled]="false"
|
||||
|
@ -28,6 +28,7 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { MatLegacySlideToggleChange } from '@angular/material/legacy-slide-toggle';
|
||||
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
|
||||
import {
|
||||
BASIC_AUTH_METHOD,
|
||||
@ -51,7 +52,7 @@ const MAX_ALLOWED_SIZE = 1 * 1024 * 1024;
|
||||
export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
private subscription: Subscription = new Subscription();
|
||||
private destroyed$: Subject<void> = new Subject();
|
||||
public devmode: boolean = false;
|
||||
public pro: boolean = false;
|
||||
public projectId: string = '';
|
||||
public loading: boolean = false;
|
||||
|
||||
@ -239,7 +240,16 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
this.oidcAppRequest.setPostLogoutRedirectUrisList(value);
|
||||
}
|
||||
|
||||
public get devMode() {
|
||||
return this.oidcAppRequest.toObject().devMode;
|
||||
}
|
||||
|
||||
public set devMode(value: boolean) {
|
||||
this.oidcAppRequest.setDevMode(value);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.devMode = false;
|
||||
this.subscription = this.route.params.subscribe((params) => this.getData(params));
|
||||
|
||||
const projectId = this.route.snapshot.paramMap.get('projectid');
|
||||
@ -362,9 +372,9 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public createApp(): void {
|
||||
const appOIDCCheck = this.devmode ? this.isDevOIDC : this.isStepperOIDC;
|
||||
const appAPICheck = this.devmode ? this.isDevAPI : this.isStepperAPI;
|
||||
const appSAMLCheck = this.devmode ? this.isDevSAML : this.isStepperSAML;
|
||||
const appOIDCCheck = this.pro ? this.isDevOIDC : this.isStepperOIDC;
|
||||
const appAPICheck = this.pro ? this.isDevAPI : this.isStepperAPI;
|
||||
const appSAMLCheck = this.pro ? this.isDevSAML : this.isStepperSAML;
|
||||
|
||||
if (appOIDCCheck) {
|
||||
this.requestRedirectValuesSubject$.next();
|
||||
|
@ -427,7 +427,7 @@
|
||||
|
||||
<div class="app-info-row">
|
||||
<div class="app-info-wrapper" *ngFor="let wellKnownV of wellKnownMap$ | async | keyvalue">
|
||||
<ng-container *ngIf="wellKnownV.key.endsWith('endpoint')">
|
||||
<ng-container *ngIf="wellKnownV.key.endsWith('endpoint') || wellKnownV.key.toString() === 'jwks_uri'">
|
||||
<p class="app-info-row-title cnsl-secondary-text">{{ wellKnownV.key }}</p>
|
||||
<div class="app-copy-row">
|
||||
<div *ngIf="wellKnownV.value" class="environment">
|
||||
|
@ -3,6 +3,7 @@ import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
|
||||
import { BehaviorSubject, from, lastValueFrom, Observable } from 'rxjs';
|
||||
|
||||
import { StatehandlerService } from './statehandler/statehandler.service';
|
||||
import { ToastService } from './toast.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@ -15,6 +16,7 @@ export class AuthenticationService {
|
||||
constructor(
|
||||
private oauthService: OAuthService,
|
||||
private statehandler: StatehandlerService,
|
||||
private toast: ToastService,
|
||||
) {}
|
||||
|
||||
public initConfig(data: AuthConfig): void {
|
||||
@ -39,7 +41,10 @@ export class AuthenticationService {
|
||||
}
|
||||
this.oauthService.configure(this.authConfig);
|
||||
this.oauthService.strictDiscoveryDocumentValidation = false;
|
||||
await this.oauthService.loadDiscoveryDocumentAndTryLogin();
|
||||
await this.oauthService.loadDiscoveryDocumentAndTryLogin().catch((error) => {
|
||||
this.toast.showError(error, false, false);
|
||||
});
|
||||
|
||||
this._authenticated = this.oauthService.hasValidAccessToken();
|
||||
if (!this.oauthService.hasValidIdToken() || !this.authenticated || partialConfig || force) {
|
||||
const newState = await lastValueFrom(this.statehandler.createState());
|
||||
|
@ -1,19 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SortDirection } from '@angular/material/sort';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import { BehaviorSubject, from, merge, Observable, of, Subject } from 'rxjs';
|
||||
import {
|
||||
catchError,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
finalize,
|
||||
map,
|
||||
mergeMap,
|
||||
switchMap,
|
||||
take,
|
||||
timeout,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
import { BehaviorSubject, forkJoin, from, Observable, of, Subject } from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, filter, finalize, map, switchMap, timeout, withLatestFrom } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
AddMyAuthFactorOTPEmailRequest,
|
||||
@ -184,25 +173,21 @@ export class GrpcAuthService {
|
||||
},
|
||||
});
|
||||
|
||||
this.user = merge(
|
||||
of(this.oauthService.getAccessToken()).pipe(filter((token) => (token ? true : false))),
|
||||
this.user = forkJoin([
|
||||
of(this.oauthService.getAccessToken()),
|
||||
this.oauthService.events.pipe(
|
||||
filter((e) => e.type === 'token_received'),
|
||||
timeout(this.oauthService.waitForTokenInMsec || 0),
|
||||
catchError((_) => of(null)), // timeout is not an error
|
||||
map((_) => this.oauthService.getAccessToken()),
|
||||
),
|
||||
).pipe(
|
||||
take(1),
|
||||
mergeMap(() => {
|
||||
]).pipe(
|
||||
filter((token) => (token ? true : false)),
|
||||
distinctUntilChanged(),
|
||||
switchMap(() => {
|
||||
return from(
|
||||
this.getMyUser().then((resp) => {
|
||||
const user = resp.user;
|
||||
if (user) {
|
||||
return user;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
return resp.user;
|
||||
}),
|
||||
);
|
||||
}),
|
||||
|
@ -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": "Доставчици на идентичност",
|
||||
@ -1975,6 +1977,8 @@
|
||||
"CLIENTSECRET_DESCRIPTION": "Пазете клиентската си тайна на сигурно място, тъй като тя ще изчезне, след като диалоговият прозорец бъде затворен.",
|
||||
"REGENERATESECRET": "Повторно генериране на клиентска тайна",
|
||||
"DEVMODE": "Режим на разработка",
|
||||
"DEVMODE_ENABLED": "Активиран",
|
||||
"DEVMODE_DISABLED": "Деактивиран",
|
||||
"DEVMODEDESC": "Внимание: При активиран режим на разработка URI адресите за пренасочване няма да бъдат валидирани.",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE": "Пропуснете страницата за успешно влизане",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Пропуснете страницата за успех след влизане в това родно приложение.",
|
||||
|
@ -1019,6 +1019,8 @@
|
||||
"LOCKOUT": "Sperrmechanismen",
|
||||
"COMPLEXITY": "Passwordkomplexität",
|
||||
"NOTIFICATIONS": "Benachrichtigungseinstellungen",
|
||||
"SMTP_PROVIDER": "SMTP-Anbieter",
|
||||
"SMS_PROVIDER": "SMS / Telefon Anbieter",
|
||||
"NOTIFICATIONS_DESC": "SMTP und SMS Einstellungen",
|
||||
"MESSAGETEXTS": "Benachrichtigungstexte",
|
||||
"IDP": "Identitätsanbieter",
|
||||
@ -1984,6 +1986,8 @@
|
||||
"CLIENTSECRET_DESCRIPTION": "Verwahre das Client Secret an einem sicheren Ort, da es nicht mehr angezeigt werden kann, sobald der Dialog geschlossen wird.",
|
||||
"REGENERATESECRET": "Client Secret neu generieren",
|
||||
"DEVMODE": "Entwicklermodus",
|
||||
"DEVMODE_ENABLED": "Aktiviert",
|
||||
"DEVMODE_DISABLED": "Deaktiviert",
|
||||
"DEVMODEDESC": "Bei eingeschaltetem Entwicklermodus werden die Weiterleitungs-URIs im OIDC-Flow nicht validiert.",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE": "Login Erfolgseite überspringen",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Erfolgseite nach dem Login für diese Native Applikation überspringen.",
|
||||
|
@ -1019,7 +1019,9 @@
|
||||
"LOGIN": "Login Behavior and Security",
|
||||
"LOCKOUT": "Lockout",
|
||||
"COMPLEXITY": "Password complexity",
|
||||
"NOTIFICATIONS": "Notification settings",
|
||||
"NOTIFICATIONS": "Notifications",
|
||||
"SMTP_PROVIDER": "SMTP Provider",
|
||||
"SMS_PROVIDER": "SMS/Phone Provider",
|
||||
"NOTIFICATIONS_DESC": "SMTP and SMS Settings",
|
||||
"MESSAGETEXTS": "Message Texts",
|
||||
"IDP": "Identity Providers",
|
||||
@ -1993,6 +1995,8 @@
|
||||
"CLIENTSECRET_DESCRIPTION": "Keep your client secret at a safe place as it will disappear once the dialog is closed.",
|
||||
"REGENERATESECRET": "Regenerate Client Secret",
|
||||
"DEVMODE": "Development Mode",
|
||||
"DEVMODE_ENABLED": "Enabled",
|
||||
"DEVMODE_DISABLED": "Disabled",
|
||||
"DEVMODEDESC": "Beware: With development mode enabled redirect URIs will not be validated.",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE": "Skip Login Success Page",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Skip the success page after a login for this native app.",
|
||||
|
@ -1020,6 +1020,8 @@
|
||||
"LOCKOUT": "Bloqueo",
|
||||
"COMPLEXITY": "Complejidad de contraseña",
|
||||
"NOTIFICATIONS": "Ajustes de notificación",
|
||||
"SMTP_PROVIDER": "Proveedor SMTP",
|
||||
"SMS_PROVIDER": "Proveedor SMS/Teléfono",
|
||||
"NOTIFICATIONS_DESC": "Ajustes SMTP y SMS",
|
||||
"MESSAGETEXTS": "Mensajes de texto",
|
||||
"IDP": "Proveedores de identidad",
|
||||
@ -1981,6 +1983,8 @@
|
||||
"CLIENTSECRET_DESCRIPTION": "Mantén tu secreto de cliente en un lugar seguro puesto que desaparecerá una vez que se cierre el diálogo.",
|
||||
"REGENERATESECRET": "Regenerar secreto de cliente",
|
||||
"DEVMODE": "Modo Desarrollo",
|
||||
"DEVMODE_ENABLED": "Activado",
|
||||
"DEVMODE_DISABLED": "Desactivado",
|
||||
"DEVMODEDESC": "Cuidado: Si el modo de desarrollo está activado las URIs de redirección no serán validadas.",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE": "Saltar página de inicio de sesión con éxito",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Sáltate la página de éxito después de iniciar sesión en esta app nativa.",
|
||||
|
@ -1019,6 +1019,8 @@
|
||||
"LOCKOUT": "Verrouillage",
|
||||
"COMPLEXITY": "Complexité du mot de passe",
|
||||
"NOTIFICATIONS": "Paramètres de notification",
|
||||
"SMTP_PROVIDER": "Fournisseur SMTP",
|
||||
"SMS_PROVIDER": "SMS/Téléphone Fournisseur",
|
||||
"NOTIFICATIONS_DESC": "Paramètres SMTP et SMS",
|
||||
"MESSAGETEXTS": "Textes des messages",
|
||||
"IDP": "Fournisseurs d'identité",
|
||||
@ -1985,6 +1987,8 @@
|
||||
"CLIENTSECRET_DESCRIPTION": "Conservez votre secret client dans un endroit sûr car il disparaîtra une fois la boîte de dialogue fermée.",
|
||||
"REGENERATESECRET": "Régénérer le secret du client",
|
||||
"DEVMODE": "Mode développement",
|
||||
"DEVMODE_ENABLED": "Activé",
|
||||
"DEVMODE_DISABLED": "Désactivé",
|
||||
"DEVMODEDESC": "Attention",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE": "Sauter la page de succès de connexion",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Sauter la page de succès après la connexion pour cette application native",
|
||||
|
@ -1019,6 +1019,8 @@
|
||||
"LOCKOUT": "Meccanismi di bloccaggio",
|
||||
"COMPLEXITY": "Complessità della password",
|
||||
"NOTIFICATIONS": "Impostazioni di notifica",
|
||||
"SMTP_PROVIDER": "Fornitore SMTP",
|
||||
"SMS_PROVIDER": "Fornitore di servizi SMS/telefonici",
|
||||
"NOTIFICATIONS_DESC": "Impostazioni SMTP e SMS",
|
||||
"MESSAGETEXTS": "Testi di notifica",
|
||||
"IDP": "Fornitori di identità",
|
||||
@ -1985,6 +1987,8 @@
|
||||
"CLIENTSECRET_DESCRIPTION": "Salvate il Client Secret in un luogo sicuro, perch\u00e9 non sarà più disponibile dopo aver chiuso la finestra di dialogo",
|
||||
"REGENERATESECRET": "Rigenera il Client Secret",
|
||||
"DEVMODE": "Modalit\u00e0 di sviluppo (DEV Mode)",
|
||||
"DEVMODE_ENABLED": "Attivato",
|
||||
"DEVMODE_DISABLED": "Disattivato",
|
||||
"DEVMODEDESC": "Attenzione: Con la modalit\u00e0 di sviluppo abilitata, gli URI di reindirizzamento non saranno convalidati.",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE": "Salta la pagina di successo dopo il login",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Salta la pagina di successo dopo il login per questa applicazione nativa",
|
||||
|
@ -1020,6 +1020,8 @@
|
||||
"LOCKOUT": "ロックアウト",
|
||||
"COMPLEXITY": "パスワードの複雑さ",
|
||||
"NOTIFICATIONS": "通知設定",
|
||||
"SMTP_PROVIDER": "SMTPプロバイダー",
|
||||
"SMS_PROVIDER": "SMS/電話プロバイダー",
|
||||
"NOTIFICATIONS_DESC": "SMTPおよびSMS設定",
|
||||
"MESSAGETEXTS": "メッセージテキスト",
|
||||
"IDP": "IDプロバイダー",
|
||||
@ -1976,6 +1978,8 @@
|
||||
"CLIENTSECRET_DESCRIPTION": "クライアントシークレットは、ダイアログを閉じると消えてしまうので、安全な場所に保管してください。",
|
||||
"REGENERATESECRET": "クライアントシークレットを再生成する",
|
||||
"DEVMODE": "開発モード",
|
||||
"DEVMODE_ENABLED": "アクティブ化された",
|
||||
"DEVMODE_DISABLED": "無効化されました",
|
||||
"DEVMODEDESC": "注意:開発モードを有効にすると、URIが認証されません。",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE": "ログイン後に成功ページをスキップする",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "このネイティブアプリのログイン後に成功ページをスキップする",
|
||||
|
@ -1021,6 +1021,8 @@
|
||||
"LOCKOUT": "Забрана на пристап",
|
||||
"COMPLEXITY": "Сложеност на лозинката",
|
||||
"NOTIFICATIONS": "Подесувања за известувања",
|
||||
"SMTP_PROVIDER": "SMTP провајдер",
|
||||
"SMS_PROVIDER": "СМС/Провајдер на телефон",
|
||||
"NOTIFICATIONS_DESC": "Подесувања за SMTP и SMS",
|
||||
"MESSAGETEXTS": "Текстови на пораки",
|
||||
"IDP": "Identity Providers",
|
||||
@ -1982,6 +1984,8 @@
|
||||
"CLIENTSECRET_DESCRIPTION": "Чувајте ја вашата клиентска тајна на безбедно место, бидејќи ќе исчезне откако ќе се затвори дијалогот.",
|
||||
"REGENERATESECRET": "Генерирај нова клиентска тајна",
|
||||
"DEVMODE": "Development mode",
|
||||
"DEVMODE_ENABLED": "Активиран",
|
||||
"DEVMODE_DISABLED": "Деактивирано",
|
||||
"DEVMODEDESC": "Внимавајте: Со овозможен Development mode, URIs за пренасочување нема да бидат валидирани.",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE": "Прескокни страница за успешна најава",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Прескокнете ја страницата за успешна најава за оваа нативна апликација.",
|
||||
|
@ -1019,6 +1019,8 @@
|
||||
"LOCKOUT": "Blokada",
|
||||
"COMPLEXITY": "Złożoność hasła",
|
||||
"NOTIFICATIONS": "Ustawienia powiadomień",
|
||||
"SMTP_PROVIDER": "Dostawca SMTP",
|
||||
"SMS_PROVIDER": "Dostawca SMS-ów/telefonów",
|
||||
"NOTIFICATIONS_DESC": "Ustawienia SMTP i SMS",
|
||||
"MESSAGETEXTS": "Teksty wiadomości",
|
||||
"IDP": "Dostawcy tożsamości",
|
||||
@ -1985,6 +1987,8 @@
|
||||
"CLIENTSECRET_DESCRIPTION": "Trzymaj swój sekret klienta w bezpiecznym miejscu, ponieważ zniknie on po zamknięciu okna dialogowego.",
|
||||
"REGENERATESECRET": "Odtwórz sekret klienta",
|
||||
"DEVMODE": "Tryb rozwoju",
|
||||
"DEVMODE_ENABLED": "Aktywowany",
|
||||
"DEVMODE_DISABLED": "Dezaktywowane",
|
||||
"DEVMODEDESC": "Uwaga: przy włączonym trybie rozwoju adresy URI przekierowania nie będą sprawdzane.",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE": "Pomiń stronę sukcesu po zalogowaniu",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Pomiń stronę sukcesu po zalogowaniu dla tej Natywny aplikację",
|
||||
|
@ -1021,6 +1021,8 @@
|
||||
"LOCKOUT": "Bloqueio",
|
||||
"COMPLEXITY": "Complexidade de Senha",
|
||||
"NOTIFICATIONS": "Configurações de Notificação",
|
||||
"SMTP_PROVIDER": "Provedor SMTP",
|
||||
"SMS_PROVIDER": "Provedor de SMS/Telefone",
|
||||
"NOTIFICATIONS_DESC": "Configurações de SMTP e SMS",
|
||||
"MESSAGETEXTS": "Textos de Mensagem",
|
||||
"IDP": "Provedores de Identidade",
|
||||
@ -1980,6 +1982,8 @@
|
||||
"CLIENTSECRET_DESCRIPTION": "Mantenha o segredo do cliente em um local seguro, pois ele desaparecerá assim que o diálogo for fechado.",
|
||||
"REGENERATESECRET": "Regenerar Segredo do Cliente",
|
||||
"DEVMODE": "Modo de Desenvolvimento",
|
||||
"DEVMODE_ENABLED": "Ativado",
|
||||
"DEVMODE_DISABLED": "Desativado",
|
||||
"DEVMODEDESC": "Atenção: Com o modo de desenvolvimento habilitado, as URIs de redirecionamento não serão validadas.",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE": "Pular Página de Sucesso de Login",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Pule a página de sucesso após o login para este aplicativo nativo.",
|
||||
|
@ -1019,6 +1019,8 @@
|
||||
"LOCKOUT": "安全锁策略",
|
||||
"COMPLEXITY": "密码复杂性",
|
||||
"NOTIFICATIONS": "通知设置",
|
||||
"SMTP_PROVIDER": "SMTP 提供商",
|
||||
"SMS_PROVIDER": "短信/电话提供商",
|
||||
"NOTIFICATIONS_DESC": "SMTP 和 SMS 设置",
|
||||
"MESSAGETEXTS": "消息文本",
|
||||
"IDP": "身份提供者",
|
||||
@ -1984,6 +1986,8 @@
|
||||
"CLIENTSECRET_DESCRIPTION": "将您的客户保密在一个安全的地方,因为一旦对话框关闭,便无法再次查看。",
|
||||
"REGENERATESECRET": "重新生成客户端密钥",
|
||||
"DEVMODE": "开发模式",
|
||||
"DEVMODE_ENABLED": "活性",
|
||||
"DEVMODE_DISABLED": "已停用",
|
||||
"DEVMODEDESC": "注意:启用开发模式的重定向 URI 将不会被验证。",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE": "登录后跳过成功页面",
|
||||
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "登录后跳过本机应用的成功页面",
|
||||
|
69
docs/docs/guides/integrate/identity-providers/keycloak.mdx
Normal file
@ -0,0 +1,69 @@
|
||||
---
|
||||
title: Configure Keycloak as an Identity Provider in ZITADEL
|
||||
sidebar_label: Keycloak generic OIDC
|
||||
id: keycloak
|
||||
---
|
||||
|
||||
import GeneralConfigDescription from './_general_config_description.mdx';
|
||||
import Intro from './_intro.mdx';
|
||||
import CustomLoginPolicy from './_custom_login_policy.mdx';
|
||||
import IDPsOverview from './_idps_overview.mdx';
|
||||
import GenericOIDC from './_generic_oidc.mdx';
|
||||
import Activate from './_activate.mdx';
|
||||
import TestSetup from './_test_setup.mdx';
|
||||
|
||||
<Intro provider="Keycloak"/>
|
||||
|
||||
## Keycloak Configuration
|
||||
|
||||
### Register a new client
|
||||
|
||||
1. Login to your Keycloak account and go to the clients list: <$KEYCLOAK-DOMAIN/auth/admin/$REALM/console/#/$REALM/clients>
|
||||
2. Click on "Create Client"
|
||||
3. Choose OpenID Connect as Client Type and give your client an ID
|
||||
4. Enable Client authentication and the standard flow and direct access grants as authentication flow
|
||||
5. Add the valid redirect URIs
|
||||
- {your-domain}/ui/login/login/externalidp/callback
|
||||
- Example redirect url for the domain `https://acme-gzoe4x.zitadel.cloud` would look like this: `https://acme-gzoe4x.zitadel.cloud/ui/login/login/externalidp/callback`
|
||||
6. Go to the credentials tab and copy the secret
|
||||
|
||||

|
||||

|
||||
|
||||
## ZITADEL configuration
|
||||
|
||||
### Add custom login policy
|
||||
|
||||
<CustomLoginPolicy/>
|
||||
|
||||
### Go to the IdP providers overview
|
||||
|
||||
<IDPsOverview templates="Generic OIDC"/>
|
||||
|
||||
### Create a new generic OIDC provider
|
||||
|
||||
<GenericOIDC
|
||||
name=": e.g. Keycloak"
|
||||
issuer=": The domain where your Keycloak can be reached with the path /auth/realms/$REALM, Example: https://lemur-0.cloud-iam.com/auth/realms/acme"
|
||||
clientid=": Client id from the client previously created in your Keycloak account"
|
||||
/>
|
||||
|
||||
|
||||
<GeneralConfigDescription provider_account="Keycloak account" />
|
||||
|
||||

|
||||
|
||||
### Activate IdP
|
||||
|
||||
<Activate/>
|
||||
|
||||

|
||||
|
||||
## Test the setup
|
||||
|
||||
<TestSetup loginscreen="your Keycloak login"/>
|
||||
|
||||
|
||||

|
||||
|
||||

|
@ -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.
|
@ -20,7 +20,7 @@ This means that the users and also their authorizations will be managed within Z
|
||||
An organization is the ZITADEL resource which contains users, projects, applications, policies and so on.
|
||||
In an organization projects and users are managed by the organization.
|
||||
You need at least one organization for your own company in our case "The Timing Company".
|
||||
As next step grate an organization for each of your costumers.
|
||||
Your next step is to create an organization for each of your customers.
|
||||
|
||||
## Project
|
||||
|
||||
@ -39,7 +39,7 @@ You can configure `check roles on authentication` on the project, if you want to
|
||||
|
||||
To give a customer permissions to a project, a project grant to the customers organization is needed (search the granted organization by its domain).
|
||||
It is also possible to delegate only specific roles of the project to a certain customer.
|
||||
As soon as a project grant exists, the customer will see the project in the granted projects section of his organization and will be able to authorize his own users to the given project.
|
||||
As soon as a project grant exists, the customer will see the project in the granted projects section of their organization and will be able to authorize their own users to the given project.
|
||||
|
||||
## Authorizations
|
||||
|
||||
|
@ -6,14 +6,14 @@ title: Technical Advisory 10002
|
||||
|
||||
Version: TBD
|
||||
|
||||
Date: Calendar week 40/41
|
||||
Date: Calendar week 44
|
||||
|
||||
## Description
|
||||
|
||||
Since Angular Material v15 many of the UI components have been refactored
|
||||
to be based on the official Material Design Components for Web (MDC).
|
||||
These refactored components do not support dynamic styling, so in order to keep the library up-to-date,
|
||||
the console UI will loose its dynamic theming capability.
|
||||
the console UI will lose its dynamic theming capability.
|
||||
|
||||
## Statement
|
||||
|
||||
@ -23,7 +23,7 @@ As soon as the release version is published, we will include the version here.
|
||||
## Mitigation
|
||||
|
||||
If you need users to have your branding settings
|
||||
(background-, button-, link and text coloring), you should implemement your
|
||||
(background-, button-, link and text coloring), you should implement your
|
||||
own user facing UI yourself and not use ZITADELs console UI. Assets like your logo and icons will still be used.
|
||||
|
||||
## Impact
|
||||
|
46
docs/docs/support/advisory/a10003.md
Normal file
@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Technical Advisory 10003
|
||||
---
|
||||
|
||||
## Date and Version
|
||||
|
||||
Version: 2.38.0
|
||||
|
||||
Date: Calendar week 41
|
||||
|
||||
## Description
|
||||
|
||||
When users are redirected to the ZITADEL Login-UI without any organizational context, they're currently presented a login screen,
|
||||
based on the instance settings, e.g. available IDPs and possible login mechanisms. If the user will then register himself,
|
||||
by the registration form or through an IDP, the user will always be created on the default organization.
|
||||
|
||||
This behaviour led to confusion, e.g. when activating IDPs on default org would not show up in the Login-UI, because they would still be loaded from the instance settings.
|
||||
|
||||
To improve this, we're introducing the following change:
|
||||
If users are redirected to the Login-UI without any organizational context, they will be presented a login screen based on the settings of the default organization (incl. IDPs).
|
||||
|
||||
:::note
|
||||
If the registration (and also authentication) needs to occur on a specified organization, apps can already
|
||||
specify this by providing [an organization scope](https://zitadel.com/docs/apis/openidoauth/scopes#reserved-scopes).
|
||||
:::
|
||||
|
||||
## Statement
|
||||
|
||||
This change was tracked in the following PR:
|
||||
[feat(login): use default org for login without provided org context](https://github.com/zitadel/zitadel/pull/6625), which was released in Version [2.38.0](https://github.com/zitadel/zitadel/releases/tag/v2.38.0)
|
||||
|
||||
## Mitigation
|
||||
|
||||
There's no action needed on your side currently as existing instances are not affected directly and IAM_OWNER can activate the flag at their own pace.
|
||||
|
||||
## Impact
|
||||
|
||||
Once this update has been released and deployed, newly created instances will always use the default organization and its settings as default context for the login.
|
||||
|
||||
Already existing instances will still use the instance settings by default and can switch to the new default by ["Activating the 'LoginDefaultOrg' feature"](https://zitadel.com/docs/apis/resources/admin/admin-service-activate-feature-login-default-org) through the Admin API.
|
||||
**This change is irreversible!**
|
||||
|
||||
:::note
|
||||
Regardless of the change:
|
||||
If a known username is entered on the first screen, the login switches its context to the organization of that user and settings will be updated to that organization as well.
|
||||
:::
|
37
docs/docs/support/advisory/a10004.md
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Technical Advisory 10004
|
||||
---
|
||||
|
||||
## Date and Version
|
||||
|
||||
Version: 2.39.0
|
||||
|
||||
Date: 2023-10-14
|
||||
|
||||
## Description
|
||||
|
||||
Due to storage optimisations ZITADEL changes the behaviour of sequences.
|
||||
This change improves command (create, update, delete) performance of ZITADEL.
|
||||
|
||||
Sequences are no longer unique inside an instance.
|
||||
From now on sequences are upcounting per aggregate id.
|
||||
For example sequences of newly created users begin at 1.
|
||||
Existing sequences remain untouched.
|
||||
|
||||
## Statement
|
||||
|
||||
This change is tracked in the following PR: [new eventstore framework](https://github.com/zitadel/zitadel/issues/5358).
|
||||
As soon as the release version is published, we will include the version here.
|
||||
|
||||
## Mitigation
|
||||
|
||||
If you use the ListEvents API to scrape events use the creation date instead of the sequence.
|
||||
If you use sequences on a list of objects it's no longer garanteed to have unique sequences across the list.
|
||||
Therefore it's recommended to use the change data of the objects instead.
|
||||
|
||||
## Impact
|
||||
|
||||
Once this update has been released and deployed, sequences are no longer unique inside an instance.
|
||||
ZITADEL will increase parallel write capabilities, because there is no global sequence to track anymore.
|
||||
Editor service does not respond the different services of ZITADEL anymore, it returns zitadel.
|
||||
As we are switching to resource based API's there is no need for this field anymore.
|
33
docs/docs/support/advisory/a10005.md
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
title: Technical Advisory 10005
|
||||
---
|
||||
|
||||
## Date and Version
|
||||
|
||||
Version: 2.39.0
|
||||
|
||||
Date: Calendar week 41/42 2023
|
||||
|
||||
## Description
|
||||
|
||||
Migrating to version >= 2.39 from < 2.39 will cause down time during setup starts and the new version is started.
|
||||
This is caused by storage optimisations which replace the `eventstore.events` database table with the new `eventstore.events2` table.
|
||||
All existing events are migrated during the execution of the `zitadel setup` command.
|
||||
New events will be inserted into the new `eventstore.events2` table. The old table `evetstore.events` is renamed to `eventstore.events_old` and will be dropped in a future release of ZITADEL.
|
||||
|
||||
## Statement
|
||||
|
||||
This change is tracked in the following PR: [new eventstore framework](https://github.com/zitadel/zitadel/issues/5358).
|
||||
As soon as the release version is published, we will include the version here.
|
||||
|
||||
## Mitigation
|
||||
|
||||
If you use this table for TRIGGERS or Change data capture please check the new table definition and change your code to use `eventstore.events2`.
|
||||
|
||||
## Impact
|
||||
|
||||
Once the setup step renamed the table. Old versions of ZITADEL are not able to read/write events.
|
||||
|
||||
:::note
|
||||
If the upgrade fails make sure to rename `eventstore.events_old` to `eventstore.events`. This change enables older ZITADEL versions to work properly.
|
||||
:::
|
@ -68,7 +68,55 @@ We understand that these advisories may include breaking changes, and we aim to
|
||||
ZITADEL hosted Login-UI is not affected by this change.
|
||||
</td>
|
||||
<td>TBD</td>
|
||||
<td>Calendar week 40/41</td>
|
||||
<td>Calendar week 44</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="./advisory/a10003">A-10003</a>
|
||||
</td>
|
||||
<td>Login-UI - Default Context</td>
|
||||
<td>Breaking Behaviour Change</td>
|
||||
<td>
|
||||
When users are redirected to the ZITADEL Login-UI without any organizational context,
|
||||
they're currently presented a login screen, based on the instance settings,
|
||||
e.g. available IDPs and possible login mechanisms. If the user will then register himself,
|
||||
by the registration form or through an IDP, the user will always be created on the default organization.
|
||||
With the introduced change, the settings will no longer be loaded from the instance, but rather the default organization directly.
|
||||
</td>
|
||||
<td>2.38.0</td>
|
||||
<td>Calendar week 41</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="./advisory/a10004">A-10004</a>
|
||||
</td>
|
||||
<td>Sequence uniquenes</td>
|
||||
<td>Breaking Behaviour Change</td>
|
||||
<td>
|
||||
Due to storage optimisations ZITADEL changes the behaviour of sequences.
|
||||
This change improves command (create, update, delete) performance of ZITADEL.
|
||||
Sequences are no longer unique inside an instance.
|
||||
From now on sequences are upcounting per aggregate id.
|
||||
For example sequences of newly created users begin at 1.
|
||||
Existing sequences remain untouched.
|
||||
</td>
|
||||
<td>2.39.0</td>
|
||||
<td>2023-10-14</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="./advisory/a10005">A-10005</a>
|
||||
</td>
|
||||
<td>Expected downtime during upgrade</td>
|
||||
<td>Expected downtime during upgrade</td>
|
||||
<td>
|
||||
Migrating to versions >= 2.39 from < 2.39 will cause down time during setup starts and the new version is started.
|
||||
This is caused by storage optimisations which replace the `eventstore.events` database table with the new `eventstore.events2` table.
|
||||
All existing events are migrated during the execution of the `zitadel setup` command.
|
||||
New events will be inserted into the new `eventstore.events2` table. The old table `evetstore.events` is renamed to `eventstore.events_old` and will be dropped in a future release of ZITADEL.
|
||||
</td>
|
||||
<td>2.39.0</td>
|
||||
<td>Calendar week 41/42 2023</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
@ -125,6 +125,7 @@ module.exports = {
|
||||
items: [
|
||||
"guides/migrate/sources/zitadel",
|
||||
"guides/migrate/sources/auth0",
|
||||
"guides/migrate/sources/keycloak",
|
||||
]
|
||||
},
|
||||
]
|
||||
@ -243,6 +244,7 @@ module.exports = {
|
||||
"guides/integrate/identity-providers/openldap",
|
||||
"guides/integrate/identity-providers/migrate",
|
||||
"guides/integrate/identity-providers/okta",
|
||||
"guides/integrate/identity-providers/keycloak",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
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/keycloak_add_client.png
vendored
Normal file
After Width: | Height: | Size: 339 KiB |
BIN
docs/static/img/guides/keycloak_client_secret.png
vendored
Normal file
After Width: | Height: | Size: 286 KiB |
BIN
docs/static/img/guides/keycloak_login.png
vendored
Normal file
After Width: | Height: | Size: 148 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 |