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

r/aws_iam_organization_features: IAM Organizations Root Access Management new resource #40164

Merged
merged 12 commits into from
Nov 26, 2024
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
Loading