Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return complete Organization data for Delegated Administrator accounts #32056

Merged
merged 17 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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