Prechádzať zdrojové kódy

feat: add aws provider runtime

Moritz Johner 1 mesiac pred
rodič
commit
b5589552ba
34 zmenil súbory, kde vykonal 10065 pridanie a 0 odobranie
  1. 25 0
      providers/v2/aws/Dockerfile
  2. 94 0
      providers/v2/aws/config.go
  3. 120 0
      providers/v2/aws/config_test.go
  4. 189 0
      providers/v2/aws/generator/ecr.go
  5. 201 0
      providers/v2/aws/generator/ecr_test.go
  6. 69 0
      providers/v2/aws/generator/resolver.go
  7. 142 0
      providers/v2/aws/generator/sts.go
  8. 155 0
      providers/v2/aws/generator/sts_test.go
  9. 139 0
      providers/v2/aws/go.mod
  10. 301 0
      providers/v2/aws/go.sum
  11. 160 0
      providers/v2/aws/main.go
  12. 34 0
      providers/v2/aws/provider.yaml
  13. 356 0
      providers/v2/aws/store/auth/auth.go
  14. 831 0
      providers/v2/aws/store/auth/auth_test.go
  15. 54 0
      providers/v2/aws/store/auth/fake/assumeroler.go
  16. 52 0
      providers/v2/aws/store/auth/resolver.go
  17. 39 0
      providers/v2/aws/store/auth/resolver_test.go
  18. 54 0
      providers/v2/aws/store/auth/token_fetcher.go
  19. 36 0
      providers/v2/aws/store/auth/token_fetcher_test.go
  20. 186 0
      providers/v2/aws/store/parameterstore/fake/fake.go
  21. 710 0
      providers/v2/aws/store/parameterstore/parameterstore.go
  22. 1308 0
      providers/v2/aws/store/parameterstore/parameterstore_test.go
  23. 48 0
      providers/v2/aws/store/parameterstore/resolver.go
  24. 272 0
      providers/v2/aws/store/secretsmanager/fake/fake.go
  25. 52 0
      providers/v2/aws/store/secretsmanager/resolver.go
  26. 964 0
      providers/v2/aws/store/secretsmanager/secretsmanager.go
  27. 2308 0
      providers/v2/aws/store/secretsmanager/secretsmanager_test.go
  28. 247 0
      providers/v2/aws/store/store.go
  29. 542 0
      providers/v2/aws/store/store_test.go
  30. 37 0
      providers/v2/aws/store/util/errors.go
  31. 53 0
      providers/v2/aws/store/util/errors_test.go
  32. 107 0
      providers/v2/aws/store/util/provider.go
  33. 135 0
      providers/v2/aws/store/util/provider_test.go
  34. 45 0
      providers/v2/aws/store/util/validation.go

+ 25 - 0
providers/v2/aws/Dockerfile

@@ -0,0 +1,25 @@
+# Multi-stage build for AWS Provider Provider
+# Generated by providers/v2/hack/generate-provider-main.go. DO NOT EDIT.
+FROM golang:1.26.2-alpine AS builder
+
+WORKDIR /workspace
+COPY apis/ apis/
+COPY providers/ providers/
+COPY runtime/ runtime/
+COPY generators/ generators/
+
+# Build the provider binary
+WORKDIR /workspace/providers/v2/aws
+RUN go mod tidy && CGO_ENABLED=0 GOOS=linux go build -a -o provider-aws .
+
+# Use distroless as minimal base image
+FROM gcr.io/distroless/static:nonroot
+
+WORKDIR /
+
+# Copy the binary
+COPY --from=builder /workspace/providers/v2/aws/provider-aws .
+
+USER 65532:65532
+
+ENTRYPOINT ["/provider-aws"]

+ 94 - 0
providers/v2/aws/config.go

@@ -0,0 +1,94 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package main provides the AWS provider configuration and spec mapper implementation.
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	v1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+	pb "github.com/external-secrets/external-secrets/proto/provider"
+)
+
+// GetSpecMapper returns the spec mapper function for the AWS provider.
+// This function converts v2 ProviderReference to v1 SecretStoreSpec.
+func GetSpecMapper(kubeClient client.Client) func(*pb.ProviderReference, string) (*v1.SecretStoreSpec, error) {
+	return func(ref *pb.ProviderReference, sourceNamespace string) (*v1.SecretStoreSpec, error) {
+		namespace := ref.Namespace
+		if namespace == "" {
+			namespace = sourceNamespace
+		}
+
+		switch ref.Kind {
+		case awsv2alpha1.SecretsManagerKind:
+			var awsProvider awsv2alpha1.SecretsManager
+			err := kubeClient.Get(context.Background(), client.ObjectKey{
+				Namespace: namespace,
+				Name:      ref.Name,
+			}, &awsProvider)
+			if err != nil {
+				return nil, err
+			}
+			return &v1.SecretStoreSpec{
+				Provider: &v1.SecretStoreProvider{
+					AWS: &v1.AWSProvider{
+						Service:           v1.AWSServiceSecretsManager,
+						Auth:              awsProvider.Spec.Auth,
+						Role:              awsProvider.Spec.Role,
+						Region:            awsProvider.Spec.Region,
+						AdditionalRoles:   awsProvider.Spec.AdditionalRoles,
+						ExternalID:        awsProvider.Spec.ExternalID,
+						SecretsManager:    awsProvider.Spec.SecretsManager,
+						SessionTags:       awsProvider.Spec.SessionTags,
+						TransitiveTagKeys: awsProvider.Spec.TransitiveTagKeys,
+						Prefix:            awsProvider.Spec.Prefix,
+					},
+				},
+			}, nil
+		case awsv2alpha1.ParameterStoreKind:
+			var awsProvider awsv2alpha1.ParameterStore
+			err := kubeClient.Get(context.Background(), client.ObjectKey{
+				Namespace: namespace,
+				Name:      ref.Name,
+			}, &awsProvider)
+			if err != nil {
+				return nil, err
+			}
+			return &v1.SecretStoreSpec{
+				Provider: &v1.SecretStoreProvider{
+					AWS: &v1.AWSProvider{
+						Service:           v1.AWSServiceParameterStore,
+						Auth:              awsProvider.Spec.Auth,
+						Role:              awsProvider.Spec.Role,
+						Region:            awsProvider.Spec.Region,
+						AdditionalRoles:   awsProvider.Spec.AdditionalRoles,
+						ExternalID:        awsProvider.Spec.ExternalID,
+						SessionTags:       awsProvider.Spec.SessionTags,
+						TransitiveTagKeys: awsProvider.Spec.TransitiveTagKeys,
+						Prefix:            awsProvider.Spec.Prefix,
+					},
+				},
+			}, nil
+		default:
+			return nil, fmt.Errorf("unsupported provider kind: %s", ref.Kind)
+		}
+	}
+}

+ 120 - 0
providers/v2/aws/config_test.go

@@ -0,0 +1,120 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"testing"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+	pb "github.com/external-secrets/external-secrets/proto/provider"
+)
+
+func TestGetSpecMapperMapsParameterStore(t *testing.T) {
+	t.Parallel()
+
+	scheme := runtime.NewScheme()
+	if err := clientgoscheme.AddToScheme(scheme); err != nil {
+		t.Fatalf("AddToScheme() error = %v", err)
+	}
+	if err := awsv2alpha1.AddToScheme(scheme); err != nil {
+		t.Fatalf("AddToScheme() error = %v", err)
+	}
+
+	kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&awsv2alpha1.ParameterStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "ps-config",
+			Namespace: "provider-ns",
+		},
+		Spec: awsv2alpha1.ParameterStoreSpec{
+			Region:     "eu-central-1",
+			Role:       "arn:aws:iam::123456789012:role/eso-ssm",
+			Prefix:     "/team-a/",
+			ExternalID: "ext-id",
+		},
+	}).Build()
+
+	spec, err := GetSpecMapper(kubeClient)(&pb.ProviderReference{
+		ApiVersion: awsv2alpha1.GroupVersion.String(),
+		Kind:       awsv2alpha1.ParameterStoreKind,
+		Name:       "ps-config",
+		Namespace:  "provider-ns",
+	}, "workload-ns")
+	if err != nil {
+		t.Fatalf("GetSpecMapper() error = %v", err)
+	}
+	if spec.Provider == nil || spec.Provider.AWS == nil {
+		t.Fatal("expected AWS provider spec to be returned")
+	}
+	if spec.Provider.AWS.Service != esv1.AWSServiceParameterStore {
+		t.Fatalf("expected service %q, got %q", esv1.AWSServiceParameterStore, spec.Provider.AWS.Service)
+	}
+	if spec.Provider.AWS.Region != "eu-central-1" {
+		t.Fatalf("expected region to be preserved, got %q", spec.Provider.AWS.Region)
+	}
+	if spec.Provider.AWS.Role != "arn:aws:iam::123456789012:role/eso-ssm" {
+		t.Fatalf("expected role to be preserved, got %q", spec.Provider.AWS.Role)
+	}
+	if spec.Provider.AWS.Prefix != "/team-a/" {
+		t.Fatalf("expected prefix to be preserved, got %q", spec.Provider.AWS.Prefix)
+	}
+	if spec.Provider.AWS.ExternalID != "ext-id" {
+		t.Fatalf("expected external ID to be preserved, got %q", spec.Provider.AWS.ExternalID)
+	}
+}
+
+func TestGetSpecMapperUsesSourceNamespaceForParameterStore(t *testing.T) {
+	t.Parallel()
+
+	scheme := runtime.NewScheme()
+	if err := clientgoscheme.AddToScheme(scheme); err != nil {
+		t.Fatalf("AddToScheme() error = %v", err)
+	}
+	if err := awsv2alpha1.AddToScheme(scheme); err != nil {
+		t.Fatalf("AddToScheme() error = %v", err)
+	}
+
+	kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&awsv2alpha1.ParameterStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "ps-source-ns",
+			Namespace: "workload-ns",
+		},
+		Spec: awsv2alpha1.ParameterStoreSpec{
+			Region: "eu-west-1",
+		},
+	}).Build()
+
+	spec, err := GetSpecMapper(kubeClient)(&pb.ProviderReference{
+		ApiVersion: awsv2alpha1.GroupVersion.String(),
+		Kind:       awsv2alpha1.ParameterStoreKind,
+		Name:       "ps-source-ns",
+	}, "workload-ns")
+	if err != nil {
+		t.Fatalf("GetSpecMapper() error = %v", err)
+	}
+	if spec.Provider == nil || spec.Provider.AWS == nil {
+		t.Fatal("expected AWS provider spec to be returned")
+	}
+	if spec.Provider.AWS.Service != esv1.AWSServiceParameterStore {
+		t.Fatalf("expected service %q, got %q", esv1.AWSServiceParameterStore, spec.Provider.AWS.Service)
+	}
+}

+ 189 - 0
providers/v2/aws/generator/ecr.go

@@ -0,0 +1,189 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package generator provides functionality for generating authentication tokens for AWS Elastic Container Registry.
+package generator
+
+import (
+	"context"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/ecr"
+	"github.com/aws/aws-sdk-go-v2/service/ecrpublic"
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/yaml"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+	awsauth "github.com/external-secrets/external-secrets/providers/v2/aws/store/auth"
+)
+
+type ecrAPI interface {
+	GetAuthorizationToken(ctx context.Context, params *ecr.GetAuthorizationTokenInput, optFuncs ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error)
+}
+
+type ecrPublicAPI interface {
+	GetAuthorizationToken(ctx context.Context, params *ecrpublic.GetAuthorizationTokenInput, optFuncs ...func(*ecrpublic.Options)) (*ecrpublic.GetAuthorizationTokenOutput, error)
+}
+
+// ECRGenerator implements ECR token generation functionality.
+type ECRGenerator struct{}
+
+const (
+	errECRNoSpec       = "no config spec provided"
+	errECRParseSpec    = "unable to parse spec: %w"
+	errECRCreateSess   = "unable to create aws session: %w"
+	errGetPrivateToken = "unable to get authorization token: %w"
+	errGetPublicToken  = "unable to get public authorization token: %w"
+)
+
+// Generate creates an authentication token for AWS ECR.
+func (g *ECRGenerator) Generate(ctx context.Context, jsonSpec *apiextensions.JSON, kube client.Client, namespace string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
+	return g.generate(ctx, jsonSpec, kube, namespace, ecrPrivateFactory, ecrPublicFactory)
+}
+
+// Cleanup performs any necessary cleanup after token generation.
+func (g *ECRGenerator) Cleanup(_ context.Context, _ *apiextensions.JSON, _ genv1alpha1.GeneratorProviderState, _ client.Client, _ string) error {
+	return nil
+}
+
+func (g *ECRGenerator) generate(
+	ctx context.Context,
+	jsonSpec *apiextensions.JSON,
+	kube client.Client,
+	namespace string,
+	ecrPrivateFunc ecrPrivateFactoryFunc,
+	ecrPublicFunc ecrPublicFactoryFunc,
+) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
+	if jsonSpec == nil {
+		return nil, nil, errors.New(errECRNoSpec)
+	}
+	res, err := parseECRSpec(jsonSpec.Raw)
+	if err != nil {
+		return nil, nil, fmt.Errorf(errECRParseSpec, err)
+	}
+	cfg, err := awsauth.NewGeneratorSession(
+		ctx,
+		esv1.AWSAuth{
+			SecretRef: (*esv1.AWSAuthSecretRef)(res.Spec.Auth.SecretRef),
+			JWTAuth:   (*esv1.AWSJWTAuth)(res.Spec.Auth.JWTAuth),
+		},
+		res.Spec.Role,
+		res.Spec.Region,
+		kube,
+		namespace,
+		awsauth.DefaultSTSProvider,
+		awsauth.DefaultJWTProvider)
+	if err != nil {
+		return nil, nil, fmt.Errorf(errECRCreateSess, err)
+	}
+
+	if res.Spec.Scope == "public" {
+		return fetchECRPublicToken(ctx, cfg, ecrPublicFunc)
+	}
+
+	return fetchECRPrivateToken(ctx, cfg, ecrPrivateFunc)
+}
+
+func fetchECRPrivateToken(ctx context.Context, cfg *aws.Config, ecrPrivateFunc ecrPrivateFactoryFunc) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
+	client := ecrPrivateFunc(cfg)
+	out, err := client.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
+	if err != nil {
+		return nil, nil, fmt.Errorf(errGetPrivateToken, err)
+	}
+	if len(out.AuthorizationData) != 1 {
+		return nil, nil, fmt.Errorf("unexpected number of authorization tokens. expected 1, found %d", len(out.AuthorizationData))
+	}
+
+	// AuthorizationToken is base64 encoded {username}:{password} string
+	decodedToken, err := base64.StdEncoding.DecodeString(*out.AuthorizationData[0].AuthorizationToken)
+	if err != nil {
+		return nil, nil, err
+	}
+	parts := strings.Split(string(decodedToken), ":")
+	if len(parts) != 2 {
+		return nil, nil, errors.New("unexpected token format")
+	}
+
+	exp := out.AuthorizationData[0].ExpiresAt.UTC().Unix()
+	return map[string][]byte{
+		"username":       []byte(parts[0]),
+		"password":       []byte(parts[1]),
+		"proxy_endpoint": []byte(*out.AuthorizationData[0].ProxyEndpoint),
+		"expires_at":     []byte(strconv.FormatInt(exp, 10)),
+	}, nil, nil
+}
+
+func fetchECRPublicToken(ctx context.Context, cfg *aws.Config, ecrPublicFunc ecrPublicFactoryFunc) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
+	client := ecrPublicFunc(cfg)
+	out, err := client.GetAuthorizationToken(ctx, &ecrpublic.GetAuthorizationTokenInput{})
+	if err != nil {
+		return nil, nil, fmt.Errorf(errGetPublicToken, err)
+	}
+
+	decodedToken, err := base64.StdEncoding.DecodeString(*out.AuthorizationData.AuthorizationToken)
+	if err != nil {
+		return nil, nil, err
+	}
+	parts := strings.Split(string(decodedToken), ":")
+	if len(parts) != 2 {
+		return nil, nil, errors.New("unexpected token format")
+	}
+
+	exp := out.AuthorizationData.ExpiresAt.UTC().Unix()
+	return map[string][]byte{
+		"username":   []byte(parts[0]),
+		"password":   []byte(parts[1]),
+		"expires_at": []byte(strconv.FormatInt(exp, 10)),
+	}, nil, nil
+}
+
+type ecrPrivateFactoryFunc func(aws *aws.Config) ecrAPI
+type ecrPublicFactoryFunc func(aws *aws.Config) ecrPublicAPI
+
+func ecrPrivateFactory(cfg *aws.Config) ecrAPI {
+	return ecr.NewFromConfig(*cfg, func(o *ecr.Options) {
+		o.EndpointResolverV2 = ecrCustomEndpointResolver{}
+	})
+}
+
+func ecrPublicFactory(cfg *aws.Config) ecrPublicAPI {
+	return ecrpublic.NewFromConfig(*cfg, func(o *ecrpublic.Options) {
+		o.EndpointResolverV2 = ecrPublicCustomEndpointResolver{}
+	})
+}
+
+func parseECRSpec(data []byte) (*genv1alpha1.ECRAuthorizationToken, error) {
+	var spec genv1alpha1.ECRAuthorizationToken
+	err := yaml.Unmarshal(data, &spec)
+	return &spec, err
+}
+
+// NewECRGenerator creates a new ECRGenerator instance.
+func NewECRGenerator() genv1alpha1.Generator {
+	return &ECRGenerator{}
+}
+
+// ECRKind returns the generator kind.
+func ECRKind() string {
+	return string(genv1alpha1.GeneratorKindECRAuthorizationToken)
+}

+ 201 - 0
providers/v2/aws/generator/ecr_test.go

@@ -0,0 +1,201 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package generator
+
+import (
+	"context"
+	"encoding/base64"
+	"errors"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/ecr"
+	ecrtypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
+	"github.com/aws/aws-sdk-go-v2/service/ecrpublic"
+	ecrpublictypes "github.com/aws/aws-sdk-go-v2/service/ecrpublic/types"
+	v1 "k8s.io/api/core/v1"
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+func TestECRGenerate(t *testing.T) {
+	type args struct {
+		ctx                  context.Context
+		jsonSpec             *apiextensions.JSON
+		kube                 client.Client
+		namespace            string
+		authTokenPrivateFunc func(*ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error)
+		authTokenPublicFunc  func(*ecrpublic.GetAuthorizationTokenInput) (*ecrpublic.GetAuthorizationTokenOutput, error)
+	}
+	tests := []struct {
+		name    string
+		g       *ECRGenerator
+		args    args
+		want    map[string][]byte
+		wantErr bool
+	}{
+		{
+			name: "nil spec",
+			args: args{
+				ctx:      context.Background(),
+				jsonSpec: nil,
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid json",
+			args: args{
+				ctx: context.Background(),
+				authTokenPrivateFunc: func(gati *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) {
+					return nil, errors.New("boom")
+				},
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(``),
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "private ECR full spec",
+			args: args{
+				ctx:       context.Background(),
+				namespace: "foobar",
+				kube: clientfake.NewClientBuilder().WithObjects(&v1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "my-aws-creds",
+						Namespace: "foobar",
+					},
+					Data: map[string][]byte{
+						"key-id":        []byte("foo"),
+						"access-secret": []byte("bar"),
+					},
+				}).Build(),
+				authTokenPrivateFunc: func(in *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) {
+					t := time.Unix(1234, 0)
+					return &ecr.GetAuthorizationTokenOutput{
+						AuthorizationData: []ecrtypes.AuthorizationData{
+							{
+								AuthorizationToken: new(base64.StdEncoding.EncodeToString([]byte("uuser:pass"))),
+								ProxyEndpoint:      new("foo"),
+								ExpiresAt:          &t,
+							},
+						},
+					}, nil
+				},
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(`apiVersion: generators.external-secrets.io/v1alpha1
+kind: ECRAuthorizationToken
+spec:
+  region: eu-west-1
+  role: "my-role"
+  scope: private
+  auth:
+    secretRef:
+      accessKeyIDSecretRef:
+        name: "my-aws-creds"
+        key: "key-id"
+      secretAccessKeySecretRef:
+        name: "my-aws-creds"
+        key: "access-secret"`),
+				},
+			},
+			want: map[string][]byte{
+				"username":       []byte("uuser"),
+				"password":       []byte("pass"),
+				"proxy_endpoint": []byte("foo"),
+				"expires_at":     []byte("1234"),
+			},
+		},
+		{
+			name: "public ECR full spec",
+			args: args{
+				ctx:       context.Background(),
+				namespace: "foobar",
+				authTokenPublicFunc: func(in *ecrpublic.GetAuthorizationTokenInput) (*ecrpublic.GetAuthorizationTokenOutput, error) {
+					t := time.Unix(5678, 0)
+					return &ecrpublic.GetAuthorizationTokenOutput{
+						AuthorizationData: &ecrpublictypes.AuthorizationData{
+							AuthorizationToken: new(base64.StdEncoding.EncodeToString([]byte("pubuser:pubpass"))),
+							ExpiresAt:          &t,
+						},
+					}, nil
+				},
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(`apiVersion: generators.external-secrets.io/v1alpha1
+kind: ECRAuthorizationToken
+spec:
+  region: us-east-1
+  role: "my-role"
+  scope: public`),
+				},
+			},
+			want: map[string][]byte{
+				"username":   []byte("pubuser"),
+				"password":   []byte("pubpass"),
+				"expires_at": []byte("5678"),
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := &ECRGenerator{}
+			got, _, err := g.generate(
+				tt.args.ctx,
+				tt.args.jsonSpec,
+				tt.args.kube,
+				tt.args.namespace,
+				func(cfg *aws.Config) ecrAPI {
+					return &FakeECRPrivate{
+						authTokenFunc: tt.args.authTokenPrivateFunc,
+					}
+				},
+				func(cfg *aws.Config) ecrPublicAPI {
+					return &FakeECRPublic{
+						authTokenFunc: tt.args.authTokenPublicFunc,
+					}
+				},
+			)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Generator.Generate() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Generator.Generate() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+type FakeECRPrivate struct {
+	authTokenFunc func(*ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error)
+}
+
+func (e *FakeECRPrivate) GetAuthorizationToken(ctx context.Context, params *ecr.GetAuthorizationTokenInput, optFns ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error) {
+	return e.authTokenFunc(params)
+}
+
+type FakeECRPublic struct {
+	authTokenFunc func(*ecrpublic.GetAuthorizationTokenInput) (*ecrpublic.GetAuthorizationTokenOutput, error)
+}
+
+func (e *FakeECRPublic) GetAuthorizationToken(ctx context.Context, params *ecrpublic.GetAuthorizationTokenInput, optFns ...func(*ecrpublic.Options)) (*ecrpublic.GetAuthorizationTokenOutput, error) {
+	return e.authTokenFunc(params)
+}

+ 69 - 0
providers/v2/aws/generator/resolver.go

@@ -0,0 +1,69 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package generator
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+	"os"
+
+	"github.com/aws/aws-sdk-go-v2/service/ecr"
+	"github.com/aws/aws-sdk-go-v2/service/ecrpublic"
+	smithyendpoints "github.com/aws/smithy-go/endpoints"
+)
+
+const (
+	// ECREndpointEnv is the environment variable name for specifying a custom ECR endpoint.
+	ECREndpointEnv = "AWS_ECR_ENDPOINT"
+	// ECRPublicEndpointEnv is the environment variable name for specifying a custom ECR Public endpoint.
+	ECRPublicEndpointEnv = "AWS_ECR_PUBLIC_ENDPOINT"
+)
+
+type ecrCustomEndpointResolver struct{}
+
+// ResolveEndpoint returns a ResolverFunc with customizable endpoints.
+func (c ecrCustomEndpointResolver) ResolveEndpoint(ctx context.Context, params ecr.EndpointParameters) (smithyendpoints.Endpoint, error) {
+	endpoint := smithyendpoints.Endpoint{}
+	if v := os.Getenv(ECREndpointEnv); v != "" {
+		url, err := url.Parse(v)
+		if err != nil {
+			return endpoint, fmt.Errorf("failed to parse ecr endpoint %s: %w", v, err)
+		}
+		endpoint.URI = *url
+		return endpoint, nil
+	}
+	defaultResolver := ecr.NewDefaultEndpointResolverV2()
+	return defaultResolver.ResolveEndpoint(ctx, params)
+}
+
+type ecrPublicCustomEndpointResolver struct{}
+
+// ResolveEndpoint returns a ResolverFunc with customizable endpoints.
+func (c ecrPublicCustomEndpointResolver) ResolveEndpoint(ctx context.Context, params ecrpublic.EndpointParameters) (smithyendpoints.Endpoint, error) {
+	endpoint := smithyendpoints.Endpoint{}
+	if v := os.Getenv(ECRPublicEndpointEnv); v != "" {
+		url, err := url.Parse(v)
+		if err != nil {
+			return endpoint, fmt.Errorf("failed to parse ecr public endpoint %s: %w", v, err)
+		}
+		endpoint.URI = *url
+		return endpoint, nil
+	}
+	defaultResolver := ecrpublic.NewDefaultEndpointResolverV2()
+	return defaultResolver.ResolveEndpoint(ctx, params)
+}

+ 142 - 0
providers/v2/aws/generator/sts.go

@@ -0,0 +1,142 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package generator implements a generator for AWS STS session tokens.
+package generator
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strconv"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/sts"
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/yaml"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+	awsauth "github.com/external-secrets/external-secrets/providers/v2/aws/store/auth"
+)
+
+// stsAPI defines the methods needed for the STS generator.
+type stsAPI interface {
+	GetSessionToken(ctx context.Context, params *sts.GetSessionTokenInput, optFns ...func(*sts.Options)) (*sts.GetSessionTokenOutput, error)
+}
+
+// STSGenerator implements a generator for AWS STS session tokens.
+type STSGenerator struct{}
+
+// const error messages.
+const (
+	errSTSNoSpec     = "no config spec provided"
+	errSTSParseSpec  = "unable to parse spec: %w"
+	errSTSCreateSess = "unable to create aws session: %w"
+	errSTSGetToken   = "unable to get authorization token: %w"
+)
+
+// Generate creates AWS STS session tokens and returns credentials.
+func (g *STSGenerator) Generate(ctx context.Context, jsonSpec *apiextensions.JSON, kube client.Client, namespace string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
+	return g.generate(ctx, jsonSpec, kube, namespace, stsFactory)
+}
+
+func (g *STSGenerator) generate(
+	ctx context.Context,
+	jsonSpec *apiextensions.JSON,
+	kube client.Client,
+	namespace string,
+	stsFunc stsFactoryFunc,
+) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
+	if jsonSpec == nil {
+		return nil, nil, errors.New(errSTSNoSpec)
+	}
+	res, err := parseSTSSpec(jsonSpec.Raw)
+	if err != nil {
+		return nil, nil, fmt.Errorf(errSTSParseSpec, err)
+	}
+	if res.Spec.Auth.JWTAuth != nil {
+		return nil, nil, errors.New("jwt auth cannot be used for STS Session Token generation")
+	}
+	cfg, err := awsauth.NewGeneratorSession(
+		ctx,
+		esv1.AWSAuth{
+			SecretRef: (*esv1.AWSAuthSecretRef)(res.Spec.Auth.SecretRef),
+		},
+		res.Spec.Role,
+		res.Spec.Region,
+		kube,
+		namespace,
+		awsauth.DefaultSTSProvider,
+		awsauth.DefaultJWTProvider)
+	if err != nil {
+		return nil, nil, fmt.Errorf(errSTSCreateSess, err)
+	}
+	api := stsFunc(cfg)
+	input := &sts.GetSessionTokenInput{}
+	if res.Spec.RequestParameters != nil {
+		input.DurationSeconds = res.Spec.RequestParameters.SessionDuration
+		input.TokenCode = res.Spec.RequestParameters.TokenCode
+		input.SerialNumber = res.Spec.RequestParameters.SerialNumber
+	}
+	out, err := api.GetSessionToken(ctx, input)
+	if err != nil {
+		return nil, nil, fmt.Errorf(errSTSGetToken, err)
+	}
+	if out.Credentials == nil {
+		return nil, nil, errors.New("no credentials found")
+	}
+
+	return map[string][]byte{
+		"access_key_id":     []byte(*out.Credentials.AccessKeyId),
+		"expiration":        []byte(strconv.FormatInt(out.Credentials.Expiration.Unix(), 10)),
+		"secret_access_key": []byte(*out.Credentials.SecretAccessKey),
+		"session_token":     []byte(*out.Credentials.SessionToken),
+	}, nil, nil
+}
+
+// Cleanup is a no-op for STS generator as it doesn't require any cleanup.
+func (g *STSGenerator) Cleanup(_ context.Context, _ *apiextensions.JSON, _ genv1alpha1.GeneratorProviderState, _ client.Client, _ string) error {
+	return nil
+}
+
+type stsFactoryFunc func(cfg *aws.Config) stsAPI
+
+func stsFactory(cfg *aws.Config) stsAPI {
+	return sts.NewFromConfig(*cfg)
+}
+
+func parseSTSSpec(data []byte) (*genv1alpha1.STSSessionToken, error) {
+	var spec genv1alpha1.STSSessionToken
+	err := yaml.Unmarshal(data, &spec)
+	return &spec, err
+}
+
+// NewSTSGenerator creates a new STSGenerator instance.
+func NewSTSGenerator() genv1alpha1.Generator {
+	return &STSGenerator{}
+}
+
+// NewGenerator is kept as a compatibility alias.
+func NewGenerator() genv1alpha1.Generator {
+	return NewSTSGenerator()
+}
+
+// STSKind returns the generator kind.
+func STSKind() string {
+	return string(genv1alpha1.GeneratorKindSTSSessionToken)
+}

+ 155 - 0
providers/v2/aws/generator/sts_test.go

@@ -0,0 +1,155 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package generator
+
+import (
+	"context"
+	"errors"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/sts"
+	ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
+	v1 "k8s.io/api/core/v1"
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	"github.com/external-secrets/external-secrets/runtime/esutils"
+)
+
+func TestSTSGenerate(t *testing.T) {
+	type args struct {
+		ctx       context.Context
+		jsonSpec  *apiextensions.JSON
+		kube      client.Client
+		namespace string
+		tokenFunc func(context.Context, *sts.GetSessionTokenInput, ...func(*sts.Options)) (*sts.GetSessionTokenOutput, error)
+	}
+	tests := []struct {
+		name    string
+		g       *STSGenerator
+		args    args
+		want    map[string][]byte
+		wantErr bool
+	}{
+		{
+			name: "nil spec",
+			args: args{
+				ctx:      context.Background(),
+				jsonSpec: nil,
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid json",
+			args: args{
+				ctx: context.Background(),
+				tokenFunc: func(ctx context.Context, input *sts.GetSessionTokenInput, optFns ...func(*sts.Options)) (*sts.GetSessionTokenOutput, error) {
+					return nil, errors.New("boom")
+				},
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(``),
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "full spec",
+			args: args{
+				ctx:       context.Background(),
+				namespace: "foobar",
+				kube: clientfake.NewClientBuilder().WithObjects(&v1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "my-aws-creds",
+						Namespace: "foobar",
+					},
+					Data: map[string][]byte{
+						"key-id":        []byte("foo"),
+						"access-secret": []byte("bar"),
+					},
+				}).Build(),
+				tokenFunc: func(ctx context.Context, input *sts.GetSessionTokenInput, optFns ...func(*sts.Options)) (*sts.GetSessionTokenOutput, error) {
+					t := time.Unix(1234, 0)
+					return &sts.GetSessionTokenOutput{
+						Credentials: &ststypes.Credentials{
+							AccessKeyId:     esutils.Ptr("access-key-id"),
+							Expiration:      esutils.Ptr(t),
+							SecretAccessKey: esutils.Ptr("secret-access-key"),
+							SessionToken:    esutils.Ptr("session-token"),
+						},
+					}, nil
+				},
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(`apiVersion: generators.external-secrets.io/v1alpha1
+kind: STSSessionToken
+spec:
+  region: eu-west-1
+  role: "my-role"
+  auth:
+    secretRef:
+      accessKeyIDSecretRef:
+        name: "my-aws-creds"
+        key: "key-id"
+      secretAccessKeySecretRef:
+        name: "my-aws-creds"
+        key: "access-secret"`),
+				},
+			},
+			want: map[string][]byte{
+				"access_key_id":     []byte("access-key-id"),
+				"expiration":        []byte("1234"),
+				"secret_access_key": []byte("secret-access-key"),
+				"session_token":     []byte("session-token"),
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := &STSGenerator{}
+			got, _, err := g.generate(
+				tt.args.ctx,
+				tt.args.jsonSpec,
+				tt.args.kube,
+				tt.args.namespace,
+				func(cfg *aws.Config) stsAPI {
+					return &FakeSTS{
+						getSessionToken: tt.args.tokenFunc,
+					}
+				},
+			)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Generator.Generate() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Generator.Generate() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+type FakeSTS struct {
+	getSessionToken func(context.Context, *sts.GetSessionTokenInput, ...func(*sts.Options)) (*sts.GetSessionTokenOutput, error)
+}
+
+func (e *FakeSTS) GetSessionToken(ctx context.Context, params *sts.GetSessionTokenInput, optFns ...func(*sts.Options)) (*sts.GetSessionTokenOutput, error) {
+	return e.getSessionToken(ctx, params, optFns...)
+}

+ 139 - 0
providers/v2/aws/go.mod

@@ -0,0 +1,139 @@
+module github.com/external-secrets/external-secrets/providers/v2/aws
+
+go 1.26.2
+
+require (
+	github.com/aws/aws-sdk-go-v2 v1.41.5
+	github.com/aws/aws-sdk-go-v2/config v1.31.19
+	github.com/aws/aws-sdk-go-v2/credentials v1.18.23
+	github.com/aws/aws-sdk-go-v2/service/ecr v1.57.0
+	github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.13
+	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.12
+	github.com/aws/aws-sdk-go-v2/service/ssm v1.67.1
+	github.com/aws/aws-sdk-go-v2/service/sts v1.40.1
+	github.com/aws/smithy-go v1.24.2
+	github.com/external-secrets/external-secrets/apis v0.0.0
+	github.com/external-secrets/external-secrets/proto v0.0.0
+	github.com/external-secrets/external-secrets/providers/v1/aws v0.0.0-00010101000000-000000000000
+	github.com/external-secrets/external-secrets/providers/v2/adapter/generator v0.0.0-00010101000000-000000000000
+	github.com/external-secrets/external-secrets/providers/v2/adapter/store v0.0.0
+	github.com/external-secrets/external-secrets/providers/v2/common/grpc/server v0.0.0-00010101000000-000000000000
+	github.com/external-secrets/external-secrets/runtime v0.0.0
+	github.com/google/go-cmp v0.7.0
+	github.com/google/uuid v1.6.0
+	github.com/stretchr/testify v1.11.1
+	github.com/tidwall/gjson v1.18.0
+	github.com/tidwall/sjson v1.2.5
+	google.golang.org/grpc v1.79.3
+	k8s.io/api v0.35.2
+	k8s.io/apiextensions-apiserver v0.35.2
+	k8s.io/apimachinery v0.35.2
+	k8s.io/client-go v0.35.2
+	sigs.k8s.io/controller-runtime v0.23.3
+	sigs.k8s.io/yaml v1.6.0
+)
+
+require (
+	dario.cat/mergo v1.0.2 // indirect
+	github.com/Masterminds/goutils v1.1.1 // indirect
+	github.com/Masterminds/semver/v3 v3.4.0 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.30.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.6 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
+	github.com/emicklei/go-restful/v3 v3.13.0 // indirect
+	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
+	github.com/external-secrets/external-secrets/providers/v2/common v0.0.0 // indirect
+	github.com/fsnotify/fsnotify v1.9.0 // indirect
+	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
+	github.com/go-openapi/jsonpointer v0.22.5 // indirect
+	github.com/go-openapi/jsonreference v0.21.5 // indirect
+	github.com/go-openapi/swag v0.25.5 // indirect
+	github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
+	github.com/go-openapi/swag/conv v0.25.5 // indirect
+	github.com/go-openapi/swag/fileutils v0.25.5 // indirect
+	github.com/go-openapi/swag/jsonname v0.25.5 // indirect
+	github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
+	github.com/go-openapi/swag/loading v0.25.5 // indirect
+	github.com/go-openapi/swag/mangling v0.25.5 // indirect
+	github.com/go-openapi/swag/netutils v0.25.5 // indirect
+	github.com/go-openapi/swag/stringutils v0.25.5 // indirect
+	github.com/go-openapi/swag/typeutils v0.25.5 // indirect
+	github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
+	github.com/goccy/go-json v0.10.5 // indirect
+	github.com/google/btree v1.1.3 // indirect
+	github.com/google/gnostic-models v0.7.1 // indirect
+	github.com/huandu/xstrings v1.5.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/lestrrat-go/blackmagic v1.0.4 // indirect
+	github.com/lestrrat-go/httpcc v1.0.1 // indirect
+	github.com/lestrrat-go/httprc v1.0.6 // indirect
+	github.com/lestrrat-go/iter v1.0.2 // indirect
+	github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect
+	github.com/lestrrat-go/option v1.0.1 // indirect
+	github.com/mitchellh/copystructure v1.2.0 // indirect
+	github.com/mitchellh/reflectwalk v1.0.2 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/prometheus/client_golang v1.23.2 // indirect
+	github.com/prometheus/client_model v0.6.2 // indirect
+	github.com/prometheus/common v0.67.5 // indirect
+	github.com/prometheus/procfs v0.20.1 // indirect
+	github.com/segmentio/asm v1.2.1 // indirect
+	github.com/shopspring/decimal v1.4.0 // indirect
+	github.com/spf13/cast v1.10.0 // indirect
+	github.com/spf13/pflag v1.0.10 // indirect
+	github.com/tidwall/match v1.2.0 // indirect
+	github.com/tidwall/pretty v1.2.1 // indirect
+	github.com/x448/float16 v0.8.4 // indirect
+	go.yaml.in/yaml/v2 v2.4.4 // indirect
+	go.yaml.in/yaml/v3 v3.0.4 // indirect
+	golang.org/x/crypto v0.49.0 // indirect
+	golang.org/x/net v0.52.0 // indirect
+	golang.org/x/oauth2 v0.36.0 // indirect
+	golang.org/x/sync v0.20.0 // indirect
+	golang.org/x/sys v0.42.0 // indirect
+	golang.org/x/term v0.41.0 // indirect
+	golang.org/x/text v0.35.0 // indirect
+	golang.org/x/time v0.15.0 // indirect
+	gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 // indirect
+	google.golang.org/protobuf v1.36.11 // indirect
+	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/klog/v2 v2.140.0 // indirect
+	k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf // indirect
+	k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
+	sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
+	sigs.k8s.io/randfill v1.0.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
+	software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
+)
+
+replace (
+	github.com/external-secrets/external-secrets => ../../..
+	github.com/external-secrets/external-secrets/apis => ../../../apis
+	github.com/external-secrets/external-secrets/generators/v1/ecr => ../../../generators/v1/ecr
+	github.com/external-secrets/external-secrets/generators/v1/sts => ../../../generators/v1/sts
+	github.com/external-secrets/external-secrets/proto => ../common/proto
+	github.com/external-secrets/external-secrets/providers/v1/aws => ../../v1/aws
+	github.com/external-secrets/external-secrets/providers/v2/adapter => ../../v2/adapter
+	github.com/external-secrets/external-secrets/providers/v2/adapter/generator => ../adapter/generator
+	github.com/external-secrets/external-secrets/providers/v2/adapter/store => ../adapter/store
+	github.com/external-secrets/external-secrets/providers/v2/common => ../common
+	github.com/external-secrets/external-secrets/providers/v2/common/grpc/server => ../common/grpc/server
+	github.com/external-secrets/external-secrets/providers/v2/common/proto => ../common/proto
+	github.com/external-secrets/external-secrets/runtime => ../../../runtime
+)

+ 301 - 0
providers/v2/aws/go.sum

@@ -0,0 +1,301 @@
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
+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/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
+github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
+github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
+github.com/aws/aws-sdk-go-v2/config v1.31.19 h1:qdUtOw4JhZr2YcKO3g0ho/IcFXfXrrb8xlX05Y6EvSw=
+github.com/aws/aws-sdk-go-v2/config v1.31.19/go.mod h1:tMJ8bur01t8eEm0atLadkIIFA154OJ4JCKZeQ+o+R7k=
+github.com/aws/aws-sdk-go-v2/credentials v1.18.23 h1:IQILcxVgMO2BVLaJ2aAv21dKWvE1MduNrbvuK43XL2Q=
+github.com/aws/aws-sdk-go-v2/credentials v1.18.23/go.mod h1:JRodHszhVdh5TPUknxDzJzrMiznG+M+FfR3WSWKgCI8=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.57.0 h1:ukTOYLhugNGMDgl3RGQdb+Jeo2eV6Npsn2z2nvgCvXc=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.57.0/go.mod h1:BcTorrilUv1q7JyC3X8qiVc48qJpttMTIb3bdVV92lA=
+github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.13 h1:mRgG1o6IKIDYiOtpLmQ18yf1GxDOSCzqv2ch4gf9kZU=
+github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.13/go.mod h1:9NhDlaA8e8G5r64GicBAHiIC/1ZOIIZqrKP9D6/WwLg=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.12 h1:xN4mw6Gqim0jMwjmlNST+yXVShFPwSAjt4gXqi43W6I=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.12/go.mod h1:QgVIY03/XoQs2iFr0MbQuQ/Tf1RwlkOvuySWMh1wph4=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.67.1 h1:Zl+dJQSS5RogzWXBdS3eo5aVeHm/se5BGR1JrcIU+pA=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.67.1/go.mod h1:uNHuYAQazkHqpD+hVomA2+eDSuKJzerno7Fnha6N6/Y=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.2 h1:/p6MxkbQoCzaGQT3WO0JwG0FlQyG9RD8VmdmoKc5xqU=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.2/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.6 h1:0dES42T2dhICCbVB3JSTTn7+Bz93wfJEK1b7jksZIyQ=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.6/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
+github.com/aws/aws-sdk-go-v2/service/sts v1.40.1 h1:5sbIM57lHLaEaNWdIx23JH30LNBsSDkjN/QXGcRLAFc=
+github.com/aws/aws-sdk-go-v2/service/sts v1.40.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
+github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
+github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
+github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
+github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
+github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
+github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
+github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
+github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
+github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
+github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
+github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
+github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
+github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU=
+github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA=
+github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c=
+github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
+github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
+github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
+github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk=
+github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc=
+github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
+github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
+github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
+github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
+github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
+github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
+github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw=
+github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY=
+github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU=
+github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14=
+github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
+github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
+github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
+github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
+github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
+github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
+github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=
+github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
+github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
+github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
+github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
+github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
+github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
+github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
+github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
+github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
+github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
+github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
+github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
+github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
+github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA=
+github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU=
+github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
+github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc=
+github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
+github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
+github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
+github.com/oracle/oci-go-sdk/v65 v65.103.0 h1:HfyZx+JefCPK3At0Xt45q+wr914jDXuoyzOFX3XCbno=
+github.com/oracle/oci-go-sdk/v65 v65.103.0/go.mod h1:oB8jFGVc/7/zJ+DbleE8MzGHjhs2ioCz5stRTdZdIcY=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
+github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
+github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
+github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
+github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
+github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
+github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
+github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
+github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
+github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
+github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
+go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
+go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
+go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
+go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
+go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
+go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
+go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
+go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
+go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
+go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
+golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
+golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
+golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
+golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
+golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
+golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
+golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
+golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
+golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
+golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
+golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
+golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
+golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
+golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
+gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
+gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 h1:aJmi6DVGGIStN9Mobk/tZOOQUBbj0BPjZjjnOdoZKts=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
+google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
+gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
+k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
+k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0=
+k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU=
+k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
+k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
+k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
+k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
+k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
+k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
+k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf h1:btPscg4cMql0XdYK2jLsJcNEKmACJz8l+U7geC06FiM=
+k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
+k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
+k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
+sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80=
+sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0=
+software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

+ 160 - 0
providers/v2/aws/main.go

@@ -0,0 +1,160 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by providers/v2/hack/generate-provider-main.go. DO NOT EDIT.
+
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"log"
+	"net"
+	"os/signal"
+	"syscall"
+
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+	genpb "github.com/external-secrets/external-secrets/proto/generator"
+	pb "github.com/external-secrets/external-secrets/proto/provider"
+	adaptergenerator "github.com/external-secrets/external-secrets/providers/v2/adapter/generator"
+	adapterstore "github.com/external-secrets/external-secrets/providers/v2/adapter/store"
+	generator "github.com/external-secrets/external-secrets/providers/v2/aws/generator"
+	store "github.com/external-secrets/external-secrets/providers/v2/aws/store"
+	grpcserver "github.com/external-secrets/external-secrets/providers/v2/common/grpc/server"
+	"google.golang.org/grpc/health"
+	"google.golang.org/grpc/health/grpc_health_v1"
+	"google.golang.org/grpc/reflection"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/config"
+)
+
+var (
+	port      = flag.Int("port", 8080, "The server port")
+	enableTLS = flag.Bool("enable-tls", true, "Enable TLS/mTLS for gRPC server")
+	verbose   = flag.Bool("verbose", false, "Enable verbose connection-level debugging")
+)
+
+func main() {
+	flag.Parse()
+	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+	defer stop()
+
+	log.Printf("starting on port %d (TLS: %v, Verbose: %v)", *port, *enableTLS, *verbose)
+
+	// Create Kubernetes client (required by adapter)
+	scheme := runtime.NewScheme()
+	_ = clientgoscheme.AddToScheme(scheme)
+	_ = awsv2alpha1.AddToScheme(scheme)
+	_ = genv1alpha1.AddToScheme(scheme)
+
+	cfg, err := config.GetConfig()
+	if err != nil {
+		log.Fatalf("Failed to get kubeconfig: %v", err)
+	}
+
+	kubeClient, err := client.New(cfg, client.Options{Scheme: scheme})
+	if err != nil {
+		log.Fatalf("Failed to create Kubernetes client: %v", err)
+	}
+	// Setup v1 provider(s)
+	v1Provider0 := store.NewProvider()
+	v1Provider1 := store.NewProvider()
+	compatibilityProvider := v1Provider0
+	providerMapping := adapterstore.ProviderMapping{
+		schema.GroupVersionKind{
+			Group:   "provider.external-secrets.io",
+			Version: "v2alpha1",
+			Kind:    "SecretsManager",
+		}: v1Provider0,
+		schema.GroupVersionKind{
+			Group:   "provider.external-secrets.io",
+			Version: "v2alpha1",
+			Kind:    "ParameterStore",
+		}: v1Provider1,
+	}
+
+	specMapper := GetSpecMapper(kubeClient)
+	// Setup v1 generator(s)
+	generatorMapping := adaptergenerator.Mapping{
+		schema.GroupVersionKind{
+			Group:   "generators.external-secrets.io",
+			Version: "v1alpha1",
+			Kind:    "ECRAuthorizationToken",
+		}: generator.NewECRGenerator(),
+		schema.GroupVersionKind{
+			Group:   "generators.external-secrets.io",
+			Version: "v1alpha1",
+			Kind:    "STSSessionToken",
+		}: generator.NewSTSGenerator(),
+	}
+	storeServer := adapterstore.NewServerWithCompatibilityProvider(kubeClient, providerMapping, specMapper, compatibilityProvider)
+	generatorServer := adaptergenerator.NewServer(kubeClient, scheme, generatorMapping)
+
+	log.Printf("[PROVIDER] Using v1 AWS Provider provider with generators wrapped with v2 adapter")
+	grpcServer, err := grpcserver.NewGRPCServer(grpcserver.ServerOptions{
+		EnableTLS: *enableTLS,
+		Verbose:   *verbose,
+	})
+	if err != nil {
+		log.Fatalf("Failed to create gRPC server: %v", err)
+	}
+	metricsServer := grpcserver.NewMetricsServer(grpcserver.DefaultMetricsPort, nil)
+	if err := grpcserver.RegisterMetrics(metricsServer.GetRegistry()); err != nil {
+		log.Fatalf("Failed to register metrics: %v", err)
+	}
+
+	// Register services
+	pb.RegisterSecretStoreProviderServer(grpcServer, storeServer)
+	genpb.RegisterGeneratorProviderServer(grpcServer, generatorServer)
+
+	// Register health service
+	healthServer := health.NewServer()
+	grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
+	healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
+
+	// Register reflection service for debugging
+	reflection.Register(grpcServer)
+
+	// Start listening
+	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
+	if err != nil {
+		log.Fatalf("Failed to listen: %v", err)
+	}
+
+	go func() {
+		if err := metricsServer.Start(ctx); err != nil {
+			log.Fatalf("Failed to start metrics server: %v", err)
+		}
+	}()
+
+	// Handle graceful shutdown
+	go func() {
+		<-ctx.Done()
+		log.Printf("Received shutdown signal, stopping gRPC server...")
+		grpcServer.GracefulStop()
+	}()
+
+	// Start serving
+	log.Printf("AWS Provider Provider listening on %s", lis.Addr().String())
+	if err := grpcServer.Serve(lis); err != nil {
+		log.Fatalf("Failed to serve: %v", err)
+	}
+}

+ 34 - 0
providers/v2/aws/provider.yaml

@@ -0,0 +1,34 @@
+provider:
+  name: aws
+  displayName: "AWS Provider"
+  v2Package: "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+
+stores:
+  - gvk:
+      group: "provider.external-secrets.io"
+      version: "v2alpha1"
+      kind: "SecretsManager"
+    v1Provider: "github.com/external-secrets/external-secrets/providers/v2/aws/store"
+    v1ProviderFunc: "NewProvider"
+  - gvk:
+      group: "provider.external-secrets.io"
+      version: "v2alpha1"
+      kind: "ParameterStore"
+    v1Provider: "github.com/external-secrets/external-secrets/providers/v2/aws/store"
+    v1ProviderFunc: "NewProvider"
+
+generators:
+  - gvk:
+      group: "generators.external-secrets.io"
+      version: "v1alpha1"
+      kind: "ECRAuthorizationToken"
+    v1Generator: "github.com/external-secrets/external-secrets/providers/v2/aws/generator"
+    v1GeneratorFunc: "NewECRGenerator"
+  - gvk:
+      group: "generators.external-secrets.io"
+      version: "v1alpha1"
+      kind: "STSSessionToken"
+    v1Generator: "github.com/external-secrets/external-secrets/providers/v2/aws/generator"
+    v1GeneratorFunc: "NewSTSGenerator"
+
+configPackage: "."

+ 356 - 0
providers/v2/aws/store/auth/auth.go

@@ -0,0 +1,356 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package auth provides authentication functionality for the AWS provider, handling
+// various authentication methods including static credentials, IAM roles,
+// and web identity tokens.
+package auth
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/config"
+	"github.com/aws/aws-sdk-go-v2/credentials"
+	"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
+	"github.com/aws/aws-sdk-go-v2/service/sts"
+	stsTypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/kubernetes"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	awsutil "github.com/external-secrets/external-secrets/providers/v1/aws/util"
+	"github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
+)
+
+// Config contains configuration to create a new AWS provider.
+type Config struct {
+	AssumeRole string
+	Region     string
+	APIRetries int
+}
+
+var (
+	log = ctrl.Log.WithName("provider").WithName("aws")
+)
+
+const (
+	roleARNAnnotation    = "eks.amazonaws.com/role-arn"
+	audienceAnnotation   = "eks.amazonaws.com/audience"
+	defaultTokenAudience = "sts.amazonaws.com"
+
+	errFetchAKIDSecret = "could not fetch accessKeyID secret: %w"
+	errFetchSAKSecret  = "could not fetch SecretAccessKey secret: %w"
+	errFetchSTSecret   = "could not fetch SessionToken secret: %w"
+)
+
+// Opts define options for New function.
+type Opts struct {
+	Store       esv1.GenericStore
+	Kube        client.Client
+	Namespace   string
+	AssumeRoler STSProvider
+	JWTProvider jwtProviderFactory
+}
+
+// New creates a new aws config based on the provided store
+// it uses the following authentication mechanisms in order:
+// * service-account token authentication via AssumeRoleWithWebIdentity
+// * static credentials from a Kind=Secret, optionally with doing a AssumeRole.
+// * sdk default provider chain, see: https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default
+func New(ctx context.Context, opts Opts) (*aws.Config, error) {
+	prov, err := awsutil.GetAWSProvider(opts.Store)
+	if err != nil {
+		return nil, err
+	}
+	var credsProvider aws.CredentialsProvider
+	isClusterKind := opts.Store.GetObjectKind().GroupVersionKind().Kind == esv1.ClusterSecretStoreKind
+
+	credsProvider, err = constructCredsProvider(ctx, prov, isClusterKind, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	// global endpoint resolver is deprecated, should we EndpointResolverV2 field on service client options
+	var loadCfgOpts []func(*config.LoadOptions) error
+	if credsProvider != nil {
+		loadCfgOpts = append(loadCfgOpts, config.WithCredentialsProvider(credsProvider))
+	}
+	if prov.Region != "" {
+		loadCfgOpts = append(loadCfgOpts, config.WithRegion(prov.Region))
+	}
+
+	return createConfiguration(prov, opts.AssumeRoler, loadCfgOpts)
+}
+
+func createConfiguration(prov *esv1.AWSProvider, assumeRoler STSProvider, loadCfgOpts []func(*config.LoadOptions) error) (*aws.Config, error) {
+	cfg, err := config.LoadDefaultConfig(context.TODO(), loadCfgOpts...)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, aRole := range prov.AdditionalRoles {
+		stsclient := assumeRoler(&cfg)
+		cfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, aRole)
+	}
+
+	sessExtID := prov.ExternalID
+	sessTransitiveTagKeys := prov.TransitiveTagKeys
+	sessTags := make([]stsTypes.Tag, len(prov.SessionTags))
+	for i, tag := range prov.SessionTags {
+		sessTags[i] = stsTypes.Tag{
+			Key:   aws.String(tag.Key),
+			Value: aws.String(tag.Value),
+		}
+	}
+	if prov.Role != "" {
+		stsclient := assumeRoler(&cfg)
+		if sessExtID != "" || sessTags != nil {
+			cfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, prov.Role, setAssumeRoleOptionFn(sessExtID, sessTags, sessTransitiveTagKeys))
+		} else {
+			cfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, prov.Role)
+		}
+	}
+	log.Info("using aws config", "region", cfg.Region, "external id", sessExtID, "credentials", cfg.Credentials)
+
+	return &cfg, nil
+}
+
+func setAssumeRoleOptionFn(sessExtID string, sessTags []stsTypes.Tag, sessTransitiveTagKeys []string) func(p *stscreds.AssumeRoleOptions) {
+	return func(p *stscreds.AssumeRoleOptions) {
+		if sessExtID != "" {
+			p.ExternalID = aws.String(sessExtID)
+		}
+		if sessTags != nil {
+			p.Tags = sessTags
+			if len(sessTransitiveTagKeys) > 0 {
+				p.TransitiveTagKeys = sessTransitiveTagKeys
+			}
+		}
+	}
+}
+
+func constructCredsProvider(ctx context.Context, prov *esv1.AWSProvider, isClusterKind bool, opts Opts) (aws.CredentialsProvider, error) {
+	switch {
+	case prov.Auth.JWTAuth != nil:
+		return credsFromServiceAccount(ctx, prov.Auth, prov.Region, isClusterKind, opts.Kube, opts.Namespace, opts.JWTProvider)
+	case prov.Auth.SecretRef != nil:
+		log.V(1).Info("using credentials from secretRef")
+		return credsFromSecretRef(ctx, prov.Auth, opts.Store.GetKind(), opts.Kube, opts.Namespace)
+	default:
+		return nil, nil
+	}
+}
+
+// NewGeneratorSession creates a new aws session based on the provided store
+// it uses the following authentication mechanisms in order:
+// * service-account token authentication via AssumeRoleWithWebIdentity
+// * static credentials from a Kind=Secret, optionally with doing a AssumeRole.
+// * sdk default provider chain, see: https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default
+func NewGeneratorSession(
+	ctx context.Context,
+	auth esv1.AWSAuth,
+	role, region string,
+	kube client.Client,
+	namespace string,
+	assumeRoler STSProvider,
+	jwtProvider jwtProviderFactory,
+) (*aws.Config, error) {
+	var (
+		credsProvider aws.CredentialsProvider
+		err           error
+	)
+
+	// use credentials via service account token
+	jwtAuth := auth.JWTAuth
+	if jwtAuth != nil {
+		credsProvider, err = credsFromServiceAccount(ctx, auth, region, false, kube, namespace, jwtProvider)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// use credentials from secretRef
+	secretRef := auth.SecretRef
+	if secretRef != nil {
+		log.V(1).Info("using credentials from secretRef")
+		credsProvider, err = credsFromSecretRef(ctx, auth, "", kube, namespace)
+		if err != nil {
+			return nil, err
+		}
+	}
+	awscfg, err := config.LoadDefaultConfig(ctx)
+	if err != nil {
+		return nil, err
+	}
+	if credsProvider != nil {
+		awscfg.Credentials = credsProvider
+	}
+	if region != "" {
+		awscfg.Region = region
+	}
+
+	if role != "" {
+		stsclient := assumeRoler(&awscfg)
+		awscfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, role)
+	}
+	log.Info("using aws config", "region", awscfg.Region, "credentials", awscfg.Credentials)
+	return &awscfg, nil
+}
+
+// credsFromSecretRef pulls access-key / secret-access-key from a secretRef to
+// construct a aws.Credentials object
+// The namespace of the external secret is used if the ClusterSecretStore does not specify a namespace (referentAuth)
+// If the ClusterSecretStore defines a namespace it will take precedence.
+func credsFromSecretRef(ctx context.Context, auth esv1.AWSAuth, storeKind string, kube client.Client, namespace string) (aws.CredentialsProvider, error) {
+	sak, err := resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, &auth.SecretRef.SecretAccessKey)
+	if err != nil {
+		return nil, fmt.Errorf(errFetchSAKSecret, err)
+	}
+	aks, err := resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, &auth.SecretRef.AccessKeyID)
+	if err != nil {
+		return nil, fmt.Errorf(errFetchAKIDSecret, err)
+	}
+
+	var sessionToken string
+	if auth.SecretRef.SessionToken != nil {
+		sessionToken, err = resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, auth.SecretRef.SessionToken)
+		if err != nil {
+			return nil, fmt.Errorf(errFetchSTSecret, err)
+		}
+	}
+	var credsProvider aws.CredentialsProvider = credentials.NewStaticCredentialsProvider(aks, sak, sessionToken)
+
+	return credsProvider, nil
+}
+
+// credsFromServiceAccount uses a Kubernetes Service Account to acquire temporary
+// credentials using aws.AssumeRoleWithWebIdentity. It will assume the role defined
+// in the ServiceAccount annotation.
+// If the ClusterSecretStore does not define a namespace it will use the namespace from the ExternalSecret (referentAuth).
+// If the ClusterSecretStore defines the namespace it will take precedence.
+func credsFromServiceAccount(
+	ctx context.Context,
+	auth esv1.AWSAuth,
+	region string,
+	isClusterKind bool,
+	kube client.Client,
+	namespace string,
+	jwtProvider jwtProviderFactory,
+) (aws.CredentialsProvider, error) {
+	name := auth.JWTAuth.ServiceAccountRef.Name
+	if isClusterKind && auth.JWTAuth.ServiceAccountRef.Namespace != nil {
+		namespace = *auth.JWTAuth.ServiceAccountRef.Namespace
+	}
+	sa := v1.ServiceAccount{}
+	err := kube.Get(ctx, types.NamespacedName{
+		Name:      name,
+		Namespace: namespace,
+	}, &sa)
+	if err != nil {
+		return nil, err
+	}
+	// the service account is expected to have a well-known annotation
+	// this is used as input to assumeRoleWithWebIdentity
+	roleArn := sa.Annotations[roleARNAnnotation]
+	if roleArn == "" {
+		return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", name, namespace)
+	}
+
+	tokenAud := sa.Annotations[audienceAnnotation]
+	if tokenAud == "" {
+		tokenAud = defaultTokenAudience
+	}
+	audiences := []string{tokenAud}
+	if len(auth.JWTAuth.ServiceAccountRef.Audiences) > 0 {
+		audiences = append(audiences, auth.JWTAuth.ServiceAccountRef.Audiences...)
+	}
+
+	jwtProv, err := jwtProvider(name, namespace, roleArn, audiences, region)
+	if err != nil {
+		return nil, err
+	}
+
+	log.V(1).Info("using credentials via service account", "role", roleArn, "region", region)
+
+	return jwtProv, nil
+}
+
+type jwtProviderFactory func(name, namespace, roleArn string, aud []string, region string) (aws.CredentialsProvider, error)
+
+// DefaultJWTProvider returns a credentials.Provider that calls the AssumeRoleWithWebidentity
+// controller-runtime/client does not support TokenRequest or other subresource APIs
+// so we need to construct our own client and use it to fetch tokens.
+func DefaultJWTProvider(name, namespace, roleArn string, aud []string, region string) (aws.CredentialsProvider, error) {
+	cfg, err := ctrlcfg.GetConfig()
+	if err != nil {
+		return nil, err
+	}
+	clientset, err := kubernetes.NewForConfig(cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	awscfg, err := config.LoadDefaultConfig(context.TODO(), config.WithAppID("external-secrets"),
+		config.WithRegion(region),
+		config.WithSharedConfigFiles([]string{}), // Disable shared config files:
+		config.WithSharedCredentialsFiles([]string{}))
+
+	if err != nil {
+		return nil, err
+	}
+
+	tokenFetcher := authTokenFetcher{
+		Namespace:      namespace,
+		Audiences:      aud,
+		ServiceAccount: name,
+		k8sClient:      clientset.CoreV1(),
+	}
+	stsClient := sts.NewFromConfig(awscfg, func(o *sts.Options) {
+		o.EndpointResolverV2 = customEndpointResolver{}
+	})
+	return stscreds.NewWebIdentityRoleProvider(
+		stsClient, roleArn, tokenFetcher, func(opts *stscreds.WebIdentityRoleOptions) {
+			opts.RoleSessionName = "external-secrets-provider-aws"
+		}), nil
+}
+
+// STSprovider defines the interface for interacting with AWS STS API operations.
+// This allows for mocking STS operations during testing.
+type STSprovider interface {
+	AssumeRole(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error)
+	AssumeRoleWithSAML(ctx context.Context, params *sts.AssumeRoleWithSAMLInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithSAMLOutput, error)
+	AssumeRoleWithWebIdentity(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error)
+	AssumeRoot(ctx context.Context, params *sts.AssumeRootInput, optFns ...func(*sts.Options)) (*sts.AssumeRootOutput, error)
+	DecodeAuthorizationMessage(ctx context.Context, params *sts.DecodeAuthorizationMessageInput, optFns ...func(*sts.Options)) (*sts.DecodeAuthorizationMessageOutput, error)
+}
+
+// STSProvider is a function type that returns an STSprovider implementation.
+// Used to inject custom or mock STS clients.
+type STSProvider func(*aws.Config) STSprovider
+
+// DefaultSTSProvider creates and returns a new STS client from the provided AWS config.
+func DefaultSTSProvider(cfg *aws.Config) STSprovider {
+	stsClient := sts.NewFromConfig(*cfg, func(o *sts.Options) {
+		o.EndpointResolverV2 = customEndpointResolver{}
+	})
+	return stsClient
+}

+ 831 - 0
providers/v2/aws/store/auth/auth_test.go

@@ -0,0 +1,831 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package auth
+
+import (
+	"context"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/sts"
+	ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
+	"github.com/stretchr/testify/assert"
+	authv1 "k8s.io/api/authentication/v1"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	fakesess "github.com/external-secrets/external-secrets/providers/v2/aws/store/auth/fake"
+)
+
+const (
+	esNamespaceKey      = "es-namespace"
+	platformTeamNsKey   = "platform-team-ns"
+	myServiceAccountKey = "my-service-account"
+	otherNsName         = "other-ns"
+)
+
+func TestSTSResolver(t *testing.T) {
+	endpointEnvKey := STSEndpointEnv
+	endpointURL := "http://sts.foo"
+
+	t.Setenv(endpointEnvKey, endpointURL)
+
+	f, err := customEndpointResolver{}.ResolveEndpoint(context.Background(), sts.EndpointParameters{})
+
+	assert.Nil(t, err)
+	assert.Equal(t, endpointURL, f.URI.String())
+}
+
+func TestNewSession(t *testing.T) {
+	rows := []TestSessionRow{
+		{
+			name:      "nil store",
+			expectErr: "found nil store",
+			store:     nil,
+		},
+		{
+			name:      "not store spec",
+			expectErr: "storeSpec is missing provider",
+			store:     &esv1.SecretStore{},
+		},
+		{
+			name:      "store spec has no provider",
+			expectErr: "storeSpec is missing provider",
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{},
+			},
+		},
+		{
+			name:      "spec has no awssm field",
+			expectErr: "Missing AWS field",
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{},
+				},
+			},
+		},
+		{
+			name: "configure aws using environment variables",
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{},
+					},
+				},
+			},
+			env: map[string]string{
+				"AWS_ACCESS_KEY_ID":     "1111",
+				"AWS_SECRET_ACCESS_KEY": "2222",
+			},
+			expectProvider:    true,
+			expectedKeyID:     "1111",
+			expectedSecretKey: "2222",
+		},
+		{
+			name: "configure aws using environment variables + assume role",
+			stsProvider: func(_ *aws.Config) STSprovider {
+				return &fakesess.AssumeRoler{
+					AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
+						assert.Equal(t, *input.RoleArn, "foo-bar-baz")
+						return &sts.AssumeRoleOutput{
+							AssumedRoleUser: &ststypes.AssumedRoleUser{
+								Arn:           aws.String("1123132"),
+								AssumedRoleId: aws.String("xxxxx"),
+							},
+							Credentials: &ststypes.Credentials{
+								AccessKeyId:     aws.String("3333"),
+								SecretAccessKey: aws.String("4444"),
+								Expiration:      aws.Time(time.Now().Add(time.Hour)),
+								SessionToken:    aws.String("6666"),
+							},
+						}, nil
+					},
+				}
+			},
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{
+							Role: "foo-bar-baz",
+						},
+					},
+				},
+			},
+			env: map[string]string{
+				"AWS_ACCESS_KEY_ID":     "1111",
+				"AWS_SECRET_ACCESS_KEY": "2222",
+			},
+			expectProvider:    true,
+			expectedKeyID:     "3333",
+			expectedSecretKey: "4444",
+		},
+		{
+			name:      "error out when secret with credentials does not exist",
+			namespace: "foo",
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{
+							Auth: esv1.AWSAuth{
+								SecretRef: &esv1.AWSAuthSecretRef{
+									AccessKeyID: esmeta.SecretKeySelector{
+										Name: "othersecret",
+										Key:  "one",
+									},
+									SecretAccessKey: esmeta.SecretKeySelector{
+										Name: "othersecret",
+										Key:  "two",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectErr: `secrets "othersecret" not found`,
+		},
+		{
+			name:      "use credentials from secret to configure aws",
+			namespace: "foo",
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{
+							Auth: esv1.AWSAuth{
+								SecretRef: &esv1.AWSAuthSecretRef{
+									AccessKeyID: esmeta.SecretKeySelector{
+										Name: "onesecret",
+										// Namespace is not set
+										Key: "one",
+									},
+									SecretAccessKey: esmeta.SecretKeySelector{
+										Name: "onesecret",
+										// Namespace is not set
+										Key: "two",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			secrets: []v1.Secret{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "onesecret",
+						Namespace: "foo",
+					},
+					Data: map[string][]byte{
+						"one": []byte("1111"),
+						"two": []byte("2222"),
+					},
+				},
+			},
+			expectProvider:    true,
+			expectedKeyID:     "1111",
+			expectedSecretKey: "2222",
+		},
+		{
+			name:      "error out when secret key does not exist",
+			namespace: "foo",
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{
+							Auth: esv1.AWSAuth{
+								SecretRef: &esv1.AWSAuthSecretRef{
+									AccessKeyID: esmeta.SecretKeySelector{
+										Name: "brokensecret",
+										Key:  "one",
+									},
+									SecretAccessKey: esmeta.SecretKeySelector{
+										Name: "brokensecret",
+										Key:  "two",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			secrets: []v1.Secret{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "brokensecret",
+						Namespace: "foo",
+					},
+					Data: map[string][]byte{},
+				},
+			},
+			expectErr: "could not fetch SecretAccessKey secret: cannot find secret data for key: \"two\"",
+		},
+		{
+			name:      "should not be able to access secrets from different namespace",
+			namespace: "foo",
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{
+							Auth: esv1.AWSAuth{
+								SecretRef: &esv1.AWSAuthSecretRef{
+									AccessKeyID: esmeta.SecretKeySelector{
+										Name:      "onesecret",
+										Namespace: aws.String("evil"), // this should not be possible!
+										Key:       "one",
+									},
+									SecretAccessKey: esmeta.SecretKeySelector{
+										Name:      "onesecret",
+										Namespace: aws.String("evil"),
+										Key:       "two",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			secrets: []v1.Secret{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "onesecret",
+						Namespace: "evil",
+					},
+					Data: map[string][]byte{
+						"one": []byte("1111"),
+						"two": []byte("2222"),
+					},
+				},
+			},
+			expectErr: `secrets "onesecret" not found`,
+		},
+		{
+			name:      "ClusterStore should use credentials from a specific namespace",
+			namespace: esNamespaceKey,
+			store: &esv1.ClusterSecretStore{
+				TypeMeta: metav1.TypeMeta{
+					APIVersion: esv1.ClusterSecretStoreKindAPIVersion,
+					Kind:       esv1.ClusterSecretStoreKind,
+				},
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{
+							Auth: esv1.AWSAuth{
+								SecretRef: &esv1.AWSAuthSecretRef{
+									AccessKeyID: esmeta.SecretKeySelector{
+										Name:      "onesecret",
+										Namespace: aws.String(platformTeamNsKey),
+										Key:       "one",
+									},
+									SecretAccessKey: esmeta.SecretKeySelector{
+										Name:      "onesecret",
+										Namespace: aws.String(platformTeamNsKey),
+										Key:       "two",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			secrets: []v1.Secret{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "onesecret",
+						Namespace: platformTeamNsKey,
+					},
+					Data: map[string][]byte{
+						"one": []byte("1111"),
+						"two": []byte("2222"),
+					},
+				},
+			},
+			expectProvider:    true,
+			expectedKeyID:     "1111",
+			expectedSecretKey: "2222",
+		},
+		{
+			name:      "ClusterStore should use credentials from a ExternalSecret namespace (referentAuth)",
+			namespace: esNamespaceKey,
+			store: &esv1.ClusterSecretStore{
+				TypeMeta: metav1.TypeMeta{
+					APIVersion: esv1.ClusterSecretStoreKindAPIVersion,
+					Kind:       esv1.ClusterSecretStoreKind,
+				},
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{
+							Auth: esv1.AWSAuth{
+								SecretRef: &esv1.AWSAuthSecretRef{
+									AccessKeyID: esmeta.SecretKeySelector{
+										Name: "onesecret",
+										Key:  "one",
+									},
+									SecretAccessKey: esmeta.SecretKeySelector{
+										Name: "onesecret",
+										Key:  "two",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			secrets: []v1.Secret{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "onesecret",
+						Namespace: esNamespaceKey,
+					},
+					Data: map[string][]byte{
+						"one": []byte("7777"),
+						"two": []byte("4444"),
+					},
+				},
+			},
+			expectProvider:    true,
+			expectedKeyID:     "7777",
+			expectedSecretKey: "4444",
+		},
+		{
+			name:      "jwt auth via cluster secret store",
+			namespace: esNamespaceKey,
+			sa: &v1.ServiceAccount{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      myServiceAccountKey,
+					Namespace: otherNsName,
+					Annotations: map[string]string{
+						roleARNAnnotation: "my-sa-role",
+					},
+				},
+			},
+			jwtProvider: func(name, namespace, roleArn string, _ []string, _ string) (aws.CredentialsProvider, error) {
+				assert.Equal(t, myServiceAccountKey, name)
+				assert.Equal(t, otherNsName, namespace)
+				assert.Equal(t, "my-sa-role", roleArn)
+				return fakesess.CredentialsProvider{
+					RetrieveFunc: func() (aws.Credentials, error) {
+						return aws.Credentials{
+							AccessKeyID:     "3333",
+							SecretAccessKey: "4444",
+							SessionToken:    "1234",
+							Source:          "fake",
+						}, nil
+					},
+				}, nil
+			},
+			store: &esv1.ClusterSecretStore{
+				TypeMeta: metav1.TypeMeta{
+					APIVersion: esv1.ClusterSecretStoreKindAPIVersion,
+					Kind:       esv1.ClusterSecretStoreKind,
+				},
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{
+							Auth: esv1.AWSAuth{
+								JWTAuth: &esv1.AWSJWTAuth{
+									ServiceAccountRef: &esmeta.ServiceAccountSelector{
+										Name:      myServiceAccountKey,
+										Namespace: aws.String(otherNsName),
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectProvider:    true,
+			expectedKeyID:     "3333",
+			expectedSecretKey: "4444",
+		},
+		{
+			name: "configure aws using environment variables + assume role + check external id",
+			stsProvider: func(_ *aws.Config) STSprovider {
+				return &fakesess.AssumeRoler{
+					AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
+						assert.Equal(t, *input.ExternalId, "12345678")
+						return &sts.AssumeRoleOutput{
+							AssumedRoleUser: &ststypes.AssumedRoleUser{
+								Arn:           aws.String("1123132"),
+								AssumedRoleId: aws.String("xxxxx"),
+							},
+							Credentials: &ststypes.Credentials{
+								AccessKeyId:     aws.String("3333"),
+								SecretAccessKey: aws.String("4444"),
+								Expiration:      aws.Time(time.Now().Add(time.Hour)),
+								SessionToken:    aws.String("6666"),
+							},
+						}, nil
+					},
+				}
+			},
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{
+							Role:       "foo-bar-baz",
+							ExternalID: "12345678",
+						},
+					},
+				},
+			},
+			env: map[string]string{
+				"AWS_ACCESS_KEY_ID":     "1111",
+				"AWS_SECRET_ACCESS_KEY": "2222",
+			},
+			expectProvider:    true,
+			expectedKeyID:     "3333",
+			expectedSecretKey: "4444",
+		},
+	}
+	for i := range rows {
+		row := rows[i]
+		t.Run(row.name, func(t *testing.T) {
+			testRow(t, row)
+		})
+	}
+}
+
+type TestSessionRow struct {
+	name              string
+	store             esv1.GenericStore
+	secrets           []v1.Secret
+	sa                *v1.ServiceAccount
+	jwtProvider       jwtProviderFactory
+	namespace         string
+	stsProvider       STSProvider
+	expectProvider    bool
+	expectErr         string
+	expectedKeyID     string
+	expectedSecretKey string
+	env               map[string]string
+}
+
+func testRow(t *testing.T, row TestSessionRow) {
+	kc := clientfake.NewClientBuilder().Build()
+	for i := range row.secrets {
+		err := kc.Create(context.Background(), &row.secrets[i])
+		assert.Nil(t, err)
+	}
+	for k, v := range row.env {
+		t.Setenv(k, v)
+	}
+	if row.sa != nil {
+		err := kc.Create(context.Background(), row.sa)
+		assert.Nil(t, err)
+	}
+	err := kc.Create(context.Background(), &authv1.TokenRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      myServiceAccountKey,
+			Namespace: otherNsName,
+		},
+	})
+	assert.Nil(t, err)
+	s, err := New(context.Background(), Opts{
+		Store:       row.store,
+		Kube:        kc,
+		Namespace:   row.namespace,
+		AssumeRoler: row.stsProvider,
+		JWTProvider: row.jwtProvider,
+	})
+	if !ErrorContains(err, row.expectErr) {
+		t.Errorf("expected error %s but found %s", row.expectErr, err.Error())
+	}
+	// pass test on expected error
+	if err != nil {
+		return
+	}
+	if row.expectProvider && s == nil {
+		t.Errorf("expected provider object, found nil")
+		return
+	}
+	creds, _ := s.Credentials.Retrieve(context.Background())
+	assert.Equal(t, row.expectedKeyID, creds.AccessKeyID)
+	assert.Equal(t, row.expectedSecretKey, creds.SecretAccessKey)
+}
+
+func TestSMEnvCredentials(t *testing.T) {
+	k8sClient := clientfake.NewClientBuilder().Build()
+	t.Setenv("AWS_SECRET_ACCESS_KEY", "1111")
+	t.Setenv("AWS_ACCESS_KEY_ID", "2222")
+	s, err := New(context.Background(), Opts{
+		Kube:        k8sClient,
+		Namespace:   "example-ns",
+		AssumeRoler: DefaultSTSProvider,
+		Store: &esv1.SecretStore{
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					// defaults
+					AWS: &esv1.AWSProvider{},
+				},
+			},
+		},
+	})
+	assert.Nil(t, err)
+	assert.NotNil(t, s)
+	creds, err := s.Credentials.Retrieve(context.Background())
+	assert.Nil(t, err)
+	assert.Equal(t, creds.AccessKeyID, "2222")
+	assert.Equal(t, creds.SecretAccessKey, "1111")
+}
+
+func TestSMAssumeRole(t *testing.T) {
+	k8sClient := clientfake.NewClientBuilder().Build()
+	sts := &fakesess.AssumeRoler{
+		AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
+			if *input.RoleArn == "chained-role-1" {
+				return &sts.AssumeRoleOutput{
+					AssumedRoleUser: &ststypes.AssumedRoleUser{
+						Arn:           aws.String("1111111"),
+						AssumedRoleId: aws.String("yyyyy1"),
+					},
+					Credentials: &ststypes.Credentials{
+						AccessKeyId:     aws.String("77771"),
+						SecretAccessKey: aws.String("88881"),
+						Expiration:      aws.Time(time.Now().Add(time.Hour)),
+						SessionToken:    aws.String("99991"),
+					},
+				}, nil
+			} else if *input.RoleArn == "chained-role-2" {
+				return &sts.AssumeRoleOutput{
+					AssumedRoleUser: &ststypes.AssumedRoleUser{
+						Arn:           aws.String("2222222"),
+						AssumedRoleId: aws.String("yyyyy2"),
+					},
+					Credentials: &ststypes.Credentials{
+						AccessKeyId:     aws.String("77772"),
+						SecretAccessKey: aws.String("88882"),
+						Expiration:      aws.Time(time.Now().Add(time.Hour)),
+						SessionToken:    aws.String("99992"),
+					},
+				}, nil
+			}
+
+			// make sure the correct role is passed in
+			assert.Equal(t, *input.RoleArn, "my-awesome-role")
+			return &sts.AssumeRoleOutput{
+				AssumedRoleUser: &ststypes.AssumedRoleUser{
+					Arn:           aws.String("1123132"),
+					AssumedRoleId: aws.String("xxxxx"),
+				},
+				Credentials: &ststypes.Credentials{
+					AccessKeyId:     aws.String("3333"),
+					SecretAccessKey: aws.String("4444"),
+					Expiration:      aws.Time(time.Now().Add(time.Hour)),
+					SessionToken:    aws.String("6666"),
+				},
+			}, nil
+		},
+	}
+	t.Setenv("AWS_SECRET_ACCESS_KEY", "1111")
+	t.Setenv("AWS_ACCESS_KEY_ID", "2222")
+	s, err := New(context.Background(), Opts{
+		Kube:      k8sClient,
+		Namespace: "example-ns",
+		Store: &esv1.SecretStore{
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					// do assume role!
+					AWS: &esv1.AWSProvider{
+						Role:            "my-awesome-role",
+						AdditionalRoles: []string{"chained-role-1", "chained-role-2"},
+					},
+				},
+			},
+		},
+		AssumeRoler: func(cfg *aws.Config) STSprovider {
+			// check if the correct temporary credentials were used
+			creds, err := cfg.Credentials.Retrieve(context.Background())
+			assert.Nil(t, err)
+			if creds.SessionToken == "" {
+				// called with credentials from envvars
+				assert.Equal(t, creds.AccessKeyID, "2222")
+				assert.Equal(t, creds.SecretAccessKey, "1111")
+			} else if creds.SessionToken == "99991" {
+				// called with chained role 1's credentials
+				assert.Equal(t, creds.AccessKeyID, "77771")
+				assert.Equal(t, creds.SecretAccessKey, "88881")
+			} else {
+				// called with chained role 2's credentials
+				assert.Equal(t, creds.AccessKeyID, "77772")
+				assert.Equal(t, creds.SecretAccessKey, "88882")
+			}
+			return sts
+		},
+	})
+	assert.Nil(t, err)
+	assert.NotNil(t, s)
+
+	creds, err := s.Credentials.Retrieve(context.Background())
+	assert.Nil(t, err)
+	assert.Equal(t, creds.AccessKeyID, "3333")
+	assert.Equal(t, creds.SecretAccessKey, "4444")
+}
+
+func ErrorContains(out error, want string) bool {
+	if out == nil {
+		return want == ""
+	}
+	if want == "" {
+		return false
+	}
+	return strings.Contains(out.Error(), want)
+}
+
+func TestNewGeneratorSession_DefaultCredentialChain(t *testing.T) {
+	cfg, err := NewGeneratorSession(context.Background(), esv1.AWSAuth{}, "", "us-east-1", clientfake.NewClientBuilder().Build(), "test-ns", DefaultSTSProvider, DefaultJWTProvider)
+	assert.NoError(t, err)
+	assert.NotNil(t, cfg)
+	assert.Equal(t, "us-east-1", cfg.Region)
+}
+
+func TestNewGeneratorSession_CredentialProviderPriority(t *testing.T) {
+	ctx := context.Background()
+	k8sClient := clientfake.NewClientBuilder().Build()
+
+	assert.NoError(t, k8sClient.Create(ctx, &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{Name: "aws-creds", Namespace: "test-ns"},
+		Data: map[string][]byte{
+			"access-key": []byte("SECRET_KEY_ID"),
+			"secret-key": []byte("SECRET_ACCESS_KEY"),
+		},
+	}))
+
+	assert.NoError(t, k8sClient.Create(ctx, &v1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        "test-sa",
+			Namespace:   "test-ns",
+			Annotations: map[string]string{roleARNAnnotation: "arn:aws:iam::123456789012:role/test-role"},
+		},
+	}))
+
+	jwtProviderCalled := false
+	cfg, err := NewGeneratorSession(ctx, esv1.AWSAuth{
+		JWTAuth: &esv1.AWSJWTAuth{
+			ServiceAccountRef: &esmeta.ServiceAccountSelector{Name: "test-sa"},
+		},
+		SecretRef: &esv1.AWSAuthSecretRef{
+			AccessKeyID:     esmeta.SecretKeySelector{Name: "aws-creds", Key: "access-key"},
+			SecretAccessKey: esmeta.SecretKeySelector{Name: "aws-creds", Key: "secret-key"},
+		},
+	}, "", "us-east-1", k8sClient, "test-ns", DefaultSTSProvider, func(name, namespace, roleArn string, _ []string, _ string) (aws.CredentialsProvider, error) {
+		jwtProviderCalled = true
+		assert.Equal(t, "test-sa", name)
+		assert.Equal(t, "test-ns", namespace)
+		assert.Equal(t, "arn:aws:iam::123456789012:role/test-role", roleArn)
+		return fakesess.CredentialsProvider{
+			RetrieveFunc: func() (aws.Credentials, error) {
+				return aws.Credentials{
+					AccessKeyID:     "JWT_ACCESS_KEY",
+					SecretAccessKey: "JWT_SECRET_KEY",
+					SessionToken:    "JWT_SESSION_TOKEN",
+					Source:          "jwt",
+				}, nil
+			},
+		}, nil
+	})
+
+	assert.NoError(t, err)
+	assert.NotNil(t, cfg)
+	assert.True(t, jwtProviderCalled)
+
+	creds, err := cfg.Credentials.Retrieve(ctx)
+	assert.NoError(t, err)
+	assert.Equal(t, "SECRET_KEY_ID", creds.AccessKeyID)
+	assert.Equal(t, "SECRET_ACCESS_KEY", creds.SecretAccessKey)
+}
+
+func TestNewGeneratorSession_OnlySecretRef(t *testing.T) {
+	ctx := context.Background()
+	k8sClient := clientfake.NewClientBuilder().Build()
+
+	assert.NoError(t, k8sClient.Create(ctx, &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{Name: "aws-creds", Namespace: "test-ns"},
+		Data: map[string][]byte{
+			"access-key": []byte("SECRET_KEY_ID"),
+			"secret-key": []byte("SECRET_ACCESS_KEY"),
+		},
+	}))
+
+	cfg, err := NewGeneratorSession(ctx, esv1.AWSAuth{
+		SecretRef: &esv1.AWSAuthSecretRef{
+			AccessKeyID:     esmeta.SecretKeySelector{Name: "aws-creds", Key: "access-key"},
+			SecretAccessKey: esmeta.SecretKeySelector{Name: "aws-creds", Key: "secret-key"},
+		},
+	}, "", "us-east-1", k8sClient, "test-ns", DefaultSTSProvider, DefaultJWTProvider)
+
+	assert.NoError(t, err)
+	assert.NotNil(t, cfg)
+
+	creds, err := cfg.Credentials.Retrieve(ctx)
+	assert.NoError(t, err)
+	assert.Equal(t, "SECRET_KEY_ID", creds.AccessKeyID)
+	assert.Equal(t, "SECRET_ACCESS_KEY", creds.SecretAccessKey)
+}
+
+func TestNewGeneratorSession_RegionConfiguration(t *testing.T) {
+	ctx := context.Background()
+	k8sClient := clientfake.NewClientBuilder().Build()
+
+	testCases := []struct {
+		name           string
+		region         string
+		expectedRegion string
+	}{
+		{"region specified", "us-east-1", "us-east-1"},
+		{"empty region uses default", "", ""},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			cfg, err := NewGeneratorSession(ctx, esv1.AWSAuth{}, "", tc.region, k8sClient, "test-ns", DefaultSTSProvider, DefaultJWTProvider)
+			assert.NoError(t, err)
+			assert.NotNil(t, cfg)
+			if tc.expectedRegion != "" {
+				assert.Equal(t, tc.expectedRegion, cfg.Region)
+			}
+		})
+	}
+}
+
+func TestNewGeneratorSession_AssumeRoleWithDefaultCredentials(t *testing.T) {
+	t.Setenv("AWS_ACCESS_KEY_ID", "BASE_ACCESS_KEY")
+	t.Setenv("AWS_SECRET_ACCESS_KEY", "BASE_SECRET_KEY")
+
+	stsProviderCalled := false
+	cfg, err := NewGeneratorSession(
+		context.Background(),
+		esv1.AWSAuth{},
+		"arn:aws:iam::123456789012:role/assumed-role",
+		"us-east-1",
+		clientfake.NewClientBuilder().Build(),
+		"test-ns",
+		func(cfg *aws.Config) STSprovider {
+			stsProviderCalled = true
+			creds, err := cfg.Credentials.Retrieve(context.Background())
+			assert.NoError(t, err)
+			assert.Equal(t, "BASE_ACCESS_KEY", creds.AccessKeyID)
+			return &fakesess.AssumeRoler{
+				AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
+					assert.Equal(t, "arn:aws:iam::123456789012:role/assumed-role", *input.RoleArn)
+					return &sts.AssumeRoleOutput{
+						AssumedRoleUser: &ststypes.AssumedRoleUser{
+							Arn:           aws.String("arn:aws:sts::123456789012:assumed-role/assumed-role/session"),
+							AssumedRoleId: aws.String("AROA123456"),
+						},
+						Credentials: &ststypes.Credentials{
+							AccessKeyId:     aws.String("ASSUMED_ACCESS_KEY"),
+							SecretAccessKey: aws.String("ASSUMED_SECRET_KEY"),
+							SessionToken:    aws.String("ASSUMED_SESSION_TOKEN"),
+							Expiration:      aws.Time(time.Now().Add(time.Hour)),
+						},
+					}, nil
+				},
+			}
+		},
+		DefaultJWTProvider,
+	)
+
+	assert.NoError(t, err)
+	assert.NotNil(t, cfg)
+	assert.True(t, stsProviderCalled)
+
+	creds, err := cfg.Credentials.Retrieve(context.Background())
+	assert.NoError(t, err)
+	assert.Equal(t, "ASSUMED_ACCESS_KEY", creds.AccessKeyID)
+	assert.Equal(t, "ASSUMED_SECRET_KEY", creds.SecretAccessKey)
+	assert.Equal(t, "ASSUMED_SESSION_TOKEN", creds.SessionToken)
+}
+
+func TestNewGeneratorSession_DefaultCredentialChainFallback(t *testing.T) {
+	t.Setenv("AWS_ACCESS_KEY_ID", "ENV_ACCESS_KEY")
+	t.Setenv("AWS_SECRET_ACCESS_KEY", "ENV_SECRET_KEY")
+	t.Setenv("AWS_SESSION_TOKEN", "ENV_SESSION_TOKEN")
+
+	cfg, err := NewGeneratorSession(context.Background(), esv1.AWSAuth{}, "", "us-east-1", clientfake.NewClientBuilder().Build(), "test-ns", DefaultSTSProvider, DefaultJWTProvider)
+	assert.NoError(t, err)
+	assert.NotNil(t, cfg)
+
+	creds, err := cfg.Credentials.Retrieve(context.Background())
+	assert.NoError(t, err)
+	assert.NotEmpty(t, creds.AccessKeyID)
+	assert.NotEmpty(t, creds.SecretAccessKey)
+}

+ 54 - 0
providers/v2/aws/store/auth/fake/assumeroler.go

@@ -0,0 +1,54 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package fake implements mocks for AWS auth service clients.
+package fake
+
+import (
+	"context"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/sts"
+)
+
+type stsAPI interface {
+	AssumeRole(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error)
+	AssumeRoleWithSAML(ctx context.Context, params *sts.AssumeRoleWithSAMLInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithSAMLOutput, error)
+	AssumeRoleWithWebIdentity(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error)
+	AssumeRoot(ctx context.Context, params *sts.AssumeRootInput, optFns ...func(*sts.Options)) (*sts.AssumeRootOutput, error)
+	DecodeAuthorizationMessage(ctx context.Context, params *sts.DecodeAuthorizationMessageInput, optFns ...func(*sts.Options)) (*sts.DecodeAuthorizationMessageOutput, error)
+}
+
+// AssumeRoler is a mock implementation of the AWS STS AssumeRole API.
+type AssumeRoler struct {
+	stsAPI
+	AssumeRoleFunc func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error)
+}
+
+// AssumeRole mocks the AWS STS AssumeRole API.
+func (f *AssumeRoler) AssumeRole(_ context.Context, params *sts.AssumeRoleInput, _ ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) {
+	return f.AssumeRoleFunc(params)
+}
+
+// CredentialsProvider is a mock implementation of the AWS credentials provider.
+type CredentialsProvider struct {
+	RetrieveFunc func() (aws.Credentials, error)
+}
+
+// Retrieve mocks the AWS credentials provider Retrieve method.
+func (t CredentialsProvider) Retrieve(_ context.Context) (aws.Credentials, error) {
+	return t.RetrieveFunc()
+}

+ 52 - 0
providers/v2/aws/store/auth/resolver.go

@@ -0,0 +1,52 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package auth
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+	"os"
+
+	"github.com/aws/aws-sdk-go-v2/service/sts"
+	smithyendpoints "github.com/aws/smithy-go/endpoints"
+)
+
+const (
+	// STSEndpointEnv is the environment variable name for the AWS STS endpoint URL.
+	STSEndpointEnv = "AWS_STS_ENDPOINT"
+)
+
+type customEndpointResolver struct{}
+
+// ResolveEndpoint returns a ResolverFunc with
+// customizable endpoints.
+
+// should this reside somewhere else since it's specific to sts?
+func (c customEndpointResolver) ResolveEndpoint(ctx context.Context, params sts.EndpointParameters) (smithyendpoints.Endpoint, error) {
+	endpoint := smithyendpoints.Endpoint{}
+	if v := os.Getenv(STSEndpointEnv); v != "" {
+		url, err := url.Parse(v)
+		if err != nil {
+			return endpoint, fmt.Errorf("failed to parse sts endpoint %s: %w", v, err)
+		}
+		endpoint.URI = *url
+		return endpoint, nil
+	}
+	defaultResolver := sts.NewDefaultEndpointResolverV2()
+	return defaultResolver.ResolveEndpoint(ctx, params)
+}

+ 39 - 0
providers/v2/aws/store/auth/resolver_test.go

@@ -0,0 +1,39 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package auth
+
+import (
+	"context"
+	"testing"
+
+	"github.com/aws/aws-sdk-go-v2/service/sts"
+	"github.com/stretchr/testify/assert"
+)
+
+// do we need this file now that resolving logic is isolated to each service?
+
+func TestResolver(t *testing.T) {
+	endpointEnvKey := STSEndpointEnv
+	endpointURL := "http://sts.foo"
+
+	t.Setenv(endpointEnvKey, endpointURL)
+
+	f, err := customEndpointResolver{}.ResolveEndpoint(context.Background(), sts.EndpointParameters{})
+
+	assert.Nil(t, err)
+	assert.Equal(t, endpointURL, f.URI.String())
+}

+ 54 - 0
providers/v2/aws/store/auth/token_fetcher.go

@@ -0,0 +1,54 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package auth
+
+import (
+	"context"
+	"fmt"
+
+	authv1 "k8s.io/api/authentication/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+)
+
+// mostly taken from:
+// https://github.com/aws/secrets-store-csi-driver-provider-aws/blob/main/auth/auth.go#L140-L145
+
+type authTokenFetcher struct {
+	Namespace string
+	// Audience is the token aud claim
+	// which is verified by the aws oidc provider
+	// see: https://github.com/external-secrets/external-secrets/issues/1251#issuecomment-1161745849
+	Audiences      []string
+	ServiceAccount string
+	k8sClient      corev1.CoreV1Interface
+}
+
+// GetIdentityToken satisfies the stscreds.IdentityTokenRetriever interface
+// it is used to generate service account tokens which are consumed by the aws sdk.
+func (p authTokenFetcher) GetIdentityToken() ([]byte, error) {
+	log.V(1).Info("fetching token", "ns", p.Namespace, "sa", p.ServiceAccount)
+	tokRsp, err := p.k8sClient.ServiceAccounts(p.Namespace).CreateToken(context.Background(), p.ServiceAccount, &authv1.TokenRequest{
+		Spec: authv1.TokenRequestSpec{
+			Audiences: p.Audiences,
+		},
+	}, metav1.CreateOptions{})
+	if err != nil {
+		return nil, fmt.Errorf("error creating service account token: %w", err)
+	}
+	return []byte(tokRsp.Status.Token), nil
+}

+ 36 - 0
providers/v2/aws/store/auth/token_fetcher_test.go

@@ -0,0 +1,36 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package auth
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/external-secrets/external-secrets/runtime/util/fake"
+)
+
+func TestTokenFetcher(t *testing.T) {
+	tf := &authTokenFetcher{
+		ServiceAccount: "foobar",
+		Namespace:      "example",
+		k8sClient:      fake.NewCreateTokenMock().WithToken("FAKETOKEN"),
+	}
+	token, err := tf.GetIdentityToken()
+	assert.Nil(t, err)
+	assert.Equal(t, []byte("FAKETOKEN"), token)
+}

+ 186 - 0
providers/v2/aws/store/parameterstore/fake/fake.go

@@ -0,0 +1,186 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package fake implements mocks for AWS Parameter Store service clients.
+package fake
+
+import (
+	"context"
+	"errors"
+
+	"github.com/aws/aws-sdk-go-v2/service/ssm"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+)
+
+// Client implements the aws parameterstore interface.
+type Client struct {
+	GetParameterFn           GetParameterFn
+	GetParametersByPathFn    GetParametersByPathFn
+	PutParameterFn           PutParameterFn
+	PutParameterCalledN      int
+	PutParameterFnCalledWith [][]*ssm.PutParameterInput
+	DeleteParameterFn        DeleteParameterFn
+	DescribeParametersFn     DescribeParametersFn
+	ListTagsForResourceFn    ListTagsForResourceFn
+	RemoveTagsFromResourceFn RemoveTagsFromResourceFn
+	AddTagsToResourceFn      AddTagsToResourceFn
+}
+
+// GetParameterFn defines a function type for mocking GetParameter API.
+type GetParameterFn func(context.Context, *ssm.GetParameterInput, ...func(*ssm.Options)) (*ssm.GetParameterOutput, error)
+
+// GetParametersByPathFn defines a function type for mocking GetParametersByPath API.
+type GetParametersByPathFn func(context.Context, *ssm.GetParametersByPathInput, ...func(*ssm.Options)) (*ssm.GetParametersByPathOutput, error)
+
+// PutParameterFn defines a function type for mocking PutParameter API.
+type PutParameterFn func(context.Context, *ssm.PutParameterInput, ...func(*ssm.Options)) (*ssm.PutParameterOutput, error)
+
+// DescribeParametersFn defines a function type for mocking DescribeParameters API.
+type DescribeParametersFn func(context.Context, *ssm.DescribeParametersInput, ...func(*ssm.Options)) (*ssm.DescribeParametersOutput, error)
+
+// ListTagsForResourceFn defines a function type for mocking ListTagsForResource API.
+type ListTagsForResourceFn func(context.Context, *ssm.ListTagsForResourceInput, ...func(*ssm.Options)) (*ssm.ListTagsForResourceOutput, error)
+
+// DeleteParameterFn defines a function type for mocking DeleteParameter API.
+type DeleteParameterFn func(ctx context.Context, input *ssm.DeleteParameterInput, opts ...func(*ssm.Options)) (*ssm.DeleteParameterOutput, error)
+
+// RemoveTagsFromResourceFn defines a function type for mocking RemoveTagsFromResource API.
+type RemoveTagsFromResourceFn func(ctx context.Context, params *ssm.RemoveTagsFromResourceInput, optFns ...func(*ssm.Options)) (*ssm.RemoveTagsFromResourceOutput, error)
+
+// AddTagsToResourceFn defines a function type for mocking AddTagsToResource API.
+type AddTagsToResourceFn func(ctx context.Context, params *ssm.AddTagsToResourceInput, optFns ...func(*ssm.Options)) (*ssm.AddTagsToResourceOutput, error)
+
+// ListTagsForResource executes the mocked ListTagsForResourceFn.
+func (sm *Client) ListTagsForResource(ctx context.Context, input *ssm.ListTagsForResourceInput, options ...func(*ssm.Options)) (*ssm.ListTagsForResourceOutput, error) {
+	return sm.ListTagsForResourceFn(ctx, input, options...)
+}
+
+// NewListTagsForResourceFn creates a new mock function for ListTagsForResource.
+func NewListTagsForResourceFn(output *ssm.ListTagsForResourceOutput, err error, aFunc ...func(input *ssm.ListTagsForResourceInput)) ListTagsForResourceFn {
+	return func(_ context.Context, params *ssm.ListTagsForResourceInput, _ ...func(*ssm.Options)) (*ssm.ListTagsForResourceOutput, error) {
+		if len(aFunc) > 0 {
+			for _, f := range aFunc {
+				f(params)
+			}
+		}
+		return output, err
+	}
+}
+
+// DeleteParameter executes the mocked DeleteParameterFn.
+func (sm *Client) DeleteParameter(ctx context.Context, input *ssm.DeleteParameterInput, opts ...func(*ssm.Options)) (*ssm.DeleteParameterOutput, error) {
+	return sm.DeleteParameterFn(ctx, input, opts...)
+}
+
+// NewDeleteParameterFn creates a new mock function for DeleteParameter.
+func NewDeleteParameterFn(output *ssm.DeleteParameterOutput, err error) DeleteParameterFn {
+	return func(context.Context, *ssm.DeleteParameterInput, ...func(*ssm.Options)) (*ssm.DeleteParameterOutput, error) {
+		return output, err
+	}
+}
+
+// GetParameter executes the mocked GetParameterFn.
+func (sm *Client) GetParameter(ctx context.Context, input *ssm.GetParameterInput, options ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) {
+	return sm.GetParameterFn(ctx, input, options...)
+}
+
+// GetParametersByPath executes the mocked GetParametersByPathFn.
+func (sm *Client) GetParametersByPath(ctx context.Context, input *ssm.GetParametersByPathInput, options ...func(*ssm.Options)) (*ssm.GetParametersByPathOutput, error) {
+	return sm.GetParametersByPathFn(ctx, input, options...)
+}
+
+// NewGetParameterFn creates a new mock function for GetParameter.
+func NewGetParameterFn(output *ssm.GetParameterOutput, err error) GetParameterFn {
+	return func(context.Context, *ssm.GetParameterInput, ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) {
+		return output, err
+	}
+}
+
+// DescribeParameters executes the mocked DescribeParametersFn.
+func (sm *Client) DescribeParameters(ctx context.Context, input *ssm.DescribeParametersInput, options ...func(*ssm.Options)) (*ssm.DescribeParametersOutput, error) {
+	return sm.DescribeParametersFn(ctx, input, options...)
+}
+
+// NewDescribeParametersFn creates a new mock function for DescribeParameters.
+func NewDescribeParametersFn(output *ssm.DescribeParametersOutput, err error) DescribeParametersFn {
+	return func(context.Context, *ssm.DescribeParametersInput, ...func(*ssm.Options)) (*ssm.DescribeParametersOutput, error) {
+		return output, err
+	}
+}
+
+// PutParameter executes the mocked PutParameterFn and tracks call metadata.
+func (sm *Client) PutParameter(ctx context.Context, input *ssm.PutParameterInput, options ...func(*ssm.Options)) (*ssm.PutParameterOutput, error) {
+	sm.PutParameterCalledN++
+	sm.PutParameterFnCalledWith = append(sm.PutParameterFnCalledWith, []*ssm.PutParameterInput{input})
+	return sm.PutParameterFn(ctx, input, options...)
+}
+
+// NewPutParameterFn creates a new mock function for PutParameter.
+func NewPutParameterFn(output *ssm.PutParameterOutput, err error, aFunc ...func(input *ssm.PutParameterInput)) PutParameterFn {
+	return func(_ context.Context, params *ssm.PutParameterInput, _ ...func(*ssm.Options)) (*ssm.PutParameterOutput, error) {
+		if len(aFunc) > 0 {
+			for _, f := range aFunc {
+				f(params)
+			}
+		}
+		return output, err
+	}
+}
+
+// WithValue configures the GetParameterFn with specific input and output.
+func (sm *Client) WithValue(in *ssm.GetParameterInput, val *ssm.GetParameterOutput, err error) {
+	sm.GetParameterFn = func(_ context.Context, paramIn *ssm.GetParameterInput, _ ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) {
+		if !cmp.Equal(paramIn, in, cmpopts.IgnoreUnexported(ssm.GetParameterInput{})) {
+			return nil, errors.New("unexpected test argument")
+		}
+		return val, err
+	}
+}
+
+// RemoveTagsFromResource executes the mocked RemoveTagsFromResourceFn.
+func (sm *Client) RemoveTagsFromResource(ctx context.Context, params *ssm.RemoveTagsFromResourceInput, optFns ...func(*ssm.Options)) (*ssm.RemoveTagsFromResourceOutput, error) {
+	return sm.RemoveTagsFromResourceFn(ctx, params, optFns...)
+}
+
+// NewRemoveTagsFromResourceFn creates a new mock function for RemoveTagsFromResource.
+func NewRemoveTagsFromResourceFn(output *ssm.RemoveTagsFromResourceOutput, err error, aFunc ...func(input *ssm.RemoveTagsFromResourceInput)) RemoveTagsFromResourceFn {
+	return func(_ context.Context, params *ssm.RemoveTagsFromResourceInput, _ ...func(*ssm.Options)) (*ssm.RemoveTagsFromResourceOutput, error) {
+		if len(aFunc) > 0 {
+			for _, f := range aFunc {
+				f(params)
+			}
+		}
+		return output, err
+	}
+}
+
+// AddTagsToResource executes the mocked AddTagsToResourceFn.
+func (sm *Client) AddTagsToResource(ctx context.Context, params *ssm.AddTagsToResourceInput, optFns ...func(*ssm.Options)) (*ssm.AddTagsToResourceOutput, error) {
+	return sm.AddTagsToResourceFn(ctx, params, optFns...)
+}
+
+// NewAddTagsToResourceFn creates a new mock function for AddTagsToResource.
+func NewAddTagsToResourceFn(output *ssm.AddTagsToResourceOutput, err error, aFunc ...func(input *ssm.AddTagsToResourceInput)) AddTagsToResourceFn {
+	return func(_ context.Context, params *ssm.AddTagsToResourceInput, _ ...func(*ssm.Options)) (*ssm.AddTagsToResourceOutput, error) {
+		if len(aFunc) > 0 {
+			for _, f := range aFunc {
+				f(params)
+			}
+		}
+		return output, err
+	}
+}

+ 710 - 0
providers/v2/aws/store/parameterstore/parameterstore.go

@@ -0,0 +1,710 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package parameterstore implements the AWS SSM Parameter Store provider for external-secrets
+package parameterstore
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/ssm"
+	ssmTypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
+	"github.com/aws/smithy-go"
+	"github.com/tidwall/gjson"
+	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	ctrl "sigs.k8s.io/controller-runtime"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	awsutil "github.com/external-secrets/external-secrets/providers/v1/aws/util"
+	"github.com/external-secrets/external-secrets/runtime/constants"
+	"github.com/external-secrets/external-secrets/runtime/esutils"
+	"github.com/external-secrets/external-secrets/runtime/esutils/metadata"
+	"github.com/external-secrets/external-secrets/runtime/find"
+	"github.com/external-secrets/external-secrets/runtime/metrics"
+)
+
+// Tier defines policy details for PushSecret.
+type Tier struct {
+	Type     ssmTypes.ParameterTier `json:"type"`
+	Policies *apiextensionsv1.JSON  `json:"policies"`
+}
+
+// PushSecretMetadataSpec defines the spec for the metadata for PushSecret.
+type PushSecretMetadataSpec struct {
+	SecretType      ssmTypes.ParameterType `json:"secretType,omitempty"`
+	KMSKeyID        string                 `json:"kmsKeyID,omitempty"`
+	Tier            Tier                   `json:"tier,omitempty"`
+	EncodeAsDecoded bool                   `json:"encodeAsDecoded,omitempty"`
+	Tags            map[string]string      `json:"tags,omitempty"`
+	Description     string                 `json:"description,omitempty"`
+}
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var (
+	_               esv1.SecretsClient = &ParameterStore{}
+	managedBy                          = "managed-by"
+	externalSecrets                    = "external-secrets"
+	logger                             = ctrl.Log.WithName("provider").WithName("parameterstore")
+)
+
+// ParameterStore is a provider for AWS ParameterStore.
+type ParameterStore struct {
+	cfg          *aws.Config
+	client       PMInterface
+	referentAuth bool
+	prefix       string
+}
+
+// PMInterface is a subset of the parameterstore api.
+// see: https://docs.aws.amazon.com/sdk-for-go/api/service/ssm/ssmiface/
+type PMInterface interface {
+	GetParameter(ctx context.Context, input *ssm.GetParameterInput, opts ...func(*ssm.Options)) (*ssm.GetParameterOutput, error)
+	GetParametersByPath(ctx context.Context, input *ssm.GetParametersByPathInput, opts ...func(*ssm.Options)) (*ssm.GetParametersByPathOutput, error)
+	PutParameter(ctx context.Context, input *ssm.PutParameterInput, opts ...func(*ssm.Options)) (*ssm.PutParameterOutput, error)
+	DescribeParameters(ctx context.Context, input *ssm.DescribeParametersInput, opts ...func(*ssm.Options)) (*ssm.DescribeParametersOutput, error)
+	ListTagsForResource(ctx context.Context, input *ssm.ListTagsForResourceInput, opts ...func(*ssm.Options)) (*ssm.ListTagsForResourceOutput, error)
+	RemoveTagsFromResource(ctx context.Context, params *ssm.RemoveTagsFromResourceInput, optFns ...func(*ssm.Options)) (*ssm.RemoveTagsFromResourceOutput, error)
+	AddTagsToResource(ctx context.Context, params *ssm.AddTagsToResourceInput, optFns ...func(*ssm.Options)) (*ssm.AddTagsToResourceOutput, error)
+	DeleteParameter(ctx context.Context, input *ssm.DeleteParameterInput, opts ...func(*ssm.Options)) (*ssm.DeleteParameterOutput, error)
+}
+
+const (
+	errUnexpectedFindOperator    = "unexpected find operator"
+	errCodeAccessDeniedException = "AccessDeniedException"
+)
+
+// New constructs a ParameterStore Provider that is specific to a store.
+func New(_ context.Context, cfg *aws.Config, prefix string, referentAuth bool) (*ParameterStore, error) {
+	return &ParameterStore{
+		cfg:          cfg,
+		referentAuth: referentAuth,
+		client: ssm.NewFromConfig(*cfg, func(o *ssm.Options) {
+			o.EndpointResolverV2 = customEndpointResolver{}
+		}),
+		prefix: prefix,
+	}, nil
+}
+
+func (pm *ParameterStore) getTagsByName(ctx context.Context, ref *ssm.GetParameterOutput) (map[string]string, error) {
+	parameterType := "Parameter"
+
+	parameterTags := ssm.ListTagsForResourceInput{
+		ResourceId:   ref.Parameter.Name,
+		ResourceType: ssmTypes.ResourceTypeForTagging(parameterType),
+	}
+
+	data, err := pm.client.ListTagsForResource(ctx, &parameterTags)
+	metrics.ObserveAPICall(constants.ProviderAWSPS, constants.CallAWSPSListTagsForResource, err)
+	if err != nil {
+		return nil, fmt.Errorf("error listing tags %w", err)
+	}
+
+	tags := map[string]string{}
+	for _, tag := range data.TagList {
+		tags[*tag.Key] = *tag.Value
+	}
+	return tags, nil
+}
+
+// DeleteSecret deletes a secret from AWS Parameter Store.
+// It will only delete secrets that are managed by external-secrets (have the managed-by tag).
+func (pm *ParameterStore) DeleteSecret(ctx context.Context, remoteRef esv1.PushSecretRemoteRef) error {
+	secretName := pm.prefix + remoteRef.GetRemoteKey()
+	secretValue := ssm.GetParameterInput{
+		Name: &secretName,
+	}
+	existing, err := pm.client.GetParameter(ctx, &secretValue)
+	metrics.ObserveAPICall(constants.ProviderAWSPS, constants.CallAWSPSGetParameter, err)
+	var parameterNotFoundErr *ssmTypes.ParameterNotFound
+	ok := errors.As(err, &parameterNotFoundErr)
+	if err != nil && !ok {
+		return fmt.Errorf("unexpected error getting parameter %v: %w", secretName, err)
+	}
+	if existing != nil && existing.Parameter != nil {
+		tags, err := pm.getTagsByName(ctx, existing)
+		if err != nil {
+			return fmt.Errorf("error getting the existing tags for the parameter %v: %w", secretName, err)
+		}
+
+		isManaged := isManagedByESO(tags)
+
+		if !isManaged {
+			// If the secret is not managed by external-secrets, it is "deleted" effectively by all means
+			return nil
+		}
+		deleteInput := &ssm.DeleteParameterInput{
+			Name: &secretName,
+		}
+		_, err = pm.client.DeleteParameter(ctx, deleteInput)
+		metrics.ObserveAPICall(constants.ProviderAWSPS, constants.CallAWSPSDeleteParameter, err)
+		if err != nil {
+			return fmt.Errorf("could not delete parameter %v: %w", secretName, err)
+		}
+	}
+	return nil
+}
+
+// SecretExists checks if a secret exists in AWS Parameter Store.
+func (pm *ParameterStore) SecretExists(ctx context.Context, pushSecretRef esv1.PushSecretRemoteRef) (bool, error) {
+	secretName := pm.prefix + pushSecretRef.GetRemoteKey()
+
+	secretValue := ssm.GetParameterInput{
+		Name: &secretName,
+	}
+
+	var resourceNotFoundErr *ssmTypes.ResourceNotFoundException
+	var parameterNotFoundErr *ssmTypes.ParameterNotFound
+
+	if _, err := pm.client.GetParameter(ctx, &secretValue); err != nil {
+		if errors.As(err, &resourceNotFoundErr) {
+			return false, nil
+		}
+		if errors.As(err, &parameterNotFoundErr) {
+			return false, nil
+		}
+		return false, err
+	}
+
+	return true, nil
+}
+
+// PushSecret uploads a secret to AWS Parameter Store.
+// It can create a new secret or update an existing one.
+// The secret is identified by the remote key, which is the name of the parameter in Parameter Store.
+// The value of the secret is taken from the secret data, and can be either the entire secret or a specific key within the secret.
+// Tags are applied to the secret for management and identification.
+func (pm *ParameterStore) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
+	var (
+		value []byte
+		err   error
+	)
+
+	meta, err := pm.constructMetadataWithDefaults(data.GetMetadata())
+	if err != nil {
+		return err
+	}
+
+	key := data.GetSecretKey()
+
+	if key == "" {
+		value, err = pm.encodeSecretData(meta.Spec.EncodeAsDecoded, secret.Data)
+		if err != nil {
+			return fmt.Errorf("failed to serialize secret content as JSON: %w", err)
+		}
+	} else {
+		value = secret.Data[key]
+	}
+
+	tags := make([]ssmTypes.Tag, 0, len(meta.Spec.Tags))
+
+	for k, v := range meta.Spec.Tags {
+		tags = append(tags, ssmTypes.Tag{
+			Key:   new(k),
+			Value: new(v),
+		})
+	}
+
+	secretName := pm.prefix + data.GetRemoteKey()
+	secretRequest := ssm.PutParameterInput{
+		Name:        new(pm.prefix + data.GetRemoteKey()),
+		Value:       new(string(value)),
+		Type:        meta.Spec.SecretType,
+		Overwrite:   new(true),
+		Description: new(meta.Spec.Description),
+	}
+
+	if meta.Spec.SecretType == "SecureString" {
+		secretRequest.KeyId = &meta.Spec.KMSKeyID
+	}
+
+	if meta.Spec.Tier.Type == ssmTypes.ParameterTierAdvanced {
+		secretRequest.Tier = meta.Spec.Tier.Type
+		if meta.Spec.Tier.Policies != nil {
+			secretRequest.Policies = new(string(meta.Spec.Tier.Policies.Raw))
+		}
+	}
+
+	secretValue := ssm.GetParameterInput{
+		Name:           &secretName,
+		WithDecryption: aws.Bool(true),
+	}
+
+	existing, err := pm.client.GetParameter(ctx, &secretValue)
+	metrics.ObserveAPICall(constants.ProviderAWSPS, constants.CallAWSPSGetParameter, err)
+	var parameterNotFoundErr *ssmTypes.ParameterNotFound
+	ok := errors.As(err, &parameterNotFoundErr)
+	if err != nil && !ok {
+		return fmt.Errorf("unexpected error getting parameter %v: %w", secretName, err)
+	}
+
+	// If we have a valid parameter returned to us, check its tags
+	if existing != nil && existing.Parameter != nil {
+		return pm.setExisting(ctx, existing, secretName, value, secretRequest, meta.Spec.Tags)
+	}
+
+	// let's set the secret
+	// Do we need to delete the existing parameter on the remote?
+	return pm.setManagedRemoteParameter(ctx, secretRequest, tags, true)
+}
+
+func (pm *ParameterStore) encodeSecretData(encodeAsDecoded bool, data map[string][]byte) ([]byte, error) {
+	if encodeAsDecoded {
+		// This will result in map byte slices not being base64 encoded by json.Marshal.
+		return esutils.JSONMarshal(convertMap(data))
+	}
+
+	return esutils.JSONMarshal(data)
+}
+
+func convertMap(in map[string][]byte) map[string]string {
+	m := make(map[string]string)
+	for k, v := range in {
+		m[k] = string(v)
+	}
+	return m
+}
+
+func (pm *ParameterStore) setExisting(ctx context.Context, existing *ssm.GetParameterOutput, secretName string, value []byte, secretRequest ssm.PutParameterInput, metaTags map[string]string) error {
+	tags, err := pm.getTagsByName(ctx, existing)
+	if err != nil {
+		return fmt.Errorf("error getting the existing tags for the parameter %v: %w", secretName, err)
+	}
+
+	isManaged := isManagedByESO(tags)
+
+	if !isManaged {
+		return errors.New("secret not managed by external-secrets")
+	}
+
+	// When fetching a remote SecureString parameter without decrypting, the default value will always be 'sensitive'
+	// in this case, no updates will be pushed remotely
+	if existing.Parameter.Value != nil && *existing.Parameter.Value == "sensitive" {
+		return errors.New("unable to compare 'sensitive' result, ensure to request a decrypted value")
+	}
+
+	if existing.Parameter.Value != nil && *existing.Parameter.Value == string(value) {
+		return nil
+	}
+
+	err = pm.setManagedRemoteParameter(ctx, secretRequest, []ssmTypes.Tag{}, false)
+	if err != nil {
+		return err
+	}
+
+	tagKeysToRemove := awsutil.FindTagKeysToRemove(tags, metaTags)
+	if len(tagKeysToRemove) > 0 {
+		_, err = pm.client.RemoveTagsFromResource(ctx, &ssm.RemoveTagsFromResourceInput{
+			ResourceId:   existing.Parameter.Name,
+			ResourceType: ssmTypes.ResourceTypeForTaggingParameter,
+			TagKeys:      tagKeysToRemove,
+		})
+		metrics.ObserveAPICall(constants.ProviderAWSPS, constants.CallAWSPSRemoveTagsParameter, err)
+		if err != nil {
+			return err
+		}
+	}
+
+	tagsToUpdate, isModified := computeTagsToUpdate(tags, metaTags)
+	if isModified {
+		_, err = pm.client.AddTagsToResource(ctx, &ssm.AddTagsToResourceInput{
+			ResourceId:   existing.Parameter.Name,
+			ResourceType: ssmTypes.ResourceTypeForTaggingParameter,
+			Tags:         tagsToUpdate,
+		})
+		metrics.ObserveAPICall(constants.ProviderAWSPS, constants.CallAWSPSAddTagsParameter, err)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func isManagedByESO(tags map[string]string) bool {
+	return tags[managedBy] == externalSecrets
+}
+
+func (pm *ParameterStore) setManagedRemoteParameter(ctx context.Context, secretRequest ssm.PutParameterInput, tags []ssmTypes.Tag, createManagedByTags bool) error {
+	overwrite := true
+	secretRequest.Overwrite = &overwrite
+	if createManagedByTags {
+		secretRequest.Tags = append(secretRequest.Tags, tags...)
+		overwrite = false
+	}
+
+	_, err := pm.client.PutParameter(ctx, &secretRequest)
+	metrics.ObserveAPICall(constants.ProviderAWSPS, constants.CallAWSPSPutParameter, err)
+	if err != nil {
+		return fmt.Errorf("unexpected error pushing parameter %v: %w", secretRequest.Name, err)
+	}
+	return nil
+}
+
+// GetAllSecrets fetches information from multiple secrets into a single kubernetes secret.
+func (pm *ParameterStore) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
+	if ref.Name != nil {
+		return pm.findByName(ctx, ref)
+	}
+	if ref.Tags != nil {
+		return pm.findByTags(ctx, ref)
+	}
+	return nil, errors.New(errUnexpectedFindOperator)
+}
+
+// findByName requires `ssm:GetParametersByPath` IAM permission, but the `Resource` scope can be limited.
+func (pm *ParameterStore) findByName(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
+	matcher, err := find.New(*ref.Name)
+	if err != nil {
+		return nil, err
+	}
+	if ref.Path == nil {
+		ref.Path = aws.String("/")
+	}
+	data := make(map[string][]byte)
+	var nextToken *string
+	for {
+		it, err := pm.client.GetParametersByPath(
+			ctx,
+			&ssm.GetParametersByPathInput{
+				NextToken:      nextToken,
+				Path:           ref.Path,
+				Recursive:      aws.Bool(true),
+				WithDecryption: aws.Bool(true),
+			})
+		metrics.ObserveAPICall(constants.ProviderAWSPS, constants.CallAWSPSGetParametersByPath, err)
+		if err != nil {
+			var apiErr smithy.APIError
+			if errors.As(err, &apiErr) && apiErr.ErrorCode() == errCodeAccessDeniedException {
+				logger.Info("GetParametersByPath: access denied. using fallback to describe parameters. It is recommended to add ssm:GetParametersByPath permissions", "path", ref.Path)
+				return pm.fallbackFindByName(ctx, ref)
+			}
+
+			return nil, fmt.Errorf("fetching parameters by path %s: %w", *ref.Path, err)
+		}
+
+		for _, param := range it.Parameters {
+			if !matcher.MatchName(*param.Name) {
+				continue
+			}
+			data[*param.Name] = []byte(*param.Value)
+		}
+
+		nextToken = it.NextToken
+		if nextToken == nil {
+			break
+		}
+	}
+
+	return data, nil
+}
+
+// fallbackFindByName requires `ssm:DescribeParameters` IAM permission on `"Resource": "*"`.
+func (pm *ParameterStore) fallbackFindByName(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
+	matcher, err := find.New(*ref.Name)
+	if err != nil {
+		return nil, err
+	}
+	pathFilter := make([]ssmTypes.ParameterStringFilter, 0)
+	if ref.Path != nil {
+		pathFilter = append(pathFilter, ssmTypes.ParameterStringFilter{
+			Key:    aws.String("Path"),
+			Option: aws.String("Recursive"),
+			Values: []string{*ref.Path},
+		})
+	}
+	data := make(map[string][]byte)
+	var nextToken *string
+	for {
+		it, err := pm.client.DescribeParameters(
+			ctx,
+			&ssm.DescribeParametersInput{
+				NextToken:        nextToken,
+				ParameterFilters: pathFilter,
+			})
+		metrics.ObserveAPICall(constants.ProviderAWSPS, constants.CallAWSPSDescribeParameter, err)
+		if err != nil {
+			return nil, err
+		}
+		for _, param := range it.Parameters {
+			if !matcher.MatchName(*param.Name) {
+				continue
+			}
+			err = pm.fetchAndSet(ctx, data, *param.Name)
+			if err != nil {
+				return nil, err
+			}
+		}
+		nextToken = it.NextToken
+		if nextToken == nil {
+			break
+		}
+	}
+	return data, nil
+}
+
+// findByTags requires ssm:DescribeParameters,tag:GetResources IAM permission on `"Resource": "*"`.
+func (pm *ParameterStore) findByTags(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
+	filters := make([]ssmTypes.ParameterStringFilter, 0)
+	for k, v := range ref.Tags {
+		filters = append(filters, ssmTypes.ParameterStringFilter{
+			Key:    new(fmt.Sprintf("tag:%s", k)),
+			Values: []string{v},
+			Option: new("Equals"),
+		})
+	}
+
+	if ref.Path != nil {
+		filters = append(filters, ssmTypes.ParameterStringFilter{
+			Key:    aws.String("Path"),
+			Option: aws.String("Recursive"),
+			Values: []string{*ref.Path},
+		})
+	}
+
+	data := make(map[string][]byte)
+	var nextToken *string
+	for {
+		it, err := pm.client.DescribeParameters(
+			ctx,
+			&ssm.DescribeParametersInput{
+				ParameterFilters: filters,
+				NextToken:        nextToken,
+			})
+		metrics.ObserveAPICall(constants.ProviderAWSPS, constants.CallAWSPSDescribeParameter, err)
+		if err != nil {
+			return nil, err
+		}
+		for _, param := range it.Parameters {
+			err = pm.fetchAndSet(ctx, data, *param.Name)
+			if err != nil {
+				return nil, err
+			}
+		}
+		nextToken = it.NextToken
+		if nextToken == nil {
+			break
+		}
+	}
+
+	return data, nil
+}
+
+func (pm *ParameterStore) fetchAndSet(ctx context.Context, data map[string][]byte, name string) error {
+	out, err := pm.client.GetParameter(ctx, &ssm.GetParameterInput{
+		Name:           new(name),
+		WithDecryption: aws.Bool(true),
+	})
+	metrics.ObserveAPICall(constants.ProviderAWSPS, constants.CallAWSPSGetParameter, err)
+	if err != nil {
+		return awsutil.SanitizeErr(err)
+	}
+
+	data[name] = []byte(*out.Parameter.Value)
+	return nil
+}
+
+// GetSecret returns a single secret from the provider.
+func (pm *ParameterStore) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	var out *ssm.GetParameterOutput
+	var err error
+	if ref.MetadataPolicy == esv1.ExternalSecretMetadataPolicyFetch {
+		out, err = pm.getParameterTags(ctx, ref)
+	} else {
+		out, err = pm.getParameterValue(ctx, ref)
+	}
+	metrics.ObserveAPICall(constants.ProviderAWSPS, constants.CallAWSPSGetParameter, err)
+	nsf := esv1.NoSecretError{}
+	var nf *ssmTypes.ParameterNotFound
+	if errors.As(err, &nf) || errors.As(err, &nsf) {
+		return nil, esv1.NoSecretErr
+	}
+	if err != nil {
+		return nil, awsutil.SanitizeErr(err)
+	}
+	if ref.Property == "" {
+		if out.Parameter.Value != nil {
+			return []byte(*out.Parameter.Value), nil
+		}
+		return nil, fmt.Errorf("invalid secret received. parameter value is nil for key: %s", ref.Key)
+	}
+	idx := strings.Index(ref.Property, ".")
+	if idx > -1 {
+		refProperty := strings.ReplaceAll(ref.Property, ".", "\\.")
+		val := gjson.Get(*out.Parameter.Value, refProperty)
+		if val.Exists() {
+			return []byte(val.String()), nil
+		}
+	}
+	val := gjson.Get(*out.Parameter.Value, ref.Property)
+	if !val.Exists() {
+		return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
+	}
+	return []byte(val.String()), nil
+}
+
+func (pm *ParameterStore) getParameterTags(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (*ssm.GetParameterOutput, error) {
+	param := ssm.GetParameterOutput{
+		Parameter: &ssmTypes.Parameter{
+			Name: pm.parameterNameWithVersion(ref),
+		},
+	}
+	tags, err := pm.getTagsByName(ctx, &param)
+	if err != nil {
+		return nil, err
+	}
+
+	jsonStr, err := awsutil.ParameterTagsToJSONString(tags)
+	if err != nil {
+		return nil, err
+	}
+
+	out := &ssm.GetParameterOutput{
+		Parameter: &ssmTypes.Parameter{
+			Value: &jsonStr,
+		},
+	}
+	return out, nil
+}
+
+func (pm *ParameterStore) getParameterValue(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (*ssm.GetParameterOutput, error) {
+	out, err := pm.client.GetParameter(ctx, &ssm.GetParameterInput{
+		Name:           pm.parameterNameWithVersion(ref),
+		WithDecryption: aws.Bool(true),
+	})
+
+	return out, err
+}
+
+// GetSecretMap returns multiple k/v pairs from the provider.
+func (pm *ParameterStore) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	data, err := pm.GetSecret(ctx, ref)
+	if err != nil {
+		return nil, err
+	}
+	kv := make(map[string]json.RawMessage)
+	err = json.Unmarshal(data, &kv)
+	if err != nil {
+		return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
+	}
+	secretData := make(map[string][]byte)
+	for k, v := range kv {
+		var strVal string
+		err = json.Unmarshal(v, &strVal)
+		if err == nil {
+			secretData[k] = []byte(strVal)
+		} else {
+			secretData[k] = v
+		}
+	}
+	return secretData, nil
+}
+
+func (pm *ParameterStore) parameterNameWithVersion(ref esv1.ExternalSecretDataRemoteRef) *string {
+	name := pm.prefix + ref.Key
+	if ref.Version != "" {
+		// see docs: https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-versions.html#reference-parameter-version
+		name += ":" + ref.Version
+	}
+	return &name
+}
+
+// Close cleans up resources held by the ParameterStore provider.
+func (pm *ParameterStore) Close(_ context.Context) error {
+	return nil
+}
+
+// Validate checks if the provider is configured correctly.
+func (pm *ParameterStore) Validate() (esv1.ValidationResult, error) {
+	// skip validation stack because it depends on the namespace
+	// of the ExternalSecret
+	if pm.referentAuth {
+		return esv1.ValidationResultUnknown, nil
+	}
+	_, err := pm.cfg.Credentials.Retrieve(context.Background())
+	if err != nil {
+		return esv1.ValidationResultError, err
+	}
+	return esv1.ValidationResultReady, nil
+}
+
+func (pm *ParameterStore) constructMetadataWithDefaults(data *apiextensionsv1.JSON) (*metadata.PushSecretMetadata[PushSecretMetadataSpec], error) {
+	var (
+		meta *metadata.PushSecretMetadata[PushSecretMetadataSpec]
+		err  error
+	)
+
+	meta, err = metadata.ParseMetadataParameters[PushSecretMetadataSpec](data)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse metadata: %w", err)
+	}
+
+	if meta == nil {
+		meta = &metadata.PushSecretMetadata[PushSecretMetadataSpec]{}
+	}
+
+	if meta.Spec.Description == "" {
+		meta.Spec.Description = fmt.Sprintf("secret '%s:%s'", managedBy, externalSecrets)
+	}
+
+	if meta.Spec.Tier.Type == "" {
+		meta.Spec.Tier.Type = "Standard"
+	}
+
+	if meta.Spec.SecretType == "" {
+		meta.Spec.SecretType = "String"
+	}
+
+	if meta.Spec.KMSKeyID == "" {
+		meta.Spec.KMSKeyID = "alias/aws/ssm"
+	}
+
+	if len(meta.Spec.Tags) > 0 {
+		if _, exists := meta.Spec.Tags[managedBy]; exists {
+			return nil, fmt.Errorf("error parsing tags in metadata: Cannot specify a '%s' tag", managedBy)
+		}
+	} else {
+		meta.Spec.Tags = make(map[string]string)
+	}
+	// always add the managedBy tag
+	meta.Spec.Tags[managedBy] = externalSecrets
+
+	return meta, nil
+}
+
+// computeTagsToUpdate compares the current tags with the desired metaTags and returns a slice of ssmTypes.Tag
+// that should be set on the resource. It also returns a boolean indicating if any tag was added or modified.
+func computeTagsToUpdate(tags, metaTags map[string]string) ([]ssmTypes.Tag, bool) {
+	result := make([]ssmTypes.Tag, 0, len(metaTags))
+	modified := false
+	for k, v := range metaTags {
+		if _, exists := tags[k]; !exists || tags[k] != v {
+			if k != managedBy {
+				modified = true
+			}
+		}
+		result = append(result, ssmTypes.Tag{
+			Key:   new(k),
+			Value: new(v),
+		})
+	}
+	return result, modified
+}

+ 1308 - 0
providers/v2/aws/store/parameterstore/parameterstore_test.go

@@ -0,0 +1,1308 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package parameterstore
+
+import (
+	"context"
+	"errors"
+	"strings"
+	"testing"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/ssm"
+	ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
+	"github.com/google/go-cmp/cmp"
+	"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"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	fakeps "github.com/external-secrets/external-secrets/providers/v1/aws/parameterstore/fake"
+	awsutil "github.com/external-secrets/external-secrets/providers/v1/aws/util"
+	"github.com/external-secrets/external-secrets/runtime/esutils/metadata"
+	"github.com/external-secrets/external-secrets/runtime/testing/fake"
+)
+
+const (
+	errInvalidProperty = "key INVALPROP does not exist in secret"
+	invalidProp        = "INVALPROP"
+)
+
+var (
+	fakeSecretKey = "fakeSecretKey"
+	fakeValue     = "fakeValue"
+)
+
+type parameterstoreTestCase struct {
+	fakeClient     *fakeps.Client
+	apiInput       *ssm.GetParameterInput
+	apiOutput      *ssm.GetParameterOutput
+	remoteRef      *esv1.ExternalSecretDataRemoteRef
+	apiErr         error
+	expectError    string
+	expectedSecret string
+	expectedData   map[string][]byte
+	prefix         string
+}
+
+func makeValidParameterStoreTestCase() *parameterstoreTestCase {
+	return &parameterstoreTestCase{
+		fakeClient:     &fakeps.Client{},
+		apiInput:       makeValidAPIInput(),
+		apiOutput:      makeValidAPIOutput(),
+		remoteRef:      makeValidRemoteRef(),
+		apiErr:         nil,
+		prefix:         "",
+		expectError:    "",
+		expectedSecret: "",
+		expectedData:   make(map[string][]byte),
+	}
+}
+
+func makeValidAPIInput() *ssm.GetParameterInput {
+	return &ssm.GetParameterInput{
+		Name:           aws.String("/baz"),
+		WithDecryption: aws.Bool(true),
+	}
+}
+
+func makeValidAPIOutput() *ssm.GetParameterOutput {
+	return &ssm.GetParameterOutput{
+		Parameter: &ssmtypes.Parameter{
+			Value: aws.String("RRRRR"),
+		},
+	}
+}
+
+func makeValidRemoteRef() *esv1.ExternalSecretDataRemoteRef {
+	return &esv1.ExternalSecretDataRemoteRef{
+		Key: "/baz",
+	}
+}
+
+func makeValidParameterStoreTestCaseCustom(tweaks ...func(pstc *parameterstoreTestCase)) *parameterstoreTestCase {
+	pstc := makeValidParameterStoreTestCase()
+	for _, fn := range tweaks {
+		fn(pstc)
+	}
+	pstc.fakeClient.WithValue(pstc.apiInput, pstc.apiOutput, pstc.apiErr)
+	return pstc
+}
+
+func TestSSMResolver(t *testing.T) {
+	endpointEnvKey := SSMEndpointEnv
+	endpointURL := "http://ssm.foo"
+
+	t.Setenv(endpointEnvKey, endpointURL)
+
+	f, err := customEndpointResolver{}.ResolveEndpoint(context.Background(), ssm.EndpointParameters{})
+
+	assert.Nil(t, err)
+	assert.Equal(t, endpointURL, f.URI.String())
+}
+
+func TestDeleteSecret(t *testing.T) {
+	fakeClient := fakeps.Client{}
+	parameterName := "parameter"
+	managedBy := "managed-by"
+	manager := "external-secrets"
+	ssmTag := ssmtypes.Tag{
+		Key:   &managedBy,
+		Value: &manager,
+	}
+	type args struct {
+		client                fakeps.Client
+		getParameterOutput    *ssm.GetParameterOutput
+		listTagsOutput        *ssm.ListTagsForResourceOutput
+		deleteParameterOutput *ssm.DeleteParameterOutput
+		getParameterError     error
+		listTagsError         error
+		deleteParameterError  error
+	}
+
+	type want struct {
+		err error
+	}
+
+	type testCase struct {
+		args   args
+		want   want
+		reason string
+	}
+	tests := map[string]testCase{
+		"Deletes Successfully": {
+			args: args{
+				client: fakeClient,
+				getParameterOutput: &ssm.GetParameterOutput{
+					Parameter: &ssmtypes.Parameter{
+						Name: &parameterName,
+					},
+				},
+				listTagsOutput: &ssm.ListTagsForResourceOutput{
+					TagList: []ssmtypes.Tag{ssmTag},
+				},
+				deleteParameterOutput: nil,
+				getParameterError:     nil,
+				listTagsError:         nil,
+				deleteParameterError:  nil,
+			},
+			want: want{
+				err: nil,
+			},
+			reason: "",
+		},
+		"Secret Not Found": {
+			args: args{
+				client:                fakeClient,
+				getParameterOutput:    nil,
+				listTagsOutput:        nil,
+				deleteParameterOutput: nil,
+				getParameterError: &ssmtypes.ParameterNotFound{
+					Message: aws.String("not here, sorry dude"),
+				},
+				listTagsError:        nil,
+				deleteParameterError: nil,
+			},
+			want: want{
+				err: nil,
+			},
+			reason: "",
+		},
+		"No permissions to get secret": {
+			args: args{
+				client:                fakeClient,
+				getParameterOutput:    nil,
+				listTagsOutput:        nil,
+				deleteParameterOutput: nil,
+				getParameterError:     errors.New("no permissions"),
+				listTagsError:         nil,
+				deleteParameterError:  nil,
+			},
+			want: want{
+				err: errors.New("no permissions"),
+			},
+			reason: "",
+		},
+		"No permissions to get tags": {
+			args: args{
+				client: fakeClient,
+				getParameterOutput: &ssm.GetParameterOutput{
+					Parameter: &ssmtypes.Parameter{
+						Name: &parameterName,
+					},
+				},
+				listTagsOutput:        nil,
+				deleteParameterOutput: nil,
+				getParameterError:     nil,
+				listTagsError:         errors.New("no permissions"),
+				deleteParameterError:  nil,
+			},
+			want: want{
+				err: errors.New("no permissions"),
+			},
+			reason: "",
+		},
+		"Secret Not Managed by External Secrets": {
+			args: args{
+				client: fakeClient,
+				getParameterOutput: &ssm.GetParameterOutput{
+					Parameter: &ssmtypes.Parameter{
+						Name: &parameterName,
+					},
+				},
+				listTagsOutput: &ssm.ListTagsForResourceOutput{
+					TagList: []ssmtypes.Tag{},
+				},
+				deleteParameterOutput: nil,
+				getParameterError:     nil,
+				listTagsError:         nil,
+				deleteParameterError:  nil,
+			},
+			want: want{
+				err: nil,
+			},
+			reason: "",
+		},
+		"No permissions delete secret": {
+			args: args{
+				client: fakeClient,
+				getParameterOutput: &ssm.GetParameterOutput{
+					Parameter: &ssmtypes.Parameter{
+						Name: &parameterName,
+					},
+				},
+				listTagsOutput: &ssm.ListTagsForResourceOutput{
+					TagList: []ssmtypes.Tag{ssmTag},
+				},
+				deleteParameterOutput: nil,
+				getParameterError:     nil,
+				listTagsError:         nil,
+				deleteParameterError:  errors.New("no permissions"),
+			},
+			want: want{
+				err: errors.New("no permissions"),
+			},
+			reason: "",
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			ref := fake.PushSecretData{RemoteKey: remoteKey}
+			ps := ParameterStore{
+				client: &tc.args.client,
+			}
+			tc.args.client.GetParameterFn = fakeps.NewGetParameterFn(tc.args.getParameterOutput, tc.args.getParameterError)
+			tc.args.client.ListTagsForResourceFn = fakeps.NewListTagsForResourceFn(tc.args.listTagsOutput, tc.args.listTagsError)
+			tc.args.client.DeleteParameterFn = fakeps.NewDeleteParameterFn(tc.args.deleteParameterOutput, tc.args.deleteParameterError)
+			err := ps.DeleteSecret(context.TODO(), ref)
+
+			// Error nil XOR tc.want.err nil
+			if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
+				t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
+			}
+
+			// if errors are the same type but their contents do not match
+			if err != nil && tc.want.err != nil {
+				if !strings.Contains(err.Error(), tc.want.err.Error()) {
+					t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
+				}
+			}
+		})
+	}
+}
+
+const remoteKey = "fake-key"
+
+func TestPushSecret(t *testing.T) {
+	invalidParameters := &ssmtypes.InvalidParameters{}
+	alreadyExistsError := &ssmtypes.AlreadyExistsException{}
+	fakeSecret := &corev1.Secret{
+		Data: map[string][]byte{
+			fakeSecretKey: []byte(fakeValue),
+		},
+	}
+
+	managedByESO := ssmtypes.Tag{
+		Key:   &managedBy,
+		Value: &externalSecrets,
+	}
+
+	putParameterOutput := &ssm.PutParameterOutput{}
+	getParameterOutput := &ssm.GetParameterOutput{}
+	describeParameterOutput := &ssm.DescribeParametersOutput{}
+	validListTagsForResourceOutput := &ssm.ListTagsForResourceOutput{
+		TagList: []ssmtypes.Tag{managedByESO},
+	}
+	noTagsResourceOutput := &ssm.ListTagsForResourceOutput{}
+
+	validGetParameterOutput := &ssm.GetParameterOutput{
+		Parameter: &ssmtypes.Parameter{},
+	}
+
+	sameGetParameterOutput := &ssm.GetParameterOutput{
+		Parameter: &ssmtypes.Parameter{
+			Value: &fakeValue,
+		},
+	}
+
+	type args struct {
+		store    *esv1.AWSProvider
+		metadata *apiextensionsv1.JSON
+		client   fakeps.Client
+	}
+
+	type want struct {
+		err error
+	}
+
+	tests := map[string]struct {
+		reason string
+		args   args
+		want   want
+	}{
+		"PutParameterSucceeds": {
+			reason: "a parameter can be successfully pushed to aws parameter store",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				client: fakeps.Client{
+					PutParameterFn: fakeps.NewPutParameterFn(putParameterOutput, nil, func(input *ssm.PutParameterInput) {
+						assert.Len(t, input.Tags, 1)
+						assert.Contains(t, input.Tags, managedByESO)
+					}),
+					GetParameterFn:       fakeps.NewGetParameterFn(getParameterOutput, nil),
+					DescribeParametersFn: fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+					ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(validListTagsForResourceOutput, nil, func(input *ssm.ListTagsForResourceInput) {
+						assert.Equal(t, "/external-secrets/parameters/fake-key", input.ResourceId)
+					}),
+					RemoveTagsFromResourceFn: fakeps.NewRemoveTagsFromResourceFn(&ssm.RemoveTagsFromResourceOutput{}, nil),
+					AddTagsToResourceFn:      fakeps.NewAddTagsToResourceFn(&ssm.AddTagsToResourceOutput{}, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetParameterFailsWhenNoNameProvided": {
+			reason: "test push secret with no name gives error",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				client: fakeps.Client{
+					PutParameterFn:        fakeps.NewPutParameterFn(putParameterOutput, nil),
+					GetParameterFn:        fakeps.NewGetParameterFn(getParameterOutput, invalidParameters),
+					DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+					ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(validListTagsForResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: invalidParameters,
+			},
+		},
+		"SetSecretWhenAlreadyExists": {
+			reason: "test push secret with secret that already exists gives error",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				client: fakeps.Client{
+					PutParameterFn:        fakeps.NewPutParameterFn(putParameterOutput, alreadyExistsError),
+					GetParameterFn:        fakeps.NewGetParameterFn(getParameterOutput, nil),
+					DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+					ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(validListTagsForResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: alreadyExistsError,
+			},
+		},
+		"GetSecretWithValidParameters": {
+			reason: "Get secret with valid parameters",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				client: fakeps.Client{
+					PutParameterFn:        fakeps.NewPutParameterFn(putParameterOutput, nil),
+					GetParameterFn:        fakeps.NewGetParameterFn(validGetParameterOutput, nil),
+					DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+					ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(validListTagsForResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretNotManagedByESO": {
+			reason: "SetSecret to the parameter store but tags are not managed by ESO",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				client: fakeps.Client{
+					PutParameterFn:        fakeps.NewPutParameterFn(putParameterOutput, nil),
+					GetParameterFn:        fakeps.NewGetParameterFn(validGetParameterOutput, nil),
+					DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+					ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(noTagsResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: errors.New("secret not managed by external-secrets"),
+			},
+		},
+		"SetSecretGetTagsError": {
+			reason: "SetSecret to the parameter store returns error while obtaining tags",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				client: fakeps.Client{
+					PutParameterFn:        fakeps.NewPutParameterFn(putParameterOutput, nil),
+					GetParameterFn:        fakeps.NewGetParameterFn(validGetParameterOutput, nil),
+					DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+					ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(nil, errors.New("you shall not tag")),
+				},
+			},
+			want: want{
+				err: errors.New("you shall not tag"),
+			},
+		},
+		"SetSecretContentMatches": {
+			reason: "No ops",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				client: fakeps.Client{
+					PutParameterFn:        fakeps.NewPutParameterFn(putParameterOutput, nil),
+					GetParameterFn:        fakeps.NewGetParameterFn(sameGetParameterOutput, nil),
+					DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+					ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(validListTagsForResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithValidMetadata": {
+			reason: "test push secret with valid parameterStoreType metadata",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{
+						"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+						"kind": "PushSecretMetadata",
+						"spec": {
+							"secretType": "SecureString",
+							"kmsKeyID": "arn:aws:kms:sa-east-1:00000000000:key/bb123123-b2b0-4f60-ac3a-44a13f0e6b6c"
+						}
+					}`),
+				},
+				client: fakeps.Client{
+					PutParameterFn:        fakeps.NewPutParameterFn(putParameterOutput, nil),
+					GetParameterFn:        fakeps.NewGetParameterFn(sameGetParameterOutput, nil),
+					DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+					ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(validListTagsForResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithValidMetadataListString": {
+			reason: "test push secret with valid parameterStoreType metadata and unused parameterStoreKeyID",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{
+						"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+						"kind": "PushSecretMetadata",
+						"spec": {
+							"secretType": "StringList"
+						}
+					}`),
+				},
+				client: fakeps.Client{
+					PutParameterFn:        fakeps.NewPutParameterFn(putParameterOutput, nil),
+					GetParameterFn:        fakeps.NewGetParameterFn(sameGetParameterOutput, nil),
+					DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+					ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(validListTagsForResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithInvalidMetadata": {
+			reason: "test push secret with invalid metadata structure",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{ fakeMetadataKey: "" }`),
+				},
+				client: fakeps.Client{
+					PutParameterFn:        fakeps.NewPutParameterFn(putParameterOutput, nil),
+					GetParameterFn:        fakeps.NewGetParameterFn(sameGetParameterOutput, nil),
+					DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+					ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(validListTagsForResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: errors.New(
+					`failed to parse metadata: failed to parse kubernetes.external-secrets.io/v1alpha1 PushSecretMetadata: error unmarshaling JSON: while decoding JSON: json: unknown field "fakeMetadataKey"`,
+				),
+			},
+		},
+		"GetRemoteSecretWithoutDecryption": {
+			reason: "test if push secret's get remote source is encrypted for valid comparison",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{
+						"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+						"kind": "PushSecretMetadata",
+						"spec": {
+							"secretType": "SecureString",
+							"kmsKeyID": "arn:aws:kms:sa-east-1:00000000000:key/bb123123-b2b0-4f60-ac3a-44a13f0e6b6c"
+						}
+					}`),
+				},
+				client: fakeps.Client{
+					PutParameterFn: fakeps.NewPutParameterFn(putParameterOutput, nil),
+					GetParameterFn: fakeps.NewGetParameterFn(&ssm.GetParameterOutput{
+						Parameter: &ssmtypes.Parameter{
+							Type:  ssmtypes.ParameterTypeSecureString,
+							Value: aws.String("sensitive"),
+						},
+					}, nil),
+					DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+					ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(validListTagsForResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: errors.New("unable to compare 'sensitive' result, ensure to request a decrypted value"),
+			},
+		},
+		"SecretWithAdvancedTier": {
+			reason: "test if we can provide advanced tier policies",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{
+						"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+						"kind": "PushSecretMetadata",
+						"spec": {
+							"secretType": "SecureString",
+							"kmsKeyID": "arn:aws:kms:sa-east-1:00000000000:key/bb123123-b2b0-4f60-ac3a-44a13f0e6b6c",
+							"tier": {
+								"type": "Advanced",
+								"policies": [
+										{
+												"type": "Expiration",
+												"version": "1.0",
+												"attributes": {
+														"timestamp": "2024-12-02T21:34:33.000Z"
+												}
+										},
+										{
+												"type": "ExpirationNotification",
+												"version": "1.0",
+												"attributes": {
+														"before": "2",
+														"unit": "Days"
+												}
+										}
+								]
+							}
+						}
+					}`),
+				},
+				client: fakeps.Client{
+					PutParameterFn: fakeps.NewPutParameterFn(putParameterOutput, nil),
+					GetParameterFn: fakeps.NewGetParameterFn(&ssm.GetParameterOutput{
+						Parameter: &ssmtypes.Parameter{
+							Type:  ssmtypes.ParameterTypeSecureString,
+							Value: aws.String("sensitive"),
+						},
+					}, nil),
+					DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+					ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(validListTagsForResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: errors.New("unable to compare 'sensitive' result, ensure to request a decrypted value"),
+			},
+		},
+		"SecretPatchTags": {
+			reason: "test if we can configure tags for the secret",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{
+						"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+						"kind": "PushSecretMetadata",
+						"spec": {
+							"tags": {
+								"env": "sandbox",
+								"rotation": "1h"
+							},
+						}
+					}`),
+				},
+				client: fakeps.Client{
+					PutParameterFn: fakeps.NewPutParameterFn(putParameterOutput, nil, func(input *ssm.PutParameterInput) {
+						assert.Len(t, input.Tags, 0)
+					}),
+					GetParameterFn: fakeps.NewGetParameterFn(&ssm.GetParameterOutput{
+						Parameter: &ssmtypes.Parameter{
+							Value: aws.String("some-value"),
+						},
+					}, nil),
+					DescribeParametersFn: fakeps.NewDescribeParametersFn(&ssm.DescribeParametersOutput{}, nil),
+					ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(&ssm.ListTagsForResourceOutput{
+						TagList: []ssmtypes.Tag{managedByESO,
+							{Key: new("team"), Value: new("no-longer-needed")},
+							{Key: new("rotation"), Value: new("10m")},
+						},
+					}, nil),
+					RemoveTagsFromResourceFn: fakeps.NewRemoveTagsFromResourceFn(&ssm.RemoveTagsFromResourceOutput{}, nil, func(input *ssm.RemoveTagsFromResourceInput) {
+						assert.Len(t, input.TagKeys, 1)
+						assert.Equal(t, []string{"team"}, input.TagKeys)
+					}),
+					AddTagsToResourceFn: fakeps.NewAddTagsToResourceFn(&ssm.AddTagsToResourceOutput{}, nil, func(input *ssm.AddTagsToResourceInput) {
+						assert.Len(t, input.Tags, 3)
+						assert.Contains(t, input.Tags, ssmtypes.Tag{Key: &managedBy, Value: &externalSecrets})
+						assert.Contains(t, input.Tags, ssmtypes.Tag{Key: new("env"), Value: new("sandbox")})
+						assert.Contains(t, input.Tags, ssmtypes.Tag{Key: new("rotation"), Value: new("1h")})
+					}),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			psd := fake.PushSecretData{SecretKey: fakeSecretKey, RemoteKey: remoteKey}
+			if tc.args.metadata != nil {
+				psd.Metadata = tc.args.metadata
+			}
+			ps := ParameterStore{
+				client: &tc.args.client,
+			}
+			err := ps.PushSecret(context.TODO(), fakeSecret, psd)
+
+			// Error nil XOR tc.want.err nil
+			if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
+				t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
+			}
+
+			// if errors are the same type but their contents do not match
+			if err != nil && tc.want.err != nil {
+				if !strings.Contains(err.Error(), tc.want.err.Error()) {
+					t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %s", name, tc.reason, tc.want.err, err)
+				}
+			}
+		})
+	}
+}
+
+func TestPushSecretWithPrefix(t *testing.T) {
+	fakeSecret := &corev1.Secret{
+		Data: map[string][]byte{
+			fakeSecretKey: []byte(fakeValue),
+		},
+	}
+	managedByESO := ssmtypes.Tag{
+		Key:   &managedBy,
+		Value: &externalSecrets,
+	}
+	putParameterOutput := &ssm.PutParameterOutput{}
+	getParameterOutput := &ssm.GetParameterOutput{}
+	describeParameterOutput := &ssm.DescribeParametersOutput{}
+	validListTagsForResourceOutput := &ssm.ListTagsForResourceOutput{
+		TagList: []ssmtypes.Tag{managedByESO},
+	}
+
+	client := fakeps.Client{
+		PutParameterFn:        fakeps.NewPutParameterFn(putParameterOutput, nil),
+		GetParameterFn:        fakeps.NewGetParameterFn(getParameterOutput, nil),
+		DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+		ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(validListTagsForResourceOutput, nil),
+	}
+
+	psd := fake.PushSecretData{SecretKey: fakeSecretKey, RemoteKey: remoteKey}
+	ps := ParameterStore{
+		client: &client,
+		prefix: "/test/this/thing/",
+	}
+	err := ps.PushSecret(context.TODO(), fakeSecret, psd)
+	require.NoError(t, err)
+
+	input := client.PutParameterFnCalledWith[0][0]
+	assert.Equal(t, "/test/this/thing/fake-key", *input.Name)
+}
+
+func TestPushSecretWithoutKeyAndEncodedAsDecodedTrue(t *testing.T) {
+	fakeSecret := &corev1.Secret{
+		Data: map[string][]byte{
+			fakeSecretKey: []byte(fakeValue),
+		},
+	}
+	managedByESO := ssmtypes.Tag{
+		Key:   &managedBy,
+		Value: &externalSecrets,
+	}
+	putParameterOutput := &ssm.PutParameterOutput{}
+	getParameterOutput := &ssm.GetParameterOutput{}
+	describeParameterOutput := &ssm.DescribeParametersOutput{}
+	validListTagsForResourceOutput := &ssm.ListTagsForResourceOutput{
+		TagList: []ssmtypes.Tag{managedByESO},
+	}
+
+	client := fakeps.Client{
+		PutParameterFn:        fakeps.NewPutParameterFn(putParameterOutput, nil),
+		GetParameterFn:        fakeps.NewGetParameterFn(getParameterOutput, nil),
+		DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+		ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(validListTagsForResourceOutput, nil),
+	}
+
+	psd := fake.PushSecretData{RemoteKey: remoteKey, Metadata: &apiextensionsv1.JSON{Raw: []byte(`
+apiVersion: kubernetes.external-secrets.io/v1alpha1
+kind: PushSecretMetadata
+spec:
+  encodeAsDecoded: true
+`)}}
+	ps := ParameterStore{
+		client: &client,
+		prefix: "/test/this/thing/",
+	}
+	err := ps.PushSecret(context.TODO(), fakeSecret, psd)
+	require.NoError(t, err)
+
+	input := client.PutParameterFnCalledWith[0][0]
+	assert.Equal(t, "{\"fakeSecretKey\":\"fakeValue\"}", *input.Value)
+}
+
+func TestPushSecretCalledOnlyOnce(t *testing.T) {
+	fakeSecret := &corev1.Secret{
+		Data: map[string][]byte{
+			fakeSecretKey: []byte(fakeValue),
+		},
+	}
+
+	managedByESO := ssmtypes.Tag{
+		Key:   &managedBy,
+		Value: &externalSecrets,
+	}
+
+	putParameterOutput := &ssm.PutParameterOutput{}
+	validGetParameterOutput := &ssm.GetParameterOutput{
+		Parameter: &ssmtypes.Parameter{
+			Value: &fakeValue,
+		},
+	}
+	describeParameterOutput := &ssm.DescribeParametersOutput{}
+	validListTagsForResourceOutput := &ssm.ListTagsForResourceOutput{
+		TagList: []ssmtypes.Tag{managedByESO},
+	}
+
+	client := fakeps.Client{
+		PutParameterFn:        fakeps.NewPutParameterFn(putParameterOutput, nil),
+		GetParameterFn:        fakeps.NewGetParameterFn(validGetParameterOutput, nil),
+		DescribeParametersFn:  fakeps.NewDescribeParametersFn(describeParameterOutput, nil),
+		ListTagsForResourceFn: fakeps.NewListTagsForResourceFn(validListTagsForResourceOutput, nil),
+	}
+
+	psd := fake.PushSecretData{SecretKey: fakeSecretKey, RemoteKey: remoteKey}
+	ps := ParameterStore{
+		client: &client,
+	}
+
+	require.NoError(t, ps.PushSecret(context.TODO(), fakeSecret, psd))
+
+	assert.Equal(t, 0, client.PutParameterCalledN)
+}
+
+// test the ssm<->aws interface
+// make sure correct values are passed and errors are handled accordingly.
+func TestGetSecret(t *testing.T) {
+	// good case: key is passed in, output is sent back
+	setSecretString := func(pstc *parameterstoreTestCase) {
+		pstc.apiOutput.Parameter.Value = aws.String("RRRRR")
+		pstc.expectedSecret = "RRRRR"
+	}
+
+	// good case: key is passed in and prefix is set, output is sent back
+	setSecretStringWithPrefix := func(pstc *parameterstoreTestCase) {
+		pstc.apiInput = &ssm.GetParameterInput{
+			Name:           aws.String("/test/this/baz"),
+			WithDecryption: aws.Bool(true),
+		}
+		pstc.prefix = "/test/this"
+		pstc.apiOutput.Parameter.Value = aws.String("RRRRR")
+		pstc.expectedSecret = "RRRRR"
+	}
+
+	// good case: extract property
+	setExtractProperty := func(pstc *parameterstoreTestCase) {
+		pstc.apiOutput.Parameter.Value = aws.String(`{"/shmoo": "bang"}`)
+		pstc.expectedSecret = "bang"
+		pstc.remoteRef.Property = "/shmoo"
+	}
+
+	// good case: extract property with `.`
+	setExtractPropertyWithDot := func(pstc *parameterstoreTestCase) {
+		pstc.apiOutput.Parameter.Value = aws.String(`{"/shmoo.boom": "bang"}`)
+		pstc.expectedSecret = "bang"
+		pstc.remoteRef.Property = "/shmoo.boom"
+	}
+
+	// bad case: missing property
+	setMissingProperty := func(pstc *parameterstoreTestCase) {
+		pstc.apiOutput.Parameter.Value = aws.String(`{"/shmoo": "bang"}`)
+		pstc.remoteRef.Property = "INVALPROP"
+		pstc.expectError = "key INVALPROP does not exist in secret"
+	}
+
+	// bad case: parameter.Value not found
+	setParameterValueNotFound := func(pstc *parameterstoreTestCase) {
+		pstc.apiOutput.Parameter.Value = aws.String("NONEXISTENT")
+		pstc.apiErr = esv1.NoSecretErr
+		pstc.expectError = "Secret does not exist"
+	}
+
+	// bad case: extract property failure due to invalid json
+	setPropertyFail := func(pstc *parameterstoreTestCase) {
+		pstc.apiOutput.Parameter.Value = aws.String(`------`)
+		pstc.remoteRef.Property = invalidProp
+		pstc.expectError = errInvalidProperty
+	}
+
+	// bad case: parameter.Value may be nil but binary is set
+	setParameterValueNil := func(pstc *parameterstoreTestCase) {
+		pstc.apiOutput.Parameter.Value = nil
+		pstc.expectError = "parameter value is nil for key"
+	}
+
+	// base case: api output return error
+	setAPIError := func(pstc *parameterstoreTestCase) {
+		pstc.apiOutput = &ssm.GetParameterOutput{}
+		pstc.apiErr = errors.New("oh no")
+		pstc.expectError = "oh no"
+	}
+
+	// good case: metadata returned
+	setMetadataString := func(pstc *parameterstoreTestCase) {
+		pstc.remoteRef.MetadataPolicy = esv1.ExternalSecretMetadataPolicyFetch
+		output := ssm.ListTagsForResourceOutput{
+			TagList: getTagSlice(),
+		}
+		pstc.fakeClient.ListTagsForResourceFn = fakeps.NewListTagsForResourceFn(&output, nil)
+		pstc.expectedSecret, _ = awsutil.ParameterTagsToJSONString(normaliseTags(getTagSlice()))
+	}
+
+	// good case: metadata property returned
+	setMetadataProperty := func(pstc *parameterstoreTestCase) {
+		pstc.remoteRef.MetadataPolicy = esv1.ExternalSecretMetadataPolicyFetch
+		output := ssm.ListTagsForResourceOutput{
+			TagList: getTagSlice(),
+		}
+		pstc.fakeClient.ListTagsForResourceFn = fakeps.NewListTagsForResourceFn(&output, nil)
+		pstc.remoteRef.Property = "tagname2"
+		pstc.expectedSecret = "tagvalue2"
+	}
+
+	// bad case: metadata property not found
+	setMetadataMissingProperty := func(pstc *parameterstoreTestCase) {
+		pstc.remoteRef.MetadataPolicy = esv1.ExternalSecretMetadataPolicyFetch
+		output := ssm.ListTagsForResourceOutput{
+			TagList: getTagSlice(),
+		}
+		pstc.fakeClient.ListTagsForResourceFn = fakeps.NewListTagsForResourceFn(&output, nil)
+		pstc.remoteRef.Property = invalidProp
+		pstc.expectError = errInvalidProperty
+	}
+
+	successCases := []*parameterstoreTestCase{
+		makeValidParameterStoreTestCaseCustom(setSecretStringWithPrefix),
+		makeValidParameterStoreTestCaseCustom(setSecretString),
+		makeValidParameterStoreTestCaseCustom(setExtractProperty),
+		makeValidParameterStoreTestCaseCustom(setMissingProperty),
+		makeValidParameterStoreTestCaseCustom(setPropertyFail),
+		makeValidParameterStoreTestCaseCustom(setParameterValueNil),
+		makeValidParameterStoreTestCaseCustom(setAPIError),
+		makeValidParameterStoreTestCaseCustom(setExtractPropertyWithDot),
+		makeValidParameterStoreTestCaseCustom(setParameterValueNotFound),
+		makeValidParameterStoreTestCaseCustom(setMetadataString),
+		makeValidParameterStoreTestCaseCustom(setMetadataProperty),
+		makeValidParameterStoreTestCaseCustom(setMetadataMissingProperty),
+	}
+
+	ps := ParameterStore{}
+	for k, v := range successCases {
+		ps.client = v.fakeClient
+		ps.prefix = v.prefix
+		out, err := ps.GetSecret(context.Background(), *v.remoteRef)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
+		}
+		if cmp.Equal(out, v.expectedSecret) {
+			t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedSecret, out)
+		}
+	}
+}
+
+func TestGetSecretMap(t *testing.T) {
+	// good case: default version & deserialization
+	simpleJSON := func(pstc *parameterstoreTestCase) {
+		pstc.apiOutput.Parameter.Value = aws.String(`{"foo":"bar"}`)
+		pstc.expectedData["foo"] = []byte("bar")
+	}
+
+	// good case: default version & complex json
+	complexJSON := func(pstc *parameterstoreTestCase) {
+		pstc.apiOutput.Parameter.Value = aws.String(`{"int": 42, "str": "str", "nested": {"foo":"bar"}}`)
+		pstc.expectedData["int"] = []byte("42")
+		pstc.expectedData["str"] = []byte("str")
+		pstc.expectedData["nested"] = []byte(`{"foo":"bar"}`)
+	}
+
+	// bad case: api error returned
+	setAPIError := func(pstc *parameterstoreTestCase) {
+		pstc.apiOutput.Parameter = &ssmtypes.Parameter{}
+		pstc.expectError = "some api err"
+		pstc.apiErr = errors.New("some api err")
+	}
+	// bad case: invalid json
+	setInvalidJSON := func(pstc *parameterstoreTestCase) {
+		pstc.apiOutput.Parameter.Value = aws.String(`-----------------`)
+		pstc.expectError = "unable to unmarshal secret"
+	}
+
+	successCases := []*parameterstoreTestCase{
+		makeValidParameterStoreTestCaseCustom(simpleJSON),
+		makeValidParameterStoreTestCaseCustom(complexJSON),
+		makeValidParameterStoreTestCaseCustom(setAPIError),
+		makeValidParameterStoreTestCaseCustom(setInvalidJSON),
+	}
+
+	ps := ParameterStore{}
+	for k, v := range successCases {
+		ps.client = v.fakeClient
+		out, err := ps.GetSecretMap(context.Background(), *v.remoteRef)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf("[%d] unexpected error: %q, expected: %q", k, err.Error(), v.expectError)
+		}
+		if err == nil && !cmp.Equal(out, v.expectedData) {
+			t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out)
+		}
+	}
+}
+
+func makeValidParameterStore() *esv1.SecretStore {
+	return &esv1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "aws-parameterstore",
+			Namespace: "default",
+		},
+		Spec: esv1.SecretStoreSpec{
+			Provider: &esv1.SecretStoreProvider{
+				AWS: &esv1.AWSProvider{
+					Service: esv1.AWSServiceParameterStore,
+					Region:  "us-east-1",
+				},
+			},
+		},
+	}
+}
+
+func ErrorContains(out error, want string) bool {
+	if out == nil {
+		return want == ""
+	}
+	if want == "" {
+		return false
+	}
+	return strings.Contains(out.Error(), want)
+}
+
+func getTagSlice() []ssmtypes.Tag {
+	tagKey1 := "tagname1"
+	tagValue1 := "tagvalue1"
+	tagKey2 := "tagname2"
+	tagValue2 := "tagvalue2"
+
+	return []ssmtypes.Tag{
+		{
+			Key:   &tagKey1,
+			Value: &tagValue1,
+		},
+		{
+			Key:   &tagKey2,
+			Value: &tagValue2,
+		},
+	}
+}
+
+func normaliseTags(input []ssmtypes.Tag) map[string]string {
+	tags := make(map[string]string, len(input))
+	for _, tag := range input {
+		if tag.Key != nil && tag.Value != nil {
+			tags[*tag.Key] = *tag.Value
+		}
+	}
+	return tags
+}
+
+func TestSecretExists(t *testing.T) {
+	parameterOutput := &ssm.GetParameterOutput{
+		Parameter: &ssmtypes.Parameter{
+			Value: aws.String("sensitive"),
+		},
+	}
+
+	blankParameterOutput := &ssm.GetParameterOutput{}
+	getParameterCorrectErr := ssmtypes.ResourceNotFoundException{}
+	getParameterWrongErr := ssmtypes.InvalidParameters{}
+
+	pushSecretDataWithoutProperty := fake.PushSecretData{SecretKey: "fake-secret-key", RemoteKey: fakeSecretKey, Property: ""}
+
+	type args struct {
+		store          *esv1.AWSProvider
+		client         fakeps.Client
+		pushSecretData fake.PushSecretData
+	}
+
+	type want struct {
+		err       error
+		wantError bool
+	}
+
+	tests := map[string]struct {
+		args args
+		want want
+	}{
+		"SecretExistsReturnsTrueForExistingParameter": {
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				client: fakeps.Client{
+					GetParameterFn: fakeps.NewGetParameterFn(parameterOutput, nil),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err:       nil,
+				wantError: true,
+			},
+		},
+		"SecretExistsReturnsFalseForNonExistingParameter": {
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				client: fakeps.Client{
+					GetParameterFn: fakeps.NewGetParameterFn(blankParameterOutput, &getParameterCorrectErr),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err:       nil,
+				wantError: false,
+			},
+		},
+		"SecretExistsReturnsFalseForErroredParameter": {
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				client: fakeps.Client{
+					GetParameterFn: fakeps.NewGetParameterFn(blankParameterOutput, &getParameterWrongErr),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err:       &getParameterWrongErr,
+				wantError: false,
+			},
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			ps := &ParameterStore{
+				client: &tc.args.client,
+			}
+			got, err := ps.SecretExists(context.Background(), tc.args.pushSecretData)
+
+			assert.Equal(
+				t,
+				tc.want,
+				want{
+					err:       err,
+					wantError: got,
+				})
+		})
+	}
+}
+
+func TestConstructMetadataWithDefaults(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       *apiextensionsv1.JSON
+		expected    *metadata.PushSecretMetadata[PushSecretMetadataSpec]
+		expectError bool
+	}{
+		{
+			name: "Valid metadata with multiple fields",
+			input: &apiextensionsv1.JSON{Raw: []byte(`{
+				"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+				"kind": "PushSecretMetadata",
+				"spec": {
+ 					"description": "test description",
+					"tier": {"type": "Advanced"},
+					"secretType":"SecureString",
+					"kmsKeyID": "custom-kms-key",
+					"tags": {
+						"customKey": "customValue"
+					},
+				}
+			}`)},
+			expected: &metadata.PushSecretMetadata[PushSecretMetadataSpec]{
+				APIVersion: "kubernetes.external-secrets.io/v1alpha1",
+				Kind:       "PushSecretMetadata",
+				Spec: PushSecretMetadataSpec{
+					Description: "test description",
+					Tier: Tier{
+						Type: "Advanced",
+					},
+					SecretType: "SecureString",
+					KMSKeyID:   "custom-kms-key",
+					Tags: map[string]string{
+						"customKey":  "customValue",
+						"managed-by": "external-secrets",
+					},
+				},
+			},
+		},
+		{
+			name:  "Empty metadata, defaults applied",
+			input: nil,
+			expected: &metadata.PushSecretMetadata[PushSecretMetadataSpec]{
+				Spec: PushSecretMetadataSpec{
+					Description: "secret 'managed-by:external-secrets'",
+					Tier: Tier{
+						Type: "Standard",
+					},
+					SecretType: "String",
+					KMSKeyID:   "alias/aws/ssm",
+					Tags: map[string]string{
+						"managed-by": "external-secrets",
+					},
+				},
+			},
+		},
+		{
+			name: "Added default metadata with 'managed-by' tag",
+			input: &apiextensionsv1.JSON{Raw: []byte(`{
+				"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+				"kind": "PushSecretMetadata",
+				"spec": {
+					"description": "adding managed-by tag explicitly",
+					"tags": {
+						"managed-by": "external-secrets",
+						"customKey": "customValue"
+					},
+				}
+			}`)},
+			expectError: true,
+		},
+		{
+			name:        "Invalid metadata format",
+			input:       &apiextensionsv1.JSON{Raw: []byte(`invalid-json`)},
+			expected:    nil,
+			expectError: true,
+		},
+		{
+			name:        "Metadata with 'managed-by' tag specified",
+			input:       &apiextensionsv1.JSON{Raw: []byte(`{"tags":{"managed-by":"invalid"}}`)},
+			expected:    nil,
+			expectError: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result, err := (&ParameterStore{}).constructMetadataWithDefaults(tt.input)
+
+			if tt.expectError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.expected, result)
+			}
+		})
+	}
+}
+
+func TestComputeTagsToUpdate(t *testing.T) {
+	tests := []struct {
+		name     string
+		tags     map[string]string
+		metaTags map[string]string
+		expected []ssmtypes.Tag
+		modified bool
+	}{
+		{
+			name: "No tags to update",
+			tags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+			metaTags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+			expected: []ssmtypes.Tag{
+				{Key: new("key1"), Value: new("value1")},
+				{Key: new("key2"), Value: new("value2")},
+			},
+			modified: false,
+		},
+		{
+			name: "No tags to update as managed-by tag is ignored",
+			tags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+			metaTags: map[string]string{
+				"key1":    "value1",
+				"key2":    "value2",
+				managedBy: externalSecrets,
+			},
+			expected: []ssmtypes.Tag{
+				{Key: new("key1"), Value: new("value1")},
+				{Key: new("key2"), Value: new("value2")},
+				{Key: new(managedBy), Value: new(externalSecrets)},
+			},
+			modified: false,
+		},
+		{
+			name: "Add new tag",
+			tags: map[string]string{
+				"key1": "value1",
+			},
+			metaTags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+			expected: []ssmtypes.Tag{
+				{Key: new("key1"), Value: new("value1")},
+				{Key: new("key2"), Value: new("value2")},
+			},
+			modified: true,
+		},
+		{
+			name: "Update existing tag value",
+			tags: map[string]string{
+				"key1": "value1",
+			},
+			metaTags: map[string]string{
+				"key1": "newValue",
+			},
+			expected: []ssmtypes.Tag{
+				{Key: new("key1"), Value: new("newValue")},
+			},
+			modified: true,
+		},
+		{
+			name:     "Empty tags and metaTags",
+			tags:     map[string]string{},
+			metaTags: map[string]string{},
+			expected: []ssmtypes.Tag{},
+			modified: false,
+		},
+		{
+			name: "Empty tags with non-empty metaTags",
+			tags: map[string]string{},
+			metaTags: map[string]string{
+				"key1": "value1",
+			},
+			expected: []ssmtypes.Tag{
+				{Key: new("key1"), Value: new("value1")},
+			},
+			modified: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result, modified := computeTagsToUpdate(tt.tags, tt.metaTags)
+			assert.ElementsMatch(t, tt.expected, result)
+			assert.Equal(t, tt.modified, modified)
+		})
+	}
+}

+ 48 - 0
providers/v2/aws/store/parameterstore/resolver.go

@@ -0,0 +1,48 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package parameterstore
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+	"os"
+
+	"github.com/aws/aws-sdk-go-v2/service/ssm"
+	smithyendpoints "github.com/aws/smithy-go/endpoints"
+)
+
+// SSMEndpointEnv is the environment variable to use for Parameter Store endpoint.
+const SSMEndpointEnv = "AWS_SSM_ENDPOINT"
+
+// customEndpointResolver is a custom resolver for AWS Parameter Store endpoint.
+type customEndpointResolver struct{}
+
+// ResolveEndpoint resolves the endpoint for the Parameter Store service.
+func (c customEndpointResolver) ResolveEndpoint(ctx context.Context, params ssm.EndpointParameters) (smithyendpoints.Endpoint, error) {
+	endpoint := smithyendpoints.Endpoint{}
+	if v := os.Getenv(SSMEndpointEnv); v != "" {
+		url, err := url.Parse(v)
+		if err != nil {
+			return endpoint, fmt.Errorf("failed to parse ssm endpoint %s: %w", v, err)
+		}
+		endpoint.URI = *url
+		return endpoint, nil
+	}
+	defaultResolver := ssm.NewDefaultEndpointResolverV2()
+	return defaultResolver.ResolveEndpoint(ctx, params)
+}

+ 272 - 0
providers/v2/aws/store/secretsmanager/fake/fake.go

@@ -0,0 +1,272 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package fake provides mock implementations of AWS Secrets Manager interfaces for testing.
+// It allows simulating AWS API responses without making actual API calls.
+package fake
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"time"
+
+	awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+)
+
+// Client implements the aws secretsmanager interface.
+type Client struct {
+	ExecutionCounter       int
+	valFn                  map[string]func(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error)
+	CreateSecretFn         CreateSecretFn
+	GetSecretValueFn       GetSecretValueFn
+	PutSecretValueFn       PutSecretValueFn
+	DescribeSecretFn       DescribeSecretFn
+	DeleteSecretFn         DeleteSecretFn
+	ListSecretsFn          ListSecretsFn
+	BatchGetSecretValueFn  BatchGetSecretValueFn
+	TagResourceFn          TagResourceFn
+	UntagResourceFn        UntagResourceFn
+	PutResourcePolicyFn    PutResourcePolicyFn
+	GetResourcePolicyFn    GetResourcePolicyFn
+	DeleteResourcePolicyFn DeleteResourcePolicyFn
+}
+type CreateSecretFn func(context.Context, *awssm.CreateSecretInput, ...func(*awssm.Options)) (*awssm.CreateSecretOutput, error)
+type GetSecretValueFn func(context.Context, *awssm.GetSecretValueInput, ...func(*awssm.Options)) (*awssm.GetSecretValueOutput, error)
+type PutSecretValueFn func(context.Context, *awssm.PutSecretValueInput, ...func(*awssm.Options)) (*awssm.PutSecretValueOutput, error)
+type DescribeSecretFn func(context.Context, *awssm.DescribeSecretInput, ...func(*awssm.Options)) (*awssm.DescribeSecretOutput, error)
+type DeleteSecretFn func(context.Context, *awssm.DeleteSecretInput, ...func(*awssm.Options)) (*awssm.DeleteSecretOutput, error)
+type ListSecretsFn func(context.Context, *awssm.ListSecretsInput, ...func(*awssm.Options)) (*awssm.ListSecretsOutput, error)
+type BatchGetSecretValueFn func(context.Context, *awssm.BatchGetSecretValueInput, ...func(*awssm.Options)) (*awssm.BatchGetSecretValueOutput, error)
+
+type TagResourceFn func(context.Context, *awssm.TagResourceInput, ...func(*awssm.Options)) (*awssm.TagResourceOutput, error)
+type UntagResourceFn func(context.Context, *awssm.UntagResourceInput, ...func(*awssm.Options)) (*awssm.UntagResourceOutput, error)
+type PutResourcePolicyFn func(context.Context, *awssm.PutResourcePolicyInput, ...func(*awssm.Options)) (*awssm.PutResourcePolicyOutput, error)
+type GetResourcePolicyFn func(context.Context, *awssm.GetResourcePolicyInput, ...func(*awssm.Options)) (*awssm.GetResourcePolicyOutput, error)
+type DeleteResourcePolicyFn func(context.Context, *awssm.DeleteResourcePolicyInput, ...func(*awssm.Options)) (*awssm.DeleteResourcePolicyOutput, error)
+
+func (sm *Client) CreateSecret(ctx context.Context, input *awssm.CreateSecretInput, options ...func(*awssm.Options)) (*awssm.CreateSecretOutput, error) {
+	return sm.CreateSecretFn(ctx, input, options...)
+}
+
+func NewCreateSecretFn(output *awssm.CreateSecretOutput, err error, expectedSecretBinary ...[]byte) CreateSecretFn {
+	return func(ctx context.Context, actualInput *awssm.CreateSecretInput, options ...func(*awssm.Options)) (*awssm.CreateSecretOutput, error) {
+		if *actualInput.ClientRequestToken != "00000000-0000-0000-0000-000000000001" {
+			return nil, errors.New("expected the version to be 1 at creation")
+		}
+		if len(expectedSecretBinary) == 1 {
+			if bytes.Equal(actualInput.SecretBinary, expectedSecretBinary[0]) {
+				return output, err
+			}
+			return nil, fmt.Errorf("expected secret to be '%s' but was '%s'", string(expectedSecretBinary[0]), string(actualInput.SecretBinary))
+		}
+		return output, err
+	}
+}
+
+func (sm *Client) DeleteSecret(ctx context.Context, input *awssm.DeleteSecretInput, opts ...func(*awssm.Options)) (*awssm.DeleteSecretOutput, error) {
+	return sm.DeleteSecretFn(ctx, input, opts...)
+}
+
+// NewDeleteSecretFn returns a DeleteSecretFn that simulates AWS DeleteSecret API behavior.
+func NewDeleteSecretFn(output *awssm.DeleteSecretOutput, err error) DeleteSecretFn {
+	return func(_ context.Context, input *awssm.DeleteSecretInput, opts ...func(*awssm.Options)) (*awssm.DeleteSecretOutput, error) {
+		if input.ForceDeleteWithoutRecovery != nil && *input.ForceDeleteWithoutRecovery {
+			output.DeletionDate = new(time.Now())
+		}
+		return output, err
+	}
+}
+
+// NewGetSecretValueFn returns a GetSecretValueFn that returns the provided output and error.
+func NewGetSecretValueFn(output *awssm.GetSecretValueOutput, err error) GetSecretValueFn {
+	return func(_ context.Context, input *awssm.GetSecretValueInput, options ...func(*awssm.Options)) (*awssm.GetSecretValueOutput, error) {
+		return output, err
+	}
+}
+
+func (sm *Client) PutSecretValue(ctx context.Context, input *awssm.PutSecretValueInput, options ...func(*awssm.Options)) (*awssm.PutSecretValueOutput, error) {
+	return sm.PutSecretValueFn(ctx, input, options...)
+}
+
+type ExpectedPutSecretValueInput struct {
+	SecretBinary []byte
+	Version      *string
+}
+
+func (e ExpectedPutSecretValueInput) assertEquals(actualInput *awssm.PutSecretValueInput) error {
+	errSecretBinary := e.assertSecretBinary(actualInput)
+	if errSecretBinary != nil {
+		return errSecretBinary
+	}
+	errSecretVersion := e.assertVersion(actualInput)
+	if errSecretVersion != nil {
+		return errSecretVersion
+	}
+
+	return nil
+}
+
+func (e ExpectedPutSecretValueInput) assertSecretBinary(actualInput *awssm.PutSecretValueInput) error {
+	if e.SecretBinary != nil && !bytes.Equal(actualInput.SecretBinary, e.SecretBinary) {
+		return fmt.Errorf("expected secret to be '%s' but was '%s'", string(e.SecretBinary), string(actualInput.SecretBinary))
+	}
+	return nil
+}
+
+func (e ExpectedPutSecretValueInput) assertVersion(actualInput *awssm.PutSecretValueInput) error {
+	if e.Version != nil && (*actualInput.ClientRequestToken != *e.Version) {
+		return fmt.Errorf("expected version to be '%s', but was '%s'", *e.Version, *actualInput.ClientRequestToken)
+	}
+	return nil
+}
+
+func NewPutSecretValueFn(output *awssm.PutSecretValueOutput, err error, expectedInput ...ExpectedPutSecretValueInput) PutSecretValueFn {
+	return func(ctx context.Context, actualInput *awssm.PutSecretValueInput, actualOptions ...func(*awssm.Options)) (*awssm.PutSecretValueOutput, error) {
+		if len(expectedInput) == 1 {
+			assertErr := expectedInput[0].assertEquals(actualInput)
+			if assertErr != nil {
+				return nil, assertErr
+			}
+		}
+		return output, err
+	}
+}
+
+func (sm *Client) DescribeSecret(ctx context.Context, input *awssm.DescribeSecretInput, options ...func(*awssm.Options)) (*awssm.DescribeSecretOutput, error) {
+	return sm.DescribeSecretFn(ctx, input, options...)
+}
+
+func NewDescribeSecretFn(output *awssm.DescribeSecretOutput, err error) DescribeSecretFn {
+	return func(ctx context.Context, input *awssm.DescribeSecretInput, options ...func(*awssm.Options)) (*awssm.DescribeSecretOutput, error) {
+		return output, err
+	}
+}
+
+// NewClient init a new fake client.
+func NewClient() *Client {
+	return &Client{
+		valFn: make(map[string]func(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error)),
+	}
+}
+
+func (sm *Client) GetSecretValue(ctx context.Context, in *awssm.GetSecretValueInput, options ...func(*awssm.Options)) (*awssm.GetSecretValueOutput, error) {
+	// check if there's a direct fake function for this input
+	if sm.GetSecretValueFn != nil {
+		return sm.GetSecretValueFn(ctx, in, options...)
+	}
+	sm.ExecutionCounter++
+	if entry, found := sm.valFn[sm.cacheKeyForInput(in)]; found {
+		return entry(in)
+	}
+	return nil, errors.New("test case not found")
+}
+
+func (sm *Client) ListSecrets(ctx context.Context, input *awssm.ListSecretsInput, options ...func(*awssm.Options)) (*awssm.ListSecretsOutput, error) {
+	return sm.ListSecretsFn(ctx, input, options...)
+}
+
+func (sm *Client) BatchGetSecretValue(ctx context.Context, in *awssm.BatchGetSecretValueInput, options ...func(*awssm.Options)) (*awssm.BatchGetSecretValueOutput, error) {
+	return sm.BatchGetSecretValueFn(ctx, in, options...)
+}
+
+func (sm *Client) cacheKeyForInput(in *awssm.GetSecretValueInput) string {
+	var secretID, versionID string
+	if in.SecretId != nil {
+		secretID = *in.SecretId
+	}
+	if in.VersionId != nil {
+		versionID = *in.VersionId
+	}
+	return fmt.Sprintf("%s#%s", secretID, versionID)
+}
+
+func (sm *Client) WithValue(in *awssm.GetSecretValueInput, val *awssm.GetSecretValueOutput, err error) {
+	sm.valFn[sm.cacheKeyForInput(in)] = func(paramIn *awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error) {
+		if !cmp.Equal(paramIn, in, cmpopts.IgnoreUnexported(awssm.GetSecretValueInput{})) {
+			return nil, errors.New("unexpected test argument")
+		}
+		return val, err
+	}
+}
+
+func (sm *Client) TagResource(ctx context.Context, params *awssm.TagResourceInput, optFns ...func(*awssm.Options)) (*awssm.TagResourceOutput, error) {
+	return sm.TagResourceFn(ctx, params, optFns...)
+}
+
+func NewTagResourceFn(output *awssm.TagResourceOutput, err error, aFunc ...func(input *awssm.TagResourceInput)) TagResourceFn {
+	return func(ctx context.Context, params *awssm.TagResourceInput, optFns ...func(*awssm.Options)) (*awssm.TagResourceOutput, error) {
+		for _, f := range aFunc {
+			f(params)
+		}
+		return output, err
+	}
+}
+
+func (sm *Client) UntagResource(ctx context.Context, params *awssm.UntagResourceInput, optFuncs ...func(*awssm.Options)) (*awssm.UntagResourceOutput, error) {
+	return sm.UntagResourceFn(ctx, params, optFuncs...)
+}
+
+func NewUntagResourceFn(output *awssm.UntagResourceOutput, err error, aFunc ...func(input *awssm.UntagResourceInput)) UntagResourceFn {
+	return func(ctx context.Context, params *awssm.UntagResourceInput, optFuncs ...func(*awssm.Options)) (*awssm.UntagResourceOutput, error) {
+		for _, f := range aFunc {
+			f(params)
+		}
+		return output, err
+	}
+}
+
+func (sm *Client) PutResourcePolicy(ctx context.Context, params *awssm.PutResourcePolicyInput, optFns ...func(*awssm.Options)) (*awssm.PutResourcePolicyOutput, error) {
+	return sm.PutResourcePolicyFn(ctx, params, optFns...)
+}
+
+func NewPutResourcePolicyFn(output *awssm.PutResourcePolicyOutput, err error, aFunc ...func(input *awssm.PutResourcePolicyInput)) PutResourcePolicyFn {
+	return func(ctx context.Context, params *awssm.PutResourcePolicyInput, optFns ...func(*awssm.Options)) (*awssm.PutResourcePolicyOutput, error) {
+		for _, f := range aFunc {
+			f(params)
+		}
+		return output, err
+	}
+}
+
+func (sm *Client) GetResourcePolicy(ctx context.Context, params *awssm.GetResourcePolicyInput, optFns ...func(*awssm.Options)) (*awssm.GetResourcePolicyOutput, error) {
+	return sm.GetResourcePolicyFn(ctx, params, optFns...)
+}
+
+func NewGetResourcePolicyFn(output *awssm.GetResourcePolicyOutput, err error, aFunc ...func(input *awssm.GetResourcePolicyInput)) GetResourcePolicyFn {
+	return func(ctx context.Context, params *awssm.GetResourcePolicyInput, optFns ...func(*awssm.Options)) (*awssm.GetResourcePolicyOutput, error) {
+		for _, f := range aFunc {
+			f(params)
+		}
+		return output, err
+	}
+}
+
+func (sm *Client) DeleteResourcePolicy(ctx context.Context, params *awssm.DeleteResourcePolicyInput, optFns ...func(*awssm.Options)) (*awssm.DeleteResourcePolicyOutput, error) {
+	return sm.DeleteResourcePolicyFn(ctx, params, optFns...)
+}
+
+func NewDeleteResourcePolicyFn(output *awssm.DeleteResourcePolicyOutput, err error, aFunc ...func(input *awssm.DeleteResourcePolicyInput)) DeleteResourcePolicyFn {
+	return func(ctx context.Context, params *awssm.DeleteResourcePolicyInput, optFns ...func(*awssm.Options)) (*awssm.DeleteResourcePolicyOutput, error) {
+		for _, f := range aFunc {
+			f(params)
+		}
+		return output, err
+	}
+}

+ 52 - 0
providers/v2/aws/store/secretsmanager/resolver.go

@@ -0,0 +1,52 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package secretsmanager implements AWS Secrets Manager provider for External Secrets Operator
+package secretsmanager
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+	"os"
+
+	"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
+	smithyendpoints "github.com/aws/smithy-go/endpoints"
+)
+
+const (
+	// SecretsManagerEndpointEnv is the environment variable name for custom AWS Secrets Manager endpoint.
+	SecretsManagerEndpointEnv = "AWS_SECRETSMANAGER_ENDPOINT"
+)
+
+type customEndpointResolver struct{}
+
+// ResolveEndpoint returns a ResolverFunc with
+// customizable endpoints.
+
+func (c customEndpointResolver) ResolveEndpoint(ctx context.Context, params secretsmanager.EndpointParameters) (smithyendpoints.Endpoint, error) {
+	endpoint := smithyendpoints.Endpoint{}
+	if v := os.Getenv(SecretsManagerEndpointEnv); v != "" {
+		url, err := url.Parse(v)
+		if err != nil {
+			return endpoint, fmt.Errorf("failed to parse secretsmanager endpoint %s: %w", v, err)
+		}
+		endpoint.URI = *url
+		return endpoint, nil
+	}
+	defaultResolver := secretsmanager.NewDefaultEndpointResolverV2()
+	return defaultResolver.ResolveEndpoint(ctx, params)
+}

+ 964 - 0
providers/v2/aws/store/secretsmanager/secretsmanager.go

@@ -0,0 +1,964 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package secretsmanager
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"reflect"
+	"slices"
+	"strings"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
+	"github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
+	"github.com/aws/smithy-go"
+	"github.com/google/uuid"
+	"github.com/tidwall/gjson"
+	"github.com/tidwall/sjson"
+	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	awsutil "github.com/external-secrets/external-secrets/providers/v1/aws/util"
+	"github.com/external-secrets/external-secrets/runtime/constants"
+	"github.com/external-secrets/external-secrets/runtime/esutils"
+	"github.com/external-secrets/external-secrets/runtime/esutils/metadata"
+	"github.com/external-secrets/external-secrets/runtime/find"
+	"github.com/external-secrets/external-secrets/runtime/metrics"
+)
+
+// PushSecretMetadataSpec contains metadata information for pushing secrets to AWS Secret Manager.
+type PushSecretMetadataSpec struct {
+	Tags             map[string]string   `json:"tags,omitempty"`
+	Description      string              `json:"description,omitempty"`
+	SecretPushFormat string              `json:"secretPushFormat,omitempty"`
+	KMSKeyID         string              `json:"kmsKeyId,omitempty"`
+	ResourcePolicy   *ResourcePolicySpec `json:"resourcePolicy,omitempty"`
+}
+
+// ResourcePolicySpec defines the resource policy configuration using PolicySourceRef for AWS Secrets Manager.
+type ResourcePolicySpec struct {
+	BlockPublicPolicy *bool            `json:"blockPublicPolicy,omitempty"`
+	PolicySourceRef   *PolicySourceRef `json:"policySourceRef,omitempty"`
+}
+
+// PolicySourceRef defines the source reference for the resource policy.
+type PolicySourceRef struct {
+	Kind string `json:"kind"`
+	Name string `json:"name"`
+	Key  string `json:"key"`
+}
+
+// Declares metadata information for pushing secrets to AWS Secret Store.
+const (
+	SecretPushFormatKey       = "secretPushFormat"
+	SecretPushFormatString    = "string"
+	SecretPushFormatBinary    = "binary"
+	ResourceNotFoundException = "ResourceNotFoundException"
+)
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1.SecretsClient = &SecretsManager{}
+
+// SecretsManager is a provider for AWS SecretsManager.
+type SecretsManager struct {
+	cfg          *aws.Config
+	client       SMInterface // Keep the interface
+	referentAuth bool
+	cache        map[string]*awssm.GetSecretValueOutput
+	config       *esv1.SecretsManager
+	prefix       string
+	newUUID      func() string
+	kube         client.Client
+	namespace    string
+}
+
+// SMInterface is a subset of the smiface api.
+// see: https://docs.aws.amazon.com/sdk-for-go/api/service/secretsmanager/secretsmanageriface/
+type SMInterface interface {
+	BatchGetSecretValue(ctx context.Context, params *awssm.BatchGetSecretValueInput, optFuncs ...func(*awssm.Options)) (*awssm.BatchGetSecretValueOutput, error)
+	ListSecrets(ctx context.Context, params *awssm.ListSecretsInput, optFuncs ...func(*awssm.Options)) (*awssm.ListSecretsOutput, error)
+	GetSecretValue(ctx context.Context, params *awssm.GetSecretValueInput, optFuncs ...func(*awssm.Options)) (*awssm.GetSecretValueOutput, error)
+	CreateSecret(ctx context.Context, params *awssm.CreateSecretInput, optFuncs ...func(*awssm.Options)) (*awssm.CreateSecretOutput, error)
+	PutSecretValue(ctx context.Context, params *awssm.PutSecretValueInput, optFuncs ...func(*awssm.Options)) (*awssm.PutSecretValueOutput, error)
+	DescribeSecret(ctx context.Context, params *awssm.DescribeSecretInput, optFuncs ...func(*awssm.Options)) (*awssm.DescribeSecretOutput, error)
+	DeleteSecret(ctx context.Context, params *awssm.DeleteSecretInput, optFuncs ...func(*awssm.Options)) (*awssm.DeleteSecretOutput, error)
+	TagResource(ctx context.Context, params *awssm.TagResourceInput, optFuncs ...func(*awssm.Options)) (*awssm.TagResourceOutput, error)
+	UntagResource(ctx context.Context, params *awssm.UntagResourceInput, optFuncs ...func(*awssm.Options)) (*awssm.UntagResourceOutput, error)
+	PutResourcePolicy(ctx context.Context, params *awssm.PutResourcePolicyInput, optFuncs ...func(*awssm.Options)) (*awssm.PutResourcePolicyOutput, error)
+	GetResourcePolicy(ctx context.Context, params *awssm.GetResourcePolicyInput, optFuncs ...func(*awssm.Options)) (*awssm.GetResourcePolicyOutput, error)
+	DeleteResourcePolicy(ctx context.Context, params *awssm.DeleteResourcePolicyInput, optFuncs ...func(*awssm.Options)) (*awssm.DeleteResourcePolicyOutput, error)
+}
+
+const (
+	errUnexpectedFindOperator = "unexpected find operator"
+	managedBy                 = "managed-by"
+	externalSecrets           = "external-secrets"
+	initialVersion            = "00000000-0000-0000-0000-000000000001"
+)
+
+var log = ctrl.Log.WithName("provider").WithName("aws").WithName("secretsmanager")
+
+// New creates a new SecretsManager client.
+func New(_ context.Context, cfg *aws.Config, secretsManagerCfg *esv1.SecretsManager, prefix string, referentAuth bool, kube client.Client, namespace string) (*SecretsManager, error) {
+	return &SecretsManager{
+		cfg: cfg,
+		client: awssm.NewFromConfig(*cfg, func(o *awssm.Options) {
+			o.EndpointResolverV2 = customEndpointResolver{}
+		}),
+		referentAuth: referentAuth,
+		cache:        make(map[string]*awssm.GetSecretValueOutput),
+		config:       secretsManagerCfg,
+		prefix:       prefix,
+		kube:         kube,
+		namespace:    namespace,
+	}, nil
+}
+
+func (sm *SecretsManager) fetch(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (*awssm.GetSecretValueOutput, error) {
+	ver := "AWSCURRENT"
+	valueFrom := "SECRET"
+	if ref.Version != "" {
+		ver = ref.Version
+	}
+	if ref.MetadataPolicy == esv1.ExternalSecretMetadataPolicyFetch {
+		valueFrom = "TAG"
+	}
+
+	key := sm.prefix + ref.Key
+	log.Info("fetching secret value", "key", key, "version", ver, "value", valueFrom)
+
+	cacheKey := fmt.Sprintf("%s#%s#%s", key, ver, valueFrom)
+	if secretOut, found := sm.cache[cacheKey]; found {
+		log.Info("found secret in cache", "key", key, "version", ver)
+		return secretOut, nil
+	}
+
+	secretOut, err := sm.constructSecretValue(ctx, key, ver, ref.MetadataPolicy)
+	if err != nil {
+		return nil, err
+	}
+
+	sm.cache[cacheKey] = secretOut
+
+	return secretOut, nil
+}
+
+// DeleteSecret deletes a secret from AWS Secrets Manager.
+func (sm *SecretsManager) DeleteSecret(ctx context.Context, remoteRef esv1.PushSecretRemoteRef) error {
+	secretName := sm.prefix + remoteRef.GetRemoteKey()
+	secretValue := awssm.GetSecretValueInput{
+		SecretId: &secretName,
+	}
+	secretInput := awssm.DescribeSecretInput{
+		SecretId: &secretName,
+	}
+	awsSecret, err := sm.client.GetSecretValue(ctx, &secretValue)
+	metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMGetSecretValue, err)
+	var aerr smithy.APIError
+	if err != nil {
+		if ok := errors.As(err, &aerr); !ok {
+			return err
+		}
+		if aerr.ErrorCode() == ResourceNotFoundException {
+			return nil
+		}
+		return err
+	}
+	data, err := sm.client.DescribeSecret(ctx, &secretInput)
+	metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMDescribeSecret, err)
+	if err != nil {
+		return err
+	}
+	if !isManagedByESO(data) {
+		return nil
+	}
+	deleteInput := &awssm.DeleteSecretInput{
+		SecretId: awsSecret.ARN,
+	}
+	if sm.config != nil && sm.config.ForceDeleteWithoutRecovery {
+		deleteInput.ForceDeleteWithoutRecovery = &sm.config.ForceDeleteWithoutRecovery
+	}
+	if sm.config != nil && sm.config.RecoveryWindowInDays > 0 {
+		deleteInput.RecoveryWindowInDays = &sm.config.RecoveryWindowInDays
+	}
+	err = awsutil.ValidateDeleteSecretInput(*deleteInput)
+	if err != nil {
+		return err
+	}
+	_, err = sm.client.DeleteSecret(ctx, deleteInput)
+	metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMDeleteSecret, err)
+	return err
+}
+
+// SecretExists checks if a secret exists in AWS Secrets Manager.
+func (sm *SecretsManager) SecretExists(ctx context.Context, pushSecretRef esv1.PushSecretRemoteRef) (bool, error) {
+	secretName := sm.prefix + pushSecretRef.GetRemoteKey()
+	secretValue := awssm.GetSecretValueInput{
+		SecretId: &secretName,
+	}
+	_, err := sm.client.GetSecretValue(ctx, &secretValue)
+	if err != nil {
+		return sm.handleSecretError(err)
+	}
+	return true, nil
+}
+
+func (sm *SecretsManager) handleSecretError(err error) (bool, error) {
+	var aerr smithy.APIError
+	if ok := errors.As(err, &aerr); !ok {
+		return false, err
+	}
+	if aerr.ErrorCode() == ResourceNotFoundException {
+		return false, nil
+	}
+	return false, err
+}
+
+// PushSecret pushes a secret to AWS Secrets Manager.
+func (sm *SecretsManager) PushSecret(ctx context.Context, secret *corev1.Secret, psd esv1.PushSecretData) error {
+	value, err := esutils.ExtractSecretData(psd, secret)
+	if err != nil {
+		return fmt.Errorf("failed to extract secret data: %w", err)
+	}
+
+	secretName := sm.prefix + psd.GetRemoteKey()
+	describeSecretInput := awssm.DescribeSecretInput{SecretId: &secretName}
+	describeSecretOutput, err := sm.client.DescribeSecret(ctx, &describeSecretInput)
+	metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMDescribeSecret, err)
+	var aerr smithy.APIError
+	if err != nil {
+		if ok := errors.As(err, &aerr); !ok {
+			return err
+		}
+		if aerr.ErrorCode() == ResourceNotFoundException {
+			finalValue, err := sm.getNewSecretValue(value, psd.GetProperty(), nil)
+			if err != nil {
+				return err
+			}
+			return sm.createSecretWithContext(ctx, secretName, psd, finalValue)
+		}
+		return err
+	} else if !isManagedByESO(describeSecretOutput) {
+		return errors.New("secret not managed by external-secrets")
+	}
+
+	if len(describeSecretOutput.VersionIdsToStages) == 0 {
+		finalValue, err := sm.getNewSecretValue(value, psd.GetProperty(), nil)
+		if err != nil {
+			return err
+		}
+		return sm.putSecretValueWithContext(ctx, secretName, nil, psd, finalValue, describeSecretOutput.Tags)
+	}
+
+	getSecretValueInput := awssm.GetSecretValueInput{SecretId: &secretName}
+	getSecretValueOutput, err := sm.client.GetSecretValue(ctx, &getSecretValueInput)
+	metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMGetSecretValue, err)
+	if err != nil {
+		return err
+	}
+
+	finalValue, err := sm.getNewSecretValue(value, psd.GetProperty(), getSecretValueOutput)
+	if err != nil {
+		return err
+	}
+	return sm.putSecretValueWithContext(ctx, secretName, getSecretValueOutput, psd, finalValue, describeSecretOutput.Tags)
+}
+
+func (sm *SecretsManager) getNewSecretValue(value []byte, property string, existingSecret *awssm.GetSecretValueOutput) ([]byte, error) {
+	if property == "" {
+		return value, nil
+	}
+
+	if existingSecret == nil {
+		value, _ = sjson.SetBytes([]byte{}, property, value)
+		return value, nil
+	}
+
+	currentSecret := sm.retrievePayload(existingSecret)
+	if currentSecret != "" && !gjson.Valid(currentSecret) {
+		return nil, errors.New("PushSecret for aws secrets manager with a pushSecretData property requires a json secret")
+	}
+	value, _ = sjson.SetBytes([]byte(currentSecret), property, value)
+	return value, nil
+}
+
+func isManagedByESO(data *awssm.DescribeSecretOutput) bool {
+	managedBy := managedBy
+	externalSecrets := externalSecrets
+	for _, tag := range data.Tags {
+		if *tag.Key == managedBy && *tag.Value == externalSecrets {
+			return true
+		}
+	}
+	return false
+}
+
+// GetAllSecrets syncs multiple secrets from aws provider into a single Kubernetes Secret.
+func (sm *SecretsManager) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
+	hasName := ref.Name != nil
+	hasTags := len(ref.Tags) > 0
+
+	filters := make([]types.Filter, 0)
+	switch {
+	case !hasName && !hasTags:
+		return nil, errors.New(errUnexpectedFindOperator)
+	case hasName && !hasTags:
+		return sm.findByName(ctx, ref, filters)
+	case !hasName && hasTags:
+		return sm.findByTags(ctx, ref)
+	case hasName && hasTags:
+		return sm.findByNameAndTags(ctx, ref, filters)
+	default:
+		return nil, errors.New(errUnexpectedFindOperator)
+	}
+}
+
+func (sm *SecretsManager) findByNameAndTags(ctx context.Context, ref esv1.ExternalSecretFind, filters []types.Filter) (map[string][]byte, error) {
+	for k, v := range ref.Tags {
+		filters = append(filters, types.Filter{
+			Key: types.FilterNameStringTypeTagKey,
+			Values: []string{
+				k,
+			},
+		}, types.Filter{
+			Key: types.FilterNameStringTypeTagValue,
+			Values: []string{
+				v,
+			},
+		})
+	}
+	return sm.findByName(ctx, ref, filters)
+}
+
+func (sm *SecretsManager) findByName(ctx context.Context, ref esv1.ExternalSecretFind, filters []types.Filter) (map[string][]byte, error) {
+	matcher, err := find.New(*ref.Name)
+	if err != nil {
+		return nil, err
+	}
+
+	if ref.Path != nil {
+		filters = append(filters, types.Filter{
+			Key: types.FilterNameStringTypeName,
+			Values: []string{
+				*ref.Path,
+			},
+		})
+
+		return sm.fetchWithBatch(ctx, filters, matcher)
+	}
+
+	data := make(map[string][]byte)
+	var nextToken *string
+
+	for {
+		// I put this into the for loop on purpose.
+		log.V(0).Info("using ListSecret to fetch all secrets; this is a costly operations, please use batching by defining a _path_")
+		it, err := sm.client.ListSecrets(ctx, &awssm.ListSecretsInput{
+			Filters:   filters,
+			NextToken: nextToken,
+		})
+		metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMListSecrets, err)
+		if err != nil {
+			return nil, err
+		}
+		log.V(1).Info("aws sm findByName found", "secrets", len(it.SecretList))
+		for _, secret := range it.SecretList {
+			if !matcher.MatchName(*secret.Name) {
+				continue
+			}
+			log.V(1).Info("aws sm findByName matches", "name", *secret.Name)
+			if err := sm.fetchAndSet(ctx, data, *secret.Name); err != nil {
+				return nil, err
+			}
+		}
+		nextToken = it.NextToken
+		if nextToken == nil {
+			break
+		}
+	}
+	return data, nil
+}
+
+func (sm *SecretsManager) findByTags(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
+	filters := make([]types.Filter, 0)
+	for k, v := range ref.Tags {
+		filters = append(filters, types.Filter{
+			Key: types.FilterNameStringTypeTagKey,
+			Values: []string{
+				k,
+			},
+		}, types.Filter{
+			Key: types.FilterNameStringTypeTagValue,
+			Values: []string{
+				v,
+			},
+		})
+	}
+
+	if ref.Path != nil {
+		filters = append(filters, types.Filter{
+			Key: types.FilterNameStringTypeName,
+			Values: []string{
+				*ref.Path,
+			},
+		})
+	}
+
+	return sm.fetchWithBatch(ctx, filters, nil)
+}
+
+func (sm *SecretsManager) fetchAndSet(ctx context.Context, data map[string][]byte, name string) error {
+	sec, err := sm.fetch(ctx, esv1.ExternalSecretDataRemoteRef{
+		Key: name,
+	})
+	if err != nil {
+		return err
+	}
+	if sec.SecretString != nil {
+		data[name] = []byte(*sec.SecretString)
+	}
+	if sec.SecretBinary != nil {
+		data[name] = sec.SecretBinary
+	}
+	return nil
+}
+
+// GetSecret returns a single secret from the provider.
+func (sm *SecretsManager) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	secretOut, err := sm.fetch(ctx, ref)
+	if errors.Is(err, esv1.NoSecretErr) {
+		return nil, err
+	}
+	if err != nil {
+		return nil, awsutil.SanitizeErr(err)
+	}
+	if ref.Property == "" {
+		if secretOut.SecretString != nil {
+			return []byte(*secretOut.SecretString), nil
+		}
+		if secretOut.SecretBinary != nil {
+			return secretOut.SecretBinary, nil
+		}
+		return nil, fmt.Errorf("invalid secret received. no secret string nor binary for key: %s", ref.Key)
+	}
+	val := sm.mapSecretToGjson(secretOut, ref.Property)
+	if !val.Exists() {
+		return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
+	}
+	return []byte(val.String()), nil
+}
+
+func (sm *SecretsManager) mapSecretToGjson(secretOut *awssm.GetSecretValueOutput, property string) gjson.Result {
+	payload := sm.retrievePayload(secretOut)
+	refProperty := sm.escapeDotsIfRequired(property, payload)
+	val := gjson.Get(payload, refProperty)
+	return val
+}
+
+func (sm *SecretsManager) retrievePayload(secretOut *awssm.GetSecretValueOutput) string {
+	if secretOut == nil {
+		return ""
+	}
+
+	var payload string
+	if secretOut.SecretString != nil {
+		payload = *secretOut.SecretString
+	}
+	if secretOut.SecretBinary != nil {
+		payload = string(secretOut.SecretBinary)
+	}
+	return payload
+}
+
+func (sm *SecretsManager) escapeDotsIfRequired(currentRefProperty, payload string) string {
+	// We need to search if a given key with a . exists before using gjson operations.
+	refProperty := currentRefProperty
+	if strings.Contains(currentRefProperty, ".") {
+		refProperty = strings.ReplaceAll(currentRefProperty, ".", "\\.")
+		val := gjson.Get(payload, refProperty)
+		if !val.Exists() {
+			refProperty = currentRefProperty
+		}
+	}
+	return refProperty
+}
+
+// GetSecretMap returns multiple k/v pairs from the provider.
+func (sm *SecretsManager) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	log.Info("fetching secret map", "key", ref.Key)
+	data, err := sm.GetSecret(ctx, ref)
+	if err != nil {
+		return nil, err
+	}
+	kv := make(map[string]json.RawMessage)
+	err = json.Unmarshal(data, &kv)
+	if err != nil {
+		return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
+	}
+	secretData := make(map[string][]byte)
+	for k, v := range kv {
+		var strVal string
+		err = json.Unmarshal(v, &strVal)
+		if err == nil {
+			secretData[k] = []byte(strVal)
+		} else {
+			secretData[k] = v
+		}
+	}
+	return secretData, nil
+}
+
+// Close closes the provider client connection.
+func (sm *SecretsManager) Close(_ context.Context) error {
+	return nil
+}
+
+// Validate validates the provider configuration.
+func (sm *SecretsManager) Validate() (esv1.ValidationResult, error) {
+	// skip validation stack because it depends on the namespace
+	// of the ExternalSecret
+	if sm.referentAuth {
+		return esv1.ValidationResultUnknown, nil
+	}
+	_, err := sm.cfg.Credentials.Retrieve(context.Background())
+	if err != nil {
+		return esv1.ValidationResultError, awsutil.SanitizeErr(err)
+	}
+
+	return esv1.ValidationResultReady, nil
+}
+
+// Capabilities returns the provider's esv1.SecretStoreCapabilities.
+func (sm *SecretsManager) Capabilities() esv1.SecretStoreCapabilities {
+	return esv1.SecretStoreReadWrite
+}
+
+func (sm *SecretsManager) createSecretWithContext(ctx context.Context, secretName string, psd esv1.PushSecretData, value []byte) error {
+	mdata, err := sm.constructMetadataWithDefaults(psd.GetMetadata())
+	if err != nil {
+		return fmt.Errorf("failed to parse push secret metadata: %w", err)
+	}
+
+	tags := make([]types.Tag, 0)
+
+	for k, v := range mdata.Spec.Tags {
+		tags = append(tags, types.Tag{
+			Key:   new(k),
+			Value: new(v),
+		})
+	}
+
+	input := &awssm.CreateSecretInput{
+		Name:               &secretName,
+		SecretBinary:       value,
+		Tags:               tags,
+		Description:        new(mdata.Spec.Description),
+		ClientRequestToken: new(initialVersion),
+		KmsKeyId:           new(mdata.Spec.KMSKeyID),
+	}
+	if mdata.Spec.SecretPushFormat == SecretPushFormatString {
+		input.SecretBinary = nil
+		input.SecretString = aws.String(string(value))
+	}
+
+	createOutput, err := sm.client.CreateSecret(ctx, input)
+	metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMCreateSecret, err)
+	if err != nil {
+		return err
+	}
+
+	// Apply resource policy if specified
+	if mdata.Spec.ResourcePolicy != nil && mdata.Spec.ResourcePolicy.PolicySourceRef != nil {
+		policyJSON, err := sm.resolveResourcePolicy(ctx, mdata.Spec.ResourcePolicy.PolicySourceRef)
+		if err != nil {
+			return fmt.Errorf("failed to resolve resource policy: %w", err)
+		}
+
+		putPolicyInput := &awssm.PutResourcePolicyInput{
+			SecretId:       createOutput.ARN,
+			ResourcePolicy: aws.String(policyJSON),
+		}
+		if mdata.Spec.ResourcePolicy.BlockPublicPolicy != nil {
+			putPolicyInput.BlockPublicPolicy = mdata.Spec.ResourcePolicy.BlockPublicPolicy
+		}
+
+		_, err = sm.client.PutResourcePolicy(ctx, putPolicyInput)
+		metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMPutResourcePolicy, err)
+		if err != nil {
+			return fmt.Errorf("failed to put resource policy: %w", err)
+		}
+	}
+
+	return nil
+}
+
+func (sm *SecretsManager) putSecretValueWithContext(ctx context.Context, secretArn string, awsSecret *awssm.GetSecretValueOutput, psd esv1.PushSecretData, value []byte, tags []types.Tag) error {
+	if awsSecret != nil && (bytes.Equal(awsSecret.SecretBinary, value) || esutils.CompareStringAndByteSlices(awsSecret.SecretString, value)) {
+		return nil
+	}
+
+	newVersionNumber := initialVersion
+	if awsSecret != nil {
+		if sm.newUUID == nil {
+			newVersionNumber = uuid.NewString()
+		} else {
+			newVersionNumber = sm.newUUID()
+		}
+	}
+	input := &awssm.PutSecretValueInput{
+		SecretId:           &secretArn,
+		SecretBinary:       value,
+		ClientRequestToken: aws.String(newVersionNumber),
+	}
+	secretPushFormat, err := esutils.FetchValueFromMetadata(SecretPushFormatKey, psd.GetMetadata(), SecretPushFormatBinary)
+	if err != nil {
+		return fmt.Errorf("failed to parse metadata: %w", err)
+	}
+	if secretPushFormat == SecretPushFormatString {
+		input.SecretBinary = nil
+		input.SecretString = aws.String(string(value))
+	}
+
+	_, err = sm.client.PutSecretValue(ctx, input)
+	metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMPutSecretValue, err)
+	if err != nil {
+		return err
+	}
+
+	currentTags := make(map[string]string, len(tags))
+	for _, tag := range tags {
+		currentTags[*tag.Key] = *tag.Value
+	}
+	if err := sm.patchTags(ctx, psd.GetMetadata(), &secretArn, currentTags); err != nil {
+		return err
+	}
+
+	// Manage resource policy if specified in metadata
+	return sm.manageResourcePolicy(ctx, psd.GetMetadata(), &secretArn)
+}
+
+func (sm *SecretsManager) patchTags(ctx context.Context, metadata *apiextensionsv1.JSON, secretID *string, tags map[string]string) error {
+	meta, err := sm.constructMetadataWithDefaults(metadata)
+	if err != nil {
+		return err
+	}
+
+	tagKeysToRemove := awsutil.FindTagKeysToRemove(tags, meta.Spec.Tags)
+	if len(tagKeysToRemove) > 0 {
+		_, err = sm.client.UntagResource(ctx, &awssm.UntagResourceInput{
+			SecretId: secretID,
+			TagKeys:  tagKeysToRemove,
+		})
+		metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMUntagResource, err)
+		if err != nil {
+			return err
+		}
+	}
+
+	tagsToUpdate, isModified := computeTagsToUpdate(tags, meta.Spec.Tags)
+	if isModified {
+		_, err = sm.client.TagResource(ctx, &awssm.TagResourceInput{
+			SecretId: secretID,
+			Tags:     tagsToUpdate,
+		})
+		metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMTagResource, err)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (sm *SecretsManager) fetchWithBatch(ctx context.Context, filters []types.Filter, matcher *find.Matcher) (map[string][]byte, error) {
+	data := make(map[string][]byte)
+	var nextToken *string
+
+	for {
+		it, err := sm.client.BatchGetSecretValue(ctx, &awssm.BatchGetSecretValueInput{
+			Filters:   filters,
+			NextToken: nextToken,
+		})
+		metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMBatchGetSecretValue, err)
+		if err != nil {
+			return nil, err
+		}
+		log.V(1).Info("aws sm findByName found", "secrets", len(it.SecretValues))
+		for _, secret := range it.SecretValues {
+			if matcher != nil && !matcher.MatchName(*secret.Name) {
+				continue
+			}
+			log.V(1).Info("aws sm findByName matches", "name", *secret.Name)
+
+			sm.setSecretValues(&secret, data)
+		}
+		nextToken = it.NextToken
+		if nextToken == nil {
+			break
+		}
+	}
+
+	return data, nil
+}
+
+func (sm *SecretsManager) setSecretValues(secret *types.SecretValueEntry, data map[string][]byte) {
+	if secret.SecretString != nil {
+		data[*secret.Name] = []byte(*secret.SecretString)
+	}
+	if secret.SecretBinary != nil {
+		data[*secret.Name] = secret.SecretBinary
+	}
+}
+
+func (sm *SecretsManager) constructSecretValue(ctx context.Context, key, ver string, metadataPolicy esv1.ExternalSecretMetadataPolicy) (*awssm.GetSecretValueOutput, error) {
+	if metadataPolicy == esv1.ExternalSecretMetadataPolicyFetch {
+		describeSecretInput := &awssm.DescribeSecretInput{
+			SecretId: &key,
+		}
+
+		descOutput, err := sm.client.DescribeSecret(ctx, describeSecretInput)
+		if err != nil {
+			return nil, err
+		}
+		log.Info("found metadata secret", "key", key, "output", descOutput)
+
+		jsonTags, err := awsutil.SecretTagsToJSONString(descOutput.Tags)
+		if err != nil {
+			return nil, err
+		}
+		return &awssm.GetSecretValueOutput{
+			ARN:          descOutput.ARN,
+			CreatedDate:  descOutput.CreatedDate,
+			Name:         descOutput.Name,
+			SecretString: &jsonTags,
+			VersionId:    &ver,
+		}, nil
+	}
+
+	var getSecretValueInput *awssm.GetSecretValueInput
+	if versionID, ok := strings.CutPrefix(ver, "uuid/"); ok {
+		getSecretValueInput = &awssm.GetSecretValueInput{
+			SecretId:  &key,
+			VersionId: &versionID,
+		}
+	} else {
+		getSecretValueInput = &awssm.GetSecretValueInput{
+			SecretId:     &key,
+			VersionStage: &ver,
+		}
+	}
+	secretOut, err := sm.client.GetSecretValue(ctx, getSecretValueInput)
+	metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMGetSecretValue, err)
+	var (
+		nf *types.ResourceNotFoundException
+		ie *types.InvalidParameterException
+	)
+	if errors.As(err, &nf) {
+		return nil, esv1.NoSecretErr
+	}
+
+	if errors.As(err, &ie) && strings.Contains(ie.Error(), "was marked for deletion") {
+		return nil, esv1.NoSecretErr
+	}
+
+	return secretOut, err
+}
+
+func (sm *SecretsManager) constructMetadataWithDefaults(data *apiextensionsv1.JSON) (*metadata.PushSecretMetadata[PushSecretMetadataSpec], error) {
+	var (
+		meta *metadata.PushSecretMetadata[PushSecretMetadataSpec]
+		err  error
+	)
+
+	meta, err = metadata.ParseMetadataParameters[PushSecretMetadataSpec](data)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse metadata: %w", err)
+	}
+
+	if meta == nil {
+		meta = &metadata.PushSecretMetadata[PushSecretMetadataSpec]{}
+	}
+
+	if meta.Spec.SecretPushFormat == "" {
+		meta.Spec.SecretPushFormat = SecretPushFormatBinary
+	} else if !slices.Contains([]string{SecretPushFormatBinary, SecretPushFormatString}, meta.Spec.SecretPushFormat) {
+		return nil, fmt.Errorf("invalid secret push format: %s", meta.Spec.SecretPushFormat)
+	}
+
+	if meta.Spec.Description == "" {
+		meta.Spec.Description = fmt.Sprintf("secret '%s:%s'", managedBy, externalSecrets)
+	}
+
+	if meta.Spec.KMSKeyID == "" {
+		meta.Spec.KMSKeyID = "alias/aws/secretsmanager"
+	}
+
+	if len(meta.Spec.Tags) > 0 {
+		if _, exists := meta.Spec.Tags[managedBy]; exists {
+			return nil, fmt.Errorf("error parsing tags in metadata: Cannot specify a '%s' tag", managedBy)
+		}
+	} else {
+		meta.Spec.Tags = make(map[string]string)
+	}
+	meta.Spec.Tags[managedBy] = externalSecrets
+
+	return meta, nil
+}
+
+// resolveResourcePolicy resolves the policy JSON from the PolicySourceRef.
+func (sm *SecretsManager) resolveResourcePolicy(ctx context.Context, policyRef *PolicySourceRef) (string, error) {
+	if policyRef == nil {
+		return "", errors.New("policySourceRef is nil")
+	}
+
+	switch policyRef.Kind {
+	case "ConfigMap":
+		cm := &corev1.ConfigMap{}
+		if err := sm.kube.Get(ctx, client.ObjectKey{
+			Namespace: sm.namespace,
+			Name:      policyRef.Name,
+		}, cm); err != nil {
+			return "", fmt.Errorf("failed to get ConfigMap %s/%s: %w", sm.namespace, policyRef.Name, err)
+		}
+		policy, ok := cm.Data[policyRef.Key]
+		if !ok {
+			return "", fmt.Errorf("key %s not found in ConfigMap %s/%s", policyRef.Key, sm.namespace, policyRef.Name)
+		}
+		return policy, nil
+
+	case "Secret":
+		secret := &corev1.Secret{}
+		if err := sm.kube.Get(ctx, client.ObjectKey{
+			Namespace: sm.namespace,
+			Name:      policyRef.Name,
+		}, secret); err != nil {
+			return "", fmt.Errorf("failed to get Secret %s/%s: %w", sm.namespace, policyRef.Name, err)
+		}
+		policyBytes, ok := secret.Data[policyRef.Key]
+		if !ok {
+			return "", fmt.Errorf("key %s not found in Secret %s/%s", policyRef.Key, sm.namespace, policyRef.Name)
+		}
+		return string(policyBytes), nil
+
+	default:
+		return "", fmt.Errorf("unsupported PolicySourceRef kind: %s (must be ConfigMap or Secret)", policyRef.Kind)
+	}
+}
+
+// manageResourcePolicy applies or removes the resource policy based on metadata.
+func (sm *SecretsManager) manageResourcePolicy(ctx context.Context, metadata *apiextensionsv1.JSON, secretID *string) error {
+	meta, err := sm.constructMetadataWithDefaults(metadata)
+	if err != nil {
+		return err
+	}
+
+	// Delete policy if policyRef is nil and the policy exists.
+	if meta.Spec.ResourcePolicy == nil {
+		deletePolicyInput := &awssm.DeleteResourcePolicyInput{
+			SecretId: secretID,
+		}
+		_, err = sm.client.DeleteResourcePolicy(ctx, deletePolicyInput)
+		metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMDeleteResourcePolicy, err)
+
+		var nf *types.ResourceNotFoundException
+		if err != nil && !errors.As(err, &nf) {
+			return fmt.Errorf("failed to delete resource policy: %w", err)
+		}
+
+		return nil
+	}
+
+	// Normal flow, is to create the policy.
+	policyJSON, err := sm.resolveResourcePolicy(ctx, meta.Spec.ResourcePolicy.PolicySourceRef)
+	if err != nil {
+		return fmt.Errorf("failed to resolve resource policy: %w", err)
+	}
+
+	getCurrentPolicyInput := &awssm.GetResourcePolicyInput{
+		SecretId: secretID,
+	}
+	currentPolicyOutput, err := sm.client.GetResourcePolicy(ctx, getCurrentPolicyInput)
+	metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMGetResourcePolicy, err)
+
+	var nf *types.ResourceNotFoundException
+	if err != nil && !errors.As(err, &nf) {
+		return fmt.Errorf("failed to get current resource policy: %w", err)
+	}
+
+	currentPolicy := ""
+	if currentPolicyOutput != nil && currentPolicyOutput.ResourcePolicy != nil {
+		currentPolicy = *currentPolicyOutput.ResourcePolicy
+	}
+
+	// convert to maps so we can do a stable comparison.
+	var (
+		currentPolicyMap map[string]any
+		policyJSONMaps   map[string]any
+	)
+
+	if err := json.Unmarshal([]byte(currentPolicy), &currentPolicyMap); err != nil {
+		return fmt.Errorf("failed to unmarshal current resource policy: %w", err)
+	}
+	if err := json.Unmarshal([]byte(policyJSON), &policyJSONMaps); err != nil {
+		return fmt.Errorf("failed to unmarshal current resource policy: %w", err)
+	}
+
+	if reflect.DeepEqual(currentPolicyMap, policyJSONMaps) {
+		return nil
+	}
+
+	putPolicyInput := &awssm.PutResourcePolicyInput{
+		SecretId:       secretID,
+		ResourcePolicy: aws.String(policyJSON),
+	}
+	if meta.Spec.ResourcePolicy.BlockPublicPolicy != nil {
+		putPolicyInput.BlockPublicPolicy = meta.Spec.ResourcePolicy.BlockPublicPolicy
+	}
+
+	_, err = sm.client.PutResourcePolicy(ctx, putPolicyInput)
+	metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMPutResourcePolicy, err)
+	if err != nil {
+		return fmt.Errorf("failed to put resource policy: %w", err)
+	}
+
+	return nil
+}
+
+// computeTagsToUpdate compares the current tags with the desired metaTags and returns a slice of ssmTypes.Tag
+// that should be set on the resource. It also returns a boolean indicating if any tag was added or modified.
+func computeTagsToUpdate(tags, metaTags map[string]string) ([]types.Tag, bool) {
+	result := make([]types.Tag, 0, len(metaTags))
+	modified := false
+	for k, v := range metaTags {
+		if _, exists := tags[k]; !exists || tags[k] != v {
+			if k != managedBy {
+				modified = true
+			}
+		}
+		result = append(result, types.Tag{
+			Key:   new(k),
+			Value: new(v),
+		})
+	}
+	return result, modified
+}

+ 2308 - 0
providers/v2/aws/store/secretsmanager/secretsmanager_test.go

@@ -0,0 +1,2308 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package secretsmanager
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/credentials"
+	awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
+	"github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
+	"github.com/google/go-cmp/cmp"
+	"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"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	fakesm "github.com/external-secrets/external-secrets/providers/v1/aws/secretsmanager/fake"
+	awsutil "github.com/external-secrets/external-secrets/providers/v1/aws/util"
+	"github.com/external-secrets/external-secrets/runtime/esutils/metadata"
+	"github.com/external-secrets/external-secrets/runtime/testing/fake"
+)
+
+type secretsManagerTestCase struct {
+	fakeClient     *fakesm.Client
+	apiInput       *awssm.GetSecretValueInput
+	apiOutput      *awssm.GetSecretValueOutput
+	remoteRef      *esv1.ExternalSecretDataRemoteRef
+	apiErr         error
+	expectError    string
+	expectedSecret string
+	// for testing secretmap
+	expectedData map[string][]byte
+	// for testing caching
+	expectedCounter *int
+	prefix          string
+}
+
+const unexpectedErrorString = "[%d] unexpected error: %s, expected: '%s'"
+const (
+	tagname1  = "tagname1"
+	tagvalue1 = "tagvalue1"
+	tagname2  = "tagname2"
+	tagvalue2 = "tagvalue2"
+	fakeKey   = "fake-key"
+)
+
+func makeValidSecretsManagerTestCase() *secretsManagerTestCase {
+	smtc := secretsManagerTestCase{
+		fakeClient:     fakesm.NewClient(),
+		apiInput:       makeValidAPIInput(),
+		remoteRef:      makeValidRemoteRef(),
+		apiOutput:      makeValidAPIOutput(),
+		apiErr:         nil,
+		expectError:    "",
+		expectedSecret: "",
+		expectedData:   map[string][]byte{},
+	}
+	smtc.fakeClient.WithValue(smtc.apiInput, smtc.apiOutput, smtc.apiErr)
+	return &smtc
+}
+
+func makeValidRemoteRef() *esv1.ExternalSecretDataRemoteRef {
+	return &esv1.ExternalSecretDataRemoteRef{
+		Key:     "/baz",
+		Version: "AWSCURRENT",
+	}
+}
+
+func makeValidAPIInput() *awssm.GetSecretValueInput {
+	return &awssm.GetSecretValueInput{
+		SecretId:     aws.String("/baz"),
+		VersionStage: aws.String("AWSCURRENT"),
+	}
+}
+
+func makeValidAPIOutput() *awssm.GetSecretValueOutput {
+	return &awssm.GetSecretValueOutput{
+		SecretString: aws.String(""),
+	}
+}
+
+func makeValidGetResourcePolicyOutput() *awssm.GetResourcePolicyOutput {
+	return &awssm.GetResourcePolicyOutput{
+		ResourcePolicy: aws.String(`{
+			"Version": "2012-10-17",
+			"Statement": [
+				{
+					"Sid": "DenyPolicyChangesExceptAdmins",
+					"Effect": "Deny",
+					"Principal": "*",
+					"Action": [
+						"secretsmanager:PutResourcePolicy",
+						"secretsmanager:DeleteResourcePolicy",
+						"secretsmanager:GetResourcePolicy"
+					],
+					"Resource": "*",
+					"Condition": {
+						"ArnNotEquals": {
+							"aws:PrincipalArn": [
+								"arn:aws:iam::000000000000:root",
+								"arn:aws:iam::000000000000:role/admin"
+							]
+						}
+					}
+				}
+			]
+		}`),
+	}
+}
+
+func makeValidSecretsManagerTestCaseCustom(tweaks ...func(smtc *secretsManagerTestCase)) *secretsManagerTestCase {
+	smtc := makeValidSecretsManagerTestCase()
+	for _, fn := range tweaks {
+		fn(smtc)
+	}
+	smtc.fakeClient.WithValue(smtc.apiInput, smtc.apiOutput, smtc.apiErr)
+	return smtc
+}
+
+// This case can be shared by both GetSecret and GetSecretMap tests.
+// bad case: set apiErr.
+var setAPIErr = func(smtc *secretsManagerTestCase) {
+	smtc.apiErr = errors.New("oh no")
+	smtc.expectError = "oh no"
+}
+
+func TestSecretsManagerResolver(t *testing.T) {
+	endpointEnvKey := SecretsManagerEndpointEnv
+	endpointURL := "http://sm.foo"
+
+	t.Setenv(endpointEnvKey, endpointURL)
+
+	f, err := customEndpointResolver{}.ResolveEndpoint(context.Background(), awssm.EndpointParameters{})
+
+	assert.Nil(t, err)
+	assert.Equal(t, endpointURL, f.URI.String())
+}
+
+// test the sm<->aws interface
+// make sure correct values are passed and errors are handled accordingly.
+func TestSecretsManagerGetSecret(t *testing.T) {
+	// good case: default version is set
+	// key is passed in, output is sent back
+	setSecretString := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = aws.String("testtesttest")
+		smtc.expectedSecret = "testtesttest"
+	}
+
+	// good case: key is passed in with prefix
+	setSecretStringWithPrefix := func(smtc *secretsManagerTestCase) {
+		smtc.remoteRef.Key = "secret-key"
+		smtc.apiInput = &awssm.GetSecretValueInput{
+			SecretId:     aws.String("my-prefix/secret-key"),
+			VersionStage: aws.String("AWSCURRENT"),
+		}
+		smtc.prefix = "my-prefix/"
+	}
+
+	// good case: extract property
+	// Testing that the property exists in the SecretString
+	setRemoteRefPropertyExistsInKey := func(smtc *secretsManagerTestCase) {
+		smtc.remoteRef.Property = "/shmoo"
+		smtc.apiOutput.SecretString = aws.String(`{"/shmoo": "bang"}`)
+		smtc.expectedSecret = "bang"
+	}
+
+	// bad case: missing property
+	setRemoteRefMissingProperty := func(smtc *secretsManagerTestCase) {
+		smtc.remoteRef.Property = "INVALPROP"
+		smtc.expectError = "key INVALPROP does not exist in secret"
+	}
+
+	// bad case: extract property failure due to invalid json
+	setRemoteRefMissingPropertyInvalidJSON := func(smtc *secretsManagerTestCase) {
+		smtc.remoteRef.Property = "INVALPROP"
+		smtc.apiOutput.SecretString = aws.String(`------`)
+		smtc.expectError = "key INVALPROP does not exist in secret"
+	}
+
+	// good case: set .SecretString to nil but set binary with value
+	setSecretBinaryNotSecretString := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretBinary = []byte("yesplease")
+		// needs to be set as nil, empty quotes ("") is considered existing
+		smtc.apiOutput.SecretString = nil
+		smtc.expectedSecret = "yesplease"
+	}
+
+	// bad case: both .SecretString and .SecretBinary are nil
+	setSecretBinaryAndSecretStringToNil := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretBinary = nil
+		smtc.apiOutput.SecretString = nil
+		smtc.expectError = "no secret string nor binary for key"
+	}
+	// good case: secretOut.SecretBinary JSON parsing
+	setNestedSecretValueJSONParsing := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = nil
+		smtc.apiOutput.SecretBinary = []byte(`{"foobar":{"baz":"nestedval"}}`)
+		smtc.remoteRef.Property = "foobar.baz"
+		smtc.expectedSecret = "nestedval"
+	}
+	// good case: secretOut.SecretBinary no JSON parsing if name on key
+	setSecretValueWithDot := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = nil
+		smtc.apiOutput.SecretBinary = []byte(`{"foobar.baz":"nestedval"}`)
+		smtc.remoteRef.Property = "foobar.baz"
+		smtc.expectedSecret = "nestedval"
+	}
+
+	// good case: custom version stage set
+	setCustomVersionStage := func(smtc *secretsManagerTestCase) {
+		smtc.apiInput.VersionStage = aws.String("1234")
+		smtc.remoteRef.Version = "1234"
+		smtc.apiOutput.SecretString = aws.String("FOOBA!")
+		smtc.expectedSecret = "FOOBA!"
+	}
+
+	// good case: custom version id set
+	setCustomVersionID := func(smtc *secretsManagerTestCase) {
+		smtc.apiInput.VersionStage = nil
+		smtc.apiInput.VersionId = aws.String("1234-5678")
+		smtc.remoteRef.Version = "uuid/1234-5678"
+		smtc.apiOutput.SecretString = aws.String("myvalue")
+		smtc.expectedSecret = "myvalue"
+	}
+
+	fetchMetadata := func(smtc *secretsManagerTestCase) {
+		smtc.remoteRef.MetadataPolicy = esv1.ExternalSecretMetadataPolicyFetch
+		describeSecretOutput := &awssm.DescribeSecretOutput{
+			Tags: getTagSlice(),
+		}
+		smtc.fakeClient.DescribeSecretFn = fakesm.NewDescribeSecretFn(describeSecretOutput, nil)
+		jsonTags, _ := awsutil.SecretTagsToJSONString(getTagSlice())
+		smtc.apiOutput.SecretString = &jsonTags
+		smtc.expectedSecret = jsonTags
+	}
+
+	fetchMetadataProperty := func(smtc *secretsManagerTestCase) {
+		smtc.remoteRef.MetadataPolicy = esv1.ExternalSecretMetadataPolicyFetch
+		describeSecretOutput := &awssm.DescribeSecretOutput{
+			Tags: getTagSlice(),
+		}
+		smtc.fakeClient.DescribeSecretFn = fakesm.NewDescribeSecretFn(describeSecretOutput, nil)
+		smtc.remoteRef.Property = tagname2
+		jsonTags, _ := awsutil.SecretTagsToJSONString(getTagSlice())
+		smtc.apiOutput.SecretString = &jsonTags
+		smtc.expectedSecret = tagvalue2
+	}
+
+	failMetadataWrongProperty := func(smtc *secretsManagerTestCase) {
+		smtc.remoteRef.MetadataPolicy = esv1.ExternalSecretMetadataPolicyFetch
+		describeSecretOutput := &awssm.DescribeSecretOutput{
+			Tags: getTagSlice(),
+		}
+		smtc.fakeClient.DescribeSecretFn = fakesm.NewDescribeSecretFn(describeSecretOutput, nil)
+		smtc.remoteRef.Property = "fail"
+		jsonTags, _ := awsutil.SecretTagsToJSONString(getTagSlice())
+		smtc.apiOutput.SecretString = &jsonTags
+		smtc.expectError = "key fail does not exist in secret /baz"
+	}
+
+	successCases := []*secretsManagerTestCase{
+		makeValidSecretsManagerTestCase(),
+		makeValidSecretsManagerTestCaseCustom(setSecretString),
+		makeValidSecretsManagerTestCaseCustom(setSecretStringWithPrefix),
+		makeValidSecretsManagerTestCaseCustom(setRemoteRefPropertyExistsInKey),
+		makeValidSecretsManagerTestCaseCustom(setRemoteRefMissingProperty),
+		makeValidSecretsManagerTestCaseCustom(setRemoteRefMissingPropertyInvalidJSON),
+		makeValidSecretsManagerTestCaseCustom(setSecretBinaryNotSecretString),
+		makeValidSecretsManagerTestCaseCustom(setSecretBinaryAndSecretStringToNil),
+		makeValidSecretsManagerTestCaseCustom(setNestedSecretValueJSONParsing),
+		makeValidSecretsManagerTestCaseCustom(setSecretValueWithDot),
+		makeValidSecretsManagerTestCaseCustom(setCustomVersionStage),
+		makeValidSecretsManagerTestCaseCustom(setCustomVersionID),
+		makeValidSecretsManagerTestCaseCustom(setAPIErr),
+		makeValidSecretsManagerTestCaseCustom(fetchMetadata),
+		makeValidSecretsManagerTestCaseCustom(fetchMetadataProperty),
+		makeValidSecretsManagerTestCaseCustom(failMetadataWrongProperty),
+	}
+
+	for k, v := range successCases {
+		sm := SecretsManager{
+			cache:  make(map[string]*awssm.GetSecretValueOutput),
+			client: v.fakeClient,
+			prefix: v.prefix,
+		}
+		out, err := sm.GetSecret(context.Background(), *v.remoteRef)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf(unexpectedErrorString, k, err.Error(), v.expectError)
+		}
+		if err == nil && string(out) != v.expectedSecret {
+			t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out))
+		}
+	}
+}
+func TestCaching(t *testing.T) {
+	fakeClient := fakesm.NewClient()
+
+	// good case: first call, since we are using the same key, results should be cached and the counter should not go
+	// over 1
+	firstCall := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = aws.String(`{"foo":"bar", "bar":"vodka"}`)
+		smtc.remoteRef.Property = "foo"
+		smtc.expectedSecret = "bar"
+		smtc.expectedCounter = aws.Int(1)
+		smtc.fakeClient = fakeClient
+	}
+	secondCall := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = aws.String(`{"foo":"bar", "bar":"vodka"}`)
+		smtc.remoteRef.Property = "bar"
+		smtc.expectedSecret = "vodka"
+		smtc.expectedCounter = aws.Int(1)
+		smtc.fakeClient = fakeClient
+	}
+	notCachedCall := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = aws.String(`{"sheldon":"bazinga", "bar":"foo"}`)
+		smtc.remoteRef.Property = "sheldon"
+		smtc.expectedSecret = "bazinga"
+		smtc.expectedCounter = aws.Int(2)
+		smtc.fakeClient = fakeClient
+		smtc.apiInput.SecretId = aws.String("xyz")
+		smtc.remoteRef.Key = "xyz" // it should reset the cache since the key is different
+	}
+
+	cachedCases := []*secretsManagerTestCase{
+		makeValidSecretsManagerTestCaseCustom(firstCall),
+		makeValidSecretsManagerTestCaseCustom(firstCall),
+		makeValidSecretsManagerTestCaseCustom(secondCall),
+		makeValidSecretsManagerTestCaseCustom(notCachedCall),
+	}
+	sm := SecretsManager{
+		cache: make(map[string]*awssm.GetSecretValueOutput),
+	}
+	for k, v := range cachedCases {
+		sm.client = v.fakeClient
+		out, err := sm.GetSecret(context.Background(), *v.remoteRef)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf(unexpectedErrorString, k, err.Error(), v.expectError)
+		}
+		if err == nil && string(out) != v.expectedSecret {
+			t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out))
+		}
+		if v.expectedCounter != nil && v.fakeClient.ExecutionCounter != *v.expectedCounter {
+			t.Errorf("[%d] unexpected counter value: expected %d, got %d", k, v.expectedCounter, v.fakeClient.ExecutionCounter)
+		}
+	}
+}
+
+func TestGetSecretMap(t *testing.T) {
+	// good case: default version & deserialization
+	setDeserialization := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = aws.String(`{"foo":"bar"}`)
+		smtc.expectedData["foo"] = []byte("bar")
+	}
+
+	// good case: nested json
+	setNestedJSON := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = aws.String(`{"foobar":{"baz":"nestedval"}}`)
+		smtc.expectedData["foobar"] = []byte("{\"baz\":\"nestedval\"}")
+	}
+
+	// good case: caching
+	cachedMap := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = aws.String(`{"foo":"bar", "plus": "one"}`)
+		smtc.expectedData["foo"] = []byte("bar")
+		smtc.expectedData["plus"] = []byte("one")
+		smtc.expectedCounter = aws.Int(1)
+	}
+
+	// bad case: invalid json
+	setInvalidJSON := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = aws.String(`-----------------`)
+		smtc.expectError = "unable to unmarshal secret"
+	}
+
+	successCases := []*secretsManagerTestCase{
+		makeValidSecretsManagerTestCaseCustom(setDeserialization),
+		makeValidSecretsManagerTestCaseCustom(setNestedJSON),
+		makeValidSecretsManagerTestCaseCustom(setAPIErr),
+		makeValidSecretsManagerTestCaseCustom(setInvalidJSON),
+		makeValidSecretsManagerTestCaseCustom(cachedMap),
+	}
+
+	for k, v := range successCases {
+		sm := SecretsManager{
+			cache:  make(map[string]*awssm.GetSecretValueOutput),
+			client: v.fakeClient,
+		}
+		out, err := sm.GetSecretMap(context.Background(), *v.remoteRef)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf(unexpectedErrorString, k, err.Error(), v.expectError)
+		}
+		if err == nil && !cmp.Equal(out, v.expectedData) {
+			t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out)
+		}
+		if v.expectedCounter != nil && v.fakeClient.ExecutionCounter != *v.expectedCounter {
+			t.Errorf("[%d] unexpected counter value: expected %d, got %d", k, v.expectedCounter, v.fakeClient.ExecutionCounter)
+		}
+	}
+}
+
+func ErrorContains(out error, want string) bool {
+	if out == nil {
+		return want == ""
+	}
+	if want == "" {
+		return false
+	}
+	return strings.Contains(out.Error(), want)
+}
+
+func TestSetSecret(t *testing.T) {
+	managedBy := managedBy
+	notManagedBy := "not-managed-by"
+	secretKey := "fake-secret-key"
+	secretValue := []byte("fake-value")
+	fakeSecret := &corev1.Secret{
+		Data: map[string][]byte{
+			secretKey: secretValue,
+		},
+	}
+	externalSecrets := externalSecrets
+	noPermission := errors.New("no permission")
+	arn := "arn:aws:secretsmanager:us-east-1:702902267788:secret:foo-bar5-Robbgh"
+
+	getSecretCorrectErr := types.ResourceNotFoundException{}
+	getSecretWrongErr := types.InvalidRequestException{}
+
+	secretOutput := &awssm.CreateSecretOutput{
+		ARN: &arn,
+	}
+
+	externalSecretsTag := []types.Tag{
+		{
+			Key:   &managedBy,
+			Value: &externalSecrets,
+		},
+		{
+			Key:   new("taname1"),
+			Value: new("tagvalue1"),
+		},
+	}
+
+	externalSecretsTagFaulty := []types.Tag{
+		{
+			Key:   &notManagedBy,
+			Value: &externalSecrets,
+		},
+	}
+
+	tagSecretOutputNoVersions := &awssm.DescribeSecretOutput{
+		ARN:  &arn,
+		Tags: externalSecretsTag,
+	}
+
+	defaultVersion := "00000000-0000-0000-0000-000000000002"
+
+	tagSecretOutput := &awssm.DescribeSecretOutput{
+		ARN:  &arn,
+		Tags: externalSecretsTag,
+		VersionIdsToStages: map[string][]string{
+			defaultVersion: {"AWSCURRENT"},
+		},
+	}
+
+	tagSecretOutputFaulty := &awssm.DescribeSecretOutput{
+		ARN:  &arn,
+		Tags: externalSecretsTagFaulty,
+	}
+
+	tagSecretOutputFrom := func(versionId string) *awssm.DescribeSecretOutput {
+		return &awssm.DescribeSecretOutput{
+			ARN:  &arn,
+			Tags: externalSecretsTag,
+			VersionIdsToStages: map[string][]string{
+				versionId: {"AWSCURRENT"},
+			},
+		}
+	}
+
+	initialVersion := "00000000-0000-0000-0000-000000000001"
+	defaultUpdatedVersion := "6c70d57a-f53d-bf4d-9525-3503dd5abe8c"
+	randomUUIDVersion := "9d6202c2-c216-433e-a2f0-5836c4f025af"
+	randomUUIDVersionIncremented := "4346824b-7da1-4d82-addf-dee197fd5d71"
+	unparsableVersion := "IAM UNPARSABLE"
+
+	secretValueOutput := &awssm.GetSecretValueOutput{
+		ARN:       &arn,
+		VersionId: &defaultVersion,
+	}
+
+	secretValueOutput2 := &awssm.GetSecretValueOutput{
+		ARN:          &arn,
+		SecretBinary: secretValue,
+		VersionId:    &defaultVersion,
+	}
+	blankDescribeSecretOutput := &awssm.DescribeSecretOutput{}
+
+	type params struct {
+		s       string
+		b       []byte
+		version *string
+	}
+	secretValueOutputFrom := func(params params) *awssm.GetSecretValueOutput {
+		var version *string
+		if params.version == nil {
+			version = &defaultVersion
+		} else {
+			version = params.version
+		}
+
+		return &awssm.GetSecretValueOutput{
+			ARN:          &arn,
+			SecretString: &params.s,
+			SecretBinary: params.b,
+			VersionId:    version,
+		}
+	}
+
+	putSecretOutput := &awssm.PutSecretValueOutput{
+		ARN: &arn,
+	}
+
+	pushSecretDataWithoutProperty := fake.PushSecretData{SecretKey: secretKey, RemoteKey: fakeKey, Property: ""}
+	pushSecretDataWithoutSecretKey := fake.PushSecretData{RemoteKey: fakeKey, Property: ""}
+	pushSecretDataWithMetadata := fake.PushSecretData{SecretKey: secretKey, RemoteKey: fakeKey, Property: "", Metadata: &apiextensionsv1.JSON{
+		Raw: []byte(`{
+					"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+					"kind": "PushSecretMetadata",
+					"spec": {
+						"secretPushFormat": "string"
+					}
+				}`)}}
+	pushSecretDataWithProperty := fake.PushSecretData{SecretKey: secretKey, RemoteKey: fakeKey, Property: "other-fake-property"}
+
+	type args struct {
+		store          *esv1.AWSProvider
+		client         fakesm.Client
+		pushSecretData fake.PushSecretData
+		newUUID        string
+		kubeclient     client.Client
+	}
+
+	type want struct {
+		err error
+	}
+	tests := map[string]struct {
+		reason string
+		args   args
+		want   want
+	}{
+		"SetSecretSucceedsWithExistingSecret": {
+			reason: "a secret can be pushed to aws secrets manager when it already exists",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn:       fakesm.NewGetSecretValueFn(secretValueOutput, nil),
+					PutSecretValueFn:       fakesm.NewPutSecretValueFn(putSecretOutput, nil),
+					DescribeSecretFn:       fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+					TagResourceFn:          fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:        fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretSucceedsWithExistingSecretButNoSecretVersionsWithoutProperty": {
+			reason: "a secret can be pushed to aws secrets manager when it already exists but has no secret versions",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(tagSecretOutputNoVersions, nil),
+					PutSecretValueFn: fakesm.NewPutSecretValueFn(putSecretOutput, nil, fakesm.ExpectedPutSecretValueInput{
+						SecretBinary: []byte(`fake-value`),
+						Version:      aws.String(initialVersion),
+					}),
+					TagResourceFn:          fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:        fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretSucceedsWithExistingSecretButNoSecretVersionsWithProperty": {
+			reason: "a secret can be pushed to aws secrets manager when it already exists but has no secret versions",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(tagSecretOutputNoVersions, nil),
+					PutSecretValueFn: fakesm.NewPutSecretValueFn(putSecretOutput, nil, fakesm.ExpectedPutSecretValueInput{
+						SecretBinary: []byte(`{"other-fake-property":"fake-value"}`),
+						Version:      aws.String(initialVersion),
+					}),
+					TagResourceFn:          fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:        fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: pushSecretDataWithProperty,
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretSucceedsWithoutSecretKey": {
+			reason: "a secret can be pushed to aws secrets manager without secret key",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn:       fakesm.NewGetSecretValueFn(secretValueOutput, nil),
+					PutSecretValueFn:       fakesm.NewPutSecretValueFn(putSecretOutput, nil),
+					DescribeSecretFn:       fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+					TagResourceFn:          fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:        fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: pushSecretDataWithoutSecretKey,
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretSucceedsWithExistingSecretAndStringFormat": {
+			reason: "a secret can be pushed to aws secrets manager when it already exists",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn:       fakesm.NewGetSecretValueFn(secretValueOutput, nil),
+					PutSecretValueFn:       fakesm.NewPutSecretValueFn(putSecretOutput, nil),
+					DescribeSecretFn:       fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+					TagResourceFn:          fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:        fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: pushSecretDataWithMetadata,
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretSucceedsWithExistingSecretAndKMSKeyAndDescription": {
+			reason: "a secret can be pushed to aws secrets manager when it already exists",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(secretValueOutput, &getSecretCorrectErr),
+					PutSecretValueFn: fakesm.NewPutSecretValueFn(putSecretOutput, nil),
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+				},
+				pushSecretData: fake.PushSecretData{SecretKey: secretKey, RemoteKey: fakeKey, Property: "", Metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{
+							"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+							"kind": "PushSecretMetadata",
+							"spec": {
+								"kmsKeyID": "bb123123-b2b0-4f60-ac3a-44a13f0e6b6c",
+								"description": "this is a description"
+							}
+						}`)}},
+			},
+			want: want{
+				err: &getSecretCorrectErr,
+			},
+		},
+		"SetSecretSucceedsWithExistingSecretAndAdditionalTags": {
+			reason: "a secret can be pushed to aws secrets manager when it already exists",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn:       fakesm.NewGetSecretValueFn(secretValueOutput, nil),
+					PutSecretValueFn:       fakesm.NewPutSecretValueFn(putSecretOutput, nil),
+					DescribeSecretFn:       fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+					TagResourceFn:          fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:        fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: fake.PushSecretData{SecretKey: secretKey, RemoteKey: fakeKey, Property: "", Metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{
+							"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+							"kind": "PushSecretMetadata",
+							"spec": {
+								"tags": {"tagname12": "tagvalue1"}
+							}
+						}`)}},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretSucceedsWithNewSecret": {
+			reason: "a secret can be pushed to aws secrets manager if it doesn't already exist",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					DescribeSecretFn:    fakesm.NewDescribeSecretFn(blankDescribeSecretOutput, &getSecretCorrectErr),
+					CreateSecretFn:      fakesm.NewCreateSecretFn(secretOutput, nil),
+					PutResourcePolicyFn: fakesm.NewPutResourcePolicyFn(&awssm.PutResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithPropertySucceedsWithNewSecret": {
+			reason: "if a new secret is pushed to aws sm and a pushSecretData property is specified, create a json secret with the pushSecretData property as a key",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(blankDescribeSecretOutput, &getSecretCorrectErr),
+					CreateSecretFn:   fakesm.NewCreateSecretFn(secretOutput, nil, []byte(`{"other-fake-property":"fake-value"}`)),
+				},
+				pushSecretData: pushSecretDataWithProperty,
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithPropertySucceedsWithExistingSecretAndNewPropertyBinary": {
+			reason: "when a pushSecretData property is specified, this property will be added to the sm secret if it is currently absent (sm secret is binary)",
+			args: args{
+				newUUID: defaultUpdatedVersion,
+				store:   makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(secretValueOutputFrom(params{b: []byte((`{"fake-property":"fake-value"}`))}), nil),
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+					PutSecretValueFn: fakesm.NewPutSecretValueFn(putSecretOutput, nil, fakesm.ExpectedPutSecretValueInput{
+						SecretBinary: []byte(`{"fake-property":"fake-value","other-fake-property":"fake-value"}`),
+						Version:      &defaultUpdatedVersion,
+					}),
+					TagResourceFn:          fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:        fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: pushSecretDataWithProperty,
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithPropertySucceedsWithExistingSecretAndRandomUUIDVersion": {
+			reason: "When a secret version is not specified, the client sets a random uuid by default. We should treat a version that can't be parsed to an int as not having a version",
+			args: args{
+				store:   makeValidSecretStore().Spec.Provider.AWS,
+				newUUID: randomUUIDVersionIncremented,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(secretValueOutputFrom(params{
+						b:       []byte((`{"fake-property":"fake-value"}`)),
+						version: &randomUUIDVersion,
+					}), nil),
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(tagSecretOutputFrom(randomUUIDVersion), nil),
+					PutSecretValueFn: fakesm.NewPutSecretValueFn(putSecretOutput, nil, fakesm.ExpectedPutSecretValueInput{
+						SecretBinary: []byte(`{"fake-property":"fake-value","other-fake-property":"fake-value"}`),
+						Version:      &randomUUIDVersionIncremented,
+					}),
+					TagResourceFn:          fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:        fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: pushSecretDataWithProperty,
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithPropertySucceedsWithExistingSecretAndVersionThatCantBeParsed": {
+			reason: "A manually set secret version doesn't have to be a UUID",
+			args: args{
+				newUUID: unparsableVersion,
+				store:   makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(secretValueOutputFrom(params{
+						b:       []byte((`{"fake-property":"fake-value"}`)),
+						version: &unparsableVersion,
+					}), nil),
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+					PutSecretValueFn: fakesm.NewPutSecretValueFn(putSecretOutput, nil, fakesm.ExpectedPutSecretValueInput{
+						SecretBinary: []byte((`fake-value`)),
+						Version:      &unparsableVersion,
+					}),
+					TagResourceFn:          fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:        fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithPropertySucceedsWithExistingSecretAndAbsentVersion": {
+			reason: "When a secret version is not specified, set it to 1",
+			args: args{
+				newUUID: initialVersion,
+				store:   makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(&awssm.GetSecretValueOutput{
+						ARN:          &arn,
+						SecretBinary: []byte((`{"fake-property":"fake-value"}`)),
+					}, nil),
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+					PutSecretValueFn: fakesm.NewPutSecretValueFn(putSecretOutput, nil, fakesm.ExpectedPutSecretValueInput{
+						SecretBinary: []byte(`{"fake-property":"fake-value","other-fake-property":"fake-value"}`),
+						Version:      &initialVersion,
+					}),
+					TagResourceFn:          fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:        fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: pushSecretDataWithProperty,
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithPropertySucceedsWithExistingSecretAndNewPropertyString": {
+			reason: "when a pushSecretData property is specified, this property will be added to the sm secret if it is currently absent (sm secret is a string)",
+			args: args{
+				newUUID: defaultUpdatedVersion,
+				store:   makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(secretValueOutputFrom(params{s: `{"fake-property":"fake-value"}`}), nil),
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+					PutSecretValueFn: fakesm.NewPutSecretValueFn(putSecretOutput, nil, fakesm.ExpectedPutSecretValueInput{
+						SecretBinary: []byte(`{"fake-property":"fake-value","other-fake-property":"fake-value"}`),
+						Version:      &defaultUpdatedVersion,
+					}),
+					TagResourceFn:          fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:        fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: pushSecretDataWithProperty,
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithPropertySucceedsWithExistingSecretAndNewPropertyWithDot": {
+			reason: "when a pushSecretData property is specified, this property will be added to the sm secret if it is currently absent (pushSecretData property is a sub-object)",
+			args: args{
+				newUUID: defaultUpdatedVersion,
+				store:   makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(secretValueOutputFrom(params{s: `{"fake-property":{"fake-property":"fake-value"}}`}), nil),
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+					PutSecretValueFn: fakesm.NewPutSecretValueFn(putSecretOutput, nil, fakesm.ExpectedPutSecretValueInput{
+						SecretBinary: []byte(`{"fake-property":{"fake-property":"fake-value","other-fake-property":"fake-value"}}`),
+						Version:      &defaultUpdatedVersion,
+					}),
+					TagResourceFn:          fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:        fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: fake.PushSecretData{SecretKey: secretKey, RemoteKey: fakeKey, Property: "fake-property.other-fake-property"},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithPropertyFailsExistingNonJsonSecret": {
+			reason: "setting a pushSecretData property is only supported for json secrets",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(secretValueOutputFrom(params{s: `non-json-secret`}), nil),
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+				},
+				pushSecretData: pushSecretDataWithProperty,
+			},
+			want: want{
+				err: errors.New("PushSecret for aws secrets manager with a pushSecretData property requires a json secret"),
+			},
+		},
+		"SetSecretCreateSecretFails": {
+			reason: "CreateSecretWithContext returns an error if it fails",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(blankDescribeSecretOutput, &getSecretCorrectErr),
+					CreateSecretFn:   fakesm.NewCreateSecretFn(nil, noPermission),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err: noPermission,
+			},
+		},
+		"SetSecretGetSecretFails": {
+			reason: "GetSecretValueWithContext returns an error if it fails",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(blankDescribeSecretOutput, noPermission),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err: noPermission,
+			},
+		},
+		"SetSecretWillNotPushSameSecret": {
+			reason: "secret with the same value will not be pushed",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(secretValueOutput2, nil),
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretPutSecretValueFails": {
+			reason: "PutSecretValueWithContext returns an error if it fails",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn:       fakesm.NewGetSecretValueFn(secretValueOutput, nil),
+					PutSecretValueFn:       fakesm.NewPutSecretValueFn(nil, noPermission),
+					DescribeSecretFn:       fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+					TagResourceFn:          fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:        fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err: noPermission,
+			},
+		},
+		"SetSecretWrongGetSecretErrFails": {
+			reason: "DescribeSecret errors out when anything except awssm.ErrCodeResourceNotFoundException",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(blankDescribeSecretOutput, &getSecretWrongErr),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err: &getSecretWrongErr,
+			},
+		},
+		"SetSecretDescribeSecretFails": {
+			reason: "secret cannot be described",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(secretValueOutput, nil),
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(nil, noPermission),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err: noPermission,
+			},
+		},
+		"SetSecretDoesNotOverwriteUntaggedSecret": {
+			reason: "secret cannot be described",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(secretValueOutput, nil),
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(tagSecretOutputFaulty, nil),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err: errors.New("secret not managed by external-secrets"),
+			},
+		},
+		"PatchSecretTags": {
+			reason: "secret key is configured with tags to remove and add",
+			args: args{
+				store: &esv1.AWSProvider{
+					Service: esv1.AWSServiceSecretsManager,
+					Region:  "eu-west-2",
+				},
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(secretValueOutputFrom(params{s: `{"fake-property":{"fake-property":"fake-value"}}`}), nil),
+					DescribeSecretFn: fakesm.NewDescribeSecretFn(&awssm.DescribeSecretOutput{
+						ARN: &arn,
+						Tags: []types.Tag{
+							{Key: &managedBy, Value: &externalSecrets},
+							{Key: new("team"), Value: new("paradox")},
+						},
+					}, nil),
+					PutSecretValueFn: fakesm.NewPutSecretValueFn(putSecretOutput, nil),
+					TagResourceFn: fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil, func(input *awssm.TagResourceInput) {
+						assert.Len(t, input.Tags, 2)
+						assert.Contains(t, input.Tags, types.Tag{Key: &managedBy, Value: &externalSecrets})
+						assert.Contains(t, input.Tags, types.Tag{Key: new("env"), Value: new("sandbox")})
+					}),
+					UntagResourceFn: fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil, func(input *awssm.UntagResourceInput) {
+						assert.Len(t, input.TagKeys, 1)
+						assert.Equal(t, []string{"team"}, input.TagKeys)
+						assert.NotContains(t, input.TagKeys, managedBy)
+					}),
+					DeleteResourcePolicyFn: fakesm.NewDeleteResourcePolicyFn(&awssm.DeleteResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: fake.PushSecretData{SecretKey: secretKey, RemoteKey: fakeKey, Property: "", Metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{
+					"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+					"kind": "PushSecretMetadata",
+					"spec": {
+						"secretPushFormat": "string",
+						"tags": {
+							"env": "sandbox"
+						}
+					}
+				}`)}},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithExistingNonChangingResourcePolicy": {
+			reason: "sync an existing secret without syncing resource policy that has no change",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					// NO call to PutResourcePolicy
+					GetSecretValueFn:    fakesm.NewGetSecretValueFn(secretValueOutput, nil),
+					PutSecretValueFn:    fakesm.NewPutSecretValueFn(putSecretOutput, nil),
+					DescribeSecretFn:    fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+					TagResourceFn:       fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:     fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					GetResourcePolicyFn: fakesm.NewGetResourcePolicyFn(makeValidGetResourcePolicyOutput(), nil),
+				},
+				pushSecretData: fake.PushSecretData{
+					SecretKey: secretKey,
+					RemoteKey: fakeKey,
+					Property:  "",
+					Metadata: &apiextensionsv1.JSON{
+						Raw: []byte(`{
+							"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+							"kind": "PushSecretMetadata",
+							"spec": {
+								"secretPushFormat": "string",
+								"resourcePolicy": {
+										"blockPublicPolicy": true,
+										"policySourceRef": {
+											"kind": "ConfigMap",
+											"name": "resource-policy",
+											"key": "policy.json"
+									}
+								}
+							}
+						}`),
+					},
+				},
+				kubeclient: clientfake.NewFakeClient(&corev1.ConfigMap{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "resource-policy",
+					},
+					// Create a policy that does not match object order of the
+					// existing one
+					Data: map[string]string{
+						"policy.json": `
+							{
+								"Version": "2012-10-17",
+								"Statement": [
+									{
+										"Resource": "*",
+										"Effect": "Deny",
+										"Principal": "*",
+										"Action": [
+											"secretsmanager:PutResourcePolicy",
+											"secretsmanager:DeleteResourcePolicy",
+											"secretsmanager:GetResourcePolicy"
+										],
+										"Condition": {
+											"ArnNotEquals": {
+												"aws:PrincipalArn": [
+													"arn:aws:iam::000000000000:root",
+													"arn:aws:iam::000000000000:role/admin"
+												]
+											}
+										},
+										"Sid": "DenyPolicyChangesExceptAdmins"
+									}
+								]
+							}
+						`,
+					},
+				}),
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithExistingChangingResourcePolicy": {
+			reason: "sync an existing secret and the resource policy when it has changes",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn:    fakesm.NewGetSecretValueFn(secretValueOutput, nil),
+					PutSecretValueFn:    fakesm.NewPutSecretValueFn(putSecretOutput, nil),
+					DescribeSecretFn:    fakesm.NewDescribeSecretFn(tagSecretOutput, nil),
+					TagResourceFn:       fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil),
+					UntagResourceFn:     fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil),
+					GetResourcePolicyFn: fakesm.NewGetResourcePolicyFn(makeValidGetResourcePolicyOutput(), nil),
+					// Call to PutResourcePolicy since policy does not match
+					PutResourcePolicyFn: fakesm.NewPutResourcePolicyFn(&awssm.PutResourcePolicyOutput{}, nil),
+				},
+				pushSecretData: fake.PushSecretData{
+					SecretKey: secretKey,
+					RemoteKey: fakeKey,
+					Property:  "",
+					Metadata: &apiextensionsv1.JSON{
+						Raw: []byte(`{
+							"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+							"kind": "PushSecretMetadata",
+							"spec": {
+								"secretPushFormat": "string",
+								"resourcePolicy": {
+										"blockPublicPolicy": true,
+										"policySourceRef": {
+											"kind": "ConfigMap",
+											"name": "resource-policy",
+											"key": "policy.json"
+									}
+								}
+							}
+						}`),
+					},
+				},
+				kubeclient: clientfake.NewFakeClient(&corev1.ConfigMap{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "resource-policy",
+					},
+					// Create a policy that does not match object order of the
+					// existing one
+					Data: map[string]string{
+						"policy.json": `
+							{
+								"Version": "2012-10-17",
+								"Statement": [
+									{
+										"Resource": "*",
+										"Effect": "Deny",
+										"Principal": "*",
+										"Action": [
+											"secretsmanager:PutResourcePolicy",
+											"secretsmanager:DeleteResourcePolicy",
+											"secretsmanager:GetResourcePolicy"
+										],
+										"Condition": {
+											"ArnNotEquals": {
+												"aws:PrincipalArn": [
+													"arn:aws:iam::000000000000:root",
+													"arn:aws:iam::000000000000:role/sudo"
+												]
+											}
+										},
+										"Sid": "DenyPolicyChangesExceptAdmins"
+									}
+								]
+							}
+						`,
+					},
+				}),
+			},
+			want: want{
+				err: nil,
+			},
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			sm := SecretsManager{
+				client:  &tc.args.client,
+				prefix:  tc.args.store.Prefix,
+				newUUID: func() string { return tc.args.newUUID },
+				kube:    tc.args.kubeclient,
+			}
+
+			err := sm.PushSecret(context.Background(), fakeSecret, tc.args.pushSecretData)
+
+			// Error nil XOR tc.want.err nil
+			if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
+				t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
+			}
+
+			// if errors are the same type but their contents do not match
+			if err != nil && tc.want.err != nil {
+				if !strings.Contains(err.Error(), tc.want.err.Error()) {
+					t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
+				}
+			}
+		})
+	}
+}
+
+func TestDeleteSecret(t *testing.T) {
+	fakeClient := fakesm.Client{}
+	managed := managedBy
+	manager := externalSecrets
+	secretTag := types.Tag{
+		Key:   &managed,
+		Value: &manager,
+	}
+	type args struct {
+		client               fakesm.Client
+		config               esv1.SecretsManager
+		prefix               string
+		getSecretOutput      *awssm.GetSecretValueOutput
+		describeSecretOutput *awssm.DescribeSecretOutput
+		deleteSecretOutput   *awssm.DeleteSecretOutput
+		getSecretErr         error
+		describeSecretErr    error
+		deleteSecretErr      error
+	}
+	type want struct {
+		err error
+	}
+	type testCase struct {
+		args   args
+		want   want
+		reason string
+	}
+	tests := map[string]testCase{
+		"Deletes Successfully": {
+			args: args{
+
+				client:          fakeClient,
+				config:          esv1.SecretsManager{},
+				getSecretOutput: &awssm.GetSecretValueOutput{},
+				describeSecretOutput: &awssm.DescribeSecretOutput{
+					Tags: []types.Tag{secretTag},
+				},
+				deleteSecretOutput: &awssm.DeleteSecretOutput{},
+				getSecretErr:       nil,
+				describeSecretErr:  nil,
+				deleteSecretErr:    nil,
+			},
+			want: want{
+				err: nil,
+			},
+			reason: "",
+		},
+		"Deletes Successfully with ForceDeleteWithoutRecovery": {
+			args: args{
+
+				client: fakeClient,
+				config: esv1.SecretsManager{
+					ForceDeleteWithoutRecovery: true,
+				},
+				getSecretOutput: &awssm.GetSecretValueOutput{},
+				describeSecretOutput: &awssm.DescribeSecretOutput{
+					Tags: []types.Tag{secretTag},
+				},
+				deleteSecretOutput: &awssm.DeleteSecretOutput{
+					DeletionDate: aws.Time(time.Now()),
+				},
+				getSecretErr:      nil,
+				describeSecretErr: nil,
+				deleteSecretErr:   nil,
+			},
+			want: want{
+				err: nil,
+			},
+			reason: "",
+		},
+		"Not Managed by ESO": {
+			args: args{
+
+				client:          fakeClient,
+				config:          esv1.SecretsManager{},
+				getSecretOutput: &awssm.GetSecretValueOutput{},
+				describeSecretOutput: &awssm.DescribeSecretOutput{
+					Tags: []types.Tag{},
+				},
+				deleteSecretOutput: &awssm.DeleteSecretOutput{},
+				getSecretErr:       nil,
+				describeSecretErr:  nil,
+				deleteSecretErr:    nil,
+			},
+			want: want{
+				err: nil,
+			},
+			reason: "",
+		},
+		"Invalid Recovery Window": {
+			args: args{
+
+				client: fakesm.Client{},
+				config: esv1.SecretsManager{
+					RecoveryWindowInDays: 1,
+				},
+				getSecretOutput: &awssm.GetSecretValueOutput{},
+				describeSecretOutput: &awssm.DescribeSecretOutput{
+					Tags: []types.Tag{secretTag},
+				},
+				deleteSecretOutput: &awssm.DeleteSecretOutput{},
+				getSecretErr:       nil,
+				describeSecretErr:  nil,
+				deleteSecretErr:    nil,
+			},
+			want: want{
+				err: errors.New("invalid DeleteSecretInput: RecoveryWindowInDays must be between 7 and 30 days"),
+			},
+			reason: "",
+		},
+		"RecoveryWindowInDays is supplied with ForceDeleteWithoutRecovery": {
+			args: args{
+
+				client: fakesm.Client{},
+				config: esv1.SecretsManager{
+					RecoveryWindowInDays:       7,
+					ForceDeleteWithoutRecovery: true,
+				},
+				getSecretOutput: &awssm.GetSecretValueOutput{},
+				describeSecretOutput: &awssm.DescribeSecretOutput{
+					Tags: []types.Tag{secretTag},
+				},
+				deleteSecretOutput: &awssm.DeleteSecretOutput{},
+				getSecretErr:       nil,
+				describeSecretErr:  nil,
+				deleteSecretErr:    nil,
+			},
+			want: want{
+				err: errors.New("invalid DeleteSecretInput: ForceDeleteWithoutRecovery conflicts with RecoveryWindowInDays"),
+			},
+			reason: "",
+		},
+		"Failed to get Tags": {
+			args: args{
+
+				client:               fakeClient,
+				config:               esv1.SecretsManager{},
+				getSecretOutput:      &awssm.GetSecretValueOutput{},
+				describeSecretOutput: nil,
+				deleteSecretOutput:   nil,
+				getSecretErr:         nil,
+				describeSecretErr:    errors.New("failed to get tags"),
+				deleteSecretErr:      nil,
+			},
+			want: want{
+				err: errors.New("failed to get tags"),
+			},
+			reason: "",
+		},
+		"Secret Not Found": {
+			args: args{
+				client:               fakeClient,
+				config:               esv1.SecretsManager{},
+				getSecretOutput:      nil,
+				describeSecretOutput: nil,
+				deleteSecretOutput:   nil,
+				getSecretErr:         errors.New("not here, sorry dude"),
+				describeSecretErr:    nil,
+				deleteSecretErr:      nil,
+			},
+			want: want{
+				err: errors.New("not here, sorry dude"),
+			},
+		},
+		"Not expected AWS error": {
+			args: args{
+				client:               fakeClient,
+				config:               esv1.SecretsManager{},
+				getSecretOutput:      nil,
+				describeSecretOutput: nil,
+				deleteSecretOutput:   nil,
+				getSecretErr:         errors.New("aws unavailable"),
+				describeSecretErr:    nil,
+				deleteSecretErr:      nil,
+			},
+			want: want{
+				err: errors.New("aws unavailable"),
+			},
+		},
+		"unexpected error": {
+			args: args{
+				client:               fakeClient,
+				config:               esv1.SecretsManager{},
+				getSecretOutput:      nil,
+				describeSecretOutput: nil,
+				deleteSecretOutput:   nil,
+				getSecretErr:         errors.New("timeout"),
+				describeSecretErr:    nil,
+				deleteSecretErr:      nil,
+			},
+			want: want{
+				err: errors.New("timeout"),
+			},
+		},
+		"DeleteWithPrefix": {
+			args: args{
+				client: fakesm.Client{
+					GetSecretValueFn: func(ctx context.Context, input *awssm.GetSecretValueInput, opts ...func(*awssm.Options)) (*awssm.GetSecretValueOutput, error) {
+						// Verify that the input secret ID has the prefix applied
+						if *input.SecretId != "my-prefix-"+fakeKey {
+							return nil, fmt.Errorf("expected secret name to be prefixed with 'my-prefix-', got %s", *input.SecretId)
+						}
+						return &awssm.GetSecretValueOutput{}, nil
+					},
+					DescribeSecretFn: func(ctx context.Context, input *awssm.DescribeSecretInput, opts ...func(*awssm.Options)) (*awssm.DescribeSecretOutput, error) {
+						// Verify that the input secret ID has the prefix applied
+						if *input.SecretId != "my-prefix-"+fakeKey {
+							return nil, fmt.Errorf("expected secret name to be prefixed with 'my-prefix-', got %s", *input.SecretId)
+						}
+						return &awssm.DescribeSecretOutput{
+							Tags: []types.Tag{secretTag},
+						}, nil
+					},
+					DeleteSecretFn: func(ctx context.Context, input *awssm.DeleteSecretInput, opts ...func(*awssm.Options)) (*awssm.DeleteSecretOutput, error) {
+						return &awssm.DeleteSecretOutput{}, nil
+					},
+				},
+				config:               esv1.SecretsManager{},
+				prefix:               "my-prefix-",
+				getSecretOutput:      nil,
+				describeSecretOutput: nil,
+				deleteSecretOutput:   nil,
+				getSecretErr:         nil,
+				describeSecretErr:    nil,
+				deleteSecretErr:      nil,
+			},
+			want: want{
+				err: nil,
+			},
+			reason: "Verifies that the prefix is correctly applied when deleting a secret",
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			ref := fake.PushSecretData{RemoteKey: fakeKey}
+			sm := SecretsManager{
+				client: &tc.args.client,
+				config: &tc.args.config,
+				prefix: tc.args.prefix,
+			}
+
+			if tc.args.client.GetSecretValueFn == nil {
+				tc.args.client.GetSecretValueFn = fakesm.NewGetSecretValueFn(tc.args.getSecretOutput, tc.args.getSecretErr)
+			}
+			if tc.args.client.DescribeSecretFn == nil {
+				tc.args.client.DescribeSecretFn = fakesm.NewDescribeSecretFn(tc.args.describeSecretOutput, tc.args.describeSecretErr)
+			}
+			if tc.args.client.DeleteSecretFn == nil {
+				tc.args.client.DeleteSecretFn = fakesm.NewDeleteSecretFn(tc.args.deleteSecretOutput, tc.args.deleteSecretErr)
+			}
+
+			err := sm.DeleteSecret(context.TODO(), ref)
+			t.Logf("DeleteSecret error: %v", err)
+
+			// Error nil XOR tc.want.err nil
+			if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
+				t.Errorf("\nTesting DeleteSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
+			}
+
+			// if errors are the same type but their contents do not match
+			if err != nil && tc.want.err != nil {
+				if !strings.Contains(err.Error(), tc.want.err.Error()) {
+					t.Errorf("\nTesting DeleteSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
+				}
+			}
+		})
+	}
+}
+func makeValidSecretStore() *esv1.SecretStore {
+	return &esv1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "aws-secret-store",
+			Namespace: "default",
+		},
+		Spec: esv1.SecretStoreSpec{
+			Provider: &esv1.SecretStoreProvider{
+				AWS: &esv1.AWSProvider{
+					Service: esv1.AWSServiceSecretsManager,
+					Region:  "eu-west-2",
+				},
+			},
+		},
+	}
+}
+
+func getTagSlice() []types.Tag {
+	tagKey1 := tagname1
+	tagValue1 := tagvalue1
+	tagKey2 := tagname2
+	tagValue2 := tagvalue2
+
+	return []types.Tag{
+		{
+			Key:   &tagKey1,
+			Value: &tagValue1,
+		},
+		{
+			Key:   &tagKey2,
+			Value: &tagValue2,
+		},
+	}
+}
+func TestSecretsManagerGetAllSecrets(t *testing.T) {
+	ctx := context.Background()
+
+	errBoom := errors.New("boom")
+	secretName := "my-secret"
+	secretVersion := "AWSCURRENT"
+	secretPath := "/path/to/secret"
+	secretValue := "secret value"
+	secretTags := map[string]string{
+		"foo": "bar",
+	}
+	// Test cases
+	testCases := []struct {
+		name                  string
+		ref                   esv1.ExternalSecretFind
+		secretName            string
+		secretVersion         string
+		secretValue           string
+		batchGetSecretValueFn func(context.Context, *awssm.BatchGetSecretValueInput, ...func(*awssm.Options)) (*awssm.BatchGetSecretValueOutput, error)
+		listSecretsFn         func(context.Context, *awssm.ListSecretsInput, ...func(*awssm.Options)) (*awssm.ListSecretsOutput, error)
+		getSecretValueFn      func(context.Context, *awssm.GetSecretValueInput, ...func(*awssm.Options)) (*awssm.GetSecretValueOutput, error)
+		expectedData          map[string][]byte
+		expectedError         string
+	}{
+		{
+			name: "Matching secrets found",
+			ref: esv1.ExternalSecretFind{
+				Name: &esv1.FindName{
+					RegExp: secretName,
+				},
+				Path: new(secretPath),
+			},
+			secretName:    secretName,
+			secretVersion: secretVersion,
+			secretValue:   secretValue,
+			batchGetSecretValueFn: func(_ context.Context, input *awssm.BatchGetSecretValueInput, _ ...func(*awssm.Options)) (*awssm.BatchGetSecretValueOutput, error) {
+				assert.Len(t, input.Filters, 1)
+				assert.Equal(t, "name", string(input.Filters[0].Key))
+				assert.Equal(t, secretPath, input.Filters[0].Values[0])
+				return &awssm.BatchGetSecretValueOutput{
+					SecretValues: []types.SecretValueEntry{
+						{
+							Name:          new(secretName),
+							VersionStages: []string{secretVersion},
+							SecretBinary:  []byte(secretValue),
+						},
+					},
+				}, nil
+			},
+			expectedData: map[string][]byte{
+				secretName: []byte(secretValue),
+			},
+			expectedError: "",
+		},
+		{
+			name: "Error occurred while fetching secret value",
+			ref: esv1.ExternalSecretFind{
+				Name: &esv1.FindName{
+					RegExp: secretName,
+				},
+				Path: new(secretPath),
+			},
+			secretName:    secretName,
+			secretVersion: secretVersion,
+			secretValue:   secretValue,
+			batchGetSecretValueFn: func(_ context.Context, input *awssm.BatchGetSecretValueInput, _ ...func(*awssm.Options)) (*awssm.BatchGetSecretValueOutput, error) {
+				return &awssm.BatchGetSecretValueOutput{
+					SecretValues: []types.SecretValueEntry{
+						{
+							Name: new(secretName),
+						},
+					},
+				}, errBoom
+			},
+			expectedData:  nil,
+			expectedError: errBoom.Error(),
+		},
+		{
+			name: "regexp: error occurred while listing secrets",
+			ref: esv1.ExternalSecretFind{
+				Name: &esv1.FindName{
+					RegExp: secretName,
+				},
+			},
+			listSecretsFn: func(_ context.Context, input *awssm.ListSecretsInput, _ ...func(*awssm.Options)) (*awssm.ListSecretsOutput, error) {
+				return nil, errBoom
+			},
+			expectedData:  nil,
+			expectedError: errBoom.Error(),
+		},
+		{
+			name: "regep: no matching secrets found",
+			ref: esv1.ExternalSecretFind{
+				Name: &esv1.FindName{
+					RegExp: secretName,
+				},
+			},
+			listSecretsFn: func(_ context.Context, input *awssm.ListSecretsInput, _ ...func(*awssm.Options)) (*awssm.ListSecretsOutput, error) {
+				return &awssm.ListSecretsOutput{
+					SecretList: []types.SecretListEntry{
+						{
+							Name: new("other-secret"),
+						},
+					},
+				}, nil
+			},
+			batchGetSecretValueFn: func(_ context.Context, input *awssm.BatchGetSecretValueInput, _ ...func(*awssm.Options)) (*awssm.BatchGetSecretValueOutput, error) {
+				return &awssm.BatchGetSecretValueOutput{
+					SecretValues: []types.SecretValueEntry{
+						{
+							Name: new("other-secret"),
+						},
+					},
+				}, nil
+			},
+			expectedData:  make(map[string][]byte),
+			expectedError: "",
+		},
+		{
+			name: "invalid regexp",
+			ref: esv1.ExternalSecretFind{
+				Name: &esv1.FindName{
+					RegExp: "[",
+				},
+			},
+			expectedData:  nil,
+			expectedError: "could not compile find.name.regexp [[]: error parsing regexp: missing closing ]: `[`",
+		},
+
+		{
+			name: "tags: Matching secrets found",
+			ref: esv1.ExternalSecretFind{
+				Tags: secretTags,
+			},
+			secretName:    secretName,
+			secretVersion: secretVersion,
+			secretValue:   secretValue,
+			batchGetSecretValueFn: func(_ context.Context, input *awssm.BatchGetSecretValueInput, _ ...func(*awssm.Options)) (*awssm.BatchGetSecretValueOutput, error) {
+				assert.Len(t, input.Filters, 2)
+				assert.Equal(t, "tag-key", string(input.Filters[0].Key))
+				assert.Equal(t, "foo", input.Filters[0].Values[0])
+				assert.Equal(t, "tag-value", string(input.Filters[1].Key))
+				assert.Equal(t, "bar", input.Filters[1].Values[0])
+				return &awssm.BatchGetSecretValueOutput{
+					SecretValues: []types.SecretValueEntry{
+						{
+							Name:          new(secretName),
+							VersionStages: []string{secretVersion},
+							SecretBinary:  []byte(secretValue),
+						},
+					},
+				}, nil
+			},
+			expectedData: map[string][]byte{
+				secretName: []byte(secretValue),
+			},
+			expectedError: "",
+		},
+		{
+			name: "name and tags: matching secrets found",
+			ref: esv1.ExternalSecretFind{
+				Name: &esv1.FindName{
+					RegExp: secretName,
+				},
+				Tags: secretTags,
+			},
+			listSecretsFn: func(_ context.Context, input *awssm.ListSecretsInput, _ ...func(*awssm.Options)) (*awssm.ListSecretsOutput, error) {
+				allSecrets := []types.SecretListEntry{
+					{
+						Name: new(secretName),
+						Tags: []types.Tag{
+							{Key: new("foo"), Value: new("bar")},
+						},
+					},
+					{
+						Name: new(fmt.Sprintf("%ssomeothertext", secretName)),
+					},
+					{
+						Name: new("unmatched-secret"),
+						Tags: []types.Tag{
+							{Key: new("foo"), Value: new("bar")},
+						},
+					},
+				}
+
+				filtered := make([]types.SecretListEntry, 0, len(allSecrets))
+				for _, secret := range allSecrets {
+					exclude := false
+
+					tagMap := map[string]string{}
+					for _, t := range secret.Tags {
+						if t.Key != nil && t.Value != nil {
+							tagMap[*t.Key] = *t.Value
+						}
+					}
+
+					for _, f := range input.Filters {
+						switch f.Key {
+						case types.FilterNameStringTypeName:
+							if secret.Name != nil {
+								for _, v := range f.Values {
+									if strings.Contains(*secret.Name, v) {
+										exclude = true
+										break
+									}
+								}
+							}
+						case types.FilterNameStringTypeTagKey:
+							for _, v := range f.Values {
+								if tagMap[v] == "" {
+									exclude = true
+									break
+								}
+							}
+						case types.FilterNameStringTypeDescription,
+							types.FilterNameStringTypeTagValue,
+							types.FilterNameStringTypePrimaryRegion,
+							types.FilterNameStringTypeOwningService,
+							types.FilterNameStringTypeAll:
+							continue
+						}
+					}
+
+					if !exclude {
+						filtered = append(filtered, secret)
+					}
+				}
+				return &awssm.ListSecretsOutput{SecretList: filtered}, nil
+			},
+			getSecretValueFn: func(_ context.Context, input *awssm.GetSecretValueInput, _ ...func(*awssm.Options)) (*awssm.GetSecretValueOutput, error) {
+				if *input.SecretId == secretName {
+					return &awssm.GetSecretValueOutput{
+						Name:          new(secretName),
+						VersionStages: []string{secretVersion},
+						SecretBinary:  []byte(secretValue),
+					}, nil
+				}
+				if *input.SecretId == "unmatched-secret" {
+					return &awssm.GetSecretValueOutput{
+						Name:          new("unmatched-secret"),
+						VersionStages: []string{secretVersion},
+						SecretBinary:  []byte("othervalue"),
+					}, nil
+				}
+				return &awssm.GetSecretValueOutput{
+					Name:          new(fmt.Sprintf("%ssomeothertext", secretName)),
+					VersionStages: []string{secretVersion},
+					SecretBinary:  []byte("someothervalue"),
+				}, nil
+			},
+			expectedData: map[string][]byte{
+				secretName: []byte(secretValue),
+			},
+			expectedError: "",
+		},
+		{
+			name: "tags: error occurred while fetching secret value",
+			ref: esv1.ExternalSecretFind{
+				Tags: secretTags,
+			},
+			secretName:    secretName,
+			secretVersion: secretVersion,
+			secretValue:   secretValue,
+			batchGetSecretValueFn: func(_ context.Context, input *awssm.BatchGetSecretValueInput, _ ...func(*awssm.Options)) (*awssm.BatchGetSecretValueOutput, error) {
+				return &awssm.BatchGetSecretValueOutput{
+					SecretValues: []types.SecretValueEntry{
+						{
+							Name:          new(secretName),
+							VersionStages: []string{secretVersion},
+							SecretBinary:  []byte(secretValue),
+						},
+					},
+				}, errBoom
+			},
+			expectedData:  nil,
+			expectedError: errBoom.Error(),
+		},
+		{
+			name: "tags: error occurred while listing secrets",
+			ref: esv1.ExternalSecretFind{
+				Tags: secretTags,
+			},
+			batchGetSecretValueFn: func(_ context.Context, input *awssm.BatchGetSecretValueInput, _ ...func(*awssm.Options)) (*awssm.BatchGetSecretValueOutput, error) {
+				return nil, errBoom
+			},
+			expectedData:  nil,
+			expectedError: errBoom.Error(),
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			fc := fakesm.NewClient()
+			fc.BatchGetSecretValueFn = tc.batchGetSecretValueFn
+			fc.ListSecretsFn = tc.listSecretsFn
+			fc.GetSecretValueFn = tc.getSecretValueFn
+			sm := SecretsManager{
+				client: fc,
+				cache:  make(map[string]*awssm.GetSecretValueOutput),
+			}
+			data, err := sm.GetAllSecrets(ctx, tc.ref)
+			if err != nil && err.Error() != tc.expectedError {
+				t.Errorf("unexpected error: got %v, want %v", err, tc.expectedError)
+			}
+			if !reflect.DeepEqual(data, tc.expectedData) {
+				t.Errorf("unexpected data: got %v, want %v", data, tc.expectedData)
+			}
+		})
+	}
+}
+
+func TestSecretsManagerValidate(t *testing.T) {
+	type fields struct {
+		cfg          *aws.Config
+		referentAuth bool
+	}
+
+	validConfig := &aws.Config{
+		Credentials: credentials.NewStaticCredentialsProvider(
+			"fake",
+			"fake",
+			"fake",
+		),
+	}
+
+	invalidConfig := &aws.Config{
+		Credentials: &FakeCredProvider{
+			retrieveFunc: func() (aws.Credentials, error) {
+				return aws.Credentials{}, errors.New("invalid credentials")
+			},
+		},
+	}
+
+	tests := []struct {
+		name    string
+		fields  fields
+		want    esv1.ValidationResult
+		wantErr bool
+	}{
+		{
+			name: "ReferentAuth should always return unknown",
+			fields: fields{
+				referentAuth: true,
+			},
+			want: esv1.ValidationResultUnknown,
+		},
+		{
+			name: "Valid credentials should return ready",
+			fields: fields{
+				cfg: validConfig,
+			},
+			want: esv1.ValidationResultReady,
+		},
+		{
+			name: "Invalid credentials should return error",
+			fields: fields{
+				cfg: invalidConfig,
+			},
+			want:    esv1.ValidationResultError,
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			sm := &SecretsManager{
+				cfg:          tt.fields.cfg,
+				referentAuth: tt.fields.referentAuth,
+			}
+			got, err := sm.Validate()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("SecretsManager.Validate() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("SecretsManager.Validate() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+func TestSecretExists(t *testing.T) {
+	arn := "arn:aws:secretsmanager:us-east-1:702902267788:secret:foo-bar5-Robbgh"
+	defaultVersion := "00000000-0000-0000-0000-000000000002"
+	secretValueOutput := &awssm.GetSecretValueOutput{
+		ARN:       &arn,
+		VersionId: &defaultVersion,
+	}
+
+	blankSecretValueOutput := &awssm.GetSecretValueOutput{}
+
+	getSecretCorrectErr := types.ResourceNotFoundException{}
+	getSecretWrongErr := types.InvalidRequestException{}
+
+	pushSecretDataWithoutProperty := fake.PushSecretData{SecretKey: "fake-secret-key", RemoteKey: fakeKey, Property: ""}
+
+	type args struct {
+		store          *esv1.AWSProvider
+		client         fakesm.Client
+		pushSecretData fake.PushSecretData
+	}
+
+	type want struct {
+		err       error
+		wantError bool
+	}
+
+	tests := map[string]struct {
+		args args
+		want want
+	}{
+		"SecretExistsReturnsTrueForExistingSecret": {
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(secretValueOutput, nil),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err:       nil,
+				wantError: true,
+			},
+		},
+		"SecretExistsReturnsFalseForNonExistingSecret": {
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(blankSecretValueOutput, &getSecretCorrectErr),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err:       nil,
+				wantError: false,
+			},
+		},
+		"SecretExistsReturnsFalseForErroredSecret": {
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueFn: fakesm.NewGetSecretValueFn(blankSecretValueOutput, &getSecretWrongErr),
+				},
+				pushSecretData: pushSecretDataWithoutProperty,
+			},
+			want: want{
+				err:       &getSecretWrongErr,
+				wantError: false,
+			},
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			sm := &SecretsManager{
+				client: &tc.args.client,
+			}
+			got, err := sm.SecretExists(context.Background(), tc.args.pushSecretData)
+
+			assert.Equal(
+				t,
+				tc.want,
+				want{
+					err:       err,
+					wantError: got,
+				})
+		})
+	}
+}
+
+func TestConstructMetadataWithDefaults(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       *apiextensionsv1.JSON
+		expected    *metadata.PushSecretMetadata[PushSecretMetadataSpec]
+		expectError bool
+	}{
+		{
+			name: "Valid metadata with multiple fields",
+			input: &apiextensionsv1.JSON{Raw: []byte(`{
+				"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+				"kind": "PushSecretMetadata",
+				"spec": {
+					"description": "test description",
+					"secretPushFormat":"string",
+					"kmsKeyID": "custom-kms-key",
+					"tags": {
+						"customKey": "customValue"
+					},
+				}
+			}`)},
+			expected: &metadata.PushSecretMetadata[PushSecretMetadataSpec]{
+				APIVersion: "kubernetes.external-secrets.io/v1alpha1",
+				Kind:       "PushSecretMetadata",
+				Spec: PushSecretMetadataSpec{
+					Description:      "test description",
+					SecretPushFormat: "string",
+					KMSKeyID:         "custom-kms-key",
+					Tags: map[string]string{
+						"customKey": "customValue",
+						managedBy:   externalSecrets,
+					},
+				},
+			},
+		},
+		{
+			name:  "Empty metadata, defaults applied",
+			input: nil,
+			expected: &metadata.PushSecretMetadata[PushSecretMetadataSpec]{
+				Spec: PushSecretMetadataSpec{
+					Description:      fmt.Sprintf("secret '%s:%s'", managedBy, externalSecrets),
+					SecretPushFormat: "binary",
+					KMSKeyID:         "alias/aws/secretsmanager",
+					Tags: map[string]string{
+						managedBy: externalSecrets,
+					},
+				},
+			},
+		},
+		{
+			name: "Added default metadata with 'managed-by' tag",
+			input: &apiextensionsv1.JSON{Raw: []byte(`{
+				"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+				"kind": "PushSecretMetadata",
+				"spec": {
+					"tags": {
+                        "managed-by": "external-secrets",
+						"customKey": "customValue"
+					},
+				}
+			}`)},
+			expected:    nil,
+			expectError: true,
+		},
+		{
+			name:        "Invalid metadata format",
+			input:       &apiextensionsv1.JSON{Raw: []byte(`invalid-json`)},
+			expected:    nil,
+			expectError: true,
+		},
+		{
+			name:        "Metadata with 'managed-by' tag specified",
+			input:       &apiextensionsv1.JSON{Raw: []byte(`{"tags":{"managed-by":"invalid"}}`)},
+			expected:    nil,
+			expectError: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result, err := (&SecretsManager{}).constructMetadataWithDefaults(tt.input)
+
+			if tt.expectError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.expected, result)
+			}
+		})
+	}
+}
+
+func TestComputeTagsToUpdate(t *testing.T) {
+	tests := []struct {
+		name     string
+		tags     map[string]string
+		metaTags map[string]string
+		expected []types.Tag
+		modified bool
+	}{
+		{
+			name: "No tags to update",
+			tags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+			metaTags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+			expected: []types.Tag{
+				{Key: new("key1"), Value: new("value1")},
+				{Key: new("key2"), Value: new("value2")},
+			},
+			modified: false,
+		},
+		{
+			name: "No tags to update as managed-by tag is ignored",
+			tags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+			metaTags: map[string]string{
+				"key1":    "value1",
+				"key2":    "value2",
+				managedBy: externalSecrets,
+			},
+			expected: []types.Tag{
+				{Key: new("key1"), Value: new("value1")},
+				{Key: new("key2"), Value: new("value2")},
+				{Key: new(managedBy), Value: new(externalSecrets)},
+			},
+			modified: false,
+		},
+		{
+			name: "Add new tag",
+			tags: map[string]string{
+				"key1": "value1",
+			},
+			metaTags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+			expected: []types.Tag{
+				{Key: new("key1"), Value: new("value1")},
+				{Key: new("key2"), Value: new("value2")},
+			},
+			modified: true,
+		},
+		{
+			name: "Update existing tag value",
+			tags: map[string]string{
+				"key1": "value1",
+			},
+			metaTags: map[string]string{
+				"key1": "newValue",
+			},
+			expected: []types.Tag{
+				{Key: new("key1"), Value: new("newValue")},
+			},
+			modified: true,
+		},
+		{
+			name:     "Empty tags and metaTags",
+			tags:     map[string]string{},
+			metaTags: map[string]string{},
+			expected: []types.Tag{},
+			modified: false,
+		},
+		{
+			name: "Empty tags with non-empty metaTags",
+			tags: map[string]string{},
+			metaTags: map[string]string{
+				"key1": "value1",
+			},
+			expected: []types.Tag{
+				{Key: new("key1"), Value: new("value1")},
+			},
+			modified: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result, modified := computeTagsToUpdate(tt.tags, tt.metaTags)
+			assert.ElementsMatch(t, tt.expected, result)
+			assert.Equal(t, tt.modified, modified)
+		})
+	}
+}
+
+func TestPatchTags(t *testing.T) {
+	type call struct {
+		untagCalled bool
+		tagCalled   bool
+	}
+	tests := []struct {
+		name         string
+		existingTags map[string]string
+		metaTags     map[string]string
+		expectUntag  bool
+		expectTag    bool
+		assertsTag   func(input *awssm.TagResourceInput)
+		assertsUntag func(input *awssm.UntagResourceInput)
+	}{
+		{
+			name:         "no changes",
+			existingTags: map[string]string{"a": "1"},
+			metaTags:     map[string]string{"a": "1"},
+			expectUntag:  false,
+			expectTag:    false,
+			assertsTag: func(input *awssm.TagResourceInput) {
+				assert.Fail(t, "Expected TagResource to not be called")
+			},
+			assertsUntag: func(input *awssm.UntagResourceInput) {
+				assert.Fail(t, "Expected UntagResource to not be called")
+			},
+		},
+		{
+			name:         "update tag value",
+			existingTags: map[string]string{"a": "1"},
+			metaTags:     map[string]string{"a": "2"},
+			expectUntag:  false,
+			expectTag:    true,
+			assertsTag: func(input *awssm.TagResourceInput) {
+				assert.Contains(t, input.Tags, types.Tag{Key: new(managedBy), Value: new(externalSecrets)})
+				assert.Contains(t, input.Tags, types.Tag{Key: new("a"), Value: new("2")})
+			},
+			assertsUntag: func(input *awssm.UntagResourceInput) {
+				assert.Fail(t, "Expected UntagResource to not be called")
+			},
+		},
+		{
+			name:         "remove tag",
+			existingTags: map[string]string{"a": "1", "b": "2"},
+			metaTags:     map[string]string{"a": "1"},
+			expectUntag:  true,
+			expectTag:    false,
+			assertsTag: func(input *awssm.TagResourceInput) {
+				assert.Fail(t, "Expected TagResource to not be called")
+			},
+			assertsUntag: func(input *awssm.UntagResourceInput) {
+				assert.Equal(t, []string{"b"}, input.TagKeys)
+			},
+		},
+		{
+			name:         "add tags",
+			existingTags: map[string]string{"a": "1"},
+			metaTags:     map[string]string{"a": "1", "b": "2"},
+			expectUntag:  false,
+			expectTag:    true,
+			assertsTag: func(input *awssm.TagResourceInput) {
+				assert.Contains(t, input.Tags, types.Tag{Key: new(managedBy), Value: new(externalSecrets)})
+				assert.Contains(t, input.Tags, types.Tag{Key: new("a"), Value: new("1")})
+				assert.Contains(t, input.Tags, types.Tag{Key: new("b"), Value: new("2")})
+			},
+			assertsUntag: func(input *awssm.UntagResourceInput) {
+				assert.Fail(t, "Expected UntagResource to not be called")
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			calls := call{}
+			fakeClient := &fakesm.Client{
+				TagResourceFn: fakesm.NewTagResourceFn(&awssm.TagResourceOutput{}, nil, func(input *awssm.TagResourceInput) {
+					tt.assertsTag(input)
+					calls.tagCalled = true
+				}),
+				UntagResourceFn: fakesm.NewUntagResourceFn(&awssm.UntagResourceOutput{}, nil, func(input *awssm.UntagResourceInput) {
+					tt.assertsUntag(input)
+					calls.untagCalled = true
+				}),
+			}
+
+			sm := &SecretsManager{client: fakeClient}
+			metaMap := map[string]any{
+				"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+				"kind":       "PushSecretMetadata",
+				"spec": map[string]any{
+					"description": "adding managed-by tag explicitly",
+					"tags":        tt.metaTags,
+				},
+			}
+			raw, err := json.Marshal(metaMap)
+			require.NoError(t, err)
+			meta := &apiextensionsv1.JSON{Raw: raw}
+
+			secretId := "secret"
+			err = sm.patchTags(context.Background(), meta, &secretId, tt.existingTags)
+			require.NoError(t, err)
+			assert.Equal(t, tt.expectUntag, calls.untagCalled)
+			assert.Equal(t, tt.expectTag, calls.tagCalled)
+		})
+	}
+}
+
+// FakeCredProvider implements the AWS credentials.Provider interface
+// It is used to inject an error into the AWS config to cause a
+// validation error.
+type FakeCredProvider struct {
+	retrieveFunc func() (aws.Credentials, error)
+}
+
+func (f *FakeCredProvider) Retrieve(ctx context.Context) (aws.Credentials, error) {
+	return f.retrieveFunc()
+}
+
+func (f *FakeCredProvider) IsExpired() bool {
+	return true
+}

+ 247 - 0
providers/v2/aws/store/store.go

@@ -0,0 +1,247 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package store implements AWS provider interfaces for External Secrets Operator,
+// supporting SecretManager and ParameterStore services.
+package store
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/aws/retry"
+	"github.com/aws/aws-sdk-go-v2/config"
+	awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
+	"github.com/aws/aws-sdk-go-v2/service/ssm"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	"github.com/external-secrets/external-secrets/providers/v2/aws/store/auth"
+	"github.com/external-secrets/external-secrets/providers/v2/aws/store/parameterstore"
+	"github.com/external-secrets/external-secrets/providers/v2/aws/store/secretsmanager"
+	awsutil "github.com/external-secrets/external-secrets/providers/v2/aws/store/util"
+	"github.com/external-secrets/external-secrets/runtime/esutils"
+)
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1.ProviderInterface = &Provider{}
+
+// Provider satisfies the provider interface.
+type Provider struct{}
+
+const (
+	errUnableCreateSession    = "unable to create session: %w"
+	errUnknownProviderService = "unknown AWS Provider Service: %s"
+	errRegionNotFound         = "region not found: %s"
+	errInitAWSProvider        = "unable to initialize aws provider: %s"
+	errInvalidSecretsManager  = "invalid SecretsManager settings: %s"
+)
+
+// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
+func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
+	return esv1.SecretStoreReadWrite
+}
+
+// NewClient constructs a new secrets client based on the provided store.
+func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube client.Client, namespace string) (esv1.SecretsClient, error) {
+	return newClient(ctx, store, kube, namespace, auth.DefaultSTSProvider)
+}
+
+// ValidateStore validates the configuration of the AWS SecretStore.
+func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
+	prov, err := awsutil.GetAWSProvider(store)
+	if err != nil {
+		return nil, err
+	}
+	err = validateRegion(prov)
+	if err != nil {
+		return nil, err
+	}
+	err = validateSecretsManagerConfig(prov)
+	if err != nil {
+		return nil, err
+	}
+
+	// case: static credentials
+	if prov.Auth.SecretRef != nil {
+		if err := esutils.ValidateReferentSecretSelector(store, prov.Auth.SecretRef.AccessKeyID); err != nil {
+			return nil, fmt.Errorf("invalid Auth.SecretRef.AccessKeyID: %w", err)
+		}
+		if err := esutils.ValidateReferentSecretSelector(store, prov.Auth.SecretRef.SecretAccessKey); err != nil {
+			return nil, fmt.Errorf("invalid Auth.SecretRef.SecretAccessKey: %w", err)
+		}
+		if prov.Auth.SecretRef.SessionToken != nil {
+			if err := esutils.ValidateReferentSecretSelector(store, *prov.Auth.SecretRef.SessionToken); err != nil {
+				return nil, fmt.Errorf("invalid Auth.SecretRef.SessionToken: %w", err)
+			}
+		}
+	}
+
+	// case: jwt credentials
+	if prov.Auth.JWTAuth != nil && prov.Auth.JWTAuth.ServiceAccountRef != nil {
+		if err := esutils.ValidateReferentServiceAccountSelector(store, *prov.Auth.JWTAuth.ServiceAccountRef); err != nil {
+			return nil, fmt.Errorf("invalid Auth.JWT.ServiceAccountRef: %w", err)
+		}
+	}
+
+	return nil, nil
+}
+
+func validateRegion(prov *esv1.AWSProvider) error {
+	switch prov.Service {
+	case esv1.AWSServiceSecretsManager:
+		resolver := awssm.NewDefaultEndpointResolverV2()
+		_, err := resolver.ResolveEndpoint(context.TODO(), awssm.EndpointParameters{
+			Region: &prov.Region,
+		})
+		if err != nil {
+			return fmt.Errorf(errRegionNotFound, prov.Region)
+		}
+		return nil
+	case esv1.AWSServiceParameterStore:
+		resolver := ssm.NewDefaultEndpointResolverV2()
+		_, err := resolver.ResolveEndpoint(context.TODO(), ssm.EndpointParameters{
+			Region: &prov.Region,
+		})
+		if err != nil {
+			return fmt.Errorf(errRegionNotFound, prov.Region)
+		}
+		return nil
+	}
+	return fmt.Errorf(errUnknownProviderService, prov.Service)
+}
+
+func validateSecretsManagerConfig(prov *esv1.AWSProvider) error {
+	if prov.SecretsManager == nil {
+		return nil
+	}
+	return awsutil.ValidateDeleteSecretInput(awssm.DeleteSecretInput{
+		ForceDeleteWithoutRecovery: &prov.SecretsManager.ForceDeleteWithoutRecovery,
+		RecoveryWindowInDays:       &prov.SecretsManager.RecoveryWindowInDays,
+	})
+}
+
+func newClient(ctx context.Context, store esv1.GenericStore, kube client.Client, namespace string, assumeRoler auth.STSProvider) (esv1.SecretsClient, error) {
+	prov, err := awsutil.GetAWSProvider(store)
+	if err != nil {
+		return nil, err
+	}
+	if store == nil {
+		return nil, fmt.Errorf(errInitAWSProvider, "nil store")
+	}
+	storeSpec := store.GetSpec()
+	var cfg *aws.Config
+
+	// allow SecretStore controller validation to pass
+	// when using referent namespace.
+	if awsutil.IsReferentSpec(prov.Auth) && namespace == "" &&
+		store.GetObjectKind().GroupVersionKind().Kind == esv1.ClusterSecretStoreKind {
+		cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("eu-west-1"))
+		if err != nil {
+			return nil, fmt.Errorf(errInitAWSProvider, err)
+		}
+		switch prov.Service {
+		case esv1.AWSServiceSecretsManager:
+			return secretsmanager.New(ctx, &cfg, prov.SecretsManager, storeSpec.Provider.AWS.Prefix, true, kube, namespace)
+		case esv1.AWSServiceParameterStore:
+			return parameterstore.New(ctx, &cfg, storeSpec.Provider.AWS.Prefix, true)
+		}
+		return nil, fmt.Errorf(errUnknownProviderService, prov.Service)
+	}
+
+	cfg, err = auth.New(ctx, auth.Opts{
+		Store:       store,
+		Kube:        kube,
+		Namespace:   namespace,
+		AssumeRoler: assumeRoler,
+		JWTProvider: auth.DefaultJWTProvider,
+	})
+	if err != nil {
+		return nil, fmt.Errorf(errUnableCreateSession, err)
+	}
+
+	// Setup retry options, if present in storeSpec
+	if storeSpec.RetrySettings != nil {
+		var retryAmount int
+		var retryDuration time.Duration
+
+		if storeSpec.RetrySettings.MaxRetries != nil {
+			retryAmount = int(*storeSpec.RetrySettings.MaxRetries)
+		} else {
+			retryAmount = 3
+		}
+
+		if storeSpec.RetrySettings.RetryInterval != nil {
+			retryDuration, err = time.ParseDuration(*storeSpec.RetrySettings.RetryInterval)
+		}
+		if err != nil {
+			return nil, fmt.Errorf(errInitAWSProvider, err)
+		}
+		// awsRetryer := awsclient.DefaultRetryer{
+		// 	NumMaxRetries:    retryAmount,
+		// 	MinRetryDelay:    retryDuration,
+		// 	MaxThrottleDelay: 120 * time.Second,  Not sure how to set this in sdk go v2
+		// }
+
+		cfg.Retryer = func() aws.Retryer {
+			return retry.AddWithMaxAttempts(
+				retry.NewStandard(func(o *retry.StandardOptions) {
+					if retryDuration > 0 {
+						o.Backoff = fixedDelayer{delay: retryDuration}
+					}
+				}),
+				retryAmount,
+			)
+		}
+	}
+
+	switch prov.Service {
+	case esv1.AWSServiceSecretsManager:
+		return secretsmanager.New(ctx, cfg, prov.SecretsManager, storeSpec.Provider.AWS.Prefix, false, kube, namespace)
+	case esv1.AWSServiceParameterStore:
+		return parameterstore.New(ctx, cfg, storeSpec.Provider.AWS.Prefix, false)
+	}
+	return nil, fmt.Errorf(errUnknownProviderService, prov.Service)
+}
+
+// Add this type at package level.
+type fixedDelayer struct {
+	delay time.Duration
+}
+
+func (f fixedDelayer) BackoffDelay(int, error) (time.Duration, error) {
+	return f.delay, nil
+}
+
+// NewProvider creates a new AWS Provider instance.
+func NewProvider() esv1.ProviderInterface {
+	return &Provider{}
+}
+
+// ProviderSpec returns the provider specification for registration.
+func ProviderSpec() *esv1.SecretStoreProvider {
+	return &esv1.SecretStoreProvider{
+		AWS: &esv1.AWSProvider{},
+	}
+}
+
+// MaintenanceStatus returns the maintenance status of the provider.
+func MaintenanceStatus() esv1.MaintenanceStatus {
+	return esv1.MaintenanceStatusMaintained
+}

+ 542 - 0
providers/v2/aws/store/store_test.go

@@ -0,0 +1,542 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package store
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	awsauth "github.com/external-secrets/external-secrets/providers/v2/aws/store/auth"
+	"github.com/external-secrets/external-secrets/providers/v2/aws/store/parameterstore"
+	"github.com/external-secrets/external-secrets/providers/v2/aws/store/secretsmanager"
+)
+
+func TestProvider(t *testing.T) {
+	cl := clientfake.NewClientBuilder().Build()
+	p := Provider{}
+
+	// inject fake static credentials because we test
+	// if we are able to get credentials when constructing the client
+	// see #415
+	t.Setenv("AWS_ACCESS_KEY_ID", "1234")
+	t.Setenv("AWS_SECRET_ACCESS_KEY", "1234")
+
+	tbl := []struct {
+		test    string
+		store   esv1.GenericStore
+		expType any
+		expErr  bool
+	}{
+		{
+			test:   "should not create provider due to nil store",
+			store:  nil,
+			expErr: true,
+		},
+		{
+			test:   "should not create provider due to missing provider",
+			expErr: true,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{},
+			},
+		},
+		{
+			test:   "should not create provider due to missing provider field",
+			expErr: true,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{},
+				},
+			},
+		},
+		{
+			test:    "should create parameter store client",
+			expErr:  false,
+			expType: &parameterstore.ParameterStore{},
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{
+							Service: esv1.AWSServiceParameterStore,
+						},
+					},
+				},
+			},
+		},
+		{
+			test:    "should create secretsmanager client",
+			expErr:  false,
+			expType: &secretsmanager.SecretsManager{},
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{
+							Service: esv1.AWSServiceSecretsManager,
+						},
+					},
+				},
+			},
+		},
+		{
+			test:   "invalid service should return an error",
+			expErr: true,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{
+							Service: "HIHIHIHHEHEHEHEHEHE",
+						},
+					},
+				},
+			},
+		},
+		{
+			test:   "newSession error should be returned",
+			expErr: true,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{
+							Service: esv1.AWSServiceParameterStore,
+							Auth: esv1.AWSAuth{
+								SecretRef: &esv1.AWSAuthSecretRef{
+									AccessKeyID: esmeta.SecretKeySelector{
+										Name:      "foo",
+										Namespace: aws.String("NOOP"),
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	for i := range tbl {
+		row := tbl[i]
+		t.Run(row.test, func(t *testing.T) {
+			sc, err := p.NewClient(context.TODO(), row.store, cl, "foo")
+			if row.expErr {
+				assert.Error(t, err)
+				assert.Nil(t, sc)
+			} else {
+				assert.Nil(t, err)
+				assert.NotNil(t, sc)
+				assert.IsType(t, row.expType, sc)
+			}
+		})
+	}
+}
+
+const (
+	validRegion                  = "eu-central-1"
+	validFipsSecretManagerRegion = "us-east-1-fips"
+	validFipsSsmRegion           = "fips-us-east-1"
+)
+
+func TestValidateStore(t *testing.T) {
+	type args struct {
+		store esv1.GenericStore
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name:    "invalid region",
+			wantErr: true,
+			args: args{
+				store: &esv1.SecretStore{
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region: "noop.",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "valid region secrets manager",
+			args: args{
+				store: &esv1.SecretStore{
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1.AWSServiceSecretsManager,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "valid fips region secrets manager",
+			args: args{
+				store: &esv1.SecretStore{
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validFipsSecretManagerRegion,
+								Service: esv1.AWSServiceSecretsManager,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "valid fips region parameter store",
+			args: args{
+				store: &esv1.SecretStore{
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validFipsSsmRegion,
+								Service: esv1.AWSServiceParameterStore,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "valid secretsmanager config: force delete without recovery",
+			args: args{
+				store: &esv1.SecretStore{
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1.AWSServiceSecretsManager,
+								SecretsManager: &esv1.SecretsManager{
+									ForceDeleteWithoutRecovery: true,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "valid secretsmanager config: recovery window",
+			args: args{
+				store: &esv1.SecretStore{
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1.AWSServiceSecretsManager,
+								SecretsManager: &esv1.SecretsManager{
+									RecoveryWindowInDays: 30,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid static creds auth / AccessKeyID",
+			wantErr: true,
+			args: args{
+				store: &esv1.SecretStore{
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1.AWSServiceSecretsManager,
+								Auth: esv1.AWSAuth{
+									SecretRef: &esv1.AWSAuthSecretRef{
+										AccessKeyID: esmeta.SecretKeySelector{
+											Name:      "foobar",
+											Namespace: new("unacceptable"),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid static creds auth / SecretAccessKey",
+			wantErr: true,
+			args: args{
+				store: &esv1.SecretStore{
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1.AWSServiceSecretsManager,
+								Auth: esv1.AWSAuth{
+									SecretRef: &esv1.AWSAuthSecretRef{
+										SecretAccessKey: esmeta.SecretKeySelector{
+											Name:      "foobar",
+											Namespace: new("unacceptable"),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "referentAuth static creds / SecretAccessKey without namespace",
+			wantErr: false,
+			args: args{
+				store: &esv1.ClusterSecretStore{
+					TypeMeta: v1.TypeMeta{
+						Kind: esv1.ClusterSecretStoreKind,
+					},
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1.AWSServiceSecretsManager,
+								Auth: esv1.AWSAuth{
+									SecretRef: &esv1.AWSAuthSecretRef{
+										SecretAccessKey: esmeta.SecretKeySelector{
+											Name: "foobar",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "referentAuth static creds / AccessKeyID without namespace",
+			wantErr: false,
+			args: args{
+				store: &esv1.ClusterSecretStore{
+					TypeMeta: v1.TypeMeta{
+						Kind: esv1.ClusterSecretStoreKind,
+					},
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1.AWSServiceSecretsManager,
+								Auth: esv1.AWSAuth{
+									SecretRef: &esv1.AWSAuthSecretRef{
+										AccessKeyID: esmeta.SecretKeySelector{
+											Name: "foobar",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "referentAuth jwt: sa selector without namespace",
+			wantErr: false,
+			args: args{
+				store: &esv1.ClusterSecretStore{
+					TypeMeta: v1.TypeMeta{
+						Kind: esv1.ClusterSecretStoreKind,
+					},
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1.AWSServiceSecretsManager,
+								Auth: esv1.AWSAuth{
+									JWTAuth: &esv1.AWSJWTAuth{
+										ServiceAccountRef: &esmeta.ServiceAccountSelector{
+											Name: "foobar",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid jwt auth: not allowed sa selector namespace",
+			wantErr: true,
+			args: args{
+				store: &esv1.SecretStore{
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1.AWSServiceSecretsManager,
+								Auth: esv1.AWSAuth{
+									JWTAuth: &esv1.AWSJWTAuth{
+										ServiceAccountRef: &esmeta.ServiceAccountSelector{
+											Name:      "foobar",
+											Namespace: new("unacceptable"),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid SecretsManager config: conflicting settings",
+			wantErr: true,
+			args: args{
+				store: &esv1.SecretStore{
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1.AWSServiceSecretsManager,
+								SecretsManager: &esv1.SecretsManager{
+									ForceDeleteWithoutRecovery: true,
+									RecoveryWindowInDays:       7,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid SecretsManager config: recovery window too small",
+			wantErr: true,
+			args: args{
+				store: &esv1.SecretStore{
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1.AWSServiceSecretsManager,
+								SecretsManager: &esv1.SecretsManager{
+									RecoveryWindowInDays: 6,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid SecretsManager config: recovery window too big",
+			wantErr: true,
+			args: args{
+				store: &esv1.SecretStore{
+					Spec: esv1.SecretStoreSpec{
+						Provider: &esv1.SecretStoreProvider{
+							AWS: &esv1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1.AWSServiceSecretsManager,
+								SecretsManager: &esv1.SecretsManager{
+									RecoveryWindowInDays: 31,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := &Provider{}
+			if _, err := p.ValidateStore(tt.args.store); (err != nil) != tt.wantErr {
+				t.Errorf("Provider.ValidateStore() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestValidRetryInput(t *testing.T) {
+	invalid := "Invalid"
+	spec := &esv1.SecretStore{
+		Spec: esv1.SecretStoreSpec{
+			Provider: &esv1.SecretStoreProvider{
+				AWS: &esv1.AWSProvider{
+					Service: "ParameterStore",
+					Region:  validRegion,
+					Auth: esv1.AWSAuth{
+						SecretRef: &esv1.AWSAuthSecretRef{
+							SecretAccessKey: esmeta.SecretKeySelector{
+								Name: "creds",
+								Key:  "sak",
+							},
+							AccessKeyID: esmeta.SecretKeySelector{
+								Name: "creds",
+								Key:  "ak",
+							},
+						},
+					},
+				},
+			},
+			RetrySettings: &esv1.SecretStoreRetrySettings{
+				RetryInterval: &invalid,
+			},
+		},
+	}
+
+	expected := fmt.Sprintf("unable to initialize aws provider: time: invalid duration %q", invalid)
+	ctx := context.TODO()
+
+	kube := clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
+		ObjectMeta: v1.ObjectMeta{
+			Name:      "creds",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"sak": []byte("OK"),
+			"ak":  []byte("OK"),
+		},
+	}).Build()
+	provider := func(*aws.Config) awsauth.STSprovider { return nil }
+
+	_, err := newClient(ctx, spec, kube, "default", provider)
+
+	if !ErrorContains(err, expected) {
+		t.Errorf("CheckValidRetryInput unexpected error: %s, expected: '%s'", err.Error(), expected)
+	}
+}
+
+func ErrorContains(out error, want string) bool {
+	if out == nil {
+		return want == ""
+	}
+	if want == "" {
+		return false
+	}
+	return strings.Contains(out.Error(), want)
+}

+ 37 - 0
providers/v2/aws/store/util/errors.go

@@ -0,0 +1,37 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package awsutil provides utility functions for AWS providers in External Secrets Operator
+package awsutil
+
+import (
+	"errors"
+	"regexp"
+)
+
+var regexReqIDs = []*regexp.Regexp{
+	regexp.MustCompile(`request id: (\S+)`),
+	regexp.MustCompile(` Credential=.+`),
+}
+
+// SanitizeErr sanitizes the error string.
+func SanitizeErr(err error) error {
+	msg := err.Error()
+	for _, regex := range regexReqIDs {
+		msg = string(regex.ReplaceAll([]byte(msg), nil))
+	}
+	return errors.New(msg)
+}

+ 53 - 0
providers/v2/aws/store/util/errors_test.go

@@ -0,0 +1,53 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package awsutil
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSanitize(t *testing.T) {
+	tbl := []struct {
+		err      error
+		expected string
+	}{
+		{
+			err: errors.New(
+				"some AccessDeniedException: User: arn:aws:sts::123123123123:assumed-role/foobar is not authorized to perform: secretsmanager:GetSecretValue on resource: example\n\tstatus code: 400, request id: df34-75f-0c5f-4b4c-a71a-f93d581d177c",
+			),
+			expected: "some AccessDeniedException: User: arn:aws:sts::123123123123:assumed-role/foobar is not authorized to perform: secretsmanager:GetSecretValue on resource: example\n\tstatus code: 400, ",
+		},
+		{
+			err: errors.New(
+				"IncompleteSignature: 'something' not a valid key=value pair (missing equal-sign) in Authorization header: 'AWS4-HMAC-SHA256 Credential=You,Can Get\"Almost{Anything}Here', SignedHeaders=content-length;content-type;host;x-amz-date, Signature=42ee80d90508ee472701f8fb7014f10c0ac16b6d6ac59379f0612ca2d35d7464'",
+			),
+			expected: "IncompleteSignature: 'something' not a valid key=value pair (missing equal-sign) in Authorization header: 'AWS4-HMAC-SHA256",
+		},
+		{
+			err:      errors.New("some generic error"),
+			expected: "some generic error",
+		},
+	}
+
+	for _, c := range tbl {
+		out := SanitizeErr(c.err)
+		assert.Equal(t, c.expected, out.Error())
+	}
+}

+ 107 - 0
providers/v2/aws/store/util/provider.go

@@ -0,0 +1,107 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package awsutil
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+const (
+	errNilStore         = "found nil store"
+	errMissingStoreSpec = "store is missing spec"
+	errMissingProvider  = "storeSpec is missing provider"
+	errInvalidProvider  = "invalid provider spec. Missing AWS field in store %s"
+)
+
+// GetAWSProvider does the necessary nil checks on the generic store
+// it returns the aws provider or an error.
+func GetAWSProvider(store esv1.GenericStore) (*esv1.AWSProvider, error) {
+	if store == nil {
+		return nil, errors.New(errNilStore)
+	}
+	spc := store.GetSpec()
+	if spc == nil {
+		return nil, errors.New(errMissingStoreSpec)
+	}
+	if spc.Provider == nil {
+		return nil, errors.New(errMissingProvider)
+	}
+	prov := spc.Provider.AWS
+	if prov == nil {
+		return nil, fmt.Errorf(errInvalidProvider, store.GetObjectMeta().String())
+	}
+	return prov, nil
+}
+
+// IsReferentSpec checks if the AWS authentication configuration refers to resources in a different namespace.
+func IsReferentSpec(prov esv1.AWSAuth) bool {
+	if prov.JWTAuth != nil && prov.JWTAuth.ServiceAccountRef != nil && prov.JWTAuth.ServiceAccountRef.Namespace == nil {
+		return true
+	}
+	if prov.SecretRef != nil &&
+		(prov.SecretRef.AccessKeyID.Namespace == nil ||
+			prov.SecretRef.SecretAccessKey.Namespace == nil ||
+			(prov.SecretRef.SessionToken != nil && prov.SecretRef.SessionToken.Namespace == nil)) {
+		return true
+	}
+
+	return false
+}
+
+// SecretTagsToJSONString converts AWS Secrets Manager tags to a JSON string.
+func SecretTagsToJSONString(tags []awssm.Tag) (string, error) {
+	tagMap := make(map[string]string, len(tags))
+	for _, tag := range tags {
+		tagMap[*tag.Key] = *tag.Value
+	}
+
+	byteArr, err := json.Marshal(tagMap)
+	if err != nil {
+		return "", err
+	}
+
+	return string(byteArr), nil
+}
+
+// ParameterTagsToJSONString converts parameter tags map to a JSON string.
+func ParameterTagsToJSONString(tags map[string]string) (string, error) {
+	byteArr, err := json.Marshal(tags)
+	if err != nil {
+		return "", err
+	}
+
+	return string(byteArr), nil
+}
+
+// FindTagKeysToRemove returns a slice of tag keys that exist in the current tags
+// but are not present in the desired metaTags. These keys should be removed to
+// synchronize the tags with the desired state.
+func FindTagKeysToRemove(tags, metaTags map[string]string) []string {
+	var diff []string
+	for key := range tags {
+		if _, ok := metaTags[key]; !ok {
+			diff = append(diff, key)
+		}
+	}
+	return diff
+}

+ 135 - 0
providers/v2/aws/store/util/provider_test.go

@@ -0,0 +1,135 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package awsutil
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParameterTagsToJSONString(t *testing.T) {
+	tests := []struct {
+		name     string
+		tags     map[string]string
+		expected string
+		wantErr  bool
+	}{
+		{
+			name: "Valid tags",
+			tags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+			expected: `{"key1":"value1","key2":"value2"}`,
+			wantErr:  false,
+		},
+		{
+			name:     "Empty tags",
+			tags:     map[string]string{},
+			expected: `{}`,
+			wantErr:  false,
+		},
+		{
+			name:     "Nil tags",
+			tags:     nil,
+			wantErr:  false,
+			expected: "null",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result, err := ParameterTagsToJSONString(tt.tags)
+			if tt.wantErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				var resultMap map[string]string
+				err := json.Unmarshal([]byte(result), &resultMap)
+				assert.NoError(t, err)
+				assert.Equal(t, tt.expected, result)
+			}
+		})
+	}
+}
+
+func TestFindTagKeysToRemove(t *testing.T) {
+	tests := []struct {
+		name     string
+		tags     map[string]string
+		metaTags map[string]string
+		expected []string
+	}{
+		{
+			name: "No tags to remove",
+			tags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+			metaTags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+			expected: []string{},
+		},
+		{
+			name: "Some tags to remove",
+			tags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+				"key3": "value3",
+			},
+			metaTags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+			expected: []string{"key3"},
+		},
+		{
+			name: "All tags to remove",
+			tags: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+			metaTags: map[string]string{},
+			expected: []string{"key1", "key2"},
+		},
+		{
+			name:     "Empty tags and metaTags",
+			tags:     map[string]string{},
+			metaTags: map[string]string{},
+			expected: []string{},
+		},
+		{
+			name: "Empty metaTags with non-empty tags",
+			tags: map[string]string{
+				"key1": "value1",
+			},
+			metaTags: map[string]string{},
+			expected: []string{"key1"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := FindTagKeysToRemove(tt.tags, tt.metaTags)
+			assert.ElementsMatch(t, tt.expected, result)
+		})
+	}
+}

+ 45 - 0
providers/v2/aws/store/util/validation.go

@@ -0,0 +1,45 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package awsutil provides utility functions for AWS provider integration
+package awsutil
+
+import (
+	"fmt"
+
+	awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
+)
+
+const (
+	errInvalidDeleteSecretInput = "invalid DeleteSecretInput: %s"
+)
+
+// ValidateDeleteSecretInput validates the DeleteSecretInput.
+// The AWS sdk v2 does not validate the input before making the API call, leaving it to the API to return the error.
+// This function allows one to validate the input before any such call is made.
+// See: https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/secretsmanager#DeleteSecretInput
+func ValidateDeleteSecretInput(input awssm.DeleteSecretInput) error {
+	// Validate range for RecoveryWindowInDays
+	// See: https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/secretsmanager#DeleteSecretInput
+	if input.RecoveryWindowInDays != nil && *input.RecoveryWindowInDays != 0 && (*input.RecoveryWindowInDays < 7 || *input.RecoveryWindowInDays > 30) {
+		return fmt.Errorf(errInvalidDeleteSecretInput, "RecoveryWindowInDays must be between 7 and 30 days")
+	}
+	// Validate that ForceDeleteWithoutRecovery is not set when RecoveryWindowInDays is set
+	if input.RecoveryWindowInDays != nil && *input.RecoveryWindowInDays != 0 && input.ForceDeleteWithoutRecovery != nil && *input.ForceDeleteWithoutRecovery {
+		return fmt.Errorf(errInvalidDeleteSecretInput, "ForceDeleteWithoutRecovery conflicts with RecoveryWindowInDays")
+	}
+	return nil
+}