1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

Add support for Delinea DevOps Secrets Vault (#2415)

* Add support for Delinea DevOps Secrets Vault

Closes #1709.

Signed-off-by: Michael Sauter <michael.sauter@boehringer-ingelheim.com>

* fix: remove merge conflict

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

* Improve documentation

Signed-off-by: Michael Sauter <michael.sauter@boehringer-ingelheim.com>

---------

Signed-off-by: Michael Sauter <michael.sauter@boehringer-ingelheim.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Co-authored-by: Moritz Johner <beller.moritz@googlemail.com>
Co-authored-by: Moritz Johner <moolen@users.noreply.github.com>
This commit is contained in:
Michael Sauter 2023-07-06 18:01:43 +02:00 committed by GitHub
parent 69fe93ea49
commit bdf437c2e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1653 additions and 1 deletions

View file

@ -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"`
}

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -1775,6 +1775,132 @@ ConjurAuth
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.DelineaProvider">DelineaProvider
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
</p>
<p>
<p>See <a href="https://github.com/DelineaXPM/dsv-sdk-go/blob/main/vault/vault.go">https://github.com/DelineaXPM/dsv-sdk-go/blob/main/vault/vault.go</a>.</p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>clientId</code></br>
<em>
<a href="#external-secrets.io/v1beta1.DelineaProviderSecretRef">
DelineaProviderSecretRef
</a>
</em>
</td>
<td>
<p>ClientID is the non-secret part of the credential.</p>
</td>
</tr>
<tr>
<td>
<code>clientSecret</code></br>
<em>
<a href="#external-secrets.io/v1beta1.DelineaProviderSecretRef">
DelineaProviderSecretRef
</a>
</em>
</td>
<td>
<p>ClientSecret is the secret part of the credential.</p>
</td>
</tr>
<tr>
<td>
<code>tenant</code></br>
<em>
string
</em>
</td>
<td>
<p>Tenant is the chosen hostname / site name.</p>
</td>
</tr>
<tr>
<td>
<code>urlTemplate</code></br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>URLTemplate
If unset, defaults to &ldquo;https://%s.secretsvaultcloud.%s/v1/%s%s&rdquo;.</p>
</td>
</tr>
<tr>
<td>
<code>tld</code></br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>TLD is based on the server location that was chosen during provisioning.
If unset, defaults to &ldquo;com&rdquo;.</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.DelineaProviderSecretRef">DelineaProviderSecretRef
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.DelineaProvider">DelineaProvider</a>)
</p>
<p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>value</code></br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Value can be specified directly to set a value without using a secret.</p>
</td>
</tr>
<tr>
<td>
<code>secretRef</code></br>
<em>
<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
External Secrets meta/v1.SecretKeySelector
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>SecretRef references a key in a secret that will be used as value.</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.DopplerAuth">DopplerAuth
</h3>
<p>
@ -4865,6 +4991,21 @@ ConjurProvider
<p>Conjur configures this store to sync secrets using conjur provider</p>
</td>
</tr>
<tr>
<td>
<code>delinea</code></br>
<em>
<a href="#external-secrets.io/v1beta1.DelineaProvider">
DelineaProvider
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Delinea DevOps Secrets Vault
<a href="https://docs.delinea.com/online-help/products/devops-secrets-vault/current">https://docs.delinea.com/online-help/products/devops-secrets-vault/current</a></p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef

View file

@ -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

56
docs/provider/delinea.md Normal file
View file

@ -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: <TENANT>
tld: <TLD>
clientId:
value: <CLIENT_ID>
clientSecret:
secretRef:
name: <NAME_OF_KUBE_SECRET>
key: <KEY_IN_KUBE_SECRET>
```
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: <KEY_IN_KUBE_SECRET>
remoteRef:
key: <SECRET_PATH>
```

View file

@ -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

View file

@ -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=

View file

@ -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"}}' \

View file

@ -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},

View file

@ -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
}

View file

@ -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())
}

View file

@ -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())
}

View file

@ -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"

1
go.mod
View file

@ -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

2
go.sum
View file

@ -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=

View file

@ -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

View file

@ -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))
}
}

View file

@ -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)
}
})
}
}

View file

@ -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{},
})
}

View file

@ -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}
}

View file

@ -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)
}

View file

@ -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"