diff --git a/docs/resources/tenant.md b/docs/resources/tenant.md index fcba954..98c6832 100644 --- a/docs/resources/tenant.md +++ b/docs/resources/tenant.md @@ -233,6 +233,13 @@ resource "fusionauth_tenant" "example" { require_number = false validate_on_login = false } + rate_limit_configuration { + failed_login { + enabled = true + limit = 5 + time_period_in_seconds = 60 + } + } theme_id = fusionauth_theme.example_theme.id user_delete_policy { unverified_enabled = false @@ -405,6 +412,11 @@ resource "fusionauth_tenant" "example" { - `require_non_alpha` - (Optional) Whether to force the user to use at least one non-alphanumeric character. - `require_number` - (Optional) Whether to force the user to use at least one number. - `validate_on_login` - (Optional) When enabled the user’s password will be validated during login. If the password does not meet the currently configured validation rules the user will be required to change their password. +* `rate_limit_configuration` - (Optional) + - `failed_login` - (Optional) + - `enabled` - (Optional) Whether rate limiting is enabled for failed login. + - `limit` - (Optional) The number of times a user can fail to login within the configured timePeriodInSeconds duration. If a Failed authentication action has been configured then it will take precedence. + - `time_period_in_seconds` - (Optional) The duration for the number of times a user can fail login before being rate limited. * `theme_id` - (Required) The unique Id of the theme to be used to style the login page and other end user templates. * `username_configuration` - (Optional) - `unique` - (Optional) Indicates that users without a verified email address will be permanently deleted after tenant.userDeletePolicy.unverified.numberOfDaysToRetain days. diff --git a/fusionauth/resource_fusionauth_form_field.go b/fusionauth/resource_fusionauth_form_field.go index 88e58b4..b3ab3eb 100644 --- a/fusionauth/resource_fusionauth_form_field.go +++ b/fusionauth/resource_fusionauth_form_field.go @@ -266,7 +266,7 @@ func validateKey(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) - return + return warnings, errors } switch v { @@ -287,7 +287,7 @@ func validateKey(i interface{}, k string) (warnings []string, errors []error) { "user.timezone", "user.twoFactorEnabled", "user.username": - return + return warnings, errors default: if !strings.HasPrefix(v, "user.data.") && !strings.HasPrefix(v, "registration.data.") { errors = append( @@ -329,11 +329,11 @@ func validateRegex(i interface{}, k string) (warnings []string, errors []error) v, ok := i.(string) if !ok { errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) - return + return warnings, errors } if _, err := regexp.Compile(v); err != nil { return warnings, append(errors, err) } - return + return warnings, errors } diff --git a/fusionauth/resource_fusionauth_tenant.go b/fusionauth/resource_fusionauth_tenant.go index e1a38c7..e633e3d 100644 --- a/fusionauth/resource_fusionauth_tenant.go +++ b/fusionauth/resource_fusionauth_tenant.go @@ -507,6 +507,46 @@ func newTenant() *schema.Resource { Computed: true, Elem: newPasswordValidationRules(), }, + "rate_limit_configuration": { + Optional: true, + Computed: true, + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "failed_login": { + Optional: true, + Computed: true, + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether rate limiting is enabled for failed login.", + }, + "limit": { + Type: schema.TypeInt, + Optional: true, + Default: 5, + Description: "The number of times a user can fail to login within the configured timePeriodInSeconds duration. If a Failed authentication action has been configured then it will take precedence. Value must be greater than 0.", + ValidateFunc: validation.IntAtLeast(1), + }, + "time_period_in_seconds": { + Type: schema.TypeInt, + Optional: true, + Default: 60, + Description: "The duration for the number of times a user can fail login before being rate limited. Value must be greater than 0.", + ValidateFunc: validation.IntAtLeast(1), + }, + }, + }, + }, + }, + }, + }, "theme_id": { Type: schema.TypeString, Required: true, diff --git a/fusionauth/resource_fusionauth_tenant_helpers.go b/fusionauth/resource_fusionauth_tenant_helpers.go index 2a9844b..cc8aa66 100644 --- a/fusionauth/resource_fusionauth_tenant_helpers.go +++ b/fusionauth/resource_fusionauth_tenant_helpers.go @@ -238,6 +238,13 @@ func buildTenant(data *schema.ResourceData) (fusionauth.Tenant, diag.Diagnostics RequireNumber: data.Get("password_validation_rules.0.require_number").(bool), ValidateOnLogin: data.Get("password_validation_rules.0.validate_on_login").(bool), }, + RateLimitConfiguration: fusionauth.TenantRateLimitConfiguration{ + FailedLogin: fusionauth.RateLimitedRequestConfiguration{ + Enableable: buildEnableable("rate_limit_configuration.0.failed_login.0.enabled", data), + Limit: data.Get("rate_limit_configuration.0.failed_login.0.limit").(int), + TimePeriodInSeconds: data.Get("rate_limit_configuration.0.failed_login.0.time_period_in_seconds").(int), + }, + }, ThemeId: data.Get("theme_id").(string), UserDeletePolicy: fusionauth.TenantUserDeletePolicy{ Unverified: fusionauth.TimeBasedDeletePolicy{ @@ -275,7 +282,7 @@ func buildConnectorPolicies(data *schema.ResourceData) (connectorPolicies []fusi } // Nothing to do here! - return + return connectorPolicies, diags } connectorPolicies = make([]fusionauth.ConnectorPolicy, len(policiesData)) diff --git a/fusionauth/resource_fusionauth_tenant_test.go b/fusionauth/resource_fusionauth_tenant_test.go index 85c1461..5577349 100644 --- a/fusionauth/resource_fusionauth_tenant_test.go +++ b/fusionauth/resource_fusionauth_tenant_test.go @@ -241,6 +241,12 @@ func testTenantAccTestCheckFuncs( resource.TestCheckResourceAttr(tfResourcePath, "password_validation_rules.0.require_number", "true"), resource.TestCheckResourceAttr(tfResourcePath, "password_validation_rules.0.validate_on_login", "true"), + // rate_limit_configuration + resource.TestCheckResourceAttrSet(tfResourcePath, "rate_limit_configuration"), + resource.TestCheckResourceAttr(tfResourcePath, "rate_limit_configuration.0.failed_login.0.enabled", "true"), + resource.TestCheckResourceAttr(tfResourcePath, "rate_limit_configuration.0.failed_login.0.limit", "5"), + resource.TestCheckResourceAttr(tfResourcePath, "rate_limit_configuration.0.failed_login.0.time_period_in_seconds", "60"), + resource.TestCheckResourceAttrSet(tfResourcePath, "theme_id"), // user_delete_policy @@ -594,6 +600,13 @@ resource "fusionauth_tenant" "test_%[1]s" { require_number = true validate_on_login = true } + rate_limit_configuration { + failed_login { + enabled = true + limit = 5 + time_period_in_seconds = 60 + } + } # theme_id%[2]s user_delete_policy { unverified_enabled = true diff --git a/fusionauth/resource_fusionauth_user.go b/fusionauth/resource_fusionauth_user.go index 2a669ee..e1a7b98 100644 --- a/fusionauth/resource_fusionauth_user.go +++ b/fusionauth/resource_fusionauth_user.go @@ -278,7 +278,7 @@ func dataToTwoFactorMethods(data *schema.ResourceData) (twoFactorMethods []fusio } // Nothing to do here! - return + return twoFactorMethods, diags } twoFactorMethods = make([]fusionauth.TwoFactorMethod, len(twoFactorMethodsData))