diff --git a/twingate/internal/provider/resource/helper_test.go b/twingate/internal/provider/resource/helper_test.go index d0a5b1f8..37bc74f1 100644 --- a/twingate/internal/provider/resource/helper_test.go +++ b/twingate/internal/provider/resource/helper_test.go @@ -153,3 +153,37 @@ func stringPtr(s string) *string { func boolPtr(b bool) *bool { return &b } + +func TestIsWildcardAddress(t *testing.T) { + cases := []struct { + address string + expected bool + }{ + { + address: "hello.com", + expected: false, + }, + { + address: "*.hello.com", + expected: true, + }, + { + address: "redis-?-blah.internal", + expected: true, + }, + { + address: "redis-*-blah.internal", + expected: true, + }, + { + address: "10.0.0.0/16", + expected: true, + }, + } + + for n, c := range cases { + t.Run(fmt.Sprintf("case_%d", n), func(t *testing.T) { + assert.Equal(t, c.expected, isWildcardAddress(c.address)) + }) + } +} diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index d851ef39..82ba876b 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "reflect" + "regexp" "strings" "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" @@ -17,9 +18,10 @@ import ( ) var ( - ErrPortsWithPolicyAllowAll = errors.New(model.PolicyAllowAll + " policy does not allow specifying ports.") - ErrPortsWithPolicyDenyAll = errors.New(model.PolicyDenyAll + " policy does not allow specifying ports.") - ErrPolicyRestrictedWithoutPorts = errors.New(model.PolicyRestricted + " policy requires specifying ports.") + ErrPortsWithPolicyAllowAll = errors.New(model.PolicyAllowAll + " policy does not allow specifying ports.") + ErrPortsWithPolicyDenyAll = errors.New(model.PolicyDenyAll + " policy does not allow specifying ports.") + ErrPolicyRestrictedWithoutPorts = errors.New(model.PolicyRestricted + " policy requires specifying ports.") + ErrWildcardAddressWithEnabledShortcut = errors.New("Resources with a CIDR range or wildcard can't have the browser shortcut enabled.") ) func Resource() *schema.Resource { //nolint:funlen @@ -497,13 +499,30 @@ func convertResource(data *schema.ResourceData) (*model.Resource, error) { } isBrowserShortcutEnabled, ok := data.GetOkExists(attr.IsBrowserShortcutEnabled) //nolint - if val := isBrowserShortcutEnabled.(bool); ok { + if val := isBrowserShortcutEnabled.(bool); ok && isAttrKnown(data, attr.IsBrowserShortcutEnabled) { res.IsBrowserShortcutEnabled = &val } + if res.IsBrowserShortcutEnabled != nil && *res.IsBrowserShortcutEnabled && isWildcardAddress(res.Address) { + return nil, ErrWildcardAddressWithEnabledShortcut + } + return res, nil } +var cidrRgxp = regexp.MustCompile(`(\d{1,3}\.){3}\d{1,3}(/\d+)?`) + +func isWildcardAddress(address string) bool { + return strings.ContainsAny(address, "*?") || cidrRgxp.MatchString(address) +} + +func isAttrKnown(data *schema.ResourceData, attr string) bool { + cfg := data.GetRawConfig() + val := cfg.GetAttr(attr) + + return !val.IsNull() && val.IsKnown() +} + func getOptionalString(data *schema.ResourceData, attr string) *string { var result *string diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index 7e5efcf4..a5a97811 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -2421,3 +2421,124 @@ func TestAccTwingateResourcePolicyTransitionAllowAllToDenyAll(t *testing.T) { }, }) } + +func TestAccTwingateResourceWithBrowserOption(t *testing.T) { + const terraformResourceName = "test40" + theResource := acctests.TerraformResource(terraformResourceName) + remoteNetworkName := test.RandomName() + resourceName := test.RandomResourceName() + wildcardAddress := "*.acc-test.com" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithoutBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + { + Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress, false), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + { + Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress, true), + ExpectError: regexp.MustCompile(resource.ErrWildcardAddressWithEnabledShortcut.Error()), + }, + }, + }) +} + +func TestAccTwingateResourceWithBrowserOptionFailOnUpdate(t *testing.T) { + const terraformResourceName = "test41" + theResource := acctests.TerraformResource(terraformResourceName) + remoteNetworkName := test.RandomName() + resourceName := test.RandomResourceName() + wildcardAddress := "*.acc-test.com" + simpleAddress := "acc-test.com" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithoutBrowserOption(terraformResourceName, remoteNetworkName, resourceName, simpleAddress), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + { + Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, simpleAddress, true), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + { + Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress, true), + ExpectError: regexp.MustCompile(resource.ErrWildcardAddressWithEnabledShortcut.Error()), + }, + }, + }) +} + +func TestAccTwingateResourceWithBrowserOptionRecovered(t *testing.T) { + const terraformResourceName = "test42" + theResource := acctests.TerraformResource(terraformResourceName) + remoteNetworkName := test.RandomName() + resourceName := test.RandomResourceName() + wildcardAddress := "*.acc-test.com" + simpleAddress := "acc-test.com" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, simpleAddress, true), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + { + Config: createResourceWithoutBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + }, + }) +} + +func createResourceWithoutBrowserOption(name, networkName, resourceName, address string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[2]s" + } + resource "twingate_resource" "%[1]s" { + name = "%[3]s" + address = "%[4]s" + remote_network_id = twingate_remote_network.%[1]s.id + } + `, name, networkName, resourceName, address) +} + +func createResourceWithBrowserOption(name, networkName, resourceName, address string, browserOption bool) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[2]s" + } + resource "twingate_resource" "%[1]s" { + name = "%[3]s" + address = "%[4]s" + remote_network_id = twingate_remote_network.%[1]s.id + is_browser_shortcut_enabled = %[5]v + } + `, name, networkName, resourceName, address, browserOption) +}