From bde2638c1040bba5ffb54627badabd18d2e0b26e Mon Sep 17 00:00:00 2001 From: Artur Sawicki Date: Sun, 1 Sep 2024 19:57:18 +0200 Subject: [PATCH] feat: Rework user resource (#3026) Rework user resource: - schema adjusted - handle show output properly - updated user acceptance tests with new builders and assertions - introduce new helpers in `resource_helpers_create.go`, `resource_helpers_update.go`, and `resource_helpers_read.go` - cleaned parameters handling a bit - reused `parameterDef` between user and schema - removed old way of handling keys in tests - added float property to the SDK - added integration tests documenting specific attributes behavior Next PRs: - handle policies for user (SNOW-1645875) - deal with improper custom diffs with diff suppression (SNOW-1629468) - move collection util (SNOW-1473414) - remove `checkBool` helper - datasource - rename methods in `resource_helpers_read.go` - update all changes in README (+ note that external changes for a few attributes) - change description of user public keys resource (should be used only if user is not managed by terraform) - add one more test for login and display name - check the exact behavior of default_namespace and default_role because it looks like it is handled in a case-insensitive manner on Snowflake side - check if any user parameter needs diffsuppression or validation - test if hasMfa is always non-nullable, check if this has mfa helps with disable mfa, add to the describe output too - test what is visible if everything is set if there is no ownership on the active role (+ update docs) - handle `snowflake_user_password_policy_attachment` and `snowflake_user_public_keys` - add `IgnoreChangeToCurrentSnowflakeValueInShow` and other suppressors References: - #1155 - #1572 --- MIGRATION_GUIDE.md | 37 +- docs/data-sources/database_roles.md | 1 + docs/resources/database_role.md | 1 + docs/resources/user.md | 35 +- .../assert/objectassert/user_snowflake_ext.go | 13 +- .../assert/objectassert/user_snowflake_gen.go | 22 + .../resourceassert/user_resource_ext.go | 18 + .../resourceassert/user_resource_gen.go | 70 +- .../resourceassert/view_resource_gen.go | 30 +- .../user_resource_parameters_ext.go | 69 ++ ...o => warehouse_resource_parameters_ext.go} | 0 .../user_show_output_gen.go | 10 + .../config/model/user_model_ext.go | 6 + .../config/model/user_model_gen.go | 85 ++- .../config/model/view_model_gen.go | 74 +- pkg/acceptance/helpers/random/certs.go | 4 +- pkg/acceptance/helpers/user_client.go | 50 +- .../collections/collection_helpers.go | 8 + pkg/resources/custom_diffs.go | 1 - pkg/resources/database_commons.go | 1 + pkg/resources/deprecated_test.go | 24 + pkg/resources/diff_suppressions.go | 22 +- pkg/resources/helpers.go | 31 - pkg/resources/resource_helpers_create.go | 63 ++ pkg/resources/resource_helpers_read.go | 52 ++ pkg/resources/resource_helpers_update.go | 106 +++ pkg/resources/schema.go | 22 +- pkg/resources/schema_parameters.go | 8 +- pkg/resources/testdata/userkey1 | 1 - pkg/resources/testdata/userkey2 | 1 - pkg/resources/user.go | 578 ++++++++------- pkg/resources/user_acceptance_test.go | 664 +++++++++++++----- pkg/resources/user_parameters.go | 41 +- .../user_public_keys_acceptance_test.go | 77 +- pkg/resources/validators.go | 15 + pkg/resources/validators_test.go | 51 ++ pkg/schemas/database_role_gen.go | 5 + pkg/schemas/external_function_gen.go | 5 + pkg/schemas/policy_reference_gen.go | 2 +- pkg/schemas/procedure_gen.go | 5 + pkg/schemas/user_gen.go | 10 + pkg/sdk/common_types.go | 28 + pkg/sdk/common_types_test.go | 132 +++- pkg/sdk/parameters.go | 81 +++ pkg/sdk/testint/users_integration_test.go | 492 +++++++++++-- pkg/sdk/users.go | 39 +- pkg/sdk/users_test.go | 46 +- pkg/testhelpers/fixtures.go | 21 - 48 files changed, 2330 insertions(+), 827 deletions(-) create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceparametersassert/user_resource_parameters_ext.go rename pkg/acceptance/bettertestspoc/assert/resourceparametersassert/{warehouse_parameters_ext.go => warehouse_resource_parameters_ext.go} (100%) create mode 100644 pkg/resources/deprecated_test.go create mode 100644 pkg/resources/resource_helpers_create.go create mode 100644 pkg/resources/resource_helpers_read.go create mode 100644 pkg/resources/resource_helpers_update.go delete mode 100644 pkg/resources/testdata/userkey1 delete mode 100644 pkg/resources/testdata/userkey2 delete mode 100644 pkg/testhelpers/fixtures.go diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 67a266ffbb..eec752d556 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -206,24 +206,55 @@ The following set of [parameters](https://docs.snowflake.com/en/sql-reference/pa Connected issues: [#2938](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2938) -### *(breaking change)* Changes in sensitiveness of name and login_name +#### *(breaking change)* Changes in sensitiveness of name, login_name, and display_name According to https://docs.snowflake.com/en/sql-reference/functions/all_user_names#usage-notes, `NAME`s are not considered sensitive data and `LOGIN_NAME`s are. Previous versions of the provider had this the other way around. In this version, `name` attribute was unmarked as sensitive, whereas `login_name` was marked as sensitive. This may break your configuration if you were using `login_name`s before e.g. in a `for_each` loop. +The `display_name` attribute was marked as sensitive. It defaults to `name` if not provided on Snowflake side. Because `name` is no longer sensitive, we also change the setting for the `display_name`. + Connected issues: [#2662](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2662), [#2668](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2668). -### *(bugfix)* Correctly handle `default_warehouse`, `default_namespace`, and `default_role` +#### *(bugfix)* Correctly handle `default_warehouse`, `default_namespace`, and `default_role` During the [identifiers rework](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/ROADMAP.md#identifiers-rework), we generalized how we compute the differences correctly for the identifier fields (read more in [this document](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md)). Proper suppressor was applied to `default_warehouse`, `default_namespace`, and `default_role`. Also, all these three attributes were corrected (e.g. handling spaces/hyphens in names). Connected issues: [#2836](https://github.com/Snowflake-Labs/terraform-provider-snowflake/pull/2836), [#2942](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2942) -### *(bugfix)* Correctly handle failed update +#### *(bugfix)* Correctly handle failed update Not every attribute can be updated in the state during read (like `password` in the `snowflake_user` resource). In situations where update fails, we may end up with an incorrect state (read more in https://github.com/hashicorp/terraform-plugin-sdk/issues/476). We use a deprecated method from the plugin SDK, and now, for partially failed updates, we preserve the resource's previous state. It fixed this kind of situations for `snowflake_user` resource. Connected issues: [#2970](https://github.com/Snowflake-Labs/terraform-provider-snowflake/pull/2970) +#### *(breaking change)* Attributes changes + +Attributes that are no longer computed: +- `login_name` +- `display_name` +- `disabled` +- `default_role` + +New fields: +- `middle_name` +- `days_to_expiry` +- `mins_to_unlock` +- `mins_to_bypass_mfa` +- `disable_mfa` + +Removed fields: +- `has_rsa_public_key` + +Default changes: +- `must_change_password` +- `disabled` + +Type changes: +- `must_change_password`: bool -> string +- `disabled`: bool -> string + +Validation changes: +- `default_secondary_roles` - only 1-element lists with `"ALL"` element are now supported. Check [Snowflake docs](https://docs.snowflake.com/en/sql-reference/sql/create-user#optional-object-properties-objectproperties) for more details. + ## v0.94.0 ➞ v0.94.1 ### changes in snowflake_schema diff --git a/docs/data-sources/database_roles.md b/docs/data-sources/database_roles.md index 8298c3d9ad..ea95e8737b 100644 --- a/docs/data-sources/database_roles.md +++ b/docs/data-sources/database_roles.md @@ -62,6 +62,7 @@ Read-Only: - `comment` (String) - `created_on` (String) +- `database_name` (String) - `granted_database_roles` (Number) - `granted_to_database_roles` (Number) - `granted_to_roles` (Number) diff --git a/docs/resources/database_role.md b/docs/resources/database_role.md index 613024f51e..0774975d4b 100644 --- a/docs/resources/database_role.md +++ b/docs/resources/database_role.md @@ -52,6 +52,7 @@ Read-Only: - `comment` (String) - `created_on` (String) +- `database_name` (String) - `granted_database_roles` (Number) - `granted_to_database_roles` (Number) - `granted_to_roles` (Number) diff --git a/docs/resources/user.md b/docs/resources/user.md index 37317c96cb..9d03f9066d 100644 --- a/docs/resources/user.md +++ b/docs/resources/user.md @@ -2,14 +2,14 @@ page_title: "snowflake_user Resource - terraform-provider-snowflake" subcategory: "" description: |- - + Resource used to manage user objects. For more information, check user documentation https://docs.snowflake.com/en/sql-reference/commands-user-role. --- !> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v094x--v0950) to use it. # snowflake_user (Resource) - +Resource used to manage user objects. For more information, check [user documentation](https://docs.snowflake.com/en/sql-reference/commands-user-role). ## Example Usage @@ -43,7 +43,7 @@ resource "snowflake_user" "user" { ### Required -- `name` (String) Name of the user. Note that if you do not supply login_name this will be used as login_name. [doc](https://docs.snowflake.net/manuals/sql-reference/sql/create-user.html#required-parameters) +- `name` (String) Name of the user. Note that if you do not supply login_name this will be used as login_name. Check the [docs](https://docs.snowflake.net/manuals/sql-reference/sql/create-user.html#required-parameters). Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` ### Optional @@ -59,15 +59,17 @@ resource "snowflake_user" "user" { - `client_session_keep_alive` (Boolean) Parameter that indicates whether to force a user to log in again after a period of inactivity in the session. For more information, check [CLIENT_SESSION_KEEP_ALIVE docs](https://docs.snowflake.com/en/sql-reference/parameters#client-session-keep-alive). - `client_session_keep_alive_heartbeat_frequency` (Number) Number of seconds in-between client attempts to update the token for the session. For more information, check [CLIENT_SESSION_KEEP_ALIVE_HEARTBEAT_FREQUENCY docs](https://docs.snowflake.com/en/sql-reference/parameters#client-session-keep-alive-heartbeat-frequency). - `client_timestamp_type_mapping` (String) Specifies the [TIMESTAMP_* variation](https://docs.snowflake.com/en/sql-reference/data-types-datetime.html#label-datatypes-timestamp-variations) to use when binding timestamp variables for JDBC or ODBC applications that use the bind API to load data. For more information, check [CLIENT_TIMESTAMP_TYPE_MAPPING docs](https://docs.snowflake.com/en/sql-reference/parameters#client-timestamp-type-mapping). -- `comment` (String) +- `comment` (String) Specifies a comment for the user. - `date_input_format` (String) Specifies the input format for the DATE data type. For more information, see [Date and time input and output formats](https://docs.snowflake.com/en/sql-reference/date-time-input-output). For more information, check [DATE_INPUT_FORMAT docs](https://docs.snowflake.com/en/sql-reference/parameters#date-input-format). - `date_output_format` (String) Specifies the display format for the DATE data type. For more information, see [Date and time input and output formats](https://docs.snowflake.com/en/sql-reference/date-time-input-output). For more information, check [DATE_OUTPUT_FORMAT docs](https://docs.snowflake.com/en/sql-reference/parameters#date-output-format). -- `default_namespace` (String) Specifies the namespace (database only or database and schema) that is active by default for the user’s session upon login. -- `default_role` (String) Specifies the role that is active by default for the user’s session upon login. -- `default_secondary_roles` (Set of String) Specifies the set of secondary roles that are active for the user’s session upon login. Currently only ["ALL"] value is supported - more information can be found in [doc](https://docs.snowflake.com/en/sql-reference/sql/create-user#optional-object-properties-objectproperties) -- `default_warehouse` (String) Specifies the virtual warehouse that is active by default for the user’s session upon login. -- `disabled` (Boolean) -- `display_name` (String, Sensitive) Name displayed for the user in the Snowflake web interface. +- `days_to_expiry` (Number) Specifies the number of days after which the user status is set to `Expired` and the user is no longer allowed to log in. This is useful for defining temporary users (i.e. users who should only have access to Snowflake for a limited time period). In general, you should not set this property for [account administrators](https://docs.snowflake.com/en/user-guide/security-access-control-considerations.html#label-accountadmin-users) (i.e. users with the `ACCOUNTADMIN` role) because Snowflake locks them out when they become `Expired`. +- `default_namespace` (String) Specifies the namespace (database only or database and schema) that is active by default for the user’s session upon login. Note that the CREATE USER operation does not verify that the namespace exists. +- `default_role` (String) Specifies the role that is active by default for the user’s session upon login. Note that specifying a default role for a user does **not** grant the role to the user. The role must be granted explicitly to the user using the [GRANT ROLE](https://docs.snowflake.com/en/sql-reference/sql/grant-role) command. In addition, the CREATE USER operation does not verify that the role exists. +- `default_secondary_roles` (Set of String) Specifies the set of secondary roles that are active for the user’s session upon login. Currently only ["ALL"] value is supported - more information can be found in [doc](https://docs.snowflake.com/en/sql-reference/sql/create-user#optional-object-properties-objectproperties). +- `default_warehouse` (String) Specifies the virtual warehouse that is active by default for the user’s session upon login. Note that the CREATE USER operation does not verify that the warehouse exists. +- `disable_mfa` (String) Allows enabling or disabling [multi-factor authentication](https://docs.snowflake.com/en/user-guide/security-mfa). Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. +- `disabled` (String) Specifies whether the user is disabled, which prevents logging in and aborts all the currently-running queries for the user. Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. +- `display_name` (String) Name displayed for the user in the Snowflake web interface. - `email` (String, Sensitive) Email address for the user. - `enable_unload_physical_type_optimization` (Boolean) Specifies whether to set the schema for unloaded Parquet files based on the logical column data types (i.e. the types in the unload SQL query or source table) or on the unloaded column values (i.e. the smallest data types and precision that support the values in the output columns of the unload SQL statement or source table). For more information, check [ENABLE_UNLOAD_PHYSICAL_TYPE_OPTIMIZATION docs](https://docs.snowflake.com/en/sql-reference/parameters#enable-unload-physical-type-optimization). - `enable_unredacted_query_syntax_error` (Boolean) Controls whether query text is redacted if a SQL query fails due to a syntax or parsing error. If `FALSE`, the content of a failed query is redacted in the views, pages, and functions that provide a query history. Only users with a role that is granted or inherits the AUDIT privilege can set the ENABLE_UNREDACTED_QUERY_SYNTAX_ERROR parameter. When using the ALTER USER command to set the parameter to `TRUE` for a particular user, modify the user that you want to see the query text, not the user who executed the query (if those are different users). For more information, check [ENABLE_UNREDACTED_QUERY_SYNTAX_ERROR docs](https://docs.snowflake.com/en/sql-reference/parameters#enable-unredacted-query-syntax-error). @@ -83,13 +85,16 @@ resource "snowflake_user" "user" { - `last_name` (String, Sensitive) Last name of the user. - `lock_timeout` (Number) Number of seconds to wait while trying to lock a resource, before timing out and aborting the statement. For more information, check [LOCK_TIMEOUT docs](https://docs.snowflake.com/en/sql-reference/parameters#lock-timeout). - `log_level` (String) Specifies the severity level of messages that should be ingested and made available in the active event table. Messages at the specified level (and at more severe levels) are ingested. For more information about log levels, see [Setting log level](https://docs.snowflake.com/en/developer-guide/logging-tracing/logging-log-level). For more information, check [LOG_LEVEL docs](https://docs.snowflake.com/en/sql-reference/parameters#log-level). -- `login_name` (String, Sensitive) The name users use to log in. If not supplied, snowflake will use name instead. +- `login_name` (String, Sensitive) The name users use to log in. If not supplied, snowflake will use name instead. Login names are always case-insensitive. +- `middle_name` (String, Sensitive) Middle name of the user. +- `mins_to_bypass_mfa` (Number) Specifies the number of minutes to temporarily bypass MFA for the user. This property can be used to allow a MFA-enrolled user to temporarily bypass MFA during login in the event that their MFA device is not available. +- `mins_to_unlock` (Number) Specifies the number of minutes until the temporary lock on the user login is cleared. To protect against unauthorized user login, Snowflake places a temporary lock on a user after five consecutive unsuccessful login attempts. When creating a user, this property can be set to prevent them from logging in until the specified amount of time passes. To remove a lock immediately for a user, specify a value of 0 for this parameter. - `multi_statement_count` (Number) Number of statements to execute when using the multi-statement capability. For more information, check [MULTI_STATEMENT_COUNT docs](https://docs.snowflake.com/en/sql-reference/parameters#multi-statement-count). -- `must_change_password` (Boolean) Specifies whether the user is forced to change their password on next login (including their first/initial login) into the system. +- `must_change_password` (String) Specifies whether the user is forced to change their password on next login (including their first/initial login) into the system. Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. - `network_policy` (String) Specifies the network policy to enforce for your account. Network policies enable restricting access to your account based on users’ IP address. For more details, see [Controlling network traffic with network policies](https://docs.snowflake.com/en/user-guide/network-policies). Any existing network policy (created using [CREATE NETWORK POLICY](https://docs.snowflake.com/en/sql-reference/sql/create-network-policy)). For more information, check [NETWORK_POLICY docs](https://docs.snowflake.com/en/sql-reference/parameters#network-policy). - `noorder_sequence_as_default` (Boolean) Specifies whether the ORDER or NOORDER property is set by default when you create a new sequence or add a new table column. The ORDER and NOORDER properties determine whether or not the values are generated for the sequence or auto-incremented column in [increasing or decreasing order](https://docs.snowflake.com/en/user-guide/querying-sequences.html#label-querying-sequences-increasing-values). For more information, check [NOORDER_SEQUENCE_AS_DEFAULT docs](https://docs.snowflake.com/en/sql-reference/parameters#noorder-sequence-as-default). - `odbc_treat_decimal_as_int` (Boolean) Specifies how ODBC processes columns that have a scale of zero (0). For more information, check [ODBC_TREAT_DECIMAL_AS_INT docs](https://docs.snowflake.com/en/sql-reference/parameters#odbc-treat-decimal-as-int). -- `password` (String, Sensitive) **WARNING:** this will put the password in the terraform state file. Use carefully. +- `password` (String, Sensitive) Password for the user. **WARNING:** this will put the password in the terraform state file. Use carefully. - `prevent_unload_to_internal_stages` (Boolean) Specifies whether to prevent data unload operations to internal (Snowflake) stages using [COPY INTO ](https://docs.snowflake.com/en/sql-reference/sql/copy-into-location) statements. For more information, check [PREVENT_UNLOAD_TO_INTERNAL_STAGES docs](https://docs.snowflake.com/en/sql-reference/parameters#prevent-unload-to-internal-stages). - `query_tag` (String) Optional string that can be used to tag queries and other SQL statements executed within a session. The tags are displayed in the output of the [QUERY_HISTORY, QUERY_HISTORY_BY_*](https://docs.snowflake.com/en/sql-reference/functions/query_history) functions. For more information, check [QUERY_TAG docs](https://docs.snowflake.com/en/sql-reference/parameters#query-tag). - `quoted_identifiers_ignore_case` (Boolean) Specifies whether letters in double-quoted object identifiers are stored and resolved as uppercase letters. By default, Snowflake preserves the case of alphabetic characters when storing and resolving double-quoted identifiers (see [Identifier resolution](https://docs.snowflake.com/en/sql-reference/identifiers-syntax.html#label-identifier-casing)). You can use this parameter in situations in which [third-party applications always use double quotes around identifiers](https://docs.snowflake.com/en/sql-reference/identifiers-syntax.html#label-identifier-casing-parameter). For more information, check [QUOTED_IDENTIFIERS_IGNORE_CASE docs](https://docs.snowflake.com/en/sql-reference/parameters#quoted-identifiers-ignore-case). @@ -124,10 +129,10 @@ resource "snowflake_user" "user" { ### Read-Only - `fully_qualified_name` (String) Fully qualified name of the resource. For more information, see [object name resolution](https://docs.snowflake.com/en/sql-reference/name-resolution). -- `has_rsa_public_key` (Boolean) Will be true if user as an RSA key set. - `id` (String) The ID of this resource. - `parameters` (List of Object) Outputs the result of `SHOW PARAMETERS IN USER` for the given user. (see [below for nested schema](#nestedatt--parameters)) - `show_output` (List of Object) Outputs the result of `SHOW USER` for the given user. (see [below for nested schema](#nestedatt--show_output)) +- `user_type` (String) Specifies a type for the user. ### Nested Schema for `parameters` @@ -909,6 +914,7 @@ Read-Only: - `ext_authn_duo` (Boolean) - `ext_authn_uid` (String) - `first_name` (String) +- `has_mfa` (Boolean) - `has_password` (Boolean) - `has_rsa_public_key` (Boolean) - `last_name` (String) @@ -921,6 +927,7 @@ Read-Only: - `name` (String) - `owner` (String) - `snowflake_lock` (Boolean) +- `type` (String) ## Import diff --git a/pkg/acceptance/bettertestspoc/assert/objectassert/user_snowflake_ext.go b/pkg/acceptance/bettertestspoc/assert/objectassert/user_snowflake_ext.go index 5e1e916473..f6dcf68819 100644 --- a/pkg/acceptance/bettertestspoc/assert/objectassert/user_snowflake_ext.go +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/user_snowflake_ext.go @@ -121,7 +121,18 @@ func (w *UserAssert) HasDaysToExpiryNotEmpty() *UserAssert { w.AddAssertion(func(t *testing.T, o *sdk.User) error { t.Helper() if o.DaysToExpiry == "" { - return fmt.Errorf("expected days to expiry not empty; got: %v", o.DaysToExpiry) + return fmt.Errorf("expected days to expiry not empty; got empty") + } + return nil + }) + return w +} + +func (w *UserAssert) HasDaysToExpiryEmpty() *UserAssert { + w.AddAssertion(func(t *testing.T, o *sdk.User) error { + t.Helper() + if o.DaysToExpiry != "" { + return fmt.Errorf("expected days to expiry empty; got: %v", o.DaysToExpiry) } return nil }) diff --git a/pkg/acceptance/bettertestspoc/assert/objectassert/user_snowflake_gen.go b/pkg/acceptance/bettertestspoc/assert/objectassert/user_snowflake_gen.go index 39bde579b3..5ad4c714b5 100644 --- a/pkg/acceptance/bettertestspoc/assert/objectassert/user_snowflake_gen.go +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/user_snowflake_gen.go @@ -316,3 +316,25 @@ func (u *UserAssert) HasHasRsaPublicKey(expected bool) *UserAssert { }) return u } + +func (u *UserAssert) HasType(expected string) *UserAssert { + u.AddAssertion(func(t *testing.T, o *sdk.User) error { + t.Helper() + if o.Type != expected { + return fmt.Errorf("expected type: %v; got: %v", expected, o.Type) + } + return nil + }) + return u +} + +func (u *UserAssert) HasHasMfa(expected bool) *UserAssert { + u.AddAssertion(func(t *testing.T, o *sdk.User) error { + t.Helper() + if o.HasMfa != expected { + return fmt.Errorf("expected has mfa: %v; got: %v", expected, o.HasMfa) + } + return nil + }) + return u +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/user_resource_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/user_resource_ext.go index fb98079028..26e2511674 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/user_resource_ext.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/user_resource_ext.go @@ -1,6 +1,7 @@ package resourceassert import ( + "fmt" "strconv" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" @@ -15,3 +16,20 @@ func (u *UserResourceAssert) HasEmptyPassword() *UserResourceAssert { u.AddAssertion(assert.ValueSet("password", "")) return u } + +func (u *UserResourceAssert) HasDefaultSecondaryRoles(roles ...string) *UserResourceAssert { + for idx, role := range roles { + u.AddAssertion(assert.ValueSet(fmt.Sprintf("default_secondary_roles.%d", idx), role)) + } + return u +} + +func (u *UserResourceAssert) HasDefaultSecondaryRolesEmpty() *UserResourceAssert { + u.AddAssertion(assert.ValueSet("default_secondary_roles.#", "0")) + return u +} + +func (u *UserResourceAssert) HasMustChangePassword(expected bool) *UserResourceAssert { + u.AddAssertion(assert.ValueSet("must_change_password", strconv.FormatBool(expected))) + return u +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/user_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/user_resource_gen.go index c491866e99..d709986e7b 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/user_resource_gen.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/user_resource_gen.go @@ -107,6 +107,11 @@ func (u *UserResourceAssert) HasDateOutputFormatString(expected string) *UserRes return u } +func (u *UserResourceAssert) HasDaysToExpiryString(expected string) *UserResourceAssert { + u.AddAssertion(assert.ValueSet("days_to_expiry", expected)) + return u +} + func (u *UserResourceAssert) HasDefaultNamespaceString(expected string) *UserResourceAssert { u.AddAssertion(assert.ValueSet("default_namespace", expected)) return u @@ -127,6 +132,11 @@ func (u *UserResourceAssert) HasDefaultWarehouseString(expected string) *UserRes return u } +func (u *UserResourceAssert) HasDisableMfaString(expected string) *UserResourceAssert { + u.AddAssertion(assert.ValueSet("disable_mfa", expected)) + return u +} + func (u *UserResourceAssert) HasDisabledString(expected string) *UserResourceAssert { u.AddAssertion(assert.ValueSet("disabled", expected)) return u @@ -182,11 +192,6 @@ func (u *UserResourceAssert) HasGeometryOutputFormatString(expected string) *Use return u } -func (u *UserResourceAssert) HasHasRsaPublicKeyString(expected string) *UserResourceAssert { - u.AddAssertion(assert.ValueSet("has_rsa_public_key", expected)) - return u -} - func (u *UserResourceAssert) HasJdbcTreatDecimalAsIntString(expected string) *UserResourceAssert { u.AddAssertion(assert.ValueSet("jdbc_treat_decimal_as_int", expected)) return u @@ -227,6 +232,21 @@ func (u *UserResourceAssert) HasLoginNameString(expected string) *UserResourceAs return u } +func (u *UserResourceAssert) HasMiddleNameString(expected string) *UserResourceAssert { + u.AddAssertion(assert.ValueSet("middle_name", expected)) + return u +} + +func (u *UserResourceAssert) HasMinsToBypassMfaString(expected string) *UserResourceAssert { + u.AddAssertion(assert.ValueSet("mins_to_bypass_mfa", expected)) + return u +} + +func (u *UserResourceAssert) HasMinsToUnlockString(expected string) *UserResourceAssert { + u.AddAssertion(assert.ValueSet("mins_to_unlock", expected)) + return u +} + func (u *UserResourceAssert) HasMultiStatementCountString(expected string) *UserResourceAssert { u.AddAssertion(assert.ValueSet("multi_statement_count", expected)) return u @@ -402,6 +422,11 @@ func (u *UserResourceAssert) HasUseCachedResultString(expected string) *UserReso return u } +func (u *UserResourceAssert) HasUserTypeString(expected string) *UserResourceAssert { + u.AddAssertion(assert.ValueSet("user_type", expected)) + return u +} + func (u *UserResourceAssert) HasWeekOfYearPolicyString(expected string) *UserResourceAssert { u.AddAssertion(assert.ValueSet("week_of_year_policy", expected)) return u @@ -491,6 +516,11 @@ func (u *UserResourceAssert) HasNoDateOutputFormat() *UserResourceAssert { return u } +func (u *UserResourceAssert) HasNoDaysToExpiry() *UserResourceAssert { + u.AddAssertion(assert.ValueNotSet("days_to_expiry")) + return u +} + func (u *UserResourceAssert) HasNoDefaultNamespace() *UserResourceAssert { u.AddAssertion(assert.ValueNotSet("default_namespace")) return u @@ -511,6 +541,11 @@ func (u *UserResourceAssert) HasNoDefaultWarehouse() *UserResourceAssert { return u } +func (u *UserResourceAssert) HasNoDisableMfa() *UserResourceAssert { + u.AddAssertion(assert.ValueNotSet("disable_mfa")) + return u +} + func (u *UserResourceAssert) HasNoDisabled() *UserResourceAssert { u.AddAssertion(assert.ValueNotSet("disabled")) return u @@ -566,11 +601,6 @@ func (u *UserResourceAssert) HasNoGeometryOutputFormat() *UserResourceAssert { return u } -func (u *UserResourceAssert) HasNoHasRsaPublicKey() *UserResourceAssert { - u.AddAssertion(assert.ValueNotSet("has_rsa_public_key")) - return u -} - func (u *UserResourceAssert) HasNoJdbcTreatDecimalAsInt() *UserResourceAssert { u.AddAssertion(assert.ValueNotSet("jdbc_treat_decimal_as_int")) return u @@ -611,6 +641,21 @@ func (u *UserResourceAssert) HasNoLoginName() *UserResourceAssert { return u } +func (u *UserResourceAssert) HasNoMiddleName() *UserResourceAssert { + u.AddAssertion(assert.ValueNotSet("middle_name")) + return u +} + +func (u *UserResourceAssert) HasNoMinsToBypassMfa() *UserResourceAssert { + u.AddAssertion(assert.ValueNotSet("mins_to_bypass_mfa")) + return u +} + +func (u *UserResourceAssert) HasNoMinsToUnlock() *UserResourceAssert { + u.AddAssertion(assert.ValueNotSet("mins_to_unlock")) + return u +} + func (u *UserResourceAssert) HasNoMultiStatementCount() *UserResourceAssert { u.AddAssertion(assert.ValueNotSet("multi_statement_count")) return u @@ -786,6 +831,11 @@ func (u *UserResourceAssert) HasNoUseCachedResult() *UserResourceAssert { return u } +func (u *UserResourceAssert) HasNoUserType() *UserResourceAssert { + u.AddAssertion(assert.ValueNotSet("user_type")) + return u +} + func (u *UserResourceAssert) HasNoWeekOfYearPolicy() *UserResourceAssert { u.AddAssertion(assert.ValueNotSet("week_of_year_policy")) return u diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go index 089d8fded2..9b2994e865 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go @@ -42,11 +42,6 @@ func (v *ViewResourceAssert) HasChangeTrackingString(expected string) *ViewResou return v } -func (v *ViewResourceAssert) HasColumnsString(expected string) *ViewResourceAssert { - v.AddAssertion(assert.ValueSet("columns", expected)) - return v -} - func (v *ViewResourceAssert) HasCommentString(expected string) *ViewResourceAssert { v.AddAssertion(assert.ValueSet("comment", expected)) return v @@ -72,6 +67,11 @@ func (v *ViewResourceAssert) HasDatabaseString(expected string) *ViewResourceAss return v } +func (v *ViewResourceAssert) HasFullyQualifiedNameString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("fully_qualified_name", expected)) + return v +} + func (v *ViewResourceAssert) HasIsRecursiveString(expected string) *ViewResourceAssert { v.AddAssertion(assert.ValueSet("is_recursive", expected)) return v @@ -92,11 +92,6 @@ func (v *ViewResourceAssert) HasNameString(expected string) *ViewResourceAssert return v } -func (v *ViewResourceAssert) HasOrReplaceString(expected string) *ViewResourceAssert { - v.AddAssertion(assert.ValueSet("or_replace", expected)) - return v -} - func (v *ViewResourceAssert) HasRowAccessPolicyString(expected string) *ViewResourceAssert { v.AddAssertion(assert.ValueSet("row_access_policy", expected)) return v @@ -126,11 +121,6 @@ func (v *ViewResourceAssert) HasNoChangeTracking() *ViewResourceAssert { return v } -func (v *ViewResourceAssert) HasNoColumns() *ViewResourceAssert { - v.AddAssertion(assert.ValueNotSet("columns")) - return v -} - func (v *ViewResourceAssert) HasNoComment() *ViewResourceAssert { v.AddAssertion(assert.ValueNotSet("comment")) return v @@ -156,6 +146,11 @@ func (v *ViewResourceAssert) HasNoDatabase() *ViewResourceAssert { return v } +func (v *ViewResourceAssert) HasNoFullyQualifiedName() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("fully_qualified_name")) + return v +} + func (v *ViewResourceAssert) HasNoIsRecursive() *ViewResourceAssert { v.AddAssertion(assert.ValueNotSet("is_recursive")) return v @@ -176,11 +171,6 @@ func (v *ViewResourceAssert) HasNoName() *ViewResourceAssert { return v } -func (v *ViewResourceAssert) HasNoOrReplace() *ViewResourceAssert { - v.AddAssertion(assert.ValueNotSet("or_replace")) - return v -} - func (v *ViewResourceAssert) HasNoRowAccessPolicy() *ViewResourceAssert { v.AddAssertion(assert.ValueNotSet("row_access_policy")) return v diff --git a/pkg/acceptance/bettertestspoc/assert/resourceparametersassert/user_resource_parameters_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceparametersassert/user_resource_parameters_ext.go new file mode 100644 index 0000000000..7799d35eb6 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceparametersassert/user_resource_parameters_ext.go @@ -0,0 +1,69 @@ +package resourceparametersassert + +import ( + "strings" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +func (u *UserResourceParametersAssert) HasAllDefaults() *UserResourceParametersAssert { + return u. + HasEnableUnredactedQuerySyntaxError(false). + HasNetworkPolicy(""). + HasPreventUnloadToInternalStages(false). + HasAbortDetachedQuery(false). + HasAutocommit(true). + HasBinaryInputFormat(sdk.BinaryInputFormatHex). + HasBinaryOutputFormat(sdk.BinaryOutputFormatHex). + HasClientMemoryLimit(1536). + HasClientMetadataRequestUseConnectionCtx(false). + HasClientPrefetchThreads(4). + HasClientResultChunkSize(160). + HasClientResultColumnCaseInsensitive(false). + HasClientSessionKeepAlive(false). + HasClientSessionKeepAliveHeartbeatFrequency(3600). + HasClientTimestampTypeMapping(sdk.ClientTimestampTypeMappingLtz). + HasDateInputFormat("AUTO"). + HasDateOutputFormat("YYYY-MM-DD"). + HasEnableUnloadPhysicalTypeOptimization(true). + HasErrorOnNondeterministicMerge(true). + HasErrorOnNondeterministicUpdate(false). + HasGeographyOutputFormat(sdk.GeographyOutputFormatGeoJSON). + HasGeometryOutputFormat(sdk.GeometryOutputFormatGeoJSON). + HasJdbcTreatDecimalAsInt(true). + HasJdbcTreatTimestampNtzAsUtc(false). + HasJdbcUseSessionTimezone(true). + HasJsonIndent(2). + HasLockTimeout(43200). + HasLogLevel(sdk.LogLevelOff). + HasMultiStatementCount(1). + HasNoorderSequenceAsDefault(true). + HasOdbcTreatDecimalAsInt(false). + HasQueryTag(""). + HasQuotedIdentifiersIgnoreCase(false). + HasRowsPerResultset(0). + HasS3StageVpceDnsName(""). + HasSearchPath("$current, $public"). + HasSimulatedDataSharingConsumer(""). + HasStatementQueuedTimeoutInSeconds(0). + HasStatementTimeoutInSeconds(172800). + HasStrictJsonOutput(false). + HasTimestampDayIsAlways24h(false). + HasTimestampInputFormat("AUTO"). + HasTimestampLtzOutputFormat(""). + HasTimestampNtzOutputFormat("YYYY-MM-DD HH24:MI:SS.FF3"). + HasTimestampOutputFormat("YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM"). + HasTimestampTypeMapping(sdk.TimestampTypeMappingNtz). + HasTimestampTzOutputFormat(""). + HasTimezone("America/Los_Angeles"). + HasTimeInputFormat("AUTO"). + HasTimeOutputFormat("HH24:MI:SS"). + HasTraceLevel(sdk.TraceLevelOff). + HasTransactionAbortOnError(false). + HasTransactionDefaultIsolationLevel(sdk.TransactionDefaultIsolationLevelReadCommitted). + HasTwoDigitCenturyStart(1970). + HasUnsupportedDdlAction(sdk.UnsupportedDDLAction(strings.ToLower(string(sdk.UnsupportedDDLActionIgnore)))). + HasUseCachedResult(true). + HasWeekOfYearPolicy(0). + HasWeekStart(0) +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceparametersassert/warehouse_parameters_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceparametersassert/warehouse_resource_parameters_ext.go similarity index 100% rename from pkg/acceptance/bettertestspoc/assert/resourceparametersassert/warehouse_parameters_ext.go rename to pkg/acceptance/bettertestspoc/assert/resourceparametersassert/warehouse_resource_parameters_ext.go diff --git a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/user_show_output_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/user_show_output_gen.go index 0e64e7432e..7d13a4e883 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/user_show_output_gen.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/user_show_output_gen.go @@ -170,3 +170,13 @@ func (u *UserShowOutputAssert) HasHasRsaPublicKey(expected bool) *UserShowOutput u.AddAssertion(assert.ResourceShowOutputBoolValueSet("has_rsa_public_key", expected)) return u } + +func (u *UserShowOutputAssert) HasType(expected string) *UserShowOutputAssert { + u.AddAssertion(assert.ResourceShowOutputValueSet("type", expected)) + return u +} + +func (u *UserShowOutputAssert) HasHasMfa(expected bool) *UserShowOutputAssert { + u.AddAssertion(assert.ResourceShowOutputBoolValueSet("has_mfa", expected)) + return u +} diff --git a/pkg/acceptance/bettertestspoc/config/model/user_model_ext.go b/pkg/acceptance/bettertestspoc/config/model/user_model_ext.go index 11b707d742..3912d76253 100644 --- a/pkg/acceptance/bettertestspoc/config/model/user_model_ext.go +++ b/pkg/acceptance/bettertestspoc/config/model/user_model_ext.go @@ -4,6 +4,7 @@ import ( tfconfig "github.com/hashicorp/terraform-plugin-testing/config" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" ) @@ -65,3 +66,8 @@ func (u *UserModel) WithNetworkPolicyId(networkPolicy sdk.AccountObjectIdentifie func (u *UserModel) WithNullPassword() *UserModel { return u.WithPasswordValue(config.NullVariable()) } + +func (u *UserModel) WithDefaultSecondaryRolesStringList(items ...string) *UserModel { + itemsAsVariables := collections.Map(items, func(s string) tfconfig.Variable { return tfconfig.StringVariable(s) }) + return u.WithDefaultSecondaryRolesValue(tfconfig.ListVariable(itemsAsVariables...)) +} diff --git a/pkg/acceptance/bettertestspoc/config/model/user_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/user_model_gen.go index d24e499644..a5649d334a 100644 --- a/pkg/acceptance/bettertestspoc/config/model/user_model_gen.go +++ b/pkg/acceptance/bettertestspoc/config/model/user_model_gen.go @@ -25,10 +25,12 @@ type UserModel struct { Comment tfconfig.Variable `json:"comment,omitempty"` DateInputFormat tfconfig.Variable `json:"date_input_format,omitempty"` DateOutputFormat tfconfig.Variable `json:"date_output_format,omitempty"` + DaysToExpiry tfconfig.Variable `json:"days_to_expiry,omitempty"` DefaultNamespace tfconfig.Variable `json:"default_namespace,omitempty"` DefaultRole tfconfig.Variable `json:"default_role,omitempty"` DefaultSecondaryRoles tfconfig.Variable `json:"default_secondary_roles,omitempty"` DefaultWarehouse tfconfig.Variable `json:"default_warehouse,omitempty"` + DisableMfa tfconfig.Variable `json:"disable_mfa,omitempty"` Disabled tfconfig.Variable `json:"disabled,omitempty"` DisplayName tfconfig.Variable `json:"display_name,omitempty"` Email tfconfig.Variable `json:"email,omitempty"` @@ -40,7 +42,6 @@ type UserModel struct { FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` GeographyOutputFormat tfconfig.Variable `json:"geography_output_format,omitempty"` GeometryOutputFormat tfconfig.Variable `json:"geometry_output_format,omitempty"` - HasRsaPublicKey tfconfig.Variable `json:"has_rsa_public_key,omitempty"` JdbcTreatDecimalAsInt tfconfig.Variable `json:"jdbc_treat_decimal_as_int,omitempty"` JdbcTreatTimestampNtzAsUtc tfconfig.Variable `json:"jdbc_treat_timestamp_ntz_as_utc,omitempty"` JdbcUseSessionTimezone tfconfig.Variable `json:"jdbc_use_session_timezone,omitempty"` @@ -49,6 +50,9 @@ type UserModel struct { LockTimeout tfconfig.Variable `json:"lock_timeout,omitempty"` LogLevel tfconfig.Variable `json:"log_level,omitempty"` LoginName tfconfig.Variable `json:"login_name,omitempty"` + MiddleName tfconfig.Variable `json:"middle_name,omitempty"` + MinsToBypassMfa tfconfig.Variable `json:"mins_to_bypass_mfa,omitempty"` + MinsToUnlock tfconfig.Variable `json:"mins_to_unlock,omitempty"` MultiStatementCount tfconfig.Variable `json:"multi_statement_count,omitempty"` MustChangePassword tfconfig.Variable `json:"must_change_password,omitempty"` Name tfconfig.Variable `json:"name,omitempty"` @@ -84,6 +88,7 @@ type UserModel struct { TwoDigitCenturyStart tfconfig.Variable `json:"two_digit_century_start,omitempty"` UnsupportedDdlAction tfconfig.Variable `json:"unsupported_ddl_action,omitempty"` UseCachedResult tfconfig.Variable `json:"use_cached_result,omitempty"` + UserType tfconfig.Variable `json:"user_type,omitempty"` WeekOfYearPolicy tfconfig.Variable `json:"week_of_year_policy,omitempty"` WeekStart tfconfig.Variable `json:"week_start,omitempty"` @@ -190,6 +195,11 @@ func (u *UserModel) WithDateOutputFormat(dateOutputFormat string) *UserModel { return u } +func (u *UserModel) WithDaysToExpiry(daysToExpiry int) *UserModel { + u.DaysToExpiry = tfconfig.IntegerVariable(daysToExpiry) + return u +} + func (u *UserModel) WithDefaultNamespace(defaultNamespace string) *UserModel { u.DefaultNamespace = tfconfig.StringVariable(defaultNamespace) return u @@ -207,8 +217,13 @@ func (u *UserModel) WithDefaultWarehouse(defaultWarehouse string) *UserModel { return u } -func (u *UserModel) WithDisabled(disabled bool) *UserModel { - u.Disabled = tfconfig.BoolVariable(disabled) +func (u *UserModel) WithDisableMfa(disableMfa string) *UserModel { + u.DisableMfa = tfconfig.StringVariable(disableMfa) + return u +} + +func (u *UserModel) WithDisabled(disabled string) *UserModel { + u.Disabled = tfconfig.StringVariable(disabled) return u } @@ -262,11 +277,6 @@ func (u *UserModel) WithGeometryOutputFormat(geometryOutputFormat string) *UserM return u } -func (u *UserModel) WithHasRsaPublicKey(hasRsaPublicKey bool) *UserModel { - u.HasRsaPublicKey = tfconfig.BoolVariable(hasRsaPublicKey) - return u -} - func (u *UserModel) WithJdbcTreatDecimalAsInt(jdbcTreatDecimalAsInt bool) *UserModel { u.JdbcTreatDecimalAsInt = tfconfig.BoolVariable(jdbcTreatDecimalAsInt) return u @@ -307,13 +317,28 @@ func (u *UserModel) WithLoginName(loginName string) *UserModel { return u } +func (u *UserModel) WithMiddleName(middleName string) *UserModel { + u.MiddleName = tfconfig.StringVariable(middleName) + return u +} + +func (u *UserModel) WithMinsToBypassMfa(minsToBypassMfa int) *UserModel { + u.MinsToBypassMfa = tfconfig.IntegerVariable(minsToBypassMfa) + return u +} + +func (u *UserModel) WithMinsToUnlock(minsToUnlock int) *UserModel { + u.MinsToUnlock = tfconfig.IntegerVariable(minsToUnlock) + return u +} + func (u *UserModel) WithMultiStatementCount(multiStatementCount int) *UserModel { u.MultiStatementCount = tfconfig.IntegerVariable(multiStatementCount) return u } -func (u *UserModel) WithMustChangePassword(mustChangePassword bool) *UserModel { - u.MustChangePassword = tfconfig.BoolVariable(mustChangePassword) +func (u *UserModel) WithMustChangePassword(mustChangePassword string) *UserModel { + u.MustChangePassword = tfconfig.StringVariable(mustChangePassword) return u } @@ -482,6 +507,11 @@ func (u *UserModel) WithUseCachedResult(useCachedResult bool) *UserModel { return u } +func (u *UserModel) WithUserType(userType string) *UserModel { + u.UserType = tfconfig.StringVariable(userType) + return u +} + func (u *UserModel) WithWeekOfYearPolicy(weekOfYearPolicy int) *UserModel { u.WeekOfYearPolicy = tfconfig.IntegerVariable(weekOfYearPolicy) return u @@ -571,6 +601,11 @@ func (u *UserModel) WithDateOutputFormatValue(value tfconfig.Variable) *UserMode return u } +func (u *UserModel) WithDaysToExpiryValue(value tfconfig.Variable) *UserModel { + u.DaysToExpiry = value + return u +} + func (u *UserModel) WithDefaultNamespaceValue(value tfconfig.Variable) *UserModel { u.DefaultNamespace = value return u @@ -591,6 +626,11 @@ func (u *UserModel) WithDefaultWarehouseValue(value tfconfig.Variable) *UserMode return u } +func (u *UserModel) WithDisableMfaValue(value tfconfig.Variable) *UserModel { + u.DisableMfa = value + return u +} + func (u *UserModel) WithDisabledValue(value tfconfig.Variable) *UserModel { u.Disabled = value return u @@ -646,11 +686,6 @@ func (u *UserModel) WithGeometryOutputFormatValue(value tfconfig.Variable) *User return u } -func (u *UserModel) WithHasRsaPublicKeyValue(value tfconfig.Variable) *UserModel { - u.HasRsaPublicKey = value - return u -} - func (u *UserModel) WithJdbcTreatDecimalAsIntValue(value tfconfig.Variable) *UserModel { u.JdbcTreatDecimalAsInt = value return u @@ -691,6 +726,21 @@ func (u *UserModel) WithLoginNameValue(value tfconfig.Variable) *UserModel { return u } +func (u *UserModel) WithMiddleNameValue(value tfconfig.Variable) *UserModel { + u.MiddleName = value + return u +} + +func (u *UserModel) WithMinsToBypassMfaValue(value tfconfig.Variable) *UserModel { + u.MinsToBypassMfa = value + return u +} + +func (u *UserModel) WithMinsToUnlockValue(value tfconfig.Variable) *UserModel { + u.MinsToUnlock = value + return u +} + func (u *UserModel) WithMultiStatementCountValue(value tfconfig.Variable) *UserModel { u.MultiStatementCount = value return u @@ -866,6 +916,11 @@ func (u *UserModel) WithUseCachedResultValue(value tfconfig.Variable) *UserModel return u } +func (u *UserModel) WithUserTypeValue(value tfconfig.Variable) *UserModel { + u.UserType = value + return u +} + func (u *UserModel) WithWeekOfYearPolicyValue(value tfconfig.Variable) *UserModel { u.WeekOfYearPolicy = value return u diff --git a/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go index 62e2d6cadb..2dc0920dd1 100644 --- a/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go +++ b/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go @@ -10,22 +10,21 @@ import ( ) type ViewModel struct { - AggregationPolicy tfconfig.Variable `json:"aggregation_policy,omitempty"` - ChangeTracking tfconfig.Variable `json:"change_tracking,omitempty"` - Columns tfconfig.Variable `json:"columns,omitempty"` - Comment tfconfig.Variable `json:"comment,omitempty"` - CopyGrants tfconfig.Variable `json:"copy_grants,omitempty"` - DataMetricFunctions tfconfig.Variable `json:"data_metric_function,omitempty"` - DataMetricSchedule tfconfig.Variable `json:"data_metric_schedule,omitempty"` - Database tfconfig.Variable `json:"database,omitempty"` - IsRecursive tfconfig.Variable `json:"is_recursive,omitempty"` - IsSecure tfconfig.Variable `json:"is_secure,omitempty"` - IsTemporary tfconfig.Variable `json:"is_temporary,omitempty"` - Name tfconfig.Variable `json:"name,omitempty"` - OrReplace tfconfig.Variable `json:"or_replace,omitempty"` - RowAccessPolicy tfconfig.Variable `json:"row_access_policy,omitempty"` - Schema tfconfig.Variable `json:"schema,omitempty"` - Statement tfconfig.Variable `json:"statement,omitempty"` + AggregationPolicy tfconfig.Variable `json:"aggregation_policy,omitempty"` + ChangeTracking tfconfig.Variable `json:"change_tracking,omitempty"` + Comment tfconfig.Variable `json:"comment,omitempty"` + CopyGrants tfconfig.Variable `json:"copy_grants,omitempty"` + DataMetricFunction tfconfig.Variable `json:"data_metric_function,omitempty"` + DataMetricSchedule tfconfig.Variable `json:"data_metric_schedule,omitempty"` + Database tfconfig.Variable `json:"database,omitempty"` + FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + IsRecursive tfconfig.Variable `json:"is_recursive,omitempty"` + IsSecure tfconfig.Variable `json:"is_secure,omitempty"` + IsTemporary tfconfig.Variable `json:"is_temporary,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + RowAccessPolicy tfconfig.Variable `json:"row_access_policy,omitempty"` + Schema tfconfig.Variable `json:"schema,omitempty"` + Statement tfconfig.Variable `json:"statement,omitempty"` *config.ResourceModelMeta } @@ -36,7 +35,10 @@ type ViewModel struct { func View( resourceName string, - database string, name string, schema string, statement string, + database string, + name string, + schema string, + statement string, ) *ViewModel { v := &ViewModel{ResourceModelMeta: config.Meta(resourceName, resources.View)} v.WithDatabase(database) @@ -47,7 +49,10 @@ func View( } func ViewWithDefaultMeta( - database string, name string, schema string, statement string, + database string, + name string, + schema string, + statement string, ) *ViewModel { v := &ViewModel{ResourceModelMeta: config.DefaultMeta(resources.View)} v.WithDatabase(database) @@ -68,8 +73,6 @@ func (v *ViewModel) WithChangeTracking(changeTracking string) *ViewModel { return v } -// columns attribute type is not yet supported, so WithColumns can't be generated - func (v *ViewModel) WithComment(comment string) *ViewModel { v.Comment = tfconfig.StringVariable(comment) return v @@ -80,7 +83,7 @@ func (v *ViewModel) WithCopyGrants(copyGrants bool) *ViewModel { return v } -// data_metric_function attribute type is not yet supported, so WithDataMetricFunctions can't be generated +// data_metric_function attribute type is not yet supported, so WithDataMetricFunction can't be generated // data_metric_schedule attribute type is not yet supported, so WithDataMetricSchedule can't be generated @@ -89,6 +92,11 @@ func (v *ViewModel) WithDatabase(database string) *ViewModel { return v } +func (v *ViewModel) WithFullyQualifiedName(fullyQualifiedName string) *ViewModel { + v.FullyQualifiedName = tfconfig.StringVariable(fullyQualifiedName) + return v +} + func (v *ViewModel) WithIsRecursive(isRecursive string) *ViewModel { v.IsRecursive = tfconfig.StringVariable(isRecursive) return v @@ -109,11 +117,6 @@ func (v *ViewModel) WithName(name string) *ViewModel { return v } -func (v *ViewModel) WithOrReplace(orReplace bool) *ViewModel { - v.OrReplace = tfconfig.BoolVariable(orReplace) - return v -} - // row_access_policy attribute type is not yet supported, so WithRowAccessPolicy can't be generated func (v *ViewModel) WithSchema(schema string) *ViewModel { @@ -140,11 +143,6 @@ func (v *ViewModel) WithChangeTrackingValue(value tfconfig.Variable) *ViewModel return v } -func (v *ViewModel) WithColumnsValue(value tfconfig.Variable) *ViewModel { - v.Columns = value - return v -} - func (v *ViewModel) WithCommentValue(value tfconfig.Variable) *ViewModel { v.Comment = value return v @@ -155,8 +153,8 @@ func (v *ViewModel) WithCopyGrantsValue(value tfconfig.Variable) *ViewModel { return v } -func (v *ViewModel) WithDataMetricFunctionsValue(value tfconfig.Variable) *ViewModel { - v.DataMetricFunctions = value +func (v *ViewModel) WithDataMetricFunctionValue(value tfconfig.Variable) *ViewModel { + v.DataMetricFunction = value return v } @@ -170,6 +168,11 @@ func (v *ViewModel) WithDatabaseValue(value tfconfig.Variable) *ViewModel { return v } +func (v *ViewModel) WithFullyQualifiedNameValue(value tfconfig.Variable) *ViewModel { + v.FullyQualifiedName = value + return v +} + func (v *ViewModel) WithIsRecursiveValue(value tfconfig.Variable) *ViewModel { v.IsRecursive = value return v @@ -190,11 +193,6 @@ func (v *ViewModel) WithNameValue(value tfconfig.Variable) *ViewModel { return v } -func (v *ViewModel) WithOrReplaceValue(value tfconfig.Variable) *ViewModel { - v.OrReplace = value - return v -} - func (v *ViewModel) WithRowAccessPolicyValue(value tfconfig.Variable) *ViewModel { v.RowAccessPolicy = value return v diff --git a/pkg/acceptance/helpers/random/certs.go b/pkg/acceptance/helpers/random/certs.go index c4bc17460d..eb0cf4aaf9 100644 --- a/pkg/acceptance/helpers/random/certs.go +++ b/pkg/acceptance/helpers/random/certs.go @@ -18,7 +18,7 @@ import ( "github.com/stretchr/testify/require" ) -// Generate X509 returns base64 encoded certificate on a single line without the leading -----BEGIN CERTIFICATE----- and ending -----END CERTIFICATE----- markers. +// GenerateX509 returns base64 encoded certificate on a single line without the leading -----BEGIN CERTIFICATE----- and ending -----END CERTIFICATE----- markers. func GenerateX509(t *testing.T) string { t.Helper() ca := &x509.Certificate{ @@ -40,7 +40,7 @@ func GenerateX509(t *testing.T) string { return encode(t, "CERTIFICATE", caBytes) } -// GenerateRSA returns an RSA public key without BEGIN and END markers, and key's hash. +// GenerateRSAPublicKey returns an RSA public key without BEGIN and END markers, and key's hash. func GenerateRSAPublicKey(t *testing.T) (string, string) { t.Helper() key, err := rsa.GenerateKey(rand.Reader, 2048) diff --git a/pkg/acceptance/helpers/user_client.go b/pkg/acceptance/helpers/user_client.go index 54d5ce9995..16f9aeb106 100644 --- a/pkg/acceptance/helpers/user_client.go +++ b/pkg/acceptance/helpers/user_client.go @@ -2,6 +2,7 @@ package helpers import ( "context" + "fmt" "testing" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" @@ -65,6 +66,53 @@ func (c *UserClient) Disable(t *testing.T, id sdk.AccountObjectIdentifier) { t.Helper() ctx := context.Background() - err := c.client().Alter(ctx, id, &sdk.AlterUserOptions{Set: &sdk.UserSet{ObjectProperties: &sdk.UserObjectProperties{Disable: sdk.Bool(true)}}}) + err := c.client().Alter(ctx, id, &sdk.AlterUserOptions{ + Set: &sdk.UserSet{ + ObjectProperties: &sdk.UserAlterObjectProperties{ + UserObjectProperties: sdk.UserObjectProperties{ + Disable: sdk.Bool(true), + }, + }, + }, + }) + require.NoError(t, err) +} + +func (c *UserClient) SetDaysToExpiry(t *testing.T, id sdk.AccountObjectIdentifier, value int) { + t.Helper() + ctx := context.Background() + + err := c.client().Alter(ctx, id, &sdk.AlterUserOptions{ + Set: &sdk.UserSet{ + ObjectProperties: &sdk.UserAlterObjectProperties{ + UserObjectProperties: sdk.UserObjectProperties{ + DaysToExpiry: sdk.Int(value), + }, + }, + }, + }) + require.NoError(t, err) +} + +func (c *UserClient) SetType(t *testing.T, id sdk.AccountObjectIdentifier, value string) { + t.Helper() + ctx := context.Background() + + // TODO [SNOW-1645348]: use type from SDK + _, err := c.context.client.ExecForTests(ctx, fmt.Sprintf("ALTER USER %s SET TYPE = %s", id.FullyQualifiedName(), value)) + require.NoError(t, err) +} + +func (c *UserClient) UnsetDefaultSecondaryRoles(t *testing.T, id sdk.AccountObjectIdentifier) { + t.Helper() + ctx := context.Background() + + err := c.client().Alter(ctx, id, &sdk.AlterUserOptions{ + Unset: &sdk.UserUnset{ + ObjectProperties: &sdk.UserObjectPropertiesUnset{ + DefaultSecondaryRoles: sdk.Bool(true), + }, + }, + }) require.NoError(t, err) } diff --git a/pkg/internal/collections/collection_helpers.go b/pkg/internal/collections/collection_helpers.go index 0244492488..72950e189c 100644 --- a/pkg/internal/collections/collection_helpers.go +++ b/pkg/internal/collections/collection_helpers.go @@ -15,3 +15,11 @@ func FindOne[T any](collection []T, condition func(T) bool) (*T, error) { } return nil, ErrObjectNotFound } + +func Map[T any, R any](collection []T, mapper func(T) R) []R { + result := make([]R, len(collection)) + for i, elem := range collection { + result[i] = mapper(elem) + } + return result +} diff --git a/pkg/resources/custom_diffs.go b/pkg/resources/custom_diffs.go index 853c4fd55e..d49370eb73 100644 --- a/pkg/resources/custom_diffs.go +++ b/pkg/resources/custom_diffs.go @@ -83,7 +83,6 @@ func ForceNewIfChangeToEmptyString(key string) schema.CustomizeDiffFunc { }) } -// TODO [follow-up PR]: test func ComputedIfAnyAttributeChanged(key string, changedAttributeKeys ...string) schema.CustomizeDiffFunc { return customdiff.ComputedIf(key, func(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) bool { var result bool diff --git a/pkg/resources/database_commons.go b/pkg/resources/database_commons.go index 9dbe13ede1..ab40bc7085 100644 --- a/pkg/resources/database_commons.go +++ b/pkg/resources/database_commons.go @@ -51,6 +51,7 @@ func databaseParametersProviderFunc(c *sdk.Client) showParametersFunc[sdk.Accoun } func init() { + // TODO [SNOW-1645342]: use parameterDef databaseParameterFields := []struct { Name sdk.ObjectParameter Type schema.ValueType diff --git a/pkg/resources/deprecated_test.go b/pkg/resources/deprecated_test.go new file mode 100644 index 0000000000..815237aef8 --- /dev/null +++ b/pkg/resources/deprecated_test.go @@ -0,0 +1,24 @@ +package resources_test + +import ( + "fmt" + "strconv" + + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +// checkBool is deprecated and will be removed with resources rework (and replaced with new assertions) +func checkBool(path, attr string, value bool) func(*terraform.State) error { + return func(state *terraform.State) error { + is := state.RootModule().Resources[path].Primary + d := is.Attributes[attr] + b, err := strconv.ParseBool(d) + if err != nil { + return err + } + if b != value { + return fmt.Errorf("at %s expected %t but got %t", path, value, b) + } + return nil + } +} diff --git a/pkg/resources/diff_suppressions.go b/pkg/resources/diff_suppressions.go index 447bb970ce..5b47345c82 100644 --- a/pkg/resources/diff_suppressions.go +++ b/pkg/resources/diff_suppressions.go @@ -6,9 +6,9 @@ import ( "slices" "strings" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -57,6 +57,24 @@ func NormalizeAndCompareIdentifiersInSet(key string) schema.SchemaDiffSuppressFu } } +func SuppressCaseInSet(key string) schema.SchemaDiffSuppressFunc { + return func(k, oldValue, newValue string, d *schema.ResourceData) bool { + if strings.HasSuffix(k, ".#") { + return false + } + + if oldValue == "" && !d.GetRawState().IsNull() { + return slices.Contains(collections.Map(ctyValToSliceString(d.GetRawState().AsValueMap()[key].AsValueSet().Values()), strings.ToUpper), strings.ToUpper(newValue)) + } + + if newValue == "" { + return slices.Contains(collections.Map(expandStringList(d.Get(key).(*schema.Set).List()), strings.ToUpper), strings.ToUpper(oldValue)) + } + + return false + } +} + // IgnoreAfterCreation should be used to ignore changes to the given attribute post creation. func IgnoreAfterCreation(_, _, _ string, d *schema.ResourceData) bool { // For new resources always show the diff and in every other case we do not want to use this attribute diff --git a/pkg/resources/helpers.go b/pkg/resources/helpers.go index f88ae30bd2..a7c78d2532 100644 --- a/pkg/resources/helpers.go +++ b/pkg/resources/helpers.go @@ -12,10 +12,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func isOk(_ interface{}, ok bool) bool { - return ok -} - func dataTypeValidateFunc(val interface{}, _ string) (warns []string, errs []error) { if ok := sdk.IsValidDataType(val.(string)); !ok { errs = append(errs, fmt.Errorf("%v is not a valid data type", val)) @@ -80,33 +76,6 @@ func ignoreCaseAndTrimSpaceSuppressFunc(_, old, new string, _ *schema.ResourceDa return strings.EqualFold(strings.TrimSpace(old), strings.TrimSpace(new)) } -func setIntProperty(d *schema.ResourceData, key string, property *sdk.IntProperty) error { - if property != nil && property.Value != nil { - if err := d.Set(key, *property.Value); err != nil { - return err - } - } - return nil -} - -func setStringProperty(d *schema.ResourceData, key string, property *sdk.StringProperty) error { - if property != nil { - if err := d.Set(key, property.Value); err != nil { - return err - } - } - return nil -} - -func setBoolProperty(d *schema.ResourceData, key string, property *sdk.BoolProperty) error { - if property != nil { - if err := d.Set(key, property.Value); err != nil { - return err - } - } - return nil -} - func getTagObjectIdentifier(v map[string]any) sdk.ObjectIdentifier { if _, ok := v["database"]; ok { if _, ok := v["schema"]; ok { diff --git a/pkg/resources/resource_helpers_create.go b/pkg/resources/resource_helpers_create.go new file mode 100644 index 0000000000..c97d694998 --- /dev/null +++ b/pkg/resources/resource_helpers_create.go @@ -0,0 +1,63 @@ +package resources + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func stringAttributeCreate(d *schema.ResourceData, key string, createField **string) error { + if v, ok := d.GetOk(key); ok { + *createField = sdk.String(v.(string)) + } + return nil +} + +func intAttributeCreate(d *schema.ResourceData, key string, createField **int) error { + if v, ok := d.GetOk(key); ok { + *createField = sdk.Int(v.(int)) + } + return nil +} + +func intAttributeWithSpecialDefaultCreate(d *schema.ResourceData, key string, createField **int) error { + if v := d.Get(key).(int); v != IntDefault { + *createField = sdk.Int(v) + } + return nil +} + +func booleanStringAttributeCreate(d *schema.ResourceData, key string, createField **bool) error { + if v := d.Get(key).(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) + if err != nil { + return err + } + *createField = sdk.Bool(parsed) + } + return nil +} + +func accountObjectIdentifierAttributeCreate(d *schema.ResourceData, key string, createField **sdk.AccountObjectIdentifier) error { + if v, ok := d.GetOk(key); ok { + *createField = sdk.Pointer(sdk.NewAccountObjectIdentifier(v.(string))) + } + return nil +} + +func objectIdentifierAttributeCreate(d *schema.ResourceData, key string, createField **sdk.ObjectIdentifier) error { + if v, ok := d.GetOk(key); ok { + objectIdentifier, err := sdk.ParseObjectIdentifierString(v.(string)) + if err != nil { + return err + } + *createField = sdk.Pointer(objectIdentifier) + } + return nil +} + +func attributeDirectValueCreate[T any](d *schema.ResourceData, key string, createField **T, value *T) error { + if _, ok := d.GetOk(key); ok { + *createField = value + } + return nil +} diff --git a/pkg/resources/resource_helpers_read.go b/pkg/resources/resource_helpers_read.go new file mode 100644 index 0000000000..b0862858ba --- /dev/null +++ b/pkg/resources/resource_helpers_read.go @@ -0,0 +1,52 @@ +package resources + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// TODO [SNOW-1348101 - next PR]: rename all these methods to setFromXxxProperty +func setIntProperty(d *schema.ResourceData, key string, property *sdk.IntProperty) error { + if property != nil && property.Value != nil { + if err := d.Set(key, *property.Value); err != nil { + return err + } + } + return nil +} + +func setStringProperty(d *schema.ResourceData, key string, property *sdk.StringProperty) error { + if property != nil { + if err := d.Set(key, property.Value); err != nil { + return err + } + } + return nil +} + +func setStringPropertyIfNotEmpty(d *schema.ResourceData, key string, property *sdk.StringProperty) error { + if property != nil && property.Value != "" { + if err := d.Set(key, property.Value); err != nil { + return err + } + } + return nil +} + +func setBoolProperty(d *schema.ResourceData, key string, property *sdk.BoolProperty) error { + if property != nil { + if err := d.Set(key, property.Value); err != nil { + return err + } + } + return nil +} + +func setBooleanStringFromBoolProperty(d *schema.ResourceData, key string, property *sdk.BoolProperty) error { + if property != nil { + if err := d.Set(key, booleanStringFromBool(property.Value)); err != nil { + return err + } + } + return nil +} diff --git a/pkg/resources/resource_helpers_update.go b/pkg/resources/resource_helpers_update.go new file mode 100644 index 0000000000..602b2408b0 --- /dev/null +++ b/pkg/resources/resource_helpers_update.go @@ -0,0 +1,106 @@ +package resources + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func stringAttributeUpdate(d *schema.ResourceData, key string, setField **string, unsetField **bool) error { + if d.HasChange(key) { + if v, ok := d.GetOk(key); ok { + *setField = sdk.String(v.(string)) + } else { + *unsetField = sdk.Bool(true) + } + } + return nil +} + +func intAttributeUpdate(d *schema.ResourceData, key string, setField **int, unsetField **bool) error { + if d.HasChange(key) { + if v, ok := d.GetOk(key); ok { + *setField = sdk.Int(v.(int)) + } else { + *unsetField = sdk.Bool(true) + } + } + return nil +} + +func intAttributeWithSpecialDefaultUpdate(d *schema.ResourceData, key string, setField **int, unsetField **bool) error { + if d.HasChange(key) { + if v := d.Get(key).(int); v != IntDefault { + *setField = sdk.Int(v) + } else { + *unsetField = sdk.Bool(true) + } + } + return nil +} + +func booleanStringAttributeUpdate(d *schema.ResourceData, key string, setField **bool, unsetField **bool) error { + if d.HasChange(key) { + if v := d.Get(key).(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) + if err != nil { + return err + } + *setField = sdk.Bool(parsed) + } else { + *unsetField = sdk.Bool(true) + } + } + return nil +} + +func booleanStringAttributeUnsetFallbackUpdate(d *schema.ResourceData, key string, setField **bool, fallbackValue bool) error { + if d.HasChange(key) { + if v := d.Get(key).(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) + if err != nil { + return err + } + *setField = sdk.Bool(parsed) + } else { + *setField = sdk.Bool(fallbackValue) + } + } + return nil +} + +func accountObjectIdentifierAttributeUpdate(d *schema.ResourceData, key string, setField **sdk.AccountObjectIdentifier, unsetField **bool) error { + if d.HasChange(key) { + if v, ok := d.GetOk(key); ok { + *setField = sdk.Pointer(sdk.NewAccountObjectIdentifier(v.(string))) + } else { + *unsetField = sdk.Bool(true) + } + } + return nil +} + +func objectIdentifierAttributeUpdate(d *schema.ResourceData, key string, setField **sdk.ObjectIdentifier, unsetField **bool) error { + if d.HasChange(key) { + if v, ok := d.GetOk(key); ok { + objectIdentifier, err := sdk.ParseObjectIdentifierString(v.(string)) + if err != nil { + return err + } + *setField = sdk.Pointer(objectIdentifier) + } else { + *unsetField = sdk.Bool(true) + } + } + return nil +} + +func attributeDirectValueUpdate[T any](d *schema.ResourceData, key string, setField **T, value *T, unsetField **bool) error { + if d.HasChange(key) { + if _, ok := d.GetOk(key); ok { + *setField = value + } else { + *unsetField = sdk.Bool(true) + } + } + return nil +} diff --git a/pkg/resources/schema.go b/pkg/resources/schema.go index e485292ede..55f54d02cf 100644 --- a/pkg/resources/schema.go +++ b/pkg/resources/schema.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" @@ -99,26 +100,7 @@ func Schema() *schema.Resource { ComputedIfAnyAttributeChangedWithSuppressDiff(ShowOutputAttributeName, suppressIdentifierQuoting, "name"), ComputedIfAnyAttributeChangedWithSuppressDiff(DescribeOutputAttributeName, suppressIdentifierQuoting, "name"), ComputedIfAnyAttributeChangedWithSuppressDiff(FullyQualifiedNameAttributeName, suppressIdentifierQuoting, "name"), - // TODO [SNOW-1348101]: use list from schema parameters definition instead listing all here (after moving to the SDK) - ComputedIfAnyAttributeChanged(ParametersAttributeName, - strings.ToLower(string(sdk.ObjectParameterDataRetentionTimeInDays)), - strings.ToLower(string(sdk.ObjectParameterMaxDataExtensionTimeInDays)), - strings.ToLower(string(sdk.ObjectParameterExternalVolume)), - strings.ToLower(string(sdk.ObjectParameterCatalog)), - strings.ToLower(string(sdk.ObjectParameterReplaceInvalidCharacters)), - strings.ToLower(string(sdk.ObjectParameterDefaultDDLCollation)), - strings.ToLower(string(sdk.ObjectParameterStorageSerializationPolicy)), - strings.ToLower(string(sdk.ObjectParameterLogLevel)), - strings.ToLower(string(sdk.ObjectParameterTraceLevel)), - strings.ToLower(string(sdk.ObjectParameterSuspendTaskAfterNumFailures)), - strings.ToLower(string(sdk.ObjectParameterTaskAutoRetryAttempts)), - strings.ToLower(string(sdk.ObjectParameterUserTaskManagedInitialWarehouseSize)), - strings.ToLower(string(sdk.ObjectParameterUserTaskTimeoutMs)), - strings.ToLower(string(sdk.ObjectParameterUserTaskMinimumTriggerIntervalInSeconds)), - strings.ToLower(string(sdk.ObjectParameterQuotedIdentifiersIgnoreCase)), - strings.ToLower(string(sdk.ObjectParameterEnableConsoleOutput)), - strings.ToLower(string(sdk.ObjectParameterPipeExecutionPaused)), - ), + ComputedIfAnyAttributeChanged(ParametersAttributeName, collections.Map(sdk.AsStringList(sdk.AllSchemaParameters), strings.ToLower)...), schemaParametersCustomDiff, ), diff --git a/pkg/resources/schema_parameters.go b/pkg/resources/schema_parameters.go index 9f81ab05c6..fe8deedbcc 100644 --- a/pkg/resources/schema_parameters.go +++ b/pkg/resources/schema_parameters.go @@ -37,13 +37,7 @@ var ( ) func init() { - // TODO [SNOW-1348101][next PR]: merge this struct with the one in user parameters - type parameterDef struct { - Name sdk.ObjectParameter - Type schema.ValueType - Description string - } - additionalSchemaParameterFields := []parameterDef{ + additionalSchemaParameterFields := []parameterDef[sdk.ObjectParameter]{ {Name: sdk.ObjectParameterPipeExecutionPaused, Type: schema.TypeBool, Description: "Specifies whether to pause a running pipe, primarily in preparation for transferring ownership of the pipe to a different role."}, } diff --git a/pkg/resources/testdata/userkey1 b/pkg/resources/testdata/userkey1 deleted file mode 100644 index 84d01dfd98..0000000000 --- a/pkg/resources/testdata/userkey1 +++ /dev/null @@ -1 +0,0 @@ -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt+6NUOR4njk6/77p0XXcGvtf1gdkLqnHor7nA9JpClCvEPCNhAm3zJ8wWYZu1bcXIu4ZYNAWsbwsVy9l93HonS0aQAV3QjSu+aY8uIdUiVdSY8rYZfKKWrlLr4HNrUjHvGQ7tZ+HVQgGtqMQec8ib8FyCBsRLSs2PEaT/8i1lM8dLru1+wMP0nc0OktXIEsDPJvi7L9bQZ9qWLgbbJofiFEprPfOCJlhiEbSEdcSUNQsdRgxnKvHQ8+W4sWNBXlXY4Lsr+w+YwaUBu3sKvFA8BVw/SO9VvL+B8edqVG/ittrmt0lpKXmd696Q5OuaFo53m4fIMVP+aX+jqiOl/491QIDAQAB \ No newline at end of file diff --git a/pkg/resources/testdata/userkey2 b/pkg/resources/testdata/userkey2 deleted file mode 100644 index 2da5189067..0000000000 --- a/pkg/resources/testdata/userkey2 +++ /dev/null @@ -1 +0,0 @@ -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2jnJgXfwKUuAW+lqEmlr3GJKCGcjpSVU+0BDAchuH1AoPsrZfI6138p+pEaDJiRcD7Dv8QareGyOjHNWrSc2YH9Mwe8DMY+WFonQ5/VRqzFflisu4eFqXtvLGvp3atCySpkVavuj6BtrlJMgQqMpwrFrbes6KTHHu+/nzsXWH3h18kHWzpvBrJ1a4ulUuzqqSSegCkfHYE4xYurlibD+FO1liaOn8jLv37NztrvEMuoPnodsIMO7fPT9K21OfvOmM/sEnC7uRKztMVBcEVEAPp2Y4vU0NJ3usDN80N0szQlatK+jgUog7urnDqHSGZUES1oT3rXsrLaGSfE0wLS7/QIDAQAB \ No newline at end of file diff --git a/pkg/resources/user.go b/pkg/resources/user.go index 513329a13a..6c1bef83ee 100644 --- a/pkg/resources/user.go +++ b/pkg/resources/user.go @@ -5,74 +5,139 @@ import ( "errors" "fmt" "log" + "slices" "strings" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/logging" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +// TODO [SNOW-1348101 - next PR]: update all changes in README +// TODO [SNOW-1348101 - next PR]: add IgnoreChangeToCurrentSnowflakeValueInShow and other suppressors var userSchema = map[string]*schema.Schema{ "name": { + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("Name of the user. Note that if you do not supply login_name this will be used as login_name. Check the [docs](https://docs.snowflake.net/manuals/sql-reference/sql/create-user.html#required-parameters)."), + DiffSuppressFunc: suppressIdentifierQuoting, + }, + "password": { Type: schema.TypeString, - Required: true, - Description: "Name of the user. Note that if you do not supply login_name this will be used as login_name. [doc](https://docs.snowflake.net/manuals/sql-reference/sql/create-user.html#required-parameters)", + Optional: true, + Sensitive: true, + Description: "Password for the user. **WARNING:** this will put the password in the terraform state file. Use carefully.", }, "login_name": { Type: schema.TypeString, Optional: true, - Computed: true, Sensitive: true, - Description: "The name users use to log in. If not supplied, snowflake will use name instead.", + Description: "The name users use to log in. If not supplied, snowflake will use name instead. Login names are always case-insensitive.", // login_name is case-insensitive DiffSuppressFunc: ignoreCaseSuppressFunc, }, - "comment": { - Type: schema.TypeString, - Optional: true, - // TODO validation + // TODO [SNOW-1348101 - next PR]: handle external changes and the default behavior correctly; same with the login_name + "display_name": { + Type: schema.TypeString, + Optional: true, + Description: "Name displayed for the user in the Snowflake web interface.", }, - "password": { + "first_name": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "First name of the user.", + }, + "middle_name": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Middle name of the user.", + }, + "last_name": { Type: schema.TypeString, Optional: true, Sensitive: true, - Description: "**WARNING:** this will put the password in the terraform state file. Use carefully.", - // TODO validation https://docs.snowflake.net/manuals/sql-reference/sql/create-user.html#optional-parameters + Description: "Last name of the user.", + }, + "email": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Email address for the user.", + }, + "must_change_password": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: validateBooleanString, + Description: booleanStringFieldDescription("Specifies whether the user is forced to change their password on next login (including their first/initial login) into the system."), + Default: BooleanDefault, }, "disabled": { - Type: schema.TypeBool, - Optional: true, - Computed: true, + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: validateBooleanString, + Description: booleanStringFieldDescription("Specifies whether the user is disabled, which prevents logging in and aborts all the currently-running queries for the user."), + Default: BooleanDefault, + }, + // TODO [SNOW-1348101 - next PR]: consider handling external change to 0 or from 0? + "days_to_expiry": { + Type: schema.TypeInt, + Optional: true, + Description: "Specifies the number of days after which the user status is set to `Expired` and the user is no longer allowed to log in. This is useful for defining temporary users (i.e. users who should only have access to Snowflake for a limited time period). In general, you should not set this property for [account administrators](https://docs.snowflake.com/en/user-guide/security-access-control-considerations.html#label-accountadmin-users) (i.e. users with the `ACCOUNTADMIN` role) because Snowflake locks them out when they become `Expired`.", + }, + "mins_to_unlock": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), + Default: IntDefault, + Description: "Specifies the number of minutes until the temporary lock on the user login is cleared. To protect against unauthorized user login, Snowflake places a temporary lock on a user after five consecutive unsuccessful login attempts. When creating a user, this property can be set to prevent them from logging in until the specified amount of time passes. To remove a lock immediately for a user, specify a value of 0 for this parameter.", }, "default_warehouse": { Type: schema.TypeString, Optional: true, DiffSuppressFunc: suppressIdentifierQuoting, - Description: "Specifies the virtual warehouse that is active by default for the user’s session upon login.", + Description: "Specifies the virtual warehouse that is active by default for the user’s session upon login. Note that the CREATE USER operation does not verify that the warehouse exists.", }, // TODO [SNOW-1348101 - next PR]: check the exact behavior of default_namespace and default_role because it looks like it is handled in a case-insensitive manner on Snowflake side "default_namespace": { Type: schema.TypeString, Optional: true, DiffSuppressFunc: suppressIdentifierQuoting, - Description: "Specifies the namespace (database only or database and schema) that is active by default for the user’s session upon login.", + Description: "Specifies the namespace (database only or database and schema) that is active by default for the user’s session upon login. Note that the CREATE USER operation does not verify that the namespace exists.", }, "default_role": { Type: schema.TypeString, Optional: true, - Computed: true, DiffSuppressFunc: suppressIdentifierQuoting, - Description: "Specifies the role that is active by default for the user’s session upon login.", + Description: "Specifies the role that is active by default for the user’s session upon login. Note that specifying a default role for a user does **not** grant the role to the user. The role must be granted explicitly to the user using the [GRANT ROLE](https://docs.snowflake.com/en/sql-reference/sql/grant-role) command. In addition, the CREATE USER operation does not verify that the role exists.", }, "default_secondary_roles": { - Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, - Optional: true, - Description: "Specifies the set of secondary roles that are active for the user’s session upon login. Currently only [\"ALL\"] value is supported - more information can be found in [doc](https://docs.snowflake.com/en/sql-reference/sql/create-user#optional-object-properties-objectproperties)", + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: isValidSecondaryRole(), + }, + DiffSuppressFunc: SuppressCaseInSet("default_secondary_roles"), + MaxItems: 1, + MinItems: 1, + Optional: true, + Description: "Specifies the set of secondary roles that are active for the user’s session upon login. Currently only [\"ALL\"] value is supported - more information can be found in [doc](https://docs.snowflake.com/en/sql-reference/sql/create-user#optional-object-properties-objectproperties).", + }, + // TODO [SNOW-1348101 - next PR]: note that external changes are not handled (and with other params that this is true) + "mins_to_bypass_mfa": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), + Default: IntDefault, + Description: "Specifies the number of minutes to temporarily bypass MFA for the user. This property can be used to allow a MFA-enrolled user to temporarily bypass MFA during login in the event that their MFA device is not available.", }, "rsa_public_key": { Type: schema.TypeString, @@ -84,52 +149,23 @@ var userSchema = map[string]*schema.Schema{ Optional: true, Description: "Specifies the user’s second RSA public key; used to rotate the public and private keys for key-pair authentication based on an expiration schedule set by your organization. Must be on 1 line without header and trailer.", }, - "has_rsa_public_key": { - Type: schema.TypeBool, - Computed: true, - Description: "Will be true if user as an RSA key set.", - }, - "must_change_password": { - Type: schema.TypeBool, - Optional: true, - Description: "Specifies whether the user is forced to change their password on next login (including their first/initial login) into the system.", - }, - "email": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - Description: "Email address for the user.", - }, - "display_name": { + "comment": { Type: schema.TypeString, - Computed: true, Optional: true, - Sensitive: true, - Description: "Name displayed for the user in the Snowflake web interface.", + Description: "Specifies a comment for the user.", }, - "first_name": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - Description: "First name of the user.", + "disable_mfa": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: validateBooleanString, + Description: booleanStringFieldDescription("Allows enabling or disabling [multi-factor authentication](https://docs.snowflake.com/en/user-guide/security-mfa)."), + Default: BooleanDefault, }, - "last_name": { + "user_type": { Type: schema.TypeString, - Optional: true, - Sensitive: true, - Description: "Last name of the user.", + Computed: true, + Description: "Specifies a type for the user.", }, - // MIDDLE_NAME = - // SNOWFLAKE_LOCK = TRUE | FALSE - // SNOWFLAKE_SUPPORT = TRUE | FALSE - // TODO [SNOW-1348101 - next PR]: handle #1155 by either forceNew or not reading this value from SF (because it changes constantly after setting; check https://docs.snowflake.com/en/sql-reference/sql/create-user#optional-object-properties-objectproperties) - // DAYS_TO_EXPIRY = - // MINS_TO_UNLOCK = - // EXT_AUTHN_DUO = TRUE | FALSE - // EXT_AUTHN_UID = - // MINS_TO_BYPASS_MFA = - // DISABLE_MFA = TRUE | FALSE - // MINS_TO_BYPASS_NETWORK POLICY = ShowOutputAttributeName: { Type: schema.TypeList, Computed: true, @@ -155,23 +191,62 @@ func User() *schema.Resource { UpdateContext: UpdateUser, ReadContext: GetReadUserFunc(true), DeleteContext: DeleteUser, + Description: "Resource used to manage user objects. For more information, check [user documentation](https://docs.snowflake.com/en/sql-reference/commands-user-role).", Schema: helpers.MergeMaps(userSchema, userParametersSchema), Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: ImportUser, }, CustomizeDiff: customdiff.All( - // TODO [SNOW-1348101]: fill after adding all the attributes - // ComputedIfAnyAttributeChanged(ShowOutputAttributeName), - // TODO [SNOW-1348101]: use list from user parameters definition instead listing all here - ComputedIfAnyAttributeChanged(ParametersAttributeName, strings.ToLower(string(sdk.UserParameterAbortDetachedQuery)), strings.ToLower(string(sdk.UserParameterAutocommit)), strings.ToLower(string(sdk.UserParameterBinaryInputFormat)), strings.ToLower(string(sdk.UserParameterBinaryOutputFormat)), strings.ToLower(string(sdk.UserParameterClientMemoryLimit)), strings.ToLower(string(sdk.UserParameterClientMetadataRequestUseConnectionCtx)), strings.ToLower(string(sdk.UserParameterClientPrefetchThreads)), strings.ToLower(string(sdk.UserParameterClientResultChunkSize)), strings.ToLower(string(sdk.UserParameterClientResultColumnCaseInsensitive)), strings.ToLower(string(sdk.UserParameterClientSessionKeepAlive)), strings.ToLower(string(sdk.UserParameterClientSessionKeepAliveHeartbeatFrequency)), strings.ToLower(string(sdk.UserParameterClientTimestampTypeMapping)), strings.ToLower(string(sdk.UserParameterDateInputFormat)), strings.ToLower(string(sdk.UserParameterDateOutputFormat)), strings.ToLower(string(sdk.UserParameterEnableUnloadPhysicalTypeOptimization)), strings.ToLower(string(sdk.UserParameterErrorOnNondeterministicMerge)), strings.ToLower(string(sdk.UserParameterErrorOnNondeterministicUpdate)), strings.ToLower(string(sdk.UserParameterGeographyOutputFormat)), strings.ToLower(string(sdk.UserParameterGeometryOutputFormat)), strings.ToLower(string(sdk.UserParameterJdbcTreatDecimalAsInt)), strings.ToLower(string(sdk.UserParameterJdbcTreatTimestampNtzAsUtc)), strings.ToLower(string(sdk.UserParameterJdbcUseSessionTimezone)), strings.ToLower(string(sdk.UserParameterJsonIndent)), strings.ToLower(string(sdk.UserParameterLockTimeout)), strings.ToLower(string(sdk.UserParameterLogLevel)), strings.ToLower(string(sdk.UserParameterMultiStatementCount)), strings.ToLower(string(sdk.UserParameterNoorderSequenceAsDefault)), strings.ToLower(string(sdk.UserParameterOdbcTreatDecimalAsInt)), strings.ToLower(string(sdk.UserParameterQueryTag)), strings.ToLower(string(sdk.UserParameterQuotedIdentifiersIgnoreCase)), strings.ToLower(string(sdk.UserParameterRowsPerResultset)), strings.ToLower(string(sdk.UserParameterS3StageVpceDnsName)), strings.ToLower(string(sdk.UserParameterSearchPath)), strings.ToLower(string(sdk.UserParameterSimulatedDataSharingConsumer)), strings.ToLower(string(sdk.UserParameterStatementQueuedTimeoutInSeconds)), strings.ToLower(string(sdk.UserParameterStatementTimeoutInSeconds)), strings.ToLower(string(sdk.UserParameterStrictJsonOutput)), strings.ToLower(string(sdk.UserParameterTimestampDayIsAlways24h)), strings.ToLower(string(sdk.UserParameterTimestampInputFormat)), strings.ToLower(string(sdk.UserParameterTimestampLtzOutputFormat)), strings.ToLower(string(sdk.UserParameterTimestampNtzOutputFormat)), strings.ToLower(string(sdk.UserParameterTimestampOutputFormat)), strings.ToLower(string(sdk.UserParameterTimestampTypeMapping)), strings.ToLower(string(sdk.UserParameterTimestampTzOutputFormat)), strings.ToLower(string(sdk.UserParameterTimezone)), strings.ToLower(string(sdk.UserParameterTimeInputFormat)), strings.ToLower(string(sdk.UserParameterTimeOutputFormat)), strings.ToLower(string(sdk.UserParameterTraceLevel)), strings.ToLower(string(sdk.UserParameterTransactionAbortOnError)), strings.ToLower(string(sdk.UserParameterTransactionDefaultIsolationLevel)), strings.ToLower(string(sdk.UserParameterTwoDigitCenturyStart)), strings.ToLower(string(sdk.UserParameterUnsupportedDdlAction)), strings.ToLower(string(sdk.UserParameterUseCachedResult)), strings.ToLower(string(sdk.UserParameterWeekOfYearPolicy)), strings.ToLower(string(sdk.UserParameterWeekStart)), strings.ToLower(string(sdk.UserParameterEnableUnredactedQuerySyntaxError)), strings.ToLower(string(sdk.UserParameterNetworkPolicy)), strings.ToLower(string(sdk.UserParameterPreventUnloadToInternalStages))), + // TODO [SNOW-1629468 - next pr]: handle diff suppression correctly; add "default_role", "default_secondary_roles" + ComputedIfAnyAttributeChanged(ShowOutputAttributeName, "password", "login_name", "display_name", "first_name", "middle_name", "last_name", "email", "must_change_password", "disabled", "days_to_expiry", "mins_to_unlock", "default_warehouse", "default_namespace", "mins_to_bypass_mfa", "rsa_public_key", "rsa_public_key_2", "comment", "disable_mfa"), + ComputedIfAnyAttributeChanged(ParametersAttributeName, collections.Map(sdk.AsStringList(sdk.AllUserParameters), strings.ToLower)...), ComputedIfAnyAttributeChanged(FullyQualifiedNameAttributeName, "name"), userParametersCustomDiff, + // TODO [SNOW-1645348]: revisit with service user work + func(_ context.Context, diff *schema.ResourceDiff, _ interface{}) error { + if n := diff.Get("user_type"); n != nil { + logging.DebugLogger.Printf("[DEBUG] new external value for user type %s\n", n.(string)) + if !slices.Contains([]string{"", "PERSON"}, strings.ToUpper(n.(string))) { + return errors.Join(diff.SetNewComputed("user_type"), diff.ForceNew("user_type")) + } + } + return nil + }, ), } } +func ImportUser(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + logging.DebugLogger.Printf("[DEBUG] Starting user import") + client := meta.(*provider.Context).Client + id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + if err != nil { + return nil, err + } + + userDetails, err := client.Users.Describe(ctx, id) + if err != nil { + return nil, err + } + + err = errors.Join( + d.Set("name", id.Name()), + setStringPropertyIfNotEmpty(d, "login_name", userDetails.LoginName), + setStringPropertyIfNotEmpty(d, "display_name", userDetails.DisplayName), + setStringPropertyIfNotEmpty(d, "default_namespace", userDetails.DefaultNamespace), + setBooleanStringFromBoolProperty(d, "must_change_password", userDetails.MustChangePassword), + setBooleanStringFromBoolProperty(d, "disabled", userDetails.Disabled), + // all others are set in read + ) + if err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + func CreateUser(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client @@ -181,84 +256,78 @@ func CreateUser(ctx context.Context, d *schema.ResourceData, meta any) diag.Diag SessionParameters: &sdk.SessionParameters{}, } name := d.Get("name").(string) - objectIdentifier := sdk.NewAccountObjectIdentifier(name) + id := sdk.NewAccountObjectIdentifier(name) + + errs := errors.Join( + stringAttributeCreate(d, "password", &opts.ObjectProperties.Password), + stringAttributeCreate(d, "login_name", &opts.ObjectProperties.LoginName), + stringAttributeCreate(d, "display_name", &opts.ObjectProperties.DisplayName), + stringAttributeCreate(d, "first_name", &opts.ObjectProperties.FirstName), + stringAttributeCreate(d, "middle_name", &opts.ObjectProperties.MiddleName), + stringAttributeCreate(d, "last_name", &opts.ObjectProperties.LastName), + stringAttributeCreate(d, "email", &opts.ObjectProperties.Email), + booleanStringAttributeCreate(d, "must_change_password", &opts.ObjectProperties.MustChangePassword), + booleanStringAttributeCreate(d, "disabled", &opts.ObjectProperties.Disable), + intAttributeCreate(d, "days_to_expiry", &opts.ObjectProperties.DaysToExpiry), + intAttributeWithSpecialDefaultCreate(d, "mins_to_unlock", &opts.ObjectProperties.MinsToUnlock), + accountObjectIdentifierAttributeCreate(d, "default_warehouse", &opts.ObjectProperties.DefaultWarehouse), + objectIdentifierAttributeCreate(d, "default_namespace", &opts.ObjectProperties.DefaultNamespace), + accountObjectIdentifierAttributeCreate(d, "default_role", &opts.ObjectProperties.DefaultRole), + // We do not need value because it is validated on the schema level and ALL is the only supported value currently. + // Check more in https://docs.snowflake.com/en/sql-reference/sql/create-user#optional-object-properties-objectproperties. + attributeDirectValueCreate(d, "default_secondary_roles", &opts.ObjectProperties.DefaultSecondaryRoles, &sdk.SecondaryRoles{}), + intAttributeWithSpecialDefaultCreate(d, "mins_to_bypass_mfa", &opts.ObjectProperties.MinsToBypassMFA), + stringAttributeCreate(d, "rsa_public_key", &opts.ObjectProperties.RSAPublicKey), + stringAttributeCreate(d, "rsa_public_key_2", &opts.ObjectProperties.RSAPublicKey2), + stringAttributeCreate(d, "comment", &opts.ObjectProperties.Comment), + // disable mfa cannot be set in create, alter is run after creation + ) + if errs != nil { + return diag.FromErr(errs) + } if parametersCreateDiags := handleUserParametersCreate(d, opts); len(parametersCreateDiags) > 0 { return parametersCreateDiags } - if loginName, ok := d.GetOk("login_name"); ok { - opts.ObjectProperties.LoginName = sdk.String(loginName.(string)) + err := client.Users.Create(ctx, id, opts) + if err != nil { + return diag.FromErr(err) } + d.SetId(helpers.EncodeResourceIdentifier(id)) - if comment, ok := d.GetOk("comment"); ok { - opts.ObjectProperties.Comment = sdk.String(comment.(string)) - } - if password, ok := d.GetOk("password"); ok { - opts.ObjectProperties.Password = sdk.String(password.(string)) - } - if v, ok := d.GetOk("disabled"); ok { - disabled := v.(bool) - opts.ObjectProperties.Disable = &disabled - } - if defaultWarehouse, ok := d.GetOk("default_warehouse"); ok { - opts.ObjectProperties.DefaultWarehouse = sdk.Pointer(sdk.NewAccountObjectIdentifierFromFullyQualifiedName(defaultWarehouse.(string))) - } - if defaultNamespace, ok := d.GetOk("default_namespace"); ok { - defaultNamespaceId, err := helpers.DecodeSnowflakeParameterID(defaultNamespace.(string)) + // disable mfa cannot be set in create, we need to alter if set in config + var diags diag.Diagnostics + if disableMfa := d.Get("disable_mfa").(string); disableMfa != BooleanDefault { + parsed, err := booleanStringToBool(disableMfa) if err != nil { - return diag.FromErr(err) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("Setting disable mfa failed after create for user %s, err: %v", id.FullyQualifiedName(), err), + }) } - opts.ObjectProperties.DefaultNamespace = sdk.Pointer(defaultNamespaceId) - } - if displayName, ok := d.GetOk("display_name"); ok { - opts.ObjectProperties.DisplayName = sdk.String(displayName.(string)) - } - if defaultRole, ok := d.GetOk("default_role"); ok { - opts.ObjectProperties.DefaultRole = sdk.Pointer(sdk.NewAccountObjectIdentifierFromFullyQualifiedName(defaultRole.(string))) - } - if v, ok := d.GetOk("default_secondary_roles"); ok { - roles := expandStringList(v.(*schema.Set).List()) - secondaryRoles := []sdk.SecondaryRole{} - for _, role := range roles { - secondaryRoles = append(secondaryRoles, sdk.SecondaryRole{Value: role}) + alterDisableMfa := sdk.AlterUserOptions{Set: &sdk.UserSet{ObjectProperties: &sdk.UserAlterObjectProperties{DisableMfa: sdk.Bool(parsed)}}} + err = client.Users.Alter(ctx, id, &alterDisableMfa) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("Setting disable mfa failed after create for user %s, err: %v", id.FullyQualifiedName(), err), + }) } - opts.ObjectProperties.DefaultSecondaryRoles = &sdk.SecondaryRoles{Roles: secondaryRoles} - } - if rsaPublicKey, ok := d.GetOk("rsa_public_key"); ok { - opts.ObjectProperties.RSAPublicKey = sdk.String(rsaPublicKey.(string)) - } - if rsaPublicKey2, ok := d.GetOk("rsa_public_key_2"); ok { - opts.ObjectProperties.RSAPublicKey2 = sdk.String(rsaPublicKey2.(string)) } - if v, ok := d.GetOk("must_change_password"); ok { - mustChangePassword := v.(bool) - opts.ObjectProperties.MustChangePassword = &mustChangePassword - } - if email, ok := d.GetOk("email"); ok { - opts.ObjectProperties.Email = sdk.String(email.(string)) - } - if firstName, ok := d.GetOk("first_name"); ok { - opts.ObjectProperties.FirstName = sdk.String(firstName.(string)) - } - if lastName, ok := d.GetOk("last_name"); ok { - opts.ObjectProperties.LastName = sdk.String(lastName.(string)) - } - err := client.Users.Create(ctx, objectIdentifier, opts) - if err != nil { - return diag.FromErr(err) - } - d.SetId(helpers.EncodeSnowflakeID(objectIdentifier)) - return GetReadUserFunc(false)(ctx, d, meta) + + return append(diags, GetReadUserFunc(false)(ctx, d, meta)...) } func GetReadUserFunc(withExternalChangesMarking bool) schema.ReadContextFunc { return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - // We use User.Describe instead of User.Show because the "SHOW USERS ..." command - // requires the "MANAGE GRANTS" global privilege - id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) - user, err := client.Users.Describe(ctx, id) + id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + userDetails, err := client.Users.Describe(ctx, id) if err != nil { if errors.Is(err, sdk.ErrObjectNotExistOrAuthorized) { log.Printf("[DEBUG] user (%s) not found or we are not authorized. Err: %s", d.Id(), err) @@ -293,66 +362,62 @@ func GetReadUserFunc(withExternalChangesMarking bool) schema.ReadContextFunc { return diag.FromErr(err) } - if err := d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()); err != nil { - return diag.FromErr(err) - } - - if err := setStringProperty(d, "name", user.Name); err != nil { - return diag.FromErr(err) - } - if err := setStringProperty(d, "comment", user.Comment); err != nil { - return diag.FromErr(err) - } - if err := setStringProperty(d, "login_name", user.LoginName); err != nil { - return diag.FromErr(err) - } - if err := setBoolProperty(d, "disabled", user.Disabled); err != nil { - return diag.FromErr(err) - } - if err := setStringProperty(d, "default_role", user.DefaultRole); err != nil { - return diag.FromErr(err) - } - - var defaultSecondaryRoles []string - if user.DefaultSecondaryRoles != nil && len(user.DefaultSecondaryRoles.Value) > 0 { - defaultSecondaryRoles = sdk.ParseCommaSeparatedStringArray(user.DefaultSecondaryRoles.Value, true) - } - if err = d.Set("default_secondary_roles", defaultSecondaryRoles); err != nil { - return diag.FromErr(err) - } - if err := setStringProperty(d, "default_namespace", user.DefaultNamespace); err != nil { - return diag.FromErr(err) - } - if err := setStringProperty(d, "default_warehouse", user.DefaultWarehouse); err != nil { - return diag.FromErr(err) - } - if user.RsaPublicKeyFp != nil { - if err = d.Set("has_rsa_public_key", user.RsaPublicKeyFp.Value != ""); err != nil { + if withExternalChangesMarking { + if err = handleExternalChangesToObjectInShow(d, + showMapping{"login_name", "login_name", u.LoginName, u.LoginName, nil}, + showMapping{"display_name", "display_name", u.DisplayName, u.DisplayName, nil}, + showMapping{"must_change_password", "must_change_password", u.MustChangePassword, fmt.Sprintf("%t", u.MustChangePassword), nil}, + showMapping{"disabled", "disabled", u.Disabled, fmt.Sprintf("%t", u.Disabled), nil}, + showMapping{"default_namespace", "default_namespace", u.DefaultNamespace, u.DefaultNamespace, nil}, + ); err != nil { return diag.FromErr(err) } } - if err := setStringProperty(d, "email", user.Email); err != nil { - return diag.FromErr(err) - } - if err := setStringProperty(d, "display_name", user.DisplayName); err != nil { - return diag.FromErr(err) - } - if err := setStringProperty(d, "first_name", user.FirstName); err != nil { - return diag.FromErr(err) - } - if err := setStringProperty(d, "last_name", user.LastName); err != nil { - return diag.FromErr(err) - } - - if diags := handleUserParameterRead(d, userParameters); diags != nil { - return diags - } - if err = d.Set(ShowOutputAttributeName, []map[string]any{schemas.UserToSchema(u)}); err != nil { + if err = setStateToValuesFromConfig(d, userSchema, []string{ + "login_name", + "display_name", + "must_change_password", + "disabled", + "default_namespace", + }); err != nil { return diag.FromErr(err) } - if err = d.Set(ParametersAttributeName, []map[string]any{schemas.UserParametersToSchema(userParameters)}); err != nil { + var defaultSecondaryRoles []string + if userDetails.DefaultSecondaryRoles != nil && len(userDetails.DefaultSecondaryRoles.Value) > 0 { + defaultSecondaryRoles = sdk.ParseCommaSeparatedStringArray(userDetails.DefaultSecondaryRoles.Value, true) + } + errs := errors.Join( + // not reading name on purpose (we never update the name externally) + // can't read password + // not reading login_name on purpose (handled as external change to show output) + // not reading display_name on purpose (handled as external change to show output) + setStringPropertyIfNotEmpty(d, "first_name", userDetails.FirstName), + setStringPropertyIfNotEmpty(d, "middle_name", userDetails.MiddleName), + setStringPropertyIfNotEmpty(d, "last_name", userDetails.LastName), + setStringPropertyIfNotEmpty(d, "email", userDetails.Email), + // not reading must_change_password on purpose (handled as external change to show output) + // not reading disabled on purpose (handled as external change to show output) + // not reading days_to_expiry on purpose (they always change) + // not reading mins_to_unlock on purpose (they always change) + setStringPropertyIfNotEmpty(d, "default_warehouse", userDetails.DefaultWarehouse), + // not reading default_namespace because one-part namespace seems to be capitalized on Snowflake side (handled as external change to show output) + setStringPropertyIfNotEmpty(d, "default_role", userDetails.DefaultRole), + d.Set("default_secondary_roles", defaultSecondaryRoles), + // not reading mins_to_bypass_mfa on purpose (they always change) + setStringPropertyIfNotEmpty(d, "rsa_public_key", userDetails.RsaPublicKey), + setStringPropertyIfNotEmpty(d, "rsa_public_key_2", userDetails.RsaPublicKey2), + setStringPropertyIfNotEmpty(d, "comment", userDetails.Comment), + // can't read disable_mfa + d.Set("user_type", u.Type), + + d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), + handleUserParameterRead(d, userParameters), + d.Set(ShowOutputAttributeName, []map[string]any{schemas.UserToSchema(u)}), + d.Set(ParametersAttributeName, []map[string]any{schemas.UserParametersToSchema(userParameters)}), + ) + if errs != nil { return diag.FromErr(err) } @@ -362,7 +427,10 @@ func GetReadUserFunc(withExternalChangesMarking bool) schema.ReadContextFunc { func UpdateUser(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) + id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } if d.HasChange("name") { newID := sdk.NewAccountObjectIdentifier(d.Get("name").(string)) @@ -374,121 +442,49 @@ func UpdateUser(ctx context.Context, d *schema.ResourceData, meta any) diag.Diag return diag.FromErr(err) } - d.SetId(helpers.EncodeSnowflakeID(newID)) + d.SetId(helpers.EncodeResourceIdentifier(newID)) id = newID } - runSet := false - alterOptions := &sdk.AlterUserOptions{ - Set: &sdk.UserSet{ - ObjectProperties: &sdk.UserObjectProperties{}, - }, - } - if d.HasChange("login_name") { - runSet = true - _, n := d.GetChange("login_name") - alterOptions.Set.ObjectProperties.LoginName = sdk.String(n.(string)) - } - if d.HasChange("comment") { - runSet = true - _, n := d.GetChange("comment") - alterOptions.Set.ObjectProperties.Comment = sdk.String(n.(string)) - } - if d.HasChange("password") { - if v, ok := d.GetOk("password"); ok { - runSet = true - alterOptions.Set.ObjectProperties.Password = sdk.String(v.(string)) - } else { - // TODO [SNOW-1348101 - next PR]: this is temporary, update logic will be changed with the resource rework - unsetOptions := &sdk.AlterUserOptions{ - Unset: &sdk.UserUnset{ - ObjectProperties: &sdk.UserObjectPropertiesUnset{ - Password: sdk.Bool(true), - }, - }, - } - err := client.Users.Alter(ctx, id, unsetOptions) - if err != nil { - d.Partial(true) - return diag.FromErr(err) - } - } - } - - if d.HasChange("disabled") { - runSet = true - _, n := d.GetChange("disabled") - disabled := n.(bool) - alterOptions.Set.ObjectProperties.Disable = &disabled - } - if d.HasChange("default_warehouse") { - runSet = true - _, n := d.GetChange("default_warehouse") - alterOptions.Set.ObjectProperties.DefaultWarehouse = sdk.Pointer(sdk.NewAccountObjectIdentifierFromFullyQualifiedName(n.(string))) - } - if d.HasChange("default_namespace") { - runSet = true - _, n := d.GetChange("default_namespace") - defaultNamespaceId, err := helpers.DecodeSnowflakeParameterID(n.(string)) + setObjectProperties := sdk.UserAlterObjectProperties{} + unsetObjectProperties := sdk.UserObjectPropertiesUnset{} + errs := errors.Join( + stringAttributeUpdate(d, "password", &setObjectProperties.Password, &unsetObjectProperties.Password), + stringAttributeUpdate(d, "login_name", &setObjectProperties.LoginName, &unsetObjectProperties.LoginName), + stringAttributeUpdate(d, "display_name", &setObjectProperties.DisplayName, &unsetObjectProperties.DisplayName), + stringAttributeUpdate(d, "first_name", &setObjectProperties.FirstName, &unsetObjectProperties.FirstName), + stringAttributeUpdate(d, "middle_name", &setObjectProperties.MiddleName, &unsetObjectProperties.MiddleName), + stringAttributeUpdate(d, "last_name", &setObjectProperties.LastName, &unsetObjectProperties.LastName), + stringAttributeUpdate(d, "email", &setObjectProperties.Email, &unsetObjectProperties.Email), + booleanStringAttributeUpdate(d, "must_change_password", &setObjectProperties.MustChangePassword, &unsetObjectProperties.MustChangePassword), + booleanStringAttributeUpdate(d, "disabled", &setObjectProperties.Disable, &unsetObjectProperties.Disable), + intAttributeUpdate(d, "days_to_expiry", &setObjectProperties.DaysToExpiry, &unsetObjectProperties.DaysToExpiry), + intAttributeWithSpecialDefaultUpdate(d, "mins_to_unlock", &setObjectProperties.MinsToUnlock, &unsetObjectProperties.MinsToUnlock), + accountObjectIdentifierAttributeUpdate(d, "default_warehouse", &setObjectProperties.DefaultWarehouse, &unsetObjectProperties.DefaultWarehouse), + objectIdentifierAttributeUpdate(d, "default_namespace", &setObjectProperties.DefaultNamespace, &unsetObjectProperties.DefaultNamespace), + accountObjectIdentifierAttributeUpdate(d, "default_role", &setObjectProperties.DefaultRole, &unsetObjectProperties.DefaultRole), + // We do not need value because it is validated on the schema level and ALL is the only supported value currently. + // Check more in https://docs.snowflake.com/en/sql-reference/sql/create-user#optional-object-properties-objectproperties. + attributeDirectValueUpdate(d, "default_secondary_roles", &setObjectProperties.DefaultSecondaryRoles, &sdk.SecondaryRoles{}, &unsetObjectProperties.DefaultSecondaryRoles), + intAttributeWithSpecialDefaultUpdate(d, "mins_to_bypass_mfa", &setObjectProperties.MinsToBypassMFA, &unsetObjectProperties.MinsToBypassMFA), + stringAttributeUpdate(d, "rsa_public_key", &setObjectProperties.RSAPublicKey, &unsetObjectProperties.RSAPublicKey), + stringAttributeUpdate(d, "rsa_public_key_2", &setObjectProperties.RSAPublicKey2, &unsetObjectProperties.RSAPublicKey2), + stringAttributeUpdate(d, "comment", &setObjectProperties.Comment, &unsetObjectProperties.Comment), + booleanStringAttributeUpdate(d, "disable_mfa", &setObjectProperties.DisableMfa, &unsetObjectProperties.DisableMfa), + ) + if errs != nil { + return diag.FromErr(errs) + } + + if (setObjectProperties != sdk.UserAlterObjectProperties{}) { + err := client.Users.Alter(ctx, id, &sdk.AlterUserOptions{Set: &sdk.UserSet{ObjectProperties: &setObjectProperties}}) if err != nil { + d.Partial(true) return diag.FromErr(err) } - alterOptions.Set.ObjectProperties.DefaultNamespace = sdk.Pointer(defaultNamespaceId) - } - if d.HasChange("default_role") { - runSet = true - _, n := d.GetChange("default_role") - alterOptions.Set.ObjectProperties.DefaultRole = sdk.Pointer(sdk.NewAccountObjectIdentifierFromFullyQualifiedName(n.(string))) } - if d.HasChange("default_secondary_roles") { - runSet = true - _, n := d.GetChange("default_secondary_roles") - roles := expandStringList(n.(*schema.Set).List()) - secondaryRoles := []sdk.SecondaryRole{} - for _, role := range roles { - secondaryRoles = append(secondaryRoles, sdk.SecondaryRole{Value: role}) - } - alterOptions.Set.ObjectProperties.DefaultSecondaryRoles = &sdk.SecondaryRoles{Roles: secondaryRoles} - } - if d.HasChange("rsa_public_key") { - runSet = true - _, n := d.GetChange("rsa_public_key") - alterOptions.Set.ObjectProperties.RSAPublicKey = sdk.String(n.(string)) - } - if d.HasChange("rsa_public_key_2") { - runSet = true - _, n := d.GetChange("rsa_public_key_2") - alterOptions.Set.ObjectProperties.RSAPublicKey2 = sdk.String(n.(string)) - } - if d.HasChange("must_change_password") { - runSet = true - _, n := d.GetChange("must_change_password") - mustChangePassword := n.(bool) - alterOptions.Set.ObjectProperties.MustChangePassword = &mustChangePassword - } - if d.HasChange("email") { - runSet = true - _, n := d.GetChange("email") - alterOptions.Set.ObjectProperties.Email = sdk.String(n.(string)) - } - if d.HasChange("display_name") { - runSet = true - _, n := d.GetChange("display_name") - alterOptions.Set.ObjectProperties.DisplayName = sdk.String(n.(string)) - } - if d.HasChange("first_name") { - runSet = true - _, n := d.GetChange("first_name") - alterOptions.Set.ObjectProperties.FirstName = sdk.String(n.(string)) - } - if d.HasChange("last_name") { - runSet = true - _, n := d.GetChange("last_name") - alterOptions.Set.ObjectProperties.LastName = sdk.String(n.(string)) - } - - if runSet { - err := client.Users.Alter(ctx, id, alterOptions) + if (unsetObjectProperties != sdk.UserObjectPropertiesUnset{}) { + err := client.Users.Alter(ctx, id, &sdk.AlterUserOptions{Unset: &sdk.UserUnset{ObjectProperties: &unsetObjectProperties}}) if err != nil { d.Partial(true) return diag.FromErr(err) diff --git a/pkg/resources/user_acceptance_test.go b/pkg/resources/user_acceptance_test.go index 97bc4763c5..50dace3a99 100644 --- a/pkg/resources/user_acceptance_test.go +++ b/pkg/resources/user_acceptance_test.go @@ -3,13 +3,12 @@ package resources_test import ( "errors" "fmt" - "log" "regexp" - "strconv" "strings" "testing" acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + r "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" tfjson "github.com/hashicorp/terraform-json" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" @@ -17,6 +16,7 @@ import ( "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/objectparametersassert" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceassert" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceparametersassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/helpers/random" @@ -24,45 +24,72 @@ import ( "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/testhelpers" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-plugin-testing/tfversion" - "github.com/stretchr/testify/require" ) -func checkBool(path, attr string, value bool) func(*terraform.State) error { - return func(state *terraform.State) error { - is := state.RootModule().Resources[path].Primary - d := is.Attributes[attr] - b, err := strconv.ParseBool(d) - if err != nil { - return err - } - if b != value { - return fmt.Errorf("at %s expected %t but got %t", path, value, b) - } - return nil - } -} - -// TODO [SNOW-1348101]: handle 1-part default namespace -func TestAcc_User(t *testing.T) { - r := require.New(t) - prefix := acc.TestClient().Ids.Alpha() - prefix2 := acc.TestClient().Ids.Alpha() - id := sdk.NewAccountObjectIdentifier(prefix) - id2 := sdk.NewAccountObjectIdentifier(prefix2) +func TestAcc_User_BasicFlows(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + id2 := acc.TestClient().Ids.RandomAccountObjectIdentifier() comment := random.Comment() newComment := random.Comment() - sshkey1, err := testhelpers.Fixture("userkey1") - r.NoError(err) + key1, _ := random.GenerateRSAPublicKey(t) + key2, _ := random.GenerateRSAPublicKey(t) + + pass := random.Password() + newPass := random.Password() + + userModelNoAttributes := model.User("w", id.Name()) + userModelNoAttributesRenamed := model.User("w", id2.Name()). + WithComment(newComment) + + userModelAllAttributes := model.User("w", id.Name()). + WithPassword(pass). + WithLoginName(id.Name() + "_login"). + WithDisplayName("Display Name"). + WithFirstName("Jan"). + WithMiddleName("Jakub"). + WithLastName("Testowski"). + WithEmail("fake@email.com"). + WithMustChangePassword("true"). + WithDisabled("false"). + WithDaysToExpiry(8). + WithMinsToUnlock(9). + WithDefaultWarehouse("some_warehouse"). + WithDefaultNamespace("some.namespace"). + WithDefaultRole("some_role"). + WithDefaultSecondaryRolesStringList("ALL"). + WithMinsToBypassMfa(10). + WithRsaPublicKey(key1). + WithRsaPublicKey2(key2). + WithComment(comment). + WithDisableMfa("true") - sshkey2, err := testhelpers.Fixture("userkey2") - r.NoError(err) + userModelAllAttributesChanged := model.User("w", id.Name()). + WithPassword(newPass). + WithLoginName(id.Name() + "_other_login"). + WithDisplayName("New Display Name"). + WithFirstName("Janek"). + WithMiddleName("Kuba"). + WithLastName("Terraformowski"). + WithEmail("fake@email.net"). + WithMustChangePassword("false"). + WithDisabled("true"). + WithDaysToExpiry(12). + WithMinsToUnlock(13). + WithDefaultWarehouse("other_warehouse"). + WithDefaultNamespace("one_part_namespace"). + WithDefaultRole("other_role"). + WithDefaultSecondaryRolesStringList("ALL"). + WithMinsToBypassMfa(14). + WithRsaPublicKey(key2). + WithRsaPublicKey2(key1). + WithComment(newComment). + WithDisableMfa("false") resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -72,77 +99,171 @@ func TestAcc_User(t *testing.T) { PreCheck: func() { acc.TestAccPreCheck(t) }, CheckDestroy: acc.CheckDestroy(t, resources.User), Steps: []resource.TestStep{ + // CREATE WITHOUT ATTRIBUTES { - Config: uConfig(prefix, sshkey1, sshkey2, comment), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_user.w", "name", prefix), - resource.TestCheckResourceAttr("snowflake_user.w", "comment", comment), - resource.TestCheckResourceAttr("snowflake_user.w", "login_name", strings.ToUpper(fmt.Sprintf("%s_login", prefix))), - resource.TestCheckResourceAttr("snowflake_user.w", "display_name", "Display Name"), - resource.TestCheckResourceAttr("snowflake_user.w", "first_name", "Marcin"), - resource.TestCheckResourceAttr("snowflake_user.w", "last_name", "Zukowski"), - resource.TestCheckResourceAttr("snowflake_user.w", "email", "fake@email.com"), - checkBool("snowflake_user.w", "disabled", false), - resource.TestCheckResourceAttr("snowflake_user.w", "default_warehouse", "foo"), - resource.TestCheckResourceAttr("snowflake_user.w", "default_role", "foo"), - resource.TestCheckResourceAttr("snowflake_user.w", "default_secondary_roles.0", "ALL"), - resource.TestCheckResourceAttr("snowflake_user.w", "default_namespace", "foo.bar"), - resource.TestCheckResourceAttr("snowflake_user.w", "fully_qualified_name", id.FullyQualifiedName()), - checkBool("snowflake_user.w", "has_rsa_public_key", true), - checkBool("snowflake_user.w", "must_change_password", true), - ), - }, - // RENAME - { - Config: uConfig(prefix2, sshkey1, sshkey2, newComment), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("snowflake_user.w", plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_user.w", "name", prefix2), - resource.TestCheckResourceAttr("snowflake_user.w", "comment", newComment), - resource.TestCheckResourceAttr("snowflake_user.w", "login_name", strings.ToUpper(fmt.Sprintf("%s_login", prefix2))), - resource.TestCheckResourceAttr("snowflake_user.w", "display_name", "Display Name"), - resource.TestCheckResourceAttr("snowflake_user.w", "first_name", "Marcin"), - resource.TestCheckResourceAttr("snowflake_user.w", "last_name", "Zukowski"), - resource.TestCheckResourceAttr("snowflake_user.w", "email", "fake@email.com"), - checkBool("snowflake_user.w", "disabled", false), - resource.TestCheckResourceAttr("snowflake_user.w", "default_warehouse", "foo"), - resource.TestCheckResourceAttr("snowflake_user.w", "default_role", "foo"), - resource.TestCheckResourceAttr("snowflake_user.w", "default_secondary_roles.0", "ALL"), - resource.TestCheckResourceAttr("snowflake_user.w", "default_namespace", "foo.bar"), - resource.TestCheckResourceAttr("snowflake_user.w", "fully_qualified_name", id2.FullyQualifiedName()), + Config: config.FromModel(t, userModelNoAttributes), + Check: assert.AssertThat(t, + resourceassert.UserResource(t, userModelNoAttributes.ResourceReference()). + HasNameString(id.Name()). + HasNoPassword(). + HasNoLoginName(). + HasNoDisplayName(). + HasNoFirstName(). + HasNoMiddleName(). + HasNoLastName(). + HasNoEmail(). + HasMustChangePasswordString(r.BooleanDefault). + HasDisabledString(r.BooleanDefault). + HasNoDaysToExpiry(). + HasMinsToUnlockString(r.IntDefaultString). + HasNoDefaultWarehouse(). + HasNoDefaultNamespace(). + HasNoDefaultRole(). + HasNoDefaultSecondaryRoles(). + HasMinsToBypassMfaString(r.IntDefaultString). + HasNoRsaPublicKey(). + HasNoRsaPublicKey2(). + HasNoComment(). + HasDisableMfaString(r.BooleanDefault). + HasFullyQualifiedNameString(id.FullyQualifiedName()), + resourceshowoutputassert.UserShowOutput(t, userModelNoAttributes.ResourceReference()). + HasLoginName(strings.ToUpper(id.Name())). + HasDisplayName(id.Name()), + ), + }, + // RENAME AND CHANGE ONE PROP + { + Config: config.FromModel(t, userModelNoAttributesRenamed), + Check: assert.AssertThat(t, + resourceassert.UserResource(t, userModelNoAttributes.ResourceReference()). + HasNameString(id2.Name()). + HasCommentString(newComment), + // default names stay the same + resourceshowoutputassert.UserShowOutput(t, userModelNoAttributes.ResourceReference()). + HasLoginName(strings.ToUpper(id.Name())). + HasDisplayName(id.Name()), + ), + }, + // IMPORT + { + ResourceName: userModelNoAttributesRenamed.ResourceReference(), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password", "disable_mfa", "days_to_expiry", "mins_to_unlock", "mins_to_bypass_mfa", "login_name", "display_name", "disabled", "must_change_password"}, + ImportStateCheck: assert.AssertThatImport(t, + resourceassert.ImportedUserResource(t, id2.Name()). + HasLoginNameString(strings.ToUpper(id.Name())). + HasDisplayNameString(id.Name()). + HasDisabled(false). + HasMustChangePassword(false), + ), + }, + // DESTROY + { + Config: config.FromModel(t, userModelNoAttributes), + Destroy: true, + }, + // CREATE WITH ALL ATTRIBUTES + { + Config: config.FromModel(t, userModelAllAttributes), + Check: assert.AssertThat(t, + resourceassert.UserResource(t, userModelAllAttributes.ResourceReference()). + HasNameString(id.Name()). + HasPasswordString(pass). + HasLoginNameString(fmt.Sprintf("%s_login", id.Name())). + HasDisplayNameString("Display Name"). + HasFirstNameString("Jan"). + HasMiddleNameString("Jakub"). + HasLastNameString("Testowski"). + HasEmailString("fake@email.com"). + HasMustChangePassword(true). + HasDisabled(false). + HasDaysToExpiryString("8"). + HasMinsToUnlockString("9"). + HasDefaultWarehouseString("some_warehouse"). + HasDefaultNamespaceString("some.namespace"). + HasDefaultRoleString("some_role"). + HasDefaultSecondaryRoles("ALL"). + HasMinsToBypassMfaString("10"). + HasRsaPublicKeyString(key1). + HasRsaPublicKey2String(key2). + HasCommentString(comment). + HasDisableMfaString(r.BooleanTrue). + HasFullyQualifiedNameString(id.FullyQualifiedName()), ), }, // CHANGE PROPERTIES { - Config: uConfig2(prefix2), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_user.w", "name", prefix2), - resource.TestCheckResourceAttr("snowflake_user.w", "comment", "test comment 2"), - resource.TestCheckResourceAttr("snowflake_user.w", "password", "best password"), - resource.TestCheckResourceAttr("snowflake_user.w", "login_name", strings.ToUpper(fmt.Sprintf("%s_login", prefix2))), - resource.TestCheckResourceAttr("snowflake_user.w", "display_name", "New Name"), - resource.TestCheckResourceAttr("snowflake_user.w", "first_name", "Benoit"), - resource.TestCheckResourceAttr("snowflake_user.w", "last_name", "Dageville"), - resource.TestCheckResourceAttr("snowflake_user.w", "email", "fake@email.net"), - checkBool("snowflake_user.w", "disabled", true), - resource.TestCheckResourceAttr("snowflake_user.w", "default_warehouse", "bar"), - resource.TestCheckResourceAttr("snowflake_user.w", "default_role", "bar"), - resource.TestCheckResourceAttr("snowflake_user.w", "default_secondary_roles.#", "0"), - resource.TestCheckResourceAttr("snowflake_user.w", "default_namespace", "bar.baz"), - checkBool("snowflake_user.w", "has_rsa_public_key", false), - resource.TestCheckResourceAttr("snowflake_user.w", "fully_qualified_name", id2.FullyQualifiedName()), + Config: config.FromModel(t, userModelAllAttributesChanged), + Check: assert.AssertThat(t, + resourceassert.UserResource(t, userModelAllAttributesChanged.ResourceReference()). + HasNameString(id.Name()). + HasPasswordString(newPass). + HasLoginNameString(fmt.Sprintf("%s_other_login", id.Name())). + HasDisplayNameString("New Display Name"). + HasFirstNameString("Janek"). + HasMiddleNameString("Kuba"). + HasLastNameString("Terraformowski"). + HasEmailString("fake@email.net"). + HasMustChangePassword(false). + HasDisabled(true). + HasDaysToExpiryString("12"). + HasMinsToUnlockString("13"). + HasDefaultWarehouseString("other_warehouse"). + HasDefaultNamespaceString("one_part_namespace"). + HasDefaultRoleString("other_role"). + HasDefaultSecondaryRoles("ALL"). + HasMinsToBypassMfaString("14"). + HasRsaPublicKeyString(key2). + HasRsaPublicKey2String(key1). + HasCommentString(newComment). + HasDisableMfaString(r.BooleanFalse). + HasFullyQualifiedNameString(id.FullyQualifiedName()), ), }, // IMPORT { - ResourceName: "snowflake_user.w", + ResourceName: userModelAllAttributesChanged.ResourceReference(), ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"password", "rsa_public_key", "rsa_public_key_2", "must_change_password"}, + ImportStateVerifyIgnore: []string{"password", "disable_mfa", "days_to_expiry", "mins_to_unlock", "mins_to_bypass_mfa", "default_namespace", "login_name", "show_output.0.days_to_expiry"}, + ImportStateCheck: assert.AssertThatImport(t, + resourceassert.ImportedUserResource(t, id.Name()). + HasDefaultNamespaceString("ONE_PART_NAMESPACE"). + HasLoginNameString(fmt.Sprintf("%s_OTHER_LOGIN", id.Name())), + ), + }, + // UNSET ALL + { + Config: config.FromModel(t, userModelNoAttributes), + Check: assert.AssertThat(t, + resourceassert.UserResource(t, userModelNoAttributes.ResourceReference()). + HasNameString(id.Name()). + HasPasswordString(""). + HasLoginNameString(""). + HasDisplayNameString(""). + HasFirstNameString(""). + HasMiddleNameString(""). + HasLastNameString(""). + HasEmailString(""). + HasMustChangePasswordString(r.BooleanDefault). + HasDisabledString(r.BooleanDefault). + HasDaysToExpiryString("0"). + HasMinsToUnlockString(r.IntDefaultString). + HasDefaultWarehouseString(""). + HasDefaultNamespaceString(""). + HasDefaultRoleString(""). + HasDefaultSecondaryRolesEmpty(). + HasMinsToBypassMfaString(r.IntDefaultString). + HasRsaPublicKeyString(""). + HasRsaPublicKey2String(""). + HasCommentString(""). + HasDisableMfaString(r.BooleanDefault). + HasFullyQualifiedNameString(id.FullyQualifiedName()), + resourceshowoutputassert.UserShowOutput(t, userModelNoAttributes.ResourceReference()). + HasLoginName(strings.ToUpper(id.Name())). + HasDisplayName(""), + ), }, }, }) @@ -150,12 +271,9 @@ func TestAcc_User(t *testing.T) { // proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2481 has been fixed func TestAcc_User_RemovedOutsideOfTerraform(t *testing.T) { - userName := acc.TestClient().Ids.RandomAccountObjectIdentifier() - config := fmt.Sprintf(` -resource "snowflake_user" "test" { - name = "%s" -} -`, userName.Name()) + userId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + + userModel := model.User("u", userId.Name()) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -165,7 +283,7 @@ resource "snowflake_user" "test" { }, Steps: []resource.TestStep{ { - Config: config, + Config: config.FromModel(t, userModel), ConfigPlanChecks: resource.ConfigPlanChecks{ PostApplyPostRefresh: []plancheck.PlanCheck{ plancheck.ExpectEmptyPlan(), @@ -173,8 +291,8 @@ resource "snowflake_user" "test" { }, }, { - PreConfig: acc.TestClient().User.DropUserFunc(t, userName), - Config: config, + PreConfig: acc.TestClient().User.DropUserFunc(t, userId), + Config: config.FromModel(t, userModel), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectNonEmptyPlan(), @@ -196,68 +314,13 @@ resource "snowflake_user" "test" { }) } -func uConfig(prefix, key1, key2, comment string) string { - s := ` -resource "snowflake_user" "w" { - name = "%s" - comment = "%s" - login_name = "%s_login" - display_name = "Display Name" - first_name = "Marcin" - last_name = "Zukowski" - email = "fake@email.com" - disabled = false - default_warehouse="foo" - default_role="foo" - default_secondary_roles=["ALL"] - default_namespace="foo.bar" - rsa_public_key = <](https://docs.snowflake.com/en/sql-reference/sql/copy-into-location) statements."}, } - // TODO [SNOW-1348101][next PR]: extract this method after moving to SDK + // TODO [SNOW-1645342]: extract this method after moving to SDK for _, field := range userParameterFields { fieldName := strings.ToLower(string(field.Name)) @@ -157,7 +158,7 @@ func init() { Description: enrichWithReferenceToParameterDocs(field.Name, field.Description), Computed: true, Optional: true, - // TODO [SNOW-1348101][next PR]: uncomment and fill out + // TODO [SNOW-1348101 - next PR]: uncomment and fill out // ValidateDiagFunc: field.ValidateDiag, // DiffSuppressFunc: field.DiffSuppress, } @@ -172,8 +173,8 @@ func userParametersProviderFunc(c *sdk.Client) showParametersFunc[sdk.AccountObj return c.Users.ShowParameters } -// TODO [SNOW-1348101][next PR]: make generic based on type definition -func handleUserParameterRead(d *schema.ResourceData, warehouseParameters []*sdk.Parameter) diag.Diagnostics { +// TODO [SNOW-1645342]: make generic based on type definition +func handleUserParameterRead(d *schema.ResourceData, warehouseParameters []*sdk.Parameter) error { for _, p := range warehouseParameters { switch p.Key { case @@ -192,10 +193,10 @@ func handleUserParameterRead(d *schema.ResourceData, warehouseParameters []*sdk. string(sdk.UserParameterWeekStart): value, err := strconv.Atoi(p.Value) if err != nil { - return diag.FromErr(err) + return err } if err := d.Set(strings.ToLower(p.Key), value); err != nil { - return diag.FromErr(err) + return err } case string(sdk.UserParameterBinaryInputFormat), @@ -224,7 +225,7 @@ func handleUserParameterRead(d *schema.ResourceData, warehouseParameters []*sdk. string(sdk.UserParameterUnsupportedDdlAction), string(sdk.UserParameterNetworkPolicy): if err := d.Set(strings.ToLower(p.Key), p.Value); err != nil { - return diag.FromErr(err) + return err } case string(sdk.UserParameterAbortDetachedQuery), @@ -249,10 +250,10 @@ func handleUserParameterRead(d *schema.ResourceData, warehouseParameters []*sdk. string(sdk.UserParameterPreventUnloadToInternalStages): value, err := strconv.ParseBool(p.Value) if err != nil { - return diag.FromErr(err) + return err } if err := d.Set(strings.ToLower(p.Key), value); err != nil { - return diag.FromErr(err) + return err } } } @@ -264,7 +265,7 @@ func handleUserParameterRead(d *schema.ResourceData, warehouseParameters []*sdk. // (because currently setParam already is able to set the right parameter based on the string value input, // but GetConfigPropertyAsPointerAllowingZeroValue receives typed value, // so this would be unnecessary running in circles) -// TODO [SNOW-1348101]: include mappers in the param definition (after moving it to the SDK: identity versus concrete) +// TODO [SNOW-1645342]: include mappers in the param definition (after moving it to the SDK: identity versus concrete) func handleUserParametersCreate(d *schema.ResourceData, createOpts *sdk.CreateUserOptions) diag.Diagnostics { return JoinDiags( handleParameterCreate(d, sdk.UserParameterAbortDetachedQuery, &createOpts.SessionParameters.AbortDetachedQuery), diff --git a/pkg/resources/user_public_keys_acceptance_test.go b/pkg/resources/user_public_keys_acceptance_test.go index 0d1d4985cc..c6e15c9f54 100644 --- a/pkg/resources/user_public_keys_acceptance_test.go +++ b/pkg/resources/user_public_keys_acceptance_test.go @@ -1,25 +1,22 @@ package resources_test import ( - "bytes" + "fmt" "testing" - "text/template" acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/testhelpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/helpers/random" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/tfversion" - "github.com/stretchr/testify/require" ) +// TODO [SNOW-1348101 - next PR]: change description of user public keys resource (should be used only if user is not managed by terraform) func TestAcc_UserPublicKeys(t *testing.T) { - r := require.New(t) - prefix := acc.TestClient().Ids.Alpha() - sshkey1, err := testhelpers.Fixture("userkey1") - r.NoError(err) - sshkey2, err := testhelpers.Fixture("userkey2") - r.NoError(err) + userId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + key1, _ := random.GenerateRSAPublicKey(t) + key2, _ := random.GenerateRSAPublicKey(t) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -30,66 +27,32 @@ func TestAcc_UserPublicKeys(t *testing.T) { CheckDestroy: nil, Steps: []resource.TestStep{ { - Config: uPublicKeysConfig(r, PublicKeyData{ - Prefix: prefix, - PublicKey1: sshkey1, - PublicKey2: sshkey2, - }), + PreConfig: func() { + _, userCleanup := acc.TestClient().User.CreateUserWithOptions(t, userId, nil) + t.Cleanup(userCleanup) + }, + Config: uPublicKeysConfig(userId, key1, key2), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_user.w", "name", prefix), - - resource.TestCheckResourceAttr("snowflake_user_public_keys.foobar", "rsa_public_key", sshkey1), - resource.TestCheckResourceAttr("snowflake_user_public_keys.foobar", "rsa_public_key_2", sshkey2), + resource.TestCheckResourceAttr("snowflake_user_public_keys.foobar", "rsa_public_key", key1), + resource.TestCheckResourceAttr("snowflake_user_public_keys.foobar", "rsa_public_key_2", key2), resource.TestCheckNoResourceAttr("snowflake_user_public_keys.foobar", "has_rsa_public_key"), ), }, - // IMPORT - { - ResourceName: "snowflake_user.w", - ImportState: true, - ImportStateVerify: true, - // Ignoring because keys are currently altered outside of snowflake_user resource (in snowflake_user_public_keys). - ImportStateVerifyIgnore: []string{"password", "rsa_public_key", "rsa_public_key_2", "has_rsa_public_key", "must_change_password", "show_output"}, - }, }, }) } -type PublicKeyData struct { - Prefix string - PublicKey1 string - PublicKey2 string -} - -func uPublicKeysConfig(r *require.Assertions, data PublicKeyData) string { - t := ` -resource "snowflake_user" "w" { - name = "{{.Prefix}}" - comment = "test comment" - login_name = "{{.Prefix}}_login" - display_name = "Display Name" - first_name = "Marcin" - last_name = "Zukowski" - email = "fake@email.com" - disabled = false - default_warehouse="foo" - default_role="FOO" - default_namespace="FOO" -} - +func uPublicKeysConfig(userId sdk.AccountObjectIdentifier, key1 string, key2 string) string { + return fmt.Sprintf(` resource "snowflake_user_public_keys" "foobar" { - name = snowflake_user.w.name + name = %s rsa_public_key = <