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, } }