Skip to content

Commit

Permalink
feat: Resource to manage a user's public keys (#540)
Browse files Browse the repository at this point in the history
<!-- Feel free to delete comments as you fill this in -->

<!-- summary of changes -->
A resource to manage a user's public keys. Note that this resource is intended to be used when users are managed through an external source (eg SCIM) and it'll conflict with the `snowflake_user` resource.

## Test Plan
Unit tests

## References
  • Loading branch information
Eduardo Lopez authored May 12, 2021
1 parent da8e4f1 commit 590c22e
Show file tree
Hide file tree
Showing 7 changed files with 375 additions and 2 deletions.
28 changes: 28 additions & 0 deletions docs/resources/user_public_keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "snowflake_user_public_keys Resource - terraform-provider-snowflake"
subcategory: ""
description: |-
---

# snowflake_user_public_keys (Resource)





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

### Required

- **name** (String) Name of the user.

### Optional

- **id** (String) The ID of this resource.
- **rsa_public_key** (String) Specifies the user’s RSA public key; used for key-pair authentication. Must be on 1 line without header and trailer.
- **rsa_public_key_2** (String) Specifies the user’s second RSA public key; used to rotate the public and Public keys for key-pair authentication based on an expiration schedule set by your organization. Must be on 1 line without header and trailer.


1 change: 0 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,6 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
Expand Down
4 changes: 3 additions & 1 deletion pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ func GetGrantResources() resources.TerraformGrantResources {
}

func getResources() map[string]*schema.Resource {
// NOTE(): do not add grant resources here
others := map[string]*schema.Resource{
"snowflake_api_integration": resources.APIIntegration(),
"snowflake_database": resources.Database(),
Expand All @@ -165,8 +166,8 @@ func getResources() map[string]*schema.Resource {
"snowflake_network_policy": resources.NetworkPolicy(),
"snowflake_pipe": resources.Pipe(),
"snowflake_resource_monitor": resources.ResourceMonitor(),
"snowflake_role_grants": resources.RoleGrants(),
"snowflake_role": resources.Role(),
"snowflake_role_grants": resources.RoleGrants(),
"snowflake_schema": resources.Schema(),
"snowflake_share": resources.Share(),
"snowflake_stage": resources.Stage(),
Expand All @@ -176,6 +177,7 @@ func getResources() map[string]*schema.Resource {
"snowflake_external_table": resources.ExternalTable(),
"snowflake_task": resources.Task(),
"snowflake_user": resources.User(),
"snowflake_user_public_keys": resources.UserPublicKeys(),
"snowflake_view": resources.View(),
"snowflake_warehouse": resources.Warehouse(),
}
Expand Down
176 changes: 176 additions & 0 deletions pkg/resources/user_public_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package resources

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

"github.com/chanzuckerberg/go-misc/sets"
"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

var userPublicKeyProperties = []string{
"rsa_public_key",
"rsa_public_key_2",
}

// sanitize input to supress diffs, etc
func publicKeyStateFunc(v interface{}) string {
value := v.(string)
value = strings.TrimSuffix(value, "\n")
return value
}

var userPublicKeysSchema = map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
Description: "Name of the user.",
ForceNew: true,
},

"rsa_public_key": {
Type: schema.TypeString,
Optional: true,
Description: "Specifies the user’s RSA public key; used for key-pair authentication. Must be on 1 line without header and trailer.",
StateFunc: publicKeyStateFunc,
},
"rsa_public_key_2": {
Type: schema.TypeString,
Optional: true,
Description: "Specifies the user’s second RSA public key; used to rotate the public and Public keys for key-pair authentication based on an expiration schedule set by your organization. Must be on 1 line without header and trailer.",
StateFunc: publicKeyStateFunc,
},
}

func UserPublicKeys() *schema.Resource {
return &schema.Resource{
Create: CreateUserPublicKeys,
Read: ReadUserPublicKeys,
Update: UpdateUserPublicKeys,
Delete: DeleteUserPublicKeys,

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

func checkUserExists(db *sql.DB, name string) (bool, error) {
// First check if user exists
stmt := snowflake.User(name).Show()
row := snowflake.QueryRow(db, stmt)
_, err := snowflake.ScanUser(row)
if err == sql.ErrNoRows {
log.Printf("[DEBUG] user (%s) not found", name)
return false, nil
}
if err != nil {
return false, err
}

return true, nil
}

func ReadUserPublicKeys(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
id := d.Id()

exists, err := checkUserExists(db, id)
if err != nil {
return err
}
// If not found, mark resource to be removed from statefile during apply or refresh
if !exists {
d.SetId("")
return nil
}
// we can't really read the public keys back from Snowflake so assume they haven't changed
return nil
}

func CreateUserPublicKeys(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
name := d.Get("name").(string)

for _, prop := range userPublicKeyProperties {
publicKey, publicKeyOK := d.GetOk(prop)
if !publicKeyOK {
continue
}
err := updateUserPublicKeys(db, name, prop, publicKey.(string))
if err != nil {
return err
}
}

d.SetId(name)
return ReadUserPublicKeys(d, meta)
}

func UpdateUserPublicKeys(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
name := d.Id()

propsToSet := map[string]string{}
propsToUnset := sets.NewStringSet()

for _, prop := range userPublicKeyProperties {
// if key hasn't changed, continue
if !d.HasChange(prop) {
continue
}
// if it has changed then we should do something about it
publicKey, publicKeyOK := d.GetOk(prop)
if publicKeyOK { // if set, then we should update the value
propsToSet[prop] = publicKey.(string)
} else { // if now unset, we should unset the key from the user
propsToUnset.Add(publicKey.(string))
}
}

// set the keys we decided should be set
for prop, value := range propsToSet {
err := updateUserPublicKeys(db, name, prop, value)
if err != nil {
return err
}
}

// unset the keys we decided should be unset
for _, prop := range propsToUnset.List() {
err := unsetUserPublicKeys(db, name, prop)
if err != nil {
return err
}
}
// re-sync
return ReadUserPublicKeys(d, meta)
}

func DeleteUserPublicKeys(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
name := d.Id()

for _, prop := range userPublicKeyProperties {
err := unsetUserPublicKeys(db, name, prop)
if err != nil {
return err
}
}

d.SetId("")
return nil
}

func updateUserPublicKeys(db *sql.DB, name string, prop string, value string) error {
stmt := fmt.Sprintf(`ALTER USER "%s" SET %s = '%s'`, name, prop, value)
return snowflake.Exec(db, stmt)
}
func unsetUserPublicKeys(db *sql.DB, name string, prop string) error {
stmt := fmt.Sprintf(`ALTER USER "%s" UNSET %s`, name, prop)
return snowflake.Exec(db, stmt)
}
87 changes: 87 additions & 0 deletions pkg/resources/user_public_keys_acceptance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package resources_test

import (
"bytes"
"strings"
"testing"
"text/template"

"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/testhelpers"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/stretchr/testify/require"
)

func TestAcc_UserPublicKeys(t *testing.T) {
r := require.New(t)
prefix := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
sshkey1, err := testhelpers.Fixture("userkey1")
r.NoError(err)
sshkey2, err := testhelpers.Fixture("userkey2")
r.NoError(err)

resource.ParallelTest(t, resource.TestCase{
Providers: providers(),
Steps: []resource.TestStep{
{
Config: uPublicKeysConfig(r, PublicKeyData{
Prefix: prefix,
PublicKey1: sshkey1,
PublicKey2: sshkey2,
}),
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),
),
},
// IMPORT
{
ResourceName: "snowflake_user.w",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"password", "rsa_public_key", "rsa_public_key_2", "must_change_password"},
},
},
})
}

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"
}
resource "snowflake_user_public_keys" "foobar" {
name = snowflake_user.w.name
rsa_public_key = <<KEY
{{ .PublicKey1 }}
KEY
rsa_public_key_2 = <<KEY
{{ .PublicKey2 }}
KEY
}
`
conf := bytes.NewBuffer(nil)
err := template.Must(template.New("user").Parse(t)).Execute(conf, data)
r.NoError(err)
return conf.String()
}
Loading

0 comments on commit 590c22e

Please sign in to comment.