Skip to content
This repository has been archived by the owner on Jan 12, 2021. It is now read-only.

Adding authentication plugin support, mainly for AWSAuthenticationPlu… #26

Merged
merged 4 commits into from
Mar 12, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
50 changes: 48 additions & 2 deletions mysql/resource_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"fmt"
"log"

"errors"
"github.com/hashicorp/go-version"

"github.com/hashicorp/terraform/helper/schema"
)

Expand Down Expand Up @@ -36,20 +36,50 @@ func resourceUser() *schema.Resource {
Sensitive: true,
StateFunc: hashSum,
},

"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"plaintext_password"},
Sensitive: true,
Deprecated: "Please use plaintext_password instead",
},

"auth": &schema.Schema{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be modified to auth_plugin, and make to be a flat TypeString value? I'm not too sure if we will ever be putting anything else in here - grants are already handled by mysql_grant, and we already have separate fields for plaintext passwords. So having the extra layer here is not entirely useful as it stands right now.

The field should also be made to conflict with all other authentication fields (including password as well, even though it's deprecated, it can still be used).

Type: schema.TypeMap,
Optional: true,
ConflictsWith: []string{"plaintext_password"},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"plugin": {
Type: schema.TypeString,
Optional: false,
},
},
},
},
},
}
}

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))
Expand All @@ -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)
}

Expand All @@ -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")
Expand Down
56 changes: 56 additions & 0 deletions mysql/resource_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) },
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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"
}
}
`
27 changes: 25 additions & 2 deletions website/docs/r/user.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ resource "mysql_user" "jdoe" {
}
```

```hcl
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could a heading be added here denoting that this example is a an example using an authentication plugin?

resource "mysql_user" "nologin" {
user = "nologin"
host = "example.com"
auth {
plugin = "mysql_no_login"
}
}
```

## Argument Reference

The following arguments are supported:
Expand All @@ -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

Expand Down