Repository: crossplane/provider-aws (go)

Issue:
Enhance the assumeRoleARN authentication to apply session tags in AWS STS 
### What problem are you facing?
We are creating a provider config per namespace and are trying to create a scalable way to provide the correct amount of access to the aws-provider for each configuration.  The process we use outside of crossplane is to have an IAM user (could be a role) in one account and it can assume some default roles that are in all of our other accounts.  Those roles look at session tags to make sure that what you are deploying/changing have the same tag on the resource you are creating or deploying.

### How could Crossplane help solve your problem?

If crossplane (aws-provider) was able to pass tags to the STS session, we would be able to use our standard roles in all of the accounts that we have.  It would make it so much easier to scale.

Pull request description:
Add support for IAM AssumeRole tags
Signed-off-by: Steven Borrelli <steve@borrelli.org>


### Description of your changes

Fixes #1212


This PR adds the ability to add session tags and transitive tag keys (see <https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html>) to the IAM Assume Role functionality settings in the ProviderConfig.

The top-level settings for `assumeRoleARN` and `axternalID` have been marked as deprecated in favor of putting all options into the `assumeRole` stanza, as per the example below:

```yaml
apiVersion: aws.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
  name: aws-provider-account-b
spec:
  assumeRole:
    roleARN: "arn:aws:iam::999999999999:role/account_b"
    externalID: "my-optional-id"
    tags:
      - key: Project
        value: Crossplane
      - key: Department
        value: Infrastructure   
    transitiveTagKeys: [ "Project", "Department"]
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: aws-creds
      key: creds
```

I have:

- [ ] Read and followed Crossplane's [contribution process].
- [ ] Run `make reviewable test` to ensure this PR is ready for review.

### How has this code been tested


Patch:
diff --git a/AUTHENTICATION.md b/AUTHENTICATION.md
index c0d8d9279..25b2f5814 100644
--- a/AUTHENTICATION.md
+++ b/AUTHENTICATION.md
@@ -2,10 +2,14 @@
 
 ## Table of Contents
 
-- [Overview](#overview)
-- [Using IAM Roles for `ServiceAccounts`](#using-iam-roles-for-serviceaccounts)
-- [Using kube2iam](#using-kube2iam)
-- [Using `assumeRoleARN`](#using-assumerolearn)
+- [Authenticating to AWS API](#authenticating-to-aws-api)
+  - [Table of Contents](#table-of-contents)
+  - [Overview](#overview)
+  - [Using IAM Roles for `ServiceAccounts`](#using-iam-roles-for-serviceaccounts)
+    - [Steps](#steps)
+  - [Using kube2iam](#using-kube2iam)
+    - [Steps](#steps-1)
+  - [Using `assumeRole`](#using-assumerole)
 
 ## Overview
 
@@ -285,22 +289,22 @@ EOF
 
 *Note: Because the name of the `ProviderConfig` is `default` it will be used by any managed resources that do not explicitly reference a `ProviderConfig`.*
 
-## Using `assumeRoleARN`
+## Using `assumeRole`
 
 `provider-aws` will be configured to connect to AWS Account *A* via `InjectedIdentity`,
-request temporary security credentials, and then `assumeRoleARN` to assume
+request temporary security credentials, and then setting `RoleARN` to assume
 a role in AWS Account *B* to manage the resources within AWS Account *B*.
 
-The first thing that needs to be done is to create an IAM role within AWS Account *B* that `provider-aws` will `assumeRoleARN` into.
+The first thing that needs to be done is to create an IAM role within AWS Account *B* that `provider-aws` will Assume into.
 
 - From within the AWS console of AWS Account *B*, navigate to `IAM > Roles > Create role > Another AWS account`.
-  - Enter the Account ID of Account *A* (the account `provider-aws` will call `assumeRoleARN` from).
+  - Enter the Account ID of Account *A* (the account `provider-aws` will call `roleARN` from).
   - (Optional) Check the box for `Require external ID`. This ensures requests
     coming from Account *A* can only use 'assumeRoleARN' if these requests pass the specified `externalID`.
 
-Next, the `provider-aws` must be configured to use `assumeRoleARN`.
+Next, the `provider-aws` must be configured to use `assumeRole`.
 The code snippet below shows how to configure `provider-aws` to connect to
-AWS Account *A* and `assumeRoleARN` into a role within AWS Account *B*.
+AWS Account *A* and `assumeRole` into a role within AWS Account *B*.
 
 ```console
 $ cat <<EOF | kubectl apply -f -
@@ -309,7 +313,32 @@ kind: ProviderConfig
 metadata:
   name: account-b
 spec:
-  assumeRoleARN: "arn:aws:iam::999999999999:role/account-b"
+  assumeRole:
+    roleARN: "arn:aws:iam::999999999999:role/account_b"
+    externalID: "my-optional-id"
+  credentials:
+    source: InjectedIdentity
+EOF
+```
+
+Session tags and TransitiveKey tags are supported (see <https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html>).
+
+```console
+$ cat <<EOF | kubectl apply -f -
+apiVersion: aws.crossplane.io/v1beta1
+kind: ProviderConfig
+metadata:
+  name: account-b
+spec:
+  assumeRole:
+    roleARN: "arn:aws:iam::999999999999:role/account_b"
+    externalID: "my-optional-id"
+    tags:
+      - key: Project
+        value: Crossplane
+      - key: Department
+        value: Infrastructure   
+    transitiveTagKeys: [ "Project", "Department"]
   credentials:
     source: InjectedIdentity
 EOF
diff --git a/apis/v1beta1/providerconfig_types.go b/apis/v1beta1/providerconfig_types.go
index d1c25af5d..0291da374 100644
--- a/apis/v1beta1/providerconfig_types.go
+++ b/apis/v1beta1/providerconfig_types.go
@@ -27,11 +27,16 @@ type ProviderConfigSpec struct {
 	// Credentials required to authenticate to this provider.
 	Credentials ProviderCredentials `json:"credentials"`
 
+	// AssumeRole defines the options for assuming an IAM role
+	AssumeRole *AssumeRoleOptions `json:"assumeRole,omitempty"`
+
 	// AssumeRoleARN to assume with provider credentials
+	// This setting will be deprecated. Use the roleARN field under assumeRole instead.
 	// +optional
 	AssumeRoleARN *string `json:"assumeRoleARN,omitempty"`
 
 	// ExternalID is the external ID used when assuming role.
+	// This setting will be deprecated. Use the externalID field under assumeRole instead.
 	// +optional
 	ExternalID *string `json:"externalID,omitempty"`
 
@@ -50,6 +55,42 @@ type ProviderCredentials struct {
 	xpv1.CommonCredentialSelectors `json:",inline"`
 }
 
+// Tag is session tag that can be used to assume an IAM Role
+type Tag struct {
+	// Name of the tag.
+	// Key is a required field
+	Key *string `json:"key"`
+
+	// Value of the tag.
+	// Value is a required field
+	Value *string `json:"value"`
+}
+
+// AssumeRoleOptions define the options for assuming an IAM Role
+// Fields are similar to the STS AssumeRoleOptions in the AWS SDK
+type AssumeRoleOptions struct {
+	// AssumeRoleARN to assume with provider credentials
+	RoleARN *string `json:"roleARN,omitempty"`
+
+	// ExternalID is the external ID used when assuming role.
+	// +optional
+	ExternalID *string `json:"externalID,omitempty"`
+
+	// Tags is list of session tags that you want to pass. Each session tag consists of a key
+	// name and an associated value. For more information about session tags, see
+	// Tagging STS Sessions
+	// (https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html).
+	// +optional
+	Tags []Tag `json:"tags,omitempty"`
+
+	// TransitiveTagKeys is a list of keys for session tags that you want to set as transitive. If you set a
+	// tag key as transitive, the corresponding key and value passes to subsequent
+	// sessions in a role chain. For more information, see Chaining Roles with Session Tags
+	// (https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_role-chaining).
+	// +optional
+	TransitiveTagKeys []string `json:"transitiveTagKeys,omitempty"`
+}
+
 // EndpointConfig is used to configure the AWS client for a custom endpoint.
 type EndpointConfig struct {
 	// URL lets you configure the endpoint URL to be used in SDK calls.
diff --git a/apis/v1beta1/zz_generated.deepcopy.go b/apis/v1beta1/zz_generated.deepcopy.go
index d1838759a..392bceaab 100644
--- a/apis/v1beta1/zz_generated.deepcopy.go
+++ b/apis/v1beta1/zz_generated.deepcopy.go
@@ -25,6 +25,43 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AssumeRoleOptions) DeepCopyInto(out *AssumeRoleOptions) {
+	*out = *in
+	if in.RoleARN != nil {
+		in, out := &in.RoleARN, &out.RoleARN
+		*out = new(string)
+		**out = **in
+	}
+	if in.ExternalID != nil {
+		in, out := &in.ExternalID, &out.ExternalID
+		*out = new(string)
+		**out = **in
+	}
+	if in.Tags != nil {
+		in, out := &in.Tags, &out.Tags
+		*out = make([]Tag, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.TransitiveTagKeys != nil {
+		in, out := &in.TransitiveTagKeys, &out.TransitiveTagKeys
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AssumeRoleOptions.
+func (in *AssumeRoleOptions) DeepCopy() *AssumeRoleOptions {
+	if in == nil {
+		return nil
+	}
+	out := new(AssumeRoleOptions)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *DynamicURLConfig) DeepCopyInto(out *DynamicURLConfig) {
 	*out = *in
@@ -149,6 +186,11 @@ func (in *ProviderConfigList) DeepCopyObject() runtime.Object {
 func (in *ProviderConfigSpec) DeepCopyInto(out *ProviderConfigSpec) {
 	*out = *in
 	in.Credentials.DeepCopyInto(&out.Credentials)
+	if in.AssumeRole != nil {
+		in, out := &in.AssumeRole, &out.AssumeRole
+		*out = new(AssumeRoleOptions)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.AssumeRoleARN != nil {
 		in, out := &in.AssumeRoleARN, &out.AssumeRoleARN
 		*out = new(string)
@@ -266,6 +308,31 @@ func (in *ProviderCredentials) DeepCopy() *ProviderCredentials {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Tag) DeepCopyInto(out *Tag) {
+	*out = *in
+	if in.Key != nil {
+		in, out := &in.Key, &out.Key
+		*out = new(string)
+		**out = **in
+	}
+	if in.Value != nil {
+		in, out := &in.Value, &out.Value
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Tag.
+func (in *Tag) DeepCopy() *Tag {
+	if in == nil {
+		return nil
+	}
+	out := new(Tag)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *URLConfig) DeepCopyInto(out *URLConfig) {
 	*out = *in
diff --git a/examples/providerconfig/assumeRoleARN-injected.yaml b/examples/providerconfig/assumeRoleARN-injected.yaml
index d159b73b2..0ed2bb809 100644
--- a/examples/providerconfig/assumeRoleARN-injected.yaml
+++ b/examples/providerconfig/assumeRoleARN-injected.yaml
@@ -3,6 +3,14 @@ kind: ProviderConfig
 metadata:
   name: aws-provider-injected-account-b
 spec:
-  assumeRoleARN: "arn:aws:iam::999999999999:role/account_b"
+  assumeRole:
+    roleARN: "arn:aws:iam::999999999999:role/account_b"
+    externalID: "my-optional-id"
+    tags:
+      - key: Project
+        value: Crossplane
+      - key: Department
+        value: Infrastructure   
+    transitiveTagKeys: [ "Project", "Department"]
   credentials:
     source: InjectedIdentity
diff --git a/examples/providerconfig/assumeRoleARN.yaml b/examples/providerconfig/assumeRoleARN.yaml
index 49f2dfac7..738b31f51 100644
--- a/examples/providerconfig/assumeRoleARN.yaml
+++ b/examples/providerconfig/assumeRoleARN.yaml
@@ -3,7 +3,15 @@ kind: ProviderConfig
 metadata:
   name: aws-provider-account-b
 spec:
-  assumeRoleARN: "arn:aws:iam::999999999999:role/account_b"
+  assumeRole:
+    roleARN: "arn:aws:iam::999999999999:role/account_b"
+    externalID: "my-optional-id"
+    tags:
+      - key: Project
+        value: Crossplane
+      - key: Department
+        value: Infrastructure   
+    transitiveTagKeys: [ "Project", "Department"]
   credentials:
     source: Secret
     secretRef:
diff --git a/package/crds/aws.crossplane.io_providerconfigs.yaml b/package/crds/aws.crossplane.io_providerconfigs.yaml
index ad43c8867..d6b6cf48e 100644
--- a/package/crds/aws.crossplane.io_providerconfigs.yaml
+++ b/package/crds/aws.crossplane.io_providerconfigs.yaml
@@ -48,8 +48,50 @@ spec:
           spec:
             description: A ProviderConfigSpec defines the desired state of a ProviderConfig.
             properties:
+              assumeRole:
+                description: AssumeRole defines the options for assuming an IAM role
+                properties:
+                  externalID:
+                    description: ExternalID is the external ID used when assuming
+                      role.
+                    type: string
+                  roleARN:
+                    description: AssumeRoleARN to assume with provider credentials
+                    type: string
+                  tags:
+                    description: Tags is list of session tags that you want to pass.
+                      Each session tag consists of a key name and an associated value.
+                      For more information about session tags, see Tagging STS Sessions
+                      (https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html).
+                    items:
+                      description: Tag is session tag that can be used to assume an
+                        IAM Role
+                      properties:
+                        key:
+                          description: Name of the tag. Key is a required field
+                          type: string
+                        value:
+                          description: Value of the tag. Value is a required field
+                          type: string
+                      required:
+                      - key
+                      - value
+                      type: object
+                    type: array
+                  transitiveTagKeys:
+                    description: TransitiveTagKeys is a list of keys for session tags
+                      that you want to set as transitive. If you set a tag key as
+                      transitive, the corresponding key and value passes to subsequent
+                      sessions in a role chain. For more information, see Chaining
+                      Roles with Session Tags (https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_role-chaining).
+                    items:
+                      type: string
+                    type: array
+                type: object
               assumeRoleARN:
-                description: AssumeRoleARN to assume with provider credentials
+                description: AssumeRoleARN to assume with provider credentials This
+                  setting will be deprecated. Use the roleARN field under assumeRole
+                  instead.
                 type: string
               credentials:
                 description: Credentials required to authenticate to this provider.
@@ -210,6 +252,8 @@ spec:
                 type: object
               externalID:
                 description: ExternalID is the external ID used when assuming role.
+                  This setting will be deprecated. Use the externalID field under
+                  assumeRole instead.
                 type: string
             required:
             - credentials
diff --git a/pkg/clients/aws.go b/pkg/clients/aws.go
index ac17741f0..dcc1343ac 100644
--- a/pkg/clients/aws.go
+++ b/pkg/clients/aws.go
@@ -30,6 +30,8 @@ import (
 	"github.com/aws/aws-sdk-go-v2/config"
 	"github.com/aws/aws-sdk-go-v2/credentials"
 	stscreds "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
+	stscredstypesv2 "github.com/aws/aws-sdk-go-v2/service/sts/types"
+
 	ec2type "github.com/aws/aws-sdk-go-v2/service/ec2/types"
 	"github.com/aws/aws-sdk-go-v2/service/sts"
 	awsv1 "github.com/aws/aws-sdk-go/aws"
@@ -108,7 +110,7 @@ func UseProviderConfig(ctx context.Context, c client.Client, mg resource.Managed
 
 	switch s := pc.Spec.Credentials.Source; s { //nolint:exhaustive
 	case xpv1.CredentialsSourceInjectedIdentity:
-		if pc.Spec.AssumeRoleARN != nil {
+		if pc.Spec.AssumeRole != nil || pc.Spec.AssumeRoleARN != nil {
 			cfg, err := UsePodServiceAccountAssumeRole(ctx, []byte{}, DefaultSection, region, pc)
 			if err != nil {
 				return nil, err
@@ -125,7 +127,7 @@ func UseProviderConfig(ctx context.Context, c client.Client, mg resource.Managed
 		if err != nil {
 			return nil, errors.Wrap(err, "cannot get credentials")
 		}
-		if pc.Spec.AssumeRoleARN != nil {
+		if pc.Spec.AssumeRole != nil || pc.Spec.AssumeRoleARN != nil {
 			cfg, err := UseProviderSecretAssumeRole(ctx, data, DefaultSection, region, pc)
 			if err != nil {
 				return nil, err
@@ -296,12 +298,22 @@ func UseProviderSecretAssumeRole(ctx context.Context, data []byte, profile, regi
 	config, err := config.LoadDefaultConfig(ctx, config.WithRegion(region), config.WithCredentialsProvider(credentials.StaticCredentialsProvider{
 		Value: creds,
 	}))
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to load default AWS config")
+	}
+
+	roleArn, err := GetAssumeRoleARN(pc.Spec.DeepCopy())
+	if err != nil {
+		return nil, err
+	}
 
 	stsSvc := sts.NewFromConfig(config)
+
+	stsAssumeRoleOptions := SetAssumeRoleOptions(pc)
 	stsAssume := stscreds.NewAssumeRoleProvider(
 		stsSvc,
-		StringValue(pc.Spec.AssumeRoleARN),
-		func(opt *stscreds.AssumeRoleOptions) { opt.ExternalID = pc.Spec.ExternalID },
+		StringValue(roleArn),
+		stsAssumeRoleOptions,
 	)
 	config.Credentials = aws.NewCredentialsCache(stsAssume)
 
@@ -316,15 +328,22 @@ func UsePodServiceAccountAssumeRole(ctx context.Context, _ []byte, _, region str
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to load default AWS config")
 	}
+
+	roleArn, err := GetAssumeRoleARN(pc.Spec.DeepCopy())
+	if err != nil {
+		return nil, err
+	}
+
 	stsclient := sts.NewFromConfig(cfg)
+	stsAssumeRoleOptions := SetAssumeRoleOptions(pc)
 	cnf, err := config.LoadDefaultConfig(
 		ctx,
 		config.WithRegion(region),
 		config.WithCredentialsProvider(aws.NewCredentialsCache(
 			stscreds.NewAssumeRoleProvider(
 				stsclient,
-				StringValue(pc.Spec.AssumeRoleARN),
-				func(opt *stscreds.AssumeRoleOptions) { opt.ExternalID = pc.Spec.ExternalID },
+				StringValue(roleArn),
+				stsAssumeRoleOptions,
 			)),
 		),
 	)
@@ -385,7 +404,7 @@ func GetConfigV1(ctx context.Context, c client.Client, mg resource.Managed, regi
 			return nil, errors.Wrap(err, "cannot get credentials")
 		}
 
-		if pc.Spec.AssumeRoleARN != nil {
+		if pc.Spec.AssumeRole != nil || pc.Spec.AssumeRoleARN != nil {
 			cfg, err := UseProviderSecretV1AssumeRole(ctx, data, pc, DefaultSection, region)
 			if err != nil {
 				return nil, errors.Wrap(err, "cannot use secret")
@@ -415,11 +434,17 @@ func UseProviderSecretV1AssumeRole(ctx context.Context, data []byte, pc *v1beta1
 		return nil, errors.Wrap(err, "failed to load credentials")
 	}
 
+	roleArn, err := GetAssumeRoleARN(pc.Spec.DeepCopy())
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to assume IAM Role")
+	}
+
 	stsSvc := sts.NewFromConfig(config)
+	stsAssumeRoleOptions := SetAssumeRoleOptions(pc)
 	stsAssume := stscreds.NewAssumeRoleProvider(
 		stsSvc,
-		StringValue(pc.Spec.AssumeRoleARN),
-		func(opt *stscreds.AssumeRoleOptions) { opt.ExternalID = pc.Spec.ExternalID },
+		StringValue(roleArn),
+		stsAssumeRoleOptions,
 	)
 	config.Credentials = aws.NewCredentialsCache(stsAssume)
 
@@ -476,15 +501,21 @@ func UsePodServiceAccountV1AssumeRole(ctx context.Context, _ []byte, pc *v1beta1
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to load default AWS config")
 	}
+
+	roleArn, err := GetAssumeRoleARN(pc.Spec.DeepCopy())
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to assume IAM Role")
+	}
 	stsclient := sts.NewFromConfig(cfg)
+	stsAssumeRoleOptions := SetAssumeRoleOptions(pc)
 	cnf, err := config.LoadDefaultConfig(
 		ctx,
 		config.WithRegion(region),
 		config.WithCredentialsProvider(aws.NewCredentialsCache(
 			stscreds.NewAssumeRoleProvider(
 				stsclient,
-				StringValue(pc.Spec.AssumeRoleARN),
-				func(opt *stscreds.AssumeRoleOptions) { opt.ExternalID = pc.Spec.ExternalID },
+				StringValue(roleArn),
+				stsAssumeRoleOptions,
 			)),
 		),
 	)
@@ -573,6 +604,52 @@ func SetResolverV1(pc *v1beta1.ProviderConfig, cfg *awsv1.Config) *awsv1.Config
 	return cfg
 }
 
+// GetAssumeRoleARN gets the AssumeRoleArn from a ProviderConfigSpec
+func GetAssumeRoleARN(pcs *v1beta1.ProviderConfigSpec) (*string, error) {
+	if pcs.AssumeRole != nil {
+		if pcs.AssumeRole.RoleARN != nil && StringValue(pcs.AssumeRole.RoleARN) != "" {
+			return pcs.AssumeRole.RoleARN, nil
+		}
+	}
+
+	// Deprecated. Use AssumeRole.RoleARN
+	if pcs.AssumeRoleARN != nil {
+		return pcs.AssumeRoleARN, nil
+	}
+
+	return nil, errors.New("a RoleARN must be set to assume an IAM Role")
+}
+
+// SetAssumeRoleOptions sets options when Assuming an IAM Role
+func SetAssumeRoleOptions(pc *v1beta1.ProviderConfig) func(*stscreds.AssumeRoleOptions) {
+	if pc.Spec.AssumeRole != nil {
+		return func(opt *stscreds.AssumeRoleOptions) {
+			if pc.Spec.AssumeRole.ExternalID != nil {
+				opt.ExternalID = pc.Spec.AssumeRole.ExternalID
+			}
+
+			if pc.Spec.AssumeRole.Tags != nil && len(pc.Spec.AssumeRole.Tags) > 0 {
+				for _, t := range pc.Spec.AssumeRole.Tags {
+					opt.Tags = append(
+						opt.Tags,
+						stscredstypesv2.Tag{Key: t.Key, Value: t.Value})
+				}
+			}
+
+			if pc.Spec.AssumeRole.TransitiveTagKeys != nil && len(pc.Spec.AssumeRole.TransitiveTagKeys) > 0 {
+				opt.TransitiveTagKeys = pc.Spec.AssumeRole.TransitiveTagKeys
+			}
+		}
+	}
+
+	// Deprecated. Use AssumeRole.ExternalID
+	if pc.Spec.ExternalID != nil {
+		return func(opt *stscreds.AssumeRoleOptions) { opt.ExternalID = pc.Spec.ExternalID }
+	}
+
+	return func(opt *stscreds.AssumeRoleOptions) {}
+}
+
 // TODO(muvaf): All the types that use CreateJSONPatch are known during
 // development time. In order to avoid unnecessary panic checks, we can generate
 // the code that creates a patch between two objects that share the same type.


Test patch:
diff --git a/pkg/clients/aws_test.go b/pkg/clients/aws_test.go
index 511ad1434..f8484a994 100644
--- a/pkg/clients/aws_test.go
+++ b/pkg/clients/aws_test.go
@@ -23,7 +23,9 @@ import (
 
 	"github.com/aws/aws-sdk-go-v2/aws"
 	"github.com/aws/aws-sdk-go-v2/aws/transport/http"
+	stscreds "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
 	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
+	stscredstypesv2 "github.com/aws/aws-sdk-go-v2/service/sts/types"
 	"github.com/aws/smithy-go"
 	"github.com/aws/smithy-go/document"
 	smithyhttp "github.com/aws/smithy-go/transport/http"
@@ -92,6 +94,212 @@ func TestUseProviderSecret(t *testing.T) {
 	g.Expect(config).NotTo(BeNil())
 }
 
+func TestGetAssumeRoleARN(t *testing.T) {
+	roleARN := "test-arn"
+	roleARNDep := "test-arn-deprecated"
+
+	type args struct {
+		pcs v1beta1.ProviderConfigSpec
+	}
+	type want struct {
+		arn string
+		err error
+	}
+	cases := map[string]struct {
+		args args
+		want want
+	}{
+		"NoArnSetError": {
+			args: args{
+				pcs: v1beta1.ProviderConfigSpec{},
+			},
+			want: want{
+				err: errors.New("a RoleARN must be set to assume an IAM Role"),
+			},
+		},
+		"EmptyAssumeRoleOptions": {
+			args: args{
+				pcs: v1beta1.ProviderConfigSpec{
+					AssumeRole: &v1beta1.AssumeRoleOptions{},
+				},
+			},
+			want: want{
+				err: errors.New("a RoleARN must be set to assume an IAM Role"),
+			},
+		},
+		"AssumeRoleOptions": {
+			args: args{
+				pcs: v1beta1.ProviderConfigSpec{
+					AssumeRole: &v1beta1.AssumeRoleOptions{
+						RoleARN: &roleARN,
+					},
+				},
+			},
+			want: want{
+				arn: "test-arn",
+			},
+		},
+		"IgnoreDeprecatedSetting": {
+			args: args{
+				pcs: v1beta1.ProviderConfigSpec{
+					AssumeRoleARN: &roleARNDep,
+					AssumeRole: &v1beta1.AssumeRoleOptions{
+						RoleARN: &roleARN,
+					},
+				},
+			},
+			want: want{
+				arn: "test-arn",
+			},
+		},
+		"EmptyAssumeRoleOptionsOldSetting": {
+			args: args{
+				pcs: v1beta1.ProviderConfigSpec{
+					AssumeRoleARN: &roleARNDep,
+					AssumeRole:    &v1beta1.AssumeRoleOptions{},
+				},
+			},
+			want: want{
+				arn: "test-arn-deprecated",
+			},
+		},
+		"DeprecatedArn": {
+			args: args{
+				pcs: v1beta1.ProviderConfigSpec{
+					AssumeRoleARN: &roleARNDep,
+				},
+			},
+			want: want{
+				arn: "test-arn-deprecated",
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+
+			roleArn, err := GetAssumeRoleARN(&tc.args.pcs)
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("Wrap: -want, +got:\n%s", diff)
+			}
+
+			if diff := cmp.Diff(tc.want.arn, StringValue(roleArn)); diff != "" {
+				t.Errorf("Wrap: -want, +got:\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestSetAssumeroleOptions(t *testing.T) {
+	externalID := "test-id"
+	externalIDDep := "test-id-deprecated"
+
+	key1 := "key1"
+	value1 := "value1"
+
+	type args struct {
+		pc v1beta1.ProviderConfig
+	}
+	type want struct {
+		aro stscreds.AssumeRoleOptions
+	}
+	cases := map[string]struct {
+		args args
+		want want
+	}{
+
+		"NoOptionsSet": {
+			args: args{
+				pc: v1beta1.ProviderConfig{
+					Spec: v1beta1.ProviderConfigSpec{},
+				},
+			},
+			want: want{
+				aro: stscreds.AssumeRoleOptions{},
+			},
+		},
+		"BasicAssumerole": {
+			args: args{
+				pc: v1beta1.ProviderConfig{
+					Spec: v1beta1.ProviderConfigSpec{
+						AssumeRole: &v1beta1.AssumeRoleOptions{
+							ExternalID: &externalID,
+						},
+					},
+				},
+			},
+			want: want{
+				aro: stscreds.AssumeRoleOptions{
+					ExternalID: &externalID,
+				},
+			},
+		},
+		"SpecExternalIDDeprecated": {
+			args: args{
+				pc: v1beta1.ProviderConfig{
+					Spec: v1beta1.ProviderConfigSpec{
+						ExternalID: &externalIDDep,
+					},
+				},
+			},
+			want: want{
+				aro: stscreds.AssumeRoleOptions{
+					ExternalID: &externalIDDep,
+				},
+			},
+		},
+		"SetTagsAndTransitiveTagKeys": {
+			args: args{
+				pc: v1beta1.ProviderConfig{
+					Spec: v1beta1.ProviderConfigSpec{
+						ExternalID: &externalIDDep, // should be ignored if AssumeRoleOptions set
+						AssumeRole: &v1beta1.AssumeRoleOptions{
+							ExternalID:        &externalID,
+							Tags:              []v1beta1.Tag{{Key: &key1, Value: &value1}},
+							TransitiveTagKeys: []string{"a", "b", "c"},
+						},
+					},
+				},
+			},
+			want: want{
+				aro: stscreds.AssumeRoleOptions{
+					ExternalID:        &externalID,
+					Tags:              []stscredstypesv2.Tag{{Key: &key1, Value: &value1}},
+					TransitiveTagKeys: []string{"a", "b", "c"},
+				},
+			},
+		},
+		"ZeroLengthTags": {
+			args: args{
+				pc: v1beta1.ProviderConfig{
+					Spec: v1beta1.ProviderConfigSpec{
+						AssumeRole: &v1beta1.AssumeRoleOptions{
+							Tags:              []v1beta1.Tag{},
+							TransitiveTagKeys: []string{},
+						},
+					},
+				},
+			},
+			want: want{
+				aro: stscreds.AssumeRoleOptions{},
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+
+			aro := stscreds.AssumeRoleOptions{}
+			f := SetAssumeRoleOptions(&tc.args.pc)
+			f(&aro)
+
+			if diff := cmp.Diff(tc.want.aro, aro, cmpopts.IgnoreUnexported(stscredstypesv2.Tag{})); diff != "" {
+				t.Errorf("Wrap: -want, +got:\n%s", diff)
+			}
+		})
+	}
+}
+
 func TestDiffTags(t *testing.T) {
 	type args struct {
 		local  map[string]string