Browse Source

Merge pull request #1075 from lfraga/feat/provider-senhasegura-dsm

Add senhasegura DevOps Secrets Management (DSM) provider
paul-the-alien[bot] 4 years ago
parent
commit
9838d44bae

+ 57 - 0
apis/externalsecrets/v1beta1/secretstore_senhasegura_types.go

@@ -0,0 +1,57 @@
+/*
+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"
+
+/*
+	SenhaseguraAuth tells the controller how to do auth in senhasegura
+*/
+type SenhaseguraAuth struct {
+	ClientID     string                   `json:"clientId"`
+	ClientSecret esmeta.SecretKeySelector `json:"clientSecretSecretRef"`
+}
+
+/*
+	SenhaseguraModuleType enum defines senhasegura target module to fetch secrets
+	+kubebuilder:validation:Enum=DSM
+*/
+type SenhaseguraModuleType string
+
+const (
+	/*
+		SenhaseguraModuleDSM is the senhasegura DevOps Secrets Management module
+		see: https://senhasegura.com/devops
+	*/
+	SenhaseguraModuleDSM SenhaseguraModuleType = "DSM"
+)
+
+/*
+	SenhaseguraProvider setup a store to sync secrets with senhasegura
+*/
+type SenhaseguraProvider struct {
+	/* URL of senhasegura */
+	URL string `json:"url"`
+
+	/* Module defines which senhasegura module should be used to get secrets */
+	Module SenhaseguraModuleType `json:"module"`
+
+	/* Auth defines parameters to authenticate in senhasegura */
+	Auth SenhaseguraAuth `json:"auth"`
+
+	// IgnoreSslCertificate defines if SSL certificate must be ignored
+	// +kubebuilder:default=false
+	IgnoreSslCertificate bool `json:"ignoreSslCertificate,omitempty"`
+}

+ 4 - 0
apis/externalsecrets/v1beta1/secretstore_types.go

@@ -93,6 +93,10 @@ type SecretStoreProvider struct {
 	// Fake configures a store with static key/value pairs
 	// +optional
 	Fake *FakeProvider `json:"fake,omitempty"`
+
+	// Senhasegura configures this store to sync secrets using senhasegura provider
+	// +optional
+	Senhasegura *SenhaseguraProvider `json:"senhasegura,omitempty"`
 }
 
 type SecretStoreRetrySettings struct {

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

@@ -1351,6 +1351,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(FakeProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Senhasegura != nil {
+		in, out := &in.Senhasegura, &out.Senhasegura
+		*out = new(SenhaseguraProvider)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
@@ -1467,6 +1472,38 @@ func (in *SecretStoreStatusCondition) DeepCopy() *SecretStoreStatusCondition {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SenhaseguraAuth) DeepCopyInto(out *SenhaseguraAuth) {
+	*out = *in
+	in.ClientSecret.DeepCopyInto(&out.ClientSecret)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SenhaseguraAuth.
+func (in *SenhaseguraAuth) DeepCopy() *SenhaseguraAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(SenhaseguraAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SenhaseguraProvider) DeepCopyInto(out *SenhaseguraProvider) {
+	*out = *in
+	in.Auth.DeepCopyInto(&out.Auth)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SenhaseguraProvider.
+func (in *SenhaseguraProvider) DeepCopy() *SenhaseguraProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(SenhaseguraProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ServiceAccountAuth) DeepCopyInto(out *ServiceAccountAuth) {
 	*out = *in
 	in.ServiceAccountRef.DeepCopyInto(&out.ServiceAccountRef)

+ 50 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -2164,6 +2164,56 @@ spec:
                     - region
                     - vault
                     type: object
+                  senhasegura:
+                    description: Senhasegura configures this store to sync secrets
+                      using senhasegura provider
+                    properties:
+                      auth:
+                        description: Auth defines parameters to authenticate in senhasegura
+                        properties:
+                          clientId:
+                            type: string
+                          clientSecretSecretRef:
+                            description: A reference to a specific 'key' within a
+                              Secret resource, In some instances, `key` is a required
+                              field.
+                            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
+                        required:
+                        - clientId
+                        - clientSecretSecretRef
+                        type: object
+                      ignoreSslCertificate:
+                        default: false
+                        description: IgnoreSslCertificate defines if SSL certificate
+                          must be ignored
+                        type: boolean
+                      module:
+                        description: Module defines which senhasegura module should
+                          be used to get secrets
+                        type: string
+                      url:
+                        description: URL of senhasegura
+                        type: string
+                    required:
+                    - auth
+                    - module
+                    - url
+                    type: object
                   vault:
                     description: Vault configures this store to sync secrets using
                       Hashi provider

+ 50 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -2167,6 +2167,56 @@ spec:
                     - region
                     - vault
                     type: object
+                  senhasegura:
+                    description: Senhasegura configures this store to sync secrets
+                      using senhasegura provider
+                    properties:
+                      auth:
+                        description: Auth defines parameters to authenticate in senhasegura
+                        properties:
+                          clientId:
+                            type: string
+                          clientSecretSecretRef:
+                            description: A reference to a specific 'key' within a
+                              Secret resource, In some instances, `key` is a required
+                              field.
+                            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
+                        required:
+                        - clientId
+                        - clientSecretSecretRef
+                        type: object
+                      ignoreSslCertificate:
+                        default: false
+                        description: IgnoreSslCertificate defines if SSL certificate
+                          must be ignored
+                        type: boolean
+                      module:
+                        description: Module defines which senhasegura module should
+                          be used to get secrets
+                        type: string
+                      url:
+                        description: URL of senhasegura
+                        type: string
+                    required:
+                    - auth
+                    - module
+                    - url
+                    type: object
                   vault:
                     description: Vault configures this store to sync secrets using
                       Hashi provider

+ 80 - 0
deploy/crds/bundle.yaml

@@ -1939,6 +1939,46 @@ spec:
                         - region
                         - vault
                       type: object
+                    senhasegura:
+                      description: Senhasegura configures this store to sync secrets using senhasegura provider
+                      properties:
+                        auth:
+                          description: Auth defines parameters to authenticate in senhasegura
+                          properties:
+                            clientId:
+                              type: string
+                            clientSecretSecretRef:
+                              description: A reference to a specific 'key' within a Secret resource, In some instances, `key` is a required field.
+                              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
+                          required:
+                            - clientId
+                            - clientSecretSecretRef
+                          type: object
+                        ignoreSslCertificate:
+                          default: false
+                          description: IgnoreSslCertificate defines if SSL certificate must be ignored
+                          type: boolean
+                        module:
+                          description: Module defines which senhasegura module should be used to get secrets
+                          type: string
+                        url:
+                          description: URL of senhasegura
+                          type: string
+                      required:
+                        - auth
+                        - module
+                        - url
+                      type: object
                     vault:
                       description: Vault configures this store to sync secrets using Hashi provider
                       properties:
@@ -4493,6 +4533,46 @@ spec:
                         - region
                         - vault
                       type: object
+                    senhasegura:
+                      description: Senhasegura configures this store to sync secrets using senhasegura provider
+                      properties:
+                        auth:
+                          description: Auth defines parameters to authenticate in senhasegura
+                          properties:
+                            clientId:
+                              type: string
+                            clientSecretSecretRef:
+                              description: A reference to a specific 'key' within a Secret resource, In some instances, `key` is a required field.
+                              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
+                          required:
+                            - clientId
+                            - clientSecretSecretRef
+                          type: object
+                        ignoreSslCertificate:
+                          default: false
+                          description: IgnoreSslCertificate defines if SSL certificate must be ignored
+                          type: boolean
+                        module:
+                          description: Module defines which senhasegura module should be used to get secrets
+                          type: string
+                        url:
+                          description: URL of senhasegura
+                          type: string
+                      required:
+                        - auth
+                        - module
+                        - url
+                      type: object
                     vault:
                       description: Vault configures this store to sync secrets using Hashi provider
                       properties:

+ 144 - 0
docs/provider-senhasegura-dsm.md

@@ -0,0 +1,144 @@
+## senhasegura DevOps Secrets Management (DSM)
+
+External Secrets Operator integrates with [senhasegura](https://senhasegura.com/) [DevOps Secrets Management (DSM)](https://senhasegura.com/devops) module to sync application secrets to secrets held on the Kubernetes cluster.
+
+---
+
+## Authentication
+
+Authentication in senhasegura uses DevOps Secrets Management (DSM) application authorization schema
+
+You need to create an Kubernetes Secret with desired auth parameters, for example:
+
+Instructions to setup authorizations and secrets in senhasegura DSM can be found at [senhasegura docs for DSM](https://helpcenter.senhasegura.io/docs/3.22/dsm) and [senhasegura YouTube channel](https://www.youtube.com/channel/UCpDms35l3tcrfb8kZSpeNYw/search?query=DSM%2C%20en-US)
+
+```yaml
+{% include 'senhasegura-dsm-secret.yaml' %}
+```
+
+---
+
+## Examples
+
+To sync secrets between senhasegura and Kubernetes with External Secrets, we need to define an SecretStore or ClusterSecretStore resource with senhasegura provider, setting authentication in DSM module with Secret defined before
+
+### SecretStore
+
+``` yaml
+{% include 'senhasegura-dsm-secretstore.yaml' %}
+```
+
+### ClusterSecretStore
+
+``` yaml
+{% include 'senhasegura-dsm-clustersecretstore.yaml' %}
+```
+
+---
+
+## Syncing secrets
+
+In examples below, consider that three secrets (api-settings, db-settings and hsm-settings) are defined in senhasegura DSM
+
+---
+
+**Secret Identifier: ** api-settings
+
+**Secret data:** 
+
+```bash
+URL=https://example.com/api/example
+TOKEN=example-token-value
+```
+
+---
+
+**Secret Identifier: ** db-settings
+
+**Secret data:** 
+
+```bash
+DB_HOST='db.example'
+DB_PORT='5432'
+DB_USERNAME='example'
+DB_PASSWORD='example'
+```
+
+---
+
+**Secret Identifier: ** hsm-settings
+
+**Secret data:** 
+
+```bash
+HSM_ADDRESS='hsm.example'
+HSM_PORT='9223'
+```
+
+
+---
+
+### Sync DSM secrets using Secret Identifiers
+
+You can fetch all key/value pairs for a given secret identifier If you leave the remoteRef.property empty. This returns the json-encoded secret value for that path.
+
+If you only need a specific key, you can select it using remoteRef.property as the key name.
+
+In this method, you can overwrites data name in Kubernetes Secret object (e.g API_SETTINGS and API_SETTINGS_TOKEN)
+
+``` yaml
+{% include 'senhasegura-dsm-external-secret-single.yaml' %}
+```
+
+Kubernetes Secret will be create with follow `.data.X`
+
+```bash
+API_SETTINGS='[{"TOKEN":"example-token-value","URL":"https://example.com/api/example"}]'
+API_SETTINGS_TOKEN='example-token-value'
+```
+
+---
+
+### Sync DSM secrets using Secret Identifiers with automatically name assignments
+
+If your app requires multiples secrets, it is not required to create multiple ExternalSecret resources, you can aggregate secrets using a single ExternalSecret resource
+
+In this method, every secret data in senhasegura creates an Kubernetes Secret `.data.X` field
+
+``` yaml
+{% include 'senhasegura-dsm-external-secret-multiple.yaml' %}
+```
+
+Kubernetes Secret will be create with follow `.data.X`
+
+```bash
+URL='https://example.com/api/example'
+TOKEN='example-token-value'
+DB_HOST='db.example'
+DB_PORT='5432'
+DB_USERNAME='example'
+DB_PASSWORD='example'
+```
+
+<!-- https://github.com/external-secrets/external-secrets/pull/830#discussion_r858657107 -->
+
+<!-- ### Sync all secrets from DSM authorization
+
+You can sync all secrets that your authorization in DSM has using find, in a future release you will be able to filter secrets by name, path or tags
+
+``` yaml
+{% include 'senhasegura-dsm-external-secret-all.yaml' %}
+```
+
+Kubernetes Secret will be create with follow `.data.X`
+
+```bash
+URL='https://example.com/api/example'
+TOKEN='example-token-value'
+DB_HOST='db.example'
+DB_PORT='5432'
+DB_USERNAME='example'
+DB_PASSWORD='example'
+HSM_ADDRESS='hsm.example'
+HSM_PORT='9223'
+``` -->

+ 17 - 0
docs/snippets/senhasegura-dsm-clustersecretstore.yaml

@@ -0,0 +1,17 @@
+---
+apiVersion: external-secrets.io/v1beta1
+kind: ClusterSecretStore
+metadata:
+  name: senhasegura
+spec:
+  provider:
+    senhasegura:
+      url: "https://senhasegura.changeme.com"
+      module: DSM # Select senhasegura DSM module to sync secrets
+      auth:
+        clientId: "CHANGEME"
+        clientSecretSecretRef:
+          name: senhasegura-dsm-auth
+          key: CLIENT_SECRET
+          namespace: senhasegura # Namespace of Secret "senhasegura-dsm-auth"
+      ignoreSslCertificate: false # Optional

+ 14 - 0
docs/snippets/senhasegura-dsm-external-secret-all.yaml

@@ -0,0 +1,14 @@
+---
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: example-secret
+spec:
+  refreshInterval: "30s"
+  secretStoreRef:
+    name: senhasegura
+    kind: SecretStore
+  target:
+    name: example-secret
+  dataFrom:
+  - find: {}

+ 18 - 0
docs/snippets/senhasegura-dsm-external-secret-multiple.yaml

@@ -0,0 +1,18 @@
+---
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: example-secret
+spec:
+  refreshInterval: "30s"
+  secretStoreRef:
+    name: senhasegura
+    kind: SecretStore
+  target:
+    name: example-secret
+  dataFrom:
+  # Define Kubernetes Secret key with any k/v pair in senhasegura Secret with identifier "api-settings" or "db-settings"
+  - extract:
+      key: api-settings
+  - extract:
+      key: db-settings

+ 22 - 0
docs/snippets/senhasegura-dsm-external-secret-single.yaml

@@ -0,0 +1,22 @@
+---
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: example-secret
+spec:
+  refreshInterval: "30s"
+  secretStoreRef:
+    name: senhasegura
+    kind: SecretStore
+  target:
+    name: example-secret
+  data:
+  # Define API_SETTINGS Kubernetes Secret key, with json-encoded values from senhasegura secret with identifier "api-settings"
+  - secretKey: API_SETTINGS
+    remoteRef:
+      key: api-settings # Secret Identifier in senhasegura
+  # Define API_SETTINGS_TOKEN Kubernetes Secret key, with single secret key (TOKEN) from senhasegura as string
+  - secretKey: API_SETTINGS_TOKEN
+    remoteRef:
+      key: api-settings # Secret Identifier in senhasegura
+      property: TOKEN # Optional, Key name within secret

+ 7 - 0
docs/snippets/senhasegura-dsm-secret.yaml

@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: senhasegura-dsm-auth
+stringData:
+  CLIENT_SECRET: "CHANGEME"

+ 16 - 0
docs/snippets/senhasegura-dsm-secretstore.yaml

@@ -0,0 +1,16 @@
+---
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: senhasegura
+spec:
+  provider:
+    senhasegura:
+      url: "https://senhasegura.changeme.com"
+      module: DSM # Select senhasegura DSM module to sync secrets
+      auth:
+        clientId: "CHANGEME"
+        clientSecretSecretRef:
+          name: senhasegura-dsm-auth
+          key: CLIENT_SECRET
+      ignoreSslCertificate: false # Optional

+ 147 - 1
docs/spec.md

@@ -3258,7 +3258,7 @@ GitlabProvider
 </td>
 <td>
 <em>(Optional)</em>
-<p>GItlab configures this store to sync secrets using Gitlab Variables provider</p>
+<p>Gitlab configures this store to sync secrets using Gitlab Variables provider</p>
 </td>
 </tr>
 <tr>
@@ -3317,6 +3317,20 @@ FakeProvider
 <p>Fake configures a store with static key/value pairs</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>senhasegura</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.SenhaseguraProvider">
+SenhaseguraProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Senhasegura configures this store to sync secrets using senhasegura provider</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef
@@ -3587,6 +3601,138 @@ Kubernetes meta/v1.Time
 <p>
 <p>SecretsClient provides access to secrets.</p>
 </p>
+<h3 id="external-secrets.io/v1beta1.SenhaseguraAuth">SenhaseguraAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.SenhaseguraProvider">SenhaseguraProvider</a>)
+</p>
+<p>
+<pre><code>SenhaseguraAuth tells the controller how to do auth in senhasegura
+</code></pre>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>clientId</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>clientSecretSecretRef</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.SenhaseguraModuleType">SenhaseguraModuleType
+(<code>string</code> alias)</p></h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.SenhaseguraProvider">SenhaseguraProvider</a>)
+</p>
+<p>
+<pre><code>SenhaseguraModuleType enum defines senhasegura target module to fetch secrets
+</code></pre>
+</p>
+<table>
+<thead>
+<tr>
+<th>Value</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody><tr><td><p>&#34;DSM&#34;</p></td>
+<td><pre><code>	SenhaseguraModuleDSM is the senhasegura DevOps Secrets Management module
+see: https://senhasegura.com/devops
+</code></pre>
+</td>
+</tr></tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.SenhaseguraProvider">SenhaseguraProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<pre><code>SenhaseguraProvider setup a store to sync secrets with senhasegura
+</code></pre>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>url</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>URL of senhasegura</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>module</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.SenhaseguraModuleType">
+SenhaseguraModuleType
+</a>
+</em>
+</td>
+<td>
+<p>Module defines which senhasegura module should be used to get secrets</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.SenhaseguraAuth">
+SenhaseguraAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth defines parameters to authenticate in senhasegura</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>ignoreSslCertificate</code></br>
+<em>
+bool
+</em>
+</td>
+<td>
+<p>IgnoreSslCertificate defines if SSL certificate must be ignored</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.ServiceAccountAuth">ServiceAccountAuth
 </h3>
 <p>

+ 2 - 0
hack/api-docs/mkdocs.yml

@@ -68,6 +68,8 @@ nav:
     - Webhook: provider-webhook.md
     - Fake: provider-fake.md
     - Kubernetes: provider-kubernetes.md
+    - senhasegura:
+      - DevOps Secrets Management (DSM): provider-senhasegura-dsm.md
   - Examples:
     - FluxCD: examples-gitops-using-fluxcd.md
     - Anchore Engine: examples-anchore-engine-credentials.md

+ 1 - 0
pkg/provider/register/register.go

@@ -27,6 +27,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/kubernetes"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/oracle"
+	_ "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"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox"

+ 146 - 0
pkg/provider/senhasegura/auth/iso.go

@@ -0,0 +1,146 @@
+/*
+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 auth
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+type ISOInterface interface {
+	IsoSessionFromSecretRef(ctx context.Context, provider *esv1beta1.SenhaseguraProvider, store esv1beta1.GenericStore, kube client.Client, namespace string) (*SenhaseguraIsoSession, error)
+	GetIsoToken(clientID, clientSecret, systemURL string, ignoreSslCertificate bool) (token string, err error)
+}
+
+/*
+	SenhaseguraIsoSession contains information about senhasegura ISO API for any request
+*/
+type SenhaseguraIsoSession struct {
+	URL                  string
+	Token                string
+	IgnoreSslCertificate bool
+	isoClient            ISOInterface
+}
+
+/*
+	isoGetTokenResponse contains response from OAuth2 authentication endpoint in senhasegura API
+*/
+type isoGetTokenResponse struct {
+	TokenType   string `json:"token_type"`
+	ExpiresIn   int    `json:"expires_in"`
+	AccessToken string `json:"access_token"`
+}
+
+var (
+	errCannotCreateRequest = errors.New("cannot create request to senhasegura resource /iso/oauth2/token")
+	errCannotDoRequest     = errors.New("cannot do request in senhasegura, SSL certificate is valid ?")
+	errInvalidResponseBody = errors.New("invalid HTTP response body received from senhasegura")
+	errInvalidHTTPCode     = errors.New("received invalid HTTP code from senhasegura")
+)
+
+/*
+	Authenticate check required authentication method based on provider spec and initialize ISO OAuth2 session
+*/
+func Authenticate(ctx context.Context, store esv1beta1.GenericStore, provider *esv1beta1.SenhaseguraProvider, kube client.Client, namespace string) (isoSession *SenhaseguraIsoSession, err error) {
+	isoSession, err = isoSession.IsoSessionFromSecretRef(ctx, provider, store, kube, namespace)
+	if err != nil {
+		return nil, err
+	}
+	return isoSession, nil
+}
+
+/*
+	IsoSessionFromSecretRef initialize an ISO OAuth2 flow with .spec.provider.senhasegura.auth.isoSecretRef parameters
+*/
+func (s *SenhaseguraIsoSession) IsoSessionFromSecretRef(ctx context.Context, provider *esv1beta1.SenhaseguraProvider, store esv1beta1.GenericStore, kube client.Client, namespace string) (*SenhaseguraIsoSession, error) {
+	clientSecret, err := getKubernetesSecret(ctx, provider.Auth.ClientSecret, store, kube, namespace)
+	if err != nil {
+		return &SenhaseguraIsoSession{}, err
+	}
+
+	isoToken, err := s.GetIsoToken(provider.Auth.ClientID, clientSecret, provider.URL, provider.IgnoreSslCertificate)
+	if err != nil {
+		return &SenhaseguraIsoSession{}, err
+	}
+
+	return &SenhaseguraIsoSession{
+		URL:                  provider.URL,
+		Token:                isoToken,
+		IgnoreSslCertificate: provider.IgnoreSslCertificate,
+		isoClient:            &SenhaseguraIsoSession{},
+	}, nil
+}
+
+/*
+	GetIsoToken calls senhasegura OAuth2 endpoint to get a token
+*/
+func (s *SenhaseguraIsoSession) GetIsoToken(clientID, clientSecret, systemURL string, ignoreSslCertificate bool) (token string, err error) {
+	data := url.Values{}
+	data.Set("grant_type", "client_credentials")
+	data.Set("client_id", clientID)
+	data.Set("client_secret", clientSecret)
+
+	u, _ := url.ParseRequestURI(systemURL)
+	u.Path = "/iso/oauth2/token"
+
+	tr := &http.Transport{
+		// nolint
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreSslCertificate},
+	}
+
+	client := &http.Client{Transport: tr}
+
+	r, err := http.NewRequest("POST", u.String(), strings.NewReader(data.Encode()))
+	if err != nil {
+		return "", errCannotCreateRequest
+	}
+
+	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	r.Header.Set("Content-Length", strconv.Itoa(len(data.Encode())))
+
+	resp, err := client.Do(r)
+	if err != nil {
+		return "", errCannotDoRequest
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		return "", errInvalidHTTPCode
+	}
+
+	respData, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return "", errInvalidResponseBody
+	}
+
+	var respObj isoGetTokenResponse
+	err = json.Unmarshal(respData, &respObj)
+	if err != nil {
+		return "", errInvalidResponseBody
+	}
+
+	return respObj.AccessToken, nil
+}

+ 57 - 0
pkg/provider/senhasegura/auth/kubernetes_secret.go

@@ -0,0 +1,57 @@
+/*
+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 auth
+
+import (
+	"context"
+	"fmt"
+
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+const (
+	errRequiredNamespaceNotFound   = "invalid ClusterSecretStore: missing namespace in %s"
+	errCannotFetchKubernetesSecret = "could not fetch Kubernetes secret %s"
+)
+
+/*
+	getKubernetesSecret get Kubernetes Secret based on object parameter in namespace where ESO is installed or another, if ClusterSecretStore is used
+*/
+func getKubernetesSecret(ctx context.Context, object esmeta.SecretKeySelector, store esv1beta1.GenericStore, kube client.Client, namespace string) (string, error) {
+	ke := client.ObjectKey{
+		Name:      object.Name,
+		Namespace: namespace, // Default to ExternalSecret namespace
+	}
+
+	// Only ClusterStore is allowed to set namespace (and then it's required)
+	if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
+		if object.Namespace == nil {
+			return "", fmt.Errorf(errRequiredNamespaceNotFound, object.Key)
+		}
+		ke.Namespace = *object.Namespace
+	}
+
+	secret := v1.Secret{}
+	err := kube.Get(ctx, ke, &secret)
+	if err != nil {
+		return "", fmt.Errorf(errCannotFetchKubernetesSecret, object.Name)
+	}
+
+	return string(secret.Data[object.Key]), nil
+}

+ 227 - 0
pkg/provider/senhasegura/dsm/dsm.go

@@ -0,0 +1,227 @@
+/*
+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 dsm
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	senhaseguraAuth "github.com/external-secrets/external-secrets/pkg/provider/senhasegura/auth"
+)
+
+type clientDSMInterface interface {
+	FetchSecrets() (respObj IsoDappResponse, err error)
+}
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1beta1.SecretsClient = &DSM{}
+
+/*
+	DSM service for SenhaseguraProvider
+*/
+type DSM struct {
+	isoSession *senhaseguraAuth.SenhaseguraIsoSession
+	dsmClient  clientDSMInterface
+}
+
+/*
+	IsoDappResponse is a response object from senhasegura /iso/dapp/response (DevOps Secrets Management API endpoint)
+	Contains information about API request and Secrets linked with authorization
+*/
+type IsoDappResponse struct {
+	Response struct {
+		Status    int    `json:"status"`
+		Message   string `json:"message"`
+		Error     bool   `json:"error"`
+		ErrorCode int    `json:"error_code"`
+	} `json:"response"`
+	Application struct {
+		Name        string   `json:"name"`
+		Description string   `json:"description"`
+		Tags        []string `json:"tags"`
+		System      string   `json:"system"`
+		Environment string   `json:"Environment"`
+		Secrets     []struct {
+			SecretID       string              `json:"secret_id"`
+			SecretName     string              `json:"secret_name"`
+			Identity       string              `json:"identity"`
+			Version        string              `json:"version"`
+			ExpirationDate string              `json:"expiration_date"`
+			Engine         string              `json:"engine"`
+			Data           []map[string]string `json:"data"`
+		} `json:"secrets"`
+	} `json:"application"`
+}
+
+var (
+	errCannotCreateRequest = errors.New("cannot create request to senhasegura resource /iso/dapp/application")
+	errCannotDoRequest     = errors.New("cannot do request in senhasegura, SSL certificate is valid ?")
+	errInvalidResponseBody = errors.New("invalid HTTP response body received from senhasegura")
+	errInvalidHTTPCode     = errors.New("received invalid HTTP code from senhasegura")
+	errApplicationError    = errors.New("received application error from senhasegura")
+)
+
+/*
+	New creates an senhasegura DSM client based on ISO session
+*/
+func New(isoSession *senhaseguraAuth.SenhaseguraIsoSession) (*DSM, error) {
+	return &DSM{
+		isoSession: isoSession,
+		dsmClient:  &DSM{},
+	}, nil
+}
+
+/*
+	GetSecret implements ESO interface and get a single secret from senhasegura provider with DSM service
+*/
+func (dsm *DSM) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (resp []byte, err error) {
+	appSecrets, err := dsm.FetchSecrets()
+	if err != nil {
+		return []byte(""), err
+	}
+
+	for _, v := range appSecrets.Application.Secrets {
+		if ref.Key == v.Identity {
+			// Return whole data content in json-encoded when ref.Property is empty
+			if ref.Property == "" {
+				jsonStr, err := json.Marshal(v.Data)
+				if err != nil {
+					return nil, err
+				}
+				return jsonStr, nil
+			}
+
+			// Return raw data content when ref.Property is provided
+			for _, v2 := range v.Data {
+				for k, v3 := range v2 {
+					if k == ref.Property {
+						resp = []byte(v3)
+						return resp, nil
+					}
+				}
+			}
+		}
+	}
+
+	return []byte(""), esv1beta1.NoSecretErr
+}
+
+/*
+	GetSecretMap implements ESO interface and returns miltiple k/v pairs from senhasegura provider with DSM service
+*/
+func (dsm *DSM) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (secretData map[string][]byte, err error) {
+	secretData = make(map[string][]byte)
+	appSecrets, err := dsm.FetchSecrets()
+	if err != nil {
+		return secretData, err
+	}
+
+	for _, v := range appSecrets.Application.Secrets {
+		if v.Identity == ref.Key {
+			for _, v2 := range v.Data {
+				for k, v3 := range v2 {
+					secretData[k] = []byte(v3)
+				}
+			}
+		}
+	}
+	return secretData, nil
+}
+
+/*
+	GetAllSecrets implements ESO interface and returns multiple secrets from senhasegura provider with DSM service
+
+	TODO: GetAllSecrets functionality is to get secrets from either regexp-matching against the names or via metadata label matching.
+	https://github.com/external-secrets/external-secrets/pull/830#discussion_r858657107
+*/
+func (dsm *DSM) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (secretData map[string][]byte, err error) {
+	return nil, fmt.Errorf("GetAllSecrets not implemented yet")
+}
+
+/*
+	fetchSecrets calls senhasegura DSM /iso/dapp/application API endpoint
+	Return an IsoDappResponse with all related information from senhasegura provider with DSM service and error
+*/
+func (dsm *DSM) FetchSecrets() (respObj IsoDappResponse, err error) {
+	u, _ := url.ParseRequestURI(dsm.isoSession.URL)
+	u.Path = "/iso/dapp/application"
+
+	tr := &http.Transport{
+		// nolint
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: dsm.isoSession.IgnoreSslCertificate},
+	}
+
+	client := &http.Client{Transport: tr}
+
+	r, err := http.NewRequest("GET", u.String(), nil)
+	if err != nil {
+		return respObj, errCannotCreateRequest
+	}
+
+	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	r.Header.Set("Authorization", "Bearer "+dsm.isoSession.Token)
+
+	resp, err := client.Do(r)
+	if err != nil {
+		return respObj, errCannotDoRequest
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		return respObj, errInvalidHTTPCode
+	}
+
+	respData, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return respObj, errInvalidResponseBody
+	}
+
+	err = json.Unmarshal(respData, &respObj)
+	if err != nil {
+		return respObj, errInvalidResponseBody
+	}
+
+	if respObj.Response.Error {
+		return respObj, errApplicationError
+	}
+
+	return respObj, nil
+}
+
+/*
+	Close implements ESO interface and do nothing in senhasegura
+*/
+func (dsm *DSM) Close(ctx context.Context) error {
+	return nil
+}
+
+// Validate if has valid connection with senhasegura, credentials, authorization using fetchSecrets method
+// fetchSecrets method implement required check about request
+// https://github.com/external-secrets/external-secrets/pull/830#discussion_r833275463
+func (dsm *DSM) Validate() (esv1beta1.ValidationResult, error) {
+	_, err := dsm.FetchSecrets()
+	if err != nil {
+		return esv1beta1.ValidationResultError, err
+	}
+
+	return esv1beta1.ValidationResultReady, nil
+}

+ 120 - 0
pkg/provider/senhasegura/provider.go

@@ -0,0 +1,120 @@
+/*
+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 senhasegura
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	senhaseguraAuth "github.com/external-secrets/external-secrets/pkg/provider/senhasegura/auth"
+	"github.com/external-secrets/external-secrets/pkg/provider/senhasegura/dsm"
+)
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1beta1.Provider = &Provider{}
+
+// Provider struct that satisfier ESO interface.
+type Provider struct{}
+
+const (
+	errUnknownProviderService     = "unknown senhasegura Provider Service: %s"
+	errNilStore                   = "nil store found"
+	errMissingStoreSpec           = "store is missing spec"
+	errMissingProvider            = "storeSpec is missing provider"
+	errInvalidProvider            = "invalid provider spec. Missing senhasegura field in store %s"
+	errInvalidSenhaseguraURL      = "invalid senhasegura URL"
+	errInvalidSenhaseguraURLHTTPS = "invalid senhasegura URL, must be HTTPS for security reasons"
+	errMissingClientID            = "missing senhasegura authentication Client ID"
+)
+
+/*
+	Construct a new secrets client based on provided store
+*/
+func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
+	spec := store.GetSpec()
+	provider := spec.Provider.Senhasegura
+
+	isoSession, err := senhaseguraAuth.Authenticate(ctx, store, provider, kube, namespace)
+	if err != nil {
+		return nil, err
+	}
+
+	if provider.Module == esv1beta1.SenhaseguraModuleDSM {
+		return dsm.New(isoSession)
+	}
+
+	return nil, fmt.Errorf(errUnknownProviderService, provider.Module)
+}
+
+// Validate store using Validating webhook during secret store creating
+// Checks here are usually the best experience for the user, as the SecretStore will not be created until it is a 'valid' one.
+// https://github.com/external-secrets/external-secrets/pull/830#discussion_r833278518
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
+	return validateStore(store)
+}
+
+func validateStore(store esv1beta1.GenericStore) error {
+	if store == nil {
+		return fmt.Errorf(errNilStore)
+	}
+
+	spec := store.GetSpec()
+	if spec == nil {
+		return fmt.Errorf(errMissingStoreSpec)
+	}
+
+	if spec.Provider == nil {
+		return fmt.Errorf(errMissingProvider)
+	}
+
+	provider := spec.Provider.Senhasegura
+	if provider == nil {
+		return fmt.Errorf(errInvalidProvider, store.GetObjectMeta().String())
+	}
+
+	url, err := url.Parse(provider.URL)
+	if err != nil {
+		return fmt.Errorf(errInvalidSenhaseguraURL)
+	}
+
+	// senhasegura doesn't accept requests without SSL/TLS layer for security reasons
+	// DSM doesn't provides gRPC schema, only HTTPS
+	if url.Scheme != "https" {
+		return fmt.Errorf(errInvalidSenhaseguraURLHTTPS)
+	}
+
+	if url.Host == "" {
+		return fmt.Errorf(errInvalidSenhaseguraURL)
+	}
+
+	if provider.Auth.ClientID == "" {
+		return fmt.Errorf(errMissingClientID)
+	}
+
+	return nil
+}
+
+/*
+	Register SenhaseguraProvider in ESO init
+*/
+func init() {
+	esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+		Senhasegura: &esv1beta1.SenhaseguraProvider{},
+	})
+}

+ 146 - 0
pkg/provider/senhasegura/provider_test.go

@@ -0,0 +1,146 @@
+/*
+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 senhasegura
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+func TestValidateStore(t *testing.T) {
+	tbl := []struct {
+		test   string
+		store  esv1beta1.GenericStore
+		expErr bool
+	}{
+		{
+			test:   "should not create provider due to nil store",
+			store:  nil,
+			expErr: true,
+		},
+		{
+			test:   "should not create provider due to missing provider",
+			expErr: true,
+			store: &esv1beta1.SecretStore{
+				Spec: esv1beta1.SecretStoreSpec{},
+			},
+		},
+		{
+			test:   "should not create provider due to missing provider field",
+			expErr: true,
+			store: &esv1beta1.SecretStore{
+				Spec: esv1beta1.SecretStoreSpec{
+					Provider: &esv1beta1.SecretStoreProvider{},
+				},
+			},
+		},
+		{
+			test:   "should not create provider due to missing provider module",
+			expErr: true,
+			store: &esv1beta1.SecretStore{
+				Spec: esv1beta1.SecretStoreSpec{
+					Provider: &esv1beta1.SecretStoreProvider{
+						Senhasegura: &esv1beta1.SenhaseguraProvider{},
+					},
+				},
+			},
+		},
+		{
+			test:   "should not create provider due to missing provider auth client ID",
+			expErr: true,
+			store: &esv1beta1.SecretStore{
+				Spec: esv1beta1.SecretStoreSpec{
+					Provider: &esv1beta1.SecretStoreProvider{
+						Senhasegura: &esv1beta1.SenhaseguraProvider{
+							Module: esv1beta1.SenhaseguraModuleDSM,
+						},
+					},
+				},
+			},
+		},
+		{
+			test:   "invalid module should return an error",
+			expErr: true,
+			store: &esv1beta1.SecretStore{
+				Spec: esv1beta1.SecretStoreSpec{
+					Provider: &esv1beta1.SecretStoreProvider{
+						Senhasegura: &esv1beta1.SenhaseguraProvider{
+							Module: "HIHIHIHHEHEHEHEHEHE",
+						},
+					},
+				},
+			},
+		},
+		{
+			test:   "should not create provider due senhasegura URL without https scheme",
+			expErr: true,
+			store: &esv1beta1.SecretStore{
+				Spec: esv1beta1.SecretStoreSpec{
+					Provider: &esv1beta1.SecretStoreProvider{
+						Senhasegura: &esv1beta1.SenhaseguraProvider{
+							Module: esv1beta1.SenhaseguraModuleDSM,
+							URL:    "http://dev.null",
+						},
+					},
+				},
+			},
+		},
+		{
+			test:   "should not create provider due senhasegura URL without valid name",
+			expErr: true,
+			store: &esv1beta1.SecretStore{
+				Spec: esv1beta1.SecretStoreSpec{
+					Provider: &esv1beta1.SecretStoreProvider{
+						Senhasegura: &esv1beta1.SenhaseguraProvider{
+							Module: esv1beta1.SenhaseguraModuleDSM,
+							URL:    "https://",
+						},
+					},
+				},
+			},
+		},
+		{
+			test:   "should create provider",
+			expErr: false,
+			store: &esv1beta1.SecretStore{
+				Spec: esv1beta1.SecretStoreSpec{
+					Provider: &esv1beta1.SecretStoreProvider{
+						Senhasegura: &esv1beta1.SenhaseguraProvider{
+							Module: esv1beta1.SenhaseguraModuleDSM,
+							URL:    "https://senhasegura.local",
+							Auth: esv1beta1.SenhaseguraAuth{
+								ClientID: "example",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	for i := range tbl {
+		row := tbl[i]
+		t.Run(row.test, func(t *testing.T) {
+			err := validateStore(row.store)
+			if row.expErr {
+				assert.Error(t, err)
+			} else {
+				assert.Nil(t, err)
+			}
+		})
+	}
+}