diff --git a/.changelog/29099.txt b/.changelog/29099.txt new file mode 100644 index 00000000000..088d3e94445 --- /dev/null +++ b/.changelog/29099.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_auditmanager_assessment_delegation +``` diff --git a/internal/service/auditmanager/assessment_delegation.go b/internal/service/auditmanager/assessment_delegation.go new file mode 100644 index 00000000000..689061f38a5 --- /dev/null +++ b/internal/service/auditmanager/assessment_delegation.go @@ -0,0 +1,346 @@ +package auditmanager + +import ( + "context" + "errors" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/auditmanager" + awstypes "github.com/aws/aws-sdk-go-v2/service/auditmanager/types" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + sdkv2resource "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func init() { + _sp.registerFrameworkResourceFactory(newResourceAssessmentDelegation) +} + +func newResourceAssessmentDelegation(_ context.Context) (resource.ResourceWithConfigure, error) { + return &resourceAssessmentDelegation{}, nil +} + +const ( + ResNameAssessmentDelegation = "AssessmentDelegation" +) + +type resourceAssessmentDelegation struct { + framework.ResourceWithConfigure +} + +func (r *resourceAssessmentDelegation) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_auditmanager_assessment_delegation" +} + +func (r *resourceAssessmentDelegation) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "assessment_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "comment": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "control_set_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + // The AWS-generated ID for delegations has been observed to change between creation + // and subseqeunt read operations. As such, this value cannot be used as the resource ID + // or the input to finder functions. However, it is still required as part of the delete + // request input, so will be stored as a separate computed attribute. + "delegation_id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "id": framework.IDAttribute(), + "role_arn": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "role_type": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + enum.FrameworkValidate[awstypes.RoleType](), + }, + }, + "status": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +func (r *resourceAssessmentDelegation) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().AuditManagerClient() + + var plan resourceAssessmentDelegationData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + delegationIn := awstypes.CreateDelegationRequest{ + RoleArn: aws.String(plan.RoleARN.ValueString()), + RoleType: awstypes.RoleType(plan.RoleType.ValueString()), + ControlSetId: aws.String(plan.ControlSetID.ValueString()), + } + if !plan.Comment.IsNull() { + delegationIn.Comment = aws.String(plan.Comment.ValueString()) + } + in := auditmanager.BatchCreateDelegationByAssessmentInput{ + AssessmentId: aws.String(plan.AssessmentID.ValueString()), + CreateDelegationRequests: []awstypes.CreateDelegationRequest{delegationIn}, + } + + // Include retry handling to allow for IAM propagation + // + // Example: + // ResourceNotFoundException: The operation tried to access a nonexistent resource. The resource + // might not be specified correctly, or its status might not be active. Check and try again. + var out *auditmanager.BatchCreateDelegationByAssessmentOutput + err := tfresource.Retry(ctx, iamPropagationTimeout, func() *sdkv2resource.RetryError { + var err error + out, err = conn.BatchCreateDelegationByAssessment(ctx, &in) + if err != nil { + var nfe *awstypes.ResourceNotFoundException + if errors.As(err, &nfe) { + return sdkv2resource.RetryableError(err) + } + return sdkv2resource.NonRetryableError(err) + } + + return nil + }) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.AuditManager, create.ErrActionCreating, ResNameAssessmentDelegation, plan.RoleARN.String(), nil), + err.Error(), + ) + return + } + if out == nil || len(out.Delegations) == 0 { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.AuditManager, create.ErrActionCreating, ResNameAssessmentDelegation, plan.RoleARN.String(), nil), + errors.New("empty output").Error(), + ) + return + } + + // This response object will return ALL delegations assigned to the assessment, not just those + // added in this batch request. In order to write to state, the response should be filtered to + // the item with a matching role_arn and control_set_id. + // + // Also, assessment_id is returned as null in the BatchCreateDelegationByAssessment response + // object, and therefore is not included as one of the matching parameters. + delegation, err := getMatchingDelegation(out.Delegations, plan.RoleARN.ValueString(), plan.ControlSetID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.AuditManager, create.ErrActionCreating, ResNameAssessmentDelegation, plan.RoleARN.String(), nil), + err.Error(), + ) + return + } + + state := plan + + // The AWS-generated ID for delegations has been observed to change between creation + // and subseqeunt read operations. As such, the ID attribute will use a combination of + // attributes that are unique to a single delegation instead. + id := toID(plan.AssessmentID.ValueString(), plan.RoleARN.ValueString(), plan.ControlSetID.ValueString()) + state.ID = types.StringValue(id) + + state.refreshFromOutput(ctx, delegation) + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *resourceAssessmentDelegation) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().AuditManagerClient() + + var state resourceAssessmentDelegationData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := FindAssessmentDelegationByID(ctx, conn, state.ID.ValueString()) + if tfresource.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.AuditManager, create.ErrActionReading, ResNameAssessmentDelegation, state.ID.String(), nil), + err.Error(), + ) + return + } + + state.refreshFromOutputMetadata(ctx, out) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +// There is no update API, so this method is a no-op +func (r *resourceAssessmentDelegation) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +} + +func (r *resourceAssessmentDelegation) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().AuditManagerClient() + + var state resourceAssessmentDelegationData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + _, err := conn.BatchDeleteDelegationByAssessment(ctx, &auditmanager.BatchDeleteDelegationByAssessmentInput{ + AssessmentId: aws.String(state.AssessmentID.ValueString()), + DelegationIds: []string{state.DelegationID.ValueString()}, + }) + if err != nil { + var nfe *awstypes.ResourceNotFoundException + if errors.As(err, &nfe) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.AuditManager, create.ErrActionDeleting, ResNameAssessmentDelegation, state.ID.String(), nil), + err.Error(), + ) + } +} + +func (r *resourceAssessmentDelegation) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func FindAssessmentDelegationByID(ctx context.Context, conn *auditmanager.Client, id string) (*awstypes.DelegationMetadata, error) { + assessmentID, roleARN, controlSetID := fromID(id) + + // The GetDelegations API behaves like a List* API, so the results are paged + // through until an entry with a matching ID is found + in := &auditmanager.GetDelegationsInput{} + pages := auditmanager.NewGetDelegationsPaginator(conn, in) + + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + return nil, err + } + + for _, d := range page.Delegations { + if aws.ToString(d.AssessmentId) == assessmentID && + strings.EqualFold(aws.ToString(d.RoleArn), roleARN) && // IAM role names are case-insensitive + aws.ToString(d.ControlSetName) == controlSetID { + return &d, nil + } + } + } + + return nil, &sdkv2resource.NotFoundError{ + LastRequest: in, + } +} + +// getMatchingDelegation will return the delegation matching the provided role ARN and +// control set ID. If no match is found, an error is returned. +func getMatchingDelegation(out []awstypes.Delegation, roleARN, controlSetID string) (*awstypes.Delegation, error) { + for _, d := range out { + if strings.EqualFold(aws.ToString(d.RoleArn), roleARN) && // IAM role names are case-insensitive + aws.ToString(d.ControlSetId) == controlSetID { + return &d, nil + } + } + return nil, errors.New("no matching delegations in response") +} + +func fromID(id string) (string, string, string) { + parts := strings.Split(id, ",") + if len(parts) != 3 { + return "", "", "" + } + return parts[0], parts[1], parts[2] +} + +func toID(assessmentID, roleARN, controlSetID string) string { + return strings.Join([]string{assessmentID, roleARN, controlSetID}, ",") +} + +type resourceAssessmentDelegationData struct { + AssessmentID types.String `tfsdk:"assessment_id"` + Comment types.String `tfsdk:"comment"` + ControlSetID types.String `tfsdk:"control_set_id"` + DelegationID types.String `tfsdk:"delegation_id"` + ID types.String `tfsdk:"id"` + RoleARN types.String `tfsdk:"role_arn"` + RoleType types.String `tfsdk:"role_type"` + Status types.String `tfsdk:"status"` +} + +// refreshFromOutput writes state data from an AWS response object +// +// This variant of the refresh method is for use with the create operation +// response type (Delegation). +func (rd *resourceAssessmentDelegationData) refreshFromOutput(ctx context.Context, out *awstypes.Delegation) { + if out == nil { + return + } + + // The response from create operations always includes a nil AssessmentId. This is likely + // a bug in the AWS API, so for now skip using the response output and copy the state + // value directly from plan. + // rd.AssessmentID = flex.StringToFramework(ctx, out.AssessmentId) + + rd.Comment = flex.StringToFramework(ctx, out.Comment) + rd.ControlSetID = flex.StringToFramework(ctx, out.ControlSetId) + rd.DelegationID = flex.StringToFramework(ctx, out.Id) + rd.RoleARN = flex.StringToFramework(ctx, out.RoleArn) + rd.RoleType = flex.StringValueToFramework(ctx, out.RoleType) + rd.Status = flex.StringValueToFramework(ctx, out.Status) +} + +// refreshFromOutputMetadata writes state data from an AWS response object +// +// This variant of the refresh method is for use with the get operation +// response type (DelegationMetadata). Notably, this response omits certain +// attributes such as comment, control_set_id, and role_type which means +// drift cannot be detected after the initial create action. +func (rd *resourceAssessmentDelegationData) refreshFromOutputMetadata(ctx context.Context, out *awstypes.DelegationMetadata) { + if out == nil { + return + } + + rd.AssessmentID = flex.StringToFramework(ctx, out.AssessmentId) + rd.DelegationID = flex.StringToFramework(ctx, out.Id) + rd.RoleARN = flex.StringToFramework(ctx, out.RoleArn) + rd.Status = flex.StringValueToFramework(ctx, out.Status) +} diff --git a/internal/service/auditmanager/assessment_delegation_test.go b/internal/service/auditmanager/assessment_delegation_test.go new file mode 100644 index 00000000000..59b0a5c9510 --- /dev/null +++ b/internal/service/auditmanager/assessment_delegation_test.go @@ -0,0 +1,377 @@ +package auditmanager_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/auditmanager/types" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + tfauditmanager "github.com/hashicorp/terraform-provider-aws/internal/service/auditmanager" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccAuditManagerAssessmentDelegation_basic(t *testing.T) { + ctx := acctest.Context(t) + var delegation types.DelegationMetadata + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_auditmanager_assessment_delegation.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(names.AuditManagerEndpointID, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.AuditManagerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAssessmentDelegationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAssessmentDelegationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAssessmentDelegationExists(ctx, resourceName, &delegation), + resource.TestCheckResourceAttrPair(resourceName, "assessment_id", "aws_auditmanager_assessment.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "role_arn", "aws_iam_role.test_delegation", "arn"), + resource.TestCheckResourceAttr(resourceName, "control_set_id", rName), + resource.TestCheckResourceAttr(resourceName, "role_type", string(types.RoleTypeResourceOwner)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"control_set_id", "role_type"}, + }, + }, + }) +} + +func TestAccAuditManagerAssessmentDelegation_disappears(t *testing.T) { + ctx := acctest.Context(t) + var delegation types.DelegationMetadata + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_auditmanager_assessment_delegation.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(names.AuditManagerEndpointID, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.AuditManagerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAssessmentDelegationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAssessmentDelegationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAssessmentDelegationExists(ctx, resourceName, &delegation), + acctest.CheckFrameworkResourceDisappears(acctest.Provider, tfauditmanager.ResourceAssessmentDelegation, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAuditManagerAssessmentDelegation_optional(t *testing.T) { + ctx := acctest.Context(t) + var delegation types.DelegationMetadata + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_auditmanager_assessment_delegation.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(names.AuditManagerEndpointID, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.AuditManagerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAssessmentDelegationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAssessmentDelegationConfig_optional(rName, "text"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAssessmentDelegationExists(ctx, resourceName, &delegation), + resource.TestCheckResourceAttrPair(resourceName, "assessment_id", "aws_auditmanager_assessment.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "role_arn", "aws_iam_role.test_delegation", "arn"), + resource.TestCheckResourceAttr(resourceName, "control_set_id", rName), + resource.TestCheckResourceAttr(resourceName, "role_type", string(types.RoleTypeResourceOwner)), + resource.TestCheckResourceAttr(resourceName, "comment", "text"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"control_set_id", "role_type", "comment"}, + }, + { + Config: testAccAssessmentDelegationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAssessmentDelegationExists(ctx, resourceName, &delegation), + resource.TestCheckResourceAttrPair(resourceName, "assessment_id", "aws_auditmanager_assessment.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "role_arn", "aws_iam_role.test_delegation", "arn"), + resource.TestCheckResourceAttr(resourceName, "control_set_id", rName), + resource.TestCheckResourceAttr(resourceName, "role_type", string(types.RoleTypeResourceOwner)), + ), + }, + { + Config: testAccAssessmentDelegationConfig_optional(rName, "text-updated"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAssessmentDelegationExists(ctx, resourceName, &delegation), + resource.TestCheckResourceAttrPair(resourceName, "assessment_id", "aws_auditmanager_assessment.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "role_arn", "aws_iam_role.test_delegation", "arn"), + resource.TestCheckResourceAttr(resourceName, "control_set_id", rName), + resource.TestCheckResourceAttr(resourceName, "role_type", string(types.RoleTypeResourceOwner)), + resource.TestCheckResourceAttr(resourceName, "comment", "text-updated"), + ), + }, + }, + }) +} + +func TestAccAuditManagerAssessmentDelegation_multiple(t *testing.T) { + ctx := acctest.Context(t) + var delegation types.DelegationMetadata + var delegation2 types.DelegationMetadata + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_auditmanager_assessment_delegation.test" + resourceName2 := "aws_auditmanager_assessment_delegation.test2" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(names.AuditManagerEndpointID, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.AuditManagerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAssessmentDelegationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAssessmentDelegationConfig_multiple(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAssessmentDelegationExists(ctx, resourceName, &delegation), + resource.TestCheckResourceAttrPair(resourceName, "assessment_id", "aws_auditmanager_assessment.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "role_arn", "aws_iam_role.test_delegation", "arn"), + resource.TestCheckResourceAttr(resourceName, "control_set_id", rName), + resource.TestCheckResourceAttr(resourceName, "role_type", string(types.RoleTypeResourceOwner)), + testAccCheckAssessmentDelegationExists(ctx, resourceName2, &delegation2), + resource.TestCheckResourceAttrPair(resourceName2, "assessment_id", "aws_auditmanager_assessment.test", "id"), + resource.TestCheckResourceAttrPair(resourceName2, "role_arn", "aws_iam_role.test_delegation2", "arn"), + resource.TestCheckResourceAttr(resourceName2, "control_set_id", rName), + resource.TestCheckResourceAttr(resourceName2, "role_type", string(types.RoleTypeResourceOwner)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"control_set_id", "role_type"}, + }, + { + ResourceName: resourceName2, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"control_set_id", "role_type"}, + }, + }, + }) +} + +func testAccCheckAssessmentDelegationDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).AuditManagerClient() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_auditmanager_assessment_delegation" { + continue + } + + _, err := tfauditmanager.FindAssessmentDelegationByID(ctx, conn, rs.Primary.ID) + if err != nil { + var nfe *resource.NotFoundError + if errors.As(err, &nfe) { + return nil + } + return err + } + + return create.Error(names.AuditManager, create.ErrActionCheckingDestroyed, tfauditmanager.ResNameAssessmentDelegation, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckAssessmentDelegationExists(ctx context.Context, name string, delegation *types.DelegationMetadata) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.AuditManager, create.ErrActionCheckingExistence, tfauditmanager.ResNameAssessmentDelegation, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.AuditManager, create.ErrActionCheckingExistence, tfauditmanager.ResNameAssessmentDelegation, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).AuditManagerClient() + resp, err := tfauditmanager.FindAssessmentDelegationByID(ctx, conn, rs.Primary.ID) + if err != nil { + return create.Error(names.AuditManager, create.ErrActionCheckingExistence, tfauditmanager.ResNameAssessmentDelegation, rs.Primary.ID, err) + } + + *delegation = *resp + + return nil + } +} + +func testAccAssessmentDelegationConfigBase(rName string) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} + +resource "aws_s3_bucket" "test" { + bucket = %[1]q +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.test.id + acl = "private" +} + +data "aws_iam_policy_document" "test" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["auditmanager.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "test" { + name = %[1]q + assume_role_policy = data.aws_iam_policy_document.test.json +} + +resource "aws_auditmanager_control" "test" { + name = %[1]q + + control_mapping_sources { + source_name = %[1]q + source_set_up_option = "Procedural_Controls_Mapping" + source_type = "MANUAL" + } +} + +resource "aws_auditmanager_framework" "test" { + name = %[1]q + + control_sets { + name = %[1]q + controls { + id = aws_auditmanager_control.test.id + } + } +} + +resource "aws_auditmanager_assessment" "test" { + name = %[1]q + + assessment_reports_destination { + destination = "s3://${aws_s3_bucket.test.id}" + destination_type = "S3" + } + + framework_id = aws_auditmanager_framework.test.id + + roles { + role_arn = aws_iam_role.test.arn + role_type = "PROCESS_OWNER" + } + + scope { + aws_accounts { + id = data.aws_caller_identity.current.account_id + } + aws_services { + service_name = "S3" + } + } +} +`, rName) +} + +func testAccAssessmentDelegationConfig_basic(rName string) string { + return acctest.ConfigCompose( + testAccAssessmentDelegationConfigBase(rName), + fmt.Sprintf(` +resource "aws_iam_role" "test_delegation" { + name = "%[1]s-delegation" + assume_role_policy = data.aws_iam_policy_document.test.json +} + +resource "aws_auditmanager_assessment_delegation" "test" { + assessment_id = aws_auditmanager_assessment.test.id + role_arn = aws_iam_role.test_delegation.arn + role_type = "RESOURCE_OWNER" + control_set_id = %[1]q +} +`, rName)) +} + +func testAccAssessmentDelegationConfig_optional(rName, comment string) string { + return acctest.ConfigCompose( + testAccAssessmentDelegationConfigBase(rName), + fmt.Sprintf(` +resource "aws_iam_role" "test_delegation" { + name = "%[1]s-delegation" + assume_role_policy = data.aws_iam_policy_document.test.json +} + +resource "aws_auditmanager_assessment_delegation" "test" { + assessment_id = aws_auditmanager_assessment.test.id + role_arn = aws_iam_role.test_delegation.arn + role_type = "RESOURCE_OWNER" + control_set_id = %[1]q + + comment = %[2]q +} +`, rName, comment)) +} + +func testAccAssessmentDelegationConfig_multiple(rName string) string { + return acctest.ConfigCompose( + testAccAssessmentDelegationConfigBase(rName), + fmt.Sprintf(` +resource "aws_iam_role" "test_delegation" { + name = "%[1]s-delegation" + assume_role_policy = data.aws_iam_policy_document.test.json +} + +resource "aws_auditmanager_assessment_delegation" "test" { + assessment_id = aws_auditmanager_assessment.test.id + role_arn = aws_iam_role.test_delegation.arn + role_type = "RESOURCE_OWNER" + control_set_id = %[1]q +} + +resource "aws_iam_role" "test_delegation2" { + name = "%[1]s-delegation2" + assume_role_policy = data.aws_iam_policy_document.test.json +} + +resource "aws_auditmanager_assessment_delegation" "test2" { + assessment_id = aws_auditmanager_assessment.test.id + role_arn = aws_iam_role.test_delegation2.arn + role_type = "RESOURCE_OWNER" + control_set_id = %[1]q +} +`, rName)) +} diff --git a/internal/service/auditmanager/exports_test.go b/internal/service/auditmanager/exports_test.go index 31e83b5812e..2ae72094478 100644 --- a/internal/service/auditmanager/exports_test.go +++ b/internal/service/auditmanager/exports_test.go @@ -5,6 +5,7 @@ var ( ResourceAccountRegistration = newResourceAccountRegistration ResourceOrganizationAdminAccountRegistration = newResourceOrganizationAdminAccountRegistration ResourceAssessment = newResourceAssessment + ResourceAssessmentDelegation = newResourceAssessmentDelegation ResourceAssessmentReport = newResourceAssessmentReport ResourceControl = newResourceControl ResourceFramework = newResourceFramework diff --git a/website/docs/r/auditmanager_assessment_delegation.html.markdown b/website/docs/r/auditmanager_assessment_delegation.html.markdown new file mode 100644 index 00000000000..27596859ba1 --- /dev/null +++ b/website/docs/r/auditmanager_assessment_delegation.html.markdown @@ -0,0 +1,53 @@ +--- +subcategory: "Audit Manager" +layout: "aws" +page_title: "AWS: aws_auditmanager_assessment_delegation" +description: |- + Terraform resource for managing an AWS Audit Manager Assessment Delegation. +--- + +# Resource: aws_auditmanager_assessment_delegation + +Terraform resource for managing an AWS Audit Manager Assessment Delegation. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_auditmanager_assessment_delegation" "example" { + assessment_id = aws_auditmanager_assessment.example.id + role_arn = aws_iam_role.example.arn + role_type = "RESOURCE_OWNER" + control_set_id = "example" +} +``` + +## Argument Reference + +The following arguments are required: + +* `assessment_id` - (Required) Identifier for the assessment. +* `control_set_id` - (Required) Assessment control set name. This value is the control set name used during assessment creation (not the AWS-generated ID). The `_id` suffix on this attribute has been preserved to be consistent with the underlying AWS API. +* `role_arn` - (Required) Amazon Resource Name (ARN) of the IAM role. +* `role_type` - (Required) Type of customer persona. For assessment delegation, type must always be `RESOURCE_OWNER`. + +The following arguments are optional: + +* `comment` - (Optional) Comment describing the delegation request. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `delegation_id` - Unique identifier for the delegation. +* `id` - Unique identifier for the resource. This is a comma-separated string containing `assessment_id`, `role_arn`, and `control_set_id`. +* `status` - Status of the delegation. + +## Import + +Audit Manager Assessment Delegation can be imported using the `id`, e.g., + +``` +$ terraform import aws_auditmanager_assessment_delegation.example abcdef-123456,arn:aws:iam::012345678901:role/example,example +```