1
0
Fork 0
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:
David Recuenco 2024-02-28 10:59:47 +01:00 committed by GitHub
parent 983488ca57
commit af38fc68d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 931 additions and 0 deletions

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

View file

@ -149,6 +149,10 @@ type SecretStoreProvider struct {
// Pulumi configures this store to sync secrets using the Pulumi provider // Pulumi configures this store to sync secrets using the Pulumi provider
// +optional // +optional
Pulumi *PulumiProvider `json:"pulumi,omitempty"` 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 type CAProviderType string

View file

@ -1375,6 +1375,46 @@ func (in *FindName) DeepCopy() *FindName {
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GCPSMAuth) DeepCopyInto(out *GCPSMAuth) { func (in *GCPSMAuth) DeepCopyInto(out *GCPSMAuth) {
*out = *in *out = *in
@ -2094,6 +2134,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
*out = new(PulumiProvider) *out = new(PulumiProvider)
(*in).DeepCopyInto(*out) (*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. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.

View file

@ -2574,6 +2574,37 @@ spec:
required: required:
- data - data
type: object 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: gcpsm:
description: GCPSM configures this store to sync secrets using description: GCPSM configures this store to sync secrets using
Google Cloud Platform Secret Manager provider Google Cloud Platform Secret Manager provider

View file

@ -2574,6 +2574,37 @@ spec:
required: required:
- data - data
type: object 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: gcpsm:
description: GCPSM configures this store to sync secrets using description: GCPSM configures this store to sync secrets using
Google Cloud Platform Secret Manager provider Google Cloud Platform Secret Manager provider

View file

@ -3007,6 +3007,34 @@ spec:
required: required:
- data - data
type: object 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: gcpsm:
description: GCPSM configures this store to sync secrets using Google Cloud Platform Secret Manager provider description: GCPSM configures this store to sync secrets using Google Cloud Platform Secret Manager provider
properties: properties:
@ -8069,6 +8097,34 @@ spec:
required: required:
- data - data
type: object 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: gcpsm:
description: GCPSM configures this store to sync secrets using Google Cloud Platform Secret Manager provider description: GCPSM configures this store to sync secrets using Google Cloud Platform Secret Manager provider
properties: properties:

View file

@ -3658,6 +3658,79 @@ string
</tr> </tr>
</tbody> </tbody>
</table> </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 id="external-secrets.io/v1beta1.GCPSMAuth">GCPSMAuth
</h3> </h3>
<p> <p>
@ -5533,6 +5606,20 @@ PulumiProvider
<p>Pulumi configures this store to sync secrets using the Pulumi provider</p> <p>Pulumi configures this store to sync secrets using the Pulumi provider</p>
</td> </td>
</tr> </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> </tbody>
</table> </table>
<h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef <h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef

50
docs/provider/fortanix.md Normal file
View 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
View file

@ -72,6 +72,7 @@ require (
github.com/aliyun/credentials-go v1.3.2 github.com/aliyun/credentials-go v1.3.2
github.com/avast/retry-go/v4 v4.5.1 github.com/avast/retry-go/v4 v4.5.1
github.com/cyberark/conjur-api-go v0.11.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/go-openapi/strfmt v0.22.0
github.com/golang-jwt/jwt/v5 v5.2.0 github.com/golang-jwt/jwt/v5 v5.2.0
github.com/hashicorp/golang-lru v1.0.2 github.com/hashicorp/golang-lru v1.0.2

2
go.sum
View file

@ -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/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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 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 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=

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

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

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

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

View file

@ -27,6 +27,7 @@ import (
_ "github.com/external-secrets/external-secrets/pkg/provider/delinea" _ "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/doppler"
_ "github.com/external-secrets/external-secrets/pkg/provider/fake" _ "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/gcp/secretmanager"
_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab" _ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
_ "github.com/external-secrets/external-secrets/pkg/provider/ibm" _ "github.com/external-secrets/external-secrets/pkg/provider/ibm"