Prechádzať zdrojové kódy

Merge pull request #365 from KianTigger/oracle-provider

Oracle provider
Lucas Severo Alves 4 rokov pred
rodič
commit
9d3b05a2c7

+ 4 - 1
README.md

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

+ 46 - 0
apis/externalsecrets/v1alpha1/secretstore_oracle_types.go

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

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

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

+ 54 - 0
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

@@ -659,6 +659,55 @@ func (in *IBMProvider) DeepCopy() *IBMProvider {
 }
 
 // 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
 	out.TypeMeta = in.TypeMeta
@@ -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)

+ 70 - 0
deploy/crds/external-secrets.io_clustersecretstores.yaml

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

+ 70 - 0
deploy/crds/external-secrets.io_secretstores.yaml

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

BIN
docs/pictures/screenshot_API_key.png


BIN
docs/pictures/screenshot_fingerprint.png


BIN
docs/pictures/screenshot_region.png


BIN
docs/pictures/screenshot_tenancy_OCID.png


BIN
docs/pictures/screenshot_user_OCID.png


+ 54 - 0
docs/provider-oracle-vault.md

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

+ 10 - 0
docs/snippets/oracle-credentials-secret.yaml

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

+ 16 - 0
docs/snippets/oracle-external-secret.yaml

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

+ 18 - 0
docs/snippets/oracle-secret-store.yaml

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

+ 5 - 0
e2e/run.sh

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

+ 47 - 0
e2e/suite/oracle/oracle.go

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

+ 124 - 0
e2e/suite/oracle/provider.go

@@ -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 - 0
go.mod

@@ -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 - 0
go.sum

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

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

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

+ 36 - 0
pkg/provider/oracle/fake/fake.go

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

+ 213 - 0
pkg/provider/oracle/oracle.go

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

+ 173 - 0
pkg/provider/oracle/oracle_test.go

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

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

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