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 Secret Server (#3468)

* implements secretserver

Signed-off-by: Bill Hamilton <bill.hamilton@delinea.com>

* bump to align e2e

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>

* bump

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>

---------

Signed-off-by: Bill Hamilton <bill.hamilton@delinea.com>
Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
Co-authored-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
This commit is contained in:
Bill Hamilton 2024-07-10 10:32:17 -07:00 committed by GitHub
parent 9512254a04
commit 1876ff88d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1721 additions and 0 deletions

View file

@ -0,0 +1,45 @@
/*
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 SecretServerProviderRef 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/tss-sdk-go/blob/main/server/server.go.
type SecretServerProvider struct {
// Username is the secret server account username.
// +required
Username *SecretServerProviderRef `json:"username"`
// Password is the secret server account password.
// +required
Password *SecretServerProviderRef `json:"password"`
// ServerURL
// URL to your secret server installation
// +required
ServerURL string `json:"serverURL"`
}

View file

@ -155,6 +155,11 @@ type SecretStoreProvider struct {
// +optional // +optional
Delinea *DelineaProvider `json:"delinea,omitempty"` Delinea *DelineaProvider `json:"delinea,omitempty"`
// SecretServer configures this store to sync secrets using SecretServer provider
// https://docs.delinea.com/online-help/secret-server/start.htm
// +optional
SecretServer *SecretServerProvider `json:"secretserver,omitempty"`
// Chef configures this store to sync secrets with chef server // Chef configures this store to sync secrets with chef server
// +optional // +optional
Chef *ChefProvider `json:"chef,omitempty"` Chef *ChefProvider `json:"chef,omitempty"`

View file

@ -2266,6 +2266,51 @@ func (in *ScalewayProviderSecretRef) DeepCopy() *ScalewayProviderSecretRef {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretServerProvider) DeepCopyInto(out *SecretServerProvider) {
*out = *in
if in.Username != nil {
in, out := &in.Username, &out.Username
*out = new(SecretServerProviderRef)
(*in).DeepCopyInto(*out)
}
if in.Password != nil {
in, out := &in.Password, &out.Password
*out = new(SecretServerProviderRef)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretServerProvider.
func (in *SecretServerProvider) DeepCopy() *SecretServerProvider {
if in == nil {
return nil
}
out := new(SecretServerProvider)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretServerProviderRef) DeepCopyInto(out *SecretServerProviderRef) {
*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 SecretServerProviderRef.
func (in *SecretServerProviderRef) DeepCopy() *SecretServerProviderRef {
if in == nil {
return nil
}
out := new(SecretServerProviderRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretStore) DeepCopyInto(out *SecretStore) { func (in *SecretStore) DeepCopyInto(out *SecretStore) {
*out = *in *out = *in
@ -2443,6 +2488,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
*out = new(DelineaProvider) *out = new(DelineaProvider)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.SecretServer != nil {
in, out := &in.SecretServer, &out.SecretServer
*out = new(SecretServerProvider)
(*in).DeepCopyInto(*out)
}
if in.Chef != nil { if in.Chef != nil {
in, out := &in.Chef, &out.Chef in, out := &in.Chef, &out.Chef
*out = new(ChefProvider) *out = new(ChefProvider)

View file

@ -3732,6 +3732,75 @@ spec:
- region - region
- secretKey - secretKey
type: object type: object
secretserver:
description: |-
SecretServer configures this store to sync secrets using SecretServer provider
https://docs.delinea.com/online-help/secret-server/start.htm
properties:
password:
description: Password is the secret server account password.
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
serverURL:
description: |-
ServerURL
URL to your secret server installation
type: string
username:
description: Username is the secret server account username.
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
required:
- password
- serverURL
- username
type: object
senhasegura: senhasegura:
description: Senhasegura configures this store to sync secrets description: Senhasegura configures this store to sync secrets
using senhasegura provider using senhasegura provider

View file

@ -3732,6 +3732,75 @@ spec:
- region - region
- secretKey - secretKey
type: object type: object
secretserver:
description: |-
SecretServer configures this store to sync secrets using SecretServer provider
https://docs.delinea.com/online-help/secret-server/start.htm
properties:
password:
description: Password is the secret server account password.
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
serverURL:
description: |-
ServerURL
URL to your secret server installation
type: string
username:
description: Username is the secret server account username.
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
required:
- password
- serverURL
- username
type: object
senhasegura: senhasegura:
description: Senhasegura configures this store to sync secrets description: Senhasegura configures this store to sync secrets
using senhasegura provider using senhasegura provider

View file

@ -4121,6 +4121,69 @@ spec:
- region - region
- secretKey - secretKey
type: object type: object
secretserver:
description: |-
SecretServer configures this store to sync secrets using SecretServer provider
https://docs.delinea.com/online-help/secret-server/start.htm
properties:
password:
description: Password is the secret server account password.
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
serverURL:
description: |-
ServerURL
URL to your secret server installation
type: string
username:
description: Username is the secret server account username.
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
required:
- password
- serverURL
- username
type: object
senhasegura: senhasegura:
description: Senhasegura configures this store to sync secrets using senhasegura provider description: Senhasegura configures this store to sync secrets using senhasegura provider
properties: properties:
@ -9684,6 +9747,69 @@ spec:
- region - region
- secretKey - secretKey
type: object type: object
secretserver:
description: |-
SecretServer configures this store to sync secrets using SecretServer provider
https://docs.delinea.com/online-help/secret-server/start.htm
properties:
password:
description: Password is the secret server account password.
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
serverURL:
description: |-
ServerURL
URL to your secret server installation
type: string
username:
description: Username is the secret server account username.
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
required:
- password
- serverURL
- username
type: object
senhasegura: senhasegura:
description: Senhasegura configures this store to sync secrets using senhasegura provider description: Senhasegura configures this store to sync secrets using senhasegura provider
properties: properties:

View file

@ -5924,6 +5924,107 @@ External Secrets meta/v1.SecretKeySelector
</tr> </tr>
</tbody> </tbody>
</table> </table>
<h3 id="external-secrets.io/v1beta1.SecretServerProvider">SecretServerProvider
</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/tss-sdk-go/blob/main/server/server.go">https://github.com/DelineaXPM/tss-sdk-go/blob/main/server/server.go</a>.</p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>username</code></br>
<em>
<a href="#external-secrets.io/v1beta1.SecretServerProviderRef">
SecretServerProviderRef
</a>
</em>
</td>
<td>
<p>Username is the secret server account username.</p>
</td>
</tr>
<tr>
<td>
<code>password</code></br>
<em>
<a href="#external-secrets.io/v1beta1.SecretServerProviderRef">
SecretServerProviderRef
</a>
</em>
</td>
<td>
<p>Password is the secret server account password.</p>
</td>
</tr>
<tr>
<td>
<code>serverURL</code></br>
<em>
string
</em>
</td>
<td>
<p>ServerURL
URL to your secret server installation</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.SecretServerProviderRef">SecretServerProviderRef
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.SecretServerProvider">SecretServerProvider</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.SecretStore">SecretStore <h3 id="external-secrets.io/v1beta1.SecretStore">SecretStore
</h3> </h3>
<p> <p>
@ -6432,6 +6533,21 @@ DelineaProvider
</tr> </tr>
<tr> <tr>
<td> <td>
<code>secretserver</code></br>
<em>
<a href="#external-secrets.io/v1beta1.SecretServerProvider">
SecretServerProvider
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>SecretServer configures this store to sync secrets using SecretServer provider
<a href="https://docs.delinea.com/online-help/secret-server/start.htm">https://docs.delinea.com/online-help/secret-server/start.htm</a></p>
</td>
</tr>
<tr>
<td>
<code>chef</code></br> <code>chef</code></br>
<em> <em>
<a href="#external-secrets.io/v1beta1.ChefProvider"> <a href="#external-secrets.io/v1beta1.ChefProvider">

View file

@ -53,6 +53,7 @@ The following table describes the stability level of each provider and who's res
| [Scaleway](https://external-secrets.io/latest/provider/scaleway) | alpha | [@azert9](https://github.com/azert9/) | | [Scaleway](https://external-secrets.io/latest/provider/scaleway) | alpha | [@azert9](https://github.com/azert9/) |
| [Conjur](https://external-secrets.io/latest/provider/conjur) | stable | [@davidh-cyberark](https://github.com/davidh-cyberark/) [@szh](https://github.com/szh) | | [Conjur](https://external-secrets.io/latest/provider/conjur) | stable | [@davidh-cyberark](https://github.com/davidh-cyberark/) [@szh](https://github.com/szh) |
| [Delinea](https://external-secrets.io/latest/provider/delinea) | alpha | [@michaelsauter](https://github.com/michaelsauter/) | | [Delinea](https://external-secrets.io/latest/provider/delinea) | alpha | [@michaelsauter](https://github.com/michaelsauter/) |
| [SecretServer](https://external-secrets.io/latest/provider/secretserver) | alpha | [@billhamilton](https://github.com/pacificcode/) |
| [Pulumi ESC](https://external-secrets.io/latest/provider/pulumi) | alpha | [@dirien](https://github.com/dirien) | | [Pulumi ESC](https://external-secrets.io/latest/provider/pulumi) | alpha | [@dirien](https://github.com/dirien) |
| [Passbolt](https://external-secrets.io/latest/provider/passbolt) | alpha | | | [Passbolt](https://external-secrets.io/latest/provider/passbolt) | alpha | |
| [Infisical](https://external-secrets.io/latest/provider/infisical) | alpha | [@akhilmhdh](https://github.com/akhilmhdh) | | [Infisical](https://external-secrets.io/latest/provider/infisical) | alpha | [@akhilmhdh](https://github.com/akhilmhdh) |
@ -85,6 +86,7 @@ The following table show the support for features across different providers.
| Scaleway | x | x | | | x | x | x | | Scaleway | x | x | | | x | x | x |
| Conjur | x | x | | | x | | | | Conjur | x | x | | | x | | |
| Delinea | x | | | | x | | | | Delinea | x | | | | x | | |
| SecretServer | x | | | | x | | |
| Pulumi ESC | x | | | | x | | | | Pulumi ESC | x | | | | x | | |
| Passbolt | x | | | | x | | | | Passbolt | x | | | | x | | |
| Infisical | x | | | x | x | | | | Infisical | x | | | x | x | | |

View file

@ -0,0 +1,133 @@
# Delinea Secret Server
External Secrets Operator integration with [Delinea Secret Server](https://docs.delinea.com/online-help/secret-server/start.htm).
### Creating a SecretStore
You need a username, password and a fully qualified Secret Server tenant URL to authenticate
i.e. `https://yourTenantName.secretservercloud.com`.
Both username and password can be specified either directly in your `SecretStore` yaml config, or by referencing a kubernetes secret.
To acquire a username and password, refer to the Secret Server [user management](https://docs.delinea.com/online-help/secret-server/users/creating-users/index.htm) documentation.
Both `username` and `password` can either be specified directly via the `value` field (example below)
>spec.provider.secretserver.username.value: "yourusername"<br />
spec.provider.secretserver.password.value: "yourpassword" <br />
Or you can reference a kubernetes secret (password example below).
```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: secret-server-store
spec:
provider:
secretserver:
serverURL: "https://yourtenantname.secretservercloud.com"
username:
value: "yourusername"
password:
secretRef:
name: <NAME_OF_K8S_SECRET>
key: <KEY_IN_K8S_SECRET>
```
### Referencing Secrets
Secrets may be referenced by secret ID or secret name.
>Please note if using the secret name
the name field must not contain spaces or control characters.<br />
If multiple secrets are found, *`only the first found secret will be returned`*.
Please note: `Retrieving a specific version of a secret is not yet supported.`
Note that because all Secret Server secrets are JSON objects, you must specify the `remoteRef.property`
in your ExternalSecret configuration.<br />
You can access nested values or arrays using [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md).
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: secret-server-external-secret
spec:
refreshInterval: 15s
secretStoreRef:
kind: SecretStore
name: secret-server-store
data:
- secretKey: SecretServerValue #<SECRET_VALUE_RETURNED_HERE>
remoteRef:
key: "52622" #<SECRET_ID>
property: "array.0.value" #<GJSON_PROPERTY> * an empty property will return the entire secret
```
### Preparing your secret
You can either retrieve your entire secret or you can use a JSON formatted string
stored in your secret located at Items[0].ItemValue to retrieve a specific value.<br />
See example JSON secret below.
### Examples
Using the json formatted secret below:
- Lookup a single top level property using secret ID.
>spec.data.remoteRef.key = 52622 (id of the secret)<br />
spec.data.remoteRef.property = "user" (Items.0.ItemValue user attribute)<br />
returns: marktwain@hannibal.com
- Lookup a nested property using secret name.
>spec.data.remoteRef.key = "external-secret-testing" (name of the secret)<br />
spec.data.remoteRef.property = "books.1" (Items.0.ItemValue books.1 attribute)<br />
returns: huckleberryFinn
- Lookup by secret ID (*secret name will work as well*) and return the entire secret.
>spec.data.remoteRef.key = "52622" (id of the secret)<br />
spec.data.remoteRef.property = "" <br />
returns: The entire secret in JSON format as displayed below
```JSON
{
"Name": "external-secret-testing",
"FolderID": 73,
"ID": 52622,
"SiteID": 1,
"SecretTemplateID": 6098,
"SecretPolicyID": -1,
"PasswordTypeWebScriptID": -1,
"LauncherConnectAsSecretID": -1,
"CheckOutIntervalMinutes": -1,
"Active": true,
"CheckedOut": false,
"CheckOutEnabled": false,
"AutoChangeEnabled": false,
"CheckOutChangePasswordEnabled": false,
"DelayIndexing": false,
"EnableInheritPermissions": true,
"EnableInheritSecretPolicy": true,
"ProxyEnabled": false,
"RequiresComment": false,
"SessionRecordingEnabled": false,
"WebLauncherRequiresIncognitoMode": false,
"Items": [
{
"ItemID": 280265,
"FieldID": 439,
"FileAttachmentID": 0,
"FieldName": "Data",
"Slug": "data",
"FieldDescription": "json text field",
"Filename": "",
"ItemValue": "{ \"user\": \"marktwain@hannibal.com\", \"occupation\": \"author\",\"books\":[ \"tomSawyer\",\"huckleberryFinn\",\"Pudd'nhead Wilson\"] }",
"IsFile": false,
"IsNotes": false,
"IsPassword": false
}
]
}
```

View file

@ -44,6 +44,7 @@ require (
github.com/Azure/go-autorest/autorest v0.11.29 github.com/Azure/go-autorest/autorest v0.11.29
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 github.com/Azure/go-autorest/autorest/azure/auth v0.5.13
github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.2 github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.2
github.com/DelineaXPM/tss-sdk-go/v2 v2.0.1
github.com/akeylesslabs/akeyless-go-cloud-id v0.3.5 github.com/akeylesslabs/akeyless-go-cloud-id v0.3.5
github.com/akeylesslabs/akeyless-go/v3 v3.6.3 github.com/akeylesslabs/akeyless-go/v3 v3.6.3
github.com/aliyun/alibaba-cloud-sdk-go v1.62.271 github.com/aliyun/alibaba-cloud-sdk-go v1.62.271

View file

@ -97,6 +97,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 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.2 h1:cmX2QC9s5kPqmghWLLZP8YRFO1ZD/C59BpNH2ujP99w= github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.2 h1:cmX2QC9s5kPqmghWLLZP8YRFO1ZD/C59BpNH2ujP99w=
github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.2/go.mod h1:tNlpIXJlIwQlRbobXDPme4qv/Rc8+a1GbuUhE3m4JhQ= github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.2/go.mod h1:tNlpIXJlIwQlRbobXDPme4qv/Rc8+a1GbuUhE3m4JhQ=
github.com/DelineaXPM/tss-sdk-go/v2 v2.0.1 h1:/rzzzaBuj/FYTcbt8sYZ9IzlnENqcgh5zKqBhHiBBm4=
github.com/DelineaXPM/tss-sdk-go/v2 v2.0.1/go.mod h1:xz6FXP2Do88Vc5Hx7OamZgZC1W45yfmLy4+iDKxlGXo=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=

View file

@ -84,6 +84,9 @@ kubectl run --rm \
--env="DELINEA_TENANT=${DELINEA_TENANT:-}" \ --env="DELINEA_TENANT=${DELINEA_TENANT:-}" \
--env="DELINEA_CLIENT_ID=${DELINEA_CLIENT_ID:-}" \ --env="DELINEA_CLIENT_ID=${DELINEA_CLIENT_ID:-}" \
--env="DELINEA_CLIENT_SECRET=${DELINEA_CLIENT_SECRET:-}" \ --env="DELINEA_CLIENT_SECRET=${DELINEA_CLIENT_SECRET:-}" \
--env="SECRETSERVER_USERNAME=${SECRETSERVER_USERNAME:-}" \
--env="SECRETSERVER_PASSWORD=${SECRETSERVER_PASSWORD:-}" \
--env="SECRETSERVER_URL=${SECRETSERVER_URL:-}" \
--env="VERSION=${VERSION}" \ --env="VERSION=${VERSION}" \
--env="TEST_SUITES=${TEST_SUITES}" \ --env="TEST_SUITES=${TEST_SUITES}" \
--overrides='{ "apiVersion": "v1", "spec":{"serviceAccountName": "external-secrets-e2e"}}' \ --overrides='{ "apiVersion": "v1", "spec":{"serviceAccountName": "external-secrets-e2e"}}' \

View file

@ -27,4 +27,5 @@ import (
_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/template" _ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/template"
_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/vault" _ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/vault"
_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/conjur" _ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/conjur"
_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/secretserver"
) )

View file

@ -0,0 +1,41 @@
package secretserver
import (
"fmt"
"os"
)
type config struct {
username string
password string
serverURL string
}
func loadConfigFromEnv() (*config, error) {
var cfg config
var err error
// Required settings
cfg.username, err = getEnv("SECRETSERVER_USERNAME")
if err != nil {
return nil, err
}
cfg.password, err = getEnv("SECRETSERVER_PASSWORD")
if err != nil {
return nil, err
}
cfg.serverURL, err = getEnv("SECRETSERVER_URL")
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,58 @@
package secretserver
import (
"encoding/json"
"github.com/DelineaXPM/tss-sdk-go/v2/server"
"github.com/external-secrets/external-secrets-e2e/framework"
"github.com/onsi/gomega"
)
type secretStoreProvider struct {
api *server.Server
cfg *config
framework *framework.Framework
secretID map[string]int
}
func (p *secretStoreProvider) init(cfg *config, f *framework.Framework) {
p.cfg = cfg
p.secretID = make(map[string]int)
p.framework = f
secretserverClient, err := server.New(server.Configuration{
Credentials: server.UserCredential{
Username: cfg.username,
Password: cfg.password,
},
ServerURL: cfg.serverURL,
})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
p.api = secretserverClient
}
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())
fields := make([]server.SecretField, 1)
fields[0].FieldID = 329 // Data
fields[0].ItemValue = val.Value
s, err := p.api.CreateSecret(server.Secret{
SecretTemplateID: 6051, // custom template
SiteID: 1,
FolderID: 10,
Name: key,
Fields: fields,
})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
p.secretID[key] = s.ID
}
func (p *secretStoreProvider) DeleteSecret(key string) {
err := p.api.DeleteSecret(p.secretID[key])
gomega.Expect(err).ToNot(gomega.HaveOccurred())
}

View file

@ -0,0 +1,92 @@
package secretserver
import (
"context"
_"fmt"
"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("[secretserver]", ginkgo.Label("secretserver"), func() {
f := framework.New("eso-secretserver")
// Initialization is deferred so that assertions work.
provider := &secretStoreProvider{}
ginkgo.BeforeEach(func() {
cfg, err := loadConfigFromEnv()
gomega.Expect(err).ToNot(gomega.HaveOccurred())
provider.init(cfg, f)
createResources(context.Background(), f, cfg)
})
ginkgo.DescribeTable("sync secrets", framework.TableFuncWithExternalSecret(f, provider),
ginkgo.Entry(common.JSONDataWithTemplate(f)),
ginkgo.Entry(common.JSONDataWithProperty(f)),
ginkgo.Entry(common.JSONDataWithoutTargetName(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)), // <--
)
})
func createResources(ctx context.Context, f *framework.Framework, cfg *config) {
secretName := "secretserver-credential"
secretKey := "password"
// 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.password,
},
}
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{
SecretServer: &esv1beta1.SecretServerProvider{
ServerURL: cfg.serverURL,
Username: &esv1beta1.SecretServerProviderRef{
Value: cfg.username,
},
Password: &esv1beta1.SecretServerProviderRef{
SecretRef: &esmeta.SecretKeySelector{
Name: secretName,
Key: secretKey,
},
},
},
},
},
}
err = f.CRClient.Create(ctx, &secretStoreSpec)
gomega.Expect(err).ToNot(gomega.HaveOccurred())
}

1
go.mod
View file

@ -65,6 +65,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.2 github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.2
github.com/DelineaXPM/tss-sdk-go/v2 v2.0.1
github.com/Onboardbase/go-cryptojs-aes-decrypt v0.0.0-20230430095000-27c0d3a9016d github.com/Onboardbase/go-cryptojs-aes-decrypt v0.0.0-20230430095000-27c0d3a9016d
github.com/akeylesslabs/akeyless-go/v3 v3.6.3 github.com/akeylesslabs/akeyless-go/v3 v3.6.3
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.8 github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.8

2
go.sum
View file

@ -101,6 +101,8 @@ github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 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.2 h1:cmX2QC9s5kPqmghWLLZP8YRFO1ZD/C59BpNH2ujP99w= github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.2 h1:cmX2QC9s5kPqmghWLLZP8YRFO1ZD/C59BpNH2ujP99w=
github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.2/go.mod h1:tNlpIXJlIwQlRbobXDPme4qv/Rc8+a1GbuUhE3m4JhQ= github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.2/go.mod h1:tNlpIXJlIwQlRbobXDPme4qv/Rc8+a1GbuUhE3m4JhQ=
github.com/DelineaXPM/tss-sdk-go/v2 v2.0.1 h1:/rzzzaBuj/FYTcbt8sYZ9IzlnENqcgh5zKqBhHiBBm4=
github.com/DelineaXPM/tss-sdk-go/v2 v2.0.1/go.mod h1:xz6FXP2Do88Vc5Hx7OamZgZC1W45yfmLy4+iDKxlGXo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSgcJLc= github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSgcJLc=

View file

@ -116,6 +116,7 @@ nav:
- Cloak End 2 End Encrypted Secrets: provider/cloak.md - Cloak End 2 End Encrypted Secrets: provider/cloak.md
- Scaleway: provider/scaleway.md - Scaleway: provider/scaleway.md
- Delinea: provider/delinea.md - Delinea: provider/delinea.md
- Secret Server: provider/delinea.md
- Passbolt: provider/passbolt.md - Passbolt: provider/passbolt.md
- Pulumi ESC: provider/pulumi.md - Pulumi ESC: provider/pulumi.md
- Onboardbase: provider/onboardbase.md - Onboardbase: provider/onboardbase.md

View file

@ -42,6 +42,7 @@ import (
_ "github.com/external-secrets/external-secrets/pkg/provider/passworddepot" _ "github.com/external-secrets/external-secrets/pkg/provider/passworddepot"
_ "github.com/external-secrets/external-secrets/pkg/provider/pulumi" _ "github.com/external-secrets/external-secrets/pkg/provider/pulumi"
_ "github.com/external-secrets/external-secrets/pkg/provider/scaleway" _ "github.com/external-secrets/external-secrets/pkg/provider/scaleway"
_ "github.com/external-secrets/external-secrets/pkg/provider/secretserver"
_ "github.com/external-secrets/external-secrets/pkg/provider/senhasegura" _ "github.com/external-secrets/external-secrets/pkg/provider/senhasegura"
_ "github.com/external-secrets/external-secrets/pkg/provider/vault" _ "github.com/external-secrets/external-secrets/pkg/provider/vault"
_ "github.com/external-secrets/external-secrets/pkg/provider/webhook" _ "github.com/external-secrets/external-secrets/pkg/provider/webhook"

View file

@ -0,0 +1,147 @@
/*
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 secretserver
import (
"context"
"encoding/json"
"errors"
"strconv"
"github.com/DelineaXPM/tss-sdk-go/v2/server"
"github.com/tidwall/gjson"
corev1 "k8s.io/api/core/v1"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/utils"
)
type client struct {
api secretAPI
}
var _ esv1beta1.SecretsClient = &client{}
// GetSecret supports two types:
// 1. Get the secrets using the secret ID in ref.key i.e. key: 53974
// 2. Get the secret using the secret "name" i.e. key: "secretNameHere"
// - Secret names must not contain spaces.
// - If using the secret "name" and multiple secrets are found ...
// the first secret in the array will be the secret returned.
// 3. get the full secret as json-encoded value
// by leaving the ref.Property empty.
// 4. get a specific value by using a key from the json formatted secret in Items.0.ItemValue.
// 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 contains no fields
if secret.Fields == nil {
return nil, nil
}
jsonStr, err := json.Marshal(secret)
if err != nil {
return nil, err
}
// If no property is defined return the full secret as raw json
if ref.Property == "" {
return jsonStr, nil
}
// extract first "field" i.e. Items.0.ItemValue, data from secret using gjson
val := gjson.Get(string(jsonStr), "Items.0.ItemValue")
if !val.Exists() {
return nil, esv1beta1.NoSecretError{}
}
// extract specific value from data directly above using gjson
out := gjson.Get(val.String(), ref.Property)
if !out.Exists() {
return nil, esv1beta1.NoSecretError{}
}
return []byte(out.String()), nil
}
// Not supported at this time.
func (c *client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
return errors.New("pushing secrets is not supported by Secret Server at this time")
}
// Not supported at this time.
func (c *client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
return errors.New("deleting secrets is not supported by Secret Server at this time")
}
// Not supported at this time.
func (c *client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
return false, errors.New("not implemented")
}
// Not supported at this time.
func (c *client) Validate() (esv1beta1.ValidationResult, error) {
return esv1beta1.ValidationResultReady, nil
}
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
}
secretData := make(map[string]any)
err = json.Unmarshal([]byte(secret.Fields[0].ItemValue), &secretData)
if err != nil {
return nil, err
}
data := make(map[string][]byte)
for k, v := range secretData {
data[k], err = utils.GetByteValue(v)
if err != nil {
return nil, err
}
}
return data, nil
}
// Not supported at this time.
func (c *client) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
return nil, errors.New("getting all secrets is not supported by Delinea Secret Server at this time")
}
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) (*server.Secret, error) {
if ref.Version != "" {
return nil, errors.New("specifying a version is not supported")
}
id, err := strconv.Atoi(ref.Key)
if err != nil {
s, err := c.api.Secrets(ref.Key, "Name")
if err != nil {
return nil, err
}
if len(s) == 0 {
return nil, errors.New("unable to retrieve secret at this time")
}
return &s[0], nil
}
return c.api.Secret(id)
}

View file

@ -0,0 +1,162 @@
/*
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 secretserver
import (
"context"
"encoding/json"
"errors"
"io"
"os"
"testing"
"github.com/DelineaXPM/tss-sdk-go/v2/server"
"github.com/stretchr/testify/assert"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
)
var (
errNotFound = errors.New("not found")
)
type fakeAPI struct {
secrets []*server.Secret
}
func (f *fakeAPI) Secret(id int) (*server.Secret, error) {
for _, s := range f.secrets {
if s.ID == id {
return s, nil
}
}
return nil, errNotFound
}
func (f *fakeAPI) Secrets(searchText, _ string) ([]server.Secret, error) {
secret := make([]server.Secret, 1)
for _, s := range f.secrets {
if s.Name == searchText {
secret[0] = *s
return secret, nil
}
}
return nil, errNotFound
}
// createSecret assembles a server.Secret from file test_data.json.
func createSecret(id int, itemValue string) *server.Secret {
s, _ := getJSONData()
s.ID = id
s.Fields[0].ItemValue = itemValue
return s
}
func getJSONData() (*server.Secret, error) {
var s = &server.Secret{}
jsonFile, err := os.Open("test_data.json")
if err != nil {
return nil, err
}
defer jsonFile.Close()
byteValue, _ := io.ReadAll(jsonFile)
err = json.Unmarshal(byteValue, &s)
if err != nil {
return nil, err
}
return s, nil
}
func newTestClient() esv1beta1.SecretsClient {
return &client{
api: &fakeAPI{
secrets: []*server.Secret{
createSecret(1000, "{ \"user\": \"robertOppenheimer\", \"password\": \"badPassword\",\"server\":\"192.168.1.50\"}"),
createSecret(2000, "{ \"user\": \"helloWorld\", \"password\": \"badPassword\",\"server\":[ \"192.168.1.50\",\"192.168.1.51\"] }"),
createSecret(3000, "{ \"user\": \"chuckTesta\", \"password\": \"badPassword\",\"server\":\"192.168.1.50\"}"),
},
},
}
}
func TestGetSecret(t *testing.T) {
ctx := context.Background()
c := newTestClient()
s, _ := getJSONData()
jsonStr, _ := json.Marshal(s)
testCases := map[string]struct {
ref esv1beta1.ExternalSecretDataRemoteRef
want []byte
err error
}{
"incorrect key returns nil and error": {
ref: esv1beta1.ExternalSecretDataRemoteRef{
Key: "0",
},
want: []byte(nil),
err: errNotFound,
},
"key = 'secret name' and user property returns a single value": {
ref: esv1beta1.ExternalSecretDataRemoteRef{
Key: "ESO-test-secret",
Property: "user",
},
want: []byte(`robertOppenheimer`),
},
"key and password property returns a single value": {
ref: esv1beta1.ExternalSecretDataRemoteRef{
Key: "1000",
Property: "password",
},
want: []byte(`badPassword`),
},
"key and nested property returns a single value": {
ref: esv1beta1.ExternalSecretDataRemoteRef{
Key: "2000",
Property: "server.1",
},
want: []byte(`192.168.1.51`),
},
"existent key with non-existing propery": {
ref: esv1beta1.ExternalSecretDataRemoteRef{
Key: "3000",
Property: "foo.bar",
},
err: esv1beta1.NoSecretError{},
},
"existent 'name' key with no propery": {
ref: esv1beta1.ExternalSecretDataRemoteRef{
Key: "1000",
},
want: jsonStr,
},
}
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,179 @@
/*
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 secretserver
import (
"context"
"errors"
"github.com/DelineaXPM/tss-sdk-go/v2/server"
kubeClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/utils"
"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
)
var (
errEmptyUserName = errors.New("username must not be empty")
errEmptyPassword = errors.New("password must be set")
errEmptyServerURL = errors.New("serverURL 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 secret server provider")
errClusterStoreRequiresNamespace = errors.New("when using a ClusterSecretStore, namespaces must be explicitly set")
errMissingSecretName = errors.New("must specify a secret name")
errMissingSecretKey = errors.New("must specify a secret key")
)
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
}
username, err := loadConfigSecret(ctx, store.GetKind(), cfg.Username, kube, namespace)
if err != nil {
return nil, err
}
password, err := loadConfigSecret(ctx, store.GetKind(), cfg.Password, kube, namespace)
if err != nil {
return nil, err
}
secretServer, err := server.New(server.Configuration{
Credentials: server.UserCredential{
Username: username,
Password: password,
},
ServerURL: cfg.ServerURL,
})
if err != nil {
return nil, err
}
return &client{
api: secretServer,
}, nil
}
func loadConfigSecret(
ctx context.Context,
storeKind string,
ref *esv1beta1.SecretServerProviderRef,
kube kubeClient.Client,
namespace string) (string, error) {
if ref.SecretRef == nil {
return ref.Value, nil
}
if err := validateSecretRef(ref); err != nil {
return "", err
}
return resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, ref.SecretRef)
}
func validateStoreSecretRef(store esv1beta1.GenericStore, ref *esv1beta1.SecretServerProviderRef) error {
if ref.SecretRef != nil {
if err := utils.ValidateReferentSecretSelector(store, *ref.SecretRef); err != nil {
return err
}
}
return validateSecretRef(ref)
}
func validateSecretRef(ref *esv1beta1.SecretServerProviderRef) 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.SecretServerProvider) bool {
if cfg.Username.SecretRef != nil && cfg.Username.SecretRef.Namespace == nil {
return true
}
if cfg.Password.SecretRef != nil && cfg.Password.SecretRef.Namespace == nil {
return true
}
return false
}
func getConfig(store esv1beta1.GenericStore) (*esv1beta1.SecretServerProvider, error) {
if store == nil {
return nil, errMissingStore
}
storeSpec := store.GetSpec()
if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.SecretServer == nil {
return nil, errInvalidSpec
}
cfg := storeSpec.Provider.SecretServer
if cfg.Username == nil {
return nil, errEmptyUserName
}
if cfg.Password == nil {
return nil, errEmptyPassword
}
if cfg.ServerURL == "" {
return nil, errEmptyServerURL
}
err := validateStoreSecretRef(store, cfg.Username)
if err != nil {
return nil, err
}
err = validateStoreSecretRef(store, cfg.Password)
if err != nil {
return nil, err
}
return cfg, nil
}
func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
_, err := getConfig(store)
return nil, err
}
func init() {
esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
SecretServer: &esv1beta1.SecretServerProvider{},
})
}

View file

@ -0,0 +1,351 @@
/*
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 secretserver
import (
"context"
"math/rand"
"testing"
"github.com/DelineaXPM/tss-sdk-go/v2/server"
"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.SecretServerProvider
want bool
}{
"true when Username references a secret without explicit namespace": {
cfg: esv1beta1.SecretServerProvider{
Username: &esv1beta1.SecretServerProviderRef{
SecretRef: &v1.SecretKeySelector{Name: "foo"},
},
Password: &esv1beta1.SecretServerProviderRef{SecretRef: nil},
},
want: true,
},
"true when password references a secret without explicit namespace": {
cfg: esv1beta1.SecretServerProvider{
Username: &esv1beta1.SecretServerProviderRef{SecretRef: nil},
Password: &esv1beta1.SecretServerProviderRef{
SecretRef: &v1.SecretKeySelector{Name: "foo"},
},
},
want: true,
},
"false when neither Username or Password reference a secret": {
cfg: esv1beta1.SecretServerProvider{
Username: &esv1beta1.SecretServerProviderRef{SecretRef: nil},
Password: &esv1beta1.SecretServerProviderRef{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.SecretServerProviderRef{
SecretRef: &v1.SecretKeySelector{Name: "foo"}, Value: "foo",
}
testURL := "https://example.com"
tests := map[string]struct {
cfg esv1beta1.SecretServerProvider
want error
}{
"invalid without username": {
cfg: esv1beta1.SecretServerProvider{
Username: nil,
Password: validSecretRefUsingValue,
ServerURL: testURL,
},
want: errEmptyUserName,
},
"invalid without password": {
cfg: esv1beta1.SecretServerProvider{
Username: validSecretRefUsingValue,
Password: nil,
ServerURL: testURL,
},
want: errEmptyPassword,
},
"invalid without serverURL": {
cfg: esv1beta1.SecretServerProvider{
Username: validSecretRefUsingValue,
Password: validSecretRefUsingValue,
/*ServerURL: testURL,*/
},
want: errEmptyServerURL,
},
"invalid with ambiguous Username": {
cfg: esv1beta1.SecretServerProvider{
Username: ambiguousSecretRef,
Password: validSecretRefUsingValue,
ServerURL: testURL,
},
want: errSecretRefAndValueConflict,
},
"invalid with ambiguous Password": {
cfg: esv1beta1.SecretServerProvider{
Username: validSecretRefUsingValue,
Password: ambiguousSecretRef,
ServerURL: testURL,
},
want: errSecretRefAndValueConflict,
},
"invalid with invalid Username": {
cfg: esv1beta1.SecretServerProvider{
Username: makeSecretRefUsingValue(""),
Password: validSecretRefUsingValue,
ServerURL: testURL,
},
want: errSecretRefAndValueMissing,
},
"invalid with invalid Password": {
cfg: esv1beta1.SecretServerProvider{
Username: validSecretRefUsingValue,
Password: makeSecretRefUsingValue(""),
ServerURL: testURL,
},
want: errSecretRefAndValueMissing,
},
"valid with tenant/clientID/clientSecret": {
cfg: esv1beta1.SecretServerProvider{
Username: validSecretRefUsingValue,
Password: validSecretRefUsingValue,
ServerURL: testURL,
},
want: nil,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
s := esv1beta1.SecretStore{
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
SecretServer: &tc.cfg,
},
},
}
p := &Provider{}
_, got := p.ValidateStore(&s)
assert.Equal(t, tc.want, got)
})
}
}
func TestNewClient(t *testing.T) {
userNameKey := "username"
userNameValue := "foo"
passwordKey := "password"
passwordValue := generateRandomString()
clientSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"},
Data: map[string][]byte{
userNameKey: []byte(userNameValue),
passwordKey: []byte(passwordValue),
},
}
validProvider := &esv1beta1.SecretServerProvider{
Username: makeSecretRefUsingRef(clientSecret.Name, userNameKey),
Password: makeSecretRefUsingRef(clientSecret.Name, passwordKey),
ServerURL: "https://example.com",
}
tests := map[string]struct {
store esv1beta1.GenericStore // leave nil for namespaced store
provider *esv1beta1.SecretServerProvider // 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{
SecretServer: validProvider,
},
},
},
errCheck: func(t *testing.T, err error) {
assert.ErrorIs(t, err, errClusterStoreRequiresNamespace)
},
},
"dangling password ref": {
provider: &esv1beta1.SecretServerProvider{
Username: validProvider.Username,
Password: makeSecretRefUsingRef("typo", passwordKey),
ServerURL: validProvider.ServerURL,
},
kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
errCheck: func(t *testing.T, err error) {
assert.True(t, kubeErrors.IsNotFound(err))
},
},
"dangling username ref": {
provider: &esv1beta1.SecretServerProvider{
Username: makeSecretRefUsingRef("typo", userNameKey),
Password: validProvider.Password,
ServerURL: validProvider.ServerURL,
},
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.SecretServerProvider{
Username: makeSecretRefUsingRef("", userNameKey),
Password: validProvider.Password,
ServerURL: validProvider.ServerURL,
},
kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
errCheck: func(t *testing.T, err error) {
assert.ErrorIs(t, err, errMissingSecretName)
},
},
"secret ref without key": {
provider: &esv1beta1.SecretServerProvider{
Username: validProvider.Password,
Password: makeSecretRefUsingRef(clientSecret.Name, ""),
ServerURL: validProvider.ServerURL,
},
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.SecretServerProvider{
Username: makeSecretRefUsingRef(clientSecret.Name, "typo"),
Password: makeSecretRefUsingRef(clientSecret.Name, passwordKey),
ServerURL: validProvider.ServerURL,
},
kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
errCheck: func(t *testing.T, err error) {
assert.EqualError(t, err, "cannot find secret data for key: \"typo\"")
},
},
"valid secret refs": {
provider: validProvider,
kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
},
"secret values": {
provider: &esv1beta1.SecretServerProvider{
Username: makeSecretRefUsingValue(userNameValue),
Password: makeSecretRefUsingValue(passwordValue),
ServerURL: validProvider.ServerURL,
},
kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
},
"cluster secret store": {
store: &esv1beta1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{Kind: esv1beta1.ClusterSecretStoreKind},
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
SecretServer: &esv1beta1.SecretServerProvider{
Username: makeSecretRefUsingNamespacedRef(clientSecret.Namespace, clientSecret.Name, userNameKey),
Password: makeSecretRefUsingNamespacedRef(clientSecret.Namespace, clientSecret.Name, passwordKey),
ServerURL: validProvider.ServerURL,
},
},
},
},
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{
SecretServer: 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)
secretServerClient, ok := delineaClient.api.(*server.Server)
assert.True(t, ok)
assert.Equal(t, server.UserCredential{
Username: userNameValue,
Password: passwordValue,
}, secretServerClient.Configuration.Credentials)
} else {
assert.Nil(t, sc)
tc.errCheck(t, err)
}
})
}
}
func makeSecretRefUsingNamespacedRef(namespace, name, key string) *esv1beta1.SecretServerProviderRef {
return &esv1beta1.SecretServerProviderRef{
SecretRef: &v1.SecretKeySelector{Namespace: utils.Ptr(namespace), Name: name, Key: key},
}
}
func makeSecretRefUsingValue(val string) *esv1beta1.SecretServerProviderRef {
return &esv1beta1.SecretServerProviderRef{Value: val}
}
func makeSecretRefUsingRef(name, key string) *esv1beta1.SecretServerProviderRef {
return &esv1beta1.SecretServerProviderRef{
SecretRef: &v1.SecretKeySelector{Name: name, Key: key},
}
}
func generateRandomString() string {
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
b := make([]rune, 10)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

View file

@ -0,0 +1,26 @@
/*
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 secretserver
import (
"github.com/DelineaXPM/tss-sdk-go/v2/server"
)
// secretAPI represents the subset of the Secret Server API
// which is supported by tss-sdk-go/v2.
type secretAPI interface {
Secret(id int) (*server.Secret, error)
Secrets(searchText, field string) ([]server.Secret, error)
}

View file

@ -0,0 +1,38 @@
{
"Name": "ESO-test-secret",
"FolderID": 73,
"ID": 1000,
"SiteID": 1,
"SecretTemplateID": 6098,
"SecretPolicyID": -1,
"PasswordTypeWebScriptID": -1,
"LauncherConnectAsSecretID": -1,
"CheckOutIntervalMinutes": -1,
"Active": true,
"CheckedOut": false,
"CheckOutEnabled": false,
"AutoChangeEnabled": false,
"CheckOutChangePasswordEnabled": false,
"DelayIndexing": false,
"EnableInheritPermissions": false,
"EnableInheritSecretPolicy": false,
"ProxyEnabled": false,
"RequiresComment": false,
"SessionRecordingEnabled": false,
"WebLauncherRequiresIncognitoMode": false,
"Items": [
{
"ItemID": 286259,
"FieldID": 439,
"FileAttachmentID": 0,
"FieldName": "Data",
"Slug": "data",
"FieldDescription": "json text field",
"Filename": "",
"ItemValue": "{ \"user\": \"robertOppenheimer\", \"password\": \"badPassword\",\"server\":\"192.168.1.50\"}",
"IsFile": false,
"IsNotes": false,
"IsPassword": false
}
]
}