Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for CREATE AADUSER to mysql_user resource #77

Merged
merged 5 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
128 changes: 120 additions & 8 deletions mysql/resource_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"log"
"regexp"
"strings"
Expand Down Expand Up @@ -59,12 +60,38 @@ func resourceUser() *schema.Resource {
ConflictsWith: []string{"plaintext_password", "password"},
},

"aad_identity": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "user",
ValidateFunc: validation.StringInSlice([]string{
"user",
"group",
"service_principal",
}, false),
},
"identity": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
},
},

"auth_string_hashed": {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
DiffSuppressFunc: NewEmptyStringSuppressFunc,
RequiredWith: []string{"auth_plugin"},
ConflictsWith: []string{"plaintext_password", "password"},
kratkyzobak marked this conversation as resolved.
Show resolved Hide resolved
},

Expand All @@ -85,12 +112,20 @@ func CreateUser(ctx context.Context, d *schema.ResourceData, meta interface{}) d

var authStm string
var auth string
var createObj = "USER"

if v, ok := d.GetOk("auth_plugin"); ok {
auth = v.(string)
}

if len(auth) > 0 {
if auth == "AWSAuthenticationPlugin" {
if auth == "aad_auth" {
// aad_auth is plugin but Microsoft uses another statement to create this kind of users
createObj = "AADUSER"
if _, ok := d.GetOk("aad_identity"); !ok {
return diag.Errorf("aad_identity is required for aad_auth")
}
} else if auth == "AWSAuthenticationPlugin" {
authStm = " IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'"
} else {
// mysql_no_login, auth_pam, ...
Expand All @@ -100,13 +135,36 @@ func CreateUser(ctx context.Context, d *schema.ResourceData, meta interface{}) d
if v, ok := d.GetOk("auth_string_hashed"); ok {
hashed := v.(string)
if hashed != "" {
if authStm == "" {
return diag.Errorf("auth_string_hashed is not supported for auth plugin %s", auth)
}
authStm = fmt.Sprintf("%s AS '%s'", authStm, hashed)
}
}

stmtSQL := fmt.Sprintf("CREATE USER '%s'@'%s'",
d.Get("user").(string),
d.Get("host").(string))
var stmtSQL string

if createObj == "AADUSER" {
var aadIdentity = d.Get("aad_identity").(*schema.Set).List()[0].(map[string]interface{})

if aadIdentity["type"].(string) == "service_principal" {
// CREATE AADUSER 'mysqlProtocolLoginName"@"mysqlHostRestriction' IDENTIFIED BY 'identityId'
stmtSQL = fmt.Sprintf("CREATE AADUSER '%s'@'%s' IDENTIFIED BY '%s'",
d.Get("user").(string),
d.Get("host").(string),
aadIdentity["identity"].(string))
} else {
// CREATE AADUSER 'identityName"@"mysqlHostRestriction' AS 'mysqlProtocolLoginName'
stmtSQL = fmt.Sprintf("CREATE AADUSER '%s'@'%s' AS '%s'",
aadIdentity["identity"].(string),
d.Get("host").(string),
d.Get("user").(string))
}
} else {
stmtSQL = fmt.Sprintf("CREATE USER '%s'@'%s'",
d.Get("user").(string),
d.Get("host").(string))
}

var password string
if v, ok := d.GetOk("plaintext_password"); ok {
Expand All @@ -121,14 +179,23 @@ func CreateUser(ctx context.Context, d *schema.ResourceData, meta interface{}) d

if authStm != "" {
stmtSQL = stmtSQL + authStm
} else {
} else if password != "" {
stmtSQL = stmtSQL + fmt.Sprintf(" IDENTIFIED BY '%s'", password)
}

requiredVersion, _ := version.NewVersion("5.7.0")

var updateStmtSql = ""

if getVersionFromMeta(ctx, meta).GreaterThan(requiredVersion) && d.Get("tls_option").(string) != "" {
stmtSQL += fmt.Sprintf(" REQUIRE %s", d.Get("tls_option").(string))
if createObj == "AADUSER" {
kratkyzobak marked this conversation as resolved.
Show resolved Hide resolved
updateStmtSql = fmt.Sprintf("ALTER USER '%s'@'%s' REQUIRE %s",
d.Get("user").(string),
d.Get("host").(string),
d.Get("tls_option").(string))
} else {
stmtSQL += fmt.Sprintf(" REQUIRE %s", d.Get("tls_option").(string))
}
}

log.Println("Executing statement:", stmtSQL)
Expand All @@ -140,6 +207,15 @@ func CreateUser(ctx context.Context, d *schema.ResourceData, meta interface{}) d
user := fmt.Sprintf("%s@%s", d.Get("user").(string), d.Get("host").(string))
d.SetId(user)

if updateStmtSql != "" {
log.Println("Executing statement:", updateStmtSql)
_, err = db.ExecContext(ctx, updateStmtSql)
if err != nil {
d.Set("tls_option", "")
return diag.Errorf("failed executing SQL: %v", err)
}
}

return nil
}

Expand Down Expand Up @@ -257,8 +333,44 @@ func ReadUser(ctx context.Context, d *schema.ResourceData, meta interface{}) dia
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])

if m[3] == "aad_auth" {
// AADGroup:98e61c8d-e104-4f8c-b1a6-7ae873617fe6:upn:Doe_Family_Group
// AADUser:98e61c8d-e104-4f8c-b1a6-7ae873617fe6:upn:little.johny@does.onmicrosoft.com
// AADSP:98e61c8d-e104-4f8c-b1a6-7ae873617fe6:upn:mysqlUserName
parts := strings.Split(m[4], ":")
if parts[0] == "AADSP" {
// service principals are referenced by UUID only
kratkyzobak marked this conversation as resolved.
Show resolved Hide resolved
d.Set("aad_identity", []map[string]interface{}{
{
"type": "service_principal",
"identity": parts[1],
},
})
} else if len(parts) >= 4 {
// users and groups should be referenced by UPN / group name
if parts[0] == "AADUser" {
d.Set("aad_identity", []map[string]interface{}{
{
"type": "user",
"identity": strings.Join(parts[3:], ":"),
},
})
} else {
d.Set("aad_identity", []map[string]interface{}{
{
"type": "group",
"identity": strings.Join(parts[3:], ":"),
},
})
}
} else {
return diag.Errorf("AAD identity couldn't be parsed - it is %s", m[4])
}
} else {
d.Set("auth_string_hashed", m[4])
}
return nil
}

Expand Down
22 changes: 22 additions & 0 deletions website/docs/r/user.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ resource "mysql_user" "nologin" {
}
```

## Example Usage with AzureAD Authentication Plugin

```hcl
resource "mysql_user" "aadupn" {
user = "aliasToUseWhenConnectiong"
auth_plugin = "aad_auth"
aad_identity {
type = "user" # user | group | service_principal
identity = "little.johny@doe.onmicrosoft.com" # upn | group name | client id of service principal
}
}
```

## Argument Reference

The following arguments are supported:
Expand All @@ -57,6 +70,7 @@ The following arguments are supported:
* `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.
* `aad_identity` - (Optional) Required when `auth_plugin` is `aad_auth`. This should be block containing `type` and `identity`. `type` can be one of `user`, `group` and `service_principal`. `identity` then should containt either UPN of user, name of group or Client ID of service principal.
* `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
Expand All @@ -76,6 +90,14 @@ The `auth_plugin` value supports:

[ref-mysql-no-login]: https://dev.mysql.com/doc/refman/5.7/en/no-login-pluggable-authentication.html

* `aad_auth` - Uses `CREATE AADUSER` statement to create user instead of `CREATE USER` to create user
with [AzureAD authentication][ref-azure-aadauth] to [Azure Database for MySQL][ref-azure-mysql].
When specified, you need to specify `aad_identity`. For more information about AzureAD authentication into MySQL
see [here][ref-azure-aadauth]. You have to use AAD authenticated administrator mysql session to use this plugin.

[ref-azure-aadauth]: https://learn.microsoft.com/en-us/azure/mysql/flexible-server/how-to-azure-ad
[ref-azure-mysql]: https://learn.microsoft.com/en-us/azure/mysql/

* any other auth plugin supported by MySQL.
## Attributes Reference

Expand Down