From ad5fd281b7ff79b0515a6d4fdca0ed5bded9b581 Mon Sep 17 00:00:00 2001 From: Keith Miller Date: Fri, 1 Dec 2017 11:33:10 -0800 Subject: [PATCH 1/4] Adding authentication plugin support, mainly for AWSAuthenticationPlugin use with Aurora in RDS, issue #16175 --- README.md | 1 + mysql/resource_user.go | 50 +++++++++++++++++++++++++-- mysql/resource_user_test.go | 56 +++++++++++++++++++++++++++++++ website/docs/r/user.html.markdown | 27 +++++++++++++-- 4 files changed, 130 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4388eecbb..7847550db 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ $ # wait for a few seconds to let MySQL stand up, check the logs with: docker lo $ export MYSQL_USERNAME=root $ export MYSQL_ENDPOINT=localhost:3306 $ export MYSQL_PASSWORD=my-secret-pw +$ mysql -h localhost -u root -p -e "INSTALL PLUGIN mysql_no_login SONAME 'mysql_no_login.so';" $ make testacc $ docker rm -f some-mysql ``` diff --git a/mysql/resource_user.go b/mysql/resource_user.go index b30b2e90f..67c59089b 100644 --- a/mysql/resource_user.go +++ b/mysql/resource_user.go @@ -4,8 +4,8 @@ import ( "fmt" "log" + "errors" "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform/helper/schema" ) @@ -36,6 +36,7 @@ func resourceUser() *schema.Resource { Sensitive: true, StateFunc: hashSum, }, + "password": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -43,6 +44,20 @@ func resourceUser() *schema.Resource { Sensitive: true, Deprecated: "Please use plaintext_password instead", }, + + "auth": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ConflictsWith: []string{"plaintext_password"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "plugin": { + Type: schema.TypeString, + Optional: false, + }, + }, + }, + }, }, } } @@ -50,6 +65,21 @@ func resourceUser() *schema.Resource { func CreateUser(d *schema.ResourceData, meta interface{}) error { db := meta.(*providerConfiguration).DB + var auth_stm string = "" + var auth = make(map[string]string) + for k, v := range d.Get("auth").(map[string]interface{}) { + auth[k] = v.(string) + } + + if len(auth) > 0 { + switch auth["plugin"] { + case "AWSAuthenticationPlugin": + auth_stm = " IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'" + case "mysql_no_login": + auth_stm = " IDENTIFIED WITH mysql_no_login" + } + } + stmtSQL := fmt.Sprintf("CREATE USER '%s'@'%s'", d.Get("user").(string), d.Get("host").(string)) @@ -61,7 +91,13 @@ func CreateUser(d *schema.ResourceData, meta interface{}) error { password = d.Get("password").(string) } - if password != "" { + if auth["plugin"] == "AWSAuthenticationPlugin" && d.Get("host").(string) == "localhost" { + return errors.New("cannot use IAM auth against localhost") + } + + if auth_stm != "" { + stmtSQL = stmtSQL + auth_stm + } else { stmtSQL = stmtSQL + fmt.Sprintf(" IDENTIFIED BY '%s'", password) } @@ -80,6 +116,16 @@ func CreateUser(d *schema.ResourceData, meta interface{}) error { func UpdateUser(d *schema.ResourceData, meta interface{}) error { conf := meta.(*providerConfiguration) + var auth = make(map[string]string) + for k, v := range d.Get("auth").(map[string]interface{}) { + auth[k] = v.(string) + } + + if len(auth) > 0 { + // nothing to change, return + return nil + } + var newpw interface{} if d.HasChange("plaintext_password") { _, newpw = d.GetChange("plaintext_password") diff --git a/mysql/resource_user_test.go b/mysql/resource_user_test.go index 9f887a85d..9708f0cb4 100644 --- a/mysql/resource_user_test.go +++ b/mysql/resource_user_test.go @@ -38,6 +38,25 @@ func TestAccUser_basic(t *testing.T) { }) } +func TestAccUser_auth(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccUserCheckDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccUserConfig_auth_iam_plugin, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "auth.plugin", "mysql_no_login"), + ), + }, + }, + }) +} + func TestAccUser_deprecated(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -93,6 +112,33 @@ func testAccUserExists(rn string) resource.TestCheckFunc { } } +func testAccUserAuthExists(rn string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[rn] + if !ok { + return fmt.Errorf("resource not found: %s", rn) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("user id not set") + } + + db := testAccProvider.Meta().(*providerConfiguration).DB + stmtSQL := fmt.Sprintf("SELECT count(*) from mysql.user where CONCAT(user, '@', host) = '%s' and plugin = 'mysql_no_login'", rs.Primary.ID) + log.Println("Executing statement:", stmtSQL) + var count int + err := db.QueryRow(stmtSQL).Scan(&count) + if err != nil { + if err == sql.ErrNoRows { + return fmt.Errorf("expected 1 row reading user but got no rows") + } + return fmt.Errorf("error reading user: %s", err) + } + + return nil + } +} + func testAccUserCheckDestroy(s *terraform.State) error { db := testAccProvider.Meta().(*providerConfiguration).DB @@ -146,3 +192,13 @@ resource "mysql_user" "test" { password = "password2" } ` + +const testAccUserConfig_auth_iam_plugin = ` +resource "mysql_user" "test" { + user = "jdoe" + host = "example.com" + auth { + plugin = "mysql_no_login" + } +} +` diff --git a/website/docs/r/user.html.markdown b/website/docs/r/user.html.markdown index 1811fb5c9..609c33f28 100644 --- a/website/docs/r/user.html.markdown +++ b/website/docs/r/user.html.markdown @@ -26,6 +26,16 @@ resource "mysql_user" "jdoe" { } ``` +```hcl +resource "mysql_user" "nologin" { + user = "nologin" + host = "example.com" + auth { + plugin = "mysql_no_login" + } +} +``` + ## Argument Reference The following arguments are supported: @@ -36,11 +46,24 @@ The following arguments are supported: * `plaintext_password` - (Optional) The password for the user. This must be provided in plain text, so the data source for it must be secured. - An _unsalted_ hash of the provided password is stored in state. + An _unsalted_ hash of the provided password is stored in state. Conflicts + with `auth`. * `password` - (Optional) Deprecated alias of `plaintext_password`, whose value is *stored as plaintext in state*. Prefer to use `plaintext_password` - instead, which stores the password as an unsalted hash. + instead, which stores the password as an unsalted hash. Conflicts with + `auth`. + +* `auth` - (Optional) Block which supports the use of authentication plugins. + Description of the fields allowed in the block below. Conflicts with `password` + and `plaintext_password`. + +The auth block supports: + + * `plugin` - (Required) The plugin to use with the user. Currently only uses + "AWSAuthenticationPlugin" and "mysql_no_login". For more information about + "AWSAuthenticationPlugin" and using it with Aurora: + http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html#UsingWithRDS.IAMDBAuth.Creating ## Attributes Reference From ed549cc472220f62056a2957d9001cee817225b8 Mon Sep 17 00:00:00 2001 From: Keith Miller Date: Mon, 12 Feb 2018 10:19:29 -0800 Subject: [PATCH 2/4] Fixes @vancluever requested at https://github.com/terraform-providers/terraform-provider-mysql/pull/26#pullrequestreview-94892969 --- mysql/resource_user.go | 32 ++++++++++++------------------- website/docs/r/user.html.markdown | 2 ++ 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/mysql/resource_user.go b/mysql/resource_user.go index 67c59089b..ccdf11692 100644 --- a/mysql/resource_user.go +++ b/mysql/resource_user.go @@ -46,17 +46,9 @@ func resourceUser() *schema.Resource { }, "auth": &schema.Schema{ - Type: schema.TypeMap, + Type: schema.TypeString, Optional: true, - ConflictsWith: []string{"plaintext_password"}, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "plugin": { - Type: schema.TypeString, - Optional: false, - }, - }, - }, + ConflictsWith: []string{"plaintext_password", "password"}, }, }, } @@ -65,18 +57,18 @@ func resourceUser() *schema.Resource { func CreateUser(d *schema.ResourceData, meta interface{}) error { db := meta.(*providerConfiguration).DB - var auth_stm string = "" - var auth = make(map[string]string) - for k, v := range d.Get("auth").(map[string]interface{}) { - auth[k] = v.(string) + var authStm string + var auth string + if v, ok := d.GetOk("auth"); ok { + auth = v.(string) } if len(auth) > 0 { - switch auth["plugin"] { + switch auth { case "AWSAuthenticationPlugin": - auth_stm = " IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'" + authStm = " IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'" case "mysql_no_login": - auth_stm = " IDENTIFIED WITH mysql_no_login" + authStm = " IDENTIFIED WITH mysql_no_login" } } @@ -91,12 +83,12 @@ func CreateUser(d *schema.ResourceData, meta interface{}) error { password = d.Get("password").(string) } - if auth["plugin"] == "AWSAuthenticationPlugin" && d.Get("host").(string) == "localhost" { + if auth == "AWSAuthenticationPlugin" && d.Get("host").(string) == "localhost" { return errors.New("cannot use IAM auth against localhost") } - if auth_stm != "" { - stmtSQL = stmtSQL + auth_stm + if authStm != "" { + stmtSQL = stmtSQL + authStm } else { stmtSQL = stmtSQL + fmt.Sprintf(" IDENTIFIED BY '%s'", password) } diff --git a/website/docs/r/user.html.markdown b/website/docs/r/user.html.markdown index 609c33f28..6ea481800 100644 --- a/website/docs/r/user.html.markdown +++ b/website/docs/r/user.html.markdown @@ -26,6 +26,8 @@ resource "mysql_user" "jdoe" { } ``` +## Example Usage with an Authentication Plugin + ```hcl resource "mysql_user" "nologin" { user = "nologin" From 30996cdf953f1409b11594e9fa1734605ae38fc4 Mon Sep 17 00:00:00 2001 From: Keith Miller Date: Mon, 12 Feb 2018 13:57:06 -0800 Subject: [PATCH 3/4] Fixed update user to use 'auth' as a string, also updated unit tests to match. --- mysql/resource_user.go | 7 ++++--- mysql/resource_user_test.go | 6 ++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/mysql/resource_user.go b/mysql/resource_user.go index ccdf11692..93ba94099 100644 --- a/mysql/resource_user.go +++ b/mysql/resource_user.go @@ -48,6 +48,7 @@ func resourceUser() *schema.Resource { "auth": &schema.Schema{ Type: schema.TypeString, Optional: true, + ForceNew: true, ConflictsWith: []string{"plaintext_password", "password"}, }, }, @@ -108,9 +109,9 @@ func CreateUser(d *schema.ResourceData, meta interface{}) error { func UpdateUser(d *schema.ResourceData, meta interface{}) error { conf := meta.(*providerConfiguration) - var auth = make(map[string]string) - for k, v := range d.Get("auth").(map[string]interface{}) { - auth[k] = v.(string) + var auth string + if v, ok := d.GetOk("auth"); ok { + auth = v.(string) } if len(auth) > 0 { diff --git a/mysql/resource_user_test.go b/mysql/resource_user_test.go index 9708f0cb4..32958bbde 100644 --- a/mysql/resource_user_test.go +++ b/mysql/resource_user_test.go @@ -50,7 +50,7 @@ func TestAccUser_auth(t *testing.T) { testAccUserAuthExists("mysql_user.test"), resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), - resource.TestCheckResourceAttr("mysql_user.test", "auth.plugin", "mysql_no_login"), + resource.TestCheckResourceAttr("mysql_user.test", "auth", "mysql_no_login"), ), }, }, @@ -197,8 +197,6 @@ const testAccUserConfig_auth_iam_plugin = ` resource "mysql_user" "test" { user = "jdoe" host = "example.com" - auth { - plugin = "mysql_no_login" - } + auth = "mysql_no_login" } ` From 5b3ee767da92eaafc9ad5f3429d9b8b5a0b03f64 Mon Sep 17 00:00:00 2001 From: Keith Miller Date: Tue, 20 Feb 2018 15:58:22 -0800 Subject: [PATCH 4/4] Changed 'auth' to 'auth_plugin' as per @vancluever's input and updated the docs to reflect the new auth changes. --- mysql/resource_user.go | 6 +++--- mysql/resource_user_test.go | 8 ++++---- website/docs/r/user.html.markdown | 20 ++++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/mysql/resource_user.go b/mysql/resource_user.go index 93ba94099..05f0906a0 100644 --- a/mysql/resource_user.go +++ b/mysql/resource_user.go @@ -45,7 +45,7 @@ func resourceUser() *schema.Resource { Deprecated: "Please use plaintext_password instead", }, - "auth": &schema.Schema{ + "auth_plugin": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, @@ -60,7 +60,7 @@ func CreateUser(d *schema.ResourceData, meta interface{}) error { var authStm string var auth string - if v, ok := d.GetOk("auth"); ok { + if v, ok := d.GetOk("auth_plugin"); ok { auth = v.(string) } @@ -110,7 +110,7 @@ func UpdateUser(d *schema.ResourceData, meta interface{}) error { conf := meta.(*providerConfiguration) var auth string - if v, ok := d.GetOk("auth"); ok { + if v, ok := d.GetOk("auth_plugin"); ok { auth = v.(string) } diff --git a/mysql/resource_user_test.go b/mysql/resource_user_test.go index 32958bbde..1aaa87489 100644 --- a/mysql/resource_user_test.go +++ b/mysql/resource_user_test.go @@ -50,7 +50,7 @@ func TestAccUser_auth(t *testing.T) { testAccUserAuthExists("mysql_user.test"), resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), - resource.TestCheckResourceAttr("mysql_user.test", "auth", "mysql_no_login"), + resource.TestCheckResourceAttr("mysql_user.test", "auth_plugin", "mysql_no_login"), ), }, }, @@ -195,8 +195,8 @@ resource "mysql_user" "test" { const testAccUserConfig_auth_iam_plugin = ` resource "mysql_user" "test" { - user = "jdoe" - host = "example.com" - auth = "mysql_no_login" + user = "jdoe" + host = "example.com" + auth_plugin = "mysql_no_login" } ` diff --git a/website/docs/r/user.html.markdown b/website/docs/r/user.html.markdown index 6ea481800..5c311f3c6 100644 --- a/website/docs/r/user.html.markdown +++ b/website/docs/r/user.html.markdown @@ -32,9 +32,7 @@ resource "mysql_user" "jdoe" { resource "mysql_user" "nologin" { user = "nologin" host = "example.com" - auth { - plugin = "mysql_no_login" - } + auth_plugin = "mysql_no_login" } ``` @@ -49,23 +47,25 @@ The following arguments are supported: * `plaintext_password` - (Optional) The password for the user. This must be provided in plain text, so the data source for it must be secured. An _unsalted_ hash of the provided password is stored in state. Conflicts - with `auth`. + with `auth_plugin`. * `password` - (Optional) Deprecated alias of `plaintext_password`, whose value is *stored as plaintext in state*. Prefer to use `plaintext_password` instead, which stores the password as an unsalted hash. Conflicts with - `auth`. + `auth_plugin`. -* `auth` - (Optional) Block which supports the use of authentication plugins. +* `auth_plugin` - (Optional) Block which supports the use of authentication plugins. Description of the fields allowed in the block below. Conflicts with `password` and `plaintext_password`. -The auth block supports: +The `auth_plugin` value supports: - * `plugin` - (Required) The plugin to use with the user. Currently only uses - "AWSAuthenticationPlugin" and "mysql_no_login". For more information about - "AWSAuthenticationPlugin" and using it with Aurora: + * `AWSAuthenticationPlugin` - For more information about "AWSAuthenticationPlugin" + and using it with Aurora: http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html#UsingWithRDS.IAMDBAuth.Creating + * `mysql_no_login` - Uses the MySQL No-Login Authentication Plugin. The No-Login + Authentication Plugin must be active in MySQL. For more information: + https://dev.mysql.com/doc/refman/5.7/en/no-login-pluggable-authentication.html ## Attributes Reference