diff --git a/mysql/provider.go b/mysql/provider.go index 5feee7b9..b7ce4d02 100644 --- a/mysql/provider.go +++ b/mysql/provider.go @@ -7,7 +7,6 @@ import ( "database/sql" "encoding/json" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "log" "net" "net/url" @@ -20,6 +19,7 @@ import ( "github.com/go-sql-driver/mysql" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -28,6 +28,7 @@ import ( "cloud.google.com/go/cloudsqlconn" cloudsql "cloud.google.com/go/cloudsqlconn/mysql/mysql" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" ) @@ -37,6 +38,10 @@ const ( nativePasswords = "native" userNotFoundErrCode = 1133 unknownUserErrCode = 1396 + azEnvPublic = "public" + azEnvChina = "china" + azEnvGerman = "german" + azEnvUSGovernment = "usgovernment" ) type OneConnection struct { @@ -106,7 +111,7 @@ func Provider() *schema.Provider { "ALL_PROXY", "all_proxy", }, nil), - ValidateFunc: validation.StringMatch(regexp.MustCompile("^socks5h?://.*:\\d+$"), "The proxy URL is not a valid socks url."), + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^socks5h?://.*:\d+$`), "The proxy URL is not a valid socks url."), }, "tls": { @@ -186,6 +191,53 @@ func Provider() *schema.Provider { Optional: true, Default: false, }, + "azure_config": { + Type: schema.TypeList, + Optional: true, + Default: nil, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "tenant_id": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "AZURE_TENANT_ID", + "ARM_TENANT_ID", + }, nil), + }, + "client_id": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "AZURE_CLIENT_ID", + "ARM_CLIENT_ID", + }, nil), + }, + "client_secret": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "AZURE_CLIENT_SECRET", + "ARM_CLIENT_SECRET", + }, nil), + }, + "environment": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + azEnvPublic, + azEnvChina, + azEnvGerman, + azEnvUSGovernment, + }, false), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "AZURE_ENVIRONMENT", + "ARM_ENVIRONMENT", + }, nil), + }, + }, + }, + }, }, DataSourcesMap: map[string]*schema.Resource{ @@ -303,18 +355,56 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{} } } else if strings.HasPrefix(endpoint, "azure://") { + var azCredential azcore.TokenCredential + var azTenantId, azClientId, azClientSecret, azEnvironment string + var err error + + azEnvironment = os.Getenv("AZURE_ENVIRONMENT") + if azEnvironment == "" { + azEnvironment = os.Getenv("ARM_ENVIRONMENT") + } + + azAuthList := d.Get("azure_config").([]interface{}) + if len(azAuthList) > 0 { + azAuthMap := azAuthList[0].(map[string]interface{}) + if azAuthMap["tenant_id"] != nil { + azTenantId = azAuthMap["tenant_id"].(string) + } + if azAuthMap["client_id"] != nil { + azClientId = azAuthMap["client_id"].(string) + } + if azAuthMap["client_secret"] != nil { + azClientSecret = azAuthMap["client_secret"].(string) + } + if azAuthMap["environment"] != nil { + azEnvironment = azAuthMap["environment"].(string) + } + } + + if azTenantId != "" && azClientId != "" && azClientSecret != "" { + log.Printf("[DEBUG] Using Azure Client Secret Credentials: client_id = %s, tenant_id = %s", azClientId, azTenantId) + azCredential, err = azidentity.NewClientSecretCredential(azTenantId, azClientId, azClientSecret, nil) + } else { + log.Printf("[DEBUG] Using Azure Default Credentials") + azCredential, err = azidentity.NewDefaultAzureCredential(nil) + } // Azure AD does not support native password authentication but go-sql-driver/mysql // has to be configured only with ?allowClearTextPasswords=true not with allowNativePasswords=false in this case allowClearTextPasswords = true - azCredential, err := azidentity.NewDefaultAzureCredential(nil) endpoint = strings.ReplaceAll(endpoint, "azure://", "") - azScope := "https://ossrdbms-aad.database.windows.net" - if os.Getenv("ARM_ENVIRONMENT") == "china" { + + var azScope string + switch azEnvironment { + case azEnvChina: azScope = "https://ossrdbms-aad.database.chinacloudapi.cn" - } else if os.Getenv("ARM_ENVIRONMENT") == "german" { + case azEnvGerman: azScope = "https://ossrdbms-aad.database.chinacloudapi.de" - } else if os.Getenv("ARM_ENVIRONMENT") == "usgovernment" { + case azEnvUSGovernment: azScope = "https://ossrdbms-aad.database.usgovcloudapi.net" + case azEnvPublic: + fallthrough + default: + azScope = "https://ossrdbms-aad.database.windows.net" } if err != nil { @@ -327,7 +417,7 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{} ) if err != nil { - return nil, diag.Errorf("failed to get token from Azure AD %v", err) + return nil, diag.Errorf("failed to get token from Azure AD: %v", err) } password = azToken.Token diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index 82f15586..93e6281b 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -143,8 +143,6 @@ See also: [Authentication at Google](https://cloud.google.com/docs/authenticatio ### Azure MySQL server with AzureAD auth enabled connection -For connections to Azure MySQL server with AzureAD auth enabled, the provider connects using DefaultAzureCredential from the Azure SDK for Go. - To use this authentication, add `azure://` to the endpoint. This will lead to ignore `password` field which would be replaced by Azure AD token of currently obtained identity. You have to use `username` as stated in Azure documentation. @@ -157,6 +155,24 @@ provider "mysql" { } ``` +By default the provider will connect using DefaultAzureCredential from the Azure SDK for Go. The credentials can be provided by setting the `AZURE_*` environment variables, using a workload identity or a managed identity present on the host. + +You can also further configure the Azure connection using the `azure_config` block: + +```hcl +# Configure the MySQL provider for Azure Mysql Server with specific credentials +provider "mysql" { + endpoint = "azure://your-azure-instance-name.mysql.database.azure.com" + username = "username@yourtenant.onmicrosoft.com" + + azure_config { + tenant_id = "your-tenant-id" + client_id = "your-client-id" + client_secret = var.client_secret + } +} +``` + See also: [Azure Active Directory authentication for MySQL](https://learn.microsoft.com/en-us/azure/mysql/flexible-server/how-to-azure-ad). ## SOCKS5 Proxy Support @@ -187,3 +203,8 @@ The following arguments are supported: * `authentication_plugin` - (Optional) Sets the authentication plugin, it can be one of the following: `native` or `cleartext`. Defaults to `native`. * `iam_database_authentication` - (Optional) For Cloud SQL databases, it enabled the use of IAM authentication. Make sure to declare the `password` field with a temporary OAuth2 token of the user that will connect to the MySQL server. * `private_ip` - (Optional) Whether to use a connection to an instance with a private ip. Defaults to `false`. This argument only applies to CloudSQL and is ignored elsewhere. +* `azure_config` - (Optional) Sets the Azure configuration for the connection. This is a block containing the following arguments: + * `client_id` - (Optional) The client ID for the Azure AD application. Can also be sourced from the `AZURE_CLIENT_ID` or `ARM_CLIENT_ID` environment variables. + * `client_secret` - (Optional) The client secret for the Azure AD application. Can also be sourced from the `AZURE_CLIENT_SECRET` or `ARM_CLIENT_SECRET` environment variables. + * `tenant_id` - (Optional) The tenant ID for the Azure AD application. Can also be sourced from the `AZURE_TENANT_ID` or `ARM_TENANT_ID` environment variables. + * `environment` - (Optional) The Azure environment to use. Can also be sourced from the `AZURE_ENVIRONMENT` or `ARM_ENVIRONMENT` environment variables. Possible values are `public`, `china`, `german`, `usgovernment`. Defaults to `public`.