Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: added security_policy_id to resource definition #425

Merged
merged 10 commits into from
Nov 28, 2023
1 change: 1 addition & 0 deletions docs/resources/resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
twingate-blee marked this conversation as resolved.
Show resolved Hide resolved
twingate-blee marked this conversation as resolved.
Show resolved Hide resolved

### Read-Only

Expand Down
2 changes: 1 addition & 1 deletion twingate/internal/client/query/resource-create.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions twingate/internal/client/query/resource-read.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type ResourceNode struct {
IsVisible bool
IsBrowserShortcutEnabled bool
Alias string
SecurityPolicy *gqlSecurityPolicy
}

type Protocols struct {
Expand Down Expand Up @@ -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,
Expand All @@ -100,6 +106,7 @@ func (r ResourceNode) ToModel() *model.Resource {
IsVisible: &r.IsVisible,
IsBrowserShortcutEnabled: &r.IsBrowserShortcutEnabled,
Alias: optionalString(r.Alias),
SecurityPolicyID: optionalString(securityPolicy),
}
}

Expand Down
2 changes: 1 addition & 1 deletion twingate/internal/client/query/resource-update.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions twingate/internal/client/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
Expand All @@ -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
}

Expand Down Expand Up @@ -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),
)
Expand All @@ -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
}

Expand Down
5 changes: 5 additions & 0 deletions twingate/internal/client/variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,18 @@ func getValue(val any) any {
}
}

//nolint:unparam
func gqlNullableID(val interface{}, name string) gqlVarOption {
return func(values map[string]interface{}) map[string]interface{} {
var (
gqlValue interface{}
defaultID *graphql.ID
)

if value, ok := val.(*string); ok && value != nil {
val = *value
}

if isZeroValue(val) {
gqlValue = defaultID
} else {
Expand Down
1 change: 1 addition & 0 deletions twingate/internal/model/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Resource struct {
IsVisible *bool
IsBrowserShortcutEnabled *bool
Alias *string
SecurityPolicyID *string
}

func (r Resource) AccessToTerraform() []interface{} {
Expand Down
46 changes: 33 additions & 13 deletions twingate/internal/provider/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down
76 changes: 76 additions & 0 deletions twingate/internal/test/acctests/resource/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
),
},
romankor marked this conversation as resolved.
Show resolved Hide resolved
{
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 {
vmanilo marked this conversation as resolved.
Show resolved Hide resolved
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)
}