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