Skip to content

Commit

Permalink
Merge pull request #32056 from bodgit/delegated-administrators
Browse files Browse the repository at this point in the history
Return complete Organization data for Delegated Administrator accounts
  • Loading branch information
ewbankkit authored Jun 20, 2023
2 parents 498cba7 + d78d5a6 commit a65fd77
Show file tree
Hide file tree
Showing 20 changed files with 1,023 additions and 417 deletions.
7 changes: 7 additions & 0 deletions .changelog/32056.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
data-source/aws_organizations_organization: Return the full set of attributes when running as a [delegated administrator for AWS Organizations](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_delegate_policies.html)
```

```release-note:new-resource
aws_organizations_resource_policy
```
22 changes: 20 additions & 2 deletions internal/acctest/acctest.go
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,24 @@ func PreCheckOrganizationManagementAccount(ctx context.Context, t *testing.T) {
}
}

func PreCheckOrganizationMemberAccount(ctx context.Context, t *testing.T) {
organization, err := tforganizations.FindOrganization(ctx, Provider.Meta().(*conns.AWSClient).OrganizationsConn(ctx))

if err != nil {
t.Fatalf("describing AWS Organization: %s", err)
}

callerIdentity, err := tfsts.FindCallerIdentity(ctx, Provider.Meta().(*conns.AWSClient).STSConn(ctx))

if err != nil {
t.Fatalf("getting current identity: %s", err)
}

if aws.StringValue(organization.MasterAccountId) == aws.StringValue(callerIdentity.Account) {
t.Skip("this AWS account must not be the management account of an AWS Organization")
}
}

func PreCheckSSOAdminInstances(ctx context.Context, t *testing.T) {
conn := Provider.Meta().(*conns.AWSClient).SSOAdminConn(ctx)
input := &ssoadmin.ListInstancesInput{}
Expand Down Expand Up @@ -2255,7 +2273,7 @@ func CheckResourceAttrGreaterThanValue(n, key string, val int) resource.TestChec
}

if v <= val {
return fmt.Errorf("%s: Attribute %q is not greater than %d, got %d", n, key, val, v)
return fmt.Errorf("got %d, want > %d", v, val)
}

return nil
Expand All @@ -2271,7 +2289,7 @@ func CheckResourceAttrGreaterThanOrEqualValue(n, key string, val int) resource.T
}

if v < val {
return fmt.Errorf("%s: Attribute %q is not greater than or equal to %d, got %d", n, key, val, v)
return fmt.Errorf("got %d, want >= %d", v, val)
}

return nil
Expand Down
142 changes: 95 additions & 47 deletions internal/service/organizations/delegated_administrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/internal/verify"
)

Expand All @@ -35,12 +37,6 @@ func ResourceDelegatedAdministrator() *schema.Resource {
ForceNew: true,
ValidateFunc: verify.ValidAccountID,
},
"service_principal": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringLenBetween(1, 128),
},
"arn": {
Type: schema.TypeString,
Computed: true,
Expand All @@ -65,6 +61,12 @@ func ResourceDelegatedAdministrator() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"service_principal": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringLenBetween(1, 128),
},
"status": {
Type: schema.TypeString,
Computed: true,
Expand All @@ -74,95 +76,141 @@ func ResourceDelegatedAdministrator() *schema.Resource {
}

func resourceDelegatedAdministratorCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).OrganizationsConn(ctx)

accountID := d.Get("account_id").(string)
servicePrincipal := d.Get("service_principal").(string)
id := DelegatedAdministratorCreateResourceID(accountID, servicePrincipal)
input := &organizations.RegisterDelegatedAdministratorInput{
AccountId: aws.String(accountID),
ServicePrincipal: aws.String(servicePrincipal),
}

_, err := conn.RegisterDelegatedAdministratorWithContext(ctx, input)

if err != nil {
return diag.Errorf("creating Organizations DelegatedAdministrator (%s): %s", accountID, err)
return sdkdiag.AppendErrorf(diags, "creating Organizations Delegated Administrator (%s): %s", id, err)
}

d.SetId(fmt.Sprintf("%s/%s", accountID, servicePrincipal))
d.SetId(id)

return resourceDelegatedAdministratorRead(ctx, d, meta)
return append(diags, resourceDelegatedAdministratorRead(ctx, d, meta)...)
}

func resourceDelegatedAdministratorRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).OrganizationsConn(ctx)

accountID, servicePrincipal, err := DecodeOrganizationDelegatedAdministratorID(d.Id())
if err != nil {
return diag.Errorf("decoding ID AWS Organization (%s) DelegatedAdministrators: %s", d.Id(), err)
}
input := &organizations.ListDelegatedAdministratorsInput{
ServicePrincipal: aws.String(servicePrincipal),
}
var delegatedAccount *organizations.DelegatedAdministrator
err = conn.ListDelegatedAdministratorsPagesWithContext(ctx, input, func(page *organizations.ListDelegatedAdministratorsOutput, lastPage bool) bool {
for _, delegated := range page.DelegatedAdministrators {
if aws.StringValue(delegated.Id) == accountID {
delegatedAccount = delegated
}
}
accountID, servicePrincipal, err := DelegatedAdministratorParseResourceID(d.Id())

return !lastPage
})
if err != nil {
return diag.Errorf("listing AWS Organization (%s) DelegatedAdministrators: %s", d.Id(), err)
return sdkdiag.AppendFromErr(diags, err)
}

if delegatedAccount == nil {
if !d.IsNewResource() {
log.Printf("[WARN] AWS Organization DelegatedAdministrators not found (%s), removing from state", d.Id())
d.SetId("")
return nil
}
delegatedAccount, err := findDelegatedAdministratorByTwoPartKey(ctx, conn, accountID, servicePrincipal)

if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] Organizations Delegated Administrator %s not found, removing from state", d.Id())
d.SetId("")
return diags
}

return diag.FromErr(&retry.NotFoundError{})
if err != nil {
return sdkdiag.AppendErrorf(diags, "reading Organizations Delegated Administrator (%s): %s", d.Id(), err)
}

d.Set("account_id", accountID)
d.Set("arn", delegatedAccount.Arn)
d.Set("delegation_enabled_date", aws.TimeValue(delegatedAccount.DelegationEnabledDate).Format(time.RFC3339))
d.Set("email", delegatedAccount.Email)
d.Set("joined_method", delegatedAccount.JoinedMethod)
d.Set("joined_timestamp", aws.TimeValue(delegatedAccount.JoinedTimestamp).Format(time.RFC3339))
d.Set("name", delegatedAccount.Name)
d.Set("status", delegatedAccount.Status)
d.Set("account_id", accountID)
d.Set("service_principal", servicePrincipal)
d.Set("status", delegatedAccount.Status)

return nil
return diags
}

func resourceDelegatedAdministratorDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).OrganizationsConn(ctx)

accountID, servicePrincipal, err := DecodeOrganizationDelegatedAdministratorID(d.Id())
accountID, servicePrincipal, err := DelegatedAdministratorParseResourceID(d.Id())

if err != nil {
return diag.Errorf("decoding ID AWS Organization (%s) DelegatedAdministrators: %s", d.Id(), err)
return sdkdiag.AppendFromErr(diags, err)
}
input := &organizations.DeregisterDelegatedAdministratorInput{

log.Printf("[DEBUG] Deleting Organizations Delegated Administrator: %s", d.Id())
_, err = conn.DeregisterDelegatedAdministratorWithContext(ctx, &organizations.DeregisterDelegatedAdministratorInput{
AccountId: aws.String(accountID),
ServicePrincipal: aws.String(servicePrincipal),
})

if err != nil {
return sdkdiag.AppendErrorf(diags, "deleting Organizations Delegated Administrator (%s): %s", d.Id(), err)
}

return diags
}

func findDelegatedAdministratorByTwoPartKey(ctx context.Context, conn *organizations.Organizations, accountID, servicePrincipal string) (*organizations.DelegatedAdministrator, error) {
input := &organizations.ListDelegatedAdministratorsInput{
ServicePrincipal: aws.String(servicePrincipal),
}

_, err = conn.DeregisterDelegatedAdministratorWithContext(ctx, input)
output, err := findDelegatedAdministrators(ctx, conn, input)

if err != nil {
return diag.Errorf("deleting Organizations DelegatedAdministrator (%s): %s", d.Id(), err)
return nil, err
}

for _, v := range output {
if aws.StringValue(v.Id) == accountID {
return v, nil
}
}
return nil

return nil, &retry.NotFoundError{}
}

func DecodeOrganizationDelegatedAdministratorID(id string) (string, string, error) {
idParts := strings.Split(id, "/")
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
return "", "", fmt.Errorf("expected ID in the form of account_id/service_principal, given: %q", id)
func findDelegatedAdministrators(ctx context.Context, conn *organizations.Organizations, input *organizations.ListDelegatedAdministratorsInput) ([]*organizations.DelegatedAdministrator, error) {
var output []*organizations.DelegatedAdministrator

err := conn.ListDelegatedAdministratorsPagesWithContext(ctx, input, func(page *organizations.ListDelegatedAdministratorsOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

output = append(output, page.DelegatedAdministrators...)

return !lastPage
})

if err != nil {
return nil, err
}
return idParts[0], idParts[1], nil

return output, nil
}

const delegatedAdministratorResourceIDSeparator = "/"

func DelegatedAdministratorCreateResourceID(accountID, servicePrincipal string) string {
parts := []string{accountID, servicePrincipal}
id := strings.Join(parts, delegatedAdministratorResourceIDSeparator)

return id
}

func DelegatedAdministratorParseResourceID(id string) (string, string, error) {
parts := strings.Split(id, delegatedAdministratorResourceIDSeparator)

if len(parts) == 2 && parts[0] != "" && parts[1] != "" {
return parts[0], parts[1], nil
}

return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected ACCOUNTID%[2]sSERVICEPRINCIPAL", id, delegatedAdministratorResourceIDSeparator)
}
Loading

0 comments on commit a65fd77

Please sign in to comment.