diff --git a/apis/externalsecrets/v1beta1/secretsstore_secretserver_types.go b/apis/externalsecrets/v1beta1/secretsstore_secretserver_types.go
new file mode 100644
index 000000000..41d75dade
--- /dev/null
+++ b/apis/externalsecrets/v1beta1/secretsstore_secretserver_types.go
@@ -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"`
+}
diff --git a/apis/externalsecrets/v1beta1/secretstore_types.go b/apis/externalsecrets/v1beta1/secretstore_types.go
index 112e58860..d482c078c 100644
--- a/apis/externalsecrets/v1beta1/secretstore_types.go
+++ b/apis/externalsecrets/v1beta1/secretstore_types.go
@@ -155,6 +155,11 @@ type SecretStoreProvider struct {
// +optional
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
// +optional
Chef *ChefProvider `json:"chef,omitempty"`
diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
index 2fc04558c..05a706316 100644
--- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
+++ b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
@@ -2266,6 +2266,51 @@ func (in *ScalewayProviderSecretRef) DeepCopy() *ScalewayProviderSecretRef {
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.
func (in *SecretStore) DeepCopyInto(out *SecretStore) {
*out = *in
@@ -2443,6 +2488,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
*out = new(DelineaProvider)
(*in).DeepCopyInto(*out)
}
+ if in.SecretServer != nil {
+ in, out := &in.SecretServer, &out.SecretServer
+ *out = new(SecretServerProvider)
+ (*in).DeepCopyInto(*out)
+ }
if in.Chef != nil {
in, out := &in.Chef, &out.Chef
*out = new(ChefProvider)
diff --git a/config/crds/bases/external-secrets.io_clustersecretstores.yaml b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
index cba6e4b8d..310c1aa79 100644
--- a/config/crds/bases/external-secrets.io_clustersecretstores.yaml
+++ b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
@@ -3732,6 +3732,75 @@ spec:
- region
- secretKey
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:
description: Senhasegura configures this store to sync secrets
using senhasegura provider
diff --git a/config/crds/bases/external-secrets.io_secretstores.yaml b/config/crds/bases/external-secrets.io_secretstores.yaml
index e9ff3e815..b6a25e66e 100644
--- a/config/crds/bases/external-secrets.io_secretstores.yaml
+++ b/config/crds/bases/external-secrets.io_secretstores.yaml
@@ -3732,6 +3732,75 @@ spec:
- region
- secretKey
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:
description: Senhasegura configures this store to sync secrets
using senhasegura provider
diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml
index c4c9a04b5..d15190a08 100644
--- a/deploy/crds/bundle.yaml
+++ b/deploy/crds/bundle.yaml
@@ -4121,6 +4121,69 @@ spec:
- region
- secretKey
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:
description: Senhasegura configures this store to sync secrets using senhasegura provider
properties:
@@ -9684,6 +9747,69 @@ spec:
- region
- secretKey
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:
description: Senhasegura configures this store to sync secrets using senhasegura provider
properties:
diff --git a/docs/api/spec.md b/docs/api/spec.md
index 2613780dc..554706bba 100644
--- a/docs/api/spec.md
+++ b/docs/api/spec.md
@@ -5924,6 +5924,107 @@ External Secrets meta/v1.SecretKeySelector
+
SecretServerProvider
+
+
+(Appears on:
+SecretStoreProvider)
+
+
+
See https://github.com/DelineaXPM/tss-sdk-go/blob/main/server/server.go.
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+username
+
+
+SecretServerProviderRef
+
+
+ |
+
+ Username is the secret server account username.
+ |
+
+
+
+password
+
+
+SecretServerProviderRef
+
+
+ |
+
+ Password is the secret server account password.
+ |
+
+
+
+serverURL
+
+string
+
+ |
+
+ ServerURL
+URL to your secret server installation
+ |
+
+
+
+SecretServerProviderRef
+
+
+(Appears on:
+SecretServerProvider)
+
+
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+value
+
+string
+
+ |
+
+(Optional)
+ Value can be specified directly to set a value without using a secret.
+ |
+
+
+
+secretRef
+
+
+External Secrets meta/v1.SecretKeySelector
+
+
+ |
+
+(Optional)
+ SecretRef references a key in a secret that will be used as value.
+ |
+
+
+
SecretStore
@@ -6432,6 +6533,21 @@ DelineaProvider
+secretserver
+
+
+SecretServerProvider
+
+
+ |
+
+(Optional)
+ SecretServer configures this store to sync secrets using SecretServer provider
+https://docs.delinea.com/online-help/secret-server/start.htm
+ |
+
+
+
chef
diff --git a/docs/introduction/stability-support.md b/docs/introduction/stability-support.md
index 189dbfa00..f617a9255 100644
--- a/docs/introduction/stability-support.md
+++ b/docs/introduction/stability-support.md
@@ -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/) |
| [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/) |
+| [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) |
| [Passbolt](https://external-secrets.io/latest/provider/passbolt) | alpha | |
| [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 |
| Conjur | x | x | | | x | | |
| Delinea | x | | | | x | | |
+| SecretServer | x | | | | x | | |
| Pulumi ESC | x | | | | x | | |
| Passbolt | x | | | | x | | |
| Infisical | x | | | x | x | | |
diff --git a/docs/provider/secretserver.md b/docs/provider/secretserver.md
new file mode 100644
index 000000000..e7eeafcf9
--- /dev/null
+++ b/docs/provider/secretserver.md
@@ -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"
+spec.provider.secretserver.password.value: "yourpassword"
+
+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:
+ key:
+```
+
+### 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.
+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.
+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 #
+ remoteRef:
+ key: "52622" #
+ property: "array.0.value" # * 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.
+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)
+spec.data.remoteRef.property = "user" (Items.0.ItemValue user attribute)
+returns: marktwain@hannibal.com
+
+- Lookup a nested property using secret name.
+
+>spec.data.remoteRef.key = "external-secret-testing" (name of the secret)
+spec.data.remoteRef.property = "books.1" (Items.0.ItemValue books.1 attribute)
+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)
+spec.data.remoteRef.property = ""
+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
+ }
+ ]
+}
+```
diff --git a/e2e/go.mod b/e2e/go.mod
index 5ba119845..9cc0cc00b 100644
--- a/e2e/go.mod
+++ b/e2e/go.mod
@@ -44,6 +44,7 @@ require (
github.com/Azure/go-autorest/autorest v0.11.29
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13
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/v3 v3.6.3
github.com/aliyun/alibaba-cloud-sdk-go v1.62.271
diff --git a/e2e/go.sum b/e2e/go.sum
index ef1a96309..39c136281 100644
--- a/e2e/go.sum
+++ b/e2e/go.sum
@@ -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/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/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/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
diff --git a/e2e/run.sh b/e2e/run.sh
index f71c134a1..52cebd976 100755
--- a/e2e/run.sh
+++ b/e2e/run.sh
@@ -84,6 +84,9 @@ kubectl run --rm \
--env="DELINEA_TENANT=${DELINEA_TENANT:-}" \
--env="DELINEA_CLIENT_ID=${DELINEA_CLIENT_ID:-}" \
--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="TEST_SUITES=${TEST_SUITES}" \
--overrides='{ "apiVersion": "v1", "spec":{"serviceAccountName": "external-secrets-e2e"}}' \
diff --git a/e2e/suites/provider/cases/import.go b/e2e/suites/provider/cases/import.go
index e8561c561..5a20e529f 100644
--- a/e2e/suites/provider/cases/import.go
+++ b/e2e/suites/provider/cases/import.go
@@ -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/vault"
_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/conjur"
+ _ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/secretserver"
)
diff --git a/e2e/suites/provider/cases/secretserver/config.go b/e2e/suites/provider/cases/secretserver/config.go
new file mode 100644
index 000000000..bb7bc8651
--- /dev/null
+++ b/e2e/suites/provider/cases/secretserver/config.go
@@ -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
+}
diff --git a/e2e/suites/provider/cases/secretserver/provider.go b/e2e/suites/provider/cases/secretserver/provider.go
new file mode 100644
index 000000000..9b1b7cf33
--- /dev/null
+++ b/e2e/suites/provider/cases/secretserver/provider.go
@@ -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())
+}
diff --git a/e2e/suites/provider/cases/secretserver/secretserver.go b/e2e/suites/provider/cases/secretserver/secretserver.go
new file mode 100644
index 000000000..f0ba2bfee
--- /dev/null
+++ b/e2e/suites/provider/cases/secretserver/secretserver.go
@@ -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())
+}
diff --git a/go.mod b/go.mod
index 3e42fa63b..9fc3771e0 100644
--- a/go.mod
+++ b/go.mod
@@ -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/azidentity v1.7.0
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/akeylesslabs/akeyless-go/v3 v3.6.3
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.8
diff --git a/go.sum b/go.sum
index 6197090c1..964d60d3c 100644
--- a/go.sum
+++ b/go.sum
@@ -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/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/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/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSgcJLc=
diff --git a/hack/api-docs/mkdocs.yml b/hack/api-docs/mkdocs.yml
index 700701001..2d5d8bffa 100644
--- a/hack/api-docs/mkdocs.yml
+++ b/hack/api-docs/mkdocs.yml
@@ -116,6 +116,7 @@ nav:
- Cloak End 2 End Encrypted Secrets: provider/cloak.md
- Scaleway: provider/scaleway.md
- Delinea: provider/delinea.md
+ - Secret Server: provider/delinea.md
- Passbolt: provider/passbolt.md
- Pulumi ESC: provider/pulumi.md
- Onboardbase: provider/onboardbase.md
diff --git a/pkg/provider/register/register.go b/pkg/provider/register/register.go
index 976a825cb..ab0e5407f 100644
--- a/pkg/provider/register/register.go
+++ b/pkg/provider/register/register.go
@@ -42,6 +42,7 @@ import (
_ "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/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/vault"
_ "github.com/external-secrets/external-secrets/pkg/provider/webhook"
diff --git a/pkg/provider/secretserver/client.go b/pkg/provider/secretserver/client.go
new file mode 100644
index 000000000..c3f19db3b
--- /dev/null
+++ b/pkg/provider/secretserver/client.go
@@ -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)
+}
diff --git a/pkg/provider/secretserver/client_test.go b/pkg/provider/secretserver/client_test.go
new file mode 100644
index 000000000..c338de70e
--- /dev/null
+++ b/pkg/provider/secretserver/client_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/pkg/provider/secretserver/provider.go b/pkg/provider/secretserver/provider.go
new file mode 100644
index 000000000..e44701316
--- /dev/null
+++ b/pkg/provider/secretserver/provider.go
@@ -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{},
+ })
+}
diff --git a/pkg/provider/secretserver/provider_test.go b/pkg/provider/secretserver/provider_test.go
new file mode 100644
index 000000000..53a14fc4f
--- /dev/null
+++ b/pkg/provider/secretserver/provider_test.go
@@ -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)
+}
diff --git a/pkg/provider/secretserver/secret_api.go b/pkg/provider/secretserver/secret_api.go
new file mode 100644
index 000000000..f8beacff6
--- /dev/null
+++ b/pkg/provider/secretserver/secret_api.go
@@ -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)
+}
diff --git a/pkg/provider/secretserver/test_data.json b/pkg/provider/secretserver/test_data.json
new file mode 100644
index 000000000..611e5906d
--- /dev/null
+++ b/pkg/provider/secretserver/test_data.json
@@ -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
+ }
+]
+}
|