From 4393a7bf9ba6324d6a9342111327b9514228235f Mon Sep 17 00:00:00 2001 From: Michael Lin Date: Thu, 13 Jun 2024 18:50:45 -0700 Subject: [PATCH] Add support for GCP IAM impersonation --- go.mod | 2 +- postgresql/config.go | 55 ++++++++++++++++++++++---------- postgresql/provider.go | 37 +++++++++++++-------- website/docs/index.html.markdown | 23 ++++++++++++- 4 files changed, 85 insertions(+), 32 deletions(-) diff --git a/go.mod b/go.mod index fc4c880a..5d4290d5 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( gocloud.dev v0.34.0 golang.org/x/net v0.26.0 golang.org/x/oauth2 v0.10.0 + google.golang.org/api v0.134.0 ) require ( @@ -91,7 +92,6 @@ require ( golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/api v0.134.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230731193218-e0aa005b6bdf // indirect google.golang.org/grpc v1.57.0 // indirect diff --git a/postgresql/config.go b/postgresql/config.go index c4ed30e8..02d3e5d9 100644 --- a/postgresql/config.go +++ b/postgresql/config.go @@ -12,9 +12,12 @@ import ( "github.com/blang/semver" _ "github.com/lib/pq" // PostgreSQL db + "gocloud.dev/gcp" + "gocloud.dev/gcp/cloudsql" "gocloud.dev/postgres" _ "gocloud.dev/postgres/awspostgres" - _ "gocloud.dev/postgres/gcppostgres" + "gocloud.dev/postgres/gcppostgres" + "google.golang.org/api/impersonate" ) type featureName uint @@ -157,21 +160,22 @@ type ClientCertificateConfig struct { // Config - provider config type Config struct { - Scheme string - Host string - Port int - Username string - Password string - DatabaseUsername string - Superuser bool - SSLMode string - ApplicationName string - Timeout int - ConnectTimeoutSec int - MaxConns int - ExpectedVersion semver.Version - SSLClientCert *ClientCertificateConfig - SSLRootCertPath string + Scheme string + Host string + Port int + Username string + Password string + DatabaseUsername string + Superuser bool + SSLMode string + ApplicationName string + Timeout int + ConnectTimeoutSec int + MaxConns int + ExpectedVersion semver.Version + SSLClientCert *ClientCertificateConfig + SSLRootCertPath string + GCPIAMImpersonateServiceAccount string } // Client struct holding connection string @@ -280,6 +284,25 @@ func (c *Client) Connect() (*DBConnection, error) { var err error if c.config.Scheme == "postgres" { db, err = sql.Open(proxyDriverName, dsn) + } else if c.config.Scheme == "gcppostgres" && c.config.GCPIAMImpersonateServiceAccount != "" { + ts, err := impersonate.CredentialsTokenSource(context.Background(), impersonate.CredentialsConfig{ + TargetPrincipal: c.config.GCPIAMImpersonateServiceAccount, + Scopes: []string{"https://www.googleapis.com/auth/sqlservice.admin"}, + }) + if err != nil { + return nil, fmt.Errorf("Error creating token source with service account impersonation of %s: %w", c.config.GCPIAMImpersonateServiceAccount, err) + } + client, err := gcp.NewHTTPClient(gcp.DefaultTransport(), ts) + if err != nil { + return nil, fmt.Errorf("Error creating HTTP client with service account impersonation of %s: %w", c.config.GCPIAMImpersonateServiceAccount, err) + } + certSource := cloudsql.NewCertSourceWithIAM(client, ts) + opener := gcppostgres.URLOpener{CertSource: certSource} + dbURL, err := url.Parse(dsn) + if err != nil { + return nil, fmt.Errorf("Error parsing connection string: %w", err) + } + db, err = opener.OpenPostgresURL(context.Background(), dbURL) } else { db, err = postgres.Open(context.Background(), dsn) } diff --git a/postgresql/provider.go b/postgresql/provider.go index 7f15c92e..2743d7b6 100644 --- a/postgresql/provider.go +++ b/postgresql/provider.go @@ -3,9 +3,10 @@ package postgresql import ( "context" "fmt" + "os" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "os" "github.com/blang/semver" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -103,6 +104,13 @@ func Provider() *schema.Provider { Description: "MS Azure tenant ID (see: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config.html)", }, + "gcp_iam_impersonate_service_account": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: "Service account to impersonate when using GCP IAM authentication.", + }, + // Conection username can be different than database username with user name mapas (e.g.: in Azure) // See https://www.postgresql.org/docs/current/auth-username-maps.html "database_username": { @@ -323,19 +331,20 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { } config := Config{ - Scheme: d.Get("scheme").(string), - Host: host, - Port: port, - Username: username, - Password: password, - DatabaseUsername: d.Get("database_username").(string), - Superuser: d.Get("superuser").(bool), - SSLMode: sslMode, - ApplicationName: "Terraform provider", - ConnectTimeoutSec: d.Get("connect_timeout").(int), - MaxConns: d.Get("max_connections").(int), - ExpectedVersion: version, - SSLRootCertPath: d.Get("sslrootcert").(string), + Scheme: d.Get("scheme").(string), + Host: host, + Port: port, + Username: username, + Password: password, + DatabaseUsername: d.Get("database_username").(string), + Superuser: d.Get("superuser").(bool), + SSLMode: sslMode, + ApplicationName: "Terraform provider", + ConnectTimeoutSec: d.Get("connect_timeout").(int), + MaxConns: d.Get("max_connections").(int), + ExpectedVersion: version, + SSLRootCertPath: d.Get("sslrootcert").(string), + GCPIAMImpersonateServiceAccount: d.Get("gcp_iam_impersonate_service_account").(string), } if value, ok := d.GetOk("clientcert"); ok { diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index d8ff9062..2d7de76c 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -214,7 +214,28 @@ To enable GoCloud for GCP SQL, set `scheme` to `gcppostgres` and `host` to the c For GCP, GoCloud also requires the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to be set to the service account credentials file. These credentials can be created here: https://console.cloud.google.com/iam-admin/serviceaccounts -See also: https://cloud.google.com/docs/authentication/production +In addition, the provider supports service account impersonation with the `gcp_iam_impersonate_service_account` option. You must ensure: + +- The IAM database user has sufficient permissions to connect to the database, e.g., `roles/cloudsql.instanceUser` +- The principal (IAM user or IAM service account) behind the `GOOGLE_APPLICATION_CREDENTIALS` has sufficient permissions to impersonate the provided service account. Learn more from [roles for service account authentication](https://cloud.google.com/iam/docs/service-account-permissions). + +```hcl +provider "postgresql" { + scheme = "gcppostgres" + host = "test-project/europe-west3/test-instance" + port = 5432 + + username = "service_account_id@$project_id.iam" + gcp_iam_impersonate_service_account = "service_account_id@$project_id.iam.gserviceaccount.com" + + superuser = false +} +``` + +See also: + +- https://cloud.google.com/docs/authentication/production +- https://cloud.google.com/sql/docs/postgres/iam-logins --- **Note**