Browse Source

feat: Oracle PushSecret & find implementation (#2840)

Signed-off-by: anders-swanson <anders.swanson@oracle.com>
Anders Swanson 2 years ago
parent
commit
f4a7c95b54

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

@@ -36,6 +36,16 @@ type OracleProvider struct {
 	// Vault is the vault's OCID of the specific vault where secret is located.
 	Vault string `json:"vault"`
 
+	// Compartment is the vault compartment OCID.
+	// Required for PushSecret
+	// +optional
+	Compartment string `json:"compartment,omitempty"`
+
+	// EncryptionKey is the OCID of the encryption key within the vault.
+	// Required for PushSecret
+	// +optional
+	EncryptionKey string `json:"encryptionKey,omitempty"`
+
 	// The type of principal to use for authentication. If left blank, the Auth struct will
 	// determine the principal type. This optional field must be specified if using
 	// workload identity.

+ 10 - 0
apis/externalsecrets/v1beta1/secretstore_oracle_types.go

@@ -36,6 +36,16 @@ type OracleProvider struct {
 	// Vault is the vault's OCID of the specific vault where secret is located.
 	Vault string `json:"vault"`
 
+	// Compartment is the vault compartment OCID.
+	// Required for PushSecret
+	// +optional
+	Compartment string `json:"compartment,omitempty"`
+
+	// EncryptionKey is the OCID of the encryption key within the vault.
+	// Required for PushSecret
+	// +optional
+	EncryptionKey string `json:"encryptionKey,omitempty"`
+
 	// The type of principal to use for authentication. If left blank, the Auth struct will
 	// determine the principal type. This optional field must be specified if using
 	// workload identity.

+ 16 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -943,6 +943,14 @@ spec:
                         - tenancy
                         - user
                         type: object
+                      compartment:
+                        description: Compartment is the vault compartment OCID. Required
+                          for PushSecret
+                        type: string
+                      encryptionKey:
+                        description: EncryptionKey is the OCID of the encryption key
+                          within the vault. Required for PushSecret
+                        type: string
                       principalType:
                         description: The type of principal to use for authentication.
                           If left blank, the Auth struct will determine the principal
@@ -3048,6 +3056,14 @@ spec:
                         - tenancy
                         - user
                         type: object
+                      compartment:
+                        description: Compartment is the vault compartment OCID. Required
+                          for PushSecret
+                        type: string
+                      encryptionKey:
+                        description: EncryptionKey is the OCID of the encryption key
+                          within the vault. Required for PushSecret
+                        type: string
                       principalType:
                         description: The type of principal to use for authentication.
                           If left blank, the Auth struct will determine the principal

+ 16 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -943,6 +943,14 @@ spec:
                         - tenancy
                         - user
                         type: object
+                      compartment:
+                        description: Compartment is the vault compartment OCID. Required
+                          for PushSecret
+                        type: string
+                      encryptionKey:
+                        description: EncryptionKey is the OCID of the encryption key
+                          within the vault. Required for PushSecret
+                        type: string
                       principalType:
                         description: The type of principal to use for authentication.
                           If left blank, the Auth struct will determine the principal
@@ -3048,6 +3056,14 @@ spec:
                         - tenancy
                         - user
                         type: object
+                      compartment:
+                        description: Compartment is the vault compartment OCID. Required
+                          for PushSecret
+                        type: string
+                      encryptionKey:
+                        description: EncryptionKey is the OCID of the encryption key
+                          within the vault. Required for PushSecret
+                        type: string
                       principalType:
                         description: The type of principal to use for authentication.
                           If left blank, the Auth struct will determine the principal

+ 24 - 0
deploy/crds/bundle.yaml

@@ -1226,6 +1226,12 @@ spec:
                             - tenancy
                             - user
                           type: object
+                        compartment:
+                          description: Compartment is the vault compartment OCID. Required for PushSecret
+                          type: string
+                        encryptionKey:
+                          description: EncryptionKey is the OCID of the encryption key within the vault. Required for PushSecret
+                          type: string
                         principalType:
                           description: The type of principal to use for authentication. If left blank, the Auth struct will determine the principal type. This optional field must be specified if using workload identity.
                           type: string
@@ -2764,6 +2770,12 @@ spec:
                             - tenancy
                             - user
                           type: object
+                        compartment:
+                          description: Compartment is the vault compartment OCID. Required for PushSecret
+                          type: string
+                        encryptionKey:
+                          description: EncryptionKey is the OCID of the encryption key within the vault. Required for PushSecret
+                          type: string
                         principalType:
                           description: The type of principal to use for authentication. If left blank, the Auth struct will determine the principal type. This optional field must be specified if using workload identity.
                           type: string
@@ -5137,6 +5149,12 @@ spec:
                             - tenancy
                             - user
                           type: object
+                        compartment:
+                          description: Compartment is the vault compartment OCID. Required for PushSecret
+                          type: string
+                        encryptionKey:
+                          description: EncryptionKey is the OCID of the encryption key within the vault. Required for PushSecret
+                          type: string
                         principalType:
                           description: The type of principal to use for authentication. If left blank, the Auth struct will determine the principal type. This optional field must be specified if using workload identity.
                           type: string
@@ -6675,6 +6693,12 @@ spec:
                             - tenancy
                             - user
                           type: object
+                        compartment:
+                          description: Compartment is the vault compartment OCID. Required for PushSecret
+                          type: string
+                        encryptionKey:
+                          description: EncryptionKey is the OCID of the encryption key within the vault. Required for PushSecret
+                          type: string
                         principalType:
                           description: The type of principal to use for authentication. If left blank, the Auth struct will determine the principal type. This optional field must be specified if using workload identity.
                           type: string

+ 26 - 0
docs/api/spec.md

@@ -4543,6 +4543,32 @@ string
 </tr>
 <tr>
 <td>
+<code>compartment</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Compartment is the vault compartment OCID.
+Required for PushSecret</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>encryptionKey</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>EncryptionKey is the OCID of the encryption key within the vault.
+Required for PushSecret</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>principalType</code></br>
 <em>
 <a href="#external-secrets.io/v1beta1.OraclePrincipalType">

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

@@ -57,3 +57,14 @@ 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
 ```
+
+### PushSecrets and retrieving multiple secrets.
+When using [PushSecrets](https://external-secrets.io/latest/guides/pushsecrets/), the compartment OCID and encryption key OCID must be specified in the
+Oracle SecretStore. You can find your compartment and encrpytion key OCIDs in the OCI console.
+
+If [retrieving multiple secrets](https://external-secrets.io/latest/guides/getallsecrets/) by tag or regex, only the compartment OCID must be specified.
+
+```yaml
+{% include 'oracle-secret-store-pushsecret.yaml' %}
+```
+

+ 11 - 0
docs/snippets/oracle-secret-store-pushsecret.yaml

@@ -0,0 +1,11 @@
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: example-instance-principal
+spec:
+  provider:
+    oracle:
+      vault: # The vault OCID
+      compartment: # The compartment OCID where the vault is located. Required when using PushSecrets or retrieving multiple secrets.
+      encryptionKey: # The OCID of the master encryption key that will be used for PushSecret encryption. Must exist in the vault, required when using PushSecrets.
+      principalType: Workload

+ 63 - 2
pkg/provider/oracle/fake/fake.go

@@ -16,14 +16,52 @@ package fake
 import (
 	"context"
 
-	secrets "github.com/oracle/oci-go-sdk/v65/secrets"
+	"github.com/oracle/oci-go-sdk/v65/secrets"
+	"github.com/oracle/oci-go-sdk/v65/vault"
 )
 
+type OracleMockVaultClient struct {
+	SecretSummaries []vault.SecretSummary
+	CreatedCount    int
+	UpdatedCount    int
+	DeletedCount    int
+}
+
+func (o *OracleMockVaultClient) ListSecrets(_ context.Context, _ vault.ListSecretsRequest) (response vault.ListSecretsResponse, err error) {
+	return vault.ListSecretsResponse{
+		Items: o.SecretSummaries,
+	}, nil
+}
+
+func (o *OracleMockVaultClient) CreateSecret(_ context.Context, _ vault.CreateSecretRequest) (response vault.CreateSecretResponse, err error) {
+	o.CreatedCount++
+	return vault.CreateSecretResponse{}, nil
+}
+
+func (o *OracleMockVaultClient) UpdateSecret(_ context.Context, _ vault.UpdateSecretRequest) (response vault.UpdateSecretResponse, err error) {
+	o.UpdatedCount++
+	return vault.UpdateSecretResponse{}, nil
+}
+
+func (o *OracleMockVaultClient) ScheduleSecretDeletion(_ context.Context, _ vault.ScheduleSecretDeletionRequest) (response vault.ScheduleSecretDeletionResponse, err error) {
+	o.DeletedCount++
+	return vault.ScheduleSecretDeletionResponse{}, nil
+}
+
 type OracleMockClient struct {
-	getSecret func(ctx context.Context, request secrets.GetSecretBundleByNameRequest) (response secrets.GetSecretBundleByNameResponse, err error)
+	getSecret     func(ctx context.Context, request secrets.GetSecretBundleByNameRequest) (response secrets.GetSecretBundleByNameResponse, err error)
+	SecretBundles map[string]secrets.SecretBundle
 }
 
 func (mc *OracleMockClient) GetSecretBundleByName(ctx context.Context, request secrets.GetSecretBundleByNameRequest) (response secrets.GetSecretBundleByNameResponse, err error) {
+	if mc.SecretBundles != nil {
+		if bundle, ok := mc.SecretBundles[*request.SecretName]; ok {
+			return secrets.GetSecretBundleByNameResponse{
+				SecretBundle: bundle,
+			}, nil
+		}
+		return secrets.GetSecretBundleByNameResponse{}, &ServiceError{Code: 404}
+	}
 	return mc.getSecret(ctx, request)
 }
 
@@ -34,3 +72,26 @@ func (mc *OracleMockClient) WithValue(_ secrets.GetSecretBundleByNameRequest, ou
 		}
 	}
 }
+
+type ServiceError struct {
+	Code int
+}
+
+func (s *ServiceError) Error() string {
+	return ""
+}
+func (s *ServiceError) GetHTTPStatusCode() int {
+	return s.Code
+}
+
+func (s *ServiceError) GetMessage() string {
+	return ""
+}
+
+func (s *ServiceError) GetCode() string {
+	return ""
+}
+
+func (s *ServiceError) GetOpcRequestID() string {
+	return ""
+}

+ 200 - 25
pkg/provider/oracle/oracle.go

@@ -14,12 +14,14 @@ limitations under the License.
 package oracle
 
 import (
+	"bytes"
 	"context"
 	"encoding/base64"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"os"
+	"regexp"
 	"sync"
 	"time"
 
@@ -27,6 +29,7 @@ import (
 	"github.com/oracle/oci-go-sdk/v65/common/auth"
 	"github.com/oracle/oci-go-sdk/v65/keymanagement"
 	"github.com/oracle/oci-go-sdk/v65/secrets"
+	"github.com/oracle/oci-go-sdk/v65/vault"
 	"github.com/tidwall/gjson"
 	corev1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -37,7 +40,6 @@ import (
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
-	"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
@@ -65,7 +67,10 @@ var _ esv1beta1.Provider = &VaultManagementService{}
 type VaultManagementService struct {
 	Client                VMInterface
 	KmsVaultClient        KmsVCInterface
+	VaultClient           VaultInterface
 	vault                 string
+	compartment           string
+	encryptionKey         string
 	workloadIdentityMutex sync.Mutex
 }
 
@@ -77,19 +82,98 @@ type KmsVCInterface interface {
 	GetVault(ctx context.Context, request keymanagement.GetVaultRequest) (response keymanagement.GetVaultResponse, err error)
 }
 
-// Not Implemented PushSecret.
-func (vms *VaultManagementService) PushSecret(_ context.Context, _ []byte, _ corev1.SecretType, _ *apiextensionsv1.JSON, _ esv1beta1.PushRemoteRef) error {
-	return fmt.Errorf("not implemented")
+type VaultInterface interface {
+	ListSecrets(ctx context.Context, request vault.ListSecretsRequest) (response vault.ListSecretsResponse, err error)
+	CreateSecret(ctx context.Context, request vault.CreateSecretRequest) (response vault.CreateSecretResponse, err error)
+	UpdateSecret(ctx context.Context, request vault.UpdateSecretRequest) (response vault.UpdateSecretResponse, err error)
+	ScheduleSecretDeletion(ctx context.Context, request vault.ScheduleSecretDeletionRequest) (response vault.ScheduleSecretDeletionResponse, err error)
 }
 
-func (vms *VaultManagementService) DeleteSecret(_ context.Context, _ esv1beta1.PushRemoteRef) error {
-	return fmt.Errorf("not implemented")
+const (
+	SecretNotFound = iota
+	SecretExists
+	SecretAPIError
+)
+
+func (vms *VaultManagementService) PushSecret(ctx context.Context, value []byte, _ corev1.SecretType, _ *apiextensionsv1.JSON, remoteRef esv1beta1.PushRemoteRef) error {
+	secretName := remoteRef.GetRemoteKey()
+	encodedValue := base64.StdEncoding.EncodeToString(value)
+	sec, action, err := vms.getSecretBundleWithCode(ctx, secretName)
+	switch action {
+	case SecretNotFound:
+		_, err = vms.VaultClient.CreateSecret(ctx, vault.CreateSecretRequest{
+			CreateSecretDetails: vault.CreateSecretDetails{
+				CompartmentId: &vms.compartment,
+				KeyId:         &vms.encryptionKey,
+				SecretContent: vault.Base64SecretContentDetails{
+					Content: &encodedValue,
+				},
+				SecretName: &secretName,
+				VaultId:    &vms.vault,
+			},
+		})
+		return sanitizeOCISDKErr(err)
+	case SecretExists:
+		payload, err := decodeBundle(sec)
+		if err != nil {
+			return err
+		}
+		if bytes.Equal(payload, value) {
+			return nil
+		}
+		_, err = vms.VaultClient.UpdateSecret(ctx, vault.UpdateSecretRequest{
+			SecretId: sec.SecretId,
+			UpdateSecretDetails: vault.UpdateSecretDetails{
+				SecretContent: vault.Base64SecretContentDetails{
+					Content: &encodedValue,
+				},
+			},
+		})
+		return sanitizeOCISDKErr(err)
+	default:
+		return sanitizeOCISDKErr(err)
+	}
 }
 
-// Empty GetAllSecrets.
-func (vms *VaultManagementService) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
-	// TO be implemented
-	return nil, fmt.Errorf("GetAllSecrets not implemented")
+func (vms *VaultManagementService) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
+	secretName := remoteRef.GetRemoteKey()
+	resp, action, err := vms.getSecretBundleWithCode(ctx, secretName)
+	switch action {
+	case SecretNotFound:
+		return nil
+	case SecretExists:
+		if resp.TimeOfDeletion != nil {
+			return nil
+		}
+		_, err = vms.VaultClient.ScheduleSecretDeletion(ctx, vault.ScheduleSecretDeletionRequest{
+			SecretId: resp.SecretId,
+		})
+		return sanitizeOCISDKErr(err)
+	default:
+		return sanitizeOCISDKErr(err)
+	}
+}
+
+func (vms *VaultManagementService) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	var page *string
+	var summaries []vault.SecretSummary
+
+	for {
+		resp, err := vms.VaultClient.ListSecrets(ctx, vault.ListSecretsRequest{
+			CompartmentId: &vms.compartment,
+			Page:          page,
+			VaultId:       &vms.vault,
+		})
+		if err != nil {
+			return nil, sanitizeOCISDKErr(err)
+		}
+		summaries = append(summaries, resp.Items...)
+		if page = resp.OpcNextPage; resp.OpcNextPage == nil {
+			break
+		}
+	}
+
+	return vms.filteredSummaryResult(ctx, summaries, ref)
 }
 
 func (vms *VaultManagementService) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
@@ -103,25 +187,18 @@ func (vms *VaultManagementService) GetSecret(ctx context.Context, ref esv1beta1.
 		Stage:      secrets.GetSecretBundleByNameStageEnum(ref.Version),
 	})
 	if err != nil {
-		return nil, util.SanitizeErr(err)
-	}
-
-	bt, ok := sec.SecretBundleContent.(secrets.Base64SecretBundleContentDetails)
-	if !ok {
-		return nil, fmt.Errorf(errUnexpectedContent)
+		return nil, sanitizeOCISDKErr(err)
 	}
 
-	payload, err := base64.StdEncoding.DecodeString(*bt.Content)
+	payload, err := decodeBundle(sec)
 	if err != nil {
 		return nil, err
 	}
-
 	if ref.Property == "" {
 		return payload, nil
 	}
 
 	val := gjson.Get(string(payload), ref.Property)
-
 	if !val.Exists() {
 		return nil, fmt.Errorf(errMissingKey, ref.Key)
 	}
@@ -129,10 +206,22 @@ func (vms *VaultManagementService) GetSecret(ctx context.Context, ref esv1beta1.
 	return []byte(val.String()), nil
 }
 
+func decodeBundle(sec secrets.GetSecretBundleByNameResponse) ([]byte, error) {
+	bt, ok := sec.SecretBundleContent.(secrets.Base64SecretBundleContentDetails)
+	if !ok {
+		return nil, fmt.Errorf(errUnexpectedContent)
+	}
+	payload, err := base64.StdEncoding.DecodeString(*bt.Content)
+	if err != nil {
+		return nil, err
+	}
+	return payload, nil
+}
+
 func (vms *VaultManagementService) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 	data, err := vms.GetSecret(ctx, ref)
 	if err != nil {
-		return nil, err
+		return nil, sanitizeOCISDKErr(err)
 	}
 	kv := make(map[string]string)
 	err = json.Unmarshal(data, &kv)
@@ -184,16 +273,20 @@ func (vms *VaultManagementService) NewClient(ctx context.Context, store esv1beta
 	if err != nil {
 		return nil, fmt.Errorf(errOracleClient, err)
 	}
-
 	secretManagementService.SetRegion(oracleSpec.Region)
 
 	kmsVaultClient, err := keymanagement.NewKmsVaultClientWithConfigurationProvider(configurationProvider)
 	if err != nil {
 		return nil, fmt.Errorf(errOracleClient, err)
 	}
-
 	kmsVaultClient.SetRegion(oracleSpec.Region)
 
+	vaultClient, err := vault.NewVaultsClientWithConfigurationProvider(configurationProvider)
+	if err != nil {
+		return nil, fmt.Errorf(errOracleClient, err)
+	}
+	vaultClient.SetRegion(oracleSpec.Region)
+
 	if storeSpec.RetrySettings != nil {
 		opts := []common.RetryPolicyOption{common.WithShouldRetryOperation(common.DefaultShouldRetryOperation)}
 
@@ -218,15 +311,85 @@ func (vms *VaultManagementService) NewClient(ctx context.Context, store esv1beta
 		kmsVaultClient.SetCustomClientConfiguration(common.CustomClientConfiguration{
 			RetryPolicy: &customRetryPolicy,
 		})
+
+		vaultClient.SetCustomClientConfiguration(common.CustomClientConfiguration{
+			RetryPolicy: &customRetryPolicy,
+		})
 	}
 
 	return &VaultManagementService{
 		Client:         secretManagementService,
 		KmsVaultClient: kmsVaultClient,
+		VaultClient:    vaultClient,
 		vault:          oracleSpec.Vault,
+		compartment:    oracleSpec.Compartment,
+		encryptionKey:  oracleSpec.EncryptionKey,
 	}, nil
 }
 
+func (vms *VaultManagementService) getSecretBundleWithCode(ctx context.Context, secretName string) (secrets.GetSecretBundleByNameResponse, int, error) {
+	// Try to look up the secret, which will determine if we should create or update the secret.
+	resp, err := vms.Client.GetSecretBundleByName(ctx, secrets.GetSecretBundleByNameRequest{
+		SecretName: &secretName,
+		VaultId:    &vms.vault,
+	})
+	// Get a PushSecret action depending on the ListSecrets response.
+	action := getSecretBundleCode(err)
+	return resp, action, err
+}
+
+func getSecretBundleCode(err error) int {
+	if err != nil {
+		// If we got a 404 service error, try to create the secret.
+		//nolint:all
+		if serviceErr, ok := err.(common.ServiceError); ok && serviceErr.GetHTTPStatusCode() == 404 {
+			return SecretNotFound
+		}
+		return SecretAPIError
+	}
+	// Otherwise, update the existing secret.
+	return SecretExists
+}
+
+func (vms *VaultManagementService) filteredSummaryResult(ctx context.Context, secretSummaries []vault.SecretSummary, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	secretMap := map[string][]byte{}
+	for _, summary := range secretSummaries {
+		matches, err := matchesRef(summary, ref)
+		if err != nil {
+			return nil, err
+		}
+		if !matches || summary.TimeOfDeletion != nil {
+			continue
+		}
+		secret, err := vms.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{
+			Key: *summary.SecretName,
+		})
+		if err != nil {
+			return nil, err
+		}
+		secretMap[*summary.SecretName] = secret
+	}
+	return secretMap, nil
+}
+
+func matchesRef(secretSummary vault.SecretSummary, ref esv1beta1.ExternalSecretFind) (bool, error) {
+	if ref.Name != nil {
+		matchString, err := regexp.MatchString(ref.Name.RegExp, *secretSummary.SecretName)
+		if err != nil {
+			return false, err
+		}
+		return matchString, nil
+	}
+	for k, v := range ref.Tags {
+		if val, ok := secretSummary.FreeformTags[k]; ok {
+			if val == v {
+				return true, nil
+			}
+		}
+	}
+	return false, nil
+}
+
 func getSecretData(ctx context.Context, kube kclient.Client, namespace, storeKind string, secretRef esmeta.SecretKeySelector) (string, error) {
 	if secretRef.Name == "" {
 		return "", fmt.Errorf(errORACLECredSecretName)
@@ -298,7 +461,7 @@ func (vms *VaultManagementService) Validate() (esv1beta1.ValidationResult, error
 			code := failure.GetCode()
 			switch code {
 			case "NotAuthenticated":
-				return esv1beta1.ValidationResultError, err
+				return esv1beta1.ValidationResultError, sanitizeOCISDKErr(err)
 			case "NotAuthorizedOrNotFound":
 				// User authentication was successful, but user might not have a permission like:
 				//
@@ -309,9 +472,9 @@ func (vms *VaultManagementService) Validate() (esv1beta1.ValidationResult, error
 				// Allow group external_secrets to read secret-family in tenancy
 				//
 				// But we can't test for this permission without knowing the name of a secret
-				return esv1beta1.ValidationResultUnknown, err
+				return esv1beta1.ValidationResultUnknown, sanitizeOCISDKErr(err)
 			default:
-				return esv1beta1.ValidationResultError, err
+				return esv1beta1.ValidationResultError, sanitizeOCISDKErr(err)
 			}
 		} else {
 			return esv1beta1.ValidationResultError, err
@@ -426,6 +589,18 @@ func (vms *VaultManagementService) getWorkloadIdentityProvider(store esv1beta1.G
 	return auth.OkeWorkloadIdentityConfigurationProviderWithServiceAccountTokenProvider(tokenProvider)
 }
 
+func sanitizeOCISDKErr(err error) error {
+	if err == nil {
+		return nil
+	}
+	// If we have a ServiceError from the OCI SDK, strip only the message from the verbose error
+	//nolint:all
+	if serviceError, ok := err.(common.ServiceErrorRichInfo); ok {
+		return fmt.Errorf("%s service failed to %s, HTTP status code %d: %s", serviceError.GetTargetService(), serviceError.GetOperationName(), serviceError.GetHTTPStatusCode(), serviceError.GetMessage())
+	}
+	return err
+}
+
 func init() {
 	esv1beta1.Register(&VaultManagementService{}, &esv1beta1.SecretStoreProvider{
 		Oracle: &esv1beta1.OracleProvider{},

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

@@ -24,14 +24,19 @@ import (
 	"reflect"
 	"strings"
 	"testing"
+	"time"
 
+	"github.com/oracle/oci-go-sdk/v65/common"
 	"github.com/oracle/oci-go-sdk/v65/secrets"
+	"github.com/oracle/oci-go-sdk/v65/vault"
+	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/utils/ptr"
 	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
 
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 	fakeoracle "github.com/external-secrets/external-secrets/pkg/provider/oracle/fake"
@@ -504,3 +509,249 @@ func TestVaultManagementService_NewClient(t *testing.T) {
 		})
 	}
 }
+
+func TestOracleVaultGetAllSecrets(t *testing.T) {
+	var testCases = map[string]struct {
+		vms    *VaultManagementService
+		ref    esv1beta1.ExternalSecretFind
+		result map[string][]byte
+	}{
+		"filters secrets that don't match the pattern": {
+			&VaultManagementService{
+				Client: &fakeoracle.OracleMockClient{
+					SecretBundles: map[string]secrets.SecretBundle{
+						s1id: s1bundle,
+						s2id: s2bundle,
+					},
+				},
+				VaultClient: &fakeoracle.OracleMockVaultClient{
+					SecretSummaries: []vault.SecretSummary{
+						s1summary,
+						s2summary,
+					},
+				},
+			},
+			esv1beta1.ExternalSecretFind{
+				Name: &esv1beta1.FindName{
+					RegExp: "^test.*",
+				},
+			},
+			map[string][]byte{
+				s1id: []byte(s1id),
+			},
+		},
+		"filters secrets that are deleting": {
+			&VaultManagementService{
+				Client: &fakeoracle.OracleMockClient{
+					SecretBundles: map[string]secrets.SecretBundle{
+						s1id: s1bundle,
+						s2id: s2bundle,
+						s3id: s3bundle,
+					},
+				},
+				VaultClient: &fakeoracle.OracleMockVaultClient{
+					SecretSummaries: []vault.SecretSummary{
+						s1summary,
+						s2summary,
+						s3summary,
+					},
+				},
+			},
+			esv1beta1.ExternalSecretFind{
+				Name: &esv1beta1.FindName{
+					RegExp: ".*",
+				},
+			},
+			map[string][]byte{
+				s1id: []byte(s1id),
+				s2id: []byte(s2id),
+			},
+		},
+	}
+	for name, testCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			result, err := testCase.vms.GetAllSecrets(context.Background(), testCase.ref)
+			assert.NoError(t, err)
+			assert.EqualValues(t, testCase.result, result)
+		})
+	}
+}
+
+func TestOracleVaultPushSecret(t *testing.T) {
+	var testCases = map[string]struct {
+		vms       *VaultManagementService
+		remoteRef esv1beta1.PushRemoteRef
+		validator func(service *VaultManagementService) bool
+		content   string
+	}{
+		"create a secret if not exists": {
+			&VaultManagementService{
+				Client: &fakeoracle.OracleMockClient{
+					SecretBundles: map[string]secrets.SecretBundle{
+						s2id: s2bundle,
+					},
+				},
+				VaultClient: &fakeoracle.OracleMockVaultClient{},
+			},
+			esv1alpha1.PushSecretRemoteRef{
+				RemoteKey: s1id,
+			},
+			func(vms *VaultManagementService) bool {
+				return vms.VaultClient.(*fakeoracle.OracleMockVaultClient).CreatedCount == 1
+			},
+			"created",
+		},
+		"update a secret if exists": {
+			&VaultManagementService{
+				Client: &fakeoracle.OracleMockClient{
+					SecretBundles: map[string]secrets.SecretBundle{
+						s1id: s1bundle,
+						s2id: s2bundle,
+					},
+				},
+				VaultClient: &fakeoracle.OracleMockVaultClient{},
+			},
+			esv1alpha1.PushSecretRemoteRef{
+				RemoteKey: s1id,
+			},
+			func(vms *VaultManagementService) bool {
+				return vms.VaultClient.(*fakeoracle.OracleMockVaultClient).UpdatedCount == 1
+			},
+			"updated",
+		},
+		"neither create nor update if secret content is unchanged": {
+			&VaultManagementService{
+				Client: &fakeoracle.OracleMockClient{
+					SecretBundles: map[string]secrets.SecretBundle{
+						s1id: s1bundle,
+						s2id: s2bundle,
+					},
+				},
+				VaultClient: &fakeoracle.OracleMockVaultClient{},
+			},
+			esv1alpha1.PushSecretRemoteRef{
+				RemoteKey: s1id,
+			},
+			func(vms *VaultManagementService) bool {
+				return vms.VaultClient.(*fakeoracle.OracleMockVaultClient).UpdatedCount == 0 &&
+					vms.VaultClient.(*fakeoracle.OracleMockVaultClient).CreatedCount == 0
+			},
+			s1id,
+		},
+	}
+	for name, testCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			err := testCase.vms.PushSecret(context.Background(), []byte(testCase.content), "", nil, testCase.remoteRef)
+			assert.NoError(t, err)
+			assert.True(t, testCase.validator(testCase.vms))
+		})
+	}
+}
+
+func TestOracleVaultDeleteSecret(t *testing.T) {
+	var testCases = map[string]struct {
+		vms       *VaultManagementService
+		remoteRef esv1beta1.PushRemoteRef
+		validator func(service *VaultManagementService) bool
+	}{
+		"do not delete if secret not found": {
+			&VaultManagementService{
+				Client: &fakeoracle.OracleMockClient{
+					SecretBundles: map[string]secrets.SecretBundle{
+						s1id: s1bundle,
+					},
+				},
+				VaultClient: &fakeoracle.OracleMockVaultClient{},
+			},
+			esv1alpha1.PushSecretRemoteRef{
+				RemoteKey: s2id,
+			},
+			func(vms *VaultManagementService) bool {
+				return vms.VaultClient.(*fakeoracle.OracleMockVaultClient).DeletedCount == 0
+			},
+		},
+		"do not delete if secret os already deleting": {
+			&VaultManagementService{
+				Client: &fakeoracle.OracleMockClient{
+					SecretBundles: map[string]secrets.SecretBundle{
+						s1id: s1bundle,
+						s3id: s3bundle,
+					},
+				},
+				VaultClient: &fakeoracle.OracleMockVaultClient{},
+			},
+			esv1alpha1.PushSecretRemoteRef{
+				RemoteKey: s3id,
+			},
+			func(vms *VaultManagementService) bool {
+				return vms.VaultClient.(*fakeoracle.OracleMockVaultClient).DeletedCount == 0
+			},
+		},
+		"delete existing secret": {
+			&VaultManagementService{
+				Client: &fakeoracle.OracleMockClient{
+					SecretBundles: map[string]secrets.SecretBundle{
+						s1id: s1bundle,
+						s3id: s3bundle,
+					},
+				},
+				VaultClient: &fakeoracle.OracleMockVaultClient{},
+			},
+			esv1alpha1.PushSecretRemoteRef{
+				RemoteKey: s1id,
+			},
+			func(vms *VaultManagementService) bool {
+				return vms.VaultClient.(*fakeoracle.OracleMockVaultClient).DeletedCount == 1
+			},
+		},
+	}
+	for name, testCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			err := testCase.vms.DeleteSecret(context.Background(), testCase.remoteRef)
+			assert.NoError(t, err)
+			assert.True(t, testCase.validator(testCase.vms))
+		})
+	}
+}
+
+var (
+	s1id      = "test1"
+	s2id      = "mysecret"
+	s3id      = "deleting"
+	s1bundle  = makeSecretBundle(s1id, false)
+	s2bundle  = makeSecretBundle(s2id, false)
+	s3bundle  = makeSecretBundle(s3id, true)
+	s1summary = makeSecretSummary(s1id, false)
+	s2summary = makeSecretSummary(s2id, false)
+	s3summary = makeSecretSummary(s3id, true)
+)
+
+func makeSecretBundle(id string, deleting bool) secrets.SecretBundle {
+	var deletionTime *common.SDKTime
+	if deleting {
+		deletionTime = &common.SDKTime{
+			Time: time.Now(),
+		}
+	}
+	return secrets.SecretBundle{
+		SecretId: &id,
+		SecretBundleContent: secrets.Base64SecretBundleContentDetails{
+			Content: ptr.To(base64.StdEncoding.EncodeToString([]byte(id))),
+		},
+		TimeOfDeletion: deletionTime,
+	}
+}
+
+func makeSecretSummary(id string, deleting bool) vault.SecretSummary {
+	var deletionTime *common.SDKTime
+	if deleting {
+		deletionTime = &common.SDKTime{
+			Time: time.Now(),
+		}
+	}
+	return vault.SecretSummary{
+		Id:             &id,
+		SecretName:     &id,
+		TimeOfDeletion: deletionTime,
+	}
+}