diff --git a/docs/data-sources/users.md b/docs/data-sources/users.md index 16c07f10..40352a81 100644 --- a/docs/data-sources/users.md +++ b/docs/data-sources/users.md @@ -13,12 +13,57 @@ Users in Twingate can be given access to Twingate Resources and may either be ad ## Example Usage ```terraform -data "twingate_users" "all" {} +data "twingate_users" "all" { + # email = "" + # email_regexp = "" + # email_contains = "" + # email_exclude = "" + # email_prefix = "" + # email_suffix = "" + + # first_name = "" + # first_name_regexp = "" + # first_name_contains = "" + # first_name_exclude = "" + # first_name_prefix = "" + # first_name_suffix = "" + + # last_name = "" + # last_name_regexp = "" + # last_name_contains = "" + # last_name_exclude = "" + # last_name_prefix = "" + # last_name_suffix = "" + + # roles = ["ADMIN", "DEVOPS", "SUPPORT", "MEMBER"] +} ``` ## Schema +### Optional + +- `email` (String) Returns only users that exactly match this email. +- `email_contains` (String) Match when the value exist in the email of the user. +- `email_exclude` (String) Match when the value does not exist in the email of the user. +- `email_prefix` (String) The email of the user must start with the value. +- `email_regexp` (String) The regular expression match of the email of the user. +- `email_suffix` (String) The email of the user must end with the value. +- `first_name` (String) Returns only users that exactly match the first name. +- `first_name_contains` (String) Match when the value exist in the first name of the user. +- `first_name_exclude` (String) Match when the value does not exist in the first name of the user. +- `first_name_prefix` (String) The first name of the user must start with the value. +- `first_name_regexp` (String) The regular expression match of the first name of the user. +- `first_name_suffix` (String) The first name of the user must end with the value. +- `last_name` (String) Returns only users that exactly match the last name. +- `last_name_contains` (String) Match when the value exist in the last name of the user. +- `last_name_exclude` (String) Match when the value does not exist in the last name of the user. +- `last_name_prefix` (String) The last name of the user must start with the value. +- `last_name_regexp` (String) The regular expression match of the last name of the user. +- `last_name_suffix` (String) The last name of the user must end with the value. +- `roles` (Set of String) Returns users that match a list of roles. Valid roles: `ADMIN`, `DEVOPS`, `SUPPORT`, `MEMBER`. + ### Read-Only - `id` (String) The ID of this resource. diff --git a/examples/data-sources/twingate_users/data-source.tf b/examples/data-sources/twingate_users/data-source.tf index d3b1e5a5..fa707720 100644 --- a/examples/data-sources/twingate_users/data-source.tf +++ b/examples/data-sources/twingate_users/data-source.tf @@ -1 +1,24 @@ -data "twingate_users" "all" {} +data "twingate_users" "all" { + # email = "" + # email_regexp = "" + # email_contains = "" + # email_exclude = "" + # email_prefix = "" + # email_suffix = "" + + # first_name = "" + # first_name_regexp = "" + # first_name_contains = "" + # first_name_exclude = "" + # first_name_prefix = "" + # first_name_suffix = "" + + # last_name = "" + # last_name_regexp = "" + # last_name_contains = "" + # last_name_exclude = "" + # last_name_prefix = "" + # last_name_suffix = "" + + # roles = ["ADMIN", "DEVOPS", "SUPPORT", "MEMBER"] +} diff --git a/twingate/internal/attr/user.go b/twingate/internal/attr/user.go index 53975ca7..1259f27d 100644 --- a/twingate/internal/attr/user.go +++ b/twingate/internal/attr/user.go @@ -5,6 +5,7 @@ const ( LastName = "last_name" Email = "email" Role = "role" + Roles = "roles" Users = "users" SendInvite = "send_invite" State = "state" diff --git a/twingate/internal/client/query/groups-read.go b/twingate/internal/client/query/groups-read.go index f137da01..31f42aab 100644 --- a/twingate/internal/client/query/groups-read.go +++ b/twingate/internal/client/query/groups-read.go @@ -37,12 +37,13 @@ type GroupFilterInput struct { } type StringFilterOperationInput struct { - Eq *string `json:"eq"` - Ne *string `json:"ne"` - StartsWith *string `json:"startsWith"` - EndsWith *string `json:"endsWith"` - Regexp *string `json:"regexp"` - Contains *string `json:"contains"` + Eq *string `json:"eq"` + Ne *string `json:"ne"` + StartsWith *string `json:"startsWith"` + EndsWith *string `json:"endsWith"` + Regexp *string `json:"regexp"` + Contains *string `json:"contains"` + In []string `json:"in"` } func NewStringFilterOperationInput(name, filter string) *StringFilterOperationInput { diff --git a/twingate/internal/client/query/users-read.go b/twingate/internal/client/query/users-read.go index f4b79336..1d60e6d4 100644 --- a/twingate/internal/client/query/users-read.go +++ b/twingate/internal/client/query/users-read.go @@ -8,7 +8,7 @@ import ( const CursorUsers = "usersEndCursor" type ReadUsers struct { - Users `graphql:"users(after: $usersEndCursor, first: $pageLimit)"` + Users `graphql:"users(filter: $filter, after: $usersEndCursor, first: $pageLimit)"` } func (q ReadUsers) IsEmpty() bool { @@ -28,3 +28,14 @@ func (u Users) ToModel() []*model.User { return edge.Node.ToModel() }) } + +type UserFilterInput struct { + FirstName *StringFilterOperationInput `json:"firstName"` + LastName *StringFilterOperationInput `json:"lastName"` + Email *StringFilterOperationInput `json:"email"` + Role *UserRoleFilterOperationInput `json:"role"` +} + +type UserRoleFilterOperationInput struct { + In []UserRole `json:"in"` +} diff --git a/twingate/internal/client/user.go b/twingate/internal/client/user.go index 49e0b309..5fa91d82 100644 --- a/twingate/internal/client/user.go +++ b/twingate/internal/client/user.go @@ -6,12 +6,56 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/client/query" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" ) -func (client *Client) ReadUsers(ctx context.Context) ([]*model.User, error) { +type StringFilter struct { + Name string + Filter string +} + +type UsersFilter struct { + Email *StringFilter + FirstName *StringFilter + LastName *StringFilter + Roles []string +} + +func NewUserFilterInput(filter *UsersFilter) *query.UserFilterInput { + if filter == nil { + return nil + } + + queryFilter := &query.UserFilterInput{} + + if filter.FirstName != nil { + queryFilter.FirstName = query.NewStringFilterOperationInput(filter.FirstName.Name, filter.FirstName.Filter) + } + + if filter.LastName != nil { + queryFilter.LastName = query.NewStringFilterOperationInput(filter.LastName.Name, filter.LastName.Filter) + } + + if filter.Email != nil { + queryFilter.Email = query.NewStringFilterOperationInput(filter.Email.Name, filter.Email.Filter) + } + + if len(filter.Roles) > 0 { + queryFilter.Role = &query.UserRoleFilterOperationInput{ + In: utils.Map(filter.Roles, func(item string) query.UserRole { + return query.UserRole(item) + }), + } + } + + return queryFilter +} + +func (client *Client) ReadUsers(ctx context.Context, filter *UsersFilter) ([]*model.User, error) { opr := resourceUser.read() variables := newVars( + gqlNullable(NewUserFilterInput(filter), "filter"), cursor(query.CursorUsers), pageLimit(client.pageLimit), ) diff --git a/twingate/internal/provider/datasource/connectors.go b/twingate/internal/provider/datasource/connectors.go index 1d3e23a6..960ecd7e 100644 --- a/twingate/internal/provider/datasource/connectors.go +++ b/twingate/internal/provider/datasource/connectors.go @@ -121,7 +121,6 @@ func (d *connectors) Schema(ctx context.Context, req datasource.SchemaRequest, r } } -//nolint:cyclop func (d *connectors) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var data connectorsModel @@ -132,36 +131,7 @@ func (d *connectors) Read(ctx context.Context, req datasource.ReadRequest, resp return } - var name, filter string - - if data.Name.ValueString() != "" { - name = data.Name.ValueString() - } - - if data.NameRegexp.ValueString() != "" { - name = data.NameRegexp.ValueString() - filter = attr.FilterByRegexp - } - - if data.NameContains.ValueString() != "" { - name = data.NameContains.ValueString() - filter = attr.FilterByContains - } - - if data.NameExclude.ValueString() != "" { - name = data.NameExclude.ValueString() - filter = attr.FilterByExclude - } - - if data.NamePrefix.ValueString() != "" { - name = data.NamePrefix.ValueString() - filter = attr.FilterByPrefix - } - - if data.NameSuffix.ValueString() != "" { - name = data.NameSuffix.ValueString() - filter = attr.FilterBySuffix - } + name, filter := getNameFilter(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 { addErr(&resp.Diagnostics, ErrConnectorsDatasourceShouldSetOneOptionalNameAttribute, TwingateResources) diff --git a/twingate/internal/provider/datasource/helper.go b/twingate/internal/provider/datasource/helper.go index 6dc0d13c..a5203ff6 100644 --- a/twingate/internal/provider/datasource/helper.go +++ b/twingate/internal/provider/datasource/helper.go @@ -3,6 +3,7 @@ package datasource import ( "fmt" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -29,3 +30,38 @@ func countOptionalAttributes(attributes ...types.String) int { return count } + +func getNameFilter(name, nameRegexp, nameContains, nameExclude, namePrefix, nameSuffix types.String) (string, string) { + var value, filter string + + if name.ValueString() != "" { + value = name.ValueString() + } + + if nameRegexp.ValueString() != "" { + value = nameRegexp.ValueString() + filter = attr.FilterByRegexp + } + + if nameContains.ValueString() != "" { + value = nameContains.ValueString() + filter = attr.FilterByContains + } + + if nameExclude.ValueString() != "" { + value = nameExclude.ValueString() + filter = attr.FilterByExclude + } + + if namePrefix.ValueString() != "" { + value = namePrefix.ValueString() + filter = attr.FilterByPrefix + } + + if nameSuffix.ValueString() != "" { + value = nameSuffix.ValueString() + filter = attr.FilterBySuffix + } + + return value, filter +} diff --git a/twingate/internal/provider/datasource/remote-networks.go b/twingate/internal/provider/datasource/remote-networks.go index 4efa3343..ff80f7be 100644 --- a/twingate/internal/provider/datasource/remote-networks.go +++ b/twingate/internal/provider/datasource/remote-networks.go @@ -118,7 +118,6 @@ func (d *remoteNetworks) Schema(ctx context.Context, req datasource.SchemaReques } } -//nolint:cyclop func (d *remoteNetworks) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var data remoteNetworksModel @@ -129,36 +128,7 @@ func (d *remoteNetworks) Read(ctx context.Context, req datasource.ReadRequest, r return } - var name, filter string - - if data.Name.ValueString() != "" { - name = data.Name.ValueString() - } - - if data.NameRegexp.ValueString() != "" { - name = data.NameRegexp.ValueString() - filter = attr.FilterByRegexp - } - - if data.NameContains.ValueString() != "" { - name = data.NameContains.ValueString() - filter = attr.FilterByContains - } - - if data.NameExclude.ValueString() != "" { - name = data.NameExclude.ValueString() - filter = attr.FilterByExclude - } - - if data.NamePrefix.ValueString() != "" { - name = data.NamePrefix.ValueString() - filter = attr.FilterByPrefix - } - - if data.NameSuffix.ValueString() != "" { - name = data.NameSuffix.ValueString() - filter = attr.FilterBySuffix - } + name, filter := getNameFilter(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 { addErr(&resp.Diagnostics, ErrRemoteNetworksDatasourceShouldSetOneOptionalNameAttribute, TwingateRemoteNetworks) diff --git a/twingate/internal/provider/datasource/resources.go b/twingate/internal/provider/datasource/resources.go index 27a24330..a6015004 100644 --- a/twingate/internal/provider/datasource/resources.go +++ b/twingate/internal/provider/datasource/resources.go @@ -150,7 +150,6 @@ func (d *resources) Schema(ctx context.Context, req datasource.SchemaRequest, re } } -//nolint:cyclop func (d *resources) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var data resourcesModel @@ -161,36 +160,7 @@ func (d *resources) Read(ctx context.Context, req datasource.ReadRequest, resp * return } - var name, filter string - - if data.Name.ValueString() != "" { - name = data.Name.ValueString() - } - - if data.NameRegexp.ValueString() != "" { - name = data.NameRegexp.ValueString() - filter = attr.FilterByRegexp - } - - if data.NameContains.ValueString() != "" { - name = data.NameContains.ValueString() - filter = attr.FilterByContains - } - - if data.NameExclude.ValueString() != "" { - name = data.NameExclude.ValueString() - filter = attr.FilterByExclude - } - - if data.NamePrefix.ValueString() != "" { - name = data.NamePrefix.ValueString() - filter = attr.FilterByPrefix - } - - if data.NameSuffix.ValueString() != "" { - name = data.NameSuffix.ValueString() - filter = attr.FilterBySuffix - } + name, filter := getNameFilter(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 { addErr(&resp.Diagnostics, ErrResourcesDatasourceShouldSetOneOptionalNameAttribute, TwingateResources) diff --git a/twingate/internal/provider/datasource/security-policies.go b/twingate/internal/provider/datasource/security-policies.go index 61ebff60..089266a1 100644 --- a/twingate/internal/provider/datasource/security-policies.go +++ b/twingate/internal/provider/datasource/security-policies.go @@ -109,7 +109,6 @@ func (d *securityPolicies) Schema(ctx context.Context, req datasource.SchemaRequ } } -//nolint:cyclop func (d *securityPolicies) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var data securityPoliciesModel @@ -120,36 +119,7 @@ func (d *securityPolicies) Read(ctx context.Context, req datasource.ReadRequest, return } - var name, filter string - - if data.Name.ValueString() != "" { - name = data.Name.ValueString() - } - - if data.NameRegexp.ValueString() != "" { - name = data.NameRegexp.ValueString() - filter = attr.FilterByRegexp - } - - if data.NameContains.ValueString() != "" { - name = data.NameContains.ValueString() - filter = attr.FilterByContains - } - - if data.NameExclude.ValueString() != "" { - name = data.NameExclude.ValueString() - filter = attr.FilterByExclude - } - - if data.NamePrefix.ValueString() != "" { - name = data.NamePrefix.ValueString() - filter = attr.FilterByPrefix - } - - if data.NameSuffix.ValueString() != "" { - name = data.NameSuffix.ValueString() - filter = attr.FilterBySuffix - } + name, filter := getNameFilter(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 { addErr(&resp.Diagnostics, ErrSecurityPoliciesDatasourceShouldSetOneOptionalNameAttribute, TwingateSecurityPolicies) diff --git a/twingate/internal/provider/datasource/service-accounts.go b/twingate/internal/provider/datasource/service-accounts.go index 0528fdef..08a55145 100644 --- a/twingate/internal/provider/datasource/service-accounts.go +++ b/twingate/internal/provider/datasource/service-accounts.go @@ -127,7 +127,6 @@ func (d *serviceAccounts) Schema(ctx context.Context, req datasource.SchemaReque } } -//nolint:cyclop func (d *serviceAccounts) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var data serviceAccountsModel @@ -138,36 +137,7 @@ func (d *serviceAccounts) Read(ctx context.Context, req datasource.ReadRequest, return } - var name, filter string - - if data.Name.ValueString() != "" { - name = data.Name.ValueString() - } - - if data.NameRegexp.ValueString() != "" { - name = data.NameRegexp.ValueString() - filter = attr.FilterByRegexp - } - - if data.NameContains.ValueString() != "" { - name = data.NameContains.ValueString() - filter = attr.FilterByContains - } - - if data.NameExclude.ValueString() != "" { - name = data.NameExclude.ValueString() - filter = attr.FilterByExclude - } - - if data.NamePrefix.ValueString() != "" { - name = data.NamePrefix.ValueString() - filter = attr.FilterByPrefix - } - - if data.NameSuffix.ValueString() != "" { - name = data.NameSuffix.ValueString() - filter = attr.FilterBySuffix - } + name, filter := getNameFilter(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 { addErr(&resp.Diagnostics, ErrServiceAccountsDatasourceShouldSetOneOptionalNameAttribute, TwingateResources) diff --git a/twingate/internal/provider/datasource/users.go b/twingate/internal/provider/datasource/users.go index 3b9aa08d..e19f9c1e 100644 --- a/twingate/internal/provider/datasource/users.go +++ b/twingate/internal/provider/datasource/users.go @@ -9,11 +9,21 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + tfattr "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) +var ( + ErrUsersDatasourceShouldSetOneOptionalEmailAttribute = errors.New("Only one of email, email_regex, email_contains, email_exclude, email_prefix or email_suffix must be set.") + ErrUsersDatasourceShouldSetOneOptionalFirstNameAttribute = errors.New("Only one of first_name, first_name_regex, first_name_contains, first_name_exclude, first_name_prefix or first_name_suffix must be set.") + ErrUsersDatasourceShouldSetOneOptionalLastNameAttribute = errors.New("Only one of last_name, last_name_regex, last_name_contains, last_name_exclude, last_name_prefix or last_name_suffix must be set.") +) + // Ensure the implementation satisfies the desired interfaces. var _ datasource.DataSource = &users{} @@ -26,8 +36,27 @@ type users struct { } type usersModel struct { - ID types.String `tfsdk:"id"` - Users []userModel `tfsdk:"users"` + ID types.String `tfsdk:"id"` + Email types.String `tfsdk:"email"` + EmailRegexp types.String `tfsdk:"email_regexp"` + EmailContains types.String `tfsdk:"email_contains"` + EmailExclude types.String `tfsdk:"email_exclude"` + EmailPrefix types.String `tfsdk:"email_prefix"` + EmailSuffix types.String `tfsdk:"email_suffix"` + FirstName types.String `tfsdk:"first_name"` + FirstNameRegexp types.String `tfsdk:"first_name_regexp"` + FirstNameContains types.String `tfsdk:"first_name_contains"` + FirstNameExclude types.String `tfsdk:"first_name_exclude"` + FirstNamePrefix types.String `tfsdk:"first_name_prefix"` + FirstNameSuffix types.String `tfsdk:"first_name_suffix"` + LastName types.String `tfsdk:"last_name"` + LastNameRegexp types.String `tfsdk:"last_name_regexp"` + LastNameContains types.String `tfsdk:"last_name_contains"` + LastNameExclude types.String `tfsdk:"last_name_exclude"` + LastNamePrefix types.String `tfsdk:"last_name_prefix"` + LastNameSuffix types.String `tfsdk:"last_name_suffix"` + Roles types.Set `tfsdk:"roles"` + Users []userModel `tfsdk:"users"` } func (d *users) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -52,6 +81,7 @@ func (d *users) Configure(ctx context.Context, req datasource.ConfigureRequest, d.client = client } +//nolint:funlen func (d *users) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Description: userDescription, @@ -61,6 +91,96 @@ func (d *users) Schema(ctx context.Context, req datasource.SchemaRequest, resp * Description: computedDatasourceIDDescription, }, + // email + + attr.Email: schema.StringAttribute{ + Optional: true, + Description: "Returns only users that exactly match this email.", + }, + attr.Email + attr.FilterByRegexp: schema.StringAttribute{ + Optional: true, + Description: "The regular expression match of the email of the user.", + }, + attr.Email + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the email of the user.", + }, + attr.Email + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the email of the user.", + }, + attr.Email + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The email of the user must start with the value.", + }, + attr.Email + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The email of the user must end with the value.", + }, + + // first name + + attr.FirstName: schema.StringAttribute{ + Optional: true, + Description: "Returns only users that exactly match the first name.", + }, + attr.FirstName + attr.FilterByRegexp: schema.StringAttribute{ + Optional: true, + Description: "The regular expression match of the first name of the user.", + }, + attr.FirstName + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the first name of the user.", + }, + attr.FirstName + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the first name of the user.", + }, + attr.FirstName + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The first name of the user must start with the value.", + }, + attr.FirstName + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The first name of the user must end with the value.", + }, + + // last name + + attr.LastName: schema.StringAttribute{ + Optional: true, + Description: "Returns only users that exactly match the last name.", + }, + attr.LastName + attr.FilterByRegexp: schema.StringAttribute{ + Optional: true, + Description: "The regular expression match of the last name of the user.", + }, + attr.LastName + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the last name of the user.", + }, + attr.LastName + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the last name of the user.", + }, + attr.LastName + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The last name of the user must start with the value.", + }, + attr.LastName + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The last name of the user must end with the value.", + }, + + attr.Roles: schema.SetAttribute{ + Optional: true, + ElementType: types.StringType, + Description: "Returns users that match a list of roles. Valid roles: `ADMIN`, `DEVOPS`, `SUPPORT`, `MEMBER`.", + Validators: []validator.Set{ + setvalidator.ValueStringsAre(stringvalidator.OneOf(model.UserRoles...)), + }, + }, + attr.Users: schema.ListNestedAttribute{ Computed: true, NestedObject: schema.NestedAttributeObject{ @@ -96,18 +216,76 @@ func (d *users) Schema(ctx context.Context, req datasource.SchemaRequest, resp * } } -func (d *users) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - users, err := d.client.ReadUsers(ctx) +func (d *users) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint + var data usersModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // email + email, emailFilter := getNameFilter(data.Email, data.EmailRegexp, data.EmailContains, data.EmailExclude, data.EmailPrefix, data.EmailSuffix) + + if countOptionalAttributes(data.Email, data.EmailRegexp, data.EmailContains, data.EmailExclude, data.EmailPrefix, data.EmailSuffix) > 1 { + addErr(&resp.Diagnostics, ErrUsersDatasourceShouldSetOneOptionalEmailAttribute, TwingateResources) + + return + } + + // first name + firstName, firstNameFilter := getNameFilter(data.FirstName, data.FirstNameRegexp, data.FirstNameContains, data.FirstNameExclude, data.FirstNamePrefix, data.FirstNameSuffix) + + if countOptionalAttributes(data.FirstName, data.FirstNameRegexp, data.FirstNameContains, data.FirstNameExclude, data.FirstNamePrefix, data.FirstNameSuffix) > 1 { + addErr(&resp.Diagnostics, ErrUsersDatasourceShouldSetOneOptionalFirstNameAttribute, TwingateResources) + + return + } + + // last name + lastName, lastNameFilter := getNameFilter(data.LastName, data.LastNameRegexp, data.LastNameContains, data.LastNameExclude, data.LastNamePrefix, data.LastNameSuffix) + + if countOptionalAttributes(data.LastName, data.LastNameRegexp, data.LastNameContains, data.LastNameExclude, data.LastNamePrefix, data.LastNameSuffix) > 1 { + addErr(&resp.Diagnostics, ErrUsersDatasourceShouldSetOneOptionalLastNameAttribute, TwingateResources) + + return + } + + var filter *client.UsersFilter + + if email != "" || firstName != "" || lastName != "" || len(data.Roles.Elements()) > 0 { + filter = &client.UsersFilter{} + } + + if email != "" { + filter.Email = &client.StringFilter{Name: email, Filter: emailFilter} + } + + if firstName != "" { + filter.FirstName = &client.StringFilter{Name: firstName, Filter: firstNameFilter} + } + + if lastName != "" { + filter.LastName = &client.StringFilter{Name: lastName, Filter: lastNameFilter} + } + + if len(data.Roles.Elements()) > 0 { + filter.Roles = utils.Map(data.Roles.Elements(), func(item tfattr.Value) string { + return item.(types.String).ValueString() + }) + } + + users, err := d.client.ReadUsers(ctx, filter) if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) { addErr(&resp.Diagnostics, err, TwingateUsers) return } - data := usersModel{ - ID: types.StringValue("users-all"), - Users: convertUsersToTerraform(users), - } + data.ID = types.StringValue("users-all") + data.Users = convertUsersToTerraform(users) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/twingate/internal/test/acctests/datasource/resources_test.go b/twingate/internal/test/acctests/datasource/resources_test.go index 5f29f3f1..7b01de05 100644 --- a/twingate/internal/test/acctests/datasource/resources_test.go +++ b/twingate/internal/test/acctests/datasource/resources_test.go @@ -8,6 +8,7 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) @@ -221,7 +222,7 @@ func TestAccDatasourceTwingateResourcesFilterByContains(t *testing.T) { func TestAccDatasourceTwingateResourcesFilterByRegexp(t *testing.T) { t.Parallel() - prefix := test.Prefix() + prefix := acctest.RandString(6) resourceName := test.RandomResourceName() networkName := test.RandomName() theDatasource := "data.twingate_resources." + resourceName diff --git a/twingate/internal/test/acctests/datasource/users_test.go b/twingate/internal/test/acctests/datasource/users_test.go index 3d919756..6b09d9cc 100644 --- a/twingate/internal/test/acctests/datasource/users_test.go +++ b/twingate/internal/test/acctests/datasource/users_test.go @@ -3,10 +3,13 @@ package datasource import ( "errors" "fmt" + "strings" "testing" "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) @@ -62,3 +65,646 @@ func testCheckResourceAttrNotEqual(name, key, value string) resource.TestCheckFu return nil } } + +func join(configs ...string) string { + return strings.Join(configs, "\n") +} + +func TestAccDatasourceTwingateUsers_filterByEmail(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + prefix := test.TerraformRandName("email") + email := prefix + "_" + test.RandomEmail() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUser(resourceName, email), + terraformDatasourceUsersByEmail(datasourceName, "", email, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByEmailPrefix(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + prefix := test.TerraformRandName("email_prefix") + email := prefix + "_" + test.RandomEmail() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUser(resourceName, email), + terraformDatasourceUsersByEmail(datasourceName, attr.FilterByPrefix, prefix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByEmailSuffix(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + const suffix = "suf" + email := test.RandomEmail() + "." + suffix + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUser(resourceName, email), + terraformDatasourceUsersByEmail(datasourceName, attr.FilterBySuffix, suffix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByEmailContains(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + + val := acctest.RandString(6) + email := test.TerraformRandName(val) + "_" + test.RandomEmail() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUser(resourceName, email), + terraformDatasourceUsersByEmail(datasourceName, attr.FilterByContains, val, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByEmailRegexp(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + + prefix := acctest.RandString(6) + email := test.TerraformRandName(prefix) + "_email_by_regexp_" + test.RandomEmail() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUser(resourceName, email), + terraformDatasourceUsersByEmail(datasourceName, attr.FilterByRegexp, prefix+".*_regexp_.*", resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + ), + }, + }, + }) +} + +func terraformResourceTwingateUser(terraformResourceName, email string) string { + return fmt.Sprintf(` + resource "twingate_user" "%s" { + email = "%s" + send_invite = false + } + `, terraformResourceName, email) +} + +func terraformDatasourceUsersByEmail(datasourceName, filter, email, resourceName string) string { + return fmt.Sprintf(` + data "twingate_users" "%[1]s" { + email%[2]s = "%[3]s" + + depends_on = [%[4]s] + } +`, datasourceName, filter, email, acctests.TerraformUser(resourceName)) +} + +func TestAccDatasourceTwingateUsers_filterByFirstName(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + prefix := test.TerraformRandName("first_name") + email := prefix + "_" + test.RandomEmail() + firstName := prefix + "_" + test.RandomName() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstName(resourceName, email, firstName), + terraformDatasourceUsersByFirstName(datasourceName, "", firstName, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByFirstNamePrefix(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + prefix := test.TerraformRandName("first_name") + email := test.RandomEmail() + firstName := prefix + "_" + test.RandomName() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstName(resourceName, email, firstName), + terraformDatasourceUsersByFirstName(datasourceName, attr.FilterByPrefix, prefix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByFirstNameSuffix(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + suffix := acctest.RandString(5) + email := test.RandomEmail() + firstName := test.RandomName() + "_" + suffix + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstName(resourceName, email, firstName), + terraformDatasourceUsersByFirstName(datasourceName, attr.FilterBySuffix, suffix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByFirstNameContains(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + val := acctest.RandString(6) + suffix := acctest.RandString(5) + email := test.RandomEmail() + firstName := fmt.Sprintf("%s_%s_%s", test.RandomName(), val, suffix) + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstName(resourceName, email, firstName), + terraformDatasourceUsersByFirstName(datasourceName, attr.FilterByContains, val, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByFirstNameRegexp(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + val := acctest.RandString(6) + suffix := acctest.RandString(5) + email := test.RandomEmail() + firstName := fmt.Sprintf("%s_%s_%s", test.RandomName(), val, suffix) + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstName(resourceName, email, firstName), + terraformDatasourceUsersByFirstName(datasourceName, attr.FilterByRegexp, fmt.Sprintf(".*_%s_.*", val), resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + ), + }, + }, + }) +} + +func terraformDatasourceUsersByFirstName(datasourceName, filter, name, resourceName string) string { + return fmt.Sprintf(` + data "twingate_users" "%[1]s" { + first_name%[2]s = "%[3]s" + + depends_on = [%[4]s] + } +`, datasourceName, filter, name, acctests.TerraformUser(resourceName)) +} + +func TestAccDatasourceTwingateUsers_filterByEmailAndFirstName(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + prefix := test.TerraformRandName("orange") + email := prefix + "_" + test.RandomEmail() + firstName := prefix + "_" + test.RandomName() + const theDatasource = "data.twingate_users.filter_by_email_and_first_name_prefix" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstName(resourceName, email, firstName), + terraformDatasourceUsersByEmailAndFirstNamePrefix(prefix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + ), + }, + }, + }) +} + +func terraformResourceTwingateUserWithFirstName(resourceName, email, firstName string) string { + return fmt.Sprintf(` + resource "twingate_user" "%s" { + email = "%s" + first_name = "%s" + send_invite = false + } + `, resourceName, email, firstName) +} + +func terraformDatasourceUsersByEmailAndFirstNamePrefix(prefix, resourceName string) string { + return fmt.Sprintf(` + data "twingate_users" "filter_by_email_and_first_name_prefix" { + email_prefix = "%[1]s" + first_name_prefix = "%[1]s" + + depends_on = [twingate_user.%[2]s] + } +`, prefix, resourceName) +} + +func TestAccDatasourceTwingateUsers_filterByLastName(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + email := test.RandomEmail() + prefix := test.TerraformRandName("last_name") + lastName := prefix + "_" + test.RandomName() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithLastName(resourceName, email, lastName), + terraformDatasourceUsersByLastName(datasourceName, "", lastName, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByLastNamePrefix(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + email := test.RandomEmail() + prefix := test.TerraformRandName("last_name") + lastName := prefix + "_" + test.RandomName() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithLastName(resourceName, email, lastName), + terraformDatasourceUsersByLastName(datasourceName, attr.FilterByPrefix, prefix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByLastNameSuffix(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + email := test.RandomEmail() + suffix := acctest.RandString(5) + lastName := test.RandomName() + "_" + suffix + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithLastName(resourceName, email, lastName), + terraformDatasourceUsersByLastName(datasourceName, attr.FilterBySuffix, suffix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByLastNameContains(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + email := test.RandomEmail() + val := acctest.RandString(6) + suffix := acctest.RandString(5) + lastName := fmt.Sprintf("%s_%s_%s", test.RandomName(), val, suffix) + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithLastName(resourceName, email, lastName), + terraformDatasourceUsersByLastName(datasourceName, attr.FilterByContains, val, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByLastNameRegexp(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + email := test.RandomEmail() + val := acctest.RandString(6) + suffix := acctest.RandString(5) + lastName := fmt.Sprintf("%s_%s_%s", test.RandomName(), val, suffix) + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithLastName(resourceName, email, lastName), + terraformDatasourceUsersByLastName(datasourceName, attr.FilterByRegexp, fmt.Sprintf(".*_%s_.*", val), resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + ), + }, + }, + }) +} + +func terraformResourceTwingateUserWithLastName(resourceName, email, lastName string) string { + return fmt.Sprintf(` + resource "twingate_user" "%s" { + email = "%s" + last_name = "%s" + send_invite = false + } + `, resourceName, email, lastName) +} + +func terraformDatasourceUsersByLastName(datasourceName, filter, name, resourceName string) string { + return fmt.Sprintf(` + data "twingate_users" "%[1]s" { + last_name%[2]s = "%[3]s" + + depends_on = [%[4]s] + } +`, datasourceName, filter, name, acctests.TerraformUser(resourceName)) +} + +func TestAccDatasourceTwingateUsers_filterByEmailFirstNameAndLastName(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + prefix := test.TerraformRandName("yellow") + email := prefix + "_" + test.RandomEmail() + firstName := prefix + "_" + test.RandomName() + lastName := prefix + "_" + test.RandomName() + const theDatasource = "data.twingate_users.filter_by_email_and_first_name_and_last_name_prefix" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstNameAndLastName(resourceName, email, firstName, lastName), + terraformDatasourceUsersByEmailAndFirstNameAndLastNamePrefix(prefix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + ), + }, + }, + }) +} + +func terraformResourceTwingateUserWithFirstNameAndLastName(resourceName, email, firstName, lastName string) string { + return fmt.Sprintf(` + resource "twingate_user" "%s" { + email = "%s" + first_name = "%s" + last_name = "%s" + send_invite = false + } + `, resourceName, email, firstName, lastName) +} + +func terraformDatasourceUsersByEmailAndFirstNameAndLastNamePrefix(prefix, resourceName string) string { + return fmt.Sprintf(` + data "twingate_users" "filter_by_email_and_first_name_and_last_name_prefix" { + email_prefix = "%[1]s" + first_name_prefix = "%[1]s" + last_name_prefix = "%[1]s" + + depends_on = [twingate_user.%[2]s] + } +`, prefix, resourceName) +} + +func TestAccDatasourceTwingateUsers_filterByEmailFirstNameLastNameAndRole(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + prefix := test.TerraformRandName("tree") + email := prefix + "_" + test.RandomEmail() + firstName := prefix + "_" + test.RandomName() + lastName := prefix + "_" + test.RandomName() + const theDatasource = "data.twingate_users.filter_by_email_first-name_last-name_prefix_and_role" + const role = "DEVOPS" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstNameLastNameAndRole(resourceName, email, firstName, lastName, role), + terraformDatasourceUsersByEmailAndFirstNameLastNamePrefixAndRole(prefix, resourceName, role), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Role), role), + ), + }, + }, + }) +} + +func terraformResourceTwingateUserWithFirstNameLastNameAndRole(resourceName, email, firstName, lastName, role string) string { + return fmt.Sprintf(` + resource "twingate_user" "%s" { + email = "%s" + first_name = "%s" + last_name = "%s" + role = "%s" + send_invite = false + } + `, resourceName, email, firstName, lastName, role) +} + +func terraformDatasourceUsersByEmailAndFirstNameLastNamePrefixAndRole(prefix, resourceName, role string) string { + return fmt.Sprintf(` + data "twingate_users" "filter_by_email_first-name_last-name_prefix_and_role" { + email_prefix = "%[1]s" + first_name_prefix = "%[1]s" + last_name_prefix = "%[1]s" + roles = ["%[2]s"] + + depends_on = [twingate_user.%[3]s] + } +`, prefix, role, resourceName) +} diff --git a/twingate/internal/test/acctests/helper.go b/twingate/internal/test/acctests/helper.go index 2eade002..89aa7162 100644 --- a/twingate/internal/test/acctests/helper.go +++ b/twingate/internal/test/acctests/helper.go @@ -15,6 +15,7 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/datasource" "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/resource" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/hashicorp/terraform-plugin-framework/providerserver" @@ -175,6 +176,10 @@ func GetTwingateResourceID(resourceName string, resourceID **string) sdk.TestChe } } +func DatasourceName(resource, name string) string { + return fmt.Sprintf("data.%s.%s", resource, name) +} + func ResourceName(resource, name string) string { return fmt.Sprintf("%s.%s", resource, name) } @@ -211,6 +216,10 @@ func TerraformUser(name string) string { return ResourceName(resource.TwingateUser, name) } +func TerraformDatasourceUsers(name string) string { + return DatasourceName(datasource.TwingateUsers, name) +} + func DeleteTwingateResource(resourceName, resourceType string) sdk.TestCheckFunc { return func(s *terraform.State) error { resourceState, ok := s.RootModule().Resources[resourceName] @@ -774,7 +783,7 @@ func GetTestUsers() ([]*model.User, error) { return nil, ErrClientNotInited } - users, err := providerClient.ReadUsers(context.Background()) + users, err := providerClient.ReadUsers(context.Background(), nil) if err != nil { return nil, err //nolint } @@ -828,7 +837,7 @@ func GetTestUser() (*model.User, error) { return nil, ErrClientNotInited } - users, err := providerClient.ReadUsers(context.Background()) + users, err := providerClient.ReadUsers(context.Background(), nil) if err != nil { return nil, fmt.Errorf("failed to get test users: %w", err) } diff --git a/twingate/internal/test/client/users_test.go b/twingate/internal/test/client/users_test.go index ab6bb09f..0d2adc0c 100644 --- a/twingate/internal/test/client/users_test.go +++ b/twingate/internal/test/client/users_test.go @@ -79,7 +79,7 @@ func TestClientUsersReadOk(t *testing.T) { }), ) - users, err := client.ReadUsers(context.Background()) + users, err := client.ReadUsers(context.Background(), nil) assert.Nil(t, err) assert.Equal(t, expected, users) @@ -99,7 +99,7 @@ func TestClientUsersReadEmptyResult(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewStringResponder(200, jsonResponse)) - users, err := client.ReadUsers(context.Background()) + users, err := client.ReadUsers(context.Background(), nil) assert.Nil(t, err) assert.Nil(t, users) @@ -114,7 +114,7 @@ func TestClientUsersReadRequestError(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewErrorResponder(errBadRequest)) - users, err := client.ReadUsers(context.Background()) + users, err := client.ReadUsers(context.Background(), nil) assert.Nil(t, users) assert.EqualError(t, err, graphqlErr(client, "failed to read user with id All", errBadRequest)) @@ -169,7 +169,7 @@ func TestClientUsersReadNextPageEmptyResponse(t *testing.T) { }), ) - users, err := client.ReadUsers(context.Background()) + users, err := client.ReadUsers(context.Background(), nil) assert.Nil(t, users) assert.EqualError(t, err, "failed to read user with id All: query result is empty") @@ -219,7 +219,7 @@ func TestClientReadUsersAfterRequestError(t *testing.T) { ), ) - users, err := client.ReadUsers(context.Background()) + users, err := client.ReadUsers(context.Background(), nil) assert.Nil(t, users) assert.EqualError(t, err, graphqlErr(client, "failed to read user with id All", errBadRequest)) diff --git a/twingate/internal/test/sweepers/user_test.go b/twingate/internal/test/sweepers/user_test.go index 292d7211..9a49515a 100644 --- a/twingate/internal/test/sweepers/user_test.go +++ b/twingate/internal/test/sweepers/user_test.go @@ -14,7 +14,7 @@ func init() { Name: name, F: newTestSweeper(name, func(client *client.Client, ctx context.Context) ([]Resource, error) { - resources, err := client.ReadUsers(ctx) + resources, err := client.ReadUsers(ctx, nil) if err != nil { return nil, err }