Parcourir la source

test: migrate aws provider v2 suites

Moritz Johner il y a 2 mois
Parent
commit
ccb4c5ec11
21 fichiers modifiés avec 2407 ajouts et 94 suppressions
  1. 4 4
      e2e/suites/provider/cases/aws/common.go
  2. 145 0
      e2e/suites/provider/cases/aws/common_test.go
  3. 109 0
      e2e/suites/provider/cases/aws/parameterstore/clusterprovider_v2.go
  4. 3 4
      e2e/suites/provider/cases/aws/parameterstore/parameterstore.go
  5. 3 4
      e2e/suites/provider/cases/aws/parameterstore/parameterstore_managed.go
  6. 5 6
      e2e/suites/provider/cases/aws/parameterstore/provider.go
  7. 343 0
      e2e/suites/provider/cases/aws/parameterstore/provider_support_v2.go
  8. 56 0
      e2e/suites/provider/cases/aws/parameterstore/provider_support_v2_test.go
  9. 121 0
      e2e/suites/provider/cases/aws/parameterstore/provider_v2.go
  10. 72 0
      e2e/suites/provider/cases/aws/parameterstore/provider_v2_test.go
  11. 153 0
      e2e/suites/provider/cases/aws/parameterstore/push_v2.go
  12. 109 0
      e2e/suites/provider/cases/aws/secretsmanager/clusterprovider_v2.go
  13. 20 68
      e2e/suites/provider/cases/aws/secretsmanager/provider.go
  14. 484 0
      e2e/suites/provider/cases/aws/secretsmanager/provider_support.go
  15. 211 0
      e2e/suites/provider/cases/aws/secretsmanager/provider_support_test.go
  16. 181 0
      e2e/suites/provider/cases/aws/secretsmanager/provider_v2.go
  17. 162 0
      e2e/suites/provider/cases/aws/secretsmanager/push_v2.go
  18. 3 4
      e2e/suites/provider/cases/aws/secretsmanager/secretsmanager.go
  19. 3 4
      e2e/suites/provider/cases/aws/secretsmanager/secretsmanager_managed.go
  20. 88 0
      e2e/suites/provider/cases/aws/secretsmanager/secretsmanager_v2_managed.go
  21. 132 0
      e2e/suites/provider/cases/aws/v2_support.go

+ 4 - 4
e2e/suites/provider/cases/aws/common.go

@@ -17,16 +17,16 @@ limitations under the License.
 package common
 
 import (
-
-	// nolint
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/external-secrets/external-secrets-e2e/framework"
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	esmetav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+
+	// nolint
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
 )
 
 const (

+ 145 - 0
e2e/suites/provider/cases/aws/common_test.go

@@ -0,0 +1,145 @@
+/*
+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 common
+
+import (
+	"strings"
+	"testing"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+func TestCredentialsSecretName(t *testing.T) {
+	t.Parallel()
+
+	if got := CredentialsSecretName("aws-config"); got != "aws-config-credentials" {
+		t.Fatalf("unexpected credentials secret name: %q", got)
+	}
+}
+
+func TestStaticCredentialsSecretDataPreservesSessionToken(t *testing.T) {
+	t.Parallel()
+
+	got := StaticCredentialsSecretData("kid", "sak", "st")
+	if got[StaticAccessKeyIDKey] != "kid" {
+		t.Fatalf("unexpected access key id: %q", got[StaticAccessKeyIDKey])
+	}
+	if got[StaticSecretAccessKeyKey] != "sak" {
+		t.Fatalf("unexpected secret access key: %q", got[StaticSecretAccessKeyKey])
+	}
+	if got[StaticSessionTokenKey] != "st" {
+		t.Fatalf("unexpected session token: %q", got[StaticSessionTokenKey])
+	}
+}
+
+func TestProviderConfigNamespaceForManifestScope(t *testing.T) {
+	t.Parallel()
+
+	if got := ProviderConfigNamespace(esv1.AuthenticationScopeManifestNamespace, "provider-ns", "workload-ns"); got != "workload-ns" {
+		t.Fatalf("expected workload namespace, got %q", got)
+	}
+}
+
+func TestProviderConfigNamespaceForProviderScope(t *testing.T) {
+	t.Parallel()
+
+	if got := ProviderConfigNamespace(esv1.AuthenticationScopeProviderNamespace, "provider-ns", "workload-ns"); got != "provider-ns" {
+		t.Fatalf("expected provider namespace, got %q", got)
+	}
+}
+
+func TestProviderReferenceNamespaceForManifestScope(t *testing.T) {
+	t.Parallel()
+
+	if got := ProviderReferenceNamespace(esv1.AuthenticationScopeManifestNamespace, "provider-ns"); got != "" {
+		t.Fatalf("expected empty provider reference namespace, got %q", got)
+	}
+}
+
+func TestProviderReferenceNamespaceForProviderScope(t *testing.T) {
+	t.Parallel()
+
+	if got := ProviderReferenceNamespace(esv1.AuthenticationScopeProviderNamespace, "provider-ns"); got != "provider-ns" {
+		t.Fatalf("expected provider namespace, got %q", got)
+	}
+}
+
+func TestNewV2ClusterProviderScenarioManifestScope(t *testing.T) {
+	t.Parallel()
+
+	called := false
+	got := NewV2ClusterProviderScenario("workload-ns", "case", esv1.AuthenticationScopeManifestNamespace, func(prefix string) string {
+		called = true
+		return prefix + "-provider"
+	})
+	if called {
+		t.Fatal("expected provider namespace factory to be unused for manifest scope")
+	}
+	if got.ConfigName != "case-config" {
+		t.Fatalf("unexpected config name: %q", got.ConfigName)
+	}
+	if got.ConfigNamespace != "workload-ns" {
+		t.Fatalf("unexpected config namespace: %q", got.ConfigNamespace)
+	}
+	if got.ProviderNamespace != "workload-ns" {
+		t.Fatalf("unexpected provider namespace: %q", got.ProviderNamespace)
+	}
+	if got.ProviderRefNamespace != "" {
+		t.Fatalf("expected empty provider reference namespace, got %q", got.ProviderRefNamespace)
+	}
+	if got.WorkloadNamespace != "workload-ns" {
+		t.Fatalf("unexpected workload namespace: %q", got.WorkloadNamespace)
+	}
+	if got.NamePrefix != "workload-ns-case" {
+		t.Fatalf("unexpected name prefix: %q", got.NamePrefix)
+	}
+}
+
+func TestNewV2ClusterProviderScenarioProviderScope(t *testing.T) {
+	t.Parallel()
+
+	var gotPrefix string
+	got := NewV2ClusterProviderScenario("workload-ns", "case", esv1.AuthenticationScopeProviderNamespace, func(prefix string) string {
+		gotPrefix = prefix
+		return "provider-ns"
+	})
+	if gotPrefix != "case-provider" {
+		t.Fatalf("unexpected provider namespace prefix: %q", gotPrefix)
+	}
+	if got.ConfigNamespace != "provider-ns" {
+		t.Fatalf("unexpected config namespace: %q", got.ConfigNamespace)
+	}
+	if got.ProviderNamespace != "provider-ns" {
+		t.Fatalf("unexpected provider namespace: %q", got.ProviderNamespace)
+	}
+	if got.ProviderRefNamespace != "provider-ns" {
+		t.Fatalf("unexpected provider reference namespace: %q", got.ProviderRefNamespace)
+	}
+}
+
+func TestPushSecretMetadataWithRemoteNamespace(t *testing.T) {
+	t.Parallel()
+
+	got := PushSecretMetadataWithRemoteNamespace("target-ns")
+	if got == nil {
+		t.Fatal("expected metadata payload")
+	}
+	raw := string(got.Raw)
+	if !strings.Contains(raw, `"remoteNamespace":"target-ns"`) {
+		t.Fatalf("expected remote namespace in metadata, got %q", raw)
+	}
+}

+ 109 - 0
e2e/suites/provider/cases/aws/parameterstore/clusterprovider_v2.go

@@ -0,0 +1,109 @@
+/*
+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 aws
+
+import (
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+
+	. "github.com/onsi/ginkgo/v2"
+)
+
+var _ = Describe("[aws] v2 cluster provider", Label("aws", "parameterstore", "v2", "cluster-provider"), func() {
+	f := framework.New("eso-aws-ps-v2-clusterprovider")
+	prov := NewProviderV2(f)
+	harness := newAWSClusterProviderExternalSecretHarness(f, prov)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("cluster provider external secrets",
+		framework.TableFuncWithExternalSecret(f, prov),
+		Entry(common.ClusterProviderManifestNamespace(f, harness)),
+		Entry(common.ClusterProviderProviderNamespace(f, harness)),
+		Entry(common.ClusterProviderDeniedByConditions(f, harness)),
+	)
+})
+
+type awsClusterProviderScenario struct {
+	common    awscommon.V2ClusterProviderScenario
+	access    awsV2AccessConfig
+	authScope esv1.AuthenticationScope
+	backend   *parameterStoreBackend
+	f         *framework.Framework
+}
+
+func newAWSClusterProviderScenario(f *framework.Framework, prefix string, authScope esv1.AuthenticationScope, access awsV2AccessConfig, backend *parameterStoreBackend) *awsClusterProviderScenario {
+	shared := awscommon.NewV2ClusterProviderScenario(f.Namespace.Name, prefix, authScope, func(prefix string) string {
+		return common.CreateProviderCaseNamespace(f, prefix, defaultV2PollInterval)
+	})
+	s := &awsClusterProviderScenario{
+		common:    shared,
+		access:    access,
+		authScope: authScope,
+		backend:   backend,
+		f:         f,
+	}
+	createParameterStoreV2Config(s.f, s.common.ConfigNamespace, s.common.ConfigName, s.access)
+	return s
+}
+
+func (s *awsClusterProviderScenario) createClusterProvider(conditions []esv1.ClusterSecretStoreCondition) string {
+	clusterProviderName := s.common.ClusterProviderName()
+	frameworkv2.CreateClusterProviderConnection(
+		s.f,
+		clusterProviderName,
+		frameworkv2.ProviderAddress("aws"),
+		awsProviderAPIVersion,
+		awsv2alpha1.ParameterStoreKind,
+		s.common.ConfigName,
+		s.common.ProviderRefNamespace,
+		s.common.AuthScope,
+		conditions,
+	)
+	return clusterProviderName
+}
+
+func (s *awsClusterProviderScenario) CreateSecret(key string, val framework.SecretEntry) {
+	s.backend.CreateSecret(key, val)
+}
+
+func (s *awsClusterProviderScenario) DeleteSecret(key string) {
+	s.backend.DeleteSecret(key)
+}
+
+func newAWSClusterProviderExternalSecretHarness(f *framework.Framework, prov *ProviderV2) common.ClusterProviderExternalSecretHarness {
+	return common.ClusterProviderExternalSecretHarness{
+		Prepare: func(_ *framework.TestCase, cfg common.ClusterProviderConfig) *common.ClusterProviderExternalSecretRuntime {
+			s := newAWSClusterProviderScenario(f, cfg.Name, cfg.AuthScope, prov.access, prov.backend)
+			clusterProviderName := s.createClusterProvider(cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.ClusterProviderExternalSecretRuntime{
+				ClusterProviderName: clusterProviderName,
+				Provider:            s,
+			}
+		},
+	}
+}

+ 3 - 4
e2e/suites/provider/cases/aws/parameterstore/parameterstore.go

@@ -17,14 +17,13 @@ limitations under the License.
 package aws
 
 import (
-
-	// nolint
-	. "github.com/onsi/ginkgo/v2"
-
 	"github.com/external-secrets/external-secrets-e2e/framework"
 	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
 	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
 	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+
+	// nolint
+	. "github.com/onsi/ginkgo/v2"
 )
 
 const (

+ 3 - 4
e2e/suites/provider/cases/aws/parameterstore/parameterstore_managed.go

@@ -17,14 +17,13 @@ limitations under the License.
 package aws
 
 import (
-
-	// nolint
-	. "github.com/onsi/ginkgo/v2"
-
 	"github.com/external-secrets/external-secrets-e2e/framework"
 	"github.com/external-secrets/external-secrets-e2e/framework/addon"
 	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
 	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+
+	// nolint
+	. "github.com/onsi/ginkgo/v2"
 )
 
 // here we use the global eso instance

+ 5 - 6
e2e/suites/provider/cases/aws/parameterstore/provider.go

@@ -26,12 +26,6 @@ import (
 	"github.com/aws/aws-sdk-go-v2/credentials"
 	"github.com/aws/aws-sdk-go-v2/service/ssm"
 	ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
-
-	//nolint
-	. "github.com/onsi/ginkgo/v2"
-
-	// nolint
-	. "github.com/onsi/gomega"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
@@ -40,6 +34,11 @@ import (
 	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	esmetav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+
+	//nolint
+	. "github.com/onsi/ginkgo/v2"
+	// nolint
+	. "github.com/onsi/gomega"
 )
 
 type Provider struct {

+ 343 - 0
e2e/suites/provider/cases/aws/parameterstore/provider_support_v2.go

@@ -0,0 +1,343 @@
+/*
+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 aws
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+	"sync"
+	"time"
+
+	"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/service/ssm"
+	ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	"github.com/external-secrets/external-secrets-e2e/framework/log"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmetav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+const (
+	awsProviderAPIVersion = "provider.external-secrets.io/v2alpha1"
+	defaultV2WaitTimeout  = 60 * time.Second
+	defaultV2PollInterval = 2 * time.Second
+)
+
+type awsV2AccessConfig struct {
+	KID    string
+	SAK    string
+	ST     string
+	Region string
+}
+
+type parameterStoreBackend struct {
+	access     awsV2AccessConfig
+	client     *ssm.Client
+	clientErr  error
+	clientOnce sync.Once
+}
+
+func loadAWSV2AccessConfigFromEnv() awsV2AccessConfig {
+	return awsV2AccessConfig{
+		KID:    os.Getenv("AWS_ACCESS_KEY_ID"),
+		SAK:    os.Getenv("AWS_SECRET_ACCESS_KEY"),
+		ST:     os.Getenv("AWS_SESSION_TOKEN"),
+		Region: os.Getenv("AWS_REGION"),
+	}
+}
+
+func (c awsV2AccessConfig) missingStaticCredentials() []string {
+	var missing []string
+	if c.KID == "" {
+		missing = append(missing, "AWS_ACCESS_KEY_ID")
+	}
+	if c.SAK == "" {
+		missing = append(missing, "AWS_SECRET_ACCESS_KEY")
+	}
+	if c.Region == "" {
+		missing = append(missing, "AWS_REGION")
+	}
+	return missing
+}
+
+func skipIfAWSV2StaticCredentialsMissing(access awsV2AccessConfig) {
+	if missing := access.missingStaticCredentials(); len(missing) > 0 {
+		Skip("missing AWS e2e credentials: " + strings.Join(missing, ", "))
+	}
+}
+
+func staticAWSV2Auth(secretName string) esv1.AWSAuth {
+	return esv1.AWSAuth{
+		SecretRef: &esv1.AWSAuthSecretRef{
+			AccessKeyID: esmetav1.SecretKeySelector{
+				Name: secretName,
+				Key:  awscommon.StaticAccessKeyIDKey,
+			},
+			SecretAccessKey: esmetav1.SecretKeySelector{
+				Name: secretName,
+				Key:  awscommon.StaticSecretAccessKeyKey,
+			},
+			SessionToken: &esmetav1.SecretKeySelector{
+				Name: secretName,
+				Key:  awscommon.StaticSessionTokenKey,
+			},
+		},
+	}
+}
+
+func newStaticCredentialsSecret(namespace, name string, access awsV2AccessConfig) *corev1.Secret {
+	return &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		StringData: awscommon.StaticCredentialsSecretData(access.KID, access.SAK, access.ST),
+	}
+}
+
+func createStaticCredentialsSecret(f *framework.Framework, namespace, name string, access awsV2AccessConfig) {
+	Expect(f.CRClient.Create(GinkgoT().Context(), newStaticCredentialsSecret(namespace, name, access))).To(Succeed())
+}
+
+func newParameterStoreV2Config(namespace, name string, access awsV2AccessConfig) *awsv2alpha1.ParameterStore {
+	return &awsv2alpha1.ParameterStore{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: awsv2alpha1.GroupVersion.String(),
+			Kind:       awsv2alpha1.ParameterStoreKind,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		Spec: awsv2alpha1.ParameterStoreSpec{
+			Region: access.Region,
+			Auth:   staticAWSV2Auth(awscommon.CredentialsSecretName(name)),
+		},
+	}
+}
+
+func createParameterStoreV2Config(f *framework.Framework, namespace, name string, access awsV2AccessConfig) *awsv2alpha1.ParameterStore {
+	createStaticCredentialsSecret(f, namespace, awscommon.CredentialsSecretName(name), access)
+	cfg := newParameterStoreV2Config(namespace, name, access)
+	Expect(f.CRClient.Create(GinkgoT().Context(), cfg)).To(Succeed())
+	return cfg
+}
+
+func loadParameterStoreAWSConfig(access awsV2AccessConfig) (aws.Config, error) {
+	loadOptions := []func(*config.LoadOptions) error{
+		config.WithRegion(access.Region),
+	}
+	if access.KID != "" || access.SAK != "" || access.ST != "" {
+		loadOptions = append(loadOptions, config.WithCredentialsProvider(
+			credentials.NewStaticCredentialsProvider(access.KID, access.SAK, access.ST),
+		))
+	}
+	return config.LoadDefaultConfig(context.Background(), loadOptions...)
+}
+
+func newParameterStoreBackend(access awsV2AccessConfig) *parameterStoreBackend {
+	return &parameterStoreBackend{access: access}
+}
+
+func (b *parameterStoreBackend) ensureClient() {
+	b.clientOnce.Do(func() {
+		cfg, err := loadParameterStoreAWSConfig(b.access)
+		if err != nil {
+			b.clientErr = err
+			return
+		}
+		b.client = ssm.NewFromConfig(cfg)
+	})
+
+	Expect(b.clientErr).ToNot(HaveOccurred())
+	Expect(b.client).NotTo(BeNil())
+}
+
+func (b *parameterStoreBackend) CreateSecret(key string, val framework.SecretEntry) {
+	b.ensureClient()
+
+	psTags := make([]ssmtypes.Tag, 0, len(val.Tags))
+	for tagKey, tagValue := range val.Tags {
+		psTags = append(psTags, ssmtypes.Tag{
+			Key:   aws.String(tagKey),
+			Value: aws.String(tagValue),
+		})
+	}
+
+	overwrite := len(psTags) == 0
+	_, err := b.client.PutParameter(GinkgoT().Context(), &ssm.PutParameterInput{
+		Name:      aws.String(key),
+		Value:     aws.String(val.Value),
+		DataType:  aws.String("text"),
+		Type:      ssmtypes.ParameterTypeString,
+		Overwrite: aws.Bool(overwrite),
+		Tags:      psTags,
+	})
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (b *parameterStoreBackend) DeleteSecret(key string) {
+	b.ensureClient()
+
+	_, err := b.client.DeleteParameter(GinkgoT().Context(), &ssm.DeleteParameterInput{
+		Name: aws.String(key),
+	})
+	var parameterNotFound *ssmtypes.ParameterNotFound
+	var resourceNotFound *ssmtypes.ResourceNotFoundException
+	if errors.As(err, &parameterNotFound) || errors.As(err, &resourceNotFound) {
+		return
+	}
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (b *parameterStoreBackend) WaitForSecretValue(name, expectedValue string) {
+	b.ensureClient()
+
+	Eventually(func(g Gomega) {
+		out, err := b.client.GetParameter(GinkgoT().Context(), &ssm.GetParameterInput{
+			Name:           aws.String(name),
+			WithDecryption: aws.Bool(true),
+		})
+		g.Expect(err).NotTo(HaveOccurred())
+		g.Expect(out.Parameter).NotTo(BeNil())
+		g.Expect(aws.ToString(out.Parameter.Value)).To(Equal(expectedValue))
+	}, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
+}
+
+func (b *parameterStoreBackend) ExpectSecretAbsent(name string) {
+	b.ensureClient()
+
+	Eventually(func() bool {
+		_, err := b.client.GetParameter(GinkgoT().Context(), &ssm.GetParameterInput{
+			Name:           aws.String(name),
+			WithDecryption: aws.Bool(true),
+		})
+		return parameterStoreReadErrorIndicatesAbsence(err)
+	}, defaultV2WaitTimeout, defaultV2PollInterval).Should(BeTrue(), fmt.Sprintf("expected AWS parameter %q to be absent", name))
+}
+
+func parameterStoreReadErrorIndicatesAbsence(err error) bool {
+	if err == nil {
+		return false
+	}
+	var parameterNotFound *ssmtypes.ParameterNotFound
+	var resourceNotFound *ssmtypes.ResourceNotFoundException
+	return errors.As(err, &parameterNotFound) || errors.As(err, &resourceNotFound)
+}
+
+type ProviderV2 struct {
+	access    awsV2AccessConfig
+	backend   *parameterStoreBackend
+	framework *framework.Framework
+}
+
+func NewProviderV2(f *framework.Framework) *ProviderV2 {
+	access := loadAWSV2AccessConfigFromEnv()
+	f.MakeRemoteRefKey = func(base string) string {
+		if f.Namespace == nil {
+			return parameterStoreRemoteRefKey(base, "")
+		}
+		return parameterStoreRemoteRefKey(base, f.Namespace.Name)
+	}
+
+	prov := &ProviderV2{
+		access:    access,
+		backend:   newParameterStoreBackend(access),
+		framework: f,
+	}
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			return
+		}
+		skipIfAWSV2StaticCredentialsMissing(access)
+	})
+
+	return prov
+}
+
+func parameterStoreRemoteRefKey(base, namespace string) string {
+	base = strings.Trim(base, "/")
+	if namespace == "" {
+		return "/e2e/" + base
+	}
+	return fmt.Sprintf("/e2e/%s/%s", namespace, base)
+}
+
+func (p *ProviderV2) CreateSecret(key string, val framework.SecretEntry) {
+	p.backend.CreateSecret(key, val)
+}
+
+func (p *ProviderV2) DeleteSecret(key string) {
+	p.backend.DeleteSecret(key)
+}
+
+func useV2StaticAuth(prov *ProviderV2) func(*framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		tc.Prepare = prov.prepareNamespacedProvider()
+	}
+}
+
+func (p *ProviderV2) prepareNamespacedProvider() func(*framework.TestCase, framework.SecretStoreProvider) {
+	return func(_ *framework.TestCase, _ framework.SecretStoreProvider) {
+		configName := p.providerConfigName()
+		createParameterStoreV2Config(p.framework, p.framework.Namespace.Name, configName, p.access)
+		frameworkv2.CreateProviderConnection(
+			p.framework,
+			p.framework.Namespace.Name,
+			p.framework.Namespace.Name,
+			frameworkv2.ProviderAddress("aws"),
+			awsProviderAPIVersion,
+			awsv2alpha1.ParameterStoreKind,
+			configName,
+			p.framework.Namespace.Name,
+		)
+		frameworkv2.WaitForProviderConnectionReady(p.framework, p.framework.Namespace.Name, p.framework.Namespace.Name, defaultV2WaitTimeout)
+	}
+}
+
+func (p *ProviderV2) providerConfigName() string {
+	return fmt.Sprintf("%s-parameterstore", p.framework.Namespace.Name)
+}
+
+func createParameterStoreV2ProviderConnection(f *framework.Framework, namespace, name, providerName, providerNamespace string) {
+	frameworkv2.CreateProviderConnection(
+		f,
+		namespace,
+		name,
+		frameworkv2.ProviderAddress("aws"),
+		awsProviderAPIVersion,
+		awsv2alpha1.ParameterStoreKind,
+		providerName,
+		providerNamespace,
+	)
+	log.Logf("created ParameterStore Provider connection: %s/%s", namespace, name)
+}

+ 56 - 0
e2e/suites/provider/cases/aws/parameterstore/provider_support_v2_test.go

@@ -0,0 +1,56 @@
+/*
+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 aws
+
+import (
+	"strings"
+	"testing"
+
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+)
+
+func TestNewParameterStoreV2ConfigUsesStaticSessionTokenSelector(t *testing.T) {
+	t.Parallel()
+
+	cfg := newParameterStoreV2Config("ns", "ps-static", awsV2AccessConfig{
+		Region: "eu-central-1",
+	})
+	if cfg.TypeMeta.Kind != awsv2alpha1.ParameterStoreKind {
+		t.Fatalf("expected kind %q, got %q", awsv2alpha1.ParameterStoreKind, cfg.TypeMeta.Kind)
+	}
+	if cfg.Spec.Auth.SecretRef == nil || cfg.Spec.Auth.SecretRef.SessionToken == nil {
+		t.Fatal("expected session token selector to be configured for static auth")
+	}
+	if cfg.Spec.Auth.SecretRef.SessionToken.Name != "ps-static-credentials" || cfg.Spec.Auth.SecretRef.SessionToken.Key != "st" {
+		t.Fatalf("unexpected session token selector: %+v", cfg.Spec.Auth.SecretRef.SessionToken)
+	}
+}
+
+func TestParameterStoreRemoteRefKeyAvoidsReservedPrefixes(t *testing.T) {
+	t.Parallel()
+
+	got := parameterStoreRemoteRefKey("aws-v2-ps-refresh-remote", "e2e-tests-eso-aws-ps-v2-6s27x")
+	if !strings.HasPrefix(got, "/e2e/") {
+		t.Fatalf("expected /e2e/ prefix, got %q", got)
+	}
+	if strings.HasPrefix(strings.TrimPrefix(got, "/"), "aws") || strings.HasPrefix(strings.TrimPrefix(got, "/"), "ssm") {
+		t.Fatalf("expected non-reserved parameter prefix, got %q", got)
+	}
+	if !strings.Contains(got, "aws-v2-ps-refresh-remote") {
+		t.Fatalf("expected remote key to retain base name, got %q", got)
+	}
+}

+ 121 - 0
e2e/suites/provider/cases/aws/parameterstore/provider_v2.go

@@ -0,0 +1,121 @@
+/*
+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 aws
+
+import (
+	"fmt"
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+
+	. "github.com/onsi/ginkgo/v2"
+)
+
+var _ = Describe("[aws] v2 namespaced provider", Label("aws", "parameterstore", "v2", "namespaced-provider"), func() {
+	f := framework.New("eso-aws-ps-v2")
+	prov := NewProviderV2(f)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("namespaced provider",
+		framework.TableFuncWithExternalSecret(f, prov),
+		framework.Compose(withStaticAuth, f, func(_ *framework.Framework) (string, func(*framework.TestCase)) {
+			return common.NamespacedProviderSync(f, common.NamespacedProviderSyncConfig{
+				Description:        "[aws] should sync an ExternalSecret through a namespaced ParameterStore Provider using static credentials",
+				ExternalSecretName: "aws-v2-ps-static-es",
+				TargetSecretName:   "aws-v2-ps-static-target",
+				RemoteKey:          f.MakeRemoteRefKey("aws-v2-ps-static-remote"),
+				RemoteSecretValue:  "aws-v2-ps-static-value",
+				SecretKey:          "value",
+				ExpectedValue:      "aws-v2-ps-static-value",
+			})
+		}, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, func(_ *framework.Framework) (string, func(*framework.TestCase)) {
+			return common.NamespacedProviderRefresh(f, common.NamespacedProviderRefreshConfig{
+				Description:         "[aws] should refresh synced ParameterStore secrets after the remote parameter changes",
+				ExternalSecretName:  "aws-v2-ps-refresh-es",
+				TargetSecretName:    "aws-v2-ps-refresh-target",
+				RemoteKey:           f.MakeRemoteRefKey("aws-v2-ps-refresh-remote"),
+				InitialSecretValue:  "aws-v2-ps-initial",
+				UpdatedSecretValue:  "aws-v2-ps-updated",
+				SecretKey:           "value",
+				InitialExpectedData: "aws-v2-ps-initial",
+				UpdatedExpectedData: "aws-v2-ps-updated",
+				RefreshInterval:     10 * time.Second,
+				WaitTimeout:         30 * time.Second,
+			})
+		}, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, FindByName, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, FindByTag, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, versionedParameterV2(prov), useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, common.StatusNotUpdatedAfterSuccessfulSync, useV2StaticAuth(prov)),
+	)
+})
+
+func versionedParameterV2(prov framework.SecretStoreProvider) func(*framework.Framework) (string, func(*framework.TestCase)) {
+	return func(f *framework.Framework) (string, func(*framework.TestCase)) {
+		return "[common] should read versioned secrets", func(tc *framework.TestCase) {
+			secretKey := fmt.Sprintf("/e2e/versioned/%s/%s", f.Namespace.Name, "one")
+			versions := []int{1, 2, 3, 4, 5}
+
+			tc.ExpectedSecret = commonVersionedExpectedSecret(versions)
+			tc.ExternalSecret.Spec.Data = commonVersionedExternalSecretData(secretKey, versions)
+			tc.Cleanup = func() {
+				prov.DeleteSecret(secretKey)
+			}
+
+			for _, version := range versions {
+				prov.CreateSecret(secretKey, framework.SecretEntry{
+					Value: fmt.Sprintf("value%d", version),
+				})
+			}
+		}
+	}
+}
+
+func commonVersionedExpectedSecret(versions []int) *corev1.Secret {
+	data := make(map[string][]byte, len(versions))
+	for _, version := range versions {
+		data[fmt.Sprintf("v%d", version)] = []byte(fmt.Sprintf("value%d", version))
+	}
+	return &corev1.Secret{
+		Type: corev1.SecretTypeOpaque,
+		Data: data,
+	}
+}
+
+func commonVersionedExternalSecretData(secretKey string, versions []int) []esapi.ExternalSecretData {
+	data := make([]esapi.ExternalSecretData, 0, len(versions))
+	for _, version := range versions {
+		data = append(data, esapi.ExternalSecretData{
+			SecretKey: fmt.Sprintf("v%d", version),
+			RemoteRef: esapi.ExternalSecretDataRemoteRef{
+				Key:     secretKey,
+				Version: fmt.Sprintf("%d", version),
+			},
+		})
+	}
+	return data
+}

+ 72 - 0
e2e/suites/provider/cases/aws/parameterstore/provider_v2_test.go

@@ -0,0 +1,72 @@
+/*
+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 aws
+
+import (
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+type recordingSecretStoreProvider struct {
+	created []string
+	deleted []string
+}
+
+func (p *recordingSecretStoreProvider) CreateSecret(key string, _ framework.SecretEntry) {
+	p.created = append(p.created, key)
+}
+
+func (p *recordingSecretStoreProvider) DeleteSecret(key string) {
+	p.deleted = append(p.deleted, key)
+}
+
+func TestVersionedParameterV2RegistersCleanupWithoutDeletingDuringSetup(t *testing.T) {
+	fakeProvider := &recordingSecretStoreProvider{}
+
+	f := &framework.Framework{
+		Namespace: &corev1.Namespace{
+			ObjectMeta: metav1.ObjectMeta{Name: "test-ns"},
+		},
+	}
+	_, tweak := versionedParameterV2(fakeProvider)(f)
+	tc := &framework.TestCase{
+		ExternalSecret: &esapi.ExternalSecret{},
+	}
+
+	tweak(tc)
+
+	if got, want := len(fakeProvider.created), 5; got != want {
+		t.Fatalf("expected %d created versions, got %d", want, got)
+	}
+	if got := len(fakeProvider.deleted); got != 0 {
+		t.Fatalf("expected no deletes during setup, got %d", got)
+	}
+	if tc.Cleanup == nil {
+		t.Fatalf("expected cleanup callback to be registered")
+	}
+
+	tc.Cleanup()
+
+	if got, want := len(fakeProvider.deleted), 1; got != want {
+		t.Fatalf("expected %d delete after cleanup, got %d", want, got)
+	}
+}

+ 153 - 0
e2e/suites/provider/cases/aws/parameterstore/push_v2.go

@@ -0,0 +1,153 @@
+/*
+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 aws
+
+import (
+	"context"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("[aws] v2 push secret", Label("aws", "parameterstore", "v2", "push-secret"), func() {
+	f := framework.New("eso-aws-ps-v2-push")
+	prov := NewProviderV2(f)
+	harness := newAWSClusterProviderPushHarness(f, prov)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("push secret",
+		framework.TableFuncWithPushSecret(f, prov, nil),
+		Entry(awsPushSecretImplicitProviderKind(f, prov)),
+		Entry(awsPushSecretRejectsNamespacedRemoteNamespaceOverride(f, prov)),
+		Entry(common.ClusterProviderPushManifestNamespace(f, harness)),
+		Entry(common.ClusterProviderPushProviderNamespace(f, harness)),
+		Entry(common.ClusterProviderPushDeniedByConditions(f, harness)),
+	)
+})
+
+func newAWSClusterProviderPushHarness(f *framework.Framework, prov *ProviderV2) common.ClusterProviderPushHarness {
+	return common.ClusterProviderPushHarness{
+		Prepare: func(_ *framework.TestCase, cfg common.ClusterProviderConfig) *common.ClusterProviderPushRuntime {
+			s := newAWSClusterProviderScenario(f, cfg.Name, cfg.AuthScope, prov.access, prov.backend)
+			clusterProviderName := s.createClusterProvider(cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.ClusterProviderPushRuntime{
+				ClusterProviderName:    clusterProviderName,
+				DefaultRemoteNamespace: "",
+				WaitForRemoteSecretValue: func(_, name, _, expectedValue string) {
+					s.backend.WaitForSecretValue(name, expectedValue)
+				},
+				ExpectNoRemoteSecret: func(_, name string) {
+					s.backend.ExpectSecretAbsent(name)
+				},
+			}
+		},
+	}
+}
+
+func awsPushSecretImplicitProviderKind(f *framework.Framework, prov *ProviderV2) (string, func(*framework.TestCase)) {
+	return "[aws] should support namespaced Provider refs when push kind is omitted", func(tc *framework.TestCase) {
+		remoteKey := f.MakeRemoteRefKey("aws-v2-ps-push-implicit")
+		tc.Prepare = prov.prepareNamespacedProvider()
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "aws-v2-ps-push-implicit-source",
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte("value1"),
+			},
+		}
+		tc.PushSecret.ObjectMeta.Name = "aws-v2-ps-push-implicit"
+		tc.PushSecret.Spec.DeletionPolicy = esv1alpha1.PushSecretDeletionPolicyDelete
+		tc.PushSecret.Spec.SecretStoreRefs[0].Kind = ""
+		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
+			Secret: &esv1alpha1.PushSecretSecret{
+				Name: tc.PushSecretSource.Name,
+			},
+		}
+		tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{{
+			Match: esv1alpha1.PushSecretMatch{
+				SecretKey: "value",
+				RemoteRef: esv1alpha1.PushSecretRemoteRef{
+					RemoteKey: remoteKey,
+					Property:  "value",
+				},
+			},
+		}}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			awscommon.WaitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+			prov.backend.WaitForSecretValue(remoteKey, "value1")
+
+			Expect(tc.Framework.CRClient.Delete(context.Background(), ps)).To(Succeed())
+			prov.backend.ExpectSecretAbsent(remoteKey)
+		}
+	}
+}
+
+func awsPushSecretRejectsNamespacedRemoteNamespaceOverride(f *framework.Framework, prov *ProviderV2) (string, func(*framework.TestCase)) {
+	return "[aws] should reject remote namespace overrides when pushing through a namespaced Provider", func(tc *framework.TestCase) {
+		remoteKey := f.MakeRemoteRefKey("aws-v2-ps-push-override")
+		tc.Prepare = prov.prepareNamespacedProvider()
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "aws-v2-ps-push-override-source",
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte("should-not-push"),
+			},
+		}
+		tc.PushSecret.ObjectMeta.Name = "aws-v2-ps-push-override"
+		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
+			Secret: &esv1alpha1.PushSecretSecret{
+				Name: tc.PushSecretSource.Name,
+			},
+		}
+		tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{{
+			Match: esv1alpha1.PushSecretMatch{
+				SecretKey: "value",
+				RemoteRef: esv1alpha1.PushSecretRemoteRef{
+					RemoteKey: remoteKey,
+					Property:  "value",
+				},
+			},
+			Metadata: awscommon.PushSecretMetadataWithRemoteNamespace("ignored-aws-namespace"),
+		}}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			awscommon.WaitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionFalse)
+			prov.backend.ExpectSecretAbsent(remoteKey)
+			awscommon.ExpectPushSecretEventMessage(tc.Framework, ps.Namespace, ps.Name, `unknown field "remoteNamespace"`)
+		}
+	}
+}

+ 109 - 0
e2e/suites/provider/cases/aws/secretsmanager/clusterprovider_v2.go

@@ -0,0 +1,109 @@
+/*
+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 aws
+
+import (
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+
+	. "github.com/onsi/ginkgo/v2"
+)
+
+var _ = Describe("[aws] v2 cluster provider", Label("aws", "secretsmanager", "v2", "cluster-provider"), func() {
+	f := framework.New("eso-aws-sm-v2-clusterprovider")
+	prov := NewProviderV2(f)
+	harness := newAWSClusterProviderExternalSecretHarness(f, prov)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("cluster provider external secrets",
+		framework.TableFuncWithExternalSecret(f, prov),
+		Entry(common.ClusterProviderManifestNamespace(f, harness)),
+		Entry(common.ClusterProviderProviderNamespace(f, harness)),
+		Entry(common.ClusterProviderDeniedByConditions(f, harness)),
+	)
+})
+
+type awsClusterProviderScenario struct {
+	common    awscommon.V2ClusterProviderScenario
+	access    awsAccessConfig
+	authScope esv1.AuthenticationScope
+	backend   *secretsManagerBackend
+	f         *framework.Framework
+}
+
+func newAWSClusterProviderScenario(f *framework.Framework, prefix string, authScope esv1.AuthenticationScope, access awsAccessConfig, backend *secretsManagerBackend) *awsClusterProviderScenario {
+	shared := awscommon.NewV2ClusterProviderScenario(f.Namespace.Name, prefix, authScope, func(prefix string) string {
+		return common.CreateProviderCaseNamespace(f, prefix, defaultV2PollInterval)
+	})
+	s := &awsClusterProviderScenario{
+		common:    shared,
+		access:    access,
+		authScope: authScope,
+		backend:   backend,
+		f:         f,
+	}
+	createSecretsManagerV2Config(s.f, s.common.ConfigNamespace, s.common.ConfigName, s.access, awsAuthProfileStatic)
+	return s
+}
+
+func (s *awsClusterProviderScenario) createClusterProvider(conditions []esv1.ClusterSecretStoreCondition) string {
+	clusterProviderName := s.common.ClusterProviderName()
+	frameworkv2.CreateClusterProviderConnection(
+		s.f,
+		clusterProviderName,
+		frameworkv2.ProviderAddress("aws"),
+		awsProviderAPIVersion,
+		awsv2alpha1.SecretsManagerKind,
+		s.common.ConfigName,
+		s.common.ProviderRefNamespace,
+		s.common.AuthScope,
+		conditions,
+	)
+	return clusterProviderName
+}
+
+func (s *awsClusterProviderScenario) CreateSecret(key string, val framework.SecretEntry) {
+	s.backend.CreateSecret(key, val)
+}
+
+func (s *awsClusterProviderScenario) DeleteSecret(key string) {
+	s.backend.DeleteSecret(key)
+}
+
+func newAWSClusterProviderExternalSecretHarness(f *framework.Framework, prov *ProviderV2) common.ClusterProviderExternalSecretHarness {
+	return common.ClusterProviderExternalSecretHarness{
+		Prepare: func(_ *framework.TestCase, cfg common.ClusterProviderConfig) *common.ClusterProviderExternalSecretRuntime {
+			s := newAWSClusterProviderScenario(f, cfg.Name, cfg.AuthScope, prov.access, prov.backend)
+			clusterProviderName := s.createClusterProvider(cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.ClusterProviderExternalSecretRuntime{
+				ClusterProviderName: clusterProviderName,
+				Provider:            s,
+			}
+		},
+	}
+}

+ 20 - 68
e2e/suites/provider/cases/aws/secretsmanager/provider.go

@@ -17,22 +17,6 @@ limitations under the License.
 package aws
 
 import (
-	"context"
-	"errors"
-	"os"
-	"time"
-
-	"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/service/secretsmanager"
-	secretsmanagertypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
-
-	//nolint
-	. "github.com/onsi/ginkgo/v2"
-
-	// nolint
-	. "github.com/onsi/gomega"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
@@ -41,32 +25,41 @@ import (
 	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	esmetav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+
+	//nolint
+	. "github.com/onsi/ginkgo/v2"
+	// nolint
+	. "github.com/onsi/gomega"
 )
 
 type Provider struct {
 	ServiceAccountName      string
 	ServiceAccountNamespace string
 
+	backend   *secretsManagerBackend
 	region    string
-	client    *secretsmanager.Client
 	framework *framework.Framework
 }
 
 func NewProvider(f *framework.Framework, kid, sak, st, region, saName, saNamespace string) *Provider {
+	access := awsAccessConfig{
+		KID:         kid,
+		SAK:         sak,
+		ST:          st,
+		Region:      region,
+		SAName:      saName,
+		SANamespace: saNamespace,
+	}
 	prov := &Provider{
 		ServiceAccountName:      saName,
 		ServiceAccountNamespace: saNamespace,
+		backend:                 newSecretsManagerBackend(f, access),
 		region:                  region,
 		framework:               f,
 	}
 
-	BeforeAll(func() {
-		config, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(region), config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(kid, sak, st)))
-		Expect(err).ToNot(HaveOccurred())
-		prov.client = secretsmanager.NewFromConfig(config)
-	})
-
 	BeforeEach(func() {
+		skipIfAWSStaticCredentialsMissing(access)
 		awscommon.SetupStaticStore(f, awscommon.AccessOpts{KID: kid, SAK: sak, ST: st, Region: region}, esv1.AWSServiceSecretsManager)
 		awscommon.SetupExternalIDStore(
 			f,
@@ -95,61 +88,20 @@ func NewProvider(f *framework.Framework, kid, sak, st, region, saName, saNamespa
 }
 
 func NewFromEnv(f *framework.Framework) *Provider {
-	kid := os.Getenv("AWS_ACCESS_KEY_ID")
-	sak := os.Getenv("AWS_SECRET_ACCESS_KEY")
-	st := os.Getenv("AWS_SESSION_TOKEN")
-	region := os.Getenv("AWS_REGION")
-	saName := os.Getenv("AWS_SA_NAME")
-	saNamespace := os.Getenv("AWS_SA_NAMESPACE")
-	return NewProvider(f, kid, sak, st, region, saName, saNamespace)
+	access := loadAWSAccessConfigFromEnv()
+	return NewProvider(f, access.KID, access.SAK, access.ST, access.Region, access.SAName, access.SANamespace)
 }
 
 // CreateSecret creates a secret at the provider.
 func (s *Provider) CreateSecret(key string, val framework.SecretEntry) {
-	smTags := make([]secretsmanagertypes.Tag, 0)
-	for k, v := range val.Tags {
-		smTags = append(smTags, secretsmanagertypes.Tag{
-			Key:   aws.String(k),
-			Value: aws.String(v),
-		})
-	}
-
-	// we re-use some secret names throughout our test suite
-	// due to the fact that there is a short delay before the secret is actually deleted
-	// we have to retry creating the secret
-	attempts := 20
-	for {
-		log.Logf("creating secret %s / attempts left: %d", key, attempts)
-		_, err := s.client.CreateSecret(GinkgoT().Context(), &secretsmanager.CreateSecretInput{
-			Name:         aws.String(key),
-			SecretString: aws.String(val.Value),
-			Tags:         smTags,
-		})
-		if err == nil {
-			return
-		}
-		attempts--
-		if attempts < 0 {
-			Fail("unable to create secret: " + err.Error())
-		}
-		<-time.After(time.Second * 5)
-	}
+	s.backend.CreateSecret(key, val)
 }
 
 // DeleteSecret deletes a secret at the provider.
 // There may be a short delay between calling this function
 // and the removal of the secret on the provider side.
 func (s *Provider) DeleteSecret(key string) {
-	log.Logf("deleting secret %s", key)
-	_, err := s.client.DeleteSecret(GinkgoT().Context(), &secretsmanager.DeleteSecretInput{
-		SecretId:                   aws.String(key),
-		ForceDeleteWithoutRecovery: aws.Bool(true),
-	})
-	var nf *secretsmanagertypes.ResourceNotFoundException
-	if errors.As(err, &nf) {
-		return
-	}
-	Expect(err).ToNot(HaveOccurred())
+	s.backend.DeleteSecret(key)
 }
 
 // MountedIRSAStore is a SecretStore without auth config

+ 484 - 0
e2e/suites/provider/cases/aws/secretsmanager/provider_support.go

@@ -0,0 +1,484 @@
+/*
+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 aws
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/config"
+	"github.com/aws/aws-sdk-go-v2/credentials"
+	awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
+	secretsmanagertypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
+	"github.com/aws/aws-sdk-go-v2/service/sts"
+	ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	"github.com/external-secrets/external-secrets-e2e/framework/log"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmetav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+const (
+	awsProviderAPIVersion = "provider.external-secrets.io/v2alpha1"
+	defaultV2WaitTimeout  = 60 * time.Second
+	defaultV2PollInterval = 2 * time.Second
+)
+
+const (
+	assumeRoleSessionName = "eso-e2e-probe"
+)
+
+type awsAuthProfile string
+
+const (
+	awsAuthProfileStatic         awsAuthProfile = "static"
+	awsAuthProfileExternalID     awsAuthProfile = "external-id"
+	awsAuthProfileSessionTags    awsAuthProfile = "session-tags"
+	awsAuthProfileReferencedIRSA awsAuthProfile = "referenced-irsa"
+	awsAuthProfileMountedIRSA    awsAuthProfile = "mounted-irsa"
+)
+
+type awsAccessConfig struct {
+	KID         string
+	SAK         string
+	ST          string
+	Region      string
+	Role        string
+	SAName      string
+	SANamespace string
+}
+
+type secretsManagerBackend struct {
+	access     awsAccessConfig
+	client     *awssm.Client
+	clientErr  error
+	clientOnce sync.Once
+	framework  *framework.Framework
+}
+
+type stsAssumeRoleClient interface {
+	AssumeRole(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error)
+}
+
+type assumeRoleProbeKey struct {
+	access  awsAccessConfig
+	profile awsAuthProfile
+}
+
+type assumeRoleProbeResult struct {
+	err error
+}
+
+var assumeRoleProbeCache sync.Map
+
+func loadAWSAccessConfigFromEnv() awsAccessConfig {
+	return awsAccessConfig{
+		KID:         os.Getenv("AWS_ACCESS_KEY_ID"),
+		SAK:         os.Getenv("AWS_SECRET_ACCESS_KEY"),
+		ST:          os.Getenv("AWS_SESSION_TOKEN"),
+		Region:      os.Getenv("AWS_REGION"),
+		SAName:      os.Getenv("AWS_SA_NAME"),
+		SANamespace: os.Getenv("AWS_SA_NAMESPACE"),
+	}
+}
+
+func newBackendFromEnv(f *framework.Framework) *secretsManagerBackend {
+	return newSecretsManagerBackend(f, loadAWSAccessConfigFromEnv())
+}
+
+func newSecretsManagerBackend(f *framework.Framework, access awsAccessConfig) *secretsManagerBackend {
+	return &secretsManagerBackend{
+		access:    access,
+		framework: f,
+	}
+}
+
+func (c awsAccessConfig) missingStaticCredentials() []string {
+	var missing []string
+	if c.KID == "" {
+		missing = append(missing, "AWS_ACCESS_KEY_ID")
+	}
+	if c.SAK == "" {
+		missing = append(missing, "AWS_SECRET_ACCESS_KEY")
+	}
+	if c.Region == "" {
+		missing = append(missing, "AWS_REGION")
+	}
+	return missing
+}
+
+func skipIfAWSStaticCredentialsMissing(access awsAccessConfig) {
+	if missing := access.missingStaticCredentials(); len(missing) > 0 {
+		Skip("missing AWS e2e credentials: " + strings.Join(missing, ", "))
+	}
+}
+
+func skipIfAWSManagedIRSAEnvMissing(access awsAccessConfig) {
+	var missing []string
+	if access.Region == "" {
+		missing = append(missing, "AWS_REGION")
+	}
+	if access.SAName == "" {
+		missing = append(missing, "AWS_SA_NAME")
+	}
+	if access.SANamespace == "" {
+		missing = append(missing, "AWS_SA_NAMESPACE")
+	}
+	if len(missing) > 0 {
+		Skip("missing AWS managed IRSA environment: " + strings.Join(missing, ", "))
+	}
+}
+
+func skipIfAWSAssumeRoleProbeDenied(access awsAccessConfig, profile awsAuthProfile) {
+	if profile != awsAuthProfileExternalID && profile != awsAuthProfileSessionTags {
+		return
+	}
+
+	cacheKey := assumeRoleProbeKey{
+		access:  access,
+		profile: profile,
+	}
+	if cached, ok := assumeRoleProbeCache.Load(cacheKey); ok {
+		handleAssumeRoleProbeResult(access, profile, cached.(assumeRoleProbeResult).err)
+		return
+	}
+
+	cfg, err := loadAWSConfig(access)
+	Expect(err).NotTo(HaveOccurred())
+
+	err = probeAssumeRoleAccess(context.Background(), sts.NewFromConfig(cfg), access, profile)
+	assumeRoleProbeCache.Store(cacheKey, assumeRoleProbeResult{err: err})
+	handleAssumeRoleProbeResult(access, profile, err)
+}
+
+func handleAssumeRoleProbeResult(access awsAccessConfig, profile awsAuthProfile, err error) {
+	if err == nil {
+		return
+	}
+	if isAssumeRoleAccessDenied(err) {
+		Skip(fmt.Sprintf("skipping AWS %s auth e2e: %s is not authorized to assume role %q with the current credentials", profile, assumeRoleAction(profile), roleARNForProfile(access, profile)))
+	}
+	Expect(err).NotTo(HaveOccurred())
+}
+
+func assumeRoleAction(profile awsAuthProfile) string {
+	if profile == awsAuthProfileSessionTags {
+		return "sts:TagSession"
+	}
+	return "sts:AssumeRole"
+}
+
+func staticAWSAuth(secretName string) esv1.AWSAuth {
+	return esv1.AWSAuth{
+		SecretRef: &esv1.AWSAuthSecretRef{
+			AccessKeyID: esmetav1.SecretKeySelector{
+				Name: secretName,
+				Key:  awscommon.StaticAccessKeyIDKey,
+			},
+			SecretAccessKey: esmetav1.SecretKeySelector{
+				Name: secretName,
+				Key:  awscommon.StaticSecretAccessKeyKey,
+			},
+			SessionToken: &esmetav1.SecretKeySelector{
+				Name: secretName,
+				Key:  awscommon.StaticSessionTokenKey,
+			},
+		},
+	}
+}
+
+func newStaticCredentialsSecret(namespace, name string, access awsAccessConfig) *corev1.Secret {
+	return &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		StringData: awscommon.StaticCredentialsSecretData(access.KID, access.SAK, access.ST),
+	}
+}
+
+func createStaticCredentialsSecret(f *framework.Framework, namespace, name string, access awsAccessConfig) {
+	Expect(f.CRClient.Create(GinkgoT().Context(), newStaticCredentialsSecret(namespace, name, access))).To(Succeed())
+}
+
+func roleARNForProfile(access awsAccessConfig, profile awsAuthProfile) string {
+	if access.Role != "" {
+		return access.Role
+	}
+	switch profile {
+	case awsAuthProfileExternalID:
+		return awscommon.IAMRoleExternalID
+	case awsAuthProfileSessionTags:
+		return awscommon.IAMRoleSessionTags
+	default:
+		return ""
+	}
+}
+
+func sessionTagsForProfile(profile awsAuthProfile) []ststypes.Tag {
+	if profile != awsAuthProfileSessionTags {
+		return nil
+	}
+
+	return []ststypes.Tag{{
+		Key:   aws.String("namespace"),
+		Value: aws.String("e2e-test"),
+	}}
+}
+
+func probeAssumeRoleAccess(ctx context.Context, client stsAssumeRoleClient, access awsAccessConfig, profile awsAuthProfile) error {
+	if profile != awsAuthProfileExternalID && profile != awsAuthProfileSessionTags {
+		return nil
+	}
+
+	input := &sts.AssumeRoleInput{
+		RoleArn:         aws.String(roleARNForProfile(access, profile)),
+		RoleSessionName: aws.String(assumeRoleSessionName),
+		Tags:            sessionTagsForProfile(profile),
+	}
+	if profile == awsAuthProfileExternalID {
+		input.ExternalId = aws.String(awscommon.IAMTrustedExternalID)
+	}
+
+	_, err := client.AssumeRole(ctx, input)
+	return err
+}
+
+func isAssumeRoleAccessDenied(err error) bool {
+	if err == nil {
+		return false
+	}
+
+	msg := strings.ToLower(err.Error())
+	if !strings.Contains(msg, "accessdenied") {
+		return false
+	}
+	return strings.Contains(msg, "sts:assumerole") || strings.Contains(msg, "sts:tagsession")
+}
+
+func newSecretsManagerV2Config(namespace, name string, access awsAccessConfig, profile awsAuthProfile) *awsv2alpha1.SecretsManager {
+	cfg := &awsv2alpha1.SecretsManager{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: awsv2alpha1.GroupVersion.String(),
+			Kind:       awsv2alpha1.SecretsManagerKind,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		Spec: awsv2alpha1.SecretsManagerSpec{
+			Region: access.Region,
+		},
+	}
+
+	switch profile {
+	case awsAuthProfileStatic:
+		cfg.Spec.Auth = staticAWSAuth(awscommon.CredentialsSecretName(name))
+	case awsAuthProfileExternalID:
+		cfg.Spec.Auth = staticAWSAuth(awscommon.CredentialsSecretName(name))
+		cfg.Spec.Role = access.Role
+		if cfg.Spec.Role == "" {
+			cfg.Spec.Role = awscommon.IAMRoleExternalID
+		}
+		cfg.Spec.ExternalID = awscommon.IAMTrustedExternalID
+	case awsAuthProfileSessionTags:
+		cfg.Spec.Auth = staticAWSAuth(awscommon.CredentialsSecretName(name))
+		cfg.Spec.Role = access.Role
+		if cfg.Spec.Role == "" {
+			cfg.Spec.Role = awscommon.IAMRoleSessionTags
+		}
+		cfg.Spec.SessionTags = []*esv1.Tag{{
+			Key:   "namespace",
+			Value: "e2e-test",
+		}}
+	case awsAuthProfileReferencedIRSA:
+		cfg.Spec.Auth = esv1.AWSAuth{
+			JWTAuth: &esv1.AWSJWTAuth{
+				ServiceAccountRef: &esmetav1.ServiceAccountSelector{
+					Name:      access.SAName,
+					Namespace: &access.SANamespace,
+				},
+			},
+		}
+	case awsAuthProfileMountedIRSA:
+		cfg.Spec.Auth = esv1.AWSAuth{}
+	default:
+		cfg.Spec.Auth = staticAWSAuth(awscommon.CredentialsSecretName(name))
+	}
+
+	return cfg
+}
+
+func createSecretsManagerV2Config(f *framework.Framework, namespace, name string, access awsAccessConfig, profile awsAuthProfile) *awsv2alpha1.SecretsManager {
+	if profile == awsAuthProfileStatic || profile == awsAuthProfileExternalID || profile == awsAuthProfileSessionTags {
+		createStaticCredentialsSecret(f, namespace, awscommon.CredentialsSecretName(name), access)
+	}
+
+	cfg := newSecretsManagerV2Config(namespace, name, access, profile)
+	Expect(f.CRClient.Create(GinkgoT().Context(), cfg)).To(Succeed())
+	return cfg
+}
+
+func createSecretsManagerV2ProviderConnection(f *framework.Framework, namespace, name, providerName, providerNamespace string) {
+	frameworkv2.CreateProviderConnection(
+		f,
+		namespace,
+		name,
+		frameworkv2.ProviderAddress("aws"),
+		awsProviderAPIVersion,
+		awsv2alpha1.SecretsManagerKind,
+		providerName,
+		providerNamespace,
+	)
+}
+
+func loadAWSConfig(access awsAccessConfig) (aws.Config, error) {
+	loadOptions := []func(*config.LoadOptions) error{
+		config.WithRegion(access.Region),
+	}
+	if access.KID != "" || access.SAK != "" || access.ST != "" {
+		loadOptions = append(loadOptions, config.WithCredentialsProvider(
+			credentials.NewStaticCredentialsProvider(access.KID, access.SAK, access.ST),
+		))
+	}
+
+	return config.LoadDefaultConfig(context.Background(), loadOptions...)
+}
+
+func (b *secretsManagerBackend) ensureClient() {
+	b.clientOnce.Do(func() {
+		cfg, err := loadAWSConfig(b.access)
+		if err != nil {
+			b.clientErr = err
+			return
+		}
+		b.client = awssm.NewFromConfig(cfg)
+	})
+
+	Expect(b.clientErr).ToNot(HaveOccurred())
+	Expect(b.client).NotTo(BeNil())
+}
+
+func (b *secretsManagerBackend) CreateSecret(key string, val framework.SecretEntry) {
+	b.ensureClient()
+
+	smTags := make([]secretsmanagertypes.Tag, 0, len(val.Tags))
+	for tagKey, tagValue := range val.Tags {
+		smTags = append(smTags, secretsmanagertypes.Tag{
+			Key:   aws.String(tagKey),
+			Value: aws.String(tagValue),
+		})
+	}
+
+	attempts := 20
+	for {
+		log.Logf("creating secret %s / attempts left: %d", key, attempts)
+		_, err := b.client.CreateSecret(GinkgoT().Context(), &awssm.CreateSecretInput{
+			Name:         aws.String(key),
+			SecretString: aws.String(val.Value),
+			Tags:         smTags,
+		})
+		if err == nil {
+			return
+		}
+		attempts--
+		if attempts < 0 {
+			Fail("unable to create secret: " + err.Error())
+		}
+		<-time.After(5 * time.Second)
+	}
+}
+
+func (b *secretsManagerBackend) DeleteSecret(key string) {
+	b.ensureClient()
+
+	log.Logf("deleting secret %s", key)
+	_, err := b.client.DeleteSecret(GinkgoT().Context(), &awssm.DeleteSecretInput{
+		SecretId:                   aws.String(key),
+		ForceDeleteWithoutRecovery: aws.Bool(true),
+	})
+	var notFound *secretsmanagertypes.ResourceNotFoundException
+	if errors.As(err, &notFound) {
+		return
+	}
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (b *secretsManagerBackend) WaitForSecretValue(name, expectedValue string) {
+	b.ensureClient()
+
+	Eventually(func(g Gomega) {
+		out, err := b.client.GetSecretValue(GinkgoT().Context(), &awssm.GetSecretValueInput{
+			SecretId: aws.String(name),
+		})
+		g.Expect(err).NotTo(HaveOccurred())
+		g.Expect(secretValueString(out)).To(Equal(expectedValue))
+	}, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
+}
+
+func (b *secretsManagerBackend) ExpectSecretAbsent(name string) {
+	b.ensureClient()
+
+	Eventually(func() bool {
+		_, err := b.client.GetSecretValue(GinkgoT().Context(), &awssm.GetSecretValueInput{
+			SecretId: aws.String(name),
+		})
+		return secretReadErrorIndicatesAbsence(err)
+	}, defaultV2WaitTimeout, defaultV2PollInterval).Should(BeTrue(), fmt.Sprintf("expected AWS secret %q to be absent", name))
+}
+
+func secretValueString(out *awssm.GetSecretValueOutput) string {
+	if out == nil {
+		return ""
+	}
+	if out.SecretString != nil {
+		return aws.ToString(out.SecretString)
+	}
+	if len(out.SecretBinary) > 0 {
+		return string(out.SecretBinary)
+	}
+	return ""
+}
+
+func secretReadErrorIndicatesAbsence(err error) bool {
+	if err == nil {
+		return false
+	}
+
+	var notFound *secretsmanagertypes.ResourceNotFoundException
+	if errors.As(err, &notFound) {
+		return true
+	}
+
+	msg := strings.ToLower(err.Error())
+	return strings.Contains(msg, "marked for deletion") || strings.Contains(msg, "scheduled for deletion")
+}

+ 211 - 0
e2e/suites/provider/cases/aws/secretsmanager/provider_support_test.go

@@ -0,0 +1,211 @@
+/*
+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 aws
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"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/sts"
+
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+type fakeSTSAssumeRoleClient struct {
+	input *sts.AssumeRoleInput
+	err   error
+}
+
+func (f *fakeSTSAssumeRoleClient) AssumeRole(_ context.Context, input *sts.AssumeRoleInput, _ ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) {
+	f.input = input
+	if f.err != nil {
+		return nil, f.err
+	}
+	return &sts.AssumeRoleOutput{}, nil
+}
+
+func TestProviderAddressInNamespace(t *testing.T) {
+	t.Parallel()
+
+	got := frameworkv2.ProviderAddressInNamespace("aws", "aws-irsa-system")
+	if got != "provider-aws.aws-irsa-system.svc:8080" {
+		t.Fatalf("unexpected address: %s", got)
+	}
+}
+
+func TestStaticAWSAuthUsesSessionTokenSelector(t *testing.T) {
+	t.Parallel()
+
+	auth := staticAWSAuth("aws-creds")
+	if auth.SecretRef == nil || auth.SecretRef.SessionToken == nil {
+		t.Fatal("expected session token selector to be preserved")
+	}
+	if auth.SecretRef.SessionToken.Name != "aws-creds" || auth.SecretRef.SessionToken.Key != "st" {
+		t.Fatalf("unexpected session token selector: %+v", auth.SecretRef.SessionToken)
+	}
+}
+
+func TestSecretsManagerConfigForExternalID(t *testing.T) {
+	t.Parallel()
+
+	cfg := newSecretsManagerV2Config("ns", "sm-extid", awsAccessConfig{
+		Region: "eu-west-1",
+		Role:   awscommon.IAMRoleExternalID,
+	}, awsAuthProfileExternalID)
+	if cfg.Spec.ExternalID != awscommon.IAMTrustedExternalID {
+		t.Fatalf("expected external ID %q, got %q", awscommon.IAMTrustedExternalID, cfg.Spec.ExternalID)
+	}
+}
+
+func TestSecretsManagerConfigForSessionTags(t *testing.T) {
+	t.Parallel()
+
+	cfg := newSecretsManagerV2Config("ns", "sm-tags", awsAccessConfig{
+		Region: "eu-west-1",
+		Role:   awscommon.IAMRoleSessionTags,
+	}, awsAuthProfileSessionTags)
+	if len(cfg.Spec.SessionTags) != 1 {
+		t.Fatalf("expected one session tag, got %d", len(cfg.Spec.SessionTags))
+	}
+	if cfg.Spec.SessionTags[0].Key != "namespace" || cfg.Spec.SessionTags[0].Value != "e2e-test" {
+		t.Fatalf("unexpected session tags: %+v", cfg.Spec.SessionTags)
+	}
+}
+
+func TestProviderConfigNamespaceForManifestScope(t *testing.T) {
+	t.Parallel()
+
+	if got := awscommon.ProviderConfigNamespace(esv1.AuthenticationScopeManifestNamespace, "provider-ns", "workload-ns"); got != "workload-ns" {
+		t.Fatalf("expected workload namespace, got %q", got)
+	}
+}
+
+func TestProviderConfigNamespaceForProviderScope(t *testing.T) {
+	t.Parallel()
+
+	if got := awscommon.ProviderConfigNamespace(esv1.AuthenticationScopeProviderNamespace, "provider-ns", "workload-ns"); got != "provider-ns" {
+		t.Fatalf("expected provider namespace, got %q", got)
+	}
+}
+
+func TestProviderReferenceNamespaceForManifestScope(t *testing.T) {
+	t.Parallel()
+
+	if got := awscommon.ProviderReferenceNamespace(esv1.AuthenticationScopeManifestNamespace, "provider-ns"); got != "" {
+		t.Fatalf("expected empty provider reference namespace, got %q", got)
+	}
+}
+
+func TestProviderReferenceNamespaceForProviderScope(t *testing.T) {
+	t.Parallel()
+
+	if got := awscommon.ProviderReferenceNamespace(esv1.AuthenticationScopeProviderNamespace, "provider-ns"); got != "provider-ns" {
+		t.Fatalf("expected provider namespace, got %q", got)
+	}
+}
+
+func TestSecretValueStringUsesSecretString(t *testing.T) {
+	t.Parallel()
+
+	got := secretValueString(&awssm.GetSecretValueOutput{
+		SecretString: aws.String(`{"value":"from-string"}`),
+		SecretBinary: []byte(`{"value":"from-binary"}`),
+	})
+	if got != `{"value":"from-string"}` {
+		t.Fatalf("expected SecretString payload, got %q", got)
+	}
+}
+
+func TestSecretValueStringFallsBackToSecretBinary(t *testing.T) {
+	t.Parallel()
+
+	got := secretValueString(&awssm.GetSecretValueOutput{
+		SecretBinary: []byte(`{"value":"from-binary"}`),
+	})
+	if got != `{"value":"from-binary"}` {
+		t.Fatalf("expected SecretBinary payload, got %q", got)
+	}
+}
+
+func TestSecretReadErrorIndicatesAbsenceRecognizesMarkedForDeletion(t *testing.T) {
+	t.Parallel()
+
+	err := errors.New("InvalidRequestException: You can't perform this operation on the secret because it was marked for deletion.")
+	if !secretReadErrorIndicatesAbsence(err) {
+		t.Fatal("expected marked-for-deletion error to be treated as absence")
+	}
+}
+
+func TestProbeAssumeRoleAccessBuildsExternalIDRequest(t *testing.T) {
+	t.Parallel()
+
+	client := &fakeSTSAssumeRoleClient{}
+	access := awsAccessConfig{
+		Role: awscommon.IAMRoleExternalID,
+	}
+	if err := probeAssumeRoleAccess(context.Background(), client, access, awsAuthProfileExternalID); err != nil {
+		t.Fatalf("probeAssumeRoleAccess() error = %v", err)
+	}
+	if client.input == nil {
+		t.Fatal("expected AssumeRole input to be recorded")
+	}
+	if got := aws.ToString(client.input.RoleArn); got != awscommon.IAMRoleExternalID {
+		t.Fatalf("expected role ARN %q, got %q", awscommon.IAMRoleExternalID, got)
+	}
+	if got := aws.ToString(client.input.ExternalId); got != awscommon.IAMTrustedExternalID {
+		t.Fatalf("expected external ID %q, got %q", awscommon.IAMTrustedExternalID, got)
+	}
+}
+
+func TestProbeAssumeRoleAccessBuildsSessionTagsRequest(t *testing.T) {
+	t.Parallel()
+
+	client := &fakeSTSAssumeRoleClient{}
+	access := awsAccessConfig{
+		Role: awscommon.IAMRoleSessionTags,
+	}
+	if err := probeAssumeRoleAccess(context.Background(), client, access, awsAuthProfileSessionTags); err != nil {
+		t.Fatalf("probeAssumeRoleAccess() error = %v", err)
+	}
+	if client.input == nil {
+		t.Fatal("expected AssumeRole input to be recorded")
+	}
+	if got := aws.ToString(client.input.RoleArn); got != awscommon.IAMRoleSessionTags {
+		t.Fatalf("expected role ARN %q, got %q", awscommon.IAMRoleSessionTags, got)
+	}
+	if len(client.input.Tags) != 1 {
+		t.Fatalf("expected one session tag, got %d", len(client.input.Tags))
+	}
+	tag := client.input.Tags[0]
+	if aws.ToString(tag.Key) != "namespace" || aws.ToString(tag.Value) != "e2e-test" {
+		t.Fatalf("unexpected session tag: %+v", tag)
+	}
+}
+
+func TestIsAssumeRoleAccessDeniedRecognizesSTSAccessDeniedErrors(t *testing.T) {
+	t.Parallel()
+
+	err := errors.New("api error AccessDenied: User is not authorized to perform: sts:TagSession")
+	if !isAssumeRoleAccessDenied(err) {
+		t.Fatal("expected sts access denied error to be recognized")
+	}
+}

+ 181 - 0
e2e/suites/provider/cases/aws/secretsmanager/provider_v2.go

@@ -0,0 +1,181 @@
+/*
+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 aws
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+
+	. "github.com/onsi/ginkgo/v2"
+)
+
+var _ = Describe("[aws] v2 namespaced provider", Label("aws", "secretsmanager", "v2", "namespaced-provider"), func() {
+	f := framework.New("eso-aws-sm-v2")
+	prov := NewProviderV2(f)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("namespaced provider",
+		framework.TableFuncWithExternalSecret(f, prov),
+		framework.Compose(withStaticAuth, f, func(f *framework.Framework) (string, func(*framework.TestCase)) {
+			return common.NamespacedProviderSync(f, common.NamespacedProviderSyncConfig{
+				Description:        "[aws] should sync an ExternalSecret through a namespaced Provider using static credentials",
+				ExternalSecretName: "aws-v2-static-es",
+				TargetSecretName:   "aws-v2-static-target",
+				RemoteKey:          f.MakeRemoteRefKey("aws-v2-static-remote"),
+				RemoteSecretValue:  `{"value":"aws-v2-static-value"}`,
+				RemoteProperty:     "value",
+				SecretKey:          "value",
+				ExpectedValue:      "aws-v2-static-value",
+			})
+		}, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, func(f *framework.Framework) (string, func(*framework.TestCase)) {
+			return common.NamespacedProviderRefresh(f, common.NamespacedProviderRefreshConfig{
+				Description:         "[aws] should refresh synced secrets after the remote AWS secret changes",
+				ExternalSecretName:  "aws-v2-refresh-es",
+				TargetSecretName:    "aws-v2-refresh-target",
+				RemoteKey:           f.MakeRemoteRefKey("aws-v2-refresh-remote"),
+				InitialSecretValue:  `{"value":"aws-v2-initial"}`,
+				UpdatedSecretValue:  `{"value":"aws-v2-updated"}`,
+				RemoteProperty:      "value",
+				SecretKey:           "value",
+				InitialExpectedData: "aws-v2-initial",
+				UpdatedExpectedData: "aws-v2-updated",
+				RefreshInterval:     10 * time.Second,
+				WaitTimeout:         30 * time.Second,
+			})
+		}, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, func(f *framework.Framework) (string, func(*framework.TestCase)) {
+			return common.NamespacedProviderFind(f, common.NamespacedProviderFindConfig{
+				Description:        "[aws] should sync ExternalSecret dataFrom.find through a namespaced Provider",
+				ExternalSecretName: "aws-v2-find-es",
+				TargetSecretName:   "aws-v2-find-target",
+				MatchRegExp:        "^aws-v2-find-(one|two)$",
+				MatchingSecrets: map[string]string{
+					"aws-v2-find-one": "aws-v2-one",
+					"aws-v2-find-two": "aws-v2-two",
+				},
+				IgnoredSecrets: map[string]string{
+					"aws-v2-ignore": "aws-v2-ignore",
+				},
+			})
+		}, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, common.StatusNotUpdatedAfterSuccessfulSync, useV2StaticAuth(prov)),
+		framework.Compose(withExtID, f, SimpleSyncWithNamespaceTags(nil), useV2ExternalIDAuth(prov)),
+		framework.Compose(withSessionTags, f, SimpleSyncWithNamespaceTags(nil), useV2SessionTagsAuth(prov)),
+	)
+})
+
+type ProviderV2 struct {
+	access    awsAccessConfig
+	backend   *secretsManagerBackend
+	framework *framework.Framework
+}
+
+func NewProviderV2(f *framework.Framework) *ProviderV2 {
+	access := loadAWSAccessConfigFromEnv()
+	f.MakeRemoteRefKey = func(base string) string {
+		if f.Namespace == nil {
+			return base
+		}
+		suffix := f.Namespace.Name
+		if len(suffix) > 8 {
+			suffix = suffix[len(suffix)-8:]
+		}
+		if suffix == "" {
+			return base
+		}
+		return fmt.Sprintf("%s-%s", base, suffix)
+	}
+	prov := &ProviderV2{
+		access:    access,
+		backend:   newSecretsManagerBackend(f, access),
+		framework: f,
+	}
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			return
+		}
+		skipIfAWSStaticCredentialsMissing(access)
+	})
+
+	return prov
+}
+
+func (p *ProviderV2) CreateSecret(key string, val framework.SecretEntry) {
+	p.backend.CreateSecret(key, val)
+}
+
+func (p *ProviderV2) DeleteSecret(key string) {
+	p.backend.DeleteSecret(key)
+}
+
+func useV2StaticAuth(prov *ProviderV2) func(*framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		tc.Prepare = prov.prepareNamespacedProvider(awsAuthProfileStatic)
+	}
+}
+
+func useV2ExternalIDAuth(prov *ProviderV2) func(*framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		tc.Prepare = prov.prepareNamespacedProvider(awsAuthProfileExternalID)
+	}
+}
+
+func useV2SessionTagsAuth(prov *ProviderV2) func(*framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		tc.Prepare = prov.prepareNamespacedProvider(awsAuthProfileSessionTags)
+	}
+}
+
+func (p *ProviderV2) prepareNamespacedProvider(profile awsAuthProfile) func(*framework.TestCase, framework.SecretStoreProvider) {
+	return p.prepareNamespacedProviderAtAddress(profile, frameworkv2.ProviderAddress("aws"))
+}
+
+func (p *ProviderV2) prepareNamespacedProviderAtAddress(profile awsAuthProfile, address string) func(*framework.TestCase, framework.SecretStoreProvider) {
+	return func(_ *framework.TestCase, _ framework.SecretStoreProvider) {
+		skipIfAWSAssumeRoleProbeDenied(p.access, profile)
+
+		configName := p.providerConfigName(profile)
+		createSecretsManagerV2Config(p.framework, p.framework.Namespace.Name, configName, p.access, profile)
+		frameworkv2.CreateProviderConnection(
+			p.framework,
+			p.framework.Namespace.Name,
+			p.framework.Namespace.Name,
+			address,
+			awsProviderAPIVersion,
+			awsv2alpha1.SecretsManagerKind,
+			configName,
+			p.framework.Namespace.Name,
+		)
+		frameworkv2.WaitForProviderConnectionReady(p.framework, p.framework.Namespace.Name, p.framework.Namespace.Name, defaultV2WaitTimeout)
+	}
+}
+
+func (p *ProviderV2) providerConfigName(profile awsAuthProfile) string {
+	return fmt.Sprintf("%s-%s", p.framework.Namespace.Name, profile)
+}

+ 162 - 0
e2e/suites/provider/cases/aws/secretsmanager/push_v2.go

@@ -0,0 +1,162 @@
+/*
+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 aws
+
+import (
+	"context"
+	"fmt"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("[aws] v2 push secret", Label("aws", "secretsmanager", "v2", "push-secret"), func() {
+	f := framework.New("eso-aws-sm-v2-push")
+	prov := NewProviderV2(f)
+	harness := newAWSClusterProviderPushHarness(f, prov)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("push secret",
+		framework.TableFuncWithPushSecret(f, prov, nil),
+		Entry(awsPushSecretImplicitProviderKind(f, prov)),
+		Entry(awsPushSecretRejectsNamespacedRemoteNamespaceOverride(f, prov)),
+		Entry(common.ClusterProviderPushManifestNamespace(f, harness)),
+		Entry(common.ClusterProviderPushProviderNamespace(f, harness)),
+		Entry(common.ClusterProviderPushDeniedByConditions(f, harness)),
+	)
+})
+
+func newAWSClusterProviderPushHarness(f *framework.Framework, prov *ProviderV2) common.ClusterProviderPushHarness {
+	return common.ClusterProviderPushHarness{
+		Prepare: func(_ *framework.TestCase, cfg common.ClusterProviderConfig) *common.ClusterProviderPushRuntime {
+			s := newAWSClusterProviderScenario(f, cfg.Name, cfg.AuthScope, prov.access, prov.backend)
+			clusterProviderName := s.createClusterProvider(cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.ClusterProviderPushRuntime{
+				ClusterProviderName:    clusterProviderName,
+				DefaultRemoteNamespace: "",
+				WaitForRemoteSecretValue: func(_, name, key, expectedValue string) {
+					s.waitForRemoteSecretValue(name, key, expectedValue)
+				},
+				ExpectNoRemoteSecret: func(_, name string) {
+					s.backend.ExpectSecretAbsent(name)
+				},
+			}
+		},
+	}
+}
+
+func (s *awsClusterProviderScenario) waitForRemoteSecretValue(name, key, expectedValue string) {
+	if key == "" {
+		s.backend.WaitForSecretValue(name, expectedValue)
+		return
+	}
+	s.backend.WaitForSecretValue(name, fmt.Sprintf(`{"%s":"%s"}`, key, expectedValue))
+}
+
+func awsPushSecretImplicitProviderKind(f *framework.Framework, prov *ProviderV2) (string, func(*framework.TestCase)) {
+	return "[aws] should support namespaced Provider refs when push kind is omitted", func(tc *framework.TestCase) {
+		remoteKey := f.MakeRemoteRefKey("aws-v2-push-implicit")
+		tc.Prepare = prov.prepareNamespacedProvider(awsAuthProfileStatic)
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "aws-v2-push-implicit-source",
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte("value1"),
+			},
+		}
+		tc.PushSecret.ObjectMeta.Name = "aws-v2-push-implicit"
+		tc.PushSecret.Spec.DeletionPolicy = esv1alpha1.PushSecretDeletionPolicyDelete
+		tc.PushSecret.Spec.SecretStoreRefs[0].Kind = ""
+		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
+			Secret: &esv1alpha1.PushSecretSecret{
+				Name: tc.PushSecretSource.Name,
+			},
+		}
+		tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{{
+			Match: esv1alpha1.PushSecretMatch{
+				SecretKey: "value",
+				RemoteRef: esv1alpha1.PushSecretRemoteRef{
+					RemoteKey: remoteKey,
+					Property:  "value",
+				},
+			},
+		}}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			awscommon.WaitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+			prov.backend.WaitForSecretValue(remoteKey, `{"value":"value1"}`)
+
+			Expect(tc.Framework.CRClient.Delete(context.Background(), ps)).To(Succeed())
+			prov.backend.ExpectSecretAbsent(remoteKey)
+		}
+	}
+}
+
+func awsPushSecretRejectsNamespacedRemoteNamespaceOverride(f *framework.Framework, prov *ProviderV2) (string, func(*framework.TestCase)) {
+	return "[aws] should reject remote namespace overrides when pushing through a namespaced Provider", func(tc *framework.TestCase) {
+		remoteKey := f.MakeRemoteRefKey("aws-v2-push-override")
+		tc.Prepare = prov.prepareNamespacedProvider(awsAuthProfileStatic)
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "aws-v2-push-override-source",
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte("should-not-push"),
+			},
+		}
+		tc.PushSecret.ObjectMeta.Name = "aws-v2-push-override"
+		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
+			Secret: &esv1alpha1.PushSecretSecret{
+				Name: tc.PushSecretSource.Name,
+			},
+		}
+		tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{{
+			Match: esv1alpha1.PushSecretMatch{
+				SecretKey: "value",
+				RemoteRef: esv1alpha1.PushSecretRemoteRef{
+					RemoteKey: remoteKey,
+					Property:  "value",
+				},
+			},
+			Metadata: awscommon.PushSecretMetadataWithRemoteNamespace("ignored-aws-namespace"),
+		}}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			awscommon.WaitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionFalse)
+			prov.backend.ExpectSecretAbsent(remoteKey)
+			awscommon.ExpectPushSecretEventMessage(tc.Framework, ps.Namespace, ps.Name, `unknown field "remoteNamespace"`)
+		}
+	}
+}

+ 3 - 4
e2e/suites/provider/cases/aws/secretsmanager/secretsmanager.go

@@ -17,14 +17,13 @@ limitations under the License.
 package aws
 
 import (
-
-	// nolint
-	. "github.com/onsi/ginkgo/v2"
-
 	"github.com/external-secrets/external-secrets-e2e/framework"
 	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
 	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
 	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+
+	// nolint
+	. "github.com/onsi/ginkgo/v2"
 )
 
 const (

+ 3 - 4
e2e/suites/provider/cases/aws/secretsmanager/secretsmanager_managed.go

@@ -17,14 +17,13 @@ limitations under the License.
 package aws
 
 import (
-
-	// nolint
-	. "github.com/onsi/ginkgo/v2"
-
 	"github.com/external-secrets/external-secrets-e2e/framework"
 	"github.com/external-secrets/external-secrets-e2e/framework/addon"
 	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
 	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+
+	// nolint
+	. "github.com/onsi/ginkgo/v2"
 )
 
 // here we use the global eso instance

+ 88 - 0
e2e/suites/provider/cases/aws/secretsmanager/secretsmanager_v2_managed.go

@@ -0,0 +1,88 @@
+/*
+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 aws
+
+import (
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	"github.com/external-secrets/external-secrets-e2e/framework/addon"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+
+	. "github.com/onsi/ginkgo/v2"
+)
+
+var _ = Describe("[awsmanaged] v2 IRSA via referenced service account", Label("aws", "secretsmanager", "managed", "v2"), Ordered, func() {
+	f := framework.New("eso-aws-managed-v2-ref")
+	prov := NewProviderV2(f)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+		skipIfAWSManagedIRSAEnvMissing(prov.access)
+	})
+
+	DescribeTable("sync secretsmanager secrets",
+		framework.TableFuncWithExternalSecret(f, prov),
+		framework.Compose(awscommon.WithReferencedIRSA, f, common.SimpleDataSync, useV2ReferencedIRSA(prov)),
+		framework.Compose(awscommon.WithReferencedIRSA, f, common.FindByName, useV2ReferencedIRSA(prov)),
+	)
+})
+
+var _ = Describe("[awsmanaged] v2 with mounted IRSA", Label("aws", "secretsmanager", "managed", "v2"), Ordered, func() {
+	f := framework.New("eso-aws-managed-v2-mounted")
+	prov := NewProviderV2(f)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+		skipIfAWSManagedIRSAEnvMissing(prov.access)
+
+		f.Install(addon.NewESO(
+			addon.WithControllerClass(f.BaseName+"-mounted"),
+			addon.WithReleaseName(f.Namespace.Name),
+			addon.WithNamespace(prov.access.SANamespace),
+			addon.WithoutWebhook(),
+			addon.WithoutCertController(),
+			addon.WithV2AWSProvider(),
+			addon.WithV2ProviderServiceAccount("aws", prov.access.SAName),
+		))
+	})
+
+	DescribeTable("sync secretsmanager secrets",
+		framework.TableFuncWithExternalSecret(f, prov),
+		framework.Compose(awscommon.WithMountedIRSA, f, common.SimpleDataSync, useV2MountedIRSA(prov)),
+		framework.Compose(awscommon.WithMountedIRSA, f, common.FindByName, useV2MountedIRSA(prov)),
+	)
+})
+
+func useV2ReferencedIRSA(prov *ProviderV2) func(*framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		tc.Prepare = prov.prepareNamespacedProvider(awsAuthProfileReferencedIRSA)
+	}
+}
+
+func useV2MountedIRSA(prov *ProviderV2) func(*framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		tc.Prepare = prov.prepareNamespacedProviderAtAddress(
+			awsAuthProfileMountedIRSA,
+			frameworkv2.ProviderAddressInNamespace("aws", prov.access.SANamespace),
+		)
+	}
+}

+ 132 - 0
e2e/suites/provider/cases/aws/v2_support.go

@@ -0,0 +1,132 @@
+/*
+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 common
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+
+	. "github.com/onsi/gomega"
+)
+
+const (
+	StaticAccessKeyIDKey     = "kid"
+	StaticSecretAccessKeyKey = "sak"
+	StaticSessionTokenKey    = "st"
+)
+
+type V2ClusterProviderScenario struct {
+	AuthScope            esv1.AuthenticationScope
+	ConfigName           string
+	ConfigNamespace      string
+	NamePrefix           string
+	ProviderNamespace    string
+	ProviderRefNamespace string
+	WorkloadNamespace    string
+}
+
+func CredentialsSecretName(name string) string {
+	return name + "-credentials"
+}
+
+func StaticCredentialsSecretData(kid, sak, st string) map[string]string {
+	return map[string]string{
+		StaticAccessKeyIDKey:     kid,
+		StaticSecretAccessKeyKey: sak,
+		StaticSessionTokenKey:    st,
+	}
+}
+
+func ProviderConfigNamespace(authScope esv1.AuthenticationScope, providerNamespace, workloadNamespace string) string {
+	if authScope == esv1.AuthenticationScopeProviderNamespace {
+		return providerNamespace
+	}
+	return workloadNamespace
+}
+
+func ProviderReferenceNamespace(authScope esv1.AuthenticationScope, providerNamespace string) string {
+	if authScope == esv1.AuthenticationScopeProviderNamespace {
+		return providerNamespace
+	}
+	return ""
+}
+
+func NewV2ClusterProviderScenario(workloadNamespace, prefix string, authScope esv1.AuthenticationScope, createProviderNamespace func(prefix string) string) V2ClusterProviderScenario {
+	providerNamespace := workloadNamespace
+	if authScope == esv1.AuthenticationScopeProviderNamespace && createProviderNamespace != nil {
+		providerNamespace = createProviderNamespace(prefix + "-provider")
+	}
+
+	return V2ClusterProviderScenario{
+		AuthScope:            authScope,
+		ConfigName:           fmt.Sprintf("%s-config", prefix),
+		ConfigNamespace:      ProviderConfigNamespace(authScope, providerNamespace, workloadNamespace),
+		NamePrefix:           fmt.Sprintf("%s-%s", workloadNamespace, prefix),
+		ProviderNamespace:    providerNamespace,
+		ProviderRefNamespace: ProviderReferenceNamespace(authScope, providerNamespace),
+		WorkloadNamespace:    workloadNamespace,
+	}
+}
+
+func (s V2ClusterProviderScenario) ClusterProviderName() string {
+	return fmt.Sprintf("%s-cluster-provider", s.NamePrefix)
+}
+
+func WaitForPushSecretStatus(f *framework.Framework, namespace, name string, status corev1.ConditionStatus) {
+	Eventually(func(g Gomega) {
+		var ps esv1alpha1.PushSecret
+		g.Expect(f.CRClient.Get(context.Background(), types.NamespacedName{Name: name, Namespace: namespace}, &ps)).To(Succeed())
+		g.Expect(ps.Status.Conditions).NotTo(BeEmpty())
+		for _, condition := range ps.Status.Conditions {
+			if condition.Type == esv1alpha1.PushSecretReady && condition.Status == status {
+				return
+			}
+		}
+		g.Expect(false).To(BeTrue())
+	}, time.Minute, 5*time.Second).Should(Succeed())
+}
+
+func ExpectPushSecretEventMessage(f *framework.Framework, namespace, objectName, expectedMessage string) {
+	Eventually(func() string {
+		events, err := f.KubeClientSet.CoreV1().Events(namespace).List(context.Background(), metav1.ListOptions{
+			FieldSelector: "involvedObject.name=" + objectName + ",involvedObject.kind=PushSecret",
+		})
+		Expect(err).NotTo(HaveOccurred())
+
+		messages := make([]string, 0, len(events.Items))
+		for _, event := range events.Items {
+			if event.Message != "" {
+				messages = append(messages, event.Message)
+			}
+		}
+		return fmt.Sprintf("%v", messages)
+	}, time.Minute, 5*time.Second).Should(ContainSubstring(expectedMessage))
+}
+
+func PushSecretMetadataWithRemoteNamespace(namespace string) *apiextensionsv1.JSON {
+	return &apiextensionsv1.JSON{Raw: []byte(fmt.Sprintf(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"remoteNamespace":"%s"}}`, namespace))}
+}