From 9a5533027cedbd69c802c57c1689984a15dfe454 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Wed, 21 Aug 2024 19:55:02 +0200 Subject: [PATCH 01/12] wip --- twingate/internal/attr/dlp-policy.go | 3 + twingate/internal/client/dlp-policy.go | 65 ++++++++ twingate/internal/client/operation.go | 1 + .../client/query/dlp-policies-read.go | 40 +++++ .../internal/client/query/dlp-policy-read.go | 32 ++++ twingate/internal/model/dlp-policy.go | 14 ++ .../provider/datasource/all-datasources.go | 2 + .../provider/datasource/dlp-policies.go | 141 ++++++++++++++++++ .../provider/datasource/dlp-policy.go | 106 +++++++++++++ .../internal/provider/datasource/groups.go | 32 +--- twingate/provider.go | 1 + 11 files changed, 406 insertions(+), 31 deletions(-) create mode 100644 twingate/internal/attr/dlp-policy.go create mode 100644 twingate/internal/client/dlp-policy.go create mode 100644 twingate/internal/client/query/dlp-policies-read.go create mode 100644 twingate/internal/client/query/dlp-policy-read.go create mode 100644 twingate/internal/model/dlp-policy.go create mode 100644 twingate/internal/provider/datasource/dlp-policies.go create mode 100644 twingate/internal/provider/datasource/dlp-policy.go diff --git a/twingate/internal/attr/dlp-policy.go b/twingate/internal/attr/dlp-policy.go new file mode 100644 index 00000000..fb948d4b --- /dev/null +++ b/twingate/internal/attr/dlp-policy.go @@ -0,0 +1,3 @@ +package attr + +const DLPPolicies = "dlp_policies" diff --git a/twingate/internal/client/dlp-policy.go b/twingate/internal/client/dlp-policy.go new file mode 100644 index 00000000..2e6c687b --- /dev/null +++ b/twingate/internal/client/dlp-policy.go @@ -0,0 +1,65 @@ +package client + +import ( + "context" + + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/client/query" + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/model" +) + +func (client *Client) ReadDLPPolicy(ctx context.Context, policy *model.DLPPolicy) (*model.DLPPolicy, error) { + opr := resourceDLPPolicy.read() + + if policy.ID == "" && policy.Name == "" { + return nil, opr.apiError(ErrGraphqlEmptyBothNameAndID) + } + + variables := newVars( + gqlNullableID(policy.ID, "id"), + gqlNullable(policy.Name, "name"), + ) + + response := query.ReadDLPPolicy{} + if err := client.query(ctx, &response, variables, opr, attr{id: policy.ID, name: policy.Name}); err != nil { + return nil, err + } + + return response.ToModel(), nil +} + +func (client *Client) ReadDLPPolicies(ctx context.Context, name, filter string) ([]*model.DLPPolicy, error) { + opr := resourceDLPPolicy.read().withCustomName("readDLPPolicies") + + variables := newVars( + gqlNullable(query.NewDLPPoliciesFilterInput(name, filter), "filter"), + cursor(query.CursorDLPPolicies), + pageLimit(client.pageLimit), + ) + + response := query.ReadDLPPolicies{} + if err := client.query(ctx, &response, variables, opr, + attr{id: "All", name: name}); err != nil { + return nil, err + } + + oprCtx := withOperationCtx(ctx, opr) + + if err := response.FetchPages(oprCtx, client.readDLPPoliciesAfter, variables); err != nil { + return nil, err //nolint + } + + return response.ToModel(), nil +} + +func (client *Client) readDLPPoliciesAfter(ctx context.Context, variables map[string]interface{}, cursor string) (*query.PaginatedResource[*query.DLPPolicyEdge], error) { + opr := resourceDLPPolicy.read().withCustomName("readDLPPoliciesAfter") + + variables[query.CursorDLPPolicies] = cursor + + response := query.ReadDLPPolicies{} + if err := client.query(ctx, &response, variables, opr, attr{id: "All"}); err != nil { + return nil, err + } + + return &response.PaginatedResource, nil +} diff --git a/twingate/internal/client/operation.go b/twingate/internal/client/operation.go index c70033aa..be8a261e 100644 --- a/twingate/internal/client/operation.go +++ b/twingate/internal/client/operation.go @@ -22,6 +22,7 @@ const ( resourceServiceAccount resource = "service account" resourceServiceKey resource = "service account key" resourceUser resource = "user" + resourceDLPPolicy resource = "dlp policy" ) const ( diff --git a/twingate/internal/client/query/dlp-policies-read.go b/twingate/internal/client/query/dlp-policies-read.go new file mode 100644 index 00000000..dd9ac1b5 --- /dev/null +++ b/twingate/internal/client/query/dlp-policies-read.go @@ -0,0 +1,40 @@ +package query + +import ( + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/model" + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/utils" +) + +type DataLossPreventionPolicyFilterInput struct { + Name *StringFilterOperationInput `json:"name"` +} + +func NewDLPPoliciesFilterInput(name, filter string) *DataLossPreventionPolicyFilterInput { + return &DataLossPreventionPolicyFilterInput{ + Name: NewStringFilterOperationInput(name, filter), + } +} + +const CursorDLPPolicies = "dlpPoliciesEndCursor" + +type ReadDLPPolicies struct { + DLPPolicies `graphql:"dlpPolicies(filter: $filter, after: $dlpPoliciesEndCursor, first: $pageLimit)"` +} + +type DLPPolicies struct { + PaginatedResource[*DLPPolicyEdge] +} + +type DLPPolicyEdge struct { + Node *gqlDLPPolicy +} + +func (r ReadDLPPolicies) ToModel() []*model.DLPPolicy { + return utils.Map[*DLPPolicyEdge, *model.DLPPolicy](r.Edges, func(edge *DLPPolicyEdge) *model.DLPPolicy { + return edge.Node.ToModel() + }) +} + +func (r ReadDLPPolicies) IsEmpty() bool { + return len(r.DLPPolicies.Edges) == 0 +} diff --git a/twingate/internal/client/query/dlp-policy-read.go b/twingate/internal/client/query/dlp-policy-read.go new file mode 100644 index 00000000..511a2a7d --- /dev/null +++ b/twingate/internal/client/query/dlp-policy-read.go @@ -0,0 +1,32 @@ +package query + +import ( + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/model" +) + +type ReadDLPPolicy struct { + DLPPolicy *gqlDLPPolicy `graphql:"dlpPolicy(id: $id, name: $name)"` +} + +type gqlDLPPolicy struct { + IDName +} + +func (r gqlDLPPolicy) ToModel() *model.DLPPolicy { + return &model.DLPPolicy{ + ID: string(r.ID), + Name: r.Name, + } +} + +func (r ReadDLPPolicy) ToModel() *model.DLPPolicy { + if r.DLPPolicy == nil { + return nil + } + + return r.DLPPolicy.ToModel() +} + +func (r ReadDLPPolicy) IsEmpty() bool { + return r.DLPPolicy == nil +} diff --git a/twingate/internal/model/dlp-policy.go b/twingate/internal/model/dlp-policy.go new file mode 100644 index 00000000..1502b5f0 --- /dev/null +++ b/twingate/internal/model/dlp-policy.go @@ -0,0 +1,14 @@ +package model + +type DLPPolicy struct { + ID string + Name string +} + +func (p DLPPolicy) GetName() string { + return p.Name +} + +func (p DLPPolicy) GetID() string { + return p.ID +} diff --git a/twingate/internal/provider/datasource/all-datasources.go b/twingate/internal/provider/datasource/all-datasources.go index 3831603f..be3ae214 100644 --- a/twingate/internal/provider/datasource/all-datasources.go +++ b/twingate/internal/provider/datasource/all-datasources.go @@ -14,6 +14,8 @@ const ( TwingateServiceAccounts = "twingate_service_accounts" TwingateSecurityPolicy = "twingate_security_policy" // #nosec G101 TwingateSecurityPolicies = "twingate_security_policies" + TwingateDLPPolicy = "twingate_dlp_policy" + TwingateDLPPolicies = "twingate_dlp_policies" computedDatasourceIDDescription = "The ID of this resource." diff --git a/twingate/internal/provider/datasource/dlp-policies.go b/twingate/internal/provider/datasource/dlp-policies.go new file mode 100644 index 00000000..4d554bc4 --- /dev/null +++ b/twingate/internal/provider/datasource/dlp-policies.go @@ -0,0 +1,141 @@ +package datasource + +import ( + "context" + "fmt" + + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/client" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure the implementation satisfies the desired interfaces. +var _ datasource.DataSource = &dlpPolicies{} + +func NewDLPPoliciesDatasource() datasource.DataSource { + return &dlpPolicies{} +} + +type dlpPolicies struct { + client *client.Client +} + +type dlpPoliciesModel 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"` + Policies []dlpPolicyModel `tfsdk:"dlp_policies"` +} + +func (d *dlpPolicies) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = TwingateDLPPolicies +} + +func (d *dlpPolicies) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.client = client +} + +func (d *dlpPolicies) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + // TODO: update description + Description: "TODO. For more information, see Twingate's [documentation](https://docs.twingate.com/docs/groups).", + Attributes: map[string]schema.Attribute{ + attr.ID: schema.StringAttribute{ + Computed: true, + Description: "The ID of this data source.", + }, + attr.Name: schema.StringAttribute{ + Optional: true, + Description: "Returns only DLP policies that exactly match this name. If no options are passed, returns all DLP policies.", + }, + attr.Name + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Returns only DLP policies that contain this string.", + }, + attr.Name + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Returns only DLP policies that do not include this string.", + }, + attr.Name + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "Returns only DLP policies that start in this string.", + }, + attr.Name + attr.FilterByRegexp: schema.StringAttribute{ + Optional: true, + Description: "Returns only DLP policies that satisfy this regex.", + }, + attr.Name + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "Returns only DLP policies that end in this string.", + }, + + attr.DLPPolicies: schema.ListNestedAttribute{ + Computed: true, + Description: "List of DLP policies", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + attr.ID: schema.StringAttribute{ + Computed: true, + Description: "The ID of the DLP policy", + }, + attr.Name: schema.StringAttribute{ + Computed: true, + Description: "The name of the DLP policy", + }, + }, + }, + }, + }, + } +} + +func (d *dlpPolicies) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data dlpPoliciesModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 { + addErr(&resp.Diagnostics, ErrGroupsDatasourceShouldSetOneOptionalNameAttribute, TwingateDLPPolicies) + + return + } + + name, filter := getNameFilter(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) + policy, err := d.client.ReadDLPPolicies(client.WithCallerCtx(ctx, datasourceKey), name, filter) + if err != nil { + addErr(&resp.Diagnostics, err, TwingateDLPPolicy) + + return + } + + data.ID = types.StringValue(policy.ID) + data.Name = types.StringValue(policy.Name) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/twingate/internal/provider/datasource/dlp-policy.go b/twingate/internal/provider/datasource/dlp-policy.go new file mode 100644 index 00000000..32aa6fa7 --- /dev/null +++ b/twingate/internal/provider/datasource/dlp-policy.go @@ -0,0 +1,106 @@ +package datasource + +import ( + "context" + "fmt" + + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/client" + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/model" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure the implementation satisfies the desired interfaces. +var _ datasource.DataSource = &dlpPolicy{} + +func NewDLPPolicyDatasource() datasource.DataSource { + return &dlpPolicy{} +} + +type dlpPolicy struct { + client *client.Client +} + +type dlpPolicyModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` +} + +func (d *dlpPolicy) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = TwingateDLPPolicy +} + +func (d *dlpPolicy) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.client = client +} + +func (d *dlpPolicy) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + // TODO: update description + Description: "TODO. For more information, see Twingate's [documentation](https://docs.twingate.com/docs/groups).", + Attributes: map[string]schema.Attribute{ + attr.ID: schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The DLP policy's ID. Returns a DLP policy that has this ID.", + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.Root(attr.ID).Expression(), path.Root(attr.Name).Expression()), + }, + }, + attr.Name: schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The DLP policy's name. Returns a DLP policy that exactly matches this name.", + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.Root(attr.ID).Expression(), path.Root(attr.Name).Expression()), + }, + }, + }, + } +} + +func (d *dlpPolicy) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data dlpPolicyModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + policy, err := d.client.ReadDLPPolicy(client.WithCallerCtx(ctx, datasourceKey), &model.DLPPolicy{ + ID: data.ID.ValueString(), + Name: data.Name.ValueString(), + }) + if err != nil { + addErr(&resp.Diagnostics, err, TwingateDLPPolicy) + + return + } + + data.ID = types.StringValue(policy.ID) + data.Name = types.StringValue(policy.Name) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/twingate/internal/provider/datasource/groups.go b/twingate/internal/provider/datasource/groups.go index a48f7930..6245222c 100644 --- a/twingate/internal/provider/datasource/groups.go +++ b/twingate/internal/provider/datasource/groups.go @@ -182,38 +182,8 @@ func (d *groups) Read(ctx context.Context, req datasource.ReadRequest, resp *dat resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } -//nolint:cyclop func buildFilter(data *groupsModel) *model.GroupsFilter { - 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 - } + name, filter := getNameFilter(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) groupFilter := &model.GroupsFilter{ Name: &name, diff --git a/twingate/provider.go b/twingate/provider.go index 4c8be2fa..8659ca35 100644 --- a/twingate/provider.go +++ b/twingate/provider.go @@ -198,6 +198,7 @@ func (t Twingate) DataSources(ctx context.Context) []func() datasource.DataSourc twingateDatasource.NewSecurityPoliciesDatasource, twingateDatasource.NewResourceDatasource, twingateDatasource.NewResourcesDatasource, + twingateDatasource.NewDLPPolicyDatasource, } } From a14c671c62aad1f91954434c6d7a943e894eba0b Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Tue, 3 Sep 2024 20:53:43 +0200 Subject: [PATCH 02/12] added DLP policy --- docs/data-sources/dlp_policies.md | 49 +++ docs/data-sources/dlp_policy.md | 30 ++ docs/resources/resource.md | 2 + .../twingate_dlp_policies/data-source.tf | 8 + .../twingate_dlp_policy/data-source.tf | 6 + twingate/internal/attr/resource.go | 1 + twingate/internal/client/dlp-policy.go | 2 +- twingate/internal/client/query/common.go | 4 + .../internal/client/query/resource-create.go | 2 +- .../internal/client/query/resource-read.go | 32 +- twingate/internal/client/resource.go | 6 + twingate/internal/client/variables.go | 1 - twingate/internal/model/resource.go | 2 + .../internal/provider/datasource/converter.go | 13 + .../provider/datasource/converter_test.go | 61 +++ .../provider/datasource/dlp-policies.go | 13 +- .../provider/datasource/dlp-policy.go | 3 +- .../internal/provider/resource/resource.go | 34 +- .../acctests/datasource/dlp-policies_test.go | 37 ++ .../acctests/datasource/dlp-policy_test.go | 101 +++++ .../test/acctests/resource/resource_test.go | 54 +++ .../internal/test/client/dlp-policy_test.go | 363 ++++++++++++++++++ twingate/provider.go | 1 + 23 files changed, 802 insertions(+), 23 deletions(-) create mode 100644 docs/data-sources/dlp_policies.md create mode 100644 docs/data-sources/dlp_policy.md create mode 100644 examples/data-sources/twingate_dlp_policies/data-source.tf create mode 100644 examples/data-sources/twingate_dlp_policy/data-source.tf create mode 100644 twingate/internal/test/acctests/datasource/dlp-policies_test.go create mode 100644 twingate/internal/test/acctests/datasource/dlp-policy_test.go create mode 100644 twingate/internal/test/client/dlp-policy_test.go diff --git a/docs/data-sources/dlp_policies.md b/docs/data-sources/dlp_policies.md new file mode 100644 index 00000000..b2ba7854 --- /dev/null +++ b/docs/data-sources/dlp_policies.md @@ -0,0 +1,49 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "twingate_dlp_policies Data Source - terraform-provider-twingate" +subcategory: "" +description: |- + DLP policies are currently in early access. For more information, reach out to your account manager. +--- + +# twingate_dlp_policies (Data Source) + +DLP policies are currently in early access. For more information, reach out to your account manager. + +## Example Usage + +```terraform +data "twingate_dlp_policies" "foo" { + name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" +} +``` + + +## Schema + +### Optional + +- `name` (String) Returns only DLP policies that exactly match this name. If no options are passed, returns all DLP policies. +- `name_contains` (String) Returns only DLP policies that contain this string. +- `name_exclude` (String) Returns only DLP policies that do not include this string. +- `name_prefix` (String) Returns only DLP policies that start in this string. +- `name_regexp` (String) Returns only DLP policies that satisfy this regex. +- `name_suffix` (String) Returns only DLP policies that end in this string. + +### Read-Only + +- `dlp_policies` (Attributes List) List of DLP policies (see [below for nested schema](#nestedatt--dlp_policies)) +- `id` (String) The ID of this data source. + + +### Nested Schema for `dlp_policies` + +Read-Only: + +- `id` (String) The ID of the DLP policy +- `name` (String) The name of the DLP policy diff --git a/docs/data-sources/dlp_policy.md b/docs/data-sources/dlp_policy.md new file mode 100644 index 00000000..c4e00529 --- /dev/null +++ b/docs/data-sources/dlp_policy.md @@ -0,0 +1,30 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "twingate_dlp_policy Data Source - terraform-provider-twingate" +subcategory: "" +description: |- + DLP policies are currently in early access. For more information, reach out to your account manager. +--- + +# twingate_dlp_policy (Data Source) + +DLP policies are currently in early access. For more information, reach out to your account manager. + +## Example Usage + +```terraform +data "twingate_dlp_policy" "foo" { + id = "" +# name = "" +} + +# DLP policy can be queried by name or id +``` + + +## Schema + +### Optional + +- `id` (String) The DLP policy's ID. Returns a DLP policy that has this ID. +- `name` (String) The DLP policy's name. Returns a DLP policy that exactly matches this name. diff --git a/docs/resources/resource.md b/docs/resources/resource.md index 0582b2bf..b747958c 100644 --- a/docs/resources/resource.md +++ b/docs/resources/resource.md @@ -124,6 +124,7 @@ resource "twingate_resource" "resource" { - `access_group` (Block Set) Restrict access to certain group (see [below for nested schema](#nestedblock--access_group)) - `access_service` (Block Set) Restrict access to certain service account (see [below for nested schema](#nestedblock--access_service)) - `alias` (String) Set a DNS alias address for the Resource. Must be a DNS-valid name string. +- `dlp_policy_id` (String) The ID of a DLP policy to be used as the default DLP policy for this Resource. Defaults to null. - `is_active` (Boolean) Set the resource as active or inactive. Default is `true`. - `is_authoritative` (Boolean) Determines whether assignments in the access block will override any existing assignments. Default is `true`. If set to `false`, assignments made outside of Terraform will be ignored. - `is_browser_shortcut_enabled` (Boolean) Controls whether an "Open in Browser" shortcut will be shown for this Resource in the Twingate Client. Default is `false`. @@ -140,6 +141,7 @@ resource "twingate_resource" "resource" { Optional: +- `dlp_policy_id` (String) The ID of a DLP policy to be used as the DLP policy for the group in this access block. - `group_id` (String) Group ID that will have permission to access the Resource. - `security_policy_id` (String) The ID of a `twingate_security_policy` to use as the access policy for the group IDs in the access block. - `usage_based_autolock_duration_days` (Number) The usage-based auto-lock duration configured on the edge (in days). diff --git a/examples/data-sources/twingate_dlp_policies/data-source.tf b/examples/data-sources/twingate_dlp_policies/data-source.tf new file mode 100644 index 00000000..21299209 --- /dev/null +++ b/examples/data-sources/twingate_dlp_policies/data-source.tf @@ -0,0 +1,8 @@ +data "twingate_dlp_policies" "foo" { + name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" +} diff --git a/examples/data-sources/twingate_dlp_policy/data-source.tf b/examples/data-sources/twingate_dlp_policy/data-source.tf new file mode 100644 index 00000000..5a1ff9a0 --- /dev/null +++ b/examples/data-sources/twingate_dlp_policy/data-source.tf @@ -0,0 +1,6 @@ +data "twingate_dlp_policy" "foo" { + id = "" +# name = "" +} + +# DLP policy can be queried by name or id \ No newline at end of file diff --git a/twingate/internal/attr/resource.go b/twingate/internal/attr/resource.go index 7cd1b7c1..b5c3ff21 100644 --- a/twingate/internal/attr/resource.go +++ b/twingate/internal/attr/resource.go @@ -19,4 +19,5 @@ const ( IsVisible = "is_visible" IsBrowserShortcutEnabled = "is_browser_shortcut_enabled" Resources = "resources" + DLPPolicyID = "dlp_policy_id" ) diff --git a/twingate/internal/client/dlp-policy.go b/twingate/internal/client/dlp-policy.go index 2e6c687b..1892a04e 100644 --- a/twingate/internal/client/dlp-policy.go +++ b/twingate/internal/client/dlp-policy.go @@ -10,7 +10,7 @@ import ( func (client *Client) ReadDLPPolicy(ctx context.Context, policy *model.DLPPolicy) (*model.DLPPolicy, error) { opr := resourceDLPPolicy.read() - if policy.ID == "" && policy.Name == "" { + if policy == nil || policy.ID == "" && policy.Name == "" { return nil, opr.apiError(ErrGraphqlEmptyBothNameAndID) } diff --git a/twingate/internal/client/query/common.go b/twingate/internal/client/query/common.go index afdbc548..8c3fe762 100644 --- a/twingate/internal/client/query/common.go +++ b/twingate/internal/client/query/common.go @@ -9,6 +9,10 @@ type IDName struct { Name string `json:"name"` } +func (node IDName) GetID() string { + return string(node.ID) +} + type OkError struct { Ok bool `json:"ok"` Error string `json:"error"` diff --git a/twingate/internal/client/query/resource-create.go b/twingate/internal/client/query/resource-create.go index 1be824c5..f807fc58 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, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias, securityPolicyId: $securityPolicyId)"` + ResourceEntityResponse `graphql:"resourceCreate(name: $name, address: $address, remoteNetworkId: $remoteNetworkId, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias, securityPolicyId: $securityPolicyId, dlpPolicyId: $dlpPolicyId)"` } func (q CreateResource) IsEmpty() bool { diff --git a/twingate/internal/client/query/resource-read.go b/twingate/internal/client/query/resource-read.go index a96fbe7c..1f2b75ca 100644 --- a/twingate/internal/client/query/resource-read.go +++ b/twingate/internal/client/query/resource-read.go @@ -1,6 +1,8 @@ package query import ( + "reflect" + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/model" "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/utils" "github.com/hasura/go-graphql-client" @@ -33,6 +35,7 @@ type Access struct { type AccessEdge struct { Node Principal SecurityPolicy *gqlSecurityPolicy + DLPPolicy *gqlDLPPolicy UsageBasedAutolockDurationDays *int64 } @@ -59,6 +62,7 @@ type ResourceNode struct { IsBrowserShortcutEnabled bool Alias string SecurityPolicy *gqlSecurityPolicy + DLPPolicy *gqlDLPPolicy } type Protocols struct { @@ -81,16 +85,12 @@ func (r gqlResource) ToModel() *model.Resource { resource := r.ResourceNode.ToModel() for _, access := range r.Access.Edges { - var securityPolicyID *string - if access.SecurityPolicy != nil { - securityPolicyID = optionalString(string(access.SecurityPolicy.ID)) - } - switch access.Node.Type { case AccessGroup: resource.GroupsAccess = append(resource.GroupsAccess, model.AccessGroup{ GroupID: string(access.Node.ID), - SecurityPolicyID: securityPolicyID, + SecurityPolicyID: optionalID(access.SecurityPolicy), + DLPPolicyID: optionalID(access.DLPPolicy), UsageBasedDuration: access.UsageBasedAutolockDurationDays, }) case AccessServiceAccount: @@ -102,11 +102,6 @@ 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, @@ -117,7 +112,8 @@ func (r ResourceNode) ToModel() *model.Resource { IsVisible: &r.IsVisible, IsBrowserShortcutEnabled: &r.IsBrowserShortcutEnabled, Alias: optionalString(r.Alias), - SecurityPolicyID: optionalString(securityPolicy), + SecurityPolicyID: optionalID(r.SecurityPolicy), + DLPPolicyID: optionalID(r.DLPPolicy), } } @@ -164,3 +160,15 @@ func optionalString(str string) *string { return &str } + +type HasID interface { + GetID() string +} + +func optionalID(obj HasID) *string { + if obj == nil || reflect.ValueOf(obj).IsNil() { + return nil + } + + return optionalString(obj.GetID()) +} diff --git a/twingate/internal/client/resource.go b/twingate/internal/client/resource.go index 6747af04..cd2798d1 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.IsBrowserShortcutEnabled, "isBrowserShortcutEnabled"), gqlNullable(input.Alias, "alias"), gqlNullableID(input.SecurityPolicyID, "securityPolicyId"), + gqlNullableID(input.DLPPolicyID, "dlpPolicyId"), cursor(query.CursorAccess), pageLimit(client.pageLimit), ) @@ -96,6 +97,10 @@ func (client *Client) CreateResource(ctx context.Context, input *model.Resource) resource.SecurityPolicyID = nil } + if input.DLPPolicyID == nil { + resource.DLPPolicyID = nil + } + return resource, nil } @@ -375,6 +380,7 @@ func (client *Client) RemoveResourceAccess(ctx context.Context, resourceID strin type AccessInput struct { PrincipalID string `json:"principalId"` SecurityPolicyID *string `json:"securityPolicyId"` + DLPPolicyID *string `json:"dlpPolicyId"` UsageBasedAutolockDurationDays *int64 `json:"usageBasedAutolockDurationDays"` } diff --git a/twingate/internal/client/variables.go b/twingate/internal/client/variables.go index d26a0e73..c8aa072f 100644 --- a/twingate/internal/client/variables.go +++ b/twingate/internal/client/variables.go @@ -105,7 +105,6 @@ func getValue(val any) any { } } -//nolint:unparam func gqlNullableID(val interface{}, name string) gqlVarOption { return func(values map[string]interface{}) map[string]interface{} { var ( diff --git a/twingate/internal/model/resource.go b/twingate/internal/model/resource.go index 0ea0d05f..1451de88 100644 --- a/twingate/internal/model/resource.go +++ b/twingate/internal/model/resource.go @@ -24,6 +24,7 @@ var Policies = []string{PolicyRestricted, PolicyAllowAll, PolicyDenyAll} type AccessGroup struct { GroupID string SecurityPolicyID *string + DLPPolicyID *string UsageBasedDuration *int64 } @@ -59,6 +60,7 @@ type Resource struct { IsBrowserShortcutEnabled *bool Alias *string SecurityPolicyID *string + DLPPolicyID *string } func (r Resource) AccessToTerraform() []interface{} { diff --git a/twingate/internal/provider/datasource/converter.go b/twingate/internal/provider/datasource/converter.go index d43c3652..3c5b36f2 100644 --- a/twingate/internal/provider/datasource/converter.go +++ b/twingate/internal/provider/datasource/converter.go @@ -97,3 +97,16 @@ func convertStringListToSet(items []string) types.Set { return types.SetValueMust(types.StringType, values) } + +func convertPoliciesToTerraform(policies []*model.DLPPolicy) []dlpPolicyModel { + return utils.Map(policies, func(policy *model.DLPPolicy) dlpPolicyModel { + return dlpPolicyModel{ + ID: types.StringValue(policy.ID), + Name: types.StringValue(policy.Name), + } + }) +} + +func sanitizeName(name string) string { + return invalidNameRegex.ReplaceAllString(name, "") +} diff --git a/twingate/internal/provider/datasource/converter_test.go b/twingate/internal/provider/datasource/converter_test.go index 278d2cd9..027deb5f 100644 --- a/twingate/internal/provider/datasource/converter_test.go +++ b/twingate/internal/provider/datasource/converter_test.go @@ -292,3 +292,64 @@ func TestConvertSecurityPoliciesToTerraform(t *testing.T) { }) } } + +func TestConverterPoliciesToTerraform(t *testing.T) { + cases := []struct { + input []*model.DLPPolicy + expected []dlpPolicyModel + }{ + { + input: nil, + expected: []dlpPolicyModel{}, + }, + { + input: []*model.DLPPolicy{}, + expected: []dlpPolicyModel{}, + }, + { + input: []*model.DLPPolicy{ + {ID: "policy-id", Name: "policy-name"}, + }, + expected: []dlpPolicyModel{ + { + ID: types.StringValue("policy-id"), + Name: types.StringValue("policy-name"), + }, + }, + }, + } + + for n, c := range cases { + t.Run(fmt.Sprintf("case_%d", n), func(t *testing.T) { + actual := convertPoliciesToTerraform(c.input) + assert.Equal(t, c.expected, actual) + }) + } +} + +func TestConverterValidaNameRegex(t *testing.T) { + cases := []struct { + input string + expected string + }{ + { + input: "", + expected: "", + }, + { + input: "jaj29da;1;--213=", + expected: "jaj29da1213", + }, + { + input: ";;;;;kawodkada;1;--213=", + expected: "kawodkada1213", + }, + } + + for n, c := range cases { + t.Run(fmt.Sprintf("case_%d", n), func(t *testing.T) { + actual := sanitizeName(c.input) + assert.Equal(t, c.expected, actual) + }) + } +} diff --git a/twingate/internal/provider/datasource/dlp-policies.go b/twingate/internal/provider/datasource/dlp-policies.go index 4d554bc4..8e70c165 100644 --- a/twingate/internal/provider/datasource/dlp-policies.go +++ b/twingate/internal/provider/datasource/dlp-policies.go @@ -3,6 +3,7 @@ package datasource import ( "context" "fmt" + "regexp" "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/client" @@ -18,6 +19,8 @@ func NewDLPPoliciesDatasource() datasource.DataSource { return &dlpPolicies{} } +var invalidNameRegex = regexp.MustCompile(`\W+`) + type dlpPolicies struct { client *client.Client } @@ -57,8 +60,7 @@ func (d *dlpPolicies) Configure(ctx context.Context, req datasource.ConfigureReq func (d *dlpPolicies) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ - // TODO: update description - Description: "TODO. For more information, see Twingate's [documentation](https://docs.twingate.com/docs/groups).", + Description: "DLP policies are currently in early access. For more information, reach out to your account manager.", Attributes: map[string]schema.Attribute{ attr.ID: schema.StringAttribute{ Computed: true, @@ -126,15 +128,16 @@ func (d *dlpPolicies) Read(ctx context.Context, req datasource.ReadRequest, resp } name, filter := getNameFilter(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) - policy, err := d.client.ReadDLPPolicies(client.WithCallerCtx(ctx, datasourceKey), name, filter) + policies, err := d.client.ReadDLPPolicies(client.WithCallerCtx(ctx, datasourceKey), name, filter) + if err != nil { addErr(&resp.Diagnostics, err, TwingateDLPPolicy) return } - data.ID = types.StringValue(policy.ID) - data.Name = types.StringValue(policy.Name) + data.ID = types.StringValue("policies-by-name-" + sanitizeName(name)) + data.Policies = convertPoliciesToTerraform(policies) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/twingate/internal/provider/datasource/dlp-policy.go b/twingate/internal/provider/datasource/dlp-policy.go index 32aa6fa7..5fecc599 100644 --- a/twingate/internal/provider/datasource/dlp-policy.go +++ b/twingate/internal/provider/datasource/dlp-policy.go @@ -55,8 +55,7 @@ func (d *dlpPolicy) Configure(ctx context.Context, req datasource.ConfigureReque func (d *dlpPolicy) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ - // TODO: update description - Description: "TODO. For more information, see Twingate's [documentation](https://docs.twingate.com/docs/groups).", + Description: "DLP policies are currently in early access. For more information, reach out to your account manager.", Attributes: map[string]schema.Attribute{ attr.ID: schema.StringAttribute{ Optional: true, diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index 4dc8e390..d6002126 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -75,6 +75,7 @@ type resourceModel struct { IsBrowserShortcutEnabled types.Bool `tfsdk:"is_browser_shortcut_enabled"` Alias types.String `tfsdk:"alias"` SecurityPolicyID types.String `tfsdk:"security_policy_id"` + DLPPolicyID types.String `tfsdk:"dlp_policy_id"` } func (r *twingateResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -178,6 +179,13 @@ func (r *twingateResource) Schema(_ context.Context, _ resource.SchemaRequest, r Default: stringdefault.StaticString(DefaultSecurityPolicyID), PlanModifiers: []planmodifier.String{UseDefaultPolicyForUnknownModifier()}, }, + attr.DLPPolicyID: schema.StringAttribute{ + Optional: true, + //Computed: true, + Description: "The ID of a DLP policy to be used as the default DLP policy for this Resource. Defaults to null.", + //Default: stringdefault.StaticString(""), + //PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, attr.IsVisible: schema.BoolAttribute{ Optional: true, Computed: true, @@ -301,6 +309,12 @@ func groupAccessBlock() schema.SetNestedBlock { UseNullIntWhenValueOmitted(), }, }, + attr.DLPPolicyID: schema.StringAttribute{ + Optional: true, + //Computed: true, + Description: "The ID of a DLP policy to be used as the DLP policy for the group in this access block.", + //PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, }, }, } @@ -465,6 +479,7 @@ func convertResourceAccess(serviceAccounts []string, groupsAccess []model.Access access = append(access, client.AccessInput{ PrincipalID: group.GroupID, SecurityPolicyID: group.SecurityPolicyID, + DLPPolicyID: group.DLPPolicyID, UsageBasedAutolockDurationDays: group.UsageBasedDuration, }) } @@ -514,6 +529,11 @@ func getGroupAccessAttribute(list types.Set) []model.AccessGroup { accessGroup.SecurityPolicyID = securityPolicyVal.(types.String).ValueStringPointer() } + dlpPolicyVal := obj.Attributes()[attr.DLPPolicyID] + if dlpPolicyVal != nil && !dlpPolicyVal.IsNull() && !dlpPolicyVal.IsUnknown() { + accessGroup.DLPPolicyID = dlpPolicyVal.(types.String).ValueStringPointer() + } + usageBasedDuration := obj.Attributes()[attr.UsageBasedAutolockDurationDays] if usageBasedDuration != nil && !usageBasedDuration.IsNull() && !usageBasedDuration.IsUnknown() { accessGroup.UsageBasedDuration = usageBasedDuration.(types.Int64).ValueInt64Pointer() @@ -594,6 +614,7 @@ func convertResource(plan *resourceModel) (*model.Resource, error) { IsVisible: getOptionalBool(plan.IsVisible), IsBrowserShortcutEnabled: isBrowserShortcutEnabled, SecurityPolicyID: plan.SecurityPolicyID.ValueStringPointer(), + DLPPolicyID: plan.DLPPolicyID.ValueStringPointer(), }, nil } @@ -773,10 +794,15 @@ func (r *twingateResource) Read(ctx context.Context, req resource.ReadRequest, r if resource != nil { resource.IsAuthoritative = convertAuthoritativeFlag(state.IsAuthoritative) + emptyPolicy := "" + if state.SecurityPolicyID.ValueString() == "" { - emptyPolicy := "" resource.SecurityPolicyID = &emptyPolicy } + + if state.DLPPolicyID.ValueString() == "" { + resource.DLPPolicyID = &emptyPolicy + } } r.helper(ctx, resource, &state, &state, &resp.State, &resp.Diagnostics, err, operationRead) @@ -1014,6 +1040,10 @@ func setState(ctx context.Context, state, reference *resourceModel, resource *mo state.Alias = reference.Alias } + if !state.DLPPolicyID.IsNull() || !reference.DLPPolicyID.IsUnknown() { + state.DLPPolicyID = reference.DLPPolicyID + } + if !state.Protocols.IsNull() || !reference.Protocols.IsUnknown() { protocols, diags := convertProtocolsToTerraform(resource.Protocols, &reference.Protocols) diagnostics.Append(diags...) @@ -1243,6 +1273,7 @@ func convertGroupsAccessToTerraform(ctx context.Context, groupAccess []model.Acc attributes := map[string]tfattr.Value{ attr.GroupID: types.StringValue(access.GroupID), attr.SecurityPolicyID: types.StringPointerValue(access.SecurityPolicyID), + attr.DLPPolicyID: types.StringPointerValue(access.DLPPolicyID), attr.UsageBasedAutolockDurationDays: types.Int64PointerValue(access.UsageBasedDuration), } @@ -1354,6 +1385,7 @@ func accessGroupAttributeTypes() map[string]tfattr.Type { return map[string]tfattr.Type{ attr.GroupID: types.StringType, attr.SecurityPolicyID: types.StringType, + attr.DLPPolicyID: types.StringType, attr.UsageBasedAutolockDurationDays: types.Int64Type, } } diff --git a/twingate/internal/test/acctests/datasource/dlp-policies_test.go b/twingate/internal/test/acctests/datasource/dlp-policies_test.go new file mode 100644 index 00000000..81c9602d --- /dev/null +++ b/twingate/internal/test/acctests/datasource/dlp-policies_test.go @@ -0,0 +1,37 @@ +package datasource + +import ( + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/test/acctests" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "testing" +) + +func TestAccDatasourceTwingateDLPPolicies_basic(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateDLPPolicies(), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckOutput("test_policy", "Test"), + ), + }, + }, + }) +} + +func testDatasourceTwingateDLPPolicies() string { + return ` + data "twingate_dlp_policies" "test" { + name_prefix = "Te" + } + + output "test_policy" { + value = data.twingate_dlp_policies.test.dlp_policies[0].name + } + ` +} diff --git a/twingate/internal/test/acctests/datasource/dlp-policy_test.go b/twingate/internal/test/acctests/datasource/dlp-policy_test.go new file mode 100644 index 00000000..fe2d2a96 --- /dev/null +++ b/twingate/internal/test/acctests/datasource/dlp-policy_test.go @@ -0,0 +1,101 @@ +package datasource + +import ( + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/test/acctests" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "regexp" + "testing" +) + +func TestAccDatasourceTwingateDLPPolicy_queryByName(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateDLPPolicy(), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckOutput("test_policy", "Test"), + ), + }, + }, + }) +} + +func testDatasourceTwingateDLPPolicy() string { + return ` + data "twingate_dlp_policy" "test" { + name = "Test" + } + + output "test_policy" { + value = data.twingate_dlp_policy.test.name + } + ` +} + +func TestAccDatasourceTwingateDLPPolicy_shouldFail(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateDLPPolicyShouldFail(), + ExpectError: regexp.MustCompile("Error: Invalid Attribute Combination"), + }, + }, + }) +} + +func testDatasourceTwingateDLPPolicyShouldFail() string { + return ` + data "twingate_dlp_policy" "test" { + name = "policy-name" + id = "policy-id" + } + + output "test_policy" { + value = data.twingate_dlp_policy.test.name + } + ` +} + +func TestAccDatasourceTwingateDLPPolicy_queryByID(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateDLPPolicyQueryByID(), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckOutput("test_policy", "Test"), + ), + }, + }, + }) +} + +func testDatasourceTwingateDLPPolicyQueryByID() string { + return ` + data "twingate_dlp_policy" "test" { + name = "Test" + } + + data "twingate_dlp_policy" "test_by_id" { + id = data.twingate_dlp_policy.test.id + } + + output "test_policy" { + value = data.twingate_dlp_policy.test_by_id.name + } + ` +} diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index b81df06c..fb76ae16 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -3559,3 +3559,57 @@ func TestAccTwingateWithMultipleGroups(t *testing.T) { }, }) } + +func TestAccTwingateResourceWithDLPPolicy(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + remoteNetworkName := test.RandomName() + groupName := test.RandomGroupName() + + theResource := acctests.TerraformResource(resourceName) + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithDLPPolicy(remoteNetworkName, resourceName, groupName), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttrSet(theResource, attr.DLPPolicyID), + sdk.TestCheckResourceAttrSet(theResource, attr.Path(attr.AccessGroup, attr.DLPPolicyID)), + ), + }, + }, + }) +} + +func createResourceWithDLPPolicy(remoteNetwork, resource, groupName string) string { + return fmt.Sprintf(` + resource "twingate_group" "g21" { + name = "%[3]s" + } + + data "twingate_dlp_policy" "test" { + name = "Test" + } + + 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 + + dlp_policy_id = data.twingate_dlp_policy.test.id + + access_group { + group_id = twingate_group.g21.id + dlp_policy_id = data.twingate_dlp_policy.test.id + } + } + `, remoteNetwork, resource, groupName) +} diff --git a/twingate/internal/test/client/dlp-policy_test.go b/twingate/internal/test/client/dlp-policy_test.go new file mode 100644 index 00000000..83f434ee --- /dev/null +++ b/twingate/internal/test/client/dlp-policy_test.go @@ -0,0 +1,363 @@ +package client + +import ( + "context" + "fmt" + "github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/model" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "net/http" + "testing" +) + +func TestClientDLPPolicyReadOk(t *testing.T) { + t.Run("Test Twingate Resource : Read DLP Policy Ok", func(t *testing.T) { + expected := &model.DLPPolicy{ + ID: "policy-id", + Name: "policy-name", + } + + jsonResponse := `{ + "data": { + "dlpPolicy": { + "id": "policy-id", + "name": "policy-name" + } + } + }` + + c := newHTTPMockClient() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("POST", c.GraphqlServerURL, + httpmock.NewStringResponder(200, jsonResponse)) + + policy, err := c.ReadDLPPolicy(context.Background(), &model.DLPPolicy{ID: "policy-id"}) + + assert.NoError(t, err) + assert.Equal(t, expected, policy) + }) +} + +func TestClientDLPPolicyReadOkQueryByName(t *testing.T) { + t.Run("Test Twingate Resource : Read DLP Policy Ok Query By Name", func(t *testing.T) { + expected := &model.DLPPolicy{ + ID: "policy-id", + Name: "policy-name", + } + + jsonResponse := `{ + "data": { + "dlpPolicy": { + "id": "policy-id", + "name": "policy-name" + } + } + }` + + c := newHTTPMockClient() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("POST", c.GraphqlServerURL, + httpmock.NewStringResponder(200, jsonResponse)) + + policy, err := c.ReadDLPPolicy(context.Background(), &model.DLPPolicy{Name: "policy-name"}) + + assert.NoError(t, err) + assert.Equal(t, expected, policy) + }) +} + +func TestClientDLPPolicyReadError(t *testing.T) { + t.Run("Test Twingate Resource : Read DLP Policy Error", func(t *testing.T) { + jsonResponse := `{ + "data": { + "dlpPolicy": null + } + }` + + c := newHTTPMockClient() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("POST", c.GraphqlServerURL, + httpmock.NewStringResponder(200, jsonResponse)) + + const policyID = "policy-id" + policy, err := c.ReadDLPPolicy(context.Background(), &model.DLPPolicy{ID: policyID}) + + assert.Nil(t, policy) + assert.EqualError(t, err, fmt.Sprintf("failed to read dlp policy with id %s: query result is empty", policyID)) + }) +} + +func TestClientDLPPolicyReadRequestError(t *testing.T) { + t.Run("Test Twingate Resource : Read DLP Policy Request Error", func(t *testing.T) { + c := newHTTPMockClient() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("POST", c.GraphqlServerURL, + httpmock.NewErrorResponder(errBadRequest)) + + const policyID = "policy-id" + policy, err := c.ReadDLPPolicy(context.Background(), &model.DLPPolicy{ID: policyID}) + + assert.Nil(t, policy) + assert.EqualError(t, err, graphqlErr(c, "failed to read dlp policy with id "+policyID, errBadRequest)) + }) +} + +func TestClientReadEmptyDLPPolicyErrorWithNullPolicy(t *testing.T) { + t.Run("Test Twingate Resource : Read Empty DLP Policy Error with null policy", func(t *testing.T) { + + c := newHTTPMockClient() + defer httpmock.DeactivateAndReset() + + policy, err := c.ReadDLPPolicy(context.Background(), nil) + + assert.EqualError(t, err, "failed to read dlp policy: both name and id should not be empty") + assert.Nil(t, policy) + }) +} + +func TestClientReadEmptyDLPPolicyError(t *testing.T) { + t.Run("Test Twingate Resource : Read Empty DLP Policy Error", func(t *testing.T) { + + c := newHTTPMockClient() + defer httpmock.DeactivateAndReset() + + policy, err := c.ReadDLPPolicy(context.Background(), &model.DLPPolicy{}) + + assert.EqualError(t, err, "failed to read dlp policy: both name and id should not be empty") + assert.Nil(t, policy) + }) +} + +func TestClientDLPPoliciesReadOk(t *testing.T) { + t.Run("Test Twingate Resource : Read DLP Policies Ok", func(t *testing.T) { + expected := []*model.DLPPolicy{ + { + ID: "id1", + Name: "policy1", + }, + { + ID: "id2", + Name: "policy2", + }, + { + ID: "id3", + Name: "policy3", + }, + } + + jsonResponse := `{ + "data": { + "dlpPolicies": { + "edges": [ + { + "node": { + "id": "id1", + "name": "policy1" + } + }, + { + "node": { + "id": "id2", + "name": "policy2" + } + }, + { + "node": { + "id": "id3", + "name": "policy3" + } + } + ] + } + } + }` + + c := newHTTPMockClient() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("POST", c.GraphqlServerURL, + httpmock.NewStringResponder(200, jsonResponse)) + + policies, err := c.ReadDLPPolicies(context.Background(), "policy", "_prefix") + + assert.NoError(t, err) + assert.Equal(t, expected, policies) + }) +} + +func TestClientDLPPoliciesReadError(t *testing.T) { + t.Run("Test Twingate Resource : Read DLP Policies Error", func(t *testing.T) { + emptyResponse := `{ + "data": { + "dlpPolicies": null + } + }` + + c := newHTTPMockClient() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("POST", c.GraphqlServerURL, + httpmock.NewStringResponder(200, emptyResponse)) + + policies, err := c.ReadDLPPolicies(context.Background(), "", "") + + assert.Nil(t, policies) + assert.EqualError(t, err, "failed to read dlp policy with id All: query result is empty") + }) +} + +func TestClientDLPPoliciesReadRequestError(t *testing.T) { + t.Run("Test Twingate Resource : Read DLP Policies Request Error", func(t *testing.T) { + c := newHTTPMockClient() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("POST", c.GraphqlServerURL, + httpmock.NewErrorResponder(errBadRequest)) + + policies, err := c.ReadDLPPolicies(context.Background(), "", "") + + assert.Nil(t, policies) + assert.EqualError(t, err, graphqlErr(c, "failed to read dlp policy with id All", errBadRequest)) + }) +} + +func TestClientDLPPoliciesReadRequestErrorOnFetching(t *testing.T) { + t.Run("Test Twingate Resource : Read DLP Policies - Request Error on Fetching", func(t *testing.T) { + jsonResponse := `{ + "data": { + "dlpPolicies": { + "pageInfo": { + "endCursor": "cursor-001", + "hasNextPage": true + }, + "edges": [ + { + "node": { + "id": "id1", + "name": "policy1" + } + } + ] + } + } + }` + + c := newHTTPMockClient() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("POST", c.GraphqlServerURL, + MultipleResponders( + httpmock.NewStringResponder(200, jsonResponse), + httpmock.NewErrorResponder(errBadRequest), + ), + ) + + policies, err := c.ReadDLPPolicies(context.Background(), "policy", "_regexp") + + assert.Nil(t, policies) + assert.EqualError(t, err, graphqlErr(c, "failed to read dlp policy with id All", errBadRequest)) + }) +} + +func TestClientDLPPoliciesReadEmptyResultOnFetching(t *testing.T) { + t.Run("Test Twingate Resource : Read DLP Policies - Empty Result on Fetching", func(t *testing.T) { + response1 := `{ + "data": { + "dlpPolicies": { + "pageInfo": { + "endCursor": "cursor-001", + "hasNextPage": true + }, + "edges": [ + { + "node": { + "id": "id1", + "name": "policy1" + } + } + ] + } + } + }` + + response2 := `{}` + + c := newHTTPMockClient() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("POST", c.GraphqlServerURL, + MultipleResponders( + httpmock.NewStringResponder(200, response1), + httpmock.NewStringResponder(200, response2), + ), + ) + + policies, err := c.ReadDLPPolicies(context.Background(), "policy1", "_suffix") + + assert.Nil(t, policies) + assert.EqualError(t, err, `failed to read dlp policy with id All: query result is empty`) + }) +} + +func TestClientDLPPoliciesReadAllOk(t *testing.T) { + t.Run("Test Twingate Resource : Read DLP Policies All - Ok", func(t *testing.T) { + expected := []*model.DLPPolicy{ + {ID: "id-1", Name: "policy-1"}, + {ID: "id-2", Name: "policy-2"}, + {ID: "id-3", Name: "policy-3"}, + } + + jsonResponse := `{ + "data": { + "dlpPolicies": { + "pageInfo": { + "endCursor": "cursor-001", + "hasNextPage": true + }, + "edges": [ + { + "node": { + "id": "id-1", + "name": "policy-1" + } + }, + { + "node": { + "id": "id-2", + "name": "policy-2" + } + } + ] + } + } + }` + + nextPage := `{ + "data": { + "dlpPolicies": { + "pageInfo": { + "hasNextPage": false + }, + "edges": [ + { + "node": { + "id": "id-3", + "name": "policy-3" + } + } + ] + } + } + }` + + c := newHTTPMockClient() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("POST", c.GraphqlServerURL, + httpmock.ResponderFromMultipleResponses( + []*http.Response{ + httpmock.NewStringResponse(200, jsonResponse), + httpmock.NewStringResponse(200, nextPage), + }), + ) + + policies, err := c.ReadDLPPolicies(context.Background(), "policy", "_contains") + + assert.NoError(t, err) + assert.Equal(t, expected, policies) + }) +} diff --git a/twingate/provider.go b/twingate/provider.go index 8b8841c9..d87bdaa4 100644 --- a/twingate/provider.go +++ b/twingate/provider.go @@ -200,6 +200,7 @@ func (t Twingate) DataSources(ctx context.Context) []func() datasource.DataSourc twingateDatasource.NewResourcesDatasource, twingateDatasource.NewDNSFilteringProfileDatasource, twingateDatasource.NewDLPPolicyDatasource, + twingateDatasource.NewDLPPoliciesDatasource, } } From 3e4eef16bc39d90ec566cc74862586c9897ed8b3 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Tue, 3 Sep 2024 20:54:19 +0200 Subject: [PATCH 03/12] enable test --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc5ce2f4..00c22e69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ on: - 'README.md' branches: - main + - feature/add-support-for-DLP-policies # Ensures only 1 action runs per PR and previous is canceled on new trigger concurrency: From 5499cd6b5750d0c6e3b4399ca9e32f8cdfc69e80 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Tue, 3 Sep 2024 21:12:13 +0200 Subject: [PATCH 04/12] revert ci changes --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00c22e69..fc5ce2f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,6 @@ on: - 'README.md' branches: - main - - feature/add-support-for-DLP-policies # Ensures only 1 action runs per PR and previous is canceled on new trigger concurrency: From 4afebe94fc7497829838cfe3e920fa3fde996180 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Thu, 19 Sep 2024 06:07:35 +0200 Subject: [PATCH 05/12] fix update dlp policy --- .../client/query/resource-access-set.go | 9 ++++++ twingate/internal/client/resource.go | 23 +++++++++++++++ twingate/internal/model/resource.go | 1 + .../internal/provider/resource/resource.go | 2 +- .../test/acctests/resource/resource_test.go | 29 +++++++++++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 twingate/internal/client/query/resource-access-set.go diff --git a/twingate/internal/client/query/resource-access-set.go b/twingate/internal/client/query/resource-access-set.go new file mode 100644 index 00000000..2bd82072 --- /dev/null +++ b/twingate/internal/client/query/resource-access-set.go @@ -0,0 +1,9 @@ +package query + +type SetResourceAccess struct { + OkError `graphql:"resourceAccessSet(resourceId: $id, access: $access)"` +} + +func (q SetResourceAccess) IsEmpty() bool { + return false +} diff --git a/twingate/internal/client/resource.go b/twingate/internal/client/resource.go index cd2798d1..631e6ab5 100644 --- a/twingate/internal/client/resource.go +++ b/twingate/internal/client/resource.go @@ -406,3 +406,26 @@ func (client *Client) AddResourceAccess(ctx context.Context, resourceID string, return client.mutate(ctx, &response, variables, opr, attr{id: resourceID}) } + +func (client *Client) SetResourceAccess(ctx context.Context, resourceID string, access []AccessInput) error { + opr := resourceResourceAccess.update() + + if len(access) == 0 { + return nil + } + + if resourceID == "" { + return opr.apiError(ErrGraphqlIDIsEmpty) + } + + invalidateResource[*model.Resource](resourceID) + + variables := newVars( + gqlID(resourceID), + gqlNullable(access, "access"), + ) + + response := query.SetResourceAccess{} + + return client.mutate(ctx, &response, variables, opr, attr{id: resourceID}) +} diff --git a/twingate/internal/model/resource.go b/twingate/internal/model/resource.go index 1451de88..3a8f9922 100644 --- a/twingate/internal/model/resource.go +++ b/twingate/internal/model/resource.go @@ -30,6 +30,7 @@ type AccessGroup struct { func (g AccessGroup) Equals(another AccessGroup) bool { if g.GroupID == another.GroupID && + equalsOptionalString(g.DLPPolicyID, another.DLPPolicyID) && equalsOptionalString(g.SecurityPolicyID, another.SecurityPolicyID) && equalsOptionalInt64(g.UsageBasedDuration, another.UsageBasedDuration) { return true diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index d6002126..97cba895 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -909,7 +909,7 @@ func (r *twingateResource) updateResourceAccess(ctx context.Context, plan, state return fmt.Errorf("failed to update resource access: %w", err) } - if err := r.client.AddResourceAccess(ctx, input.ID, convertResourceAccess(serviceAccountsToAdd, groupsToAdd)); err != nil { + if err := r.client.SetResourceAccess(ctx, input.ID, convertResourceAccess(serviceAccountsToAdd, groupsToAdd)); err != nil { return fmt.Errorf("failed to update resource access: %w", err) } diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index fb76ae16..1eee9fbe 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -3581,6 +3581,13 @@ func TestAccTwingateResourceWithDLPPolicy(t *testing.T) { sdk.TestCheckResourceAttrSet(theResource, attr.Path(attr.AccessGroup, attr.DLPPolicyID)), ), }, + { + Config: resourceWithoutDLPPolicy(remoteNetworkName, resourceName, groupName), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckNoResourceAttr(theResource, attr.DLPPolicyID), + sdk.TestCheckNoResourceAttr(theResource, attr.Path(attr.AccessGroup, attr.DLPPolicyID)), + ), + }, }, }) } @@ -3613,3 +3620,25 @@ func createResourceWithDLPPolicy(remoteNetwork, resource, groupName string) stri } `, remoteNetwork, resource, groupName) } + +func resourceWithoutDLPPolicy(remoteNetwork, resource, groupName string) string { + return fmt.Sprintf(` + resource "twingate_group" "g21" { + name = "%[3]s" + } + + 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 + + access_group { + group_id = twingate_group.g21.id + } + } + `, remoteNetwork, resource, groupName) +} From 46161f2fd0bc93fe1d0b763e8f69df5e7e9f090d Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Thu, 19 Sep 2024 06:08:13 +0200 Subject: [PATCH 06/12] enable tests --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc5ce2f4..00c22e69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ on: - 'README.md' branches: - main + - feature/add-support-for-DLP-policies # Ensures only 1 action runs per PR and previous is canceled on new trigger concurrency: From ef22b416572bf894abe2a5b9d3f01525d2c3ad6e Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Thu, 19 Sep 2024 12:09:43 +0200 Subject: [PATCH 07/12] fix updateResourceAccess --- twingate/internal/provider/resource/helper.go | 34 +++++++---------- .../internal/provider/resource/resource.go | 38 ++++++++++++++++++- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/twingate/internal/provider/resource/helper.go b/twingate/internal/provider/resource/helper.go index 67f19d9f..d7f3168c 100644 --- a/twingate/internal/provider/resource/helper.go +++ b/twingate/internal/provider/resource/helper.go @@ -29,16 +29,8 @@ func setIntersection(a, b []string) []string { } func setIntersectionGroupAccess(inputA, inputB []model.AccessGroup) []model.AccessGroup { - setA := map[string]model.AccessGroup{} - setB := map[string]model.AccessGroup{} - - for _, access := range inputA { - setA[access.GroupID] = access - } - - for _, access := range inputB { - setB[access.GroupID] = access - } + setA := convertAccessGroupsToMap(inputA) + setB := convertAccessGroupsToMap(inputB) result := make([]model.AccessGroup, 0, len(setA)) @@ -77,16 +69,8 @@ func setDifference(inputA, inputB []string) []string { } func setDifferenceGroupAccess(inputA, inputB []model.AccessGroup) []model.AccessGroup { - setA := map[string]model.AccessGroup{} - setB := map[string]model.AccessGroup{} - - for _, access := range inputA { - setA[access.GroupID] = access - } - - for _, access := range inputB { - setB[access.GroupID] = access - } + setA := convertAccessGroupsToMap(inputA) + setB := convertAccessGroupsToMap(inputB) result := make([]model.AccessGroup, 0, len(setA)) @@ -99,6 +83,16 @@ func setDifferenceGroupAccess(inputA, inputB []model.AccessGroup) []model.Access return result } +func convertAccessGroupsToMap(groups []model.AccessGroup) map[string]model.AccessGroup { + set := map[string]model.AccessGroup{} + + for _, access := range groups { + set[access.GroupID] = access + } + + return set +} + func setDifferenceGroups(inputA, inputB []model.AccessGroup) []string { groupsA := utils.Map(inputA, func(item model.AccessGroup) string { return item.GroupID diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index 97cba895..4e769c4e 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -909,13 +909,49 @@ func (r *twingateResource) updateResourceAccess(ctx context.Context, plan, state return fmt.Errorf("failed to update resource access: %w", err) } - if err := r.client.SetResourceAccess(ctx, input.ID, convertResourceAccess(serviceAccountsToAdd, groupsToAdd)); err != nil { + if err := r.client.AddResourceAccess(ctx, input.ID, convertResourceAccess(serviceAccountsToAdd, groupsToAdd)); err != nil { return fmt.Errorf("failed to update resource access: %w", err) } + if hasToResetDLPPolicy(groupsToAdd) { + res, err := r.client.ReadResource(ctx, input.ID) + if err != nil { + return fmt.Errorf("failed to fetch resource data: %w", err) + } + + remoteGroups := overwriteDLPPolicies(res.GroupsAccess, groupsToAdd) + if err := r.client.SetResourceAccess(ctx, input.ID, convertResourceAccess(res.ServiceAccounts, remoteGroups)); err != nil { + return fmt.Errorf("failed to update resource access: %w", err) + } + } + return nil } +func overwriteDLPPolicies(remoteGroups, groupsToAdd []model.AccessGroup) []model.AccessGroup { + setGroupsToAdd := convertAccessGroupsToMap(groupsToAdd) + + for i, group := range remoteGroups { + if newGroup, ok := setGroupsToAdd[group.GroupID]; ok { + remoteGroups[i].DLPPolicyID = newGroup.DLPPolicyID + } + } + + return remoteGroups +} + +func hasToResetDLPPolicy(groupsToAdd []model.AccessGroup) bool { + if len(groupsToAdd) > 0 { + for _, group := range groupsToAdd { + if group.DLPPolicyID == nil { + return true + } + } + } + + return false +} + func (r *twingateResource) getChangedAccessIDs(ctx context.Context, plan, state *resourceModel, resource *model.Resource) ([]string, []string, []model.AccessGroup, error) { remote, err := r.client.ReadResource(ctx, resource.ID) if err != nil { From 6fd1dc5ec56dca4e4bd36176fa02d0d0476ced20 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Thu, 19 Sep 2024 12:41:20 +0200 Subject: [PATCH 08/12] revert ci changes --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00c22e69..fc5ce2f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,6 @@ on: - 'README.md' branches: - main - - feature/add-support-for-DLP-policies # Ensures only 1 action runs per PR and previous is canceled on new trigger concurrency: From 2c69ae1c6cadf259f2b35bfea9b8483913c199d3 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Fri, 20 Sep 2024 22:24:40 +0200 Subject: [PATCH 09/12] fix update dlp policy --- docs/resources/resource.md | 6 ++++ .../resources/twingate_resource/resource.tf | 6 ++++ go.mod | 3 +- .../internal/client/query/resource-update.go | 2 +- twingate/internal/client/resource.go | 1 + .../internal/provider/resource/resource.go | 3 +- twingate/internal/test/acctests/helper.go | 32 +++++++++++++++++++ .../test/acctests/resource/resource_test.go | 1 + 8 files changed, 51 insertions(+), 3 deletions(-) diff --git a/docs/resources/resource.md b/docs/resources/resource.md index b747958c..fc91b3d3 100644 --- a/docs/resources/resource.md +++ b/docs/resources/resource.md @@ -46,12 +46,17 @@ data "twingate_security_policy" "test_policy" { name = "Test Policy" } +data twingate_dlp_policy access_example { + name = "DLP Policy Access Example" +} + 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 + dlp_policy_id = data.twingate_dlp_policy.access_example.id protocols = { allow_icmp = true @@ -69,6 +74,7 @@ resource "twingate_resource" "resource" { group_id = twingate_group.aws.id security_policy_id = data.twingate_security_policy.test_policy.id usage_based_autolock_duration_days = 30 + dlp_policy_id = data.twingate_dlp_policy.access_example.id } // Adding multiple groups by individual ID diff --git a/examples/resources/twingate_resource/resource.tf b/examples/resources/twingate_resource/resource.tf index 2c7a892c..ba919543 100644 --- a/examples/resources/twingate_resource/resource.tf +++ b/examples/resources/twingate_resource/resource.tf @@ -31,12 +31,17 @@ data "twingate_security_policy" "test_policy" { name = "Test Policy" } +data twingate_dlp_policy access_example { + name = "DLP Policy Access Example" +} + 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 + dlp_policy_id = data.twingate_dlp_policy.access_example.id protocols = { allow_icmp = true @@ -54,6 +59,7 @@ resource "twingate_resource" "resource" { group_id = twingate_group.aws.id security_policy_id = data.twingate_security_policy.test_policy.id usage_based_autolock_duration_days = 30 + dlp_policy_id = data.twingate_dlp_policy.access_example.id } // Adding multiple groups by individual ID diff --git a/go.mod b/go.mod index 187eb378..a2f764c0 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/Twingate/terraform-provider-twingate/v3 -go 1.22 +go 1.22.0 + toolchain go1.22.5 require ( diff --git a/twingate/internal/client/query/resource-update.go b/twingate/internal/client/query/resource-update.go index 48ae871b..c7b2c4e0 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, securityPolicyId: $securityPolicyId, isActive: $isActive)"` + ResourceEntityResponse `graphql:"resourceUpdate(id: $id, name: $name, address: $address, remoteNetworkId: $remoteNetworkId, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias, securityPolicyId: $securityPolicyId, isActive: $isActive, dlpPolicyId: $dlpPolicyId)"` } func (q UpdateResource) IsEmpty() bool { diff --git a/twingate/internal/client/resource.go b/twingate/internal/client/resource.go index 631e6ab5..a5a780f7 100644 --- a/twingate/internal/client/resource.go +++ b/twingate/internal/client/resource.go @@ -258,6 +258,7 @@ func (client *Client) UpdateResource(ctx context.Context, input *model.Resource) gqlNullable(input.IsBrowserShortcutEnabled, "isBrowserShortcutEnabled"), gqlNullable(input.Alias, "alias"), gqlNullableID(input.SecurityPolicyID, "securityPolicyId"), + gqlNullableID(input.DLPPolicyID, "dlpPolicyId"), cursor(query.CursorAccess), pageLimit(client.pageLimit), ) diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index 4e769c4e..059f9e95 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -896,7 +896,8 @@ func isResourceChanged(plan, state *resourceModel) bool { !plan.IsVisible.Equal(state.IsVisible) || !plan.IsBrowserShortcutEnabled.Equal(state.IsBrowserShortcutEnabled) || !plan.Alias.Equal(state.Alias) || - !plan.SecurityPolicyID.Equal(state.SecurityPolicyID) + !plan.SecurityPolicyID.Equal(state.SecurityPolicyID) || + !plan.DLPPolicyID.Equal(state.DLPPolicyID) } func (r *twingateResource) updateResourceAccess(ctx context.Context, plan, state *resourceModel, input *model.Resource) error { diff --git a/twingate/internal/test/acctests/helper.go b/twingate/internal/test/acctests/helper.go index 8dbe3989..d726813c 100644 --- a/twingate/internal/test/acctests/helper.go +++ b/twingate/internal/test/acctests/helper.go @@ -36,6 +36,7 @@ var ( ErrSecurityPoliciesNotFound = errors.New("security policies not found") ErrInvalidPath = errors.New("invalid path: the path value cannot be asserted as string") ErrNotNullSecurityPolicy = errors.New("expected null security policy in GroupAccess, got non null") + ErrNotNullDLPPolicy = errors.New("expected null DLP policy, got non null") ErrNotNullUsageBased = errors.New("expected null usage based duration in GroupAccess, got non null") ErrNullSecurityPolicy = errors.New("expected non null security policy in GroupAccess, got null") ErrNullUsageBased = errors.New("expected non null usage based duration in GroupAccess, got null") @@ -447,6 +448,37 @@ func CheckTwingateResourceUsageBasedIsNullOnGroupAccess(resourceName string) sdk } } +func CheckTwingateResourceHasNoDLPPolicy(resourceName string) sdk.TestCheckFunc { + return func(s *terraform.State) error { + resourceState, ok := s.RootModule().Resources[resourceName] + + if !ok { + return fmt.Errorf("%w: %s", ErrResourceNotFound, resourceName) + } + + if resourceState.Primary.ID == "" { + return ErrResourceIDNotSet + } + + res, err := providerClient.ReadResource(context.Background(), resourceState.Primary.ID) + if err != nil { + return fmt.Errorf("failed to read resource: %w", err) + } + + if res.DLPPolicyID != nil { + return ErrNotNullDLPPolicy + } + + for _, access := range res.GroupsAccess { + if access.DLPPolicyID != nil { + return ErrNotNullDLPPolicy + } + } + + return nil + } +} + func CheckTwingateResourceActiveState(resourceName string, expectedActiveState bool) sdk.TestCheckFunc { return func(s *terraform.State) error { resourceState, ok := s.RootModule().Resources[resourceName] diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index 1eee9fbe..e9ad7e27 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -3586,6 +3586,7 @@ func TestAccTwingateResourceWithDLPPolicy(t *testing.T) { Check: acctests.ComposeTestCheckFunc( sdk.TestCheckNoResourceAttr(theResource, attr.DLPPolicyID), sdk.TestCheckNoResourceAttr(theResource, attr.Path(attr.AccessGroup, attr.DLPPolicyID)), + acctests.CheckTwingateResourceHasNoDLPPolicy(theResource), ), }, }, From 4623023266f2c95a492b557fc92f5b465b01b714 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Sun, 22 Sep 2024 07:45:55 +0200 Subject: [PATCH 10/12] fix dlp policy update --- .github/workflows/ci.yml | 1 + .../resources/twingate_resource/resource.tf | 85 ++----------------- .../internal/provider/resource/resource.go | 9 +- twingate/internal/test/acctests/helper.go | 23 +++++ .../test/acctests/resource/resource_test.go | 40 +++++++++ 5 files changed, 72 insertions(+), 86 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc5ce2f4..00c22e69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ on: - 'README.md' branches: - main + - feature/add-support-for-DLP-policies # Ensures only 1 action runs per PR and previous is canceled on new trigger concurrency: diff --git a/examples/resources/twingate_resource/resource.tf b/examples/resources/twingate_resource/resource.tf index ba919543..eaa03e93 100644 --- a/examples/resources/twingate_resource/resource.tf +++ b/examples/resources/twingate_resource/resource.tf @@ -1,38 +1,22 @@ provider "twingate" { - api_token = "1234567890abcdef" - network = "mynetwork" +# api_token = "1234567890abcdef" +# network = "mynetwork" } resource "twingate_remote_network" "aws_network" { - name = "aws_remote_network" + name = "aws_remote_network-2" } resource "twingate_group" "aws" { name = "aws_group" } -data "twingate_group" "security" { - id = "securityGroupID" -} - -data "twingate_groups" "devops" { - name_contains = "DevOps" -} - -data "twingate_groups" "sre" { - name_contains = "SRE" -} - -resource "twingate_service_account" "github_actions_prod" { - name = "Github Actions PROD" -} - data "twingate_security_policy" "test_policy" { name = "Test Policy" } -data twingate_dlp_policy access_example { - name = "DLP Policy Access Example" +data twingate_dlp_policy test { + name = "Test" } resource "twingate_resource" "resource" { @@ -41,62 +25,7 @@ resource "twingate_resource" "resource" { remote_network_id = twingate_remote_network.aws_network.id security_policy_id = data.twingate_security_policy.test_policy.id - dlp_policy_id = data.twingate_dlp_policy.access_example.id - - protocols = { - allow_icmp = true - tcp = { - policy = "RESTRICTED" - ports = ["80", "82-83"] - } - udp = { - policy = "ALLOW_ALL" - } - } - - // Adding a single group via `access_group` - access_group { - group_id = twingate_group.aws.id - security_policy_id = data.twingate_security_policy.test_policy.id - usage_based_autolock_duration_days = 30 - dlp_policy_id = data.twingate_dlp_policy.access_example.id - } - - // Adding multiple groups by individual ID - dynamic "access_group" { - for_each = toset([twingate_group.aws.id, data.twingate_group.security.id]) - content { - group_id = access_group.value - security_policy_id = data.twingate_security_policy.test_policy.id - usage_based_autolock_duration_days = 30 - } - } - - // Adding multiple groups from twingate_groups data sources - dynamic "access_group" { - for_each = setunion( - data.twingate_groups.devops.groups[*].id, - data.twingate_groups.sre.groups[*].id, - // Single IDs can be added by wrapping them in a set - toset([data.twingate_group.security.id]) - ) - content { - group_id = access_group.value - security_policy_id = data.twingate_security_policy.test_policy.id - usage_based_autolock_duration_days = 30 - - } - } - - // Service acoount access is specified similarly - // A `for_each` block may be used like above to assign access to multiple - // service accounts in a single configuration block. - access_service { - content { - service_account_id = twingate_service_account.github_actions_prod.id - } - } + dlp_policy_id = data.twingate_dlp_policy.test.id is_active = true -} - +} \ No newline at end of file diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index 059f9e95..fa510c10 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -799,10 +799,6 @@ func (r *twingateResource) Read(ctx context.Context, req resource.ReadRequest, r if state.SecurityPolicyID.ValueString() == "" { resource.SecurityPolicyID = &emptyPolicy } - - if state.DLPPolicyID.ValueString() == "" { - resource.DLPPolicyID = &emptyPolicy - } } r.helper(ctx, resource, &state, &state, &resp.State, &resp.Diagnostics, err, operationRead) @@ -1064,6 +1060,7 @@ func setState(ctx context.Context, state, reference *resourceModel, resource *mo state.IsActive = types.BoolValue(resource.IsActive) state.IsAuthoritative = types.BoolValue(resource.IsAuthoritative) state.SecurityPolicyID = types.StringPointerValue(resource.SecurityPolicyID) + state.DLPPolicyID = types.StringPointerValue(resource.DLPPolicyID) if !state.IsVisible.IsNull() || !reference.IsVisible.IsUnknown() { state.IsVisible = types.BoolPointerValue(resource.IsVisible) @@ -1077,10 +1074,6 @@ func setState(ctx context.Context, state, reference *resourceModel, resource *mo state.Alias = reference.Alias } - if !state.DLPPolicyID.IsNull() || !reference.DLPPolicyID.IsUnknown() { - state.DLPPolicyID = reference.DLPPolicyID - } - if !state.Protocols.IsNull() || !reference.Protocols.IsUnknown() { protocols, diags := convertProtocolsToTerraform(resource.Protocols, &reference.Protocols) diagnostics.Append(diags...) diff --git a/twingate/internal/test/acctests/helper.go b/twingate/internal/test/acctests/helper.go index d726813c..aad2a888 100644 --- a/twingate/internal/test/acctests/helper.go +++ b/twingate/internal/test/acctests/helper.go @@ -941,6 +941,29 @@ func UpdateResourceSecurityPolicy(resourceName, securityPolicyID string) sdk.Tes } } +func DeleteResourceDLPPolicy(resourceName 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.DLPPolicyID = nil + + _, err = providerClient.UpdateResource(context.Background(), resource) + if err != nil { + return fmt.Errorf("resource with ID %s failed to update dlp_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 e9ad7e27..b6c595d9 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -3643,3 +3643,43 @@ func resourceWithoutDLPPolicy(remoteNetwork, resource, groupName string) string } `, remoteNetwork, resource, groupName) } + +func TestAccTwingateResourceUpdateDLPPolicy(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + remoteNetworkName := test.RandomName() + groupName := test.RandomGroupName() + + theResource := acctests.TerraformResource(resourceName) + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithDLPPolicy(remoteNetworkName, resourceName, groupName), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttrSet(theResource, attr.DLPPolicyID), + ), + }, + { + Config: createResourceWithDLPPolicy(remoteNetworkName, resourceName, groupName), + ExpectNonEmptyPlan: true, + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttrSet(theResource, attr.DLPPolicyID), + // deletes DLP policy from resource via API + acctests.DeleteResourceDLPPolicy(theResource), + acctests.WaitTestFunc(), + ), + }, + { + Config: createResourceWithDLPPolicy(remoteNetworkName, resourceName, groupName), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttrSet(theResource, attr.DLPPolicyID), + ), + }, + }, + }) +} From 875a442e95367f00e48c6972aa4483ac506d6dc1 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Sun, 22 Sep 2024 08:20:24 +0200 Subject: [PATCH 11/12] update docs --- docs/resources/resource.md | 2 +- .../resources/twingate_resource/resource.tf | 84 +++++++++++++++++-- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/docs/resources/resource.md b/docs/resources/resource.md index fc91b3d3..c9b83a56 100644 --- a/docs/resources/resource.md +++ b/docs/resources/resource.md @@ -104,7 +104,7 @@ resource "twingate_resource" "resource" { } // Service acoount access is specified similarly - // A `for_each` block may be used like above to assign access to multiple + // A `for_each` block may be used like above to assign access to multiple // service accounts in a single configuration block. access_service { content { diff --git a/examples/resources/twingate_resource/resource.tf b/examples/resources/twingate_resource/resource.tf index eaa03e93..8ec83cf7 100644 --- a/examples/resources/twingate_resource/resource.tf +++ b/examples/resources/twingate_resource/resource.tf @@ -1,22 +1,38 @@ provider "twingate" { -# api_token = "1234567890abcdef" -# network = "mynetwork" + api_token = "1234567890abcdef" + network = "mynetwork" } resource "twingate_remote_network" "aws_network" { - name = "aws_remote_network-2" + name = "aws_remote_network" } resource "twingate_group" "aws" { name = "aws_group" } +data "twingate_group" "security" { + id = "securityGroupID" +} + +data "twingate_groups" "devops" { + name_contains = "DevOps" +} + +data "twingate_groups" "sre" { + name_contains = "SRE" +} + +resource "twingate_service_account" "github_actions_prod" { + name = "Github Actions PROD" +} + data "twingate_security_policy" "test_policy" { name = "Test Policy" } -data twingate_dlp_policy test { - name = "Test" +data twingate_dlp_policy access_example { + name = "DLP Policy Access Example" } resource "twingate_resource" "resource" { @@ -25,7 +41,61 @@ resource "twingate_resource" "resource" { remote_network_id = twingate_remote_network.aws_network.id security_policy_id = data.twingate_security_policy.test_policy.id - dlp_policy_id = data.twingate_dlp_policy.test.id + dlp_policy_id = data.twingate_dlp_policy.access_example.id + + protocols = { + allow_icmp = true + tcp = { + policy = "RESTRICTED" + ports = ["80", "82-83"] + } + udp = { + policy = "ALLOW_ALL" + } + } + + // Adding a single group via `access_group` + access_group { + group_id = twingate_group.aws.id + security_policy_id = data.twingate_security_policy.test_policy.id + usage_based_autolock_duration_days = 30 + dlp_policy_id = data.twingate_dlp_policy.access_example.id + } + + // Adding multiple groups by individual ID + dynamic "access_group" { + for_each = toset([twingate_group.aws.id, data.twingate_group.security.id]) + content { + group_id = access_group.value + security_policy_id = data.twingate_security_policy.test_policy.id + usage_based_autolock_duration_days = 30 + } + } + + // Adding multiple groups from twingate_groups data sources + dynamic "access_group" { + for_each = setunion( + data.twingate_groups.devops.groups[*].id, + data.twingate_groups.sre.groups[*].id, + // Single IDs can be added by wrapping them in a set + toset([data.twingate_group.security.id]) + ) + content { + group_id = access_group.value + security_policy_id = data.twingate_security_policy.test_policy.id + usage_based_autolock_duration_days = 30 + + } + } + + // Service acoount access is specified similarly + // A `for_each` block may be used like above to assign access to multiple + // service accounts in a single configuration block. + access_service { + content { + service_account_id = twingate_service_account.github_actions_prod.id + } + } is_active = true -} \ No newline at end of file +} From ca4affc1d5ba955f933dcf089c2d9b9ba72cf275 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Sun, 22 Sep 2024 08:40:42 +0200 Subject: [PATCH 12/12] remove feature branch --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00c22e69..fc5ce2f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,6 @@ on: - 'README.md' branches: - main - - feature/add-support-for-DLP-policies # Ensures only 1 action runs per PR and previous is canceled on new trigger concurrency: