Skip to content

Commit

Permalink
Merge pull request #40164 from alexbacchin/r-aws_iam_organization_fea…
Browse files Browse the repository at this point in the history
…tures

r/aws_iam_organization_features: IAM Organizations Root Access Management new resource
  • Loading branch information
ewbankkit authored Nov 26, 2024
2 parents 56807ca + eb6f0cb commit bf676bb
Show file tree
Hide file tree
Showing 7 changed files with 501 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .changelog/40164.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_iam_organizations_features
```
25 changes: 19 additions & 6 deletions internal/acctest/acctest.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
"net"
"os"
"os/exec"
"reflect"
"regexp"
"slices"
"strconv"
"strings"
"sync"
Expand All @@ -31,7 +31,6 @@ import (
dstypes "github.com/aws/aws-sdk-go-v2/service/directoryservice/types"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/iam"
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/aws/aws-sdk-go-v2/service/inspector2"
inspector2types "github.com/aws/aws-sdk-go-v2/service/inspector2/types"
organizationstypes "github.com/aws/aws-sdk-go-v2/service/organizations/types"
Expand Down Expand Up @@ -1122,6 +1121,20 @@ func PreCheckOrganizationsEnabled(ctx context.Context, t *testing.T) *organizati
return PreCheckOrganizationsEnabledWithProvider(ctx, t, func() *schema.Provider { return Provider })
}

func PreCheckOrganizationsEnabledServicePrincipal(ctx context.Context, t *testing.T, servicePrincipalName string) {
t.Helper()

servicePrincipalNames, err := tforganizations.FindEnabledServicePrincipalNames(ctx, Provider.Meta().(*conns.AWSClient).OrganizationsClient(ctx))

if err != nil {
t.Fatalf("reading Organization service principals: %s", err)
}

if !slices.Contains(servicePrincipalNames, servicePrincipalName) {
t.Skipf("trusted access for %s must be enabled in AWS Organizations", servicePrincipalName)
}
}

func PreCheckOrganizationsEnabledWithProvider(ctx context.Context, t *testing.T, providerF ProviderFunc) *organizationstypes.Organization {
t.Helper()

Expand Down Expand Up @@ -1270,7 +1283,7 @@ func PreCheckIAMServiceLinkedRoleWithProvider(ctx context.Context, t *testing.T,
input := &iam.ListRolesInput{
PathPrefix: aws.String(pathPrefix),
}
var role iamtypes.Role
var roleFound bool

pages := iam.NewListRolesPaginator(conn, input)
for pages.HasMorePages() {
Expand All @@ -1282,13 +1295,13 @@ func PreCheckIAMServiceLinkedRoleWithProvider(ctx context.Context, t *testing.T,
t.Fatalf("listing IAM roles: %s", err)
}

for _, r := range page.Roles {
role = r
if len(page.Roles) > 0 {
roleFound = true
break
}
}

if reflect.ValueOf(role).IsZero() {
if !roleFound {
t.Skipf("skipping tests; missing IAM service-linked role %s. Please create the role and retry", pathPrefix)
}
}
Expand Down
2 changes: 2 additions & 0 deletions internal/service/iam/exports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var (
ResourceGroupPolicyAttachment = resourceGroupPolicyAttachment
ResourceInstanceProfile = resourceInstanceProfile
ResourceOpenIDConnectProvider = resourceOpenIDConnectProvider
ResourceOrganizationsFeatures = newOrganizationsFeaturesResource
ResourcePolicy = resourcePolicy
ResourcePolicyAttachment = resourcePolicyAttachment
ResourceRolePolicy = resourceRolePolicy
Expand Down Expand Up @@ -45,6 +46,7 @@ var (
FindGroupPolicyAttachmentsByName = findGroupPolicyAttachmentsByName
FindInstanceProfileByName = findInstanceProfileByName
FindOpenIDConnectProviderByARN = findOpenIDConnectProviderByARN
FindOrganizationsFeatures = findOrganizationsFeatures
FindPolicyByARN = findPolicyByARN
FindRolePoliciesByName = findRolePoliciesByName
FindRolePolicyAttachmentsByName = findRolePolicyAttachmentsByName
Expand Down
250 changes: 250 additions & 0 deletions internal/service/iam/organizations_features.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package iam

import (
"context"
"fmt"
"slices"

"github.com/aws/aws-sdk-go-v2/service/iam"
awstypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-provider-aws/internal/errs"
"github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag"
"github.com/hashicorp/terraform-provider-aws/internal/framework"
fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex"
fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
itypes "github.com/hashicorp/terraform-provider-aws/internal/types"
"github.com/hashicorp/terraform-provider-aws/names"
)

// @FrameworkResource("aws_iam_organizations_features", name="Organizations Features")
func newOrganizationsFeaturesResource(context.Context) (resource.ResourceWithConfigure, error) {
r := &organizationsFeaturesResource{}

return r, nil
}

type organizationsFeaturesResource struct {
framework.ResourceWithConfigure
framework.WithImportByID
}

func (*organizationsFeaturesResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) {
response.TypeName = "aws_iam_organizations_features"
}

func (r *organizationsFeaturesResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) {
response.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"enabled_features": schema.SetAttribute{
CustomType: fwtypes.NewSetTypeOf[fwtypes.StringEnum[awstypes.FeatureType]](ctx),
Required: true,
ElementType: fwtypes.StringEnumType[awstypes.FeatureType](),
},
names.AttrID: framework.IDAttribute(),
},
}
}

func (r *organizationsFeaturesResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
var data organizationsFeaturesResourceModel
response.Diagnostics.Append(request.Plan.Get(ctx, &data)...)
if response.Diagnostics.HasError() {
return
}

conn := r.Meta().IAMClient(ctx)

var enabledFeatures []awstypes.FeatureType
response.Diagnostics.Append(fwflex.Expand(ctx, data.EnabledFeatures, &enabledFeatures)...)
if response.Diagnostics.HasError() {
return
}

if err := updateOrganizationFeatures(ctx, conn, enabledFeatures, []awstypes.FeatureType{}); err != nil {
response.Diagnostics.AddError("creating IAM Organizations Features", err.Error())

return
}

output, err := findOrganizationsFeatures(ctx, conn)

if err != nil {
response.Diagnostics.AddError("reading IAM Organizations Features", err.Error())

return
}

// Set values for unknowns.
data.OrganizationID = fwflex.StringToFramework(ctx, output.OrganizationId)

response.Diagnostics.Append(response.State.Set(ctx, data)...)
}

func (r *organizationsFeaturesResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
var data organizationsFeaturesResourceModel
response.Diagnostics.Append(request.State.Get(ctx, &data)...)
if response.Diagnostics.HasError() {
return
}

conn := r.Meta().IAMClient(ctx)

output, err := findOrganizationsFeatures(ctx, conn)

if tfresource.NotFound(err) {
response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err))
response.State.RemoveResource(ctx)

return
}

if err != nil {
response.Diagnostics.AddError(fmt.Sprintf("reading IAM Organizations Features (%s)", data.OrganizationID.ValueString()), err.Error())

return
}

response.Diagnostics.Append(fwflex.Flatten(ctx, output.EnabledFeatures, &data.EnabledFeatures)...)
if response.Diagnostics.HasError() {
return
}

response.Diagnostics.Append(response.State.Set(ctx, &data)...)
}

func (r *organizationsFeaturesResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
var old, new organizationsFeaturesResourceModel
response.Diagnostics.Append(request.Plan.Get(ctx, &new)...)
if response.Diagnostics.HasError() {
return
}
response.Diagnostics.Append(request.State.Get(ctx, &old)...)
if response.Diagnostics.HasError() {
return
}

var oldFeatures, newFeatures []awstypes.FeatureType
response.Diagnostics.Append(fwflex.Expand(ctx, old.EnabledFeatures, &oldFeatures)...)
if response.Diagnostics.HasError() {
return
}
response.Diagnostics.Append(fwflex.Expand(ctx, new.EnabledFeatures, &newFeatures)...)
if response.Diagnostics.HasError() {
return
}

conn := r.Meta().IAMClient(ctx)

if err := updateOrganizationFeatures(ctx, conn, newFeatures, oldFeatures); err != nil {
response.Diagnostics.AddError(fmt.Sprintf("updating IAM Organizations Features (%s)", new.OrganizationID.ValueString()), err.Error())

return
}

response.Diagnostics.Append(response.State.Set(ctx, &new)...)
}

func (r *organizationsFeaturesResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
var data organizationsFeaturesResourceModel
response.Diagnostics.Append(request.State.Get(ctx, &data)...)
if response.Diagnostics.HasError() {
return
}

conn := r.Meta().IAMClient(ctx)

var enabledFeatures []awstypes.FeatureType
response.Diagnostics.Append(fwflex.Expand(ctx, data.EnabledFeatures, &enabledFeatures)...)
if response.Diagnostics.HasError() {
return
}

if err := updateOrganizationFeatures(ctx, conn, []awstypes.FeatureType{}, enabledFeatures); err != nil {
response.Diagnostics.AddError(fmt.Sprintf("deleting IAM Organizations Features (%s)", data.OrganizationID.ValueString()), err.Error())

return
}
}

type organizationsFeaturesResourceModel struct {
EnabledFeatures fwtypes.SetValueOf[fwtypes.StringEnum[awstypes.FeatureType]] `tfsdk:"enabled_features"`
OrganizationID types.String `tfsdk:"id"`
}

func findOrganizationsFeatures(ctx context.Context, conn *iam.Client) (*iam.ListOrganizationsFeaturesOutput, error) {
input := &iam.ListOrganizationsFeaturesInput{}

output, err := conn.ListOrganizationsFeatures(ctx, input)

if errs.IsA[*awstypes.OrganizationNotFoundException](err) {
return nil, &retry.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if err != nil {
return nil, err
}

if output == nil || len(output.EnabledFeatures) == 0 {
return nil, tfresource.NewEmptyResultError(input)
}

return output, nil
}

func updateOrganizationFeatures(ctx context.Context, conn *iam.Client, new, old []awstypes.FeatureType) error {
toEnable := itypes.Set[awstypes.FeatureType](new).Difference(old)
toDisable := itypes.Set[awstypes.FeatureType](old).Difference(new)

if slices.Contains(toEnable, awstypes.FeatureTypeRootCredentialsManagement) {
input := &iam.EnableOrganizationsRootCredentialsManagementInput{}

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

if err != nil {
return fmt.Errorf("enabling Organizations root credentials management: %w", err)
}
}

if slices.Contains(toEnable, awstypes.FeatureTypeRootSessions) {
input := &iam.EnableOrganizationsRootSessionsInput{}

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

if err != nil {
return fmt.Errorf("enabling Organizations root sessions: %w", err)
}
}

if slices.Contains(toDisable, awstypes.FeatureTypeRootCredentialsManagement) {
input := &iam.DisableOrganizationsRootCredentialsManagementInput{}

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

if err != nil {
return fmt.Errorf("disabling Organizations root credentials management: %w", err)
}
}

if slices.Contains(toDisable, awstypes.FeatureTypeRootSessions) {
input := &iam.DisableOrganizationsRootSessionsInput{}

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

if err != nil {
return fmt.Errorf("disabling Organizations root sessions: %w", err)
}
}

return nil
}
Loading

0 comments on commit bf676bb

Please sign in to comment.