From 489c394b12825c78aac2ae316f467b957d683da0 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Fri, 10 Nov 2023 04:34:26 +0100 Subject: [PATCH] added security_policy_id to resource definition --- .github/workflows/ci.yml | 6 +- docs/resources/resource.md | 1 + .../internal/client/query/resource-create.go | 2 +- .../internal/client/query/resource-read.go | 7 ++ .../internal/client/query/resource-update.go | 2 +- twingate/internal/client/resource.go | 10 +++ twingate/internal/client/variables.go | 5 ++ twingate/internal/model/resource.go | 1 + .../internal/provider/resource/resource.go | 46 +++++++---- .../test/acctests/resource/resource_test.go | 76 +++++++++++++++++++ 10 files changed, 139 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee1bfee8..7394cd65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - main + - feature/add-security_policy-to-resource paths-ignore: - 'README.md' @@ -15,6 +16,7 @@ on: - 'README.md' branches: - main + - feature/add-security_policy-to-resource # Ensures only 1 action runs per PR and previous is canceled on new trigger concurrency: @@ -118,7 +120,7 @@ jobs: name: Matrix Acceptance Tests needs: build runs-on: ubuntu-latest - if: "!github.event.pull_request.head.repo.fork" +# if: "!github.event.pull_request.head.repo.fork" timeout-minutes: 15 strategy: fail-fast: false @@ -169,7 +171,7 @@ jobs: cleanup: name: Cleanup - if: "!github.event.pull_request.head.repo.fork" +# if: "!github.event.pull_request.head.repo.fork" needs: tests-acceptance runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/docs/resources/resource.md b/docs/resources/resource.md index 7c5a03e1..8330976c 100644 --- a/docs/resources/resource.md +++ b/docs/resources/resource.md @@ -70,6 +70,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. ### Read-Only 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..e16cd252 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -136,6 +136,11 @@ 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.", + }, // computed attr.IsVisible: { Type: schema.TypeBool, @@ -222,6 +227,7 @@ func resourceUpdate(ctx context.Context, resourceData *schema.ResourceData, meta attr.IsVisible, attr.IsBrowserShortcutEnabled, attr.Alias, + attr.SecurityPolicyID, ) { resource, err = client.UpdateResource(ctx, resource) } else { @@ -239,9 +245,15 @@ func resourceUpdate(ctx context.Context, resourceData *schema.ResourceData, meta 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 +360,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 @@ -483,14 +494,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 +536,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/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index a5a97811..8de6ba07 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -2542,3 +2542,79 @@ func createResourceWithBrowserOption(name, networkName, resourceName, address st } `, name, networkName, resourceName, address, browserOption) } + +func TestAccTwingateResourceUpdateSecurityPolicy(t *testing.T) { + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() + + 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") + } + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithSecurityPolicy(remoteNetworkName, resourceName, policies[0].ID), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, policies[0].ID), + ), + }, + { + Config: createResourceWithSecurityPolicy(remoteNetworkName, resourceName, policies[1].ID), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, policies[1].ID), + ), + }, + { + Config: createResourceWithoutSecurityPolicy(remoteNetworkName, resourceName), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, ""), + ), + }, + { + 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) +}