From bdf437c2e1a1a441955b1036b84eed5f63bda5d9 Mon Sep 17 00:00:00 2001
From: Michael Sauter
Date: Thu, 6 Jul 2023 18:01:43 +0200
Subject: [PATCH] Add support for Delinea DevOps Secrets Vault (#2415)
* Add support for Delinea DevOps Secrets Vault
Closes #1709.
Signed-off-by: Michael Sauter
* fix: remove merge conflict
Signed-off-by: Moritz Johner
* Improve documentation
Signed-off-by: Michael Sauter
---------
Signed-off-by: Michael Sauter
Signed-off-by: Moritz Johner
Co-authored-by: Moritz Johner
Co-authored-by: Moritz Johner
---
.../v1beta1/secretsstore_delinea_types.go | 51 +++
.../v1beta1/secretstore_types.go | 5 +
.../v1beta1/zz_generated.deepcopy.go | 50 +++
...ternal-secrets.io_clustersecretstores.yaml | 72 ++++
.../external-secrets.io_secretstores.yaml | 72 ++++
deploy/crds/bundle.yaml | 114 ++++++
docs/api/spec.md | 141 +++++++
docs/introduction/stability-support.md | 2 +
docs/provider/delinea.md | 56 +++
e2e/go.mod | 1 +
e2e/go.sum | 2 +
e2e/run.sh | 5 +
e2e/suites/provider/cases/common/common.go | 2 +-
e2e/suites/provider/cases/delinea/config.go | 47 +++
e2e/suites/provider/cases/delinea/delinea.go | 115 ++++++
e2e/suites/provider/cases/delinea/provider.go | 47 +++
e2e/suites/provider/cases/import.go | 1 +
go.mod | 1 +
go.sum | 2 +
hack/api-docs/mkdocs.yml | 1 +
pkg/provider/delinea/client.go | 148 +++++++
pkg/provider/delinea/client_test.go | 117 ++++++
pkg/provider/delinea/provider.go | 207 ++++++++++
pkg/provider/delinea/provider_test.go | 369 ++++++++++++++++++
pkg/provider/delinea/secret_api.go | 25 ++
pkg/provider/register/register.go | 1 +
26 files changed, 1653 insertions(+), 1 deletion(-)
create mode 100644 apis/externalsecrets/v1beta1/secretsstore_delinea_types.go
create mode 100644 docs/provider/delinea.md
create mode 100644 e2e/suites/provider/cases/delinea/config.go
create mode 100644 e2e/suites/provider/cases/delinea/delinea.go
create mode 100644 e2e/suites/provider/cases/delinea/provider.go
create mode 100644 pkg/provider/delinea/client.go
create mode 100644 pkg/provider/delinea/client_test.go
create mode 100644 pkg/provider/delinea/provider.go
create mode 100644 pkg/provider/delinea/provider_test.go
create mode 100644 pkg/provider/delinea/secret_api.go
diff --git a/apis/externalsecrets/v1beta1/secretsstore_delinea_types.go b/apis/externalsecrets/v1beta1/secretsstore_delinea_types.go
new file mode 100644
index 000000000..ab3482239
--- /dev/null
+++ b/apis/externalsecrets/v1beta1/secretsstore_delinea_types.go
@@ -0,0 +1,51 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta1
+
+import esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+
+type DelineaProviderSecretRef struct {
+
+ // Value can be specified directly to set a value without using a secret.
+ // +optional
+ Value string `json:"value,omitempty"`
+
+ // SecretRef references a key in a secret that will be used as value.
+ // +optional
+ SecretRef *esmeta.SecretKeySelector `json:"secretRef,omitempty"`
+}
+
+// See https://github.com/DelineaXPM/dsv-sdk-go/blob/main/vault/vault.go.
+type DelineaProvider struct {
+
+ // ClientID is the non-secret part of the credential.
+ ClientID *DelineaProviderSecretRef `json:"clientId"`
+
+ // ClientSecret is the secret part of the credential.
+ ClientSecret *DelineaProviderSecretRef `json:"clientSecret"`
+
+ // Tenant is the chosen hostname / site name.
+ Tenant string `json:"tenant"`
+
+ // URLTemplate
+ // If unset, defaults to "https://%s.secretsvaultcloud.%s/v1/%s%s".
+ // +optional
+ URLTemplate string `json:"urlTemplate,omitempty"`
+
+ // TLD is based on the server location that was chosen during provisioning.
+ // If unset, defaults to "com".
+ // +optional
+ TLD string `json:"tld,omitempty"`
+}
diff --git a/apis/externalsecrets/v1beta1/secretstore_types.go b/apis/externalsecrets/v1beta1/secretstore_types.go
index f7d0f2879..efa23ef0b 100644
--- a/apis/externalsecrets/v1beta1/secretstore_types.go
+++ b/apis/externalsecrets/v1beta1/secretstore_types.go
@@ -136,6 +136,11 @@ type SecretStoreProvider struct {
// Conjur configures this store to sync secrets using conjur provider
// +optional
Conjur *ConjurProvider `json:"conjur,omitempty"`
+
+ // Delinea DevOps Secrets Vault
+ // https://docs.delinea.com/online-help/products/devops-secrets-vault/current
+ // +optional
+ Delinea *DelineaProvider `json:"delinea,omitempty"`
}
type CAProviderType string
diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
index 37ec00270..b9c7b21f4 100644
--- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
+++ b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
@@ -701,6 +701,51 @@ func (in *ConjurProvider) DeepCopy() *ConjurProvider {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DelineaProvider) DeepCopyInto(out *DelineaProvider) {
+ *out = *in
+ if in.ClientID != nil {
+ in, out := &in.ClientID, &out.ClientID
+ *out = new(DelineaProviderSecretRef)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.ClientSecret != nil {
+ in, out := &in.ClientSecret, &out.ClientSecret
+ *out = new(DelineaProviderSecretRef)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DelineaProvider.
+func (in *DelineaProvider) DeepCopy() *DelineaProvider {
+ if in == nil {
+ return nil
+ }
+ out := new(DelineaProvider)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DelineaProviderSecretRef) DeepCopyInto(out *DelineaProviderSecretRef) {
+ *out = *in
+ if in.SecretRef != nil {
+ in, out := &in.SecretRef, &out.SecretRef
+ *out = new(metav1.SecretKeySelector)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DelineaProviderSecretRef.
+func (in *DelineaProviderSecretRef) DeepCopy() *DelineaProviderSecretRef {
+ if in == nil {
+ return nil
+ }
+ out := new(DelineaProviderSecretRef)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DopplerAuth) DeepCopyInto(out *DopplerAuth) {
*out = *in
@@ -1861,6 +1906,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
*out = new(ConjurProvider)
(*in).DeepCopyInto(*out)
}
+ if in.Delinea != nil {
+ in, out := &in.Delinea, &out.Delinea
+ *out = new(DelineaProvider)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
diff --git a/config/crds/bases/external-secrets.io_clustersecretstores.yaml b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
index 40eb66103..52a285e08 100644
--- a/config/crds/bases/external-secrets.io_clustersecretstores.yaml
+++ b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
@@ -2262,6 +2262,78 @@ spec:
- auth
- url
type: object
+ delinea:
+ description: Delinea DevOps Secrets Vault https://docs.delinea.com/online-help/products/devops-secrets-vault/current
+ properties:
+ clientId:
+ description: ClientID is the non-secret part of the credential.
+ properties:
+ secretRef:
+ description: SecretRef references a key in a secret that
+ will be used as value.
+ properties:
+ key:
+ description: The key of the entry in the Secret resource's
+ `data` field to be used. Some instances of this
+ field may be defaulted, in others it may be required.
+ type: string
+ name:
+ description: The name of the Secret resource being
+ referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred
+ to. Ignored if referent is not cluster-scoped. cluster-scoped
+ defaults to the namespace of the referent.
+ type: string
+ type: object
+ value:
+ description: Value can be specified directly to set a
+ value without using a secret.
+ type: string
+ type: object
+ clientSecret:
+ description: ClientSecret is the secret part of the credential.
+ properties:
+ secretRef:
+ description: SecretRef references a key in a secret that
+ will be used as value.
+ properties:
+ key:
+ description: The key of the entry in the Secret resource's
+ `data` field to be used. Some instances of this
+ field may be defaulted, in others it may be required.
+ type: string
+ name:
+ description: The name of the Secret resource being
+ referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred
+ to. Ignored if referent is not cluster-scoped. cluster-scoped
+ defaults to the namespace of the referent.
+ type: string
+ type: object
+ value:
+ description: Value can be specified directly to set a
+ value without using a secret.
+ type: string
+ type: object
+ tenant:
+ description: Tenant is the chosen hostname / site name.
+ type: string
+ tld:
+ description: TLD is based on the server location that was
+ chosen during provisioning. If unset, defaults to "com".
+ type: string
+ urlTemplate:
+ description: URLTemplate If unset, defaults to "https://%s.secretsvaultcloud.%s/v1/%s%s".
+ type: string
+ required:
+ - clientId
+ - clientSecret
+ - tenant
+ type: object
doppler:
description: Doppler configures this store to sync secrets using
the Doppler provider
diff --git a/config/crds/bases/external-secrets.io_secretstores.yaml b/config/crds/bases/external-secrets.io_secretstores.yaml
index a8d9b5309..612660c26 100644
--- a/config/crds/bases/external-secrets.io_secretstores.yaml
+++ b/config/crds/bases/external-secrets.io_secretstores.yaml
@@ -2262,6 +2262,78 @@ spec:
- auth
- url
type: object
+ delinea:
+ description: Delinea DevOps Secrets Vault https://docs.delinea.com/online-help/products/devops-secrets-vault/current
+ properties:
+ clientId:
+ description: ClientID is the non-secret part of the credential.
+ properties:
+ secretRef:
+ description: SecretRef references a key in a secret that
+ will be used as value.
+ properties:
+ key:
+ description: The key of the entry in the Secret resource's
+ `data` field to be used. Some instances of this
+ field may be defaulted, in others it may be required.
+ type: string
+ name:
+ description: The name of the Secret resource being
+ referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred
+ to. Ignored if referent is not cluster-scoped. cluster-scoped
+ defaults to the namespace of the referent.
+ type: string
+ type: object
+ value:
+ description: Value can be specified directly to set a
+ value without using a secret.
+ type: string
+ type: object
+ clientSecret:
+ description: ClientSecret is the secret part of the credential.
+ properties:
+ secretRef:
+ description: SecretRef references a key in a secret that
+ will be used as value.
+ properties:
+ key:
+ description: The key of the entry in the Secret resource's
+ `data` field to be used. Some instances of this
+ field may be defaulted, in others it may be required.
+ type: string
+ name:
+ description: The name of the Secret resource being
+ referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred
+ to. Ignored if referent is not cluster-scoped. cluster-scoped
+ defaults to the namespace of the referent.
+ type: string
+ type: object
+ value:
+ description: Value can be specified directly to set a
+ value without using a secret.
+ type: string
+ type: object
+ tenant:
+ description: Tenant is the chosen hostname / site name.
+ type: string
+ tld:
+ description: TLD is based on the server location that was
+ chosen during provisioning. If unset, defaults to "com".
+ type: string
+ urlTemplate:
+ description: URLTemplate If unset, defaults to "https://%s.secretsvaultcloud.%s/v1/%s%s".
+ type: string
+ required:
+ - clientId
+ - clientSecret
+ - tenant
+ type: object
doppler:
description: Doppler configures this store to sync secrets using
the Doppler provider
diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml
index a4802c081..31eea9bec 100644
--- a/deploy/crds/bundle.yaml
+++ b/deploy/crds/bundle.yaml
@@ -2109,6 +2109,63 @@ spec:
- auth
- url
type: object
+ delinea:
+ description: Delinea DevOps Secrets Vault https://docs.delinea.com/online-help/products/devops-secrets-vault/current
+ properties:
+ clientId:
+ description: ClientID is the non-secret part of the credential.
+ properties:
+ secretRef:
+ description: SecretRef references a key in a secret that will be used as value.
+ properties:
+ key:
+ description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+ type: string
+ name:
+ description: The name of the Secret resource being referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+ type: string
+ type: object
+ value:
+ description: Value can be specified directly to set a value without using a secret.
+ type: string
+ type: object
+ clientSecret:
+ description: ClientSecret is the secret part of the credential.
+ properties:
+ secretRef:
+ description: SecretRef references a key in a secret that will be used as value.
+ properties:
+ key:
+ description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+ type: string
+ name:
+ description: The name of the Secret resource being referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+ type: string
+ type: object
+ value:
+ description: Value can be specified directly to set a value without using a secret.
+ type: string
+ type: object
+ tenant:
+ description: Tenant is the chosen hostname / site name.
+ type: string
+ tld:
+ description: TLD is based on the server location that was chosen during provisioning. If unset, defaults to "com".
+ type: string
+ urlTemplate:
+ description: URLTemplate If unset, defaults to "https://%s.secretsvaultcloud.%s/v1/%s%s".
+ type: string
+ required:
+ - clientId
+ - clientSecret
+ - tenant
+ type: object
doppler:
description: Doppler configures this store to sync secrets using the Doppler provider
properties:
@@ -5755,6 +5812,63 @@ spec:
- auth
- url
type: object
+ delinea:
+ description: Delinea DevOps Secrets Vault https://docs.delinea.com/online-help/products/devops-secrets-vault/current
+ properties:
+ clientId:
+ description: ClientID is the non-secret part of the credential.
+ properties:
+ secretRef:
+ description: SecretRef references a key in a secret that will be used as value.
+ properties:
+ key:
+ description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+ type: string
+ name:
+ description: The name of the Secret resource being referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+ type: string
+ type: object
+ value:
+ description: Value can be specified directly to set a value without using a secret.
+ type: string
+ type: object
+ clientSecret:
+ description: ClientSecret is the secret part of the credential.
+ properties:
+ secretRef:
+ description: SecretRef references a key in a secret that will be used as value.
+ properties:
+ key:
+ description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+ type: string
+ name:
+ description: The name of the Secret resource being referred to.
+ type: string
+ namespace:
+ description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+ type: string
+ type: object
+ value:
+ description: Value can be specified directly to set a value without using a secret.
+ type: string
+ type: object
+ tenant:
+ description: Tenant is the chosen hostname / site name.
+ type: string
+ tld:
+ description: TLD is based on the server location that was chosen during provisioning. If unset, defaults to "com".
+ type: string
+ urlTemplate:
+ description: URLTemplate If unset, defaults to "https://%s.secretsvaultcloud.%s/v1/%s%s".
+ type: string
+ required:
+ - clientId
+ - clientSecret
+ - tenant
+ type: object
doppler:
description: Doppler configures this store to sync secrets using the Doppler provider
properties:
diff --git a/docs/api/spec.md b/docs/api/spec.md
index 371dddf49..a1758009a 100644
--- a/docs/api/spec.md
+++ b/docs/api/spec.md
@@ -1775,6 +1775,132 @@ ConjurAuth
+DelineaProvider
+
+
+(Appears on:
+SecretStoreProvider)
+
+
+
See https://github.com/DelineaXPM/dsv-sdk-go/blob/main/vault/vault.go.
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+clientId
+
+
+DelineaProviderSecretRef
+
+
+ |
+
+ ClientID is the non-secret part of the credential.
+ |
+
+
+
+clientSecret
+
+
+DelineaProviderSecretRef
+
+
+ |
+
+ ClientSecret is the secret part of the credential.
+ |
+
+
+
+tenant
+
+string
+
+ |
+
+ Tenant is the chosen hostname / site name.
+ |
+
+
+
+urlTemplate
+
+string
+
+ |
+
+(Optional)
+ URLTemplate
+If unset, defaults to “https://%s.secretsvaultcloud.%s/v1/%s%s”.
+ |
+
+
+
+tld
+
+string
+
+ |
+
+(Optional)
+ TLD is based on the server location that was chosen during provisioning.
+If unset, defaults to “com”.
+ |
+
+
+
+DelineaProviderSecretRef
+
+
+(Appears on:
+DelineaProvider)
+
+
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+value
+
+string
+
+ |
+
+(Optional)
+ Value can be specified directly to set a value without using a secret.
+ |
+
+
+
+secretRef
+
+
+External Secrets meta/v1.SecretKeySelector
+
+
+ |
+
+(Optional)
+ SecretRef references a key in a secret that will be used as value.
+ |
+
+
+
DopplerAuth
@@ -4865,6 +4991,21 @@ ConjurProvider
Conjur configures this store to sync secrets using conjur provider
+
+
+delinea
+
+
+DelineaProvider
+
+
+ |
+
+(Optional)
+ Delinea DevOps Secrets Vault
+https://docs.delinea.com/online-help/products/devops-secrets-vault/current
+ |
+
SecretStoreRef
diff --git a/docs/introduction/stability-support.md b/docs/introduction/stability-support.md
index 81807bfa1..78ebdd435 100644
--- a/docs/introduction/stability-support.md
+++ b/docs/introduction/stability-support.md
@@ -52,6 +52,7 @@ The following table describes the stability level of each provider and who's res
| [Keeper Security](https://www.keepersecurity.com/) | alpha | [@ppodevlab](https://github.com/ppodevlab) |
| [Scaleway](https://external-secrets.io/latest/provider/scaleway) | alpha | [@azert9](https://github.com/azert9/) |
| [Conjur](https://external-secrets.io/latest/provider/conjur) | alpha | [@davidh-cyberark](https://github.com/davidh-cyberark/) |
+| [Delinea](https://external-secrets.io/latest/provider/delinea) | alpha | [@michaelsauter](https://github.com/michaelsauter/) |
## Provider Feature Support
@@ -78,6 +79,7 @@ The following table show the support for features across different providers.
| Keeper Security | x | | | | x | x | |
| Scaleway | x | x | | | x | x | x |
| Conjur | | | | | x | | |
+| Delinea | x | | | | x | | |
## Support Policy
diff --git a/docs/provider/delinea.md b/docs/provider/delinea.md
new file mode 100644
index 000000000..3b92f152a
--- /dev/null
+++ b/docs/provider/delinea.md
@@ -0,0 +1,56 @@
+## Delinea DevOps Secrets Vault
+
+External Secrets Operator integrates with [Delinea DevOps Secrets Vault](https://docs.delinea.com/online-help/products/devops-secrets-vault/current).
+
+Please note that the [Delinea Secret Server](https://delinea.com/products/secret-server) product is NOT in scope of this integration.
+
+### Creating a SecretStore
+
+You need client ID, client secret and tenant to authenticate with DSV.
+Both client ID and client secret can be specified either directly in the config, or by referencing a kubernetes secret.
+
+To acquire client ID and client secret, refer to the [policy management](https://docs.delinea.com/dsv/current/tutorials/policy.md) and [client management](https://docs.delinea.com/dsv/current/usage/cli-ref/client.md) documentation.
+
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+ name: secret-store
+spec:
+ provider:
+ delinea:
+ tenant:
+ tld:
+ clientId:
+ value:
+ clientSecret:
+ secretRef:
+ name:
+ key:
+```
+
+Both `clientId` and `clientSecret` can either be specified directly via the `value` field or can reference a kubernetes secret.
+
+The `tenant` field must correspond to the host name / site name of your DevOps vault. If you selected a region other than the US you must also specify the TLD, e.g. `tld: eu`.
+
+If required, the URL template (`urlTemplate`) can be customized as well.
+
+### Referencing Secrets
+
+Secrets can be referenced by path. Getting a specific version of a secret is not yet supported.
+
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: secret
+spec:
+ refreshInterval: 20s
+ secretStoreRef:
+ kind: SecretStore
+ name: secret-store
+ data:
+ - secretKey:
+ remoteRef:
+ key:
+```
diff --git a/e2e/go.mod b/e2e/go.mod
index fa2a35360..99f6bdef1 100644
--- a/e2e/go.mod
+++ b/e2e/go.mod
@@ -39,6 +39,7 @@ require (
cloud.google.com/go/secretmanager v1.11.1
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12
+ github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.0
github.com/akeylesslabs/akeyless-go-cloud-id v0.3.4
github.com/akeylesslabs/akeyless-go/v3 v3.3.12
github.com/aliyun/alibaba-cloud-sdk-go v1.62.271
diff --git a/e2e/go.sum b/e2e/go.sum
index 995e0533f..2cda3bc37 100644
--- a/e2e/go.sum
+++ b/e2e/go.sum
@@ -77,6 +77,8 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.0 h1:+XXJ43iH4js8LIBr4MUGq1J09ycivNkTNhtn4mFyhY8=
+github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.0/go.mod h1:NTdQaRBIRZ/8gIzs010CS/u69aVSmqD1zbESW25y2cE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/akeylesslabs/akeyless-go-cloud-id v0.3.4 h1:vTckjyBhHOBiOWSC/oaEU2Oo4OH5eAlQiwKu2RMxsFg=
github.com/akeylesslabs/akeyless-go-cloud-id v0.3.4/go.mod h1:As/RomC2w/fa3y+yHRlVHPmkbP+zrKBFRow41y5dk+E=
diff --git a/e2e/run.sh b/e2e/run.sh
index c1a029620..9977afe46 100755
--- a/e2e/run.sh
+++ b/e2e/run.sh
@@ -79,6 +79,11 @@ kubectl run --rm \
--env="SCALEWAY_PROJECT_ID=${SCALEWAY_PROJECT_ID:-}" \
--env="SCALEWAY_ACCESS_KEY=${SCALEWAY_ACCESS_KEY:-}" \
--env="SCALEWAY_SECRET_KEY=${SCALEWAY_SECRET_KEY:-}" \
+ --env="DELINEA_TLD=${DELINEA_TLD:-}" \
+ --env="DELINEA_URL_TEMPLATE=${DELINEA_URL_TEMPLATE:-}" \
+ --env="DELINEA_TENANT=${DELINEA_TENANT:-}" \
+ --env="DELINEA_CLIENT_ID=${DELINEA_CLIENT_ID:-}" \
+ --env="DELINEA_CLIENT_SECRET=${DELINEA_CLIENT_SECRET:-}" \
--env="VERSION=${VERSION}" \
--env="TEST_SUITES=${TEST_SUITES}" \
--overrides='{ "apiVersion": "v1", "spec":{"serviceAccountName": "external-secrets-e2e"}}' \
diff --git a/e2e/suites/provider/cases/common/common.go b/e2e/suites/provider/cases/common/common.go
index cf2b9da5c..67d4c1fb3 100644
--- a/e2e/suites/provider/cases/common/common.go
+++ b/e2e/suites/provider/cases/common/common.go
@@ -512,7 +512,7 @@ func DockerJSONConfig(f *framework.Framework) (string, func(*framework.TestCase)
return "[common] should sync docker configurated json secrets with template simple", func(tc *framework.TestCase) {
cloudSecretName := fmt.Sprintf("%s-%s", f.Namespace.Name, dockerConfigExampleName)
cloudRemoteRefKey := f.MakeRemoteRefKey(cloudSecretName)
- dockerconfig := `{"auths":{"https://index.docker.io/v1/": {"auth": "c3R...zE2"}}}`
+ dockerconfig := `{"auths":{"https://index.docker.io/v1/":{"auth":"c3R...zE2"}}}`
cloudSecretValue := fmt.Sprintf(`{"dockerconfig": %s}`, dockerconfig)
tc.Secrets = map[string]framework.SecretEntry{
cloudRemoteRefKey: {Value: cloudSecretValue},
diff --git a/e2e/suites/provider/cases/delinea/config.go b/e2e/suites/provider/cases/delinea/config.go
new file mode 100644
index 000000000..96da8e9b7
--- /dev/null
+++ b/e2e/suites/provider/cases/delinea/config.go
@@ -0,0 +1,47 @@
+package delinea
+
+import (
+ "fmt"
+ "os"
+)
+
+type config struct {
+ tld string
+ urlTemplate string
+ tenant string
+ clientID string
+ clientSecret string
+}
+
+func loadConfigFromEnv() (*config, error) {
+ var cfg config
+ var err error
+
+ // Optional settings
+ cfg.tld, _ = getEnv("DELINEA_TLD")
+ cfg.urlTemplate, _ = getEnv("DELINEA_URL_TEMPLATE")
+
+ // Required settings
+ cfg.tenant, err = getEnv("DELINEA_TENANT")
+ if err != nil {
+ return nil, err
+ }
+ cfg.clientID, err = getEnv("DELINEA_CLIENT_ID")
+ if err != nil {
+ return nil, err
+ }
+ cfg.clientSecret, err = getEnv("DELINEA_CLIENT_SECRET")
+ if err != nil {
+ return nil, err
+ }
+
+ return &cfg, nil
+}
+
+func getEnv(name string) (string, error) {
+ value, ok := os.LookupEnv(name)
+ if !ok {
+ return "", fmt.Errorf("environment variable %q is not set", name)
+ }
+ return value, nil
+}
diff --git a/e2e/suites/provider/cases/delinea/delinea.go b/e2e/suites/provider/cases/delinea/delinea.go
new file mode 100644
index 000000000..93d22c4a1
--- /dev/null
+++ b/e2e/suites/provider/cases/delinea/delinea.go
@@ -0,0 +1,115 @@
+package delinea
+
+import (
+ "context"
+
+ "github.com/external-secrets/external-secrets-e2e/framework"
+ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+ esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+ esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+ "github.com/onsi/ginkgo/v2"
+ "github.com/onsi/gomega"
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var _ = ginkgo.Describe("[delinea]", ginkgo.Label("delinea"), func() {
+
+ f := framework.New("eso-delinea")
+
+ // Initialization is deferred so that assertions work.
+ provider := &secretStoreProvider{}
+
+ ginkgo.BeforeEach(func() {
+
+ cfg, err := loadConfigFromEnv()
+ gomega.Expect(err).ToNot(gomega.HaveOccurred())
+
+ provider.init(cfg)
+
+ createResources(context.Background(), f, cfg)
+ })
+
+ ginkgo.DescribeTable("sync secrets", framework.TableFunc(f, provider),
+
+ ginkgo.Entry(common.JSONDataWithProperty(f)),
+ ginkgo.Entry(common.JSONDataWithoutTargetName(f)),
+ ginkgo.Entry(common.JSONDataWithTemplate(f)),
+ ginkgo.Entry(common.JSONDataWithTemplateFromLiteral(f)),
+ ginkgo.Entry(common.TemplateFromConfigmaps(f)),
+ ginkgo.Entry(common.JSONDataFromSync(f)),
+ ginkgo.Entry(common.JSONDataFromRewrite(f)),
+ ginkgo.Entry(common.NestedJSONWithGJSON(f)),
+ ginkgo.Entry(common.DockerJSONConfig(f)),
+ ginkgo.Entry(common.DataPropertyDockerconfigJSON(f)),
+ ginkgo.Entry(common.SSHKeySyncDataProperty(f)),
+ ginkgo.Entry(common.DecodingPolicySync(f)),
+
+ // V1Alpha1 is not supported.
+ // ginkgo.Entry(common.SyncV1Alpha1(f)),
+
+ // Non-JSON values are not supported by DSV.
+ // ginkgo.Entry(common.SimpleDataSync(f)),
+ // ginkgo.Entry(common.SyncWithoutTargetName(f)),
+ // ginkgo.Entry(common.SSHKeySync(f)),
+ // ginkgo.Entry(common.DeletionPolicyDelete(f)),
+
+ // FindByName is not supported.
+ // ginkgo.Entry(common.FindByName(f)),
+ // ginkgo.Entry(common.FindByNameAndRewrite(f)),
+ // ginkgo.Entry(common.FindByNameWithPath(f)),
+
+ // FindByTag is not supported.
+ // ginkgo.Entry(common.FindByTag(f)),
+ // ginkgo.Entry(common.FindByTagWithPath(f)),
+ )
+})
+
+func createResources(ctx context.Context, f *framework.Framework, cfg *config) {
+
+ secretName := "delinea-credential"
+ secretKey := "client-secret"
+
+ // Creating a secret to hold the Delinea client secret.
+ secretSpec := v1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: secretName,
+ Namespace: f.Namespace.Name,
+ },
+ StringData: map[string]string{
+ secretKey: cfg.clientSecret,
+ },
+ }
+
+ err := f.CRClient.Create(ctx, &secretSpec)
+ gomega.Expect(err).ToNot(gomega.HaveOccurred())
+
+ // Creating SecretStore.
+ secretStoreSpec := esv1beta1.SecretStore{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: f.Namespace.Name,
+ Namespace: f.Namespace.Name,
+ },
+ Spec: esv1beta1.SecretStoreSpec{
+ Provider: &esv1beta1.SecretStoreProvider{
+ Delinea: &esv1beta1.DelineaProvider{
+ Tenant: cfg.tenant,
+ TLD: cfg.tld,
+ URLTemplate: cfg.urlTemplate,
+ ClientID: &esv1beta1.DelineaProviderSecretRef{
+ Value: cfg.clientID,
+ },
+ ClientSecret: &esv1beta1.DelineaProviderSecretRef{
+ SecretRef: &esmeta.SecretKeySelector{
+ Name: secretName,
+ Key: secretKey,
+ },
+ },
+ },
+ },
+ },
+ }
+
+ err = f.CRClient.Create(ctx, &secretStoreSpec)
+ gomega.Expect(err).ToNot(gomega.HaveOccurred())
+}
diff --git a/e2e/suites/provider/cases/delinea/provider.go b/e2e/suites/provider/cases/delinea/provider.go
new file mode 100644
index 000000000..845143571
--- /dev/null
+++ b/e2e/suites/provider/cases/delinea/provider.go
@@ -0,0 +1,47 @@
+package delinea
+
+import (
+ "encoding/json"
+
+ "github.com/DelineaXPM/dsv-sdk-go/v2/vault"
+ "github.com/external-secrets/external-secrets-e2e/framework"
+ "github.com/onsi/gomega"
+)
+
+type secretStoreProvider struct {
+ api *vault.Vault
+ cfg *config
+}
+
+func (p *secretStoreProvider) init(cfg *config) {
+
+ p.cfg = cfg
+
+ dsvClient, err := vault.New(vault.Configuration{
+ Credentials: vault.ClientCredential{
+ ClientID: cfg.clientID,
+ ClientSecret: cfg.clientSecret,
+ },
+ Tenant: cfg.tenant,
+ URLTemplate: cfg.urlTemplate,
+ TLD: cfg.tld,
+ })
+ gomega.Expect(err).ToNot(gomega.HaveOccurred())
+
+ p.api = dsvClient
+}
+
+func (p *secretStoreProvider) CreateSecret(key string, val framework.SecretEntry) {
+ var data map[string]interface{}
+ err := json.Unmarshal([]byte(val.Value), &data)
+ gomega.Expect(err).ToNot(gomega.HaveOccurred())
+ _, err = p.api.CreateSecret(key, &vault.SecretCreateRequest{
+ Data: data,
+ })
+ gomega.Expect(err).ToNot(gomega.HaveOccurred())
+}
+
+func (p *secretStoreProvider) DeleteSecret(key string) {
+ err := p.api.DeleteSecret(key)
+ gomega.Expect(err).ToNot(gomega.HaveOccurred())
+}
diff --git a/e2e/suites/provider/cases/import.go b/e2e/suites/provider/cases/import.go
index c3b2fd2ec..8a88f7de3 100644
--- a/e2e/suites/provider/cases/import.go
+++ b/e2e/suites/provider/cases/import.go
@@ -19,6 +19,7 @@ import (
_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws/parameterstore"
_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws/secretsmanager"
_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/azure"
+ _ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/delinea"
_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/gcp"
_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/kubernetes"
_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/scaleway"
diff --git a/go.mod b/go.mod
index 535ca751c..3475f2a56 100644
--- a/go.mod
+++ b/go.mod
@@ -63,6 +63,7 @@ require github.com/1Password/connect-sdk-go v1.5.1
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0
+ github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.0
github.com/akeylesslabs/akeyless-go/v3 v3.3.12
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.4
github.com/alibabacloud-go/kms-20160120/v3 v3.0.2
diff --git a/go.sum b/go.sum
index 730e7dd2b..586350fd7 100644
--- a/go.sum
+++ b/go.sum
@@ -87,6 +87,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkM
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.0 h1:+XXJ43iH4js8LIBr4MUGq1J09ycivNkTNhtn4mFyhY8=
+github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.0/go.mod h1:NTdQaRBIRZ/8gIzs010CS/u69aVSmqD1zbESW25y2cE=
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
github.com/IBM/go-sdk-core/v5 v5.13.4 h1:kJvBNQOwhFRkXCPapjNvKVC7n7n2vd1Nr6uUtDZGcfo=
github.com/IBM/go-sdk-core/v5 v5.13.4/go.mod h1:gKRSB+YyKsGlRQW7v5frlLbue5afulSvrRa4O26o4MM=
diff --git a/hack/api-docs/mkdocs.yml b/hack/api-docs/mkdocs.yml
index 00eef1012..c6f22a739 100644
--- a/hack/api-docs/mkdocs.yml
+++ b/hack/api-docs/mkdocs.yml
@@ -106,6 +106,7 @@ nav:
- Doppler: provider/doppler.md
- Keeper Security: provider/keeper-security.md
- Scaleway: provider/scaleway.md
+ - Delinea: provider/delinea.md
- Examples:
- FluxCD: examples/gitops-using-fluxcd.md
- Anchore Engine: examples/anchore-engine-credentials.md
diff --git a/pkg/provider/delinea/client.go b/pkg/provider/delinea/client.go
new file mode 100644
index 000000000..72172d461
--- /dev/null
+++ b/pkg/provider/delinea/client.go
@@ -0,0 +1,148 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package delinea
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "reflect"
+ "strconv"
+ "strings"
+
+ "github.com/DelineaXPM/dsv-sdk-go/v2/vault"
+ "github.com/tidwall/gjson"
+
+ esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+const (
+ errSecretKeyFmt = "cannot find secret data for key: %q"
+ errUnexpectedKey = "unexpected key in data: %s"
+ errSecretFormat = "secret data for property %s not in expected format: %s"
+)
+
+type client struct {
+ api secretAPI
+}
+
+var _ esv1beta1.SecretsClient = &client{}
+
+// GetSecret supports two types:
+// 1. get the full secret as json-encoded value
+// by leaving the ref.Property empty.
+// 2. get a key from the secret.
+// Nested values are supported by specifying a gjson expression
+func (c *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+ secret, err := c.getSecret(ctx, ref)
+ if err != nil {
+ return nil, err
+ }
+ // Return nil if secret value is null
+ if secret.Data == nil {
+ return nil, nil
+ }
+ jsonStr, err := json.Marshal(secret.Data)
+ if err != nil {
+ return nil, err
+ }
+ // return raw json if no property is defined
+ if ref.Property == "" {
+ return jsonStr, nil
+ }
+ // extract key from secret using gjson
+ val := gjson.Get(string(jsonStr), ref.Property)
+ if !val.Exists() {
+ return nil, esv1beta1.NoSecretError{}
+ }
+ return []byte(val.String()), nil
+}
+
+func (c *client) PushSecret(_ context.Context, _ []byte, _ esv1beta1.PushRemoteRef) error {
+ return errors.New("pushing secrets is not supported by Delinea DevOps Secrets Vault")
+}
+
+func (c *client) DeleteSecret(_ context.Context, _ esv1beta1.PushRemoteRef) error {
+ return errors.New("deleting secrets is not supported by Delinea DevOps Secrets Vault")
+}
+
+func (c *client) Validate() (esv1beta1.ValidationResult, error) {
+ return esv1beta1.ValidationResultReady, nil
+}
+
+// GetSecret gets the full secret as json-encoded value.
+func (c *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+ secret, err := c.getSecret(ctx, ref)
+ if err != nil {
+ return nil, err
+ }
+ byteMap := make(map[string][]byte, len(secret.Data))
+ for k := range secret.Data {
+ byteMap[k], err = getTypedKey(secret.Data, k)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return byteMap, nil
+}
+
+// GetAllSecrets lists secrets matching the given criteria and return their latest versions.
+func (c *client) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+ return nil, errors.New("getting all secrets is not supported by Delinea DevOps Secrets Vault")
+}
+
+func (c *client) Close(context.Context) error {
+ return nil
+}
+
+// getSecret retrieves the secret referenced by ref from the Vault API.
+func (c *client) getSecret(_ context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (*vault.Secret, error) {
+ if ref.Version != "" {
+ return nil, errors.New("specifying a version is not yet supported")
+ }
+ return c.api.Secret(ref.Key)
+}
+
+// getTypedKey is copied from pkg/provider/vault/vault.go.
+func getTypedKey(data map[string]interface{}, key string) ([]byte, error) {
+ v, ok := data[key]
+ if !ok {
+ return nil, fmt.Errorf(errUnexpectedKey, key)
+ }
+ switch t := v.(type) {
+ case string:
+ return []byte(t), nil
+ case map[string]interface{}:
+ return json.Marshal(t)
+ case []string:
+ return []byte(strings.Join(t, "\n")), nil
+ case []byte:
+ return t, nil
+ // also covers int and float32 due to json.Marshal
+ case float64:
+ return []byte(strconv.FormatFloat(t, 'f', -1, 64)), nil
+ case json.Number:
+ return []byte(t.String()), nil
+ case []interface{}:
+ return json.Marshal(t)
+ case bool:
+ return []byte(strconv.FormatBool(t)), nil
+ case nil:
+ return []byte(nil), nil
+ default:
+ return nil, fmt.Errorf(errSecretFormat, key, reflect.TypeOf(t))
+ }
+}
diff --git a/pkg/provider/delinea/client_test.go b/pkg/provider/delinea/client_test.go
new file mode 100644
index 000000000..281e0e7d4
--- /dev/null
+++ b/pkg/provider/delinea/client_test.go
@@ -0,0 +1,117 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package delinea
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/DelineaXPM/dsv-sdk-go/v2/vault"
+ "github.com/stretchr/testify/assert"
+
+ esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+type fakeAPI struct {
+ secrets []*vault.Secret
+}
+
+// createVaultSecret assembles a vault.Secret.
+// vault.Secret has unexported nested types, and is therefore quite
+// tricky from outside the vault package. This function facilitates easy setup.
+func createVaultSecret(path string, data map[string]interface{}) *vault.Secret {
+ s := &vault.Secret{}
+ s.Path = path
+ s.Data = data
+ return s
+}
+
+// Secret returns secret matching path.
+func (f *fakeAPI) Secret(path string) (*vault.Secret, error) {
+ for _, s := range f.secrets {
+ if s.Path == path {
+ return s, nil
+ }
+ }
+ return nil, errors.New("not found")
+}
+
+func newTestClient() esv1beta1.SecretsClient {
+ return &client{
+ api: &fakeAPI{
+ secrets: []*vault.Secret{
+ createVaultSecret("a", map[string]interface{}{}),
+ createVaultSecret("b", map[string]interface{}{
+ "hello": "world",
+ }),
+ createVaultSecret("c", map[string]interface{}{
+ "foo": map[string]string{"bar": "baz"},
+ }),
+ },
+ },
+ }
+}
+
+func TestGetSecret(t *testing.T) {
+ ctx := context.Background()
+ c := newTestClient()
+
+ testCases := map[string]struct {
+ ref esv1beta1.ExternalSecretDataRemoteRef
+ want []byte
+ err error
+ }{
+ "querying for the key returns the map": {
+ ref: esv1beta1.ExternalSecretDataRemoteRef{
+ Key: "b",
+ },
+ want: []byte(`{"hello":"world"}`),
+ },
+ "querying for the key and property returns a single value": {
+ ref: esv1beta1.ExternalSecretDataRemoteRef{
+ Key: "b",
+ Property: "hello",
+ },
+ want: []byte(`world`),
+ },
+ "querying for the key and nested property returns a single value": {
+ ref: esv1beta1.ExternalSecretDataRemoteRef{
+ Key: "c",
+ Property: "foo.bar",
+ },
+ want: []byte(`baz`),
+ },
+ "querying for existent key and non-existing propery": {
+ ref: esv1beta1.ExternalSecretDataRemoteRef{
+ Key: "c",
+ Property: "foo.bar.x",
+ },
+ err: esv1beta1.NoSecretErr,
+ },
+ }
+ for name, tc := range testCases {
+ t.Run(name, func(t *testing.T) {
+ got, err := c.GetSecret(ctx, tc.ref)
+ if tc.err == nil {
+ assert.NoError(t, err)
+ assert.Equal(t, tc.want, got)
+ } else {
+ assert.Nil(t, got)
+ assert.ErrorIs(t, err, tc.err)
+ assert.Equal(t, tc.err, err)
+ }
+ })
+ }
+}
diff --git a/pkg/provider/delinea/provider.go b/pkg/provider/delinea/provider.go
new file mode 100644
index 000000000..68e809cb3
--- /dev/null
+++ b/pkg/provider/delinea/provider.go
@@ -0,0 +1,207 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package delinea
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/DelineaXPM/dsv-sdk-go/v2/vault"
+ corev1 "k8s.io/api/core/v1"
+ kubeClient "sigs.k8s.io/controller-runtime/pkg/client"
+
+ esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+ "github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+var (
+ errEmptyTenant = errors.New("tenant must not be empty")
+ errEmptyClientID = errors.New("clientID must be set")
+ errEmptyClientSecret = errors.New("clientSecret must be set")
+ errSecretRefAndValueConflict = errors.New("cannot specify both secret reference and value")
+ errSecretRefAndValueMissing = errors.New("must specify either secret reference or direct value")
+ errMissingStore = errors.New("missing store specification")
+ errInvalidSpec = errors.New("invalid specification for delinea provider")
+ errMissingSecretName = errors.New("must specify a secret name")
+ errMissingSecretKey = errors.New("must specify a secret key")
+ errClusterStoreRequiresNamespace = errors.New("when using a ClusterSecretStore, namespaces must be explicitly set")
+
+ errNoSuchKeyFmt = "no such key in secret: %q"
+)
+
+type Provider struct{}
+
+var _ esv1beta1.Provider = &Provider{}
+
+// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
+func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
+ return esv1beta1.SecretStoreReadOnly
+}
+
+func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kubeClient.Client, namespace string) (esv1beta1.SecretsClient, error) {
+ cfg, err := getConfig(store)
+ if err != nil {
+ return nil, err
+ }
+
+ if store.GetKind() == esv1beta1.ClusterSecretStoreKind && doesConfigDependOnNamespace(cfg) {
+ // we are not attached to a specific namespace, but some config values are dependent on it
+ return nil, errClusterStoreRequiresNamespace
+ }
+
+ clientID, err := loadConfigSecret(ctx, cfg.ClientID, kube, namespace)
+ if err != nil {
+ return nil, err
+ }
+
+ clientSecret, err := loadConfigSecret(ctx, cfg.ClientSecret, kube, namespace)
+ if err != nil {
+ return nil, err
+ }
+
+ dsvClient, err := vault.New(vault.Configuration{
+ Credentials: vault.ClientCredential{
+ ClientID: clientID,
+ ClientSecret: clientSecret,
+ },
+ Tenant: cfg.Tenant,
+ TLD: cfg.TLD,
+ URLTemplate: cfg.URLTemplate,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return &client{
+ api: dsvClient,
+ }, nil
+}
+
+func loadConfigSecret(ctx context.Context, ref *esv1beta1.DelineaProviderSecretRef, kube kubeClient.Client, defaultNamespace string) (string, error) {
+ if ref.SecretRef == nil {
+ return ref.Value, nil
+ }
+
+ if err := validateSecretRef(ref); err != nil {
+ return "", err
+ }
+
+ namespace := defaultNamespace
+ if ref.SecretRef.Namespace != nil {
+ namespace = *ref.SecretRef.Namespace
+ }
+
+ objKey := kubeClient.ObjectKey{Namespace: namespace, Name: ref.SecretRef.Name}
+ secret := corev1.Secret{}
+ err := kube.Get(ctx, objKey, &secret)
+ if err != nil {
+ return "", err
+ }
+
+ value, ok := secret.Data[ref.SecretRef.Key]
+ if !ok {
+ return "", fmt.Errorf(errNoSuchKeyFmt, ref.SecretRef.Key)
+ }
+
+ return string(value), nil
+}
+
+func validateStoreSecretRef(store esv1beta1.GenericStore, ref *esv1beta1.DelineaProviderSecretRef) error {
+ if ref.SecretRef != nil {
+ if err := utils.ValidateReferentSecretSelector(store, *ref.SecretRef); err != nil {
+ return err
+ }
+ }
+
+ return validateSecretRef(ref)
+}
+
+func validateSecretRef(ref *esv1beta1.DelineaProviderSecretRef) error {
+ if ref.SecretRef != nil {
+ if ref.Value != "" {
+ return errSecretRefAndValueConflict
+ }
+ if ref.SecretRef.Name == "" {
+ return errMissingSecretName
+ }
+ if ref.SecretRef.Key == "" {
+ return errMissingSecretKey
+ }
+ } else if ref.Value == "" {
+ return errSecretRefAndValueMissing
+ }
+
+ return nil
+}
+
+func doesConfigDependOnNamespace(cfg *esv1beta1.DelineaProvider) bool {
+ if cfg.ClientID.SecretRef != nil && cfg.ClientID.SecretRef.Namespace == nil {
+ return true
+ }
+
+ if cfg.ClientSecret.SecretRef != nil && cfg.ClientSecret.SecretRef.Namespace == nil {
+ return true
+ }
+
+ return false
+}
+
+func getConfig(store esv1beta1.GenericStore) (*esv1beta1.DelineaProvider, error) {
+ if store == nil {
+ return nil, errMissingStore
+ }
+ storeSpec := store.GetSpec()
+
+ if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Delinea == nil {
+ return nil, errInvalidSpec
+ }
+ cfg := storeSpec.Provider.Delinea
+
+ if cfg.Tenant == "" {
+ return nil, errEmptyTenant
+ }
+
+ if cfg.ClientID == nil {
+ return nil, errEmptyClientID
+ }
+
+ if cfg.ClientSecret == nil {
+ return nil, errEmptyClientSecret
+ }
+
+ err := validateStoreSecretRef(store, cfg.ClientID)
+ if err != nil {
+ return nil, err
+ }
+
+ err = validateStoreSecretRef(store, cfg.ClientSecret)
+ if err != nil {
+ return nil, err
+ }
+
+ return cfg, nil
+}
+
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
+ _, err := getConfig(store)
+ return err
+}
+
+func init() {
+ esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+ Delinea: &esv1beta1.DelineaProvider{},
+ })
+}
diff --git a/pkg/provider/delinea/provider_test.go b/pkg/provider/delinea/provider_test.go
new file mode 100644
index 000000000..cc779ba5d
--- /dev/null
+++ b/pkg/provider/delinea/provider_test.go
@@ -0,0 +1,369 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package delinea
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/DelineaXPM/dsv-sdk-go/v2/vault"
+ "github.com/stretchr/testify/assert"
+ corev1 "k8s.io/api/core/v1"
+ kubeErrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ kubeClient "sigs.k8s.io/controller-runtime/pkg/client"
+ clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+ esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+ v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+ "github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+func TestDoesConfigDependOnNamespace(t *testing.T) {
+ tests := map[string]struct {
+ cfg esv1beta1.DelineaProvider
+ want bool
+ }{
+ "true when client ID references a secret without explicit namespace": {
+ cfg: esv1beta1.DelineaProvider{
+ ClientID: &esv1beta1.DelineaProviderSecretRef{
+ SecretRef: &v1.SecretKeySelector{Name: "foo"},
+ },
+ ClientSecret: &esv1beta1.DelineaProviderSecretRef{SecretRef: nil},
+ },
+ want: true,
+ },
+ "true when client secret references a secret without explicit namespace": {
+ cfg: esv1beta1.DelineaProvider{
+ ClientID: &esv1beta1.DelineaProviderSecretRef{SecretRef: nil},
+ ClientSecret: &esv1beta1.DelineaProviderSecretRef{
+ SecretRef: &v1.SecretKeySelector{Name: "foo"},
+ },
+ },
+ want: true,
+ },
+ "false when neither client ID nor secret reference a secret": {
+ cfg: esv1beta1.DelineaProvider{
+ ClientID: &esv1beta1.DelineaProviderSecretRef{SecretRef: nil},
+ ClientSecret: &esv1beta1.DelineaProviderSecretRef{SecretRef: nil},
+ },
+ want: false,
+ },
+ }
+ for name, tc := range tests {
+ t.Run(name, func(t *testing.T) {
+ got := doesConfigDependOnNamespace(&tc.cfg)
+ assert.Equal(t, tc.want, got)
+ })
+ }
+}
+
+func TestValidateStore(t *testing.T) {
+ validSecretRefUsingValue := makeSecretRefUsingValue("foo")
+ ambiguousSecretRef := &esv1beta1.DelineaProviderSecretRef{
+ SecretRef: &v1.SecretKeySelector{Name: "foo"}, Value: "foo",
+ }
+ tests := map[string]struct {
+ cfg esv1beta1.DelineaProvider
+ want error
+ }{
+ "invalid without tenant": {
+ cfg: esv1beta1.DelineaProvider{
+ Tenant: "",
+ ClientID: validSecretRefUsingValue,
+ ClientSecret: validSecretRefUsingValue,
+ },
+ want: errEmptyTenant,
+ },
+ "invalid without clientID": {
+ cfg: esv1beta1.DelineaProvider{
+ Tenant: "foo",
+ // ClientID omitted
+ ClientSecret: validSecretRefUsingValue,
+ },
+ want: errEmptyClientID,
+ },
+ "invalid without clientSecret": {
+ cfg: esv1beta1.DelineaProvider{
+ Tenant: "foo",
+ ClientID: validSecretRefUsingValue,
+ // ClientSecret omitted
+ },
+ want: errEmptyClientSecret,
+ },
+ "invalid with ambiguous clientID": {
+ cfg: esv1beta1.DelineaProvider{
+ Tenant: "foo",
+ ClientID: ambiguousSecretRef,
+ ClientSecret: validSecretRefUsingValue,
+ },
+ want: errSecretRefAndValueConflict,
+ },
+ "invalid with ambiguous clientSecret": {
+ cfg: esv1beta1.DelineaProvider{
+ Tenant: "foo",
+ ClientID: validSecretRefUsingValue,
+ ClientSecret: ambiguousSecretRef,
+ },
+ want: errSecretRefAndValueConflict,
+ },
+ "invalid with invalid clientID": {
+ cfg: esv1beta1.DelineaProvider{
+ Tenant: "foo",
+ ClientID: makeSecretRefUsingValue(""),
+ ClientSecret: validSecretRefUsingValue,
+ },
+ want: errSecretRefAndValueMissing,
+ },
+ "invalid with invalid clientSecret": {
+ cfg: esv1beta1.DelineaProvider{
+ Tenant: "foo",
+ ClientID: validSecretRefUsingValue,
+ ClientSecret: makeSecretRefUsingValue(""),
+ },
+ want: errSecretRefAndValueMissing,
+ },
+ "valid with tenant/clientID/clientSecret": {
+ cfg: esv1beta1.DelineaProvider{
+ Tenant: "foo",
+ ClientID: validSecretRefUsingValue,
+ ClientSecret: validSecretRefUsingValue,
+ },
+ want: nil,
+ },
+ }
+ for name, tc := range tests {
+ t.Run(name, func(t *testing.T) {
+ s := esv1beta1.SecretStore{
+ Spec: esv1beta1.SecretStoreSpec{
+ Provider: &esv1beta1.SecretStoreProvider{
+ Delinea: &tc.cfg,
+ },
+ },
+ }
+ p := &Provider{}
+ got := p.ValidateStore(&s)
+ assert.Equal(t, tc.want, got)
+ })
+ }
+}
+
+func TestValidateStoreBailsOnUnexpectedStore(t *testing.T) {
+ tests := map[string]struct {
+ store esv1beta1.GenericStore
+ want error
+ }{
+ "missing store": {nil, errMissingStore},
+ "missing spec": {&esv1beta1.SecretStore{}, errInvalidSpec},
+ "missing provider": {&esv1beta1.SecretStore{
+ Spec: esv1beta1.SecretStoreSpec{Provider: nil},
+ }, errInvalidSpec},
+ "missing delinea": {&esv1beta1.SecretStore{
+ Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{
+ Delinea: nil,
+ }},
+ }, errInvalidSpec},
+ }
+ for name, tc := range tests {
+ t.Run(name, func(t *testing.T) {
+ p := &Provider{}
+ got := p.ValidateStore(tc.store)
+ assert.Equal(t, tc.want, got)
+ })
+ }
+}
+
+func TestNewClient(t *testing.T) {
+ tenant := "foo"
+ clientIDKey := "username"
+ clientIDValue := "client id"
+ clientSecretKey := "password"
+ clientSecretValue := "client secret"
+
+ clientSecret := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"},
+ Data: map[string][]byte{
+ clientIDKey: []byte(clientIDValue),
+ clientSecretKey: []byte(clientSecretValue),
+ },
+ }
+
+ validProvider := &esv1beta1.DelineaProvider{
+ Tenant: tenant,
+ ClientID: makeSecretRefUsingRef(clientSecret.Name, clientIDKey),
+ ClientSecret: makeSecretRefUsingRef(clientSecret.Name, clientSecretKey),
+ }
+
+ tests := map[string]struct {
+ store esv1beta1.GenericStore // leave nil for namespaced store
+ provider *esv1beta1.DelineaProvider // discarded when store is set
+ kube kubeClient.Client
+ errCheck func(t *testing.T, err error)
+ }{
+ "missing provider config": {
+ provider: nil,
+ errCheck: func(t *testing.T, err error) {
+ assert.ErrorIs(t, err, errInvalidSpec)
+ },
+ },
+ "namespace-dependent cluster secret store": {
+ store: &esv1beta1.ClusterSecretStore{
+ TypeMeta: metav1.TypeMeta{Kind: esv1beta1.ClusterSecretStoreKind},
+ Spec: esv1beta1.SecretStoreSpec{
+ Provider: &esv1beta1.SecretStoreProvider{
+ Delinea: validProvider,
+ },
+ },
+ },
+ errCheck: func(t *testing.T, err error) {
+ assert.ErrorIs(t, err, errClusterStoreRequiresNamespace)
+ },
+ },
+ "dangling client ID ref": {
+ provider: &esv1beta1.DelineaProvider{
+ Tenant: tenant,
+ ClientID: makeSecretRefUsingRef("typo", clientIDKey),
+ ClientSecret: makeSecretRefUsingRef(clientSecret.Name, clientSecretKey),
+ },
+ kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
+ errCheck: func(t *testing.T, err error) {
+ assert.True(t, kubeErrors.IsNotFound(err))
+ },
+ },
+ "dangling client secret ref": {
+ provider: &esv1beta1.DelineaProvider{
+ Tenant: tenant,
+ ClientID: makeSecretRefUsingRef(clientSecret.Name, clientIDKey),
+ ClientSecret: makeSecretRefUsingRef("typo", clientSecretKey),
+ },
+ kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
+ errCheck: func(t *testing.T, err error) {
+ assert.True(t, kubeErrors.IsNotFound(err))
+ },
+ },
+ "secret ref without name": {
+ provider: &esv1beta1.DelineaProvider{
+ Tenant: tenant,
+ ClientID: makeSecretRefUsingRef("", clientIDKey),
+ ClientSecret: makeSecretRefUsingRef(clientSecret.Name, clientSecretKey),
+ },
+ kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
+ errCheck: func(t *testing.T, err error) {
+ assert.ErrorIs(t, err, errMissingSecretName)
+ },
+ },
+ "secret ref without key": {
+ provider: &esv1beta1.DelineaProvider{
+ Tenant: tenant,
+ ClientID: makeSecretRefUsingRef(clientSecret.Name, ""),
+ ClientSecret: makeSecretRefUsingRef(clientSecret.Name, clientSecretKey),
+ },
+ kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
+ errCheck: func(t *testing.T, err error) {
+ assert.ErrorIs(t, err, errMissingSecretKey)
+ },
+ },
+ "secret ref with non-existent keys": {
+ provider: &esv1beta1.DelineaProvider{
+ Tenant: tenant,
+ ClientID: makeSecretRefUsingRef(clientSecret.Name, "typo"),
+ ClientSecret: makeSecretRefUsingRef(clientSecret.Name, clientSecretKey),
+ },
+ kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
+ errCheck: func(t *testing.T, err error) {
+ assert.EqualError(t, err, fmt.Sprintf(errNoSuchKeyFmt, "typo"))
+ },
+ },
+ "valid secret refs": {
+ provider: validProvider,
+ kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
+ },
+ "secret values": {
+ provider: &esv1beta1.DelineaProvider{
+ Tenant: tenant,
+ ClientID: makeSecretRefUsingValue(clientIDValue),
+ ClientSecret: makeSecretRefUsingValue(clientSecretValue),
+ },
+ kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
+ },
+ "cluster secret store": {
+ store: &esv1beta1.ClusterSecretStore{
+ TypeMeta: metav1.TypeMeta{Kind: esv1beta1.ClusterSecretStoreKind},
+ Spec: esv1beta1.SecretStoreSpec{
+ Provider: &esv1beta1.SecretStoreProvider{
+ Delinea: &esv1beta1.DelineaProvider{
+ Tenant: tenant,
+ ClientID: makeSecretRefUsingNamespacedRef(clientSecret.Namespace, clientSecret.Name, clientIDKey),
+ ClientSecret: makeSecretRefUsingNamespacedRef(clientSecret.Namespace, clientSecret.Name, clientSecretKey),
+ },
+ },
+ },
+ },
+ kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
+ },
+ }
+ for name, tc := range tests {
+ t.Run(name, func(t *testing.T) {
+ p := &Provider{}
+ store := tc.store
+ if store == nil {
+ store = &esv1beta1.SecretStore{
+ TypeMeta: metav1.TypeMeta{Kind: esv1beta1.SecretStoreKind},
+ Spec: esv1beta1.SecretStoreSpec{
+ Provider: &esv1beta1.SecretStoreProvider{
+ Delinea: tc.provider,
+ },
+ },
+ }
+ }
+ sc, err := p.NewClient(context.Background(), store, tc.kube, clientSecret.Namespace)
+ if tc.errCheck == nil {
+ assert.NoError(t, err)
+ delineaClient, ok := sc.(*client)
+ assert.True(t, ok)
+ dsvClient, ok := delineaClient.api.(*vault.Vault)
+ assert.True(t, ok)
+ assert.Equal(t, vault.Configuration{
+ Credentials: vault.ClientCredential{
+ ClientID: clientIDValue,
+ ClientSecret: clientSecretValue,
+ },
+ Tenant: tenant,
+ TLD: "com", // Default from Delinea
+ URLTemplate: "https://%s.secretsvaultcloud.%s/v1/%s%s", // Default from Delinea
+ }, dsvClient.Configuration)
+ } else {
+ assert.Nil(t, sc)
+ tc.errCheck(t, err)
+ }
+ })
+ }
+}
+
+func makeSecretRefUsingRef(name, key string) *esv1beta1.DelineaProviderSecretRef {
+ return &esv1beta1.DelineaProviderSecretRef{
+ SecretRef: &v1.SecretKeySelector{Name: name, Key: key},
+ }
+}
+
+func makeSecretRefUsingNamespacedRef(namespace, name, key string) *esv1beta1.DelineaProviderSecretRef {
+ return &esv1beta1.DelineaProviderSecretRef{
+ SecretRef: &v1.SecretKeySelector{Namespace: utils.Ptr(namespace), Name: name, Key: key},
+ }
+}
+
+func makeSecretRefUsingValue(val string) *esv1beta1.DelineaProviderSecretRef {
+ return &esv1beta1.DelineaProviderSecretRef{Value: val}
+}
diff --git a/pkg/provider/delinea/secret_api.go b/pkg/provider/delinea/secret_api.go
new file mode 100644
index 000000000..52a0eba0d
--- /dev/null
+++ b/pkg/provider/delinea/secret_api.go
@@ -0,0 +1,25 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package delinea
+
+import (
+ "github.com/DelineaXPM/dsv-sdk-go/v2/vault"
+)
+
+// secretAPI represents the subset of the Delinea DevOps Secrets Vault API
+// which is supported by dsv-sdk-go/v2.
+// See https://dsv.secretsvaultcloud.com/api for full API documentation.
+type secretAPI interface {
+ Secret(path string) (*vault.Secret, error)
+}
diff --git a/pkg/provider/register/register.go b/pkg/provider/register/register.go
index 1f5499405..68004c4a0 100644
--- a/pkg/provider/register/register.go
+++ b/pkg/provider/register/register.go
@@ -23,6 +23,7 @@ import (
_ "github.com/external-secrets/external-secrets/pkg/provider/aws"
_ "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault"
_ "github.com/external-secrets/external-secrets/pkg/provider/conjur"
+ _ "github.com/external-secrets/external-secrets/pkg/provider/delinea"
_ "github.com/external-secrets/external-secrets/pkg/provider/doppler"
_ "github.com/external-secrets/external-secrets/pkg/provider/fake"
_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"