From 586495a0be4d470d14dda8a25ae2b3d4fd7fcf5d Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 15 Feb 2023 09:14:59 +0100 Subject: [PATCH] feat: add management for ldap idp template (#5220) Add management functionality for LDAP idps with templates and the basic functionality for the LDAP provider, which can then be used with a separate login page in the future. --------- Co-authored-by: Livio Spring --- docs/docs/apis/proto/admin.md | 208 +++++ docs/docs/apis/proto/idp.md | 105 +++ docs/docs/apis/proto/management.md | 209 +++++ go.mod | 11 +- go.sum | 27 +- internal/api/grpc/admin/idp.go | 54 ++ internal/api/grpc/admin/idp_converter.go | 77 ++ internal/api/grpc/idp/converter.go | 140 ++++ internal/api/grpc/management/idp.go | 54 ++ internal/api/grpc/management/idp_converter.go | 79 ++ internal/command/idp.go | 17 + internal/command/idp_model.go | 208 +++++ internal/command/instance_idp.go | 205 +++++ internal/command/instance_idp_model.go | 163 ++++ internal/command/instance_idp_test.go | 684 ++++++++++++++++ internal/command/org_idp.go | 204 +++++ internal/command/org_idp_model.go | 163 ++++ internal/command/org_idp_test.go | 698 +++++++++++++++++ internal/domain/idp.go | 36 + internal/idp/providers/ldap/ldap.go | 237 ++++++ internal/idp/providers/ldap/ldap_test.go | 185 +++++ internal/idp/providers/ldap/session.go | 98 +++ internal/idp/providers/ldap/user.go | 91 +++ internal/query/idp_template.go | 621 +++++++++++++++ internal/query/idp_template_test.go | 727 ++++++++++++++++++ internal/query/projection/idp_template.go | 412 ++++++++++ .../query/projection/idp_template_test.go | 515 +++++++++++++ internal/query/projection/projection.go | 3 + internal/repository/idp/idp.go | 104 +++ internal/repository/idp/ldap.go | 351 +++++++++ internal/repository/instance/eventstore.go | 3 + internal/repository/instance/idp.go | 142 ++++ internal/repository/org/eventstore.go | 3 + internal/repository/org/idp.go | 142 ++++ proto/zitadel/admin.proto | 134 ++++ proto/zitadel/idp.proto | 67 ++ proto/zitadel/management.proto | 135 ++++ 37 files changed, 7298 insertions(+), 14 deletions(-) create mode 100644 internal/command/idp.go create mode 100644 internal/command/idp_model.go create mode 100644 internal/command/instance_idp.go create mode 100644 internal/command/instance_idp_model.go create mode 100644 internal/command/instance_idp_test.go create mode 100644 internal/command/org_idp.go create mode 100644 internal/command/org_idp_model.go create mode 100644 internal/command/org_idp_test.go create mode 100644 internal/domain/idp.go create mode 100644 internal/idp/providers/ldap/ldap.go create mode 100644 internal/idp/providers/ldap/ldap_test.go create mode 100644 internal/idp/providers/ldap/session.go create mode 100644 internal/idp/providers/ldap/user.go create mode 100644 internal/query/idp_template.go create mode 100644 internal/query/idp_template_test.go create mode 100644 internal/query/projection/idp_template.go create mode 100644 internal/query/projection/idp_template_test.go create mode 100644 internal/repository/idp/idp.go create mode 100644 internal/repository/idp/ldap.go create mode 100644 internal/repository/instance/idp.go create mode 100644 internal/repository/org/idp.go 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;