Просмотр исходного кода

feat(providers): Implement PushSecret for Delinea Secret Server (#6031)

Co-authored-by: Gergely Bräutigam <gergely.brautigam@sap.com>
Co-authored-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Marc Singer 2 месяцев назад
Родитель
Сommit
93b430936a

+ 1 - 1
docs/introduction/stability-support.md

@@ -119,7 +119,7 @@ The following table show the support for features across different providers.
 | CyberArk Secrets Manager  |      x       |      x       |                      |                         |        x         |             |                             |
 | Delinea                   |      x       |              |                      |                         |        x         |             |                             |
 | Beyondtrust               |      x       |              |                      |                         |        x         |             |                             |
-| SecretServer              |      x       |              |                      |                         |        x         |             |                             |
+| SecretServer              |      x       |              |                      |                         |        x         |      x      |              x              |
 | Pulumi ESC                |      x       |              |                      |                         |        x         |             |                             |
 | Passbolt                  |      x       |              |                      |                         |        x         |             |                             |
 | Infisical                 |      x       |              |                      |            x            |        x         |             |                             |

+ 153 - 15
docs/provider/secretserver.md

@@ -35,18 +35,22 @@ spec:
 
 ### Referencing Secrets
 
-Secrets may be referenced by:
->Secret ID<br />
-Secret Name<br />
-Secret Path (/FolderName/SecretName)<br />
+Secrets can be referenced using four different key formats in the `remoteRef.key` field:
 
-Please note if using the secret name or path,
-the name field must not contain spaces or control characters.<br />
-If multiple secrets are found, *`only the first found secret will be returned`*.
+| Format | Example | Description |
+|--------|---------|-------------|
+| Secret ID | `52622` | Numeric ID of the secret. Always unambiguous. |
+| Secret Name | `my-secret` | Name of the secret. If multiple secrets share the same name across different folders, the first match is returned. |
+| Secret Path | `/FolderName/SecretName` | Full folder path including the secret name. Uniquely identifies a secret across folders. |
+| Folder-scoped Name | `folderId:73/my-secret` | Name-based lookup scoped to a specific folder ID. Use this when multiple secrets share the same name in different folders and you need to target a specific one. |
 
-Please note: `Retrieving a specific version of a secret is not yet supported.`
+**Notes:**
 
-Note that because all Secret-Server/Platform secrets are JSON objects, you must specify the `remoteRef.property`
+- If using the secret name or path, the name must not contain spaces or control characters.
+- Retrieving a specific version of a secret is not yet supported.
+- The **folder-scoped name** format (`folderId:<id>/<name>`) is particularly important when using `PushSecret` with `deletionPolicy: Delete`, because the deletion and existence-check operations need to identify the correct secret without access to metadata. See [Pushing Secrets](#pushing-secrets) for details.
+
+Because all Secret-Server/Platform secrets are JSON objects, you must specify the `remoteRef.property`
 in your ExternalSecret configuration.<br />
 You can access nested values or arrays using [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md).
 
@@ -118,18 +122,18 @@ spec:
     - secretKey: SecretServerValue  # Key in the Kubernetes Secret
       remoteRef:
         key: "/secretFolder/secretname"  # Path format: /<Folder>/<SecretName>
-        property: ""                    # Optional: use gjson syntax to extract a specific field
+        property: ""                    # Optional: matched against field Slug/FieldName first, then gjson on Items.0.ItemValue as fallback
 ```
 
 #### Notes:
 
 The path must exactly match the folder and secret name in Secret-Server/Platform.
 If multiple secrets with the same name exist in different folders, the path helps to uniquely identify the correct one.
-You can still use property to extract values from JSON-formatted secrets or omit it to retrieve the entire secret.
+You can still use property to match fields by Slug/FieldName, extract values from JSON-formatted secrets via gjson, or omit it to retrieve the entire secret.
 
 ### Preparing your secret
-You can either retrieve your entire secret or you can use a JSON formatted string
-stored in your secret located at Items[0].ItemValue to retrieve a specific value.<br />
+You can either retrieve your entire secret, match a field by its Slug or FieldName, or use a JSON formatted string
+stored in your secret located at Items[0].ItemValue to retrieve a specific value using gjson syntax.<br />
 See example JSON secret below.
 
 #### Examples
@@ -195,9 +199,9 @@ returns: The entire secret in JSON format as displayed below
 }
 ```
 
-### Referencing Secrets in multiple Items secrets
+### Referencing Secrets by Field Name or Slug
 
-If there is more then one Item in the secret, it supports to retrieve them (all Item.\*.ItemValue) looking up by Item.\*.FieldName or Item.\*.Slug, instead of the above behaviour to use gjson only on the first item Items.0.ItemValue only.
+When `property` is set, the provider first tries to match it against each field's `Slug` or `FieldName` and returns the corresponding `ItemValue`. This works for secrets with any number of fields. If no field matches, it falls back to treating the first field's `ItemValue` as JSON and extracting the property using gjson syntax (supporting nested paths like `"books.1"`).
 
 #### Examples
 
@@ -272,3 +276,137 @@ returns: The entire secret in JSON format as displayed below
   ]
 }
 ```
+
+### Pushing Secrets
+
+The Delinea Secret-Server/Platform provider supports pushing secrets from Kubernetes back to your Secret Server instance using the `PushSecret` resource. You can both create new secrets and update existing ones.
+
+#### Remote Key Formats for PushSecret
+
+When using `PushSecret`, the `remoteRef.remoteKey` field determines how the provider identifies
+the target secret in Secret Server. The same key formats described in [Referencing Secrets](#referencing-secrets) apply here:
+
+| Format | Example | When to Use |
+|--------|---------|-------------|
+| Secret ID | `52622` | Updating an existing secret by its numeric ID. |
+| Secret Name | `my-secret` | Simple environments where secret names are unique across all folders. |
+| Secret Path | `/FolderName/SecretName` | When you know the full folder path of the secret. |
+| Folder-scoped Name | `folderId:73/my-secret` | **Recommended for new secrets.** Ensures all operations (push, delete, existence check) target the correct folder. |
+
+**Why the folder-scoped name format matters:**
+
+The `PushSecret` controller performs three distinct operations on secrets: **push** (create/update),
+**delete**, and **existence check**. While the push operation has access to the `metadata` field
+(which can carry a `folderId`), the delete and existence-check operations only receive the
+`remoteKey` and `property` — they do **not** have access to metadata.
+
+This means that if you use a plain secret name like `my-secret` and multiple secrets with that name
+exist in different folders, the delete and existence-check operations cannot distinguish between them
+and will act on the **first match** returned by the API.
+
+By using the `folderId:<id>/<name>` format (e.g., `folderId:73/my-secret`), the folder ID is
+encoded directly in the key and is available to **all** operations, ensuring consistent behavior.
+
+**Precedence rule:** If both a `folderId` in the `remoteKey` and a `folderId` in the metadata are
+specified, the value from the `remoteKey` takes precedence for lookups. The metadata `folderId` and
+`secretTemplateId` are still required when **creating** a new secret (they tell the API which folder
+and template to use for the new secret).
+
+#### Requirements for Creating New Secrets
+
+When creating a **new** secret in Secret Server, you must provide a `folderId` and a `secretTemplateId`. These are passed as `metadata` in the `PushSecret` spec:
+
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: push-secret-example
+spec:
+  refreshInterval: 1h
+  secretStoreRefs:
+    - name: secret-server-store
+      kind: SecretStore
+  selector:
+    secret:
+      name: my-k8s-secret
+  data:
+    - match:
+        secretKey: username
+        remoteRef:
+          remoteKey: "folderId:73/my-new-secret" # Folder-scoped name ensures correct matching for all operations
+          property: username # Maps to the 'Username' field/slug in Secret Server
+      metadata:
+        apiVersion: kubernetes.external-secrets.io/v1alpha1
+        kind: PushSecretMetadata
+        spec:
+          folderId: 73 # Required for new secrets: folder to create the secret in
+          secretTemplateId: 6098 # Required for new secrets: template to use
+    - match:
+        secretKey: password
+        remoteRef:
+          remoteKey: "folderId:73/my-new-secret"
+          property: password # Maps to the 'Password' field/slug in Secret Server
+      metadata:
+        apiVersion: kubernetes.external-secrets.io/v1alpha1
+        kind: PushSecretMetadata
+        spec:
+          folderId: 73
+          secretTemplateId: 6098
+```
+
+> **Note:** The `folderId` in the `remoteKey` (`folderId:73/...`) is used when **looking up** the
+> secret (for push, delete, and existence checks). The `folderId` and `secretTemplateId` in
+> `metadata` are used when **creating** a new secret via the Secret Server API.
+
+#### Updating Existing Secrets
+
+When updating an existing secret, you do not strictly need the `folderId` or `secretTemplateId` metadata, as the provider will fetch the existing secret by its name or ID to update the corresponding fields.
+
+However, if multiple secrets share the same name across different folders, you should use either the
+`folderId:<id>/<name>` format, a path-based key, or a numeric ID to ensure the correct secret is
+updated. Using a plain name will update the **first match** returned by the API.
+
+#### Deletion Behavior
+
+The `PushSecret` resource allows you to configure what happens to the remote secret in Secret Server when the `PushSecret` itself is deleted, via the `PushSecret.spec.deletionPolicy` field. Supported values are:
+- `Retain`: (Default) The remote secret is left intact in Secret Server when the `PushSecret` is deleted.
+- `Delete`: The provider will attempt to delete the remote secret from Secret Server when the `PushSecret` is removed.
+
+When `Delete` is specified, the deletion operation is idempotent; if the secret has already been removed or cannot be found, the provider will safely ignore the error and proceed.
+
+**Important:** The deletion operation does **not** have access to `metadata`. If your Secret Server
+has multiple secrets with the same name in different folders and you use `deletionPolicy: Delete`,
+you **must** use a key format that uniquely identifies the secret — either `folderId:<id>/<name>`,
+a full path (`/Folder/SecretName`), or a numeric ID. Using a plain name risks deleting the wrong
+secret.
+
+#### Pushing Without a Property
+
+If you omit `property` from the `remoteRef`, the provider writes the value selected by `data.match.secretKey` (e.g., the content stored under the `config` key in your Kubernetes Secret) into the **first** field of the Secret Server secret. This is useful when your secret value is a single JSON payload that you want to store in a text field like `Data` or `Notes`.
+
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: push-secret-json-example
+spec:
+  refreshInterval: 1h
+  secretStoreRefs:
+    - name: secret-server-store
+      kind: SecretStore
+  selector:
+    secret:
+      name: my-k8s-json-secret
+  data:
+    - match:
+        secretKey: config # The key in your k8s secret whose value will be pushed
+        remoteRef:
+          remoteKey: "folderId:73/my-new-json-secret"
+          # property is omitted: the value is stored in the first template field
+      metadata:
+        apiVersion: kubernetes.external-secrets.io/v1alpha1
+        kind: PushSecretMetadata
+        spec:
+          folderId: 73
+          secretTemplateId: 6098
+```

+ 429 - 50
providers/v1/secretserver/client.go

@@ -20,8 +20,10 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"strconv"
 	"strings"
+	"unicode/utf8"
 
 	"github.com/DelineaXPM/tss-sdk-go/v3/server"
 	"github.com/tidwall/gjson"
@@ -29,32 +31,134 @@ import (
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	"github.com/external-secrets/external-secrets/runtime/esutils"
+	"github.com/external-secrets/external-secrets/runtime/esutils/metadata"
 )
 
+const (
+	// errMsgNoMatchingSecrets is returned by getSecretByName when a search returns zero results.
+	// This preserves backward compatibility with the original error message used
+	// before the PushSecret feature was added.
+	errMsgNoMatchingSecrets = "unable to retrieve secret at this time"
+	// errMsgNotFound is returned when a secret is not found in a specific folder.
+	errMsgNotFound = "not found"
+	// errMsgAmbiguousName is returned by lookupSecretStrict when a plain name
+	// matches multiple secrets across folders and no folder scope is provided.
+	errMsgAmbiguousName = "multiple secrets found with the same name across different folders; use the 'folderId:<id>/<name>' key format, a path-based key, or a numeric ID to disambiguate"
+
+	// folderPrefix is the prefix used to encode a folder ID in a remote key.
+	// Format: "folderId:<id>/<name>" (e.g. "folderId:73/my-secret").
+	folderPrefix = "folderId:"
+)
+
+// isNotFoundError checks if an error indicates a secret was not found.
+// The TSS SDK (v3) returns all errors as plain fmt.Errorf strings with format
+// "<StatusCode> <StatusText>: <body>" — no typed/sentinel errors.
+//
+// This function uses case-insensitive substring matching with explicit exclusions
+// for false-positive patterns produced by our own code (e.g. "not found in secret"
+// from updateSecret or "not found in secret template" from createSecret) and
+// non-404 HTTP errors that happen to contain "not found" in their body.
+func isNotFoundError(err error) bool {
+	if err == nil {
+		return false
+	}
+	msg := strings.ToLower(err.Error())
+
+	// Our own sentinel from getSecretByName / getSecretByNameStrict.
+	if strings.Contains(msg, errMsgNoMatchingSecrets) {
+		return true
+	}
+
+	// SDK HTTP 404 responses start with "404 ".
+	if strings.HasPrefix(msg, "404 ") {
+		return true
+	}
+
+	// Generic "not found" substring — but exclude false positives.
+	if strings.Contains(msg, errMsgNotFound) {
+		// Patterns like "field X not found in secret" or "field X not found in secret template"
+		// are field-level errors, not secret-not-found errors.
+		if strings.Contains(msg, "not found in secret") {
+			return false
+		}
+		// Exclude non-404 HTTP errors that happen to contain "not found" in the body
+		// (e.g. "401 Unauthorized: user not found"). The SDK formats all HTTP errors
+		// as "<StatusCode> <StatusText>: <body>", so any message starting with a
+		// 3-digit code followed by a space is an HTTP error.
+		if len(msg) >= 4 && msg[3] == ' ' && msg[0] >= '0' && msg[0] <= '9' && msg[1] >= '0' && msg[1] <= '9' && msg[2] >= '0' && msg[2] <= '9' {
+			return false // non-404 HTTP error (404 was already handled above)
+		}
+		return true
+	}
+
+	return false
+}
+
+// parseFolderPrefix extracts a folder ID and secret name from a key with the
+// format "folderId:<id>/<name>".  If the key does not match the prefix format,
+// it returns (0, key, false) so callers can fall through to other resolution
+// strategies.
+func parseFolderPrefix(key string) (folderID int, name string, hasFolderPrefix bool) {
+	if !strings.HasPrefix(key, folderPrefix) {
+		return 0, key, false
+	}
+
+	rest := strings.TrimPrefix(key, folderPrefix) // "<id>/<name>"
+
+	slashIdx := strings.Index(rest, "/") //nolint:modernize // Need index of first slash to separate folder ID from name
+	if slashIdx < 0 {
+		// "folderId:73" with no slash/name — treat as not having the prefix.
+		return 0, key, false
+	}
+
+	idStr := rest[:slashIdx]
+	secretName := rest[slashIdx+1:]
+
+	id, err := strconv.Atoi(idStr)
+	if err != nil || id <= 0 {
+		// Non-numeric or non-positive folder ID — treat as not having the prefix.
+		return 0, key, false
+	}
+
+	if secretName == "" {
+		// "folderId:73/" with empty name — treat as not having the prefix.
+		return 0, key, false
+	}
+
+	return id, secretName, true
+}
+
+// PushSecretMetadataSpec contains metadata information for pushing secrets to Delinea Secret Server.
+type PushSecretMetadataSpec struct {
+	FolderID         int `json:"folderId"`
+	SecretTemplateID int `json:"secretTemplateId"`
+}
+
 type client struct {
 	api secretAPI
 }
 
 var _ esv1.SecretsClient = &client{}
 
-// GetSecret supports two types:
-//  1. Get the secrets using the secret ID in ref.key i.e. key: 53974
-//  2. Get the secret using the secret "name" i.e. key: "secretNameHere"
+// GetSecret supports several lookup modes:
+//  1. Get the secret using the secret ID in ref.Key (e.g. key: 53974).
+//  2. Get the secret using the secret "name" (e.g. key: "secretNameHere").
 //     - Secret names must not contain spaces.
-//     - If using the secret "name" and multiple secrets are found ...
+//     - If using the secret "name" and multiple secrets are found,
 //     the first secret in the array will be the secret returned.
-//  3. get the full secret as json-encoded value
-//     by leaving the ref.Property empty.
-//  4. get a specific value by using a key from the json formatted secret in Items.0.ItemValue.
-//     Nested values are supported by specifying a gjson expression
+//  3. Get the full secret as a JSON-encoded value by leaving ref.Property empty.
+//  4. Get a specific value by using a key from the JSON-formatted secret in
+//     Items.0.ItemValue via gjson (supports nested paths like "server.1").
+//     If the first field's ItemValue is not valid JSON or the gjson path
+//     does not match, fall back to matching ref.Property against each field's
+//     Slug or FieldName (useful for multi-field secrets).
 func (c *client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
 	secret, err := c.getSecret(ctx, ref)
 	if err != nil {
 		return nil, err
 	}
-	// Return nil if secret contains no fields
-	if secret.Fields == nil {
-		return nil, nil
+	if len(secret.Fields) == 0 {
+		return nil, errors.New("secret contains no fields")
 	}
 	jsonStr, err := json.Marshal(secret)
 	if err != nil {
@@ -66,45 +170,211 @@ func (c *client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemot
 		return jsonStr, nil
 	}
 
-	// extract first "field" i.e. Items.0.ItemValue, data from secret using gjson
+	// Primary path: extract ref.Property from the first field's ItemValue via gjson.
+	// This preserves backward compatibility with the original single-field JSON blob pattern.
 	val := gjson.Get(string(jsonStr), "Items.0.ItemValue")
 	if val.Exists() && gjson.Valid(val.String()) {
-		// extract specific value from data directly above using gjson
 		out := gjson.Get(val.String(), ref.Property)
 		if out.Exists() {
 			return []byte(out.String()), nil
 		}
 	}
 
-	// More general case Fields is an array in DelineaXPM/tss-sdk-go/v3/server
-	// https://github.com/DelineaXPM/tss-sdk-go/blob/571e5674a8103031ad6f873453db27959ec1ca67/server/secret.go#L23
-	secretMap := make(map[string]string)
+	// Fallback: match ref.Property against field Slug or FieldName.
+	// This supports multi-field secrets where fields are accessed by name.
 	for index := range secret.Fields {
-		secretMap[secret.Fields[index].FieldName] = secret.Fields[index].ItemValue
-		secretMap[secret.Fields[index].Slug] = secret.Fields[index].ItemValue
+		if secret.Fields[index].Slug == ref.Property || secret.Fields[index].FieldName == ref.Property {
+			return []byte(secret.Fields[index].ItemValue), nil
+		}
 	}
 
-	out, ok := secretMap[ref.Property]
-	if !ok {
-		return nil, esv1.NoSecretError{}
+	return nil, esv1.NoSecretError{}
+}
+
+// PushSecret creates or updates a secret in Delinea Secret Server.
+func (c *client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
+	if data.GetRemoteKey() == "" {
+		return errors.New("remote key must be defined")
 	}
 
-	return []byte(out), nil
+	value, err := esutils.ExtractSecretData(data, secret)
+	if err != nil {
+		return fmt.Errorf("failed to extract secret data: %w", err)
+	}
+
+	if !utf8.Valid(value) {
+		return errors.New("secret value is not valid UTF-8")
+	}
+
+	meta, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](data.GetMetadata())
+	if err != nil {
+		return fmt.Errorf("failed to parse metadata: %w", err)
+	}
+
+	// Resolve the effective folder ID for both lookups AND creation.
+	// A folderId encoded in the remoteKey takes precedence over metadata
+	// (delete and existence-check never see metadata, so the prefix must win
+	// to keep lookup and creation consistent).
+	folderID := 0
+	if meta != nil {
+		folderID = meta.Spec.FolderID
+	}
+	if prefixFolderID, _, ok := parseFolderPrefix(data.GetRemoteKey()); ok {
+		folderID = prefixFolderID
+	}
+
+	existingSecret, err := c.findExistingSecret(ctx, data.GetRemoteKey(), folderID)
+	if err != nil {
+		if !isNotFoundError(err) {
+			return fmt.Errorf("failed to get secret: %w", err)
+		}
+		existingSecret = nil
+	}
+
+	if existingSecret != nil {
+		// Update existing secret
+		return c.updateSecret(existingSecret, data.GetProperty(), string(value))
+	}
+
+	if meta == nil || meta.Spec.SecretTemplateID <= 0 {
+		return errors.New("folderId and secretTemplateId must be provided in metadata to create a new secret")
+	}
+
+	// Use the effective folderID (prefix-overridden or metadata-supplied) for creation.
+	if folderID <= 0 {
+		return errors.New("folderId and secretTemplateId must be provided in metadata to create a new secret")
+	}
+
+	createSpec := meta.Spec
+	createSpec.FolderID = folderID
+	return c.createSecret(data.GetRemoteKey(), data.GetProperty(), string(value), createSpec)
 }
 
-// PushSecret not supported at this time.
-func (c *client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1.PushSecretData) error {
-	return errors.New("pushing secrets is not supported by Secret Server at this time")
+// updateSecret updates an existing secret in Delinea Secret Server.
+func (c *client) updateSecret(secret *server.Secret, property, value string) error {
+	if property == "" {
+		// If property is empty, put the JSON value in the first field, matching GetSecretMap logic
+		if len(secret.Fields) > 0 {
+			secret.Fields[0].ItemValue = value
+		} else {
+			return errors.New("secret has no fields to update")
+		}
+	} else {
+		found := false
+		for i, field := range secret.Fields {
+			if field.Slug == property || field.FieldName == property {
+				secret.Fields[i].ItemValue = value
+				found = true
+				break
+			}
+		}
+		if !found {
+			return fmt.Errorf("field %s not found in secret", property)
+		}
+	}
+
+	_, err := c.api.UpdateSecret(*secret)
+	if err != nil {
+		return fmt.Errorf("failed to update secret: %w", err)
+	}
+	return nil
 }
 
-// DeleteSecret not supported at this time.
-func (c *client) DeleteSecret(_ context.Context, _ esv1.PushSecretRemoteRef) error {
-	return errors.New("deleting secrets is not supported by Secret Server at this time")
+// createSecret creates a new secret in Delinea Secret Server.
+// Only the targeted field is populated; other required template fields
+// may cause an API error.
+func (c *client) createSecret(name, property, value string, meta PushSecretMetadataSpec) error {
+	template, err := c.api.SecretTemplate(meta.SecretTemplateID)
+	if err != nil {
+		return fmt.Errorf("failed to get secret template: %w", err)
+	}
+
+	if strings.HasSuffix(name, "/") {
+		return fmt.Errorf("invalid secret name %q: name must not be empty or end with a trailing slash", name)
+	}
+
+	// Strip the "folderId:<id>/" prefix if present so the secret is created
+	// with just the plain name.  The folder is already specified in meta.FolderID.
+	if _, stripped, ok := parseFolderPrefix(name); ok {
+		name = stripped
+		// After prefix stripping, the name should be a simple name (no slashes).
+		// The folderId format is "folderId:<id>/<name>", not "folderId:<id>/<path>".
+		if strings.Contains(name, "/") {
+			return fmt.Errorf("invalid secret name %q in folderId prefix: name must not contain path separators", name)
+		}
+	}
+
+	// For path-based keys (e.g. "/Folder/SubFolder/SecretName"), extract the
+	// basename. The folder structure is controlled by meta.FolderID.
+	normalizedName := strings.TrimPrefix(name, "/")
+	if strings.Contains(normalizedName, "/") {
+		parts := strings.Split(normalizedName, "/")
+		normalizedName = parts[len(parts)-1]
+	}
+
+	newSecret := server.Secret{
+		Name:             normalizedName,
+		FolderID:         meta.FolderID,
+		SecretTemplateID: meta.SecretTemplateID,
+		Fields:           make([]server.SecretField, 0),
+	}
+
+	if property == "" {
+		// No property specified: use the first template field.
+		if len(template.Fields) == 0 {
+			return errors.New("secret template has no fields")
+		}
+		newSecret.Fields = append(newSecret.Fields, server.SecretField{
+			FieldID:   template.Fields[0].SecretTemplateFieldID,
+			ItemValue: value,
+		})
+	} else {
+		// Use the field matching the specified property.
+		fieldID, found := findTemplateFieldID(template, property)
+		if !found {
+			return fmt.Errorf("field %s not found in secret template", property)
+		}
+		newSecret.Fields = append(newSecret.Fields, server.SecretField{
+			FieldID:   fieldID,
+			ItemValue: value,
+		})
+	}
+
+	_, err = c.api.CreateSecret(newSecret)
+	if err != nil {
+		return fmt.Errorf("failed to create secret: %w", err)
+	}
+	return nil
+}
+
+// DeleteSecret deletes a secret in Delinea Secret Server.
+func (c *client) DeleteSecret(_ context.Context, ref esv1.PushSecretRemoteRef) error {
+	secret, err := c.lookupSecretStrict(ref.GetRemoteKey())
+	if err != nil {
+		// If already deleted/not found, ignore
+		if isNotFoundError(err) {
+			return nil
+		}
+		return fmt.Errorf("failed to get secret for deletion: %w", err)
+	}
+
+	err = c.api.DeleteSecret(secret.ID)
+	if err != nil {
+		return fmt.Errorf("failed to delete secret: %w", err)
+	}
+	return nil
 }
 
-// SecretExists not supported at this time.
-func (c *client) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
-	return false, errors.New("not implemented")
+// SecretExists checks if a secret exists in Delinea Secret Server.
+func (c *client) SecretExists(_ context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
+	_, err := c.lookupSecretStrict(ref.GetRemoteKey())
+	if err != nil {
+		if isNotFoundError(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("failed to check if secret exists: %w", err)
+	}
+	return true, nil
 }
 
 // Validate not supported at this time.
@@ -120,7 +390,7 @@ func (c *client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRe
 		return nil, err
 	}
 	// Ensure secret has fields before indexing into them
-	if secret.Fields == nil || len(secret.Fields) == 0 {
+	if len(secret.Fields) == 0 {
 		return nil, errors.New("secret contains no fields")
 	}
 
@@ -143,43 +413,152 @@ func (c *client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRe
 	return data, nil
 }
 
-// GetAllSecrets not supported at this time.
+// GetAllSecrets is not supported. The tss-sdk-go v3 SDK search is hard-capped
+// at 30 results with no pagination, no tag filtering, and no folder enumeration.
 func (c *client) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
-	return nil, errors.New("getting all secrets is not supported by Delinea Secret Server at this time")
+	return nil, errors.New("getting all secrets is not supported by Delinea Secret Server")
 }
 
 func (c *client) Close(context.Context) error {
 	return nil
 }
 
-// getSecret retrieves the secret referenced by ref from the Vault API.
+// getSecret retrieves the secret referenced by ref from the Secret Server API.
 func (c *client) getSecret(_ context.Context, ref esv1.ExternalSecretDataRemoteRef) (*server.Secret, error) {
 	if ref.Version != "" {
 		return nil, errors.New("specifying a version is not supported")
 	}
 
-	// If the ref.Key looks like a full path (starts with "/"), fetch by path.
-	// Example: "/Folder/Subfolder/SecretName"
-	if strings.HasPrefix(ref.Key, "/") {
-		s, err := c.api.SecretByPath(ref.Key)
-		if err != nil {
+	return c.lookupSecret(ref.Key, 0)
+}
+
+// findExistingSecret looks up a secret for PushSecret's find-or-create logic.
+// Unlike getSecret (used for reads), this refuses ambiguous plain-name matches
+// when no folder scope is available, matching the safety behavior of DeleteSecret
+// and SecretExists.
+func (c *client) findExistingSecret(_ context.Context, key string, folderID int) (*server.Secret, error) {
+	// When a folder scope is available (either from prefix or metadata),
+	// the lookup is unambiguous — use the regular (non-strict) resolver.
+	if folderID > 0 {
+		return c.lookupSecret(key, folderID)
+	}
+	// No folder scope: use strict lookup to reject ambiguous plain names.
+	return c.lookupSecretStrict(key)
+}
+
+// lookupSecret resolves a secret by path ("/..."), numeric ID, folder-scoped
+// name ("folderId:<id>/<name>"), or plain name.
+// The folderID scopes name-based lookups (0 = any folder).  A folder prefix
+// encoded in the key takes precedence over the folderID argument.
+func (c *client) lookupSecret(key string, folderID int) (*server.Secret, error) {
+	// 1. Folder-scoped prefix: "folderId:<id>/<name>" — override folderID and
+	//    resolve by name within the specified folder.
+	if prefixFolderID, name, ok := parseFolderPrefix(key); ok {
+		return c.getSecretByName(name, prefixFolderID)
+	}
+
+	// 2. Path-based key: fully qualified, no disambiguation needed.
+	if strings.HasPrefix(key, "/") {
+		return c.api.SecretByPath(key)
+	}
+
+	// 3. Numeric key: treat as ID first; fall back to name-based lookup so that
+	//    secrets whose name happens to be a numeric string can still be resolved.
+	if id, err := strconv.Atoi(key); err == nil {
+		secret, err := c.api.Secret(id)
+		if err == nil && secret != nil {
+			return secret, nil
+		}
+		if !isNotFoundError(err) {
 			return nil, err
 		}
-		return s, nil
 	}
 
-	// Otherwise try converting it to an ID
-	id, err := strconv.Atoi(ref.Key)
-	if err != nil {
-		s, err := c.api.Secrets(ref.Key, "Name")
-		if err != nil {
+	return c.getSecretByName(key, folderID)
+}
+
+// lookupSecretStrict resolves a secret like lookupSecret but refuses to
+// silently pick the first match when a plain name (no folderId prefix, no
+// path, no numeric ID) matches more than one secret across folders.
+// This is used by destructive operations (DeleteSecret) and existence checks
+// (SecretExists) that must not accidentally act on the wrong secret.
+func (c *client) lookupSecretStrict(key string) (*server.Secret, error) {
+	// 1. Folder-scoped prefix: unambiguous — delegate directly.
+	if prefixFolderID, name, ok := parseFolderPrefix(key); ok {
+		return c.getSecretByName(name, prefixFolderID)
+	}
+
+	// 2. Path-based key: unambiguous.
+	if strings.HasPrefix(key, "/") {
+		return c.api.SecretByPath(key)
+	}
+
+	// 3. Numeric key: try as ID first; fall back to name-based lookup.
+	if id, err := strconv.Atoi(key); err == nil {
+		secret, err := c.api.Secret(id)
+		if err == nil && secret != nil {
+			return secret, nil
+		}
+		if !isNotFoundError(err) {
 			return nil, err
 		}
-		if len(s) == 0 {
-			return nil, errors.New("unable to retrieve secret at this time")
+	}
+
+	// 4. Plain name: reject if ambiguous (multiple matches without folder scope).
+	return c.getSecretByNameStrict(key)
+}
+
+// getSecretByNameStrict searches for a secret by name and returns an error if
+// multiple secrets share the same name across different folders.
+func (c *client) getSecretByNameStrict(name string) (*server.Secret, error) {
+	secrets, err := c.api.Secrets(name, "Name")
+	if err != nil {
+		return nil, err
+	}
+	if len(secrets) == 0 {
+		return nil, errors.New(errMsgNoMatchingSecrets)
+	}
+	if len(secrets) > 1 {
+		return nil, errors.New(errMsgAmbiguousName)
+	}
+	return &secrets[0], nil
+}
+
+func (c *client) getSecretByName(name string, folderID int) (*server.Secret, error) {
+	secrets, err := c.api.Secrets(name, "Name")
+	if err != nil {
+		return nil, err
+	}
+	if len(secrets) == 0 {
+		return nil, errors.New(errMsgNoMatchingSecrets)
+	}
+
+	// No folder constraint: return the first match.
+	if folderID == 0 {
+		return &secrets[0], nil
+	}
+
+	// Find the first secret matching the requested folder.
+	for i, s := range secrets {
+		if s.FolderID == folderID {
+			return &secrets[i], nil
 		}
+	}
+	return nil, errors.New(errMsgNotFound)
+}
 
-		return &s[0], nil
+func findTemplateFieldID(template *server.SecretTemplate, property string) (int, bool) {
+	fieldID, found := template.FieldSlugToId(property)
+	if found {
+		return fieldID, true
 	}
-	return c.api.Secret(id)
+
+	// fallback check if they used name instead of slug
+	for _, f := range template.Fields {
+		if f.Name == property || f.FieldSlugName == property {
+			return f.SecretTemplateFieldID, true
+		}
+	}
+
+	return 0, false
 }

+ 1360 - 51
providers/v1/secretserver/client_test.go

@@ -19,6 +19,7 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"io"
 	"os"
 	"testing"
@@ -26,6 +27,8 @@ import (
 	"github.com/DelineaXPM/tss-sdk-go/v3/server"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 )
@@ -53,25 +56,111 @@ func (f *fakeAPI) Secret(id int) (*server.Secret, error) {
 }
 
 func (f *fakeAPI) Secrets(searchText, _ string) ([]server.Secret, error) {
-	secret := make([]server.Secret, 1)
+	// Match real SDK behavior: return ([]Secret{}, nil) for zero matches,
+	// NOT (nil, errNotFound). The real SDK's searchResources returns an empty
+	// SearchResult.Records slice and make([]Secret, 0).
+	var secrets []server.Secret
 	for _, s := range f.secrets {
 		if s.Name == searchText {
-			secret[0] = *s
-			return secret, nil
+			secrets = append(secrets, *s)
 		}
 	}
-	return nil, errNotFound
+	if secrets == nil {
+		secrets = []server.Secret{}
+	}
+	return secrets, nil
 }
 
 func (f *fakeAPI) SecretByPath(path string) (*server.Secret, error) {
 	for _, s := range f.secrets {
-		if "/"+s.Name == path {
+		if "/"+s.Name == path || s.Name == path {
 			return s, nil
 		}
 	}
 	return nil, errNotFound
 }
 
+// CreateSecret is a mock implementation of the Secret Server API CreateSecret method.
+// It returns a predefined secret based on the SecretTemplateID provided.
+func (f *fakeAPI) CreateSecret(secret server.Secret) (*server.Secret, error) {
+	if secret.Name == "simulate-create-error" {
+		return nil, errors.New("simulated create error")
+	}
+	secret.ID = len(f.secrets) + 10000
+
+	// Simulate populating FieldName and Slug based on FieldID
+	template, _ := f.SecretTemplate(secret.SecretTemplateID)
+	if template != nil {
+		for i, field := range secret.Fields {
+			for _, tField := range template.Fields {
+				if tField.SecretTemplateFieldID == field.FieldID {
+					secret.Fields[i].Slug = tField.FieldSlugName
+					secret.Fields[i].FieldName = tField.Name
+				}
+			}
+		}
+	}
+
+	f.secrets = append(f.secrets, &secret)
+	return &secret, nil
+}
+
+// UpdateSecret is a mock implementation of the Secret Server API UpdateSecret method.
+// It returns an error if a predefined test condition is met, otherwise it simulates success.
+func (f *fakeAPI) UpdateSecret(secret server.Secret) (*server.Secret, error) {
+	for i, s := range f.secrets {
+		if s.ID == secret.ID {
+			f.secrets[i] = &secret
+			return &secret, nil
+		}
+	}
+	return nil, errNotFound
+}
+
+// DeleteSecret is a mock implementation of the Secret Server API DeleteSecret method.
+// It returns an error if the id corresponds to a simulated failure case.
+func (f *fakeAPI) DeleteSecret(id int) error {
+	if id == 9999 {
+		return errors.New("simulated backend deletion error")
+	}
+	for i, s := range f.secrets {
+		if s.ID == id {
+			f.secrets = append(f.secrets[:i], f.secrets[i+1:]...)
+			return nil
+		}
+	}
+	return errNotFound
+}
+
+// SecretTemplate is a mock implementation of the Secret Server API SecretTemplate method.
+// It returns a predefined template or an error based on the requested id.
+func (f *fakeAPI) SecretTemplate(id int) (*server.SecretTemplate, error) {
+	if id == 999 {
+		return nil, errors.New("template not found")
+	}
+	return &server.SecretTemplate{
+		ID:   id,
+		Name: "Test Template",
+		Fields: []server.SecretTemplateField{
+			{
+				SecretTemplateFieldID: 1,
+				FieldSlugName:         "username",
+				Name:                  "Username",
+			},
+			{
+				SecretTemplateFieldID: 2,
+				FieldSlugName:         "password",
+				Name:                  "Password",
+			},
+			{
+				SecretTemplateFieldID: 3,
+				FieldSlugName:         "notes",
+				Name:                  "Notes",
+			},
+		},
+	}, nil
+}
+
 func createSecret(id int, itemValue string) (*server.Secret, error) {
 	s, err := jsonData()
 	if err != nil {
@@ -175,7 +264,22 @@ func newTestClient(t *testing.T) esv1.SecretsClient {
 	s6, err := createSecret(6000, "{ \"user\": \"betaTest\", \"password\": \"badPassword\" }")
 	require.NoError(t, err)
 
-	secrets = append(secrets, s6, createNilFieldsSecret(7000), createEmptyFieldsSecret(8000), createTestFolderSecret(9000, 4))
+	secrets = append(secrets, s6, createNilFieldsSecret(7000), createEmptyFieldsSecret(8000), createTestFolderSecret(9000, 4), createTestFolderSecret(9001, 5))
+
+	// Create a secret for path-based test
+	pathSecret := &server.Secret{
+		ID:       9002,
+		Name:     "/some/path/secret",
+		FolderID: 6,
+		Fields: []server.SecretField{
+			{FieldName: "Password", Slug: "password", ItemValue: "old_path_value"},
+		},
+	}
+	secrets = append(secrets, pathSecret)
+
+	s9999, err := createSecret(9999, "simulated error")
+	require.NoError(t, err)
+	secrets = append(secrets, s9999)
 
 	return &client{
 		api: &fakeAPI{
@@ -199,16 +303,17 @@ func TestGetSecretSecretServer(t *testing.T) {
 	require.NoError(t, err)
 
 	testCases := map[string]struct {
-		ref  esv1.ExternalSecretDataRemoteRef
-		want []byte
-		err  error
+		ref    esv1.ExternalSecretDataRemoteRef
+		want   []byte
+		err    error
+		errMsg string // when set, asserts Contains(err.Error(), errMsg) instead of exact error match
 	}{
 		"incorrect key returns nil and error": {
 			ref: esv1.ExternalSecretDataRemoteRef{
 				Key: "0",
 			},
-			want: []byte(nil),
-			err:  errNotFound,
+			want:   []byte(nil),
+			errMsg: errMsgNoMatchingSecrets,
 		},
 		"key = 'secret name' and user property returns a single value": {
 			ref: esv1.ExternalSecretDataRemoteRef{
@@ -278,19 +383,19 @@ func TestGetSecretSecretServer(t *testing.T) {
 			want: []byte(nil),
 			err:  esv1.NoSecretError{},
 		},
-		"Secret from code: valid ItemValue but nil Fields returns nil": {
+		"Secret from code: nil Fields returns error": {
 			ref: esv1.ExternalSecretDataRemoteRef{
 				Key: "7000",
 			},
-			want: []byte(nil),
+			want:   []byte(nil),
+			errMsg: "secret contains no fields",
 		},
-		"Secret from code: empty Fields returns noSecretError": {
+		"Secret from code: empty Fields returns error": {
 			ref: esv1.ExternalSecretDataRemoteRef{
-				Key:      "8000",
-				Property: "missing",
+				Key: "8000",
 			},
-			want: []byte(nil),
-			err:  esv1.NoSecretError{},
+			want:   []byte(nil),
+			errMsg: "secret contains no fields",
 		},
 		"Secret from code: 'name' and password slug returns a single value": {
 			ref: esv1.ExternalSecretDataRemoteRef{
@@ -299,13 +404,13 @@ func TestGetSecretSecretServer(t *testing.T) {
 			},
 			want: []byte(`passwordvalue`),
 		},
-		"Secret from code: 'name' not found and password slug returns error": {
+		"Secret from code: 'name' not found returns unable to retrieve secret error": {
 			ref: esv1.ExternalSecretDataRemoteRef{
 				Key:      "Secretnameerror",
 				Property: "password",
 			},
-			want: []byte(nil),
-			err:  errNotFound,
+			want:   []byte(nil),
+			errMsg: errMsgNoMatchingSecrets,
 		},
 		"Secret from code: 'name' found and non-existent attribute slug returns noSecretError": {
 			ref: esv1.ExternalSecretDataRemoteRef{
@@ -325,8 +430,8 @@ func TestGetSecretSecretServer(t *testing.T) {
 			ref: esv1.ExternalSecretDataRemoteRef{
 				Key: "/invalid/secret/path",
 			},
-			want: []byte(nil),
-			err:  errNotFound,
+			want:   []byte(nil),
+			errMsg: "not found",
 		},
 	}
 
@@ -334,28 +439,31 @@ func TestGetSecretSecretServer(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			got, err := c.GetSecret(ctx, tc.ref)
 
-			if tc.err == nil {
+			if tc.err == nil && tc.errMsg == "" {
 				assert.NoError(t, err)
 				assert.Equal(t, tc.want, got)
 			} else {
 				assert.Nil(t, got)
-				assert.ErrorIs(t, err, tc.err)
-				assert.Equal(t, tc.err, err)
+				if tc.errMsg != "" {
+					assert.ErrorContains(t, err, tc.errMsg)
+				} else {
+					assert.ErrorIs(t, err, tc.err)
+				}
 			}
 		})
 	}
 }
 
-// TestGetSecretJSONMarshalFailure tests GetSecret when json.Marshal fails.
-func TestGetSecretJSONMarshalFailure(t *testing.T) {
+// TestGetSecretWithInvalidUTF8ItemValue tests GetSecret with invalid UTF-8 in ItemValue.
+// json.Marshal in Go handles invalid UTF-8 strings without error, so this verifies
+// that GetSecret succeeds in this edge case.
+func TestGetSecretWithInvalidUTF8ItemValue(t *testing.T) {
 	ctx := t.Context()
 
 	bad := &server.Secret{
 		ID:     0,
 		Fields: []server.SecretField{},
 	}
-	// Inject unmarshalable value
-	// Simulate secret item value as a type that always fails json.Marshal
 	c := &client{
 		api: &fakeAPI{
 			secrets: []*server.Secret{bad},
@@ -364,13 +472,12 @@ func TestGetSecretJSONMarshalFailure(t *testing.T) {
 	bad.Fields = []server.SecretField{
 		{
 			FieldName: "Foo",
-			ItemValue: string([]byte{0xff, 0xfe}), // invalid UTF-8 → forces marshal failure
+			ItemValue: string([]byte{0xff, 0xfe}), // invalid UTF-8
 		},
 	}
 
-	// GetSecret calls getSecret which returns the secret, so no error expected
+	// GetSecret with no property returns the full JSON; json.Marshal handles invalid UTF-8.
 	_, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "0"})
-	// The secret is found but ItemValue is invalid; fail-fast if error
 	require.NoError(t, err)
 }
 
@@ -384,8 +491,9 @@ func TestGetSecretEmptySecretsList(t *testing.T) {
 
 	_, err := c.getSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "nonexistent"})
 	assert.Error(t, err)
-	// When secret not found, the fakeAPI returns errNotFound
-	assert.Contains(t, err.Error(), "not found")
+	// fakeAPI.Secrets now returns ([]Secret{}, nil) for zero matches (matching real SDK),
+	// so getSecretByName returns errMsgNoMatchingSecrets.
+	assert.Contains(t, err.Error(), errMsgNoMatchingSecrets)
 }
 
 // TestGetSecretWithVersion tests that specifying a version returns an error.
@@ -419,15 +527,208 @@ func TestGetSecretWithVersion(t *testing.T) {
 	}
 }
 
+// fakePushSecretData implements esv1.PushSecretData for testing.
+type fakePushSecretData struct {
+	remoteKey string
+	property  string
+	secretKey string
+	metadata  *apiextensionsv1.JSON
+}
+
+// GetRemoteKey returns the remote key for the fake push secret data.
+func (f fakePushSecretData) GetRemoteKey() string { return f.remoteKey }
+
+// GetProperty returns the property for the fake push secret data.
+func (f fakePushSecretData) GetProperty() string { return f.property }
+
+// GetSecretKey returns the secret key for the fake push secret data.
+func (f fakePushSecretData) GetSecretKey() string { return f.secretKey }
+
+// GetMetadata returns the metadata for the fake push secret data.
+func (f fakePushSecretData) GetMetadata() *apiextensionsv1.JSON { return f.metadata }
+
+// fakePushSecretRemoteRef implements esv1.PushSecretRemoteRef for testing.
+type fakePushSecretRemoteRef struct {
+	remoteKey string
+	property  string
+}
+
+// GetRemoteKey returns the remote key for the fake remote ref.
+func (f fakePushSecretRemoteRef) GetRemoteKey() string { return f.remoteKey }
+
+// GetProperty returns the property for the fake remote ref.
+func (f fakePushSecretRemoteRef) GetProperty() string { return f.property }
+
 // TestPushSecret tests the PushSecret functionality.
 func TestPushSecret(t *testing.T) {
 	ctx := context.Background()
 	c := newTestClient(t)
 
-	var data esv1.PushSecretData
-	err := c.PushSecret(ctx, nil, data)
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"my-key": []byte("my-value"),
+		},
+	}
+
+	metadataJSON := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
+	}
+
+	// Create a new secret
+	data := fakePushSecretData{
+		remoteKey: "new-secret",
+		property:  "username",
+		secretKey: "my-key",
+		metadata:  &metadataJSON,
+	}
+	err := c.PushSecret(ctx, secret, data)
+	assert.NoError(t, err)
+
+	// Verify the secret was created
+	createdSecret, _ := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "new-secret", Property: "username"})
+	assert.Equal(t, []byte("my-value"), createdSecret)
+
+	// Create a new secret with path-like key and folderId
+	dataPathCreate := fakePushSecretData{
+		remoteKey: "/some/new/path/secretname",
+		property:  "username",
+		secretKey: "my-key",
+		metadata:  &metadataJSON,
+	}
+	err = c.PushSecret(ctx, secret, dataPathCreate)
+	assert.NoError(t, err)
+
+	// verify that the created secret has just the basename "secretname"
+	// and since it's the 10th secret created by fakeAPI, its ID would be 10000 + len(secrets)
+	foundSecrets, _ := c.(*client).api.Secrets("secretname", "Name")
+	assert.Len(t, foundSecrets, 1)
+	assert.Equal(t, "secretname", foundSecrets[0].Name)
+	assert.Equal(t, 1, foundSecrets[0].FolderID)
+
+	// Update an existing secret
+	dataUpdate := fakePushSecretData{
+		remoteKey: "4000",
+		property:  "password",
+		secretKey: "my-key", // "my-value" will replace the badPassword
+	}
+	err = c.PushSecret(ctx, secret, dataUpdate)
+	assert.NoError(t, err)
+
+	// Verify update
+	updatedSecret, _ := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "4000", Property: "password"})
+	assert.Equal(t, []byte("my-value"), updatedSecret)
+
+	// Missing metadata for new secret
+	dataMissingMeta := fakePushSecretData{
+		remoteKey: "new-secret-no-meta",
+		property:  "username",
+		secretKey: "my-key",
+		metadata:  nil,
+	}
+	err = c.PushSecret(ctx, secret, dataMissingMeta)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "folderId and secretTemplateId must be provided in metadata to create a new secret")
+
+	// Invalid secretTemplateId in metadata
+	invalidMetadataJSON := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 999}}`), // non-existent template
+	}
+	dataInvalidMeta := fakePushSecretData{
+		remoteKey: "new-secret-invalid-meta",
+		property:  "username",
+		secretKey: "my-key",
+		metadata:  &invalidMetadataJSON,
+	}
+	err = c.PushSecret(ctx, secret, dataInvalidMeta)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "failed to get secret template")
+
+	// Simulate create error
+	// Requires modifying fakeAPI to return an error when Name == "simulate-create-error"
+	dataCreateError := fakePushSecretData{
+		remoteKey: "simulate-create-error",
+		property:  "username",
+		secretKey: "my-key",
+		metadata:  &metadataJSON,
+	}
+	err = c.PushSecret(ctx, secret, dataCreateError)
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "not supported")
+	assert.Contains(t, err.Error(), "failed to create secret")
+
+	// Update with non-existent property
+	dataUpdateInvalidProp := fakePushSecretData{
+		remoteKey: "4000",
+		property:  "non-existent-property",
+		secretKey: "my-key",
+	}
+	err = c.PushSecret(ctx, secret, dataUpdateInvalidProp)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "field non-existent-property not found in secret")
+
+	// Update duplicate-named secret in specific folder (ID 9001 in FolderID 5)
+	metadataFolder5 := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 5, "secretTemplateId": 1}}`),
+	}
+	dataFolderUpdate := fakePushSecretData{
+		remoteKey: "FolderSecretname",
+		property:  "password",
+		secretKey: "my-key",
+		metadata:  &metadataFolder5,
+	}
+	err = c.PushSecret(ctx, secret, dataFolderUpdate)
+	assert.NoError(t, err)
+
+	// Verify only the secret in folder 5 was updated
+	s9001, _ := c.(*client).api.Secret(9001)
+	s9000, _ := c.(*client).api.Secret(9000)
+	// Check the password field
+	var s9001PW, s9000PW string
+	for _, f := range s9001.Fields {
+		if f.Slug == passwordSlug {
+			s9001PW = f.ItemValue
+		}
+	}
+	for _, f := range s9000.Fields {
+		if f.Slug == passwordSlug {
+			s9000PW = f.ItemValue
+		}
+	}
+	assert.Equal(t, "my-value", s9001PW)
+	assert.Equal(t, "passwordvalue", s9000PW) // Unchanged
+
+	// Update path-based key secret
+	dataPathUpdate := fakePushSecretData{
+		remoteKey: "/some/path/secret",
+		property:  "password",
+		secretKey: "my-key",
+	}
+	err = c.PushSecret(ctx, secret, dataPathUpdate)
+	assert.NoError(t, err)
+
+	sPath, _ := c.(*client).api.Secret(9002)
+	var sPathPW string
+	for _, f := range sPath.Fields {
+		if f.Slug == passwordSlug {
+			sPathPW = f.ItemValue
+		}
+	}
+	assert.Equal(t, "my-value", sPathPW)
+
+	// Push invalid UTF-8 secret
+	invalidUtf8Secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"invalid-utf8": {0xff, 0xfe, 0xfd},
+		},
+	}
+	dataInvalidUtf8 := fakePushSecretData{
+		remoteKey: "new-secret-utf8",
+		property:  "username",
+		secretKey: "invalid-utf8",
+		metadata:  &metadataJSON,
+	}
+	err = c.PushSecret(ctx, invalidUtf8Secret, dataInvalidUtf8)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "secret value is not valid UTF-8")
 }
 
 // TestDeleteSecret tests the DeleteSecret functionality.
@@ -435,10 +736,68 @@ func TestDeleteSecret(t *testing.T) {
 	ctx := context.Background()
 	c := newTestClient(t)
 
-	var data esv1.PushSecretRemoteRef
-	err := c.DeleteSecret(ctx, data)
+	ref := fakePushSecretRemoteRef{
+		remoteKey: "1000",
+	}
+
+	// Should exist initially
+	exists, err := c.SecretExists(ctx, ref)
+	assert.NoError(t, err)
+	assert.True(t, exists)
+
+	// Delete it
+	err = c.DeleteSecret(ctx, ref)
+	assert.NoError(t, err)
+
+	// Should not exist now
+	exists, err = c.SecretExists(ctx, ref)
+	assert.NoError(t, err)
+	assert.False(t, exists)
+
+	// Test idempotency: delete again should not error
+	err = c.DeleteSecret(ctx, ref)
+	assert.NoError(t, err)
+
+	// Test path-based key deletion
+	pathRef := fakePushSecretRemoteRef{
+		remoteKey: "/some/path/secret",
+	}
+
+	exists, err = c.SecretExists(ctx, pathRef)
+	assert.NoError(t, err)
+	assert.True(t, exists)
+
+	err = c.DeleteSecret(ctx, pathRef)
+	assert.NoError(t, err)
+
+	exists, err = c.SecretExists(ctx, pathRef)
+	assert.NoError(t, err)
+	assert.False(t, exists)
+}
+
+// TestDeleteSecret_Error tests that an error from the backend during DeleteSecret is propagated.
+func TestDeleteSecret_Error(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	ref := fakePushSecretRemoteRef{
+		remoteKey: "9999",
+	}
+
+	// Should exist initially
+	exists, err := c.SecretExists(ctx, ref)
+	assert.NoError(t, err)
+	assert.True(t, exists)
+
+	// Attempt to delete it, expecting an error
+	err = c.DeleteSecret(ctx, ref)
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "not supported")
+	assert.Contains(t, err.Error(), "failed to delete secret")
+
+	// Verify it still exists
+	exists, err = c.SecretExists(ctx, ref)
+	assert.NoError(t, err)
+	assert.True(t, exists)
 }
 
 // TestSecretExists tests the SecretExists functionality.
@@ -446,11 +805,34 @@ func TestSecretExists(t *testing.T) {
 	ctx := context.Background()
 	c := newTestClient(t)
 
-	var data esv1.PushSecretRemoteRef
-	exists, err := c.SecretExists(ctx, data)
-	assert.False(t, exists)
-	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "not implemented")
+	testCases := map[string]struct {
+		ref     esv1.PushSecretRemoteRef
+		want    bool
+		wantErr bool
+	}{
+		"existing secret": {
+			ref:     fakePushSecretRemoteRef{remoteKey: "1000"},
+			want:    true,
+			wantErr: false,
+		},
+		"non-existing secret": {
+			ref:     fakePushSecretRemoteRef{remoteKey: "does-not-exist"},
+			want:    false,
+			wantErr: false,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			got, err := c.SecretExists(ctx, tc.ref)
+			if tc.wantErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tc.want, got)
+			}
+		})
+	}
 }
 
 // TestValidate tests the Validate functionality.
@@ -492,6 +874,9 @@ func TestGetSecretMap(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		// The following test case expects an error because the secret with Key "9999"
+		// contains invalid JSON ("simulated error") which causes unmarshalling to fail
+		// in GetSecretMap, rather than because the secret is missing.
 		"error when secret not found": {
 			ref: esv1.ExternalSecretDataRemoteRef{
 				Key: "9999",
@@ -554,15 +939,17 @@ func TestGetSecretMapInvalidJSON(t *testing.T) {
 	assert.Error(t, err)
 }
 
-// TestGetSecretMapGetByteValueError tests GetSecretMap when GetByteValue fails.
-func TestGetSecretMapGetByteValueError(t *testing.T) {
+// TestGetSecretMapValidJSON tests GetSecretMap with valid JSON data succeeds.
+func TestGetSecretMapValidJSON(t *testing.T) {
 	ctx := context.Background()
 
 	c := newTestClient(t)
 
 	// GetSecretMap with valid JSON should succeed
-	_, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{Key: "1000"})
+	result, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{Key: "1000"})
 	assert.NoError(t, err)
+	assert.NotNil(t, result)
+	assert.Equal(t, []byte("robertOppenheimer"), result["user"])
 }
 
 // TestClose tests the Close functionality.
@@ -589,12 +976,12 @@ func TestGetAllSecrets(t *testing.T) {
 				Path: new("some-path"),
 			},
 			wantErr: true,
-			errMsg:  "getting all secrets is not supported by Delinea Secret Server at this time",
+			errMsg:  "getting all secrets is not supported by Delinea Secret Server",
 		},
 		"returns error with nil path": {
 			ref:     esv1.ExternalSecretFind{},
 			wantErr: true,
-			errMsg:  "getting all secrets is not supported by Delinea Secret Server at this time",
+			errMsg:  "getting all secrets is not supported by Delinea Secret Server",
 		},
 	}
 
@@ -608,3 +995,925 @@ func TestGetAllSecrets(t *testing.T) {
 		})
 	}
 }
+
+// TestIsNotFoundError tests the isNotFoundError function with various error formats.
+func TestIsNotFoundError(t *testing.T) {
+	testCases := map[string]struct {
+		err  error
+		want bool
+	}{
+		"nil error": {
+			err:  nil,
+			want: false,
+		},
+		"exact lowercase not found": {
+			err:  errors.New("not found"),
+			want: true,
+		},
+		"SDK HTTP 404 format": {
+			err:  errors.New("404 Not Found: no secret was found"),
+			want: true,
+		},
+		"SDK HTTP 404 with empty body": {
+			err:  errors.New("404 Not Found: "),
+			want: true,
+		},
+		"unable to retrieve secret at this time": {
+			err:  errors.New("unable to retrieve secret at this time"),
+			want: true,
+		},
+		"unrelated error": {
+			err:  errors.New("connection refused"),
+			want: false,
+		},
+		"field not found in secret (false positive excluded)": {
+			// This error from updateSecret should NOT be treated as not-found.
+			err:  fmt.Errorf("field password not found in secret"),
+			want: false,
+		},
+		"field not found in secret template (false positive excluded)": {
+			// This error from createSecret should NOT be treated as not-found.
+			err:  fmt.Errorf("field username not found in secret template"),
+			want: false,
+		},
+		"wrapped field not found in secret": {
+			// Even when wrapped, the false-positive exclusion applies.
+			err:  fmt.Errorf("failed to update secret: %w", fmt.Errorf("field password not found in secret")),
+			want: false,
+		},
+		"mixed case Not Found": {
+			err:  errors.New("Not Found"),
+			want: true,
+		},
+		"SDK HTTP 401 with not found in body": {
+			// Auth errors that happen to contain "not found" in the body should NOT
+			// be treated as secret-not-found errors. Only 404 is a true not-found.
+			err:  errors.New("401 Unauthorized: user not found"),
+			want: false,
+		},
+		"SDK HTTP 500 error": {
+			err:  errors.New("500 Internal Server Error: something went wrong"),
+			want: false,
+		},
+		"our errMsgNotFound sentinel": {
+			// From getSecretByName folder mismatch: errors.New(errMsgNotFound)
+			err:  errors.New(errMsgNotFound),
+			want: true,
+		},
+		"errMsgAmbiguousName is not a not-found error": {
+			err:  errors.New(errMsgAmbiguousName),
+			want: false,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			got := isNotFoundError(tc.err)
+			assert.Equal(t, tc.want, got)
+		})
+	}
+}
+
+// TestPushSecretInvalidPathKeys tests that PushSecret rejects path-style keys with
+// empty final segments (root slash, double slash, etc.) that would produce an empty secret name.
+func TestPushSecretInvalidPathKeys(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"my-key": []byte("my-value"),
+		},
+	}
+
+	metadataJSON := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
+	}
+
+	testCases := map[string]struct {
+		remoteKey string
+		errMsg    string
+	}{
+		"root slash only": {
+			remoteKey: "/",
+			errMsg:    "invalid secret name",
+		},
+		"double slash": {
+			remoteKey: "//",
+			errMsg:    "invalid secret name",
+		},
+		"triple slash": {
+			remoteKey: "///",
+			errMsg:    "invalid secret name",
+		},
+		"trailing slash on path": {
+			remoteKey: "/Folder/Subfolder/",
+			errMsg:    "invalid secret name",
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			data := fakePushSecretData{
+				remoteKey: tc.remoteKey,
+				property:  "username",
+				secretKey: "my-key",
+				metadata:  &metadataJSON,
+			}
+			err := c.PushSecret(ctx, secret, data)
+			assert.Error(t, err)
+			assert.Contains(t, err.Error(), tc.errMsg)
+		})
+	}
+}
+
+// TestParseFolderPrefix tests the parseFolderPrefix helper function.
+func TestParseFolderPrefix(t *testing.T) {
+	testCases := map[string]struct {
+		key              string
+		wantFolderID     int
+		wantName         string
+		wantHasFolderPfx bool
+	}{
+		"valid prefix": {
+			key:              "folderId:73/my-secret",
+			wantFolderID:     73,
+			wantName:         "my-secret",
+			wantHasFolderPfx: true,
+		},
+		"valid prefix with large folder ID": {
+			key:              "folderId:99999/secret-name",
+			wantFolderID:     99999,
+			wantName:         "secret-name",
+			wantHasFolderPfx: true,
+		},
+		"valid prefix with name containing slashes": {
+			key:              "folderId:73/sub/path/secret",
+			wantFolderID:     73,
+			wantName:         "sub/path/secret",
+			wantHasFolderPfx: true,
+		},
+		"no prefix - plain name": {
+			key:              "my-secret",
+			wantFolderID:     0,
+			wantName:         "my-secret",
+			wantHasFolderPfx: false,
+		},
+		"no prefix - numeric key": {
+			key:              "12345",
+			wantFolderID:     0,
+			wantName:         "12345",
+			wantHasFolderPfx: false,
+		},
+		"no prefix - path key": {
+			key:              "/Folder/SecretName",
+			wantFolderID:     0,
+			wantName:         "/Folder/SecretName",
+			wantHasFolderPfx: false,
+		},
+		"prefix without slash": {
+			key:              "folderId:73",
+			wantFolderID:     0,
+			wantName:         "folderId:73",
+			wantHasFolderPfx: false,
+		},
+		"prefix with empty name": {
+			key:              "folderId:73/",
+			wantFolderID:     0,
+			wantName:         "folderId:73/",
+			wantHasFolderPfx: false,
+		},
+		"prefix with non-numeric ID": {
+			key:              "folderId:abc/my-secret",
+			wantFolderID:     0,
+			wantName:         "folderId:abc/my-secret",
+			wantHasFolderPfx: false,
+		},
+		"prefix with zero ID": {
+			key:              "folderId:0/my-secret",
+			wantFolderID:     0,
+			wantName:         "folderId:0/my-secret",
+			wantHasFolderPfx: false,
+		},
+		"prefix with negative ID": {
+			key:              "folderId:-1/my-secret",
+			wantFolderID:     0,
+			wantName:         "folderId:-1/my-secret",
+			wantHasFolderPfx: false,
+		},
+		"empty key": {
+			key:              "",
+			wantFolderID:     0,
+			wantName:         "",
+			wantHasFolderPfx: false,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			folderID, secretName, hasFolderPrefix := parseFolderPrefix(tc.key)
+			assert.Equal(t, tc.wantFolderID, folderID)
+			assert.Equal(t, tc.wantName, secretName)
+			assert.Equal(t, tc.wantHasFolderPfx, hasFolderPrefix)
+		})
+	}
+}
+
+// TestPushSecretWithFolderPrefix tests PushSecret with the "folderId:<id>/<name>" key format.
+func TestPushSecretWithFolderPrefix(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"my-key": []byte("folder-prefix-value"),
+		},
+	}
+
+	metadataJSON := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 5, "secretTemplateId": 1}}`),
+	}
+
+	// Update an existing secret using folderId prefix — should target folder 5 (ID 9001)
+	dataUpdate := fakePushSecretData{
+		remoteKey: "folderId:5/FolderSecretname",
+		property:  "password",
+		secretKey: "my-key",
+		metadata:  &metadataJSON,
+	}
+	err := c.PushSecret(ctx, secret, dataUpdate)
+	assert.NoError(t, err)
+
+	// Verify only the secret in folder 5 was updated
+	s9001, _ := c.(*client).api.Secret(9001)
+	s9000, _ := c.(*client).api.Secret(9000)
+	var s9001PW, s9000PW string
+	for _, f := range s9001.Fields {
+		if f.Slug == passwordSlug {
+			s9001PW = f.ItemValue
+		}
+	}
+	for _, f := range s9000.Fields {
+		if f.Slug == passwordSlug {
+			s9000PW = f.ItemValue
+		}
+	}
+	assert.Equal(t, "folder-prefix-value", s9001PW)
+	assert.Equal(t, "passwordvalue", s9000PW) // Unchanged
+
+	// Create a new secret using folderId prefix
+	metadataCreate := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 42, "secretTemplateId": 1}}`),
+	}
+	dataCreate := fakePushSecretData{
+		remoteKey: "folderId:42/brand-new-secret",
+		property:  "username",
+		secretKey: "my-key",
+		metadata:  &metadataCreate,
+	}
+	err = c.PushSecret(ctx, secret, dataCreate)
+	assert.NoError(t, err)
+
+	// Verify the created secret has the plain name (prefix stripped)
+	foundSecrets, _ := c.(*client).api.Secrets("brand-new-secret", "Name")
+	assert.Len(t, foundSecrets, 1)
+	assert.Equal(t, "brand-new-secret", foundSecrets[0].Name)
+	assert.Equal(t, 42, foundSecrets[0].FolderID)
+
+	// Test precedence: remoteKey folderId overrides metadata folderId for lookups.
+	// Metadata says folderId:4, but remoteKey says folderId:5 — should target folder 5.
+	metadataFolder4 := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 4, "secretTemplateId": 1}}`),
+	}
+	dataPrecedence := fakePushSecretData{
+		remoteKey: "folderId:5/FolderSecretname",
+		property:  "username",
+		secretKey: "my-key",
+		metadata:  &metadataFolder4,
+	}
+	err = c.PushSecret(ctx, secret, dataPrecedence)
+	assert.NoError(t, err)
+
+	// Verify the secret in folder 5 was updated (not folder 4)
+	s9001, _ = c.(*client).api.Secret(9001)
+	var s9001User string
+	for _, f := range s9001.Fields {
+		if f.Slug == usernameSlug {
+			s9001User = f.ItemValue
+		}
+	}
+	assert.Equal(t, "folder-prefix-value", s9001User)
+}
+
+// TestDeleteSecretWithFolderPrefix tests that DeleteSecret correctly uses the
+// folderId prefix in the remote key to target the right secret.
+func TestDeleteSecretWithFolderPrefix(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	// Both secrets 9000 (folder 4) and 9001 (folder 5) have name "FolderSecretname".
+	// Delete only the one in folder 5.
+	ref := fakePushSecretRemoteRef{
+		remoteKey: "folderId:5/FolderSecretname",
+	}
+
+	// Should exist initially
+	exists, err := c.SecretExists(ctx, ref)
+	assert.NoError(t, err)
+	assert.True(t, exists)
+
+	// Delete it
+	err = c.DeleteSecret(ctx, ref)
+	assert.NoError(t, err)
+
+	// Should not exist now
+	exists, err = c.SecretExists(ctx, ref)
+	assert.NoError(t, err)
+	assert.False(t, exists)
+
+	// The secret in folder 4 should still exist
+	refFolder4 := fakePushSecretRemoteRef{
+		remoteKey: "folderId:4/FolderSecretname",
+	}
+	exists, err = c.SecretExists(ctx, refFolder4)
+	assert.NoError(t, err)
+	assert.True(t, exists)
+}
+
+// TestSecretExistsWithFolderPrefix tests that SecretExists correctly uses the
+// folderId prefix in the remote key.
+func TestSecretExistsWithFolderPrefix(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	testCases := map[string]struct {
+		ref  esv1.PushSecretRemoteRef
+		want bool
+	}{
+		"existing secret in folder 4": {
+			ref:  fakePushSecretRemoteRef{remoteKey: "folderId:4/FolderSecretname"},
+			want: true,
+		},
+		"existing secret in folder 5": {
+			ref:  fakePushSecretRemoteRef{remoteKey: "folderId:5/FolderSecretname"},
+			want: true,
+		},
+		"non-existing secret in wrong folder": {
+			ref:  fakePushSecretRemoteRef{remoteKey: "folderId:99/FolderSecretname"},
+			want: false,
+		},
+		"non-existing secret name": {
+			ref:  fakePushSecretRemoteRef{remoteKey: "folderId:4/does-not-exist"},
+			want: false,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			got, err := c.SecretExists(ctx, tc.ref)
+			assert.NoError(t, err)
+			assert.Equal(t, tc.want, got)
+		})
+	}
+}
+
+// TestDeleteSecretAmbiguousName tests that DeleteSecret returns an error when a
+// plain name matches multiple secrets across different folders.
+func TestDeleteSecretAmbiguousName(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	// "FolderSecretname" exists in both folder 4 (ID 9000) and folder 5 (ID 9001).
+	// Using just the plain name should fail with an ambiguous error.
+	ref := fakePushSecretRemoteRef{
+		remoteKey: "FolderSecretname",
+	}
+
+	err := c.DeleteSecret(ctx, ref)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "multiple secrets found with the same name")
+	assert.Contains(t, err.Error(), "folderId:")
+
+	// Both secrets should still exist (nothing was deleted).
+	s9000, err := c.(*client).api.Secret(9000)
+	assert.NoError(t, err)
+	assert.NotNil(t, s9000)
+
+	s9001, err := c.(*client).api.Secret(9001)
+	assert.NoError(t, err)
+	assert.NotNil(t, s9001)
+}
+
+// TestSecretExistsAmbiguousName tests that SecretExists returns an error when a
+// plain name matches multiple secrets across different folders.
+func TestSecretExistsAmbiguousName(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	// "FolderSecretname" exists in both folder 4 (ID 9000) and folder 5 (ID 9001).
+	// Using just the plain name should fail with an ambiguous error.
+	ref := fakePushSecretRemoteRef{
+		remoteKey: "FolderSecretname",
+	}
+
+	exists, err := c.SecretExists(ctx, ref)
+	assert.Error(t, err)
+	assert.False(t, exists)
+	assert.Contains(t, err.Error(), "multiple secrets found with the same name")
+}
+
+// TestDeleteSecretUniqueName tests that DeleteSecret still works with a plain
+// name when only one secret has that name (no ambiguity).
+func TestDeleteSecretUniqueName(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	// "Secretname" is unique (only ID 4000 has this name).
+	ref := fakePushSecretRemoteRef{
+		remoteKey: "Secretname",
+	}
+
+	exists, err := c.SecretExists(ctx, ref)
+	assert.NoError(t, err)
+	assert.True(t, exists)
+
+	err = c.DeleteSecret(ctx, ref)
+	assert.NoError(t, err)
+
+	exists, err = c.SecretExists(ctx, ref)
+	assert.NoError(t, err)
+	assert.False(t, exists)
+}
+
+// TestGetSecretByNameStrict tests the getSecretByNameStrict helper directly.
+func TestGetSecretByNameStrict(t *testing.T) {
+	c := newTestClient(t).(*client)
+
+	testCases := map[string]struct {
+		name    string
+		wantErr bool
+		errMsg  string
+	}{
+		"unique name returns secret": {
+			name:    "Secretname",
+			wantErr: false,
+		},
+		"duplicate name returns ambiguous error": {
+			name:    "FolderSecretname",
+			wantErr: true,
+			errMsg:  "multiple secrets found with the same name",
+		},
+		"non-existent name returns unable to retrieve secret error": {
+			name:    "does-not-exist",
+			wantErr: true,
+			errMsg:  errMsgNoMatchingSecrets,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			secret, err := c.getSecretByNameStrict(tc.name)
+			if tc.wantErr {
+				assert.Error(t, err)
+				assert.Nil(t, secret)
+				assert.Contains(t, err.Error(), tc.errMsg)
+			} else {
+				assert.NoError(t, err)
+				assert.NotNil(t, secret)
+			}
+		})
+	}
+}
+
+// TestPushSecretEmptyProperty tests PushSecret with an empty property, which
+// should target the first field of the secret/template.
+func TestPushSecretEmptyProperty(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"my-key": []byte("whole-value"),
+		},
+	}
+
+	metadataJSON := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
+	}
+
+	// Create new secret with empty property → uses first template field
+	data := fakePushSecretData{
+		remoteKey: "empty-prop-secret",
+		property:  "",
+		secretKey: "my-key",
+		metadata:  &metadataJSON,
+	}
+	err := c.PushSecret(ctx, secret, data)
+	assert.NoError(t, err)
+
+	// Verify: the first field should have the value
+	foundSecrets, _ := c.(*client).api.Secrets("empty-prop-secret", "Name")
+	require.Len(t, foundSecrets, 1)
+	require.Len(t, foundSecrets[0].Fields, 1)
+	assert.Equal(t, "whole-value", foundSecrets[0].Fields[0].ItemValue)
+
+	// Update existing secret with empty property → updates first field
+	data2 := fakePushSecretData{
+		remoteKey: "4000",
+		property:  "",
+		secretKey: "my-key",
+	}
+	err = c.PushSecret(ctx, secret, data2)
+	assert.NoError(t, err)
+
+	s4000, _ := c.(*client).api.Secret(4000)
+	assert.Equal(t, "whole-value", s4000.Fields[0].ItemValue)
+}
+
+// TestPushSecretConflictingFolderIDs tests that when the remoteKey has a folderId
+// prefix, it overrides the metadata folderId for both lookup AND creation.
+func TestPushSecretConflictingFolderIDs(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"my-key": []byte("prefix-wins"),
+		},
+	}
+
+	// Metadata says folderId:99, but prefix says folderId:42.
+	// The prefix should win for creation.
+	metadataJSON := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 99, "secretTemplateId": 1}}`),
+	}
+
+	data := fakePushSecretData{
+		remoteKey: "folderId:42/conflict-test",
+		property:  "username",
+		secretKey: "my-key",
+		metadata:  &metadataJSON,
+	}
+	err := c.PushSecret(ctx, secret, data)
+	assert.NoError(t, err)
+
+	// Verify: the secret was created in folder 42, not 99.
+	foundSecrets, _ := c.(*client).api.Secrets("conflict-test", "Name")
+	require.Len(t, foundSecrets, 1)
+	assert.Equal(t, 42, foundSecrets[0].FolderID)
+}
+
+// TestPushSecretAmbiguousPlainName tests that PushSecret returns an error when
+// a plain name (no prefix, no path, no numeric ID) matches multiple secrets.
+func TestPushSecretAmbiguousPlainName(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"my-key": []byte("value"),
+		},
+	}
+
+	// "FolderSecretname" exists in both folder 4 (ID 9000) and folder 5 (ID 9001).
+	// Without a folderId prefix or metadata folderId, this should fail.
+	data := fakePushSecretData{
+		remoteKey: "FolderSecretname",
+		property:  "password",
+		secretKey: "my-key",
+	}
+	err := c.PushSecret(ctx, secret, data)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "multiple secrets found with the same name")
+}
+
+// TestPushSecretEmptyRemoteKey tests that PushSecret rejects empty remote keys.
+func TestPushSecretEmptyRemoteKey(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"my-key": []byte("value"),
+		},
+	}
+
+	data := fakePushSecretData{
+		remoteKey: "",
+		property:  "username",
+		secretKey: "my-key",
+	}
+	err := c.PushSecret(ctx, secret, data)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "remote key must be defined")
+}
+
+// TestCreateSecretFolderPrefixWithSlashes tests that createSecret rejects
+// folderId prefixed names that contain slashes after prefix stripping.
+func TestCreateSecretFolderPrefixWithSlashes(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"my-key": []byte("value"),
+		},
+	}
+
+	metadataJSON := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 73, "secretTemplateId": 1}}`),
+	}
+
+	data := fakePushSecretData{
+		remoteKey: "folderId:73/sub/path/secret",
+		property:  "username",
+		secretKey: "my-key",
+		metadata:  &metadataJSON,
+	}
+	err := c.PushSecret(ctx, secret, data)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "must not contain path separators")
+}
+
+// TestCreateSecretEmptyTemplateFields tests createSecret when the template has
+// no fields and no property is specified.
+func TestCreateSecretEmptyTemplateFields(t *testing.T) {
+	// Create a fakeAPI that returns a template with no fields
+	fake := &fakeAPI{secrets: []*server.Secret{}}
+	// Override SecretTemplate to return empty fields (template ID 888)
+	c := &client{api: &emptyTemplateAPI{fakeAPI: fake}}
+
+	err := c.createSecret("test-secret", "", "value", PushSecretMetadataSpec{
+		FolderID:         1,
+		SecretTemplateID: 888,
+	})
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "secret template has no fields")
+}
+
+// emptyTemplateAPI wraps fakeAPI but returns an empty template for ID 888.
+type emptyTemplateAPI struct {
+	*fakeAPI
+}
+
+func (e *emptyTemplateAPI) SecretTemplate(id int) (*server.SecretTemplate, error) {
+	if id == 888 {
+		return &server.SecretTemplate{
+			ID:     888,
+			Name:   "Empty Template",
+			Fields: []server.SecretTemplateField{},
+		}, nil
+	}
+	return e.fakeAPI.SecretTemplate(id)
+}
+
+// TestGetSecretGjsonPriorityOverField tests that gjson extraction from
+// Fields[0].ItemValue takes priority over field Slug/FieldName matching.
+// This preserves backward compatibility: existing users relying on gjson
+// extraction from the first field's JSON blob are not broken.
+func TestGetSecretGjsonPriorityOverField(t *testing.T) {
+	ctx := context.Background()
+
+	// Create a secret where:
+	// - Fields[0].ItemValue is JSON containing key "password"
+	// - Fields[1] has Slug "password" with a DIFFERENT value
+	// gjson should win because it is checked first (backward compat).
+	s := &server.Secret{
+		ID:   100,
+		Name: "priority-test",
+		Fields: []server.SecretField{
+			{
+				FieldName: "Data",
+				Slug:      "data",
+				ItemValue: `{"password": "from-json-blob"}`,
+			},
+			{
+				FieldName: "Password",
+				Slug:      "password",
+				ItemValue: "from-field-slug",
+			},
+		},
+	}
+
+	c := &client{api: &fakeAPI{secrets: []*server.Secret{s}}}
+
+	got, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
+		Key:      "100",
+		Property: "password",
+	})
+	assert.NoError(t, err)
+	// gjson extraction should return "from-json-blob" (backward compat takes precedence)
+	assert.Equal(t, []byte("from-json-blob"), got)
+}
+
+// TestGetSecretGjsonFallback tests that gjson extraction from Fields[0].ItemValue
+// works as a fallback when no field slug/name matches.
+func TestGetSecretGjsonFallback(t *testing.T) {
+	ctx := context.Background()
+
+	s := &server.Secret{
+		ID:   101,
+		Name: "gjson-fallback-test",
+		Fields: []server.SecretField{
+			{
+				FieldName: "Data",
+				Slug:      "data",
+				ItemValue: `{"nested": {"key": "deep-value"}}`,
+			},
+		},
+	}
+
+	c := &client{api: &fakeAPI{secrets: []*server.Secret{s}}}
+
+	// "nested.key" doesn't match any field slug/name, so gjson fallback kicks in
+	got, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
+		Key:      "101",
+		Property: "nested.key",
+	})
+	assert.NoError(t, err)
+	assert.Equal(t, []byte("deep-value"), got)
+}
+
+// TestLookupSecretNon404Error tests that lookupSecret and lookupSecretStrict
+// correctly propagate non-404 API errors instead of falling through.
+func TestLookupSecretNon404Error(t *testing.T) {
+	// Create an API that returns a non-404 error for Secret()
+	fake := &errorAPI{
+		fakeAPI:   &fakeAPI{secrets: []*server.Secret{}},
+		secretErr: errors.New("500 Internal Server Error: database connection failed"),
+	}
+	c := &client{api: fake}
+
+	// lookupSecret with numeric key: Secret() returns non-404 error, should propagate
+	_, err := c.lookupSecret("42", 0)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "database connection failed")
+
+	// lookupSecretStrict with numeric key: same behavior
+	_, err = c.lookupSecretStrict("42")
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "database connection failed")
+}
+
+// errorAPI wraps fakeAPI but returns a configurable error for Secret().
+type errorAPI struct {
+	*fakeAPI
+	secretErr error
+}
+
+func (e *errorAPI) Secret(_ int) (*server.Secret, error) {
+	if e.secretErr != nil {
+		return nil, e.secretErr
+	}
+	return e.fakeAPI.Secret(0)
+}
+
+// TestFakeAPISecretsReturnsEmptySlice verifies the fakeAPI mock matches real SDK
+// behavior: Secrets() returns ([]Secret{}, nil) for zero matches, not an error.
+func TestFakeAPISecretsReturnsEmptySlice(t *testing.T) {
+	fake := &fakeAPI{secrets: []*server.Secret{}}
+
+	secrets, err := fake.Secrets("nonexistent", "Name")
+	assert.NoError(t, err)
+	assert.NotNil(t, secrets)
+	assert.Empty(t, secrets)
+}
+
+// TestPushSecretMetadataNoFolderID tests that PushSecret requires a folderId
+// for creation when the prefix doesn't provide one.
+func TestPushSecretMetadataNoFolderID(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"my-key": []byte("value"),
+		},
+	}
+
+	// Metadata has secretTemplateId but no folderId
+	metadataJSON := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 0, "secretTemplateId": 1}}`),
+	}
+
+	data := fakePushSecretData{
+		remoteKey: "no-folder-secret",
+		property:  "username",
+		secretKey: "my-key",
+		metadata:  &metadataJSON,
+	}
+	err := c.PushSecret(ctx, secret, data)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "folderId and secretTemplateId must be provided")
+}
+
+// TestPushSecretCreateWithFolderPrefixNoMetadataFolder tests that PushSecret can
+// create a secret when the folderId comes from the prefix even if metadata has
+// folderId: 0, as long as secretTemplateId is provided.
+func TestPushSecretCreateWithFolderPrefixNoMetadataFolder(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"my-key": []byte("prefix-folder-value"),
+		},
+	}
+
+	// Metadata has secretTemplateId but folderId is 0; prefix provides the folder.
+	metadataJSON := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 0, "secretTemplateId": 1}}`),
+	}
+
+	data := fakePushSecretData{
+		remoteKey: "folderId:55/prefix-only-folder",
+		property:  "username",
+		secretKey: "my-key",
+		metadata:  &metadataJSON,
+	}
+	err := c.PushSecret(ctx, secret, data)
+	assert.NoError(t, err)
+
+	// Verify: created in folder 55
+	foundSecrets, _ := c.(*client).api.Secrets("prefix-only-folder", "Name")
+	require.Len(t, foundSecrets, 1)
+	assert.Equal(t, 55, foundSecrets[0].FolderID)
+}
+
+// TestGetSecretByFolderPrefix tests GetSecret with the folderId prefix format.
+func TestGetSecretByFolderPrefix(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	// Secret 9000 is in folder 4, secret 9001 is in folder 5, both named "FolderSecretname"
+	got, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
+		Key:      "folderId:4/FolderSecretname",
+		Property: "username",
+	})
+	assert.NoError(t, err)
+	assert.Equal(t, []byte("usernamevalue"), got)
+
+	// Non-existent folder
+	_, err = c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
+		Key:      "folderId:99/FolderSecretname",
+		Property: "username",
+	})
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "not found")
+}
+
+// TestPushSecretUpdateSecretNoFields tests updating a secret that has no fields.
+func TestPushSecretUpdateSecretNoFields(t *testing.T) {
+	ctx := context.Background()
+
+	// Create a secret with no fields
+	s := &server.Secret{
+		ID:     200,
+		Name:   "no-fields-secret",
+		Fields: []server.SecretField{},
+	}
+	c := &client{api: &fakeAPI{secrets: []*server.Secret{s}}}
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"my-key": []byte("value"),
+		},
+	}
+
+	// Update with empty property → tries to write to first field, but there are none
+	data := fakePushSecretData{
+		remoteKey: "200",
+		property:  "",
+		secretKey: "my-key",
+	}
+	err := c.PushSecret(ctx, secret, data)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "secret has no fields to update")
+}
+
+// TestPushSecretNonExistentTemplateField tests creating a secret with a property that
+// doesn't match any template field.
+func TestPushSecretNonExistentTemplateField(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"my-key": []byte("value"),
+		},
+	}
+
+	metadataJSON := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
+	}
+
+	data := fakePushSecretData{
+		remoteKey: "nonexistent-field-secret",
+		property:  "nonexistent-field",
+		secretKey: "my-key",
+		metadata:  &metadataJSON,
+	}
+	err := c.PushSecret(ctx, secret, data)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "field nonexistent-field not found in secret template")
+}

+ 1 - 1
providers/v1/secretserver/go.mod

@@ -9,6 +9,7 @@ require (
 	github.com/stretchr/testify v1.11.1
 	github.com/tidwall/gjson v1.18.0
 	k8s.io/api v0.35.0
+	k8s.io/apiextensions-apiserver v0.35.0
 	k8s.io/apimachinery v0.35.0
 	sigs.k8s.io/controller-runtime v0.23.1
 )
@@ -86,7 +87,6 @@ require (
 	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/apiextensions-apiserver v0.35.0 // indirect
 	k8s.io/client-go v0.35.0 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect

+ 1 - 1
providers/v1/secretserver/provider.go

@@ -52,7 +52,7 @@ var _ esv1.Provider = &Provider{}
 
 // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
 func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
-	return esv1.SecretStoreReadOnly
+	return esv1.SecretStoreReadWrite
 }
 
 // NewClient creates a new secrets client based on provided store.

+ 4 - 6
providers/v1/secretserver/provider_test.go

@@ -167,7 +167,7 @@ func TestValidateStore(t *testing.T) {
 func TestNewClient(t *testing.T) {
 	userNameKey := "username"
 	userNameValue := "foo"
-	passwordKey := "password"
+	passwordKey := passwordSlug
 	passwordValue := generateRandomString()
 	domain := "domain1"
 
@@ -633,8 +633,8 @@ func TestCapabilities(t *testing.T) {
 	tests := map[string]struct {
 		want esv1.SecretStoreCapabilities
 	}{
-		"returns ReadOnly capability": {
-			want: esv1.SecretStoreReadOnly,
+		"returns ReadWrite capability": {
+			want: esv1.SecretStoreReadWrite,
 		},
 	}
 
@@ -646,9 +646,7 @@ func TestCapabilities(t *testing.T) {
 
 			// Edge: call Capabilities on nil Provider
 			var nilP *Provider
-			if nilP != nil {
-				assert.Equal(t, esv1.SecretStoreReadOnly, nilP.Capabilities())
-			}
+			assert.Equal(t, esv1.SecretStoreReadWrite, nilP.Capabilities())
 		})
 	}
 }

+ 11 - 0
providers/v1/secretserver/secret_api.go

@@ -23,7 +23,18 @@ import (
 // secretAPI represents the subset of the Secret Server API
 // which is supported by tss-sdk-go/v3.
 type secretAPI interface {
+	// Secret retrieves a secret by its ID.
 	Secret(id int) (*server.Secret, error)
+	// Secrets searches for secrets by text and field name.
 	Secrets(searchText, field string) ([]server.Secret, error)
+	// SecretByPath retrieves a secret using its folder path.
 	SecretByPath(secretPath string) (*server.Secret, error)
+	// CreateSecret creates a new secret in Secret Server.
+	CreateSecret(secret server.Secret) (*server.Secret, error)
+	// UpdateSecret updates an existing secret in Secret Server.
+	UpdateSecret(secret server.Secret) (*server.Secret, error)
+	// DeleteSecret deletes a secret by its ID.
+	DeleteSecret(id int) error
+	// SecretTemplate retrieves a secret template by its ID.
+	SecretTemplate(id int) (*server.SecretTemplate, error)
 }