diff --git a/.changelog/1176.txt b/.changelog/1176.txt new file mode 100644 index 000000000..810d83487 --- /dev/null +++ b/.changelog/1176.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +pingone_rate_limit_configuration +``` diff --git a/docs/resources/rate_limit_configuration.md b/docs/resources/rate_limit_configuration.md new file mode 100644 index 000000000..a3941019d --- /dev/null +++ b/docs/resources/rate_limit_configuration.md @@ -0,0 +1,51 @@ +--- +page_title: "pingone_rate_limit_configuration Resource - terraform-provider-pingone" +subcategory: "Platform" +description: |- + Resource to create and manage rate limit configurations in PingOne. Rate limit configurations allow you to exclude specific IP addresses or CIDR ranges from rate limiting. +--- + +# pingone_rate_limit_configuration (Resource) + +Resource to create and manage rate limit configurations in PingOne. Rate limit configurations allow you to exclude specific IP addresses or CIDR ranges from rate limiting. + +## Example Usage + +```terraform +resource "pingone_environment" "my_environment" { + # ... +} + +resource "pingone_rate_limit_configuration" "my_rate_limit_configuration" { + environment_id = pingone_environment.my_environment.id + + type = "WHITELIST" + value = "192.0.2.0/24" +} +``` + + +## Schema + +### Required + +- `environment_id` (String) The ID of the environment to create the rate limit configuration in. Must be a valid PingOne resource ID. This field is immutable and will trigger a replace plan if changed. +- `value` (String) The IP address (IPv4 or IPv6), or a CIDR range, for the IP address or addresses to be excluded from rate limiting. + +### Optional + +- `type` (String) The type of rate limit configuration. Currently, the only type supported is `WHITELIST`, indicating that the IP address in `value` is to be excluded from rate limiting. Defaults to `WHITELIST`. + +### Read-Only + +- `created_at` (String) A string that specifies the time the resource was created. +- `id` (String) The ID of this resource. +- `updated_at` (String) A string that specifies the time the resource was last updated. + +## Import + +Import is supported using the following syntax, where attributes in `<>` brackets are replaced with the relevant ID. For example, `` should be replaced with the ID of the environment to import from. + +```shell +terraform import pingone_rate_limit_configuration.example / +``` diff --git a/examples/resources/pingone_rate_limit_configuration/import.sh b/examples/resources/pingone_rate_limit_configuration/import.sh new file mode 100644 index 000000000..650e802a3 --- /dev/null +++ b/examples/resources/pingone_rate_limit_configuration/import.sh @@ -0,0 +1 @@ +terraform import pingone_rate_limit_configuration.example / diff --git a/examples/resources/pingone_rate_limit_configuration/resource.tf b/examples/resources/pingone_rate_limit_configuration/resource.tf new file mode 100644 index 000000000..fc6697899 --- /dev/null +++ b/examples/resources/pingone_rate_limit_configuration/resource.tf @@ -0,0 +1,10 @@ +resource "pingone_environment" "my_environment" { + # ... +} + +resource "pingone_rate_limit_configuration" "my_rate_limit_configuration" { + environment_id = pingone_environment.my_environment.id + + type = "WHITELIST" + value = "192.0.2.0/24" +} diff --git a/go.mod b/go.mod index 9337b15b8..43bdca397 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/patrickcping/pingone-go-sdk-v2 v0.14.3 github.com/patrickcping/pingone-go-sdk-v2/authorize v0.8.2 github.com/patrickcping/pingone-go-sdk-v2/credentials v0.12.0 - github.com/patrickcping/pingone-go-sdk-v2/management v0.62.0 + github.com/patrickcping/pingone-go-sdk-v2/management v0.62.1-0.20251203115510-4232d8f7d174 github.com/patrickcping/pingone-go-sdk-v2/mfa v0.23.2 github.com/patrickcping/pingone-go-sdk-v2/risk v0.21.0 github.com/patrickcping/pingone-go-sdk-v2/verify v0.10.0 diff --git a/go.sum b/go.sum index 3e0a7c9cc..4d013e951 100644 --- a/go.sum +++ b/go.sum @@ -1480,8 +1480,8 @@ github.com/patrickcping/pingone-go-sdk-v2/authorize v0.8.2 h1:9tsJMj3aHOaF4Bdxvg github.com/patrickcping/pingone-go-sdk-v2/authorize v0.8.2/go.mod h1:42Te708LzeYC5z8axZopRPvQseDb1cvmWn8VNxO+YUc= github.com/patrickcping/pingone-go-sdk-v2/credentials v0.12.0 h1:NH1rJ4RuciyKEjV8WQ6chOSdwNbtTPuCUOedHX7GKas= github.com/patrickcping/pingone-go-sdk-v2/credentials v0.12.0/go.mod h1:oUdH/pGBp9JI/k0ptDgdprs/peruIwLurEHinuHDK90= -github.com/patrickcping/pingone-go-sdk-v2/management v0.62.0 h1:wO018lINer2iNWjM0RwqMH0/fiNQ4xOnUV+MtOmIYmk= -github.com/patrickcping/pingone-go-sdk-v2/management v0.62.0/go.mod h1:AuR8b02ntdqjAzgEYFFEeaOLig+LcsdQXmnpbG/1S2A= +github.com/patrickcping/pingone-go-sdk-v2/management v0.62.1-0.20251203115510-4232d8f7d174 h1:/Cw4CgWgN/4NR/KCoXpY4Q8ExPPG25mZC8K6x4tJV+o= +github.com/patrickcping/pingone-go-sdk-v2/management v0.62.1-0.20251203115510-4232d8f7d174/go.mod h1:AuR8b02ntdqjAzgEYFFEeaOLig+LcsdQXmnpbG/1S2A= github.com/patrickcping/pingone-go-sdk-v2/mfa v0.23.2 h1:V2iW5DT7JUZd7HBRcF8cOMjRCJ+UG9MCckWVZYVSA/0= github.com/patrickcping/pingone-go-sdk-v2/mfa v0.23.2/go.mod h1:2c47ma7Nu7X+hQTuBcB70FosnlUxjhsWPmlm/ErEJ80= github.com/patrickcping/pingone-go-sdk-v2/risk v0.21.0 h1:oW3EPdempOpVQXlCfKBqhQKusbeIZZMO3exImxXOFp4= diff --git a/internal/acctest/service/base/rate_limit_configuration.go b/internal/acctest/service/base/rate_limit_configuration.go new file mode 100644 index 000000000..7f1577749 --- /dev/null +++ b/internal/acctest/service/base/rate_limit_configuration.go @@ -0,0 +1,88 @@ +// Copyright © 2025 Ping Identity Corporation + +package base + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/patrickcping/pingone-go-sdk-v2/management" + "github.com/pingidentity/terraform-provider-pingone/internal/acctest" + "github.com/pingidentity/terraform-provider-pingone/internal/acctest/legacysdk" +) + +func RateLimitConfiguration_CheckDestroy(s *terraform.State) error { + var ctx = context.Background() + + p1Client, err := legacysdk.TestClient(ctx) + + if err != nil { + return err + } + + apiClient := p1Client.API.ManagementAPIClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "pingone_rate_limit_configuration" { + continue + } + + shouldContinue, err := legacysdk.CheckParentEnvironmentDestroy(ctx, apiClient, rs.Primary.Attributes["environment_id"]) + if err != nil { + return err + } + + if shouldContinue { + continue + } + + _, r, err := apiClient.RateLimitingApi.ReadOneRateLimitConfiguration(ctx, rs.Primary.Attributes["environment_id"], rs.Primary.ID).Execute() + + shouldContinue, err = acctest.CheckForResourceDestroy(r, err) + if err != nil { + return err + } + + if shouldContinue { + continue + } + + return fmt.Errorf("PingOne Rate Limit Configuration %s still exists", rs.Primary.ID) + } + + return nil +} + +func RateLimitConfiguration_GetIDs(resourceName string, environmentID, resourceID *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Resource Not found: %s", resourceName) + } + + if resourceID != nil { + *resourceID = rs.Primary.ID + } + + if environmentID != nil { + *environmentID = rs.Primary.Attributes["environment_id"] + } + + return nil + } +} + +func RateLimitConfiguration_RemovalDrift_PreConfig(ctx context.Context, apiClient *management.APIClient, t *testing.T, environmentID, rateLimitConfigurationID string) { + if environmentID == "" || rateLimitConfigurationID == "" { + t.Fatalf("One of environment ID or rate limit configuration ID cannot be determined. Environment ID: %s, Rate Limit Configuration ID: %s", environmentID, rateLimitConfigurationID) + } + + _, err := apiClient.RateLimitingApi.DeleteRateLimitConfiguration(ctx, environmentID, rateLimitConfigurationID).Execute() + if err != nil { + t.Fatalf("Failed to delete rate limit configuration: %v", err) + } +} diff --git a/internal/service/base/resource_rate_limit_configuration.go b/internal/service/base/resource_rate_limit_configuration.go new file mode 100644 index 000000000..6e54c8430 --- /dev/null +++ b/internal/service/base/resource_rate_limit_configuration.go @@ -0,0 +1,351 @@ +// Copyright © 2025 Ping Identity Corporation + +package base + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/stringdefault" + "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/patrickcping/pingone-go-sdk-v2/management" + "github.com/pingidentity/terraform-provider-pingone/internal/framework" + "github.com/pingidentity/terraform-provider-pingone/internal/framework/customtypes/pingonetypes" + "github.com/pingidentity/terraform-provider-pingone/internal/framework/legacysdk" + "github.com/pingidentity/terraform-provider-pingone/internal/sdk" + "github.com/pingidentity/terraform-provider-pingone/internal/verify" +) + +// Types +type RateLimitConfigurationResource serviceClientType + +type RateLimitConfigurationResourceModel struct { + Id pingonetypes.ResourceIDValue `tfsdk:"id"` + EnvironmentId pingonetypes.ResourceIDValue `tfsdk:"environment_id"` + Type types.String `tfsdk:"type"` + Value types.String `tfsdk:"value"` + CreatedAt timetypes.RFC3339 `tfsdk:"created_at"` + UpdatedAt timetypes.RFC3339 `tfsdk:"updated_at"` +} + +// Framework interfaces +var ( + _ resource.Resource = &RateLimitConfigurationResource{} + _ resource.ResourceWithConfigure = &RateLimitConfigurationResource{} + _ resource.ResourceWithImportState = &RateLimitConfigurationResource{} +) + +// New Object +func NewRateLimitConfigurationResource() resource.Resource { + return &RateLimitConfigurationResource{} +} + +// Metadata +func (r *RateLimitConfigurationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_rate_limit_configuration" +} + +// Schema +func (r *RateLimitConfigurationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + + providerDescription := framework.SchemaAttributeDescriptionFromMarkdown( + "Resource to create and manage rate limit configurations in PingOne. Rate limit configurations allow you to exclude specific IP addresses or CIDR ranges from rate limiting.", + ) + + typeDescription := framework.SchemaAttributeDescriptionFromMarkdown( + "The type of rate limit configuration. Currently, the only type supported is `WHITELIST`, indicating that the IP address in `value` is to be excluded from rate limiting.", + ).DefaultValue(string(management.ENUMRATELIMITCONFIGURATIONTYPE_WHITELIST)) + + valueDescription := framework.SchemaAttributeDescriptionFromMarkdown( + "The IP address (IPv4 or IPv6), or a CIDR range, for the IP address or addresses to be excluded from rate limiting.", + ) + + createdAtDescription := framework.SchemaAttributeDescriptionFromMarkdown( + "A string that specifies the time the resource was created.", + ) + + updatedAtDescription := framework.SchemaAttributeDescriptionFromMarkdown( + "A string that specifies the time the resource was last updated.", + ) + + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: providerDescription.MarkdownDescription, + Description: providerDescription.Description, + + Attributes: map[string]schema.Attribute{ + "id": framework.Attr_ID(), + "environment_id": framework.Attr_LinkID( + framework.SchemaAttributeDescriptionFromMarkdown("The ID of the environment to create the rate limit configuration in."), + ), + "type": schema.StringAttribute{ + MarkdownDescription: typeDescription.MarkdownDescription, + Description: typeDescription.Description, + Optional: true, + Computed: true, + Default: stringdefault.StaticString(string(management.ENUMRATELIMITCONFIGURATIONTYPE_WHITELIST)), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(string(management.ENUMRATELIMITCONFIGURATIONTYPE_WHITELIST)), + }, + }, + + "value": schema.StringAttribute{ + MarkdownDescription: valueDescription.MarkdownDescription, + Description: valueDescription.Description, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(verify.IPv4IPv6Regexp, "Values must be valid IPv4 or IPv6 CIDR format."), + }, + }, + + "created_at": schema.StringAttribute{ + MarkdownDescription: createdAtDescription.MarkdownDescription, + Description: createdAtDescription.Description, + Computed: true, + CustomType: timetypes.RFC3339Type{}, + }, + + "updated_at": schema.StringAttribute{ + MarkdownDescription: updatedAtDescription.MarkdownDescription, + Description: updatedAtDescription.Description, + Computed: true, + CustomType: timetypes.RFC3339Type{}, + }, + }, + } +} + +func (r *RateLimitConfigurationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + resourceConfig, ok := req.ProviderData.(legacysdk.ResourceType) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected the provider client, got: %T. Please report this issue to the provider maintainers.", req.ProviderData), + ) + + return + } + + r.Client = resourceConfig.Client.API + if r.Client == nil { + resp.Diagnostics.AddError( + "Client not initialised", + "Expected the PingOne client, got nil. Please report this issue to the provider maintainers.", + ) + return + } +} + +func (r *RateLimitConfigurationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan, state RateLimitConfigurationResourceModel + + if r.Client == nil || r.Client.ManagementAPIClient == nil { + resp.Diagnostics.AddError( + "Client not initialized", + "Expected the PingOne client, got nil. Please report this issue to the provider maintainers.") + return + } + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // Build the model for the API + rateLimitConfiguration := plan.expand() + + // Run the API call + var response *management.RateLimitConfiguration + resp.Diagnostics.Append(legacysdk.ParseResponse( + ctx, + + func() (any, *http.Response, error) { + fO, fR, fErr := r.Client.ManagementAPIClient.RateLimitingApi.CreateRateLimitConfiguration(ctx, plan.EnvironmentId.ValueString()).RateLimitConfiguration(*rateLimitConfiguration).Execute() + return legacysdk.CheckEnvironmentExistsOnPermissionsError(ctx, r.Client.ManagementAPIClient, plan.EnvironmentId.ValueString(), fO, fR, fErr) + }, + "CreateRateLimitConfiguration", + legacysdk.DefaultCustomError, + sdk.DefaultCreateReadRetryable, + &response, + )...) + if resp.Diagnostics.HasError() { + return + } + + // Create the state to save + state = plan + + // Save updated data into Terraform state + resp.Diagnostics.Append(state.toState(response)...) + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *RateLimitConfigurationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *RateLimitConfigurationResourceModel + + if r.Client == nil || r.Client.ManagementAPIClient == nil { + resp.Diagnostics.AddError( + "Client not initialized", + "Expected the PingOne client, got nil. Please report this issue to the provider maintainers.") + return + } + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Run the API call + var response *management.RateLimitConfiguration + resp.Diagnostics.Append(legacysdk.ParseResponse( + ctx, + + func() (any, *http.Response, error) { + fO, fR, fErr := r.Client.ManagementAPIClient.RateLimitingApi.ReadOneRateLimitConfiguration(ctx, data.EnvironmentId.ValueString(), data.Id.ValueString()).Execute() + return legacysdk.CheckEnvironmentExistsOnPermissionsError(ctx, r.Client.ManagementAPIClient, data.EnvironmentId.ValueString(), fO, fR, fErr) + }, + "ReadOneRateLimitConfiguration", + legacysdk.CustomErrorResourceNotFoundWarning, + sdk.DefaultCreateReadRetryable, + &response, + )...) + if resp.Diagnostics.HasError() { + return + } + + // Remove from state if resource is not found + if response == nil { + resp.State.RemoveResource(ctx) + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(data.toState(response)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *RateLimitConfigurationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Rate limit configurations are immutable - all fields require replacement + // This method should never be called as all fields have RequiresReplace plan modifiers +} + +func (r *RateLimitConfigurationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *RateLimitConfigurationResourceModel + + if r.Client == nil || r.Client.ManagementAPIClient == nil { + resp.Diagnostics.AddError( + "Client not initialized", + "Expected the PingOne client, got nil. Please report this issue to the provider maintainers.") + return + } + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Run the API call + resp.Diagnostics.Append(legacysdk.ParseResponse( + ctx, + + func() (any, *http.Response, error) { + fR, fErr := r.Client.ManagementAPIClient.RateLimitingApi.DeleteRateLimitConfiguration(ctx, data.EnvironmentId.ValueString(), data.Id.ValueString()).Execute() + return legacysdk.CheckEnvironmentExistsOnPermissionsError(ctx, r.Client.ManagementAPIClient, data.EnvironmentId.ValueString(), nil, fR, fErr) + }, + "DeleteRateLimitConfiguration", + legacysdk.CustomErrorResourceNotFoundWarning, + sdk.DefaultCreateReadRetryable, + nil, + )...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *RateLimitConfigurationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + + idComponents := []framework.ImportComponent{ + { + Label: "environment_id", + Regexp: verify.P1ResourceIDRegexp, + }, + { + Label: "rate_limit_configuration_id", + Regexp: verify.P1ResourceIDRegexp, + PrimaryID: true, + }, + } + + attributes, err := framework.ParseImportID(req.ID, idComponents...) + if err != nil { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + err.Error(), + ) + return + } + + for _, idComponent := range idComponents { + pathKey := idComponent.Label + + if idComponent.PrimaryID { + pathKey = "id" + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(pathKey), attributes[idComponent.Label])...) + } +} + +func (p *RateLimitConfigurationResourceModel) expand() *management.RateLimitConfiguration { + data := management.NewRateLimitConfiguration( + management.EnumRateLimitConfigurationType(p.Type.ValueString()), + p.Value.ValueString(), + ) + + return data +} + +func (p *RateLimitConfigurationResourceModel) toState(apiObject *management.RateLimitConfiguration) diag.Diagnostics { + var diags diag.Diagnostics + + if apiObject == nil { + diags.AddError( + "Data object missing", + "Cannot convert the data object to state as the data object is nil. Please report this to the provider maintainers.", + ) + + return diags + } + + p.Id = framework.PingOneResourceIDToTF(apiObject.GetId()) + p.Type = framework.EnumOkToTF(apiObject.GetTypeOk()) + p.Value = framework.StringOkToTF(apiObject.GetValueOk()) + p.CreatedAt = framework.TimeOkToTF(apiObject.GetCreatedAtOk()) + p.UpdatedAt = framework.TimeOkToTF(apiObject.GetUpdatedAtOk()) + + return diags +} diff --git a/internal/service/base/resource_rate_limit_configuration_test.go b/internal/service/base/resource_rate_limit_configuration_test.go new file mode 100644 index 000000000..d5bc4de34 --- /dev/null +++ b/internal/service/base/resource_rate_limit_configuration_test.go @@ -0,0 +1,390 @@ +// Copyright © 2025 Ping Identity Corporation + +package base_test + +import ( + "context" + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/pingidentity/terraform-provider-pingone/internal/acctest" + acctestlegacysdk "github.com/pingidentity/terraform-provider-pingone/internal/acctest/legacysdk" + "github.com/pingidentity/terraform-provider-pingone/internal/acctest/service/base" + baselegacysdk "github.com/pingidentity/terraform-provider-pingone/internal/acctest/service/base/legacysdk" + client "github.com/pingidentity/terraform-provider-pingone/internal/client" + "github.com/pingidentity/terraform-provider-pingone/internal/verify" +) + +func TestAccRateLimitConfiguration_RemovalDrift(t *testing.T) { + t.Parallel() + + resourceName := acctest.ResourceNameGen() + resourceFullName := fmt.Sprintf("pingone_rate_limit_configuration.%s", resourceName) + + environmentName := acctest.ResourceNameGenEnvironment() + + licenseID := os.Getenv("PINGONE_LICENSE_ID") + + var rateLimitConfigurationID, environmentID string + + var p1Client *client.Client + var ctx = context.Background() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheckClient(t) + acctest.PreCheckNewEnvironment(t) + acctest.PreCheckNoBeta(t) + p1Client = acctestlegacysdk.PreCheckTestClient(ctx, t) + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: base.RateLimitConfiguration_CheckDestroy, + ErrorCheck: acctest.ErrorCheck(t), + Steps: []resource.TestStep{ + // Test removal of the resource + { + Config: testAccRateLimitConfigurationConfig_RemovalDrift(resourceName), + Check: base.RateLimitConfiguration_GetIDs(resourceFullName, &environmentID, &rateLimitConfigurationID), + }, + { + PreConfig: func() { + base.RateLimitConfiguration_RemovalDrift_PreConfig(ctx, p1Client.API.ManagementAPIClient, t, environmentID, rateLimitConfigurationID) + }, + RefreshState: true, + ExpectNonEmptyPlan: true, + }, + // Test removal of the environment + { + Config: testAccRateLimitConfigurationConfig_IPv4_NewEnv(environmentName, licenseID, resourceName), + Check: base.RateLimitConfiguration_GetIDs(resourceFullName, &environmentID, &rateLimitConfigurationID), + }, + { + PreConfig: func() { + baselegacysdk.Environment_RemovalDrift_PreConfig(ctx, p1Client.API.ManagementAPIClient, t, environmentID) + }, + RefreshState: true, + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccRateLimitConfiguration_Full(t *testing.T) { + t.Parallel() + + resourceName := acctest.ResourceNameGen() + resourceFullName := fmt.Sprintf("pingone_rate_limit_configuration.%s", resourceName) + + ipv4Step := resource.TestStep{ + Config: testAccRateLimitConfigurationConfig_IPv4(resourceName), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr(resourceFullName, "id", verify.P1ResourceIDRegexpFullString), + resource.TestMatchResourceAttr(resourceFullName, "environment_id", verify.P1ResourceIDRegexpFullString), + resource.TestCheckResourceAttr(resourceFullName, "type", "WHITELIST"), + resource.TestCheckResourceAttr(resourceFullName, "value", "192.0.2.1"), + ), + } + + ipv4CIDRStep := resource.TestStep{ + Config: testAccRateLimitConfigurationConfig_IPv4CIDR(resourceName), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr(resourceFullName, "id", verify.P1ResourceIDRegexpFullString), + resource.TestMatchResourceAttr(resourceFullName, "environment_id", verify.P1ResourceIDRegexpFullString), + resource.TestCheckResourceAttr(resourceFullName, "type", "WHITELIST"), + resource.TestCheckResourceAttr(resourceFullName, "value", "198.51.100.0/28"), + ), + } + + ipv6Step := resource.TestStep{ + Config: testAccRateLimitConfigurationConfig_IPv6(resourceName), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr(resourceFullName, "id", verify.P1ResourceIDRegexpFullString), + resource.TestMatchResourceAttr(resourceFullName, "environment_id", verify.P1ResourceIDRegexpFullString), + resource.TestCheckResourceAttr(resourceFullName, "type", "WHITELIST"), + resource.TestCheckResourceAttr(resourceFullName, "value", "2001:0DB8:0000:0001:0000:0000:0000:0001"), + ), + } + + ipv6CIDRStep := resource.TestStep{ + Config: testAccRateLimitConfigurationConfig_IPv6CIDR(resourceName), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr(resourceFullName, "id", verify.P1ResourceIDRegexpFullString), + resource.TestMatchResourceAttr(resourceFullName, "environment_id", verify.P1ResourceIDRegexpFullString), + resource.TestCheckResourceAttr(resourceFullName, "type", "WHITELIST"), + resource.TestCheckResourceAttr(resourceFullName, "value", "2001:0DB8:0001:0000:0000:0000:0000:0000/48"), + ), + } + + defaultTypeStep := resource.TestStep{ + Config: testAccRateLimitConfigurationConfig_DefaultType(resourceName), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr(resourceFullName, "id", verify.P1ResourceIDRegexpFullString), + resource.TestMatchResourceAttr(resourceFullName, "environment_id", verify.P1ResourceIDRegexpFullString), + resource.TestCheckResourceAttr(resourceFullName, "type", "WHITELIST"), + resource.TestCheckResourceAttr(resourceFullName, "value", "203.0.113.0/28"), + ), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheckClient(t) + acctest.PreCheckNoBeta(t) + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: base.RateLimitConfiguration_CheckDestroy, + ErrorCheck: acctest.ErrorCheck(t), + Steps: []resource.TestStep{ + ipv4Step, + { + Config: testAccRateLimitConfigurationConfig_IPv4(resourceName), + Destroy: true, + }, + ipv4CIDRStep, + { + Config: testAccRateLimitConfigurationConfig_IPv4CIDR(resourceName), + Destroy: true, + }, + ipv6Step, + { + Config: testAccRateLimitConfigurationConfig_IPv6(resourceName), + Destroy: true, + }, + ipv6CIDRStep, + { + Config: testAccRateLimitConfigurationConfig_IPv6CIDR(resourceName), + Destroy: true, + }, + defaultTypeStep, + { + Config: testAccRateLimitConfigurationConfig_DefaultType(resourceName), + Destroy: true, + }, + ipv4Step, + ipv4CIDRStep, + ipv6Step, + ipv6CIDRStep, + defaultTypeStep, + { + ResourceName: resourceFullName, + ImportStateIdFunc: func() resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceFullName] + if !ok { + return "", fmt.Errorf("Resource Not found: %s", resourceFullName) + } + + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["environment_id"], rs.Primary.ID), nil + } + }(), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccRateLimitConfiguration_ValidationChecks(t *testing.T) { + t.Parallel() + + resourceName := acctest.ResourceNameGen() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheckClient(t) + acctest.PreCheckNoBeta(t) + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: base.RateLimitConfiguration_CheckDestroy, + ErrorCheck: acctest.ErrorCheck(t), + Steps: []resource.TestStep{ + { + Config: testAccRateLimitConfigurationConfig_InvalidIPv4(resourceName), + ExpectError: regexp.MustCompile(`Invalid Attribute Value Match`), + }, + { + Config: testAccRateLimitConfigurationConfig_InvalidIPv6(resourceName), + ExpectError: regexp.MustCompile(`Invalid Attribute Value Match`), + }, + { + Config: testAccRateLimitConfigurationConfig_InvalidCIDR(resourceName), + ExpectError: regexp.MustCompile(`Invalid Attribute Value Match`), + }, + }, + }) +} + +func TestAccRateLimitConfiguration_BadParameters(t *testing.T) { + t.Parallel() + + resourceName := acctest.ResourceNameGen() + resourceFullName := fmt.Sprintf("pingone_rate_limit_configuration.%s", resourceName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheckClient(t) + acctest.PreCheckNoBeta(t) + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: base.RateLimitConfiguration_CheckDestroy, + ErrorCheck: acctest.ErrorCheck(t), + Steps: []resource.TestStep{ + // Configure + { + Config: testAccRateLimitConfigurationConfig_BadParameters(resourceName), + }, + // Errors + { + ResourceName: resourceFullName, + ImportState: true, + ExpectError: regexp.MustCompile(`Unexpected Import Identifier`), + }, + { + ResourceName: resourceFullName, + ImportStateId: "/", + ImportState: true, + ExpectError: regexp.MustCompile(`Unexpected Import Identifier`), + }, + { + ResourceName: resourceFullName, + ImportStateId: "badformat/badformat", + ImportState: true, + ExpectError: regexp.MustCompile(`Unexpected Import Identifier`), + }, + }, + }) +} + +func testAccRateLimitConfigurationConfig_IPv4_NewEnv(environmentName, licenseID, resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "pingone_rate_limit_configuration" "%[3]s" { + environment_id = pingone_environment.%[2]s.id + + type = "WHITELIST" + value = "192.168.1.1" +}`, acctestlegacysdk.MinimalSandboxEnvironment(environmentName, licenseID), environmentName, resourceName) +} + +func testAccRateLimitConfigurationConfig_IPv4(resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "pingone_rate_limit_configuration" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + + type = "WHITELIST" + value = "192.0.2.1" +}`, acctest.GenericSandboxEnvironment(), resourceName) +} + +func testAccRateLimitConfigurationConfig_IPv4CIDR(resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "pingone_rate_limit_configuration" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + + type = "WHITELIST" + value = "198.51.100.0/28" +}`, acctest.GenericSandboxEnvironment(), resourceName) +} + +func testAccRateLimitConfigurationConfig_IPv6(resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "pingone_rate_limit_configuration" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + + type = "WHITELIST" + value = "2001:0DB8:0000:0001:0000:0000:0000:0001" +}`, acctest.GenericSandboxEnvironment(), resourceName) +} + +func testAccRateLimitConfigurationConfig_IPv6CIDR(resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "pingone_rate_limit_configuration" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + + type = "WHITELIST" + value = "2001:0DB8:0001:0000:0000:0000:0000:0000/48" +}`, acctest.GenericSandboxEnvironment(), resourceName) +} + +func testAccRateLimitConfigurationConfig_DefaultType(resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "pingone_rate_limit_configuration" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + + value = "203.0.113.0/28" +}`, acctest.GenericSandboxEnvironment(), resourceName) +} + +func testAccRateLimitConfigurationConfig_InvalidIPv4(resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "pingone_rate_limit_configuration" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + + type = "WHITELIST" + value = "not-an-ip-address" +}`, acctest.GenericSandboxEnvironment(), resourceName) +} + +func testAccRateLimitConfigurationConfig_InvalidIPv6(resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "pingone_rate_limit_configuration" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + + type = "WHITELIST" + value = "ZZZZ:ZZZZ:ZZZZ:ZZZZ:ZZZZ:ZZZZ:ZZZZ:ZZZZ" +}`, acctest.GenericSandboxEnvironment(), resourceName) +} + +func testAccRateLimitConfigurationConfig_InvalidCIDR(resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "pingone_rate_limit_configuration" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + + type = "WHITELIST" + value = "10.0.0.0/999" +}`, acctest.GenericSandboxEnvironment(), resourceName) +} + +func testAccRateLimitConfigurationConfig_RemovalDrift(resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "pingone_rate_limit_configuration" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + + type = "WHITELIST" + value = "192.0.2.10" +}`, acctest.GenericSandboxEnvironment(), resourceName) +} + +func testAccRateLimitConfigurationConfig_BadParameters(resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "pingone_rate_limit_configuration" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + + type = "WHITELIST" + value = "192.0.2.20" +}`, acctest.GenericSandboxEnvironment(), resourceName) +} diff --git a/internal/service/base/service.go b/internal/service/base/service.go index 269279e41..62afb6f7a 100644 --- a/internal/service/base/service.go +++ b/internal/service/base/service.go @@ -41,6 +41,7 @@ func Resources() []func() resource.Resource { NewNotificationSettingsResource, NewNotificationTemplateContentResource, NewPhoneDeliverySettingsResource, + NewRateLimitConfigurationResource, NewRoleAssignmentUserResource, NewSystemApplicationResource, NewTrustedEmailAddressResource, diff --git a/internal/service/sso/resource_application.go b/internal/service/sso/resource_application.go index 0b474d41f..5b6fc1f6b 100644 --- a/internal/service/sso/resource_application.go +++ b/internal/service/sso/resource_application.go @@ -2551,9 +2551,9 @@ func (p *applicationResourceModelV1) expandApplicationOIDC(ctx context.Context) p.Name.ValueString(), management.ENUMAPPLICATIONPROTOCOL_OPENID_CONNECT, management.EnumApplicationType(plan.Type.ValueString()), - grantTypes, management.EnumApplicationOIDCTokenAuthMethod(plan.TokenEndpointAuthnMethod.ValueString()), ) + data.SetGrantTypes(grantTypes) applicationCommon, d := p.expandApplicationCommon(ctx) diags.Append(d...) diff --git a/templates/resources/rate_limit_configuration.md.tmpl b/templates/resources/rate_limit_configuration.md.tmpl new file mode 100644 index 000000000..331581321 --- /dev/null +++ b/templates/resources/rate_limit_configuration.md.tmpl @@ -0,0 +1,26 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.RenderedProviderName}}" +subcategory: "Platform" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "%s%s%s" "examples/resources/" .Name "/resource.tf") }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} + +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax, where attributes in `<>` brackets are replaced with the relevant ID. For example, `` should be replaced with the ID of the environment to import from. + +{{ codefile "shell" (printf "%s%s%s" "examples/resources/" .Name "/import.sh") }} +{{- end }}