Skip to content

Commit

Permalink
feat: adds option to remove old user assignment on account update (#223)
Browse files Browse the repository at this point in the history
feat: adds option to remove old user assignment on account update
  • Loading branch information
wanisfahmyDE authored Jan 10, 2025
1 parent 83fd6b3 commit c1f74fb
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 6 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,22 @@ You can test the provider locally before creating a PR by following the steps be

```sh
$ make build # make sure to have the build version in the executable name as a postfix e.g. terraform-provider-controltower_v2.0.0
$ mkdir -p ~/.terraform.d/plugins/registry.terraform.io/idealo/controltower/<some version>/darwin_arm64 # arch can be different depending on your system
$ mv bin/terraform-provider-controltower_<some version> ~/.terraform.d/plugins/registry.terraform.io/idealo/controltower/<some version>/darwin_arm64 # some version should be the future version of the provider after the changes.
```
create a `~/.terraformrc` file your home directory with the following content:
```hcl
provider_installation {
dev_overrides {
"registry.terraform.io/idealo/controltower" = "path-to-the-built-binary/terraform-provider-controltower" # e.g /Users/username/repo/terraform-provider-controltower/bin/terraform-provider-controltower"
}
# For all other providers, install them directly from their origin provider
# registries as normal. If you omit this, Terraform will _only_ use
# the dev_overrides block, and so no other providers will be available.
direct {}
}
Then you can test your changes in your terraform configuration by running `terraform init` in the directory where your terraform configuration is located.
```
Then you can test your changes in your terraform configuration by running `terraform init` (which will fail but that's expected) and then `terraform plan` in the directory where your terraform configuration is located.

Make sure to define the new version under the `required_providers` block.

Alternatively, if you're using terraform 0.14 or later, you can make use of `dev_overrides` as described [here](https://developer.hashicorp.com/terraform/cli/config/config-file#development-overrides-for-provider-developers) and point the provider to your `~/.terraformrc`.
A complete reference can be found [here](https://developer.hashicorp.com/terraform/cli/config/config-file#development-overrides-for-provider-developers).
5 changes: 5 additions & 0 deletions docs/resources/aws_account.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ Required:
- `email` (String) Email address of the user. If you use automatic provisioning this email address should already exist in AWS SSO.
- `first_name` (String) First name of the user.
- `last_name` (String) Last name of the user.

Optional:

- `permission_set_name` (String) Permission set name for the sso user. Defaults to AWSAdministratorAccess.
- `remove_account_assignment_on_update` (Boolean) If enabled, this will remove the account assignment for the old SSO user when the resource is updated.
2 changes: 1 addition & 1 deletion examples/resources/controltower_aws_account/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ resource "controltower_aws_account" "account" {
last_name = "Doe"
email = "john.doe@example.com"
}
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ require (
github.com/aws/aws-sdk-go-v2 v1.32.7
github.com/aws/aws-sdk-go-v2/config v1.28.7
github.com/aws/aws-sdk-go-v2/credentials v1.17.48
github.com/aws/aws-sdk-go-v2/service/identitystore v1.27.8
github.com/aws/aws-sdk-go-v2/service/organizations v1.36.2
github.com/aws/aws-sdk-go-v2/service/servicecatalog v1.32.8
github.com/aws/aws-sdk-go-v2/service/ssoadmin v1.29.8
github.com/aws/smithy-go v1.22.1
github.com/hashicorp/terraform-plugin-docs v0.20.1
github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 h1:zXFLuEuMMUOvEARXFU
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26/go.mod h1:3o2Wpy0bogG1kyOPrgkXA8pgIfEEv0+m19O9D5+W8y8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/service/identitystore v1.27.8 h1:Lg2UE1jqXgvhaWnHbnUuFdFORQLxKbJY4TSU87q6zGU=
github.com/aws/aws-sdk-go-v2/service/identitystore v1.27.8/go.mod h1:M5UW9CJQV78QiCxGihlGzwRbAD4B+fJf3y8yAij62Y0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 h1:8eUsivBQzZHqe/3FE+cqwfH+0p5Jo8PFM/QYQSmeZ+M=
Expand All @@ -45,6 +47,8 @@ github.com/aws/aws-sdk-go-v2/service/servicecatalog v1.32.8 h1:uDR8FMmsEd/g+eihj
github.com/aws/aws-sdk-go-v2/service/servicecatalog v1.32.8/go.mod h1:8ugnqCHkdv41d2Mo5F/mdPtaXauABVDYL3kV9U7LNbM=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 h1:CvuUmnXI7ebaUAhbJcDy9YQx8wHR69eZ9I7q5hszt/g=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.8/go.mod h1:XDeGv1opzwm8ubxddF0cgqkZWsyOtw4lr6dxwmb6YQg=
github.com/aws/aws-sdk-go-v2/service/ssoadmin v1.29.8 h1:nCDnD8rVurC8E43scFw1lDHBRi1aSxAyOgfDHauTUsg=
github.com/aws/aws-sdk-go-v2/service/ssoadmin v1.29.8/go.mod h1:gs/HuXKm8GZigCov15NZ9pt/u9EhD5gij4/uAEEnlJM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 h1:F2rBfNAL5UyswqoeWv9zs74N/NanhK16ydHW1pahX6E=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7/go.mod h1:JfyQ0g2JG8+Krq0EuZNnRwX0mU0HrwY/tG6JNfcqh4k=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 h1:Xgv/hyNgvLda/M9l9qxXc4UFSgppnRczLxlMs5Ae/QY=
Expand Down
118 changes: 117 additions & 1 deletion internal/provider/resource_aws_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ import (
"context"
"errors"
"fmt"
"github.com/aws/aws-sdk-go-v2/service/identitystore"
"github.com/aws/aws-sdk-go-v2/service/identitystore/document"

"github.com/aws/aws-sdk-go-v2/service/identitystore/types"
orgTypes "github.com/aws/aws-sdk-go-v2/service/organizations/types"
scTypes "github.com/aws/aws-sdk-go-v2/service/servicecatalog/types"
"github.com/aws/aws-sdk-go-v2/service/ssoadmin"

"regexp"
"sync"
"time"
Expand Down Expand Up @@ -74,6 +80,19 @@ func resourceAWSAccount() *schema.Resource {
Required: true,
ValidateFunc: validateEmailAddress,
},
"remove_account_assignment_on_update": {
Description: "If enabled, this will remove the account assignment for the old SSO user when the resource is updated.",
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"permission_set_name": {
Description: "Permission set name for the sso user. Defaults to AWSAdministratorAccess.",
Type: schema.TypeString,
Required: false,
Optional: true,
Default: "AWSAdministratorAccess",
},
},
},
},
Expand Down Expand Up @@ -335,6 +354,7 @@ func resourceAWSAccountUpdate(ctx context.Context, d *schema.ResourceData, m int

scconn := servicecatalog.NewFromConfig(cfg)
organizationsconn := organizations.NewFromConfig(cfg)
sso := d.Get("sso").([]interface{})[0].(map[string]interface{})

if d.HasChangesExcept("tags", "organizational_unit_id_on_delete", "close_account_on_delete") {
productId, artifactId, err := findServiceCatalogAccountProductId(ctx, scconn)
Expand All @@ -346,7 +366,6 @@ func resourceAWSAccountUpdate(ctx context.Context, d *schema.ResourceData, m int
name := d.Get("name").(string)
email := d.Get("email").(string)
ou := d.Get("organizational_unit").(string)
sso := d.Get("sso").([]interface{})[0].(map[string]interface{})

// Create a new parameters struct.
params := &servicecatalog.UpdateProvisionedProductInput{
Expand Down Expand Up @@ -410,9 +429,106 @@ func resourceAWSAccountUpdate(ctx context.Context, d *schema.ResourceData, m int
}
}

isRemoveAccountAssignmentOnUpdate := sso["remove_account_assignment_on_update"].(bool)

if isRemoveAccountAssignmentOnUpdate && d.HasChange("sso") {
ssoadminconn := ssoadmin.NewFromConfig(cfg)
identitystoreconn := identitystore.NewFromConfig(cfg)

accountId := d.Get("account_id").(string)
permissionSetName := sso["permission_set_name"].(string)

o, n := d.GetChange("sso")
if err := updateAccountAssignment(ctx, ssoadminconn, identitystoreconn, accountId, permissionSetName, o, n); err != nil {
return diag.Errorf("error updating account assignment: %v", err)
}
}

return resourceAWSAccountRead(ctx, d, m)
}

func updateAccountAssignment(ctx context.Context, ssoadminconn *ssoadmin.Client, identitystoreconn *identitystore.Client, accountId string, permissionSetName string, oldSSO interface{}, newSSO interface{}) error {

oldSSOMap := oldSSO.([]interface{})[0].(map[string]interface{})
newSSOMap := newSSO.([]interface{})[0].(map[string]interface{})
oldEmail := oldSSOMap["email"].(string)
newEmail := newSSOMap["email"].(string)

ssoInstances, err := ssoadminconn.ListInstances(ctx, &ssoadmin.ListInstancesInput{})
if err != nil {
return fmt.Errorf("error listing SSO instances: %v", err)
}
instanceArn := ssoInstances.Instances[0].InstanceArn
principalUserId, err := findPrincipalUserId(ctx, ssoInstances, oldEmail, err, identitystoreconn)
if err != nil {
return err
}

permissionSetArn, err := findPermissionSetArn(ctx, ssoadminconn, instanceArn, permissionSetName)

if err != nil {
return fmt.Errorf("error finding permission set: %v", err)
}
if oldEmail != newEmail {

_, err := ssoadminconn.DeleteAccountAssignment(ctx, &ssoadmin.DeleteAccountAssignmentInput{
InstanceArn: instanceArn,
TargetId: &accountId,
TargetType: "AWS_ACCOUNT",
PrincipalType: "USER",
PrincipalId: principalUserId,
PermissionSetArn: &permissionSetArn,
})
if err != nil {
return fmt.Errorf("error unassigning SSO user from account (%s): %v", accountId, err)
}
}
return nil
}

func findPrincipalUserId(ctx context.Context, ssoInstances *ssoadmin.ListInstancesOutput, oldEmail string, err error, identitystoreconn *identitystore.Client) (*string, error) {
identityStoreId := ssoInstances.Instances[0].IdentityStoreId

alternateIdentifier := &types.AlternateIdentifierMemberUniqueAttribute{
Value: types.UniqueAttribute{
AttributePath: aws.String("UserName"),
AttributeValue: document.NewLazyDocument(oldEmail),
},
}

principal, err := identitystoreconn.GetUserId(ctx, &identitystore.GetUserIdInput{
IdentityStoreId: identityStoreId,
AlternateIdentifier: alternateIdentifier,
})
if err != nil {
return nil, fmt.Errorf("error getting principal id: %v", err)
}
return principal.UserId, nil
}
func findPermissionSetArn(ctx context.Context, ssoadminconn *ssoadmin.Client, instanceArn *string, permissionSetName string) (string, error) {
paginator := ssoadmin.NewListPermissionSetsPaginator(ssoadminconn, &ssoadmin.ListPermissionSetsInput{
InstanceArn: instanceArn,
})
for paginator.HasMorePages() {
output, err := paginator.NextPage(ctx)
if err != nil {
return "", fmt.Errorf("error listing permission sets: %w", err)
}
for _, permissionSetArn := range output.PermissionSets {
describeInput, err := ssoadminconn.DescribePermissionSet(ctx, &ssoadmin.DescribePermissionSetInput{
InstanceArn: instanceArn,
PermissionSetArn: &permissionSetArn,
})
if err != nil {
return "", fmt.Errorf("error describing permission set: %w", err)
}
if *describeInput.PermissionSet.Name == permissionSetName {
return permissionSetArn, nil
}
}
}
return "", fmt.Errorf("permission set not found")
}
func resourceAWSAccountDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
cfg := m.(aws.Config)

Expand Down

0 comments on commit c1f74fb

Please sign in to comment.