1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-15 17:51:01 +00:00

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

Add senhasegura DevOps Secrets Management (DSM) provider
This commit is contained in:
paul-the-alien[bot] 2022-05-03 12:57:59 +00:00 committed by GitHub
commit 9838d44bae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1362 additions and 1 deletions

View file

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

View file

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

View file

@ -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.
@ -1466,6 +1471,38 @@ func (in *SecretStoreStatusCondition) DeepCopy() *SecretStoreStatusCondition {
return out
}
// 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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