diff --git a/.changelog/1926.txt b/.changelog/1926.txt new file mode 100644 index 00000000000..bc9387d9ab6 --- /dev/null +++ b/.changelog/1926.txt @@ -0,0 +1,11 @@ +```release-note:new-resource +cloudflare_device_policy +``` + +```release-note:enhancement +resource/cloudflare_split_tunnel: Add configuring split tunnel for device policies +``` + +```release-note:enhancement +resource/cloudflare_fallback_domain: Add creating fallback domains for device policies +``` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f0c4a827007..11e6f13f4a4 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -214,6 +214,7 @@ func New(version string) func() *schema.Provider { "cloudflare_custom_hostname": resourceCloudflareCustomHostname(), "cloudflare_custom_pages": resourceCloudflareCustomPages(), "cloudflare_custom_ssl": resourceCloudflareCustomSsl(), + "cloudflare_device_policy": resourceCloudflareDeviceSettingsPolicy(), "cloudflare_device_policy_certificates": resourceCloudflareDevicePolicyCertificates(), "cloudflare_device_posture_integration": resourceCloudflareDevicePostureIntegration(), "cloudflare_device_posture_rule": resourceCloudflareDevicePostureRule(), diff --git a/internal/provider/resource_cloudflare_device_policy.go b/internal/provider/resource_cloudflare_device_policy_certificates.go similarity index 100% rename from internal/provider/resource_cloudflare_device_policy.go rename to internal/provider/resource_cloudflare_device_policy_certificates.go diff --git a/internal/provider/resource_cloudflare_device_policy_test.go b/internal/provider/resource_cloudflare_device_policy_certificates_test.go similarity index 100% rename from internal/provider/resource_cloudflare_device_policy_test.go rename to internal/provider/resource_cloudflare_device_policy_certificates_test.go diff --git a/internal/provider/resource_cloudflare_device_settings_policy.go b/internal/provider/resource_cloudflare_device_settings_policy.go new file mode 100644 index 00000000000..b8f08dd27e6 --- /dev/null +++ b/internal/provider/resource_cloudflare_device_settings_policy.go @@ -0,0 +1,261 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudflareDeviceSettingsPolicy() *schema.Resource { + return &schema.Resource{ + Schema: resourceCloudflareDeviceSettingsPolicySchema(), + CreateContext: resourceCloudflareDeviceSettingsPolicyCreate, + ReadContext: resourceCloudflareDeviceSettingsPolicyRead, + UpdateContext: resourceCloudflareDeviceSettingsPolicyUpdate, + DeleteContext: resourceCloudflareDeviceSettingsPolicyDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceCloudflareDeviceSettingsPolicyImport, + }, + } +} + +func resourceCloudflareDeviceSettingsPolicyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + accountID := d.Get("account_id").(string) + defaultPolicy := d.Get("default").(bool) + + tflog.Debug(ctx, fmt.Sprintf("Creating Cloudflare device settings policy: accountID=%s default=%t", accountID, defaultPolicy)) + + if defaultPolicy { + d.SetId(fmt.Sprintf("%s/default", accountID)) + return resourceCloudflareDeviceSettingsPolicyUpdate(ctx, d, meta) + } + + req, err := buildDeviceSettingsPolicyRequest(d) + if err != nil { + return diag.FromErr(fmt.Errorf("error creating Cloudflare device settings policy request: %q: %w", accountID, err)) + } + + policy, err := client.CreateDeviceSettingsPolicy(ctx, accountID, req) + if err != nil { + return diag.FromErr(fmt.Errorf("error creating Cloudflare device settings policy %q: %w", accountID, err)) + } + + if policy.Result.PolicyID == nil { + return diag.FromErr(fmt.Errorf("error creating Cloudflare device settings policy: returned policyID was missing after creating policy for account: %q", accountID)) + } + d.SetId(fmt.Sprintf("%s/%s", accountID, *policy.Result.PolicyID)) + return resourceCloudflareDeviceSettingsPolicyRead(ctx, d, meta) +} + +func resourceCloudflareDeviceSettingsPolicyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + accountID, policyID, err := parseDeviceSettingsID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + tflog.Debug(ctx, fmt.Sprintf("Updating Cloudflare device settings policy: accountID=%s policyID=%s", accountID, policyID)) + + req, err := buildDeviceSettingsPolicyRequest(d) + if err != nil { + return diag.FromErr(fmt.Errorf("error creating Cloudflare device settings policy request: %q: %w", accountID, err)) + } + + if policyID == "default" { + _, err = client.UpdateDefaultDeviceSettingsPolicy(ctx, accountID, req) + } else { + _, err = client.UpdateDeviceSettingsPolicy(ctx, accountID, policyID, req) + } + if err != nil { + return diag.FromErr(fmt.Errorf("error updating Cloudflare device settings policy %q: %w", accountID, err)) + } + + return resourceCloudflareDeviceSettingsPolicyRead(ctx, d, meta) +} + +func resourceCloudflareDeviceSettingsPolicyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + accountID, policyID, err := parseDeviceSettingsID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + var policy cloudflare.DeviceSettingsPolicyResponse + if policyID == "default" { + policy, err = client.GetDefaultDeviceSettingsPolicy(ctx, accountID) + } else { + policy, err = client.GetDeviceSettingsPolicy(ctx, accountID, policyID) + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading device settings policy %q %s: %w", accountID, policyID, err)) + } + + if err := d.Set("disable_auto_fallback", policy.Result.DisableAutoFallback); err != nil { + return diag.FromErr(fmt.Errorf("error parsing disable_auto_fallback")) + } + if err := d.Set("captive_portal", policy.Result.CaptivePortal); err != nil { + return diag.FromErr(fmt.Errorf("error parsing captive_portal")) + } + if err := d.Set("allow_mode_switch", policy.Result.AllowModeSwitch); err != nil { + return diag.FromErr(fmt.Errorf("error parsing allow_mode_switch")) + } + if err := d.Set("switch_locked", policy.Result.SwitchLocked); err != nil { + return diag.FromErr(fmt.Errorf("error parsing switch_locked")) + } + if err := d.Set("allow_updates", policy.Result.AllowUpdates); err != nil { + return diag.FromErr(fmt.Errorf("error parsing allow_updates")) + } + if err := d.Set("auto_connect", policy.Result.AutoConnect); err != nil { + return diag.FromErr(fmt.Errorf("error parsing auto_connect")) + } + if err := d.Set("allowed_to_leave", policy.Result.AllowedToLeave); err != nil { + return diag.FromErr(fmt.Errorf("error parsing allowed_to_leave")) + } + if err := d.Set("support_url", policy.Result.SupportURL); err != nil { + return diag.FromErr(fmt.Errorf("error parsing support_url")) + } + if err := d.Set("default", policy.Result.Default); err != nil { + return diag.FromErr(fmt.Errorf("error setting default")) + } + if err := d.Set("service_mode_v2_mode", policy.Result.ServiceModeV2.Mode); err != nil { + return diag.FromErr(fmt.Errorf("error setting service_mode_v2_mode")) + } + if err := d.Set("service_mode_v2_port", policy.Result.ServiceModeV2.Port); err != nil { + return diag.FromErr(fmt.Errorf("error setting service_mode_v2_port")) + } + // ignore setting forbidden fields for default policies + if policy.Result.Name != nil { + if err := d.Set("name", policy.Result.Name); err != nil { + return diag.FromErr(fmt.Errorf("error parsing name")) + } + } + if policy.Result.Precedence != nil { + if err := d.Set("precedence", apiToProviderRulePrecedence(uint64(*policy.Result.Precedence), d.Get("name").(string))); err != nil { + return diag.FromErr(fmt.Errorf("error parsing precedence")) + } + } + if policy.Result.Match != nil { + if err := d.Set("match", policy.Result.Match); err != nil { + return diag.FromErr(fmt.Errorf("error parsing match")) + } + } + if policy.Result.Enabled != nil { + if err := d.Set("enabled", policy.Result.Enabled); err != nil { + return diag.FromErr(fmt.Errorf("error parsing enabled")) + } + } + + return nil +} + +func resourceCloudflareDeviceSettingsPolicyImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + accountID, policyID, err := parseDeviceSettingsID(d.Id()) + if err != nil { + return nil, err + } + + tflog.Debug(ctx, fmt.Sprintf("Importing Cloudflare device settings policy: id %s for account %s", policyID, accountID)) + + d.Set("account_id", accountID) + d.SetId(fmt.Sprintf("%s/%s", accountID, policyID)) + + resourceCloudflareDeviceSettingsPolicyRead(ctx, d, meta) + + return []*schema.ResourceData{d}, nil +} + +func resourceCloudflareDeviceSettingsPolicyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + accountID, policyID, err := parseDeviceSettingsID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + client := meta.(*cloudflare.API) + if policyID == "default" { + d.SetId("") + return diag.Diagnostics{diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Cannot delete a default policy, instead simply removing from terraform management without deleting", + }} + } + + if _, err := client.DeleteDeviceSettingsPolicy(ctx, accountID, policyID); err != nil { + return diag.FromErr(fmt.Errorf("error deleting device settings policy %q: %w", accountID, err)) + } + + d.SetId("") + + return nil +} + +func buildDeviceSettingsPolicyRequest(d *schema.ResourceData) (cloudflare.DeviceSettingsPolicyRequest, error) { + defaultPolicy := (d.Get("default").(bool) || d.Id() == fmt.Sprintf("%s/default", d.Get("account_id"))) + + req := cloudflare.DeviceSettingsPolicyRequest{ + DisableAutoFallback: cloudflare.BoolPtr(d.Get("disable_auto_fallback").(bool)), + CaptivePortal: cloudflare.IntPtr(d.Get("captive_portal").(int)), + AllowModeSwitch: cloudflare.BoolPtr(d.Get("allow_mode_switch").(bool)), + SwitchLocked: cloudflare.BoolPtr(d.Get("switch_locked").(bool)), + AllowUpdates: cloudflare.BoolPtr(d.Get("allow_updates").(bool)), + AutoConnect: cloudflare.IntPtr(d.Get("auto_connect").(int)), + AllowedToLeave: cloudflare.BoolPtr(d.Get("allowed_to_leave").(bool)), + SupportURL: cloudflare.StringPtr(d.Get("support_url").(string)), + ServiceModeV2: &cloudflare.ServiceModeV2{ + Mode: d.Get("service_mode_v2_mode").(string), + Port: d.Get("service_mode_v2_port").(int), + }, + } + + name := d.Get("name").(string) + enabled := d.Get("enabled").(bool) + if defaultPolicy && !enabled { + return req, fmt.Errorf("enabled cannot be false for default policies") + } + if !defaultPolicy { + req.Name = &name + req.Enabled = &enabled + } + + match, ok := d.GetOk("match") + if defaultPolicy && ok { + return req, fmt.Errorf("match cannot be set for default policies") + } + if !defaultPolicy && !ok { + return req, fmt.Errorf("match must be set for non-default policies") + } + if ok { + matchStr := match.(string) + req.Match = &matchStr + } + + precedence, ok := d.GetOk("precedence") + if defaultPolicy && ok { + return req, fmt.Errorf("precedence cannot be set for default policies") + } + if !defaultPolicy && !ok { + return req, fmt.Errorf("precedence must be set for non-default policies") + } + if ok { + precedenceVal := int(providerToApiRulePrecedence(int64(precedence.(int)), d.Get("name").(string))) + req.Precedence = &precedenceVal + } + + return req, nil +} + +func parseDeviceSettingsID(id string) (string, string, error) { + attributes := strings.SplitN(id, "/", 2) + + if len(attributes) != 2 { + return "", "", fmt.Errorf("invalid id (\"%s\") specified, should be in format \"accountID/policyID\" or \"accountID/default\" for the default account policy", id) + } + + return attributes[0], attributes[1], nil +} diff --git a/internal/provider/resource_cloudflare_device_settings_policy_test.go b/internal/provider/resource_cloudflare_device_settings_policy_test.go new file mode 100644 index 00000000000..e821f4b4186 --- /dev/null +++ b/internal/provider/resource_cloudflare_device_settings_policy_test.go @@ -0,0 +1,165 @@ +package provider + +import ( + "context" + "fmt" + "os" + "regexp" + "testing" + + "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccCloudflareDeviceSettingsPolicyCreate(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, defaultRnd := generateRandomResourceName(), generateRandomResourceName() + name, defaultName := fmt.Sprintf("cloudflare_device_policy.%s", rnd), fmt.Sprintf("cloudflare_device_policy.%s", defaultRnd) + precedence := uint64(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccessAccPreCheck(t) + }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckCloudflareDeviceSettingsPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudflareDeviceSettingsPolicy(rnd, accountID, precedence), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "account_id", accountID), + resource.TestCheckResourceAttr(name, "allow_mode_switch", "true"), + resource.TestCheckResourceAttr(name, "allow_updates", "true"), + resource.TestCheckResourceAttr(name, "allowed_to_leave", "true"), + resource.TestCheckResourceAttr(name, "auto_connect", "0"), + resource.TestCheckResourceAttr(name, "captive_portal", "5"), + resource.TestCheckResourceAttr(name, "default", "false"), + resource.TestCheckResourceAttr(name, "disable_auto_fallback", "true"), + resource.TestCheckResourceAttr(name, "enabled", "true"), + resource.TestCheckResourceAttr(name, "match", "identity.email == \"foo@example.com\""), + resource.TestCheckResourceAttr(name, "name", rnd), + resource.TestCheckResourceAttr(name, "precedence", fmt.Sprintf("%d", precedence)), + resource.TestCheckResourceAttr(name, "service_mode_v2_mode", "warp"), + resource.TestCheckResourceAttr(name, "service_mode_v2_port", "0"), + resource.TestCheckResourceAttr(name, "support_url", "https://cloudflare.com"), + resource.TestCheckResourceAttr(name, "switch_locked", "true"), + ), + }, + { + Config: testAccCloudflareDefaultDeviceSettingsPolicy(defaultRnd, accountID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(defaultName, "id", accountID+"/default"), + resource.TestCheckResourceAttr(defaultName, "account_id", accountID), + resource.TestCheckResourceAttr(defaultName, "allow_mode_switch", "true"), + resource.TestCheckResourceAttr(defaultName, "allow_updates", "true"), + resource.TestCheckResourceAttr(defaultName, "allowed_to_leave", "true"), + resource.TestCheckResourceAttr(defaultName, "auto_connect", "0"), + resource.TestCheckResourceAttr(defaultName, "captive_portal", "5"), + resource.TestCheckResourceAttr(defaultName, "default", "true"), + resource.TestCheckResourceAttr(defaultName, "disable_auto_fallback", "true"), + resource.TestCheckResourceAttr(defaultName, "enabled", "true"), + resource.TestCheckResourceAttr(defaultName, "name", defaultRnd), + resource.TestCheckResourceAttr(defaultName, "service_mode_v2_mode", "warp"), + resource.TestCheckResourceAttr(defaultName, "service_mode_v2_port", "0"), + resource.TestCheckResourceAttr(defaultName, "support_url", "https://cloudflare.com"), + resource.TestCheckResourceAttr(defaultName, "switch_locked", "true"), + ), + }, + { + Config: testAccCloudflareInvalidDefaultDeviceSettingsPolicy(rnd, accountID), + ExpectError: regexp.MustCompile(regexp.QuoteMeta("match cannot be set for default policies")), + }, + }, + }) +} + +func testAccCloudflareDeviceSettingsPolicy(rnd, accountID string, precedence uint64) string { + return fmt.Sprintf(` +resource "cloudflare_device_policy" "%[1]s" { + account_id = "%[2]s" + allow_mode_switch = true + allow_updates = true + allowed_to_leave = true + auto_connect = 0 + captive_portal = 5 + disable_auto_fallback = true + enabled = true + match = "identity.email == \"foo@example.com\"" + name = "%[1]s" + precedence = %[3]d + support_url = "https://cloudflare.com" + switch_locked = true +} +`, rnd, accountID, precedence) +} + +func testAccCloudflareDefaultDeviceSettingsPolicy(rnd, accountID string) string { + return fmt.Sprintf(` +resource "cloudflare_device_policy" "%[1]s" { + account_id = "%[2]s" + default = true + name = "%[1]s" + allow_mode_switch = true + allow_updates = true + allowed_to_leave = true + auto_connect = 0 + captive_portal = 5 + disable_auto_fallback = true + enabled = true + support_url = "https://cloudflare.com" + switch_locked = true +} +`, rnd, accountID) +} + +// invalid configuration - not allowed to set match for default policies +func testAccCloudflareInvalidDefaultDeviceSettingsPolicy(rnd, accountID string) string { + return fmt.Sprintf(` +resource "cloudflare_device_policy" "%[1]s" { + account_id = "%[2]s" + default = true + name = "%[1]s" + allow_mode_switch = true + allow_updates = true + allowed_to_leave = true + auto_connect = 0 + captive_portal = 5 + disable_auto_fallback = true + support_url = "https://cloudflare.com" + switch_locked = true + match = "identity.email == \"foo@example.com\"" +} +`, rnd, accountID) +} + +func testAccCheckCloudflareDeviceSettingsPolicyDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*cloudflare.API) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudflare_device_policy" { + continue + } + + _, policyId, err := parseDeviceSettingsID(rs.Primary.ID) + if err != nil { + return err + } + + _, err = client.GetDeviceSettingsPolicy(context.Background(), rs.Primary.Attributes["account_id"], policyId) + if err == nil { + return fmt.Errorf("Device Posture Integration still exists") + } + } + + return nil +} diff --git a/internal/provider/resource_cloudflare_fallback_domain.go b/internal/provider/resource_cloudflare_fallback_domain.go index f75cda6c305..1a6b01a367c 100644 --- a/internal/provider/resource_cloudflare_fallback_domain.go +++ b/internal/provider/resource_cloudflare_fallback_domain.go @@ -3,6 +3,7 @@ package provider import ( "context" "fmt" + "strings" cloudflare "github.com/cloudflare/cloudflare-go" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -25,8 +26,21 @@ func resourceCloudflareFallbackDomain() *schema.Resource { func resourceCloudflareFallbackDomainRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*cloudflare.API) accountID := d.Get("account_id").(string) + policyID := d.Get("policy_id").(string) + var err error + if policyID != "" { + accountID, policyID, err = parseDeviceSettingsID(d.Get("policy_id").(string)) + if err != nil { + return diag.FromErr(err) + } + } - domain, err := client.ListFallbackDomains(ctx, accountID) + var domain []cloudflare.FallbackDomain + if policyID == "default" || policyID == "" { + domain, err = client.ListFallbackDomains(ctx, accountID) + } else { + domain, err = client.ListFallbackDomainsDeviceSettingsPolicy(ctx, accountID, policyID) + } if err != nil { return diag.FromErr(fmt.Errorf("error finding Fallback Domains: %w", err)) } @@ -41,10 +55,24 @@ func resourceCloudflareFallbackDomainRead(ctx context.Context, d *schema.Resourc func resourceCloudflareFallbackDomainUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*cloudflare.API) accountID := d.Get("account_id").(string) + policyID := d.Get("policy_id").(string) + var err error + if policyID != "" { + accountID, policyID, err = parseDeviceSettingsID(d.Get("policy_id").(string)) + if err != nil { + return diag.FromErr(err) + } + } domainList := expandFallbackDomains(d.Get("domains").(*schema.Set)) - newFallbackDomains, err := client.UpdateFallbackDomain(ctx, accountID, domainList) + var newFallbackDomains []cloudflare.FallbackDomain + if policyID == "default" || policyID == "" { + newFallbackDomains, err = client.UpdateFallbackDomain(ctx, accountID, domainList) + policyID = "default" + } else { + newFallbackDomains, err = client.UpdateFallbackDomainDeviceSettingsPolicy(ctx, accountID, policyID, domainList) + } if err != nil { return diag.FromErr(fmt.Errorf("error updating Fallback Domains: %w", err)) } @@ -53,7 +81,7 @@ func resourceCloudflareFallbackDomainUpdate(ctx context.Context, d *schema.Resou return diag.FromErr(fmt.Errorf("error setting domain attribute: %w", err)) } - d.SetId(accountID) + d.SetId(fmt.Sprintf("%s/%s", accountID, policyID)) return resourceCloudflareFallbackDomainRead(ctx, d, meta) } @@ -61,8 +89,20 @@ func resourceCloudflareFallbackDomainUpdate(ctx context.Context, d *schema.Resou func resourceCloudflareFallbackDomainDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*cloudflare.API) accountID := d.Get("account_id").(string) + policyID := d.Get("policy_id").(string) + var err error + if policyID != "" { + accountID, policyID, err = parseDeviceSettingsID(d.Get("policy_id").(string)) + if err != nil { + return diag.FromErr(err) + } + } - err := client.RestoreFallbackDomainDefaults(ctx, accountID) + if policyID == "default" || policyID == "" { + err = client.RestoreFallbackDomainDefaults(ctx, accountID) + } else { + err = client.RestoreFallbackDomainDefaultsDeviceSettingsPolicy(ctx, accountID, policyID) + } if err != nil { return diag.FromErr(err) } @@ -72,14 +112,25 @@ func resourceCloudflareFallbackDomainDelete(ctx context.Context, d *schema.Resou } func resourceCloudflareFallbackDomainImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - accountID := d.Id() + attributes := strings.SplitN(d.Id(), "/", 2) + + if len(attributes) != 2 { + return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"accountID/policyID\"", d.Id()) + } + + accountID, policyID := attributes[0], attributes[1] if accountID == "" { return nil, fmt.Errorf("must provide account ID") } + if policyID == "" { + return nil, fmt.Errorf("must provide policy ID ('default' for default policy for the given account)") + } + d.Set("account_id", accountID) - d.SetId(accountID) + d.Set("policy_id", fmt.Sprintf("%s/%s", accountID, policyID)) + d.SetId(fmt.Sprintf("%s/%s", accountID, policyID)) resourceCloudflareFallbackDomainRead(ctx, d, meta) diff --git a/internal/provider/resource_cloudflare_fallback_domain_test.go b/internal/provider/resource_cloudflare_fallback_domain_test.go index 871303470a8..305a4a53df9 100644 --- a/internal/provider/resource_cloudflare_fallback_domain_test.go +++ b/internal/provider/resource_cloudflare_fallback_domain_test.go @@ -31,31 +31,73 @@ func TestAccCloudflareFallbackDomain(t *testing.T) { ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: testAccCloudflareFallbackDomain(rnd, accountID, "example domain", "example.com", "1.0.0.1"), + Config: testAccCloudflareDefaultFallbackDomain(rnd, accountID, "example domain", "example.com", "1.0.0.1"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "account_id", accountID), resource.TestCheckResourceAttr(name, "domains.#", "1"), resource.TestCheckResourceAttr(name, "domains.0.description", "example domain"), resource.TestCheckResourceAttr(name, "domains.0.suffix", "example.com"), resource.TestCheckResourceAttr(name, "domains.0.dns_server.0", "1.0.0.1"), + resource.TestCheckNoResourceAttr(name, "policy_id"), ), }, { - Config: testAccCloudflareFallbackDomain(rnd, accountID, "second example domain", "example.net", "1.1.1.1"), + Config: testAccCloudflareDefaultFallbackDomain(rnd, accountID, "second example domain", "example.net", "1.1.1.1"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "account_id", accountID), resource.TestCheckResourceAttr(name, "domains.#", "1"), resource.TestCheckResourceAttr(name, "domains.0.description", "second example domain"), resource.TestCheckResourceAttr(name, "domains.0.suffix", "example.net"), resource.TestCheckResourceAttr(name, "domains.0.dns_server.0", "1.1.1.1"), + resource.TestCheckNoResourceAttr(name, "policy_id"), + ), + }, + { + Config: testAccCloudflareFallbackDomain(rnd, accountID, "third example domain", "example.net", "1.1.1.1"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "account_id", accountID), + resource.TestCheckResourceAttr(name, "domains.#", "1"), + resource.TestCheckResourceAttr(name, "domains.0.description", "third example domain"), + resource.TestCheckResourceAttr(name, "domains.0.suffix", "example.net"), + resource.TestCheckResourceAttr(name, "domains.0.dns_server.0", "1.1.1.1"), + resource.TestCheckResourceAttrSet(name, "policy_id"), ), }, }, }) } -func testAccCloudflareFallbackDomain(rnd, accountID string, description string, suffix string, dns_server string) string { +func testAccCloudflareDefaultFallbackDomain(rnd, accountID string, description string, suffix string, dns_server string) string { + return fmt.Sprintf(` +resource "cloudflare_fallback_domain" "%[1]s" { + account_id = "%[2]s" + domains { + description = "%[3]s" + suffix = "%[4]s" + dns_server = ["%[5]s"] + } +} +`, rnd, accountID, description, suffix, dns_server) +} + +func testAccCloudflareFallbackDomain(rnd, accountID, description string, suffix string, dns_server string) string { return fmt.Sprintf(` +resource "cloudflare_device_policy" "%[1]s" { + account_id = "%[2]s" + allow_mode_switch = true + allow_updates = true + allowed_to_leave = true + auto_connect = 0 + captive_portal = 5 + disable_auto_fallback = true + enabled = true + match = "identity.email == \"foo@example.com\"" + name = "%[1]s" + precedence = 10 + support_url = "support_url" + switch_locked = true +} + resource "cloudflare_fallback_domain" "%[1]s" { account_id = "%[2]s" domains { @@ -63,6 +105,7 @@ resource "cloudflare_fallback_domain" "%[1]s" { suffix = "%[4]s" dns_server = ["%[5]s"] } + policy_id = "${cloudflare_device_policy.%[1]s.id}" } `, rnd, accountID, description, suffix, dns_server) } diff --git a/internal/provider/resource_cloudflare_split_tunnel.go b/internal/provider/resource_cloudflare_split_tunnel.go index 165adcb2471..9255dbd336a 100644 --- a/internal/provider/resource_cloudflare_split_tunnel.go +++ b/internal/provider/resource_cloudflare_split_tunnel.go @@ -28,8 +28,24 @@ func resourceCloudflareSplitTunnelRead(ctx context.Context, d *schema.ResourceDa client := meta.(*cloudflare.API) accountID := d.Get("account_id").(string) mode := d.Get("mode").(string) + policyID := d.Get("policy_id").(string) + if policyID != "" { + var err error + accountID, policyID, err = parseDeviceSettingsID(d.Get("policy_id").(string)) + if err != nil { + return diag.FromErr(err) + } + } + + var splitTunnel []cloudflare.SplitTunnel + var err error + if policyID == "default" || policyID == "" { + splitTunnel, err = client.ListSplitTunnels(ctx, accountID, mode) + policyID = "default" + } else { + splitTunnel, err = client.ListSplitTunnelsDeviceSettingsPolicy(ctx, accountID, policyID, mode) + } - splitTunnel, err := client.ListSplitTunnels(ctx, accountID, mode) if err != nil { return diag.FromErr(fmt.Errorf("error finding %q Split Tunnels: %w", mode, err)) } @@ -45,13 +61,27 @@ func resourceCloudflareSplitTunnelUpdate(ctx context.Context, d *schema.Resource client := meta.(*cloudflare.API) accountID := d.Get("account_id").(string) mode := d.Get("mode").(string) + policyID := d.Get("policy_id").(string) + if policyID != "" { + var err error + accountID, policyID, err = parseDeviceSettingsID(d.Get("policy_id").(string)) + if err != nil { + return diag.FromErr(err) + } + } tunnelList, err := expandSplitTunnels(d.Get("tunnels").([]interface{})) if err != nil { return diag.FromErr(fmt.Errorf("error updating %q Split Tunnels: %w", mode, err)) } - newSplitTunnels, err := client.UpdateSplitTunnel(ctx, accountID, mode, tunnelList) + var newSplitTunnels []cloudflare.SplitTunnel + if policyID == "default" || policyID == "" { + newSplitTunnels, err = client.UpdateSplitTunnel(ctx, accountID, mode, tunnelList) + policyID = "default" + } else { + newSplitTunnels, err = client.UpdateSplitTunnelDeviceSettingsPolicy(ctx, accountID, policyID, mode, tunnelList) + } if err != nil { return diag.FromErr(fmt.Errorf("error updating %q Split Tunnels: %w", mode, err)) } @@ -60,7 +90,7 @@ func resourceCloudflareSplitTunnelUpdate(ctx context.Context, d *schema.Resource return diag.FromErr(fmt.Errorf("error setting %q tunnels attribute: %w", mode, err)) } - d.SetId(accountID) + d.SetId(fmt.Sprintf("%s/%s", accountID, policyID)) return resourceCloudflareSplitTunnelRead(ctx, d, meta) } @@ -69,8 +99,20 @@ func resourceCloudflareSplitTunnelDelete(ctx context.Context, d *schema.Resource client := meta.(*cloudflare.API) accountID := d.Get("account_id").(string) mode := d.Get("mode").(string) + policyID := d.Get("policy_id").(string) + var err error + if policyID != "" { + accountID, policyID, err = parseDeviceSettingsID(d.Get("policy_id").(string)) + if err != nil { + return diag.FromErr(err) + } + } - _, err := client.UpdateSplitTunnel(ctx, accountID, mode, nil) + if policyID == "default" || policyID == "" { + _, err = client.UpdateSplitTunnel(ctx, accountID, mode, nil) + } else { + _, err = client.UpdateSplitTunnelDeviceSettingsPolicy(ctx, accountID, policyID, mode, nil) + } if err != nil { return diag.FromErr(err) } @@ -80,17 +122,20 @@ func resourceCloudflareSplitTunnelDelete(ctx context.Context, d *schema.Resource } func resourceCloudflareSplitTunnelImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - attributes := strings.SplitN(d.Id(), "/", 2) + attributes := strings.SplitN(d.Id(), "/", 3) - if len(attributes) != 2 { - return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"accountID/mode\"", d.Id()) + if len(attributes) != 3 { + return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"accountID/policyID/mode\"", d.Id()) } - accountID, mode := attributes[0], attributes[1] + accountID, policyID, mode := attributes[0], attributes[1], attributes[2] d.Set("mode", mode) d.Set("account_id", accountID) - d.SetId(accountID) + // we re-write policy_id to avoid having to parse out the policyID from accountID/policyID + // which is how the ID is defined in the device_policy resource + d.Set("policy_id", fmt.Sprintf("%s/%s", accountID, policyID)) + d.SetId(fmt.Sprintf("%s/%s", accountID, policyID)) resourceCloudflareSplitTunnelRead(ctx, d, meta) diff --git a/internal/provider/resource_cloudflare_split_tunnel_test.go b/internal/provider/resource_cloudflare_split_tunnel_test.go index af252049498..3f23cd717cb 100644 --- a/internal/provider/resource_cloudflare_split_tunnel_test.go +++ b/internal/provider/resource_cloudflare_split_tunnel_test.go @@ -27,21 +27,23 @@ func TestAccCloudflareSplitTunnel_Include(t *testing.T) { ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: testAccCloudflareSplitTunnelInclude(rnd, accountID, "example domain", "*.example.com", "include"), + Config: testAccCloudflareDefaultSplitTunnelInclude(rnd, accountID, "example domain", "*.example.com", "include"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "account_id", accountID), resource.TestCheckResourceAttr(name, "mode", "include"), resource.TestCheckResourceAttr(name, "tunnels.0.description", "example domain"), resource.TestCheckResourceAttr(name, "tunnels.0.host", "*.example.com"), + resource.TestCheckNoResourceAttr(name, "policy_id"), ), }, { - Config: testAccCloudflareSplitTunnelInclude(rnd, accountID, "example domain", "test.example.com", "include"), + Config: testAccCloudflareDefaultSplitTunnelInclude(rnd, accountID, "example domain", "test.example.com", "include"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "account_id", accountID), resource.TestCheckResourceAttr(name, "mode", "include"), resource.TestCheckResourceAttr(name, "tunnels.0.description", "example domain"), resource.TestCheckResourceAttr(name, "tunnels.0.host", "test.example.com"), + resource.TestCheckNoResourceAttr(name, "policy_id"), ), }, }, @@ -72,8 +74,37 @@ func TestAccCloudflareSplitTunnel_ConflictingTunnelProperties(t *testing.T) { }) } +func testAccCloudflareDefaultSplitTunnelInclude(rnd, accountID string, description string, host string, mode string) string { + return fmt.Sprintf(` +resource "cloudflare_split_tunnel" "%[1]s" { + account_id = "%[2]s" + mode = "%[5]s" + tunnels { + description = "%[3]s" + host = "%[4]s" + } +} +`, rnd, accountID, description, host, mode) +} + func testAccCloudflareSplitTunnelInclude(rnd, accountID string, description string, host string, mode string) string { return fmt.Sprintf(` +resource "cloudflare_device_policy" "%[1]s" { + account_id = "%[2]s" + allow_mode_switch = true + allow_updates = true + allowed_to_leave = true + auto_connect = 0 + captive_portal = 5 + disable_auto_fallback = true + enabled = true + match = "identity.email == \"foo@example.com\"" + name = "%[1]s" + precedence = 10 + support_url = "https://cloudflare.com" + switch_locked = true +} + resource "cloudflare_split_tunnel" "%[1]s" { account_id = "%[2]s" mode = "%[5]s" diff --git a/internal/provider/schema_cloudflare_device_settings_policy.go b/internal/provider/schema_cloudflare_device_settings_policy.go new file mode 100644 index 00000000000..6f128af2e3d --- /dev/null +++ b/internal/provider/schema_cloudflare_device_settings_policy.go @@ -0,0 +1,95 @@ +package provider + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudflareDeviceSettingsPolicySchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "account_id": { + Description: "The account identifier to target for the resource.", + Type: schema.TypeString, + Required: true, + }, + "default": { + Description: "Whether the policy refers to the default account policy", + Type: schema.TypeBool, + Optional: true, + }, + "name": { + Description: "Name of the policy", + Type: schema.TypeString, + Required: true, + }, + "precedence": { + Description: "The precedence of the policy. Lower values indicate higher precedence", + Type: schema.TypeInt, + Optional: true, + }, + "match": { + Description: "Wirefilter expression to match a device against when evaluating whether this policy should take effect for that device", + Type: schema.TypeString, + Optional: true, + }, + "enabled": { + Description: "Whether the policy is enabled (cannot be set for default policies)", + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "disable_auto_fallback": { + Description: "Whether to disable auto fallback for this policy", + Type: schema.TypeBool, + Optional: true, + }, + "captive_portal": { + Description: "The captive portal value for this policy", + Type: schema.TypeInt, + Optional: true, + Default: 180, + }, + "allow_mode_switch": { + Description: "Whether to allow mode switch for this policy", + Type: schema.TypeBool, + Optional: true, + }, + "switch_locked": { + Description: "Enablement of the ZT client switch lock", + Type: schema.TypeBool, + Optional: true, + }, + "allow_updates": { + Description: "Whether to allow updates under this policy", + Type: schema.TypeBool, + Optional: true, + }, + "auto_connect": { + Description: "The amount of time in minutes to reconnect after having been disabled", + Type: schema.TypeInt, + Optional: true, + }, + "allowed_to_leave": { + Description: "Whether to allow devices to leave the organization", + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "support_url": { + Description: "The support URL that will be opened when sending feedback", + Type: schema.TypeString, + Optional: true, + }, + "service_mode_v2_mode": { + Description: "The service mode", + Type: schema.TypeString, + Optional: true, + Default: "warp", + }, + "service_mode_v2_port": { + Description: "The port to use for the proxy service mode", + Type: schema.TypeInt, + Optional: true, + RequiredWith: []string{"service_mode_v2_mode"}, + }, + } +} diff --git a/internal/provider/schema_cloudflare_fallback_domain.go b/internal/provider/schema_cloudflare_fallback_domain.go index 0726f4f4107..7fc17a19dfb 100644 --- a/internal/provider/schema_cloudflare_fallback_domain.go +++ b/internal/provider/schema_cloudflare_fallback_domain.go @@ -37,5 +37,10 @@ func resourceCloudflareFallbackDomainSchema() map[string]*schema.Schema { }, }, }, + "policy_id": { + Optional: true, + Type: schema.TypeString, + Description: "The settings policy for which to configure this fallback domain policy", + }, } } diff --git a/internal/provider/schema_cloudflare_split_tunnel.go b/internal/provider/schema_cloudflare_split_tunnel.go index ad50a7a7401..d231e6f80ab 100644 --- a/internal/provider/schema_cloudflare_split_tunnel.go +++ b/internal/provider/schema_cloudflare_split_tunnel.go @@ -41,5 +41,10 @@ func resourceCloudflareSplitTunnelSchema() map[string]*schema.Schema { }, }, }, + "policy_id": { + Optional: true, + Type: schema.TypeString, + Description: "The settings policy for which to configure this split tunnel policy", + }, } }