diff --git a/.changelog/32205.txt b/.changelog/32205.txt new file mode 100644 index 00000000000..005752c308c --- /dev/null +++ b/.changelog/32205.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_verifiedpermissions_policy_template +``` \ No newline at end of file diff --git a/internal/service/verifiedpermissions/exports_test.go b/internal/service/verifiedpermissions/exports_test.go index f6061a32bbf..728be6893c1 100644 --- a/internal/service/verifiedpermissions/exports_test.go +++ b/internal/service/verifiedpermissions/exports_test.go @@ -5,9 +5,15 @@ package verifiedpermissions // Exports for use in tests only. var ( - ResourcePolicyStore = newResourcePolicyStore - ResourceSchema = newResourceSchema + ResourcePolicyStore = newResourcePolicyStore + ResourcePolicyTemplate = newResourcePolicyTemplate + ResourceSchema = newResourceSchema FindPolicyStoreByID = findPolicyStoreByID + FindPolicyTemplateByID = findPolicyTemplateByID FindSchemaByPolicyStoreID = findSchemaByPolicyStoreID ) + +var ( + PolicyTemplateParseID = policyTemplateParseID +) diff --git a/internal/service/verifiedpermissions/policy_template.go b/internal/service/verifiedpermissions/policy_template.go new file mode 100644 index 00000000000..c675af2c8e6 --- /dev/null +++ b/internal/service/verifiedpermissions/policy_template.go @@ -0,0 +1,285 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package verifiedpermissions + +import ( + "context" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions" + awstypes "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions/types" + "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/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "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" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="Policy Template") +func newResourcePolicyTemplate(context.Context) (resource.ResourceWithConfigure, error) { + r := &resourcePolicyTemplate{} + + return r, nil +} + +const ( + ResNamePolicyTemplate = "Policy Template" +) + +type resourcePolicyTemplate struct { + framework.ResourceWithConfigure + framework.WithImportByID +} + +func (r *resourcePolicyTemplate) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_verifiedpermissions_policy_template" +} + +// Schema returns the schema for this resource. +func (r *resourcePolicyTemplate) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + s := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "created_date": schema.StringAttribute{ + CustomType: fwtypes.TimestampType, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "description": schema.StringAttribute{ + Optional: true, + }, + "id": framework.IDAttribute(), + "policy_store_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "policy_template_id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "statement": schema.StringAttribute{ + Required: true, + }, + }, + } + + response.Schema = s +} + +func (r *resourcePolicyTemplate) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + conn := r.Meta().VerifiedPermissionsClient(ctx) + var plan resourcePolicyTemplateData + + response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...) + + if response.Diagnostics.HasError() { + return + } + + input := &verifiedpermissions.CreatePolicyTemplateInput{} + response.Diagnostics.Append(flex.Expand(ctx, plan, input)...) + + if response.Diagnostics.HasError() { + return + } + + input.ClientToken = aws.String(id.UniqueId()) + + output, err := conn.CreatePolicyTemplate(ctx, input) + + if err != nil { + response.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionCreating, ResNamePolicyTemplate, plan.PolicyStoreID.ValueString(), err), + err.Error(), + ) + return + } + + state := plan + state.ID = flex.StringValueToFramework(ctx, fmt.Sprintf("%s:%s", aws.ToString(output.PolicyStoreId), aws.ToString(output.PolicyTemplateId))) + + response.Diagnostics.Append(flex.Flatten(ctx, output, &state)...) + + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *resourcePolicyTemplate) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + conn := r.Meta().VerifiedPermissionsClient(ctx) + var state resourcePolicyTemplateData + + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + + if response.Diagnostics.HasError() { + return + } + + policyStoreID, policyTemplateID, err := policyTemplateParseID(state.ID.ValueString()) + if err != nil { + response.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionReading, ResNamePolicyTemplate, state.ID.ValueString(), err), + err.Error(), + ) + return + } + + output, err := findPolicyTemplateByID(ctx, conn, policyStoreID, policyTemplateID) + + if tfresource.NotFound(err) { + response.State.RemoveResource(ctx) + return + } + + if err != nil { + response.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionReading, ResNamePolicyTemplate, state.ID.ValueString(), err), + err.Error(), + ) + return + } + + response.Diagnostics.Append(flex.Flatten(ctx, output, &state)...) + + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *resourcePolicyTemplate) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + conn := r.Meta().VerifiedPermissionsClient(ctx) + var state, plan resourcePolicyTemplateData + + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...) + + if response.Diagnostics.HasError() { + return + } + + if !plan.Description.Equal(state.Description) || !plan.Statement.Equal(state.Statement) { + input := &verifiedpermissions.UpdatePolicyTemplateInput{} + response.Diagnostics.Append(flex.Expand(ctx, plan, input)...) + + if response.Diagnostics.HasError() { + return + } + + output, err := conn.UpdatePolicyTemplate(ctx, input) + + if err != nil { + response.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicyTemplate, state.ID.ValueString(), err), + err.Error(), + ) + return + } + + response.Diagnostics.Append(flex.Flatten(ctx, output, &plan)...) + } + + response.Diagnostics.Append(response.State.Set(ctx, &plan)...) +} + +func (r *resourcePolicyTemplate) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + conn := r.Meta().VerifiedPermissionsClient(ctx) + var state resourcePolicyTemplateData + + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + + if response.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "deleting Verified Permissions Policy Template", map[string]interface{}{ + "id": state.ID.ValueString(), + }) + + input := &verifiedpermissions.DeletePolicyTemplateInput{ + PolicyStoreId: aws.String(state.PolicyStoreID.ValueString()), + PolicyTemplateId: aws.String(state.PolicyTemplateID.ValueString()), + } + + _, err := conn.DeletePolicyTemplate(ctx, input) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + if err != nil { + response.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionDeleting, ResNamePolicyTemplate, state.ID.ValueString(), err), + err.Error(), + ) + return + } +} + +type resourcePolicyTemplateData struct { + CreatedDate fwtypes.Timestamp `tfsdk:"created_date"` + Description types.String `tfsdk:"description"` + ID types.String `tfsdk:"id"` + PolicyStoreID types.String `tfsdk:"policy_store_id"` + PolicyTemplateID types.String `tfsdk:"policy_template_id"` + Statement types.String `tfsdk:"statement"` +} + +func findPolicyTemplateByID(ctx context.Context, conn *verifiedpermissions.Client, policyStoreId, id string) (*verifiedpermissions.GetPolicyTemplateOutput, error) { + in := &verifiedpermissions.GetPolicyTemplateInput{ + PolicyStoreId: aws.String(policyStoreId), + PolicyTemplateId: aws.String(id), + } + out, err := conn.GetPolicyTemplate(ctx, in) + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + if err != nil { + return nil, err + } + + if out == nil || out.PolicyStoreId == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out, nil +} + +func policyTemplateParseID(id string) (string, string, error) { + parts := strings.Split(id, ":") + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%s), expected POLICY-STORE-ID:POLICY-TEMPLATE-ID", id) +} diff --git a/internal/service/verifiedpermissions/policy_template_test.go b/internal/service/verifiedpermissions/policy_template_test.go new file mode 100644 index 00000000000..fe5a24549d6 --- /dev/null +++ b/internal/service/verifiedpermissions/policy_template_test.go @@ -0,0 +1,200 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package verifiedpermissions_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/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" + tfverifiedpermissions "github.com/hashicorp/terraform-provider-aws/internal/service/verifiedpermissions" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccVerifiedPermissionsPolicyTemplate_basic(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policytemplate verifiedpermissions.GetPolicyTemplateOutput + resourceName := "aws_verifiedpermissions_policy_template.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VerifiedPermissionsEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPolicyTemplateDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPolicyTemplateConfig_basic("permit (principal in ?principal, action in PhotoFlash::Action::\"FullPhotoAccess\", resource == ?resource) unless { resource.IsPrivate };", ""), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyTemplateExists(ctx, resourceName, &policytemplate), + resource.TestCheckResourceAttr(resourceName, "statement", "permit (principal in ?principal, action in PhotoFlash::Action::\"FullPhotoAccess\", resource == ?resource) unless { resource.IsPrivate };"), + resource.TestCheckResourceAttr(resourceName, "description", ""), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccVerifiedPermissionsPolicyTemplate_update(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policytemplate verifiedpermissions.GetPolicyTemplateOutput + resourceName := "aws_verifiedpermissions_policy_template.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VerifiedPermissionsEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPolicyTemplateDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPolicyTemplateConfig_basic("permit (principal in ?principal, action in PhotoFlash::Action::\"FullPhotoAccess\", resource == ?resource) unless { resource.IsPrivate };", ""), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyTemplateExists(ctx, resourceName, &policytemplate), + resource.TestCheckResourceAttr(resourceName, "statement", "permit (principal in ?principal, action in PhotoFlash::Action::\"FullPhotoAccess\", resource == ?resource) unless { resource.IsPrivate };"), + resource.TestCheckResourceAttr(resourceName, "description", ""), + ), + }, + { + Config: testAccPolicyTemplateConfig_basic("permit (principal in ?principal, action in PhotoFlash::Action::\"FullPhotoAccess\", resource == ?resource);", "test"), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyTemplateExists(ctx, resourceName, &policytemplate), + resource.TestCheckResourceAttr(resourceName, "statement", "permit (principal in ?principal, action in PhotoFlash::Action::\"FullPhotoAccess\", resource == ?resource);"), + resource.TestCheckResourceAttr(resourceName, "description", "test"), + ), + }, + }, + }) +} + +func TestAccVerifiedPermissionsPolicyTemplate_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policytemplate verifiedpermissions.GetPolicyTemplateOutput + resourceName := "aws_verifiedpermissions_policy_template.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VerifiedPermissionsEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPolicyTemplateDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPolicyTemplateConfig_basic(`permit (principal in ?principal, action in PhotoFlash::Action::"FullPhotoAccess", resource == ?resource) unless { resource.IsPrivate };`, ""), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyTemplateExists(ctx, resourceName, &policytemplate), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfverifiedpermissions.ResourcePolicyTemplate, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckPolicyTemplateDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).VerifiedPermissionsClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_verifiedpermissions_policy_template" { + continue + } + policyStoreID, policyTemplateID, err := tfverifiedpermissions.PolicyTemplateParseID(rs.Primary.ID) + if err != nil { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingDestroyed, tfverifiedpermissions.ResNamePolicyTemplate, rs.Primary.ID, err) + } + + _, err = tfverifiedpermissions.FindPolicyTemplateByID(ctx, conn, policyStoreID, policyTemplateID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingDestroyed, tfverifiedpermissions.ResNamePolicyTemplate, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckPolicyTemplateExists(ctx context.Context, name string, policytemplate *verifiedpermissions.GetPolicyTemplateOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingExistence, tfverifiedpermissions.ResNamePolicyTemplate, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingExistence, tfverifiedpermissions.ResNamePolicyTemplate, name, errors.New("not set")) + } + + policyStoreID, policyTemplateID, err := tfverifiedpermissions.PolicyTemplateParseID(rs.Primary.ID) + if err != nil { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingExistence, tfverifiedpermissions.ResNamePolicyTemplate, rs.Primary.ID, err) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).VerifiedPermissionsClient(ctx) + resp, err := tfverifiedpermissions.FindPolicyTemplateByID(ctx, conn, policyStoreID, policyTemplateID) + + if err != nil { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingExistence, tfverifiedpermissions.ResNamePolicyTemplate, rs.Primary.ID, err) + } + + *policytemplate = *resp + + return nil + } +} + +func testAccPolicyTemplateConfig_basic(statement, description string) string { + return fmt.Sprintf(` +resource "aws_verifiedpermissions_policy_store" "test" { + validation_settings { + mode = "OFF" + } +} + +resource "aws_verifiedpermissions_policy_template" "test" { + policy_store_id = aws_verifiedpermissions_policy_store.test.id + + statement = %[1]q + description = %[2]q +} +`, statement, description) +} diff --git a/internal/service/verifiedpermissions/service_package_gen.go b/internal/service/verifiedpermissions/service_package_gen.go index 38544b542f4..2c61c23e615 100644 --- a/internal/service/verifiedpermissions/service_package_gen.go +++ b/internal/service/verifiedpermissions/service_package_gen.go @@ -29,6 +29,10 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic Factory: newResourcePolicyStore, Name: "Policy Store", }, + { + Factory: newResourcePolicyTemplate, + Name: "Policy Template", + }, { Factory: newResourceSchema, Name: "Schema", diff --git a/website/docs/r/verifiedpermissions_policy_template.html.markdown b/website/docs/r/verifiedpermissions_policy_template.html.markdown new file mode 100644 index 00000000000..d836e15a8f2 --- /dev/null +++ b/website/docs/r/verifiedpermissions_policy_template.html.markdown @@ -0,0 +1,56 @@ +--- +subcategory: "Verified Permissions" +layout: "aws" +page_title: "AWS: aws_verifiedpermissions_policy_template" +description: |- + Terraform resource for managing an AWS Verified Permissions Policy Template. +--- +# Resource: aws_verifiedpermissions_policy_template + +Terraform resource for managing an AWS Verified Permissions Policy Template. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_verifiedpermissions_policy_template" "example" { + policy_store_id = aws_verifiedpermissions_policy_store.example.id + statement = "permit (principal in ?principal, action in PhotoFlash::Action::\"FullPhotoAccess\", resource == ?resource) unless { resource.IsPrivate };" +} +``` + +## Argument Reference + +The following arguments are required: + +* `policy_store_id` - (Required) The ID of the Policy Store. +* `statement` - (Required) Defines the content of the statement, written in Cedar policy language. + +The following arguments are optional: + +* `description` - (Optional) Provides a description for the policy template. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `policy_template_id` - The ID of the Policy Store. +* `created_date` - The date the Policy Store was created. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Verified Permissions Policy Store using the `policy_store_id:policy_template_id`. For example: + +```terraform +import { + to = aws_verifiedpermissions_policy_template.example + id = "DxQg2j8xvXJQ1tQCYNWj9T:X19yzj8xvXJQ1tQCYNWj9T" +} +``` + +Using `terraform import`, import Verified Permissions Policy Store using the `policy_store_id:policy_template_id`. For example: + +```console +% terraform import aws_verifiedpermissions_policy_template.example policyStoreId:policyTemplateId +```