Skip to content

Commit

Permalink
feat: add resource snowflake_user_password_policy_attachment (#2162) (#…
Browse files Browse the repository at this point in the history
…2307)

# Description
ref #2162 
This PR creates the new resource
`snowflake_user_password_policy_attachment`. The implementation mimics
the one of its brother, `snowflake_account_password_policy_attachment`

---------

Co-authored-by: Raül Bonet <[email protected]>
Co-authored-by: Artur Sawicki <[email protected]>
  • Loading branch information
3 people authored Feb 8, 2024
1 parent 018bb74 commit 93af462
Show file tree
Hide file tree
Showing 15 changed files with 647 additions and 9 deletions.
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.

## 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
}
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
}

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

0 comments on commit 93af462

Please sign in to comment.