-
Notifications
You must be signed in to change notification settings - Fork 418
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Resource to manage a user's public keys (#540)
<!-- 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
Showing
7 changed files
with
375 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
Oops, something went wrong.