From ff2e01a21ced8d7000ee7d721c77f4509423e14a Mon Sep 17 00:00:00 2001 From: taohe1012 <88763781+taohe1012@users.noreply.github.com> Date: Sat, 29 Jul 2023 15:14:53 +0800 Subject: [PATCH] PowerScale User Datasource (#5) --- docs/data-sources/user.md | 106 ++++ .../powerscale_access_zone/provider.tf | 4 +- .../powerscale_user/datasource.tf | 51 ++ .../data-sources/powerscale_user/provider.tf | 36 ++ go.mod | 1 + go.sum | 1 + powerscale/helper/user_helper.go | 91 ++++ powerscale/models/user.go | 83 ++++ powerscale/provider/provider.go | 1 + powerscale/provider/user_data_source.go | 466 ++++++++++++++++++ powerscale/provider/user_datasource_test.go | 215 ++++++++ 11 files changed, 1053 insertions(+), 2 deletions(-) create mode 100644 docs/data-sources/user.md create mode 100644 examples/data-sources/powerscale_user/datasource.tf create mode 100644 examples/data-sources/powerscale_user/provider.tf create mode 100644 powerscale/helper/user_helper.go create mode 100644 powerscale/models/user.go create mode 100644 powerscale/provider/user_data_source.go create mode 100644 powerscale/provider/user_datasource_test.go diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md new file mode 100644 index 00000000..c7af9208 --- /dev/null +++ b/docs/data-sources/user.md @@ -0,0 +1,106 @@ +--- +# Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. +# +# Licensed under the Mozilla Public License Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://mozilla.org/MPL/2.0/ +# +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +title: "powerscale_user data source" +linkTitle: "powerscale_user" +page_title: "powerscale_user Data Source - terraform-provider-powerscale" +subcategory: "" +description: |- + Data source for reading Users in PowerScale cluster. +--- + +# powerscale_user (Data Source) + +Data source for reading Users in PowerScale cluster. + + + + +## Schema + +### Optional + +- `filter` (Block, Optional) (see [below for nested schema](#nestedblock--filter)) + +### Read-Only + +- `id` (String) Unique identifier of the user instance. +- `users` (Attributes List) List of users. (see [below for nested schema](#nestedatt--users)) + + +### Nested Schema for `filter` + +Optional: + +- `cached` (Boolean) If true, only return cached objects. +- `domain` (String) Filter users by domain. +- `member_of` (Boolean) Enumerate all users that a group is a member of. +- `name_prefix` (String) Filter users by name prefix. +- `names` (Attributes List) List of user identity. (see [below for nested schema](#nestedatt--filter--names)) +- `provider` (String) Filter users by provider. +- `resolve_names` (Boolean) Resolve names of personas. +- `zone` (String) Filter users by zone. + + +### Nested Schema for `filter.names` + +Optional: + +- `name` (String) Specifies a user name. +- `uid` (Number) Specifies a numeric user identifier. + + + + +### Nested Schema for `users` + +Optional: + +- `dn` (String) Specifies a principal name for the user. +- `dns_domain` (String) Specifies the DNS domain. +- `domain` (String) Specifies the domain that the object is part of. +- `email` (String) Specifies an email address. +- `enabled` (Boolean) If true, the authenticated user is enabled. +- `expired` (Boolean) If true, the authenticated user has expired. +- `expiry` (Number) Specifies the Unix Epoch time at which the authenticated user will expire. +- `gecos` (String) Specifies the GECOS value, which is usually the full name. +- `generated_gid` (Boolean) If true, the GID was generated. +- `generated_uid` (Boolean) If true, the UID was generated. +- `generated_upn` (Boolean) If true, the UPN was generated. +- `gid` (String) Specifies a group identifier. +- `home_directory` (String) Specifies a home directory for the user. +- `id` (String) Specifies the user ID. +- `locked` (Boolean) If true, indicates that the account is locked. +- `max_password_age` (Number) Specifies the maximum time in seconds allowed before the password expires. +- `name` (String) Specifies a user name. +- `password_expired` (Boolean) If true, the password has expired. +- `password_expires` (Boolean) If true, the password is allowed to expire. +- `password_expiry` (Number) Specifies the time in Unix Epoch seconds that the password will expire. +- `password_last_set` (Number) Specifies the last time the password was set. +- `primary_group_sid` (String) Specifies the persona of the primary group. +- `prompt_password_change` (Boolean) If true, Prompts the user to change their password at the next login. +- `provider` (String) Specifies the authentication provider that the object belongs to. +- `sam_account_name` (String) Specifies a user name. +- `shell` (String) Specifies a path to the shell for the user. +- `sid` (String) Specifies a security identifier. +- `type` (String) Specifies the object type. +- `uid` (String) Specifies a user identifier. +- `upn` (String) Specifies a principal name for the user. +- `user_can_change_password` (Boolean) Specifies whether the password for the user can be changed. + +Read-Only: + +- `roles` (List of String) List of roles. \ No newline at end of file diff --git a/examples/data-sources/powerscale_access_zone/provider.tf b/examples/data-sources/powerscale_access_zone/provider.tf index 3a44c97f..75c69ec0 100644 --- a/examples/data-sources/powerscale_access_zone/provider.tf +++ b/examples/data-sources/powerscale_access_zone/provider.tf @@ -28,8 +28,8 @@ provider "powerscale" { endpoint = var.endpoint insecure = var.insecure group = var.group - volumes_path = var.volumes_path - volumes_path_permissions = var.volumes_path_permissions + volume_path = var.volume_path + volume_path_permissions = var.volume_path_permissions ignore_unresolvable_hosts = var.ignore_unresolvable_hosts auth_type = var.auth_type verbose_logging = var.verbose_logging diff --git a/examples/data-sources/powerscale_user/datasource.tf b/examples/data-sources/powerscale_user/datasource.tf new file mode 100644 index 00000000..688ba3c9 --- /dev/null +++ b/examples/data-sources/powerscale_user/datasource.tf @@ -0,0 +1,51 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +data "powerscale_user" "test_user" { + filter { + names = [ + # { + # uid = 0 + # }, + # { + # name = "admin" + # }, + { + name = "tfaccUserDatasource" + uid = 10000 + } + ] + cached = false + name_prefix = "tfacc" + resolve_names = false + member_of = false + # domain = "testDomain" + # zone = "testZone" + # provider = "testProvider" + } +} + +output "powerscale_user_filter" { + value = data.powerscale_user.test_user +} + +data "powerscale_user" "test_all_user" { +} + +output "powerscale_user_all" { + value = data.powerscale_user.test_all_user +} \ No newline at end of file diff --git a/examples/data-sources/powerscale_user/provider.tf b/examples/data-sources/powerscale_user/provider.tf new file mode 100644 index 00000000..75c69ec0 --- /dev/null +++ b/examples/data-sources/powerscale_user/provider.tf @@ -0,0 +1,36 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +terraform { + required_providers { + powerscale = { + source = "registry.terraform.io/dell/powerscale" + } + } +} + +provider "powerscale" { + username = var.username + password = var.password + endpoint = var.endpoint + insecure = var.insecure + group = var.group + volume_path = var.volume_path + volume_path_permissions = var.volume_path_permissions + ignore_unresolvable_hosts = var.ignore_unresolvable_hosts + auth_type = var.auth_type + verbose_logging = var.verbose_logging +} \ No newline at end of file diff --git a/go.mod b/go.mod index ea0e071c..f500abe4 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.3.0 github.com/joho/godotenv v1.5.1 + golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f ) require ( diff --git a/go.sum b/go.sum index 88a9f4b0..1f582c44 100644 --- a/go.sum +++ b/go.sum @@ -210,6 +210,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/powerscale/helper/user_helper.go b/powerscale/helper/user_helper.go new file mode 100644 index 00000000..06f975de --- /dev/null +++ b/powerscale/helper/user_helper.go @@ -0,0 +1,91 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helper + +import ( + powerscale "dell/powerscale-go-client" + "terraform-provider-powerscale/powerscale/models" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// UpdateUserDataSourceState updates datasource state. +func UpdateUserDataSourceState(userState *models.UserDataSourceModel, userResponse []powerscale.V1MappingUsersLookupMappingItemUser, roles []powerscale.V1AuthRoleExtended) { + for _, user := range userResponse { + var model models.UserModel + UpdateUserState(&model, user) + + var roleAttrs []attr.Value + for _, r := range roles { + for _, m := range r.Members { + if *m.Id == *user.Uid.Id { + roleAttrs = append(roleAttrs, types.StringValue(r.Name)) + } + } + } + model.Roles, _ = types.ListValue(types.StringType, roleAttrs) + userState.Users = append(userState.Users, model) + } +} + +// UpdateUserState updates user state. +func UpdateUserState(model *models.UserModel, user powerscale.V1MappingUsersLookupMappingItemUser) { + + model.Dn = types.StringValue(user.Dn) + model.DNSDomain = types.StringValue(user.DnsDomain) + model.Domain = types.StringValue(user.Domain) + model.Email = types.StringValue(user.Email) + model.Gecos = types.StringValue(user.Gecos) + model.HomeDirectory = types.StringValue(user.HomeDirectory) + model.ID = types.StringValue(user.Id) + model.Name = types.StringValue(user.Name) + model.Provider = types.StringValue(user.Provider) + model.SamAccountName = types.StringValue(user.SamAccountName) + model.Shell = types.StringValue(user.Shell) + model.Type = types.StringValue(user.Type) + model.Upn = types.StringValue(user.Upn) + if user.Gid.Id != nil { + model.GID = types.StringValue(*user.Gid.Id) + } + if user.PrimaryGroupSid.Id != nil { + model.PrimaryGroupSID = types.StringValue(*user.PrimaryGroupSid.Id) + } + if user.Sid.Id != nil { + model.SID = types.StringValue(*user.Sid.Id) + } + if user.Uid.Id != nil { + model.UID = types.StringValue(*user.Uid.Id) + } + + model.Enabled = types.BoolValue(user.Enabled) + model.Expired = types.BoolValue(user.Expired) + model.GeneratedGID = types.BoolValue(user.GeneratedGid) + model.GeneratedUID = types.BoolValue(user.GeneratedUid) + model.GeneratedUpn = types.BoolValue(user.GeneratedUpn) + model.Locked = types.BoolValue(user.Locked) + model.PasswordExpired = types.BoolValue(user.PasswordExpired) + model.PasswordExpires = types.BoolValue(user.PasswordExpires) + model.PromptPasswordChange = types.BoolValue(user.PromptPasswordChange) + model.UserCanChangePassword = types.BoolValue(user.UserCanChangePassword) + + model.Expiry = types.Int64Value(int64(user.Expiry)) + model.MaxPasswordAge = types.Int64Value(int64(user.MaxPasswordAge)) + model.PasswordExpiry = types.Int64Value(int64(user.PasswordExpiry)) + model.PasswordLastSet = types.Int64Value(int64(user.PasswordLastSet)) +} diff --git a/powerscale/models/user.go b/powerscale/models/user.go new file mode 100644 index 00000000..3b04af7c --- /dev/null +++ b/powerscale/models/user.go @@ -0,0 +1,83 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package models + +import "github.com/hashicorp/terraform-plugin-framework/types" + +// UserDataSourceModel describes the data source data model. +type UserDataSourceModel struct { + Users []UserModel `tfsdk:"users"` + ID types.String `tfsdk:"id"` + + //filter + Filter *UserFilterType `tfsdk:"filter"` +} + +// UserModel holds user data source schema attribute details. +type UserModel struct { + Dn types.String `tfsdk:"dn"` + DNSDomain types.String `tfsdk:"dns_domain"` + Domain types.String `tfsdk:"domain"` + Email types.String `tfsdk:"email"` + Enabled types.Bool `tfsdk:"enabled"` + Expired types.Bool `tfsdk:"expired"` + Expiry types.Int64 `tfsdk:"expiry"` + Gecos types.String `tfsdk:"gecos"` + GeneratedGID types.Bool `tfsdk:"generated_gid"` + GeneratedUID types.Bool `tfsdk:"generated_uid"` + GeneratedUpn types.Bool `tfsdk:"generated_upn"` + GID types.String `tfsdk:"gid"` + HomeDirectory types.String `tfsdk:"home_directory"` + ID types.String `tfsdk:"id"` + Locked types.Bool `tfsdk:"locked"` + MaxPasswordAge types.Int64 `tfsdk:"max_password_age"` + Name types.String `tfsdk:"name"` + PasswordExpired types.Bool `tfsdk:"password_expired"` + PasswordExpires types.Bool `tfsdk:"password_expires"` + PasswordExpiry types.Int64 `tfsdk:"password_expiry"` + PasswordLastSet types.Int64 `tfsdk:"password_last_set"` + PrimaryGroupSID types.String `tfsdk:"primary_group_sid"` + PromptPasswordChange types.Bool `tfsdk:"prompt_password_change"` + Provider types.String `tfsdk:"provider"` + SamAccountName types.String `tfsdk:"sam_account_name"` + Shell types.String `tfsdk:"shell"` + SID types.String `tfsdk:"sid"` + Type types.String `tfsdk:"type"` + UID types.String `tfsdk:"uid"` + Upn types.String `tfsdk:"upn"` + UserCanChangePassword types.Bool `tfsdk:"user_can_change_password"` + Roles types.List `tfsdk:"roles"` +} + +// UserFilterType holds filter attribute for user. +type UserFilterType struct { + Names []UserMemberItem `tfsdk:"names"` + NamePrefix types.String `tfsdk:"name_prefix"` + Domain types.String `tfsdk:"domain"` + Zone types.String `tfsdk:"zone"` + Provider types.String `tfsdk:"provider"` + Cached types.Bool `tfsdk:"cached"` + ResolveNames types.Bool `tfsdk:"resolve_names"` + MemberOf types.Bool `tfsdk:"member_of"` +} + +// UserMemberItem holds identity attribute for a auth member. +type UserMemberItem struct { + Name types.String `tfsdk:"name"` + UID types.Int64 `tfsdk:"uid"` +} diff --git a/powerscale/provider/provider.go b/powerscale/provider/provider.go index eae99430..afed83e8 100644 --- a/powerscale/provider/provider.go +++ b/powerscale/provider/provider.go @@ -188,6 +188,7 @@ func (p *PscaleProvider) DataSources(ctx context.Context) []func() datasource.Da return []func() datasource.DataSource{ NewAccessZoneDataSource, NewClusterDataSource, + NewUserDataSource, } } diff --git a/powerscale/provider/user_data_source.go b/powerscale/provider/user_data_source.go new file mode 100644 index 00000000..bc11d773 --- /dev/null +++ b/powerscale/provider/user_data_source.go @@ -0,0 +1,466 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + powerscale "dell/powerscale-go-client" + "fmt" + "strings" + "terraform-provider-powerscale/client" + "terraform-provider-powerscale/powerscale/helper" + "terraform-provider-powerscale/powerscale/models" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "golang.org/x/sync/errgroup" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var ( + _ datasource.DataSource = &UserDataSource{} + _ datasource.DataSourceWithConfigure = &UserDataSource{} +) + +// NewUserDataSource creates a new user data source. +func NewUserDataSource() datasource.DataSource { + return &UserDataSource{} +} + +// UserDataSource defines the data source implementation. +type UserDataSource struct { + client *client.Client +} + +// Metadata describes the data source arguments. +func (d *UserDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_user" +} + +// Schema describes the data source arguments. +func (d *UserDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Data source for reading Users in PowerScale cluster.", + Description: "Data source for reading Users in PowerScale cluster.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Unique identifier of the user instance.", + Description: "Unique identifier of the user instance.", + }, + "users": schema.ListNestedAttribute{ + MarkdownDescription: "List of users.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "Specifies a user name.", + MarkdownDescription: "Specifies a user name.", + Optional: true, + }, + "uid": schema.StringAttribute{ + Description: "Specifies a user identifier.", + MarkdownDescription: "Specifies a user identifier.", + Optional: true, + }, + "dn": schema.StringAttribute{ + Description: "Specifies a principal name for the user.", + MarkdownDescription: "Specifies a principal name for the user.", + Optional: true, + }, + "dns_domain": schema.StringAttribute{ + Description: "Specifies the DNS domain.", + MarkdownDescription: "Specifies the DNS domain.", + Optional: true, + }, + "domain": schema.StringAttribute{ + Description: "Specifies the domain that the object is part of.", + MarkdownDescription: "Specifies the domain that the object is part of.", + Optional: true, + }, + "email": schema.StringAttribute{ + Description: "Specifies an email address.", + MarkdownDescription: "Specifies an email address.", + Optional: true, + }, + "gecos": schema.StringAttribute{ + Description: "Specifies the GECOS value, which is usually the full name.", + MarkdownDescription: "Specifies the GECOS value, which is usually the full name.", + Optional: true, + }, + "gid": schema.StringAttribute{ + Description: "Specifies a group identifier.", + MarkdownDescription: "Specifies a group identifier.", + Optional: true, + }, + "home_directory": schema.StringAttribute{ + Description: "Specifies a home directory for the user.", + MarkdownDescription: "Specifies a home directory for the user.", + Optional: true, + }, + "id": schema.StringAttribute{ + Description: "Specifies the user ID.", + MarkdownDescription: "Specifies the user ID.", + Optional: true, + }, + "primary_group_sid": schema.StringAttribute{ + Description: "Specifies the persona of the primary group.", + MarkdownDescription: "Specifies the persona of the primary group.", + Optional: true, + }, + "provider": schema.StringAttribute{ + Description: "Specifies the authentication provider that the object belongs to.", + MarkdownDescription: "Specifies the authentication provider that the object belongs to.", + Optional: true, + }, + "sam_account_name": schema.StringAttribute{ + Description: "Specifies a user name.", + MarkdownDescription: "Specifies a user name.", + Optional: true, + }, + "shell": schema.StringAttribute{ + Description: "Specifies a path to the shell for the user.", + MarkdownDescription: "Specifies a path to the shell for the user.", + Optional: true, + }, + "sid": schema.StringAttribute{ + Description: "Specifies a security identifier.", + MarkdownDescription: "Specifies a security identifier.", + Optional: true, + }, + "type": schema.StringAttribute{ + Description: "Specifies the object type.", + MarkdownDescription: "Specifies the object type.", + Optional: true, + }, + "upn": schema.StringAttribute{ + Description: "Specifies a principal name for the user.", + MarkdownDescription: "Specifies a principal name for the user.", + Optional: true, + }, + "enabled": schema.BoolAttribute{ + Description: "If true, the authenticated user is enabled.", + MarkdownDescription: "If true, the authenticated user is enabled.", + Optional: true, + }, + "expired": schema.BoolAttribute{ + Description: "If true, the authenticated user has expired.", + MarkdownDescription: "If true, the authenticated user has expired.", + Optional: true, + }, + "generated_gid": schema.BoolAttribute{ + Description: "If true, the GID was generated.", + MarkdownDescription: "If true, the GID was generated.", + Optional: true, + }, + "generated_uid": schema.BoolAttribute{ + Description: "If true, the UID was generated.", + MarkdownDescription: "If true, the UID was generated.", + Optional: true, + }, + "generated_upn": schema.BoolAttribute{ + Description: "If true, the UPN was generated.", + MarkdownDescription: "If true, the UPN was generated.", + Optional: true, + }, + "locked": schema.BoolAttribute{ + Description: "If true, indicates that the account is locked.", + MarkdownDescription: "If true, indicates that the account is locked.", + Optional: true, + }, + "password_expired": schema.BoolAttribute{ + Description: "If true, the password has expired.", + MarkdownDescription: "If true, the password has expired.", + Optional: true, + }, + "password_expires": schema.BoolAttribute{ + Description: "If true, the password is allowed to expire.", + MarkdownDescription: "If true, the password is allowed to expire.", + Optional: true, + }, + "prompt_password_change": schema.BoolAttribute{ + Description: "If true, Prompts the user to change their password at the next login.", + MarkdownDescription: "If true, Prompts the user to change their password at the next login.", + Optional: true, + }, + "user_can_change_password": schema.BoolAttribute{ + Description: "Specifies whether the password for the user can be changed.", + MarkdownDescription: "Specifies whether the password for the user can be changed.", + Optional: true, + }, + "expiry": schema.Int64Attribute{ + Description: "Specifies the Unix Epoch time at which the authenticated user will expire.", + MarkdownDescription: "Specifies the Unix Epoch time at which the authenticated user will expire.", + Optional: true, + }, + "max_password_age": schema.Int64Attribute{ + Description: "Specifies the maximum time in seconds allowed before the password expires.", + MarkdownDescription: "Specifies the maximum time in seconds allowed before the password expires.", + Optional: true, + }, + "password_expiry": schema.Int64Attribute{ + Description: "Specifies the time in Unix Epoch seconds that the password will expire.", + MarkdownDescription: "Specifies the time in Unix Epoch seconds that the password will expire.", + Optional: true, + }, + "password_last_set": schema.Int64Attribute{ + Description: "Specifies the last time the password was set.", + MarkdownDescription: "Specifies the last time the password was set.", + Optional: true, + }, + "roles": schema.ListAttribute{ + Description: "List of roles.", + MarkdownDescription: "List of roles.", + ElementType: types.StringType, + Computed: true, + }, + }, + }, + }, + }, + Blocks: map[string]schema.Block{ + "filter": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "names": schema.ListNestedAttribute{ + Description: "List of user identity.", + MarkdownDescription: "List of user identity.", + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "Specifies a user name.", + MarkdownDescription: "Specifies a user name.", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "uid": schema.Int64Attribute{ + Description: "Specifies a numeric user identifier.", + MarkdownDescription: "Specifies a numeric user identifier.", + Optional: true, + }, + }, + }, + }, + "name_prefix": schema.StringAttribute{ + Optional: true, + Description: "Filter users by name prefix.", + MarkdownDescription: "Filter users by name prefix.", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "domain": schema.StringAttribute{ + Optional: true, + Description: "Filter users by domain.", + MarkdownDescription: "Filter users by domain.", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "zone": schema.StringAttribute{ + Optional: true, + Description: "Filter users by zone.", + MarkdownDescription: "Filter users by zone.", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "provider": schema.StringAttribute{ + Optional: true, + Description: "Filter users by provider.", + MarkdownDescription: "Filter users by provider.", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "cached": schema.BoolAttribute{ + Optional: true, + Description: "If true, only return cached objects.", + MarkdownDescription: "If true, only return cached objects.", + }, + "resolve_names": schema.BoolAttribute{ + Optional: true, + Description: "Resolve names of personas.", + MarkdownDescription: "Resolve names of personas.", + }, + "member_of": schema.BoolAttribute{ + Optional: true, + Description: "Enumerate all users that a group is a member of.", + MarkdownDescription: "Enumerate all users that a group is a member of.", + }, + }, + }, + }, + } +} + +// Configure configures the data source. +func (d *UserDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + pscaleClient, ok := req.ProviderData.(*client.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.client = pscaleClient +} + +// Read reads data from the data source. +func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tflog.Info(ctx, "Reading User data source ") + + var state models.UserDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + // start goroutine to cache all roles + var eg errgroup.Group + var roles []powerscale.V1AuthRoleExtended + eg.Go(func() error { + roleParams := d.client.PscaleOpenAPIClient.AuthApi.ListAuthv1AuthRoles(ctx) + result, _, err := roleParams.Execute() + if err != nil { + return err + } + + for { + roles = append(roles, result.Roles...) + if result.Resume == nil || *result.Resume == "" { + break + } + + roleParams = d.client.PscaleOpenAPIClient.AuthApi.ListAuthv1AuthRoles(ctx).Resume(*result.Resume) + if result, _, err = roleParams.Execute(); err != nil { + return err + } + } + + return err + }) + + // cache all users + var users []powerscale.V1MappingUsersLookupMappingItemUser + userParams := d.client.PscaleOpenAPIClient.AuthApi.ListAuthv1AuthUsers(ctx) + + if state.Filter != nil { + if !state.Filter.NamePrefix.IsNull() { + userParams = userParams.Filter(state.Filter.NamePrefix.ValueString()) + } + if !state.Filter.Domain.IsNull() { + userParams = userParams.Domain(state.Filter.Domain.ValueString()) + } + if !state.Filter.Zone.IsNull() { + userParams = userParams.Zone(state.Filter.Zone.ValueString()) + } + if !state.Filter.Provider.IsNull() { + userParams = userParams.Provider(state.Filter.Provider.ValueString()) + } + if !state.Filter.Cached.IsNull() { + userParams = userParams.Cached(state.Filter.Cached.ValueBool()) + } + if !state.Filter.ResolveNames.IsNull() { + userParams = userParams.ResolveNames(state.Filter.ResolveNames.ValueBool()) + } + if !state.Filter.MemberOf.IsNull() { + userParams = userParams.QueryMemberOf(state.Filter.MemberOf.ValueBool()) + } + } + + result, _, err := userParams.Execute() + if err != nil { + resp.Diagnostics.AddError( + "Error getting the list of PowerScale Users", + err.Error(), + ) + return + } + + for { + users = append(users, result.Users...) + if result.Resume == nil || *result.Resume == "" { + break + } + + userParams = d.client.PscaleOpenAPIClient.AuthApi.ListAuthv1AuthUsers(ctx).Resume(*result.Resume) + if result, _, err = userParams.Execute(); err != nil { + resp.Diagnostics.AddError("Error getting the list of PowerScale Users with resume", err.Error()) + return + } + } + + if err := eg.Wait(); err != nil { + resp.Diagnostics.AddError("Error getting the list of PowerScale Roles", err.Error()) + roles = nil + } + + // parse user response to state user model + helper.UpdateUserDataSourceState(&state, users, roles) + + // filter users by names + if state.Filter != nil && len(state.Filter.Names) > 0 { + var validUsers []string + var filteredUsers []models.UserModel + + for _, user := range state.Users { + for _, name := range state.Filter.Names { + if (!name.Name.IsNull() && user.Name.Equal(name.Name)) || + (!name.UID.IsNull() && fmt.Sprintf("UID:%d", name.UID.ValueInt64()) == user.UID.ValueString()) { + filteredUsers = append(filteredUsers, user) + validUsers = append(validUsers, fmt.Sprintf("Name: %s, UID: %s", user.Name, user.UID)) + continue + } + } + } + + state.Users = filteredUsers + + if len(state.Users) != len(state.Filter.Names) { + resp.Diagnostics.AddError( + "Error one or more of the filtered user names is not a valid powerscale user.", + fmt.Sprintf("Valid users: [%v], filtered list: [%v]", strings.Join(validUsers, " ; "), state.Filter.Names), + ) + } + } + + state.ID = types.StringValue("user_datasource") + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + tflog.Info(ctx, "Done with Read User data source ") +} diff --git a/powerscale/provider/user_datasource_test.go b/powerscale/provider/user_datasource_test.go new file mode 100644 index 00000000..9e0ed67a --- /dev/null +++ b/powerscale/provider/user_datasource_test.go @@ -0,0 +1,215 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccUserDataSourceFilter(t *testing.T) { + var userTerraformName = "data.powerscale_user.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // filter read testing + { + Config: ProviderConfig + userFilterDataSourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(userTerraformName, "users.#"), + ), + }, + }, + }) +} + +func TestAccUserDataSourceNames(t *testing.T) { + var userTerraformName = "data.powerscale_user.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // filter by names read testing + { + Config: ProviderConfig + userNamesDataSourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(userTerraformName, "users.#", "3"), + ), + }, + }, + }) +} + +func TestAccUserDataSourceFilterNames(t *testing.T) { + var userTerraformName = "data.powerscale_user.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // filter with names read testing + { + Config: ProviderConfig + userFilterNamesDataSourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(userTerraformName, "users.#", "1"), + resource.TestCheckResourceAttr(userTerraformName, "users.0.uid", "UID:10000"), + resource.TestCheckResourceAttr(userTerraformName, "users.0.name", "tfaccUserDatasource"), + resource.TestCheckResourceAttr(userTerraformName, "users.0.roles.#", "0"), + ), + }, + }, + }) +} + +func TestAccUserDataSourceInvalidFilter(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // filter with invalid filter read testing + { + Config: ProviderConfig + userInvalidFilterDataSourceConfig, + ExpectError: regexp.MustCompile(`.*Error getting the list of PowerScale Users*.`), + }, + }, + }) +} + +func TestAccUserDataSourceInvalidNames(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // filter with invalid names read testing + { + Config: ProviderConfig + userInvalidNamesDataSourceConfig, + ExpectError: regexp.MustCompile(`.*Error one or more of the filtered user names is not a valid powerscale user*.`), + }, + }, + }) +} + +func TestAccUserDataSourceAll(t *testing.T) { + var userTerraformName = "data.powerscale_user.all" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // read all testing + { + Config: ProviderConfig + userAllDataSourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(userTerraformName, "users.#"), + ), + }, + }, + }) +} + +var userFilterDataSourceConfig = ` +data "powerscale_user" "test" { + filter { + cached = false + resolve_names = false + member_of = false + # domain = "" + # zone = "" + # provider = "" + } +} +` + +var userFilterNamesDataSourceConfig = ` +data "powerscale_user" "test" { + filter { + names = [ + { + name = "tfaccUserDatasource" + uid = 10000 + } + ] + cached = false + name_prefix = "tfacc" + resolve_names = false + member_of = false + # domain = "testDomain" + # zone = "testZone" + # provider = "testProvider" + } +} +` + +var userNamesDataSourceConfig = ` +data "powerscale_user" "test" { + filter { + names = [ + { + uid = 0 + }, + { + name = "admin" + }, + { + name = "tfaccUserDatasource" + uid = 10000 + } + ] + } +} +` +var userInvalidFilterDataSourceConfig = ` +data "powerscale_user" "test" { + filter { + names = [ + { + name = "tfaccUserDatasource" + uid = 10000 + } + ] + cached = false + name_prefix = "tfacc" + resolve_names = false + member_of = false + domain = " " + zone = " " + provider = " " + } + } +` + +var userInvalidNamesDataSourceConfig = ` +data "powerscale_user" "test" { + filter { + names = [ + { + uid = 0 + }, + { + name = "invalidUser" + } + ] + } +} +` + +var userAllDataSourceConfig = ` +data "powerscale_user" "all" { +} +`