Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add resource snowflake_user_password_policy_attachment (#2162) #2307

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
4ea1227
fix: passwordPolicy is of type identifier, not parameter
raulbonet Dec 24, 2023
a8673c7
feat: snowflake_user_password_policy_attachment . Initial working ver…
raulbonet Dec 24, 2023
3f7caa9
fix: fix syntax for TestAlterUser regarding password policies
raulbonet Dec 24, 2023
dac0fb6
feat: basic acceptance test working
raulbonet Dec 24, 2023
74bcfbf
refactor: passwordPolicy is now imported directly as a SchemaObject
raulbonet Dec 24, 2023
33fa5b1
fix: all parameters are now set in the state
raulbonet Dec 24, 2023
d7a6bf8
fix: Create() now ends with returning the Read()
raulbonet Dec 24, 2023
ff00696
fix: d.Set() now is checked for errors
raulbonet Dec 24, 2023
ca77bcd
fix: for now, do not implement the read function
raulbonet Dec 25, 2023
423a631
fix: change to FullyQualifiedName() to avoid detection of changes
raulbonet Dec 25, 2023
f574c07
fix: tests now do not rely on an existing user
raulbonet Dec 25, 2023
e83c3b4
docs: documentation for the new resource generated
raulbonet Jan 3, 2024
3ab1ad6
feat: read() method implemented for Policy References
raulbonet Jan 14, 2024
7b2daa4
fix: terraform now does not detect changes due to the format in which…
raulbonet Jan 14, 2024
5194f54
feat: acceptance tests for Update(). Delete still not working
raulbonet Jan 14, 2024
ceea700
feat: acceptance tests for Delete()
raulbonet Jan 15, 2024
9db2bc5
fix: Update() method in PasswordPolicy should return Read()
raulbonet Jan 21, 2024
37c3b85
refactor: we stopped using the fully qualified name of the passwordPo…
raulbonet Jan 21, 2024
3ce49c9
feat: tests working for Read(), Update() and Import
raulbonet Jan 21, 2024
3ae77ce
refactor: the sdk for PasswordPolicies resemble more the one for Stre…
raulbonet Jan 21, 2024
38d9e7f
fix: user_name now can be also lowercase
raulbonet Jan 21, 2024
bba2aa3
fix: checkDelete working again
raulbonet Jan 21, 2024
748bb6d
refactor: cleanup comments
raulbonet Jan 21, 2024
a0a930b
feat: implement validate()
raulbonet Jan 21, 2024
d9e01eb
refactor: getForEntityPolicyReferenceOptions is better designed now
raulbonet Jan 21, 2024
678ca9b
docs: run make dev-setup && make docs
raulbonet Jan 21, 2024
fc0c309
refactor: cleanup
raulbonet Jan 21, 2024
bd1babc
refactor: go formatting
raulbonet Jan 21, 2024
7bd734c
Merge branch 'main' into feature/2162/user-attach-password-policy
raulbonet Jan 21, 2024
98cc325
fix: docs for the schema now up-to-date
raulbonet Feb 3, 2024
61dcfbd
refactor: Read() function does not have to set IDs
raulbonet Feb 5, 2024
e223416
fix: comment in id should not contain "roles"
raulbonet Feb 5, 2024
69b49cc
refactor: passwordPolicy instantiated without the need of auxiliary v…
raulbonet Feb 5, 2024
d09bab9
fix: destroy needs to set id to null
raulbonet Feb 5, 2024
f9e8978
refactor: rename destroy function check
raulbonet Feb 5, 2024
bb8eb03
fix: RefEntityName and RefEntityDomain are not optional fields
raulbonet Feb 5, 2024
65bfcca
refactor: some fields do not need to be set to true explicitly
raulbonet Feb 5, 2024
3a5fad4
fix: passwordPolicy in UserSet should not be optional
raulbonet Feb 5, 2024
160a4c7
refactor: refEntityName is better an ObjectIdentifier than a string
raulbonet Feb 5, 2024
c0d761a
docs: provided examples
raulbonet Feb 5, 2024
e245c98
refactor: id parsing in the importer
raulbonet Feb 6, 2024
da6d012
feature: some unit tests
raulbonet Feb 7, 2024
7cf198f
feature: validation is now more specific
raulbonet Feb 7, 2024
5f5a965
feature: added missing arguments validation tests
raulbonet Feb 7, 2024
40810d0
feature: added tests for "Account" entity
raulbonet Feb 7, 2024
5a12da8
fix: wrong description
raulbonet Feb 7, 2024
9715d2a
feat: added tests for integration, tag and view
raulbonet Feb 7, 2024
25e6a2c
Merge branch 'main' into feature/2162/user-attach-password-policy
sfc-gh-asawicki Feb 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions docs/resources/user_password_policy_attachment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "snowflake_user_password_policy_attachment Resource - terraform-provider-snowflake"
subcategory: ""
description: |-
Specifies the password policy to use for a certain user.
---

# snowflake_user_password_policy_attachment (Resource)

Specifies the password policy to use for a certain user.

sfc-gh-asawicki marked this conversation as resolved.
Show resolved Hide resolved
## Example Usage

```terraform
resource "snowflake_user" "user" {
name = "USER_NAME"
}
resource "snowflake_password_policy" "pp" {
database = "prod"
schema = "security"
name = "default_policy"
}

resource "snowflake_user_password_policy_attachment" "ppa" {
password_policy_database = snowflake_password_policy.pp.database
password_policy_schema = snowflake_password_policy.pp.schema
password_policy_name = snowflake_password_policy.pp.name
user_name = snowflake_user.user.name
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `password_policy_database` (String) Database name where the password policy is stored
- `password_policy_name` (String) Non-qualified name of the password policy
- `password_policy_schema` (String) Schema name where the password policy is stored
- `user_name` (String) User name of the user you want to attach the password policy to

### Read-Only

- `id` (String) The ID of this resource.

## Import

Import is supported using the following syntax:

```shell
terraform import snowflake_user_password_policy_attachment.example "MY_DATABASE|MY_SCHEMA|PASSWORD_POLICY_NAME|USER_NAME"
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import snowflake_user_password_policy_attachment.example "MY_DATABASE|MY_SCHEMA|PASSWORD_POLICY_NAME|USER_NAME"
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
resource "snowflake_user" "user" {
name = "USER_NAME"
}
resource "snowflake_password_policy" "pp" {
database = "prod"
schema = "security"
name = "default_policy"
}

resource "snowflake_user_password_policy_attachment" "ppa" {
password_policy_database = snowflake_password_policy.pp.database
password_policy_schema = snowflake_password_policy.pp.schema
password_policy_name = snowflake_password_policy.pp.name
user_name = snowflake_user.user.name
}
1 change: 1 addition & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ func getResources() map[string]*schema.Resource {
"snowflake_unsafe_execute": resources.UnsafeExecute(),
"snowflake_user": resources.User(),
"snowflake_user_ownership_grant": resources.UserOwnershipGrant(),
"snowflake_user_password_policy_attachment": resources.UserPasswordPolicyAttachment(),
"snowflake_user_public_keys": resources.UserPublicKeys(),
"snowflake_view": resources.View(),
"snowflake_warehouse": resources.Warehouse(),
Expand Down
2 changes: 1 addition & 1 deletion pkg/resources/password_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ func UpdatePasswordPolicy(d *schema.ResourceData, meta interface{}) error {
d.SetId(helpers.EncodeSnowflakeID(newID))
}

return nil
return ReadPasswordPolicy(d, meta)
}

// DeletePasswordPolicy implements schema.DeleteFunc.
Expand Down
148 changes: 148 additions & 0 deletions pkg/resources/user_password_policy_attachment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package resources

import (
"context"
"database/sql"
"fmt"
"strings"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

var userPasswordPolicyAttachmentSchema = map[string]*schema.Schema{
"user_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "User name of the user you want to attach the password policy to",
},
"password_policy_database": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Database name where the password policy is stored",
},
"password_policy_schema": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Schema name where the password policy is stored",
},
"password_policy_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Non-qualified name of the password policy",
},
}

func UserPasswordPolicyAttachment() *schema.Resource {
return &schema.Resource{
Description: "Specifies the password policy to use for a certain user.",

Create: CreateUserPasswordPolicyAttachment,
Read: ReadUserPasswordPolicyAttachment,
Delete: DeleteUserPasswordPolicyAttachment,

Schema: userPasswordPolicyAttachmentSchema,

Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
}
}

func CreateUserPasswordPolicyAttachment(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
client := sdk.NewClientFromDB(db)
ctx := context.Background()

userName := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(d.Get("user_name").(string))
passwordPolicy := sdk.NewSchemaObjectIdentifier(
d.Get("password_policy_database").(string),
d.Get("password_policy_schema").(string),
d.Get("password_policy_name").(string),
)

err := client.Users.Alter(ctx, userName, &sdk.AlterUserOptions{
Set: &sdk.UserSet{
PasswordPolicy: &passwordPolicy,
},
})
if err != nil {
return err
}
d.SetId(fmt.Sprintf(`%s|%s`, helpers.EncodeSnowflakeID(passwordPolicy), helpers.EncodeSnowflakeID(userName)))

return ReadUserPasswordPolicyAttachment(d, meta)
}

func ReadUserPasswordPolicyAttachment(d *schema.ResourceData, meta interface{}) error {
parts := strings.Split(d.Id(), helpers.IDDelimiter)
if len(parts) != 4 {
// Note: this exception handling is particularly useful when importing
return fmt.Errorf("id should be in the format 'database|schema|password_policy|user_name', but I got '%s'", d.Id())
}
// Note: there is no alphanumeric id for an attachment, so we retrieve the password policies attached to a certain user.
userName := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(parts[3])
db := meta.(*sql.DB)
client := sdk.NewClientFromDB(db)
ctx := context.Background()
policyReferences, err := client.PolicyReferences.GetForEntity(ctx, &sdk.GetForEntityPolicyReferenceRequest{
// Note: I cannot insert both single and double quotes in the SDK, so for now I need to do this
RefEntityName: userName.FullyQualifiedName(),
RefEntityDomain: "user",
})
if err != nil {
return err
}

// Note: this should never happen, but just in case: so far, Snowflake only allows one Password Policy per user.
if len(policyReferences) > 1 {
return fmt.Errorf("internal error: multiple policy references attached to a user. This should never happen")
}

// Note: this means the resource has been deleted outside of Terraform.
if len(policyReferences) == 0 {
d.SetId("")
return nil
}
if err := d.Set("password_policy_database", sdk.NewAccountIdentifierFromFullyQualifiedName(policyReferences[0].PolicyDb).Name()); err != nil {
return err
}
if err := d.Set("password_policy_schema", sdk.NewAccountIdentifierFromFullyQualifiedName(policyReferences[0].PolicySchema).Name()); err != nil {
return err
}
if err := d.Set("password_policy_name", sdk.NewAccountIdentifierFromFullyQualifiedName(policyReferences[0].PolicyName).Name()); err != nil {
return err
}
sfc-gh-jcieslak marked this conversation as resolved.
Show resolved Hide resolved
if err := d.Set("user_name", helpers.EncodeSnowflakeID(userName)); err != nil {
return err
}
return err
}

// DeleteAccountPasswordPolicyAttachment implements schema.DeleteFunc.
func DeleteUserPasswordPolicyAttachment(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
client := sdk.NewClientFromDB(db)
ctx := context.Background()

userName := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(d.Get("user_name").(string))

err := client.Users.Alter(ctx, userName, &sdk.AlterUserOptions{
Unset: &sdk.UserUnset{
PasswordPolicy: sdk.Bool(true),
},
})

d.SetId("")

if err != nil {
return err
}

sfc-gh-jcieslak marked this conversation as resolved.
Show resolved Hide resolved
return nil
}
100 changes: 100 additions & 0 deletions pkg/resources/user_password_policy_attachment_acceptance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package resources_test

import (
"context"
"database/sql"
"fmt"
"strings"
"testing"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk"

acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
)

func TestAcc_UserPasswordPolicyAttachment(t *testing.T) {
prefix := "tst-terraform" + strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
prefix2 := "tst-terraform" + strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
PreCheck: func() { acc.TestAccPreCheck(t) },
CheckDestroy: testAccCheckUserPasswordPolicyAttachmentDestroy,
Steps: []resource.TestStep{
// CREATE
{
Config: userPasswordPolicyAttachmentConfig("USER", acc.TestDatabaseName, acc.TestSchemaName, prefix),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("snowflake_user_password_policy_attachment.ppa", "id"),
),
Destroy: false,
},
// UPDATE
{
Config: userPasswordPolicyAttachmentConfig(fmt.Sprintf("USER_%s", prefix), acc.TestDatabaseName, acc.TestSchemaName, prefix2),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("snowflake_user_password_policy_attachment.ppa", "id"),
resource.TestCheckResourceAttr("snowflake_user_password_policy_attachment.ppa", "user_name", fmt.Sprintf("USER_%s", prefix)),
),
},
// IMPORT
{
ResourceName: "snowflake_user_password_policy_attachment.ppa",
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func testAccCheckUserPasswordPolicyAttachmentDestroy(s *terraform.State) error {
db := acc.TestAccProvider.Meta().(*sql.DB)
client := sdk.NewClientFromDB(db)
ctx := context.Background()
for _, rs := range s.RootModule().Resources {
// Note: I leverage the fact that the state during the test is specific to the test case, so there should only be there resources created in this test
if rs.Type != "snowflake_user_password_policy_attachment" {
continue
}
userName := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(rs.Primary.Attributes["user_name"])
policyReferences, err := client.PolicyReferences.GetForEntity(ctx, &sdk.GetForEntityPolicyReferenceRequest{
RefEntityName: userName.FullyQualifiedName(),
RefEntityDomain: "user",
})
if err != nil {
if strings.Contains(err.Error(), "does not exist or not authorized") {
// Note: this can happen if the Policy Reference or the User have been deleted as well; in this case, just ignore the error
continue
}
return err
}
if len(policyReferences) > 0 {
return fmt.Errorf("User Password Policy attachment %v still exists", policyReferences[0].PolicyName)
}
}
return nil
}

func userPasswordPolicyAttachmentConfig(userName, databaseName, schemaName, prefix string) string {
s := `
resource "snowflake_user" "user" {
name = "%s"
}
resource "snowflake_password_policy" "pp" {
database = "%s"
schema = "%s"
name = "pp_%v"
}

resource "snowflake_user_password_policy_attachment" "ppa" {
password_policy_database = snowflake_password_policy.pp.database
password_policy_schema = snowflake_password_policy.pp.schema
password_policy_name = snowflake_password_policy.pp.name
user_name = snowflake_user.user.name
}
`
return fmt.Sprintf(s, userName, databaseName, schemaName, prefix)
}
2 changes: 2 additions & 0 deletions pkg/sdk/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type Client struct {
Parameters Parameters
PasswordPolicies PasswordPolicies
Pipes Pipes
PolicyReferences PolicyReferences
Procedures Procedures
ResourceMonitors ResourceMonitors
Roles Roles
Expand Down Expand Up @@ -215,6 +216,7 @@ func (c *Client) initialize() {
c.Parameters = &parameters{client: c}
c.PasswordPolicies = &passwordPolicies{client: c}
c.Pipes = &pipes{client: c}
c.PolicyReferences = &policyReference{client: c}
c.Procedures = &procedures{client: c}
c.ReplicationFunctions = &replicationFunctions{client: c}
c.ResourceMonitors = &resourceMonitors{client: c}
Expand Down
Loading
Loading