diff --git a/README.md b/README.md index 4388eecb..7847550d 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 b30b2e90..05f0906a 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,13 @@ func resourceUser() *schema.Resource { Sensitive: true, Deprecated: "Please use plaintext_password instead", }, + + "auth_plugin": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"plaintext_password", "password"}, + }, }, } } @@ -50,6 +58,21 @@ func resourceUser() *schema.Resource { func CreateUser(d *schema.ResourceData, meta interface{}) error { db := meta.(*providerConfiguration).DB + var authStm string + var auth string + if v, ok := d.GetOk("auth_plugin"); ok { + auth = v.(string) + } + + if len(auth) > 0 { + switch auth { + case "AWSAuthenticationPlugin": + authStm = " IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'" + case "mysql_no_login": + authStm = " IDENTIFIED WITH mysql_no_login" + } + } + stmtSQL := fmt.Sprintf("CREATE USER '%s'@'%s'", d.Get("user").(string), d.Get("host").(string)) @@ -61,7 +84,13 @@ func CreateUser(d *schema.ResourceData, meta interface{}) error { password = d.Get("password").(string) } - if password != "" { + if auth == "AWSAuthenticationPlugin" && d.Get("host").(string) == "localhost" { + return errors.New("cannot use IAM auth against localhost") + } + + if authStm != "" { + stmtSQL = stmtSQL + authStm + } else { stmtSQL = stmtSQL + fmt.Sprintf(" IDENTIFIED BY '%s'", password) } @@ -80,6 +109,16 @@ func CreateUser(d *schema.ResourceData, meta interface{}) error { func UpdateUser(d *schema.ResourceData, meta interface{}) error { conf := meta.(*providerConfiguration) + var auth string + if v, ok := d.GetOk("auth_plugin"); ok { + auth = 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 9f887a85..1aaa8748 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,11 @@ 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 1811fb5c..5c311f3c 100644 --- a/website/docs/r/user.html.markdown +++ b/website/docs/r/user.html.markdown @@ -26,6 +26,16 @@ resource "mysql_user" "jdoe" { } ``` +## Example Usage with an Authentication Plugin + +```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,26 @@ 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_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. + instead, which stores the password as an unsalted hash. Conflicts with + `auth_plugin`. + +* `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_plugin` value supports: + + * `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