Skip to content

Commit

Permalink
added support for usage based access
Browse files Browse the repository at this point in the history
  • Loading branch information
vmanilo committed Apr 9, 2024
1 parent 2d64b85 commit 40ba580
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 34 deletions.
2 changes: 2 additions & 0 deletions docs/resources/resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ resource "twingate_resource" "resource" {
content {
group_id = access_group.value
security_policy_id = data.twingate_security_policy.test_policy.id
usage_based_autolock_duration_days = 30
}
}
Expand Down Expand Up @@ -103,6 +104,7 @@ Optional:

- `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).


<a id="nestedblock--access_service"></a>
Expand Down
1 change: 1 addition & 0 deletions examples/resources/twingate_resource/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ resource "twingate_resource" "resource" {
content {
group_id = access_group.value
security_policy_id = data.twingate_security_policy.test_policy.id
usage_based_autolock_duration_days = 30
}
}

Expand Down
1 change: 1 addition & 0 deletions golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ linters-settings:
- github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier.Set
- github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier.Bool
- github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier.String
- github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier.Int64
- github.com/hashicorp/terraform-plugin-testing/plancheck.PlanCheck
errcheck:
check-type-assertions: false
Expand Down
35 changes: 18 additions & 17 deletions twingate/internal/attr/resource.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
package attr

const (
Access = "access"
AccessGroup = "access_group"
AccessService = "access_service"
GroupIDs = "group_ids"
GroupID = "group_id"
ServiceAccountIDs = "service_account_ids"
IsAuthoritative = "is_authoritative"
Policy = "policy"
Ports = "ports"
Address = "address"
Protocols = "protocols"
AllowIcmp = "allow_icmp"
TCP = "tcp"
UDP = "udp"
IsVisible = "is_visible"
IsBrowserShortcutEnabled = "is_browser_shortcut_enabled"
Resources = "resources"
Access = "access"
AccessGroup = "access_group"
UsageBasedAutolockDurationDays = "usage_based_autolock_duration_days"
AccessService = "access_service"
GroupIDs = "group_ids"
GroupID = "group_id"
ServiceAccountIDs = "service_account_ids"
IsAuthoritative = "is_authoritative"
Policy = "policy"
Ports = "ports"
Address = "address"
Protocols = "protocols"
AllowIcmp = "allow_icmp"
TCP = "tcp"
UDP = "udp"
IsVisible = "is_visible"
IsBrowserShortcutEnabled = "is_browser_shortcut_enabled"
Resources = "resources"
)
10 changes: 6 additions & 4 deletions twingate/internal/client/query/resource-read.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ type Access struct {
}

type AccessEdge struct {
Node Principal
SecurityPolicy *gqlSecurityPolicy
Node Principal
SecurityPolicy *gqlSecurityPolicy
UsageBasedAutolockDurationDays *int64
}

type Principal struct {
Expand Down Expand Up @@ -88,8 +89,9 @@ func (r gqlResource) ToModel() *model.Resource {
switch access.Node.Type {
case AccessGroup:
resource.GroupsAccess = append(resource.GroupsAccess, model.AccessGroup{
GroupID: string(access.Node.ID),
SecurityPolicyID: securityPolicyID,
GroupID: string(access.Node.ID),
SecurityPolicyID: securityPolicyID,
UsageBasedDuration: access.UsageBasedAutolockDurationDays,
})
case AccessServiceAccount:
resource.ServiceAccounts = append(resource.ServiceAccounts, string(access.Node.ID))
Expand Down
5 changes: 3 additions & 2 deletions twingate/internal/client/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,9 @@ func (client *Client) RemoveResourceAccess(ctx context.Context, resourceID strin
}

type AccessInput struct {
PrincipalID string `json:"principalId"`
SecurityPolicyID *string `json:"securityPolicyId"`
PrincipalID string `json:"principalId"`
SecurityPolicyID *string `json:"securityPolicyId"`
UsageBasedAutolockDurationDays *int64 `json:"usageBasedAutolockDurationDays"`
}

func (client *Client) AddResourceAccess(ctx context.Context, resourceID string, access []AccessInput) error {
Expand Down
25 changes: 21 additions & 4 deletions twingate/internal/model/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,33 @@ const (
PolicyRestricted = "RESTRICTED"
PolicyAllowAll = "ALLOW_ALL"
PolicyDenyAll = "DENY_ALL"

NullSecurityPolicy = "none"
)

//nolint:gochecknoglobals
var Policies = []string{PolicyRestricted, PolicyAllowAll, PolicyDenyAll}

type AccessGroup struct {
GroupID string
SecurityPolicyID *string
GroupID string
SecurityPolicyID *string
UsageBasedDuration *int64
}

func (g AccessGroup) Equals(another AccessGroup) bool {
if g.GroupID == another.GroupID &&
equalsOptionalString(g.SecurityPolicyID, another.SecurityPolicyID) &&
equalsOptionalInt64(g.UsageBasedDuration, another.UsageBasedDuration) {
return true
}

return false
}

func equalsOptionalString(s1, s2 *string) bool {
return s1 == nil && s2 == nil || s1 != nil && s2 != nil && strings.EqualFold(*s1, *s2)
}

func equalsOptionalInt64(i1, i2 *int64) bool {
return i1 == nil && i2 == nil || i1 != nil && i2 != nil && *i1 == *i2
}

type Resource struct {
Expand Down
2 changes: 1 addition & 1 deletion twingate/internal/provider/resource/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func setDifferenceGroupAccess(inputA, inputB []model.AccessGroup) []model.Access
result := make([]model.AccessGroup, 0, len(setA))

for key, valA := range setA {
if valB, exist := setB[key]; !exist || valA.SecurityPolicyID != valB.SecurityPolicyID {
if valB, exist := setB[key]; !exist || !valA.Equals(valB) {
result = append(result, valA)
}
}
Expand Down
72 changes: 66 additions & 6 deletions twingate/internal/provider/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/client"
"github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/model"
"github.com/Twingate/terraform-provider-twingate/v3/twingate/internal/utils"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
tfattr "github.com/hashicorp/terraform-plugin-framework/attr"
Expand Down Expand Up @@ -289,6 +290,17 @@ func groupAccessBlock() schema.SetNestedBlock {
UseNullPolicyForGroupAccessWhenValueOmitted(),
},
},
attr.UsageBasedAutolockDurationDays: schema.Int64Attribute{
Optional: true,
Computed: true,
Description: "The usage-based auto-lock duration configured on the edge (in days).",
Validators: []validator.Int64{
int64validator.AlsoRequires(path.MatchRelative().AtParent().AtName(attr.GroupID)),
},
PlanModifiers: []planmodifier.Int64{
UseNullIntWhenValueOmitted(),
},
},
},
},
}
Expand Down Expand Up @@ -450,7 +462,11 @@ func convertResourceAccess(serviceAccounts []string, groupsAccess []model.Access
}

for _, group := range groupsAccess {
access = append(access, client.AccessInput{PrincipalID: group.GroupID, SecurityPolicyID: group.SecurityPolicyID})
access = append(access, client.AccessInput{
PrincipalID: group.GroupID,
SecurityPolicyID: group.SecurityPolicyID,
UsageBasedAutolockDurationDays: group.UsageBasedDuration,
})
}

return access
Expand All @@ -474,6 +490,7 @@ func getAccessAttribute(list types.List, attribute string) []string {
return convertIDs(val.(types.Set))
}

//nolint:cyclop
func getGroupAccessAttribute(list types.Set) []model.AccessGroup {
if list.IsNull() || list.IsUnknown() || len(list.Elements()) == 0 {
return nil
Expand All @@ -497,6 +514,11 @@ func getGroupAccessAttribute(list types.Set) []model.AccessGroup {
accessGroup.SecurityPolicyID = securityPolicyVal.(types.String).ValueStringPointer()
}

usageBasedDuration := obj.Attributes()[attr.UsageBasedAutolockDurationDays]
if usageBasedDuration != nil && !usageBasedDuration.IsNull() && !usageBasedDuration.IsUnknown() {
accessGroup.UsageBasedDuration = usageBasedDuration.(types.Int64).ValueInt64Pointer()
}

access = append(access, accessGroup)
}

Expand Down Expand Up @@ -538,7 +560,7 @@ func convertResource(plan *resourceModel) (*model.Resource, error) {
serviceAccountIDs := getServiceAccountAccessAttribute(plan.ServiceAccess)

for _, access := range accessGroups {
if access.SecurityPolicyID == nil && len(strings.TrimSpace(access.GroupID)) == 0 {
if access.SecurityPolicyID == nil && access.UsageBasedDuration == nil && len(strings.TrimSpace(access.GroupID)) == 0 {
return nil, ErrInvalidAttributeCombination
}

Expand Down Expand Up @@ -1219,8 +1241,9 @@ func convertGroupsAccessToTerraform(ctx context.Context, groupAccess []model.Acc

for _, access := range groupAccess {
attributes := map[string]tfattr.Value{
attr.GroupID: types.StringValue(access.GroupID),
attr.SecurityPolicyID: types.StringPointerValue(access.SecurityPolicyID),
attr.GroupID: types.StringValue(access.GroupID),
attr.SecurityPolicyID: types.StringPointerValue(access.SecurityPolicyID),
attr.UsageBasedAutolockDurationDays: types.Int64PointerValue(access.UsageBasedDuration),
}

obj, diags := types.ObjectValue(accessGroupAttributeTypes(), attributes)
Expand Down Expand Up @@ -1329,8 +1352,9 @@ func (m useDefaultPolicyForUnknownModifier) PlanModifyString(ctx context.Context

func accessGroupAttributeTypes() map[string]tfattr.Type {
return map[string]tfattr.Type{
attr.GroupID: types.StringType,
attr.SecurityPolicyID: types.StringType,
attr.GroupID: types.StringType,
attr.SecurityPolicyID: types.StringType,
attr.UsageBasedAutolockDurationDays: types.Int64Type,
}
}

Expand Down Expand Up @@ -1375,3 +1399,39 @@ func (m useNullPolicyForGroupAccessWhenValueOmitted) PlanModifyString(ctx contex
resp.PlanValue = types.StringNull()
}
}

func UseNullIntWhenValueOmitted() planmodifier.Int64 {
return useNullIntWhenValueOmitted{}
}

type useNullIntWhenValueOmitted struct{}

func (m useNullIntWhenValueOmitted) Description(_ context.Context) string {
return ""
}

func (m useNullIntWhenValueOmitted) MarkdownDescription(_ context.Context) string {
return ""
}

func (m useNullIntWhenValueOmitted) PlanModifyInt64(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) {
if req.StateValue.IsNull() && req.ConfigValue.IsNull() {
resp.PlanValue = types.Int64Null()

return
}

// Do nothing if there is no state value.
if req.StateValue.IsNull() {
return
}

// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
if req.ConfigValue.IsUnknown() {
return
}

if req.ConfigValue.IsNull() && !req.PlanValue.IsNull() {
resp.PlanValue = types.Int64Null()
}
}
64 changes: 64 additions & 0 deletions twingate/internal/test/acctests/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ 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")
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")
ErrEmptyGroupAccess = errors.New("expected at least one group in GroupAccess")
)

Expand Down Expand Up @@ -346,6 +348,39 @@ func CheckTwingateResourceSecurityPolicyOnGroupAccess(resourceName string, expec
}
}

func CheckTwingateResourceUsageBasedOnGroupAccess(resourceName string, expectedUsageBased int64) 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 len(res.GroupsAccess) == 0 {
return ErrEmptyGroupAccess
}

if res.GroupsAccess[0].UsageBasedDuration == nil {
return ErrNullUsageBased
}

if *res.GroupsAccess[0].UsageBasedDuration != expectedUsageBased {
return fmt.Errorf("expected usage based duration %v, got %v", expectedUsageBased, *res.GroupsAccess[0].UsageBasedDuration) //nolint:goerr113
}

return nil
}
}

func CheckTwingateResourceSecurityPolicyIsNullOnGroupAccess(resourceName string) sdk.TestCheckFunc {
return func(s *terraform.State) error {
resourceState, ok := s.RootModule().Resources[resourceName]
Expand Down Expand Up @@ -375,6 +410,35 @@ func CheckTwingateResourceSecurityPolicyIsNullOnGroupAccess(resourceName string)
}
}

func CheckTwingateResourceUsageBasedIsNullOnGroupAccess(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 len(res.GroupsAccess) == 0 {
return ErrEmptyGroupAccess
}

if res.GroupsAccess[0].UsageBasedDuration != nil {
return ErrNotNullUsageBased
}

return nil
}
}

func CheckTwingateResourceActiveState(resourceName string, expectedActiveState bool) sdk.TestCheckFunc {
return func(s *terraform.State) error {
resourceState, ok := s.RootModule().Resources[resourceName]
Expand Down
Loading

0 comments on commit 40ba580

Please sign in to comment.