Skip to content

Commit

Permalink
[After Twingate#443] Feature: add support for filtering twingate secu…
Browse files Browse the repository at this point in the history
…rity-policies data source (Twingate#457)

* added new attributes for resources datasource

* added optional name attributes for resources datasource

* added feature branch

* fix test

* remove feature branch

* enable tests

* remove feature branch

* refactore

* updated docs

* update resources datasource: allow to list all resources

* added filter options for security_policy datasource

* enable test

* revert ci changes

* fix docs

* fixed docs

---------

Co-authored-by: bertekintw <[email protected]>
  • Loading branch information
vmanilo and bertekintw authored Feb 20, 2024
1 parent 6d55782 commit 6f16df1
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 17 deletions.
17 changes: 15 additions & 2 deletions docs/data-sources/security_policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,32 @@ Security Policies are defined in the Twingate Admin Console and determine user a
## Example Usage

```terraform
data "twingate_security_policies" "all" {}
data "twingate_security_policies" "all" {
name = "<your security policy's name>"
# name_regexp = "<regular expression of security policy name>"
# name_contains = "<a string in the security policy name>"
# name_exclude = "<your security policy's name to exclude>"
# name_prefix = "<prefix of security policy name>"
# name_suffix = "<suffix of security policy name>"
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Optional

- `security_policies` (Attributes List) (see [below for nested schema](#nestedatt--security_policies))
- `name` (String) Returns only security policies that exactly match this name. If no options are passed it will return all security policies. Only one option can be used at a time.
- `name_contains` (String) Match when the value exist in the name of the security policy.
- `name_exclude` (String) Match when the value does not exist in the name of the security policy.
- `name_prefix` (String) The name of the security policy must start with the value.
- `name_regexp` (String) The regular expression match of the name of the security policy.
- `name_suffix` (String) The name of the security policy must end with the value.

### Read-Only

- `id` (String) The ID of this resource.
- `security_policies` (Attributes List) (see [below for nested schema](#nestedatt--security_policies))

<a id="nestedatt--security_policies"></a>
### Nested Schema for `security_policies`
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
data "twingate_security_policies" "all" {}
data "twingate_security_policies" "all" {
name = "<your security policy's name>"
# name_regexp = "<regular expression of security policy name>"
# name_contains = "<a string in the security policy name>"
# name_exclude = "<your security policy's name to exclude>"
# name_prefix = "<prefix of security policy name>"
# name_suffix = "<suffix of security policy name>"
}
12 changes: 11 additions & 1 deletion twingate/internal/client/query/security-policies-read.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
const CursorPolicies = "policiesEndCursor"

type ReadSecurityPolicies struct {
SecurityPolicies `graphql:"securityPolicies(after: $policiesEndCursor, first: $pageLimit)"`
SecurityPolicies `graphql:"securityPolicies(filter: $filter, after: $policiesEndCursor, first: $pageLimit)"`
}

func (q ReadSecurityPolicies) IsEmpty() bool {
Expand All @@ -29,3 +29,13 @@ func (q ReadSecurityPolicies) ToModel() []*model.SecurityPolicy {
return edge.Node.ToModel()
})
}

type SecurityPolicyFilterField struct {
Name *StringFilterOperationInput `json:"name"`
}

func NewSecurityPolicyFilterField(name, filter string) *SecurityPolicyFilterField {
return &SecurityPolicyFilterField{
Name: NewStringFilterOperationInput(name, filter),
}
}
3 changes: 2 additions & 1 deletion twingate/internal/client/security-policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ func (client *Client) ReadSecurityPolicy(ctx context.Context, securityPolicyID,
return response.ToModel(), nil
}

func (client *Client) ReadSecurityPolicies(ctx context.Context) ([]*model.SecurityPolicy, error) {
func (client *Client) ReadSecurityPolicies(ctx context.Context, name, filter string) ([]*model.SecurityPolicy, error) {
opr := resourceSecurityPolicy.read()

variables := newVars(
gqlNullable(query.NewSecurityPolicyFilterField(name, filter), "filter"),
cursor(query.CursorPolicies),
pageLimit(client.pageLimit),
)
Expand Down
88 changes: 82 additions & 6 deletions twingate/internal/provider/datasource/security-policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
)

var ErrSecurityPoliciesDatasourceShouldSetOneOptionalNameAttribute = errors.New("Only one of name, name_regex, name_contains, name_exclude, name_prefix or name_suffix must be set.")

// Ensure the implementation satisfies the desired interfaces.
var _ datasource.DataSource = &securityPolicies{}

Expand All @@ -25,6 +27,12 @@ type securityPolicies struct {

type securityPoliciesModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
NameRegexp types.String `tfsdk:"name_regexp"`
NameContains types.String `tfsdk:"name_contains"`
NameExclude types.String `tfsdk:"name_exclude"`
NamePrefix types.String `tfsdk:"name_prefix"`
NameSuffix types.String `tfsdk:"name_suffix"`
SecurityPolicies []securityPolicyModel `tfsdk:"security_policies"`
}

Expand Down Expand Up @@ -58,9 +66,32 @@ func (d *securityPolicies) Schema(ctx context.Context, req datasource.SchemaRequ
Computed: true,
Description: computedDatasourceIDDescription,
},
attr.Name: schema.StringAttribute{
Optional: true,
Description: "Returns only security policies that exactly match this name. If no options are passed it will return all security policies. Only one option can be used at a time.",
},
attr.Name + attr.FilterByRegexp: schema.StringAttribute{
Optional: true,
Description: "The regular expression match of the name of the security policy.",
},
attr.Name + attr.FilterByContains: schema.StringAttribute{
Optional: true,
Description: "Match when the value exist in the name of the security policy.",
},
attr.Name + attr.FilterByExclude: schema.StringAttribute{
Optional: true,
Description: "Match when the value does not exist in the name of the security policy.",
},
attr.Name + attr.FilterByPrefix: schema.StringAttribute{
Optional: true,
Description: "The name of the security policy must start with the value.",
},
attr.Name + attr.FilterBySuffix: schema.StringAttribute{
Optional: true,
Description: "The name of the security policy must end with the value.",
},
attr.SecurityPolicies: schema.ListNestedAttribute{
Computed: true,
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
attr.ID: schema.StringAttribute{
Expand All @@ -78,18 +109,63 @@ func (d *securityPolicies) Schema(ctx context.Context, req datasource.SchemaRequ
}
}

//nolint:cyclop
func (d *securityPolicies) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
policies, err := d.client.ReadSecurityPolicies(ctx)
var data securityPoliciesModel

// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

var name, filter string

if data.Name.ValueString() != "" {
name = data.Name.ValueString()
}

if data.NameRegexp.ValueString() != "" {
name = data.NameRegexp.ValueString()
filter = attr.FilterByRegexp
}

if data.NameContains.ValueString() != "" {
name = data.NameContains.ValueString()
filter = attr.FilterByContains
}

if data.NameExclude.ValueString() != "" {
name = data.NameExclude.ValueString()
filter = attr.FilterByExclude
}

if data.NamePrefix.ValueString() != "" {
name = data.NamePrefix.ValueString()
filter = attr.FilterByPrefix
}

if data.NameSuffix.ValueString() != "" {
name = data.NameSuffix.ValueString()
filter = attr.FilterBySuffix
}

if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 {
addErr(&resp.Diagnostics, ErrSecurityPoliciesDatasourceShouldSetOneOptionalNameAttribute, TwingateSecurityPolicies)

return
}

policies, err := d.client.ReadSecurityPolicies(ctx, name, filter)
if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) {
addErr(&resp.Diagnostics, err, TwingateSecurityPolicy)

return
}

data := securityPoliciesModel{
ID: types.StringValue("security-policies-all"),
SecurityPolicies: convertSecurityPoliciesToTerraform(policies),
}
data.ID = types.StringValue("security-policies-all")
data.SecurityPolicies = convertSecurityPoliciesToTerraform(policies)

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
sdk "github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

var securityPolicyNamePath = attr.Path(attr.SecurityPolicies, attr.Name)

func TestAccDatasourceTwingateSecurityPoliciesBasic(t *testing.T) {
t.Run("Test Twingate Datasource : Acc Security Policies - basic", func(t *testing.T) {
acctests.SetPageLimit(1)
Expand Down Expand Up @@ -38,3 +40,95 @@ func testDatasourceTwingateSecurityPolicies() string {
data "twingate_security_policies" "all" {}
`
}

func testDatasourceTwingateSecurityPoliciesFilter(filter, name string) string {
return fmt.Sprintf(`
data "twingate_security_policies" "filtered" {
name%[1]s = "%[2]s"
}
`, filter, name)
}

func TestAccDatasourceTwingateSecurityPoliciesFilterByPrefix(t *testing.T) {
t.Parallel()

theDatasource := "data.twingate_security_policies.filtered"

sdk.Test(t, sdk.TestCase{
ProtoV6ProviderFactories: acctests.ProviderFactories,
PreCheck: func() { acctests.PreCheck(t) },
CheckDestroy: acctests.CheckTwingateResourceDestroy,
Steps: []sdk.TestStep{
{
Config: testDatasourceTwingateSecurityPoliciesFilter(attr.FilterByPrefix, "Def"),
Check: acctests.ComposeTestCheckFunc(
sdk.TestCheckResourceAttr(theDatasource, attr.Len(attr.SecurityPolicies), "1"),
sdk.TestCheckResourceAttr(theDatasource, securityPolicyNamePath, "Default Policy"),
),
},
},
})
}

func TestAccDatasourceTwingateSecurityPoliciesFilterBySuffix(t *testing.T) {
t.Parallel()

theDatasource := "data.twingate_security_policies.filtered"

sdk.Test(t, sdk.TestCase{
ProtoV6ProviderFactories: acctests.ProviderFactories,
PreCheck: func() { acctests.PreCheck(t) },
CheckDestroy: acctests.CheckTwingateResourceDestroy,
Steps: []sdk.TestStep{
{
Config: testDatasourceTwingateSecurityPoliciesFilter(attr.FilterBySuffix, "ault Policy"),
Check: acctests.ComposeTestCheckFunc(
sdk.TestCheckResourceAttr(theDatasource, attr.Len(attr.SecurityPolicies), "1"),
sdk.TestCheckResourceAttr(theDatasource, securityPolicyNamePath, "Default Policy"),
),
},
},
})
}

func TestAccDatasourceTwingateSecurityPoliciesFilterByContains(t *testing.T) {
t.Parallel()

theDatasource := "data.twingate_security_policies.filtered"

sdk.Test(t, sdk.TestCase{
ProtoV6ProviderFactories: acctests.ProviderFactories,
PreCheck: func() { acctests.PreCheck(t) },
CheckDestroy: acctests.CheckTwingateResourceDestroy,
Steps: []sdk.TestStep{
{
Config: testDatasourceTwingateSecurityPoliciesFilter(attr.FilterByContains, "ault"),
Check: acctests.ComposeTestCheckFunc(
sdk.TestCheckResourceAttr(theDatasource, attr.Len(attr.SecurityPolicies), "1"),
sdk.TestCheckResourceAttr(theDatasource, securityPolicyNamePath, "Default Policy"),
),
},
},
})
}

func TestAccDatasourceTwingateSecurityPoliciesFilterByRegexp(t *testing.T) {
t.Parallel()

theDatasource := "data.twingate_security_policies.filtered"

sdk.Test(t, sdk.TestCase{
ProtoV6ProviderFactories: acctests.ProviderFactories,
PreCheck: func() { acctests.PreCheck(t) },
CheckDestroy: acctests.CheckTwingateResourceDestroy,
Steps: []sdk.TestStep{
{
Config: testDatasourceTwingateSecurityPoliciesFilter(attr.FilterByRegexp, ".*ault .*"),
Check: acctests.ComposeTestCheckFunc(
sdk.TestCheckResourceAttr(theDatasource, attr.Len(attr.SecurityPolicies), "1"),
sdk.TestCheckResourceAttr(theDatasource, securityPolicyNamePath, "Default Policy"),
),
},
},
})
}
2 changes: 1 addition & 1 deletion twingate/internal/test/acctests/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ func ListSecurityPolicies() ([]*model.SecurityPolicy, error) {
return nil, ErrClientNotInited
}

securityPolicies, err := providerClient.ReadSecurityPolicies(context.Background())
securityPolicies, err := providerClient.ReadSecurityPolicies(context.Background(), "", "")
if err != nil {
return nil, fmt.Errorf("failed to fetch all security policies: %w", err)
}
Expand Down
10 changes: 5 additions & 5 deletions twingate/internal/test/client/security-policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func TestClientSecurityPoliciesReadOk(t *testing.T) {
),
)

securityPolicies, err := c.ReadSecurityPolicies(context.Background())
securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "")

assert.NoError(t, err)
assert.Equal(t, expected, securityPolicies)
Expand All @@ -201,7 +201,7 @@ func TestClientSecurityPoliciesReadRequestError(t *testing.T) {
httpmock.RegisterResponder("POST", c.GraphqlServerURL,
httpmock.NewErrorResponder(errBadRequest))

securityPolicies, err := c.ReadSecurityPolicies(context.Background())
securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "")

assert.Nil(t, securityPolicies)
assert.EqualError(t, err, graphqlErr(c, "failed to read security policy", errBadRequest))
Expand Down Expand Up @@ -240,7 +240,7 @@ func TestClientSecurityPoliciesReadEmptyResponse(t *testing.T) {
httpmock.RegisterResponder("POST", c.GraphqlServerURL,
httpmock.NewStringResponder(http.StatusOK, resp))

securityPolicies, err := c.ReadSecurityPolicies(context.Background())
securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "")

httpmock.Reset()

Expand Down Expand Up @@ -281,7 +281,7 @@ func TestClientSecurityPoliciesReadRequestErrorOnFetching(t *testing.T) {
),
)

securityPolicies, err := c.ReadSecurityPolicies(context.Background())
securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "")

assert.Nil(t, securityPolicies)
assert.EqualError(t, err, graphqlErr(c, "failed to read security policy", errBadRequest))
Expand Down Expand Up @@ -343,7 +343,7 @@ func TestClientSecurityPoliciesReadEmptyResultOnFetching(t *testing.T) {
),
)

securityPolicies, err := c.ReadSecurityPolicies(context.Background())
securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "")

httpmock.Reset()

Expand Down

0 comments on commit 6f16df1

Please sign in to comment.