mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
ADD sdkms base implementation (#3180)
* ADD sdkms base implementation Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com> * FIX get secret object by name, unmarshalling error formatting Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com> * ADD suport for fortanix secret security objects Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com> * ADD more tests for opaque, secret, new client Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com> * FIX changes required by make reviewable Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com> * ADD missing provider registration Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com> * FIX remove unused error string, add generated assets Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com> --------- Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com>
This commit is contained in:
parent
983488ca57
commit
af38fc68d5
15 changed files with 931 additions and 0 deletions
29
apis/externalsecrets/v1beta1/secretstore_fortanix_types.go
Normal file
29
apis/externalsecrets/v1beta1/secretstore_fortanix_types.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
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 FortanixProvider struct {
|
||||
// APIURL is the URL of SDKMS API. Defaults to `sdkms.fortanix.com`.
|
||||
APIURL string `json:"apiUrl,omitempty"`
|
||||
|
||||
// APIKey is the API token to access SDKMS Applications.
|
||||
APIKey *FortanixProviderSecretRef `json:"apiKey,omitempty"`
|
||||
}
|
||||
|
||||
type FortanixProviderSecretRef struct {
|
||||
// SecretRef is a reference to a secret containing the SDKMS API Key.
|
||||
SecretRef *esmeta.SecretKeySelector `json:"secretRef,omitempty"`
|
||||
}
|
|
@ -149,6 +149,10 @@ type SecretStoreProvider struct {
|
|||
// Pulumi configures this store to sync secrets using the Pulumi provider
|
||||
// +optional
|
||||
Pulumi *PulumiProvider `json:"pulumi,omitempty"`
|
||||
|
||||
// Fortanix configures this store to sync secrets using the Fortanix provider
|
||||
// +optional
|
||||
Fortanix *FortanixProvider `json:"fortanix,omitempty"`
|
||||
}
|
||||
|
||||
type CAProviderType string
|
||||
|
|
|
@ -1375,6 +1375,46 @@ func (in *FindName) DeepCopy() *FindName {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FortanixProvider) DeepCopyInto(out *FortanixProvider) {
|
||||
*out = *in
|
||||
if in.APIKey != nil {
|
||||
in, out := &in.APIKey, &out.APIKey
|
||||
*out = new(FortanixProviderSecretRef)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FortanixProvider.
|
||||
func (in *FortanixProvider) DeepCopy() *FortanixProvider {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FortanixProvider)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FortanixProviderSecretRef) DeepCopyInto(out *FortanixProviderSecretRef) {
|
||||
*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 FortanixProviderSecretRef.
|
||||
func (in *FortanixProviderSecretRef) DeepCopy() *FortanixProviderSecretRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FortanixProviderSecretRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GCPSMAuth) DeepCopyInto(out *GCPSMAuth) {
|
||||
*out = *in
|
||||
|
@ -2094,6 +2134,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
|
|||
*out = new(PulumiProvider)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Fortanix != nil {
|
||||
in, out := &in.Fortanix, &out.Fortanix
|
||||
*out = new(FortanixProvider)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
|
||||
|
|
|
@ -2574,6 +2574,37 @@ spec:
|
|||
required:
|
||||
- data
|
||||
type: object
|
||||
fortanix:
|
||||
description: Fortanix configures this store to sync secrets using
|
||||
the Fortanix provider
|
||||
properties:
|
||||
apiKey:
|
||||
description: APIKey is the API token to access SDKMS Applications.
|
||||
properties:
|
||||
secretRef:
|
||||
description: SecretRef is a reference to a secret containing
|
||||
the SDKMS API Key.
|
||||
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
|
||||
type: object
|
||||
apiUrl:
|
||||
description: APIURL is the URL of SDKMS API. Defaults to `sdkms.fortanix.com`.
|
||||
type: string
|
||||
type: object
|
||||
gcpsm:
|
||||
description: GCPSM configures this store to sync secrets using
|
||||
Google Cloud Platform Secret Manager provider
|
||||
|
|
|
@ -2574,6 +2574,37 @@ spec:
|
|||
required:
|
||||
- data
|
||||
type: object
|
||||
fortanix:
|
||||
description: Fortanix configures this store to sync secrets using
|
||||
the Fortanix provider
|
||||
properties:
|
||||
apiKey:
|
||||
description: APIKey is the API token to access SDKMS Applications.
|
||||
properties:
|
||||
secretRef:
|
||||
description: SecretRef is a reference to a secret containing
|
||||
the SDKMS API Key.
|
||||
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
|
||||
type: object
|
||||
apiUrl:
|
||||
description: APIURL is the URL of SDKMS API. Defaults to `sdkms.fortanix.com`.
|
||||
type: string
|
||||
type: object
|
||||
gcpsm:
|
||||
description: GCPSM configures this store to sync secrets using
|
||||
Google Cloud Platform Secret Manager provider
|
||||
|
|
|
@ -3007,6 +3007,34 @@ spec:
|
|||
required:
|
||||
- data
|
||||
type: object
|
||||
fortanix:
|
||||
description: Fortanix configures this store to sync secrets using the Fortanix provider
|
||||
properties:
|
||||
apiKey:
|
||||
description: APIKey is the API token to access SDKMS Applications.
|
||||
properties:
|
||||
secretRef:
|
||||
description: SecretRef is a reference to a secret containing the SDKMS API Key.
|
||||
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
|
||||
type: object
|
||||
apiUrl:
|
||||
description: APIURL is the URL of SDKMS API. Defaults to `sdkms.fortanix.com`.
|
||||
type: string
|
||||
type: object
|
||||
gcpsm:
|
||||
description: GCPSM configures this store to sync secrets using Google Cloud Platform Secret Manager provider
|
||||
properties:
|
||||
|
@ -8069,6 +8097,34 @@ spec:
|
|||
required:
|
||||
- data
|
||||
type: object
|
||||
fortanix:
|
||||
description: Fortanix configures this store to sync secrets using the Fortanix provider
|
||||
properties:
|
||||
apiKey:
|
||||
description: APIKey is the API token to access SDKMS Applications.
|
||||
properties:
|
||||
secretRef:
|
||||
description: SecretRef is a reference to a secret containing the SDKMS API Key.
|
||||
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
|
||||
type: object
|
||||
apiUrl:
|
||||
description: APIURL is the URL of SDKMS API. Defaults to `sdkms.fortanix.com`.
|
||||
type: string
|
||||
type: object
|
||||
gcpsm:
|
||||
description: GCPSM configures this store to sync secrets using Google Cloud Platform Secret Manager provider
|
||||
properties:
|
||||
|
|
|
@ -3658,6 +3658,79 @@ string
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="external-secrets.io/v1beta1.FortanixProvider">FortanixProvider
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
|
||||
</p>
|
||||
<p>
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>apiUrl</code></br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>APIURL is the URL of SDKMS API. Defaults to <code>sdkms.fortanix.com</code>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>apiKey</code></br>
|
||||
<em>
|
||||
<a href="#external-secrets.io/v1beta1.FortanixProviderSecretRef">
|
||||
FortanixProviderSecretRef
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>APIKey is the API token to access SDKMS Applications.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="external-secrets.io/v1beta1.FortanixProviderSecretRef">FortanixProviderSecretRef
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#external-secrets.io/v1beta1.FortanixProvider">FortanixProvider</a>)
|
||||
</p>
|
||||
<p>
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>
|
||||
<p>SecretRef is a reference to a secret containing the SDKMS API Key.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="external-secrets.io/v1beta1.GCPSMAuth">GCPSMAuth
|
||||
</h3>
|
||||
<p>
|
||||
|
@ -5533,6 +5606,20 @@ PulumiProvider
|
|||
<p>Pulumi configures this store to sync secrets using the Pulumi provider</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>fortanix</code></br>
|
||||
<em>
|
||||
<a href="#external-secrets.io/v1beta1.FortanixProvider">
|
||||
FortanixProvider
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Fortanix configures this store to sync secrets using the Fortanix provider</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef
|
||||
|
|
50
docs/provider/fortanix.md
Normal file
50
docs/provider/fortanix.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
## Fortanix DSM / SDKMS
|
||||
|
||||
Populate kubernetes secrets from OPAQUE or SECRET security objects in Fortanix.
|
||||
|
||||
### Authentication
|
||||
|
||||
SDKMS [Application API Key](https://support.fortanix.com/hc/en-us/articles/360015941132-Authentication)
|
||||
|
||||
### Creating a SecretStore
|
||||
|
||||
```yaml
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: SecretStore
|
||||
metadata:
|
||||
name: secret-store
|
||||
spec:
|
||||
provider:
|
||||
fortanix:
|
||||
apiUrl: <HOST_OF_SDKMS_API>
|
||||
apiKey:
|
||||
secretRef:
|
||||
name: <NAME_OF_KUBE_SECRET>
|
||||
key: <KEY_IN_KUBE_SECRET>
|
||||
```
|
||||
|
||||
### Referencing Secrets
|
||||
|
||||
```yaml
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: secret
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
kind: SecretStore
|
||||
name: secret-store
|
||||
data:
|
||||
|
||||
# Raw stored value
|
||||
- secretKey: <KEY_IN_KUBE_SECRET>
|
||||
remoteRef:
|
||||
key: <SDKMS_SECURITY_OBJECT_NAME>
|
||||
|
||||
# From stored key-value JSON
|
||||
- secretKey: <KEY_IN_KUBE_SECRET>
|
||||
remoteRef:
|
||||
key: <SDKMS_SECURITY_OBJECT_NAME>
|
||||
property: <SECURITY_OBJECT_VALUE_INNER_PROPERTY>
|
||||
```
|
1
go.mod
1
go.mod
|
@ -72,6 +72,7 @@ require (
|
|||
github.com/aliyun/credentials-go v1.3.2
|
||||
github.com/avast/retry-go/v4 v4.5.1
|
||||
github.com/cyberark/conjur-api-go v0.11.1
|
||||
github.com/fortanix/sdkms-client-go v0.4.0
|
||||
github.com/go-openapi/strfmt v0.22.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/hashicorp/golang-lru v1.0.2
|
||||
|
|
2
go.sum
2
go.sum
|
@ -288,6 +288,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
|||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fortanix/sdkms-client-go v0.4.0 h1:5cKiFJ4rzc69mhsVVI5Ma5ynr/k5vhvws0yfzfIro/k=
|
||||
github.com/fortanix/sdkms-client-go v0.4.0/go.mod h1:gjylIGX+6poVSe+JkbNsLTvseLd+rLjvcGFgXpW56Lo=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
|
|
99
pkg/provider/fortanix/fortanix.go
Normal file
99
pkg/provider/fortanix/fortanix.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
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 fortanix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/fortanix/sdkms-client-go/sdkms"
|
||||
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 {
|
||||
sdkms sdkms.Client
|
||||
}
|
||||
|
||||
const (
|
||||
errPushSecretsNotSupported = "pushing secrets is currently not supported"
|
||||
errDeleteSecretsNotSupported = "deleting secrets is currently not supported"
|
||||
errUnmarshalSecret = "unable to unmarshal secret, is it a valid JSON?: %w"
|
||||
errUnableToGetValue = "unable to get value for key %s"
|
||||
errGettingSecretMapNotSupported = "getting secret map is currently not supported"
|
||||
errGettingAllSecretsNotSupported = "getting all secrets is currently not supported"
|
||||
)
|
||||
|
||||
func (c *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
|
||||
securityObject, err := c.sdkms.GetSobject(ctx, &sdkms.GetSobjectParams{}, *sdkms.SobjectByName(ref.Key))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if securityObject.ObjType == sdkms.ObjectTypeSecret {
|
||||
securityObject, err = c.sdkms.ExportSobject(ctx, *sdkms.SobjectByID(*securityObject.Kid))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if ref.Property == "" {
|
||||
return *securityObject.Value, nil
|
||||
}
|
||||
|
||||
kv := make(map[string]string)
|
||||
|
||||
err = json.Unmarshal(*securityObject.Value, &kv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errUnmarshalSecret, err)
|
||||
}
|
||||
|
||||
value, ok := kv[ref.Property]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(errUnableToGetValue, ref.Property)
|
||||
}
|
||||
|
||||
return utils.GetByteValue(value)
|
||||
}
|
||||
|
||||
func (c *client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
|
||||
return errors.New(errPushSecretsNotSupported)
|
||||
}
|
||||
|
||||
func (c *client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
|
||||
return errors.New(errDeleteSecretsNotSupported)
|
||||
}
|
||||
|
||||
func (c *client) Validate() (esv1beta1.ValidationResult, error) {
|
||||
return esv1beta1.ValidationResultReady, nil
|
||||
}
|
||||
|
||||
func (c *client) GetSecretMap(_ context.Context, _ esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
|
||||
return nil, errors.New(errGettingSecretMapNotSupported)
|
||||
}
|
||||
|
||||
func (c *client) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
|
||||
return nil, errors.New(errGettingAllSecretsNotSupported)
|
||||
}
|
||||
|
||||
func (c *client) Close(context.Context) error {
|
||||
return nil
|
||||
}
|
152
pkg/provider/fortanix/fortanix_test.go
Normal file
152
pkg/provider/fortanix/fortanix_test.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
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 fortanix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/fortanix/sdkms-client-go/sdkms"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||
)
|
||||
|
||||
func newTestClient(t *testing.T, handler func(w http.ResponseWriter, r *http.Request)) *client {
|
||||
const apiKey = "api-key"
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handler(w, r)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
return &client{
|
||||
sdkms: sdkms.Client{
|
||||
HTTPClient: http.DefaultClient,
|
||||
Auth: sdkms.APIKey(apiKey),
|
||||
Endpoint: server.URL,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func toJSON(t *testing.T, v interface{}) []byte {
|
||||
jsonBytes, err := json.Marshal(v)
|
||||
assert.Nil(t, err)
|
||||
return jsonBytes
|
||||
}
|
||||
|
||||
type testSecurityObjectValue struct {
|
||||
Property string `json:"property"`
|
||||
}
|
||||
|
||||
func TestGetOpaqueSecurityObject(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
securityObjectName := "securityObjectName"
|
||||
|
||||
securityObjectValue := toJSON(t, testSecurityObjectValue{
|
||||
Property: "value",
|
||||
})
|
||||
|
||||
securityObjectUser := "user"
|
||||
|
||||
securityObject := sdkms.Sobject{
|
||||
Creator: sdkms.Principal{
|
||||
User: &securityObjectUser,
|
||||
},
|
||||
Name: &securityObjectName,
|
||||
Value: &securityObjectValue,
|
||||
}
|
||||
|
||||
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
err := json.NewEncoder(w).Encode(securityObject)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("get raw secret value from opaque security object", func(t *testing.T) {
|
||||
ref := esv1beta1.ExternalSecretDataRemoteRef{
|
||||
Key: securityObjectName,
|
||||
}
|
||||
|
||||
got, err := client.GetSecret(ctx, ref)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, securityObjectValue, got)
|
||||
})
|
||||
|
||||
t.Run("get inner property value from opaque security object", func(t *testing.T) {
|
||||
ref := esv1beta1.ExternalSecretDataRemoteRef{
|
||||
Key: securityObjectName,
|
||||
Property: "property",
|
||||
}
|
||||
|
||||
got, err := client.GetSecret(ctx, ref)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte(`value`), got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSecretSecurityObject(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
securityObjectName := "securityObjectName"
|
||||
securityObjectID := "id"
|
||||
|
||||
securityObjectValue := toJSON(t, testSecurityObjectValue{
|
||||
Property: "value",
|
||||
})
|
||||
|
||||
securityObjectUser := "user"
|
||||
|
||||
securityObject := sdkms.Sobject{
|
||||
Creator: sdkms.Principal{
|
||||
User: &securityObjectUser,
|
||||
},
|
||||
Name: &securityObjectName,
|
||||
Kid: &securityObjectID,
|
||||
Value: &securityObjectValue,
|
||||
ObjType: sdkms.ObjectTypeSecret,
|
||||
}
|
||||
|
||||
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
err := json.NewEncoder(w).Encode(securityObject)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("get raw secret value from secret security object", func(t *testing.T) {
|
||||
ref := esv1beta1.ExternalSecretDataRemoteRef{
|
||||
Key: securityObjectName,
|
||||
}
|
||||
|
||||
got, err := client.GetSecret(ctx, ref)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, securityObjectValue, got)
|
||||
})
|
||||
|
||||
t.Run("get inner property value from secret security object", func(t *testing.T) {
|
||||
ref := esv1beta1.ExternalSecretDataRemoteRef{
|
||||
Key: securityObjectName,
|
||||
Property: "property",
|
||||
}
|
||||
|
||||
got, err := client.GetSecret(ctx, ref)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte(`value`), got)
|
||||
})
|
||||
}
|
124
pkg/provider/fortanix/provider.go
Normal file
124
pkg/provider/fortanix/provider.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
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 fortanix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fortanix/sdkms-client-go/sdkms"
|
||||
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"
|
||||
)
|
||||
|
||||
type Provider struct{}
|
||||
|
||||
const (
|
||||
errCannotResolveSecretKeyRef = "cannot resolve secret key ref: %w"
|
||||
errStoreIsNil = "store is nil"
|
||||
errNoStoreTypeOrWrongStoreType = "no store type or wrong store type"
|
||||
errAPIKeyIsRequired = "apiKey is required"
|
||||
errAPIKeySecretRefIsRequired = "apiKey.secretRef is required"
|
||||
errAPIKeySecretRefNameIsRequired = "apiKey.secretRef.name is required"
|
||||
errAPIKeySecretRefKeyIsRequired = "apiKey.secretRef.key is required"
|
||||
)
|
||||
|
||||
var _ esv1beta1.Provider = &Provider{}
|
||||
|
||||
func init() {
|
||||
esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
|
||||
Fortanix: &esv1beta1.FortanixProvider{},
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
config, err := getConfig(store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiKey, err := resolvers.SecretKeyRef(ctx, kube, store.GetKind(), namespace, config.APIKey.SecretRef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errCannotResolveSecretKeyRef, err)
|
||||
}
|
||||
|
||||
sdkmsClient := sdkms.Client{
|
||||
HTTPClient: http.DefaultClient,
|
||||
Auth: sdkms.APIKey(apiKey),
|
||||
Endpoint: config.APIURL,
|
||||
}
|
||||
|
||||
return &client{
|
||||
sdkms: sdkmsClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
|
||||
_, err := getConfig(store)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func getConfig(store esv1beta1.GenericStore) (*esv1beta1.FortanixProvider, error) {
|
||||
if store == nil {
|
||||
return nil, errors.New(errStoreIsNil)
|
||||
}
|
||||
|
||||
spec := store.GetSpec()
|
||||
if spec == nil || spec.Provider == nil || spec.Provider.Fortanix == nil {
|
||||
return nil, errors.New(errNoStoreTypeOrWrongStoreType)
|
||||
}
|
||||
|
||||
config := spec.Provider.Fortanix
|
||||
|
||||
if config.APIURL == "" {
|
||||
config.APIURL = "https://sdkms.fortanix.com"
|
||||
}
|
||||
|
||||
err := validateSecretStoreRef(store, config.APIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func validateSecretStoreRef(store esv1beta1.GenericStore, ref *esv1beta1.FortanixProviderSecretRef) error {
|
||||
if ref == nil {
|
||||
return errors.New(errAPIKeyIsRequired)
|
||||
}
|
||||
|
||||
if ref.SecretRef == nil {
|
||||
return errors.New(errAPIKeySecretRefIsRequired)
|
||||
}
|
||||
|
||||
if ref.SecretRef.Name == "" {
|
||||
return errors.New(errAPIKeySecretRefNameIsRequired)
|
||||
}
|
||||
|
||||
if ref.SecretRef.Key == "" {
|
||||
return errors.New(errAPIKeySecretRefKeyIsRequired)
|
||||
}
|
||||
|
||||
return utils.ValidateReferentSecretSelector(store, *ref.SecretRef)
|
||||
}
|
219
pkg/provider/fortanix/provider_test.go
Normal file
219
pkg/provider/fortanix/provider_test.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
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 fortanix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
kubeclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||
v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||
)
|
||||
|
||||
func pointer[T any](d T) *T {
|
||||
return &d
|
||||
}
|
||||
|
||||
func respondJSON(w http.ResponseWriter, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func createMockKubernetesClient(t *testing.T) kubeclient.Client {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1":
|
||||
respondJSON(w, metav1.APIResourceList{
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Name: "secrets",
|
||||
Namespaced: true,
|
||||
Kind: "Secret",
|
||||
Verbs: metav1.Verbs{
|
||||
"get",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
case "/api/v1/namespaces/test/secrets/secret-name":
|
||||
respondJSON(w, corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "secret-name",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"apiKey": []byte("apiKey"),
|
||||
},
|
||||
})
|
||||
case "/api/v1/namespaces/test/secrets/missing-secret":
|
||||
w.WriteHeader(404)
|
||||
respondJSON(w, metav1.Status{
|
||||
Code: 404,
|
||||
})
|
||||
}
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
clientConfig := clientcmd.NewDefaultClientConfig(clientcmdapi.Config{
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"test": {
|
||||
Server: server.URL,
|
||||
},
|
||||
},
|
||||
AuthInfos: map[string]*clientcmdapi.AuthInfo{
|
||||
"test": {
|
||||
Token: "token",
|
||||
},
|
||||
},
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"test": {
|
||||
Cluster: "test",
|
||||
AuthInfo: "test",
|
||||
},
|
||||
},
|
||||
CurrentContext: "test",
|
||||
}, &clientcmd.ConfigOverrides{})
|
||||
|
||||
restConfig, err := clientConfig.ClientConfig()
|
||||
assert.Nil(t, err)
|
||||
c, err := kubeclient.New(restConfig, kubeclient.Options{})
|
||||
assert.Nil(t, err)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
t.Run("should create new client", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
p := &Provider{}
|
||||
c := createMockKubernetesClient(t)
|
||||
s := esv1beta1.SecretStore{
|
||||
Spec: esv1beta1.SecretStoreSpec{
|
||||
Provider: &esv1beta1.SecretStoreProvider{
|
||||
Fortanix: &esv1beta1.FortanixProvider{
|
||||
APIKey: &esv1beta1.FortanixProviderSecretRef{
|
||||
SecretRef: &v1.SecretKeySelector{
|
||||
Name: "secret-name",
|
||||
Key: "apiKey",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := p.NewClient(ctx, &s, c, "test")
|
||||
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("should fail to create new client if secret is missing", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
p := &Provider{}
|
||||
c := createMockKubernetesClient(t)
|
||||
s := esv1beta1.SecretStore{
|
||||
Spec: esv1beta1.SecretStoreSpec{
|
||||
Provider: &esv1beta1.SecretStoreProvider{
|
||||
Fortanix: &esv1beta1.FortanixProvider{
|
||||
APIKey: &esv1beta1.FortanixProviderSecretRef{
|
||||
SecretRef: &v1.SecretKeySelector{
|
||||
Name: "missing-secret",
|
||||
Key: "apiKey",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := p.NewClient(ctx, &s, c, "test")
|
||||
|
||||
assert.ErrorContains(t, err, "cannot resolve secret key ref")
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateStore(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
cfg esv1beta1.FortanixProvider
|
||||
want error
|
||||
}{
|
||||
"missing api key": {
|
||||
cfg: esv1beta1.FortanixProvider{},
|
||||
want: errors.New("apiKey is required"),
|
||||
},
|
||||
"missing api key secret ref": {
|
||||
cfg: esv1beta1.FortanixProvider{
|
||||
APIKey: &esv1beta1.FortanixProviderSecretRef{},
|
||||
},
|
||||
want: errors.New("apiKey.secretRef is required"),
|
||||
},
|
||||
"missing api key secret ref name": {
|
||||
cfg: esv1beta1.FortanixProvider{
|
||||
APIKey: &esv1beta1.FortanixProviderSecretRef{
|
||||
SecretRef: &v1.SecretKeySelector{
|
||||
Key: "key",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: errors.New("apiKey.secretRef.name is required"),
|
||||
},
|
||||
"missing api key secret ref key": {
|
||||
cfg: esv1beta1.FortanixProvider{
|
||||
APIKey: &esv1beta1.FortanixProviderSecretRef{
|
||||
SecretRef: &v1.SecretKeySelector{
|
||||
Name: "name",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: errors.New("apiKey.secretRef.key is required"),
|
||||
},
|
||||
"disallowed namespace in store ref": {
|
||||
cfg: esv1beta1.FortanixProvider{
|
||||
APIKey: &esv1beta1.FortanixProviderSecretRef{
|
||||
SecretRef: &v1.SecretKeySelector{
|
||||
Key: "key",
|
||||
Name: "name",
|
||||
Namespace: pointer("namespace"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: errors.New("namespace not allowed with namespaced SecretStore"),
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
s := esv1beta1.SecretStore{
|
||||
Spec: esv1beta1.SecretStoreSpec{
|
||||
Provider: &esv1beta1.SecretStoreProvider{
|
||||
Fortanix: &tc.cfg,
|
||||
},
|
||||
},
|
||||
}
|
||||
p := &Provider{}
|
||||
_, got := p.ValidateStore(&s)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ import (
|
|||
_ "github.com/external-secrets/external-secrets/pkg/provider/delinea"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/doppler"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/fake"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/fortanix"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
|
||||
|
|
Loading…
Reference in a new issue