diff --git a/GNUmakefile b/GNUmakefile index 7526875e..1cf61489 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -25,7 +25,7 @@ bin/terraform: testacc: fmtcheck bin/terraform PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout=60s -acceptance: testversion5.6 testversion5.7 testversion8.0 testpercona5.7 testpercona8.0 testtidb6.1.0 +acceptance: testversion5.6 testversion5.7 testversion8.0 testpercona5.7 testpercona8.0 testmariadb10.3 testmariadb10.8 testtidb6.1.0 testversion%: $(MAKE) MYSQL_VERSION=$* MYSQL_PORT=33$(shell echo "$*" | tr -d '.') testversion @@ -59,6 +59,18 @@ testtidb: MYSQL_USERNAME="$(TEST_USER)" MYSQL_PASSWORD="" MYSQL_ENDPOINT=127.0.0.1:$(MYSQL_PORT) $(MAKE) testacc docker rm -f test-tidb$(MYSQL_VERSION) +testmariadb%: + $(MAKE) MYSQL_VERSION=$* MYSQL_PORT=36$(shell echo "$*" | tr -d '.') testmariadb + +testmariadb: + -docker run --rm --name test-mariadb$(MYSQL_VERSION) -e MYSQL_ROOT_PASSWORD="$(TEST_PASSWORD)" -d -p $(MYSQL_PORT):3306 mariadb:$(MYSQL_VERSION) + @echo 'Waiting for MySQL...' + @while ! mysql -h 127.0.0.1 -P $(MYSQL_PORT) -u "$(TEST_USER)" -p"$(TEST_PASSWORD)" -e 'SELECT 1' >/dev/null 2>&1; do printf '.'; sleep 1; done ; echo ; echo "Connected!" + MYSQL_USERNAME="$(TEST_USER)" MYSQL_PASSWORD="$(TEST_PASSWORD)" MYSQL_ENDPOINT=127.0.0.1:$(MYSQL_PORT) $(MAKE) testacc + docker rm -f test-mariadb$(MYSQL_VERSION) + + + vet: @echo "go vet ." @go vet $$(go list ./... | grep -v vendor/) ; if [ $$? -eq 1 ]; then \ diff --git a/mysql/provider.go b/mysql/provider.go index 05e69cff..46d7c284 100644 --- a/mysql/provider.go +++ b/mysql/provider.go @@ -24,6 +24,7 @@ const ( cleartextPasswords = "cleartext" nativePasswords = "native" unknownVarErrCode = 1193 + unknownUserErrCode = 1396 ) type MySQLConfiguration struct { @@ -32,6 +33,7 @@ type MySQLConfiguration struct { MaxConnLifetime time.Duration MaxOpenConns int ConnectRetryTimeoutSec time.Duration + Version *version.Version } var ( @@ -197,24 +199,26 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { } mysqlConf.Db = db - if err := afterConnect(d, db); err != nil { + if err := afterConnect(mysqlConf, db); err != nil { return nil, fmt.Errorf("Failed running after connect command: %v", err) } return mysqlConf, nil } -func afterConnect(d *schema.ResourceData, db *sql.DB) error { +func afterConnect(mysqlConf *MySQLConfiguration, db *sql.DB) error { // Set up env so that we won't create users randomly. currentVersion, err := serverVersion(db) if err != nil { return fmt.Errorf("Failed getting server version: %v", err) } + mysqlConf.Version = currentVersion + versionMinInclusive, _ := version.NewVersion("5.7.5") versionMaxExclusive, _ := version.NewVersion("8.0.0") - if currentVersion.GreaterThanOrEqual(versionMinInclusive) && - currentVersion.LessThan(versionMaxExclusive) { + if mysqlConf.Version.GreaterThanOrEqual(versionMinInclusive) && + mysqlConf.Version.LessThan(versionMaxExclusive) { // CONCAT and setting works even if there is no value. _, err := db.Exec(`SET SESSION sql_mode=CONCAT(@@sql_mode, ',NO_AUTO_CREATE_USER')`) if err != nil { diff --git a/mysql/provider_test.go b/mysql/provider_test.go index 6c254f4c..dfd94df1 100644 --- a/mysql/provider_test.go +++ b/mysql/provider_test.go @@ -83,3 +83,20 @@ func testAccPreCheckSkipTiDB(t *testing.T) { t.Skip("Skip on TiDB") } } + +func testAccPreCheckSkipMariaDB(t *testing.T) { + testAccPreCheck(t) + db, err := connectToMySQL(testAccProvider.Meta().(*MySQLConfiguration)) + if err != nil { + return + } + + currentVersionString, err := serverVersionString(db) + if err != nil { + return + } + + if strings.Contains(currentVersionString, "MariaDB") { + t.Skip("Skip on MariaDB") + } +} diff --git a/mysql/resource_database.go b/mysql/resource_database.go index 9ca5b9b9..fe352b55 100644 --- a/mysql/resource_database.go +++ b/mysql/resource_database.go @@ -77,7 +77,8 @@ func UpdateDatabase(d *schema.ResourceData, meta interface{}) error { } func ReadDatabase(d *schema.ResourceData, meta interface{}) error { - db := meta.(*MySQLConfiguration).Db + mysqlConf := meta.(*MySQLConfiguration) + db := mysqlConf.Db // This is kinda flimsy-feeling, since it depends on the formatting // of the SHOW CREATE DATABASE output... but this data doesn't seem @@ -111,10 +112,6 @@ func ReadDatabase(d *schema.ResourceData, meta interface{}) error { var empty interface{} requiredVersion, _ := version.NewVersion("8.0.0") - currentVersion, err := serverVersion(db) - if err != nil { - return err - } serverVersionString, err := serverVersionString(db) if err != nil { @@ -123,7 +120,7 @@ func ReadDatabase(d *schema.ResourceData, meta interface{}) error { // MySQL 8 returns more data in a row. var res error - if !strings.Contains(serverVersionString, "MariaDB") && currentVersion.GreaterThan(requiredVersion) { + if !strings.Contains(serverVersionString, "MariaDB") && mysqlConf.Version.GreaterThan(requiredVersion) { res = db.QueryRow(stmtSQL, defaultCharset).Scan(&defaultCollation, &empty, &empty, &empty, &empty, &empty, &empty) } else { res = db.QueryRow(stmtSQL, defaultCharset).Scan(&defaultCollation, &empty, &empty, &empty, &empty, &empty) diff --git a/mysql/resource_global_variable_test.go b/mysql/resource_global_variable_test.go index 26f851ca..b04ba7cd 100644 --- a/mysql/resource_global_variable_test.go +++ b/mysql/resource_global_variable_test.go @@ -14,7 +14,7 @@ func TestAccGlobalVar_basic(t *testing.T) { resourceName := "mysql_global_variable.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckSkipMariaDB(t) }, Providers: testAccProviders, CheckDestroy: testAccGlobalVarCheckDestroy(varName), Steps: []resource.TestStep{ diff --git a/mysql/resource_grant.go b/mysql/resource_grant.go index 72a01dee..70cb0245 100644 --- a/mysql/resource_grant.go +++ b/mysql/resource_grant.go @@ -92,10 +92,11 @@ func resourceGrant() *schema.Resource { }, "tls_option": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Default: "NONE", + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Deprecated: "Please use tls_option in mysql_user.", + Default: "NONE", }, }, } @@ -145,11 +146,8 @@ func userOrRole(user string, host string, role string, hasRoles bool) (string, b } } -func supportsRoles(db *sql.DB) (bool, error) { - currentVersion, err := serverVersion(db) - if err != nil { - return false, err - } +func supportsRoles(meta interface{}) (bool, error) { + currentVersion := meta.(*MySQLConfiguration).Version requiredVersion, _ := version.NewVersion("8.0.0") hasRoles := currentVersion.GreaterThan(requiredVersion) @@ -159,7 +157,7 @@ func supportsRoles(db *sql.DB) (bool, error) { func CreateGrant(d *schema.ResourceData, meta interface{}) error { db := meta.(*MySQLConfiguration).Db - hasRoles, err := supportsRoles(db) + hasRoles, err := supportsRoles(meta) if err != nil { return err } @@ -262,7 +260,7 @@ func CreateGrant(d *schema.ResourceData, meta interface{}) error { func ReadGrant(d *schema.ResourceData, meta interface{}) error { db := meta.(*MySQLConfiguration).Db - hasRoles, err := supportsRoles(db) + hasRoles, err := supportsRoles(meta) if err != nil { return err } @@ -322,7 +320,7 @@ func ReadGrant(d *schema.ResourceData, meta interface{}) error { func UpdateGrant(d *schema.ResourceData, meta interface{}) error { db := meta.(*MySQLConfiguration).Db - hasRoles, err := supportsRoles(db) + hasRoles, err := supportsRoles(meta) if err != nil { return err @@ -401,7 +399,7 @@ func DeleteGrant(d *schema.ResourceData, meta interface{}) error { table := formatTableName(d.Get("table").(string)) - hasRoles, err := supportsRoles(db) + hasRoles, err := supportsRoles(meta) if err != nil { return err } diff --git a/mysql/resource_grant_test.go b/mysql/resource_grant_test.go index e6176cac..ee2554bf 100644 --- a/mysql/resource_grant_test.go +++ b/mysql/resource_grant_test.go @@ -30,17 +30,15 @@ func TestAccGrant(t *testing.T) { resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), resource.TestCheckResourceAttr("mysql_grant.test", "table", "*"), - resource.TestCheckResourceAttr("mysql_grant.test", "tls_option", "NONE"), ), }, { - Config: testAccGrantConfig_ssl(dbName), + Config: testAccGrantConfig_basic(dbName), Check: resource.ComposeTestCheckFunc( testAccPrivilege("mysql_grant.test", "SELECT", true), resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), - resource.TestCheckResourceAttr("mysql_grant.test", "tls_option", "SSL"), ), }, }, @@ -162,7 +160,6 @@ func TestAccGrantComplex(t *testing.T) { resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), resource.TestCheckResourceAttr("mysql_grant.test", "table", "tbl"), - resource.TestCheckResourceAttr("mysql_grant.test", "tls_option", "NONE"), ), }, { @@ -176,7 +173,6 @@ func TestAccGrantComplex(t *testing.T) { resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), resource.TestCheckResourceAttr("mysql_grant.test", "table", "tbl"), - resource.TestCheckResourceAttr("mysql_grant.test", "tls_option", "NONE"), ), }, { @@ -187,7 +183,6 @@ func TestAccGrantComplex(t *testing.T) { resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), resource.TestCheckResourceAttr("mysql_grant.test", "table", "tbl"), - resource.TestCheckResourceAttr("mysql_grant.test", "tls_option", "NONE"), ), }, { @@ -198,7 +193,6 @@ func TestAccGrantComplex(t *testing.T) { resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), resource.TestCheckResourceAttr("mysql_grant.test", "table", "tbl"), - resource.TestCheckResourceAttr("mysql_grant.test", "tls_option", "NONE"), ), }, { @@ -209,7 +203,6 @@ func TestAccGrantComplex(t *testing.T) { resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), resource.TestCheckResourceAttr("mysql_grant.test", "table", "tbl"), - resource.TestCheckResourceAttr("mysql_grant.test", "tls_option", "NONE"), ), }, { @@ -224,7 +217,6 @@ func TestAccGrantComplex(t *testing.T) { resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), resource.TestCheckResourceAttr("mysql_grant.test", "table", "tbl"), - resource.TestCheckResourceAttr("mysql_grant.test", "tls_option", "NONE"), ), }, }, @@ -598,27 +590,6 @@ resource "mysql_grant" "test2" { } `, dbName, dbName) } -func testAccGrantConfig_ssl(dbName string) string { - return fmt.Sprintf(` -resource "mysql_database" "test" { - name = "%s" -} - -resource "mysql_user" "test" { - user = "jdoe-%s" - host = "example.com" -} - -resource "mysql_grant" "test" { - user = "${mysql_user.test.user}" - host = "${mysql_user.test.host}" - database = "${mysql_database.test.name}" - privileges = ["UPDATE", "SELECT"] - tls_option = "SSL" -} -`, dbName, dbName) -} - func testAccGrantConfig_role(dbName string, roleName string) string { return fmt.Sprintf(` resource "mysql_database" "test" { diff --git a/mysql/resource_user.go b/mysql/resource_user.go index 33116436..7bc1534a 100644 --- a/mysql/resource_user.go +++ b/mysql/resource_user.go @@ -1,9 +1,9 @@ package mysql import ( - "database/sql" "fmt" "log" + "regexp" "strings" "errors" @@ -52,16 +52,25 @@ func resourceUser() *schema.Resource { }, "auth_plugin": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - ConflictsWith: []string{"plaintext_password", "password"}, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + DiffSuppressFunc: NewEmptyStringSuppressFunc, + ConflictsWith: []string{"plaintext_password", "password"}, + }, + + "auth_string_hashed": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + DiffSuppressFunc: NewEmptyStringSuppressFunc, + RequiredWith: []string{"auth_plugin"}, + ConflictsWith: []string{"plaintext_password", "password"}, }, "tls_option": { Type: schema.TypeString, Optional: true, - ForceNew: true, Default: "NONE", }, }, @@ -85,6 +94,12 @@ func CreateUser(d *schema.ResourceData, meta interface{}) error { authStm = " IDENTIFIED WITH " + auth } } + if v, ok := d.GetOk("auth_string_hashed"); ok { + hashed := v.(string) + if hashed != "" { + authStm = fmt.Sprintf("%s AS '%s'", authStm, hashed) + } + } stmtSQL := fmt.Sprintf("CREATE USER '%s'@'%s'", d.Get("user").(string), @@ -108,17 +123,13 @@ func CreateUser(d *schema.ResourceData, meta interface{}) error { } requiredVersion, _ := version.NewVersion("5.7.0") - currentVersion, err := serverVersion(db) - if err != nil { - return err - } - if currentVersion.GreaterThan(requiredVersion) && d.Get("tls_option").(string) != "" { + if meta.(*MySQLConfiguration).Version.GreaterThan(requiredVersion) && d.Get("tls_option").(string) != "" { stmtSQL += fmt.Sprintf(" REQUIRE %s", d.Get("tls_option").(string)) } log.Println("Executing statement:", stmtSQL) - _, err = db.Exec(stmtSQL) + _, err := db.Exec(stmtSQL) if err != nil { return err } @@ -129,15 +140,10 @@ func CreateUser(d *schema.ResourceData, meta interface{}) error { return nil } -func getSetPasswordStatement(db *sql.DB) (string, error) { +func getSetPasswordStatement(meta interface{}) (string, error) { /* ALTER USER syntax introduced in MySQL 5.7.6 deprecates SET PASSWORD (GH-8230) */ - serverVersion, err := serverVersion(db) - if err != nil { - return "", fmt.Errorf("Could not determine server version: %s", err) - } - ver, _ := version.NewVersion("5.7.6") - if serverVersion.LessThan(ver) { + if meta.(*MySQLConfiguration).Version.LessThan(ver) { return "SET PASSWORD FOR ?@? = PASSWORD(?)", nil } else { return "ALTER USER ?@? IDENTIFIED BY ?", nil @@ -145,14 +151,34 @@ func getSetPasswordStatement(db *sql.DB) (string, error) { } func UpdateUser(d *schema.ResourceData, meta interface{}) error { - db := meta.(*MySQLConfiguration).Db + mysqlConf := meta.(*MySQLConfiguration) + db := mysqlConf.Db var auth string if v, ok := d.GetOk("auth_plugin"); ok { auth = v.(string) } - if len(auth) > 0 { + if d.HasChange("tls_option") || d.HasChange("auth_plugin") || d.HasChange("auth_string_hashed") { + var stmtSQL string + + authString := "" + if d.Get("auth_string_hashed").(string) != "" { + authString = fmt.Sprintf("IDENTIFIED WITH %s AS '%s'", d.Get("auth_plugin"), d.Get("auth_string_hashed")) + } + stmtSQL = fmt.Sprintf("ALTER USER '%s'@'%s' %s REQUIRE %s", + d.Get("user").(string), + d.Get("host").(string), + authString, + d.Get("tls_option").(string)) + + log.Println("Executing query:", stmtSQL) + _, err := db.Exec(stmtSQL) + if err != nil { + return err + } + } + // nothing to change, return return nil } @@ -167,7 +193,7 @@ func UpdateUser(d *schema.ResourceData, meta interface{}) error { } if newpw != nil { - stmtSQL, err := getSetPasswordStatement(db) + stmtSQL, err := getSetPasswordStatement(meta) if err != nil { return err } @@ -183,12 +209,7 @@ func UpdateUser(d *schema.ResourceData, meta interface{}) error { } requiredVersion, _ := version.NewVersion("5.7.0") - currentVersion, err := serverVersion(db) - if err != nil { - return err - } - - if d.HasChange("tls_option") && currentVersion.GreaterThan(requiredVersion) { + if d.HasChange("tls_option") && mysqlConf.Version.GreaterThan(requiredVersion) { var stmtSQL string stmtSQL = fmt.Sprintf("ALTER USER '%s'@'%s' REQUIRE %s", @@ -208,34 +229,74 @@ func UpdateUser(d *schema.ResourceData, meta interface{}) error { func ReadUser(d *schema.ResourceData, meta interface{}) error { db := meta.(*MySQLConfiguration).Db + currentVersion := meta.(*MySQLConfiguration).Version + requiredVersion, _ := version.NewVersion("5.7.0") + if currentVersion.GreaterThan(requiredVersion) { + stmt := "SHOW CREATE USER ?@?" - stmtSQL := fmt.Sprintf("SELECT USER FROM mysql.user WHERE USER='%s'", - d.Get("user").(string)) + var createUserStmt string + err := db.QueryRow(stmt, d.Get("user").(string), d.Get("host").(string)).Scan(&createUserStmt) + if err != nil { + if mysqlErrorNumber(err) == unknownUserErrCode { + d.SetId("") + return nil + } + return err + } - log.Println("Executing statement:", stmtSQL) + // Examples of create user: + // CREATE USER 'some_app'@'%' IDENTIFIED WITH 'mysql_native_password' AS '*0something' REQUIRE NONE PASSWORD EXPIRE DEFAULT ACCOUNT UNLOCK + // CREATE USER `jdoe-tf-test-47`@`example.com` IDENTIFIED WITH 'caching_sha2_password' REQUIRE NONE PASSWORD EXPIRE DEFAULT ACCOUNT UNLOCK PASSWORD HISTORY DEFAULT PASSWORD REUSE INTERVAL DEFAULT PASSWORD REQUIRE CURRENT DEFAULT + // CREATE USER `jdoe`@`example.com` IDENTIFIED WITH 'caching_sha2_password' AS '$A$005$i`xay#fG/\' TrbkNA82' REQUIRE NONE PASSWORD + re := regexp.MustCompile("^CREATE USER ['`]([^'`]*)['`]@['`]([^'`]*)['`] IDENTIFIED WITH ['`]([^'`]*)['`] (?:AS '((?:.*?[^\\\\])?)' )?REQUIRE ([^ ]*)") + if m := re.FindStringSubmatch(createUserStmt); len(m) == 6 { + d.Set("user", m[1]) + d.Set("host", m[2]) + d.Set("auth_plugin", m[3]) + d.Set("auth_string_hashed", m[4]) + d.Set("tls_option", m[5]) + return nil + } - rows, err := db.Query(stmtSQL) - if err != nil { - return err - } - defer rows.Close() + // Try 2 - just whether the user is there. + re2 := regexp.MustCompile("^CREATE USER") + if m := re2.FindStringSubmatch(createUserStmt); m != nil { + // Ok, we have at least something - it's probably in MariaDB. + return nil + } + return fmt.Errorf("Create user couldn't be parsed - it is %s", createUserStmt) + } else { + // Worse user detection, only for compat with MySQL 5.6 + stmtSQL := fmt.Sprintf("SELECT USER FROM mysql.user WHERE USER='%s'", + d.Get("user").(string)) - if !rows.Next() && rows.Err() == nil { - d.SetId("") + log.Println("Executing statement:", stmtSQL) + + rows, err := db.Query(stmtSQL) + if err != nil { + return err + } + defer rows.Close() + + if !rows.Next() && rows.Err() == nil { + d.SetId("") + return rows.Err() + } } - return rows.Err() + return nil } func DeleteUser(d *schema.ResourceData, meta interface{}) error { db := meta.(*MySQLConfiguration).Db - stmtSQL := fmt.Sprintf("DROP USER '%s'@'%s'", - d.Get("user").(string), - d.Get("host").(string)) + stmtSQL := fmt.Sprintf("DROP USER ?@?") log.Println("Executing statement:", stmtSQL) - _, err := db.Exec(stmtSQL) + _, err := db.Exec(stmtSQL, + d.Get("user").(string), + d.Get("host").(string)) + if err == nil { d.SetId("") } @@ -251,23 +312,17 @@ func ImportUser(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceDat user := userHost[0] host := userHost[1] + d.Set("user", user) + d.Set("host", host) + err := ReadUser(d, meta) - db := meta.(*MySQLConfiguration).Db - - var count int - err := db.QueryRow("SELECT COUNT(1) FROM mysql.user WHERE user = ? AND host = ?", user, host).Scan(&count) - - if err != nil { - return nil, err - } + return []*schema.ResourceData{d}, err +} - if count == 0 { - return nil, fmt.Errorf("user '%s' not found", d.Id()) +func NewEmptyStringSuppressFunc(k, old, new string, d *schema.ResourceData) bool { + if new == "" { + return true } - d.Set("user", user) - d.Set("host", host) - d.Set("tls_option", "NONE") - - return []*schema.ResourceData{d}, nil + return false } diff --git a/mysql/resource_user_password.go b/mysql/resource_user_password.go index 7ab00358..099335e5 100644 --- a/mysql/resource_user_password.go +++ b/mysql/resource_user_password.go @@ -49,7 +49,7 @@ func SetUserPassword(d *schema.ResourceData, meta interface{}) error { d.Set("plaintext_password", password) } - stmtSQL, err := getSetPasswordStatement(db) + stmtSQL, err := getSetPasswordStatement(meta) if err != nil { return err } @@ -68,12 +68,7 @@ func SetUserPassword(d *schema.ResourceData, meta interface{}) error { } func canReadPassword(meta interface{}) (bool, error) { - db := meta.(*MySQLConfiguration).Db - serverVersion, err := serverVersion(db) - if err != nil { - return false, fmt.Errorf("Could not determine server version: %s", err) - } - + serverVersion := meta.(*MySQLConfiguration).Version ver, _ := version.NewVersion("8.0.0") return serverVersion.LessThan(ver), nil } diff --git a/mysql/resource_user_test.go b/mysql/resource_user_test.go index 3814a47d..28097efb 100644 --- a/mysql/resource_user_test.go +++ b/mysql/resource_user_test.go @@ -12,7 +12,7 @@ import ( func TestAccUser_basic(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckSkipMariaDB(t) }, Providers: testAccProviders, CheckDestroy: testAccUserCheckDestroy, Steps: []resource.TestStep{ @@ -52,7 +52,7 @@ func TestAccUser_basic(t *testing.T) { func TestAccUser_auth(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheckSkipTiDB(t) }, + PreCheck: func() { testAccPreCheckSkipTiDB(t); testAccPreCheckSkipMariaDB(t) }, Providers: testAccProviders, CheckDestroy: testAccUserCheckDestroy, Steps: []resource.TestStep{ @@ -65,6 +65,24 @@ func TestAccUser_auth(t *testing.T) { resource.TestCheckResourceAttr("mysql_user.test", "auth_plugin", "mysql_no_login"), ), }, + { + Config: testAccUserConfig_auth_native, + 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_native_password"), + ), + }, + { + 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"), + ), + }, }, }) } @@ -232,3 +250,14 @@ resource "mysql_user" "test" { auth_plugin = "mysql_no_login" } ` + +const testAccUserConfig_auth_native = ` +resource "mysql_user" "test" { + user = "jdoe" + host = "example.com" + auth_plugin = "mysql_native_password" + + # Hash of "password" + auth_string_hashed = "*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19" +} +` diff --git a/website/docs/r/user.html.markdown b/website/docs/r/user.html.markdown index be4090b7..540db2ab 100644 --- a/website/docs/r/user.html.markdown +++ b/website/docs/r/user.html.markdown @@ -36,6 +36,17 @@ resource "mysql_user" "nologin" { } ``` +## Example Usage with an Authentication Plugin and hashed password + +```hcl +resource "mysql_user" "nologin" { + user = "nologin" + host = "example.com" + auth_plugin = "mysql_native_password" + auth_string_hashed = "*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19" +} +``` + ## Argument Reference The following arguments are supported: @@ -45,6 +56,7 @@ 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_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_plugin`. * `auth_plugin` - (Optional) Use an [authentication plugin][ref-auth-plugins] to authenticate the user instead of using password authentication. Description of the fields allowed in the block below. Conflicts with `password` and `plaintext_password`. +* `auth_string_hashed` - (Optional) Use an already hashed string as a parameter to `auth_plugin`. This can be used with passwords as well as with other auth strings. * `tls_option` - (Optional) An TLS-Option for the `CREATE USER` or `ALTER USER` statement. The value is suffixed to `REQUIRE`. A value of 'SSL' will generate a `CREATE USER ... REQUIRE SSL` statement. See the [MYSQL `CREATE USER` documentation](https://dev.mysql.com/doc/refman/5.7/en/create-user.html) for more. Ignored if MySQL version is under 5.7.0. [ref-auth-plugins]: https://dev.mysql.com/doc/refman/5.7/en/authentication-plugins.html @@ -64,6 +76,7 @@ The `auth_plugin` value supports: [ref-mysql-no-login]: https://dev.mysql.com/doc/refman/5.7/en/no-login-pluggable-authentication.html +* any other auth plugin supported by MySQL. ## Attributes Reference The following attributes are exported: