Merge branch 'main' into next

# Conflicts:
#	docs/docs/guides/integrate/login-ui/external-login.mdx
#	internal/command/idp_model.go
#	proto/zitadel/user/v2alpha/user_service.proto
This commit is contained in:
Livio Spring 2023-09-05 07:04:42 +02:00
commit 38f7b1bd06
No known key found for this signature in database
GPG Key ID: 26BB1C2FA5952CF0
172 changed files with 10138 additions and 2294 deletions

View File

@ -17,7 +17,7 @@ jobs:
with: with:
node_version: '18' node_version: '18'
buf_version: 'latest' buf_version: 'latest'
go_version: '1.20' go_version: '1.21'
console: console:
uses: ./.github/workflows/console.yml uses: ./.github/workflows/console.yml
@ -35,7 +35,7 @@ jobs:
needs: [core, console, version] needs: [core, console, version]
uses: ./.github/workflows/compile.yml uses: ./.github/workflows/compile.yml
with: with:
go_version: '1.20' go_version: '1.21'
core_cache_key: ${{ needs.core.outputs.cache_key }} core_cache_key: ${{ needs.core.outputs.cache_key }}
console_cache_key: ${{ needs.console.outputs.cache_key }} console_cache_key: ${{ needs.console.outputs.cache_key }}
core_cache_path: ${{ needs.core.outputs.cache_path }} core_cache_path: ${{ needs.core.outputs.cache_path }}
@ -46,7 +46,7 @@ jobs:
needs: core needs: core
uses: ./.github/workflows/core-unit-test.yml uses: ./.github/workflows/core-unit-test.yml
with: with:
go_version: '1.20' go_version: '1.21'
core_cache_key: ${{ needs.core.outputs.cache_key }} core_cache_key: ${{ needs.core.outputs.cache_key }}
core_cache_path: ${{ needs.core.outputs.cache_path }} core_cache_path: ${{ needs.core.outputs.cache_path }}
@ -54,7 +54,7 @@ jobs:
needs: core needs: core
uses: ./.github/workflows/core-integration-test.yml uses: ./.github/workflows/core-integration-test.yml
with: with:
go_version: '1.20' go_version: '1.21'
core_cache_key: ${{ needs.core.outputs.cache_key }} core_cache_key: ${{ needs.core.outputs.cache_key }}
core_cache_path: ${{ needs.core.outputs.cache_path }} core_cache_path: ${{ needs.core.outputs.cache_path }}
@ -62,7 +62,7 @@ jobs:
needs: [core, console] needs: [core, console]
uses: ./.github/workflows/lint.yml uses: ./.github/workflows/lint.yml
with: with:
go_version: '1.20' go_version: '1.21'
node_version: '18' node_version: '18'
buf_version: 'latest' buf_version: 'latest'
go_lint_version: 'v1.53.2' go_lint_version: 'v1.53.2'

View File

@ -8,7 +8,7 @@ issues:
run: run:
concurrency: 4 concurrency: 4
timeout: 10m timeout: 10m
go: '1.19' go: '1.21'
skip-dirs: skip-dirs:
- .artifacts - .artifacts
- .backups - .backups

View File

@ -135,7 +135,7 @@ ZITADEL uses [golangci-lint](https://golangci-lint.run) for code quality checks.
The commands in this section are tested against the following software versions: The commands in this section are tested against the following software versions:
- [Docker version 20.10.17](https://docs.docker.com/engine/install/) - [Docker version 20.10.17](https://docs.docker.com/engine/install/)
- [Go version 1.20](https://go.dev/doc/install) - [Go version 1.21](https://go.dev/doc/install)
- [Delve 1.9.1](https://github.com/go-delve/delve/tree/v1.9.1/Documentation/installation) - [Delve 1.9.1](https://github.com/go-delve/delve/tree/v1.9.1/Documentation/installation)
Make some changes to the source code, then run the database locally. Make some changes to the source code, then run the database locally.

View File

@ -4,21 +4,23 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/zitadel/zitadel/blob/main/LICENSE" alt="License">
<img src="https://badgen.net/github/license/zitadel/zitadel/" /></a>
<a href="https://bestpractices.coreinfrastructure.org/projects/6662"><img src="https://bestpractices.coreinfrastructure.org/projects/6662/badge"></a> <a href="https://bestpractices.coreinfrastructure.org/projects/6662"><img src="https://bestpractices.coreinfrastructure.org/projects/6662/badge"></a>
<a href="https://github.com/zitadel/zitadel/graphs/contributors" alt="Release">
<img src="https://badgen.net/github/contributors/zitadel/zitadel" /></a>
<a href="https://github.com/semantic-release/semantic-release" alt="semantic-release"> <a href="https://github.com/semantic-release/semantic-release" alt="semantic-release">
<img src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg" /></a> <img src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg" /></a>
<a href="https://github.com/zitadel/zitadel/actions" alt="ZITADEL Release"> <a href="https://github.com/zitadel/zitadel/actions" alt="ZITADEL Release">
<img src="https://github.com/zitadel/zitadel/actions/workflows/zitadel.yml/badge.svg" /></a> <img alt="GitHub Workflow Status (with event)" src="https://img.shields.io/github/actions/workflow/status/zitadel/zitadel/build.yml?event=pull_request"></a>
<a href="https://github.com/zitadel/zitadel/blob/main/LICENSE" alt="License">
<img src="https://badgen.net/github/license/zitadel/zitadel/" /></a>
<a href="https://github.com/zitadel/zitadel/releases" alt="Release"> <a href="https://github.com/zitadel/zitadel/releases" alt="Release">
<img src="https://badgen.net/github/release/zitadel/zitadel/stable" /></a> <img src="https://badgen.net/github/release/zitadel/zitadel/stable" /></a>
<a href="https://github.com/zitadel/zitadel/releases" alt="Release">
<img alt="Dynamic YAML Badge" src="https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fzitadel%2Fzitadel%2Fmain%2Frelease-channels.yaml&query=%24.stable&label=stable"></a>
<a href="https://goreportcard.com/report/github.com/zitadel/zitadel" alt="Go Report Card"> <a href="https://goreportcard.com/report/github.com/zitadel/zitadel" alt="Go Report Card">
<img src="https://goreportcard.com/badge/github.com/zitadel/zitadel" /></a> <img src="https://goreportcard.com/badge/github.com/zitadel/zitadel" /></a>
<a href="https://codecov.io/gh/zitadel/zitadel" alt="Code Coverage"> <a href="https://codecov.io/gh/zitadel/zitadel" alt="Code Coverage">
<img src="https://codecov.io/gh/zitadel/zitadel/branch/main/graph/badge.svg" /></a> <img src="https://codecov.io/gh/zitadel/zitadel/branch/main/graph/badge.svg" /></a>
<a href="https://github.com/zitadel/zitadel/graphs/contributors" alt="Release">
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/zitadel/zitadel"></a>
<a href="https://discord.gg/erh5Brh7jE" alt="Discord Chat"> <a href="https://discord.gg/erh5Brh7jE" alt="Discord Chat">
<img src="https://badgen.net/discord/online-members/erh5Brh7jE" /></a> <img src="https://badgen.net/discord/online-members/erh5Brh7jE" /></a>
</p> </p>
@ -94,7 +96,7 @@ Authentication
- Single Sign On (SSO) - Single Sign On (SSO)
- Passwordless with FIDO2 support (Including Passkeys) - Passwordless with FIDO2 support (Including Passkeys)
- Username / Password - Username / Password
- Multifactor authentication with OTP, U2F - Multifactor authentication with OTP, U2F, Email OTP, SMS OTP
- LDAP - LDAP
- [OpenID Connect certified](https://openid.net/certification/#OPs) => [OIDC Endpoints](https://zitadel.com/docs/apis/openidoauth/endpoints) - [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) - [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)
@ -103,12 +105,12 @@ Authentication
Multi-Tenancy Multi-Tenancy
- [Identity Brokering](https://zitadel.com/docs/guides/integrate/identity-brokering) with templates for popular identity providers - [Identity Brokering](https://zitadel.com/docs/guides/integrate/identity-brokering) with templates for popular identity providers
- [Delegate role management to third-parties](https://zitadel.com/docs/guides/manage/console/projects) - [Delegate role management to third-parties](https://zitadel.com/docs/guides/manage/console/projects)
- Domain discovery - [Domain discovery](https://zitadel.com/docs/guides/solution-scenarios/domain-discovery)
Integration Integration
- [GRPC and REST APIs](https://zitadel.com/docs/apis/introduction) - [GRPC and REST APIs](https://zitadel.com/docs/apis/introduction)
- [Actions](https://zitadel.com/docs/apis/actions/introduction) to call any API, send webhooks, adjust workflows, or customize tokens - [Actions](https://zitadel.com/docs/apis/actions/introduction) to call any API, send webhooks, adjust workflows, or customize tokens
- Role Based Access Control (RBAC) - [Role Based Access Control (RBAC)](https://zitadel.com/docs/guides/integrate/retrieve-user-roles)
Self-Service Self-Service
- [Self-registration](https://zitadel.com/docs/concepts/features/selfservice#registration) including verification - [Self-registration](https://zitadel.com/docs/concepts/features/selfservice#registration) including verification

View File

@ -307,6 +307,7 @@ Login:
MaxAge: 12h # ZITADEL_LOGIN_CACHE_MAXAGE MaxAge: 12h # ZITADEL_LOGIN_CACHE_MAXAGE
# 168h is 7 days, one week # 168h is 7 days, one week
SharedMaxAge: 168h # ZITADEL_LOGIN_CACHE_SHAREDMAXAGE SharedMaxAge: 168h # ZITADEL_LOGIN_CACHE_SHAREDMAXAGE
DefaultOTPEmailURLV2: "/otp/verify?loginName={{.LoginName}}&code={{.Code}}" # ZITADEL_LOGIN_CACHE_DEFAULTOTPEMAILURLV2
Console: Console:
ShortCache: ShortCache:
@ -370,10 +371,10 @@ SystemDefaults:
MachineKeySize: 2048 # ZITADEL_SYSTEMDEFAULTS_SECRETGENERATORS_MACHINEKEYSIZE MachineKeySize: 2048 # ZITADEL_SYSTEMDEFAULTS_SECRETGENERATORS_MACHINEKEYSIZE
ApplicationKeySize: 2048 # ZITADEL_SYSTEMDEFAULTS_SECRETGENERATORS_APPLICATIONKEYSIZE ApplicationKeySize: 2048 # ZITADEL_SYSTEMDEFAULTS_SECRETGENERATORS_APPLICATIONKEYSIZE
PasswordHasher: PasswordHasher:
# Set hasher configuration for user passwords. # Set hasher configuration for user passwords.
# Passwords previously hashed with a different algorithm # Passwords previously hashed with a different algorithm
# or cost are automatically re-hashed using this config, # or cost are automatically re-hashed using this config,
# upon password validation or update. # upon password validation or update.
Hasher: Hasher:
Algorithm: "bcrypt" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM Algorithm: "bcrypt" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM
Cost: 14 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_COST Cost: 14 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_COST
@ -687,6 +688,7 @@ DefaultInstance:
# If the host of the sender is different from ExternalDomain set DefaultInstance.DomainPolicy.SMTPSenderAddressMatchesInstanceDomain to false # If the host of the sender is different from ExternalDomain set DefaultInstance.DomainPolicy.SMTPSenderAddressMatchesInstanceDomain to false
From: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_FROM From: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_FROM
FromName: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_FROMNAME FromName: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_FROMNAME
ReplyToAddress: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_REPLYTOADDRESS
MessageTexts: MessageTexts:
- MessageTextType: InitCode - MessageTextType: InitCode
Language: de Language: de

View File

@ -222,7 +222,25 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
actionsLogstoreSvc := logstore.New(queries, usageReporter, actionsExecutionDBEmitter, actionsExecutionStdoutEmitter) actionsLogstoreSvc := logstore.New(queries, usageReporter, actionsExecutionDBEmitter, actionsExecutionStdoutEmitter)
actions.SetLogstoreService(actionsLogstoreSvc) actions.SetLogstoreService(actionsLogstoreSvc)
notification.Start(ctx, config.Projections.Customizations["notifications"], config.Projections.Customizations["notificationsquotas"], config.Projections.Customizations["telemetry"], *config.Telemetry, config.ExternalDomain, config.ExternalPort, config.ExternalSecure, commands, queries, eventstoreClient, assets.AssetAPIFromDomain(config.ExternalSecure, config.ExternalPort), config.SystemDefaults.Notifications.FileSystemPath, keys.User, keys.SMTP, keys.SMS) notification.Start(
ctx,
config.Projections.Customizations["notifications"],
config.Projections.Customizations["notificationsquotas"],
config.Projections.Customizations["telemetry"],
*config.Telemetry,
config.ExternalDomain,
config.ExternalPort,
config.ExternalSecure,
commands,
queries,
eventstoreClient,
assets.AssetAPIFromDomain(config.ExternalSecure, config.ExternalPort),
config.Login.DefaultOTPEmailURLV2,
config.SystemDefaults.Notifications.FileSystemPath,
keys.User,
keys.SMTP,
keys.SMS,
)
router := mux.NewRouter() router := mux.NewRouter()
tlsConfig, err := config.TLS.Config() tlsConfig, err := config.TLS.Config()
@ -362,7 +380,7 @@ func startAPIs(
apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, config.ExternalSecure, instanceInterceptor.Handler)) apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, config.ExternalSecure, instanceInterceptor.Handler))
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources) userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost)
if err != nil { if err != nil {
return err return err
} }

View File

@ -56,8 +56,8 @@
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "6mb", "maximumWarning": "8mb",
"maximumError": "7mb" "maximumError": "9mb"
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",

View File

@ -173,7 +173,43 @@ export class FilterEventsComponent implements OnInit {
return this.adminService return this.adminService
.listEventTypes(req) .listEventTypes(req)
.then((list) => { .then((list) => {
this.eventTypes = list.eventTypesList ?? []; this.eventTypes =
list.eventTypesList.sort((a, b) => {
if (b.localized && b.localized.localizedMessage) {
if (a.localized && a.localized.localizedMessage) {
if (a.localized.localizedMessage < b.localized.localizedMessage) {
return -1;
}
if (a.localized.localizedMessage > b.localized.localizedMessage) {
return 1;
}
} else {
if (a.type < b.localized.localizedMessage) {
return -1;
}
if (a.type > b.localized.localizedMessage) {
return 1;
}
}
} else {
if (a.localized && a.localized.localizedMessage) {
if (a.localized.localizedMessage < b.type) {
return -1;
}
if (a.localized.localizedMessage > b.type) {
return 1;
}
} else {
if (a.type < b.type) {
return -1;
}
if (a.type > b.type) {
return 1;
}
}
}
return 0;
}) ?? [];
}) })
.catch((error) => { .catch((error) => {
this.toast.showError(error); this.toast.showError(error);

View File

@ -80,6 +80,11 @@
<i class="idp-icon las la-building"></i> <i class="idp-icon las la-building"></i>
Active Directory / LDAP Active Directory / LDAP
</div> </div>
<div class="idp-table-provider-type" *ngSwitchCase="ProviderType.PROVIDER_TYPE_APPLE">
<img class="idp-logo apple dark" src="./assets/images/idp/apple-dark.svg" alt="apple" />
<img class="idp-logo apple light" src="./assets/images/idp/apple.svg" alt="apple" />
Apple
</div>
<div class="idp-table-provider-type" *ngSwitchDefault>coming soon</div> <div class="idp-table-provider-type" *ngSwitchDefault>coming soon</div>
</div> </div>
</td> </td>

View File

@ -21,6 +21,10 @@
width: 28px; width: 28px;
flex-shrink: 0; flex-shrink: 0;
&.apple {
margin-bottom: 4px;
}
&.dark { &.dark {
display: if($is-dark-theme, block, none); display: if($is-dark-theme, block, none);
} }

View File

@ -261,6 +261,8 @@ export class IdpTableComponent implements OnInit, OnDestroy {
]; ];
case ProviderType.PROVIDER_TYPE_GITHUB: case ProviderType.PROVIDER_TYPE_GITHUB:
return [row.owner === IDPOwnerType.IDP_OWNER_TYPE_SYSTEM ? '/instance' : '/org', 'provider', 'github', row.id]; return [row.owner === IDPOwnerType.IDP_OWNER_TYPE_SYSTEM ? '/instance' : '/org', 'provider', 'github', row.id];
case ProviderType.PROVIDER_TYPE_APPLE:
return [row.owner === IDPOwnerType.IDP_OWNER_TYPE_SYSTEM ? '/instance' : '/org', 'provider', 'apple', row.id];
} }
} }
} }

View File

@ -109,6 +109,23 @@
</div> </div>
</a> </a>
<a
class="item card"
[routerLink]="
serviceType === PolicyComponentServiceType.ADMIN
? ['/instance', 'provider', 'apple', 'create']
: serviceType === PolicyComponentServiceType.MGMT
? ['/org', 'provider', 'apple', 'create']
: []
"
>
<img class="idp-logo apple dark" src="./assets/images/idp/apple-dark.svg" alt="Apple" />
<img class="idp-logo apple light" src="./assets/images/idp/apple.svg" alt="Apple" />
<div class="text-container">
<span class="title">Apple</span>
</div>
</a>
<a <a
class="item card" class="item card"
[routerLink]=" [routerLink]="

View File

@ -64,6 +64,10 @@
height: 36px; height: 36px;
width: 36px; width: 36px;
&.apple {
margin-bottom: 4px;
}
&.dark { &.dark {
display: if($is-dark-theme, block, none); display: if($is-dark-theme, block, none);
} }

View File

@ -15,12 +15,17 @@
<form (ngSubmit)="savePolicy()" [formGroup]="form" autocomplete="off"> <form (ngSubmit)="savePolicy()" [formGroup]="form" autocomplete="off">
<cnsl-form-field class="smtp-form-field" label="Sender Address" required="true"> <cnsl-form-field class="smtp-form-field" label="Sender Address" required="true">
<cnsl-label>{{ 'SETTING.SMTP.SENDERADDRESS' | translate }}</cnsl-label> <cnsl-label>{{ 'SETTING.SMTP.SENDERADDRESS' | translate }}</cnsl-label>
<input cnslInput name="senderAddress" formControlName="senderAddress" /> <input cnslInput name="senderAddress" formControlName="senderAddress" required />
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Sender Name" required="true"> <cnsl-form-field class="smtp-form-field" label="Sender Name" required="true">
<cnsl-label>{{ 'SETTING.SMTP.SENDERNAME' | translate }}</cnsl-label> <cnsl-label>{{ 'SETTING.SMTP.SENDERNAME' | translate }}</cnsl-label>
<input cnslInput name="senderName" formControlName="senderName" /> <input cnslInput name="senderName" formControlName="senderName" required />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Reply-To Address">
<cnsl-label>{{ 'SETTING.SMTP.REPLYTOADDRESS' | translate }}</cnsl-label>
<input cnslInput name="senderReplyToAddress" formControlName="replyToAddress" />
</cnsl-form-field> </cnsl-form-field>
<mat-checkbox class="smtp-checkbox" formControlName="tls"> <mat-checkbox class="smtp-checkbox" formControlName="tls">
@ -29,12 +34,12 @@
<cnsl-form-field class="smtp-form-field" label="Host And Port" required="true"> <cnsl-form-field class="smtp-form-field" label="Host And Port" required="true">
<cnsl-label>{{ 'SETTING.SMTP.HOSTANDPORT' | translate }}</cnsl-label> <cnsl-label>{{ 'SETTING.SMTP.HOSTANDPORT' | translate }}</cnsl-label>
<input cnslInput name="hostAndPort" formControlName="hostAndPort" placeholder="smtp.mailtrap.io:2525" /> <input cnslInput name="hostAndPort" formControlName="hostAndPort" placeholder="smtp.mailtrap.io:2525" required />
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="User" required="true"> <cnsl-form-field class="smtp-form-field" label="User" required="true">
<cnsl-label>{{ 'SETTING.SMTP.USER' | translate }}</cnsl-label> <cnsl-label>{{ 'SETTING.SMTP.USER' | translate }}</cnsl-label>
<input id="smtp-user" cnslInput name="smtp-user" autocomplete="smtp-user" formControlName="user" /> <input id="smtp-user" cnslInput name="smtp-user" autocomplete="smtp-user" formControlName="user" required />
</cnsl-form-field> </cnsl-form-field>
<button <button

View File

@ -58,6 +58,7 @@ export class NotificationSettingsComponent implements OnInit {
this.form = this.fb.group({ this.form = this.fb.group({
senderAddress: [{ disabled: true, value: '' }, [requiredValidator]], senderAddress: [{ disabled: true, value: '' }, [requiredValidator]],
senderName: [{ disabled: true, value: '' }, [requiredValidator]], senderName: [{ disabled: true, value: '' }, [requiredValidator]],
replyToAddress: [{ disabled: true, value: '' }],
tls: [{ disabled: true, value: true }, [requiredValidator]], tls: [{ disabled: true, value: true }, [requiredValidator]],
hostAndPort: [{ disabled: true, value: '' }, [requiredValidator]], hostAndPort: [{ disabled: true, value: '' }, [requiredValidator]],
user: [{ disabled: true, value: '' }, [requiredValidator]], user: [{ disabled: true, value: '' }, [requiredValidator]],
@ -143,6 +144,7 @@ export class NotificationSettingsComponent implements OnInit {
req.setHost(this.hostAndPort?.value ?? ''); req.setHost(this.hostAndPort?.value ?? '');
req.setSenderAddress(this.senderAddress?.value ?? ''); req.setSenderAddress(this.senderAddress?.value ?? '');
req.setSenderName(this.senderName?.value ?? ''); req.setSenderName(this.senderName?.value ?? '');
req.setReplyToAddress(this.replyToAddress?.value ?? '');
req.setTls(this.tls?.value ?? false); req.setTls(this.tls?.value ?? false);
req.setUser(this.user?.value ?? ''); req.setUser(this.user?.value ?? '');
@ -152,6 +154,7 @@ export class NotificationSettingsComponent implements OnInit {
req.setHost(this.hostAndPort?.value ?? ''); req.setHost(this.hostAndPort?.value ?? '');
req.setSenderAddress(this.senderAddress?.value ?? ''); req.setSenderAddress(this.senderAddress?.value ?? '');
req.setSenderName(this.senderName?.value ?? ''); req.setSenderName(this.senderName?.value ?? '');
req.setReplyToAddress(this.replyToAddress?.value ?? '');
req.setTls(this.tls?.value ?? false); req.setTls(this.tls?.value ?? false);
req.setUser(this.user?.value ?? ''); req.setUser(this.user?.value ?? '');
@ -298,6 +301,10 @@ export class NotificationSettingsComponent implements OnInit {
return this.form.get('senderName'); return this.form.get('senderName');
} }
public get replyToAddress(): AbstractControl | null {
return this.form.get('replyToAddress');
}
public get tls(): AbstractControl | null { public get tls(): AbstractControl | null {
return this.form.get('tls'); return this.form.get('tls');
} }

View File

@ -0,0 +1,146 @@
<cnsl-create-layout
title="{{ id ? ('IDP.DETAIL.TITLE' | translate) : ('IDP.CREATE.TITLE' | translate) }}"
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<img class="idp-logo apple dark" src="./assets/images/idp/apple-dark.svg" alt="apple" />
<img class="idp-logo apple light" src="./assets/images/idp/apple.svg" alt="apple" />
<h1>{{ 'IDP.CREATE.APPLE.TITLE' | translate }}</h1>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<p class="identity-provider-desc cnsl-secondary-text">
{{ !provider ? ('IDP.CREATE.APPLE.DESCRIPTION' | translate) : ('IDP.DETAIL.DESCRIPTION' | translate) }}
</p>
<form [formGroup]="form" (ngSubmit)="submitForm()">
<div class="identity-provider-content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.CLIENTID' | translate }}</cnsl-label>
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.APPLE.TEAMID' | translate }}</cnsl-label>
<input cnslInput formControlName="teamId" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.APPLE.KEYID' | translate }}</cnsl-label>
<input cnslInput formControlName="keyId" />
</cnsl-form-field>
<mat-checkbox
class="update-secret-checkbox"
*ngIf="provider"
[(ngModel)]="updatePrivateKey"
[ngModelOptions]="{ standalone: true }"
>{{ 'IDP.APPLE.UPDATEPRIVATEKEY' | translate }}</mat-checkbox
>
<ng-container *ngIf="!provider || (provider && updatePrivateKey)">
<span class="pk-label cnsl-secondary-text">{{ 'IDP.APPLE.PRIVATEKEY' | translate }}</span>
<div class="private-key-wrapper">
<ng-container *ngIf="privateKey?.value; else addLogoButton">
<i class="las la-file"></i>
<button
class="dl-btn"
mat-icon-button
color="warn"
(click)="privateKey?.setValue('')"
matTooltip="{{ 'ACTIONS.DELETE' | translate }}"
>
<i class="las la-trash"></i>
</button>
</ng-container>
<ng-template #addLogoButton>
<input
#keyFileInput
style="display: none"
class="file-input"
type="file"
accept=".p8"
(change)="onDropKey($any($event.target).files)"
/>
<button
class="asset-add-btn"
mat-icon-button
matTooltip="{{ 'IDP.APPLE.UPLOADPRIVATEKEY' | translate }}"
(click)="$event.preventDefault(); keyFileInput.click()"
>
<mat-icon>add</mat-icon>
</button>
</ng-template>
</div>
</ng-container>
<div class="identity-provider-optional-h-wrapper">
<h2>{{ 'IDP.OPTIONAL' | translate }}</h2>
<button (click)="showOptional = !showOptional" type="button" mat-icon-button>
<mat-icon *ngIf="showOptional">keyboard_arrow_up</mat-icon
><mat-icon *ngIf="!showOptional">keyboard_arrow_down</mat-icon>
</button>
</div>
<div *ngIf="showOptional">
<div class="idp-scopes">
<div class="flex-line">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.SCOPESLIST' | translate }}</cnsl-label>
<input
cnslInput
[matChipInputFor]="chipScopesList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addScope($event)"
/>
</cnsl-form-field>
<button class="scope-add-button" (click)="addScope($any($event))" mat-icon-button>
<mat-icon>add</mat-icon>
</button>
</div>
<cnsl-form-field class="formfield">
<mat-chip-list #chipScopesList aria-label="scope selection">
<mat-chip
class="chip"
*ngFor="let scope of scopesList?.value"
selectable="false"
removable
(removed)="removeScope(scope)"
>
{{ scope }} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
</mat-chip-list>
</cnsl-form-field>
</div>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
<span class="name-hint cnsl-secondary-text" cnslHint>{{ 'IDP.NAMEHINT' | translate }}</span>
</cnsl-form-field>
<cnsl-provider-options
[initialOptions]="provider?.config?.options"
(optionsChanged)="options = $event"
></cnsl-provider-options>
</div>
</div>
<div class="identity-provider-create-actions">
<button
color="primary"
mat-raised-button
class="continue-button"
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
</button>
</div>
</form>
</div>
</cnsl-create-layout>

View File

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

View File

@ -0,0 +1,306 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Location } from '@angular/common';
import { Component, Injector, Type } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips';
import { ActivatedRoute } from '@angular/router';
import { take } from 'rxjs';
import {
AddAppleProviderRequest as AdminAddAppleProviderRequest,
GetProviderByIDRequest as AdminGetProviderByIDRequest,
UpdateAppleProviderRequest as AdminUpdateAppleProviderRequest,
} from 'src/app/proto/generated/zitadel/admin_pb';
import { Options, Provider } from 'src/app/proto/generated/zitadel/idp_pb';
import {
AddAppleProviderRequest as MgmtAddAppleProviderRequest,
GetProviderByIDRequest as MgmtGetProviderByIDRequest,
UpdateAppleProviderRequest as MgmtUpdateAppleProviderRequest,
} from 'src/app/proto/generated/zitadel/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
const MAX_ALLOWED_SIZE = 5 * 1024;
@Component({
selector: 'cnsl-provider-apple',
templateUrl: './provider-apple.component.html',
})
export class ProviderAppleComponent {
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
public id: string | null = '';
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
private service!: ManagementService | AdminService;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
public form!: FormGroup;
public loading: boolean = false;
public provider?: Provider.AsObject;
public updatePrivateKey: boolean = false;
constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
private toast: ToastService,
private injector: Injector,
private _location: Location,
private breadcrumbService: BreadcrumbService,
) {
this.form = new FormGroup({
name: new FormControl('', []),
clientId: new FormControl('', [requiredValidator]),
teamId: new FormControl('', [requiredValidator]),
keyId: new FormControl('', [requiredValidator]),
privateKey: new FormControl('', [requiredValidator]),
scopesList: new FormControl(['name', 'email'], []),
});
this.authService
.isAllowed(
this.serviceType === PolicyComponentServiceType.ADMIN
? ['iam.idp.write']
: this.serviceType === PolicyComponentServiceType.MGMT
? ['org.idp.write']
: [],
)
.pipe(take(1))
.subscribe((allowed) => {
if (allowed) {
this.form.enable();
} else {
this.form.disable();
}
});
this.route.data.pipe(take(1)).subscribe((data) => {
this.serviceType = data['serviceType'];
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
this.breadcrumbService.setBreadcrumb([bread]);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
const iamBread = new Breadcrumb({
type: BreadcrumbType.ORG,
name: 'Instance',
routerLink: ['/instance'],
});
this.breadcrumbService.setBreadcrumb([iamBread]);
break;
}
this.id = this.route.snapshot.paramMap.get('id');
if (this.id) {
this.privateKey?.setValidators([]);
this.getData(this.id);
}
});
}
private getData(id: string): void {
const req =
this.serviceType === PolicyComponentServiceType.ADMIN
? new AdminGetProviderByIDRequest()
: new MgmtGetProviderByIDRequest();
req.setId(id);
this.service
.getProviderByID(req)
.then((resp) => {
this.provider = resp.idp;
this.loading = false;
if (this.provider?.config?.apple) {
this.form.patchValue(this.provider.config.apple);
this.name?.setValue(this.provider.name);
}
})
.catch((error) => {
this.toast.showError(error);
this.loading = false;
});
}
public submitForm(): void {
this.provider ? this.updateAppleProvider() : this.addAppleProvider();
}
public addAppleProvider(): void {
const req =
this.serviceType === PolicyComponentServiceType.MGMT
? new MgmtAddAppleProviderRequest()
: new AdminAddAppleProviderRequest();
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setTeamId(this.teamId?.value);
req.setKeyId(this.keyId?.value);
req.setPrivateKey(this.privateKey?.value);
req.setScopesList(this.scopesList?.value);
req.setProviderOptions(this.options);
this.loading = true;
this.service
.addAppleProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
})
.catch((error) => {
this.toast.showError(error);
this.loading = false;
});
}
public updateAppleProvider(): void {
if (this.provider) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
const req = new MgmtUpdateAppleProviderRequest();
req.setId(this.provider.id);
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setTeamId(this.teamId?.value);
req.setKeyId(this.keyId?.value);
req.setScopesList(this.scopesList?.value);
req.setProviderOptions(this.options);
if (this.updatePrivateKey) {
req.setPrivateKey(this.privateKey?.value);
}
this.loading = true;
(this.service as ManagementService)
.updateAppleProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
})
.catch((error) => {
this.toast.showError(error);
this.loading = false;
});
} else if (PolicyComponentServiceType.ADMIN) {
const req = new AdminUpdateAppleProviderRequest();
req.setId(this.provider.id);
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setTeamId(this.teamId?.value);
req.setKeyId(this.keyId?.value);
req.setScopesList(this.scopesList?.value);
req.setProviderOptions(this.options);
if (this.updatePrivateKey) {
req.setPrivateKey(this.privateKey?.value);
}
this.loading = true;
(this.service as AdminService)
.updateAppleProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
})
.catch((error) => {
this.loading = false;
this.toast.showError(error);
});
}
}
}
public close(): void {
this._location.back();
}
public onDropKey(filelist: FileList): void {
const file = filelist.item(0);
if (file) {
if (file.size > MAX_ALLOWED_SIZE) {
this.toast.showInfo('IDP.APPLE.KEYMAXSIZEEXCEEDED', true);
} else {
this.privateKey?.setValue('');
const reader = new FileReader();
reader.onload = ((aXML) => {
return (e) => {
const keyBase64 = e.target?.result;
if (keyBase64 && typeof keyBase64 === 'string') {
const cropped = keyBase64.replace('data:application/octet-stream;base64,', '');
this.privateKey?.setValue(cropped);
}
};
})(file);
reader.readAsDataURL(file);
}
}
}
public addScope(event: MatChipInputEvent): void {
const input = event.chipInput?.inputElement;
const value = event.value.trim();
if (value !== '') {
if (this.scopesList?.value) {
this.scopesList.value.push(value);
if (input) {
input.value = '';
}
}
}
}
public removeScope(uri: string): void {
if (this.scopesList?.value) {
const index = this.scopesList.value.indexOf(uri);
if (index !== undefined && index >= 0) {
this.scopesList.value.splice(index, 1);
}
}
}
public get name(): AbstractControl | null {
return this.form.get('name');
}
public get clientId(): AbstractControl | null {
return this.form.get('clientId');
}
public get teamId(): AbstractControl | null {
return this.form.get('teamId');
}
public get keyId(): AbstractControl | null {
return this.form.get('keyId');
}
public get privateKey(): AbstractControl | null {
return this.form.get('privateKey');
}
public get scopesList(): AbstractControl | null {
return this.form.get('scopesList');
}
}

View File

@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { ProviderType } from 'src/app/proto/generated/zitadel/idp_pb'; import { ProviderType } from 'src/app/proto/generated/zitadel/idp_pb';
import { ProviderAppleComponent } from './provider-apple/provider-apple.component';
import { ProviderAzureADComponent } from './provider-azure-ad/provider-azure-ad.component'; import { ProviderAzureADComponent } from './provider-azure-ad/provider-azure-ad.component';
import { ProviderGithubESComponent } from './provider-github-es/provider-github-es.component'; import { ProviderGithubESComponent } from './provider-github-es/provider-github-es.component';
import { ProviderGithubComponent } from './provider-github/provider-github.component'; import { ProviderGithubComponent } from './provider-github/provider-github.component';
@ -27,6 +28,7 @@ const typeMap = {
[ProviderType.PROVIDER_TYPE_OAUTH]: { path: 'oauth', component: ProviderOAuthComponent }, [ProviderType.PROVIDER_TYPE_OAUTH]: { path: 'oauth', component: ProviderOAuthComponent },
[ProviderType.PROVIDER_TYPE_OIDC]: { path: 'oidc', component: ProviderOIDCComponent }, [ProviderType.PROVIDER_TYPE_OIDC]: { path: 'oidc', component: ProviderOIDCComponent },
[ProviderType.PROVIDER_TYPE_LDAP]: { path: 'ldap', component: ProviderLDAPComponent }, [ProviderType.PROVIDER_TYPE_LDAP]: { path: 'ldap', component: ProviderLDAPComponent },
[ProviderType.PROVIDER_TYPE_APPLE]: { path: 'apple', component: ProviderAppleComponent },
}; };
const routes: Routes = Object.entries(typeMap).map(([key, value]) => { const routes: Routes = Object.entries(typeMap).map(([key, value]) => {

View File

@ -17,6 +17,7 @@ import { InfoSectionModule } from '../info-section/info-section.module';
import { ProviderOptionsModule } from '../provider-options/provider-options.module'; import { ProviderOptionsModule } from '../provider-options/provider-options.module';
import { StringListModule } from '../string-list/string-list.module'; import { StringListModule } from '../string-list/string-list.module';
import { LDAPAttributesComponent } from './ldap-attributes/ldap-attributes.component'; import { LDAPAttributesComponent } from './ldap-attributes/ldap-attributes.component';
import { ProviderAppleComponent } from './provider-apple/provider-apple.component';
import { ProviderAzureADComponent } from './provider-azure-ad/provider-azure-ad.component'; import { ProviderAzureADComponent } from './provider-azure-ad/provider-azure-ad.component';
import { ProviderGithubESComponent } from './provider-github-es/provider-github-es.component'; import { ProviderGithubESComponent } from './provider-github-es/provider-github-es.component';
import { ProviderGithubComponent } from './provider-github/provider-github.component'; import { ProviderGithubComponent } from './provider-github/provider-github.component';
@ -43,6 +44,7 @@ import { ProvidersRoutingModule } from './providers-routing.module';
ProviderOIDCComponent, ProviderOIDCComponent,
ProviderOAuthComponent, ProviderOAuthComponent,
ProviderLDAPComponent, ProviderLDAPComponent,
ProviderAppleComponent,
], ],
imports: [ imports: [
ProvidersRoutingModule, ProvidersRoutingModule,

View File

@ -3,6 +3,7 @@
@mixin identity-provider-theme($theme) { @mixin identity-provider-theme($theme) {
$is-dark-theme: map-get($theme, is-dark); $is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background); $background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.identity-provider-desc { .identity-provider-desc {
font-size: 14px; font-size: 14px;
@ -19,6 +20,10 @@
margin-right: 1rem; margin-right: 1rem;
flex-shrink: 0; flex-shrink: 0;
&.apple {
margin-bottom: 4px;
}
&.dark { &.dark {
display: if($is-dark-theme, block, none); display: if($is-dark-theme, block, none);
} }
@ -74,6 +79,58 @@
} }
} }
.pk-label {
font-size: 12px;
margin-top: 0.5rem;
}
.private-key-wrapper {
position: relative;
height: 70px;
width: 70px;
box-sizing: border-box;
border-radius: 0.5rem;
margin-top: 0.5rem;
border: 1px solid map-get($foreground, divider);
display: flex;
justify-content: center;
align-items: center;
.dl-btn {
z-index: 2;
position: absolute;
right: 0;
top: 0;
cursor: pointer;
visibility: hidden;
transform: translateX(50%) translateY(-50%);
}
img {
top: 0;
left: 0;
width: 70px;
height: 70px;
object-fit: contain;
object-position: center;
border-radius: 0.5rem;
}
&.icon {
border-radius: 50%;
img {
border-radius: 50%;
}
}
&:hover {
.dl-btn {
visibility: visible;
}
}
}
.string-list-component-wrapper { .string-list-component-wrapper {
max-width: 400px; max-width: 400px;
} }

View File

@ -248,7 +248,7 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
this.metadata = resp.resultList.map((md) => { this.metadata = resp.resultList.map((md) => {
return { return {
key: md.key, key: md.key,
value: Buffer.from(md.value as string, 'base64').toString('ascii'), value: Buffer.from(md.value as string, 'base64').toString('utf-8'),
}; };
}); });
}) })

View File

@ -521,7 +521,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
get decodedBase64(): string { get decodedBase64(): string {
const samlReq = this.samlAppRequest.toObject(); const samlReq = this.samlAppRequest.toObject();
if (samlReq && samlReq.metadataXml && typeof samlReq.metadataXml === 'string') { if (samlReq && samlReq.metadataXml && typeof samlReq.metadataXml === 'string') {
return Buffer.from(samlReq.metadataXml, 'base64').toString('ascii'); return Buffer.from(samlReq.metadataXml, 'base64').toString('utf-8');
} else { } else {
return ''; return '';
} }
@ -529,7 +529,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
set decodedBase64(xmlString) { set decodedBase64(xmlString) {
if (this.samlAppRequest) { if (this.samlAppRequest) {
const base64 = Buffer.from(xmlString, 'ascii').toString('base64'); const base64 = Buffer.from(xmlString, 'utf-8').toString('base64');
this.samlAppRequest.setMetadataXml(base64); this.samlAppRequest.setMetadataXml(base64);
} }
} }

View File

@ -786,7 +786,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
this.app.samlConfig.metadataXml && this.app.samlConfig.metadataXml &&
typeof this.app.samlConfig.metadataXml === 'string' typeof this.app.samlConfig.metadataXml === 'string'
) { ) {
return Buffer.from(this.app?.samlConfig.metadataXml, 'base64').toString('ascii'); return Buffer.from(this.app?.samlConfig.metadataXml, 'base64').toString('utf-8');
} else { } else {
return ''; return '';
} }
@ -794,7 +794,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
set decodedBase64(xmlString: string) { set decodedBase64(xmlString: string) {
if (this.app && this.app.samlConfig && this.app.samlConfig.metadataXml) { if (this.app && this.app.samlConfig && this.app.samlConfig.metadataXml) {
const base64 = Buffer.from(xmlString, 'ascii').toString('base64'); const base64 = Buffer.from(xmlString, 'utf-8').toString('base64');
if (this.app.samlConfig) { if (this.app.samlConfig) {
this.app.samlConfig.metadataXml = base64; this.app.samlConfig.metadataXml = base64;

View File

@ -406,7 +406,7 @@ export class AuthUserDetailComponent implements OnDestroy {
this.metadata = resp.resultList.map((md) => { this.metadata = resp.resultList.map((md) => {
return { return {
key: md.key, key: md.key,
value: Buffer.from(md.value as string, 'base64').toString('ascii'), value: Buffer.from(md.value as string, 'base64').toString('utf8'),
}; };
}); });
}) })

View File

@ -508,7 +508,7 @@ export class UserDetailComponent implements OnInit {
this.metadata = resp.resultList.map((md) => { this.metadata = resp.resultList.map((md) => {
return { return {
key: md.key, key: md.key,
value: Buffer.from(md.value as string, 'base64').toString('ascii'), value: Buffer.from(md.value as string, 'base64').toString('utf-8'),
}; };
}); });
}) })

View File

@ -6,6 +6,8 @@ import {
ActivateLabelPolicyResponse, ActivateLabelPolicyResponse,
ActivateSMSProviderRequest, ActivateSMSProviderRequest,
ActivateSMSProviderResponse, ActivateSMSProviderResponse,
AddAppleProviderRequest,
AddAppleProviderResponse,
AddAzureADProviderRequest, AddAzureADProviderRequest,
AddAzureADProviderResponse, AddAzureADProviderResponse,
AddCustomDomainPolicyRequest, AddCustomDomainPolicyRequest,
@ -214,6 +216,8 @@ import {
SetSecurityPolicyResponse, SetSecurityPolicyResponse,
SetUpOrgRequest, SetUpOrgRequest,
SetUpOrgResponse, SetUpOrgResponse,
UpdateAppleProviderRequest,
UpdateAppleProviderResponse,
UpdateAzureADProviderRequest, UpdateAzureADProviderRequest,
UpdateAzureADProviderResponse, UpdateAzureADProviderResponse,
UpdateCustomDomainPolicyRequest, UpdateCustomDomainPolicyRequest,
@ -1173,6 +1177,14 @@ export class AdminService {
return this.grpcService.admin.updateGitHubEnterpriseServerProvider(req, null).then((resp) => resp.toObject()); return this.grpcService.admin.updateGitHubEnterpriseServerProvider(req, null).then((resp) => resp.toObject());
} }
public addAppleProvider(req: AddAppleProviderRequest): Promise<AddAppleProviderResponse.AsObject> {
return this.grpcService.admin.addAppleProvider(req, null).then((resp) => resp.toObject());
}
public updateAppleProvider(req: UpdateAppleProviderRequest): Promise<UpdateAppleProviderResponse.AsObject> {
return this.grpcService.admin.updateAppleProvider(req, null).then((resp) => resp.toObject());
}
public deleteProvider(id: string): Promise<DeleteProviderResponse.AsObject> { public deleteProvider(id: string): Promise<DeleteProviderResponse.AsObject> {
const req = new DeleteProviderRequest(); const req = new DeleteProviderRequest();
req.setId(id); req.setId(id);

View File

@ -15,6 +15,8 @@ import {
AddAPIAppResponse, AddAPIAppResponse,
AddAppKeyRequest, AddAppKeyRequest,
AddAppKeyResponse, AddAppKeyResponse,
AddAppleProviderRequest,
AddAppleProviderResponse,
AddAzureADProviderRequest, AddAzureADProviderRequest,
AddAzureADProviderResponse, AddAzureADProviderResponse,
AddCustomLabelPolicyRequest, AddCustomLabelPolicyRequest,
@ -441,6 +443,8 @@ import {
UpdateActionResponse, UpdateActionResponse,
UpdateAPIAppConfigRequest, UpdateAPIAppConfigRequest,
UpdateAPIAppConfigResponse, UpdateAPIAppConfigResponse,
UpdateAppleProviderRequest,
UpdateAppleProviderResponse,
UpdateAppRequest, UpdateAppRequest,
UpdateAppResponse, UpdateAppResponse,
UpdateAzureADProviderRequest, UpdateAzureADProviderRequest,
@ -1041,6 +1045,14 @@ export class ManagementService {
return this.grpcService.mgmt.updateGitHubEnterpriseServerProvider(req, null).then((resp) => resp.toObject()); return this.grpcService.mgmt.updateGitHubEnterpriseServerProvider(req, null).then((resp) => resp.toObject());
} }
public addAppleProvider(req: AddAppleProviderRequest): Promise<AddAppleProviderResponse.AsObject> {
return this.grpcService.mgmt.addAppleProvider(req, null).then((resp) => resp.toObject());
}
public updateAppleProvider(req: UpdateAppleProviderRequest): Promise<UpdateAppleProviderResponse.AsObject> {
return this.grpcService.mgmt.updateAppleProvider(req, null).then((resp) => resp.toObject());
}
public deleteProvider(id: string): Promise<DeleteProviderResponse.AsObject> { public deleteProvider(id: string): Promise<DeleteProviderResponse.AsObject> {
const req = new DeleteProviderRequest(); const req = new DeleteProviderRequest();
req.setId(id); req.setId(id);

View File

@ -1049,6 +1049,7 @@
"TITLE": "SMTP настройки", "TITLE": "SMTP настройки",
"SENDERADDRESS": "Имейл адрес на изпращача", "SENDERADDRESS": "Имейл адрес на изпращача",
"SENDERNAME": "Име на изпращача", "SENDERNAME": "Име на изпращача",
"REPLYTOADDRESS": "Reply-to адрес",
"HOSTANDPORT": "Хост и порт", "HOSTANDPORT": "Хост и порт",
"USER": "Потребител", "USER": "Потребител",
"PASSWORD": "Парола", "PASSWORD": "Парола",

View File

@ -1055,6 +1055,7 @@
"TITLE": "SMTP Einstellungen", "TITLE": "SMTP Einstellungen",
"SENDERADDRESS": "Sender Email-Adresse", "SENDERADDRESS": "Sender Email-Adresse",
"SENDERNAME": "Sender Name", "SENDERNAME": "Sender Name",
"REPLYTOADDRESS": "Reply-to-Adresse",
"HOSTANDPORT": "Host und Port", "HOSTANDPORT": "Host und Port",
"USER": "Benutzer", "USER": "Benutzer",
"PASSWORD": "Passwort", "PASSWORD": "Passwort",

View File

@ -1056,6 +1056,7 @@
"TITLE": "SMTP Settings", "TITLE": "SMTP Settings",
"SENDERADDRESS": "Sender Email Address", "SENDERADDRESS": "Sender Email Address",
"SENDERNAME": "Sender Name", "SENDERNAME": "Sender Name",
"REPLYTOADDRESS": "Reply-to Address",
"HOSTANDPORT": "Host And Port", "HOSTANDPORT": "Host And Port",
"USER": "User", "USER": "User",
"PASSWORD": "Password", "PASSWORD": "Password",
@ -1705,6 +1706,10 @@
"LDAP": { "LDAP": {
"TITLE": "Active Directory / LDAP", "TITLE": "Active Directory / LDAP",
"DESCRIPTION": "Enter the credentials for your LDAP Provider" "DESCRIPTION": "Enter the credentials for your LDAP Provider"
},
"APPLE": {
"TITLE": "Sign in with Apple",
"DESCRIPTION": "Enter the credentials for your Apple Provider"
} }
}, },
"DETAIL": { "DETAIL": {
@ -1818,6 +1823,14 @@
"JWTENDPOINT": "JWT Endpoint", "JWTENDPOINT": "JWT Endpoint",
"JWTKEYSENDPOINT": "JWT Keys Endpoint" "JWTKEYSENDPOINT": "JWT Keys Endpoint"
}, },
"APPLE": {
"TEAMID": "Team ID",
"KEYID": "Key ID",
"PRIVATEKEY": "Private Key",
"UPDATEPRIVATEKEY": "Update Private Key",
"UPLOADPRIVATEKEY": "Upload Private Key",
"KEYMAXSIZEEXCEEDED": "Maximum size of 5kB exceeded."
},
"TOAST": { "TOAST": {
"SAVED": "Successfully saved.", "SAVED": "Successfully saved.",
"REACTIVATED": "Idp reactivated.", "REACTIVATED": "Idp reactivated.",

View File

@ -1056,6 +1056,7 @@
"TITLE": "Ajustes SMTP", "TITLE": "Ajustes SMTP",
"SENDERADDRESS": "Dirección email del emisor", "SENDERADDRESS": "Dirección email del emisor",
"SENDERNAME": "Nombre del emisor", "SENDERNAME": "Nombre del emisor",
"REPLYTOADDRESS": "Dirección Reply-To",
"HOSTANDPORT": "Servidor y puerto", "HOSTANDPORT": "Servidor y puerto",
"USER": "Usuario", "USER": "Usuario",
"PASSWORD": "Contraseña", "PASSWORD": "Contraseña",

View File

@ -1055,6 +1055,7 @@
"TITLE": "Paramètres SMTP", "TITLE": "Paramètres SMTP",
"SENDERADDRESS": "Adresse e-mail de l'expéditeur", "SENDERADDRESS": "Adresse e-mail de l'expéditeur",
"SENDERNAME": "Nom de l'expéditeur", "SENDERNAME": "Nom de l'expéditeur",
"REPLYTOADDRESS": "Adresse Reply-to",
"HOSTANDPORT": "Hôte et port", "HOSTANDPORT": "Hôte et port",
"USER": "Utilisateur", "USER": "Utilisateur",
"PASSWORD": "Mot de passe", "PASSWORD": "Mot de passe",

View File

@ -1055,6 +1055,7 @@
"TITLE": "Impostazioni SMTP", "TITLE": "Impostazioni SMTP",
"SENDERADDRESS": "Indirizzo email del mittente", "SENDERADDRESS": "Indirizzo email del mittente",
"SENDERNAME": "Nome del mittente", "SENDERNAME": "Nome del mittente",
"REPLYTOADDRESS": "Indirizzo Reply-to",
"HOSTANDPORT": "Host e porta", "HOSTANDPORT": "Host e porta",
"USER": "Utente", "USER": "Utente",
"PASSWORD": "Password", "PASSWORD": "Password",

View File

@ -1056,6 +1056,7 @@
"TITLE": "SMTP設定", "TITLE": "SMTP設定",
"SENDERADDRESS": "送信者のメールアドレス", "SENDERADDRESS": "送信者のメールアドレス",
"SENDERNAME": "送信者名", "SENDERNAME": "送信者名",
"REPLYTOADDRESS": "返信先アドレス",
"HOSTANDPORT": "ホストとポート", "HOSTANDPORT": "ホストとポート",
"USER": "ユーザー", "USER": "ユーザー",
"PASSWORD": "パスワード", "PASSWORD": "パスワード",

View File

@ -1056,6 +1056,7 @@
"TITLE": "SMTP подесувања", "TITLE": "SMTP подесувања",
"SENDERADDRESS": "Адреса на испраќачот", "SENDERADDRESS": "Адреса на испраќачот",
"SENDERNAME": "Име на испраќачот", "SENDERNAME": "Име на испраќачот",
"REPLYTOADDRESS": "Reply-to адреса",
"HOSTANDPORT": "Host и Port", "HOSTANDPORT": "Host и Port",
"USER": "Корисник", "USER": "Корисник",
"PASSWORD": "Лозинка", "PASSWORD": "Лозинка",

View File

@ -1055,6 +1055,7 @@
"TITLE": "Ustawienia SMTP", "TITLE": "Ustawienia SMTP",
"SENDERADDRESS": "Adres e-mail nadawcy", "SENDERADDRESS": "Adres e-mail nadawcy",
"SENDERNAME": "Nazwa nadawcy", "SENDERNAME": "Nazwa nadawcy",
"REPLYTOADDRESS": "Adres Reply-to",
"HOSTANDPORT": "Host i port", "HOSTANDPORT": "Host i port",
"USER": "Użytkownik", "USER": "Użytkownik",
"PASSWORD": "Hasło", "PASSWORD": "Hasło",

View File

@ -1056,6 +1056,7 @@
"TITLE": "Configurações SMTP", "TITLE": "Configurações SMTP",
"SENDERADDRESS": "Endereço de e-mail do remetente", "SENDERADDRESS": "Endereço de e-mail do remetente",
"SENDERNAME": "Nome do remetente", "SENDERNAME": "Nome do remetente",
"REPLYTOADDRESS": "Endereço Reply-To",
"HOSTANDPORT": "Host e porta", "HOSTANDPORT": "Host e porta",
"USER": "Usuário", "USER": "Usuário",
"PASSWORD": "Senha", "PASSWORD": "Senha",

View File

@ -1055,6 +1055,7 @@
"TITLE": "SMTP 设置", "TITLE": "SMTP 设置",
"SENDERADDRESS": "发件人地址", "SENDERADDRESS": "发件人地址",
"SENDERNAME": "发件人名称", "SENDERNAME": "发件人名称",
"REPLYTOADDRESS": "Reply-to 地址",
"HOSTANDPORT": "主机和端口", "HOSTANDPORT": "主机和端口",
"USER": "用户名", "USER": "用户名",
"PASSWORD": "密码", "PASSWORD": "密码",

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="56px" height="56px" viewBox="18 15.5 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61 (89581) - https://sketch.com -->
<title>White Logo Square </title>
<desc>Created with Sketch.</desc>
<g id="White-Logo-Square-" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" fill="" x="6" y="6" width="44" height="44"></rect>
<path d="M28.2226562,20.3846154 C29.0546875,20.3846154 30.0976562,19.8048315 30.71875,19.0317864 C31.28125,18.3312142 31.6914062,17.352829 31.6914062,16.3744437 C31.6914062,16.2415766 31.6796875,16.1087095 31.65625,16 C30.7304687,16.0362365 29.6171875,16.640178 28.9492187,17.4494596 C28.421875,18.06548 27.9414062,19.0317864 27.9414062,20.0222505 C27.9414062,20.1671964 27.9648438,20.3121424 27.9765625,20.3604577 C28.0351562,20.3725366 28.1289062,20.3846154 28.2226562,20.3846154 Z M25.2929688,35 C26.4296875,35 26.9335938,34.214876 28.3515625,34.214876 C29.7929688,34.214876 30.109375,34.9758423 31.375,34.9758423 C32.6171875,34.9758423 33.4492188,33.792117 34.234375,32.6325493 C35.1132812,31.3038779 35.4765625,29.9993643 35.5,29.9389701 C35.4179688,29.9148125 33.0390625,28.9122695 33.0390625,26.0979021 C33.0390625,23.6579784 34.9140625,22.5588048 35.0195312,22.474253 C33.7773438,20.6382708 31.890625,20.5899555 31.375,20.5899555 C29.9804688,20.5899555 28.84375,21.4596313 28.1289062,21.4596313 C27.3554688,21.4596313 26.3359375,20.6382708 25.1289062,20.6382708 C22.8320312,20.6382708 20.5,22.5950413 20.5,26.2911634 C20.5,28.5861411 21.3671875,31.013986 22.4335938,32.5842339 C23.3476562,33.9129053 24.1445312,35 25.2929688,35 Z" id="" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="56px" height="56px" viewBox="18 15.5 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61 (89581) - https://sketch.com -->
<title>Black Logo Square</title>
<desc>Created with Sketch.</desc>
<g id="Black-Logo-Square" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" fill="" x="6" y="6" width="44" height="44"></rect>
<path d="M28.2226562,20.3846154 C29.0546875,20.3846154 30.0976562,19.8048315 30.71875,19.0317864 C31.28125,18.3312142 31.6914062,17.352829 31.6914062,16.3744437 C31.6914062,16.2415766 31.6796875,16.1087095 31.65625,16 C30.7304687,16.0362365 29.6171875,16.640178 28.9492187,17.4494596 C28.421875,18.06548 27.9414062,19.0317864 27.9414062,20.0222505 C27.9414062,20.1671964 27.9648438,20.3121424 27.9765625,20.3604577 C28.0351562,20.3725366 28.1289062,20.3846154 28.2226562,20.3846154 Z M25.2929688,35 C26.4296875,35 26.9335938,34.214876 28.3515625,34.214876 C29.7929688,34.214876 30.109375,34.9758423 31.375,34.9758423 C32.6171875,34.9758423 33.4492188,33.792117 34.234375,32.6325493 C35.1132812,31.3038779 35.4765625,29.9993643 35.5,29.9389701 C35.4179688,29.9148125 33.0390625,28.9122695 33.0390625,26.0979021 C33.0390625,23.6579784 34.9140625,22.5588048 35.0195312,22.474253 C33.7773438,20.6382708 31.890625,20.5899555 31.375,20.5899555 C29.9804688,20.5899555 28.84375,21.4596313 28.1289062,21.4596313 C27.3554688,21.4596313 26.3359375,20.6382708 25.1289062,20.6382708 C22.8320312,20.6382708 20.5,22.5950413 20.5,26.2911634 C20.5,28.5861411 21.3671875,31.013986 22.4335938,32.5842339 C23.3476562,33.9129053 24.1445312,35 25.2929688,35 Z" id="" fill="#000000" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -429,6 +429,7 @@ $custom-typography: mat.define-legacy-typography-config(
background-color: map-get($background, cards); background-color: map-get($background, cards);
transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
border-radius: 6px; border-radius: 6px;
min-height: fit-content;
} }
.mat-option { .mat-option {
@ -500,6 +501,7 @@ $custom-typography: mat.define-legacy-typography-config(
background-color: map-get($background, cards); background-color: map-get($background, cards);
transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
border-radius: 6px; border-radius: 6px;
min-height: fit-content;
} }
.mat-option { .mat-option {

View File

@ -26,7 +26,7 @@ In the response, you will get an authentication URL of the provider you like.
```bash ```bash
curl --request POST \ curl --request POST \
--url https://$ZITADEL_DOMAIN/v2alpha/idp_intents/start \ --url https://$ZITADEL_DOMAIN/v2alpha/idp_intents \
--header 'Accept: application/json' \ --header 'Accept: application/json' \
--header 'Authorization: Bearer '"$TOKEN"''\ --header 'Authorization: Bearer '"$TOKEN"''\
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \

View File

@ -212,7 +212,7 @@ Next step is to authenticate the user with the new registered passkey.
### Create Session ### Create Session
First step is to ask the user for his username and create a new session with the ZITADEL API. First step is to ask the user for his username and create a new session with the ZITADEL API.
When creating the new session make sure to include the challenge for passkey. When creating the new session make sure to include the challenge for passkey, resp. webAuthN with a required user verification and the domain of your login UI.
The response will include the public key credential request options for the passkey in the challenges. The response will include the public key credential request options for the passkey in the challenges.
More detailed information about the API: [Create Session Documentation](/apis/resources/session_service/session-service-create-session) More detailed information about the API: [Create Session Documentation](/apis/resources/session_service/session-service-create-session)
@ -231,9 +231,12 @@ curl --request POST \
} }
}, },
"metadata": {}, "metadata": {},
"challenges": [ "challenges": {
"CHALLENGE_KIND_PASSKEY" "webAuthN": {
] "domain": "example.domain.com",
"userVerificationRequirement": "USER_VERIFICATION_REQUIREMENT_REQUIRED"
}
}
}' }'
``` ```
@ -248,7 +251,7 @@ Example Response:
"sessionId": "d654e6ba-70a3-48ef-a95d-37c8d8a7901a", "sessionId": "d654e6ba-70a3-48ef-a95d-37c8d8a7901a",
"sessionToken": "string", "sessionToken": "string",
"challenges": { "challenges": {
"passkey": { "webAuthN": {
"publicKeyCredentialRequestOptions": { "publicKeyCredentialRequestOptions": {
"publicKey": { "publicKey": {
"allowCredentials": [ "allowCredentials": [
@ -274,7 +277,7 @@ After starting the passkey authentication on the side of ZITADEL you have to cha
To do this you need to call the browser API to get the credentials. To do this you need to call the browser API to get the credentials.
Make sure to send the public key credential request options you got from ZITADEL. Make sure to send the public key credential request options you got from ZITADEL.
```bash ```javascript
const credential = await navigator.credentials.get({ const credential = await navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions publicKey: publicKeyCredentialRequestOptions
}); });
@ -300,7 +303,7 @@ curl --request PATCH \
--data '{ --data '{
"sessionToken": "yMDi6uVPJAcphbbz0LaxC07ihWkNTe7m0Xqch8SzfM5Cz3HSIQIDZ65x1f5Qal0jxz0MEyo-_zYcUg", "sessionToken": "yMDi6uVPJAcphbbz0LaxC07ihWkNTe7m0Xqch8SzfM5Cz3HSIQIDZ65x1f5Qal0jxz0MEyo-_zYcUg",
"checks": { "checks": {
"passkey": { "webAuthN": {
"credentialAssertionData": {} "credentialAssertionData": {}
} }
} }

View File

@ -75,7 +75,7 @@ Authorization: "Basic " + base64( formUrlEncode(client_id) + ":" + formUrlEncode
The request from the API to the introspection endpoint should be in the following format: The request from the API to the introspection endpoint should be in the following format:
``` ```bash
curl --request POST \ curl --request POST \
--url {your_domain}/oauth/v2/introspect \ --url {your_domain}/oauth/v2/introspect \
--header 'Content-Type: application/x-www-form-urlencoded' \ --header 'Content-Type: application/x-www-form-urlencoded' \
@ -85,7 +85,7 @@ curl --request POST \
Here's an example of how this is done in Python code: Here's an example of how this is done in Python code:
``` ```python
def introspect_token(self, token_string): def introspect_token(self, token_string):
url = ZITADEL_INTROSPECTION_URL url = ZITADEL_INTROSPECTION_URL
data = {'token': token_string, 'token_type_hint': 'access_token', 'scope': 'openid'} data = {'token': token_string, 'token_type_hint': 'access_token', 'scope': 'openid'}

View File

@ -31,7 +31,7 @@ you can setup ZITADEL and either
# Install CockroachDB # Install CockroachDB
helm install crdb cockroachdb/cockroachdb \ helm install crdb cockroachdb/cockroachdb \
--set fullnameOverride=crdb \ --set fullnameOverride=crdb \
--set single-node=true \ --set conf.single-node=true \
--set statefulset.replicas=1 --set statefulset.replicas=1
# Install ZITADEL # Install ZITADEL
@ -65,7 +65,7 @@ With this setup you only get a key for a service account. Logging in at ZITADEL
# Install CockroachDB # Install CockroachDB
helm install crdb cockroachdb/cockroachdb \ helm install crdb cockroachdb/cockroachdb \
--set fullnameOverride=crdb \ --set fullnameOverride=crdb \
--set single-node=true \ --set conf.single-node=true \
--set statefulset.replicas=1 --set statefulset.replicas=1
# Install ZITADEL # Install ZITADEL

View File

@ -23,16 +23,16 @@ By executing the commands below, you will download the following files:
```bash ```bash
# Download the docker compose example configuration for a secure CockroachDB. # Download the docker compose example configuration for a secure CockroachDB.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/production/docker-compose.yaml wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/configure/docker-compose.yaml
# Download and adjust the example configuration file containing standard configuration. # Download and adjust the example configuration file containing standard configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/production/example-zitadel-config.yaml wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/configure/example-zitadel-config.yaml
# Download and adjust the example configuration file containing secret configuration. # Download and adjust the example configuration file containing secret configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/production/example-zitadel-secrets.yaml wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/configure/example-zitadel-secrets.yaml
# Download and adjust the example configuration file containing database initialization configuration. # Download and adjust the example configuration file containing database initialization configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/production/example-zitadel-init-steps.yaml wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/configure/example-zitadel-init-steps.yaml
# A single ZITADEL instance always needs the same 32 characters long masterkey # A single ZITADEL instance always needs the same 32 characters long masterkey
# If you haven't done so already, you can generate a new one # If you haven't done so already, you can generate a new one

View File

@ -200,6 +200,7 @@ DefaultInstance:
# if the host of the sender is different from ExternalDomain set DefaultInstance.DomainPolicy.SMTPSenderAddressMatchesInstanceDomain to false # if the host of the sender is different from ExternalDomain set DefaultInstance.DomainPolicy.SMTPSenderAddressMatchesInstanceDomain to false
From: From:
FromName: FromName:
ReplyToAddress:
``` ```
- If you don't want to use the DefaultInstance configuration for the first instance that ZITADEL automatically creates for you during the [setup phase](/self-hosting/manage/configure#database-initialization), you can provide a FirstInstance YAML section using the --steps argument. - If you don't want to use the DefaultInstance configuration for the first instance that ZITADEL automatically creates for you during the [setup phase](/self-hosting/manage/configure#database-initialization), you can provide a FirstInstance YAML section using the --steps argument.

View File

@ -13,8 +13,8 @@ To address this, we are going to change this behavior so that users will be auto
## Statement ## Statement
This behaviour change is tracked in the following issue: [Reuse current session if no prompt is selected ](https://github.com/zitadel/zitadel/issues/4841) This behaviour change was tracked in the following issue: [Reuse current session if no prompt is selected](https://github.com/zitadel/zitadel/issues/4841)
As soon as the release version is published, we will include the version here. and released in Version [v2.32.0](https://github.com/zitadel/zitadel/releases/tag/v2.32.0)
## Mitigation ## Mitigation

View File

@ -0,0 +1,26 @@
---
title: Technical Advisory 10001
---
## Description
Currently, disabling the `Allow Register` setting in the Login Policy, will disable any registration - local and through External Identity Providers (IDP).
This might be a good solution, if you manage all users yourself and do not want them to create any new account.
If you on the other hand want users to be able to federate their accounts from another IDP and only want to disable local registration, there's currently no option to do so.
Further ZITADEL provided the possibility to disable registration on each IDP with the introduction of IDP Templates.
To address this, we are going to change the behavior of the setting mentioned above, so that if disable, it will only prevent local registration. Registration of a federated user will still be possible - if not disabled by the corresponding IDP Template.
## Statement
This behaviour change is tracked in the following PR: [Restrict AllowRegistration check to local registration](https://github.com/zitadel/zitadel/pull/5939).
As soon as the release version is published, we will include the version here.
## Mitigation
If you want to prevent user creation / registration through an IDP, be sure to disable the `isCreationAllowed` option on the desired IDP Templates.
## Impact
Once this update has been released and deployed, the `Allow Register` setting in the Login Policy will only affect local registrations and users might be able to create a ZITADEL account through an IDP, depending on your IDP provider options.

View File

@ -26,6 +26,14 @@ We understand that these advisories may include breaking changes, and we aim to
<td>2.32.0</td> <td>2.32.0</td>
<td>Calendar week 32</td> <td>Calendar week 32</td>
</tr> </tr>
<tr>
<td><a href="./advisory/a10001">A-10001</a></td>
<td>Login Policy - Allow Register</td>
<td>Breaking Behaviour Change</td>
<td>When disabling the option, users are currently not able to register locally and also not through an external IDP. With the upcoming change, the setting will only prevent local registration. Restriction to Identity Providers can be managed through the corresponding IDP Template. No action is required on your side if this is the intended behaviour or if you already disabled registration on your IDP.</td>
<td>TBD</td>
<td>Calendar week 34/35</td>
</tr>
</table> </table>
## Subscribe to our Mailing List ## Subscribe to our Mailing List

View File

@ -20,7 +20,7 @@ module.exports = {
], ],
customFields: { customFields: {
description: description:
"Documentation for ZITADEL - The best of Auth0 and Keycloak combined. Built for the serverless era.", "Documentation for ZITADEL - Identity infrastructure, simplified for you.",
}, },
themeConfig: { themeConfig: {
metadata: [ metadata: [

View File

@ -284,6 +284,31 @@ module.exports = {
"guides/integrate/services/auth0-oidc", "guides/integrate/services/auth0-oidc",
"guides/integrate/services/auth0-saml", "guides/integrate/services/auth0-saml",
"guides/integrate/services/pingidentity-saml", "guides/integrate/services/pingidentity-saml",
{
type: 'link',
label: 'Nextcloud',
href: 'https://zitadel.com/blog/zitadel-as-sso-provider-for-selfhosting',
},
{
type: 'link',
label: 'Cloudflare workers',
href: 'https://zitadel.com/blog/increase-spa-security-with-cloudflare-workers',
},
{
type: 'link',
label: 'Firezone (firezone.dev)',
href: 'https://www.firezone.dev/docs/authenticate/oidc/zitadel',
},
{
type: 'link',
label: 'Psono (psono.com)',
href: 'https://doc.psono.com/admin/configuration/oidc-zitadel.html',
},
{
type: 'link',
label: 'Netbird (netbird.io)',
href: 'https://docs.netbird.io/selfhosted/identity-providers',
},
], ],
}, },
{ {

File diff suppressed because it is too large Load Diff

2
go.mod
View File

@ -1,6 +1,6 @@
module github.com/zitadel/zitadel module github.com/zitadel/zitadel
go 1.20 go 1.21
require ( require (
cloud.google.com/go/storage v1.32.0 cloud.google.com/go/storage v1.32.0

View File

@ -42,7 +42,7 @@ func (h *Handler) Commands() *command.Commands {
} }
func (h *Handler) ErrorHandler() ErrorHandler { func (h *Handler) ErrorHandler() ErrorHandler {
return DefaultErrorHandler return h.errorHandler
} }
func (h *Handler) Storage() static.Storage { func (h *Handler) Storage() static.Storage {
@ -75,10 +75,14 @@ type Downloader interface {
ResourceOwner(ctx context.Context, ownerPath string) string ResourceOwner(ctx context.Context, ownerPath string) string
} }
type ErrorHandler func(http.ResponseWriter, *http.Request, error, int) type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error, defaultCode int)
func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error, code int) { func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error, defaultCode int) {
logging.WithFields("uri", r.RequestURI).WithError(err).Warn("error occurred on asset api") logging.WithFields("uri", r.RequestURI).WithError(err).Warn("error occurred on asset api")
code, ok := http_util.ZitadelErrorToHTTPStatusCode(err)
if !ok {
code = defaultCode
}
http.Error(w, err.Error(), code) http.Error(w, err.Error(), code)
} }
@ -162,7 +166,7 @@ func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWrit
} }
err = uploader.UploadAsset(ctx, ctxData.OrgID, uploadInfo, s.Commands()) err = uploader.UploadAsset(ctx, ctxData.OrgID, uploadInfo, s.Commands())
if err != nil { if err != nil {
s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %v", err), http.StatusInternalServerError) s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %w", err), http.StatusInternalServerError)
return return
} }
} }
@ -190,10 +194,6 @@ func DownloadHandleFunc(s AssetsService, downloader Downloader) func(http.Respon
return return
} }
if err = GetAsset(w, r, resourceOwner, objectName, s.Storage()); err != nil { if err = GetAsset(w, r, resourceOwner, objectName, s.Storage()); err != nil {
if strings.Contains(err.Error(), "DATAB-pCP8P") {
s.ErrorHandler()(w, r, err, http.StatusNotFound)
return
}
s.ErrorHandler()(w, r, err, http.StatusInternalServerError) s.ErrorHandler()(w, r, err, http.StatusInternalServerError)
} }
} }
@ -206,11 +206,11 @@ func GetAsset(w http.ResponseWriter, r *http.Request, resourceOwner, objectName
} }
data, getInfo, err := storage.GetObject(r.Context(), authz.GetInstance(r.Context()).InstanceID(), resourceOwner, objectName) data, getInfo, err := storage.GetObject(r.Context(), authz.GetInstance(r.Context()).InstanceID(), resourceOwner, objectName)
if err != nil { if err != nil {
return fmt.Errorf("download failed: %v", err) return fmt.Errorf("download failed: %w", err)
} }
info, err := getInfo() info, err := getInfo()
if err != nil { if err != nil {
return fmt.Errorf("download failed: %v", err) return fmt.Errorf("download failed: %w", err)
} }
if info.Hash == strings.Trim(r.Header.Get(http_util.IfNoneMatch), "\"") { if info.Hash == strings.Trim(r.Header.Get(http_util.IfNoneMatch), "\"") {
w.Header().Set(http_util.LastModified, info.LastModified.Format(time.RFC1123)) w.Header().Set(http_util.LastModified, info.LastModified.Format(time.RFC1123))

View File

@ -132,9 +132,10 @@ func SecretGeneratorTypeToDomain(generatorType settings_pb.SecretGeneratorType)
func AddSMTPToConfig(req *admin_pb.AddSMTPConfigRequest) *smtp.Config { func AddSMTPToConfig(req *admin_pb.AddSMTPConfigRequest) *smtp.Config {
return &smtp.Config{ return &smtp.Config{
Tls: req.Tls, Tls: req.Tls,
From: req.SenderAddress, From: req.SenderAddress,
FromName: req.SenderName, FromName: req.SenderName,
ReplyToAddress: req.ReplyToAddress,
SMTP: smtp.SMTP{ SMTP: smtp.SMTP{
Host: req.Host, Host: req.Host,
User: req.User, User: req.User,
@ -145,9 +146,10 @@ func AddSMTPToConfig(req *admin_pb.AddSMTPConfigRequest) *smtp.Config {
func UpdateSMTPToConfig(req *admin_pb.UpdateSMTPConfigRequest) *smtp.Config { func UpdateSMTPToConfig(req *admin_pb.UpdateSMTPConfigRequest) *smtp.Config {
return &smtp.Config{ return &smtp.Config{
Tls: req.Tls, Tls: req.Tls,
From: req.SenderAddress, From: req.SenderAddress,
FromName: req.SenderName, FromName: req.SenderName,
ReplyToAddress: req.ReplyToAddress,
SMTP: smtp.SMTP{ SMTP: smtp.SMTP{
Host: req.Host, Host: req.Host,
User: req.User, User: req.User,
@ -157,12 +159,13 @@ func UpdateSMTPToConfig(req *admin_pb.UpdateSMTPConfigRequest) *smtp.Config {
func SMTPConfigToPb(smtp *query.SMTPConfig) *settings_pb.SMTPConfig { func SMTPConfigToPb(smtp *query.SMTPConfig) *settings_pb.SMTPConfig {
mapped := &settings_pb.SMTPConfig{ mapped := &settings_pb.SMTPConfig{
Tls: smtp.TLS, Tls: smtp.TLS,
SenderAddress: smtp.SenderAddress, SenderAddress: smtp.SenderAddress,
SenderName: smtp.SenderName, SenderName: smtp.SenderName,
Host: smtp.Host, ReplyToAddress: smtp.ReplyToAddress,
User: smtp.User, Host: smtp.Host,
Details: obj_grpc.ToViewDetailsPb(smtp.Sequence, smtp.CreationDate, smtp.ChangeDate, smtp.AggregateID), User: smtp.User,
Details: obj_grpc.ToViewDetailsPb(smtp.Sequence, smtp.CreationDate, smtp.ChangeDate, smtp.AggregateID),
} }
return mapped return mapped
} }

View File

@ -405,6 +405,27 @@ func (s *Server) UpdateLDAPProvider(ctx context.Context, req *admin_pb.UpdateLDA
}, nil }, nil
} }
func (s *Server) AddAppleProvider(ctx context.Context, req *admin_pb.AddAppleProviderRequest) (*admin_pb.AddAppleProviderResponse, error) {
id, details, err := s.command.AddInstanceAppleProvider(ctx, addAppleProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddAppleProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateAppleProvider(ctx context.Context, req *admin_pb.UpdateAppleProviderRequest) (*admin_pb.UpdateAppleProviderResponse, error) {
details, err := s.command.UpdateInstanceAppleProvider(ctx, req.Id, updateAppleProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateAppleProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) DeleteProvider(ctx context.Context, req *admin_pb.DeleteProviderRequest) (*admin_pb.DeleteProviderResponse, error) { func (s *Server) DeleteProvider(ctx context.Context, req *admin_pb.DeleteProviderRequest) (*admin_pb.DeleteProviderResponse, error) {
details, err := s.command.DeleteInstanceProvider(ctx, req.Id) details, err := s.command.DeleteInstanceProvider(ctx, req.Id)
if err != nil { if err != nil {

View File

@ -440,3 +440,27 @@ func updateLDAPProviderToCommand(req *admin_pb.UpdateLDAPProviderRequest) comman
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
} }
} }
func addAppleProviderToCommand(req *admin_pb.AddAppleProviderRequest) command.AppleProvider {
return command.AppleProvider{
Name: req.Name,
ClientID: req.ClientId,
TeamID: req.TeamId,
KeyID: req.KeyId,
PrivateKey: req.PrivateKey,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateAppleProviderToCommand(req *admin_pb.UpdateAppleProviderRequest) command.AppleProvider {
return command.AppleProvider{
Name: req.Name,
ClientID: req.ClientId,
TeamID: req.TeamId,
KeyID: req.KeyId,
PrivateKey: req.PrivateKey,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}

View File

@ -414,6 +414,8 @@ func providerTypeToPb(idpType domain.IDPType) idp_pb.ProviderType {
return idp_pb.ProviderType_PROVIDER_TYPE_GITLAB_SELF_HOSTED return idp_pb.ProviderType_PROVIDER_TYPE_GITLAB_SELF_HOSTED
case domain.IDPTypeGoogle: case domain.IDPTypeGoogle:
return idp_pb.ProviderType_PROVIDER_TYPE_GOOGLE return idp_pb.ProviderType_PROVIDER_TYPE_GOOGLE
case domain.IDPTypeApple:
return idp_pb.ProviderType_PROVIDER_TYPE_APPLE
case domain.IDPTypeUnspecified: case domain.IDPTypeUnspecified:
return idp_pb.ProviderType_PROVIDER_TYPE_UNSPECIFIED return idp_pb.ProviderType_PROVIDER_TYPE_UNSPECIFIED
default: default:
@ -470,6 +472,10 @@ func configToPb(config *query.IDPTemplate) *idp_pb.ProviderConfig {
ldapConfigToPb(providerConfig, config.LDAPIDPTemplate) ldapConfigToPb(providerConfig, config.LDAPIDPTemplate)
return providerConfig return providerConfig
} }
if config.AppleIDPTemplate != nil {
appleConfigToPb(providerConfig, config.AppleIDPTemplate)
return providerConfig
}
return providerConfig return providerConfig
} }
@ -620,3 +626,14 @@ func ldapAttributesToPb(attributes idp.LDAPAttributes) *idp_pb.LDAPAttributes {
ProfileAttribute: attributes.ProfileAttribute, ProfileAttribute: attributes.ProfileAttribute,
} }
} }
func appleConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.AppleIDPTemplate) {
providerConfig.Config = &idp_pb.ProviderConfig_Apple{
Apple: &idp_pb.AppleConfig{
ClientId: template.ClientID,
TeamId: template.TeamID,
KeyId: template.KeyID,
Scopes: template.Scopes,
},
}
}

View File

@ -397,6 +397,27 @@ func (s *Server) UpdateLDAPProvider(ctx context.Context, req *mgmt_pb.UpdateLDAP
}, nil }, nil
} }
func (s *Server) AddAppleProvider(ctx context.Context, req *mgmt_pb.AddAppleProviderRequest) (*mgmt_pb.AddAppleProviderResponse, error) {
id, details, err := s.command.AddOrgAppleProvider(ctx, authz.GetCtxData(ctx).OrgID, addAppleProviderToCommand(req))
if err != nil {
return nil, err
}
return &mgmt_pb.AddAppleProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateAppleProvider(ctx context.Context, req *mgmt_pb.UpdateAppleProviderRequest) (*mgmt_pb.UpdateAppleProviderResponse, error) {
details, err := s.command.UpdateOrgAppleProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id, updateAppleProviderToCommand(req))
if err != nil {
return nil, err
}
return &mgmt_pb.UpdateAppleProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) DeleteProvider(ctx context.Context, req *mgmt_pb.DeleteProviderRequest) (*mgmt_pb.DeleteProviderResponse, error) { func (s *Server) DeleteProvider(ctx context.Context, req *mgmt_pb.DeleteProviderRequest) (*mgmt_pb.DeleteProviderResponse, error) {
details, err := s.command.DeleteOrgProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id) details, err := s.command.DeleteOrgProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id)
if err != nil { if err != nil {

View File

@ -457,3 +457,27 @@ func updateLDAPProviderToCommand(req *mgmt_pb.UpdateLDAPProviderRequest) command
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
} }
} }
func addAppleProviderToCommand(req *mgmt_pb.AddAppleProviderRequest) command.AppleProvider {
return command.AppleProvider{
Name: req.Name,
ClientID: req.ClientId,
TeamID: req.TeamId,
KeyID: req.KeyId,
PrivateKey: req.PrivateKey,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateAppleProviderToCommand(req *mgmt_pb.UpdateAppleProviderRequest) command.AppleProvider {
return command.AppleProvider{
Name: req.Name,
ClientID: req.ClientId,
TeamID: req.TeamId,
KeyID: req.KeyId,
PrivateKey: req.PrivateKey,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}

View File

@ -45,7 +45,10 @@ func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRe
if err != nil { if err != nil {
return nil, err return nil, err
} }
challengeResponse, cmds := s.challengesToCommand(req.GetChallenges(), checks) challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks)
if err != nil {
return nil, err
}
set, err := s.command.CreateSession(ctx, cmds, metadata) set, err := s.command.CreateSession(ctx, cmds, metadata)
if err != nil { if err != nil {
@ -64,7 +67,10 @@ func (s *Server) SetSession(ctx context.Context, req *session.SetSessionRequest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
challengeResponse, cmds := s.challengesToCommand(req.GetChallenges(), checks) challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks)
if err != nil {
return nil, err
}
set, err := s.command.UpdateSession(ctx, req.GetSessionId(), req.GetSessionToken(), cmds, req.GetMetadata()) set, err := s.command.UpdateSession(ctx, req.GetSessionId(), req.GetSessionToken(), cmds, req.GetMetadata())
if err != nil { if err != nil {
@ -121,6 +127,8 @@ func factorsToPb(s *query.Session) *session.Factors {
WebAuthN: webAuthNFactorToPb(s.WebAuthNFactor), WebAuthN: webAuthNFactorToPb(s.WebAuthNFactor),
Intent: intentFactorToPb(s.IntentFactor), Intent: intentFactorToPb(s.IntentFactor),
Totp: totpFactorToPb(s.TOTPFactor), Totp: totpFactorToPb(s.TOTPFactor),
OtpSms: otpFactorToPb(s.OTPSMSFactor),
OtpEmail: otpFactorToPb(s.OTPEmailFactor),
} }
} }
@ -161,6 +169,15 @@ func totpFactorToPb(factor query.SessionTOTPFactor) *session.TOTPFactor {
} }
} }
func otpFactorToPb(factor query.SessionOTPFactor) *session.OTPFactor {
if factor.OTPCheckedAt.IsZero() {
return nil
}
return &session.OTPFactor{
VerifiedAt: timestamppb.New(factor.OTPCheckedAt),
}
}
func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor { func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor {
if factor.UserID == "" || factor.UserCheckedAt.IsZero() { if factor.UserID == "" || factor.UserCheckedAt.IsZero() {
return nil return nil
@ -240,7 +257,7 @@ func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([
if err != nil { if err != nil {
return nil, err return nil, err
} }
sessionChecks := make([]command.SessionCommand, 0, 3) sessionChecks := make([]command.SessionCommand, 0, 7)
if checkUser != nil { if checkUser != nil {
user, err := checkUser.search(ctx, s.query) user, err := checkUser.search(ctx, s.query)
if err != nil { if err != nil {
@ -258,14 +275,20 @@ func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([
sessionChecks = append(sessionChecks, s.command.CheckWebAuthN(passkey.GetCredentialAssertionData())) sessionChecks = append(sessionChecks, s.command.CheckWebAuthN(passkey.GetCredentialAssertionData()))
} }
if totp := checks.GetTotp(); totp != nil { if totp := checks.GetTotp(); totp != nil {
sessionChecks = append(sessionChecks, command.CheckTOTP(totp.GetTotp())) sessionChecks = append(sessionChecks, command.CheckTOTP(totp.GetCode()))
}
if otp := checks.GetOtpSms(); otp != nil {
sessionChecks = append(sessionChecks, command.CheckOTPSMS(otp.GetCode()))
}
if otp := checks.GetOtpEmail(); otp != nil {
sessionChecks = append(sessionChecks, command.CheckOTPEmail(otp.GetCode()))
} }
return sessionChecks, nil return sessionChecks, nil
} }
func (s *Server) challengesToCommand(challenges *session.RequestChallenges, cmds []command.SessionCommand) (*session.Challenges, []command.SessionCommand) { func (s *Server) challengesToCommand(challenges *session.RequestChallenges, cmds []command.SessionCommand) (*session.Challenges, []command.SessionCommand, error) {
if challenges == nil { if challenges == nil {
return nil, cmds return nil, cmds, nil
} }
resp := new(session.Challenges) resp := new(session.Challenges)
if req := challenges.GetWebAuthN(); req != nil { if req := challenges.GetWebAuthN(); req != nil {
@ -273,7 +296,20 @@ func (s *Server) challengesToCommand(challenges *session.RequestChallenges, cmds
resp.WebAuthN = challenge resp.WebAuthN = challenge
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
} }
return resp, cmds if req := challenges.GetOtpSms(); req != nil {
challenge, cmd := s.createOTPSMSChallengeCommand(req)
resp.OtpSms = challenge
cmds = append(cmds, cmd)
}
if req := challenges.GetOtpEmail(); req != nil {
challenge, cmd, err := s.createOTPEmailChallengeCommand(req)
if err != nil {
return nil, nil, err
}
resp.OtpEmail = challenge
cmds = append(cmds, cmd)
}
return resp, cmds, nil
} }
func (s *Server) createWebAuthNChallengeCommand(req *session.RequestChallenges_WebAuthN) (*session.Challenges_WebAuthN, command.SessionCommand) { func (s *Server) createWebAuthNChallengeCommand(req *session.RequestChallenges_WebAuthN) (*session.Challenges_WebAuthN, command.SessionCommand) {
@ -299,6 +335,34 @@ func userVerificationRequirementToDomain(req session.UserVerificationRequirement
} }
} }
func (s *Server) createOTPSMSChallengeCommand(req *session.RequestChallenges_OTPSMS) (*string, command.SessionCommand) {
if req.GetReturnCode() {
challenge := new(string)
return challenge, s.command.CreateOTPSMSChallengeReturnCode(challenge)
}
return nil, s.command.CreateOTPSMSChallenge()
}
func (s *Server) createOTPEmailChallengeCommand(req *session.RequestChallenges_OTPEmail) (*string, command.SessionCommand, error) {
switch t := req.GetDeliveryType().(type) {
case *session.RequestChallenges_OTPEmail_SendCode_:
cmd, err := s.command.CreateOTPEmailChallengeURLTemplate(t.SendCode.GetUrlTemplate())
if err != nil {
return nil, nil, err
}
return nil, cmd, nil
case *session.RequestChallenges_OTPEmail_ReturnCode_:
challenge := new(string)
return challenge, s.command.CreateOTPEmailChallengeReturnCode(challenge), nil
case nil:
return nil, s.command.CreateOTPEmailChallenge(), nil
default:
return nil, nil, caos_errs.ThrowUnimplementedf(nil, "SESSION-k3ng0", "delivery_type oneOf %T in OTPEmailChallenge not implemented", t)
}
}
func userCheck(user *session.CheckUser) (userSearch, error) { func userCheck(user *session.CheckUser) (userSearch, error) {
if user == nil { if user == nil {
return nil, nil return nil, nil

View File

@ -39,6 +39,14 @@ func TestMain(m *testing.M) {
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
User = Tester.CreateHumanUser(CTX) User = Tester.CreateHumanUser(CTX)
Tester.Client.UserV2.VerifyEmail(CTX, &user.VerifyEmailRequest{
UserId: User.GetUserId(),
VerificationCode: User.GetEmailCode(),
})
Tester.Client.UserV2.VerifyPhone(CTX, &user.VerifyPhoneRequest{
UserId: User.GetUserId(),
VerificationCode: User.GetPhoneCode(),
})
Tester.SetUserPassword(CTX, User.GetUserId(), integration.UserPassword) Tester.SetUserPassword(CTX, User.GetUserId(), integration.UserPassword)
Tester.RegisterUserPasskey(CTX, User.GetUserId()) Tester.RegisterUserPasskey(CTX, User.GetUserId())
return m.Run() return m.Run()
@ -75,6 +83,8 @@ const (
wantWebAuthNFactorUserVerified wantWebAuthNFactorUserVerified
wantTOTPFactor wantTOTPFactor
wantIntentFactor wantIntentFactor
wantOTPSMSFactor
wantOTPEmailFactor
) )
func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, want []wantFactor) { func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, want []wantFactor) {
@ -107,6 +117,14 @@ func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration,
pf := factors.GetIntent() pf := factors.GetIntent()
assert.NotNil(t, pf) assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window)) assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
case wantOTPSMSFactor:
pf := factors.GetOtpSms()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
case wantOTPEmailFactor:
pf := factors.GetOtpEmail()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
} }
} }
} }
@ -362,6 +380,20 @@ func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret stri
return secret return secret
} }
func registerOTPSMS(ctx context.Context, t *testing.T, userID string) {
_, err := Tester.Client.UserV2.AddOTPSMS(ctx, &user.AddOTPSMSRequest{
UserId: userID,
})
require.NoError(t, err)
}
func registerOTPEmail(ctx context.Context, t *testing.T, userID string) {
_, err := Tester.Client.UserV2.AddOTPEmail(ctx, &user.AddOTPEmailRequest{
UserId: userID,
})
require.NoError(t, err)
}
func TestServer_SetSession_flow(t *testing.T) { func TestServer_SetSession_flow(t *testing.T) {
// create new, empty session // create new, empty session
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
@ -421,6 +453,8 @@ func TestServer_SetSession_flow(t *testing.T) {
userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken) userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken)
Tester.RegisterUserU2F(userAuthCtx, User.GetUserId()) Tester.RegisterUserU2F(userAuthCtx, User.GetUserId())
totpSecret := registerTOTP(userAuthCtx, t, User.GetUserId()) totpSecret := registerTOTP(userAuthCtx, t, User.GetUserId())
registerOTPSMS(userAuthCtx, t, User.GetUserId())
registerOTPEmail(userAuthCtx, t, User.GetUserId())
t.Run("check webauthn, user not verified (U2F)", func(t *testing.T) { t.Run("check webauthn, user not verified (U2F)", func(t *testing.T) {
@ -470,7 +504,7 @@ func TestServer_SetSession_flow(t *testing.T) {
SessionToken: sessionToken, SessionToken: sessionToken,
Checks: &session.Checks{ Checks: &session.Checks{
Totp: &session.CheckTOTP{ Totp: &session.CheckTOTP{
Totp: code, Code: code,
}, },
}, },
}) })
@ -478,6 +512,66 @@ func TestServer_SetSession_flow(t *testing.T) {
sessionToken = resp.GetSessionToken() sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantTOTPFactor) verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantTOTPFactor)
}) })
t.Run("check OTP SMS", func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionToken,
Challenges: &session.RequestChallenges{
OtpSms: &session.RequestChallenges_OTPSMS{ReturnCode: true},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil)
sessionToken = resp.GetSessionToken()
otp := resp.GetChallenges().GetOtpSms()
require.NotEmpty(t, otp)
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionToken,
Checks: &session.Checks{
OtpSms: &session.CheckOTP{
Code: otp,
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantOTPSMSFactor)
})
t.Run("check OTP Email", func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionToken,
Challenges: &session.RequestChallenges{
OtpEmail: &session.RequestChallenges_OTPEmail{
DeliveryType: &session.RequestChallenges_OTPEmail_ReturnCode_{},
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil)
sessionToken = resp.GetSessionToken()
otp := resp.GetChallenges().GetOtpEmail()
require.NotEmpty(t, otp)
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionToken,
Checks: &session.Checks{
OtpEmail: &session.CheckOTP{
Code: otp,
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantOTPEmailFactor)
})
} }
func Test_ZITADEL_API_missing_authentication(t *testing.T) { func Test_ZITADEL_API_missing_authentication(t *testing.T) {

View File

@ -0,0 +1,47 @@
package http
import (
"errors"
"net/http"
caos_errs "github.com/zitadel/zitadel/internal/errors"
)
func ZitadelErrorToHTTPStatusCode(err error) (statusCode int, ok bool) {
if err == nil {
return http.StatusOK, true
}
//nolint:errorlint
switch err.(type) {
case *caos_errs.AlreadyExistsError:
return http.StatusConflict, true
case *caos_errs.DeadlineExceededError:
return http.StatusGatewayTimeout, true
case *caos_errs.InternalError:
return http.StatusInternalServerError, true
case *caos_errs.InvalidArgumentError:
return http.StatusBadRequest, true
case *caos_errs.NotFoundError:
return http.StatusNotFound, true
case *caos_errs.PermissionDeniedError:
return http.StatusForbidden, true
case *caos_errs.PreconditionFailedError:
// use the same code as grpc-gateway:
// https://github.com/grpc-ecosystem/grpc-gateway/blob/9e33e38f15cb7d2f11096366e62ea391a3459ba9/runtime/errors.go#L59
return http.StatusBadRequest, true
case *caos_errs.UnauthenticatedError:
return http.StatusUnauthorized, true
case *caos_errs.UnavailableError:
return http.StatusServiceUnavailable, true
case *caos_errs.UnimplementedError:
return http.StatusNotImplemented, true
case *caos_errs.ResourceExhaustedError:
return http.StatusTooManyRequests, true
default:
c := new(caos_errs.CaosError)
if errors.As(err, &c) {
return ZitadelErrorToHTTPStatusCode(errors.Unwrap(err))
}
return http.StatusInternalServerError, false
}
}

View File

@ -0,0 +1,138 @@
package http
import (
"errors"
"fmt"
"net/http"
"testing"
caos_errors "github.com/zitadel/zitadel/internal/errors"
)
func TestZitadelErrorToHTTPStatusCode(t *testing.T) {
type args struct {
err error
}
tests := []struct {
name string
args args
wantStatusCode int
wantOk bool
}{
{
name: "no error",
args: args{
err: nil,
},
wantStatusCode: http.StatusOK,
wantOk: true,
},
{
name: "wrapped already exists",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowAlreadyExists(nil, "id", "message")),
},
wantStatusCode: http.StatusConflict,
wantOk: true,
},
{
name: "wrapped deadline exceeded",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowDeadlineExceeded(nil, "id", "message")),
},
wantStatusCode: http.StatusGatewayTimeout,
wantOk: true,
},
{
name: "wrapped internal",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowInternal(nil, "id", "message")),
},
wantStatusCode: http.StatusInternalServerError,
wantOk: true,
},
{
name: "wrapped invalid argument",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowInvalidArgument(nil, "id", "message")),
},
wantStatusCode: http.StatusBadRequest,
wantOk: true,
},
{
name: "wrapped not found",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowNotFound(nil, "id", "message")),
},
wantStatusCode: http.StatusNotFound,
wantOk: true,
},
{
name: "wrapped permission denied",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowPermissionDenied(nil, "id", "message")),
},
wantStatusCode: http.StatusForbidden,
wantOk: true,
},
{
name: "wrapped precondition failed",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowPreconditionFailed(nil, "id", "message")),
},
wantStatusCode: http.StatusBadRequest,
wantOk: true,
},
{
name: "wrapped unauthenticated",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowUnauthenticated(nil, "id", "message")),
},
wantStatusCode: http.StatusUnauthorized,
wantOk: true,
},
{
name: "wrapped unavailable",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowUnavailable(nil, "id", "message")),
},
wantStatusCode: http.StatusServiceUnavailable,
wantOk: true,
},
{
name: "wrapped unimplemented",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowUnimplemented(nil, "id", "message")),
},
wantStatusCode: http.StatusNotImplemented,
wantOk: true,
},
{
name: "wrapped resource exhausted",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowResourceExhausted(nil, "id", "message")),
},
wantStatusCode: http.StatusTooManyRequests,
wantOk: true,
},
{
name: "no caos/zitadel error",
args: args{
err: errors.New("error"),
},
wantStatusCode: http.StatusInternalServerError,
wantOk: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotStatusCode, gotOk := ZitadelErrorToHTTPStatusCode(tt.args.err)
if gotStatusCode != tt.wantStatusCode {
t.Errorf("ZitadelErrorToHTTPStatusCode() gotStatusCode = %v, want %v", gotStatusCode, tt.wantStatusCode)
}
if gotOk != tt.wantOk {
t.Errorf("ZitadelErrorToHTTPStatusCode() gotOk = %v, want %v", gotOk, tt.wantOk)
}
})
}
}

View File

@ -16,6 +16,7 @@ import (
z_errs "github.com/zitadel/zitadel/internal/errors" z_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/form" "github.com/zitadel/zitadel/internal/form"
"github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/apple"
"github.com/zitadel/zitadel/internal/idp/providers/azuread" "github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/github" "github.com/zitadel/zitadel/internal/idp/providers/github"
"github.com/zitadel/zitadel/internal/idp/providers/gitlab" "github.com/zitadel/zitadel/internal/idp/providers/gitlab"
@ -52,6 +53,9 @@ type externalIDPCallbackData struct {
Code string `schema:"code"` Code string `schema:"code"`
Error string `schema:"error"` Error string `schema:"error"`
ErrorDescription string `schema:"error_description"` ErrorDescription string `schema:"error_description"`
// Apple returns a user on first registration
User string `schema:"user"`
} }
// CallbackURL generates the instance specific URL to the IDP callback handler // CallbackURL generates the instance specific URL to the IDP callback handler
@ -115,7 +119,7 @@ func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) {
return return
} }
idpUser, idpSession, err := h.fetchIDPUser(ctx, provider, data.Code) idpUser, idpSession, err := h.fetchIDPUser(ctx, provider, data.Code, data.User)
if err != nil { if err != nil {
cmdErr := h.commands.FailIDPIntent(ctx, intent, err.Error()) cmdErr := h.commands.FailIDPIntent(ctx, intent, err.Error())
logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent")
@ -214,7 +218,7 @@ func redirectToFailureURL(w http.ResponseWriter, r *http.Request, i *command.IDP
http.Redirect(w, r, i.FailureURL.String(), http.StatusFound) http.Redirect(w, r, i.FailureURL.String(), http.StatusFound)
} }
func (h *Handler) fetchIDPUser(ctx context.Context, identityProvider idp.Provider, code string) (user idp.User, idpTokens idp.Session, err error) { func (h *Handler) fetchIDPUser(ctx context.Context, identityProvider idp.Provider, code string, appleUser string) (user idp.User, idpTokens idp.Session, err error) {
var session idp.Session var session idp.Session
switch provider := identityProvider.(type) { switch provider := identityProvider.(type) {
case *oauth.Provider: case *oauth.Provider:
@ -229,6 +233,8 @@ func (h *Handler) fetchIDPUser(ctx context.Context, identityProvider idp.Provide
session = &openid.Session{Provider: provider.Provider, Code: code} session = &openid.Session{Provider: provider.Provider, Code: code}
case *google.Provider: case *google.Provider:
session = &openid.Session{Provider: provider.Provider, Code: code} session = &openid.Session{Provider: provider.Provider, Code: code}
case *apple.Provider:
session = &apple.Session{Session: &openid.Session{Provider: provider.Provider, Code: code}, UserFormValue: appleUser}
case *jwt.Provider, *ldap.Provider: case *jwt.Provider, *ldap.Provider:
return nil, nil, z_errs.ThrowInvalidArgument(nil, "IDP-52jmn", "Errors.ExternalIDP.IDPTypeNotImplemented") return nil, nil, z_errs.ThrowInvalidArgument(nil, "IDP-52jmn", "Errors.ExternalIDP.IDPTypeNotImplemented")
default: default:

View File

@ -3,9 +3,8 @@ package login
import ( import (
"net/http" "net/http"
"github.com/zitadel/zitadel/internal/domain"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
) )
const ( const (

View File

@ -18,6 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/apple"
"github.com/zitadel/zitadel/internal/idp/providers/azuread" "github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/github" "github.com/zitadel/zitadel/internal/idp/providers/github"
"github.com/zitadel/zitadel/internal/idp/providers/gitlab" "github.com/zitadel/zitadel/internal/idp/providers/gitlab"
@ -41,6 +42,9 @@ type externalIDPData struct {
type externalIDPCallbackData struct { type externalIDPCallbackData struct {
State string `schema:"state"` State string `schema:"state"`
Code string `schema:"code"` Code string `schema:"code"`
// Apple returns a user on first registration
User string `schema:"user"`
} }
type externalNotFoundOptionFormData struct { type externalNotFoundOptionFormData struct {
@ -159,6 +163,8 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
provider, err = l.gitlabSelfHostedProvider(r.Context(), identityProvider) provider, err = l.gitlabSelfHostedProvider(r.Context(), identityProvider)
case domain.IDPTypeGoogle: case domain.IDPTypeGoogle:
provider, err = l.googleProvider(r.Context(), identityProvider) provider, err = l.googleProvider(r.Context(), identityProvider)
case domain.IDPTypeApple:
provider, err = l.appleProvider(r.Context(), identityProvider)
case domain.IDPTypeLDAP: case domain.IDPTypeLDAP:
provider, err = l.ldapProvider(r.Context(), identityProvider) provider, err = l.ldapProvider(r.Context(), identityProvider)
case domain.IDPTypeUnspecified: case domain.IDPTypeUnspecified:
@ -180,6 +186,18 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
http.Redirect(w, r, session.GetAuthURL(), http.StatusFound) http.Redirect(w, r, session.GetAuthURL(), http.StatusFound)
} }
// handleExternalLoginCallbackForm handles the callback from a IDP with form_post.
// It will redirect to the "normal" callback endpoint with the form data as query parameter.
// This way cookies will be handled correctly (same site = lax).
func (l *Login) handleExternalLoginCallbackForm(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
l.renderLogin(w, r, nil, err)
return
}
http.Redirect(w, r, HandlerPrefix+EndpointExternalLoginCallback+"?"+r.Form.Encode(), 302)
}
// handleExternalLoginCallback handles the callback from a IDP // handleExternalLoginCallback handles the callback from a IDP
// and tries to extract the user with the provided data // and tries to extract the user with the provided data
func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Request) { func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Request) {
@ -259,6 +277,13 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
return return
} }
session = &openid.Session{Provider: provider.(*google.Provider).Provider, Code: data.Code} session = &openid.Session{Provider: provider.(*google.Provider).Provider, Code: data.Code}
case domain.IDPTypeApple:
provider, err = l.appleProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
return
}
session = &apple.Session{Session: &openid.Session{Provider: provider.(*apple.Provider).Provider, Code: data.Code}, UserFormValue: data.User}
case domain.IDPTypeJWT, case domain.IDPTypeJWT,
domain.IDPTypeLDAP, domain.IDPTypeLDAP,
domain.IDPTypeUnspecified: domain.IDPTypeUnspecified:
@ -936,6 +961,21 @@ func (l *Login) gitlabSelfHostedProvider(ctx context.Context, identityProvider *
) )
} }
func (l *Login) appleProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*apple.Provider, error) {
privateKey, err := crypto.Decrypt(identityProvider.AppleIDPTemplate.PrivateKey, l.idpConfigAlg)
if err != nil {
return nil, err
}
return apple.New(
identityProvider.AppleIDPTemplate.ClientID,
identityProvider.AppleIDPTemplate.TeamID,
identityProvider.AppleIDPTemplate.KeyID,
l.baseURL(ctx)+EndpointExternalLoginCallbackFormPost,
privateKey,
identityProvider.AppleIDPTemplate.Scopes,
)
}
func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserGrant, resourceOwner string) error { func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserGrant, resourceOwner string) error {
if len(userGrants) == 0 { if len(userGrants) == 0 {
return nil return nil
@ -971,6 +1011,8 @@ func tokens(session idp.Session) *oidc.Tokens[*oidc.IDTokenClaims] {
return s.Tokens return s.Tokens
case *azuread.Session: case *azuread.Session:
return s.Tokens return s.Tokens
case *apple.Session:
return s.Tokens
} }
return nil return nil
} }

View File

@ -47,6 +47,9 @@ type Config struct {
CSRFCookieName string CSRFCookieName string
Cache middleware.CacheConfig Cache middleware.CacheConfig
AssetCache middleware.CacheConfig AssetCache middleware.CacheConfig
// LoginV2
DefaultOTPEmailURLV2 string
} }
const ( const (
@ -117,6 +120,12 @@ func createCSRFInterceptor(cookieName string, csrfCookieKey []byte, externalSecu
handler.ServeHTTP(w, r) handler.ServeHTTP(w, r)
return return
} }
// ignore form post callback
// it will redirect to the "normal" callback, where the cookie is set again
if r.URL.Path == EndpointExternalLoginCallbackFormPost && r.Method == http.MethodPost {
handler.ServeHTTP(w, r)
return
}
csrf.Protect(csrfCookieKey, csrf.Protect(csrfCookieKey,
csrf.Secure(externalSecure), csrf.Secure(externalSecure),
csrf.CookieName(http_utils.SetCookiePrefix(cookieName, "", path, externalSecure)), csrf.CookieName(http_utils.SetCookiePrefix(cookieName, "", path, externalSecure)),

View File

@ -7,44 +7,45 @@ import (
) )
const ( const (
EndpointRoot = "/" EndpointRoot = "/"
EndpointHealthz = "/healthz" EndpointHealthz = "/healthz"
EndpointReadiness = "/ready" EndpointReadiness = "/ready"
EndpointLogin = "/login" EndpointLogin = "/login"
EndpointExternalLogin = "/login/externalidp" EndpointExternalLogin = "/login/externalidp"
EndpointExternalLoginCallback = "/login/externalidp/callback" EndpointExternalLoginCallback = "/login/externalidp/callback"
EndpointJWTAuthorize = "/login/jwt/authorize" EndpointExternalLoginCallbackFormPost = "/login/externalidp/callback/form"
EndpointJWTCallback = "/login/jwt/callback" EndpointJWTAuthorize = "/login/jwt/authorize"
EndpointLDAPLogin = "/login/ldap" EndpointJWTCallback = "/login/jwt/callback"
EndpointLDAPCallback = "/login/ldap/callback" EndpointLDAPLogin = "/login/ldap"
EndpointPasswordlessLogin = "/login/passwordless" EndpointLDAPCallback = "/login/ldap/callback"
EndpointPasswordlessRegistration = "/login/passwordless/init" EndpointPasswordlessLogin = "/login/passwordless"
EndpointPasswordlessPrompt = "/login/passwordless/prompt" EndpointPasswordlessRegistration = "/login/passwordless/init"
EndpointLoginName = "/loginname" EndpointPasswordlessPrompt = "/login/passwordless/prompt"
EndpointUserSelection = "/userselection" EndpointLoginName = "/loginname"
EndpointChangeUsername = "/username/change" EndpointUserSelection = "/userselection"
EndpointPassword = "/password" EndpointChangeUsername = "/username/change"
EndpointInitPassword = "/password/init" EndpointPassword = "/password"
EndpointChangePassword = "/password/change" EndpointInitPassword = "/password/init"
EndpointPasswordReset = "/password/reset" EndpointChangePassword = "/password/change"
EndpointInitUser = "/user/init" EndpointPasswordReset = "/password/reset"
EndpointMFAVerify = "/mfa/verify" EndpointInitUser = "/user/init"
EndpointMFAPrompt = "/mfa/prompt" EndpointMFAVerify = "/mfa/verify"
EndpointMFAInitVerify = "/mfa/init/verify" EndpointMFAPrompt = "/mfa/prompt"
EndpointMFASMSInitVerify = "/mfa/init/sms/verify" EndpointMFAInitVerify = "/mfa/init/verify"
EndpointMFAOTPVerify = "/mfa/otp/verify" EndpointMFASMSInitVerify = "/mfa/init/sms/verify"
EndpointMFAInitU2FVerify = "/mfa/init/u2f/verify" EndpointMFAOTPVerify = "/mfa/otp/verify"
EndpointU2FVerification = "/mfa/u2f/verify" EndpointMFAInitU2FVerify = "/mfa/init/u2f/verify"
EndpointMailVerification = "/mail/verification" EndpointU2FVerification = "/mfa/u2f/verify"
EndpointMailVerified = "/mail/verified" EndpointMailVerification = "/mail/verification"
EndpointRegisterOption = "/register/option" EndpointMailVerified = "/mail/verified"
EndpointRegister = "/register" EndpointRegisterOption = "/register/option"
EndpointExternalRegister = "/register/externalidp" EndpointRegister = "/register"
EndpointExternalRegisterCallback = "/register/externalidp/callback" EndpointExternalRegister = "/register/externalidp"
EndpointRegisterOrg = "/register/org" EndpointExternalRegisterCallback = "/register/externalidp/callback"
EndpointLogoutDone = "/logout/done" EndpointRegisterOrg = "/register/org"
EndpointLoginSuccess = "/login/success" EndpointLogoutDone = "/logout/done"
EndpointExternalNotFoundOption = "/externaluser/option" EndpointLoginSuccess = "/login/success"
EndpointExternalNotFoundOption = "/externaluser/option"
EndpointResources = "/resources" EndpointResources = "/resources"
EndpointDynamicResources = "/resources/dynamic" EndpointDynamicResources = "/resources/dynamic"
@ -71,6 +72,7 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
router.HandleFunc(EndpointLogin, login.handleLogin).Methods(http.MethodGet, http.MethodPost) router.HandleFunc(EndpointLogin, login.handleLogin).Methods(http.MethodGet, http.MethodPost)
router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet) router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet)
router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet) router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet)
router.HandleFunc(EndpointExternalLoginCallbackFormPost, login.handleExternalLoginCallbackForm).Methods(http.MethodPost)
router.HandleFunc(EndpointJWTAuthorize, login.handleJWTRequest).Methods(http.MethodGet) router.HandleFunc(EndpointJWTAuthorize, login.handleJWTRequest).Methods(http.MethodGet)
router.HandleFunc(EndpointJWTCallback, login.handleJWTCallback).Methods(http.MethodGet) router.HandleFunc(EndpointJWTCallback, login.handleJWTCallback).Methods(http.MethodGet)
router.HandleFunc(EndpointPasswordlessLogin, login.handlePasswordlessVerification).Methods(http.MethodPost) router.HandleFunc(EndpointPasswordlessLogin, login.handlePasswordlessVerification).Methods(http.MethodPost)

View File

@ -360,6 +360,7 @@ Footer:
PrivacyPolicy: Политика за поверителност PrivacyPolicy: Политика за поверителност
Help: Помогне Help: Помогне
SupportEmail: Поддръжка на имейл SupportEmail: Поддръжка на имейл
SignIn: Влезте с {{.Provider}}
Errors: Errors:
Internal: Възникна вътрешна грешка Internal: Възникна вътрешна грешка
AuthRequest: AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: Hilfe Help: Hilfe
SupportEmail: Support E-Mail SupportEmail: Support E-Mail
SignIn: Mit {{.Provider}} anmelden
Errors: Errors:
Internal: Es ist ein interner Fehler aufgetreten Internal: Es ist ein interner Fehler aufgetreten
AuthRequest: AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: Help Help: Help
SupportEmail: Support E-mail SupportEmail: Support E-mail
SignIn: Sign in with {{.Provider}}
Errors: Errors:
Internal: An internal error occurred Internal: An internal error occurred
AuthRequest: AuthRequest:

View File

@ -354,6 +354,8 @@ Footer:
Help: Ayuda Help: Ayuda
SupportEmail: Email de soporte SupportEmail: Email de soporte
SignIn: Iniciar sesión con {{.Provider}}
Errors: Errors:
Internal: Se produjo un error interno Internal: Se produjo un error interno
AuthRequest: AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: Aide Help: Aide
SupportEmail: E-mail d'assistance SupportEmail: E-mail d'assistance
SignIn: Connexion avec {{.Provider}}
Errors: Errors:
Internal: Une erreur interne s'est produite Internal: Une erreur interne s'est produite
AuthRequest: AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: Aiuto Help: Aiuto
SupportEmail: E-mail di supporto SupportEmail: E-mail di supporto
SignIn: Accedi con {{.Provider}}
Errors: Errors:
Internal: Si è verificato un errore interno Internal: Si è verificato un errore interno
AuthRequest: AuthRequest:

View File

@ -363,6 +363,8 @@ Footer:
PrivacyPolicy: プライバシーポリシー PrivacyPolicy: プライバシーポリシー
Help: ヘルプ Help: ヘルプ
SignIn: '{{.Provider}} でサインイン'
Errors: Errors:
Internal: 内部でエラーが発生しました Internal: 内部でエラーが発生しました
AuthRequest: AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: Помош Help: Помош
SupportEmail: Е-пошта за поддршка SupportEmail: Е-пошта за поддршка
SignIn: Пријавете се со {{.Provider}}
Errors: Errors:
Internal: Се појави внатрешна грешка Internal: Се појави внатрешна грешка
AuthRequest: AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: Pomoc Help: Pomoc
SupportEmail: E-mail wsparcia SupportEmail: E-mail wsparcia
SignIn: Zaloguj się, używając konta {{.Provider}}
Errors: Errors:
Internal: Wewnętrzny błąd Internal: Wewnętrzny błąd
AuthRequest: AuthRequest:

View File

@ -366,6 +366,8 @@ Footer:
Help: Ajuda Help: Ajuda
SupportEmail: E-mail de suporte SupportEmail: E-mail de suporte
SignIn: Iniciar sessão com a {{.Provider}}
Errors: Errors:
Internal: Ocorreu um erro interno Internal: Ocorreu um erro interno
AuthRequest: AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: 帮助 Help: 帮助
SupportEmail: 支持邮箱 SupportEmail: 支持邮箱
SignIn: 通过 {{.Provider}} 登录
Errors: Errors:
Internal: 发生了内部错误 Internal: 发生了内部错误
AuthRequest: AuthRequest:

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="56px" height="56px" viewBox="18 15.5 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61 (89581) - https://sketch.com -->
<title>White Logo Square </title>
<desc>Created with Sketch.</desc>
<g id="White-Logo-Square-" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" fill="" x="6" y="6" width="44" height="44"></rect>
<path d="M28.2226562,20.3846154 C29.0546875,20.3846154 30.0976562,19.8048315 30.71875,19.0317864 C31.28125,18.3312142 31.6914062,17.352829 31.6914062,16.3744437 C31.6914062,16.2415766 31.6796875,16.1087095 31.65625,16 C30.7304687,16.0362365 29.6171875,16.640178 28.9492187,17.4494596 C28.421875,18.06548 27.9414062,19.0317864 27.9414062,20.0222505 C27.9414062,20.1671964 27.9648438,20.3121424 27.9765625,20.3604577 C28.0351562,20.3725366 28.1289062,20.3846154 28.2226562,20.3846154 Z M25.2929688,35 C26.4296875,35 26.9335938,34.214876 28.3515625,34.214876 C29.7929688,34.214876 30.109375,34.9758423 31.375,34.9758423 C32.6171875,34.9758423 33.4492188,33.792117 34.234375,32.6325493 C35.1132812,31.3038779 35.4765625,29.9993643 35.5,29.9389701 C35.4179688,29.9148125 33.0390625,28.9122695 33.0390625,26.0979021 C33.0390625,23.6579784 34.9140625,22.5588048 35.0195312,22.474253 C33.7773438,20.6382708 31.890625,20.5899555 31.375,20.5899555 C29.9804688,20.5899555 28.84375,21.4596313 28.1289062,21.4596313 C27.3554688,21.4596313 26.3359375,20.6382708 25.1289062,20.6382708 C22.8320312,20.6382708 20.5,22.5950413 20.5,26.2911634 C20.5,28.5861411 21.3671875,31.013986 22.4335938,32.5842339 C23.3476562,33.9129053 24.1445312,35 25.2929688,35 Z" id="" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="56px" height="56px" viewBox="18 15.5 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61 (89581) - https://sketch.com -->
<title>Black Logo Square</title>
<desc>Created with Sketch.</desc>
<g id="Black-Logo-Square" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" fill="" x="6" y="6" width="44" height="44"></rect>
<path d="M28.2226562,20.3846154 C29.0546875,20.3846154 30.0976562,19.8048315 30.71875,19.0317864 C31.28125,18.3312142 31.6914062,17.352829 31.6914062,16.3744437 C31.6914062,16.2415766 31.6796875,16.1087095 31.65625,16 C30.7304687,16.0362365 29.6171875,16.640178 28.9492187,17.4494596 C28.421875,18.06548 27.9414062,19.0317864 27.9414062,20.0222505 C27.9414062,20.1671964 27.9648438,20.3121424 27.9765625,20.3604577 C28.0351562,20.3725366 28.1289062,20.3846154 28.2226562,20.3846154 Z M25.2929688,35 C26.4296875,35 26.9335938,34.214876 28.3515625,34.214876 C29.7929688,34.214876 30.109375,34.9758423 31.375,34.9758423 C32.6171875,34.9758423 33.4492188,33.792117 34.234375,32.6325493 C35.1132812,31.3038779 35.4765625,29.9993643 35.5,29.9389701 C35.4179688,29.9148125 33.0390625,28.9122695 33.0390625,26.0979021 C33.0390625,23.6579784 34.9140625,22.5588048 35.0195312,22.474253 C33.7773438,20.6382708 31.890625,20.5899555 31.375,20.5899555 C29.9804688,20.5899555 28.84375,21.4596313 28.1289062,21.4596313 C27.3554688,21.4596313 26.3359375,20.6382708 25.1289062,20.6382708 C22.8320312,20.6382708 20.5,22.5950413 20.5,26.2911634 C20.5,28.5861411 21.3671875,31.013986 22.4335938,32.5842339 C23.3476562,33.9129053 24.1445312,35 25.2929688,35 Z" id="" fill="#000000" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -2,6 +2,7 @@ $lgn-idp-margin: 0.5rem 0;
$lgn-idp-padding: 0 1px; $lgn-idp-padding: 0 1px;
$lgn-idp-provider-name-line-height: 36px; $lgn-idp-provider-name-line-height: 36px;
$lgn-idp-border-radius: 0.5rem; $lgn-idp-border-radius: 0.5rem;
$lgn-idp-logo-size: 46px;
@mixin lgn-idp-base { @mixin lgn-idp-base {
display: block; display: block;
@ -17,14 +18,14 @@ $lgn-idp-border-radius: 0.5rem;
transition: border-color 0.2s ease-in-out; transition: border-color 0.2s ease-in-out;
span.logo { span.logo {
height: 46px; height: $lgn-idp-logo-size;
width: 46px; width: $lgn-idp-logo-size;
} }
span.provider-name { span.provider-name {
line-height: $lgn-idp-provider-name-line-height; line-height: $lgn-idp-provider-name-line-height;
position: absolute; position: relative;
left: 50%; left: calc(50% - $lgn-idp-logo-size);
transform: translateX(-50%); transform: translateX(-50%);
} }
@ -75,4 +76,17 @@ $lgn-idp-border-radius: 0.5rem;
border-radius: 5px; border-radius: 5px;
} }
} }
&.apple {
span.logo {
height: 46px;
width: 46px;
background-image: var(--apple-image-src);
background-size: 25px;
background-position: center;
background-repeat: no-repeat;
border-radius: 5px;
transform: translateY(-2px);
}
}
} }

View File

@ -117,6 +117,8 @@
--zitadel-color-github-background: #ffffff; --zitadel-color-github-background: #ffffff;
--zitadel-color-gitlab-text: #8b8d8d; --zitadel-color-gitlab-text: #8b8d8d;
--zitadel-color-gitlab-background: #ffffff; --zitadel-color-gitlab-background: #ffffff;
--zitadel-color-apple-text: #8b8d8d;
--zitadel-color-apple-background: #ffffff;
--zitadel-color-qr: var(--zitadel-color-black); --zitadel-color-qr: var(--zitadel-color-black);
--zitadel-color-qr-background: var(--zitadel-color-white); --zitadel-color-qr-background: var(--zitadel-color-white);
@ -125,6 +127,7 @@
--github-image-src: url(../../../images/idp/github.png); --github-image-src: url(../../../images/idp/github.png);
--gitlab-image-src: url(../../../images/idp/gitlab.png); --gitlab-image-src: url(../../../images/idp/gitlab.png);
--azure-image-src: url(../../../images/idp/ms.svg); --azure-image-src: url(../../../images/idp/ms.svg);
--apple-image-src: url(../../../images/idp/apple.svg);
} }
.lgn-dark-theme { .lgn-dark-theme {
@ -227,9 +230,12 @@
--zitadel-color-github-background: #ffffff; --zitadel-color-github-background: #ffffff;
--zitadel-color-gitlab-text: #8b8d8d; --zitadel-color-gitlab-text: #8b8d8d;
--zitadel-color-gitlab-background: #ffffff; --zitadel-color-gitlab-background: #ffffff;
--zitadel-color-apple-text: #8b8d8d;
--zitadel-color-apple-background: #ffffff;
--google-image-src: url(../../../images/idp/google.png); --google-image-src: url(../../../images/idp/google.png);
--github-image-src: url(../../../images/idp/github-white.png); --github-image-src: url(../../../images/idp/github-white.png);
--gitlab-image-src: url(../../../images/idp/gitlab.png); --gitlab-image-src: url(../../../images/idp/gitlab.png);
--azure-image-src: url(../../../images/idp/ms.svg); --azure-image-src: url(../../../images/idp/ms.svg);
--apple-image-src: url(../../../images/idp/apple-dark.svg);
} }

View File

@ -52,7 +52,11 @@
<a href="{{ externalIDPAuthURL $reqid $provider.IDPConfigID}}" <a href="{{ externalIDPAuthURL $reqid $provider.IDPConfigID}}"
class="lgn-idp {{idpProviderClass $provider.IDPType}}"> class="lgn-idp {{idpProviderClass $provider.IDPType}}">
<span class="logo"></span> <span class="logo"></span>
{{if $provider.IDPType.IsSignInButton}}
<span class="provider-name">{{t "SignIn" "Provider" $provider.DisplayName}}</span>
{{else}}
<span class="provider-name">{{$provider.DisplayName}}</span> <span class="provider-name">{{$provider.DisplayName}}</span>
{{end}}
</a> </a>
{{end}} {{end}}
</div> </div>

View File

@ -29,7 +29,11 @@
<a href="{{ externalIDPRegisterURL $reqid $provider.IDPConfigID}}" <a href="{{ externalIDPRegisterURL $reqid $provider.IDPConfigID}}"
class="lgn-idp {{idpProviderClass $provider.IDPType}}"> class="lgn-idp {{idpProviderClass $provider.IDPType}}">
<span class="logo"></span> <span class="logo"></span>
{{if $provider.IDPType.IsSignInButton}}
<span class="provider-name">{{t "SignIn" "Provider" $provider.DisplayName}}</span>
{{else}}
<span class="provider-name">{{$provider.DisplayName}}</span> <span class="provider-name">{{$provider.DisplayName}}</span>
{{end}}
</a> </a>
{{end}} {{end}}
{{end}} {{end}}

View File

@ -200,21 +200,15 @@ func authMethodsFromSession(session *query.Session) []domain.UserAuthMethodType
if !session.IntentFactor.IntentCheckedAt.IsZero() { if !session.IntentFactor.IntentCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeIDP) types = append(types, domain.UserAuthMethodTypeIDP)
} }
// TODO: add checks with https://github.com/zitadel/zitadel/issues/5477 if !session.TOTPFactor.TOTPCheckedAt.IsZero() {
/* types = append(types, domain.UserAuthMethodTypeTOTP)
if !session.TOTPFactor.TOTPCheckedAt.IsZero() { }
types = append(types, domain.UserAuthMethodTypeTOTP) if !session.OTPSMSFactor.OTPCheckedAt.IsZero() {
} types = append(types, domain.UserAuthMethodTypeOTPSMS)
*/ }
// TODO: add checks with https://github.com/zitadel/zitadel/issues/6224 if !session.OTPEmailFactor.OTPCheckedAt.IsZero() {
/* types = append(types, domain.UserAuthMethodTypeOTPEmail)
if !session.TOTPFactor.OTPSMSCheckedAt.IsZero() { }
types = append(types, domain.UserAuthMethodTypeOTPSMS)
}
if !session.TOTPFactor.OTPEmailCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeOTPEmail)
}
*/
return types return types
} }

View File

@ -34,8 +34,9 @@ import (
type Commands struct { type Commands struct {
httpClient *http.Client httpClient *http.Client
checkPermission domain.PermissionCheck checkPermission domain.PermissionCheck
newCode cryptoCodeFunc newCode cryptoCodeFunc
newCodeWithDefault cryptoCodeWithDefaultFunc
eventstore *eventstore.Eventstore eventstore *eventstore.Eventstore
static static.Storage static static.Storage
@ -122,6 +123,7 @@ func StartCommands(
httpClient: httpClient, httpClient: httpClient,
checkPermission: permissionCheck, checkPermission: permissionCheck,
newCode: newCryptoCode, newCode: newCryptoCode,
newCodeWithDefault: newCryptoCodeWithDefaultConfig,
sessionTokenCreator: sessionTokenCreator(idGenerator, sessionAlg), sessionTokenCreator: sessionTokenCreator(idGenerator, sessionAlg),
sessionTokenVerifier: sessionTokenVerifier, sessionTokenVerifier: sessionTokenVerifier,
defaultAccessTokenLifetime: defaultAccessTokenLifetime, defaultAccessTokenLifetime: defaultAccessTokenLifetime,

View File

@ -12,6 +12,10 @@ import (
type cryptoCodeFunc func(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCode, error) type cryptoCodeFunc func(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCode, error)
type cryptoCodeWithDefaultFunc func(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto, defaultConfig *crypto.GeneratorConfig) (*CryptoCode, error)
var emptyConfig = &crypto.GeneratorConfig{}
type CryptoCode struct { type CryptoCode struct {
Crypted *crypto.CryptoValue Crypted *crypto.CryptoValue
Plain string Plain string
@ -19,7 +23,11 @@ type CryptoCode struct {
} }
func newCryptoCode(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCode, error) { func newCryptoCode(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCode, error) {
gen, config, err := secretGenerator(ctx, filter, typ, alg) return newCryptoCodeWithDefaultConfig(ctx, filter, typ, alg, emptyConfig)
}
func newCryptoCodeWithDefaultConfig(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto, defaultConfig *crypto.GeneratorConfig) (*CryptoCode, error) {
gen, config, err := secretGenerator(ctx, filter, typ, alg, defaultConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -35,15 +43,15 @@ func newCryptoCode(ctx context.Context, filter preparation.FilterToQueryReducer,
} }
func verifyCryptoCode(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto, creation time.Time, expiry time.Duration, crypted *crypto.CryptoValue, plain string) error { func verifyCryptoCode(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto, creation time.Time, expiry time.Duration, crypted *crypto.CryptoValue, plain string) error {
gen, _, err := secretGenerator(ctx, filter, typ, alg) gen, _, err := secretGenerator(ctx, filter, typ, alg, emptyConfig)
if err != nil { if err != nil {
return err return err
} }
return crypto.VerifyCode(creation, expiry, crypted, plain, gen) return crypto.VerifyCode(creation, expiry, crypted, plain, gen)
} }
func secretGenerator(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (crypto.Generator, *crypto.GeneratorConfig, error) { func secretGenerator(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto, defaultConfig *crypto.GeneratorConfig) (crypto.Generator, *crypto.GeneratorConfig, error) {
config, err := secretGeneratorConfig(ctx, filter, typ) config, err := secretGeneratorConfigWithDefault(ctx, filter, typ, defaultConfig)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -58,6 +66,10 @@ func secretGenerator(ctx context.Context, filter preparation.FilterToQueryReduce
} }
func secretGeneratorConfig(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType) (*crypto.GeneratorConfig, error) { func secretGeneratorConfig(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType) (*crypto.GeneratorConfig, error) {
return secretGeneratorConfigWithDefault(ctx, filter, typ, emptyConfig)
}
func secretGeneratorConfigWithDefault(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, defaultConfig *crypto.GeneratorConfig) (*crypto.GeneratorConfig, error) {
wm := NewInstanceSecretGeneratorConfigWriteModel(ctx, typ) wm := NewInstanceSecretGeneratorConfigWriteModel(ctx, typ)
events, err := filter(ctx, wm.Query()) events, err := filter(ctx, wm.Query())
if err != nil { if err != nil {
@ -67,6 +79,9 @@ func secretGeneratorConfig(ctx context.Context, filter preparation.FilterToQuery
if err := wm.Reduce(); err != nil { if err := wm.Reduce(); err != nil {
return nil, err return nil, err
} }
if wm.State != domain.SecretGeneratorStateActive {
return defaultConfig, nil
}
return &crypto.GeneratorConfig{ return &crypto.GeneratorConfig{
Length: wm.Length, Length: wm.Length,
Expiry: wm.Expiry, Expiry: wm.Expiry,

View File

@ -33,6 +33,21 @@ func mockCode(code string, exp time.Duration) cryptoCodeFunc {
} }
} }
func mockCodeWithDefault(code string, exp time.Duration) cryptoCodeWithDefaultFunc {
return func(ctx context.Context, filter preparation.FilterToQueryReducer, _ domain.SecretGeneratorType, alg crypto.Crypto, _ *crypto.GeneratorConfig) (*CryptoCode, error) {
return &CryptoCode{
Crypted: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte(code),
},
Plain: code,
Expiry: exp,
}, nil
}
}
var ( var (
testGeneratorConfig = crypto.GeneratorConfig{ testGeneratorConfig = crypto.GeneratorConfig{
Length: 12, Length: 12,
@ -175,8 +190,9 @@ func Test_verifyCryptoCode(t *testing.T) {
func Test_secretGenerator(t *testing.T) { func Test_secretGenerator(t *testing.T) {
type args struct { type args struct {
typ domain.SecretGeneratorType typ domain.SecretGeneratorType
alg crypto.Crypto alg crypto.Crypto
defaultConfig *crypto.GeneratorConfig
} }
tests := []struct { tests := []struct {
name string name string
@ -190,8 +206,9 @@ func Test_secretGenerator(t *testing.T) {
name: "filter config error", name: "filter config error",
eventsore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)), eventsore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)),
args: args{ args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode, typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockHashAlg(gomock.NewController(t)), alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
defaultConfig: emptyConfig,
}, },
wantErr: io.ErrClosedPipe, wantErr: io.ErrClosedPipe,
}, },
@ -201,8 +218,9 @@ func Test_secretGenerator(t *testing.T) {
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)), eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
)), )),
args: args{ args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode, typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockHashAlg(gomock.NewController(t)), alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
defaultConfig: emptyConfig,
}, },
want: crypto.NewHashGenerator(testGeneratorConfig, crypto.CreateMockHashAlg(gomock.NewController(t))), want: crypto.NewHashGenerator(testGeneratorConfig, crypto.CreateMockHashAlg(gomock.NewController(t))),
wantConf: &testGeneratorConfig, wantConf: &testGeneratorConfig,
@ -213,8 +231,31 @@ func Test_secretGenerator(t *testing.T) {
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)), eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
)), )),
args: args{ args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode, typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
defaultConfig: emptyConfig,
},
want: crypto.NewEncryptionGenerator(testGeneratorConfig, crypto.CreateMockEncryptionAlg(gomock.NewController(t))),
wantConf: &testGeneratorConfig,
},
{
name: "hash generator with default config",
eventsore: eventstoreExpect(t, expectFilter()),
args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
defaultConfig: &testGeneratorConfig,
},
want: crypto.NewHashGenerator(testGeneratorConfig, crypto.CreateMockHashAlg(gomock.NewController(t))),
wantConf: &testGeneratorConfig,
},
{
name: "encryption generator with default config",
eventsore: eventstoreExpect(t, expectFilter()),
args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
defaultConfig: &testGeneratorConfig,
}, },
want: crypto.NewEncryptionGenerator(testGeneratorConfig, crypto.CreateMockEncryptionAlg(gomock.NewController(t))), want: crypto.NewEncryptionGenerator(testGeneratorConfig, crypto.CreateMockEncryptionAlg(gomock.NewController(t))),
wantConf: &testGeneratorConfig, wantConf: &testGeneratorConfig,
@ -225,15 +266,16 @@ func Test_secretGenerator(t *testing.T) {
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)), eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
)), )),
args: args{ args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode, typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: nil, alg: nil,
defaultConfig: emptyConfig,
}, },
wantErr: errors.ThrowInternalf(nil, "COMMA-RreV6", "Errors.Internal unsupported crypto algorithm type %T", nil), wantErr: errors.ThrowInternalf(nil, "COMMA-RreV6", "Errors.Internal unsupported crypto algorithm type %T", nil),
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, gotConf, err := secretGenerator(context.Background(), tt.eventsore.Filter, tt.args.typ, tt.args.alg) got, gotConf, err := secretGenerator(context.Background(), tt.eventsore.Filter, tt.args.typ, tt.args.alg, tt.args.defaultConfig)
require.ErrorIs(t, err, tt.wantErr) require.ErrorIs(t, err, tt.wantErr)
assert.IsType(t, tt.want, got) assert.IsType(t, tt.want, got)
assert.Equal(t, tt.wantConf, gotConf) assert.Equal(t, tt.wantConf, gotConf)

View File

@ -110,6 +110,16 @@ type LDAPProvider struct {
IDPOptions idp.Options IDPOptions idp.Options
} }
type AppleProvider struct {
Name string
ClientID string
TeamID string
KeyID string
PrivateKey []byte
Scopes []string
IDPOptions idp.Options
}
func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id, orgID string) (exists bool, err error) { func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id, orgID string) (exists bool, err error) {
writeModel := NewOrgIDPRemoveWriteModel(orgID, id) writeModel := NewOrgIDPRemoveWriteModel(orgID, id)
events, err := filter(ctx, writeModel.Query()) events, err := filter(ctx, writeModel.Query())

View File

@ -14,6 +14,7 @@ import (
"github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/apple"
"github.com/zitadel/zitadel/internal/idp/providers/azuread" "github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/jwt" "github.com/zitadel/zitadel/internal/idp/providers/jwt"
"github.com/zitadel/zitadel/internal/idp/providers/oauth" "github.com/zitadel/zitadel/internal/idp/providers/oauth"
@ -215,6 +216,8 @@ func tokensForSucceededIDPIntent(session idp.Session, encryptionAlg crypto.Encry
tokens = s.Tokens tokens = s.Tokens
case *azuread.Session: case *azuread.Session:
tokens = s.Tokens tokens = s.Tokens
case *apple.Session:
tokens = s.Tokens
default: default:
return nil, "", nil return nil, "", nil
} }

View File

@ -3,6 +3,7 @@ package command
import ( import (
"net/http" "net/http"
"reflect" "reflect"
"slices"
"time" "time"
"github.com/zitadel/logging" "github.com/zitadel/logging"
@ -14,6 +15,7 @@ import (
"github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
providers "github.com/zitadel/zitadel/internal/idp" providers "github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/apple"
"github.com/zitadel/zitadel/internal/idp/providers/azuread" "github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/github" "github.com/zitadel/zitadel/internal/idp/providers/github"
"github.com/zitadel/zitadel/internal/idp/providers/gitlab" "github.com/zitadel/zitadel/internal/idp/providers/gitlab"
@ -1587,6 +1589,138 @@ func (wm *LDAPIDPWriteModel) GetProviderOptions() idp.Options {
return wm.Options return wm.Options
} }
type AppleIDPWriteModel struct {
eventstore.WriteModel
ID string
Name string
ClientID string
TeamID string
KeyID string
PrivateKey *crypto.CryptoValue
Scopes []string
idp.Options
State domain.IDPState
}
func (wm *AppleIDPWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *idp.AppleIDPAddedEvent:
wm.reduceAddedEvent(e)
case *idp.AppleIDPChangedEvent:
wm.reduceChangedEvent(e)
case *idp.RemovedEvent:
wm.State = domain.IDPStateRemoved
}
}
return wm.WriteModel.Reduce()
}
func (wm *AppleIDPWriteModel) reduceAddedEvent(e *idp.AppleIDPAddedEvent) {
wm.Name = e.Name
wm.ClientID = e.ClientID
wm.TeamID = e.TeamID
wm.KeyID = e.KeyID
wm.PrivateKey = e.PrivateKey
wm.Scopes = e.Scopes
wm.Options = e.Options
wm.State = domain.IDPStateActive
}
func (wm *AppleIDPWriteModel) reduceChangedEvent(e *idp.AppleIDPChangedEvent) {
if e.Name != nil {
wm.Name = *e.Name
}
if e.ClientID != nil {
wm.ClientID = *e.ClientID
}
if e.PrivateKey != nil {
wm.PrivateKey = e.PrivateKey
}
if e.Scopes != nil {
wm.Scopes = e.Scopes
}
wm.Options.ReduceChanges(e.OptionChanges)
}
func (wm *AppleIDPWriteModel) NewChanges(
name string,
clientID string,
teamID string,
keyID string,
privateKey []byte,
secretCrypto crypto.Crypto,
scopes []string,
options idp.Options,
) ([]idp.AppleIDPChanges, error) {
changes := make([]idp.AppleIDPChanges, 0)
var encryptedKey *crypto.CryptoValue
var err error
if len(privateKey) != 0 {
encryptedKey, err = crypto.Crypt(privateKey, secretCrypto)
if err != nil {
return nil, err
}
changes = append(changes, idp.ChangeApplePrivateKey(encryptedKey))
}
if wm.Name != name {
changes = append(changes, idp.ChangeAppleName(name))
}
if wm.ClientID != clientID {
changes = append(changes, idp.ChangeAppleClientID(clientID))
}
if wm.TeamID != teamID {
changes = append(changes, idp.ChangeAppleTeamID(teamID))
}
if wm.KeyID != keyID {
changes = append(changes, idp.ChangeAppleKeyID(keyID))
}
if slices.Compare(wm.Scopes, scopes) != 0 {
changes = append(changes, idp.ChangeAppleScopes(scopes))
}
opts := wm.Options.Changes(options)
if !opts.IsZero() {
changes = append(changes, idp.ChangeAppleOptions(opts))
}
return changes, nil
}
func (wm *AppleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
privateKey, err := crypto.Decrypt(wm.PrivateKey, idpAlg)
if err != nil {
return nil, err
}
opts := make([]oidc.ProviderOpts, 0, 4)
if wm.IsCreationAllowed {
opts = append(opts, oidc.WithCreationAllowed())
}
if wm.IsLinkingAllowed {
opts = append(opts, oidc.WithLinkingAllowed())
}
if wm.IsAutoCreation {
opts = append(opts, oidc.WithAutoCreation())
}
if wm.IsAutoUpdate {
opts = append(opts, oidc.WithAutoUpdate())
}
return apple.New(
wm.ClientID,
wm.TeamID,
wm.KeyID,
callbackURL,
privateKey,
wm.Scopes,
opts...,
)
}
func (wm *AppleIDPWriteModel) GetProviderOptions() idp.Options {
return wm.Options
}
type IDPRemoveWriteModel struct { type IDPRemoveWriteModel struct {
eventstore.WriteModel eventstore.WriteModel
@ -1617,6 +1751,8 @@ func (wm *IDPRemoveWriteModel) Reduce() error {
wm.reduceAdded(e.ID) wm.reduceAdded(e.ID)
case *idp.LDAPIDPAddedEvent: case *idp.LDAPIDPAddedEvent:
wm.reduceAdded(e.ID) wm.reduceAdded(e.ID)
case *idp.AppleIDPAddedEvent:
wm.reduceAdded(e.ID)
case *idp.RemovedEvent: case *idp.RemovedEvent:
wm.reduceRemoved(e.ID) wm.reduceRemoved(e.ID)
case *idpconfig.IDPConfigAddedEvent: case *idpconfig.IDPConfigAddedEvent:
@ -1699,6 +1835,10 @@ func (wm *IDPTypeWriteModel) Reduce() error {
wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate()) wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
case *org.LDAPIDPAddedEvent: case *org.LDAPIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate()) wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
case *instance.AppleIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeApple, e.Aggregate())
case *org.AppleIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeApple, e.Aggregate())
case *instance.OIDCIDPMigratedAzureADEvent: case *instance.OIDCIDPMigratedAzureADEvent:
wm.reduceChanged(e.ID, domain.IDPTypeAzureAD) wm.reduceChanged(e.ID, domain.IDPTypeAzureAD)
case *org.OIDCIDPMigratedAzureADEvent: case *org.OIDCIDPMigratedAzureADEvent:
@ -1774,6 +1914,7 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
instance.GitLabSelfHostedIDPAddedEventType, instance.GitLabSelfHostedIDPAddedEventType,
instance.GoogleIDPAddedEventType, instance.GoogleIDPAddedEventType,
instance.LDAPIDPAddedEventType, instance.LDAPIDPAddedEventType,
instance.AppleIDPAddedEventType,
instance.OIDCIDPMigratedAzureADEventType, instance.OIDCIDPMigratedAzureADEventType,
instance.OIDCIDPMigratedGoogleEventType, instance.OIDCIDPMigratedGoogleEventType,
instance.IDPRemovedEventType, instance.IDPRemovedEventType,
@ -1792,6 +1933,7 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
org.GitLabSelfHostedIDPAddedEventType, org.GitLabSelfHostedIDPAddedEventType,
org.GoogleIDPAddedEventType, org.GoogleIDPAddedEventType,
org.LDAPIDPAddedEventType, org.LDAPIDPAddedEventType,
org.AppleIDPAddedEventType,
org.OIDCIDPMigratedAzureADEventType, org.OIDCIDPMigratedAzureADEventType,
org.OIDCIDPMigratedGoogleEventType, org.OIDCIDPMigratedGoogleEventType,
org.IDPRemovedEventType, org.IDPRemovedEventType,
@ -1859,6 +2001,8 @@ func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idp
writeModel.model = NewGitLabSelfHostedInstanceIDPWriteModel(resourceOwner, id) writeModel.model = NewGitLabSelfHostedInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGoogle: case domain.IDPTypeGoogle:
writeModel.model = NewGoogleInstanceIDPWriteModel(resourceOwner, id) writeModel.model = NewGoogleInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeApple:
writeModel.model = NewAppleInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeUnspecified: case domain.IDPTypeUnspecified:
fallthrough fallthrough
default: default:
@ -1886,6 +2030,8 @@ func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idp
writeModel.model = NewGitLabSelfHostedOrgIDPWriteModel(resourceOwner, id) writeModel.model = NewGitLabSelfHostedOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGoogle: case domain.IDPTypeGoogle:
writeModel.model = NewGoogleOrgIDPWriteModel(resourceOwner, id) writeModel.model = NewGoogleOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeApple:
writeModel.model = NewAppleOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeUnspecified: case domain.IDPTypeUnspecified:
fallthrough fallthrough
default: default:

View File

@ -414,6 +414,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
instanceAgg, instanceAgg,
setup.SMTPConfiguration.From, setup.SMTPConfiguration.From,
setup.SMTPConfiguration.FromName, setup.SMTPConfiguration.FromName,
setup.SMTPConfiguration.ReplyToAddress,
setup.SMTPConfiguration.SMTP.Host, setup.SMTPConfiguration.SMTP.Host,
setup.SMTPConfiguration.SMTP.User, setup.SMTPConfiguration.SMTP.User,
[]byte(setup.SMTPConfiguration.SMTP.Password), []byte(setup.SMTPConfiguration.SMTP.Password),

View File

@ -467,6 +467,48 @@ func (c *Commands) UpdateInstanceLDAPProvider(ctx context.Context, id string, pr
return pushedEventsToObjectDetails(pushedEvents), nil return pushedEventsToObjectDetails(pushedEvents), nil
} }
func (c *Commands) AddInstanceAppleProvider(ctx context.Context, provider AppleProvider) (string, *domain.ObjectDetails, error) {
instanceID := authz.GetInstance(ctx).InstanceID()
instanceAgg := instance.NewAggregate(instanceID)
id, err := c.idGenerator.Next()
if err != nil {
return "", nil, err
}
writeModel := NewAppleInstanceIDPWriteModel(instanceID, id)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceAppleProvider(instanceAgg, writeModel, provider))
if err != nil {
return "", nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return "", nil, err
}
return id, pushedEventsToObjectDetails(pushedEvents), nil
}
func (c *Commands) UpdateInstanceAppleProvider(ctx context.Context, id string, provider AppleProvider) (*domain.ObjectDetails, error) {
instanceID := authz.GetInstance(ctx).InstanceID()
instanceAgg := instance.NewAggregate(instanceID)
writeModel := NewAppleInstanceIDPWriteModel(instanceID, id)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateInstanceAppleProvider(instanceAgg, writeModel, provider))
if err != nil {
return nil, err
}
if len(cmds) == 0 {
// no change, so return directly
return &domain.ObjectDetails{
Sequence: writeModel.ProcessedSequence,
EventDate: writeModel.ChangeDate,
ResourceOwner: writeModel.ResourceOwner,
}, nil
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func (c *Commands) DeleteInstanceProvider(ctx context.Context, id string) (*domain.ObjectDetails, error) { func (c *Commands) DeleteInstanceProvider(ctx context.Context, id string) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareDeleteInstanceProvider(instanceAgg, id)) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareDeleteInstanceProvider(instanceAgg, id))
@ -1518,6 +1560,98 @@ func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, writ
} }
} }
func (c *Commands) prepareAddInstanceAppleProvider(a *instance.Aggregate, writeModel *InstanceAppleIDPWriteModel, provider AppleProvider) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-jkn3w", "Errors.IDP.ClientIDMissing")
}
if provider.TeamID = strings.TrimSpace(provider.TeamID); provider.TeamID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Ffg32", "Errors.IDP.TeamIDMissing")
}
if provider.KeyID = strings.TrimSpace(provider.KeyID); provider.KeyID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-GDjm5", "Errors.IDP.KeyIDMissing")
}
if len(provider.PrivateKey) == 0 {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-GVD4n", "Errors.IDP.PrivateKeyMissing")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
privateKey, err := crypto.Encrypt(provider.PrivateKey, c.idpConfigEncryption)
if err != nil {
return nil, err
}
return []eventstore.Command{
instance.NewAppleIDPAddedEvent(
ctx,
&a.Aggregate,
writeModel.ID,
provider.Name,
provider.ClientID,
provider.TeamID,
provider.KeyID,
privateKey,
provider.Scopes,
provider.IDPOptions,
),
}, nil
}, nil
}
}
func (c *Commands) prepareUpdateInstanceAppleProvider(a *instance.Aggregate, writeModel *InstanceAppleIDPWriteModel, provider AppleProvider) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-FRHBH", "Errors.IDMissing")
}
if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SFm4l", "Errors.IDP.ClientIDMissing")
}
if provider.TeamID = strings.TrimSpace(provider.TeamID); provider.TeamID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SG34t", "Errors.IDP.TeamIDMissing")
}
if provider.KeyID = strings.TrimSpace(provider.KeyID); provider.KeyID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Gh4z2", "Errors.IDP.KeyIDMissing")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if !writeModel.State.Exists() {
return nil, caos_errs.ThrowNotFound(nil, "INST-SG3bh", "Errors.IDPConfig.NotExisting")
}
event, err := writeModel.NewChangedEvent(
ctx,
&a.Aggregate,
writeModel.ID,
provider.Name,
provider.ClientID,
provider.TeamID,
provider.KeyID,
provider.PrivateKey,
c.idpConfigEncryption,
provider.Scopes,
provider.IDPOptions,
)
if err != nil || event == nil {
return nil, err
}
return []eventstore.Command{event}, nil
}, nil
}
}
func (c *Commands) prepareDeleteInstanceProvider(a *instance.Aggregate, id string) preparation.Validation { func (c *Commands) prepareDeleteInstanceProvider(a *instance.Aggregate, id string) preparation.Validation {
return func() (preparation.CreateCommands, error) { return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {

View File

@ -793,6 +793,73 @@ func (wm *InstanceLDAPIDPWriteModel) NewChangedEvent(
return instance.NewLDAPIDPChangedEvent(ctx, aggregate, id, changes) return instance.NewLDAPIDPChangedEvent(ctx, aggregate, id, changes)
} }
type InstanceAppleIDPWriteModel struct {
AppleIDPWriteModel
}
func NewAppleInstanceIDPWriteModel(instanceID, id string) *InstanceAppleIDPWriteModel {
return &InstanceAppleIDPWriteModel{
AppleIDPWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: instanceID,
ResourceOwner: instanceID,
},
ID: id,
},
}
}
func (wm *InstanceAppleIDPWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *instance.AppleIDPAddedEvent:
wm.AppleIDPWriteModel.AppendEvents(&e.AppleIDPAddedEvent)
case *instance.AppleIDPChangedEvent:
wm.AppleIDPWriteModel.AppendEvents(&e.AppleIDPChangedEvent)
case *instance.IDPRemovedEvent:
wm.AppleIDPWriteModel.AppendEvents(&e.RemovedEvent)
default:
wm.AppleIDPWriteModel.AppendEvents(e)
}
}
}
func (wm *InstanceAppleIDPWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(instance.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
instance.AppleIDPAddedEventType,
instance.AppleIDPChangedEventType,
instance.IDPRemovedEventType,
).
EventData(map[string]interface{}{"id": wm.ID}).
Builder()
}
func (wm *InstanceAppleIDPWriteModel) NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id,
name,
clientID,
teamID,
keyID string,
privateKey []byte,
secretCrypto crypto.Crypto,
scopes []string,
options idp.Options,
) (*instance.AppleIDPChangedEvent, error) {
changes, err := wm.AppleIDPWriteModel.NewChanges(name, clientID, teamID, keyID, privateKey, secretCrypto, scopes, options)
if err != nil || len(changes) == 0 {
return nil, err
}
return instance.NewAppleIDPChangedEvent(ctx, aggregate, id, changes)
}
type InstanceIDPRemoveWriteModel struct { type InstanceIDPRemoveWriteModel struct {
IDPRemoveWriteModel IDPRemoveWriteModel
} }
@ -832,6 +899,8 @@ func (wm *InstanceIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event)
wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent) wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent)
case *instance.LDAPIDPAddedEvent: case *instance.LDAPIDPAddedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent) wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
case *instance.AppleIDPAddedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.AppleIDPAddedEvent)
case *instance.IDPRemovedEvent: case *instance.IDPRemovedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent) wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent)
case *instance.IDPConfigAddedEvent: case *instance.IDPConfigAddedEvent:
@ -861,6 +930,7 @@ func (wm *InstanceIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder {
instance.GitLabSelfHostedIDPAddedEventType, instance.GitLabSelfHostedIDPAddedEventType,
instance.GoogleIDPAddedEventType, instance.GoogleIDPAddedEventType,
instance.LDAPIDPAddedEventType, instance.LDAPIDPAddedEventType,
instance.AppleIDPAddedEventType,
instance.IDPRemovedEventType, instance.IDPRemovedEventType,
). ).
EventData(map[string]interface{}{"id": wm.ID}). EventData(map[string]interface{}{"id": wm.ID}).

View File

@ -4857,3 +4857,464 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) {
}) })
} }
} }
func TestCommandSide_AddInstanceAppleIDP(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
idGenerator id.Generator
secretCrypto crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
provider AppleProvider
}
type res struct {
id string
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"invalid clientID",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-jkn3w", "Errors.IDP.ClientIDMissing"))
},
},
},
{
"invalid teamID",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{
ClientID: "clientID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-Ffg32", "Errors.IDP.TeamIDMissing"))
},
},
},
{
"invalid keyID",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-GDjm5", "Errors.IDP.KeyIDMissing"))
},
},
},
{
"invalid privateKey",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
KeyID: "keyID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-GVD4n", "Errors.IDP.PrivateKeyMissing"))
},
},
},
{
name: "ok",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"instance1",
instance.NewAppleIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
"",
"clientID",
"teamID",
"keyID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("privateKey"),
},
nil,
idp.Options{},
)),
},
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
KeyID: "keyID",
PrivateKey: []byte("privateKey"),
},
},
res: res{
id: "id1",
want: &domain.ObjectDetails{ResourceOwner: "instance1"},
},
},
{
name: "ok all set",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"instance1",
instance.NewAppleIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
"",
"clientID",
"teamID",
"keyID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("privateKey"),
},
[]string{"name", "email"},
idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
)),
},
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
KeyID: "keyID",
PrivateKey: []byte("privateKey"),
Scopes: []string{"name", "email"},
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
},
},
res: res{
id: "id1",
want: &domain.ObjectDetails{ResourceOwner: "instance1"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
idGenerator: tt.fields.idGenerator,
idpConfigEncryption: tt.fields.secretCrypto,
}
id, got, err := c.AddInstanceAppleProvider(tt.args.ctx, tt.args.provider)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.id, id)
assert.Equal(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_UpdateInstanceAppleIDP(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
secretCrypto crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
id string
provider AppleProvider
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"invalid id",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-FRHBH", "Errors.IDMissing"))
},
},
},
{
"invalid clientID",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: AppleProvider{},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-SFm4l", "Errors.IDP.ClientIDMissing"))
},
},
},
{
"invalid teamID",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: AppleProvider{
ClientID: "clientID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-SG34t", "Errors.IDP.TeamIDMissing"))
},
},
},
{
"invalid keyID",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-Gh4z2", "Errors.IDP.KeyIDMissing"))
},
},
},
{
name: "not found",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
KeyID: "keyID",
},
},
res: res{
err: caos_errors.IsNotFound,
},
},
{
name: "no changes",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
instance.NewAppleIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
"",
"clientID",
"teamID",
"keyID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("privateKey"),
},
nil,
idp.Options{},
)),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
KeyID: "keyID",
},
},
res: res{
want: &domain.ObjectDetails{ResourceOwner: "instance1"},
},
},
{
name: "change ok",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
instance.NewAppleIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
"",
"clientID",
"teamID",
"keyID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("privateKey"),
},
nil,
idp.Options{},
)),
),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"instance1",
func() eventstore.Command {
t := true
event, _ := instance.NewAppleIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
[]idp.AppleIDPChanges{
idp.ChangeAppleClientID("clientID2"),
idp.ChangeAppleTeamID("teamID2"),
idp.ChangeAppleKeyID("keyID2"),
idp.ChangeApplePrivateKey(&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("newPrivateKey"),
}),
idp.ChangeAppleScopes([]string{"name", "email"}),
idp.ChangeAppleOptions(idp.OptionChanges{
IsCreationAllowed: &t,
IsLinkingAllowed: &t,
IsAutoCreation: &t,
IsAutoUpdate: &t,
}),
},
)
return event
}(),
),
},
),
),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: AppleProvider{
ClientID: "clientID2",
TeamID: "teamID2",
KeyID: "keyID2",
PrivateKey: []byte("newPrivateKey"),
Scopes: []string{"name", "email"},
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
},
},
res: res{
want: &domain.ObjectDetails{ResourceOwner: "instance1"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
idpConfigEncryption: tt.fields.secretCrypto,
}
got, err := c.UpdateInstanceAppleProvider(tt.args.ctx, tt.args.id, tt.args.provider)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}

View File

@ -12,13 +12,14 @@ import (
type InstanceSMTPConfigWriteModel struct { type InstanceSMTPConfigWriteModel struct {
eventstore.WriteModel eventstore.WriteModel
SenderAddress string SenderAddress string
SenderName string SenderName string
TLS bool ReplyToAddress string
Host string TLS bool
User string Host string
Password *crypto.CryptoValue User string
State domain.SMTPConfigState Password *crypto.CryptoValue
State domain.SMTPConfigState
domain string domain string
domainState domain.InstanceDomainState domainState domain.InstanceDomainState
@ -62,6 +63,7 @@ func (wm *InstanceSMTPConfigWriteModel) Reduce() error {
wm.TLS = e.TLS wm.TLS = e.TLS
wm.SenderAddress = e.SenderAddress wm.SenderAddress = e.SenderAddress
wm.SenderName = e.SenderName wm.SenderName = e.SenderName
wm.ReplyToAddress = e.ReplyToAddress
wm.Host = e.Host wm.Host = e.Host
wm.User = e.User wm.User = e.User
wm.Password = e.Password wm.Password = e.Password
@ -76,6 +78,9 @@ func (wm *InstanceSMTPConfigWriteModel) Reduce() error {
if e.FromName != nil { if e.FromName != nil {
wm.SenderName = *e.FromName wm.SenderName = *e.FromName
} }
if e.ReplyToAddress != nil {
wm.ReplyToAddress = *e.ReplyToAddress
}
if e.Host != nil { if e.Host != nil {
wm.Host = *e.Host wm.Host = *e.Host
} }
@ -87,6 +92,7 @@ func (wm *InstanceSMTPConfigWriteModel) Reduce() error {
wm.TLS = false wm.TLS = false
wm.SenderName = "" wm.SenderName = ""
wm.SenderAddress = "" wm.SenderAddress = ""
wm.ReplyToAddress = ""
wm.Host = "" wm.Host = ""
wm.User = "" wm.User = ""
wm.Password = nil wm.Password = nil
@ -122,7 +128,7 @@ func (wm *InstanceSMTPConfigWriteModel) Query() *eventstore.SearchQueryBuilder {
Builder() Builder()
} }
func (wm *InstanceSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, tls bool, fromAddress, fromName, smtpHost, smtpUser string) (*instance.SMTPConfigChangedEvent, bool, error) { func (wm *InstanceSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, tls bool, fromAddress, fromName, replyToAddress, smtpHost, smtpUser string) (*instance.SMTPConfigChangedEvent, bool, error) {
changes := make([]instance.SMTPConfigChanges, 0) changes := make([]instance.SMTPConfigChanges, 0)
var err error var err error
@ -135,6 +141,9 @@ func (wm *InstanceSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, agg
if wm.SenderName != fromName { if wm.SenderName != fromName {
changes = append(changes, instance.ChangeSMTPConfigFromName(fromName)) changes = append(changes, instance.ChangeSMTPConfigFromName(fromName))
} }
if wm.ReplyToAddress != replyToAddress {
changes = append(changes, instance.ChangeSMTPConfigReplyToAddress(replyToAddress))
}
if wm.Host != smtpHost { if wm.Host != smtpHost {
changes = append(changes, instance.ChangeSMTPConfigSMTPHost(smtpHost)) changes = append(changes, instance.ChangeSMTPConfigSMTPHost(smtpHost))
} }

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