diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md
index c1d626054..dd9c382a2 100644
--- a/docs/docs/apis/proto/admin.md
+++ b/docs/docs/apis/proto/admin.md
@@ -568,6 +568,68 @@ all fields are updated. If no value is provided the field will be empty afterwar
PUT: /idps/{idp_id}/jwt_config
+### ListProviders
+
+> **rpc** ListProviders([ListProvidersRequest](#listprovidersrequest))
+[ListProvidersResponse](#listprovidersresponse)
+
+Returns all identity providers, which match the query
+Limit should always be set, there is a default limit set by the service
+
+
+
+ POST: /idps/templates/_search
+
+
+### GetProviderByID
+
+> **rpc** GetProviderByID([GetProviderByIDRequest](#getproviderbyidrequest))
+[GetProviderByIDResponse](#getproviderbyidresponse)
+
+Returns an identity provider of the instance
+
+
+
+ GET: /idps/templates/{id}
+
+
+### AddLDAPProvider
+
+> **rpc** AddLDAPProvider([AddLDAPProviderRequest](#addldapproviderrequest))
+[AddLDAPProviderResponse](#addldapproviderresponse)
+
+Add a new ldap identity provider on the instance
+
+
+
+ POST: /idps/ldap
+
+
+### UpdateLDAPProvider
+
+> **rpc** UpdateLDAPProvider([UpdateLDAPProviderRequest](#updateldapproviderrequest))
+[UpdateLDAPProviderResponse](#updateldapproviderresponse)
+
+Change an existing ldap identity provider on the instance
+
+
+
+ POST: /idps/ldap/{id}
+
+
+### DeleteProvider
+
+> **rpc** DeleteProvider([DeleteProviderRequest](#deleteproviderrequest))
+[DeleteProviderResponse](#deleteproviderresponse)
+
+Remove an identity provider
+Will remove all linked providers of this configuration on the users
+
+
+
+ POST: /idps/templates/{id}
+
+
### GetOrgIAMPolicy
> **rpc** GetOrgIAMPolicy([GetOrgIAMPolicyRequest](#getorgiampolicyrequest))
@@ -1861,6 +1923,39 @@ This is an empty request
+### AddLDAPProviderRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| name | string | - | string.min_len: 1
string.max_len: 200
|
+| host | string | - | string.min_len: 1
string.max_len: 200
|
+| port | string | - | string.max_len: 5
|
+| tls | bool | - | |
+| base_dn | string | - | string.min_len: 1
string.max_len: 200
|
+| user_object_class | string | - | string.min_len: 1
string.max_len: 200
|
+| user_unique_attribute | string | - | string.min_len: 1
string.max_len: 200
|
+| admin | string | - | string.min_len: 1
string.max_len: 200
|
+| password | string | - | string.min_len: 1
string.max_len: 200
|
+| attributes | zitadel.idp.v1.LDAPAttributes | - | |
+| provider_options | zitadel.idp.v1.Options | - | |
+
+
+
+
+### AddLDAPProviderResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ObjectDetails | - | |
+| id | string | - | |
+
+
+
+
### AddMultiFactorToLoginPolicyRequest
@@ -2124,6 +2219,28 @@ This is an empty request
+### DeleteProviderRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| id | string | - | string.min_len: 1
string.max_len: 200
|
+
+
+
+
+### DeleteProviderResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ObjectDetails | - | |
+
+
+
+
### ExportDataRequest
@@ -2924,6 +3041,28 @@ This is an empty request
+### GetProviderByIDRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| id | string | - | string.min_len: 1
string.max_len: 200
|
+
+
+
+
+### GetProviderByIDResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| idp | zitadel.idp.v1.Provider | - | |
+
+
+
+
### GetSMSProviderRequest
@@ -3541,6 +3680,30 @@ This is an empty request
+### ListProvidersRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| query | zitadel.v1.ListQuery | list limitations and ordering | |
+| queries | repeated ProviderQuery | criteria the client is looking for | |
+
+
+
+
+### ListProvidersResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ListDetails | - | |
+| result | repeated zitadel.idp.v1.Provider | - | |
+
+
+
+
### ListSMSProvidersRequest
@@ -3605,6 +3768,18 @@ This is an empty request
+### ProviderQuery
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.idp_id_query | zitadel.idp.v1.IDPIDQuery | - | |
+| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.idp_name_query | zitadel.idp.v1.IDPNameQuery | - | |
+
+
+
+
### ReactivateIDPRequest
@@ -4715,6 +4890,39 @@ this is en empty request
+### UpdateLDAPProviderRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| id | string | - | string.min_len: 1
string.max_len: 200
|
+| name | string | - | string.min_len: 1
string.max_len: 200
|
+| host | string | - | string.min_len: 1
string.max_len: 200
|
+| port | string | - | string.max_len: 5
|
+| tls | bool | - | |
+| base_dn | string | - | string.min_len: 1
string.max_len: 200
|
+| user_object_class | string | - | string.min_len: 1
string.max_len: 200
|
+| user_unique_attribute | string | - | string.min_len: 1
string.max_len: 200
|
+| admin | string | - | string.min_len: 1
string.max_len: 200
|
+| password | string | - | string.max_len: 200
|
+| attributes | zitadel.idp.v1.LDAPAttributes | - | |
+| provider_options | zitadel.idp.v1.Options | - | |
+
+
+
+
+### UpdateLDAPProviderResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ObjectDetails | - | |
+
+
+
+
### UpdateLabelPolicyRequest
diff --git a/docs/docs/apis/proto/idp.md b/docs/docs/apis/proto/idp.md
index 1181f64e0..cf923573b 100644
--- a/docs/docs/apis/proto/idp.md
+++ b/docs/docs/apis/proto/idp.md
@@ -105,6 +105,48 @@ title: zitadel/idp.proto
+### LDAPAttributes
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| id_attribute | string | - | string.max_len: 200
|
+| first_name_attribute | string | - | string.max_len: 200
|
+| last_name_attribute | string | - | string.max_len: 200
|
+| display_name_attribute | string | - | string.max_len: 200
|
+| nick_name_attribute | string | - | string.max_len: 200
|
+| preferred_username_attribute | string | - | string.max_len: 200
|
+| email_attribute | string | - | string.max_len: 200
|
+| email_verified_attribute | string | - | string.max_len: 200
|
+| phone_attribute | string | - | string.max_len: 200
|
+| phone_verified_attribute | string | - | string.max_len: 200
|
+| preferred_language_attribute | string | - | string.max_len: 200
|
+| avatar_url_attribute | string | - | string.max_len: 200
|
+| profile_attribute | string | - | string.max_len: 200
|
+
+
+
+
+### LDAPConfig
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| host | string | - | |
+| port | string | - | |
+| tls | bool | - | |
+| base_dn | string | - | |
+| user_object_class | string | - | |
+| user_unique_attribute | string | - | |
+| admin | string | - | |
+| attributes | LDAPAttributes | - | |
+| provider_options | Options | - | |
+
+
+
+
### OIDCConfig
@@ -120,6 +162,49 @@ title: zitadel/idp.proto
+### Options
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| is_linking_allowed | bool | - | |
+| is_creation_allowed | bool | - | |
+| is_auto_creation | bool | - | |
+| is_auto_update | bool | - | |
+
+
+
+
+### Provider
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| id | string | - | |
+| details | zitadel.v1.ObjectDetails | - | |
+| state | IDPState | - | |
+| name | string | - | |
+| owner | IDPOwnerType | - | |
+| type | ProviderType | - | |
+| config | ProviderConfig | - | |
+
+
+
+
+### ProviderConfig
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| options | Options | - | |
+| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) config.ldap | LDAPConfig | - | |
+
+
+
+
## Enums
@@ -195,3 +280,23 @@ authorization framework of the identity provider
+### ProviderType {#providertype}
+
+
+| Name | Number | Description |
+| ---- | ------ | ----------- |
+| PROVIDER_TYPE_UNSPECIFIED | 0 | - |
+| PROVIDER_TYPE_OIDC | 1 | - |
+| PROVIDER_TYPE_JWT | 2 | - |
+| PROVIDER_TYPE_LDAP | 3 | - |
+| PROVIDER_TYPE_OAUTH | 4 | - |
+| PROVIDER_TYPE_AZURE_AD | 5 | - |
+| PROVIDER_TYPE_GITHUB | 6 | - |
+| PROVIDER_TYPE_GITHUB_EE | 7 | - |
+| PROVIDER_TYPE_GITLAB | 8 | - |
+| PROVIDER_TYPE_GITLAB_SELF_HOSTED | 9 | - |
+| PROVIDER_TYPE_GOOGLE | 10 | - |
+
+
+
+
diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md
index af6f65be6..5951350c5 100644
--- a/docs/docs/apis/proto/management.md
+++ b/docs/docs/apis/proto/management.md
@@ -3025,6 +3025,68 @@ Change JWT identity provider configuration of the organisation
PUT: /idps/{idp_id}/jwt_config
+### ListProviders
+
+> **rpc** ListProviders([ListProvidersRequest](#listprovidersrequest))
+[ListProvidersResponse](#listprovidersresponse)
+
+Returns all identity providers, which match the query
+Limit should always be set, there is a default limit set by the service
+
+
+
+ POST: /idps/templates/_search
+
+
+### GetProviderByID
+
+> **rpc** GetProviderByID([GetProviderByIDRequest](#getproviderbyidrequest))
+[GetProviderByIDResponse](#getproviderbyidresponse)
+
+Returns an identity provider of the organisation
+
+
+
+ GET: /idps/templates/{id}
+
+
+### AddLDAPProvider
+
+> **rpc** AddLDAPProvider([AddLDAPProviderRequest](#addldapproviderrequest))
+[AddLDAPProviderResponse](#addldapproviderresponse)
+
+Add a new ldap identity provider in the organisation
+
+
+
+ POST: /idps/ldap
+
+
+### UpdateLDAPProvider
+
+> **rpc** UpdateLDAPProvider([UpdateLDAPProviderRequest](#updateldapproviderrequest))
+[UpdateLDAPProviderResponse](#updateldapproviderresponse)
+
+Change an existing ldap identity provider in the organisation
+
+
+
+ POST: /idps/ldap/{id}
+
+
+### DeleteProvider
+
+> **rpc** DeleteProvider([DeleteProviderRequest](#deleteproviderrequest))
+[DeleteProviderResponse](#deleteproviderresponse)
+
+Remove an identity provider
+Will remove all linked providers of this configuration on the users
+
+
+
+ POST: /idps/templates/{id}
+
+
### ListActions
> **rpc** ListActions([ListActionsRequest](#listactionsrequest))
@@ -3551,6 +3613,39 @@ This is an empty request
+### AddLDAPProviderRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| name | string | - | string.min_len: 1
string.max_len: 200
|
+| host | string | - | string.min_len: 1
string.max_len: 200
|
+| port | string | - | string.max_len: 5
|
+| tls | bool | - | |
+| base_dn | string | - | string.min_len: 1
string.max_len: 200
|
+| user_object_class | string | - | string.min_len: 1
string.max_len: 200
|
+| user_unique_attribute | string | - | string.min_len: 1
string.max_len: 200
|
+| admin | string | - | string.min_len: 1
string.max_len: 200
|
+| password | string | - | string.min_len: 1
string.max_len: 200
|
+| attributes | zitadel.idp.v1.LDAPAttributes | - | |
+| provider_options | zitadel.idp.v1.Options | - | |
+
+
+
+
+### AddLDAPProviderResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ObjectDetails | - | |
+| id | string | - | |
+
+
+
+
### AddMachineKeyRequest
@@ -4450,6 +4545,28 @@ This is an empty request
+### DeleteProviderRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| id | string | - | string.min_len: 1
string.max_len: 200
|
+
+
+
+
+### DeleteProviderResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ObjectDetails | - | |
+
+
+
+
### GenerateMachineSecretRequest
@@ -5539,6 +5656,28 @@ This is an empty request
+### GetProviderByIDRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| id | string | - | string.min_len: 1
string.max_len: 200
|
+
+
+
+
+### GetProviderByIDResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| idp | zitadel.idp.v1.Provider | - | |
+
+
+
+
### GetSupportedLanguagesRequest
This is an empty request
@@ -6544,6 +6683,30 @@ This is an empty request
+### ListProvidersRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| query | zitadel.v1.ListQuery | list limitations and ordering | |
+| queries | repeated ProviderQuery | criteria the client is looking for | |
+
+
+
+
+### ListProvidersResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ListDetails | - | |
+| result | repeated zitadel.idp.v1.Provider | - | |
+
+
+
+
### ListUserChangesRequest
@@ -6689,6 +6852,19 @@ This is an empty request
+### ProviderQuery
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.idp_id_query | zitadel.idp.v1.IDPIDQuery | - | |
+| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.idp_name_query | zitadel.idp.v1.IDPNameQuery | - | |
+| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.owner_type_query | zitadel.idp.v1.IDPOwnerTypeQuery | - | |
+
+
+
+
### ReactivateActionRequest
@@ -8760,6 +8936,39 @@ This is an empty request
+### UpdateLDAPProviderRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| id | string | - | string.min_len: 1
string.max_len: 200
|
+| name | string | - | string.min_len: 1
string.max_len: 200
|
+| host | string | - | string.min_len: 1
string.max_len: 200
|
+| port | string | - | string.max_len: 5
|
+| tls | bool | - | |
+| base_dn | string | - | string.min_len: 1
string.max_len: 200
|
+| user_object_class | string | - | string.min_len: 1
string.max_len: 200
|
+| user_unique_attribute | string | - | string.min_len: 1
string.max_len: 200
|
+| admin | string | - | string.min_len: 1
string.max_len: 200
|
+| password | string | - | string.max_len: 200
|
+| attributes | zitadel.idp.v1.LDAPAttributes | - | |
+| provider_options | zitadel.idp.v1.Options | - | |
+
+
+
+
+### UpdateLDAPProviderResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ObjectDetails | - | |
+
+
+
+
### UpdateMachineRequest
diff --git a/go.mod b/go.mod
index 1ae71ea18..fa9223a69 100644
--- a/go.mod
+++ b/go.mod
@@ -20,6 +20,7 @@ require (
github.com/drone/envsubst v1.0.3
github.com/duo-labs/webauthn v0.0.0-20211216225436-9a12cd078b8a
github.com/envoyproxy/protoc-gen-validate v0.6.7
+ github.com/go-ldap/ldap/v3 v3.4.4
github.com/golang/glog v1.0.0
github.com/golang/mock v1.6.0
github.com/golang/protobuf v1.5.2
@@ -69,11 +70,11 @@ require (
go.opentelemetry.io/otel/sdk/export/metric v0.25.0
go.opentelemetry.io/otel/sdk/metric v0.25.0
go.opentelemetry.io/otel/trace v1.2.0
- golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
- golang.org/x/net v0.5.0
+ golang.org/x/crypto v0.6.0
+ golang.org/x/net v0.6.0
golang.org/x/oauth2 v0.4.0
golang.org/x/sync v0.1.0
- golang.org/x/text v0.6.0
+ golang.org/x/text v0.7.0
golang.org/x/tools v0.1.13-0.20220928184430-f80e98464e27
google.golang.org/api v0.106.0
google.golang.org/genproto v0.0.0-20230106154932-a12b697841d9
@@ -89,6 +90,7 @@ require (
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.10.0 // indirect
cloud.google.com/go/trace v1.4.0 // indirect
+ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/amdonov/xmlsig v0.1.0 // indirect
@@ -113,6 +115,7 @@ require (
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fxamacker/cbor/v2 v2.2.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
+ github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/go-errors/errors v1.0.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
@@ -187,7 +190,7 @@ require (
go.opentelemetry.io/proto/otlp v0.10.0 // indirect
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
- golang.org/x/sys v0.4.0 // indirect
+ golang.org/x/sys v0.5.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
diff --git a/go.sum b/go.sum
index 0d82253bc..e4ea81e55 100644
--- a/go.sum
+++ b/go.sum
@@ -65,6 +65,9 @@ cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5og
cloud.google.com/go/trace v1.4.0 h1:qO9eLn2esajC9sxpqp1YKX37nXC3L4BfGnPS0Cx9dYo=
cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
+github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
+github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
@@ -264,6 +267,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
+github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
+github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2 h1:xMxH9j2fNg/L4hLn/4y3M0IUsn0M6Wbu/Uh9QlOfBh4=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
@@ -274,6 +279,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
+github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
@@ -871,6 +878,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@@ -906,8 +914,6 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM=
github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0=
-github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.7 h1:CGs4gdoSrZZyZM5pGeXCf8FH12r4r8hpJL/wUR3PxRA=
-github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.7/go.mod h1:2jHMP6o/WK0EmcNJkz+FSpjeqcCuQG9YqqqzKZkfgIE=
github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.8 h1:e6sRhY3Lijku8XBzazLoWpJcjO/EniEA7C5UEgiApRY=
github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.8/go.mod h1:2jHMP6o/WK0EmcNJkz+FSpjeqcCuQG9YqqqzKZkfgIE=
github.com/zitadel/saml v0.0.9 h1:q7FRu52Wm2S5rsSGuzR2nYhEClvexga8bwnGrBL7Bbw=
@@ -1002,8 +1008,9 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
-golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1102,8 +1109,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
-golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
+golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1226,8 +1233,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
-golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1242,8 +1249,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
-golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/internal/api/grpc/admin/idp.go b/internal/api/grpc/admin/idp.go
index cfe202aca..f0e73037b 100644
--- a/internal/api/grpc/admin/idp.go
+++ b/internal/api/grpc/admin/idp.go
@@ -150,3 +150,57 @@ func (s *Server) UpdateIDPJWTConfig(ctx context.Context, req *admin_pb.UpdateIDP
),
}, nil
}
+
+func (s *Server) GetProviderByID(ctx context.Context, req *admin_pb.GetProviderByIDRequest) (*admin_pb.GetProviderByIDResponse, error) {
+ idp, err := s.query.IDPTemplateByIDAndResourceOwner(ctx, true, req.Id, authz.GetInstance(ctx).InstanceID(), false)
+ if err != nil {
+ return nil, err
+ }
+ return &admin_pb.GetProviderByIDResponse{Idp: idp_grpc.ProviderToPb(idp)}, nil
+}
+
+func (s *Server) ListProviders(ctx context.Context, req *admin_pb.ListProvidersRequest) (*admin_pb.ListProvidersResponse, error) {
+ queries, err := listProvidersToQuery(authz.GetInstance(ctx).InstanceID(), req)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := s.query.IDPTemplates(ctx, queries, false)
+ if err != nil {
+ return nil, err
+ }
+ return &admin_pb.ListProvidersResponse{
+ Result: idp_grpc.ProvidersToPb(resp.Templates),
+ Details: object_pb.ToListDetails(resp.Count, resp.Sequence, resp.Timestamp),
+ }, nil
+}
+
+func (s *Server) AddLDAPProvider(ctx context.Context, req *admin_pb.AddLDAPProviderRequest) (*admin_pb.AddLDAPProviderResponse, error) {
+ id, details, err := s.command.AddInstanceLDAPProvider(ctx, addLDAPProviderToCommand(req))
+ if err != nil {
+ return nil, err
+ }
+ return &admin_pb.AddLDAPProviderResponse{
+ Id: id,
+ Details: object_pb.DomainToAddDetailsPb(details),
+ }, nil
+}
+
+func (s *Server) UpdateLDAPProvider(ctx context.Context, req *admin_pb.UpdateLDAPProviderRequest) (*admin_pb.UpdateLDAPProviderResponse, error) {
+ details, err := s.command.UpdateInstanceLDAPProvider(ctx, req.Id, updateLDAPProviderToCommand(req))
+ if err != nil {
+ return nil, err
+ }
+ return &admin_pb.UpdateLDAPProviderResponse{
+ 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 {
+ return nil, err
+ }
+ return &admin_pb.DeleteProviderResponse{
+ Details: object_pb.DomainToChangeDetailsPb(details),
+ }, nil
+}
diff --git a/internal/api/grpc/admin/idp_converter.go b/internal/api/grpc/admin/idp_converter.go
index dd9441091..43dac5e0d 100644
--- a/internal/api/grpc/admin/idp_converter.go
+++ b/internal/api/grpc/admin/idp_converter.go
@@ -3,6 +3,7 @@ package admin
import (
idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp"
"github.com/zitadel/zitadel/internal/api/grpc/object"
+ "github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@@ -155,3 +156,79 @@ func idpUserLinksToDomain(idps []*query.IDPUserLink) []*domain.UserIDPLink {
}
return externalIDPs
}
+
+func listProvidersToQuery(instanceID string, req *admin_pb.ListProvidersRequest) (*query.IDPTemplateSearchQueries, error) {
+ offset, limit, asc := object.ListQueryToModel(req.Query)
+ queries, err := providerQueriesToQuery(req.Queries)
+ if err != nil {
+ return nil, err
+ }
+ iamQuery, err := query.NewIDPTemplateResourceOwnerSearchQuery(instanceID)
+ if err != nil {
+ return nil, err
+ }
+ queries = append(queries, iamQuery)
+ return &query.IDPTemplateSearchQueries{
+ SearchRequest: query.SearchRequest{
+ Offset: offset,
+ Limit: limit,
+ Asc: asc,
+ },
+ Queries: queries,
+ }, nil
+}
+
+func providerQueriesToQuery(queries []*admin_pb.ProviderQuery) (q []query.SearchQuery, err error) {
+ q = make([]query.SearchQuery, len(queries))
+ for i, query := range queries {
+ q[i], err = providerQueryToQuery(query)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return q, nil
+}
+
+func providerQueryToQuery(idpQuery *admin_pb.ProviderQuery) (query.SearchQuery, error) {
+ switch q := idpQuery.Query.(type) {
+ case *admin_pb.ProviderQuery_IdpNameQuery:
+ return query.NewIDPTemplateNameSearchQuery(object.TextMethodToQuery(q.IdpNameQuery.Method), q.IdpNameQuery.Name)
+ case *admin_pb.ProviderQuery_IdpIdQuery:
+ return query.NewIDPTemplateIDSearchQuery(q.IdpIdQuery.Id)
+ default:
+ return nil, errors.ThrowInvalidArgument(nil, "ADMIN-Dr2aa", "List.Query.Invalid")
+ }
+}
+
+func addLDAPProviderToCommand(req *admin_pb.AddLDAPProviderRequest) command.LDAPProvider {
+ return command.LDAPProvider{
+ Name: req.Name,
+ Host: req.Host,
+ Port: req.Port,
+ TLS: req.Tls,
+ BaseDN: req.BaseDn,
+ UserObjectClass: req.UserObjectClass,
+ UserUniqueAttribute: req.UserUniqueAttribute,
+ Admin: req.Admin,
+ Password: req.Password,
+ LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes),
+ IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
+ }
+}
+
+func updateLDAPProviderToCommand(req *admin_pb.UpdateLDAPProviderRequest) command.LDAPProvider {
+ return command.LDAPProvider{
+ Name: req.Name,
+ Host: req.Host,
+ Port: req.Port,
+ TLS: req.Tls,
+ BaseDN: req.BaseDn,
+ UserObjectClass: req.UserObjectClass,
+ UserUniqueAttribute: req.UserUniqueAttribute,
+ Admin: req.Admin,
+ Password: req.Password,
+ LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes),
+ IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
+ }
+}
diff --git a/internal/api/grpc/idp/converter.go b/internal/api/grpc/idp/converter.go
index 8fe219f73..895a2ea97 100644
--- a/internal/api/grpc/idp/converter.go
+++ b/internal/api/grpc/idp/converter.go
@@ -5,6 +5,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
iam_model "github.com/zitadel/zitadel/internal/iam/model"
"github.com/zitadel/zitadel/internal/query"
+ "github.com/zitadel/zitadel/internal/repository/idp"
idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp"
)
@@ -296,3 +297,142 @@ func ownerTypeToPB(typ domain.IdentityProviderType) idp_pb.IDPOwnerType {
return idp_pb.IDPOwnerType_IDP_OWNER_TYPE_UNSPECIFIED
}
}
+
+func OptionsToCommand(options *idp_pb.Options) idp.Options {
+ if options == nil {
+ return idp.Options{}
+ }
+ return idp.Options{
+ IsCreationAllowed: options.IsCreationAllowed,
+ IsLinkingAllowed: options.IsLinkingAllowed,
+ IsAutoCreation: options.IsAutoCreation,
+ IsAutoUpdate: options.IsAutoUpdate,
+ }
+}
+
+func LDAPAttributesToCommand(attributes *idp_pb.LDAPAttributes) idp.LDAPAttributes {
+ if attributes == nil {
+ return idp.LDAPAttributes{}
+ }
+ return idp.LDAPAttributes{
+ IDAttribute: attributes.IdAttribute,
+ FirstNameAttribute: attributes.FirstNameAttribute,
+ LastNameAttribute: attributes.LastNameAttribute,
+ DisplayNameAttribute: attributes.DisplayNameAttribute,
+ NickNameAttribute: attributes.NickNameAttribute,
+ PreferredUsernameAttribute: attributes.PreferredUsernameAttribute,
+ EmailAttribute: attributes.EmailAttribute,
+ EmailVerifiedAttribute: attributes.EmailVerifiedAttribute,
+ PhoneAttribute: attributes.PhoneAttribute,
+ PhoneVerifiedAttribute: attributes.PhoneVerifiedAttribute,
+ PreferredLanguageAttribute: attributes.PreferredLanguageAttribute,
+ AvatarURLAttribute: attributes.AvatarUrlAttribute,
+ ProfileAttribute: attributes.ProfileAttribute,
+ }
+}
+
+func ProvidersToPb(providers []*query.IDPTemplate) []*idp_pb.Provider {
+ list := make([]*idp_pb.Provider, len(providers))
+ for i, provider := range providers {
+ list[i] = ProviderToPb(provider)
+ }
+ return list
+}
+
+func ProviderToPb(provider *query.IDPTemplate) *idp_pb.Provider {
+ return &idp_pb.Provider{
+ Id: provider.ID,
+ Details: obj_grpc.ToViewDetailsPb(provider.Sequence, provider.CreationDate, provider.ChangeDate, provider.ResourceOwner),
+ State: providerStateToPb(provider.State),
+ Name: provider.Name,
+ Owner: ownerTypeToPB(provider.OwnerType),
+ Type: providerTypeToPb(provider.Type),
+ Config: configToPb(provider),
+ }
+}
+
+func providerStateToPb(state domain.IDPState) idp_pb.IDPState {
+ switch state { //nolint:exhaustive
+ case domain.IDPStateActive:
+ return idp_pb.IDPState_IDP_STATE_ACTIVE
+ case domain.IDPStateInactive:
+ return idp_pb.IDPState_IDP_STATE_INACTIVE
+ case domain.IDPStateUnspecified:
+ return idp_pb.IDPState_IDP_STATE_UNSPECIFIED
+ default:
+ return idp_pb.IDPState_IDP_STATE_UNSPECIFIED
+ }
+}
+
+func providerTypeToPb(idpType domain.IDPType) idp_pb.ProviderType {
+ switch idpType {
+ case domain.IDPTypeOIDC:
+ return idp_pb.ProviderType_PROVIDER_TYPE_OIDC
+ case domain.IDPTypeJWT:
+ return idp_pb.ProviderType_PROVIDER_TYPE_JWT
+ case domain.IDPTypeOAuth:
+ return idp_pb.ProviderType_PROVIDER_TYPE_OAUTH
+ case domain.IDPTypeLDAP:
+ return idp_pb.ProviderType_PROVIDER_TYPE_LDAP
+ case domain.IDPTypeAzureAD:
+ return idp_pb.ProviderType_PROVIDER_TYPE_AZURE_AD
+ case domain.IDPTypeGitHub:
+ return idp_pb.ProviderType_PROVIDER_TYPE_GITHUB
+ case domain.IDPTypeGitHubEE:
+ return idp_pb.ProviderType_PROVIDER_TYPE_GITHUB_EE
+ case domain.IDPTypeGitLab:
+ return idp_pb.ProviderType_PROVIDER_TYPE_GITLAB
+ case domain.IDPTypeGitLabSelfHosted:
+ return idp_pb.ProviderType_PROVIDER_TYPE_GITLAB_SELF_HOSTED
+ case domain.IDPTypeGoogle:
+ return idp_pb.ProviderType_PROVIDER_TYPE_GOOGLE
+ case domain.IDPTypeUnspecified:
+ return idp_pb.ProviderType_PROVIDER_TYPE_UNSPECIFIED
+ default:
+ return idp_pb.ProviderType_PROVIDER_TYPE_UNSPECIFIED
+ }
+}
+
+func configToPb(config *query.IDPTemplate) *idp_pb.ProviderConfig {
+ providerConfig := &idp_pb.ProviderConfig{
+ Options: &idp_pb.Options{
+ IsLinkingAllowed: config.IsLinkingAllowed,
+ IsCreationAllowed: config.IsCreationAllowed,
+ IsAutoCreation: config.IsAutoCreation,
+ IsAutoUpdate: config.IsAutoUpdate,
+ },
+ }
+ if config.LDAPIDPTemplate != nil {
+ providerConfig.Config = &idp_pb.ProviderConfig_Ldap{
+ Ldap: &idp_pb.LDAPConfig{
+ Host: config.Host,
+ Port: config.Port,
+ Tls: config.TLS,
+ BaseDn: config.BaseDN,
+ UserObjectClass: config.UserObjectClass,
+ UserUniqueAttribute: config.UserUniqueAttribute,
+ Admin: config.Admin,
+ Attributes: ldapAttributesToPb(config.LDAPAttributes),
+ },
+ }
+ }
+ return providerConfig
+}
+
+func ldapAttributesToPb(attributes idp.LDAPAttributes) *idp_pb.LDAPAttributes {
+ return &idp_pb.LDAPAttributes{
+ IdAttribute: attributes.IDAttribute,
+ FirstNameAttribute: attributes.FirstNameAttribute,
+ LastNameAttribute: attributes.LastNameAttribute,
+ DisplayNameAttribute: attributes.DisplayNameAttribute,
+ NickNameAttribute: attributes.NickNameAttribute,
+ PreferredUsernameAttribute: attributes.PreferredUsernameAttribute,
+ EmailAttribute: attributes.EmailAttribute,
+ EmailVerifiedAttribute: attributes.EmailVerifiedAttribute,
+ PhoneAttribute: attributes.PhoneAttribute,
+ PhoneVerifiedAttribute: attributes.PhoneVerifiedAttribute,
+ PreferredLanguageAttribute: attributes.PreferredLanguageAttribute,
+ AvatarUrlAttribute: attributes.AvatarURLAttribute,
+ ProfileAttribute: attributes.ProfileAttribute,
+ }
+}
diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go
index 30ba162ac..8fb798a0b 100644
--- a/internal/api/grpc/management/idp.go
+++ b/internal/api/grpc/management/idp.go
@@ -142,3 +142,57 @@ func (s *Server) UpdateOrgIDPJWTConfig(ctx context.Context, req *mgmt_pb.UpdateO
),
}, nil
}
+
+func (s *Server) GetProviderByID(ctx context.Context, req *mgmt_pb.GetProviderByIDRequest) (*mgmt_pb.GetProviderByIDResponse, error) {
+ idp, err := s.query.IDPTemplateByIDAndResourceOwner(ctx, true, req.Id, authz.GetCtxData(ctx).OrgID, false)
+ if err != nil {
+ return nil, err
+ }
+ return &mgmt_pb.GetProviderByIDResponse{Idp: idp_grpc.ProviderToPb(idp)}, nil
+}
+
+func (s *Server) ListProviders(ctx context.Context, req *mgmt_pb.ListProvidersRequest) (*mgmt_pb.ListProvidersResponse, error) {
+ queries, err := listProvidersToQuery(ctx, req)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := s.query.IDPTemplates(ctx, queries, false)
+ if err != nil {
+ return nil, err
+ }
+ return &mgmt_pb.ListProvidersResponse{
+ Result: idp_grpc.ProvidersToPb(resp.Templates),
+ Details: object_pb.ToListDetails(resp.Count, resp.Sequence, resp.Timestamp),
+ }, nil
+}
+
+func (s *Server) AddLDAPProvider(ctx context.Context, req *mgmt_pb.AddLDAPProviderRequest) (*mgmt_pb.AddLDAPProviderResponse, error) {
+ id, details, err := s.command.AddOrgLDAPProvider(ctx, authz.GetCtxData(ctx).OrgID, addLDAPProviderToCommand(req))
+ if err != nil {
+ return nil, err
+ }
+ return &mgmt_pb.AddLDAPProviderResponse{
+ Id: id,
+ Details: object_pb.DomainToAddDetailsPb(details),
+ }, nil
+}
+
+func (s *Server) UpdateLDAPProvider(ctx context.Context, req *mgmt_pb.UpdateLDAPProviderRequest) (*mgmt_pb.UpdateLDAPProviderResponse, error) {
+ details, err := s.command.UpdateOrgLDAPProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id, updateLDAPProviderToCommand(req))
+ if err != nil {
+ return nil, err
+ }
+ return &mgmt_pb.UpdateLDAPProviderResponse{
+ 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 {
+ return nil, err
+ }
+ return &mgmt_pb.DeleteProviderResponse{
+ Details: object_pb.DomainToChangeDetailsPb(details),
+ }, nil
+}
diff --git a/internal/api/grpc/management/idp_converter.go b/internal/api/grpc/management/idp_converter.go
index b8dc6ba04..ba65ef33d 100644
--- a/internal/api/grpc/management/idp_converter.go
+++ b/internal/api/grpc/management/idp_converter.go
@@ -6,6 +6,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp"
"github.com/zitadel/zitadel/internal/api/grpc/object"
+ "github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@@ -170,3 +171,81 @@ func userLinksToDomain(idps []*query.IDPUserLink) []*domain.UserIDPLink {
}
return links
}
+
+func listProvidersToQuery(ctx context.Context, req *mgmt_pb.ListProvidersRequest) (*query.IDPTemplateSearchQueries, error) {
+ offset, limit, asc := object.ListQueryToModel(req.Query)
+ queries, err := providerQueriesToQuery(req.Queries)
+ if err != nil {
+ return nil, err
+ }
+ resourceOwnerQuery, err := query.NewIDPTemplateResourceOwnerListSearchQuery(authz.GetInstance(ctx).InstanceID(), authz.GetCtxData(ctx).OrgID)
+ if err != nil {
+ return nil, err
+ }
+ queries = append(queries, resourceOwnerQuery)
+ return &query.IDPTemplateSearchQueries{
+ SearchRequest: query.SearchRequest{
+ Offset: offset,
+ Limit: limit,
+ Asc: asc,
+ },
+ Queries: queries,
+ }, nil
+}
+
+func providerQueriesToQuery(queries []*mgmt_pb.ProviderQuery) (q []query.SearchQuery, err error) {
+ q = make([]query.SearchQuery, len(queries))
+ for i, query := range queries {
+ q[i], err = providerQueryToQuery(query)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return q, nil
+}
+
+func providerQueryToQuery(idpQuery *mgmt_pb.ProviderQuery) (query.SearchQuery, error) {
+ switch q := idpQuery.Query.(type) {
+ case *mgmt_pb.ProviderQuery_IdpNameQuery:
+ return query.NewIDPTemplateNameSearchQuery(object.TextMethodToQuery(q.IdpNameQuery.Method), q.IdpNameQuery.Name)
+ case *mgmt_pb.ProviderQuery_IdpIdQuery:
+ return query.NewIDPTemplateIDSearchQuery(q.IdpIdQuery.Id)
+ case *mgmt_pb.ProviderQuery_OwnerTypeQuery:
+ return query.NewIDPTemplateOwnerTypeSearchQuery(idp_grpc.IDPProviderTypeFromPb(q.OwnerTypeQuery.OwnerType))
+ default:
+ return nil, errors.ThrowInvalidArgument(nil, "ORG-Dr2aa", "List.Query.Invalid")
+ }
+}
+
+func addLDAPProviderToCommand(req *mgmt_pb.AddLDAPProviderRequest) command.LDAPProvider {
+ return command.LDAPProvider{
+ Name: req.Name,
+ Host: req.Host,
+ Port: req.Port,
+ TLS: req.Tls,
+ BaseDN: req.BaseDn,
+ UserObjectClass: req.UserObjectClass,
+ UserUniqueAttribute: req.UserUniqueAttribute,
+ Admin: req.Admin,
+ Password: req.Password,
+ LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes),
+ IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
+ }
+}
+
+func updateLDAPProviderToCommand(req *mgmt_pb.UpdateLDAPProviderRequest) command.LDAPProvider {
+ return command.LDAPProvider{
+ Name: req.Name,
+ Host: req.Host,
+ Port: req.Port,
+ TLS: req.Tls,
+ BaseDN: req.BaseDn,
+ UserObjectClass: req.UserObjectClass,
+ UserUniqueAttribute: req.UserUniqueAttribute,
+ Admin: req.Admin,
+ Password: req.Password,
+ LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes),
+ IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
+ }
+}
diff --git a/internal/command/idp.go b/internal/command/idp.go
new file mode 100644
index 000000000..113bd3725
--- /dev/null
+++ b/internal/command/idp.go
@@ -0,0 +1,17 @@
+package command
+
+import "github.com/zitadel/zitadel/internal/repository/idp"
+
+type LDAPProvider struct {
+ Name string
+ Host string
+ Port string
+ TLS bool
+ BaseDN string
+ UserObjectClass string
+ UserUniqueAttribute string
+ Admin string
+ Password string
+ LDAPAttributes idp.LDAPAttributes
+ IDPOptions idp.Options
+}
diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go
new file mode 100644
index 000000000..f21e45e6c
--- /dev/null
+++ b/internal/command/idp_model.go
@@ -0,0 +1,208 @@
+package command
+
+import (
+ "github.com/zitadel/zitadel/internal/crypto"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/repository/idp"
+ "github.com/zitadel/zitadel/internal/repository/idpconfig"
+)
+
+type LDAPIDPWriteModel struct {
+ eventstore.WriteModel
+
+ ID string
+ Name string
+ Host string
+ Port string
+ TLS bool
+ BaseDN string
+ UserObjectClass string
+ UserUniqueAttribute string
+ Admin string
+ Password *crypto.CryptoValue
+ idp.LDAPAttributes
+ idp.Options
+
+ State domain.IDPState
+}
+
+func (wm *LDAPIDPWriteModel) Reduce() error {
+ for _, event := range wm.Events {
+ switch e := event.(type) {
+ case *idp.LDAPIDPAddedEvent:
+ if wm.ID != e.ID {
+ continue
+ }
+ wm.reduceAddeddEvent(e)
+ case *idp.LDAPIDPChangedEvent:
+ if wm.ID != e.ID {
+ continue
+ }
+ wm.reduceChangedEvent(e)
+ case *idp.RemovedEvent:
+ if wm.ID != e.ID {
+ continue
+ }
+ wm.State = domain.IDPStateRemoved
+ }
+ }
+ return wm.WriteModel.Reduce()
+}
+
+func (wm *LDAPIDPWriteModel) reduceAddeddEvent(e *idp.LDAPIDPAddedEvent) {
+ wm.Name = e.Name
+ wm.Host = e.Host
+ wm.Port = e.Port
+ wm.TLS = e.TLS
+ wm.BaseDN = e.BaseDN
+ wm.UserObjectClass = e.UserObjectClass
+ wm.UserUniqueAttribute = e.UserUniqueAttribute
+ wm.Admin = e.Admin
+ wm.Password = e.Password
+ wm.LDAPAttributes = e.LDAPAttributes
+ wm.Options = e.Options
+ wm.State = domain.IDPStateActive
+}
+
+func (wm *LDAPIDPWriteModel) reduceChangedEvent(e *idp.LDAPIDPChangedEvent) {
+ if e.Name != nil {
+ wm.Name = *e.Name
+ }
+ if e.Name != nil {
+ wm.Name = *e.Name
+ }
+ if e.Host != nil {
+ wm.Host = *e.Host
+ }
+ if e.Port != nil {
+ wm.Port = *e.Port
+ }
+ if e.TLS != nil {
+ wm.TLS = *e.TLS
+ }
+ if e.BaseDN != nil {
+ wm.BaseDN = *e.BaseDN
+ }
+ if e.UserObjectClass != nil {
+ wm.UserObjectClass = *e.UserObjectClass
+ }
+ if e.UserUniqueAttribute != nil {
+ wm.UserUniqueAttribute = *e.UserUniqueAttribute
+ }
+ if e.Admin != nil {
+ wm.Admin = *e.Admin
+ }
+ if e.Password != nil {
+ wm.Password = e.Password
+ }
+ wm.LDAPAttributes.ReduceChanges(e.LDAPAttributeChanges)
+ wm.Options.ReduceChanges(e.OptionChanges)
+}
+
+func (wm *LDAPIDPWriteModel) NewChanges(
+ name,
+ host,
+ port string,
+ tls bool,
+ baseDN,
+ userObjectClass,
+ userUniqueAttribute,
+ admin string,
+ password string,
+ secretCrypto crypto.Crypto,
+ attributes idp.LDAPAttributes,
+ options idp.Options,
+) ([]idp.LDAPIDPChanges, error) {
+ changes := make([]idp.LDAPIDPChanges, 0)
+ var cryptedPassword *crypto.CryptoValue
+ var err error
+ if password != "" {
+ cryptedPassword, err = crypto.Crypt([]byte(password), secretCrypto)
+ if err != nil {
+ return nil, err
+ }
+ changes = append(changes, idp.ChangeLDAPPassword(cryptedPassword))
+ }
+ if wm.Name != name {
+ changes = append(changes, idp.ChangeLDAPName(name))
+ }
+ if wm.Host != host {
+ changes = append(changes, idp.ChangeLDAPHost(host))
+ }
+ if wm.Port != port {
+ changes = append(changes, idp.ChangeLDAPPort(port))
+ }
+ if wm.TLS != tls {
+ changes = append(changes, idp.ChangeLDAPTLS(tls))
+ }
+ if wm.BaseDN != baseDN {
+ changes = append(changes, idp.ChangeLDAPBaseDN(baseDN))
+ }
+ if wm.UserObjectClass != userObjectClass {
+ changes = append(changes, idp.ChangeLDAPUserObjectClass(userObjectClass))
+ }
+ if wm.UserUniqueAttribute != userUniqueAttribute {
+ changes = append(changes, idp.ChangeLDAPUserUniqueAttribute(userUniqueAttribute))
+ }
+ if wm.Admin != admin {
+ changes = append(changes, idp.ChangeLDAPAdmin(admin))
+ }
+ attrs := wm.LDAPAttributes.Changes(attributes)
+ if !attrs.IsZero() {
+ changes = append(changes, idp.ChangeLDAPAttributes(attrs))
+ }
+ opts := wm.Options.Changes(options)
+ if !opts.IsZero() {
+ changes = append(changes, idp.ChangeLDAPOptions(opts))
+ }
+ return changes, nil
+}
+
+type IDPRemoveWriteModel struct {
+ eventstore.WriteModel
+
+ ID string
+ State domain.IDPState
+ name string
+}
+
+func (wm *IDPRemoveWriteModel) Reduce() error {
+ for _, event := range wm.Events {
+ switch e := event.(type) {
+ case *idp.LDAPIDPAddedEvent:
+ wm.reduceAdded(e.ID, e.Name)
+ case *idp.LDAPIDPChangedEvent:
+ wm.reduceChanged(e.ID, e.Name)
+ case *idp.RemovedEvent:
+ wm.reduceRemoved(e.ID)
+ case *idpconfig.IDPConfigAddedEvent:
+ wm.reduceAdded(e.ConfigID, "")
+ case *idpconfig.IDPConfigRemovedEvent:
+ wm.reduceRemoved(e.ConfigID)
+ }
+ }
+ return wm.WriteModel.Reduce()
+}
+
+func (wm *IDPRemoveWriteModel) reduceAdded(id string, name string) {
+ if wm.ID != id {
+ return
+ }
+ wm.State = domain.IDPStateActive
+ wm.name = name
+}
+
+func (wm *IDPRemoveWriteModel) reduceChanged(id string, name *string) {
+ if wm.ID != id || name == nil {
+ return
+ }
+ wm.name = *name
+}
+
+func (wm *IDPRemoveWriteModel) reduceRemoved(id string) {
+ if wm.ID != id {
+ return
+ }
+ wm.State = domain.IDPStateRemoved
+}
diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go
new file mode 100644
index 000000000..60f696ce5
--- /dev/null
+++ b/internal/command/instance_idp.go
@@ -0,0 +1,205 @@
+package command
+
+import (
+ "context"
+ "strings"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/command/preparation"
+ "github.com/zitadel/zitadel/internal/crypto"
+ "github.com/zitadel/zitadel/internal/domain"
+ caos_errs "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/repository/instance"
+)
+
+func (c *Commands) AddInstanceLDAPProvider(ctx context.Context, provider LDAPProvider) (string, *domain.ObjectDetails, error) {
+ instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
+ id, err := c.idGenerator.Next()
+ if err != nil {
+ return "", nil, err
+ }
+ cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceLDAPProvider(instanceAgg, id, 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) UpdateInstanceLDAPProvider(ctx context.Context, id string, provider LDAPProvider) (*domain.ObjectDetails, error) {
+ instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
+ cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateInstanceLDAPProvider(instanceAgg, id, provider))
+ if err != nil {
+ return nil, err
+ }
+ if len(cmds) == 0 {
+ // no change, so return directly
+ return &domain.ObjectDetails{}, 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))
+ if err != nil {
+ return nil, err
+ }
+ pushedEvents, err := c.eventstore.Push(ctx, cmds...)
+ if err != nil {
+ return nil, err
+ }
+ return pushedEventsToObjectDetails(pushedEvents), nil
+}
+
+func (c *Commands) prepareAddInstanceLDAPProvider(a *instance.Aggregate, id string, provider LDAPProvider) preparation.Validation {
+ return func() (preparation.CreateCommands, error) {
+ if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SAfdd", "Errors.Invalid.Argument")
+ }
+ if provider.Host = strings.TrimSpace(provider.Host); provider.Host == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SDVg2", "Errors.Invalid.Argument")
+ }
+ if provider.BaseDN = strings.TrimSpace(provider.BaseDN); provider.BaseDN == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-sv31s", "Errors.Invalid.Argument")
+ }
+ if provider.UserObjectClass = strings.TrimSpace(provider.UserObjectClass); provider.UserObjectClass == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-sdgf4", "Errors.Invalid.Argument")
+ }
+ if provider.UserUniqueAttribute = strings.TrimSpace(provider.UserUniqueAttribute); provider.UserUniqueAttribute == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-AEG2w", "Errors.Invalid.Argument")
+ }
+ if provider.Admin = strings.TrimSpace(provider.Admin); provider.Admin == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SAD5n", "Errors.Invalid.Argument")
+ }
+ if provider.Password = strings.TrimSpace(provider.Password); provider.Password == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-sdf5h", "Errors.Invalid.Argument")
+ }
+ return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
+ writeModel := NewLDAPInstanceIDPWriteModel(a.InstanceID, id)
+ events, err := filter(ctx, writeModel.Query())
+ if err != nil {
+ return nil, err
+ }
+ writeModel.AppendEvents(events...)
+ if err = writeModel.Reduce(); err != nil {
+ return nil, err
+ }
+ secret, err := crypto.Encrypt([]byte(provider.Password), c.idpConfigEncryption)
+ if err != nil {
+ return nil, err
+ }
+ return []eventstore.Command{
+ instance.NewLDAPIDPAddedEvent(
+ ctx,
+ &a.Aggregate,
+ id,
+ provider.Name,
+ provider.Host,
+ provider.Port,
+ provider.TLS,
+ provider.BaseDN,
+ provider.UserObjectClass,
+ provider.UserUniqueAttribute,
+ provider.Admin,
+ secret,
+ provider.LDAPAttributes,
+ provider.IDPOptions,
+ ),
+ }, nil
+ }, nil
+ }
+}
+
+func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, id string, provider LDAPProvider) preparation.Validation {
+ return func() (preparation.CreateCommands, error) {
+ if id = strings.TrimSpace(id); id == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Dgdbs", "Errors.Invalid.Argument")
+ }
+ if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Sffgd", "Errors.Invalid.Argument")
+ }
+ if provider.Host = strings.TrimSpace(provider.Host); provider.Host == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Dz62d", "Errors.Invalid.Argument")
+ }
+ if provider.BaseDN = strings.TrimSpace(provider.BaseDN); provider.BaseDN == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-vb3ss", "Errors.Invalid.Argument")
+ }
+ if provider.UserObjectClass = strings.TrimSpace(provider.UserObjectClass); provider.UserObjectClass == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-hbere", "Errors.Invalid.Argument")
+ }
+ if provider.UserUniqueAttribute = strings.TrimSpace(provider.UserUniqueAttribute); provider.UserUniqueAttribute == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-ASFt6", "Errors.Invalid.Argument")
+ }
+ if provider.Admin = strings.TrimSpace(provider.Admin); provider.Admin == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "INST-DG45z", "Errors.Invalid.Argument")
+ }
+ return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
+ writeModel := NewLDAPInstanceIDPWriteModel(a.InstanceID, id)
+ 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-ASF3F", "Errors.Instance.IDPConfig.NotExisting")
+ }
+ event, err := writeModel.NewChangedEvent(
+ ctx,
+ &a.Aggregate,
+ id,
+ writeModel.Name,
+ provider.Name,
+ provider.Host,
+ provider.Port,
+ provider.TLS,
+ provider.BaseDN,
+ provider.UserObjectClass,
+ provider.UserUniqueAttribute,
+ provider.Admin,
+ provider.Password,
+ c.idpConfigEncryption,
+ provider.LDAPAttributes,
+ provider.IDPOptions,
+ )
+ if err != nil {
+ return nil, err
+ }
+ if event == nil {
+ return nil, nil
+ }
+ 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) {
+ writeModel := NewInstanceIDPRemoveWriteModel(a.InstanceID, id)
+ 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-Se3tg", "Errors.Instance.IDPConfig.NotExisting")
+ }
+ return []eventstore.Command{instance.NewIDPRemovedEvent(ctx, &a.Aggregate, id, writeModel.name)}, nil
+ }, nil
+ }
+}
diff --git a/internal/command/instance_idp_model.go b/internal/command/instance_idp_model.go
new file mode 100644
index 000000000..6108ef256
--- /dev/null
+++ b/internal/command/instance_idp_model.go
@@ -0,0 +1,163 @@
+package command
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/crypto"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/repository/idp"
+ "github.com/zitadel/zitadel/internal/repository/instance"
+)
+
+type InstanceLDAPIDPWriteModel struct {
+ LDAPIDPWriteModel
+}
+
+func NewLDAPInstanceIDPWriteModel(instanceID, id string) *InstanceLDAPIDPWriteModel {
+ return &InstanceLDAPIDPWriteModel{
+ LDAPIDPWriteModel{
+ WriteModel: eventstore.WriteModel{
+ AggregateID: instanceID,
+ ResourceOwner: instanceID,
+ },
+ ID: id,
+ },
+ }
+}
+
+func (wm *InstanceLDAPIDPWriteModel) Reduce() error {
+ return wm.LDAPIDPWriteModel.Reduce()
+}
+
+func (wm *InstanceLDAPIDPWriteModel) AppendEvents(events ...eventstore.Event) {
+ for _, event := range events {
+ switch e := event.(type) {
+ case *instance.LDAPIDPAddedEvent:
+ if wm.ID != e.ID {
+ continue
+ }
+ wm.LDAPIDPWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
+ case *instance.LDAPIDPChangedEvent:
+ if wm.ID != e.ID {
+ continue
+ }
+ wm.LDAPIDPWriteModel.AppendEvents(&e.LDAPIDPChangedEvent)
+ case *instance.IDPRemovedEvent:
+ if wm.ID != e.ID {
+ continue
+ }
+ wm.LDAPIDPWriteModel.AppendEvents(&e.RemovedEvent)
+ default:
+ wm.LDAPIDPWriteModel.AppendEvents(e)
+ }
+ }
+}
+
+func (wm *InstanceLDAPIDPWriteModel) Query() *eventstore.SearchQueryBuilder {
+ return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ ResourceOwner(wm.ResourceOwner).
+ AddQuery().
+ AggregateTypes(instance.AggregateType).
+ AggregateIDs(wm.AggregateID).
+ EventTypes(
+ instance.LDAPIDPAddedEventType,
+ instance.LDAPIDPChangedEventType,
+ instance.IDPRemovedEventType,
+ ).
+ Builder()
+}
+
+func (wm *InstanceLDAPIDPWriteModel) NewChangedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ id,
+ oldName,
+ name,
+ host,
+ port string,
+ tls bool,
+ baseDN,
+ userObjectClass,
+ userUniqueAttribute,
+ admin string,
+ password string,
+ secretCrypto crypto.Crypto,
+ attributes idp.LDAPAttributes,
+ options idp.Options,
+) (*instance.LDAPIDPChangedEvent, error) {
+
+ changes, err := wm.LDAPIDPWriteModel.NewChanges(
+ name,
+ host,
+ port,
+ tls,
+ baseDN,
+ userObjectClass,
+ userUniqueAttribute,
+ admin,
+ password,
+ secretCrypto,
+ attributes,
+ options,
+ )
+ if err != nil {
+ return nil, err
+ }
+ if len(changes) == 0 {
+ return nil, nil
+ }
+ changeEvent, err := instance.NewLDAPIDPChangedEvent(ctx, aggregate, id, oldName, changes)
+ if err != nil {
+ return nil, err
+ }
+ return changeEvent, nil
+}
+
+type InstanceIDPRemoveWriteModel struct {
+ IDPRemoveWriteModel
+}
+
+func NewInstanceIDPRemoveWriteModel(instanceID, id string) *InstanceIDPRemoveWriteModel {
+ return &InstanceIDPRemoveWriteModel{
+ IDPRemoveWriteModel{
+ WriteModel: eventstore.WriteModel{
+ AggregateID: instanceID,
+ ResourceOwner: instanceID,
+ },
+ ID: id,
+ },
+ }
+}
+
+func (wm *InstanceIDPRemoveWriteModel) Reduce() error {
+ return wm.IDPRemoveWriteModel.Reduce()
+}
+
+func (wm *InstanceIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) {
+ for _, event := range events {
+ switch e := event.(type) {
+ case *instance.LDAPIDPAddedEvent:
+ wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
+ case *instance.LDAPIDPChangedEvent:
+ wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPChangedEvent)
+ case *instance.IDPRemovedEvent:
+ wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent)
+ default:
+ wm.IDPRemoveWriteModel.AppendEvents(e)
+ }
+ }
+}
+
+func (wm *InstanceIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder {
+ return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ ResourceOwner(wm.ResourceOwner).
+ AddQuery().
+ AggregateTypes(instance.AggregateType).
+ AggregateIDs(wm.AggregateID).
+ EventTypes(
+ instance.LDAPIDPAddedEventType,
+ instance.LDAPIDPChangedEventType,
+ instance.IDPRemovedEventType,
+ ).
+ Builder()
+}
diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go
new file mode 100644
index 000000000..2eb526950
--- /dev/null
+++ b/internal/command/instance_idp_test.go
@@ -0,0 +1,684 @@
+package command
+
+import (
+ "context"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/crypto"
+ "github.com/zitadel/zitadel/internal/domain"
+ caos_errors "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/repository"
+ "github.com/zitadel/zitadel/internal/id"
+ id_mock "github.com/zitadel/zitadel/internal/id/mock"
+ "github.com/zitadel/zitadel/internal/repository/idp"
+ "github.com/zitadel/zitadel/internal/repository/idpconfig"
+ "github.com/zitadel/zitadel/internal/repository/instance"
+)
+
+func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) {
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ idGenerator id.Generator
+ secretCrypto crypto.EncryptionAlgorithm
+ }
+ type args struct {
+ ctx context.Context
+ provider LDAPProvider
+ }
+ type res struct {
+ id string
+ want *domain.ObjectDetails
+ err func(error) bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ "invalid name",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ provider: LDAPProvider{},
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid host",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ provider: LDAPProvider{
+ Name: "name",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid baseDN",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid userObjectClass",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid userUniqueAttribute",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid admin",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid password",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ Admin: "admin",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ name: "ok",
+ fields: fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID(
+ "instance1",
+ instance.NewLDAPIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
+ "id1",
+ "name",
+ "host",
+ "",
+ false,
+ "baseDN",
+ "userObjectClass",
+ "userUniqueAttribute",
+ "admin",
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("password"),
+ },
+ idp.LDAPAttributes{},
+ idp.Options{},
+ )),
+ },
+ uniqueConstraintsFromEventConstraintWithInstanceID("instance1", idpconfig.NewAddIDPConfigNameUniqueConstraint("name", "instance1")),
+ ),
+ ),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ Admin: "admin",
+ Password: "password",
+ },
+ },
+ 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.NewLDAPIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
+ "id1",
+ "name",
+ "host",
+ "port",
+ true,
+ "baseDN",
+ "userObjectClass",
+ "userUniqueAttribute",
+ "admin",
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("password"),
+ },
+ idp.LDAPAttributes{
+ IDAttribute: "id",
+ FirstNameAttribute: "firstName",
+ LastNameAttribute: "lastName",
+ DisplayNameAttribute: "displayName",
+ NickNameAttribute: "nickName",
+ PreferredUsernameAttribute: "preferredUsername",
+ EmailAttribute: "email",
+ EmailVerifiedAttribute: "emailVerified",
+ PhoneAttribute: "phone",
+ PhoneVerifiedAttribute: "phoneVerified",
+ PreferredLanguageAttribute: "preferredLanguage",
+ AvatarURLAttribute: "avatarURL",
+ ProfileAttribute: "profile",
+ },
+ idp.Options{
+ IsCreationAllowed: true,
+ IsLinkingAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ },
+ )),
+ },
+ uniqueConstraintsFromEventConstraintWithInstanceID("instance1", idpconfig.NewAddIDPConfigNameUniqueConstraint("name", "instance1")),
+ ),
+ ),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ Port: "port",
+ TLS: true,
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ Admin: "admin",
+ Password: "password",
+ LDAPAttributes: idp.LDAPAttributes{
+ IDAttribute: "id",
+ FirstNameAttribute: "firstName",
+ LastNameAttribute: "lastName",
+ DisplayNameAttribute: "displayName",
+ NickNameAttribute: "nickName",
+ PreferredUsernameAttribute: "preferredUsername",
+ EmailAttribute: "email",
+ EmailVerifiedAttribute: "emailVerified",
+ PhoneAttribute: "phone",
+ PhoneVerifiedAttribute: "phoneVerified",
+ PreferredLanguageAttribute: "preferredLanguage",
+ AvatarURLAttribute: "avatarURL",
+ ProfileAttribute: "profile",
+ },
+ 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.AddInstanceLDAPProvider(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_UpdateInstanceLDAPIDP(t *testing.T) {
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ secretCrypto crypto.EncryptionAlgorithm
+ }
+ type args struct {
+ ctx context.Context
+ id string
+ provider LDAPProvider
+ }
+ 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: LDAPProvider{},
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid name",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ id: "id1",
+ provider: LDAPProvider{},
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid host",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid baseDN",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid userObjectClass",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid userUniqueAttribute",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid admin",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ name: "not found",
+ fields: fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ Admin: "admin",
+ },
+ },
+ res: res{
+ err: caos_errors.IsNotFound,
+ },
+ },
+ {
+ name: "no changes",
+ fields: fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ instance.NewLDAPIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
+ "id1",
+ "name",
+ "host",
+ "",
+ false,
+ "baseDN",
+ "userObjectClass",
+ "userUniqueAttribute",
+ "admin",
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("password"),
+ },
+ idp.LDAPAttributes{},
+ idp.Options{},
+ )),
+ ),
+ ),
+ },
+ args: args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ Admin: "admin",
+ },
+ },
+ res: res{
+ want: &domain.ObjectDetails{},
+ },
+ },
+ {
+ name: "change ok",
+ fields: fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ instance.NewLDAPIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
+ "id1",
+ "name",
+ "host",
+ "port",
+ false,
+ "baseDN",
+ "userObjectClass",
+ "userUniqueAttribute",
+ "admin",
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("password"),
+ },
+ idp.LDAPAttributes{},
+ idp.Options{},
+ )),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID(
+ "instance1",
+ func() eventstore.Command {
+ t := true
+ event, _ := instance.NewLDAPIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
+ "id1",
+ "name",
+ []idp.LDAPIDPChanges{
+ idp.ChangeLDAPName("new name"),
+ idp.ChangeLDAPHost("new host"),
+ idp.ChangeLDAPPort("new port"),
+ idp.ChangeLDAPTLS(true),
+ idp.ChangeLDAPBaseDN("new baseDN"),
+ idp.ChangeLDAPUserObjectClass("new userObjectClass"),
+ idp.ChangeLDAPUserUniqueAttribute("new userUniqueAttribute"),
+ idp.ChangeLDAPAdmin("new admin"),
+ idp.ChangeLDAPPassword(&crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("new password"),
+ }),
+ idp.ChangeLDAPAttributes(idp.LDAPAttributeChanges{
+ IDAttribute: stringPointer("new id"),
+ FirstNameAttribute: stringPointer("new firstName"),
+ LastNameAttribute: stringPointer("new lastName"),
+ DisplayNameAttribute: stringPointer("new displayName"),
+ NickNameAttribute: stringPointer("new nickName"),
+ PreferredUsernameAttribute: stringPointer("new preferredUsername"),
+ EmailAttribute: stringPointer("new email"),
+ EmailVerifiedAttribute: stringPointer("new emailVerified"),
+ PhoneAttribute: stringPointer("new phone"),
+ PhoneVerifiedAttribute: stringPointer("new phoneVerified"),
+ PreferredLanguageAttribute: stringPointer("new preferredLanguage"),
+ AvatarURLAttribute: stringPointer("new avatarURL"),
+ ProfileAttribute: stringPointer("new profile"),
+ }),
+ idp.ChangeLDAPOptions(idp.OptionChanges{
+ IsCreationAllowed: &t,
+ IsLinkingAllowed: &t,
+ IsAutoCreation: &t,
+ IsAutoUpdate: &t,
+ }),
+ },
+ )
+ return event
+ }(),
+ ),
+ },
+ uniqueConstraintsFromEventConstraintWithInstanceID("instance1", idpconfig.NewRemoveIDPConfigNameUniqueConstraint("name", "instance1")),
+ uniqueConstraintsFromEventConstraintWithInstanceID("instance1", idpconfig.NewAddIDPConfigNameUniqueConstraint("new name", "instance1")),
+ ),
+ ),
+ secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: authz.WithInstanceID(context.Background(), "instance1"),
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "new name",
+ Host: "new host",
+ Port: "new port",
+ TLS: true,
+ BaseDN: "new baseDN",
+ UserObjectClass: "new userObjectClass",
+ UserUniqueAttribute: "new userUniqueAttribute",
+ Admin: "new admin",
+ Password: "new password",
+ LDAPAttributes: idp.LDAPAttributes{
+ IDAttribute: "new id",
+ FirstNameAttribute: "new firstName",
+ LastNameAttribute: "new lastName",
+ DisplayNameAttribute: "new displayName",
+ NickNameAttribute: "new nickName",
+ PreferredUsernameAttribute: "new preferredUsername",
+ EmailAttribute: "new email",
+ EmailVerifiedAttribute: "new emailVerified",
+ PhoneAttribute: "new phone",
+ PhoneVerifiedAttribute: "new phoneVerified",
+ PreferredLanguageAttribute: "new preferredLanguage",
+ AvatarURLAttribute: "new avatarURL",
+ ProfileAttribute: "new profile",
+ },
+ 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.UpdateInstanceLDAPProvider(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)
+ }
+ })
+ }
+}
diff --git a/internal/command/org_idp.go b/internal/command/org_idp.go
new file mode 100644
index 000000000..2935b5731
--- /dev/null
+++ b/internal/command/org_idp.go
@@ -0,0 +1,204 @@
+package command
+
+import (
+ "context"
+ "strings"
+
+ "github.com/zitadel/zitadel/internal/command/preparation"
+ "github.com/zitadel/zitadel/internal/crypto"
+ "github.com/zitadel/zitadel/internal/domain"
+ caos_errs "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/repository/org"
+)
+
+func (c *Commands) AddOrgLDAPProvider(ctx context.Context, resourceOwner string, provider LDAPProvider) (string, *domain.ObjectDetails, error) {
+ orgAgg := org.NewAggregate(resourceOwner)
+ id, err := c.idGenerator.Next()
+ if err != nil {
+ return "", nil, err
+ }
+ cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddOrgLDAPProvider(orgAgg, resourceOwner, id, 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) UpdateOrgLDAPProvider(ctx context.Context, resourceOwner, id string, provider LDAPProvider) (*domain.ObjectDetails, error) {
+ orgAgg := org.NewAggregate(resourceOwner)
+ cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateOrgLDAPProvider(orgAgg, resourceOwner, id, provider))
+ if err != nil {
+ return nil, err
+ }
+ if len(cmds) == 0 {
+ // no change, so return directly
+ return &domain.ObjectDetails{}, nil
+ }
+ pushedEvents, err := c.eventstore.Push(ctx, cmds...)
+ if err != nil {
+ return nil, err
+ }
+ return pushedEventsToObjectDetails(pushedEvents), nil
+}
+
+func (c *Commands) DeleteOrgProvider(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) {
+ orgAgg := org.NewAggregate(resourceOwner)
+ cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareDeleteOrgProvider(orgAgg, resourceOwner, id))
+ if err != nil {
+ return nil, err
+ }
+ pushedEvents, err := c.eventstore.Push(ctx, cmds...)
+ if err != nil {
+ return nil, err
+ }
+ return pushedEventsToObjectDetails(pushedEvents), nil
+}
+
+func (c *Commands) prepareAddOrgLDAPProvider(a *org.Aggregate, resourceOwner, id string, provider LDAPProvider) preparation.Validation {
+ return func() (preparation.CreateCommands, error) {
+ if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SAfdd", "Errors.Invalid.Argument")
+ }
+ if provider.Host = strings.TrimSpace(provider.Host); provider.Host == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SDVg2", "Errors.Invalid.Argument")
+ }
+ if provider.BaseDN = strings.TrimSpace(provider.BaseDN); provider.BaseDN == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-sv31s", "Errors.Invalid.Argument")
+ }
+ if provider.UserObjectClass = strings.TrimSpace(provider.UserObjectClass); provider.UserObjectClass == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-sdgf4", "Errors.Invalid.Argument")
+ }
+ if provider.UserUniqueAttribute = strings.TrimSpace(provider.UserUniqueAttribute); provider.UserUniqueAttribute == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-AEG2w", "Errors.Invalid.Argument")
+ }
+ if provider.Admin = strings.TrimSpace(provider.Admin); provider.Admin == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SAD5n", "Errors.Invalid.Argument")
+ }
+ if provider.Password = strings.TrimSpace(provider.Password); provider.Password == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-sdf5h", "Errors.Invalid.Argument")
+ }
+ return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
+ writeModel := NewLDAPOrgIDPWriteModel(resourceOwner, id)
+ events, err := filter(ctx, writeModel.Query())
+ if err != nil {
+ return nil, err
+ }
+ writeModel.AppendEvents(events...)
+ if err = writeModel.Reduce(); err != nil {
+ return nil, err
+ }
+ secret, err := crypto.Encrypt([]byte(provider.Password), c.idpConfigEncryption)
+ if err != nil {
+ return nil, err
+ }
+ return []eventstore.Command{
+ org.NewLDAPIDPAddedEvent(
+ ctx,
+ &a.Aggregate,
+ id,
+ provider.Name,
+ provider.Host,
+ provider.Port,
+ provider.TLS,
+ provider.BaseDN,
+ provider.UserObjectClass,
+ provider.UserUniqueAttribute,
+ provider.Admin,
+ secret,
+ provider.LDAPAttributes,
+ provider.IDPOptions,
+ ),
+ }, nil
+ }, nil
+ }
+}
+
+func (c *Commands) prepareUpdateOrgLDAPProvider(a *org.Aggregate, resourceOwner, id string, provider LDAPProvider) preparation.Validation {
+ return func() (preparation.CreateCommands, error) {
+ if id = strings.TrimSpace(id); id == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Dgdbs", "Errors.Invalid.Argument")
+ }
+ if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Sffgd", "Errors.Invalid.Argument")
+ }
+ if provider.Host = strings.TrimSpace(provider.Host); provider.Host == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Dz62d", "Errors.Invalid.Argument")
+ }
+ if provider.BaseDN = strings.TrimSpace(provider.BaseDN); provider.BaseDN == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-vb3ss", "Errors.Invalid.Argument")
+ }
+ if provider.UserObjectClass = strings.TrimSpace(provider.UserObjectClass); provider.UserObjectClass == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-hbere", "Errors.Invalid.Argument")
+ }
+ if provider.UserUniqueAttribute = strings.TrimSpace(provider.UserUniqueAttribute); provider.UserUniqueAttribute == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-ASFt6", "Errors.Invalid.Argument")
+ }
+ if provider.Admin = strings.TrimSpace(provider.Admin); provider.Admin == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-DG45z", "Errors.Invalid.Argument")
+ }
+ return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
+ writeModel := NewLDAPOrgIDPWriteModel(resourceOwner, id)
+ 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, "ORG-ASF3F", "Errors.Org.IDPConfig.NotExisting")
+ }
+ event, err := writeModel.NewChangedEvent(
+ ctx,
+ &a.Aggregate,
+ id,
+ writeModel.Name,
+ provider.Name,
+ provider.Host,
+ provider.Port,
+ provider.TLS,
+ provider.BaseDN,
+ provider.UserObjectClass,
+ provider.UserUniqueAttribute,
+ provider.Admin,
+ provider.Password,
+ c.idpConfigEncryption,
+ provider.LDAPAttributes,
+ provider.IDPOptions,
+ )
+ if err != nil {
+ return nil, err
+ }
+ if event == nil {
+ return nil, nil
+ }
+ return []eventstore.Command{event}, nil
+ }, nil
+ }
+}
+
+func (c *Commands) prepareDeleteOrgProvider(a *org.Aggregate, resourceOwner, id string) preparation.Validation {
+ return func() (preparation.CreateCommands, error) {
+ return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
+ writeModel := NewOrgIDPRemoveWriteModel(resourceOwner, id)
+ 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, "ORG-Se3tg", "Errors.Org.IDPConfig.NotExisting")
+ }
+ return []eventstore.Command{org.NewIDPRemovedEvent(ctx, &a.Aggregate, id, writeModel.name)}, nil
+ }, nil
+ }
+}
diff --git a/internal/command/org_idp_model.go b/internal/command/org_idp_model.go
new file mode 100644
index 000000000..4ae982de1
--- /dev/null
+++ b/internal/command/org_idp_model.go
@@ -0,0 +1,163 @@
+package command
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/crypto"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/repository/idp"
+ "github.com/zitadel/zitadel/internal/repository/org"
+)
+
+type OrgLDAPIDPWriteModel struct {
+ LDAPIDPWriteModel
+}
+
+func NewLDAPOrgIDPWriteModel(orgID, id string) *OrgLDAPIDPWriteModel {
+ return &OrgLDAPIDPWriteModel{
+ LDAPIDPWriteModel{
+ WriteModel: eventstore.WriteModel{
+ AggregateID: orgID,
+ ResourceOwner: orgID,
+ },
+ ID: id,
+ },
+ }
+}
+
+func (wm *OrgLDAPIDPWriteModel) Reduce() error {
+ return wm.LDAPIDPWriteModel.Reduce()
+}
+
+func (wm *OrgLDAPIDPWriteModel) AppendEvents(events ...eventstore.Event) {
+ for _, event := range events {
+ switch e := event.(type) {
+ case *org.LDAPIDPAddedEvent:
+ if wm.ID != e.ID {
+ continue
+ }
+ wm.LDAPIDPWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
+ case *org.LDAPIDPChangedEvent:
+ if wm.ID != e.ID {
+ continue
+ }
+ wm.LDAPIDPWriteModel.AppendEvents(&e.LDAPIDPChangedEvent)
+ case *org.IDPRemovedEvent:
+ if wm.ID != e.ID {
+ continue
+ }
+ wm.LDAPIDPWriteModel.AppendEvents(&e.RemovedEvent)
+ default:
+ wm.LDAPIDPWriteModel.AppendEvents(e)
+ }
+ }
+}
+
+func (wm *OrgLDAPIDPWriteModel) Query() *eventstore.SearchQueryBuilder {
+ return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ ResourceOwner(wm.ResourceOwner).
+ AddQuery().
+ AggregateTypes(org.AggregateType).
+ AggregateIDs(wm.AggregateID).
+ EventTypes(
+ org.LDAPIDPAddedEventType,
+ org.LDAPIDPChangedEventType,
+ org.IDPRemovedEventType,
+ ).
+ Builder()
+}
+
+func (wm *OrgLDAPIDPWriteModel) NewChangedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ id,
+ oldName,
+ name,
+ host,
+ port string,
+ tls bool,
+ baseDN,
+ userObjectClass,
+ userUniqueAttribute,
+ admin string,
+ password string,
+ secretCrypto crypto.Crypto,
+ attributes idp.LDAPAttributes,
+ options idp.Options,
+) (*org.LDAPIDPChangedEvent, error) {
+
+ changes, err := wm.LDAPIDPWriteModel.NewChanges(
+ name,
+ host,
+ port,
+ tls,
+ baseDN,
+ userObjectClass,
+ userUniqueAttribute,
+ admin,
+ password,
+ secretCrypto,
+ attributes,
+ options,
+ )
+ if err != nil {
+ return nil, err
+ }
+ if len(changes) == 0 {
+ return nil, nil
+ }
+ changeEvent, err := org.NewLDAPIDPChangedEvent(ctx, aggregate, id, oldName, changes)
+ if err != nil {
+ return nil, err
+ }
+ return changeEvent, nil
+}
+
+type OrgIDPRemoveWriteModel struct {
+ IDPRemoveWriteModel
+}
+
+func NewOrgIDPRemoveWriteModel(orgID, id string) *OrgIDPRemoveWriteModel {
+ return &OrgIDPRemoveWriteModel{
+ IDPRemoveWriteModel{
+ WriteModel: eventstore.WriteModel{
+ AggregateID: orgID,
+ ResourceOwner: orgID,
+ },
+ ID: id,
+ },
+ }
+}
+
+func (wm *OrgIDPRemoveWriteModel) Reduce() error {
+ return wm.IDPRemoveWriteModel.Reduce()
+}
+
+func (wm *OrgIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) {
+ for _, event := range events {
+ switch e := event.(type) {
+ case *org.LDAPIDPAddedEvent:
+ wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
+ case *org.LDAPIDPChangedEvent:
+ wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPChangedEvent)
+ case *org.IDPRemovedEvent:
+ wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent)
+ default:
+ wm.IDPRemoveWriteModel.AppendEvents(e)
+ }
+ }
+}
+
+func (wm *OrgIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder {
+ return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ ResourceOwner(wm.ResourceOwner).
+ AddQuery().
+ AggregateTypes(org.AggregateType).
+ AggregateIDs(wm.AggregateID).
+ EventTypes(
+ org.LDAPIDPAddedEventType,
+ org.LDAPIDPChangedEventType,
+ org.IDPRemovedEventType,
+ ).
+ Builder()
+}
diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go
new file mode 100644
index 000000000..e8e1bfc88
--- /dev/null
+++ b/internal/command/org_idp_test.go
@@ -0,0 +1,698 @@
+package command
+
+import (
+ "context"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/zitadel/zitadel/internal/crypto"
+ "github.com/zitadel/zitadel/internal/domain"
+ caos_errors "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/id"
+ id_mock "github.com/zitadel/zitadel/internal/id/mock"
+ "github.com/zitadel/zitadel/internal/repository/idp"
+ "github.com/zitadel/zitadel/internal/repository/idpconfig"
+ "github.com/zitadel/zitadel/internal/repository/org"
+)
+
+func TestCommandSide_AddOrgLDAPIDP(t *testing.T) {
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ idGenerator id.Generator
+ secretCrypto crypto.EncryptionAlgorithm
+ }
+ type args struct {
+ ctx context.Context
+ resourceOwner string
+ provider LDAPProvider
+ }
+ type res struct {
+ id string
+ want *domain.ObjectDetails
+ err func(error) bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ "invalid name",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ provider: LDAPProvider{},
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid host",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ provider: LDAPProvider{
+ Name: "name",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid baseDN",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid userObjectClass",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid userUniqueAttribute",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid admin",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid password",
+ fields{
+ eventstore: eventstoreExpect(t),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ },
+ args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ Admin: "admin",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ name: "ok",
+ fields: fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(),
+ expectPush(
+ eventPusherToEvents(
+ org.NewLDAPIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
+ "id1",
+ "name",
+ "host",
+ "",
+ false,
+ "baseDN",
+ "userObjectClass",
+ "userUniqueAttribute",
+ "admin",
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("password"),
+ },
+ idp.LDAPAttributes{},
+ idp.Options{},
+ )),
+ uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name", "org1")),
+ ),
+ ),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ Admin: "admin",
+ Password: "password",
+ },
+ },
+ res: res{
+ id: "id1",
+ want: &domain.ObjectDetails{ResourceOwner: "org1"},
+ },
+ },
+ {
+ name: "ok all set",
+ fields: fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(),
+ expectPush(
+ eventPusherToEvents(
+ org.NewLDAPIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
+ "id1",
+ "name",
+ "host",
+ "port",
+ true,
+ "baseDN",
+ "userObjectClass",
+ "userUniqueAttribute",
+ "admin",
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("password"),
+ },
+ idp.LDAPAttributes{
+ IDAttribute: "id",
+ FirstNameAttribute: "firstName",
+ LastNameAttribute: "lastName",
+ DisplayNameAttribute: "displayName",
+ NickNameAttribute: "nickName",
+ PreferredUsernameAttribute: "preferredUsername",
+ EmailAttribute: "email",
+ EmailVerifiedAttribute: "emailVerified",
+ PhoneAttribute: "phone",
+ PhoneVerifiedAttribute: "phoneVerified",
+ PreferredLanguageAttribute: "preferredLanguage",
+ AvatarURLAttribute: "avatarURL",
+ ProfileAttribute: "profile",
+ },
+ idp.Options{
+ IsCreationAllowed: true,
+ IsLinkingAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ },
+ )),
+ uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name", "org1")),
+ ),
+ ),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
+ secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ Port: "port",
+ TLS: true,
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ Admin: "admin",
+ Password: "password",
+ LDAPAttributes: idp.LDAPAttributes{
+ IDAttribute: "id",
+ FirstNameAttribute: "firstName",
+ LastNameAttribute: "lastName",
+ DisplayNameAttribute: "displayName",
+ NickNameAttribute: "nickName",
+ PreferredUsernameAttribute: "preferredUsername",
+ EmailAttribute: "email",
+ EmailVerifiedAttribute: "emailVerified",
+ PhoneAttribute: "phone",
+ PhoneVerifiedAttribute: "phoneVerified",
+ PreferredLanguageAttribute: "preferredLanguage",
+ AvatarURLAttribute: "avatarURL",
+ ProfileAttribute: "profile",
+ },
+ IDPOptions: idp.Options{
+ IsCreationAllowed: true,
+ IsLinkingAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ },
+ },
+ },
+ res: res{
+ id: "id1",
+ want: &domain.ObjectDetails{ResourceOwner: "org1"},
+ },
+ },
+ }
+ 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.AddOrgLDAPProvider(tt.args.ctx, tt.args.resourceOwner, 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_UpdateOrgLDAPIDP(t *testing.T) {
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ secretCrypto crypto.EncryptionAlgorithm
+ }
+ type args struct {
+ ctx context.Context
+ resourceOwner string
+ id string
+ provider LDAPProvider
+ }
+ 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: context.Background(),
+ resourceOwner: "org1",
+ provider: LDAPProvider{},
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid name",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ id: "id1",
+ provider: LDAPProvider{},
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid host",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid baseDN",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid userObjectClass",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid userUniqueAttribute",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ "invalid admin",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ },
+ },
+ res{
+ err: caos_errors.IsErrorInvalidArgument,
+ },
+ },
+ {
+ name: "not found",
+ fields: fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ Admin: "admin",
+ },
+ },
+ res: res{
+ err: caos_errors.IsNotFound,
+ },
+ },
+ {
+ name: "no changes",
+ fields: fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ org.NewLDAPIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
+ "id1",
+ "name",
+ "host",
+ "",
+ false,
+ "baseDN",
+ "userObjectClass",
+ "userUniqueAttribute",
+ "admin",
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("password"),
+ },
+ idp.LDAPAttributes{},
+ idp.Options{},
+ )),
+ ),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "name",
+ Host: "host",
+ BaseDN: "baseDN",
+ UserObjectClass: "userObjectClass",
+ UserUniqueAttribute: "userUniqueAttribute",
+ Admin: "admin",
+ },
+ },
+ res: res{
+ want: &domain.ObjectDetails{},
+ },
+ },
+ {
+ name: "change ok",
+ fields: fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ org.NewLDAPIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
+ "id1",
+ "name",
+ "host",
+ "port",
+ false,
+ "baseDN",
+ "userObjectClass",
+ "userUniqueAttribute",
+ "admin",
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("password"),
+ },
+ idp.LDAPAttributes{},
+ idp.Options{},
+ )),
+ ),
+ expectPush(
+ eventPusherToEvents(
+ func() eventstore.Command {
+ t := true
+ event, _ := org.NewLDAPIDPChangedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
+ "id1",
+ "name",
+ []idp.LDAPIDPChanges{
+ idp.ChangeLDAPName("new name"),
+ idp.ChangeLDAPHost("new host"),
+ idp.ChangeLDAPPort("new port"),
+ idp.ChangeLDAPTLS(true),
+ idp.ChangeLDAPBaseDN("new baseDN"),
+ idp.ChangeLDAPUserObjectClass("new userObjectClass"),
+ idp.ChangeLDAPUserUniqueAttribute("new userUniqueAttribute"),
+ idp.ChangeLDAPAdmin("new admin"),
+ idp.ChangeLDAPPassword(&crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("new password"),
+ }),
+ idp.ChangeLDAPAttributes(idp.LDAPAttributeChanges{
+ IDAttribute: stringPointer("new id"),
+ FirstNameAttribute: stringPointer("new firstName"),
+ LastNameAttribute: stringPointer("new lastName"),
+ DisplayNameAttribute: stringPointer("new displayName"),
+ NickNameAttribute: stringPointer("new nickName"),
+ PreferredUsernameAttribute: stringPointer("new preferredUsername"),
+ EmailAttribute: stringPointer("new email"),
+ EmailVerifiedAttribute: stringPointer("new emailVerified"),
+ PhoneAttribute: stringPointer("new phone"),
+ PhoneVerifiedAttribute: stringPointer("new phoneVerified"),
+ PreferredLanguageAttribute: stringPointer("new preferredLanguage"),
+ AvatarURLAttribute: stringPointer("new avatarURL"),
+ ProfileAttribute: stringPointer("new profile"),
+ }),
+ idp.ChangeLDAPOptions(idp.OptionChanges{
+ IsCreationAllowed: &t,
+ IsLinkingAllowed: &t,
+ IsAutoCreation: &t,
+ IsAutoUpdate: &t,
+ }),
+ },
+ )
+ return event
+ }(),
+ ),
+ uniqueConstraintsFromEventConstraint(idpconfig.NewRemoveIDPConfigNameUniqueConstraint("name", "org1")),
+ uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("new name", "org1")),
+ ),
+ ),
+ secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: context.Background(),
+ resourceOwner: "org1",
+ id: "id1",
+ provider: LDAPProvider{
+ Name: "new name",
+ Host: "new host",
+ Port: "new port",
+ TLS: true,
+ BaseDN: "new baseDN",
+ UserObjectClass: "new userObjectClass",
+ UserUniqueAttribute: "new userUniqueAttribute",
+ Admin: "new admin",
+ Password: "new password",
+ LDAPAttributes: idp.LDAPAttributes{
+ IDAttribute: "new id",
+ FirstNameAttribute: "new firstName",
+ LastNameAttribute: "new lastName",
+ DisplayNameAttribute: "new displayName",
+ NickNameAttribute: "new nickName",
+ PreferredUsernameAttribute: "new preferredUsername",
+ EmailAttribute: "new email",
+ EmailVerifiedAttribute: "new emailVerified",
+ PhoneAttribute: "new phone",
+ PhoneVerifiedAttribute: "new phoneVerified",
+ PreferredLanguageAttribute: "new preferredLanguage",
+ AvatarURLAttribute: "new avatarURL",
+ ProfileAttribute: "new profile",
+ },
+ IDPOptions: idp.Options{
+ IsCreationAllowed: true,
+ IsLinkingAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ },
+ },
+ },
+ res: res{
+ want: &domain.ObjectDetails{ResourceOwner: "org1"},
+ },
+ },
+ }
+ 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.UpdateOrgLDAPProvider(tt.args.ctx, tt.args.resourceOwner, 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)
+ }
+ })
+ }
+}
+
+func stringPointer(s string) *string {
+ return &s
+}
diff --git a/internal/domain/idp.go b/internal/domain/idp.go
new file mode 100644
index 000000000..9df87f6ba
--- /dev/null
+++ b/internal/domain/idp.go
@@ -0,0 +1,36 @@
+package domain
+
+type IDPState int32
+
+const (
+ IDPStateUnspecified IDPState = iota
+ IDPStateActive
+ IDPStateInactive
+ IDPStateRemoved
+
+ idpStateCount
+)
+
+func (s IDPState) Valid() bool {
+ return s >= 0 && s < idpStateCount
+}
+
+func (s IDPState) Exists() bool {
+ return s != IDPStateUnspecified && s != IDPStateRemoved
+}
+
+type IDPType int32
+
+const (
+ IDPTypeUnspecified IDPType = iota
+ IDPTypeOIDC
+ IDPTypeJWT
+ IDPTypeOAuth
+ IDPTypeLDAP
+ IDPTypeAzureAD
+ IDPTypeGitHub
+ IDPTypeGitHubEE
+ IDPTypeGitLab
+ IDPTypeGitLabSelfHosted
+ IDPTypeGoogle
+)
diff --git a/internal/idp/providers/ldap/ldap.go b/internal/idp/providers/ldap/ldap.go
new file mode 100644
index 000000000..5fc14f60d
--- /dev/null
+++ b/internal/idp/providers/ldap/ldap.go
@@ -0,0 +1,237 @@
+package ldap
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/idp"
+)
+
+const DefaultPort = "389"
+
+var _ idp.Provider = (*Provider)(nil)
+
+// Provider is the [idp.Provider] implementation for a generic LDAP provider
+type Provider struct {
+ name string
+ host string
+ port string
+ tls bool
+ baseDN string
+ userObjectClass string
+ userUniqueAttribute string
+ admin string
+ password string
+ loginUrl string
+
+ isLinkingAllowed bool
+ isCreationAllowed bool
+ isAutoCreation bool
+ isAutoUpdate bool
+
+ idAttribute string
+ firstNameAttribute string
+ lastNameAttribute string
+ displayNameAttribute string
+ nickNameAttribute string
+ preferredUsernameAttribute string
+ emailAttribute string
+ emailVerifiedAttribute string
+ phoneAttribute string
+ phoneVerifiedAttribute string
+ preferredLanguageAttribute string
+ avatarURLAttribute string
+ profileAttribute string
+}
+
+type ProviderOpts func(provider *Provider)
+
+// WithLinkingAllowed allows end users to link the federated user to an existing one.
+func WithLinkingAllowed() ProviderOpts {
+ return func(p *Provider) {
+ p.isLinkingAllowed = true
+ }
+}
+
+// WithCreationAllowed allows end users to create a new user using the federated information.
+func WithCreationAllowed() ProviderOpts {
+ return func(p *Provider) {
+ p.isCreationAllowed = true
+ }
+}
+
+// WithAutoCreation enables that federated users are automatically created if not already existing.
+func WithAutoCreation() ProviderOpts {
+ return func(p *Provider) {
+ p.isAutoCreation = true
+ }
+}
+
+// WithAutoUpdate enables that information retrieved from the provider is automatically used to update
+// the existing user on each authentication.
+func WithAutoUpdate() ProviderOpts {
+ return func(p *Provider) {
+ p.isAutoUpdate = true
+ }
+}
+
+// WithCustomPort configures a custom port used for the communication instead of :389 as per default
+func WithCustomPort(port string) ProviderOpts {
+ return func(p *Provider) {
+ p.port = port
+ }
+}
+
+// Insecure configures to communication insecure with the LDAP server without TLS
+func Insecure() ProviderOpts {
+ return func(p *Provider) {
+ p.tls = false
+ }
+}
+
+// WithCustomIDAttribute configures to map the LDAP attribute to the user, default is the uniqueUserAttribute
+func WithCustomIDAttribute(name string) ProviderOpts {
+ return func(p *Provider) {
+ p.idAttribute = name
+ }
+}
+
+// WithFirstNameAttribute configures to map the LDAP attribute to the user
+func WithFirstNameAttribute(name string) ProviderOpts {
+ return func(p *Provider) {
+ p.firstNameAttribute = name
+ }
+}
+
+// WithLastNameAttribute configures to map the LDAP attribute to the user
+func WithLastNameAttribute(name string) ProviderOpts {
+ return func(p *Provider) {
+ p.lastNameAttribute = name
+ }
+}
+
+// WithDisplayNameAttribute configures to map the LDAP attribute to the user
+func WithDisplayNameAttribute(name string) ProviderOpts {
+ return func(p *Provider) {
+ p.displayNameAttribute = name
+ }
+}
+
+// WithNickNameAttribute configures to map the LDAP attribute to the user
+func WithNickNameAttribute(name string) ProviderOpts {
+ return func(p *Provider) {
+ p.nickNameAttribute = name
+ }
+}
+
+// WithPreferredUsernameAttribute configures to map the LDAP attribute to the user
+func WithPreferredUsernameAttribute(name string) ProviderOpts {
+ return func(p *Provider) {
+ p.preferredUsernameAttribute = name
+ }
+}
+
+// WithEmailAttribute configures to map the LDAP attribute to the user
+func WithEmailAttribute(name string) ProviderOpts {
+ return func(p *Provider) {
+ p.emailAttribute = name
+ }
+}
+
+// WithEmailVerifiedAttribute configures to map the LDAP attribute to the user
+func WithEmailVerifiedAttribute(name string) ProviderOpts {
+ return func(p *Provider) {
+ p.emailVerifiedAttribute = name
+ }
+}
+
+// WithPhoneAttribute configures to map the LDAP attribute to the user
+func WithPhoneAttribute(name string) ProviderOpts {
+ return func(p *Provider) {
+ p.phoneAttribute = name
+ }
+}
+
+// WithPhoneVerifiedAttribute configures to map the LDAP attribute to the user
+func WithPhoneVerifiedAttribute(name string) ProviderOpts {
+ return func(p *Provider) {
+ p.phoneVerifiedAttribute = name
+ }
+}
+
+// WithPreferredLanguageAttribute configures to map the LDAP attribute to the user
+func WithPreferredLanguageAttribute(name string) ProviderOpts {
+ return func(p *Provider) {
+ p.preferredLanguageAttribute = name
+ }
+}
+
+// WithAvatarURLAttribute configures to map the LDAP attribute to the user
+func WithAvatarURLAttribute(name string) ProviderOpts {
+ return func(p *Provider) {
+ p.avatarURLAttribute = name
+ }
+}
+
+// WithProfileAttribute configures to map the LDAP attribute to the user
+func WithProfileAttribute(name string) ProviderOpts {
+ return func(p *Provider) {
+ p.profileAttribute = name
+ }
+}
+
+func New(
+ name string,
+ host string,
+ baseDN string,
+ userObjectClass string,
+ userUniqueAttribute string,
+ admin string,
+ password string,
+ loginUrl string,
+ options ...ProviderOpts,
+) *Provider {
+ provider := &Provider{
+ name: name,
+ host: host,
+ port: DefaultPort,
+ tls: true,
+ baseDN: baseDN,
+ userObjectClass: userObjectClass,
+ userUniqueAttribute: userUniqueAttribute,
+ admin: admin,
+ password: password,
+ loginUrl: loginUrl,
+ idAttribute: userUniqueAttribute,
+ }
+ for _, option := range options {
+ option(provider)
+ }
+ return provider
+}
+
+func (p *Provider) Name() string {
+ return p.name
+}
+
+func (p *Provider) BeginAuth(ctx context.Context, state string, params ...any) (idp.Session, error) {
+ return &Session{
+ Provider: p,
+ loginUrl: p.loginUrl + "?state=" + state,
+ }, nil
+}
+
+func (p *Provider) IsLinkingAllowed() bool {
+ return p.isLinkingAllowed
+}
+
+func (p *Provider) IsCreationAllowed() bool {
+ return p.isCreationAllowed
+}
+
+func (p *Provider) IsAutoCreation() bool {
+ return p.isAutoCreation
+}
+
+func (p *Provider) IsAutoUpdate() bool {
+ return p.isAutoUpdate
+}
diff --git a/internal/idp/providers/ldap/ldap_test.go b/internal/idp/providers/ldap/ldap_test.go
new file mode 100644
index 000000000..6ec2790c9
--- /dev/null
+++ b/internal/idp/providers/ldap/ldap_test.go
@@ -0,0 +1,185 @@
+package ldap
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestProvider_Options(t *testing.T) {
+ type fields struct {
+ name string
+ host string
+ baseDN string
+ userObjectClass string
+ userUniqueAttribute string
+ admin string
+ password string
+ loginUrl string
+ opts []ProviderOpts
+ }
+ type want struct {
+ name string
+ port string
+ tls bool
+ linkingAllowed bool
+ creationAllowed bool
+ autoCreation bool
+ autoUpdate bool
+ idAttribute string
+ firstNameAttribute string
+ lastNameAttribute string
+ displayNameAttribute string
+ nickNameAttribute string
+ preferredUsernameAttribute string
+ emailAttribute string
+ emailVerifiedAttribute string
+ phoneAttribute string
+ phoneVerifiedAttribute string
+ preferredLanguageAttribute string
+ avatarURLAttribute string
+ profileAttribute string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want want
+ }{
+ {
+ name: "default",
+ fields: fields{
+ name: "ldap",
+ host: "host",
+ baseDN: "base",
+ userObjectClass: "class",
+ userUniqueAttribute: "attr",
+ admin: "admin",
+ password: "password",
+ loginUrl: "url",
+ opts: nil,
+ },
+ want: want{
+ name: "ldap",
+ port: DefaultPort,
+ tls: true,
+ linkingAllowed: false,
+ creationAllowed: false,
+ autoCreation: false,
+ autoUpdate: false,
+ idAttribute: "attr",
+ },
+ },
+ {
+ name: "all true",
+ fields: fields{
+ name: "ldap",
+ host: "host",
+ baseDN: "base",
+ userObjectClass: "class",
+ userUniqueAttribute: "attr",
+ admin: "admin",
+ password: "password",
+ loginUrl: "url",
+ opts: []ProviderOpts{
+ WithLinkingAllowed(),
+ WithCreationAllowed(),
+ WithAutoCreation(),
+ WithAutoUpdate(),
+ },
+ },
+ want: want{
+ name: "ldap",
+ port: DefaultPort,
+ tls: true,
+ linkingAllowed: true,
+ creationAllowed: true,
+ autoCreation: true,
+ autoUpdate: true,
+ idAttribute: "attr",
+ },
+ }, {
+ name: "all true, attributes set",
+ fields: fields{
+ name: "ldap",
+ host: "host",
+ baseDN: "base",
+ userObjectClass: "class",
+ userUniqueAttribute: "attr",
+ admin: "admin",
+ password: "password",
+ loginUrl: "url",
+ opts: []ProviderOpts{
+ Insecure(),
+ WithCustomPort("port"),
+ WithLinkingAllowed(),
+ WithCreationAllowed(),
+ WithAutoCreation(),
+ WithAutoUpdate(),
+ WithCustomIDAttribute("id"),
+ WithFirstNameAttribute("first"),
+ WithLastNameAttribute("last"),
+ WithDisplayNameAttribute("display"),
+ WithNickNameAttribute("nick"),
+ WithPreferredUsernameAttribute("prefUser"),
+ WithEmailAttribute("email"),
+ WithEmailVerifiedAttribute("emailVerified"),
+ WithPhoneAttribute("phone"),
+ WithPhoneVerifiedAttribute("phoneVerified"),
+ WithPreferredLanguageAttribute("prefLang"),
+ WithAvatarURLAttribute("avatar"),
+ WithProfileAttribute("profile"),
+ },
+ },
+ want: want{
+ name: "ldap",
+ port: "port",
+ tls: false,
+ linkingAllowed: true,
+ creationAllowed: true,
+ autoCreation: true,
+ autoUpdate: true,
+ idAttribute: "id",
+ firstNameAttribute: "first",
+ lastNameAttribute: "last",
+ displayNameAttribute: "display",
+ nickNameAttribute: "nick",
+ preferredUsernameAttribute: "prefUser",
+ emailAttribute: "email",
+ emailVerifiedAttribute: "emailVerified",
+ phoneAttribute: "phone",
+ phoneVerifiedAttribute: "phoneVerified",
+ preferredLanguageAttribute: "prefLang",
+ avatarURLAttribute: "avatar",
+ profileAttribute: "profile",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := assert.New(t)
+ provider := New(tt.fields.name, tt.fields.host, tt.fields.baseDN, tt.fields.userObjectClass, tt.fields.userUniqueAttribute, tt.fields.admin, tt.fields.password, tt.fields.loginUrl, tt.fields.opts...)
+
+ a.Equal(tt.want.name, provider.Name())
+ a.Equal(tt.want.port, provider.port)
+ a.Equal(tt.want.tls, provider.tls)
+ a.Equal(tt.want.linkingAllowed, provider.IsLinkingAllowed())
+ a.Equal(tt.want.creationAllowed, provider.IsCreationAllowed())
+ a.Equal(tt.want.autoCreation, provider.IsAutoCreation())
+ a.Equal(tt.want.autoUpdate, provider.IsAutoUpdate())
+
+ a.Equal(tt.want.idAttribute, provider.idAttribute)
+ a.Equal(tt.want.firstNameAttribute, provider.firstNameAttribute)
+ a.Equal(tt.want.lastNameAttribute, provider.lastNameAttribute)
+ a.Equal(tt.want.displayNameAttribute, provider.displayNameAttribute)
+ a.Equal(tt.want.nickNameAttribute, provider.nickNameAttribute)
+ a.Equal(tt.want.preferredUsernameAttribute, provider.preferredUsernameAttribute)
+ a.Equal(tt.want.emailAttribute, provider.emailAttribute)
+ a.Equal(tt.want.emailVerifiedAttribute, provider.emailVerifiedAttribute)
+ a.Equal(tt.want.phoneAttribute, provider.phoneAttribute)
+ a.Equal(tt.want.phoneVerifiedAttribute, provider.phoneVerifiedAttribute)
+ a.Equal(tt.want.preferredLanguageAttribute, provider.preferredLanguageAttribute)
+ a.Equal(tt.want.avatarURLAttribute, provider.avatarURLAttribute)
+ a.Equal(tt.want.profileAttribute, provider.profileAttribute)
+ })
+ }
+}
diff --git a/internal/idp/providers/ldap/session.go b/internal/idp/providers/ldap/session.go
new file mode 100644
index 000000000..d1491205c
--- /dev/null
+++ b/internal/idp/providers/ldap/session.go
@@ -0,0 +1,98 @@
+package ldap
+
+import (
+ "context"
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "strconv"
+
+ "github.com/go-ldap/ldap/v3"
+ "golang.org/x/text/language"
+
+ "github.com/zitadel/zitadel/internal/idp"
+)
+
+var ErrNoSingleUser = errors.New("user does not exist or too many entries returned")
+
+var _ idp.Session = (*Session)(nil)
+
+type Session struct {
+ Provider *Provider
+ loginUrl string
+ user string
+ password string
+}
+
+func (s *Session) GetAuthURL() string {
+ return s.loginUrl
+}
+func (s *Session) FetchUser(_ context.Context) (idp.User, error) {
+ l, err := ldap.DialURL("ldap://" + s.Provider.host + ":" + s.Provider.port)
+ if err != nil {
+ return nil, err
+ }
+ defer l.Close()
+
+ if s.Provider.tls {
+ err = l.StartTLS(&tls.Config{ServerName: s.Provider.host})
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Bind as the admin to search for user
+ err = l.Bind("cn="+s.Provider.admin+","+s.Provider.baseDN, s.Provider.password)
+ if err != nil {
+ return nil, err
+ }
+
+ // Search for user with the unique attribute for the userDN
+ searchRequest := ldap.NewSearchRequest(
+ s.Provider.baseDN,
+ ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
+ fmt.Sprintf("(&(objectClass="+s.Provider.userObjectClass+")("+s.Provider.userUniqueAttribute+"=%s))", ldap.EscapeFilter(s.user)),
+ []string{"dn"},
+ nil,
+ )
+
+ sr, err := l.Search(searchRequest)
+ if err != nil {
+ return nil, err
+ }
+ if len(sr.Entries) != 1 {
+ return nil, ErrNoSingleUser
+ }
+
+ user := sr.Entries[0]
+ // Bind as the user to verify their password
+ err = l.Bind(user.DN, s.password)
+ if err != nil {
+ return nil, err
+ }
+
+ emailVerified, err := strconv.ParseBool(user.GetAttributeValue(s.Provider.emailVerifiedAttribute))
+ if err != nil {
+ return nil, err
+ }
+ phoneVerified, err := strconv.ParseBool(user.GetAttributeValue(s.Provider.phoneVerifiedAttribute))
+ if err != nil {
+ return nil, err
+ }
+
+ return NewUser(
+ user.GetAttributeValue(s.Provider.idAttribute),
+ user.GetAttributeValue(s.Provider.firstNameAttribute),
+ user.GetAttributeValue(s.Provider.lastNameAttribute),
+ user.GetAttributeValue(s.Provider.displayNameAttribute),
+ user.GetAttributeValue(s.Provider.nickNameAttribute),
+ user.GetAttributeValue(s.Provider.preferredUsernameAttribute),
+ user.GetAttributeValue(s.Provider.emailAttribute),
+ emailVerified,
+ user.GetAttributeValue(s.Provider.phoneAttribute),
+ phoneVerified,
+ language.Make(user.GetAttributeValue(s.Provider.preferredLanguageAttribute)),
+ user.GetAttributeValue(s.Provider.avatarURLAttribute),
+ user.GetAttributeValue(s.Provider.profileAttribute),
+ ), nil
+}
diff --git a/internal/idp/providers/ldap/user.go b/internal/idp/providers/ldap/user.go
new file mode 100644
index 000000000..3577d88fa
--- /dev/null
+++ b/internal/idp/providers/ldap/user.go
@@ -0,0 +1,91 @@
+package ldap
+
+import "golang.org/x/text/language"
+
+type User struct {
+ id string
+ firstName string
+ lastName string
+ displayName string
+ nickName string
+ preferredUsername string
+ email string
+ emailVerified bool
+ phone string
+ phoneVerified bool
+ preferredLanguage language.Tag
+ avatarURL string
+ profile string
+}
+
+func NewUser(
+ id string,
+ firstName string,
+ lastName string,
+ displayName string,
+ nickName string,
+ preferredUsername string,
+ email string,
+ emailVerified bool,
+ phone string,
+ phoneVerified bool,
+ preferredLanguage language.Tag,
+ avatarURL string,
+ profile string,
+) *User {
+ return &User{
+ id,
+ firstName,
+ lastName,
+ displayName,
+ nickName,
+ preferredUsername,
+ email,
+ emailVerified,
+ phone,
+ phoneVerified,
+ preferredLanguage,
+ avatarURL,
+ profile,
+ }
+}
+
+func (u *User) GetID() string {
+ return u.id
+}
+func (u *User) GetFirstName() string {
+ return u.firstName
+}
+func (u *User) GetLastName() string {
+ return u.lastName
+}
+func (u *User) GetDisplayName() string {
+ return u.displayName
+}
+func (u *User) GetNickname() string {
+ return u.nickName
+}
+func (u *User) GetPreferredUsername() string {
+ return u.preferredUsername
+}
+func (u *User) GetEmail() string {
+ return u.email
+}
+func (u *User) IsEmailVerified() bool {
+ return u.emailVerified
+}
+func (u *User) GetPhone() string {
+ return u.phone
+}
+func (u *User) IsPhoneVerified() bool {
+ return u.phoneVerified
+}
+func (u *User) GetPreferredLanguage() language.Tag {
+ return u.preferredLanguage
+}
+func (u *User) GetAvatarURL() string {
+ return u.avatarURL
+}
+func (u *User) GetProfile() string {
+ return u.profile
+}
diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go
new file mode 100644
index 000000000..5340ae7ab
--- /dev/null
+++ b/internal/query/idp_template.go
@@ -0,0 +1,621 @@
+package query
+
+import (
+ "context"
+ "database/sql"
+ errs "errors"
+ "time"
+
+ sq "github.com/Masterminds/squirrel"
+ "github.com/zitadel/logging"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/crypto"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/query/projection"
+ "github.com/zitadel/zitadel/internal/repository/idp"
+ "github.com/zitadel/zitadel/internal/telemetry/tracing"
+)
+
+type IDPTemplate struct {
+ CreationDate time.Time
+ ChangeDate time.Time
+ Sequence uint64
+ ResourceOwner string
+ ID string
+ State domain.IDPState
+ Name string
+ Type domain.IDPType
+ OwnerType domain.IdentityProviderType
+ IsCreationAllowed bool
+ IsLinkingAllowed bool
+ IsAutoCreation bool
+ IsAutoUpdate bool
+ *LDAPIDPTemplate
+}
+
+type IDPTemplates struct {
+ SearchResponse
+ Templates []*IDPTemplate
+}
+
+type LDAPIDPTemplate struct {
+ IDPID string
+ Host string
+ Port string
+ TLS bool
+ BaseDN string
+ UserObjectClass string
+ UserUniqueAttribute string
+ Admin string
+ Password *crypto.CryptoValue
+ idp.LDAPAttributes
+ idp.Options
+}
+
+var (
+ idpTemplateTable = table{
+ name: projection.IDPTemplateTable,
+ instanceIDCol: projection.IDPTemplateInstanceIDCol,
+ }
+ IDPTemplateIDCol = Column{
+ name: projection.IDPTemplateIDCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateCreationDateCol = Column{
+ name: projection.IDPTemplateCreationDateCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateChangeDateCol = Column{
+ name: projection.IDPTemplateChangeDateCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateSequenceCol = Column{
+ name: projection.IDPTemplateSequenceCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateResourceOwnerCol = Column{
+ name: projection.IDPTemplateResourceOwnerCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateInstanceIDCol = Column{
+ name: projection.IDPTemplateInstanceIDCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateStateCol = Column{
+ name: projection.IDPTemplateStateCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateNameCol = Column{
+ name: projection.IDPTemplateNameCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateOwnerTypeCol = Column{
+ name: projection.IDPOwnerTypeCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateTypeCol = Column{
+ name: projection.IDPTemplateTypeCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateOwnerRemovedCol = Column{
+ name: projection.IDPTemplateOwnerRemovedCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateIsCreationAllowedCol = Column{
+ name: projection.IDPTemplateIsCreationAllowedCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateIsLinkingAllowedCol = Column{
+ name: projection.IDPTemplateIsLinkingAllowedCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateIsAutoCreationCol = Column{
+ name: projection.IDPTemplateIsAutoCreationCol,
+ table: idpTemplateTable,
+ }
+ IDPTemplateIsAutoUpdateCol = Column{
+ name: projection.IDPTemplateIsAutoUpdateCol,
+ table: idpTemplateTable,
+ }
+)
+
+var (
+ ldapIdpTemplateTable = table{
+ name: projection.IDPTemplateLDAPTable,
+ instanceIDCol: projection.IDPTemplateInstanceIDCol,
+ }
+ LDAPIDCol = Column{
+ name: projection.LDAPIDCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPInstanceIDCol = Column{
+ name: projection.LDAPInstanceIDCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPHostCol = Column{
+ name: projection.LDAPHostCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPPortCol = Column{
+ name: projection.LDAPPortCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPTlsCol = Column{
+ name: projection.LDAPTlsCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPBaseDNCol = Column{
+ name: projection.LDAPBaseDNCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPUserObjectClassCol = Column{
+ name: projection.LDAPUserObjectClassCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPUserUniqueAttributeCol = Column{
+ name: projection.LDAPUserUniqueAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPAdminCol = Column{
+ name: projection.LDAPAdminCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPPasswordCol = Column{
+ name: projection.LDAPPasswordCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPIDAttributeCol = Column{
+ name: projection.LDAPIDAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPFirstNameAttributeCol = Column{
+ name: projection.LDAPFirstNameAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPLastNameAttributeCol = Column{
+ name: projection.LDAPLastNameAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPDisplayNameAttributeCol = Column{
+ name: projection.LDAPDisplayNameAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPNickNameAttributeCol = Column{
+ name: projection.LDAPNickNameAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPPreferredUsernameAttributeCol = Column{
+ name: projection.LDAPPreferredUsernameAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPEmailAttributeCol = Column{
+ name: projection.LDAPEmailAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPEmailVerifiedAttributeCol = Column{
+ name: projection.LDAPEmailVerifiedAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPPhoneAttributeCol = Column{
+ name: projection.LDAPPhoneAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPPhoneVerifiedAttributeCol = Column{
+ name: projection.LDAPPhoneVerifiedAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPPreferredLanguageAttributeCol = Column{
+ name: projection.LDAPPreferredLanguageAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPAvatarURLAttributeCol = Column{
+ name: projection.LDAPAvatarURLAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+ LDAPProfileAttributeCol = Column{
+ name: projection.LDAPProfileAttributeCol,
+ table: ldapIdpTemplateTable,
+ }
+)
+
+// IDPTemplateByIDAndResourceOwner searches for the requested id in the context of the resource owner and IAM
+func (q *Queries) IDPTemplateByIDAndResourceOwner(ctx context.Context, shouldTriggerBulk bool, id, resourceOwner string, withOwnerRemoved bool) (_ *IDPTemplate, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ if shouldTriggerBulk {
+ err := projection.IDPTemplateProjection.Trigger(ctx)
+ logging.OnError(err).WithField("projection", idpTemplateTable.identifier()).Warn("could not trigger projection for query")
+ }
+
+ eq := sq.Eq{
+ IDPTemplateIDCol.identifier(): id,
+ IDPTemplateInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
+ }
+ if !withOwnerRemoved {
+ eq[IDPTemplateOwnerRemovedCol.identifier()] = false
+ }
+ where := sq.And{
+ eq,
+ sq.Or{
+ sq.Eq{IDPTemplateResourceOwnerCol.identifier(): resourceOwner},
+ sq.Eq{IDPTemplateResourceOwnerCol.identifier(): authz.GetInstance(ctx).InstanceID()},
+ },
+ }
+ stmt, scan := prepareIDPTemplateByIDQuery()
+ query, args, err := stmt.Where(where).ToSql()
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "QUERY-SFAew", "Errors.Query.SQLStatement")
+ }
+
+ row := q.client.QueryRowContext(ctx, query, args...)
+ return scan(row)
+}
+
+// IDPTemplates searches idp templates matching the query
+func (q *Queries) IDPTemplates(ctx context.Context, queries *IDPTemplateSearchQueries, withOwnerRemoved bool) (idps *IDPTemplates, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ query, scan := prepareIDPTemplatesQuery()
+ eq := sq.Eq{
+ IDPTemplateInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
+ }
+ if !withOwnerRemoved {
+ eq[IDPTemplateOwnerRemovedCol.identifier()] = false
+ }
+ stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
+ if err != nil {
+ return nil, errors.ThrowInvalidArgument(err, "QUERY-SAF34", "Errors.Query.InvalidRequest")
+ }
+
+ rows, err := q.client.QueryContext(ctx, stmt, args...)
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "QUERY-BDFrq", "Errors.Internal")
+ }
+ idps, err = scan(rows)
+ if err != nil {
+ return nil, err
+ }
+ idps.LatestSequence, err = q.latestSequence(ctx, idpTemplateTable)
+ return idps, err
+}
+
+type IDPTemplateSearchQueries struct {
+ SearchRequest
+ Queries []SearchQuery
+}
+
+func NewIDPTemplateIDSearchQuery(id string) (SearchQuery, error) {
+ return NewTextQuery(IDPTemplateIDCol, id, TextEquals)
+}
+
+func NewIDPTemplateOwnerTypeSearchQuery(ownerType domain.IdentityProviderType) (SearchQuery, error) {
+ return NewNumberQuery(IDPTemplateOwnerTypeCol, ownerType, NumberEquals)
+}
+
+func NewIDPTemplateNameSearchQuery(method TextComparison, value string) (SearchQuery, error) {
+ return NewTextQuery(IDPTemplateNameCol, value, method)
+}
+
+func NewIDPTemplateResourceOwnerSearchQuery(value string) (SearchQuery, error) {
+ return NewTextQuery(IDPTemplateResourceOwnerCol, value, TextEquals)
+}
+
+func NewIDPTemplateResourceOwnerListSearchQuery(ids ...string) (SearchQuery, error) {
+ list := make([]interface{}, len(ids))
+ for i, value := range ids {
+ list[i] = value
+ }
+ return NewListQuery(IDPTemplateResourceOwnerCol, list, ListIn)
+}
+
+func (q *IDPTemplateSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
+ query = q.SearchRequest.toQuery(query)
+ for _, q := range q.Queries {
+ query = q.toQuery(query)
+ }
+ return query
+}
+
+func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTemplate, error)) {
+ return sq.Select(
+ IDPTemplateIDCol.identifier(),
+ IDPTemplateResourceOwnerCol.identifier(),
+ IDPTemplateCreationDateCol.identifier(),
+ IDPTemplateChangeDateCol.identifier(),
+ IDPTemplateSequenceCol.identifier(),
+ IDPTemplateStateCol.identifier(),
+ IDPTemplateNameCol.identifier(),
+ IDPTemplateTypeCol.identifier(),
+ IDPTemplateOwnerTypeCol.identifier(),
+ IDPTemplateIsCreationAllowedCol.identifier(),
+ IDPTemplateIsLinkingAllowedCol.identifier(),
+ IDPTemplateIsAutoCreationCol.identifier(),
+ IDPTemplateIsAutoUpdateCol.identifier(),
+ LDAPIDCol.identifier(),
+ LDAPHostCol.identifier(),
+ LDAPPortCol.identifier(),
+ LDAPTlsCol.identifier(),
+ LDAPBaseDNCol.identifier(),
+ LDAPUserObjectClassCol.identifier(),
+ LDAPUserUniqueAttributeCol.identifier(),
+ LDAPAdminCol.identifier(),
+ LDAPPasswordCol.identifier(),
+ LDAPIDAttributeCol.identifier(),
+ LDAPFirstNameAttributeCol.identifier(),
+ LDAPLastNameAttributeCol.identifier(),
+ LDAPDisplayNameAttributeCol.identifier(),
+ LDAPNickNameAttributeCol.identifier(),
+ LDAPPreferredUsernameAttributeCol.identifier(),
+ LDAPEmailAttributeCol.identifier(),
+ LDAPEmailVerifiedAttributeCol.identifier(),
+ LDAPPhoneAttributeCol.identifier(),
+ LDAPPhoneVerifiedAttributeCol.identifier(),
+ LDAPPreferredLanguageAttributeCol.identifier(),
+ LDAPAvatarURLAttributeCol.identifier(),
+ LDAPProfileAttributeCol.identifier(),
+ ).From(idpTemplateTable.identifier()).
+ LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)).
+ PlaceholderFormat(sq.Dollar),
+ func(row *sql.Row) (*IDPTemplate, error) {
+ idpTemplate := new(IDPTemplate)
+
+ ldapID := sql.NullString{}
+ ldapHost := sql.NullString{}
+ ldapPort := sql.NullString{}
+ ldapTls := sql.NullBool{}
+ ldapBaseDN := sql.NullString{}
+ ldapUserObjectClass := sql.NullString{}
+ ldapUserUniqueAttribute := sql.NullString{}
+ ldapAdmin := sql.NullString{}
+ ldapPassword := new(crypto.CryptoValue)
+ ldapIDAttribute := sql.NullString{}
+ ldapFirstNameAttribute := sql.NullString{}
+ ldapLastNameAttribute := sql.NullString{}
+ ldapDisplayNameAttribute := sql.NullString{}
+ ldapNickNameAttribute := sql.NullString{}
+ ldapPreferredUsernameAttribute := sql.NullString{}
+ ldapEmailAttribute := sql.NullString{}
+ ldapEmailVerifiedAttribute := sql.NullString{}
+ ldapPhoneAttribute := sql.NullString{}
+ ldapPhoneVerifiedAttribute := sql.NullString{}
+ ldapPreferredLanguageAttribute := sql.NullString{}
+ ldapAvatarURLAttribute := sql.NullString{}
+ ldapProfileAttribute := sql.NullString{}
+
+ err := row.Scan(
+ &idpTemplate.ID,
+ &idpTemplate.ResourceOwner,
+ &idpTemplate.CreationDate,
+ &idpTemplate.ChangeDate,
+ &idpTemplate.Sequence,
+ &idpTemplate.State,
+ &idpTemplate.Name,
+ &idpTemplate.Type,
+ &idpTemplate.OwnerType,
+ &idpTemplate.IsCreationAllowed,
+ &idpTemplate.IsLinkingAllowed,
+ &idpTemplate.IsAutoCreation,
+ &idpTemplate.IsAutoUpdate,
+ &ldapID,
+ &ldapHost,
+ &ldapPort,
+ &ldapTls,
+ &ldapBaseDN,
+ &ldapUserObjectClass,
+ &ldapUserUniqueAttribute,
+ &ldapAdmin,
+ &ldapPassword,
+ &ldapIDAttribute,
+ &ldapFirstNameAttribute,
+ &ldapLastNameAttribute,
+ &ldapDisplayNameAttribute,
+ &ldapNickNameAttribute,
+ &ldapPreferredUsernameAttribute,
+ &ldapEmailAttribute,
+ &ldapEmailVerifiedAttribute,
+ &ldapPhoneAttribute,
+ &ldapPhoneVerifiedAttribute,
+ &ldapPreferredLanguageAttribute,
+ &ldapAvatarURLAttribute,
+ &ldapProfileAttribute,
+ )
+ if err != nil {
+ if errs.Is(err, sql.ErrNoRows) {
+ return nil, errors.ThrowNotFound(err, "QUERY-SAFrt", "Errors.IDPConfig.NotExisting")
+ }
+ return nil, errors.ThrowInternal(err, "QUERY-ADG42", "Errors.Internal")
+ }
+
+ if ldapID.Valid {
+ idpTemplate.LDAPIDPTemplate = &LDAPIDPTemplate{
+ IDPID: ldapID.String,
+ Host: ldapHost.String,
+ Port: ldapPort.String,
+ TLS: ldapTls.Bool,
+ BaseDN: ldapBaseDN.String,
+ UserObjectClass: ldapUserObjectClass.String,
+ UserUniqueAttribute: ldapUserUniqueAttribute.String,
+ Admin: ldapAdmin.String,
+ Password: ldapPassword,
+ LDAPAttributes: idp.LDAPAttributes{
+ IDAttribute: ldapIDAttribute.String,
+ FirstNameAttribute: ldapFirstNameAttribute.String,
+ LastNameAttribute: ldapLastNameAttribute.String,
+ DisplayNameAttribute: ldapDisplayNameAttribute.String,
+ NickNameAttribute: ldapNickNameAttribute.String,
+ PreferredUsernameAttribute: ldapPreferredUsernameAttribute.String,
+ EmailAttribute: ldapEmailAttribute.String,
+ EmailVerifiedAttribute: ldapEmailVerifiedAttribute.String,
+ PhoneAttribute: ldapPhoneAttribute.String,
+ PhoneVerifiedAttribute: ldapPhoneVerifiedAttribute.String,
+ PreferredLanguageAttribute: ldapPreferredLanguageAttribute.String,
+ AvatarURLAttribute: ldapAvatarURLAttribute.String,
+ ProfileAttribute: ldapProfileAttribute.String,
+ },
+ }
+ }
+
+ return idpTemplate, nil
+ }
+}
+
+func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplates, error)) {
+ return sq.Select(
+ IDPTemplateIDCol.identifier(),
+ IDPTemplateResourceOwnerCol.identifier(),
+ IDPTemplateCreationDateCol.identifier(),
+ IDPTemplateChangeDateCol.identifier(),
+ IDPTemplateSequenceCol.identifier(),
+ IDPTemplateStateCol.identifier(),
+ IDPTemplateNameCol.identifier(),
+ IDPTemplateTypeCol.identifier(),
+ IDPTemplateOwnerTypeCol.identifier(),
+ IDPTemplateIsCreationAllowedCol.identifier(),
+ IDPTemplateIsLinkingAllowedCol.identifier(),
+ IDPTemplateIsAutoCreationCol.identifier(),
+ IDPTemplateIsAutoUpdateCol.identifier(),
+ LDAPIDCol.identifier(),
+ LDAPHostCol.identifier(),
+ LDAPPortCol.identifier(),
+ LDAPTlsCol.identifier(),
+ LDAPBaseDNCol.identifier(),
+ LDAPUserObjectClassCol.identifier(),
+ LDAPUserUniqueAttributeCol.identifier(),
+ LDAPAdminCol.identifier(),
+ LDAPPasswordCol.identifier(),
+ LDAPIDAttributeCol.identifier(),
+ LDAPFirstNameAttributeCol.identifier(),
+ LDAPLastNameAttributeCol.identifier(),
+ LDAPDisplayNameAttributeCol.identifier(),
+ LDAPNickNameAttributeCol.identifier(),
+ LDAPPreferredUsernameAttributeCol.identifier(),
+ LDAPEmailAttributeCol.identifier(),
+ LDAPEmailVerifiedAttributeCol.identifier(),
+ LDAPPhoneAttributeCol.identifier(),
+ LDAPPhoneVerifiedAttributeCol.identifier(),
+ LDAPPreferredLanguageAttributeCol.identifier(),
+ LDAPAvatarURLAttributeCol.identifier(),
+ LDAPProfileAttributeCol.identifier(),
+ countColumn.identifier(),
+ ).From(idpTemplateTable.identifier()).
+ LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)).
+ PlaceholderFormat(sq.Dollar),
+ func(rows *sql.Rows) (*IDPTemplates, error) {
+ templates := make([]*IDPTemplate, 0)
+ var count uint64
+ for rows.Next() {
+ idpTemplate := new(IDPTemplate)
+
+ ldapID := sql.NullString{}
+ ldapHost := sql.NullString{}
+ ldapPort := sql.NullString{}
+ ldapTls := sql.NullBool{}
+ ldapBaseDN := sql.NullString{}
+ ldapUserObjectClass := sql.NullString{}
+ ldapUserUniqueAttribute := sql.NullString{}
+ ldapAdmin := sql.NullString{}
+ ldapPassword := new(crypto.CryptoValue)
+ ldapIDAttribute := sql.NullString{}
+ ldapFirstNameAttribute := sql.NullString{}
+ ldapLastNameAttribute := sql.NullString{}
+ ldapDisplayNameAttribute := sql.NullString{}
+ ldapNickNameAttribute := sql.NullString{}
+ ldapPreferredUsernameAttribute := sql.NullString{}
+ ldapEmailAttribute := sql.NullString{}
+ ldapEmailVerifiedAttribute := sql.NullString{}
+ ldapPhoneAttribute := sql.NullString{}
+ ldapPhoneVerifiedAttribute := sql.NullString{}
+ ldapPreferredLanguageAttribute := sql.NullString{}
+ ldapAvatarURLAttribute := sql.NullString{}
+ ldapProfileAttribute := sql.NullString{}
+
+ err := rows.Scan(
+ &idpTemplate.ID,
+ &idpTemplate.ResourceOwner,
+ &idpTemplate.CreationDate,
+ &idpTemplate.ChangeDate,
+ &idpTemplate.Sequence,
+ &idpTemplate.State,
+ &idpTemplate.Name,
+ &idpTemplate.Type,
+ &idpTemplate.OwnerType,
+ &idpTemplate.IsCreationAllowed,
+ &idpTemplate.IsLinkingAllowed,
+ &idpTemplate.IsAutoCreation,
+ &idpTemplate.IsAutoUpdate,
+ &ldapID,
+ &ldapHost,
+ &ldapPort,
+ &ldapTls,
+ &ldapBaseDN,
+ &ldapUserObjectClass,
+ &ldapUserUniqueAttribute,
+ &ldapAdmin,
+ &ldapPassword,
+ &ldapIDAttribute,
+ &ldapFirstNameAttribute,
+ &ldapLastNameAttribute,
+ &ldapDisplayNameAttribute,
+ &ldapNickNameAttribute,
+ &ldapPreferredUsernameAttribute,
+ &ldapEmailAttribute,
+ &ldapEmailVerifiedAttribute,
+ &ldapPhoneAttribute,
+ &ldapPhoneVerifiedAttribute,
+ &ldapPreferredLanguageAttribute,
+ &ldapAvatarURLAttribute,
+ &ldapProfileAttribute,
+ &count,
+ )
+
+ if err != nil {
+ return nil, err
+ }
+
+ if ldapID.Valid {
+ idpTemplate.LDAPIDPTemplate = &LDAPIDPTemplate{
+ IDPID: ldapID.String,
+ Host: ldapHost.String,
+ Port: ldapPort.String,
+ TLS: ldapTls.Bool,
+ BaseDN: ldapBaseDN.String,
+ UserObjectClass: ldapUserObjectClass.String,
+ UserUniqueAttribute: ldapUserUniqueAttribute.String,
+ Admin: ldapAdmin.String,
+ Password: ldapPassword,
+ LDAPAttributes: idp.LDAPAttributes{
+ IDAttribute: ldapIDAttribute.String,
+ FirstNameAttribute: ldapFirstNameAttribute.String,
+ LastNameAttribute: ldapLastNameAttribute.String,
+ DisplayNameAttribute: ldapDisplayNameAttribute.String,
+ NickNameAttribute: ldapNickNameAttribute.String,
+ PreferredUsernameAttribute: ldapPreferredUsernameAttribute.String,
+ EmailAttribute: ldapEmailAttribute.String,
+ EmailVerifiedAttribute: ldapEmailVerifiedAttribute.String,
+ PhoneAttribute: ldapPhoneAttribute.String,
+ PhoneVerifiedAttribute: ldapPhoneVerifiedAttribute.String,
+ PreferredLanguageAttribute: ldapPreferredLanguageAttribute.String,
+ AvatarURLAttribute: ldapAvatarURLAttribute.String,
+ ProfileAttribute: ldapProfileAttribute.String,
+ },
+ }
+ }
+ templates = append(templates, idpTemplate)
+ }
+
+ if err := rows.Close(); err != nil {
+ return nil, errors.ThrowInternal(err, "QUERY-SAGrt", "Errors.Query.CloseRows")
+ }
+
+ return &IDPTemplates{
+ Templates: templates,
+ SearchResponse: SearchResponse{
+ Count: count,
+ },
+ }, nil
+ }
+}
diff --git a/internal/query/idp_template_test.go b/internal/query/idp_template_test.go
new file mode 100644
index 000000000..d164fbcbe
--- /dev/null
+++ b/internal/query/idp_template_test.go
@@ -0,0 +1,727 @@
+package query
+
+import (
+ "database/sql"
+ "database/sql/driver"
+ "errors"
+ "fmt"
+ "regexp"
+ "testing"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ errs "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/repository/idp"
+)
+
+var (
+ idpTemplateQuery = `SELECT projections.idp_templates.id,` +
+ ` projections.idp_templates.resource_owner,` +
+ ` projections.idp_templates.creation_date,` +
+ ` projections.idp_templates.change_date,` +
+ ` projections.idp_templates.sequence,` +
+ ` projections.idp_templates.state,` +
+ ` projections.idp_templates.name,` +
+ ` projections.idp_templates.type,` +
+ ` projections.idp_templates.owner_type,` +
+ ` projections.idp_templates.is_creation_allowed,` +
+ ` projections.idp_templates.is_linking_allowed,` +
+ ` projections.idp_templates.is_auto_creation,` +
+ ` projections.idp_templates.is_auto_update,` +
+ ` projections.idp_templates_ldap.idp_id,` +
+ ` projections.idp_templates_ldap.host,` +
+ ` projections.idp_templates_ldap.port,` +
+ ` projections.idp_templates_ldap.tls,` +
+ ` projections.idp_templates_ldap.base_dn,` +
+ ` projections.idp_templates_ldap.user_object_class,` +
+ ` projections.idp_templates_ldap.user_unique_attribute,` +
+ ` projections.idp_templates_ldap.admin,` +
+ ` projections.idp_templates_ldap.password,` +
+ ` projections.idp_templates_ldap.id_attribute,` +
+ ` projections.idp_templates_ldap.first_name_attribute,` +
+ ` projections.idp_templates_ldap.last_name_attribute,` +
+ ` projections.idp_templates_ldap.display_name_attribute,` +
+ ` projections.idp_templates_ldap.nick_name_attribute,` +
+ ` projections.idp_templates_ldap.preferred_username_attribute,` +
+ ` projections.idp_templates_ldap.email_attribute,` +
+ ` projections.idp_templates_ldap.email_verified,` +
+ ` projections.idp_templates_ldap.phone_attribute,` +
+ ` projections.idp_templates_ldap.phone_verified_attribute,` +
+ ` projections.idp_templates_ldap.preferred_language_attribute,` +
+ ` projections.idp_templates_ldap.avatar_url_attribute,` +
+ ` projections.idp_templates_ldap.profile_attribute` +
+ ` FROM projections.idp_templates` +
+ ` LEFT JOIN projections.idp_templates_ldap ON projections.idp_templates.id = projections.idp_templates_ldap.idp_id AND projections.idp_templates.instance_id = projections.idp_templates_ldap.instance_id`
+ idpTemplateCols = []string{
+ "id",
+ "resource_owner",
+ "creation_date",
+ "change_date",
+ "sequence",
+ "state",
+ "name",
+ "type",
+ "owner_type",
+ "is_creation_allowed",
+ "is_linking_allowed",
+ "is_auto_creation",
+ "is_auto_update",
+ // ldap config
+ "idp_id",
+ "host",
+ "port",
+ "tls",
+ "base_dn",
+ "user_object_class",
+ "user_unique_attribute",
+ "admin",
+ "password",
+ "id_attribute",
+ "first_name_attribute",
+ "last_name_attribute",
+ "display_name_attribute",
+ "nick_name_attribute",
+ "preferred_username_attribute",
+ "email_attribute",
+ "email_verified",
+ "phone_attribute",
+ "phone_verified_attribute",
+ "preferred_language_attribute",
+ "avatar_url_attribute",
+ "profile_attribute",
+ }
+ idpTemplatesQuery = `SELECT projections.idp_templates.id,` +
+ ` projections.idp_templates.resource_owner,` +
+ ` projections.idp_templates.creation_date,` +
+ ` projections.idp_templates.change_date,` +
+ ` projections.idp_templates.sequence,` +
+ ` projections.idp_templates.state,` +
+ ` projections.idp_templates.name,` +
+ ` projections.idp_templates.type,` +
+ ` projections.idp_templates.owner_type,` +
+ ` projections.idp_templates.is_creation_allowed,` +
+ ` projections.idp_templates.is_linking_allowed,` +
+ ` projections.idp_templates.is_auto_creation,` +
+ ` projections.idp_templates.is_auto_update,` +
+ ` projections.idp_templates_ldap.idp_id,` +
+ ` projections.idp_templates_ldap.host,` +
+ ` projections.idp_templates_ldap.port,` +
+ ` projections.idp_templates_ldap.tls,` +
+ ` projections.idp_templates_ldap.base_dn,` +
+ ` projections.idp_templates_ldap.user_object_class,` +
+ ` projections.idp_templates_ldap.user_unique_attribute,` +
+ ` projections.idp_templates_ldap.admin,` +
+ ` projections.idp_templates_ldap.password,` +
+ ` projections.idp_templates_ldap.id_attribute,` +
+ ` projections.idp_templates_ldap.first_name_attribute,` +
+ ` projections.idp_templates_ldap.last_name_attribute,` +
+ ` projections.idp_templates_ldap.display_name_attribute,` +
+ ` projections.idp_templates_ldap.nick_name_attribute,` +
+ ` projections.idp_templates_ldap.preferred_username_attribute,` +
+ ` projections.idp_templates_ldap.email_attribute,` +
+ ` projections.idp_templates_ldap.email_verified,` +
+ ` projections.idp_templates_ldap.phone_attribute,` +
+ ` projections.idp_templates_ldap.phone_verified_attribute,` +
+ ` projections.idp_templates_ldap.preferred_language_attribute,` +
+ ` projections.idp_templates_ldap.avatar_url_attribute,` +
+ ` projections.idp_templates_ldap.profile_attribute,` +
+ ` COUNT(*) OVER ()` +
+ ` FROM projections.idp_templates` +
+ ` LEFT JOIN projections.idp_templates_ldap ON projections.idp_templates.id = projections.idp_templates_ldap.idp_id AND projections.idp_templates.instance_id = projections.idp_templates_ldap.instance_id`
+ idpTemplatesCols = []string{
+ "id",
+ "resource_owner",
+ "creation_date",
+ "change_date",
+ "sequence",
+ "state",
+ "name",
+ "type",
+ "owner_type",
+ "is_creation_allowed",
+ "is_linking_allowed",
+ "is_auto_creation",
+ "is_auto_update",
+ "idp_id",
+ "host",
+ "port",
+ "tls",
+ "base_dn",
+ "user_object_class",
+ "user_unique_attribute",
+ "admin",
+ "password",
+ "id_attribute",
+ "first_name_attribute",
+ "last_name_attribute",
+ "display_name_attribute",
+ "nick_name_attribute",
+ "preferred_username_attribute",
+ "email_attribute",
+ "email_verified",
+ "phone_attribute",
+ "phone_verified_attribute",
+ "preferred_language_attribute",
+ "avatar_url_attribute",
+ "profile_attribute",
+ "count",
+ }
+)
+
+func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
+ type want struct {
+ sqlExpectations sqlExpectation
+ err checkErr
+ }
+ tests := []struct {
+ name string
+ prepare interface{}
+ want want
+ object interface{}
+ }{
+ {
+ name: "prepareIDPTemplateByIDQuery no result",
+ prepare: prepareIDPTemplateByIDQuery,
+ want: want{
+ sqlExpectations: mockQuery(
+ regexp.QuoteMeta(idpTemplateQuery),
+ nil,
+ nil,
+ ),
+ err: func(err error) (error, bool) {
+ if !errs.IsNotFound(err) {
+ return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
+ }
+ return nil, true
+ },
+ },
+ object: (*IDPTemplate)(nil),
+ },
+ {
+ name: "prepareIDPTemplateByIDQuery ldap idp",
+ prepare: prepareIDPTemplateByIDQuery,
+ want: want{
+ sqlExpectations: mockQuery(
+ regexp.QuoteMeta(idpTemplateQuery),
+ idpTemplateCols,
+ []driver.Value{
+ "idp-id",
+ "ro",
+ testNow,
+ testNow,
+ uint64(20211109),
+ domain.IDPConfigStateActive,
+ "idp-name",
+ domain.IDPTypeLDAP,
+ domain.IdentityProviderTypeOrg,
+ true,
+ true,
+ true,
+ true,
+ // ldap config
+ "idp-id",
+ "host",
+ "port",
+ true,
+ "base",
+ "user",
+ "uid",
+ "admin",
+ nil,
+ "id",
+ "first",
+ "last",
+ "display",
+ "nickname",
+ "username",
+ "email",
+ "emailVerified",
+ "phone",
+ "phoneVerified",
+ "lang",
+ "avatar",
+ "profile",
+ },
+ ),
+ },
+ object: &IDPTemplate{
+ CreationDate: testNow,
+ ChangeDate: testNow,
+ Sequence: 20211109,
+ ResourceOwner: "ro",
+ ID: "idp-id",
+ State: domain.IDPStateActive,
+ Name: "idp-name",
+ Type: domain.IDPTypeLDAP,
+ OwnerType: domain.IdentityProviderTypeOrg,
+ IsCreationAllowed: true,
+ IsLinkingAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ LDAPIDPTemplate: &LDAPIDPTemplate{
+ IDPID: "idp-id",
+ Host: "host",
+ Port: "port",
+ TLS: true,
+ BaseDN: "base",
+ UserObjectClass: "user",
+ UserUniqueAttribute: "uid",
+ Admin: "admin",
+ LDAPAttributes: idp.LDAPAttributes{
+ IDAttribute: "id",
+ FirstNameAttribute: "first",
+ LastNameAttribute: "last",
+ DisplayNameAttribute: "display",
+ NickNameAttribute: "nickname",
+ PreferredUsernameAttribute: "username",
+ EmailAttribute: "email",
+ EmailVerifiedAttribute: "emailVerified",
+ PhoneAttribute: "phone",
+ PhoneVerifiedAttribute: "phoneVerified",
+ PreferredLanguageAttribute: "lang",
+ AvatarURLAttribute: "avatar",
+ ProfileAttribute: "profile",
+ },
+ },
+ },
+ },
+ {
+ name: "prepareIDPTemplateByIDQuery no config",
+ prepare: prepareIDPTemplateByIDQuery,
+ want: want{
+ sqlExpectations: mockQuery(
+ regexp.QuoteMeta(idpTemplateQuery),
+ idpTemplateCols,
+ []driver.Value{
+ "idp-id",
+ "ro",
+ testNow,
+ testNow,
+ uint64(20211109),
+ domain.IDPConfigStateActive,
+ "idp-name",
+ domain.IDPTypeLDAP,
+ domain.IdentityProviderTypeOrg,
+ true,
+ true,
+ true,
+ true,
+ // ldap config
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ },
+ ),
+ },
+ object: &IDPTemplate{
+ CreationDate: testNow,
+ ChangeDate: testNow,
+ Sequence: 20211109,
+ ResourceOwner: "ro",
+ ID: "idp-id",
+ State: domain.IDPStateActive,
+ Name: "idp-name",
+ Type: domain.IDPTypeLDAP,
+ OwnerType: domain.IdentityProviderTypeOrg,
+ IsCreationAllowed: true,
+ IsLinkingAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ },
+ },
+ {
+ name: "prepareIDPTemplateByIDQuery sql err",
+ prepare: prepareIDPTemplateByIDQuery,
+ want: want{
+ sqlExpectations: mockQueryErr(
+ regexp.QuoteMeta(idpTemplateQuery),
+ sql.ErrConnDone,
+ ),
+ err: func(err error) (error, bool) {
+ if !errors.Is(err, sql.ErrConnDone) {
+ return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
+ }
+ return nil, true
+ },
+ },
+ object: nil,
+ },
+ {
+ name: "prepareIDPTemplatesQuery no result",
+ prepare: prepareIDPTemplatesQuery,
+ want: want{
+ sqlExpectations: mockQueries(
+ regexp.QuoteMeta(idpTemplatesQuery),
+ nil,
+ nil,
+ ),
+ err: func(err error) (error, bool) {
+ if !errs.IsNotFound(err) {
+ return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
+ }
+ return nil, true
+ },
+ },
+ object: &IDPTemplates{Templates: []*IDPTemplate{}},
+ },
+ {
+ name: "prepareIDPTemplatesQuery ldap idp",
+ prepare: prepareIDPTemplatesQuery,
+ want: want{
+ sqlExpectations: mockQueries(
+ regexp.QuoteMeta(idpTemplatesQuery),
+ idpTemplatesCols,
+ [][]driver.Value{
+ {
+ "idp-id",
+ "ro",
+ testNow,
+ testNow,
+ uint64(20211109),
+ domain.IDPConfigStateActive,
+ "idp-name",
+ domain.IDPTypeLDAP,
+ domain.IdentityProviderTypeOrg,
+ true,
+ true,
+ true,
+ true,
+ // ldap config
+ "idp-id",
+ "host",
+ "port",
+ true,
+ "base",
+ "user",
+ "uid",
+ "admin",
+ nil,
+ "id",
+ "first",
+ "last",
+ "display",
+ "nickname",
+ "username",
+ "email",
+ "emailVerified",
+ "phone",
+ "phoneVerified",
+ "lang",
+ "avatar",
+ "profile",
+ },
+ },
+ ),
+ },
+ object: &IDPTemplates{
+ SearchResponse: SearchResponse{
+ Count: 1,
+ },
+ Templates: []*IDPTemplate{
+ {
+ CreationDate: testNow,
+ ChangeDate: testNow,
+ Sequence: 20211109,
+ ResourceOwner: "ro",
+ ID: "idp-id",
+ State: domain.IDPStateActive,
+ Name: "idp-name",
+ Type: domain.IDPTypeLDAP,
+ OwnerType: domain.IdentityProviderTypeOrg,
+ IsCreationAllowed: true,
+ IsLinkingAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ LDAPIDPTemplate: &LDAPIDPTemplate{
+ IDPID: "idp-id",
+ Host: "host",
+ Port: "port",
+ TLS: true,
+ BaseDN: "base",
+ UserObjectClass: "user",
+ UserUniqueAttribute: "uid",
+ Admin: "admin",
+ LDAPAttributes: idp.LDAPAttributes{
+ IDAttribute: "id",
+ FirstNameAttribute: "first",
+ LastNameAttribute: "last",
+ DisplayNameAttribute: "display",
+ NickNameAttribute: "nickname",
+ PreferredUsernameAttribute: "username",
+ EmailAttribute: "email",
+ EmailVerifiedAttribute: "emailVerified",
+ PhoneAttribute: "phone",
+ PhoneVerifiedAttribute: "phoneVerified",
+ PreferredLanguageAttribute: "lang",
+ AvatarURLAttribute: "avatar",
+ ProfileAttribute: "profile",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "prepareIDPTemplatesQuery no config",
+ prepare: prepareIDPTemplatesQuery,
+ want: want{
+ sqlExpectations: mockQueries(
+ regexp.QuoteMeta(idpTemplatesQuery),
+ idpTemplatesCols,
+ [][]driver.Value{
+ {
+ "idp-id",
+ "ro",
+ testNow,
+ testNow,
+ uint64(20211109),
+ domain.IDPConfigStateActive,
+ "idp-name",
+ domain.IDPTypeLDAP,
+ domain.IdentityProviderTypeOrg,
+ true,
+ true,
+ true,
+ true,
+ // ldap config
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ },
+ },
+ ),
+ },
+ object: &IDPTemplates{
+ SearchResponse: SearchResponse{
+ Count: 1,
+ },
+ Templates: []*IDPTemplate{
+ {
+ CreationDate: testNow,
+ ChangeDate: testNow,
+ Sequence: 20211109,
+ ResourceOwner: "ro",
+ ID: "idp-id",
+ State: domain.IDPStateActive,
+ Name: "idp-name",
+ Type: domain.IDPTypeLDAP,
+ OwnerType: domain.IdentityProviderTypeOrg,
+ IsCreationAllowed: true,
+ IsLinkingAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ },
+ },
+ },
+ },
+ {
+ name: "prepareIDPTemplatesQuery all config types",
+ prepare: prepareIDPTemplatesQuery,
+ want: want{
+ sqlExpectations: mockQueries(
+ regexp.QuoteMeta(idpTemplatesQuery),
+ idpTemplatesCols,
+ [][]driver.Value{
+ {
+ "idp-id-1",
+ "ro",
+ testNow,
+ testNow,
+ uint64(20211109),
+ domain.IDPConfigStateActive,
+ "idp-name",
+ domain.IDPTypeLDAP,
+ domain.IdentityProviderTypeOrg,
+ true,
+ true,
+ true,
+ true,
+ // ldap config
+ "idp-id",
+ "host",
+ "port",
+ true,
+ "base",
+ "user",
+ "uid",
+ "admin",
+ nil,
+ "id",
+ "first",
+ "last",
+ "display",
+ "nickname",
+ "username",
+ "email",
+ "emailVerified",
+ "phone",
+ "phoneVerified",
+ "lang",
+ "avatar",
+ "profile",
+ },
+ {
+ "idp-id-2",
+ "ro",
+ testNow,
+ testNow,
+ uint64(20211109),
+ domain.IDPConfigStateActive,
+ "idp-name",
+ domain.IDPTypeLDAP,
+ domain.IdentityProviderTypeOrg,
+ true,
+ true,
+ true,
+ true,
+ // ldap config
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ },
+ },
+ ),
+ },
+ object: &IDPTemplates{
+ SearchResponse: SearchResponse{
+ Count: 2,
+ },
+ Templates: []*IDPTemplate{
+ {
+ CreationDate: testNow,
+ ChangeDate: testNow,
+ Sequence: 20211109,
+ ResourceOwner: "ro",
+ ID: "idp-id-1",
+ State: domain.IDPStateActive,
+ Name: "idp-name",
+ Type: domain.IDPTypeLDAP,
+ OwnerType: domain.IdentityProviderTypeOrg,
+ IsCreationAllowed: true,
+ IsLinkingAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ LDAPIDPTemplate: &LDAPIDPTemplate{
+ IDPID: "idp-id",
+ Host: "host",
+ Port: "port",
+ TLS: true,
+ BaseDN: "base",
+ UserObjectClass: "user",
+ UserUniqueAttribute: "uid",
+ Admin: "admin",
+ LDAPAttributes: idp.LDAPAttributes{
+ IDAttribute: "id",
+ FirstNameAttribute: "first",
+ LastNameAttribute: "last",
+ DisplayNameAttribute: "display",
+ NickNameAttribute: "nickname",
+ PreferredUsernameAttribute: "username",
+ EmailAttribute: "email",
+ EmailVerifiedAttribute: "emailVerified",
+ PhoneAttribute: "phone",
+ PhoneVerifiedAttribute: "phoneVerified",
+ PreferredLanguageAttribute: "lang",
+ AvatarURLAttribute: "avatar",
+ ProfileAttribute: "profile",
+ },
+ },
+ },
+ {
+ CreationDate: testNow,
+ ChangeDate: testNow,
+ Sequence: 20211109,
+ ResourceOwner: "ro",
+ ID: "idp-id-2",
+ State: domain.IDPStateActive,
+ Name: "idp-name",
+ Type: domain.IDPTypeLDAP,
+ OwnerType: domain.IdentityProviderTypeOrg,
+ IsCreationAllowed: true,
+ IsLinkingAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ },
+ },
+ },
+ },
+ {
+ name: "prepareIDPTemplatesQuery sql err",
+ prepare: prepareIDPTemplatesQuery,
+ want: want{
+ sqlExpectations: mockQueryErr(
+ regexp.QuoteMeta(idpTemplatesQuery),
+ sql.ErrConnDone,
+ ),
+ err: func(err error) (error, bool) {
+ if !errors.Is(err, sql.ErrConnDone) {
+ return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
+ }
+ return nil, true
+ },
+ },
+ object: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err)
+ })
+ }
+}
diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go
new file mode 100644
index 000000000..fa25113a2
--- /dev/null
+++ b/internal/query/projection/idp_template.go
@@ -0,0 +1,412 @@
+package projection
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/handler"
+ "github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
+ "github.com/zitadel/zitadel/internal/repository/idp"
+ "github.com/zitadel/zitadel/internal/repository/instance"
+ "github.com/zitadel/zitadel/internal/repository/org"
+)
+
+const (
+ IDPTemplateTable = "projections.idp_templates"
+ IDPTemplateLDAPTable = IDPTemplateTable + "_" + IDPTemplateLDAPSuffix
+
+ IDPTemplateLDAPSuffix = "ldap"
+
+ IDPTemplateIDCol = "id"
+ IDPTemplateCreationDateCol = "creation_date"
+ IDPTemplateChangeDateCol = "change_date"
+ IDPTemplateSequenceCol = "sequence"
+ IDPTemplateResourceOwnerCol = "resource_owner"
+ IDPTemplateInstanceIDCol = "instance_id"
+ IDPTemplateStateCol = "state"
+ IDPTemplateNameCol = "name"
+ IDPTemplateOwnerTypeCol = "owner_type"
+ IDPTemplateTypeCol = "type"
+ IDPTemplateOwnerRemovedCol = "owner_removed"
+ IDPTemplateIsCreationAllowedCol = "is_creation_allowed"
+ IDPTemplateIsLinkingAllowedCol = "is_linking_allowed"
+ IDPTemplateIsAutoCreationCol = "is_auto_creation"
+ IDPTemplateIsAutoUpdateCol = "is_auto_update"
+
+ LDAPIDCol = "idp_id"
+ LDAPInstanceIDCol = "instance_id"
+ LDAPHostCol = "host"
+ LDAPPortCol = "port"
+ LDAPTlsCol = "tls"
+ LDAPBaseDNCol = "base_dn"
+ LDAPUserObjectClassCol = "user_object_class"
+ LDAPUserUniqueAttributeCol = "user_unique_attribute"
+ LDAPAdminCol = "admin"
+ LDAPPasswordCol = "password"
+ LDAPIDAttributeCol = "id_attribute"
+ LDAPFirstNameAttributeCol = "first_name_attribute"
+ LDAPLastNameAttributeCol = "last_name_attribute"
+ LDAPDisplayNameAttributeCol = "display_name_attribute"
+ LDAPNickNameAttributeCol = "nick_name_attribute"
+ LDAPPreferredUsernameAttributeCol = "preferred_username_attribute"
+ LDAPEmailAttributeCol = "email_attribute"
+ LDAPEmailVerifiedAttributeCol = "email_verified"
+ LDAPPhoneAttributeCol = "phone_attribute"
+ LDAPPhoneVerifiedAttributeCol = "phone_verified_attribute"
+ LDAPPreferredLanguageAttributeCol = "preferred_language_attribute"
+ LDAPAvatarURLAttributeCol = "avatar_url_attribute"
+ LDAPProfileAttributeCol = "profile_attribute"
+)
+
+type idpTemplateProjection struct {
+ crdb.StatementHandler
+}
+
+func newIDPTemplateProjection(ctx context.Context, config crdb.StatementHandlerConfig) *idpTemplateProjection {
+ p := new(idpTemplateProjection)
+ config.ProjectionName = IDPTemplateTable
+ config.Reducers = p.reducers()
+ config.InitCheck = crdb.NewMultiTableCheck(
+ crdb.NewTable([]*crdb.Column{
+ crdb.NewColumn(IDPTemplateIDCol, crdb.ColumnTypeText),
+ crdb.NewColumn(IDPTemplateCreationDateCol, crdb.ColumnTypeTimestamp),
+ crdb.NewColumn(IDPTemplateChangeDateCol, crdb.ColumnTypeTimestamp),
+ crdb.NewColumn(IDPTemplateSequenceCol, crdb.ColumnTypeInt64),
+ crdb.NewColumn(IDPTemplateResourceOwnerCol, crdb.ColumnTypeText),
+ crdb.NewColumn(IDPTemplateInstanceIDCol, crdb.ColumnTypeText),
+ crdb.NewColumn(IDPTemplateStateCol, crdb.ColumnTypeEnum),
+ crdb.NewColumn(IDPTemplateNameCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(IDPTemplateOwnerTypeCol, crdb.ColumnTypeEnum),
+ crdb.NewColumn(IDPTemplateTypeCol, crdb.ColumnTypeEnum),
+ crdb.NewColumn(IDPTemplateOwnerRemovedCol, crdb.ColumnTypeBool, crdb.Default(false)),
+ crdb.NewColumn(IDPTemplateIsCreationAllowedCol, crdb.ColumnTypeBool, crdb.Default(false)),
+ crdb.NewColumn(IDPTemplateIsLinkingAllowedCol, crdb.ColumnTypeBool, crdb.Default(false)),
+ crdb.NewColumn(IDPTemplateIsAutoCreationCol, crdb.ColumnTypeBool, crdb.Default(false)),
+ crdb.NewColumn(IDPTemplateIsAutoUpdateCol, crdb.ColumnTypeBool, crdb.Default(false)),
+ },
+ crdb.NewPrimaryKey(IDPTemplateInstanceIDCol, IDPTemplateIDCol),
+ crdb.WithIndex(crdb.NewIndex("resource_owner", []string{IDPTemplateResourceOwnerCol})),
+ crdb.WithIndex(crdb.NewIndex("owner_removed", []string{IDPTemplateOwnerRemovedCol})),
+ ),
+ crdb.NewSuffixedTable([]*crdb.Column{
+ crdb.NewColumn(LDAPIDCol, crdb.ColumnTypeText),
+ crdb.NewColumn(LDAPInstanceIDCol, crdb.ColumnTypeText),
+ crdb.NewColumn(LDAPHostCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPPortCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPTlsCol, crdb.ColumnTypeBool, crdb.Nullable()),
+ crdb.NewColumn(LDAPBaseDNCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPUserObjectClassCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPUserUniqueAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPAdminCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPPasswordCol, crdb.ColumnTypeJSONB, crdb.Nullable()),
+ crdb.NewColumn(LDAPIDAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPFirstNameAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPLastNameAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPDisplayNameAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPNickNameAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPPreferredUsernameAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPEmailAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPEmailVerifiedAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPPhoneAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPPhoneVerifiedAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPPreferredLanguageAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPAvatarURLAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(LDAPProfileAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
+ },
+ crdb.NewPrimaryKey(LDAPInstanceIDCol, LDAPIDCol),
+ IDPTemplateLDAPSuffix,
+ crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys()),
+ ),
+ )
+ p.StatementHandler = crdb.NewStatementHandler(ctx, config)
+ return p
+}
+
+func (p *idpTemplateProjection) reducers() []handler.AggregateReducer {
+ return []handler.AggregateReducer{
+ {
+ Aggregate: instance.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: instance.LDAPIDPAddedEventType,
+ Reduce: p.reduceLDAPIDPAdded,
+ },
+ {
+ Event: instance.LDAPIDPChangedEventType,
+ Reduce: p.reduceLDAPIDPChanged,
+ },
+ {
+ Event: instance.IDPRemovedEventType,
+ Reduce: p.reduceIDPRemoved,
+ },
+ {
+ Event: instance.InstanceRemovedEventType,
+ Reduce: reduceInstanceRemovedHelper(IDPTemplateInstanceIDCol),
+ },
+ },
+ },
+ {
+ Aggregate: org.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: org.LDAPIDPAddedEventType,
+ Reduce: p.reduceLDAPIDPAdded,
+ },
+ {
+ Event: org.LDAPIDPChangedEventType,
+ Reduce: p.reduceLDAPIDPChanged,
+ },
+ {
+ Event: org.IDPRemovedEventType,
+ Reduce: p.reduceIDPRemoved,
+ },
+ {
+ Event: org.OrgRemovedEventType,
+ Reduce: p.reduceOwnerRemoved,
+ },
+ },
+ },
+ }
+}
+
+func (p *idpTemplateProjection) reduceLDAPIDPAdded(event eventstore.Event) (*handler.Statement, error) {
+ var idpEvent idp.LDAPIDPAddedEvent
+ var idpOwnerType domain.IdentityProviderType
+ switch e := event.(type) {
+ case *org.LDAPIDPAddedEvent:
+ idpEvent = e.LDAPIDPAddedEvent
+ idpOwnerType = domain.IdentityProviderTypeOrg
+ case *instance.LDAPIDPAddedEvent:
+ idpEvent = e.LDAPIDPAddedEvent
+ idpOwnerType = domain.IdentityProviderTypeSystem
+ default:
+ return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-9s02m1", "reduce.wrong.event.type %v", []eventstore.EventType{org.LDAPIDPAddedEventType, instance.LDAPIDPAddedEventType})
+ }
+
+ return crdb.NewMultiStatement(
+ &idpEvent,
+ crdb.AddCreateStatement(
+ []handler.Column{
+ handler.NewCol(IDPTemplateIDCol, idpEvent.ID),
+ handler.NewCol(IDPTemplateCreationDateCol, idpEvent.CreationDate()),
+ handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()),
+ handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()),
+ handler.NewCol(IDPTemplateResourceOwnerCol, idpEvent.Aggregate().ResourceOwner),
+ handler.NewCol(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID),
+ handler.NewCol(IDPTemplateStateCol, domain.IDPStateActive),
+ handler.NewCol(IDPTemplateNameCol, idpEvent.Name),
+ handler.NewCol(IDPTemplateOwnerTypeCol, idpOwnerType),
+ handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeLDAP),
+ handler.NewCol(IDPTemplateIsCreationAllowedCol, idpEvent.IsCreationAllowed),
+ handler.NewCol(IDPTemplateIsLinkingAllowedCol, idpEvent.IsLinkingAllowed),
+ handler.NewCol(IDPTemplateIsAutoCreationCol, idpEvent.IsAutoCreation),
+ handler.NewCol(IDPTemplateIsAutoUpdateCol, idpEvent.IsAutoUpdate),
+ },
+ ),
+ crdb.AddCreateStatement(
+ []handler.Column{
+ handler.NewCol(LDAPIDCol, idpEvent.ID),
+ handler.NewCol(LDAPInstanceIDCol, idpEvent.Aggregate().InstanceID),
+ handler.NewCol(LDAPHostCol, idpEvent.Host),
+ handler.NewCol(LDAPPortCol, idpEvent.Port),
+ handler.NewCol(LDAPTlsCol, idpEvent.TLS),
+ handler.NewCol(LDAPBaseDNCol, idpEvent.BaseDN),
+ handler.NewCol(LDAPUserObjectClassCol, idpEvent.UserObjectClass),
+ handler.NewCol(LDAPUserUniqueAttributeCol, idpEvent.UserUniqueAttribute),
+ handler.NewCol(LDAPAdminCol, idpEvent.Admin),
+ handler.NewCol(LDAPPasswordCol, idpEvent.Password),
+ handler.NewCol(LDAPIDAttributeCol, idpEvent.IDAttribute),
+ handler.NewCol(LDAPFirstNameAttributeCol, idpEvent.FirstNameAttribute),
+ handler.NewCol(LDAPLastNameAttributeCol, idpEvent.LastNameAttribute),
+ handler.NewCol(LDAPDisplayNameAttributeCol, idpEvent.DisplayNameAttribute),
+ handler.NewCol(LDAPNickNameAttributeCol, idpEvent.NickNameAttribute),
+ handler.NewCol(LDAPPreferredUsernameAttributeCol, idpEvent.PreferredUsernameAttribute),
+ handler.NewCol(LDAPEmailAttributeCol, idpEvent.EmailAttribute),
+ handler.NewCol(LDAPEmailVerifiedAttributeCol, idpEvent.EmailVerifiedAttribute),
+ handler.NewCol(LDAPPhoneAttributeCol, idpEvent.PhoneAttribute),
+ handler.NewCol(LDAPPhoneVerifiedAttributeCol, idpEvent.PhoneVerifiedAttribute),
+ handler.NewCol(LDAPPreferredLanguageAttributeCol, idpEvent.PreferredLanguageAttribute),
+ handler.NewCol(LDAPAvatarURLAttributeCol, idpEvent.AvatarURLAttribute),
+ handler.NewCol(LDAPProfileAttributeCol, idpEvent.ProfileAttribute),
+ },
+ crdb.WithTableSuffix(IDPTemplateLDAPSuffix),
+ ),
+ ), nil
+}
+
+func (p *idpTemplateProjection) reduceLDAPIDPChanged(event eventstore.Event) (*handler.Statement, error) {
+ var idpEvent idp.LDAPIDPChangedEvent
+ switch e := event.(type) {
+ case *org.LDAPIDPChangedEvent:
+ idpEvent = e.LDAPIDPChangedEvent
+ case *instance.LDAPIDPChangedEvent:
+ idpEvent = e.LDAPIDPChangedEvent
+ default:
+ return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-p1582ks", "reduce.wrong.event.type %v", []eventstore.EventType{org.LDAPIDPChangedEventType, instance.LDAPIDPChangedEventType})
+ }
+
+ cols := reduceLDAPIDPChangedTemplateColumns(idpEvent)
+ ldapCols := reduceLDAPIDPChangedLDAPColumns(idpEvent)
+
+ ops := make([]func(eventstore.Event) crdb.Exec, 0, 2)
+ ops = append(ops,
+ crdb.AddUpdateStatement(
+ cols,
+ []handler.Condition{
+ handler.NewCond(IDPTemplateIDCol, idpEvent.ID),
+ handler.NewCond(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID),
+ },
+ ),
+ )
+
+ if len(ldapCols) > 0 {
+ ops = append(ops,
+ crdb.AddUpdateStatement(
+ ldapCols,
+ []handler.Condition{
+ handler.NewCond(LDAPIDCol, idpEvent.ID),
+ handler.NewCond(LDAPInstanceIDCol, idpEvent.Aggregate().InstanceID),
+ },
+ crdb.WithTableSuffix(IDPTemplateLDAPSuffix),
+ ),
+ )
+ }
+
+ return crdb.NewMultiStatement(
+ &idpEvent,
+ ops...,
+ ), nil
+}
+
+func (p *idpTemplateProjection) reduceIDPRemoved(event eventstore.Event) (*handler.Statement, error) {
+ var idpEvent idp.RemovedEvent
+ switch e := event.(type) {
+ case *org.IDPRemovedEvent:
+ idpEvent = e.RemovedEvent
+ case *instance.IDPRemovedEvent:
+ idpEvent = e.RemovedEvent
+ default:
+ return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-xbcvwin2", "reduce.wrong.event.type %v", []eventstore.EventType{org.IDPRemovedEventType, instance.IDPRemovedEventType})
+ }
+
+ return crdb.NewDeleteStatement(
+ &idpEvent,
+ []handler.Condition{
+ handler.NewCond(IDPTemplateIDCol, idpEvent.ID),
+ handler.NewCond(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID),
+ },
+ ), nil
+}
+
+func (p *idpTemplateProjection) reduceOwnerRemoved(event eventstore.Event) (*handler.Statement, error) {
+ e, ok := event.(*org.OrgRemovedEvent)
+ if !ok {
+ return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-Jp0D2K", "reduce.wrong.event.type %s", org.OrgRemovedEventType)
+ }
+
+ return crdb.NewUpdateStatement(
+ e,
+ []handler.Column{
+ handler.NewCol(IDPTemplateChangeDateCol, e.CreationDate()),
+ handler.NewCol(IDPTemplateSequenceCol, e.Sequence()),
+ handler.NewCol(IDPTemplateOwnerRemovedCol, true),
+ },
+ []handler.Condition{
+ handler.NewCond(IDPTemplateInstanceIDCol, e.Aggregate().InstanceID),
+ handler.NewCond(IDPTemplateResourceOwnerCol, e.Aggregate().ID),
+ },
+ ), nil
+}
+
+func reduceLDAPIDPChangedTemplateColumns(idpEvent idp.LDAPIDPChangedEvent) []handler.Column {
+ cols := make([]handler.Column, 0, 7)
+ if idpEvent.Name != nil {
+ cols = append(cols, handler.NewCol(IDPTemplateNameCol, *idpEvent.Name))
+ }
+ if idpEvent.IsCreationAllowed != nil {
+ cols = append(cols, handler.NewCol(IDPTemplateIsCreationAllowedCol, *idpEvent.IsCreationAllowed))
+ }
+ if idpEvent.IsLinkingAllowed != nil {
+ cols = append(cols, handler.NewCol(IDPTemplateIsLinkingAllowedCol, *idpEvent.IsLinkingAllowed))
+ }
+ if idpEvent.IsAutoCreation != nil {
+ cols = append(cols, handler.NewCol(IDPTemplateIsAutoCreationCol, *idpEvent.IsAutoCreation))
+ }
+ if idpEvent.IsAutoUpdate != nil {
+ cols = append(cols, handler.NewCol(IDPTemplateIsAutoUpdateCol, *idpEvent.IsAutoUpdate))
+ }
+ return append(cols,
+ handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()),
+ handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()),
+ )
+}
+
+func reduceLDAPIDPChangedLDAPColumns(idpEvent idp.LDAPIDPChangedEvent) []handler.Column {
+ ldapCols := make([]handler.Column, 0, 4)
+ if idpEvent.Host != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPHostCol, *idpEvent.Host))
+ }
+ if idpEvent.Port != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPPortCol, *idpEvent.Port))
+ }
+ if idpEvent.TLS != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPTlsCol, *idpEvent.TLS))
+ }
+ if idpEvent.BaseDN != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPBaseDNCol, *idpEvent.BaseDN))
+ }
+ if idpEvent.UserObjectClass != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPUserObjectClassCol, *idpEvent.UserObjectClass))
+ }
+ if idpEvent.UserUniqueAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPUserUniqueAttributeCol, *idpEvent.UserUniqueAttribute))
+ }
+ if idpEvent.Admin != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPAdminCol, *idpEvent.Admin))
+ }
+ if idpEvent.Password != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPPasswordCol, *idpEvent.Password))
+ }
+ if idpEvent.IDAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPIDAttributeCol, *idpEvent.IDAttribute))
+ }
+ if idpEvent.FirstNameAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPFirstNameAttributeCol, *idpEvent.FirstNameAttribute))
+ }
+ if idpEvent.LastNameAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPLastNameAttributeCol, *idpEvent.LastNameAttribute))
+ }
+ if idpEvent.DisplayNameAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPDisplayNameAttributeCol, *idpEvent.DisplayNameAttribute))
+ }
+ if idpEvent.NickNameAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPNickNameAttributeCol, *idpEvent.NickNameAttribute))
+ }
+ if idpEvent.PreferredUsernameAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPPreferredUsernameAttributeCol, *idpEvent.PreferredUsernameAttribute))
+ }
+ if idpEvent.EmailAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPEmailAttributeCol, *idpEvent.EmailAttribute))
+ }
+ if idpEvent.EmailVerifiedAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPEmailVerifiedAttributeCol, *idpEvent.EmailVerifiedAttribute))
+ }
+ if idpEvent.PhoneAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPPhoneAttributeCol, *idpEvent.PhoneAttribute))
+ }
+ if idpEvent.PhoneVerifiedAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPPhoneVerifiedAttributeCol, *idpEvent.PhoneVerifiedAttribute))
+ }
+ if idpEvent.PreferredLanguageAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPPreferredLanguageAttributeCol, *idpEvent.PreferredLanguageAttribute))
+ }
+ if idpEvent.AvatarURLAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPAvatarURLAttributeCol, *idpEvent.AvatarURLAttribute))
+ }
+ if idpEvent.ProfileAttribute != nil {
+ ldapCols = append(ldapCols, handler.NewCol(LDAPProfileAttributeCol, *idpEvent.ProfileAttribute))
+ }
+ return ldapCols
+}
diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go
new file mode 100644
index 000000000..ed67f0ec2
--- /dev/null
+++ b/internal/query/projection/idp_template_test.go
@@ -0,0 +1,515 @@
+package projection
+
+import (
+ "testing"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/handler"
+ "github.com/zitadel/zitadel/internal/eventstore/repository"
+ "github.com/zitadel/zitadel/internal/repository/instance"
+ "github.com/zitadel/zitadel/internal/repository/org"
+)
+
+func TestIDPTemplateProjection_reducesRemove(t *testing.T) {
+ type args struct {
+ event func(t *testing.T) eventstore.Event
+ }
+ tests := []struct {
+ name string
+ args args
+ reduce func(event eventstore.Event) (*handler.Statement, error)
+ want wantReduce
+ }{
+
+ {
+ name: "instance reduceInstanceRemoved",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(instance.InstanceRemovedEventType),
+ instance.AggregateType,
+ nil,
+ ), instance.InstanceRemovedEventMapper),
+ },
+ reduce: reduceInstanceRemovedHelper(IDPInstanceIDCol),
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("instance"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM projections.idp_templates WHERE (instance_id = $1)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org reduceOwnerRemoved",
+ reduce: (&idpTemplateProjection{}).reduceOwnerRemoved,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.OrgRemovedEventType),
+ org.AggregateType,
+ nil,
+ ), org.OrgRemovedEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE projections.idp_templates SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (resource_owner = $5)",
+ expectedArgs: []interface{}{
+ anyArg{},
+ uint64(15),
+ true,
+ "instance-id",
+ "agg-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org reduceIDPRemoved",
+ reduce: (&idpTemplateProjection{}).reduceIDPRemoved,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.IDPRemovedEventType),
+ org.AggregateType,
+ []byte(`{
+ "id": "idp-id"
+}`),
+ ), org.IDPRemovedEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM projections.idp_templates WHERE (id = $1) AND (instance_id = $2)",
+ expectedArgs: []interface{}{
+ "idp-id",
+ "instance-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ event := baseEvent(t)
+ got, err := tt.reduce(event)
+ if !errors.IsErrorInvalidArgument(err) {
+ t.Errorf("no wrong event mapping: %v, got: %v", err, got)
+ }
+
+ event = tt.args.event(t)
+ got, err = tt.reduce(event)
+ assertReduce(t, got, err, IDPTemplateTable, tt.want)
+ })
+ }
+}
+
+func TestIDPTemplateProjection_reducesLDAP(t *testing.T) {
+ type args struct {
+ event func(t *testing.T) eventstore.Event
+ }
+ tests := []struct {
+ name string
+ args args
+ reduce func(event eventstore.Event) (*handler.Statement, error)
+ want wantReduce
+ }{
+ {
+ name: "instance reduceLDAPIDPAdded",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(instance.LDAPIDPAddedEventType),
+ instance.AggregateType,
+ []byte(`{
+ "id": "idp-id",
+ "name": "custom-zitadel-instance",
+ "host": "host",
+ "port": "port",
+ "tls": true,
+ "baseDN": "base",
+ "userObjectClass": "user",
+ "userUniqueAttribute": "uid",
+ "admin": "admin",
+ "password": {
+ "cryptoType": 0,
+ "algorithm": "RSA-265",
+ "keyId": "key-id"
+ },
+ "idAttribute": "id",
+ "firstNameAttribute": "first",
+ "lastNameAttribute": "last",
+ "displayNameAttribute": "display",
+ "nickNameAttribute": "nickname",
+ "preferredUsernameAttribute": "username",
+ "emailAttribute": "email",
+ "emailVerifiedAttribute": "email_verified",
+ "phoneAttribute": "phone",
+ "phoneVerifiedAttribute": "phone_verified",
+ "preferredLanguageAttribute": "lang",
+ "avatarURLAttribute": "avatar",
+ "profileAttribute": "profile",
+ "isCreationAllowed": true,
+ "isLinkingAllowed": true,
+ "isAutoCreation": true,
+ "isAutoUpdate": true
+}`),
+ ), instance.LDAPIDPAddedEventMapper),
+ },
+ reduce: (&idpTemplateProjection{}).reduceLDAPIDPAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("instance"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "INSERT INTO projections.idp_templates (id, creation_date, change_date, sequence, resource_owner, instance_id, state, name, owner_type, type, is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)",
+ expectedArgs: []interface{}{
+ "idp-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ "ro-id",
+ "instance-id",
+ domain.IDPStateActive,
+ "custom-zitadel-instance",
+ domain.IdentityProviderTypeSystem,
+ domain.IDPTypeLDAP,
+ true,
+ true,
+ true,
+ true,
+ },
+ },
+ {
+ expectedStmt: "INSERT INTO projections.idp_templates_ldap (idp_id, instance_id, host, port, tls, base_dn, user_object_class, user_unique_attribute, admin, password, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)",
+ expectedArgs: []interface{}{
+ "idp-id",
+ "instance-id",
+ "host",
+ "port",
+ true,
+ "base",
+ "user",
+ "uid",
+ "admin",
+ anyArg{},
+ "id",
+ "first",
+ "last",
+ "display",
+ "nickname",
+ "username",
+ "email",
+ "email_verified",
+ "phone",
+ "phone_verified",
+ "lang",
+ "avatar",
+ "profile",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org reduceLDAPIDPAdded",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.LDAPIDPAddedEventType),
+ org.AggregateType,
+ []byte(`{
+ "id": "idp-id",
+ "name": "custom-zitadel-instance",
+ "host": "host",
+ "port": "port",
+ "tls": true,
+ "baseDN": "base",
+ "userObjectClass": "user",
+ "userUniqueAttribute": "uid",
+ "admin": "admin",
+ "password": {
+ "cryptoType": 0,
+ "algorithm": "RSA-265",
+ "keyId": "key-id"
+ },
+ "idAttribute": "id",
+ "firstNameAttribute": "first",
+ "lastNameAttribute": "last",
+ "displayNameAttribute": "display",
+ "nickNameAttribute": "nickname",
+ "preferredUsernameAttribute": "username",
+ "emailAttribute": "email",
+ "emailVerifiedAttribute": "email_verified",
+ "phoneAttribute": "phone",
+ "phoneVerifiedAttribute": "phone_verified",
+ "preferredLanguageAttribute": "lang",
+ "avatarURLAttribute": "avatar",
+ "profileAttribute": "profile",
+ "isCreationAllowed": true,
+ "isLinkingAllowed": true,
+ "isAutoCreation": true,
+ "isAutoUpdate": true
+}`),
+ ), org.LDAPIDPAddedEventMapper),
+ },
+ reduce: (&idpTemplateProjection{}).reduceLDAPIDPAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "INSERT INTO projections.idp_templates (id, creation_date, change_date, sequence, resource_owner, instance_id, state, name, owner_type, type, is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)",
+ expectedArgs: []interface{}{
+ "idp-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ "ro-id",
+ "instance-id",
+ domain.IDPStateActive,
+ "custom-zitadel-instance",
+ domain.IdentityProviderTypeOrg,
+ domain.IDPTypeLDAP,
+ true,
+ true,
+ true,
+ true,
+ },
+ },
+ {
+ expectedStmt: "INSERT INTO projections.idp_templates_ldap (idp_id, instance_id, host, port, tls, base_dn, user_object_class, user_unique_attribute, admin, password, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)",
+ expectedArgs: []interface{}{
+ "idp-id",
+ "instance-id",
+ "host",
+ "port",
+ true,
+ "base",
+ "user",
+ "uid",
+ "admin",
+ anyArg{},
+ "id",
+ "first",
+ "last",
+ "display",
+ "nickname",
+ "username",
+ "email",
+ "email_verified",
+ "phone",
+ "phone_verified",
+ "lang",
+ "avatar",
+ "profile",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "instance reduceLDAPIDPChanged minimal",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(instance.LDAPIDPChangedEventType),
+ instance.AggregateType,
+ []byte(`{
+ "id": "idp-id",
+ "name": "custom-zitadel-instance",
+ "host": "host"
+}`),
+ ), instance.LDAPIDPChangedEventMapper),
+ },
+ reduce: (&idpTemplateProjection{}).reduceLDAPIDPChanged,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("instance"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE projections.idp_templates SET (name, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
+ expectedArgs: []interface{}{
+ "custom-zitadel-instance",
+ anyArg{},
+ uint64(15),
+ "idp-id",
+ "instance-id",
+ },
+ },
+ {
+ expectedStmt: "UPDATE projections.idp_templates_ldap SET host = $1 WHERE (idp_id = $2) AND (instance_id = $3)",
+ expectedArgs: []interface{}{
+ "host",
+ "idp-id",
+ "instance-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "instance reduceLDAPIDPChanged",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(instance.LDAPIDPChangedEventType),
+ instance.AggregateType,
+ []byte(`{
+ "id": "idp-id",
+ "name": "custom-zitadel-instance",
+ "host": "host",
+ "port": "port",
+ "tls": true,
+ "baseDN": "base",
+ "userObjectClass": "user",
+ "userUniqueAttribute": "uid",
+ "admin": "admin",
+ "password": {
+ "cryptoType": 0,
+ "algorithm": "RSA-265",
+ "keyId": "key-id"
+ },
+ "idAttribute": "id",
+ "firstNameAttribute": "first",
+ "lastNameAttribute": "last",
+ "displayNameAttribute": "display",
+ "nickNameAttribute": "nickname",
+ "preferredUsernameAttribute": "username",
+ "emailAttribute": "email",
+ "emailVerifiedAttribute": "email_verified",
+ "phoneAttribute": "phone",
+ "phoneVerifiedAttribute": "phone_verified",
+ "preferredLanguageAttribute": "lang",
+ "avatarURLAttribute": "avatar",
+ "profileAttribute": "profile",
+ "isCreationAllowed": true,
+ "isLinkingAllowed": true,
+ "isAutoCreation": true,
+ "isAutoUpdate": true
+}`),
+ ), instance.LDAPIDPChangedEventMapper),
+ },
+ reduce: (&idpTemplateProjection{}).reduceLDAPIDPChanged,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("instance"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE projections.idp_templates SET (name, is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update, change_date, sequence) = ($1, $2, $3, $4, $5, $6, $7) WHERE (id = $8) AND (instance_id = $9)",
+ expectedArgs: []interface{}{
+ "custom-zitadel-instance",
+ true,
+ true,
+ true,
+ true,
+ anyArg{},
+ uint64(15),
+ "idp-id",
+ "instance-id",
+ },
+ },
+ {
+ expectedStmt: "UPDATE projections.idp_templates_ldap SET (host, port, tls, base_dn, user_object_class, user_unique_attribute, admin, password, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) WHERE (idp_id = $22) AND (instance_id = $23)",
+ expectedArgs: []interface{}{
+ "host",
+ "port",
+ true,
+ "base",
+ "user",
+ "uid",
+ "admin",
+ anyArg{},
+ "id",
+ "first",
+ "last",
+ "display",
+ "nickname",
+ "username",
+ "email",
+ "email_verified",
+ "phone",
+ "phone_verified",
+ "lang",
+ "avatar",
+ "profile",
+ "idp-id",
+ "instance-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org.reduceOwnerRemoved",
+ reduce: (&idpProjection{}).reduceOwnerRemoved,
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.OrgRemovedEventType),
+ org.AggregateType,
+ nil,
+ ), org.OrgRemovedEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE projections.idp_templates SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (resource_owner = $5)",
+ expectedArgs: []interface{}{
+ anyArg{},
+ uint64(15),
+ true,
+ "instance-id",
+ "agg-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ event := baseEvent(t)
+ got, err := tt.reduce(event)
+ if !errors.IsErrorInvalidArgument(err) {
+ t.Errorf("no wrong event mapping: %v, got: %v", err, got)
+ }
+
+ event = tt.args.event(t)
+ got, err = tt.reduce(event)
+ assertReduce(t, got, err, IDPTemplateTable, tt.want)
+ })
+ }
+}
diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go
index 20d263079..fd59ebfd4 100644
--- a/internal/query/projection/projection.go
+++ b/internal/query/projection/projection.go
@@ -37,6 +37,7 @@ var (
AppProjection *appProjection
IDPUserLinkProjection *idpUserLinkProjection
IDPLoginPolicyLinkProjection *idpLoginPolicyLinkProjection
+ IDPTemplateProjection *idpTemplateProjection
MailTemplateProjection *mailTemplateProjection
MessageTextProjection *messageTextProjection
CustomTextProjection *customTextProjection
@@ -111,6 +112,7 @@ func Create(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, c
AppProjection = newAppProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["apps"]))
IDPUserLinkProjection = newIDPUserLinkProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["idp_user_links"]))
IDPLoginPolicyLinkProjection = newIDPLoginPolicyLinkProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["idp_login_policy_links"]))
+ IDPTemplateProjection = newIDPTemplateProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["idp_templates"]))
MailTemplateProjection = newMailTemplateProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["mail_templates"]))
MessageTextProjection = newMessageTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["message_texts"]))
CustomTextProjection = newCustomTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["custom_texts"]))
@@ -199,6 +201,7 @@ func newProjectionsList() {
OrgDomainProjection,
LoginPolicyProjection,
IDPProjection,
+ IDPTemplateProjection,
AppProjection,
IDPUserLinkProjection,
IDPLoginPolicyLinkProjection,
diff --git a/internal/repository/idp/idp.go b/internal/repository/idp/idp.go
new file mode 100644
index 000000000..4b1fa9d81
--- /dev/null
+++ b/internal/repository/idp/idp.go
@@ -0,0 +1,104 @@
+package idp
+
+import (
+ "encoding/json"
+
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/repository"
+ "github.com/zitadel/zitadel/internal/repository/idpconfig"
+)
+
+type Options struct {
+ IsCreationAllowed bool `json:"isCreationAllowed,omitempty"`
+ IsLinkingAllowed bool `json:"isLinkingAllowed,omitempty"`
+ IsAutoCreation bool `json:"isAutoCreation,omitempty"`
+ IsAutoUpdate bool `json:"isAutoUpdate,omitempty"`
+}
+
+type OptionChanges struct {
+ IsCreationAllowed *bool `json:"isCreationAllowed,omitempty"`
+ IsLinkingAllowed *bool `json:"isLinkingAllowed,omitempty"`
+ IsAutoCreation *bool `json:"isAutoCreation,omitempty"`
+ IsAutoUpdate *bool `json:"isAutoUpdate,omitempty"`
+}
+
+func (o *Options) Changes(options Options) OptionChanges {
+ opts := OptionChanges{}
+ if o.IsCreationAllowed != options.IsCreationAllowed {
+ opts.IsCreationAllowed = &options.IsCreationAllowed
+ }
+ if o.IsLinkingAllowed != options.IsLinkingAllowed {
+ opts.IsLinkingAllowed = &options.IsLinkingAllowed
+ }
+ if o.IsAutoCreation != options.IsAutoCreation {
+ opts.IsAutoCreation = &options.IsAutoCreation
+ }
+ if o.IsAutoUpdate != options.IsAutoUpdate {
+ opts.IsAutoUpdate = &options.IsAutoUpdate
+ }
+ return opts
+}
+
+func (o *Options) ReduceChanges(changes OptionChanges) {
+ if changes.IsCreationAllowed != nil {
+ o.IsCreationAllowed = *changes.IsCreationAllowed
+ }
+ if changes.IsLinkingAllowed != nil {
+ o.IsLinkingAllowed = *changes.IsLinkingAllowed
+ }
+ if changes.IsAutoUpdate != nil {
+ o.IsAutoUpdate = *changes.IsAutoUpdate
+ }
+ if changes.IsAutoUpdate != nil {
+ o.IsAutoUpdate = *changes.IsAutoUpdate
+ }
+}
+
+func (o *OptionChanges) IsZero() bool {
+ return o.IsCreationAllowed == nil && o.IsLinkingAllowed == nil && o.IsAutoCreation == nil && o.IsAutoUpdate == nil
+}
+
+type RemovedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ ID string `json:"id"`
+
+ name string
+}
+
+func NewRemovedEvent(
+ base *eventstore.BaseEvent,
+ id string,
+ name string,
+) *RemovedEvent {
+ return &RemovedEvent{
+ BaseEvent: *base,
+ ID: id,
+ name: name,
+ }
+}
+
+func (e *RemovedEvent) Data() interface{} {
+ return e
+}
+
+func (e *RemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ if e.name == "" {
+ return nil
+ }
+ return []*eventstore.EventUniqueConstraint{idpconfig.NewRemoveIDPConfigNameUniqueConstraint(e.name, e.Aggregate().ResourceOwner)}
+}
+
+func RemovedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ e := &RemovedEvent{
+ BaseEvent: *eventstore.BaseEventFromRepo(event),
+ }
+
+ err := json.Unmarshal(event.Data, e)
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "IDP-plSD2", "unable to unmarshal event")
+ }
+
+ return e, nil
+}
diff --git a/internal/repository/idp/ldap.go b/internal/repository/idp/ldap.go
new file mode 100644
index 000000000..99e8bd1ff
--- /dev/null
+++ b/internal/repository/idp/ldap.go
@@ -0,0 +1,351 @@
+package idp
+
+import (
+ "encoding/json"
+
+ "github.com/zitadel/zitadel/internal/crypto"
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/repository"
+ "github.com/zitadel/zitadel/internal/repository/idpconfig"
+)
+
+type LDAPIDPAddedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Host string `json:"host"`
+ Port string `json:"port,omitempty"`
+ TLS bool `json:"tls"`
+ BaseDN string `json:"baseDN"`
+ UserObjectClass string `json:"userObjectClass"`
+ UserUniqueAttribute string `json:"userUniqueAttribute"`
+ Admin string `json:"admin"`
+ Password *crypto.CryptoValue `json:"password"`
+
+ LDAPAttributes
+ Options
+}
+
+type LDAPAttributes struct {
+ IDAttribute string `json:"idAttribute,omitempty"`
+ FirstNameAttribute string `json:"firstNameAttribute,omitempty"`
+ LastNameAttribute string `json:"lastNameAttribute,omitempty"`
+ DisplayNameAttribute string `json:"displayNameAttribute,omitempty"`
+ NickNameAttribute string `json:"nickNameAttribute,omitempty"`
+ PreferredUsernameAttribute string `json:"preferredUsernameAttribute,omitempty"`
+ EmailAttribute string `json:"emailAttribute,omitempty"`
+ EmailVerifiedAttribute string `json:"emailVerifiedAttribute,omitempty"`
+ PhoneAttribute string `json:"phoneAttribute,omitempty"`
+ PhoneVerifiedAttribute string `json:"phoneVerifiedAttribute,omitempty"`
+ PreferredLanguageAttribute string `json:"preferredLanguageAttribute,omitempty"`
+ AvatarURLAttribute string `json:"avatarURLAttribute,omitempty"`
+ ProfileAttribute string `json:"profileAttribute,omitempty"`
+}
+
+func (o *LDAPAttributes) Changes(attributes LDAPAttributes) LDAPAttributeChanges {
+ attrs := LDAPAttributeChanges{}
+ if o.IDAttribute != attributes.IDAttribute {
+ attrs.IDAttribute = &attributes.IDAttribute
+ }
+ if o.FirstNameAttribute != attributes.FirstNameAttribute {
+ attrs.FirstNameAttribute = &attributes.FirstNameAttribute
+ }
+ if o.LastNameAttribute != attributes.LastNameAttribute {
+ attrs.LastNameAttribute = &attributes.LastNameAttribute
+ }
+ if o.DisplayNameAttribute != attributes.DisplayNameAttribute {
+ attrs.DisplayNameAttribute = &attributes.DisplayNameAttribute
+ }
+ if o.NickNameAttribute != attributes.NickNameAttribute {
+ attrs.NickNameAttribute = &attributes.NickNameAttribute
+ }
+ if o.PreferredUsernameAttribute != attributes.PreferredUsernameAttribute {
+ attrs.PreferredUsernameAttribute = &attributes.PreferredUsernameAttribute
+ }
+ if o.EmailAttribute != attributes.EmailAttribute {
+ attrs.EmailAttribute = &attributes.EmailAttribute
+ }
+ if o.EmailVerifiedAttribute != attributes.EmailVerifiedAttribute {
+ attrs.EmailVerifiedAttribute = &attributes.EmailVerifiedAttribute
+ }
+ if o.PhoneAttribute != attributes.PhoneAttribute {
+ attrs.PhoneAttribute = &attributes.PhoneAttribute
+ }
+ if o.PhoneVerifiedAttribute != attributes.PhoneVerifiedAttribute {
+ attrs.PhoneVerifiedAttribute = &attributes.PhoneVerifiedAttribute
+ }
+ if o.PreferredLanguageAttribute != attributes.PreferredLanguageAttribute {
+ attrs.PreferredLanguageAttribute = &attributes.PreferredLanguageAttribute
+ }
+ if o.AvatarURLAttribute != attributes.AvatarURLAttribute {
+ attrs.AvatarURLAttribute = &attributes.AvatarURLAttribute
+ }
+ if o.ProfileAttribute != attributes.ProfileAttribute {
+ attrs.ProfileAttribute = &attributes.ProfileAttribute
+ }
+ return attrs
+}
+
+func (o *LDAPAttributes) ReduceChanges(changes LDAPAttributeChanges) {
+ if changes.IDAttribute != nil {
+ o.IDAttribute = *changes.IDAttribute
+ }
+ if changes.FirstNameAttribute != nil {
+ o.FirstNameAttribute = *changes.FirstNameAttribute
+ }
+ if changes.LastNameAttribute != nil {
+ o.LastNameAttribute = *changes.LastNameAttribute
+ }
+ if changes.DisplayNameAttribute != nil {
+ o.DisplayNameAttribute = *changes.DisplayNameAttribute
+ }
+ if changes.NickNameAttribute != nil {
+ o.NickNameAttribute = *changes.NickNameAttribute
+ }
+ if changes.PreferredUsernameAttribute != nil {
+ o.PreferredUsernameAttribute = *changes.PreferredUsernameAttribute
+ }
+ if changes.EmailAttribute != nil {
+ o.EmailAttribute = *changes.EmailAttribute
+ }
+ if changes.EmailVerifiedAttribute != nil {
+ o.EmailVerifiedAttribute = *changes.EmailVerifiedAttribute
+ }
+ if changes.PhoneAttribute != nil {
+ o.PhoneAttribute = *changes.PhoneAttribute
+ }
+ if changes.PhoneVerifiedAttribute != nil {
+ o.PhoneVerifiedAttribute = *changes.PhoneVerifiedAttribute
+ }
+ if changes.PreferredLanguageAttribute != nil {
+ o.PreferredLanguageAttribute = *changes.PreferredLanguageAttribute
+ }
+ if changes.AvatarURLAttribute != nil {
+ o.AvatarURLAttribute = *changes.AvatarURLAttribute
+ }
+ if changes.ProfileAttribute != nil {
+ o.ProfileAttribute = *changes.ProfileAttribute
+ }
+}
+
+func NewLDAPIDPAddedEvent(
+ base *eventstore.BaseEvent,
+ id,
+ name,
+ host,
+ port string,
+ tls bool,
+ baseDN,
+ userObjectClass,
+ userUniqueAttribute,
+ admin string,
+ password *crypto.CryptoValue,
+ attributes LDAPAttributes,
+ options Options,
+) *LDAPIDPAddedEvent {
+ return &LDAPIDPAddedEvent{
+ BaseEvent: *base,
+ ID: id,
+ Name: name,
+ Host: host,
+ Port: port,
+ TLS: tls,
+ BaseDN: baseDN,
+ UserObjectClass: userObjectClass,
+ UserUniqueAttribute: userUniqueAttribute,
+ Admin: admin,
+ Password: password,
+ LDAPAttributes: attributes,
+ Options: options,
+ }
+}
+
+func (e *LDAPIDPAddedEvent) Data() interface{} {
+ return e
+}
+
+func (e *LDAPIDPAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return []*eventstore.EventUniqueConstraint{idpconfig.NewAddIDPConfigNameUniqueConstraint(e.Name, e.Aggregate().ResourceOwner)}
+}
+
+func LDAPIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ e := &LDAPIDPAddedEvent{
+ BaseEvent: *eventstore.BaseEventFromRepo(event),
+ }
+
+ err := json.Unmarshal(event.Data, e)
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "IDP-Dgh42", "unable to unmarshal event")
+ }
+
+ return e, nil
+}
+
+type LDAPIDPChangedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ oldName string
+
+ ID string `json:"id"`
+ Name *string `json:"name,omitempty"`
+ Host *string `json:"host,omitempty"`
+ Port *string `json:"port,omitempty"`
+ TLS *bool `json:"tls,omitempty"`
+ BaseDN *string `json:"baseDN,omitempty"`
+ UserObjectClass *string `json:"userObjectClass,omitempty"`
+ UserUniqueAttribute *string `json:"userUniqueAttribute,omitempty"`
+ Admin *string `json:"admin,omitempty"`
+ Password *crypto.CryptoValue `json:"password,omitempty"`
+
+ LDAPAttributeChanges
+ OptionChanges
+}
+
+type LDAPAttributeChanges struct {
+ IDAttribute *string `json:"idAttribute,omitempty"`
+ FirstNameAttribute *string `json:"firstNameAttribute,omitempty"`
+ LastNameAttribute *string `json:"lastNameAttribute,omitempty"`
+ DisplayNameAttribute *string `json:"displayNameAttribute,omitempty"`
+ NickNameAttribute *string `json:"nickNameAttribute,omitempty"`
+ PreferredUsernameAttribute *string `json:"preferredUsernameAttribute,omitempty"`
+ EmailAttribute *string `json:"emailAttribute,omitempty"`
+ EmailVerifiedAttribute *string `json:"emailVerifiedAttribute,omitempty"`
+ PhoneAttribute *string `json:"phoneAttribute,omitempty"`
+ PhoneVerifiedAttribute *string `json:"phoneVerifiedAttribute,omitempty"`
+ PreferredLanguageAttribute *string `json:"preferredLanguageAttribute,omitempty"`
+ AvatarURLAttribute *string `json:"avatarURLAttribute,omitempty"`
+ ProfileAttribute *string `json:"profileAttribute,omitempty"`
+}
+
+func (o LDAPAttributeChanges) IsZero() bool {
+ return o.IDAttribute == nil &&
+ o.FirstNameAttribute == nil &&
+ o.LastNameAttribute == nil &&
+ o.DisplayNameAttribute == nil &&
+ o.NickNameAttribute == nil &&
+ o.PreferredUsernameAttribute == nil &&
+ o.EmailAttribute == nil &&
+ o.EmailVerifiedAttribute == nil &&
+ o.PhoneAttribute == nil &&
+ o.PhoneVerifiedAttribute == nil &&
+ o.PreferredLanguageAttribute == nil &&
+ o.AvatarURLAttribute == nil &&
+ o.ProfileAttribute == nil
+}
+
+func NewLDAPIDPChangedEvent(
+ base *eventstore.BaseEvent,
+ id string,
+ oldName string,
+ changes []LDAPIDPChanges,
+) (*LDAPIDPChangedEvent, error) {
+ if len(changes) == 0 {
+ return nil, errors.ThrowPreconditionFailed(nil, "IDP-SDf3f", "Errors.NoChangesFound")
+ }
+ changedEvent := &LDAPIDPChangedEvent{
+ BaseEvent: *base,
+ ID: id,
+ oldName: oldName,
+ }
+ for _, change := range changes {
+ change(changedEvent)
+ }
+ return changedEvent, nil
+}
+
+type LDAPIDPChanges func(*LDAPIDPChangedEvent)
+
+func ChangeLDAPName(name string) func(*LDAPIDPChangedEvent) {
+ return func(e *LDAPIDPChangedEvent) {
+ e.Name = &name
+ }
+}
+
+func ChangeLDAPHost(host string) func(*LDAPIDPChangedEvent) {
+ return func(e *LDAPIDPChangedEvent) {
+ e.Host = &host
+ }
+}
+
+func ChangeLDAPPort(port string) func(*LDAPIDPChangedEvent) {
+ return func(e *LDAPIDPChangedEvent) {
+ e.Port = &port
+ }
+}
+
+func ChangeLDAPTLS(tls bool) func(*LDAPIDPChangedEvent) {
+ return func(e *LDAPIDPChangedEvent) {
+ e.TLS = &tls
+ }
+}
+
+func ChangeLDAPBaseDN(basDN string) func(*LDAPIDPChangedEvent) {
+ return func(e *LDAPIDPChangedEvent) {
+ e.BaseDN = &basDN
+ }
+}
+
+func ChangeLDAPUserObjectClass(userObjectClass string) func(*LDAPIDPChangedEvent) {
+ return func(e *LDAPIDPChangedEvent) {
+ e.UserObjectClass = &userObjectClass
+ }
+}
+
+func ChangeLDAPUserUniqueAttribute(userUniqueAttribute string) func(*LDAPIDPChangedEvent) {
+ return func(e *LDAPIDPChangedEvent) {
+ e.UserUniqueAttribute = &userUniqueAttribute
+ }
+}
+
+func ChangeLDAPAdmin(admin string) func(*LDAPIDPChangedEvent) {
+ return func(e *LDAPIDPChangedEvent) {
+ e.Admin = &admin
+ }
+}
+
+func ChangeLDAPPassword(password *crypto.CryptoValue) func(*LDAPIDPChangedEvent) {
+ return func(e *LDAPIDPChangedEvent) {
+ e.Password = password
+ }
+}
+
+func ChangeLDAPAttributes(attributes LDAPAttributeChanges) func(*LDAPIDPChangedEvent) {
+ return func(e *LDAPIDPChangedEvent) {
+ e.LDAPAttributeChanges = attributes
+ }
+}
+
+func ChangeLDAPOptions(options OptionChanges) func(*LDAPIDPChangedEvent) {
+ return func(e *LDAPIDPChangedEvent) {
+ e.OptionChanges = options
+ }
+}
+
+func (e *LDAPIDPChangedEvent) Data() interface{} {
+ return e
+}
+
+func (e *LDAPIDPChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ if e.Name == nil || e.oldName == *e.Name { // TODO: nil check should be enough?
+ return nil
+ }
+ return []*eventstore.EventUniqueConstraint{
+ idpconfig.NewRemoveIDPConfigNameUniqueConstraint(e.oldName, e.Aggregate().ResourceOwner),
+ idpconfig.NewAddIDPConfigNameUniqueConstraint(*e.Name, e.Aggregate().ResourceOwner),
+ }
+}
+
+func LDAPIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ e := &LDAPIDPChangedEvent{
+ BaseEvent: *eventstore.BaseEventFromRepo(event),
+ }
+
+ err := json.Unmarshal(event.Data, e)
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "IDP-Sfth3", "unable to unmarshal event")
+ }
+
+ return e, nil
+}
diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go
index 4be0a9e02..642044619 100644
--- a/internal/repository/instance/eventstore.go
+++ b/internal/repository/instance/eventstore.go
@@ -70,6 +70,9 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, IDPOIDCConfigChangedEventType, IDPOIDCConfigChangedEventMapper).
RegisterFilterEventMapper(AggregateType, IDPJWTConfigAddedEventType, IDPJWTConfigAddedEventMapper).
RegisterFilterEventMapper(AggregateType, IDPJWTConfigChangedEventType, IDPJWTConfigChangedEventMapper).
+ RegisterFilterEventMapper(AggregateType, LDAPIDPAddedEventType, LDAPIDPAddedEventMapper).
+ RegisterFilterEventMapper(AggregateType, LDAPIDPChangedEventType, LDAPIDPChangedEventMapper).
+ RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderAddedEventType, IdentityProviderAddedEventMapper).
RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderRemovedEventType, IdentityProviderRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderCascadeRemovedEventType, IdentityProviderCascadeRemovedEventMapper).
diff --git a/internal/repository/instance/idp.go b/internal/repository/instance/idp.go
new file mode 100644
index 000000000..a0ae9580a
--- /dev/null
+++ b/internal/repository/instance/idp.go
@@ -0,0 +1,142 @@
+package instance
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/crypto"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/repository"
+ "github.com/zitadel/zitadel/internal/repository/idp"
+)
+
+const (
+ LDAPIDPAddedEventType eventstore.EventType = "instance.idp.ldap.added"
+ LDAPIDPChangedEventType eventstore.EventType = "instance.idp.ldap.changed"
+ IDPRemovedEventType eventstore.EventType = "instance.idp.removed"
+)
+
+type LDAPIDPAddedEvent struct {
+ idp.LDAPIDPAddedEvent
+}
+
+func NewLDAPIDPAddedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ id,
+ name,
+ host,
+ port string,
+ tls bool,
+ baseDN,
+ userObjectClass,
+ userUniqueAttribute,
+ admin string,
+ password *crypto.CryptoValue,
+ attributes idp.LDAPAttributes,
+ options idp.Options,
+) *LDAPIDPAddedEvent {
+
+ return &LDAPIDPAddedEvent{
+ LDAPIDPAddedEvent: *idp.NewLDAPIDPAddedEvent(
+ eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ LDAPIDPAddedEventType,
+ ),
+ id,
+ name,
+ host,
+ port,
+ tls,
+ baseDN,
+ userObjectClass,
+ userUniqueAttribute,
+ admin,
+ password,
+ attributes,
+ options,
+ ),
+ }
+}
+
+func LDAPIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ e, err := idp.LDAPIDPAddedEventMapper(event)
+ if err != nil {
+ return nil, err
+ }
+
+ return &LDAPIDPAddedEvent{LDAPIDPAddedEvent: *e.(*idp.LDAPIDPAddedEvent)}, nil
+}
+
+type LDAPIDPChangedEvent struct {
+ idp.LDAPIDPChangedEvent
+}
+
+func NewLDAPIDPChangedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ id,
+ oldName string,
+ changes []idp.LDAPIDPChanges,
+) (*LDAPIDPChangedEvent, error) {
+
+ changedEvent, err := idp.NewLDAPIDPChangedEvent(
+ eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ LDAPIDPChangedEventType,
+ ),
+ id,
+ oldName,
+ changes,
+ )
+ if err != nil {
+ return nil, err
+ }
+ return &LDAPIDPChangedEvent{LDAPIDPChangedEvent: *changedEvent}, nil
+}
+
+func LDAPIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ e, err := idp.LDAPIDPChangedEventMapper(event)
+ if err != nil {
+ return nil, err
+ }
+
+ return &LDAPIDPChangedEvent{LDAPIDPChangedEvent: *e.(*idp.LDAPIDPChangedEvent)}, nil
+}
+
+type IDPRemovedEvent struct {
+ idp.RemovedEvent
+}
+
+func NewIDPRemovedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ id string,
+ name string,
+) *IDPRemovedEvent {
+ return &IDPRemovedEvent{
+ RemovedEvent: *idp.NewRemovedEvent(
+ eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ IDPRemovedEventType,
+ ),
+ id,
+ name,
+ ),
+ }
+}
+
+func (e *IDPRemovedEvent) Data() interface{} {
+ return e
+}
+
+func IDPRemovedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ e, err := idp.RemovedEventMapper(event)
+ if err != nil {
+ return nil, err
+ }
+
+ return &IDPRemovedEvent{RemovedEvent: *e.(*idp.RemovedEvent)}, nil
+}
diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go
index fecf31bd3..4e8d556cc 100644
--- a/internal/repository/org/eventstore.go
+++ b/internal/repository/org/eventstore.go
@@ -78,6 +78,9 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, IDPOIDCConfigChangedEventType, IDPOIDCConfigChangedEventMapper).
RegisterFilterEventMapper(AggregateType, IDPJWTConfigAddedEventType, IDPJWTConfigAddedEventMapper).
RegisterFilterEventMapper(AggregateType, IDPJWTConfigChangedEventType, IDPJWTConfigChangedEventMapper).
+ RegisterFilterEventMapper(AggregateType, LDAPIDPAddedEventType, LDAPIDPAddedEventMapper).
+ RegisterFilterEventMapper(AggregateType, LDAPIDPChangedEventType, LDAPIDPChangedEventMapper).
+ RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, TriggerActionsSetEventType, TriggerActionsSetEventMapper).
RegisterFilterEventMapper(AggregateType, TriggerActionsCascadeRemovedEventType, TriggerActionsCascadeRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, FlowClearedEventType, FlowClearedEventMapper).
diff --git a/internal/repository/org/idp.go b/internal/repository/org/idp.go
new file mode 100644
index 000000000..fe3b971d7
--- /dev/null
+++ b/internal/repository/org/idp.go
@@ -0,0 +1,142 @@
+package org
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/crypto"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/repository"
+ "github.com/zitadel/zitadel/internal/repository/idp"
+)
+
+const (
+ LDAPIDPAddedEventType eventstore.EventType = "org.idp.ldap.added"
+ LDAPIDPChangedEventType eventstore.EventType = "org.idp.ldap.changed"
+ IDPRemovedEventType eventstore.EventType = "org.idp.removed"
+)
+
+type LDAPIDPAddedEvent struct {
+ idp.LDAPIDPAddedEvent
+}
+
+func NewLDAPIDPAddedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ id,
+ name,
+ host,
+ port string,
+ tls bool,
+ baseDN,
+ userObjectClass,
+ userUniqueAttribute,
+ admin string,
+ password *crypto.CryptoValue,
+ attributes idp.LDAPAttributes,
+ options idp.Options,
+) *LDAPIDPAddedEvent {
+
+ return &LDAPIDPAddedEvent{
+ LDAPIDPAddedEvent: *idp.NewLDAPIDPAddedEvent(
+ eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ LDAPIDPAddedEventType,
+ ),
+ id,
+ name,
+ host,
+ port,
+ tls,
+ baseDN,
+ userObjectClass,
+ userUniqueAttribute,
+ admin,
+ password,
+ attributes,
+ options,
+ ),
+ }
+}
+
+func LDAPIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ e, err := idp.LDAPIDPAddedEventMapper(event)
+ if err != nil {
+ return nil, err
+ }
+
+ return &LDAPIDPAddedEvent{LDAPIDPAddedEvent: *e.(*idp.LDAPIDPAddedEvent)}, nil
+}
+
+type LDAPIDPChangedEvent struct {
+ idp.LDAPIDPChangedEvent
+}
+
+func NewLDAPIDPChangedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ id,
+ oldName string,
+ changes []idp.LDAPIDPChanges,
+) (*LDAPIDPChangedEvent, error) {
+
+ changedEvent, err := idp.NewLDAPIDPChangedEvent(
+ eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ LDAPIDPChangedEventType,
+ ),
+ id,
+ oldName,
+ changes,
+ )
+ if err != nil {
+ return nil, err
+ }
+ return &LDAPIDPChangedEvent{LDAPIDPChangedEvent: *changedEvent}, nil
+}
+
+func LDAPIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ e, err := idp.LDAPIDPChangedEventMapper(event)
+ if err != nil {
+ return nil, err
+ }
+
+ return &LDAPIDPChangedEvent{LDAPIDPChangedEvent: *e.(*idp.LDAPIDPChangedEvent)}, nil
+}
+
+type IDPRemovedEvent struct {
+ idp.RemovedEvent
+}
+
+func NewIDPRemovedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ id string,
+ name string,
+) *IDPRemovedEvent {
+ return &IDPRemovedEvent{
+ RemovedEvent: *idp.NewRemovedEvent(
+ eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ IDPRemovedEventType,
+ ),
+ id,
+ name,
+ ),
+ }
+}
+
+func (e *IDPRemovedEvent) Data() interface{} {
+ return e
+}
+
+func IDPRemovedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ e, err := idp.RemovedEventMapper(event)
+ if err != nil {
+ return nil, err
+ }
+
+ return &IDPRemovedEvent{RemovedEvent: *e.(*idp.RemovedEvent)}, nil
+}
diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto
index 4a34848ef..0679533ea 100644
--- a/proto/zitadel/admin.proto
+++ b/proto/zitadel/admin.proto
@@ -1009,6 +1009,67 @@ service AdminService {
};
}
+ // Returns all identity providers, which match the query
+ // Limit should always be set, there is a default limit set by the service
+ rpc ListProviders(ListProvidersRequest) returns (ListProvidersResponse) {
+ option (google.api.http) = {
+ post: "/idps/templates/_search"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.idp.read"
+ };
+ }
+
+ // Returns an identity provider of the instance
+ rpc GetProviderByID(GetProviderByIDRequest) returns (GetProviderByIDResponse) {
+ option (google.api.http) = {
+ get: "/idps/templates/{id}"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.idp.read"
+ };
+ }
+
+ // Add a new ldap identity provider on the instance
+ rpc AddLDAPProvider(AddLDAPProviderRequest) returns (AddLDAPProviderResponse) {
+ option (google.api.http) = {
+ post: "/idps/ldap"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.idp.write"
+ };
+ }
+
+ // Change an existing ldap identity provider on the instance
+ rpc UpdateLDAPProvider(UpdateLDAPProviderRequest) returns (UpdateLDAPProviderResponse) {
+ option (google.api.http) = {
+ post: "/idps/ldap/{id}"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.idp.write"
+ };
+ }
+
+ // Remove an identity provider
+ // Will remove all linked providers of this configuration on the users
+ rpc DeleteProvider(DeleteProviderRequest) returns (DeleteProviderResponse) {
+ option (google.api.http) = {
+ post: "/idps/templates/{id}"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.idp.write"
+ };
+ }
+
//deprecated: please use DomainPolicy instead
//Returns the Org IAM policy defined by the administrators of ZITADEL
rpc GetOrgIAMPolicy(GetOrgIAMPolicyRequest) returns (GetOrgIAMPolicyResponse) {
@@ -3623,6 +3684,79 @@ message UpdateIDPJWTConfigResponse {
zitadel.v1.ObjectDetails details = 1;
}
+message ListProvidersRequest {
+ //list limitations and ordering
+ zitadel.v1.ListQuery query = 1;
+ //criteria the client is looking for
+ repeated ProviderQuery queries = 2;
+}
+
+message ProviderQuery {
+ oneof query {
+ zitadel.idp.v1.IDPIDQuery idp_id_query = 1;
+ zitadel.idp.v1.IDPNameQuery idp_name_query = 2;
+ }
+}
+
+message ListProvidersResponse {
+ zitadel.v1.ListDetails details = 1;
+ repeated zitadel.idp.v1.Provider result = 2;
+}
+
+message GetProviderByIDRequest {
+ string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message GetProviderByIDResponse {
+ zitadel.idp.v1.Provider idp = 1;
+}
+
+message AddLDAPProviderRequest {
+ string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string host = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string port = 3 [(validate.rules).string = {max_len: 5}];
+ bool tls = 4;
+ string base_dn = 5 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string user_object_class = 6 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string user_unique_attribute = 7 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string admin = 8 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string password = 9 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ zitadel.idp.v1.LDAPAttributes attributes = 10;
+ zitadel.idp.v1.Options provider_options = 11;
+}
+
+message AddLDAPProviderResponse {
+ zitadel.v1.ObjectDetails details = 1;
+ string id = 2;
+}
+
+message UpdateLDAPProviderRequest {
+ string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string host = 3 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string port = 4 [(validate.rules).string = {max_len: 5}];
+ bool tls = 5;
+ string base_dn = 6 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string user_object_class = 7 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string user_unique_attribute = 8 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string admin = 9 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string password = 10 [(validate.rules).string = {max_len: 200}];
+ zitadel.idp.v1.LDAPAttributes attributes = 11;
+ zitadel.idp.v1.Options provider_options = 12;
+}
+
+message UpdateLDAPProviderResponse {
+ zitadel.v1.ObjectDetails details = 1;
+}
+
+message DeleteProviderRequest {
+ string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message DeleteProviderResponse {
+ zitadel.v1.ObjectDetails details = 1;
+}
+
message GetOrgIAMPolicyRequest {}
message GetOrgIAMPolicyResponse {
diff --git a/proto/zitadel/idp.proto b/proto/zitadel/idp.proto
index 1b7f9aac1..94a3e622d 100644
--- a/proto/zitadel/idp.proto
+++ b/proto/zitadel/idp.proto
@@ -233,3 +233,70 @@ enum IDPFieldName {
IDP_FIELD_NAME_UNSPECIFIED = 0;
IDP_FIELD_NAME_NAME = 1;
}
+
+message Provider {
+ string id = 1;
+ zitadel.v1.ObjectDetails details = 2;
+ IDPState state = 3;
+ string name = 4;
+ IDPOwnerType owner = 5;
+ ProviderType type = 6;
+ ProviderConfig config = 7;
+}
+
+enum ProviderType {
+ PROVIDER_TYPE_UNSPECIFIED = 0;
+ PROVIDER_TYPE_OIDC = 1;
+ PROVIDER_TYPE_JWT = 2;
+ PROVIDER_TYPE_LDAP = 3;
+ PROVIDER_TYPE_OAUTH = 4;
+ PROVIDER_TYPE_AZURE_AD = 5;
+ PROVIDER_TYPE_GITHUB = 6;
+ PROVIDER_TYPE_GITHUB_EE = 7;
+ PROVIDER_TYPE_GITLAB = 8;
+ PROVIDER_TYPE_GITLAB_SELF_HOSTED = 9;
+ PROVIDER_TYPE_GOOGLE = 10;
+}
+
+message ProviderConfig {
+ Options options = 1;
+ oneof config {
+ LDAPConfig ldap = 2;
+ }
+}
+
+message LDAPConfig {
+ string host = 1;
+ string port = 2;
+ bool tls = 3;
+ string base_dn = 4;
+ string user_object_class = 5;
+ string user_unique_attribute = 6;
+ string admin = 7;
+ LDAPAttributes attributes = 8;
+ Options provider_options = 9;
+}
+
+message Options {
+ bool is_linking_allowed = 1;
+ bool is_creation_allowed = 2;
+ bool is_auto_creation = 3;
+ bool is_auto_update = 4;
+}
+
+message LDAPAttributes {
+ string id_attribute = 1 [(validate.rules).string = {max_len: 200}];
+ string first_name_attribute = 2 [(validate.rules).string = {max_len: 200}];
+ string last_name_attribute = 3 [(validate.rules).string = {max_len: 200}];
+ string display_name_attribute = 4 [(validate.rules).string = {max_len: 200}];
+ string nick_name_attribute = 5 [(validate.rules).string = {max_len: 200}];
+ string preferred_username_attribute = 6 [(validate.rules).string = {max_len: 200}];
+ string email_attribute = 7 [(validate.rules).string = {max_len: 200}];
+ string email_verified_attribute = 8 [(validate.rules).string = {max_len: 200}];
+ string phone_attribute = 9 [(validate.rules).string = {max_len: 200}];
+ string phone_verified_attribute = 10 [(validate.rules).string = {max_len: 200}];
+ string preferred_language_attribute = 11 [(validate.rules).string = {max_len: 200}];
+ string avatar_url_attribute = 12 [(validate.rules).string = {max_len: 200}];
+ string profile_attribute = 13 [(validate.rules).string = {max_len: 200}];
+}
+
diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto
index b5fadf52f..d368b1eb3 100644
--- a/proto/zitadel/management.proto
+++ b/proto/zitadel/management.proto
@@ -3007,6 +3007,67 @@ service ManagementService {
};
}
+ // Returns all identity providers, which match the query
+ // Limit should always be set, there is a default limit set by the service
+ rpc ListProviders(ListProvidersRequest) returns (ListProvidersResponse) {
+ option (google.api.http) = {
+ post: "/idps/templates/_search"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.idp.read"
+ };
+ }
+
+ // Returns an identity provider of the organisation
+ rpc GetProviderByID(GetProviderByIDRequest) returns (GetProviderByIDResponse) {
+ option (google.api.http) = {
+ get: "/idps/templates/{id}"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.idp.read"
+ };
+ }
+
+ // Add a new ldap identity provider in the organisation
+ rpc AddLDAPProvider(AddLDAPProviderRequest) returns (AddLDAPProviderResponse) {
+ option (google.api.http) = {
+ post: "/idps/ldap"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.idp.write"
+ };
+ }
+
+ // Change an existing ldap identity provider in the organisation
+ rpc UpdateLDAPProvider(UpdateLDAPProviderRequest) returns (UpdateLDAPProviderResponse) {
+ option (google.api.http) = {
+ post: "/idps/ldap/{id}"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.idp.write"
+ };
+ }
+
+ // Remove an identity provider
+ // Will remove all linked providers of this configuration on the users
+ rpc DeleteProvider(DeleteProviderRequest) returns (DeleteProviderResponse) {
+ option (google.api.http) = {
+ post: "/idps/templates/{id}"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.idp.write"
+ };
+ }
+
rpc ListActions(ListActionsRequest) returns (ListActionsResponse) {
option (google.api.http) = {
post: "/actions/_search"
@@ -5920,6 +5981,80 @@ message UpdateOrgIDPJWTConfigResponse {
zitadel.v1.ObjectDetails details = 1;
}
+message ListProvidersRequest {
+ //list limitations and ordering
+ zitadel.v1.ListQuery query = 1;
+ //criteria the client is looking for
+ repeated ProviderQuery queries = 2;
+}
+
+message ProviderQuery {
+ oneof query {
+ zitadel.idp.v1.IDPIDQuery idp_id_query = 1;
+ zitadel.idp.v1.IDPNameQuery idp_name_query = 2;
+ zitadel.idp.v1.IDPOwnerTypeQuery owner_type_query = 3;
+ }
+}
+
+message ListProvidersResponse {
+ zitadel.v1.ListDetails details = 1;
+ repeated zitadel.idp.v1.Provider result = 2;
+}
+
+message GetProviderByIDRequest {
+ string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message GetProviderByIDResponse {
+ zitadel.idp.v1.Provider idp = 1;
+}
+
+message AddLDAPProviderRequest {
+ string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string host = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string port = 3 [(validate.rules).string = {max_len: 5}];
+ bool tls = 4;
+ string base_dn = 5 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string user_object_class = 6 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string user_unique_attribute = 7 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string admin = 8 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string password = 9 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ zitadel.idp.v1.LDAPAttributes attributes = 10;
+ zitadel.idp.v1.Options provider_options = 11;
+}
+
+message AddLDAPProviderResponse {
+ zitadel.v1.ObjectDetails details = 1;
+ string id = 2;
+}
+
+message UpdateLDAPProviderRequest {
+ string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string host = 3 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string port = 4 [(validate.rules).string = {max_len: 5}];
+ bool tls = 5;
+ string base_dn = 6 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string user_object_class = 7 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string user_unique_attribute = 8 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string admin = 9 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ string password = 10 [(validate.rules).string = {max_len: 200}];
+ zitadel.idp.v1.LDAPAttributes attributes = 11;
+ zitadel.idp.v1.Options provider_options = 12;
+}
+
+message UpdateLDAPProviderResponse {
+ zitadel.v1.ObjectDetails details = 1;
+}
+
+message DeleteProviderRequest {
+ string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message DeleteProviderResponse {
+ zitadel.v1.ObjectDetails details = 1;
+}
+
message ListActionsRequest {
//list limitations and ordering
zitadel.v1.ListQuery query = 1;