diff --git a/.changelog/35413.txt b/.changelog/35413.txt new file mode 100644 index 00000000000..ccc131e9edb --- /dev/null +++ b/.changelog/35413.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_verifiedpermissions_policy +``` diff --git a/go.mod b/go.mod index 00a92573a86..bf225173316 100644 --- a/go.mod +++ b/go.mod @@ -169,6 +169,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/xray v1.25.4 github.com/aws/smithy-go v1.20.2 github.com/beevik/etree v1.3.0 + github.com/cedar-policy/cedar-go v0.0.0-20240318205125-470d1fe984bb github.com/davecgh/go-spew v1.1.1 github.com/gertd/go-pluralize v0.2.1 github.com/google/go-cmp v0.6.0 diff --git a/go.sum b/go.sum index cb061d54489..76ab48f31e2 100644 --- a/go.sum +++ b/go.sum @@ -377,6 +377,8 @@ github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyX github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY= github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE= +github.com/cedar-policy/cedar-go v0.0.0-20240318205125-470d1fe984bb h1:WaOlZeLno47GR/TvgUNCqB6itqhT7kMLsUwlIjxWW4Y= +github.com/cedar-policy/cedar-go v0.0.0-20240318205125-470d1fe984bb/go.mod h1:qZuNWmkhx7pxkYvgmNPcBE4NtfGBF6nmI+bjecaQp14= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= diff --git a/internal/service/verifiedpermissions/exports_test.go b/internal/service/verifiedpermissions/exports_test.go index 728be6893c1..55ade545b2c 100644 --- a/internal/service/verifiedpermissions/exports_test.go +++ b/internal/service/verifiedpermissions/exports_test.go @@ -5,10 +5,12 @@ package verifiedpermissions // Exports for use in tests only. var ( + ResourcePolicy = newResourcePolicy ResourcePolicyStore = newResourcePolicyStore ResourcePolicyTemplate = newResourcePolicyTemplate ResourceSchema = newResourceSchema + FindPolicyByID = findPolicyByID FindPolicyStoreByID = findPolicyStoreByID FindPolicyTemplateByID = findPolicyTemplateByID FindSchemaByPolicyStoreID = findSchemaByPolicyStoreID diff --git a/internal/service/verifiedpermissions/policy.go b/internal/service/verifiedpermissions/policy.go new file mode 100644 index 00000000000..2ef2cede94a --- /dev/null +++ b/internal/service/verifiedpermissions/policy.go @@ -0,0 +1,598 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package verifiedpermissions + +import ( + "context" + + "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" + cedar "github.com/cedar-policy/cedar-go/x/exp/parser" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "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/listplanmodifier" + "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" + "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" + interflex "github.com/hashicorp/terraform-provider-aws/internal/flex" + "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" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(aws_verifiedpermissions_policy, name="Policy") +func newResourcePolicy(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourcePolicy{} + + return r, nil +} + +const ( + ResNamePolicy = "Policy" +) + +type resourcePolicy struct { + framework.ResourceWithConfigure + framework.WithImportByID +} + +func (r *resourcePolicy) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "aws_verifiedpermissions_policy" +} + +func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "created_date": schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "id": framework.IDAttribute(), + "policy_id": framework.IDAttribute(), + "policy_store_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "definition": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[policyDefinition](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.IsRequired(), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "static": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[staticPolicyDefinition](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("template_linked"), + ), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "description": schema.StringAttribute{ + Optional: true, + }, + "statement": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIf( + statementReplaceIf, "Replace cedar statement diff", "Replace cedar statement diff", + ), + }, + }, + }, + }, + }, + "template_linked": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[templateLinkedPolicyDefinition](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("static"), + ), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "policy_template_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "principal": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[templateLinkedPrincipal](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "entity_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "entity_type": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + "resource": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[templateLinkedResource](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "entity_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "entity_type": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func statementReplaceIf(_ context.Context, req planmodifier.StringRequest, resp *stringplanmodifier.RequiresReplaceIfFuncResponse) { + if req.State.Raw.IsNull() { + return + } + + if req.Plan.Raw.IsNull() { + return + } + + cedarPlan, err := cedar.Tokenize([]byte(req.PlanValue.ValueString())) + if err != nil { + resp.Diagnostics.AddError(err.Error(), err.Error()) + return + } + + cedarState, err := cedar.Tokenize([]byte(req.StateValue.ValueString())) + if err != nil { + resp.Diagnostics.AddError(err.Error(), err.Error()) + return + } + + policyPlan, err := cedar.Parse(cedarPlan) + if err != nil { + resp.Diagnostics.AddError(err.Error(), err.Error()) + return + } + + policyState, err := cedar.Parse(cedarState) + if err != nil { + resp.Diagnostics.AddError(err.Error(), err.Error()) + return + } + + var policyPrincipal bool + if len(policyPlan) > 0 && len(policyState) > 0 && (len(policyPlan[0].Principal.Entity.Path) > 0 && (len(policyState[0].Principal.Entity.Path)) > 0) { + policyPrincipal = (policyPlan[0].Principal.Entity.String() != policyState[0].Principal.Entity.String()) || (policyPlan[0].Principal.Type != policyState[0].Principal.Type) + } + + var policyResource bool + if len(policyPlan) > 0 && len(policyState) > 0 && (len(policyPlan[0].Resource.Entity.Path) > 0 && (len(policyState[0].Resource.Entity.Path)) > 0) { + policyResource = (policyPlan[0].Resource.Entity.String() != policyState[0].Resource.Entity.String()) || (policyPlan[0].Resource.Type != policyState[0].Resource.Type) + } + + var policyEffect bool + if len(policyPlan) > 0 && len(policyState) > 0 { + policyEffect = policyPlan[0].Effect != policyState[0].Effect + } + + resp.RequiresReplace = policyEffect || policyResource || policyPrincipal +} + +const ( + ResourcePolicyIDPartsCount = 2 +) + +func (r *resourcePolicy) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().VerifiedPermissionsClient(ctx) + + var plan resourcePolicyData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + in := &verifiedpermissions.CreatePolicyInput{} + + in.ClientToken = aws.String(id.UniqueId()) + in.PolicyStoreId = aws.String(plan.PolicyStoreID.ValueString()) + + def, diags := plan.Definition.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + if !def.Static.IsNull() { + static, diags := def.Static.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + in.Definition = &awstypes.PolicyDefinitionMemberStatic{ + Value: awstypes.StaticPolicyDefinition{ + Statement: fwflex.StringFromFramework(ctx, static.Statement), + Description: fwflex.StringFromFramework(ctx, static.Description), + }, + } + } + + if !def.TemplateLinked.IsNull() { + templateLinked, diags := def.TemplateLinked.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + value := awstypes.TemplateLinkedPolicyDefinition{ + PolicyTemplateId: aws.String(templateLinked.PolicyTemplateID.ValueString()), + } + + if !templateLinked.Principal.IsNull() { + principal, diags := templateLinked.Principal.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + value.Principal = &awstypes.EntityIdentifier{ + EntityId: fwflex.StringFromFramework(ctx, principal.EntityID), + EntityType: fwflex.StringFromFramework(ctx, principal.EntityType), + } + } + + if !templateLinked.Resource.IsNull() { + res, diags := templateLinked.Resource.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + value.Resource = &awstypes.EntityIdentifier{ + EntityId: fwflex.StringFromFramework(ctx, res.EntityID), + EntityType: fwflex.StringFromFramework(ctx, res.EntityType), + } + } + + in.Definition = &awstypes.PolicyDefinitionMemberTemplateLinked{ + Value: value, + } + } + + out, err := conn.CreatePolicy(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionCreating, ResNamePolicy, plan.PolicyStoreID.String(), err), + err.Error(), + ) + return + } + + idParts := []string{ + aws.ToString(out.PolicyId), + aws.ToString(out.PolicyStoreId), + } + + rID, err := interflex.FlattenResourceId(idParts, ResourcePolicyIDPartsCount, false) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionCreating, ResNamePolicy, plan.PolicyStoreID.String(), err), + err.Error(), + ) + return + } + + plan.ID = fwflex.StringValueToFramework(ctx, rID) + plan.CreatedDate = timetypes.NewRFC3339TimePointerValue(out.CreatedDate) + plan.PolicyID = fwflex.StringToFramework(ctx, out.PolicyId) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourcePolicy) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().VerifiedPermissionsClient(ctx) + + var state resourcePolicyData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + rID, err := interflex.ExpandResourceId(state.ID.ValueString(), ResourcePolicyIDPartsCount, false) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionSetting, ResNamePolicy, state.ID.String(), err), + err.Error(), + ) + return + } + + out, err := findPolicyByID(ctx, conn, rID[0], rID[1]) + if tfresource.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionSetting, ResNamePolicy, state.ID.String(), err), + err.Error(), + ) + return + } + + state.PolicyID = fwflex.StringToFramework(ctx, out.PolicyId) + state.PolicyStoreID = fwflex.StringToFramework(ctx, out.PolicyStoreId) + state.CreatedDate = timetypes.NewRFC3339TimePointerValue(out.CreatedDate) + + if val, ok := out.Definition.(*awstypes.PolicyDefinitionDetailMemberStatic); ok && val != nil { + static := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &staticPolicyDefinition{ + Statement: fwflex.StringToFramework(ctx, val.Value.Statement), + Description: fwflex.StringToFramework(ctx, val.Value.Description), + }) + + state.Definition = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &policyDefinition{ + Static: static, + TemplateLinked: fwtypes.NewListNestedObjectValueOfNull[templateLinkedPolicyDefinition](ctx), + }) + } + + if val, ok := out.Definition.(*awstypes.PolicyDefinitionDetailMemberTemplateLinked); ok && val != nil { + tpl := templateLinkedPolicyDefinition{ + PolicyTemplateID: fwflex.StringToFramework(ctx, val.Value.PolicyTemplateId), + } + + if val.Value.Principal != nil { + tpl.Principal = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &templateLinkedPrincipal{ + EntityID: fwflex.StringToFramework(ctx, val.Value.Principal.EntityId), + EntityType: fwflex.StringToFramework(ctx, val.Value.Principal.EntityType), + }) + } else { + tpl.Principal = fwtypes.NewListNestedObjectValueOfNull[templateLinkedPrincipal](ctx) + } + + if val.Value.Resource != nil { + tpl.Resource = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &templateLinkedResource{ + EntityID: fwflex.StringToFramework(ctx, val.Value.Resource.EntityId), + EntityType: fwflex.StringToFramework(ctx, val.Value.Resource.EntityType), + }) + } else { + tpl.Resource = fwtypes.NewListNestedObjectValueOfNull[templateLinkedResource](ctx) + } + + templateLinked := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &tpl) + + state.Definition = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &policyDefinition{ + Static: fwtypes.NewListNestedObjectValueOfNull[staticPolicyDefinition](ctx), + TemplateLinked: templateLinked, + }) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourcePolicy) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().VerifiedPermissionsClient(ctx) + + var plan, state resourcePolicyData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if !plan.Definition.Equal(state.Definition) { + in := &verifiedpermissions.UpdatePolicyInput{} + in.PolicyId = fwflex.StringFromFramework(ctx, state.PolicyID) + in.PolicyStoreId = fwflex.StringFromFramework(ctx, state.PolicyStoreID) + + defPlan, diagsPlan := plan.Definition.ToPtr(ctx) + resp.Diagnostics.Append(diagsPlan...) + if resp.Diagnostics.HasError() { + return + } + + defState, diagsState := state.Definition.ToPtr(ctx) + resp.Diagnostics.Append(diagsState...) + if resp.Diagnostics.HasError() { + return + } + + if !defPlan.Static.Equal(defState.Static) { + static, diags := defPlan.Static.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + in.Definition = &awstypes.UpdatePolicyDefinitionMemberStatic{ + Value: awstypes.UpdateStaticPolicyDefinition{ + Statement: fwflex.StringFromFramework(ctx, static.Statement), + Description: fwflex.StringFromFramework(ctx, static.Description), + }, + } + } + + _, err := conn.UpdatePolicy(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicy, plan.ID.String(), err), + err.Error(), + ) + return + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourcePolicy) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().VerifiedPermissionsClient(ctx) + + var state resourcePolicyData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + in := &verifiedpermissions.DeletePolicyInput{ + PolicyId: aws.String(state.PolicyID.ValueString()), + PolicyStoreId: aws.String(state.PolicyStoreID.ValueString()), + } + + _, err := conn.DeletePolicy(ctx, in) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionDeleting, ResNamePolicy, state.ID.String(), err), + err.Error(), + ) + return + } +} + +func (r *resourcePolicy) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if !req.State.Raw.IsNull() && !req.Plan.Raw.IsNull() { + var plan, state resourcePolicyData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if !plan.Definition.Equal(state.Definition) { + defPlan, diagsPlan := plan.Definition.ToPtr(ctx) + resp.Diagnostics.Append(diagsPlan...) + if resp.Diagnostics.HasError() { + return + } + defState, diagsState := state.Definition.ToPtr(ctx) + resp.Diagnostics.Append(diagsState...) + if resp.Diagnostics.HasError() { + return + } + + if !defState.Static.IsNull() && defPlan.Static.IsNull() { + resp.RequiresReplace = []path.Path{path.Root("definition").AtListIndex(0).AtName("static")} + } + + if !defState.TemplateLinked.IsNull() && defPlan.TemplateLinked.IsNull() { + resp.RequiresReplace = []path.Path{path.Root("definition").AtListIndex(0).AtName("template_linked")} + } + } + } +} + +func findPolicyByID(ctx context.Context, conn *verifiedpermissions.Client, id, policyStoreId string) (*verifiedpermissions.GetPolicyOutput, error) { + in := &verifiedpermissions.GetPolicyInput{ + PolicyId: aws.String(id), + PolicyStoreId: aws.String(policyStoreId), + } + + out, err := conn.GetPolicy(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.PolicyId == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out, nil +} + +type resourcePolicyData struct { + CreatedDate timetypes.RFC3339 `tfsdk:"created_date"` + Definition fwtypes.ListNestedObjectValueOf[policyDefinition] `tfsdk:"definition"` + ID types.String `tfsdk:"id"` + PolicyID types.String `tfsdk:"policy_id"` + PolicyStoreID types.String `tfsdk:"policy_store_id"` +} + +type policyDefinition struct { + Static fwtypes.ListNestedObjectValueOf[staticPolicyDefinition] `tfsdk:"static"` + TemplateLinked fwtypes.ListNestedObjectValueOf[templateLinkedPolicyDefinition] `tfsdk:"template_linked"` +} + +type staticPolicyDefinition struct { + Statement types.String `tfsdk:"statement"` + Description types.String `tfsdk:"description"` +} + +type templateLinkedPolicyDefinition struct { + PolicyTemplateID types.String `tfsdk:"policy_template_id"` + Principal fwtypes.ListNestedObjectValueOf[templateLinkedPrincipal] `tfsdk:"principal"` + Resource fwtypes.ListNestedObjectValueOf[templateLinkedResource] `tfsdk:"resource"` +} + +type templateLinkedPrincipal struct { + EntityID types.String `tfsdk:"entity_id"` + EntityType types.String `tfsdk:"entity_type"` +} + +type templateLinkedResource struct { + EntityID types.String `tfsdk:"entity_id"` + EntityType types.String `tfsdk:"entity_type"` +} diff --git a/internal/service/verifiedpermissions/policy_test.go b/internal/service/verifiedpermissions/policy_test.go new file mode 100644 index 00000000000..975260ab2a1 --- /dev/null +++ b/internal/service/verifiedpermissions/policy_test.go @@ -0,0 +1,320 @@ +// 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" + awstypes "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "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" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + interflex "github.com/hashicorp/terraform-provider-aws/internal/flex" + tfverifiedpermissions "github.com/hashicorp/terraform-provider-aws/internal/service/verifiedpermissions" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccVerifiedPermissionsPolicy_basic(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy verifiedpermissions.GetPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_verifiedpermissions_policy.test" + + policyStatement := "permit (principal, action == Action::\"view\", resource in Album:: \"test_album\");" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VerifiedPermissionsEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPolicyConfig_basic(rName, policyStatement), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyExists(ctx, resourceName, &policy), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.description", rName), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.statement", policyStatement), + resource.TestCheckResourceAttrSet(resourceName, "policy_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccVerifiedPermissionsPolicy_templateLinked(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy verifiedpermissions.GetPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_verifiedpermissions_policy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VerifiedPermissionsEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPolicyConfig_templateLinked(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyExists(ctx, resourceName, &policy), + resource.TestCheckResourceAttrSet(resourceName, "definition.0.template_linked.0.policy_template_id"), + resource.TestCheckResourceAttr(resourceName, "definition.0.template_linked.0.principal.0.entity_id", "TestUsers"), + resource.TestCheckResourceAttr(resourceName, "definition.0.template_linked.0.principal.0.entity_type", "User"), + resource.TestCheckResourceAttr(resourceName, "definition.0.template_linked.0.resource.0.entity_id", "test_album"), + resource.TestCheckResourceAttr(resourceName, "definition.0.template_linked.0.resource.0.entity_type", "Album"), + resource.TestCheckResourceAttrSet(resourceName, "policy_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccVerifiedPermissionsPolicy_update(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy verifiedpermissions.GetPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_verifiedpermissions_policy.test" + + policyStatement := "permit (principal, action == Action::\"view\", resource in Album:: \"test_album\");" + policyStatementActionUpdated := "permit (principal, action == Action::\"write\", resource in Album:: \"test_album\");" + policyStatementEffectUpdated := "forbid (principal, action == Action::\"view\", resource in Album:: \"test_album\");" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VerifiedPermissionsEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPolicyConfig_basic(rName, policyStatement), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyExists(ctx, resourceName, &policy), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.description", rName), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.statement", policyStatement), + resource.TestCheckResourceAttrSet(resourceName, "policy_id"), + ), + }, + { + Config: testAccPolicyConfig_basic(rName, policyStatementActionUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyExists(ctx, resourceName, &policy), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.description", rName), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.statement", policyStatementActionUpdated), + resource.TestCheckResourceAttrSet(resourceName, "policy_id"), + ), + }, + { + Config: testAccPolicyConfig_basic(rName, policyStatementEffectUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyExists(ctx, resourceName, &policy), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.description", rName), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.statement", policyStatementEffectUpdated), + resource.TestCheckResourceAttrSet(resourceName, "policy_id"), + ), + }, + }, + }) +} + +func TestAccVerifiedPermissionsPolicy_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy verifiedpermissions.GetPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_verifiedpermissions_policy.test" + + policyStatement := "permit (principal, action == Action::\"view\", resource in Album:: \"test_album\");" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VerifiedPermissionsEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPolicyConfig_basic(rName, policyStatement), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyExists(ctx, resourceName, &policy), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfverifiedpermissions.ResourcePolicy, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckPolicyDestroy(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" { + continue + } + + rID, err := interflex.ExpandResourceId(rs.Primary.ID, tfverifiedpermissions.ResourcePolicyIDPartsCount, false) + if err != nil { + return err + } + + _, err = tfverifiedpermissions.FindPolicyByID(ctx, conn, rID[0], rID[1]) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil + } + + if err != nil { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingDestroyed, tfverifiedpermissions.ResNamePolicy, rs.Primary.ID, err) + } + + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingDestroyed, tfverifiedpermissions.ResNamePolicy, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckPolicyExists(ctx context.Context, name string, policy *verifiedpermissions.GetPolicyOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingExistence, tfverifiedpermissions.ResNamePolicy, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingExistence, tfverifiedpermissions.ResNamePolicy, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).VerifiedPermissionsClient(ctx) + rID, err := interflex.ExpandResourceId(rs.Primary.ID, tfverifiedpermissions.ResourcePolicyIDPartsCount, false) + if err != nil { + return err + } + + resp, err := tfverifiedpermissions.FindPolicyByID(ctx, conn, rID[0], rID[1]) + + if err != nil { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingExistence, tfverifiedpermissions.ResNamePolicy, rs.Primary.ID, err) + } + + *policy = *resp + + return nil + } +} + +func testAccPolicyConfig_base(rName string) string { + return fmt.Sprintf(` +resource "aws_verifiedpermissions_policy_store" "test" { + description = %[1]q + validation_settings { + mode = "OFF" + } +} + +resource "aws_verifiedpermissions_schema" "test" { + policy_store_id = aws_verifiedpermissions_policy_store.test.policy_store_id + + definition { + value = "{\"CHANGEDD\":{\"actions\":{},\"entityTypes\":{}}}" + } +} + +`, rName) +} + +func testAccPolicyConfig_basic(rName, policyStatement string) string { + return acctest.ConfigCompose( + testAccPolicyConfig_base(rName), + fmt.Sprintf(` +resource "aws_verifiedpermissions_policy" "test" { + policy_store_id = aws_verifiedpermissions_policy_store.test.id + + definition { + static { + description = %[1]q + statement = %[2]q + } + } +} +`, rName, policyStatement)) +} + +func testAccPolicyConfig_templateLinked(rName string) string { + return acctest.ConfigCompose( + testAccPolicyConfig_base(rName), + fmt.Sprintf(` +resource "aws_verifiedpermissions_policy_template" "test" { + policy_store_id = aws_verifiedpermissions_policy_store.test.id + + statement = "permit (principal in ?principal, action in PhotoFlash::Action::\"FullPhotoAccess\", resource == ?resource) unless { resource.IsPrivate };" + description = %[1]q +} + +resource "aws_verifiedpermissions_policy" "test" { + policy_store_id = aws_verifiedpermissions_policy_store.test.id + + definition { + template_linked { + policy_template_id = aws_verifiedpermissions_policy_template.test.policy_template_id + + principal { + entity_id = "TestUsers" + entity_type = "User" + } + + resource { + entity_id = "test_album" + entity_type = "Album" + } + } + } +} +`, rName)) +} diff --git a/internal/service/verifiedpermissions/service_package_gen.go b/internal/service/verifiedpermissions/service_package_gen.go index 2c61c23e615..6213f171433 100644 --- a/internal/service/verifiedpermissions/service_package_gen.go +++ b/internal/service/verifiedpermissions/service_package_gen.go @@ -25,6 +25,10 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { return []*types.ServicePackageFrameworkResource{ + { + Factory: newResourcePolicy, + Name: "Policy", + }, { Factory: newResourcePolicyStore, Name: "Policy Store", diff --git a/internal/service/verifiedpermissions/sweep.go b/internal/service/verifiedpermissions/sweep.go new file mode 100644 index 00000000000..3c13712867d --- /dev/null +++ b/internal/service/verifiedpermissions/sweep.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package verifiedpermissions + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/sweep" + "github.com/hashicorp/terraform-provider-aws/internal/sweep/awsv2" + "github.com/hashicorp/terraform-provider-aws/internal/sweep/framework" +) + +func RegisterSweepers() { + resource.AddTestSweepers("aws_verifiedpermissions_policy_store", &resource.Sweeper{ + Name: "aws_verifiedpermissions_policy_store", + F: sweepPolicyStores, + }) +} + +func sweepPolicyStores(region string) error { + ctx := sweep.Context(region) + client, err := sweep.SharedRegionalSweepClient(ctx, region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + + conn := client.VerifiedPermissionsClient(ctx) + sweepResources := make([]sweep.Sweepable, 0) + in := &verifiedpermissions.ListPolicyStoresInput{} + + pages := verifiedpermissions.NewListPolicyStoresPaginator(conn, in) + + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if awsv2.SkipSweepError(err) { + log.Printf("[WARN] Skipping VerifiedPermissions Policy Stores sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error retrieving VerifiedPermissions Policy Stores: %w", err) + } + + for _, store := range page.PolicyStores { + id := aws.ToString(store.PolicyStoreId) + log.Printf("[INFO] Deleting VerifiedPermissions Policy Store: %s", id) + + sweepResources = append(sweepResources, framework.NewSweepResource(newResourcePolicyStore, client, + framework.NewAttribute("id", id), + )) + } + } + + if err := sweep.SweepOrchestrator(ctx, sweepResources); err != nil { + return fmt.Errorf("error sweeping VerifiedPermissions Policy Stores for %s: %w", region, err) + } + + return nil +} diff --git a/internal/sweep/register_gen_test.go b/internal/sweep/register_gen_test.go index d6861528327..86be1dfad8b 100644 --- a/internal/sweep/register_gen_test.go +++ b/internal/sweep/register_gen_test.go @@ -147,6 +147,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/service/timestreamwrite" "github.com/hashicorp/terraform-provider-aws/internal/service/transcribe" "github.com/hashicorp/terraform-provider-aws/internal/service/transfer" + "github.com/hashicorp/terraform-provider-aws/internal/service/verifiedpermissions" "github.com/hashicorp/terraform-provider-aws/internal/service/vpclattice" "github.com/hashicorp/terraform-provider-aws/internal/service/waf" "github.com/hashicorp/terraform-provider-aws/internal/service/wafregional" @@ -299,6 +300,7 @@ func registerSweepers() { timestreamwrite.RegisterSweepers() transcribe.RegisterSweepers() transfer.RegisterSweepers() + verifiedpermissions.RegisterSweepers() vpclattice.RegisterSweepers() waf.RegisterSweepers() wafregional.RegisterSweepers() diff --git a/website/docs/r/verifiedpermissions_policy.html.markdown b/website/docs/r/verifiedpermissions_policy.html.markdown new file mode 100644 index 00000000000..963e43714ed --- /dev/null +++ b/website/docs/r/verifiedpermissions_policy.html.markdown @@ -0,0 +1,82 @@ +--- +subcategory: "Verified Permissions" +layout: "aws" +page_title: "AWS: aws_verifiedpermissions_policy" +description: |- + Terraform resource for managing an AWS Verified Permissions Policy. +--- + +# Resource: aws_verifiedpermissions_policy + +Terraform resource for managing an AWS Verified Permissions Policy. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_verifiedpermissions_policy" "test" { + policy_store_id = aws_verifiedpermissions_policy_store.test.id + + definition { + static { + statement = "permit (principal, action == Action::\"view\", resource in Album:: \"test_album\");" + } + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `policy_store_id` - (Required) The Policy Store ID of the policy store. +* `definition`- (Required) The definition of the policy. See [Definition](#definition) below. + +The following arguments are optional: + +* `optional_arg` - (Optional) Concise argument description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. + +### Definition + +* `static` - (Optional) The static policy statement. See [Static](#static) below. +* `template_linked` - (Optional) The template linked policy. See [Template Linked](#template-linked) below. + +#### Static + +* `description` - (Optional) The description of the static policy. +* `statement` - (Required) The statement of the static policy. + +#### Template Linked + +* `policy_template_id` - (Required) The ID of the template. +* `principal` - (Optional) The principal of the template linked policy. + * `entity_id` - (Required) The entity ID of the principal. + * `entity_type` - (Required) The entity type of the principal. +* `resource` - (Optional) The resource of the template linked policy. + * `entity_id` - (Required) The entity ID of the resource. + * `entity_type` - (Required) The entity type of the resource. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn` - ARN of the Policy. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. +* `example_attribute` - Concise description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Verified Permissions Policy using the `policy_id,policy_store_id`. For example: + +```terraform +import { + to = aws_verifiedpermissions_policy.example + id = "policy-id-12345678,policy-store-id-12345678" +} +``` + +Using `terraform import`, import Verified Permissions Policy using the `policy_id,policy_store_id`. For example: + +```console +% terraform import aws_verifiedpermissions_policy.example policy-id-12345678,policy-store-id-12345678 +```