diff --git a/docs/resources/resource.md b/docs/resources/resource.md index 7c5a03e1..9e9fbdf0 100644 --- a/docs/resources/resource.md +++ b/docs/resources/resource.md @@ -30,11 +30,17 @@ resource "twingate_service_account" "github_actions_prod" { name = "Github Actions PROD" } +data "twingate_security_policy" "test_policy" { + name = "Test Policy" +} + resource "twingate_resource" "resource" { name = "network" address = "internal.int" remote_network_id = twingate_remote_network.aws_network.id + security_policy_id = data.twingate_security_policy.test_policy.id + protocols { allow_icmp = true tcp { @@ -70,6 +76,7 @@ resource "twingate_resource" "resource" { - `is_browser_shortcut_enabled` (Boolean) Controls whether an "Open in Browser" shortcut will be shown for this Resource in the Twingate Client. - `is_visible` (Boolean) Controls whether this Resource will be visible in the main Resource list in the Twingate Client. - `protocols` (Block List, Max: 1) Restrict access to certain protocols and ports. By default or when this argument is not defined, there is no restriction, and all protocols and ports are allowed. (see [below for nested schema](#nestedblock--protocols)) +- `security_policy_id` (String) The ID of a `twingate_security_policy` to set as this Resource's Security Policy. Default is `Default Policy` ### Read-Only diff --git a/examples/resources/twingate_resource/resource.tf b/examples/resources/twingate_resource/resource.tf index 7c5ae842..5c7ca027 100644 --- a/examples/resources/twingate_resource/resource.tf +++ b/examples/resources/twingate_resource/resource.tf @@ -15,11 +15,17 @@ resource "twingate_service_account" "github_actions_prod" { name = "Github Actions PROD" } +data "twingate_security_policy" "test_policy" { + name = "Test Policy" +} + resource "twingate_resource" "resource" { name = "network" address = "internal.int" remote_network_id = twingate_remote_network.aws_network.id + security_policy_id = data.twingate_security_policy.test_policy.id + protocols { allow_icmp = true tcp { diff --git a/twingate/internal/client/query/resource-create.go b/twingate/internal/client/query/resource-create.go index 17c697b5..7611a8e2 100644 --- a/twingate/internal/client/query/resource-create.go +++ b/twingate/internal/client/query/resource-create.go @@ -1,7 +1,7 @@ package query type CreateResource struct { - ResourceEntityResponse `graphql:"resourceCreate(name: $name, address: $address, remoteNetworkId: $remoteNetworkId, groupIds: $groupIds, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias)"` + ResourceEntityResponse `graphql:"resourceCreate(name: $name, address: $address, remoteNetworkId: $remoteNetworkId, groupIds: $groupIds, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias, securityPolicyId: $securityPolicyId)"` } func (q CreateResource) IsEmpty() bool { diff --git a/twingate/internal/client/query/resource-read.go b/twingate/internal/client/query/resource-read.go index d641b291..83087561 100644 --- a/twingate/internal/client/query/resource-read.go +++ b/twingate/internal/client/query/resource-read.go @@ -56,6 +56,7 @@ type ResourceNode struct { IsVisible bool IsBrowserShortcutEnabled bool Alias string + SecurityPolicy *gqlSecurityPolicy } type Protocols struct { @@ -90,6 +91,11 @@ func (r gqlResource) ToModel() *model.Resource { } func (r ResourceNode) ToModel() *model.Resource { + var securityPolicy string + if r.SecurityPolicy != nil { + securityPolicy = string(r.SecurityPolicy.ID) + } + return &model.Resource{ ID: string(r.ID), Name: r.Name, @@ -100,6 +106,7 @@ func (r ResourceNode) ToModel() *model.Resource { IsVisible: &r.IsVisible, IsBrowserShortcutEnabled: &r.IsBrowserShortcutEnabled, Alias: optionalString(r.Alias), + SecurityPolicyID: optionalString(securityPolicy), } } diff --git a/twingate/internal/client/query/resource-update.go b/twingate/internal/client/query/resource-update.go index 419b4f57..4bdc0e5e 100644 --- a/twingate/internal/client/query/resource-update.go +++ b/twingate/internal/client/query/resource-update.go @@ -1,7 +1,7 @@ package query type UpdateResource struct { - ResourceEntityResponse `graphql:"resourceUpdate(id: $id, name: $name, address: $address, remoteNetworkId: $remoteNetworkId, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias)"` + ResourceEntityResponse `graphql:"resourceUpdate(id: $id, name: $name, address: $address, remoteNetworkId: $remoteNetworkId, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias, securityPolicyId: $securityPolicyId)"` } func (q UpdateResource) IsEmpty() bool { diff --git a/twingate/internal/client/resource.go b/twingate/internal/client/resource.go index 8518c30e..d3e9950c 100644 --- a/twingate/internal/client/resource.go +++ b/twingate/internal/client/resource.go @@ -70,6 +70,7 @@ func (client *Client) CreateResource(ctx context.Context, input *model.Resource) gqlNullable(input.IsVisible, "isVisible"), gqlNullable(input.IsBrowserShortcutEnabled, "isBrowserShortcutEnabled"), gqlNullable(input.Alias, "alias"), + gqlNullableID(input.SecurityPolicyID, "securityPolicyId"), cursor(query.CursorAccess), pageLimit(client.pageLimit), ) @@ -92,6 +93,10 @@ func (client *Client) CreateResource(ctx context.Context, input *model.Resource) resource.IsBrowserShortcutEnabled = nil } + if input.SecurityPolicyID == nil { + resource.SecurityPolicyID = nil + } + return resource, nil } @@ -180,6 +185,7 @@ func (client *Client) UpdateResource(ctx context.Context, input *model.Resource) gqlNullable(input.IsVisible, "isVisible"), gqlNullable(input.IsBrowserShortcutEnabled, "isBrowserShortcutEnabled"), gqlNullable(input.Alias, "alias"), + gqlNullableID(input.SecurityPolicyID, "securityPolicyId"), cursor(query.CursorAccess), pageLimit(client.pageLimit), ) @@ -204,6 +210,10 @@ func (client *Client) UpdateResource(ctx context.Context, input *model.Resource) resource.IsBrowserShortcutEnabled = nil } + if input.SecurityPolicyID == nil { + resource.SecurityPolicyID = nil + } + return resource, nil } diff --git a/twingate/internal/client/variables.go b/twingate/internal/client/variables.go index b9e6a945..e76daa91 100644 --- a/twingate/internal/client/variables.go +++ b/twingate/internal/client/variables.go @@ -105,6 +105,7 @@ func getValue(val any) any { } } +//nolint:unparam func gqlNullableID(val interface{}, name string) gqlVarOption { return func(values map[string]interface{}) map[string]interface{} { var ( @@ -112,6 +113,10 @@ func gqlNullableID(val interface{}, name string) gqlVarOption { defaultID *graphql.ID ) + if value, ok := val.(*string); ok && value != nil { + val = *value + } + if isZeroValue(val) { gqlValue = defaultID } else { diff --git a/twingate/internal/model/resource.go b/twingate/internal/model/resource.go index c073bb60..8990ca17 100644 --- a/twingate/internal/model/resource.go +++ b/twingate/internal/model/resource.go @@ -34,6 +34,7 @@ type Resource struct { IsVisible *bool IsBrowserShortcutEnabled *bool Alias *string + SecurityPolicyID *string } func (r Resource) AccessToTerraform() []interface{} { diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index 82ba876b..147f3e1c 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -17,7 +17,10 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +const DefaultSecurityPolicyName = "Default Policy" + var ( + DefaultSecurityPolicyID string //nolint:gochecknoglobals 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.") @@ -136,6 +139,13 @@ func Resource() *schema.Resource { //nolint:funlen Description: "Restrict access to certain groups or service accounts", Elem: accessSchema, }, + attr.SecurityPolicyID: { + Type: schema.TypeString, + Optional: true, + Description: "The ID of a `twingate_security_policy` to set as this Resource's Security Policy. Default is `Default Policy`", + DiffSuppressOnRefresh: true, + DiffSuppressFunc: defaultPolicyNotChanged, + }, // computed attr.IsVisible: { Type: schema.TypeBool, @@ -222,7 +232,13 @@ func resourceUpdate(ctx context.Context, resourceData *schema.ResourceData, meta attr.IsVisible, attr.IsBrowserShortcutEnabled, attr.Alias, + attr.SecurityPolicyID, ) { + diagErr := setDefaultSecurityPolicy(ctx, resource, client) + if diagErr.HasError() { + return diagErr + } + resource, err = client.UpdateResource(ctx, resource) } else { resource, err = client.ReadResource(ctx, resource.ID) @@ -236,12 +252,43 @@ func resourceUpdate(ctx context.Context, resourceData *schema.ResourceData, meta return resourceResourceReadHelper(ctx, client, resourceData, resource, err) } +func setDefaultSecurityPolicy(ctx context.Context, resource *model.Resource, client *client.Client) diag.Diagnostics { + if DefaultSecurityPolicyID == "" { + policy, _ := client.ReadSecurityPolicy(ctx, "", DefaultSecurityPolicyName) + if policy != nil { + DefaultSecurityPolicyID = policy.ID + } + } + + if DefaultSecurityPolicyID == "" { + return diag.Errorf("default policy not set") + } + + remoteResource, err := client.ReadResource(ctx, resource.ID) + if err != nil { + return diag.FromErr(err) + } + + if remoteResource.SecurityPolicyID != nil && (resource.SecurityPolicyID == nil || *resource.SecurityPolicyID == "") && + *remoteResource.SecurityPolicyID != DefaultSecurityPolicyID { + resource.SecurityPolicyID = &DefaultSecurityPolicyID + } + + return nil +} + func resourceRead(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*client.Client) + securityPolicyID := resourceData.Get(attr.SecurityPolicyID) + resource, err := client.ReadResource(ctx, resourceData.Id()) if resource != nil { resource.IsAuthoritative = convertAuthoritativeFlagLegacy(resourceData) + + if securityPolicyID == "" { + resource.SecurityPolicyID = nil + } } return resourceResourceReadHelper(ctx, client, resourceData, resource, err) @@ -348,13 +395,12 @@ func readDiagnostics(resourceData *schema.ResourceData, resource *model.Resource } } - var alias interface{} - if resource.Alias != nil { - alias = *resource.Alias + if err := resourceData.Set(attr.Alias, resource.Alias); err != nil { + return ErrAttributeSet(err, attr.Alias) } - if err := resourceData.Set(attr.Alias, alias); err != nil { - return ErrAttributeSet(err, attr.Alias) + if err := resourceData.Set(attr.SecurityPolicyID, resource.SecurityPolicyID); err != nil { + return ErrAttributeSet(err, attr.SecurityPolicyID) } return nil @@ -440,6 +486,10 @@ func protocolsNotChanged(attribute, oldValue, newValue string, data *schema.Reso return false } +func defaultPolicyNotChanged(attribute, oldValue, newValue string, data *schema.ResourceData) bool { + return oldValue == DefaultSecurityPolicyID && (newValue == "" || newValue == DefaultSecurityPolicyID) +} + func getChangedAccessIDs(ctx context.Context, resourceData *schema.ResourceData, resource *model.Resource, client *client.Client) ([]string, []string, error) { remote, err := client.ReadResource(ctx, resource.ID) if err != nil { @@ -483,14 +533,15 @@ func convertResource(data *schema.ResourceData) (*model.Resource, error) { groups, serviceAccounts := convertAccess(data) res := &model.Resource{ - Name: data.Get(attr.Name).(string), - RemoteNetworkID: data.Get(attr.RemoteNetworkID).(string), - Address: data.Get(attr.Address).(string), - Protocols: protocols, - Groups: groups, - ServiceAccounts: serviceAccounts, - IsAuthoritative: convertAuthoritativeFlagLegacy(data), - Alias: getOptionalString(data, attr.Alias), + Name: data.Get(attr.Name).(string), + RemoteNetworkID: data.Get(attr.RemoteNetworkID).(string), + Address: data.Get(attr.Address).(string), + Protocols: protocols, + Groups: groups, + ServiceAccounts: serviceAccounts, + IsAuthoritative: convertAuthoritativeFlagLegacy(data), + Alias: getOptionalString(data, attr.Alias), + SecurityPolicyID: getOptionalString(data, attr.SecurityPolicyID), } isVisible, ok := data.GetOkExists(attr.IsVisible) //nolint @@ -524,9 +575,17 @@ func isAttrKnown(data *schema.ResourceData, attr string) bool { } func getOptionalString(data *schema.ResourceData, attr string) *string { + if data == nil { + return nil + } + var result *string cfg := data.GetRawConfig() + if cfg.IsNull() { + return nil + } + val := cfg.GetAttr(attr) if !val.IsNull() { diff --git a/twingate/internal/test/acctests/helper.go b/twingate/internal/test/acctests/helper.go index b3eef07c..c58f9517 100644 --- a/twingate/internal/test/acctests/helper.go +++ b/twingate/internal/test/acctests/helper.go @@ -615,6 +615,49 @@ func CheckResourceServiceAccountsLen(resourceName string, expectedServiceAccount } } +func CheckResourceSecurityPolicy(resourceName string, expectedSecurityPolicyID string) sdk.TestCheckFunc { + return func(state *terraform.State) error { + resourceID, err := getResourceID(state, resourceName) + if err != nil { + return err + } + + resource, err := providerClient.ReadResource(context.Background(), resourceID) + if err != nil { + return fmt.Errorf("resource with ID %s failed to read: %w", resourceID, err) + } + + if resource.SecurityPolicyID != nil && *resource.SecurityPolicyID != expectedSecurityPolicyID { + return fmt.Errorf("expected security_policy_id %s, got %s", expectedSecurityPolicyID, *resource.SecurityPolicyID) //nolint + } + + return nil + } +} + +func UpdateResourceSecurityPolicy(resourceName, securityPolicyID string) sdk.TestCheckFunc { + return func(state *terraform.State) error { + resourceID, err := getResourceID(state, resourceName) + if err != nil { + return err + } + + resource, err := providerClient.ReadResource(context.Background(), resourceID) + if err != nil { + return fmt.Errorf("resource with ID %s failed to read: %w", resourceID, err) + } + + resource.SecurityPolicyID = &securityPolicyID + + _, err = providerClient.UpdateResource(context.Background(), resource) + if err != nil { + return fmt.Errorf("resource with ID %s failed to update security_policy: %w", resourceID, err) + } + + return nil + } +} + func AddGroupUser(groupResource, groupName, terraformUserID string) sdk.TestCheckFunc { return func(state *terraform.State) error { userID, err := getResourceID(state, getResourceNameFromID(terraformUserID)) diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index a5a97811..067f5b9d 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -2087,7 +2087,8 @@ func TestAccTwingateResourceUpdateWithPort(t *testing.T) { } func TestAccTwingateResourceWithPortsFailsForAllowAllAndDenyAllPolicy(t *testing.T) { - const terraformResourceName = "test28" + t.Parallel() + remoteNetworkName := test.RandomName() resourceName := test.RandomResourceName() @@ -2097,11 +2098,11 @@ func TestAccTwingateResourceWithPortsFailsForAllowAllAndDenyAllPolicy(t *testing CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), + Config: createResourceWithPorts(resourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), ExpectError: regexp.MustCompile(resource.ErrPortsWithPolicyAllowAll.Error()), }, { - Config: createResourceWithPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyDenyAll), + Config: createResourceWithPorts(resourceName, remoteNetworkName, resourceName, model.PolicyDenyAll), ExpectError: regexp.MustCompile(resource.ErrPortsWithPolicyDenyAll.Error()), }, }, @@ -2133,10 +2134,11 @@ func createResourceWithPorts(name, networkName, resourceName, policy string) str } func TestAccTwingateResourceWithoutPortsOkForAllowAllAndDenyAllPolicy(t *testing.T) { - const terraformResourceName = "test29" - remoteNetworkName := test.RandomName() + t.Parallel() + resourceName := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResourceName) + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -2144,7 +2146,7 @@ func TestAccTwingateResourceWithoutPortsOkForAllowAllAndDenyAllPolicy(t *testing CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithoutPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), + Config: createResourceWithoutPorts(resourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyAllowAll), @@ -2152,7 +2154,7 @@ func TestAccTwingateResourceWithoutPortsOkForAllowAllAndDenyAllPolicy(t *testing ), }, { - Config: createResourceWithoutPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyDenyAll), + Config: createResourceWithoutPorts(resourceName, remoteNetworkName, resourceName, model.PolicyDenyAll), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyDenyAll), @@ -2187,10 +2189,11 @@ func createResourceWithoutPorts(name, networkName, resourceName, policy string) } func TestAccTwingateResourceWithRestrictedPolicy(t *testing.T) { - const terraformResourceName = "test30" - remoteNetworkName := test.RandomName() + t.Parallel() + resourceName := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResourceName) + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -2198,7 +2201,7 @@ func TestAccTwingateResourceWithRestrictedPolicy(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyRestricted), + Config: createResourceWithPorts(resourceName, remoteNetworkName, resourceName, model.PolicyRestricted), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyRestricted), @@ -2210,10 +2213,11 @@ func TestAccTwingateResourceWithRestrictedPolicy(t *testing.T) { } func TestAccTwingateResourcePolicyTransitionDenyAllToRestricted(t *testing.T) { - const terraformResourceName = "test31" - theResource := acctests.TerraformResource(terraformResourceName) - remoteNetworkName := test.RandomName() + t.Parallel() + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -2221,7 +2225,7 @@ func TestAccTwingateResourcePolicyTransitionDenyAllToRestricted(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithoutPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyDenyAll), + Config: createResourceWithoutPorts(resourceName, remoteNetworkName, resourceName, model.PolicyDenyAll), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyDenyAll), @@ -2229,7 +2233,7 @@ func TestAccTwingateResourcePolicyTransitionDenyAllToRestricted(t *testing.T) { ), }, { - Config: createResourceWithPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyRestricted), + Config: createResourceWithPorts(resourceName, remoteNetworkName, resourceName, model.PolicyRestricted), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyRestricted), @@ -2241,10 +2245,11 @@ func TestAccTwingateResourcePolicyTransitionDenyAllToRestricted(t *testing.T) { } func TestAccTwingateResourcePolicyTransitionDenyAllToAllowAll(t *testing.T) { - const terraformResourceName = "test32" - theResource := acctests.TerraformResource(terraformResourceName) - remoteNetworkName := test.RandomName() + t.Parallel() + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -2252,7 +2257,7 @@ func TestAccTwingateResourcePolicyTransitionDenyAllToAllowAll(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithoutPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyDenyAll), + Config: createResourceWithoutPorts(resourceName, remoteNetworkName, resourceName, model.PolicyDenyAll), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyDenyAll), @@ -2260,7 +2265,7 @@ func TestAccTwingateResourcePolicyTransitionDenyAllToAllowAll(t *testing.T) { ), }, { - Config: createResourceWithoutPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), + Config: createResourceWithoutPorts(resourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyAllowAll), @@ -2272,10 +2277,11 @@ func TestAccTwingateResourcePolicyTransitionDenyAllToAllowAll(t *testing.T) { } func TestAccTwingateResourcePolicyTransitionRestrictedToDenyAll(t *testing.T) { - const terraformResourceName = "test33" - theResource := acctests.TerraformResource(terraformResourceName) - remoteNetworkName := test.RandomName() + t.Parallel() + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -2283,7 +2289,7 @@ func TestAccTwingateResourcePolicyTransitionRestrictedToDenyAll(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyRestricted), + Config: createResourceWithPorts(resourceName, remoteNetworkName, resourceName, model.PolicyRestricted), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyRestricted), @@ -2291,7 +2297,7 @@ func TestAccTwingateResourcePolicyTransitionRestrictedToDenyAll(t *testing.T) { ), }, { - Config: createResourceWithoutPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyDenyAll), + Config: createResourceWithoutPorts(resourceName, remoteNetworkName, resourceName, model.PolicyDenyAll), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyDenyAll), @@ -2303,10 +2309,11 @@ func TestAccTwingateResourcePolicyTransitionRestrictedToDenyAll(t *testing.T) { } func TestAccTwingateResourcePolicyTransitionRestrictedToAllowAll(t *testing.T) { - const terraformResourceName = "test34" - theResource := acctests.TerraformResource(terraformResourceName) - remoteNetworkName := test.RandomName() + t.Parallel() + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -2314,7 +2321,7 @@ func TestAccTwingateResourcePolicyTransitionRestrictedToAllowAll(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyRestricted), + Config: createResourceWithPorts(resourceName, remoteNetworkName, resourceName, model.PolicyRestricted), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyRestricted), @@ -2322,7 +2329,7 @@ func TestAccTwingateResourcePolicyTransitionRestrictedToAllowAll(t *testing.T) { ), }, { - Config: createResourceWithoutPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), + Config: createResourceWithoutPorts(resourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyAllowAll), @@ -2334,10 +2341,11 @@ func TestAccTwingateResourcePolicyTransitionRestrictedToAllowAll(t *testing.T) { } func TestAccTwingateResourcePolicyTransitionRestrictedToAllowAllWithPortsShouldFail(t *testing.T) { - const terraformResourceName = "test35" - theResource := acctests.TerraformResource(terraformResourceName) - remoteNetworkName := test.RandomName() + t.Parallel() + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -2345,7 +2353,7 @@ func TestAccTwingateResourcePolicyTransitionRestrictedToAllowAllWithPortsShouldF CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyRestricted), + Config: createResourceWithPorts(resourceName, remoteNetworkName, resourceName, model.PolicyRestricted), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyRestricted), @@ -2353,7 +2361,7 @@ func TestAccTwingateResourcePolicyTransitionRestrictedToAllowAllWithPortsShouldF ), }, { - Config: createResourceWithPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), + Config: createResourceWithPorts(resourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), ExpectError: regexp.MustCompile(resource.ErrPortsWithPolicyAllowAll.Error()), }, }, @@ -2361,10 +2369,11 @@ func TestAccTwingateResourcePolicyTransitionRestrictedToAllowAllWithPortsShouldF } func TestAccTwingateResourcePolicyTransitionAllowAllToRestricted(t *testing.T) { - const terraformResourceName = "test36" - theResource := acctests.TerraformResource(terraformResourceName) - remoteNetworkName := test.RandomName() + t.Parallel() + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -2372,7 +2381,7 @@ func TestAccTwingateResourcePolicyTransitionAllowAllToRestricted(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithoutPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), + Config: createResourceWithoutPorts(resourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyAllowAll), @@ -2380,7 +2389,7 @@ func TestAccTwingateResourcePolicyTransitionAllowAllToRestricted(t *testing.T) { ), }, { - Config: createResourceWithPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyRestricted), + Config: createResourceWithPorts(resourceName, remoteNetworkName, resourceName, model.PolicyRestricted), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyRestricted), @@ -2392,10 +2401,11 @@ func TestAccTwingateResourcePolicyTransitionAllowAllToRestricted(t *testing.T) { } func TestAccTwingateResourcePolicyTransitionAllowAllToDenyAll(t *testing.T) { - const terraformResourceName = "test37" - theResource := acctests.TerraformResource(terraformResourceName) - remoteNetworkName := test.RandomName() + t.Parallel() + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -2403,7 +2413,7 @@ func TestAccTwingateResourcePolicyTransitionAllowAllToDenyAll(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithoutPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), + Config: createResourceWithoutPorts(resourceName, remoteNetworkName, resourceName, model.PolicyAllowAll), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyAllowAll), @@ -2411,7 +2421,7 @@ func TestAccTwingateResourcePolicyTransitionAllowAllToDenyAll(t *testing.T) { ), }, { - Config: createResourceWithoutPorts(terraformResourceName, remoteNetworkName, resourceName, model.PolicyDenyAll), + Config: createResourceWithoutPorts(resourceName, remoteNetworkName, resourceName, model.PolicyDenyAll), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyDenyAll), @@ -2423,10 +2433,11 @@ func TestAccTwingateResourcePolicyTransitionAllowAllToDenyAll(t *testing.T) { } func TestAccTwingateResourceWithBrowserOption(t *testing.T) { - const terraformResourceName = "test40" - theResource := acctests.TerraformResource(terraformResourceName) - remoteNetworkName := test.RandomName() + t.Parallel() + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() wildcardAddress := "*.acc-test.com" sdk.Test(t, sdk.TestCase{ @@ -2435,19 +2446,19 @@ func TestAccTwingateResourceWithBrowserOption(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithoutBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress), + Config: createResourceWithoutBrowserOption(resourceName, remoteNetworkName, resourceName, wildcardAddress), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), }, { - Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress, false), + Config: createResourceWithBrowserOption(resourceName, remoteNetworkName, resourceName, wildcardAddress, false), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), }, { - Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress, true), + Config: createResourceWithBrowserOption(resourceName, remoteNetworkName, resourceName, wildcardAddress, true), ExpectError: regexp.MustCompile(resource.ErrWildcardAddressWithEnabledShortcut.Error()), }, }, @@ -2455,10 +2466,11 @@ func TestAccTwingateResourceWithBrowserOption(t *testing.T) { } func TestAccTwingateResourceWithBrowserOptionFailOnUpdate(t *testing.T) { - const terraformResourceName = "test41" - theResource := acctests.TerraformResource(terraformResourceName) - remoteNetworkName := test.RandomName() + t.Parallel() + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() wildcardAddress := "*.acc-test.com" simpleAddress := "acc-test.com" @@ -2468,19 +2480,19 @@ func TestAccTwingateResourceWithBrowserOptionFailOnUpdate(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithoutBrowserOption(terraformResourceName, remoteNetworkName, resourceName, simpleAddress), + Config: createResourceWithoutBrowserOption(resourceName, remoteNetworkName, resourceName, simpleAddress), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), }, { - Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, simpleAddress, true), + Config: createResourceWithBrowserOption(resourceName, remoteNetworkName, resourceName, simpleAddress, true), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), }, { - Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress, true), + Config: createResourceWithBrowserOption(resourceName, remoteNetworkName, resourceName, wildcardAddress, true), ExpectError: regexp.MustCompile(resource.ErrWildcardAddressWithEnabledShortcut.Error()), }, }, @@ -2488,10 +2500,11 @@ func TestAccTwingateResourceWithBrowserOptionFailOnUpdate(t *testing.T) { } func TestAccTwingateResourceWithBrowserOptionRecovered(t *testing.T) { - const terraformResourceName = "test42" - theResource := acctests.TerraformResource(terraformResourceName) - remoteNetworkName := test.RandomName() + t.Parallel() + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() wildcardAddress := "*.acc-test.com" simpleAddress := "acc-test.com" @@ -2501,13 +2514,13 @@ func TestAccTwingateResourceWithBrowserOptionRecovered(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, simpleAddress, true), + Config: createResourceWithBrowserOption(resourceName, remoteNetworkName, resourceName, simpleAddress, true), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), }, { - Config: createResourceWithoutBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress), + Config: createResourceWithoutBrowserOption(resourceName, remoteNetworkName, resourceName, wildcardAddress), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), @@ -2542,3 +2555,144 @@ func createResourceWithBrowserOption(name, networkName, resourceName, address st } `, name, networkName, resourceName, address, browserOption) } + +func TestAccTwingateResourceUpdateSecurityPolicy(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() + + defaultPolicy, testPolicy := preparePolicies(t) + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithSecurityPolicy(remoteNetworkName, resourceName, defaultPolicy), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, defaultPolicy), + ), + }, + { + Config: createResourceWithSecurityPolicy(remoteNetworkName, resourceName, testPolicy), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, testPolicy), + ), + }, + { + Config: createResourceWithoutSecurityPolicy(remoteNetworkName, resourceName), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, defaultPolicy), + ), + }, + { + Config: createResourceWithSecurityPolicy(remoteNetworkName, resourceName, ""), + // no changes + PlanOnly: true, + }, + }, + }) +} + +func createResourceWithSecurityPolicy(remoteNetwork, resource, policyID string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[1]s" + } + resource "twingate_resource" "%[2]s" { + name = "%[2]s" + address = "acc-test-address.com" + remote_network_id = twingate_remote_network.%[1]s.id + security_policy_id = "%[3]s" + } + `, remoteNetwork, resource, policyID) +} + +func createResourceWithoutSecurityPolicy(remoteNetwork, resource string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[1]s" + } + resource "twingate_resource" "%[2]s" { + name = "%[2]s" + address = "acc-test-address.com" + remote_network_id = twingate_remote_network.%[1]s.id + } + `, remoteNetwork, resource) +} + +func preparePolicies(t *testing.T) (string, string) { + policies, err := acctests.ListSecurityPolicies() + if err != nil { + t.Skipf("failed to retrieve security policies: %v", err) + } + + if len(policies) < 2 { + t.Skip("requires at least 2 security policy for the test") + } + + var defaultPolicy, testPolicy string + if policies[0].Name == resource.DefaultSecurityPolicyName { + defaultPolicy = policies[0].ID + testPolicy = policies[1].ID + } else { + testPolicy = policies[0].ID + defaultPolicy = policies[1].ID + } + + return defaultPolicy, testPolicy +} + +func TestAccTwingateResourceSetDefaultSecurityPolicyByDefault(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() + + defaultPolicy, testPolicy := preparePolicies(t) + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithSecurityPolicy(remoteNetworkName, resourceName, testPolicy), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, testPolicy), + ), + }, + { + Config: createResourceWithoutSecurityPolicy(remoteNetworkName, resourceName), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, defaultPolicy), + acctests.CheckResourceSecurityPolicy(theResource, defaultPolicy), + // set new policy via API + acctests.UpdateResourceSecurityPolicy(theResource, testPolicy), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: createResourceWithSecurityPolicy(remoteNetworkName, resourceName, ""), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, defaultPolicy), + acctests.CheckResourceSecurityPolicy(theResource, defaultPolicy), + ), + }, + { + Config: createResourceWithoutSecurityPolicy(remoteNetworkName, resourceName), + // no changes + PlanOnly: true, + }, + }, + }) +} diff --git a/twingate/provider.go b/twingate/provider.go index a21e4c78..40cfcd38 100644 --- a/twingate/provider.go +++ b/twingate/provider.go @@ -96,13 +96,19 @@ func configure(version string, _ *schema.Provider) func(context.Context, *schema httpMaxRetry = withDefault(data.Get(attr.HTTPMaxRetry).(int), httpMaxRetry) if network != "" { - return client.NewClient(url, - apiToken, - network, - time.Duration(httpTimeout)*time.Second, - httpMaxRetry, - version), - nil + client := client.NewClient(url, + apiToken, + network, + time.Duration(httpTimeout)*time.Second, + httpMaxRetry, + version) + + policy, _ := client.ReadSecurityPolicy(ctx, "", resource.DefaultSecurityPolicyName) + if policy != nil { + resource.DefaultSecurityPolicyID = policy.ID + } + + return client, nil } return nil, diag.Diagnostics{