diff --git a/auth0/provider.go b/auth0/provider.go index 06e0c0353..59ea8be21 100644 --- a/auth0/provider.go +++ b/auth0/provider.go @@ -77,6 +77,7 @@ func Provider() *schema.Provider { "auth0_organization": newOrganization(), "auth0_action": newAction(), "auth0_trigger_binding": newTriggerBinding(), + "auth0_attack_protection": newAttackProtection(), }, DataSourcesMap: map[string]*schema.Resource{ "auth0_client": newDataClient(), diff --git a/auth0/resource_auth0_attack_protection.go b/auth0/resource_auth0_attack_protection.go new file mode 100644 index 000000000..ca1001cfb --- /dev/null +++ b/auth0/resource_auth0_attack_protection.go @@ -0,0 +1,428 @@ +package auth0 + +import ( + "fmt" + "net/http" + + "github.com/auth0/go-auth0/management" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func newAttackProtection() *schema.Resource { + return &schema.Resource{ + Create: createAttackProtection, + Read: readAttackProtection, + Update: updateAttackProtection, + Delete: deleteAttackProtection, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "breached_password_detection": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Computed: true, + Description: "Breached password detection protects your applications from bad actors logging in with stolen credentials.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether or not breached password detection is active.", + }, + "shields": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + "block", + "user_notification", + "admin_notification", + }, false), + }, + Optional: true, + Description: "Action to take when a breached password is detected.", + }, + "admin_notification_frequency": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + "immediately", + "daily", + "weekly", + "monthly", + }, false), + }, + Optional: true, + Description: "When \"admin_notification\" is enabled, determines how often email notifications are sent.", + }, + "method": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "standard", "enhanced", + }, false), + Description: "The subscription level for breached password detection methods. Use \"enhanced\" to enable Credential Guard.", + }, + }, + }, + }, + "brute_force_protection": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Computed: true, + Description: "Brute-force protection safeguards against a single IP address attacking a single user account.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether or not brute force attack protections are active.", + }, + "shields": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + "block", + "user_notification", + }, false), + }, + Optional: true, + Description: "Action to take when a brute force protection threshold is violated.", + }, + "allowlist": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Description: "List of trusted IP addresses that will not have attack protection enforced against them.", + }, + "mode": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "count_per_identifier_and_ip", "count_per_identifier", + }, false), + Description: "Account Lockout: Determines whether or not IP address is used when counting failed attempts.", + }, + "max_attempts": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), + Description: "Maximum number of unsuccessful attempts.", + }, + }, + }, + }, + "suspicious_ip_throttling": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Description: "Suspicious IP throttling blocks traffic from any IP address that rapidly attempts too many logins or signups.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether or not suspicious IP throttling attack protections are active.", + }, + "shields": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + "block", + "admin_notification", + }, false), + }, + Description: "Action to take when a suspicious IP throttling threshold is violated.", + }, + "allowlist": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Description: "List of trusted IP addresses that will not have attack protection enforced against them.", + }, + "pre_login": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Configuration options that apply before every login attempt.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max_attempts": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), + Description: "Total number of attempts allowed per day.", + }, + "rate": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), + Description: "Interval of time, given in milliseconds, at which new attempts are granted.", + }, + }, + }, + }, + "pre_user_registration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Configuration options that apply before every user registration attempt.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max_attempts": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), + Description: "Total number of attempts allowed.", + }, + "rate": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), + Description: "Interval of time, given in milliseconds, at which new attempts are granted.", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func readAttackProtection(d *schema.ResourceData, m interface{}) error { + api := m.(*management.Management) + + ipThrottling, err := api.AttackProtection.GetSuspiciousIPThrottling() + if err != nil { + if mErr, ok := err.(management.Error); ok { + if mErr.Status() == http.StatusNotFound { + d.SetId("") + return nil + } + } + return err + } + + if err = d.Set("suspicious_ip_throttling", flattenSuspiciousIPThrottling(ipThrottling)); err != nil { + return err + } + + bruteForce, err := api.AttackProtection.GetBruteForceProtection() + if err != nil { + if mErr, ok := err.(management.Error); ok { + if mErr.Status() == http.StatusNotFound { + d.SetId("") + return nil + } + } + return err + } + + if err = d.Set("brute_force_protection", flattenBruteForceProtection(bruteForce)); err != nil { + return err + } + + breachedPasswords, err := api.AttackProtection.GetBreachedPasswordDetection() + if err != nil { + if mErr, ok := err.(management.Error); ok { + if mErr.Status() == http.StatusNotFound { + d.SetId("") + return nil + } + } + return err + } + + if err = d.Set("breached_password_detection", flattenBreachedPasswordProtection(breachedPasswords)); err != nil { + return err + } + + return nil +} + +func flattenSuspiciousIPThrottling(ipt *management.SuspiciousIPThrottling) []interface{} { + m := make(map[string]interface{}) + if ipt != nil { + m["enabled"] = ipt.Enabled + m["allowlist"] = ipt.AllowList + m["shields"] = ipt.Shields + m["pre_login"] = []interface{}{ + map[string]int{ + "max_attempts": ipt.Stage.PreLogin.GetMaxAttempts(), + "rate": ipt.Stage.PreLogin.GetRate(), + }, + } + m["pre_user_registration"] = []interface{}{ + map[string]int{ + "max_attempts": ipt.Stage.PreUserRegistration.GetMaxAttempts(), + "rate": ipt.Stage.PreUserRegistration.GetRate(), + }, + } + } + return []interface{}{m} +} + +func flattenBruteForceProtection(bfp *management.BruteForceProtection) []interface{} { + m := make(map[string]interface{}) + if bfp != nil { + m["enabled"] = bfp.Enabled + m["max_attempts"] = bfp.MaxAttempts + m["mode"] = bfp.Mode + m["allowlist"] = bfp.AllowList + m["shields"] = bfp.Shields + } + return []interface{}{m} +} + +func flattenBreachedPasswordProtection(bpd *management.BreachedPasswordDetection) []interface{} { + m := make(map[string]interface{}) + if bpd != nil { + m["enabled"] = bpd.Enabled + m["admin_notification_frequency"] = bpd.AdminNotificationFrequency + m["method"] = bpd.Method + m["shields"] = bpd.Shields + } + return []interface{}{m} +} + +func updateAttackProtection(d *schema.ResourceData, m interface{}) error { + api := m.(*management.Management) + + ipt := expandSuspiciousIPThrottling(d) + err := api.AttackProtection.UpdateSuspiciousIPThrottling(ipt) + if err != nil { + return err + } + + bfp := expandBruteForceProtection(d) + err = api.AttackProtection.UpdateBruteForceProtection(bfp) + if err != nil { + return err + } + + bpd := expandBreachedPasswordDetection(d) + err = api.AttackProtection.UpdateBreachedPasswordDetection(bpd) + if err != nil { + return err + } + + return readAttackProtection(d, m) +} + +func expandSuspiciousIPThrottling(d *schema.ResourceData) *management.SuspiciousIPThrottling { + ipt := &management.SuspiciousIPThrottling{} + + List(d, "suspicious_ip_throttling", IsNewResource(), HasChange()).Elem(func(d ResourceData) { + shields := []string{} + for _, s := range d.Get("shields").([]interface{}) { + shields = append(shields, fmt.Sprintf("%s", s)) + } + + allowlist := []string{} + for _, a := range d.Get("allowlist").([]interface{}) { + allowlist = append(allowlist, fmt.Sprintf("%s", a)) + } + + ipt = &management.SuspiciousIPThrottling{ + Enabled: Bool(d, "enabled"), + Shields: &shields, + AllowList: &allowlist, + Stage: &management.Stage{ + PreLogin: &management.PreLogin{}, + PreUserRegistration: &management.PreUserRegistration{}, + }, + } + + List(d, "pre_login").Elem(func(d ResourceData) { + ipt.Stage.PreLogin.MaxAttempts = Int(d, "max_attempts") + ipt.Stage.PreLogin.Rate = Int(d, "rate") + }) + + List(d, "pre_user_registration").Elem(func(d ResourceData) { + ipt.Stage.PreUserRegistration.MaxAttempts = Int(d, "max_attempts") + ipt.Stage.PreUserRegistration.Rate = Int(d, "rate") + }) + }) + + return ipt +} + +func expandBruteForceProtection(d *schema.ResourceData) *management.BruteForceProtection { + bfp := &management.BruteForceProtection{} + + List(d, "brute_force_protection", IsNewResource(), HasChange()).Elem(func(d ResourceData) { + shields := []string{} + for _, s := range d.Get("shields").([]interface{}) { + shields = append(shields, fmt.Sprintf("%s", s)) + } + + allowlist := []string{} + for _, a := range d.Get("allowlist").([]interface{}) { + allowlist = append(allowlist, fmt.Sprintf("%s", a)) + } + + bfp = &management.BruteForceProtection{ + Enabled: Bool(d, "enabled"), + Shields: &shields, + AllowList: &allowlist, + Mode: String(d, "mode"), + MaxAttempts: Int(d, "max_attempts"), + } + }) + + return bfp +} + +func expandBreachedPasswordDetection(d *schema.ResourceData) *management.BreachedPasswordDetection { + bpd := &management.BreachedPasswordDetection{} + + List(d, "breached_password_detection", IsNewResource(), HasChange()).Elem(func(d ResourceData) { + shields := []string{} + for _, s := range d.Get("shields").([]interface{}) { + shields = append(shields, fmt.Sprintf("%s", s)) + } + + notificationFreq := []string{} + for _, a := range d.Get("admin_notification_frequency").([]interface{}) { + notificationFreq = append(notificationFreq, fmt.Sprintf("%s", a)) + } + + bpd = &management.BreachedPasswordDetection{ + Enabled: Bool(d, "enabled"), + Shields: &shields, + Method: String(d, "method"), + AdminNotificationFrequency: ¬ificationFreq, + } + }) + + return bpd +} + +func createAttackProtection(d *schema.ResourceData, m interface{}) error { + d.SetId(resource.UniqueId()) + return updateAttackProtection(d, m) +} + +func deleteAttackProtection(d *schema.ResourceData, m interface{}) error { + d.SetId("") + return nil +} diff --git a/auth0/resource_auth0_attack_protection_test.go b/auth0/resource_auth0_attack_protection_test.go new file mode 100644 index 000000000..135378440 --- /dev/null +++ b/auth0/resource_auth0_attack_protection_test.go @@ -0,0 +1,97 @@ +package auth0 + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccAttackProtection(t *testing.T) { + + resource.Test(t, resource.TestCase{ + Providers: map[string]terraform.ResourceProvider{ + "auth0": Provider(), + }, + Steps: []resource.TestStep{ + { + Config: testAttackProtectionCreate, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("auth0_attack_protection.my_protection_tests", "breached_password_detection.0.enabled", "true"), + resource.TestCheckResourceAttr("auth0_attack_protection.my_protection_tests", "breached_password_detection.0.method", "standard"), + resource.TestCheckResourceAttr("auth0_attack_protection.my_protection_tests", "brute_force_protection.0.enabled", "true"), + resource.TestCheckResourceAttr("auth0_attack_protection.my_protection_tests", "brute_force_protection.0.max_attempts", "10"), + resource.TestCheckResourceAttr("auth0_attack_protection.my_protection_tests", "brute_force_protection.0.mode", "count_per_identifier_and_ip"), + resource.TestCheckResourceAttr("auth0_attack_protection.my_protection_tests", "brute_force_protection.0.shields.#", "2"), + resource.TestCheckResourceAttr("auth0_attack_protection.my_protection_tests", "suspicious_ip_throttling.0.enabled", "true"), + resource.TestCheckResourceAttr("auth0_attack_protection.my_protection_tests", "suspicious_ip_throttling.0.pre_login.0.rate", "864000"), + resource.TestCheckResourceAttr("auth0_attack_protection.my_protection_tests", "suspicious_ip_throttling.0.pre_login.0.max_attempts", "100"), + ), + }, + { + Config: testAttackProtectionUpdate, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("auth0_attack_protection.my_protection_tests", "breached_password_detection.0.enabled", "false"), + resource.TestCheckResourceAttr("auth0_attack_protection.my_protection_tests", "suspicious_ip_throttling.0.shields.0", "admin_notification"), + resource.TestCheckResourceAttr("auth0_attack_protection.my_protection_tests", "brute_force_protection.0.max_attempts", "11"), + ), + }, + }, + }) +} + +const testAttackProtectionCreate = ` +resource "auth0_attack_protection" "my_protection_tests" { + breached_password_detection { + enabled = true + method = "standard" + } + brute_force_protection { + enabled = true + max_attempts = 10 + mode = "count_per_identifier_and_ip" + shields = ["block", "user_notification"] + } + suspicious_ip_throttling { + enabled = true + shields = ["block","admin_notification"] + allowlist = ["127.0.0.1"] + pre_login { + max_attempts = 100 + rate = 864000 + } + pre_user_registration { + max_attempts = 50 + rate = 1200 + } + } +} +` + +const testAttackProtectionUpdate = ` +resource "auth0_attack_protection" "my_protection_tests" { + breached_password_detection { + enabled = false + method = "standard" + } + brute_force_protection { + enabled = true + max_attempts = 11 + mode = "count_per_identifier_and_ip" + shields = ["block", "user_notification"] + } + suspicious_ip_throttling { + enabled = true + shields = ["admin_notification"] + allowlist = ["127.0.0.1"] + pre_login { + max_attempts = 100 + rate = 864000 + } + pre_user_registration { + max_attempts = 50 + rate = 1200 + } + } +} +` diff --git a/docs/resources/attack_protection.md b/docs/resources/attack_protection.md new file mode 100644 index 000000000..678ce8483 --- /dev/null +++ b/docs/resources/attack_protection.md @@ -0,0 +1,88 @@ +--- +layout: "auth0" +page_title: "Auth0: auth0_attack_protection" +description: |- + Auth0 can detect attacks and stop malicious attempts to access your application such as blocking traffic from certain IPs and displaying CAPTCHA. +--- + +# auth0_attack_protection + +Auth0 can detect attacks and stop malicious attempts to access your application such as blocking traffic from certain IPs and displaying CAPTCHA + +## Example Usage + +```hcl +resource "auth0_attack_protection" "attack_protection" { + suspicious_ip_throttling { + enabled = true + shields = ["admin_notification", "block"] + allowlist = ["192.168.1.1"] + pre_login { + max_attempts = 100 + rate = 864000 + } + pre_user_registration { + max_attempts = 50 + rate = 1200 + } + } + brute_force_protection { + allowlist = ["127.0.0.1"] + enabled = true + max_attempts = 5 + mode = "count_per_identifier_and_ip" + shields = ["block", "user_notification"] + } + breached_password_detection { + admin_notification_frequency = ["daily"] + enabled = true + method = "standard" + shields = ["admin_notification", "block"] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `breached_password_detection` - (Optional) Breached password detection protects your applications from bad actors logging in with stolen credentials. +* `suspicious_ip_throttling` - (Optional) Suspicious IP throttling blocks traffic from any IP address that rapidly attempts too many logins or signups. +* `brute_force_protection` - (Optional) Safeguards against a single IP address attacking a single user account. + +### brute_force_protection + +The following arguments are supported for `brute_force_protection`: + +* `enabled` - (Optional) Whether or not brute force attack protections are active. +* `shields` - (Optional) Action to take when a brute force protection threshold is violated. Possible values: `block`, `user_notification`. +* `allowlist` - (Optional) List of trusted IP addresses that will not have attack protection enforced against them. +* `mode` - (Optional) Determines whether or not IP address is used when counting failed attempts. Possible values: `count_per_identifier_and_ip` or `count_per_identifier`. +* `max_attempts` - (Optional) Maximum number of unsuccessful attempts. + +### suspicious_ip_throttling + +The following arguments are supported for `suspicious_ip_throttling`: + +* `enabled` - (Optional) Whether or not suspicious IP throttling attack protections are active. +* `shields` - (Optional) Action to take when a suspicious IP throttling threshold is violated. Possible values: `block`, `admin_notification`. +* `allowlist` - (Optional) List of trusted IP addresses that will not have attack protection enforced against them. +* `pre_login` - (Optional) Configuration options that apply before every login attempt. +* `pre_user_registration` - (Optional) Configuration options that apply before every user registration attempt. + +### breached_password_protection + +* `enabled` - (Optional) Whether or not breached password detection is active. +* `shields` - (Optional) Action to take when a breached password is detected. Possible values: `block`, `user_notification`, `admin_notification`. +* `admin_notification_frequency` - (Optional) When "admin_notification" is enabled, determines how often email notifications are sent. Possible values: `immediately`, `daily`, `weekly`, `monthly`. +* `method` - (Optional) The subscription level for breached password detection methods. Use "enhanced" to enable Credential Guard. Possible values: `standard`, `enhanced`. + + +## Import + +As this is not a resource identifiable by an ID within the Auth0 Management API, guardian can be imported using a random +string. We recommend [Version 4 UUID](https://www.uuidgenerator.net/version4) e.g. + +```shell +$ terraform import auth0_guardian.default 24940d4b-4bd4-44e7-894e-f92e4de36a40 +``` \ No newline at end of file diff --git a/example/attack_protection/main.tf b/example/attack_protection/main.tf new file mode 100644 index 000000000..1406ada32 --- /dev/null +++ b/example/attack_protection/main.tf @@ -0,0 +1,30 @@ +provider "auth0" {} + +resource "auth0_attack_protection" "attack_protection" { + suspicious_ip_throttling { + enabled = true + shields = ["admin_notification", "block"] + allowlist = ["192.168.1.1"] + pre_login { + max_attempts = 100 + rate = 864000 + } + pre_user_registration { + max_attempts = 50 + rate = 1200 + } + } + brute_force_protection { + allowlist = ["127.0.0.1"] + enabled = true + max_attempts = 5 + mode = "count_per_identifier_and_ip" + shields = ["block", "user_notification"] + } + breached_password_detection { + admin_notification_frequency = ["daily"] + enabled = true + method = "standard" + shields = ["admin_notification", "block"] + } +} diff --git a/go.mod b/go.mod index 71c007c37..2da608867 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/auth0/terraform-provider-auth0 go 1.16 require ( - github.com/auth0/go-auth0 v0.5.0 + github.com/auth0/go-auth0 v0.6.1 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/terraform-plugin-sdk v1.16.1 ) diff --git a/go.sum b/go.sum index 752313e97..4612e7e2c 100644 --- a/go.sum +++ b/go.sum @@ -59,8 +59,8 @@ github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/auth0/go-auth0 v0.5.0 h1:GRXS+7yr4H7P726nwmXDtBC6LA8IcmlYHYjr3nkC98Y= -github.com/auth0/go-auth0 v0.5.0/go.mod h1:9rEJrEWFALKlt1VVCx1zToCG6+uddn4MLEgtKSRhlEU= +github.com/auth0/go-auth0 v0.6.1 h1:D6WSxLQyr1+Ozn8qW0KJAKVcy1j7ZxbRoWdZQr0qT8s= +github.com/auth0/go-auth0 v0.6.1/go.mod h1:9rEJrEWFALKlt1VVCx1zToCG6+uddn4MLEgtKSRhlEU= github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= github.com/aws/aws-sdk-go v1.37.0 h1:GzFnhOIsrGyQ69s7VgqtrG2BG8v7X7vwB3Xpbd/DBBk= github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=