Browse Source

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>
Bill Hamilton 1 year ago
parent
commit
1876ff88d7

+ 45 - 0
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"`
+}

+ 5 - 0
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"`

+ 50 - 0
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -2267,6 +2267,51 @@ func (in *ScalewayProviderSecretRef) DeepCopy() *ScalewayProviderSecretRef {
 }
 
 // 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
 	out.TypeMeta = in.TypeMeta
@@ -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)

+ 69 - 0
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

+ 69 - 0
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

+ 126 - 0
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:

+ 116 - 0
docs/api/spec.md

@@ -5924,6 +5924,107 @@ External Secrets meta/v1.SecretKeySelector
 </tr>
 </tbody>
 </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>
 <p>
@@ -6432,6 +6533,21 @@ DelineaProvider
 </tr>
 <tr>
 <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>
 <em>
 <a href="#external-secrets.io/v1beta1.ChefProvider">

+ 2 - 0
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         |             |                             |

+ 133 - 0
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"<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
+    }
+  ]
+}
+```

+ 1 - 0
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

+ 2 - 0
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=

+ 3 - 0
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"}}' \

+ 1 - 0
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"
 )

+ 41 - 0
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
+}

+ 58 - 0
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())
+}

+ 92 - 0
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())
+}

+ 1 - 0
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

+ 2 - 0
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=

+ 1 - 0
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

+ 1 - 0
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"

+ 147 - 0
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)
+}

+ 162 - 0
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)
+			}
+		})
+	}
+}

+ 179 - 0
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{},
+	})
+}

+ 351 - 0
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)
+}

+ 26 - 0
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)
+}

+ 38 - 0
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
+  }
+]
+}