Browse Source

feat(beyondtrust): enable pushing secrets in BeyondTrust provider (#5586)

* fix(beyondtrust): enable pushing secrets in BeyondTrust provider

Signed-off-by: Felipe Hernandez <fhernandez@beyondtrust.com>

* fix(beyondtrust): solve PR comments

Signed-off-by: Felipe Hernandez <fhernandez@beyondtrust.com>

* feat(beyondtrust): add SecretExists method to Beyondtrust provider

Signed-off-by: Felipe Hernandez <fhernandez@beyondtrust.com>

* fix(beyondtrust): solve PR comments

Signed-off-by: Felipe Hernandez <fhernandez@beyondtrust.com>

* docs(beyondtrust): add examples for Secret, ClusterSecretStore and PushSecret

Signed-off-by: Felipe Hernandez <fhernandez@beyondtrust.com>

* fix(beyondtrust): resolve go.mod inconsistencies

Signed-off-by: Felipe Hernandez <fhernandez@beyondtrust.com>

* added proper links and fixed a typo

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

---------

Signed-off-by: Felipe Hernandez <fhernandez@beyondtrust.com>
Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Co-authored-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Co-authored-by: Gergely Brautigam <skarlso777@gmail.com>
btfhernandez 4 months ago
parent
commit
06e184ae06

+ 37 - 16
docs/provider/beyondtrust.md

@@ -113,22 +113,7 @@ kubectl apply -f external-secret.yml
 ```
 ```
 
 
 ```yaml
 ```yaml
-apiVersion: external-secrets.io/v1
-kind: ExternalSecret
-metadata:
-  name: beyondtrust-external-secret
-spec:
-  refreshInterval: 1h
-  secretStoreRef:
-    kind: SecretStore
-    name: secretstore-beyondtrust
-  target:
-    name: my-beyondtrust-secret # name of secret to create in k8s secrets (etcd)
-    creationPolicy: Owner
-  data:
-    - secretKey: secretKey
-      remoteRef:
-        key: system01/managed_account01
+{% include 'beyondtrust-external-secret.yaml' %}
 ```
 ```
 
 
 ### Get the K8s secret
 ### Get the K8s secret
@@ -137,3 +122,39 @@ spec:
 # WARNING: this command will reveal the stored secret in plain text
 # WARNING: this command will reveal the stored secret in plain text
 kubectl get secret my-beyondtrust-secret -o jsonpath="{.data.secretKey}" | base64 --decode && echo
 kubectl get secret my-beyondtrust-secret -o jsonpath="{.data.secretKey}" | base64 --decode && echo
 ```
 ```
+
+### Creating a Secret
+
+The following example shows how to create a Kubernetes `Secret` that will later be pushed to BeyondTrust.
+
+```sh
+kubectl apply -f beyondtrust-secret.yml
+```
+
+```yaml
+{% include 'beyondtrust-secret.yaml' %}
+```
+
+### Creating an ClusterSecretStore
+
+The following example demonstrates how to create a `ClusterSecretStore` configured to use the BeyondTrust provider.
+
+```sh
+kubectl apply -f beyondtrust-cluster-secret-store.yml
+```
+
+```yaml
+{% include 'beyondtrust-cluster-secret-store.yaml' %}
+```
+
+### Creating an PushSecret
+
+The example below demonstrates how to create a `PushSecret` resource to push secret data to BeyondTrust.
+
+```sh
+kubectl apply -f beyondtrust-push-secret.yml
+```
+
+```yaml
+{% include 'beyondtrust-push-secret.yaml' %}
+```

+ 30 - 0
docs/snippets/beyondtrust-cluster-secret-store.yaml

@@ -0,0 +1,30 @@
+apiVersion: external-secrets.io/v1
+kind: ClusterSecretStore
+metadata:
+ name: beyondtrust-store
+spec:
+ provider:
+   beyondtrust:
+    auth:
+      certificate:
+        secretRef:
+            name: bt-certificate
+            key: ClientCertificate
+      certificateKey:
+        secretRef:
+            name: bt-certificatekey
+            key: ClientCertificateKey
+      clientSecret:
+        secretRef:
+          name: bt-secret
+          key: ClientSecret
+      clientId:
+        secretRef:
+          name: bt-id
+          key: ClientId
+    server:
+      retrievalType: MANAGED_ACCOUNT
+      verifyCA: true
+      clientTimeOutSeconds: 45
+      apiUrl: https://example.test.com/BeyondTrust/
+      apiVersion: "3.1"

+ 34 - 0
docs/snippets/beyondtrust-push-secret.yaml

@@ -0,0 +1,34 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: pushsecret-beyondtrust
+spec:
+  refreshInterval: 1h
+  secretStoreRefs:
+    - name: beyondtrust-store
+      kind: ClusterSecretStore
+  selector:
+    secret:
+      name: app-credentials
+  data:
+    - match:
+        secretKey: "password"
+        remoteRef:
+          remoteKey: "" # not used in Beyondtrust PushSecret
+          property: "" # not used in Beyondtrust PushSecret
+      metadata:
+        secret_type: CREDENTIAL # (FILE/CREDENTIAL/TEXT)
+        title: Secret Title 505
+        username: fhernandez
+        description: Secret Title Description
+        file_name: credentials.txt # only for FILE secret_type
+        notes: "Example Notes"
+        folder_name: folder1
+        owner_id: 1
+        group_id: 1
+        owner_type: User
+        notes: "This is a sample note for the secret"
+        urls: # List of URLs associated with the secret (optional)
+          - url: https://myapp.example.com/login
+            id: "454"
+            credential_id: "25"

+ 7 - 0
docs/snippets/beyondtrust-secret.yaml

@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: app-credentials
+type: Opaque
+stringData:
+  password: S3cr3tP@ss

+ 1 - 1
go.mod

@@ -187,7 +187,7 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect
 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
-	github.com/BeyondTrust/go-client-library-passwordsafe v0.23.0 // indirect
+	github.com/BeyondTrust/go-client-library-passwordsafe v0.25.0 // indirect
 	github.com/BurntSushi/toml v1.5.0 // indirect
 	github.com/BurntSushi/toml v1.5.0 // indirect
 	github.com/DelineaXPM/dsv-sdk-go/v2 v2.2.0 // indirect
 	github.com/DelineaXPM/dsv-sdk-go/v2 v2.2.0 // indirect
 	github.com/DelineaXPM/tss-sdk-go/v3 v3.0.0 // indirect
 	github.com/DelineaXPM/tss-sdk-go/v3 v3.0.0 // indirect

+ 2 - 2
go.sum

@@ -119,8 +119,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo
 github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
-github.com/BeyondTrust/go-client-library-passwordsafe v0.23.0 h1:2r+qMuDU92jnFJ61RqaJw812+oTrUH7IVFKG4vV7s8c=
-github.com/BeyondTrust/go-client-library-passwordsafe v0.23.0/go.mod h1:ntgg5j8QRG0XyF8WUTa57T1TwYJOJjerLMCc1XvJO0M=
+github.com/BeyondTrust/go-client-library-passwordsafe v0.25.0 h1:A+ZyF5sigY51kep48CnZezZY4WLzPCk2UnwMrUUTvu4=
+github.com/BeyondTrust/go-client-library-passwordsafe v0.25.0/go.mod h1:ntgg5j8QRG0XyF8WUTa57T1TwYJOJjerLMCc1XvJO0M=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
 github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
 github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
 github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=

+ 3 - 3
providers/v1/beyondtrust/go.mod

@@ -3,12 +3,14 @@ module github.com/external-secrets/external-secrets/providers/v1/beyondtrust
 go 1.25.1
 go 1.25.1
 
 
 require (
 require (
-	github.com/BeyondTrust/go-client-library-passwordsafe v0.23.0
+	github.com/BeyondTrust/go-client-library-passwordsafe v0.25.0
 	github.com/cenkalti/backoff/v4 v4.3.0
 	github.com/cenkalti/backoff/v4 v4.3.0
 	github.com/external-secrets/external-secrets/apis v0.0.0
 	github.com/external-secrets/external-secrets/apis v0.0.0
 	github.com/external-secrets/external-secrets/runtime v0.0.0
 	github.com/external-secrets/external-secrets/runtime v0.0.0
 	github.com/stretchr/testify v1.11.1
 	github.com/stretchr/testify v1.11.1
+	go.uber.org/zap v1.27.0
 	k8s.io/api v0.34.1
 	k8s.io/api v0.34.1
+	k8s.io/apiextensions-apiserver v0.34.1
 	k8s.io/apimachinery v0.34.1
 	k8s.io/apimachinery v0.34.1
 	k8s.io/client-go v0.34.1
 	k8s.io/client-go v0.34.1
 	k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
 	k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
@@ -78,7 +80,6 @@ require (
 	github.com/spf13/pflag v1.0.10 // indirect
 	github.com/spf13/pflag v1.0.10 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	go.uber.org/zap v1.27.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.3 // indirect
 	go.yaml.in/yaml/v2 v2.4.3 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
 	golang.org/x/crypto v0.43.0 // indirect
 	golang.org/x/crypto v0.43.0 // indirect
@@ -94,7 +95,6 @@ require (
 	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/apiextensions-apiserver v0.34.1 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
 	k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
 	sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
 	sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect

+ 2 - 2
providers/v1/beyondtrust/go.sum

@@ -1,7 +1,7 @@
 dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
 dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
 dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
 dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
-github.com/BeyondTrust/go-client-library-passwordsafe v0.23.0 h1:2r+qMuDU92jnFJ61RqaJw812+oTrUH7IVFKG4vV7s8c=
-github.com/BeyondTrust/go-client-library-passwordsafe v0.23.0/go.mod h1:ntgg5j8QRG0XyF8WUTa57T1TwYJOJjerLMCc1XvJO0M=
+github.com/BeyondTrust/go-client-library-passwordsafe v0.25.0 h1:A+ZyF5sigY51kep48CnZezZY4WLzPCk2UnwMrUUTvu4=
+github.com/BeyondTrust/go-client-library-passwordsafe v0.25.0/go.mod h1:ntgg5j8QRG0XyF8WUTa57T1TwYJOJjerLMCc1XvJO0M=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
 github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
 github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
 github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
 github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=

+ 203 - 10
providers/v1/beyondtrust/provider.go

@@ -19,6 +19,7 @@ package beyondtrust
 
 
 import (
 import (
 	"context"
 	"context"
+	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net/url"
 	"net/url"
@@ -26,12 +27,14 @@ import (
 	"time"
 	"time"
 
 
 	auth "github.com/BeyondTrust/go-client-library-passwordsafe/api/authentication"
 	auth "github.com/BeyondTrust/go-client-library-passwordsafe/api/authentication"
+	"github.com/BeyondTrust/go-client-library-passwordsafe/api/entities"
+	v1 "k8s.io/api/core/v1"
+
 	"github.com/BeyondTrust/go-client-library-passwordsafe/api/logging"
 	"github.com/BeyondTrust/go-client-library-passwordsafe/api/logging"
 	managedaccount "github.com/BeyondTrust/go-client-library-passwordsafe/api/managed_account"
 	managedaccount "github.com/BeyondTrust/go-client-library-passwordsafe/api/managed_account"
 	"github.com/BeyondTrust/go-client-library-passwordsafe/api/secrets"
 	"github.com/BeyondTrust/go-client-library-passwordsafe/api/secrets"
 	"github.com/BeyondTrust/go-client-library-passwordsafe/api/utils"
 	"github.com/BeyondTrust/go-client-library-passwordsafe/api/utils"
 	"github.com/cenkalti/backoff/v4"
 	"github.com/cenkalti/backoff/v4"
-	v1 "k8s.io/api/core/v1"
 	ctrl "sigs.k8s.io/controller-runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
@@ -50,6 +53,17 @@ const (
 	errNoSuchKeyFmt         = "no such key in secret: %q"
 	errNoSuchKeyFmt         = "no such key in secret: %q"
 	errInvalidRetrievalPath = "invalid retrieval path. Provide one path, separator and name"
 	errInvalidRetrievalPath = "invalid retrieval path. Provide one path, separator and name"
 	errNotImplemented       = "not implemented"
 	errNotImplemented       = "not implemented"
+
+	usernameFieldName    = "username"
+	folderNameFieldName  = "folder_name"
+	fileNameFieldName    = "file_name"
+	titleFieldName       = "title"
+	descriptionFieldName = "description"
+	ownerIDFieldName     = "owner_id"
+	groupIDFieldName     = "group_id"
+	ownerTypeFieldName   = "owner_type"
+	secretTypeFieldName  = "secret_type"
+	secretTypeCredential = "CREDENTIAL"
 )
 )
 
 
 var (
 var (
@@ -86,7 +100,7 @@ type AuthenticatorInput struct {
 
 
 // Capabilities implements v1beta1.Provider.
 // Capabilities implements v1beta1.Provider.
 func (*Provider) Capabilities() esv1.SecretStoreCapabilities {
 func (*Provider) Capabilities() esv1.SecretStoreCapabilities {
-	return esv1.SecretStoreReadOnly
+	return esv1.SecretStoreReadWrite
 }
 }
 
 
 // Close implements v1beta1.SecretsClient.
 // Close implements v1beta1.SecretsClient.
@@ -104,11 +118,6 @@ func (*Provider) GetSecretMap(_ context.Context, _ esv1.ExternalSecretDataRemote
 	return make(map[string][]byte), errors.New(errNotImplemented)
 	return make(map[string][]byte), errors.New(errNotImplemented)
 }
 }
 
 
-// PushSecret implements v1beta1.SecretsClient.
-func (*Provider) PushSecret(_ context.Context, _ *v1.Secret, _ esv1.PushSecretData) error {
-	return errors.New(errNotImplemented)
-}
-
 // Validate implements v1beta1.SecretsClient.
 // Validate implements v1beta1.SecretsClient.
 func (p *Provider) Validate() (esv1.ValidationResult, error) {
 func (p *Provider) Validate() (esv1.ValidationResult, error) {
 	timeout := 15 * time.Second
 	timeout := 15 * time.Second
@@ -123,9 +132,21 @@ func (p *Provider) Validate() (esv1.ValidationResult, error) {
 }
 }
 
 
 // SecretExists checks if a secret exists in the provider.
 // SecretExists checks if a secret exists in the provider.
-// Currently not implemented for this provider.
-func (*Provider) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
-	return false, errors.New(errNotImplemented)
+func (p *Provider) SecretExists(_ context.Context, pushSecretRef esv1.PushSecretRemoteRef) (bool, error) {
+	logger := logging.NewLogrLogger(&ESOLogger)
+	secretObj, err := secrets.NewSecretObj(p.authenticate, logger, maxFileSecretSizeBytes)
+
+	if err != nil {
+		return false, err
+	}
+
+	_, err = secretObj.SearchSecretByTitleFlow(pushSecretRef.GetRemoteKey())
+
+	if err == nil {
+		return true, nil
+	}
+
+	return false, nil
 }
 }
 
 
 // NewClient this is where we initialize the SecretClient and return it for the controller to use.
 // NewClient this is where we initialize the SecretClient and return it for the controller to use.
@@ -414,3 +435,175 @@ func ProviderSpec() *esv1.SecretStoreProvider {
 func MaintenanceStatus() esv1.MaintenanceStatus {
 func MaintenanceStatus() esv1.MaintenanceStatus {
 	return esv1.MaintenanceStatusMaintained
 	return esv1.MaintenanceStatusMaintained
 }
 }
+
+// PushSecret implements v1beta1.SecretsClient.
+func (p *Provider) PushSecret(_ context.Context, secret *v1.Secret, psd esv1.PushSecretData) error {
+	ESOLogger.Info("Pushing secret to BeyondTrust Password Safe")
+	value, err := esutils.ExtractSecretData(psd, secret)
+
+	if err != nil {
+		return fmt.Errorf("extract secret data failed: %w", err)
+	}
+
+	secretValue := string(value)
+
+	metadata := psd.GetMetadata()
+	data, err := json.Marshal(metadata)
+
+	if err != nil {
+		return fmt.Errorf("Error getting metadata: %w", err)
+	}
+
+	var metaDataObject map[string]interface{}
+	err = json.Unmarshal(data, &metaDataObject)
+	if err != nil {
+		return fmt.Errorf("Error in parameters: %w", err)
+	}
+
+	signAppinResponse, err := p.authenticate.GetPasswordSafeAuthentication()
+	if err != nil {
+		return fmt.Errorf("Error in authentication: %w", err)
+	}
+
+	err = p.CreateSecret(secretValue, metaDataObject, signAppinResponse)
+
+	if err != nil {
+		return fmt.Errorf("Error in creating the secret: %w", err)
+	}
+
+	return nil
+}
+
+// CreateSecret creates a secret in BeyondTrust Password Safe.
+func (p *Provider) CreateSecret(secret string, data map[string]interface{}, signAppinResponse entities.SignAppinResponse) error {
+	logger := logging.NewLogrLogger(&ESOLogger)
+	secretObj, err := secrets.NewSecretObj(p.authenticate, logger, maxFileSecretSizeBytes)
+
+	if err != nil {
+		return err
+	}
+
+	username := utils.GetStringField(data, usernameFieldName, "")
+	folderName := utils.GetStringField(data, folderNameFieldName, "")
+	fileName := utils.GetStringField(data, fileNameFieldName, "")
+	title := utils.GetStringField(data, titleFieldName, "")
+	description := utils.GetStringField(data, descriptionFieldName, "")
+	ownerID := utils.GetIntField(data, ownerIDFieldName, 0)
+	groupID := utils.GetIntField(data, groupIDFieldName, 0)
+	ownerType := utils.GetStringField(data, ownerTypeFieldName, "")
+	secretType := utils.GetStringField(data, secretTypeFieldName, secretTypeCredential)
+
+	var notes string
+	var urls []entities.UrlDetails
+	var ownerDetailsOwnerID []entities.OwnerDetailsOwnerId
+	var ownerDetailsGroupID []entities.OwnerDetailsGroupId
+
+	_, ok := data["notes"]
+	if ok {
+		notes = data["notes"].(string)
+	}
+
+	_, ok = data["urls"]
+	if ok {
+		urls = utils.GetUrlsDetailsList(data)
+	}
+
+	ownerDetailsOwnerID = utils.GetOwnerDetailsOwnerIdList(data, signAppinResponse)
+	ownerDetailsGroupID = utils.GetOwnerDetailsGroupIdList(data, groupID, signAppinResponse)
+
+	secretDetailsConfig := entities.SecretDetailsBaseConfig{
+		Title:       title,
+		Description: description,
+		Urls:        urls,
+		Notes:       notes,
+	}
+
+	var configMap map[string]interface{}
+	switch strings.ToUpper(secretType) {
+	case "CREDENTIAL":
+
+		secretCredentialDetailsConfig30 := entities.SecretCredentialDetailsConfig30{
+			SecretDetailsBaseConfig: secretDetailsConfig,
+			Username:                username,
+			Password:                secret,
+			OwnerId:                 ownerID,
+			OwnerType:               ownerType,
+			Owners:                  ownerDetailsOwnerID,
+		}
+
+		secretCredentialDetailsConfig31 := entities.SecretCredentialDetailsConfig31{
+			SecretDetailsBaseConfig: secretDetailsConfig,
+			Username:                username,
+			Password:                secret,
+			Owners:                  ownerDetailsGroupID,
+		}
+
+		configMap = map[string]interface{}{
+			"3.0": secretCredentialDetailsConfig30,
+			"3.1": secretCredentialDetailsConfig31,
+		}
+
+	case "FILE":
+
+		secretFileDetailsConfig30 := entities.SecretFileDetailsConfig30{
+			SecretDetailsBaseConfig: secretDetailsConfig,
+			FileContent:             secret,
+			FileName:                fileName,
+			OwnerId:                 ownerID,
+			OwnerType:               ownerType,
+			Owners:                  ownerDetailsOwnerID,
+		}
+
+		secretFileDetailsConfig31 := entities.SecretFileDetailsConfig31{
+			SecretDetailsBaseConfig: secretDetailsConfig,
+			FileContent:             secret,
+			FileName:                fileName,
+			Owners:                  ownerDetailsGroupID,
+		}
+
+		configMap = map[string]interface{}{
+			"3.0": secretFileDetailsConfig30,
+			"3.1": secretFileDetailsConfig31,
+		}
+
+	case "TEXT":
+
+		secretTextDetailsConfig30 := entities.SecretTextDetailsConfig30{
+			SecretDetailsBaseConfig: secretDetailsConfig,
+			Text:                    secret,
+			OwnerId:                 ownerID,
+			OwnerType:               ownerType,
+			Owners:                  ownerDetailsOwnerID,
+		}
+
+		secretTextDetailsConfig31 := entities.SecretTextDetailsConfig31{
+			SecretDetailsBaseConfig: secretDetailsConfig,
+			Text:                    secret,
+			Owners:                  ownerDetailsGroupID,
+		}
+
+		configMap = map[string]interface{}{
+			"3.0": secretTextDetailsConfig30,
+			"3.1": secretTextDetailsConfig31,
+		}
+
+	default:
+		return fmt.Errorf("Unknown secret type")
+	}
+
+	secretDetails, exists := configMap[p.authenticate.ApiVersion]
+
+	if !exists {
+		return fmt.Errorf("unsupported API version: %v", &p.authenticate.ApiVersion)
+	}
+
+	_, err = secretObj.CreateSecretFlow(folderName, secretDetails)
+
+	if err != nil {
+		return err
+	}
+
+	ESOLogger.Info("Secret pushed to BeyondTrust Password Safe")
+
+	return nil
+}

+ 376 - 29
providers/v1/beyondtrust/provider_test.go

@@ -21,9 +21,17 @@ import (
 	"net/http"
 	"net/http"
 	"net/http/httptest"
 	"net/http/httptest"
 	"testing"
 	"testing"
+	"time"
+
+	"github.com/BeyondTrust/go-client-library-passwordsafe/api/authentication"
+	"github.com/BeyondTrust/go-client-library-passwordsafe/api/logging"
+	"github.com/BeyondTrust/go-client-library-passwordsafe/api/utils"
+	"github.com/cenkalti/backoff/v4"
+	"go.uber.org/zap"
 
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
-	corev1 "k8s.io/api/core/v1"
+	"github.com/stretchr/testify/require"
+	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/client-go/tools/clientcmd"
 	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
 	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
@@ -33,20 +41,27 @@ import (
 
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 )
 )
 
 
 const (
 const (
-	errTestCase  = "Test case Failed"
-	fakeAPIURL   = "https://example.com:443/BeyondTrust/api/public/v3/"
-	apiKey       = "fakeapikey00fakeapikeydd0000000000065b010f20fakeapikey0000000008700000a93fb5d74fddc0000000000000000000000000000000000000;runas=test_user"
-	clientID     = "12345678-25fg-4b05-9ced-35e7dd5093ae"
-	clientSecret = "12345678-25fg-4b05-9ced-35e7dd5093ae"
+	errTestCase            = "Test case Failed"
+	fakeAPIURL             = "https://example.com:443/BeyondTrust/api/public/v3/"
+	apiKey                 = "fakeapikey00fakeapikeydd0000000000065b010f20fakeapikey0000000008700000a93fb5d74fddc0000000000000000000000000000000000000;runas=test_user"
+	clientID               = "12345678-25fg-4b05-9ced-35e7dd5093ae"
+	clientSecret           = "12345678-25fg-4b05-9ced-35e7dd5093ae"
+	authConnectTokenPath   = "/Auth/connect/token"
+	authSignAppInPath      = "/Auth/SignAppIn"
+	secretsSafeFoldersPath = "/secrets-safe/folders/"
+	secretsSafeSecretsPath = "/secrets-safe/secrets"
 )
 )
 
 
 func createMockPasswordSafeClient(t *testing.T) kubeclient.Client {
 func createMockPasswordSafeClient(t *testing.T) kubeclient.Client {
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.URL.Path {
 		switch r.URL.Path {
-		case "/Auth/SignAppin":
+		case authSignAppInPath:
 			_, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"fake@beyondtrust.com"}`))
 			_, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"fake@beyondtrust.com"}`))
 			if err != nil {
 			if err != nil {
 				t.Error(errTestCase)
 				t.Error(errTestCase)
@@ -58,7 +73,7 @@ func createMockPasswordSafeClient(t *testing.T) kubeclient.Client {
 				t.Error(errTestCase)
 				t.Error(errTestCase)
 			}
 			}
 
 
-		case "/secrets-safe/secrets":
+		case secretsSafeSecretsPath:
 			_, err := w.Write([]byte(`[{"SecretType": "FILE", "Password": "credential_in_sub_3_password","Id": "12345678-07d6-4955-175a-08db047219ce","Title": "credential_in_sub_3"}]`))
 			_, err := w.Write([]byte(`[{"SecretType": "FILE", "Password": "credential_in_sub_3_password","Id": "12345678-07d6-4955-175a-08db047219ce","Title": "credential_in_sub_3"}]`))
 			if err != nil {
 			if err != nil {
 				t.Error(errTestCase)
 				t.Error(errTestCase)
@@ -352,7 +367,7 @@ func TestNewClient(t *testing.T) {
 }
 }
 
 
 func TestLoadConfigSecret_NamespacedStoreCannotCrossNamespace(t *testing.T) {
 func TestLoadConfigSecret_NamespacedStoreCannotCrossNamespace(t *testing.T) {
-	kube := fake.NewClientBuilder().WithObjects(&corev1.Secret{
+	kube := fake.NewClientBuilder().WithObjects(&v1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: "foo",
 			Namespace: "foo",
 			Name:      "creds",
 			Name:      "creds",
@@ -385,31 +400,363 @@ func TestLoadConfigSecret_NamespacedStoreCannotCrossNamespace(t *testing.T) {
 	}
 	}
 }
 }
 
 
-func TestLoadConfigSecret_ClusterStoreCanAccessOtherNamespace(t *testing.T) {
-	kube := fake.NewClientBuilder().WithObjects(&corev1.Secret{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "foo",
-			Name:      "creds",
+func TestPushSecret(t *testing.T) {
+	type testCase struct {
+		name          string
+		serverHandler http.HandlerFunc
+		metadata      apiextensionsv1.JSON
+		expectedError bool
+	}
+
+	tests := []testCase{
+		{
+			name: "successfully pushes credential secret",
+			serverHandler: func(w http.ResponseWriter, r *http.Request) {
+				switch r.URL.Path {
+				case authConnectTokenPath:
+					_, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				case authSignAppInPath:
+					_, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				case secretsSafeFoldersPath:
+					_, err := w.Write([]byte(`[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}]`))
+					if err != nil {
+						t.Error(err)
+					}
+				case "/secrets-safe/folders/cb871861-8b40-4556-820c-1ca6d522adfa/secrets":
+					_, err := w.Write([]byte(`{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				default:
+					http.Error(w, "not found", http.StatusNotFound)
+				}
+			},
+			expectedError: false,
+			metadata: apiextensionsv1.JSON{
+				Raw: []byte(`{
+					"title": "Test Credential",
+					"username": "admin",
+					"description": "Test Credential Secret description",
+					"secret_type": "CREDENTIAL",
+					"folder_name": "folder1"
+				}`),
+			},
 		},
 		},
-		Data: map[string][]byte{
-			"key": []byte("value"),
+		{
+			name: "successfully pushes file secret",
+			serverHandler: func(w http.ResponseWriter, r *http.Request) {
+				switch r.URL.Path {
+				case authConnectTokenPath:
+					_, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				case authSignAppInPath:
+					_, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				case secretsSafeFoldersPath:
+					_, err := w.Write([]byte(`[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}]`))
+					if err != nil {
+						t.Error(err)
+					}
+				case "/secrets-safe/folders/cb871861-8b40-4556-820c-1ca6d522adfa/secrets/file":
+					_, err := w.Write([]byte(`{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				default:
+					http.Error(w, "not found", http.StatusNotFound)
+				}
+			},
+			expectedError: false,
+			metadata: apiextensionsv1.JSON{
+				Raw: []byte(`{
+					"title": "Test File Secret",
+					"username": "admin",
+					"description": "Test File Secret description",
+					"secret_type": "FILE",
+					"folder_name": "folder1",
+					"file_name": "credentials.txt"
+				}`),
+			},
 		},
 		},
-	}).Build()
-
-	ref := &esv1.BeyondTrustProviderSecretRef{
-		SecretRef: &esmeta.SecretKeySelector{
-			Namespace: ptr.To("foo"),
-			Name:      "creds",
-			Key:       "key",
+		{
+			name: "successfully pushes text secret",
+			serverHandler: func(w http.ResponseWriter, r *http.Request) {
+				switch r.URL.Path {
+				case authConnectTokenPath:
+					_, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				case authSignAppInPath:
+					_, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				case secretsSafeFoldersPath:
+					_, err := w.Write([]byte(`[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}]`))
+					if err != nil {
+						t.Error(err)
+					}
+				case "/secrets-safe/folders/cb871861-8b40-4556-820c-1ca6d522adfa/secrets/text":
+					_, err := w.Write([]byte(`{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				default:
+					http.Error(w, "not found", http.StatusNotFound)
+				}
+			},
+			expectedError: false,
+			metadata: apiextensionsv1.JSON{
+				Raw: []byte(`{
+					"title": "Test Text Secret",
+					"username": "admin",
+					"description": "Test File Secret description",
+					"secret_type": "TEXT",
+					"folder_name": "folder1"
+				}`),
+			},
+		},
+		{
+			name: "successfully pushes text secret - 404 error",
+			serverHandler: func(w http.ResponseWriter, r *http.Request) {
+				switch r.URL.Path {
+				case authConnectTokenPath:
+					_, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				case authSignAppInPath:
+					_, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				case secretsSafeFoldersPath:
+					_, err := w.Write([]byte(`[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}]`))
+					if err != nil {
+						t.Error(err)
+					}
+				default:
+					http.Error(w, "not found", http.StatusNotFound)
+				}
+			},
+			expectedError: true,
+			metadata: apiextensionsv1.JSON{
+				Raw: []byte(`{
+					"title": "Test Text Secret",
+					"username": "admin",
+					"description": "Test File Secret description",
+					"secret_type": "TEXT",
+					"folder_name": "folder1"
+				}`),
+			},
+		},
+		{
+			name: "fails authentication",
+			serverHandler: func(w http.ResponseWriter, r *http.Request) {
+				http.Error(w, "unauthorized", http.StatusUnauthorized)
+			},
+			expectedError: true,
 		},
 		},
 	}
 	}
 
 
-	// ClusterSecretStore may access across namespaces when a namespace is provided in the selector.
-	val, err := loadConfigSecret(t.Context(), ref, kube, "unrelated-namespace", esv1.ClusterSecretStoreKind)
-	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fakeServer := httptest.NewServer(tt.serverHandler)
+			defer fakeServer.Close()
+
+			logger, err := zap.NewDevelopment()
+
+			if err != nil {
+				t.Error(err)
+			}
+
+			zapLogger := logging.NewZapLogger(logger)
+
+			clientTimeout := 30
+			verifyCa := true
+			retryMaxElapsedTimeMinutes := 2
+
+			backoffDefinition := backoff.NewExponentialBackOff()
+			backoffDefinition.InitialInterval = 1 * time.Second
+			backoffDefinition.MaxElapsedTime = time.Duration(retryMaxElapsedTimeMinutes) * time.Second
+			backoffDefinition.RandomizationFactor = 0.5
+
+			httpClientObj, err := utils.GetHttpClient(clientTimeout, verifyCa, "", "", zapLogger)
+
+			if err != nil {
+				t.Error(err)
+			}
+
+			params := authentication.AuthenticationParametersObj{
+				HTTPClient:                 *httpClientObj,
+				BackoffDefinition:          backoffDefinition,
+				EndpointURL:                fakeServer.URL,
+				APIVersion:                 "3.1",
+				ClientID:                   "fake_clinet_id",
+				ClientSecret:               "fake_client_secret",
+				Logger:                     zapLogger,
+				RetryMaxElapsedTimeSeconds: 30,
+			}
+
+			authObj, err := authentication.Authenticate(params)
+			require.NoError(t, err)
+
+			p := &Provider{authenticate: *authObj}
+
+			secret := &v1.Secret{
+				Data: map[string][]byte{"password": []byte("supersecret")},
+			}
+
+			metadataJSON := &tt.metadata
+
+			psd := v1alpha1.PushSecretData{
+				Match: v1alpha1.PushSecretMatch{
+					SecretKey: "password",
+					RemoteRef: v1alpha1.PushSecretRemoteRef{
+						RemoteKey: "test-credential",
+					},
+				},
+				Metadata: metadataJSON,
+			}
+
+			err = p.PushSecret(context.Background(), secret, psd)
+
+			if tt.expectedError {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+			}
+		})
 	}
 	}
-	if val != "value" {
-		t.Fatalf("expected valueA, got %q", val)
+}
+
+func TestSecretExists(t *testing.T) {
+	type testCase struct {
+		name             string
+		serverHandler    http.HandlerFunc
+		expectedExisting bool
+	}
+
+	tests := []testCase{
+		{
+			name: "Secret Exists",
+			serverHandler: func(w http.ResponseWriter, r *http.Request) {
+				switch r.URL.Path {
+				case authConnectTokenPath:
+					_, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				case authSignAppInPath:
+					_, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				case secretsSafeSecretsPath:
+					_, err := w.Write([]byte(`[{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title"}]`))
+					if err != nil {
+						t.Error(err)
+					}
+				default:
+					http.Error(w, "not found", http.StatusNotFound)
+				}
+			},
+			expectedExisting: true,
+		},
+		{
+			name: "Secret does not Exist",
+			serverHandler: func(w http.ResponseWriter, r *http.Request) {
+				switch r.URL.Path {
+				case authConnectTokenPath:
+					_, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				case authSignAppInPath:
+					_, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
+					if err != nil {
+						t.Error(err)
+					}
+				case secretsSafeSecretsPath:
+					http.Error(w, "secret was not found", http.StatusNotFound)
+				default:
+					http.Error(w, "not found", http.StatusNotFound)
+				}
+			},
+			expectedExisting: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fakeServer := httptest.NewServer(tt.serverHandler)
+			defer fakeServer.Close()
+
+			logger, err := zap.NewDevelopment()
+
+			if err != nil {
+				t.Error(err)
+			}
+
+			zapLogger := logging.NewZapLogger(logger)
+
+			clientTimeout := 30
+			verifyCa := true
+			retryMaxElapsedTimeMinutes := 2
+
+			backoffDefinition := backoff.NewExponentialBackOff()
+			backoffDefinition.InitialInterval = 1 * time.Second
+			backoffDefinition.MaxElapsedTime = time.Duration(retryMaxElapsedTimeMinutes) * time.Second
+			backoffDefinition.RandomizationFactor = 0.5
+
+			httpClientObj, err := utils.GetHttpClient(clientTimeout, verifyCa, "", "", zapLogger)
+
+			if err != nil {
+				t.Error(err)
+			}
+
+			params := authentication.AuthenticationParametersObj{
+				HTTPClient:                 *httpClientObj,
+				BackoffDefinition:          backoffDefinition,
+				EndpointURL:                fakeServer.URL,
+				APIVersion:                 "3.1",
+				ClientID:                   "fake_clinet_id",
+				ClientSecret:               "fake_client_secret",
+				Logger:                     zapLogger,
+				RetryMaxElapsedTimeSeconds: 30,
+			}
+
+			authObj, err := authentication.Authenticate(params)
+			require.NoError(t, err)
+
+			p := &Provider{authenticate: *authObj}
+
+			remoteRef := v1alpha1.PushSecretRemoteRef{
+				RemoteKey: "test-credential",
+			}
+
+			exists, err := p.SecretExists(context.Background(), remoteRef)
+
+			if err != nil {
+				t.Error(err)
+			}
+
+			if tt.expectedExisting {
+				assert.True(t, exists)
+			} else {
+				assert.False(t, exists)
+			}
+		})
 	}
 	}
 }
 }