diff --git a/.changelog/28776.txt b/.changelog/28776.txt new file mode 100644 index 00000000000..d727ef44042 --- /dev/null +++ b/.changelog/28776.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_opensearchserverless_security_config +``` diff --git a/internal/service/opensearchserverless/exports_test.go b/internal/service/opensearchserverless/exports_test.go index 02f64b8ec31..47747b32ac0 100644 --- a/internal/service/opensearchserverless/exports_test.go +++ b/internal/service/opensearchserverless/exports_test.go @@ -2,8 +2,9 @@ package opensearchserverless // Exports for use in tests only. var ( - ResourceCollection = newResourceCollection ResourceAccessPolicy = newResourceAccessPolicy + ResourceCollection = newResourceCollection + ResourceSecurityConfig = newResourceSecurityConfig ResourceSecurityPolicy = newResourceSecurityPolicy ResourceVPCEndpoint = newResourceVPCEndpoint ) diff --git a/internal/service/opensearchserverless/find.go b/internal/service/opensearchserverless/find.go index 3921606b449..b78cdf8f5b9 100644 --- a/internal/service/opensearchserverless/find.go +++ b/internal/service/opensearchserverless/find.go @@ -60,6 +60,30 @@ func FindCollectionByID(ctx context.Context, conn *opensearchserverless.Client, return &out.CollectionDetails[0], nil } +func FindSecurityConfigByID(ctx context.Context, conn *opensearchserverless.Client, id string) (*types.SecurityConfigDetail, error) { + in := &opensearchserverless.GetSecurityConfigInput{ + Id: aws.String(id), + } + out, err := conn.GetSecurityConfig(ctx, in) + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + return nil, err + } + + if out == nil || out.SecurityConfigDetail == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out.SecurityConfigDetail, nil +} + func FindSecurityPolicyByNameAndType(ctx context.Context, conn *opensearchserverless.Client, name, policyType string) (*types.SecurityPolicyDetail, error) { in := &opensearchserverless.GetSecurityPolicyInput{ Name: aws.String(name), diff --git a/internal/service/opensearchserverless/security_config.go b/internal/service/opensearchserverless/security_config.go new file mode 100644 index 00000000000..335a7114ab3 --- /dev/null +++ b/internal/service/opensearchserverless/security_config.go @@ -0,0 +1,333 @@ +package opensearchserverless + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/opensearchserverless" + awstypes "github.com/aws/aws-sdk-go-v2/service/opensearchserverless/types" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + sdkid "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "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" +) + +// @FrameworkResource +func newResourceSecurityConfig(_ context.Context) (resource.ResourceWithConfigure, error) { + return &resourceSecurityConfig{}, nil +} + +const ( + ResNameSecurityConfig = "Security Config" +) + +type resourceSecurityConfig struct { + framework.ResourceWithConfigure +} + +func (r *resourceSecurityConfig) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_opensearchserverless_security_config" +} + +func (r *resourceSecurityConfig) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttribute(), + "config_version": schema.StringAttribute{ + Computed: true, + }, + "description": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 1000), + }, + }, + "name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(3, 32), + }, + }, + "type": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + enum.FrameworkValidate[awstypes.SecurityConfigType](), + }, + }, + }, + Blocks: map[string]schema.Block{ + "saml_options": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "group_attribute": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 2048), + }, + }, + "metadata": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 20480), + }, + }, + "session_timeout": schema.Int64Attribute{ + Optional: true, + Computed: true, + Validators: []validator.Int64{ + int64validator.Between(5, 1540), + }, + }, + "user_attribute": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 2048), + }, + }, + }, + }, + }, + } +} + +func (r *resourceSecurityConfig) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan resourceSecurityConfigData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + conn := r.Meta().OpenSearchServerlessClient(ctx) + + in := &opensearchserverless.CreateSecurityConfigInput{ + ClientToken: aws.String(sdkid.UniqueId()), + Name: flex.StringFromFramework(ctx, plan.Name), + Type: awstypes.SecurityConfigType(*flex.StringFromFramework(ctx, plan.Type)), + SamlOptions: expandSAMLOptions(ctx, plan.SamlOptions, &resp.Diagnostics), + } + + if !plan.Description.IsNull() { + in.Description = flex.StringFromFramework(ctx, plan.Description) + } + + out, err := conn.CreateSecurityConfig(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionCreating, ResNameSecurityConfig, plan.Name.String(), nil), + err.Error(), + ) + return + } + + if out == nil || out.SecurityConfigDetail == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionCreating, ResNameSecurityConfig, plan.Name.String(), nil), + err.Error(), + ) + return + } + + state := plan + state.refreshFromOutput(ctx, out.SecurityConfigDetail) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceSecurityConfig) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().OpenSearchServerlessClient(ctx) + + var state resourceSecurityConfigData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := FindSecurityConfigByID(ctx, conn, state.ID.ValueString()) + if tfresource.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + + state.refreshFromOutput(ctx, out) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceSecurityConfig) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().OpenSearchServerlessClient(ctx) + + var plan, state resourceSecurityConfigData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + update := false + + input := &opensearchserverless.UpdateSecurityConfigInput{ + ClientToken: aws.String(sdkid.UniqueId()), + ConfigVersion: flex.StringFromFramework(ctx, state.ConfigVersion), + Id: flex.StringFromFramework(ctx, plan.ID), + } + + if !plan.Description.Equal(state.Description) { + input.Description = aws.String(plan.Description.ValueString()) + update = true + } + + if !plan.SamlOptions.Equal(state.SamlOptions) { + input.SamlOptions = expandSAMLOptions(ctx, plan.SamlOptions, &resp.Diagnostics) + update = true + } + + if !update { + return + } + + out, err := conn.UpdateSecurityConfig(ctx, input) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("updating Security Policy (%s)", plan.Name.ValueString()), err.Error()) + return + } + plan.refreshFromOutput(ctx, out.SecurityConfigDetail) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceSecurityConfig) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().OpenSearchServerlessClient(ctx) + + var state resourceSecurityConfigData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + _, err := conn.DeleteSecurityConfig(ctx, &opensearchserverless.DeleteSecurityConfigInput{ + ClientToken: aws.String(sdkid.UniqueId()), + Id: flex.StringFromFramework(ctx, state.ID), + }) + if err != nil { + var nfe *awstypes.ResourceNotFoundException + if errors.As(err, &nfe) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionDeleting, ResNameSecurityConfig, state.Name.String(), nil), + err.Error(), + ) + } +} + +func (r *resourceSecurityConfig) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.Split(req.ID, idSeparator) + if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { + err := fmt.Errorf("unexpected format for ID (%[1]s), expected saml/account-id/name", req.ID) + resp.Diagnostics.AddError(fmt.Sprintf("importing Security Policy (%s)", req.ID), err.Error()) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), parts[2])...) +} + +type resourceSecurityConfigData struct { + ID types.String `tfsdk:"id"` + ConfigVersion types.String `tfsdk:"config_version"` + Description types.String `tfsdk:"description"` + Name types.String `tfsdk:"name"` + SamlOptions types.Object `tfsdk:"saml_options"` + Type types.String `tfsdk:"type"` +} + +// refreshFromOutput writes state data from an AWS response object +func (rd *resourceSecurityConfigData) refreshFromOutput(ctx context.Context, out *awstypes.SecurityConfigDetail) { + if out == nil { + return + } + + rd.ID = flex.StringToFramework(ctx, out.Id) + rd.ConfigVersion = flex.StringToFramework(ctx, out.ConfigVersion) + rd.Description = flex.StringToFramework(ctx, out.Description) + rd.SamlOptions = flattenSAMLOptions(ctx, out.SamlOptions) + rd.Type = flex.StringValueToFramework(ctx, out.Type) +} + +type samlOptions struct { + GroupAttribute types.String `tfsdk:"group_attribute"` + Metadata types.String `tfsdk:"metadata"` + SessionTimeout types.Int64 `tfsdk:"session_timeout"` + UserAttribute types.String `tfsdk:"user_attribute"` +} + +func (so *samlOptions) expand(ctx context.Context) *awstypes.SamlConfigOptions { + if so == nil { + return nil + } + + result := &awstypes.SamlConfigOptions{ + Metadata: flex.StringFromFramework(ctx, so.Metadata), + GroupAttribute: flex.StringFromFramework(ctx, so.GroupAttribute), + UserAttribute: flex.StringFromFramework(ctx, so.UserAttribute), + } + + if so.SessionTimeout.ValueInt64() != 0 { + result.SessionTimeout = aws.Int32(int32(so.SessionTimeout.ValueInt64())) + } + + return result +} + +func expandSAMLOptions(ctx context.Context, object types.Object, diags *diag.Diagnostics) *awstypes.SamlConfigOptions { + var options samlOptions + diags.Append(object.As(ctx, &options, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil + } + + return options.expand(ctx) +} + +func flattenSAMLOptions(ctx context.Context, so *awstypes.SamlConfigOptions) types.Object { + attributeTypes := framework.AttributeTypesMust[samlOptions](ctx) + + if so == nil { + return types.ObjectNull(attributeTypes) + } + + attrs := map[string]attr.Value{} + attrs["group_attribute"] = flex.StringToFramework(ctx, so.GroupAttribute) + attrs["metadata"] = flex.StringToFramework(ctx, so.Metadata) + timeout := int64(*so.SessionTimeout) + attrs["session_timeout"] = flex.Int64ToFramework(ctx, &timeout) + attrs["user_attribute"] = flex.StringToFramework(ctx, so.UserAttribute) + + return types.ObjectValueMust(attributeTypes, attrs) +} diff --git a/internal/service/opensearchserverless/security_config_test.go b/internal/service/opensearchserverless/security_config_test.go new file mode 100644 index 00000000000..3b588739cd8 --- /dev/null +++ b/internal/service/opensearchserverless/security_config_test.go @@ -0,0 +1,216 @@ +package opensearchserverless_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/opensearchserverless" + "github.com/aws/aws-sdk-go-v2/service/opensearchserverless/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" + tfopensearchserverless "github.com/hashicorp/terraform-provider-aws/internal/service/opensearchserverless" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccOpenSearchServerlessSecurityConfig_basic(t *testing.T) { + ctx := acctest.Context(t) + var securityconfig types.SecurityConfigDetail + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_opensearchserverless_security_config.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.OpenSearchServerlessEndpointID) + testAccPreCheckSecurityConfig(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.OpenSearchServerlessEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckSecurityConfigDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccSecurityConfig_basic(rName, "test-fixtures/idp-metadata.xml"), + Check: resource.ComposeTestCheckFunc( + testAccCheckSecurityConfigExists(ctx, resourceName, &securityconfig), + resource.TestCheckResourceAttr(resourceName, "type", "saml"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrSet(resourceName, "saml_options.session_timeout"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccOpenSearchServerlessSecurityConfig_update(t *testing.T) { + ctx := acctest.Context(t) + var securityconfig types.SecurityConfigDetail + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_opensearchserverless_security_config.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.OpenSearchServerlessEndpointID) + testAccPreCheckSecurityConfig(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.OpenSearchServerlessEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckSecurityConfigDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccSecurityConfig_update(rName, "test-fixtures/idp-metadata.xml", "description", 60), + Check: resource.ComposeTestCheckFunc( + testAccCheckSecurityConfigExists(ctx, resourceName, &securityconfig), + resource.TestCheckResourceAttr(resourceName, "type", "saml"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "saml_options.session_timeout", "60"), + resource.TestCheckResourceAttr(resourceName, "description", "description"), + ), + }, + { + Config: testAccSecurityConfig_update(rName, "test-fixtures/idp-metadata.xml", "description updated", 40), + Check: resource.ComposeTestCheckFunc( + testAccCheckSecurityConfigExists(ctx, resourceName, &securityconfig), + resource.TestCheckResourceAttr(resourceName, "type", "saml"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "saml_options.session_timeout", "40"), + resource.TestCheckResourceAttr(resourceName, "description", "description updated"), + ), + }, + }, + }) +} + +func TestAccOpenSearchServerlessSecurityConfig_disappears(t *testing.T) { + ctx := acctest.Context(t) + var securityconfig types.SecurityConfigDetail + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_opensearchserverless_security_config.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.OpenSearchServerlessEndpointID) + testAccPreCheckSecurityConfig(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.OpenSearchServerlessEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckSecurityConfigDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccSecurityConfig_basic(rName, "test-fixtures/idp-metadata.xml"), + Check: resource.ComposeTestCheckFunc( + testAccCheckSecurityConfigExists(ctx, resourceName, &securityconfig), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfopensearchserverless.ResourceSecurityConfig, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckSecurityConfigDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).OpenSearchServerlessClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_opensearchserverless_security_config" { + continue + } + + _, err := tfopensearchserverless.FindSecurityConfigByID(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return create.Error(names.OpenSearchServerless, create.ErrActionCheckingDestroyed, tfopensearchserverless.ResNameSecurityConfig, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckSecurityConfigExists(ctx context.Context, name string, securityconfig *types.SecurityConfigDetail) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.OpenSearchServerless, create.ErrActionCheckingExistence, tfopensearchserverless.ResNameSecurityConfig, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.OpenSearchServerless, create.ErrActionCheckingExistence, tfopensearchserverless.ResNameSecurityConfig, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).OpenSearchServerlessClient(ctx) + resp, err := tfopensearchserverless.FindSecurityConfigByID(ctx, conn, rs.Primary.ID) + + if err != nil { + return create.Error(names.OpenSearchServerless, create.ErrActionCheckingExistence, tfopensearchserverless.ResNameSecurityConfig, rs.Primary.ID, err) + } + + *securityconfig = *resp + + return nil + } +} + +func testAccPreCheckSecurityConfig(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).OpenSearchServerlessClient(ctx) + + input := &opensearchserverless.ListSecurityConfigsInput{ + Type: types.SecurityConfigTypeSaml, + } + _, err := conn.ListSecurityConfigs(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccSecurityConfig_basic(rName string, samlOptions string) string { + return fmt.Sprintf(` +resource "aws_opensearchserverless_security_config" "test" { + name = %[1]q + type = "saml" + saml_options { + metadata = file("%[2]s") + } +} +`, rName, samlOptions) +} + +func testAccSecurityConfig_update(rName, samlOptions, description string, sessionTimeout int) string { + return fmt.Sprintf(` +resource "aws_opensearchserverless_security_config" "test" { + name = %[1]q + description = %[3]q + type = "saml" + + saml_options { + metadata = file("%[2]s") + session_timeout = %[4]d + } +} +`, rName, samlOptions, description, sessionTimeout) +} diff --git a/internal/service/opensearchserverless/service_package_gen.go b/internal/service/opensearchserverless/service_package_gen.go index 40623a0184a..426e6bfe99e 100644 --- a/internal/service/opensearchserverless/service_package_gen.go +++ b/internal/service/opensearchserverless/service_package_gen.go @@ -27,6 +27,9 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic IdentifierAttribute: "arn", }, }, + { + Factory: newResourceSecurityConfig, + }, { Factory: newResourceSecurityPolicy, }, diff --git a/internal/service/opensearchserverless/sweep.go b/internal/service/opensearchserverless/sweep.go index 66896d5eb81..60ed7498a70 100644 --- a/internal/service/opensearchserverless/sweep.go +++ b/internal/service/opensearchserverless/sweep.go @@ -25,6 +25,10 @@ func init() { Name: "aws_opensearchserverless_collection", F: sweepCollections, }) + resource.AddTestSweepers("aws_opensearchserverless_security_config", &resource.Sweeper{ + Name: "aws_opensearchserverless_security_config", + F: sweepSecurityConfigs, + }) resource.AddTestSweepers("aws_opensearchserverless_security_policy", &resource.Sweeper{ Name: "aws_opensearchserverless_security_policy", F: sweepSecurityPolicies, @@ -129,6 +133,52 @@ func sweepCollections(region string) error { return errs.ErrorOrNil() } +func sweepSecurityConfigs(region string) error { + ctx := sweep.Context(region) + client, err := sweep.SharedRegionalSweepClient(region) + + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + + conn := client.(*conns.AWSClient).OpenSearchServerlessClient(ctx) + sweepResources := make([]sweep.Sweepable, 0) + var errs *multierror.Error + + input := &opensearchserverless.ListSecurityConfigsInput{ + Type: types.SecurityConfigTypeSaml, + } + pages := opensearchserverless.NewListSecurityConfigsPaginator(conn, input) + + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping OpenSearch Serverless Security Configs sweep for %s: %s", region, err) + return nil + } + if err != nil { + return fmt.Errorf("error retrieving OpenSearch Serverless Security Configs: %w", err) + } + + for _, sc := range page.SecurityConfigSummaries { + id := aws.ToString(sc.Id) + + log.Printf("[INFO] Deleting OpenSearch Serverless Security Config: %s", id) + sweepResources = append(sweepResources, sweep.NewSweepFrameworkResource(newResourceSecurityConfig, id, client)) + } + } + + if err := sweep.SweepOrchestratorWithContext(ctx, sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping OpenSearch Serverless Security Configs for %s: %w", region, err)) + } + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping OpenSearch Serverless Security Configs sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() +} + func sweepSecurityPolicies(region string) error { ctx := sweep.Context(region) client, err := sweep.SharedRegionalSweepClient(region) diff --git a/internal/service/opensearchserverless/test-fixtures/idp-metadata.xml b/internal/service/opensearchserverless/test-fixtures/idp-metadata.xml new file mode 100644 index 00000000000..9c82c3cd361 --- /dev/null +++ b/internal/service/opensearchserverless/test-fixtures/idp-metadata.xml @@ -0,0 +1,16 @@ + + + + + + + tls-certificate + + s + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + + + + diff --git a/website/docs/r/opensearchserverless_security_config.html.markdown b/website/docs/r/opensearchserverless_security_config.html.markdown new file mode 100644 index 00000000000..b9c066c2556 --- /dev/null +++ b/website/docs/r/opensearchserverless_security_config.html.markdown @@ -0,0 +1,55 @@ +--- +subcategory: "OpenSearch Serverless" +layout: "aws" +page_title: "AWS: aws_opensearchserverless_security_config" +description: |- + Terraform resource for managing an AWS OpenSearch Serverless Security Config. +--- + +# Resource: aws_opensearchserverless_security_config + +Terraform resource for managing an AWS OpenSearch Serverless Security Config. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_opensearchserverless_security_config" "example" { + name = "example" + type = "saml" + saml_options { + metadata = file("${path.module}/idp-metadata.xml") + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `name` - (Required, Forces new resource) Name of the policy. +* `saml_options` - (Required) Configuration block for SAML options. +* `type` - (Required) Type of configuration. Must be `saml`. + +The following arguments are optional: + +* `description` - (Optional) Description of the security configuration. + +### saml_options + +* `group_attribute` - (Optional) Group attribute for this SAML integration. +* `metadata` - (Required) The XML IdP metadata file generated from your identity provider. +* `session_timeout` - (Optional) Session timeout, in minutes. Minimum is 5 minutes and maximum is 720 minutes (12 hours). Default is 60 minutes. +* `user_attribute` - (Optional) User attribute for this SAML integration. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `config_version` - Version of the configuration. + +## Import + +OpenSearchServerless Access Policy can be imported using the `name` argument prefixed with the string `saml/account_id/` e.g +$ terraform import aws_opensearchserverless_security_config.example saml/123456789012/example