diff --git a/cloudflare/resource_cloudflare_access_group.go b/cloudflare/resource_cloudflare_access_group.go index 9d588e3dd4..50c821677b 100644 --- a/cloudflare/resource_cloudflare_access_group.go +++ b/cloudflare/resource_cloudflare_access_group.go @@ -197,38 +197,105 @@ func resourceCloudflareAccessGroupImport(d *schema.ResourceData, meta interface{ func appendConditionalAccessGroupFields(group cloudflare.AccessGroup, d *schema.ResourceData) cloudflare.AccessGroup { exclude := d.Get("exclude").([]interface{}) for _, value := range exclude { - group.Exclude = buildAccessGroupCondition(value.(map[string]interface{})) + group.Exclude = BuildAccessGroupCondition(value.(map[string]interface{})) } require := d.Get("require").([]interface{}) for _, value := range require { - group.Require = buildAccessGroupCondition(value.(map[string]interface{})) + group.Require = BuildAccessGroupCondition(value.(map[string]interface{})) } include := d.Get("include").([]interface{}) for _, value := range include { - group.Include = buildAccessGroupCondition(value.(map[string]interface{})) + group.Include = BuildAccessGroupCondition(value.(map[string]interface{})) } return group } -// buildAccessGroupCondition iterates the provided `map` of values and +// BuildAccessGroupCondition iterates the provided `map` of values and // generates the required (repetitive) structs. // -// Returns the intended combination structure of AccessGroupEmail, -// AccessGroupEmailDomain, AccessGroupIP and AccessGroupEveryone -// structs. -func buildAccessGroupCondition(options map[string]interface{}) []interface{} { +// Returns the intended combination structure of Access Groups to build the +// desired policy. +func BuildAccessGroupCondition(options map[string]interface{}) []interface{} { var group []interface{} for accessGroupType, values := range options { - // Since AccessGroupEveryone is a single boolean, we don't need to - // iterate over the values like the others so we treat it a little differently. if accessGroupType == "everyone" { if values == true { - log.Printf("[DEBUG] values for everyone %s", values) group = append(group, cloudflare.AccessGroupEveryone{}) } + } else if accessGroupType == "any_valid_service_token" { + if values == true { + group = append(group, cloudflare.AccessGroupAnyValidServiceToken{}) + } + } else if accessGroupType == "certificate" { + if values == true { + group = append(group, cloudflare.AccessGroupCertificate{}) + } + } else if accessGroupType == "common_name" { + if values != "" { + group = append(group, cloudflare.AccessGroupCertificateCommonName{CommonName: struct { + CommonName string `json:"common_name"` + }{CommonName: values.(string)}}) + } + } else if accessGroupType == "gsuite" { + for _, v := range values.([]interface{}) { + gsuiteCfg := v.(map[string]interface{}) + group = append(group, cloudflare.AccessGroupGSuite{Gsuite: struct { + Email string `json:"email"` + IdentityProviderID string `json:"identity_provider_id"` + }{ + Email: gsuiteCfg["email"].(string), + IdentityProviderID: gsuiteCfg["identity_provider_id"].(string), + }}) + } + } else if accessGroupType == "github" { + for _, v := range values.([]interface{}) { + githubCfg := v.(map[string]interface{}) + group = append(group, cloudflare.AccessGroupGitHub{GitHubOrganization: struct { + Name string `json:"name"` + IdentityProviderID string `json:"identity_provider_id"` + }{ + Name: githubCfg["name"].(string), + IdentityProviderID: githubCfg["identity_provider_id"].(string), + }}) + } + } else if accessGroupType == "azure" { + for _, v := range values.([]interface{}) { + azureCfg := v.(map[string]interface{}) + group = append(group, cloudflare.AccessGroupAzure{AzureAD: struct { + ID string `json:"id"` + IdentityProviderID string `json:"identity_provider_id"` + }{ + ID: azureCfg["id"].(string), + IdentityProviderID: azureCfg["identity_provider_id"].(string), + }}) + } + } else if accessGroupType == "okta" { + for _, v := range values.([]interface{}) { + oktaCfg := v.(map[string]interface{}) + group = append(group, cloudflare.AccessGroupOkta{Otka: struct { + Name string `json:"name"` + IdentityProviderID string `json:"identity_provider_id"` + }{ + Name: oktaCfg["name"].(string), + IdentityProviderID: oktaCfg["identity_provider_id"].(string), + }}) + } + } else if accessGroupType == "saml" { + for _, v := range values.([]interface{}) { + samlCfg := v.(map[string]interface{}) + group = append(group, cloudflare.AccessGroupSAML{Saml: struct { + AttributeName string `json:"attribute_name"` + AttributeValue string `json:"attribute_value"` + IdentityProviderID string `json:"identity_provider_id"` + }{ + AttributeName: samlCfg["attribute_name"].(string), + AttributeValue: samlCfg["attribute_value"].(string), + IdentityProviderID: samlCfg["identity_provider_id"].(string), + }}) + } } else { for _, value := range values.([]interface{}) { switch accessGroupType { @@ -244,6 +311,10 @@ func buildAccessGroupCondition(options map[string]interface{}) []interface{} { group = append(group, cloudflare.AccessGroupIP{IP: struct { IP string `json:"ip"` }{IP: value.(string)}}) + case "service_token": + group = append(group, cloudflare.AccessGroupServiceToken{ServiceToken: struct { + ID string `json:"token_id"` + }{ID: value.(string)}}) case "group": group = append(group, cloudflare.AccessGroupAccessGroup{Group: struct { ID string `json:"id"` diff --git a/cloudflare/resource_cloudflare_access_policy.go b/cloudflare/resource_cloudflare_access_policy.go index 4dd1d06b43..3c2f84d78d 100644 --- a/cloudflare/resource_cloudflare_access_policy.go +++ b/cloudflare/resource_cloudflare_access_policy.go @@ -5,7 +5,7 @@ import ( "log" "strings" - cloudflare "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/cloudflare-go" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/helper/validation" ) @@ -108,6 +108,98 @@ var policyOptionElement = &schema.Resource{ Type: schema.TypeBool, Optional: true, }, + "certificate": { + Type: schema.TypeBool, + Optional: true, + }, + "common_name": { + Type: schema.TypeString, + Optional: true, + }, + "gsuite": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "email": { + Type: schema.TypeString, + Optional: true, + }, + "identity_provider_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "github": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + }, + "identity_provider_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "azure": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + }, + "identity_provider_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "okta": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + }, + "identity_provider_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "saml": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "attribute_name": { + Type: schema.TypeString, + Optional: true, + }, + "attribute_value": { + Type: schema.TypeString, + Optional: true, + }, + "identity_provider_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, }, } @@ -231,69 +323,17 @@ func resourceCloudflareAccessPolicyImport(d *schema.ResourceData, meta interface func appendConditionalAccessPolicyFields(policy cloudflare.AccessPolicy, d *schema.ResourceData) cloudflare.AccessPolicy { exclude := d.Get("exclude").([]interface{}) for _, value := range exclude { - policy.Exclude = buildAccessPolicyCondition(value.(map[string]interface{})) + policy.Exclude = BuildAccessGroupCondition(value.(map[string]interface{})) } require := d.Get("require").([]interface{}) for _, value := range require { - policy.Require = buildAccessPolicyCondition(value.(map[string]interface{})) + policy.Require = BuildAccessGroupCondition(value.(map[string]interface{})) } include := d.Get("include").([]interface{}) for _, value := range include { - policy.Include = buildAccessPolicyCondition(value.(map[string]interface{})) - } - - return policy -} - -// buildAccessPolicyCondition iterates the provided `map` of values and -// generates the required (repetitive) structs. -// -// Returns the intended combination structure of AccessPolicyEmail, -// AccessPolicyEmailDomain, AccessPolicyIP and AccessPolicyEveryone -// structs. -func buildAccessPolicyCondition(options map[string]interface{}) []interface{} { - var policy []interface{} - for accessPolicyType, values := range options { - // Since AccessPolicyEveryone is a single boolean, we don't need to - // iterate over the values like the others so we treat it a little differently. - if accessPolicyType == "everyone" { - if values == true { - log.Printf("[DEBUG] values for everyone %s", values) - policy = append(policy, cloudflare.AccessPolicyEveryone{}) - } - } else if accessPolicyType == "any_valid_service_token" { - if values == true { - log.Printf("[DEBUG] values for any_valid_service_token %s", values) - policy = append(policy, cloudflare.AccessPolicyAnyValidServiceToken{}) - } - } else { - for _, value := range values.([]interface{}) { - switch accessPolicyType { - case "email": - policy = append(policy, cloudflare.AccessPolicyEmail{Email: struct { - Email string `json:"email"` - }{Email: value.(string)}}) - case "email_domain": - policy = append(policy, cloudflare.AccessPolicyEmailDomain{EmailDomain: struct { - Domain string `json:"domain"` - }{Domain: value.(string)}}) - case "ip": - policy = append(policy, cloudflare.AccessPolicyIP{IP: struct { - IP string `json:"ip"` - }{IP: value.(string)}}) - case "service_token": - policy = append(policy, cloudflare.AccessPolicyServiceToken{ServiceToken: struct { - ID string `json:"token_id"` - }{ID: value.(string)}}) - case "group": - policy = append(policy, cloudflare.AccessPolicyAccessGroup{Group: struct { - ID string `json:"id"` - }{ID: value.(string)}}) - } - } - } + policy.Include = BuildAccessGroupCondition(value.(map[string]interface{})) } return policy diff --git a/cloudflare/resource_cloudflare_access_policy_test.go b/cloudflare/resource_cloudflare_access_policy_test.go index e619a000c5..fe96d8f7bf 100644 --- a/cloudflare/resource_cloudflare_access_policy_test.go +++ b/cloudflare/resource_cloudflare_access_policy_test.go @@ -197,3 +197,121 @@ func testAccessPolicyGroupConfig(resourceID, zone, zoneID, accountID string) str `, resourceID, zone, zoneID, accountID) } + +func TestAccessPolicyMTLS(t *testing.T) { + // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the Access + // service does not yet support the API tokens and it results in + // misleading state error messages. + if os.Getenv("CLOUDFLARE_API_TOKEN") != "" { + defer func(apiToken string) { + os.Setenv("CLOUDFLARE_API_TOKEN", apiToken) + }(os.Getenv("CLOUDFLARE_API_TOKEN")) + os.Setenv("CLOUDFLARE_API_TOKEN", "") + } + + rnd := generateRandomResourceName() + name := "cloudflare_access_policy." + rnd + zone := os.Getenv("CLOUDFLARE_DOMAIN") + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAccount(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccessPolicyMTLSConfig(rnd, zone, zoneID, accountID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "name", rnd), + resource.TestCheckResourceAttr(name, "zone_id", zoneID), + resource.TestCheckResourceAttr(name, "include.0.certificate", "true"), + ), + }, + }, + }) +} + +func testAccessPolicyMTLSConfig(resourceID, zone, zoneID, accountID string) string { + return fmt.Sprintf(` + resource "cloudflare_access_application" "%[1]s" { + name = "%[1]s" + zone_id = "%[3]s" + domain = "%[1]s.%[2]s" + } + + resource "cloudflare_access_policy" "%[1]s" { + application_id = "${cloudflare_access_application.%[1]s.id}" + name = "%[1]s" + zone_id = "%[3]s" + decision = "non_identity" + precedence = "10" + + include { + certificate = true + } + } + + `, resourceID, zone, zoneID, accountID) +} + +func TestAccessPolicyCommonName(t *testing.T) { + // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the Access + // service does not yet support the API tokens and it results in + // misleading state error messages. + if os.Getenv("CLOUDFLARE_API_TOKEN") != "" { + defer func(apiToken string) { + os.Setenv("CLOUDFLARE_API_TOKEN", apiToken) + }(os.Getenv("CLOUDFLARE_API_TOKEN")) + os.Setenv("CLOUDFLARE_API_TOKEN", "") + } + + rnd := generateRandomResourceName() + name := "cloudflare_access_policy." + rnd + zone := os.Getenv("CLOUDFLARE_DOMAIN") + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAccount(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccessPolicyCommonNameConfig(rnd, zone, zoneID, accountID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "name", rnd), + resource.TestCheckResourceAttr(name, "zone_id", zoneID), + resource.TestCheckResourceAttr(name, "include.0.common_name", "example@example.com"), + ), + }, + }, + }) +} + +func testAccessPolicyCommonNameConfig(resourceID, zone, zoneID, accountID string) string { + return fmt.Sprintf(` + resource "cloudflare_access_application" "%[1]s" { + name = "%[1]s" + zone_id = "%[3]s" + domain = "%[1]s.%[2]s" + } + + resource "cloudflare_access_policy" "%[1]s" { + application_id = "${cloudflare_access_application.%[1]s.id}" + name = "%[1]s" + zone_id = "%[3]s" + decision = "non_identity" + precedence = "10" + + include { + common_name = "example@example.com" + } + } + + `, resourceID, zone, zoneID, accountID) +} diff --git a/website/docs/r/access_group.html.markdown b/website/docs/r/access_group.html.markdown index 2069117a41..0e298aba6d 100644 --- a/website/docs/r/access_group.html.markdown +++ b/website/docs/r/access_group.html.markdown @@ -66,9 +66,73 @@ conditions which can be applied. The conditions are: `email = ["test@example.com"]` * `email_domain` - (Optional) A list of email domains. Example: `email_domain = ["example.com"]` +* `service_token` - (Optional) A list of service token ids. Example: + `service_token = cloudflare_access_service_token.demo.id` +* `any_valid_service_token` - (Optional) Boolean indicating if allow + all tokens to be granted. Example: `any_valid_service_token = true` +* `group` - (Optional) A list of access group ids. Example: + `group = [cloudflare_access_group.demo.id]` * `everyone` - (Optional) Boolean indicating permitting access for all requests. Example: `everyone = true` +* `certificate` - (Optional) Whether to use mTLS certificate authentication. +* `common_name` - (Optional) Use a certificate common name to authenticate with. +* `gsuite` - (Optional) Use GSuite as the authentication mechanism. Example: + ```hcl + # ... other configuration + include { + gsuite { + email = "admins@example.com" + identity_provider_id = "ca298b82-93b5-41bf-bc2d-10493f09b761" + } + } + ``` +* `github` - (Optional) Use a GitHub team as the `include` condition. Example: + + ```hcl + # ... other configuration + include { + github { + name = "my-github-team-name" + identity_provider_id = "ca298b82-93b5-41bf-bc2d-10493f09b761" + } + } + ``` +* `azure` - (Optional) Use Azure AD as the `include` condition. Example: + + ```hcl + # ... other configuration + include { + azure { + id = "86773093-5feb-48dd-814b-7ccd3676ff50e" + identity_provider_id = "ca298b82-93b5-41bf-bc2d-10493f09b761" + } + } + ``` +* `okta` - (Optional) Use Okta as the `include` condition. Example: + + ```hcl + # ... other configuration + include { + okta { + name = "admins" + identity_provider_id = "ca298b82-93b5-41bf-bc2d-10493f09b761" + } + } + ``` +* `saml` - (Optional) Use an external SAML setup as the `include` condition. + Example: + + ```hcl + # ... other configuration + include { + saml { + attribute_name = "group" + attribute_value = "admins" + identity_provider_id = "ca298b82-93b5-41bf-bc2d-10493f09b761" + } + } + ``` ## Import diff --git a/website/docs/r/access_policy.html.markdown b/website/docs/r/access_policy.html.markdown index 6fda4bc49d..a6ffb2f497 100644 --- a/website/docs/r/access_policy.html.markdown +++ b/website/docs/r/access_policy.html.markdown @@ -59,32 +59,9 @@ The following arguments are supported: Allowed values: `allow`, `deny`, `non_identity`, `bypass` * `name` - (Required) Friendly name of the Access Application. * `precedence` - (Optional) The unique precedence for policies on a single application. Integer. -* `require` - (Optional) A series of access conditions, see below for - full list. -* `exclude` - (Optional) A series of access conditions, see below for - full list. -* `include` - (Required) A series of access conditions, see below for - full list. - -## Conditions - -`require`, `exclude` and `include` arguments share the available -conditions which can be applied. The conditions are: - -* `ip` - (Optional) A list of IP addresses or ranges. Example: - `ip = ["1.2.3.4", "10.0.0.0/2"]` -* `email` - (Optional) A list of email addresses. Example: - `email = ["test@example.com"]` -* `email_domain` - (Optional) A list of email domains. Example: - `email_domain = ["example.com"]` -* `service_token` - (Optional) A list of service token ids. Example: - `service_token = cloudflare_access_service_token.demo.id` -* `any_valid_service_token` - (Optional) Boolean indicating if allow - all tokens to be granted. Example: `any_valid_service_token = true` -* `group` - (Optional) A list of access group ids. Example: - `group = [cloudflare_access_group.demo.id]` -* `everyone` - (Optional) Boolean indicating permitting access for all - requests. Example: `everyone = true` +* `require` - (Optional) A series of access conditions, see [Access Groups](/docs/providers/cloudflare/r/access_group.html#conditions). +* `exclude` - (Optional) A series of access conditions, see [Access Groups](/docs/providers/cloudflare/r/access_group.html#conditions). +* `include` - (Required) A series of access conditions, see [Access Groups](/docs/providers/cloudflare/r/access_group.html#conditions). ## Import