mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-06 14:25:16 +00:00
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:
commit
38f7b1bd06
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
with:
|
||||
node_version: '18'
|
||||
buf_version: 'latest'
|
||||
go_version: '1.20'
|
||||
go_version: '1.21'
|
||||
|
||||
console:
|
||||
uses: ./.github/workflows/console.yml
|
||||
@ -35,7 +35,7 @@ jobs:
|
||||
needs: [core, console, version]
|
||||
uses: ./.github/workflows/compile.yml
|
||||
with:
|
||||
go_version: '1.20'
|
||||
go_version: '1.21'
|
||||
core_cache_key: ${{ needs.core.outputs.cache_key }}
|
||||
console_cache_key: ${{ needs.console.outputs.cache_key }}
|
||||
core_cache_path: ${{ needs.core.outputs.cache_path }}
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
needs: core
|
||||
uses: ./.github/workflows/core-unit-test.yml
|
||||
with:
|
||||
go_version: '1.20'
|
||||
go_version: '1.21'
|
||||
core_cache_key: ${{ needs.core.outputs.cache_key }}
|
||||
core_cache_path: ${{ needs.core.outputs.cache_path }}
|
||||
|
||||
@ -54,7 +54,7 @@ jobs:
|
||||
needs: core
|
||||
uses: ./.github/workflows/core-integration-test.yml
|
||||
with:
|
||||
go_version: '1.20'
|
||||
go_version: '1.21'
|
||||
core_cache_key: ${{ needs.core.outputs.cache_key }}
|
||||
core_cache_path: ${{ needs.core.outputs.cache_path }}
|
||||
|
||||
@ -62,7 +62,7 @@ jobs:
|
||||
needs: [core, console]
|
||||
uses: ./.github/workflows/lint.yml
|
||||
with:
|
||||
go_version: '1.20'
|
||||
go_version: '1.21'
|
||||
node_version: '18'
|
||||
buf_version: 'latest'
|
||||
go_lint_version: 'v1.53.2'
|
||||
|
@ -8,7 +8,7 @@ issues:
|
||||
run:
|
||||
concurrency: 4
|
||||
timeout: 10m
|
||||
go: '1.19'
|
||||
go: '1.21'
|
||||
skip-dirs:
|
||||
- .artifacts
|
||||
- .backups
|
||||
|
@ -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:
|
||||
|
||||
- [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)
|
||||
|
||||
Make some changes to the source code, then run the database locally.
|
||||
|
18
README.md
18
README.md
@ -4,21 +4,23 @@
|
||||
</p>
|
||||
|
||||
<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://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">
|
||||
<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">
|
||||
<img src="https://github.com/zitadel/zitadel/actions/workflows/zitadel.yml/badge.svg" /></a>
|
||||
<a href="https://github.com/zitadel/zitadel/blob/main/LICENSE" alt="License">
|
||||
<img src="https://badgen.net/github/license/zitadel/zitadel/" /></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/releases" alt="Release">
|
||||
<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">
|
||||
<img src="https://goreportcard.com/badge/github.com/zitadel/zitadel" /></a>
|
||||
<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>
|
||||
<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">
|
||||
<img src="https://badgen.net/discord/online-members/erh5Brh7jE" /></a>
|
||||
</p>
|
||||
@ -94,7 +96,7 @@ Authentication
|
||||
- Single Sign On (SSO)
|
||||
- Passwordless with FIDO2 support (Including Passkeys)
|
||||
- Username / Password
|
||||
- Multifactor authentication with OTP, U2F
|
||||
- Multifactor authentication with OTP, U2F, Email OTP, SMS OTP
|
||||
- LDAP
|
||||
- [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)
|
||||
@ -103,12 +105,12 @@ Authentication
|
||||
Multi-Tenancy
|
||||
- [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)
|
||||
- Domain discovery
|
||||
- [Domain discovery](https://zitadel.com/docs/guides/solution-scenarios/domain-discovery)
|
||||
|
||||
Integration
|
||||
- [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
|
||||
- Role Based Access Control (RBAC)
|
||||
- [Role Based Access Control (RBAC)](https://zitadel.com/docs/guides/integrate/retrieve-user-roles)
|
||||
|
||||
Self-Service
|
||||
- [Self-registration](https://zitadel.com/docs/concepts/features/selfservice#registration) including verification
|
||||
|
@ -307,6 +307,7 @@ Login:
|
||||
MaxAge: 12h # ZITADEL_LOGIN_CACHE_MAXAGE
|
||||
# 168h is 7 days, one week
|
||||
SharedMaxAge: 168h # ZITADEL_LOGIN_CACHE_SHAREDMAXAGE
|
||||
DefaultOTPEmailURLV2: "/otp/verify?loginName={{.LoginName}}&code={{.Code}}" # ZITADEL_LOGIN_CACHE_DEFAULTOTPEMAILURLV2
|
||||
|
||||
Console:
|
||||
ShortCache:
|
||||
@ -370,10 +371,10 @@ SystemDefaults:
|
||||
MachineKeySize: 2048 # ZITADEL_SYSTEMDEFAULTS_SECRETGENERATORS_MACHINEKEYSIZE
|
||||
ApplicationKeySize: 2048 # ZITADEL_SYSTEMDEFAULTS_SECRETGENERATORS_APPLICATIONKEYSIZE
|
||||
PasswordHasher:
|
||||
# Set hasher configuration for user passwords.
|
||||
# Passwords previously hashed with a different algorithm
|
||||
# or cost are automatically re-hashed using this config,
|
||||
# upon password validation or update.
|
||||
# Set hasher configuration for user passwords.
|
||||
# Passwords previously hashed with a different algorithm
|
||||
# or cost are automatically re-hashed using this config,
|
||||
# upon password validation or update.
|
||||
Hasher:
|
||||
Algorithm: "bcrypt" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM
|
||||
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
|
||||
From: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_FROM
|
||||
FromName: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_FROMNAME
|
||||
ReplyToAddress: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_REPLYTOADDRESS
|
||||
MessageTexts:
|
||||
- MessageTextType: InitCode
|
||||
Language: de
|
||||
|
@ -222,7 +222,25 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
|
||||
actionsLogstoreSvc := logstore.New(queries, usageReporter, actionsExecutionDBEmitter, actionsExecutionStdoutEmitter)
|
||||
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()
|
||||
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))
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -56,8 +56,8 @@
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "6mb",
|
||||
"maximumError": "7mb"
|
||||
"maximumWarning": "8mb",
|
||||
"maximumError": "9mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
|
@ -173,7 +173,43 @@ export class FilterEventsComponent implements OnInit {
|
||||
return this.adminService
|
||||
.listEventTypes(req)
|
||||
.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) => {
|
||||
this.toast.showError(error);
|
||||
|
@ -80,6 +80,11 @@
|
||||
<i class="idp-icon las la-building"></i>
|
||||
Active Directory / LDAP
|
||||
</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>
|
||||
</td>
|
||||
|
@ -21,6 +21,10 @@
|
||||
width: 28px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.apple {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
display: if($is-dark-theme, block, none);
|
||||
}
|
||||
|
@ -261,6 +261,8 @@ export class IdpTableComponent implements OnInit, OnDestroy {
|
||||
];
|
||||
case ProviderType.PROVIDER_TYPE_GITHUB:
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,6 +109,23 @@
|
||||
</div>
|
||||
</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
|
||||
class="item card"
|
||||
[routerLink]="
|
||||
|
@ -64,6 +64,10 @@
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
|
||||
&.apple {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
display: if($is-dark-theme, block, none);
|
||||
}
|
||||
|
@ -15,12 +15,17 @@
|
||||
<form (ngSubmit)="savePolicy()" [formGroup]="form" autocomplete="off">
|
||||
<cnsl-form-field class="smtp-form-field" label="Sender Address" required="true">
|
||||
<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 class="smtp-form-field" label="Sender Name" required="true">
|
||||
<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>
|
||||
|
||||
<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-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 class="smtp-form-field" label="User" required="true">
|
||||
<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>
|
||||
|
||||
<button
|
||||
|
@ -58,6 +58,7 @@ export class NotificationSettingsComponent implements OnInit {
|
||||
this.form = this.fb.group({
|
||||
senderAddress: [{ disabled: true, value: '' }, [requiredValidator]],
|
||||
senderName: [{ disabled: true, value: '' }, [requiredValidator]],
|
||||
replyToAddress: [{ disabled: true, value: '' }],
|
||||
tls: [{ disabled: true, value: true }, [requiredValidator]],
|
||||
hostAndPort: [{ disabled: true, value: '' }, [requiredValidator]],
|
||||
user: [{ disabled: true, value: '' }, [requiredValidator]],
|
||||
@ -143,6 +144,7 @@ export class NotificationSettingsComponent implements OnInit {
|
||||
req.setHost(this.hostAndPort?.value ?? '');
|
||||
req.setSenderAddress(this.senderAddress?.value ?? '');
|
||||
req.setSenderName(this.senderName?.value ?? '');
|
||||
req.setReplyToAddress(this.replyToAddress?.value ?? '');
|
||||
req.setTls(this.tls?.value ?? false);
|
||||
req.setUser(this.user?.value ?? '');
|
||||
|
||||
@ -152,6 +154,7 @@ export class NotificationSettingsComponent implements OnInit {
|
||||
req.setHost(this.hostAndPort?.value ?? '');
|
||||
req.setSenderAddress(this.senderAddress?.value ?? '');
|
||||
req.setSenderName(this.senderName?.value ?? '');
|
||||
req.setReplyToAddress(this.replyToAddress?.value ?? '');
|
||||
req.setTls(this.tls?.value ?? false);
|
||||
req.setUser(this.user?.value ?? '');
|
||||
|
||||
@ -298,6 +301,10 @@ export class NotificationSettingsComponent implements OnInit {
|
||||
return this.form.get('senderName');
|
||||
}
|
||||
|
||||
public get replyToAddress(): AbstractControl | null {
|
||||
return this.form.get('replyToAddress');
|
||||
}
|
||||
|
||||
public get tls(): AbstractControl | null {
|
||||
return this.form.get('tls');
|
||||
}
|
||||
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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');
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
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 { ProviderGithubESComponent } from './provider-github-es/provider-github-es.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_OIDC]: { path: 'oidc', component: ProviderOIDCComponent },
|
||||
[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]) => {
|
||||
|
@ -17,6 +17,7 @@ import { InfoSectionModule } from '../info-section/info-section.module';
|
||||
import { ProviderOptionsModule } from '../provider-options/provider-options.module';
|
||||
import { StringListModule } from '../string-list/string-list.module';
|
||||
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 { ProviderGithubESComponent } from './provider-github-es/provider-github-es.component';
|
||||
import { ProviderGithubComponent } from './provider-github/provider-github.component';
|
||||
@ -43,6 +44,7 @@ import { ProvidersRoutingModule } from './providers-routing.module';
|
||||
ProviderOIDCComponent,
|
||||
ProviderOAuthComponent,
|
||||
ProviderLDAPComponent,
|
||||
ProviderAppleComponent,
|
||||
],
|
||||
imports: [
|
||||
ProvidersRoutingModule,
|
||||
|
@ -3,6 +3,7 @@
|
||||
@mixin identity-provider-theme($theme) {
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
|
||||
.identity-provider-desc {
|
||||
font-size: 14px;
|
||||
@ -19,6 +20,10 @@
|
||||
margin-right: 1rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.apple {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
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 {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
@ -248,7 +248,7 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
this.metadata = resp.resultList.map((md) => {
|
||||
return {
|
||||
key: md.key,
|
||||
value: Buffer.from(md.value as string, 'base64').toString('ascii'),
|
||||
value: Buffer.from(md.value as string, 'base64').toString('utf-8'),
|
||||
};
|
||||
});
|
||||
})
|
||||
|
@ -521,7 +521,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
get decodedBase64(): string {
|
||||
const samlReq = this.samlAppRequest.toObject();
|
||||
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 {
|
||||
return '';
|
||||
}
|
||||
@ -529,7 +529,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
|
||||
set decodedBase64(xmlString) {
|
||||
if (this.samlAppRequest) {
|
||||
const base64 = Buffer.from(xmlString, 'ascii').toString('base64');
|
||||
const base64 = Buffer.from(xmlString, 'utf-8').toString('base64');
|
||||
this.samlAppRequest.setMetadataXml(base64);
|
||||
}
|
||||
}
|
||||
|
@ -786,7 +786,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
this.app.samlConfig.metadataXml &&
|
||||
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 {
|
||||
return '';
|
||||
}
|
||||
@ -794,7 +794,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
set decodedBase64(xmlString: string) {
|
||||
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) {
|
||||
this.app.samlConfig.metadataXml = base64;
|
||||
|
@ -406,7 +406,7 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
this.metadata = resp.resultList.map((md) => {
|
||||
return {
|
||||
key: md.key,
|
||||
value: Buffer.from(md.value as string, 'base64').toString('ascii'),
|
||||
value: Buffer.from(md.value as string, 'base64').toString('utf8'),
|
||||
};
|
||||
});
|
||||
})
|
||||
|
@ -508,7 +508,7 @@ export class UserDetailComponent implements OnInit {
|
||||
this.metadata = resp.resultList.map((md) => {
|
||||
return {
|
||||
key: md.key,
|
||||
value: Buffer.from(md.value as string, 'base64').toString('ascii'),
|
||||
value: Buffer.from(md.value as string, 'base64').toString('utf-8'),
|
||||
};
|
||||
});
|
||||
})
|
||||
|
@ -6,6 +6,8 @@ import {
|
||||
ActivateLabelPolicyResponse,
|
||||
ActivateSMSProviderRequest,
|
||||
ActivateSMSProviderResponse,
|
||||
AddAppleProviderRequest,
|
||||
AddAppleProviderResponse,
|
||||
AddAzureADProviderRequest,
|
||||
AddAzureADProviderResponse,
|
||||
AddCustomDomainPolicyRequest,
|
||||
@ -214,6 +216,8 @@ import {
|
||||
SetSecurityPolicyResponse,
|
||||
SetUpOrgRequest,
|
||||
SetUpOrgResponse,
|
||||
UpdateAppleProviderRequest,
|
||||
UpdateAppleProviderResponse,
|
||||
UpdateAzureADProviderRequest,
|
||||
UpdateAzureADProviderResponse,
|
||||
UpdateCustomDomainPolicyRequest,
|
||||
@ -1173,6 +1177,14 @@ export class AdminService {
|
||||
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> {
|
||||
const req = new DeleteProviderRequest();
|
||||
req.setId(id);
|
||||
|
@ -15,6 +15,8 @@ import {
|
||||
AddAPIAppResponse,
|
||||
AddAppKeyRequest,
|
||||
AddAppKeyResponse,
|
||||
AddAppleProviderRequest,
|
||||
AddAppleProviderResponse,
|
||||
AddAzureADProviderRequest,
|
||||
AddAzureADProviderResponse,
|
||||
AddCustomLabelPolicyRequest,
|
||||
@ -441,6 +443,8 @@ import {
|
||||
UpdateActionResponse,
|
||||
UpdateAPIAppConfigRequest,
|
||||
UpdateAPIAppConfigResponse,
|
||||
UpdateAppleProviderRequest,
|
||||
UpdateAppleProviderResponse,
|
||||
UpdateAppRequest,
|
||||
UpdateAppResponse,
|
||||
UpdateAzureADProviderRequest,
|
||||
@ -1041,6 +1045,14 @@ export class ManagementService {
|
||||
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> {
|
||||
const req = new DeleteProviderRequest();
|
||||
req.setId(id);
|
||||
|
@ -1049,6 +1049,7 @@
|
||||
"TITLE": "SMTP настройки",
|
||||
"SENDERADDRESS": "Имейл адрес на изпращача",
|
||||
"SENDERNAME": "Име на изпращача",
|
||||
"REPLYTOADDRESS": "Reply-to адрес",
|
||||
"HOSTANDPORT": "Хост и порт",
|
||||
"USER": "Потребител",
|
||||
"PASSWORD": "Парола",
|
||||
|
@ -1055,6 +1055,7 @@
|
||||
"TITLE": "SMTP Einstellungen",
|
||||
"SENDERADDRESS": "Sender Email-Adresse",
|
||||
"SENDERNAME": "Sender Name",
|
||||
"REPLYTOADDRESS": "Reply-to-Adresse",
|
||||
"HOSTANDPORT": "Host und Port",
|
||||
"USER": "Benutzer",
|
||||
"PASSWORD": "Passwort",
|
||||
|
@ -1056,6 +1056,7 @@
|
||||
"TITLE": "SMTP Settings",
|
||||
"SENDERADDRESS": "Sender Email Address",
|
||||
"SENDERNAME": "Sender Name",
|
||||
"REPLYTOADDRESS": "Reply-to Address",
|
||||
"HOSTANDPORT": "Host And Port",
|
||||
"USER": "User",
|
||||
"PASSWORD": "Password",
|
||||
@ -1705,6 +1706,10 @@
|
||||
"LDAP": {
|
||||
"TITLE": "Active Directory / LDAP",
|
||||
"DESCRIPTION": "Enter the credentials for your LDAP Provider"
|
||||
},
|
||||
"APPLE": {
|
||||
"TITLE": "Sign in with Apple",
|
||||
"DESCRIPTION": "Enter the credentials for your Apple Provider"
|
||||
}
|
||||
},
|
||||
"DETAIL": {
|
||||
@ -1818,6 +1823,14 @@
|
||||
"JWTENDPOINT": "JWT 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": {
|
||||
"SAVED": "Successfully saved.",
|
||||
"REACTIVATED": "Idp reactivated.",
|
||||
|
@ -1056,6 +1056,7 @@
|
||||
"TITLE": "Ajustes SMTP",
|
||||
"SENDERADDRESS": "Dirección email del emisor",
|
||||
"SENDERNAME": "Nombre del emisor",
|
||||
"REPLYTOADDRESS": "Dirección Reply-To",
|
||||
"HOSTANDPORT": "Servidor y puerto",
|
||||
"USER": "Usuario",
|
||||
"PASSWORD": "Contraseña",
|
||||
|
@ -1055,6 +1055,7 @@
|
||||
"TITLE": "Paramètres SMTP",
|
||||
"SENDERADDRESS": "Adresse e-mail de l'expéditeur",
|
||||
"SENDERNAME": "Nom de l'expéditeur",
|
||||
"REPLYTOADDRESS": "Adresse Reply-to",
|
||||
"HOSTANDPORT": "Hôte et port",
|
||||
"USER": "Utilisateur",
|
||||
"PASSWORD": "Mot de passe",
|
||||
|
@ -1055,6 +1055,7 @@
|
||||
"TITLE": "Impostazioni SMTP",
|
||||
"SENDERADDRESS": "Indirizzo email del mittente",
|
||||
"SENDERNAME": "Nome del mittente",
|
||||
"REPLYTOADDRESS": "Indirizzo Reply-to",
|
||||
"HOSTANDPORT": "Host e porta",
|
||||
"USER": "Utente",
|
||||
"PASSWORD": "Password",
|
||||
|
@ -1056,6 +1056,7 @@
|
||||
"TITLE": "SMTP設定",
|
||||
"SENDERADDRESS": "送信者のメールアドレス",
|
||||
"SENDERNAME": "送信者名",
|
||||
"REPLYTOADDRESS": "返信先アドレス",
|
||||
"HOSTANDPORT": "ホストとポート",
|
||||
"USER": "ユーザー",
|
||||
"PASSWORD": "パスワード",
|
||||
|
@ -1056,6 +1056,7 @@
|
||||
"TITLE": "SMTP подесувања",
|
||||
"SENDERADDRESS": "Адреса на испраќачот",
|
||||
"SENDERNAME": "Име на испраќачот",
|
||||
"REPLYTOADDRESS": "Reply-to адреса",
|
||||
"HOSTANDPORT": "Host и Port",
|
||||
"USER": "Корисник",
|
||||
"PASSWORD": "Лозинка",
|
||||
|
@ -1055,6 +1055,7 @@
|
||||
"TITLE": "Ustawienia SMTP",
|
||||
"SENDERADDRESS": "Adres e-mail nadawcy",
|
||||
"SENDERNAME": "Nazwa nadawcy",
|
||||
"REPLYTOADDRESS": "Adres Reply-to",
|
||||
"HOSTANDPORT": "Host i port",
|
||||
"USER": "Użytkownik",
|
||||
"PASSWORD": "Hasło",
|
||||
|
@ -1056,6 +1056,7 @@
|
||||
"TITLE": "Configurações SMTP",
|
||||
"SENDERADDRESS": "Endereço de e-mail do remetente",
|
||||
"SENDERNAME": "Nome do remetente",
|
||||
"REPLYTOADDRESS": "Endereço Reply-To",
|
||||
"HOSTANDPORT": "Host e porta",
|
||||
"USER": "Usuário",
|
||||
"PASSWORD": "Senha",
|
||||
|
@ -1055,6 +1055,7 @@
|
||||
"TITLE": "SMTP 设置",
|
||||
"SENDERADDRESS": "发件人地址",
|
||||
"SENDERNAME": "发件人名称",
|
||||
"REPLYTOADDRESS": "Reply-to 地址",
|
||||
"HOSTANDPORT": "主机和端口",
|
||||
"USER": "用户名",
|
||||
"PASSWORD": "密码",
|
||||
|
10
console/src/assets/images/idp/apple-dark.svg
Executable file
10
console/src/assets/images/idp/apple-dark.svg
Executable 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 |
10
console/src/assets/images/idp/apple.svg
Executable file
10
console/src/assets/images/idp/apple.svg
Executable 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 |
@ -429,6 +429,7 @@ $custom-typography: mat.define-legacy-typography-config(
|
||||
background-color: map-get($background, cards);
|
||||
transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
border-radius: 6px;
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
.mat-option {
|
||||
@ -500,6 +501,7 @@ $custom-typography: mat.define-legacy-typography-config(
|
||||
background-color: map-get($background, cards);
|
||||
transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
border-radius: 6px;
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
.mat-option {
|
||||
|
@ -26,7 +26,7 @@ In the response, you will get an authentication URL of the provider you like.
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url https://$ZITADEL_DOMAIN/v2alpha/idp_intents/start \
|
||||
--url https://$ZITADEL_DOMAIN/v2alpha/idp_intents \
|
||||
--header 'Accept: application/json' \
|
||||
--header 'Authorization: Bearer '"$TOKEN"''\
|
||||
--header 'Content-Type: application/json' \
|
||||
|
@ -212,7 +212,7 @@ Next step is to authenticate the user with the new registered passkey.
|
||||
### Create Session
|
||||
|
||||
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.
|
||||
|
||||
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": {},
|
||||
"challenges": [
|
||||
"CHALLENGE_KIND_PASSKEY"
|
||||
]
|
||||
"challenges": {
|
||||
"webAuthN": {
|
||||
"domain": "example.domain.com",
|
||||
"userVerificationRequirement": "USER_VERIFICATION_REQUIREMENT_REQUIRED"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
@ -248,7 +251,7 @@ Example Response:
|
||||
"sessionId": "d654e6ba-70a3-48ef-a95d-37c8d8a7901a",
|
||||
"sessionToken": "string",
|
||||
"challenges": {
|
||||
"passkey": {
|
||||
"webAuthN": {
|
||||
"publicKeyCredentialRequestOptions": {
|
||||
"publicKey": {
|
||||
"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.
|
||||
Make sure to send the public key credential request options you got from ZITADEL.
|
||||
|
||||
```bash
|
||||
```javascript
|
||||
const credential = await navigator.credentials.get({
|
||||
publicKey: publicKeyCredentialRequestOptions
|
||||
});
|
||||
@ -300,7 +303,7 @@ curl --request PATCH \
|
||||
--data '{
|
||||
"sessionToken": "yMDi6uVPJAcphbbz0LaxC07ihWkNTe7m0Xqch8SzfM5Cz3HSIQIDZ65x1f5Qal0jxz0MEyo-_zYcUg",
|
||||
"checks": {
|
||||
"passkey": {
|
||||
"webAuthN": {
|
||||
"credentialAssertionData": {}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
||||
```
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url {your_domain}/oauth/v2/introspect \
|
||||
--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:
|
||||
|
||||
```
|
||||
```python
|
||||
def introspect_token(self, token_string):
|
||||
url = ZITADEL_INTROSPECTION_URL
|
||||
data = {'token': token_string, 'token_type_hint': 'access_token', 'scope': 'openid'}
|
||||
|
@ -31,7 +31,7 @@ you can setup ZITADEL and either
|
||||
# Install CockroachDB
|
||||
helm install crdb cockroachdb/cockroachdb \
|
||||
--set fullnameOverride=crdb \
|
||||
--set single-node=true \
|
||||
--set conf.single-node=true \
|
||||
--set statefulset.replicas=1
|
||||
|
||||
# Install ZITADEL
|
||||
@ -65,7 +65,7 @@ With this setup you only get a key for a service account. Logging in at ZITADEL
|
||||
# Install CockroachDB
|
||||
helm install crdb cockroachdb/cockroachdb \
|
||||
--set fullnameOverride=crdb \
|
||||
--set single-node=true \
|
||||
--set conf.single-node=true \
|
||||
--set statefulset.replicas=1
|
||||
|
||||
# Install ZITADEL
|
||||
|
@ -23,16 +23,16 @@ By executing the commands below, you will download the following files:
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
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.
|
||||
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.
|
||||
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
|
||||
# If you haven't done so already, you can generate a new one
|
||||
|
@ -200,6 +200,7 @@ DefaultInstance:
|
||||
# if the host of the sender is different from ExternalDomain set DefaultInstance.DomainPolicy.SMTPSenderAddressMatchesInstanceDomain to false
|
||||
From:
|
||||
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.
|
||||
|
@ -13,8 +13,8 @@ To address this, we are going to change this behavior so that users will be auto
|
||||
|
||||
## 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)
|
||||
As soon as the release version is published, we will include the version here.
|
||||
This behaviour change was tracked in the following issue: [Reuse current session if no prompt is selected](https://github.com/zitadel/zitadel/issues/4841)
|
||||
and released in Version [v2.32.0](https://github.com/zitadel/zitadel/releases/tag/v2.32.0)
|
||||
|
||||
## Mitigation
|
||||
|
||||
|
26
docs/docs/support/advisory/a10001.md
Normal file
26
docs/docs/support/advisory/a10001.md
Normal 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.
|
@ -26,6 +26,14 @@ We understand that these advisories may include breaking changes, and we aim to
|
||||
<td>2.32.0</td>
|
||||
<td>Calendar week 32</td>
|
||||
</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>
|
||||
|
||||
## Subscribe to our Mailing List
|
||||
|
@ -20,7 +20,7 @@ module.exports = {
|
||||
],
|
||||
customFields: {
|
||||
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: {
|
||||
metadata: [
|
||||
|
@ -284,6 +284,31 @@ module.exports = {
|
||||
"guides/integrate/services/auth0-oidc",
|
||||
"guides/integrate/services/auth0-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',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
4717
docs/yarn.lock
4717
docs/yarn.lock
File diff suppressed because it is too large
Load Diff
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
||||
module github.com/zitadel/zitadel
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.32.0
|
||||
|
@ -42,7 +42,7 @@ func (h *Handler) Commands() *command.Commands {
|
||||
}
|
||||
|
||||
func (h *Handler) ErrorHandler() ErrorHandler {
|
||||
return DefaultErrorHandler
|
||||
return h.errorHandler
|
||||
}
|
||||
|
||||
func (h *Handler) Storage() static.Storage {
|
||||
@ -75,10 +75,14 @@ type Downloader interface {
|
||||
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")
|
||||
code, ok := http_util.ZitadelErrorToHTTPStatusCode(err)
|
||||
if !ok {
|
||||
code = defaultCode
|
||||
}
|
||||
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())
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -190,10 +194,6 @@ func DownloadHandleFunc(s AssetsService, downloader Downloader) func(http.Respon
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download failed: %v", err)
|
||||
return fmt.Errorf("download failed: %w", err)
|
||||
}
|
||||
info, err := getInfo()
|
||||
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), "\"") {
|
||||
w.Header().Set(http_util.LastModified, info.LastModified.Format(time.RFC1123))
|
||||
|
@ -132,9 +132,10 @@ func SecretGeneratorTypeToDomain(generatorType settings_pb.SecretGeneratorType)
|
||||
|
||||
func AddSMTPToConfig(req *admin_pb.AddSMTPConfigRequest) *smtp.Config {
|
||||
return &smtp.Config{
|
||||
Tls: req.Tls,
|
||||
From: req.SenderAddress,
|
||||
FromName: req.SenderName,
|
||||
Tls: req.Tls,
|
||||
From: req.SenderAddress,
|
||||
FromName: req.SenderName,
|
||||
ReplyToAddress: req.ReplyToAddress,
|
||||
SMTP: smtp.SMTP{
|
||||
Host: req.Host,
|
||||
User: req.User,
|
||||
@ -145,9 +146,10 @@ func AddSMTPToConfig(req *admin_pb.AddSMTPConfigRequest) *smtp.Config {
|
||||
|
||||
func UpdateSMTPToConfig(req *admin_pb.UpdateSMTPConfigRequest) *smtp.Config {
|
||||
return &smtp.Config{
|
||||
Tls: req.Tls,
|
||||
From: req.SenderAddress,
|
||||
FromName: req.SenderName,
|
||||
Tls: req.Tls,
|
||||
From: req.SenderAddress,
|
||||
FromName: req.SenderName,
|
||||
ReplyToAddress: req.ReplyToAddress,
|
||||
SMTP: smtp.SMTP{
|
||||
Host: req.Host,
|
||||
User: req.User,
|
||||
@ -157,12 +159,13 @@ func UpdateSMTPToConfig(req *admin_pb.UpdateSMTPConfigRequest) *smtp.Config {
|
||||
|
||||
func SMTPConfigToPb(smtp *query.SMTPConfig) *settings_pb.SMTPConfig {
|
||||
mapped := &settings_pb.SMTPConfig{
|
||||
Tls: smtp.TLS,
|
||||
SenderAddress: smtp.SenderAddress,
|
||||
SenderName: smtp.SenderName,
|
||||
Host: smtp.Host,
|
||||
User: smtp.User,
|
||||
Details: obj_grpc.ToViewDetailsPb(smtp.Sequence, smtp.CreationDate, smtp.ChangeDate, smtp.AggregateID),
|
||||
Tls: smtp.TLS,
|
||||
SenderAddress: smtp.SenderAddress,
|
||||
SenderName: smtp.SenderName,
|
||||
ReplyToAddress: smtp.ReplyToAddress,
|
||||
Host: smtp.Host,
|
||||
User: smtp.User,
|
||||
Details: obj_grpc.ToViewDetailsPb(smtp.Sequence, smtp.CreationDate, smtp.ChangeDate, smtp.AggregateID),
|
||||
}
|
||||
return mapped
|
||||
}
|
||||
|
@ -405,6 +405,27 @@ func (s *Server) UpdateLDAPProvider(ctx context.Context, req *admin_pb.UpdateLDA
|
||||
}, 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) {
|
||||
details, err := s.command.DeleteInstanceProvider(ctx, req.Id)
|
||||
if err != nil {
|
||||
|
@ -440,3 +440,27 @@ func updateLDAPProviderToCommand(req *admin_pb.UpdateLDAPProviderRequest) comman
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
@ -414,6 +414,8 @@ func providerTypeToPb(idpType domain.IDPType) idp_pb.ProviderType {
|
||||
return idp_pb.ProviderType_PROVIDER_TYPE_GITLAB_SELF_HOSTED
|
||||
case domain.IDPTypeGoogle:
|
||||
return idp_pb.ProviderType_PROVIDER_TYPE_GOOGLE
|
||||
case domain.IDPTypeApple:
|
||||
return idp_pb.ProviderType_PROVIDER_TYPE_APPLE
|
||||
case domain.IDPTypeUnspecified:
|
||||
return idp_pb.ProviderType_PROVIDER_TYPE_UNSPECIFIED
|
||||
default:
|
||||
@ -470,6 +472,10 @@ func configToPb(config *query.IDPTemplate) *idp_pb.ProviderConfig {
|
||||
ldapConfigToPb(providerConfig, config.LDAPIDPTemplate)
|
||||
return providerConfig
|
||||
}
|
||||
if config.AppleIDPTemplate != nil {
|
||||
appleConfigToPb(providerConfig, config.AppleIDPTemplate)
|
||||
return providerConfig
|
||||
}
|
||||
return providerConfig
|
||||
}
|
||||
|
||||
@ -620,3 +626,14 @@ func ldapAttributesToPb(attributes idp.LDAPAttributes) *idp_pb.LDAPAttributes {
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -397,6 +397,27 @@ func (s *Server) UpdateLDAPProvider(ctx context.Context, req *mgmt_pb.UpdateLDAP
|
||||
}, 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) {
|
||||
details, err := s.command.DeleteOrgProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id)
|
||||
if err != nil {
|
||||
|
@ -457,3 +457,27 @@ func updateLDAPProviderToCommand(req *mgmt_pb.UpdateLDAPProviderRequest) command
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,10 @@ func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRe
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
@ -64,7 +67,10 @@ func (s *Server) SetSession(ctx context.Context, req *session.SetSessionRequest)
|
||||
if err != nil {
|
||||
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())
|
||||
if err != nil {
|
||||
@ -121,6 +127,8 @@ func factorsToPb(s *query.Session) *session.Factors {
|
||||
WebAuthN: webAuthNFactorToPb(s.WebAuthNFactor),
|
||||
Intent: intentFactorToPb(s.IntentFactor),
|
||||
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 {
|
||||
if factor.UserID == "" || factor.UserCheckedAt.IsZero() {
|
||||
return nil
|
||||
@ -240,7 +257,7 @@ func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessionChecks := make([]command.SessionCommand, 0, 3)
|
||||
sessionChecks := make([]command.SessionCommand, 0, 7)
|
||||
if checkUser != nil {
|
||||
user, err := checkUser.search(ctx, s.query)
|
||||
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()))
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, cmds
|
||||
return nil, cmds, nil
|
||||
}
|
||||
resp := new(session.Challenges)
|
||||
if req := challenges.GetWebAuthN(); req != nil {
|
||||
@ -273,7 +296,20 @@ func (s *Server) challengesToCommand(challenges *session.RequestChallenges, cmds
|
||||
resp.WebAuthN = challenge
|
||||
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) {
|
||||
@ -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) {
|
||||
if user == nil {
|
||||
return nil, nil
|
||||
|
@ -39,6 +39,14 @@ func TestMain(m *testing.M) {
|
||||
|
||||
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
|
||||
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.RegisterUserPasskey(CTX, User.GetUserId())
|
||||
return m.Run()
|
||||
@ -75,6 +83,8 @@ const (
|
||||
wantWebAuthNFactorUserVerified
|
||||
wantTOTPFactor
|
||||
wantIntentFactor
|
||||
wantOTPSMSFactor
|
||||
wantOTPEmailFactor
|
||||
)
|
||||
|
||||
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()
|
||||
assert.NotNil(t, pf)
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
// create new, empty session
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
|
||||
@ -421,6 +453,8 @@ func TestServer_SetSession_flow(t *testing.T) {
|
||||
userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken)
|
||||
Tester.RegisterUserU2F(userAuthCtx, 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) {
|
||||
|
||||
@ -470,7 +504,7 @@ func TestServer_SetSession_flow(t *testing.T) {
|
||||
SessionToken: sessionToken,
|
||||
Checks: &session.Checks{
|
||||
Totp: &session.CheckTOTP{
|
||||
Totp: code,
|
||||
Code: code,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -478,6 +512,66 @@ func TestServer_SetSession_flow(t *testing.T) {
|
||||
sessionToken = resp.GetSessionToken()
|
||||
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) {
|
||||
|
47
internal/api/http/error.go
Normal file
47
internal/api/http/error.go
Normal 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
|
||||
}
|
||||
}
|
138
internal/api/http/error_test.go
Normal file
138
internal/api/http/error_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ import (
|
||||
z_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/form"
|
||||
"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/github"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/gitlab"
|
||||
@ -52,6 +53,9 @@ type externalIDPCallbackData struct {
|
||||
Code string `schema:"code"`
|
||||
Error string `schema:"error"`
|
||||
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
|
||||
@ -115,7 +119,7 @@ func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
idpUser, idpSession, err := h.fetchIDPUser(ctx, provider, data.Code)
|
||||
idpUser, idpSession, err := h.fetchIDPUser(ctx, provider, data.Code, data.User)
|
||||
if err != nil {
|
||||
cmdErr := h.commands.FailIDPIntent(ctx, intent, err.Error())
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
switch provider := identityProvider.(type) {
|
||||
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}
|
||||
case *google.Provider:
|
||||
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:
|
||||
return nil, nil, z_errs.ThrowInvalidArgument(nil, "IDP-52jmn", "Errors.ExternalIDP.IDPTypeNotImplemented")
|
||||
default:
|
||||
|
@ -3,9 +3,8 @@ package login
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"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/github"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/gitlab"
|
||||
@ -41,6 +42,9 @@ type externalIDPData struct {
|
||||
type externalIDPCallbackData struct {
|
||||
State string `schema:"state"`
|
||||
Code string `schema:"code"`
|
||||
|
||||
// Apple returns a user on first registration
|
||||
User string `schema:"user"`
|
||||
}
|
||||
|
||||
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)
|
||||
case domain.IDPTypeGoogle:
|
||||
provider, err = l.googleProvider(r.Context(), identityProvider)
|
||||
case domain.IDPTypeApple:
|
||||
provider, err = l.appleProvider(r.Context(), identityProvider)
|
||||
case domain.IDPTypeLDAP:
|
||||
provider, err = l.ldapProvider(r.Context(), identityProvider)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
// and tries to extract the user with the provided data
|
||||
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
|
||||
}
|
||||
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,
|
||||
domain.IDPTypeLDAP,
|
||||
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 {
|
||||
if len(userGrants) == 0 {
|
||||
return nil
|
||||
@ -971,6 +1011,8 @@ func tokens(session idp.Session) *oidc.Tokens[*oidc.IDTokenClaims] {
|
||||
return s.Tokens
|
||||
case *azuread.Session:
|
||||
return s.Tokens
|
||||
case *apple.Session:
|
||||
return s.Tokens
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -47,6 +47,9 @@ type Config struct {
|
||||
CSRFCookieName string
|
||||
Cache middleware.CacheConfig
|
||||
AssetCache middleware.CacheConfig
|
||||
|
||||
// LoginV2
|
||||
DefaultOTPEmailURLV2 string
|
||||
}
|
||||
|
||||
const (
|
||||
@ -117,6 +120,12 @@ func createCSRFInterceptor(cookieName string, csrfCookieKey []byte, externalSecu
|
||||
handler.ServeHTTP(w, r)
|
||||
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.Secure(externalSecure),
|
||||
csrf.CookieName(http_utils.SetCookiePrefix(cookieName, "", path, externalSecure)),
|
||||
|
@ -7,44 +7,45 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
EndpointRoot = "/"
|
||||
EndpointHealthz = "/healthz"
|
||||
EndpointReadiness = "/ready"
|
||||
EndpointLogin = "/login"
|
||||
EndpointExternalLogin = "/login/externalidp"
|
||||
EndpointExternalLoginCallback = "/login/externalidp/callback"
|
||||
EndpointJWTAuthorize = "/login/jwt/authorize"
|
||||
EndpointJWTCallback = "/login/jwt/callback"
|
||||
EndpointLDAPLogin = "/login/ldap"
|
||||
EndpointLDAPCallback = "/login/ldap/callback"
|
||||
EndpointPasswordlessLogin = "/login/passwordless"
|
||||
EndpointPasswordlessRegistration = "/login/passwordless/init"
|
||||
EndpointPasswordlessPrompt = "/login/passwordless/prompt"
|
||||
EndpointLoginName = "/loginname"
|
||||
EndpointUserSelection = "/userselection"
|
||||
EndpointChangeUsername = "/username/change"
|
||||
EndpointPassword = "/password"
|
||||
EndpointInitPassword = "/password/init"
|
||||
EndpointChangePassword = "/password/change"
|
||||
EndpointPasswordReset = "/password/reset"
|
||||
EndpointInitUser = "/user/init"
|
||||
EndpointMFAVerify = "/mfa/verify"
|
||||
EndpointMFAPrompt = "/mfa/prompt"
|
||||
EndpointMFAInitVerify = "/mfa/init/verify"
|
||||
EndpointMFASMSInitVerify = "/mfa/init/sms/verify"
|
||||
EndpointMFAOTPVerify = "/mfa/otp/verify"
|
||||
EndpointMFAInitU2FVerify = "/mfa/init/u2f/verify"
|
||||
EndpointU2FVerification = "/mfa/u2f/verify"
|
||||
EndpointMailVerification = "/mail/verification"
|
||||
EndpointMailVerified = "/mail/verified"
|
||||
EndpointRegisterOption = "/register/option"
|
||||
EndpointRegister = "/register"
|
||||
EndpointExternalRegister = "/register/externalidp"
|
||||
EndpointExternalRegisterCallback = "/register/externalidp/callback"
|
||||
EndpointRegisterOrg = "/register/org"
|
||||
EndpointLogoutDone = "/logout/done"
|
||||
EndpointLoginSuccess = "/login/success"
|
||||
EndpointExternalNotFoundOption = "/externaluser/option"
|
||||
EndpointRoot = "/"
|
||||
EndpointHealthz = "/healthz"
|
||||
EndpointReadiness = "/ready"
|
||||
EndpointLogin = "/login"
|
||||
EndpointExternalLogin = "/login/externalidp"
|
||||
EndpointExternalLoginCallback = "/login/externalidp/callback"
|
||||
EndpointExternalLoginCallbackFormPost = "/login/externalidp/callback/form"
|
||||
EndpointJWTAuthorize = "/login/jwt/authorize"
|
||||
EndpointJWTCallback = "/login/jwt/callback"
|
||||
EndpointLDAPLogin = "/login/ldap"
|
||||
EndpointLDAPCallback = "/login/ldap/callback"
|
||||
EndpointPasswordlessLogin = "/login/passwordless"
|
||||
EndpointPasswordlessRegistration = "/login/passwordless/init"
|
||||
EndpointPasswordlessPrompt = "/login/passwordless/prompt"
|
||||
EndpointLoginName = "/loginname"
|
||||
EndpointUserSelection = "/userselection"
|
||||
EndpointChangeUsername = "/username/change"
|
||||
EndpointPassword = "/password"
|
||||
EndpointInitPassword = "/password/init"
|
||||
EndpointChangePassword = "/password/change"
|
||||
EndpointPasswordReset = "/password/reset"
|
||||
EndpointInitUser = "/user/init"
|
||||
EndpointMFAVerify = "/mfa/verify"
|
||||
EndpointMFAPrompt = "/mfa/prompt"
|
||||
EndpointMFAInitVerify = "/mfa/init/verify"
|
||||
EndpointMFASMSInitVerify = "/mfa/init/sms/verify"
|
||||
EndpointMFAOTPVerify = "/mfa/otp/verify"
|
||||
EndpointMFAInitU2FVerify = "/mfa/init/u2f/verify"
|
||||
EndpointU2FVerification = "/mfa/u2f/verify"
|
||||
EndpointMailVerification = "/mail/verification"
|
||||
EndpointMailVerified = "/mail/verified"
|
||||
EndpointRegisterOption = "/register/option"
|
||||
EndpointRegister = "/register"
|
||||
EndpointExternalRegister = "/register/externalidp"
|
||||
EndpointExternalRegisterCallback = "/register/externalidp/callback"
|
||||
EndpointRegisterOrg = "/register/org"
|
||||
EndpointLogoutDone = "/logout/done"
|
||||
EndpointLoginSuccess = "/login/success"
|
||||
EndpointExternalNotFoundOption = "/externaluser/option"
|
||||
|
||||
EndpointResources = "/resources"
|
||||
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(EndpointExternalLogin, login.handleExternalLogin).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(EndpointJWTCallback, login.handleJWTCallback).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointPasswordlessLogin, login.handlePasswordlessVerification).Methods(http.MethodPost)
|
||||
|
@ -360,6 +360,7 @@ Footer:
|
||||
PrivacyPolicy: Политика за поверителност
|
||||
Help: Помогне
|
||||
SupportEmail: Поддръжка на имейл
|
||||
SignIn: Влезте с {{.Provider}}
|
||||
Errors:
|
||||
Internal: Възникна вътрешна грешка
|
||||
AuthRequest:
|
||||
|
@ -372,6 +372,8 @@ Footer:
|
||||
Help: Hilfe
|
||||
SupportEmail: Support E-Mail
|
||||
|
||||
SignIn: Mit {{.Provider}} anmelden
|
||||
|
||||
Errors:
|
||||
Internal: Es ist ein interner Fehler aufgetreten
|
||||
AuthRequest:
|
||||
|
@ -372,6 +372,8 @@ Footer:
|
||||
Help: Help
|
||||
SupportEmail: Support E-mail
|
||||
|
||||
SignIn: Sign in with {{.Provider}}
|
||||
|
||||
Errors:
|
||||
Internal: An internal error occurred
|
||||
AuthRequest:
|
||||
|
@ -354,6 +354,8 @@ Footer:
|
||||
Help: Ayuda
|
||||
SupportEmail: Email de soporte
|
||||
|
||||
SignIn: Iniciar sesión con {{.Provider}}
|
||||
|
||||
Errors:
|
||||
Internal: Se produjo un error interno
|
||||
AuthRequest:
|
||||
|
@ -372,6 +372,8 @@ Footer:
|
||||
Help: Aide
|
||||
SupportEmail: E-mail d'assistance
|
||||
|
||||
SignIn: Connexion avec {{.Provider}}
|
||||
|
||||
Errors:
|
||||
Internal: Une erreur interne s'est produite
|
||||
AuthRequest:
|
||||
|
@ -372,6 +372,8 @@ Footer:
|
||||
Help: Aiuto
|
||||
SupportEmail: E-mail di supporto
|
||||
|
||||
SignIn: Accedi con {{.Provider}}
|
||||
|
||||
Errors:
|
||||
Internal: Si è verificato un errore interno
|
||||
AuthRequest:
|
||||
|
@ -363,6 +363,8 @@ Footer:
|
||||
PrivacyPolicy: プライバシーポリシー
|
||||
Help: ヘルプ
|
||||
|
||||
SignIn: '{{.Provider}} でサインイン'
|
||||
|
||||
Errors:
|
||||
Internal: 内部でエラーが発生しました
|
||||
AuthRequest:
|
||||
|
@ -372,6 +372,8 @@ Footer:
|
||||
Help: Помош
|
||||
SupportEmail: Е-пошта за поддршка
|
||||
|
||||
SignIn: Пријавете се со {{.Provider}}
|
||||
|
||||
Errors:
|
||||
Internal: Се појави внатрешна грешка
|
||||
AuthRequest:
|
||||
|
@ -372,6 +372,8 @@ Footer:
|
||||
Help: Pomoc
|
||||
SupportEmail: E-mail wsparcia
|
||||
|
||||
SignIn: Zaloguj się, używając konta {{.Provider}}
|
||||
|
||||
Errors:
|
||||
Internal: Wewnętrzny błąd
|
||||
AuthRequest:
|
||||
|
@ -366,6 +366,8 @@ Footer:
|
||||
Help: Ajuda
|
||||
SupportEmail: E-mail de suporte
|
||||
|
||||
SignIn: Iniciar sessão com a {{.Provider}}
|
||||
|
||||
Errors:
|
||||
Internal: Ocorreu um erro interno
|
||||
AuthRequest:
|
||||
|
@ -372,6 +372,8 @@ Footer:
|
||||
Help: 帮助
|
||||
SupportEmail: 支持邮箱
|
||||
|
||||
SignIn: 通过 {{.Provider}} 登录
|
||||
|
||||
Errors:
|
||||
Internal: 发生了内部错误
|
||||
AuthRequest:
|
||||
|
10
internal/api/ui/login/static/resources/images/idp/apple-dark.svg
Executable file
10
internal/api/ui/login/static/resources/images/idp/apple-dark.svg
Executable 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 |
10
internal/api/ui/login/static/resources/images/idp/apple.svg
Executable file
10
internal/api/ui/login/static/resources/images/idp/apple.svg
Executable 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 |
@ -2,6 +2,7 @@ $lgn-idp-margin: 0.5rem 0;
|
||||
$lgn-idp-padding: 0 1px;
|
||||
$lgn-idp-provider-name-line-height: 36px;
|
||||
$lgn-idp-border-radius: 0.5rem;
|
||||
$lgn-idp-logo-size: 46px;
|
||||
|
||||
@mixin lgn-idp-base {
|
||||
display: block;
|
||||
@ -17,14 +18,14 @@ $lgn-idp-border-radius: 0.5rem;
|
||||
transition: border-color 0.2s ease-in-out;
|
||||
|
||||
span.logo {
|
||||
height: 46px;
|
||||
width: 46px;
|
||||
height: $lgn-idp-logo-size;
|
||||
width: $lgn-idp-logo-size;
|
||||
}
|
||||
|
||||
span.provider-name {
|
||||
line-height: $lgn-idp-provider-name-line-height;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
position: relative;
|
||||
left: calc(50% - $lgn-idp-logo-size);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@ -75,4 +76,17 @@ $lgn-idp-border-radius: 0.5rem;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +117,8 @@
|
||||
--zitadel-color-github-background: #ffffff;
|
||||
--zitadel-color-gitlab-text: #8b8d8d;
|
||||
--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-background: var(--zitadel-color-white);
|
||||
@ -125,6 +127,7 @@
|
||||
--github-image-src: url(../../../images/idp/github.png);
|
||||
--gitlab-image-src: url(../../../images/idp/gitlab.png);
|
||||
--azure-image-src: url(../../../images/idp/ms.svg);
|
||||
--apple-image-src: url(../../../images/idp/apple.svg);
|
||||
}
|
||||
|
||||
.lgn-dark-theme {
|
||||
@ -227,9 +230,12 @@
|
||||
--zitadel-color-github-background: #ffffff;
|
||||
--zitadel-color-gitlab-text: #8b8d8d;
|
||||
--zitadel-color-gitlab-background: #ffffff;
|
||||
--zitadel-color-apple-text: #8b8d8d;
|
||||
--zitadel-color-apple-background: #ffffff;
|
||||
|
||||
--google-image-src: url(../../../images/idp/google.png);
|
||||
--github-image-src: url(../../../images/idp/github-white.png);
|
||||
--gitlab-image-src: url(../../../images/idp/gitlab.png);
|
||||
--azure-image-src: url(../../../images/idp/ms.svg);
|
||||
--apple-image-src: url(../../../images/idp/apple-dark.svg);
|
||||
}
|
||||
|
@ -52,7 +52,11 @@
|
||||
<a href="{{ externalIDPAuthURL $reqid $provider.IDPConfigID}}"
|
||||
class="lgn-idp {{idpProviderClass $provider.IDPType}}">
|
||||
<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>
|
||||
{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
@ -29,7 +29,11 @@
|
||||
<a href="{{ externalIDPRegisterURL $reqid $provider.IDPConfigID}}"
|
||||
class="lgn-idp {{idpProviderClass $provider.IDPType}}">
|
||||
<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>
|
||||
{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
@ -200,21 +200,15 @@ func authMethodsFromSession(session *query.Session) []domain.UserAuthMethodType
|
||||
if !session.IntentFactor.IntentCheckedAt.IsZero() {
|
||||
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)
|
||||
}
|
||||
*/
|
||||
// TODO: add checks with https://github.com/zitadel/zitadel/issues/6224
|
||||
/*
|
||||
if !session.TOTPFactor.OTPSMSCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeOTPSMS)
|
||||
}
|
||||
if !session.TOTPFactor.OTPEmailCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeOTPEmail)
|
||||
}
|
||||
*/
|
||||
if !session.TOTPFactor.TOTPCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeTOTP)
|
||||
}
|
||||
if !session.OTPSMSFactor.OTPCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeOTPSMS)
|
||||
}
|
||||
if !session.OTPEmailFactor.OTPCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeOTPEmail)
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
|
@ -34,8 +34,9 @@ import (
|
||||
type Commands struct {
|
||||
httpClient *http.Client
|
||||
|
||||
checkPermission domain.PermissionCheck
|
||||
newCode cryptoCodeFunc
|
||||
checkPermission domain.PermissionCheck
|
||||
newCode cryptoCodeFunc
|
||||
newCodeWithDefault cryptoCodeWithDefaultFunc
|
||||
|
||||
eventstore *eventstore.Eventstore
|
||||
static static.Storage
|
||||
@ -122,6 +123,7 @@ func StartCommands(
|
||||
httpClient: httpClient,
|
||||
checkPermission: permissionCheck,
|
||||
newCode: newCryptoCode,
|
||||
newCodeWithDefault: newCryptoCodeWithDefaultConfig,
|
||||
sessionTokenCreator: sessionTokenCreator(idGenerator, sessionAlg),
|
||||
sessionTokenVerifier: sessionTokenVerifier,
|
||||
defaultAccessTokenLifetime: defaultAccessTokenLifetime,
|
||||
|
@ -12,6 +12,10 @@ import (
|
||||
|
||||
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 {
|
||||
Crypted *crypto.CryptoValue
|
||||
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) {
|
||||
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 {
|
||||
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 {
|
||||
gen, _, err := secretGenerator(ctx, filter, typ, alg)
|
||||
gen, _, err := secretGenerator(ctx, filter, typ, alg, emptyConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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) {
|
||||
config, err := secretGeneratorConfig(ctx, filter, typ)
|
||||
func secretGenerator(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto, defaultConfig *crypto.GeneratorConfig) (crypto.Generator, *crypto.GeneratorConfig, error) {
|
||||
config, err := secretGeneratorConfigWithDefault(ctx, filter, typ, defaultConfig)
|
||||
if err != nil {
|
||||
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) {
|
||||
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)
|
||||
events, err := filter(ctx, wm.Query())
|
||||
if err != nil {
|
||||
@ -67,6 +79,9 @@ func secretGeneratorConfig(ctx context.Context, filter preparation.FilterToQuery
|
||||
if err := wm.Reduce(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wm.State != domain.SecretGeneratorStateActive {
|
||||
return defaultConfig, nil
|
||||
}
|
||||
return &crypto.GeneratorConfig{
|
||||
Length: wm.Length,
|
||||
Expiry: wm.Expiry,
|
||||
|
@ -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 (
|
||||
testGeneratorConfig = crypto.GeneratorConfig{
|
||||
Length: 12,
|
||||
@ -175,8 +190,9 @@ func Test_verifyCryptoCode(t *testing.T) {
|
||||
|
||||
func Test_secretGenerator(t *testing.T) {
|
||||
type args struct {
|
||||
typ domain.SecretGeneratorType
|
||||
alg crypto.Crypto
|
||||
typ domain.SecretGeneratorType
|
||||
alg crypto.Crypto
|
||||
defaultConfig *crypto.GeneratorConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -190,8 +206,9 @@ func Test_secretGenerator(t *testing.T) {
|
||||
name: "filter config error",
|
||||
eventsore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
defaultConfig: emptyConfig,
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
@ -201,8 +218,9 @@ func Test_secretGenerator(t *testing.T) {
|
||||
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
||||
)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
defaultConfig: emptyConfig,
|
||||
},
|
||||
want: crypto.NewHashGenerator(testGeneratorConfig, crypto.CreateMockHashAlg(gomock.NewController(t))),
|
||||
wantConf: &testGeneratorConfig,
|
||||
@ -213,8 +231,31 @@ func Test_secretGenerator(t *testing.T) {
|
||||
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
||||
)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
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))),
|
||||
wantConf: &testGeneratorConfig,
|
||||
@ -225,15 +266,16 @@ func Test_secretGenerator(t *testing.T) {
|
||||
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
||||
)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: nil,
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: nil,
|
||||
defaultConfig: emptyConfig,
|
||||
},
|
||||
wantErr: errors.ThrowInternalf(nil, "COMMA-RreV6", "Errors.Internal unsupported crypto algorithm type %T", nil),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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)
|
||||
assert.IsType(t, tt.want, got)
|
||||
assert.Equal(t, tt.wantConf, gotConf)
|
||||
|
@ -110,6 +110,16 @@ type LDAPProvider struct {
|
||||
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) {
|
||||
writeModel := NewOrgIDPRemoveWriteModel(orgID, id)
|
||||
events, err := filter(ctx, writeModel.Query())
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"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/jwt"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
|
||||
@ -215,6 +216,8 @@ func tokensForSucceededIDPIntent(session idp.Session, encryptionAlg crypto.Encry
|
||||
tokens = s.Tokens
|
||||
case *azuread.Session:
|
||||
tokens = s.Tokens
|
||||
case *apple.Session:
|
||||
tokens = s.Tokens
|
||||
default:
|
||||
return nil, "", nil
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package command
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
@ -14,6 +15,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
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/github"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/gitlab"
|
||||
@ -1587,6 +1589,138 @@ func (wm *LDAPIDPWriteModel) GetProviderOptions() idp.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 {
|
||||
eventstore.WriteModel
|
||||
|
||||
@ -1617,6 +1751,8 @@ func (wm *IDPRemoveWriteModel) Reduce() error {
|
||||
wm.reduceAdded(e.ID)
|
||||
case *idp.LDAPIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID)
|
||||
case *idp.AppleIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID)
|
||||
case *idp.RemovedEvent:
|
||||
wm.reduceRemoved(e.ID)
|
||||
case *idpconfig.IDPConfigAddedEvent:
|
||||
@ -1699,6 +1835,10 @@ func (wm *IDPTypeWriteModel) Reduce() error {
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
|
||||
case *org.LDAPIDPAddedEvent:
|
||||
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:
|
||||
wm.reduceChanged(e.ID, domain.IDPTypeAzureAD)
|
||||
case *org.OIDCIDPMigratedAzureADEvent:
|
||||
@ -1774,6 +1914,7 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
instance.GitLabSelfHostedIDPAddedEventType,
|
||||
instance.GoogleIDPAddedEventType,
|
||||
instance.LDAPIDPAddedEventType,
|
||||
instance.AppleIDPAddedEventType,
|
||||
instance.OIDCIDPMigratedAzureADEventType,
|
||||
instance.OIDCIDPMigratedGoogleEventType,
|
||||
instance.IDPRemovedEventType,
|
||||
@ -1792,6 +1933,7 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
org.GitLabSelfHostedIDPAddedEventType,
|
||||
org.GoogleIDPAddedEventType,
|
||||
org.LDAPIDPAddedEventType,
|
||||
org.AppleIDPAddedEventType,
|
||||
org.OIDCIDPMigratedAzureADEventType,
|
||||
org.OIDCIDPMigratedGoogleEventType,
|
||||
org.IDPRemovedEventType,
|
||||
@ -1859,6 +2001,8 @@ func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idp
|
||||
writeModel.model = NewGitLabSelfHostedInstanceIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeGoogle:
|
||||
writeModel.model = NewGoogleInstanceIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeApple:
|
||||
writeModel.model = NewAppleInstanceIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeUnspecified:
|
||||
fallthrough
|
||||
default:
|
||||
@ -1886,6 +2030,8 @@ func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idp
|
||||
writeModel.model = NewGitLabSelfHostedOrgIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeGoogle:
|
||||
writeModel.model = NewGoogleOrgIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeApple:
|
||||
writeModel.model = NewAppleOrgIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeUnspecified:
|
||||
fallthrough
|
||||
default:
|
||||
|
@ -414,6 +414,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
||||
instanceAgg,
|
||||
setup.SMTPConfiguration.From,
|
||||
setup.SMTPConfiguration.FromName,
|
||||
setup.SMTPConfiguration.ReplyToAddress,
|
||||
setup.SMTPConfiguration.SMTP.Host,
|
||||
setup.SMTPConfiguration.SMTP.User,
|
||||
[]byte(setup.SMTPConfiguration.SMTP.Password),
|
||||
|
@ -467,6 +467,48 @@ func (c *Commands) UpdateInstanceLDAPProvider(ctx context.Context, id string, pr
|
||||
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) {
|
||||
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
|
||||
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 {
|
||||
return func() (preparation.CreateCommands, error) {
|
||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||
|
@ -793,6 +793,73 @@ func (wm *InstanceLDAPIDPWriteModel) NewChangedEvent(
|
||||
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 {
|
||||
IDPRemoveWriteModel
|
||||
}
|
||||
@ -832,6 +899,8 @@ func (wm *InstanceIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event)
|
||||
wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent)
|
||||
case *instance.LDAPIDPAddedEvent:
|
||||
wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
|
||||
case *instance.AppleIDPAddedEvent:
|
||||
wm.IDPRemoveWriteModel.AppendEvents(&e.AppleIDPAddedEvent)
|
||||
case *instance.IDPRemovedEvent:
|
||||
wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent)
|
||||
case *instance.IDPConfigAddedEvent:
|
||||
@ -861,6 +930,7 @@ func (wm *InstanceIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
instance.GitLabSelfHostedIDPAddedEventType,
|
||||
instance.GoogleIDPAddedEventType,
|
||||
instance.LDAPIDPAddedEventType,
|
||||
instance.AppleIDPAddedEventType,
|
||||
instance.IDPRemovedEventType,
|
||||
).
|
||||
EventData(map[string]interface{}{"id": wm.ID}).
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -12,13 +12,14 @@ import (
|
||||
type InstanceSMTPConfigWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
SenderAddress string
|
||||
SenderName string
|
||||
TLS bool
|
||||
Host string
|
||||
User string
|
||||
Password *crypto.CryptoValue
|
||||
State domain.SMTPConfigState
|
||||
SenderAddress string
|
||||
SenderName string
|
||||
ReplyToAddress string
|
||||
TLS bool
|
||||
Host string
|
||||
User string
|
||||
Password *crypto.CryptoValue
|
||||
State domain.SMTPConfigState
|
||||
|
||||
domain string
|
||||
domainState domain.InstanceDomainState
|
||||
@ -62,6 +63,7 @@ func (wm *InstanceSMTPConfigWriteModel) Reduce() error {
|
||||
wm.TLS = e.TLS
|
||||
wm.SenderAddress = e.SenderAddress
|
||||
wm.SenderName = e.SenderName
|
||||
wm.ReplyToAddress = e.ReplyToAddress
|
||||
wm.Host = e.Host
|
||||
wm.User = e.User
|
||||
wm.Password = e.Password
|
||||
@ -76,6 +78,9 @@ func (wm *InstanceSMTPConfigWriteModel) Reduce() error {
|
||||
if e.FromName != nil {
|
||||
wm.SenderName = *e.FromName
|
||||
}
|
||||
if e.ReplyToAddress != nil {
|
||||
wm.ReplyToAddress = *e.ReplyToAddress
|
||||
}
|
||||
if e.Host != nil {
|
||||
wm.Host = *e.Host
|
||||
}
|
||||
@ -87,6 +92,7 @@ func (wm *InstanceSMTPConfigWriteModel) Reduce() error {
|
||||
wm.TLS = false
|
||||
wm.SenderName = ""
|
||||
wm.SenderAddress = ""
|
||||
wm.ReplyToAddress = ""
|
||||
wm.Host = ""
|
||||
wm.User = ""
|
||||
wm.Password = nil
|
||||
@ -122,7 +128,7 @@ func (wm *InstanceSMTPConfigWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
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)
|
||||
var err error
|
||||
|
||||
@ -135,6 +141,9 @@ func (wm *InstanceSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, agg
|
||||
if wm.SenderName != fromName {
|
||||
changes = append(changes, instance.ChangeSMTPConfigFromName(fromName))
|
||||
}
|
||||
if wm.ReplyToAddress != replyToAddress {
|
||||
changes = append(changes, instance.ChangeSMTPConfigReplyToAddress(replyToAddress))
|
||||
}
|
||||
if wm.Host != smtpHost {
|
||||
changes = append(changes, instance.ChangeSMTPConfigSMTPHost(smtpHost))
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user