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 #365 from KianTigger/oracle-provider

Oracle provider
This commit is contained in:
Lucas Severo Alves 2021-09-10 12:17:37 +01:00 committed by GitHub
commit 9d3b05a2c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 950 additions and 1 deletions

View file

@ -20,6 +20,7 @@ Multiple people and organizations are joining efforts to create a single Externa
- [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/)
- [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/)
- [Alibaba Cloud KMS](https://www.alibabacloud.com/product/kms) (Docs still missing, PRs welcomed!)
- [Oracle Vault]( https://external-secrets.io/provider-oracle-vault)
## Stability and Support Level
@ -40,7 +41,9 @@ Multiple people and organizations are joining efforts to create a single Externa
| [IBM SM](https://external-secrets.io/provider-ibm-secrets-manager/) | alpha | @knelasevero @sebagomez @ricardoptcosta |
| [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/) | alpha | @AndreyZamyslov @knelasevero |
| [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/) | alpha | @Jabray5 |
| Alibaba Cloud KMS | alpha | @ElsaChelala |
| Alibaba Cloud KMS | alpha | @ElsaChelala |
| [Oracle Vault]( https://external-secrets.io/provider-oracle-vault) | alpha | @KianTigger |
## Documentation

View file

@ -0,0 +1,46 @@
/*
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 v1alpha1
import (
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
)
// Configures an store to sync secrets using a Oracle Vault
// backend.
type OracleProvider struct {
// Auth configures how secret-manager authenticates with the Oracle Vault.
Auth OracleAuth `json:"auth"`
// User is an access OCID specific to the account.
User string `json:"user,omitempty"`
// projectID is an access token specific to the secret.
Tenancy string `json:"tenancy,omitempty"`
// projectID is an access token specific to the secret.
Region string `json:"region,omitempty"`
}
type OracleAuth struct {
// SecretRef to pass through sensitive information.
SecretRef OracleSecretRef `json:"secretRef"`
}
type OracleSecretRef struct {
// The Access Token is used for authentication
PrivateKey esmeta.SecretKeySelector `json:"privatekey,omitempty"`
// projectID is an access token specific to the secret.
Fingerprint esmeta.SecretKeySelector `json:"fingerprint,omitempty"`
}

View file

@ -50,6 +50,10 @@ type SecretStoreProvider struct {
// +optional
GCPSM *GCPSMProvider `json:"gcpsm,omitempty"`
// Oracle configures this store to sync secrets using Oracle Vault provider
// +optional
Oracle *OracleProvider `json:"oracle,omitempty"`
// IBM configures this store to sync secrets using IBM Cloud provider
// +optional
IBM *IBMProvider `json:"ibm,omitempty"`

View file

@ -658,6 +658,55 @@ func (in *IBMProvider) DeepCopy() *IBMProvider {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OracleAuth) DeepCopyInto(out *OracleAuth) {
*out = *in
in.SecretRef.DeepCopyInto(&out.SecretRef)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OracleAuth.
func (in *OracleAuth) DeepCopy() *OracleAuth {
if in == nil {
return nil
}
out := new(OracleAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OracleProvider) DeepCopyInto(out *OracleProvider) {
*out = *in
in.Auth.DeepCopyInto(&out.Auth)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OracleProvider.
func (in *OracleProvider) DeepCopy() *OracleProvider {
if in == nil {
return nil
}
out := new(OracleProvider)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OracleSecretRef) DeepCopyInto(out *OracleSecretRef) {
*out = *in
in.PrivateKey.DeepCopyInto(&out.PrivateKey)
in.Fingerprint.DeepCopyInto(&out.Fingerprint)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OracleSecretRef.
func (in *OracleSecretRef) DeepCopy() *OracleSecretRef {
if in == nil {
return nil
}
out := new(OracleSecretRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretStore) DeepCopyInto(out *SecretStore) {
*out = *in
@ -740,6 +789,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
*out = new(GCPSMProvider)
(*in).DeepCopyInto(*out)
}
if in.Oracle != nil {
in, out := &in.Oracle, &out.Oracle
*out = new(OracleProvider)
(*in).DeepCopyInto(*out)
}
if in.IBM != nil {
in, out := &in.IBM, &out.IBM
*out = new(IBMProvider)

View file

@ -403,6 +403,76 @@ spec:
required:
- auth
type: object
oracle:
description: Oracle configures this store to sync secrets using
Oracle Vault provider
properties:
auth:
description: Auth configures how secret-manager authenticates
with the Oracle Vault.
properties:
secretRef:
description: SecretRef to pass through sensitive information.
properties:
fingerprint:
description: projectID is an access token specific
to the secret.
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
privatekey:
description: The Access Token is used for authentication
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
required:
- secretRef
type: object
region:
description: projectID is an access token specific to the
secret.
type: string
tenancy:
description: projectID is an access token specific to the
secret.
type: string
user:
description: User is an access OCID specific to the account.
type: string
required:
- auth
type: object
vault:
description: Vault configures this store to sync secrets using
Hashi provider

View file

@ -403,6 +403,76 @@ spec:
required:
- auth
type: object
oracle:
description: Oracle configures this store to sync secrets using
Oracle Vault provider
properties:
auth:
description: Auth configures how secret-manager authenticates
with the Oracle Vault.
properties:
secretRef:
description: SecretRef to pass through sensitive information.
properties:
fingerprint:
description: projectID is an access token specific
to the secret.
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
privatekey:
description: The Access Token is used for authentication
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
required:
- secretRef
type: object
region:
description: projectID is an access token specific to the
secret.
type: string
tenancy:
description: projectID is an access token specific to the
secret.
type: string
user:
description: User is an access OCID specific to the account.
type: string
required:
- auth
type: object
vault:
description: Vault configures this store to sync secrets using
Hashi provider

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View file

@ -0,0 +1,54 @@
## Oracle Vault
External Secrets Operator integrates with [OCI API](https://github.com/oracle/oci-go-sdk) to sync secret on the Oracle Vault to secrets held on the Kubernetes cluster.
### Authentication
The API requires a userOCID, tenancyOCID, fingerprint, key file and a region. The fingerprint and key file should be supplied in the secret with the rest being provided in the secret store.
See url for what region you you are accessing.
![userOCID-details](./pictures/screenshot_region.png)
Select tenancy in the top right to see your user OCID as shown below.
![tenancyOCID-details](./pictures/tenancy.png)
Select your user in the top right to see your user OCID as shown below.
![region-details](./pictures/screenshot_user_OCID.png)
#### Service account key authentication
Create a secret containing your private key and fingerprint:
```yaml
{% include 'oracle-credentials-secret.yaml' %}
```
Your fingerprint will be attatched to your API key, once it has been generated. Found on the same page as the user OCID.
![fingerprint-details](./pictures/screenshot_fingerprint.png)
Once you click "Add API Key" you will be shown the following, where you can download the RSA key in the necessary PEM format for API requests.
This will automatically generate a fingerprint.
![API-key-details](./pictures/screenshot_API_key.png)
### Update secret store
Be sure the `oracle` provider is listed in the `Kind=SecretStore`
```yaml
{% include 'oracle-secret-store.yaml' %}
```
### Creating external secret
To create a kubernetes secret from the Oracle Cloud Interface secret a`Kind=ExternalSecret` is needed.
```yaml
{% include 'oracle-external-secret.yaml' %}
```
### Getting the Kubernetes secret
The operator will fetch the project variable and inject it as a `Kind=Secret`.
```
kubectl get secret oracle-secret-to-create -o jsonpath='{.data.dev-secret-test}' | base64 -d
```

View file

@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: oracle-secret
labels:
type: oracle
type: Opaque
stringData:
privateKey:
fingerprint:

View file

@ -0,0 +1,16 @@
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
name: example
spec:
refreshInterval: 0.03m
secretStoreRef:
kind: SecretStore
name: example # Must match SecretStore on the cluster
target:
name: secret-to-be-created # Name for the secret on the cluster
creationPolicy: Owner
data:
- secretKey:
remoteRef:
key:

View file

@ -0,0 +1,18 @@
apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
name: example
spec:
provider:
oracle: #Needs to match value in secretstore_types.go
user:
tenancy:
region:
auth:
secretRef:
privatekey:
name: oracle-secret
key: privateKey #Needs to match stringData val in secret_oracle.yml
fingerprint:
name: oracle-secret
key: fingerprint

View file

@ -60,5 +60,10 @@ kubectl run --rm \
--env="VAULT_URL=${VAULT_URL:-}" \
--env="GITLAB_TOKEN=${GITLAB_TOKEN:-}" \
--env="GITLAB_PROJECT_ID=${GITLAB_PROJECT_ID:-}" \
--env="ORACLE_USER_OCID=${ORACLE_USER_OCID:-}" \
--env="ORACLE_TENANCY_OCID=${ORACLE_TENANCY_OCID:-}" \
--env="ORACLE_REGION=${ORACLE_REGION:-}" \
--env="ORACLE_FINGERPRINT=${ORACLE_FINGERPRINT:-}" \
--env="ORACLE_KEY=${ORACLE_KEY:-}" \
--overrides='{ "apiVersion": "v1", "spec":{"serviceAccountName": "external-secrets-e2e"}}' \
e2e --image=local/external-secrets-e2e:test

View file

@ -0,0 +1,47 @@
/*
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.
limitations under the License.
*/
package oracle
import (
"os"
// nolint
. "github.com/onsi/ginkgo"
// nolint
. "github.com/onsi/ginkgo/extensions/table"
"github.com/external-secrets/external-secrets/e2e/framework"
"github.com/external-secrets/external-secrets/e2e/suite/common"
)
var _ = Describe("[oracle] ", func() {
f := framework.New("eso-oracle")
tenancy := os.Getenv("OCI_TENANCY_OCID")
user := os.Getenv("OCI_USER_OCID")
region := os.Getenv("OCI_REGION")
fingerprint := os.Getenv("OCI_FINGERPRINT")
privateKey := os.Getenv("OCI_PRIVATE_KEY")
prov := newOracleProvider(f, tenancy, user, region, fingerprint, privateKey)
DescribeTable("sync secrets", framework.TableFunc(f, prov),
Entry(common.SimpleDataSync(f)),
Entry(common.NestedJSONWithGJSON(f)),
Entry(common.JSONDataFromSync(f)),
Entry(common.JSONDataWithProperty(f)),
Entry(common.JSONDataWithTemplate(f)),
Entry(common.DockerJSONConfig(f)),
Entry(common.DataPropertyDockerconfigJSON(f)),
Entry(common.SSHKeySync(f)),
Entry(common.SSHKeySyncDataProperty(f)),
)
})

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.
limitations under the License.
*/
package oracle
import (
"context"
// nolint
. "github.com/onsi/ginkgo"
// nolint
. "github.com/onsi/gomega"
"github.com/oracle/oci-go-sdk/v45/common"
vault "github.com/oracle/oci-go-sdk/v45/vault"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilpointer "k8s.io/utils/pointer"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/e2e/framework"
)
type oracleProvider struct {
tenancy string
user string
region string
fingerprint string
privateKey string
framework *framework.Framework
ctx context.Context
}
const (
secretName = "secretName"
)
func newOracleProvider(f *framework.Framework, tenancy, user, region, fingerprint, privateKey string) *oracleProvider {
prov := &oracleProvider{
tenancy: tenancy,
user: user,
region: region,
fingerprint: fingerprint,
privateKey: privateKey,
framework: f,
}
BeforeEach(prov.BeforeEach)
return prov
}
func (p *oracleProvider) CreateSecret(key, val string) {
configurationProvider := common.NewRawConfigurationProvider(p.tenancy, p.user, p.region, p.fingerprint, p.privateKey, nil)
client, err := vault.NewVaultsClientWithConfigurationProvider(configurationProvider)
Expect(err).ToNot(HaveOccurred())
vmssecretrequest := vault.CreateSecretRequest{}
vmssecretrequest.SecretName = utilpointer.StringPtr(secretName)
vmssecretrequest.SecretContent = vault.Base64SecretContentDetails{
Name: utilpointer.StringPtr(key),
Content: utilpointer.StringPtr(val),
}
_, err = client.CreateSecret(p.ctx, vmssecretrequest)
Expect(err).ToNot(HaveOccurred())
}
func (p *oracleProvider) DeleteSecret(key string) {
configurationProvider := common.NewRawConfigurationProvider(p.tenancy, p.user, p.region, p.fingerprint, p.privateKey, nil)
client, err := vault.NewVaultsClientWithConfigurationProvider(configurationProvider)
Expect(err).ToNot(HaveOccurred())
vmssecretrequest := vault.ScheduleSecretDeletionRequest{}
vmssecretrequest.SecretId = utilpointer.StringPtr(key)
_, err = client.ScheduleSecretDeletion(p.ctx, vmssecretrequest)
Expect(err).ToNot(HaveOccurred())
}
func (p *oracleProvider) BeforeEach() {
OracleCreds := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: p.framework.Namespace.Name,
},
StringData: map[string]string{
secretName: "value",
},
}
err := p.framework.CRClient.Create(context.Background(), OracleCreds)
Expect(err).ToNot(HaveOccurred())
secretStore := &esv1alpha1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: p.framework.Namespace.Name,
Namespace: p.framework.Namespace.Name,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
Oracle: &esv1alpha1.OracleProvider{
Auth: esv1alpha1.OracleAuth{
SecretRef: esv1alpha1.OracleSecretRef{
Fingerprint: esmeta.SecretKeySelector{
Name: "vms-secret",
Key: "keyid",
},
PrivateKey: esmeta.SecretKeySelector{
Name: "vms-secret",
Key: "accesskey",
},
},
},
},
},
},
}
err = p.framework.CRClient.Create(context.Background(), secretStore)
Expect(err).ToNot(HaveOccurred())
}

1
go.mod
View file

@ -59,6 +59,7 @@ require (
github.com/lestrrat-go/jwx v1.2.1
github.com/onsi/ginkgo v1.16.4
github.com/onsi/gomega v1.16.0
github.com/oracle/oci-go-sdk/v45 v45.2.0
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/client_model v0.2.0

2
go.sum
View file

@ -545,6 +545,8 @@ github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDs
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/oracle/oci-go-sdk/v45 v45.2.0 h1:vCPoQlE+DOrM2heJn66rvPU6fbsc/0Cxtzs2jnFut6U=
github.com/oracle/oci-go-sdk/v45 v45.2.0/go.mod h1:ZM6LGiRO5TPQJxTlrXbcHMbClE775wnGD5U/EerCsRw=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=

View file

@ -50,6 +50,8 @@ nav:
- Lockbox: provider-yandex-lockbox.md
- Gitlab:
- Gitlab Project Variables: provider-gitlab-project-variables.md
- Oracle:
- Oracle Vault: provider-oracle-vault.md
- References:
- API specification: spec.md
- Contributing:

View file

@ -0,0 +1,36 @@
/*
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 fake
import (
"context"
vault "github.com/oracle/oci-go-sdk/v45/vault"
)
type OracleMockClient struct {
getSecret func(ctx context.Context, request vault.GetSecretRequest) (response vault.GetSecretResponse, err error)
}
func (mc *OracleMockClient) GetSecret(ctx context.Context, request vault.GetSecretRequest) (response vault.GetSecretResponse, err error) {
return mc.getSecret(ctx, request)
}
func (mc *OracleMockClient) WithValue(input vault.GetSecretRequest, output vault.GetSecretResponse, err error) {
if mc != nil {
mc.getSecret = func(ctx context.Context, paramReq vault.GetSecretRequest) (vault.GetSecretResponse, error) {
return output, err
}
}
}

View file

@ -0,0 +1,213 @@
/*
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 oracle
import (
"context"
"encoding/json"
"fmt"
"github.com/oracle/oci-go-sdk/v45/common"
vault "github.com/oracle/oci-go-sdk/v45/vault"
"github.com/tidwall/gjson"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
kclient "sigs.k8s.io/controller-runtime/pkg/client"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/utils"
)
const (
VaultEndpointEnv = "ORACLE_VAULT_ENDPOINT"
STSEndpointEnv = "ORACLE_STS_ENDPOINT"
SVMEndpointEnv = "ORACLE_SVM_ENDPOINT"
errOracleClient = "cannot setup new oracle client: %w"
errORACLECredSecretName = "invalid oracle SecretStore resource: missing oracle APIKey"
errUninitalizedOracleProvider = "provider oracle is not initialized"
errInvalidClusterStoreMissingSKNamespace = "invalid ClusterStore, missing namespace"
errFetchSAKSecret = "could not fetch SecretAccessKey secret: %w"
errMissingPK = "missing PrivateKey"
errMissingUser = "missing User ID"
errMissingTenancy = "missing Tenancy ID"
errMissingRegion = "missing Region"
errMissingFingerprint = "missing Fingerprint"
errJSONSecretUnmarshal = "unable to unmarshal secret: %w"
errMissingKey = "missing Key in secret: %s"
errInvalidSecret = "invalid secret received. no secret string nor binary for key: %s"
)
type client struct {
kube kclient.Client
store *esv1alpha1.OracleProvider
namespace string
storeKind string
tenancy string
user string
region string
fingerprint string
privateKey string
}
type VaultManagementService struct {
Client VMInterface
}
type VMInterface interface {
GetSecret(ctx context.Context, request vault.GetSecretRequest) (response vault.GetSecretResponse, err error)
}
func (c *client) setAuth(ctx context.Context) error {
credentialsSecret := &corev1.Secret{}
credentialsSecretName := c.store.Auth.SecretRef.PrivateKey.Name
if credentialsSecretName == "" {
return fmt.Errorf(errORACLECredSecretName)
}
objectKey := types.NamespacedName{
Name: credentialsSecretName,
Namespace: c.namespace,
}
// only ClusterStore is allowed to set namespace (and then it's required)
if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
if c.store.Auth.SecretRef.PrivateKey.Namespace == nil {
return fmt.Errorf(errInvalidClusterStoreMissingSKNamespace)
}
objectKey.Namespace = *c.store.Auth.SecretRef.PrivateKey.Namespace
}
err := c.kube.Get(ctx, objectKey, credentialsSecret)
if err != nil {
return fmt.Errorf(errFetchSAKSecret, err)
}
c.privateKey = string(credentialsSecret.Data[c.store.Auth.SecretRef.PrivateKey.Key])
if c.privateKey == "" {
return fmt.Errorf(errMissingPK)
}
c.fingerprint = string(credentialsSecret.Data[c.store.Auth.SecretRef.Fingerprint.Key])
if c.fingerprint == "" {
return fmt.Errorf(errMissingFingerprint)
}
c.user = c.store.User
if c.user == "" {
return fmt.Errorf(errMissingUser)
}
c.tenancy = c.store.Tenancy
if c.tenancy == "" {
return fmt.Errorf(errMissingTenancy)
}
c.region = c.store.Region
if c.region == "" {
return fmt.Errorf(errMissingRegion)
}
return nil
}
func (vms *VaultManagementService) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
if utils.IsNil(vms.Client) {
return nil, fmt.Errorf(errUninitalizedOracleProvider)
}
vmsRequest := vault.GetSecretRequest{
SecretId: &ref.Key,
}
secretOut, err := vms.Client.GetSecret(context.Background(), vmsRequest)
if err != nil {
return nil, util.SanitizeErr(err)
}
if ref.Property == "" {
if *secretOut.SecretName != "" {
return []byte(*secretOut.SecretName), nil
}
return nil, fmt.Errorf(errInvalidSecret, ref.Key)
}
var payload *string
if secretOut.SecretName != nil {
payload = secretOut.SecretName
}
payloadval := *payload
val := gjson.Get(payloadval, ref.Property)
if !val.Exists() {
return nil, fmt.Errorf(errMissingKey, ref.Key)
}
return []byte(val.String()), nil
}
func (vms *VaultManagementService) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
data, err := vms.GetSecret(ctx, ref)
if err != nil {
return nil, err
}
kv := make(map[string]string)
err = json.Unmarshal(data, &kv)
if err != nil {
return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
}
secretData := make(map[string][]byte)
for k, v := range kv {
secretData[k] = []byte(v)
}
return secretData, nil
}
// NewClient constructs a new secrets client based on the provided store.
func (vms *VaultManagementService) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
storeSpec := store.GetSpec()
oracleSpec := storeSpec.Provider.Oracle
oracleStore := &client{
kube: kube,
store: oracleSpec,
namespace: namespace,
storeKind: store.GetObjectKind().GroupVersionKind().Kind,
}
if err := oracleStore.setAuth(ctx); err != nil {
return nil, err
}
oracleTenancy := oracleStore.tenancy
oracleUser := oracleStore.user
oracleRegion := oracleStore.region
oracleFingerprint := oracleStore.fingerprint
oraclePrivateKey := oracleStore.privateKey
configurationProvider := common.NewRawConfigurationProvider(oracleTenancy, oracleUser, oracleRegion, oracleFingerprint, oraclePrivateKey, nil)
vaultManagementService, err := vault.NewVaultsClientWithConfigurationProvider(configurationProvider)
if err != nil {
return nil, fmt.Errorf(errOracleClient, err)
}
vms.Client = vaultManagementService
return vms, nil
}
func (vms *VaultManagementService) Close(ctx context.Context) error {
return nil
}
func init() {
schema.Register(&VaultManagementService{}, &esv1alpha1.SecretStoreProvider{
Oracle: &esv1alpha1.OracleProvider{},
})
}

View file

@ -0,0 +1,173 @@
/*
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 oracle
import (
"context"
"fmt"
"reflect"
"strings"
"testing"
vault "github.com/oracle/oci-go-sdk/v45/vault"
utilpointer "k8s.io/utils/pointer"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
fakeoracle "github.com/external-secrets/external-secrets/pkg/provider/oracle/fake"
)
type vaultTestCase struct {
mockClient *fakeoracle.OracleMockClient
apiInput *vault.GetSecretRequest
apiOutput *vault.GetSecretResponse
ref *esv1alpha1.ExternalSecretDataRemoteRef
apiErr error
expectError string
expectedSecret string
// for testing secretmap
expectedData map[string][]byte
}
func makeValidVaultTestCase() *vaultTestCase {
smtc := vaultTestCase{
mockClient: &fakeoracle.OracleMockClient{},
apiInput: makeValidAPIInput(),
ref: makeValidRef(),
apiOutput: makeValidAPIOutput(),
apiErr: nil,
expectError: "",
expectedSecret: "",
expectedData: map[string][]byte{},
}
smtc.mockClient.WithValue(*smtc.apiInput, *smtc.apiOutput, smtc.apiErr)
return &smtc
}
func makeValidRef() *esv1alpha1.ExternalSecretDataRemoteRef {
return &esv1alpha1.ExternalSecretDataRemoteRef{
Key: "test-secret",
Version: "default",
}
}
func makeValidAPIInput() *vault.GetSecretRequest {
return &vault.GetSecretRequest{
SecretId: utilpointer.StringPtr("test-secret"),
}
}
func makeValidAPIOutput() *vault.GetSecretResponse {
return &vault.GetSecretResponse{
Etag: utilpointer.StringPtr("test-name"),
Secret: vault.Secret{},
}
}
func makeValidVaultTestCaseCustom(tweaks ...func(smtc *vaultTestCase)) *vaultTestCase {
smtc := makeValidVaultTestCase()
for _, fn := range tweaks {
fn(smtc)
}
smtc.mockClient.WithValue(*smtc.apiInput, *smtc.apiOutput, smtc.apiErr)
return smtc
}
// This case can be shared by both GetSecret and GetSecretMap tests.
// bad case: set apiErr.
var setAPIErr = func(smtc *vaultTestCase) {
smtc.apiErr = fmt.Errorf("oh no")
smtc.expectError = "oh no"
}
var setNilMockClient = func(smtc *vaultTestCase) {
smtc.mockClient = nil
smtc.expectError = errUninitalizedOracleProvider
}
func TestOracleVaultGetSecret(t *testing.T) {
secretValue := "changedvalue"
// good case: default version is set
// key is passed in, output is sent back
setSecretString := func(smtc *vaultTestCase) {
smtc.apiOutput = &vault.GetSecretResponse{
Etag: utilpointer.StringPtr("test-name"),
Secret: vault.Secret{
CompartmentId: utilpointer.StringPtr("test-compartment-id"),
Id: utilpointer.StringPtr("test-id"),
SecretName: utilpointer.StringPtr("changedvalue"),
},
}
smtc.expectedSecret = secretValue
}
successCases := []*vaultTestCase{
makeValidVaultTestCaseCustom(setAPIErr),
makeValidVaultTestCaseCustom(setNilMockClient),
makeValidVaultTestCaseCustom(setSecretString),
}
sm := VaultManagementService{}
for k, v := range successCases {
sm.Client = v.mockClient
fmt.Println(*v.ref)
out, err := sm.GetSecret(context.Background(), *v.ref)
if !ErrorContains(err, v.expectError) {
t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
}
if string(out) != v.expectedSecret {
t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out))
}
}
}
func TestGetSecretMap(t *testing.T) {
// good case: default version & deserialization
setDeserialization := func(smtc *vaultTestCase) {
smtc.apiOutput.SecretName = utilpointer.StringPtr(`{"foo":"bar"}`)
smtc.expectedData["foo"] = []byte("bar")
}
// bad case: invalid json
setInvalidJSON := func(smtc *vaultTestCase) {
smtc.apiOutput.SecretName = utilpointer.StringPtr(`-----------------`)
smtc.expectError = "unable to unmarshal secret"
}
successCases := []*vaultTestCase{
makeValidVaultTestCaseCustom(setDeserialization),
makeValidVaultTestCaseCustom(setInvalidJSON),
makeValidVaultTestCaseCustom(setNilMockClient),
makeValidVaultTestCaseCustom(setAPIErr),
}
sm := VaultManagementService{}
for k, v := range successCases {
sm.Client = v.mockClient
out, err := sm.GetSecretMap(context.Background(), *v.ref)
if !ErrorContains(err, v.expectError) {
t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
}
if err == nil && !reflect.DeepEqual(out, v.expectedData) {
t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out)
}
}
}
func ErrorContains(out error, want string) bool {
if out == nil {
return want == ""
}
if want == "" {
return false
}
return strings.Contains(out.Error(), want)
}

View file

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