Skip to content

Commit 8c46e7d

Browse files
Add support for GCP IAM impersonation (#26)
Back port of PR: cyrilgdn#448 --- Add support for GCP IAM service account impersonation ### Use cases The company has a centralized service account that is used for Terraform automation. However, such GSA should not be used to access the database directly where each database will have its own IAM DB users. This added an option to impersonate the database IAM user via the centralized GSA. As long as the centralized GSA has sufficient permissions to impersonate as the database IAM DB user, it can be used to perform database automation in Terraform. ### Testing ```hcl resource "google_sql_database_instance" "self" {} resource "google_sql_user" "admin" {} resource "google_service_account" "db_iam_admin" {} resource "google_sql_user" "iam_admin" { name = trimsuffix(google_service_account.db_iam_admin.email, ".gserviceaccount.com") instance = google_sql_database_instance.self.name type = "CLOUD_IAM_SERVICE_ACCOUNT" } resource "google_project_iam_member" "iam_admin_project_iam_members" { for_each = toset(["roles/cloudsql.client", "roles/cloudsql.instanceUser"]) member = google_service_account.db_iam_admin.member role = each.key } provider "postgresql" { scheme = "gcppostgres" host = google_sql_database_instance.self.connection_name username = trimsuffix(google_service_account.db_iam_admin.email, ".gserviceaccount.com") gcp_iam_impersonate_service_account = google_service_account.db_iam_admin.email port = 5432 superuser = false alias = "iamAdmin" } # it should work and able to apply resources using the IAM db user resource "postgresql_*" "*" { provider = postgresql.iamAdmin // * } ``` Co-authored-by: Michael Lin <mlzc@hey.com>
1 parent 1af22bb commit 8c46e7d

File tree

4 files changed

+87
-31
lines changed

4 files changed

+87
-31
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
gocloud.dev v0.34.0
1717
golang.org/x/net v0.13.0
1818
golang.org/x/oauth2 v0.10.0
19+
google.golang.org/api v0.134.0
1920
)
2021

2122
require (
@@ -91,7 +92,6 @@ require (
9192
golang.org/x/sys v0.10.0 // indirect
9293
golang.org/x/text v0.11.0 // indirect
9394
golang.org/x/time v0.3.0 // indirect
94-
google.golang.org/api v0.134.0 // indirect
9595
google.golang.org/appengine v1.6.7 // indirect
9696
google.golang.org/genproto/googleapis/rpc v0.0.0-20230731193218-e0aa005b6bdf // indirect
9797
google.golang.org/grpc v1.57.0 // indirect

postgresql/config.go

+43-16
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ import (
1212

1313
"github.com/blang/semver"
1414
_ "github.com/lib/pq" // PostgreSQL db
15+
"gocloud.dev/gcp"
16+
"gocloud.dev/gcp/cloudsql"
1517
"gocloud.dev/postgres"
1618
_ "gocloud.dev/postgres/awspostgres"
17-
_ "gocloud.dev/postgres/gcppostgres"
19+
"gocloud.dev/postgres/gcppostgres"
20+
"google.golang.org/api/impersonate"
1821
)
1922

2023
type featureName uint
@@ -157,21 +160,22 @@ type ClientCertificateConfig struct {
157160

158161
// Config - provider config
159162
type Config struct {
160-
Scheme string
161-
Host string
162-
Port int
163-
Username string
164-
Password string
165-
DatabaseUsername string
166-
Superuser bool
167-
SSLMode string
168-
ApplicationName string
169-
Timeout int
170-
ConnectTimeoutSec int
171-
MaxConns int
172-
ExpectedVersion semver.Version
173-
SSLClientCert *ClientCertificateConfig
174-
SSLRootCertPath string
163+
Scheme string
164+
Host string
165+
Port int
166+
Username string
167+
Password string
168+
DatabaseUsername string
169+
Superuser bool
170+
SSLMode string
171+
ApplicationName string
172+
Timeout int
173+
ConnectTimeoutSec int
174+
MaxConns int
175+
ExpectedVersion semver.Version
176+
SSLClientCert *ClientCertificateConfig
177+
SSLRootCertPath string
178+
GCPIAMImpersonateServiceAccount string
175179
}
176180

177181
// Client struct holding connection string
@@ -280,6 +284,8 @@ func (c *Client) Connect() (*DBConnection, error) {
280284
var err error
281285
if c.config.Scheme == "postgres" {
282286
db, err = sql.Open(proxyDriverName, dsn)
287+
} else if c.config.Scheme == "gcppostgres" && c.config.GCPIAMImpersonateServiceAccount != "" {
288+
db, err = openImpersonatedGCPDBConnection(context.Background(), dsn, c.config.GCPIAMImpersonateServiceAccount)
283289
} else {
284290
db, err = postgres.Open(context.Background(), dsn)
285291
}
@@ -345,3 +351,24 @@ func fingerprintCapabilities(db *sql.DB) (*semver.Version, error) {
345351

346352
return &version, nil
347353
}
354+
355+
func openImpersonatedGCPDBConnection(ctx context.Context, dsn string, targetServiceAccountEmail string) (*sql.DB, error) {
356+
ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{
357+
TargetPrincipal: targetServiceAccountEmail,
358+
Scopes: []string{"https://www.googleapis.com/auth/sqlservice.admin"},
359+
})
360+
if err != nil {
361+
return nil, fmt.Errorf("Error creating token source with service account impersonation of %s: %w", targetServiceAccountEmail, err)
362+
}
363+
client, err := gcp.NewHTTPClient(gcp.DefaultTransport(), ts)
364+
if err != nil {
365+
return nil, fmt.Errorf("Error creating HTTP client with service account impersonation of %s: %w", targetServiceAccountEmail, err)
366+
}
367+
certSource := cloudsql.NewCertSourceWithIAM(client, ts)
368+
opener := gcppostgres.URLOpener{CertSource: certSource}
369+
dbURL, err := url.Parse(dsn)
370+
if err != nil {
371+
return nil, fmt.Errorf("Error parsing connection string: %w", err)
372+
}
373+
return opener.OpenPostgresURL(ctx, dbURL)
374+
}

postgresql/provider.go

+21-13
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ func Provider() *schema.Provider {
104104
Description: "MS Azure tenant ID (see: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config.html)",
105105
},
106106

107+
"gcp_iam_impersonate_service_account": {
108+
Type: schema.TypeString,
109+
Optional: true,
110+
Default: "",
111+
Description: "Service account to impersonate when using GCP IAM authentication.",
112+
},
113+
107114
// Conection username can be different than database username with user name mapas (e.g.: in Azure)
108115
// See https://www.postgresql.org/docs/current/auth-username-maps.html
109116
"database_username": {
@@ -327,19 +334,20 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
327334
}
328335

329336
config := Config{
330-
Scheme: d.Get("scheme").(string),
331-
Host: host,
332-
Port: port,
333-
Username: username,
334-
Password: password,
335-
DatabaseUsername: d.Get("database_username").(string),
336-
Superuser: d.Get("superuser").(bool),
337-
SSLMode: sslMode,
338-
ApplicationName: "Terraform provider",
339-
ConnectTimeoutSec: d.Get("connect_timeout").(int),
340-
MaxConns: d.Get("max_connections").(int),
341-
ExpectedVersion: version,
342-
SSLRootCertPath: d.Get("sslrootcert").(string),
337+
Scheme: d.Get("scheme").(string),
338+
Host: host,
339+
Port: port,
340+
Username: username,
341+
Password: password,
342+
DatabaseUsername: d.Get("database_username").(string),
343+
Superuser: d.Get("superuser").(bool),
344+
SSLMode: sslMode,
345+
ApplicationName: "Terraform provider",
346+
ConnectTimeoutSec: d.Get("connect_timeout").(int),
347+
MaxConns: d.Get("max_connections").(int),
348+
ExpectedVersion: version,
349+
SSLRootCertPath: d.Get("sslrootcert").(string),
350+
GCPIAMImpersonateServiceAccount: d.Get("gcp_iam_impersonate_service_account").(string),
343351
}
344352

345353
if value, ok := d.GetOk("clientcert"); ok {

website/docs/index.html.markdown

+22-1
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,28 @@ To enable GoCloud for GCP SQL, set `scheme` to `gcppostgres` and `host` to the c
213213
For GCP, GoCloud also requires the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to be set to the service account credentials file.
214214
These credentials can be created here: https://console.cloud.google.com/iam-admin/serviceaccounts
215215

216-
See also: https://cloud.google.com/docs/authentication/production
216+
In addition, the provider supports service account impersonation with the `gcp_iam_impersonate_service_account` option. You must ensure:
217+
218+
- The IAM database user has sufficient permissions to connect to the database, e.g., `roles/cloudsql.instanceUser`
219+
- 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).
220+
221+
```hcl
222+
provider "postgresql" {
223+
scheme = "gcppostgres"
224+
host = "test-project/europe-west3/test-instance"
225+
port = 5432
226+
227+
username = "service_account_id@$project_id.iam"
228+
gcp_iam_impersonate_service_account = "service_account_id@$project_id.iam.gserviceaccount.com"
229+
230+
superuser = false
231+
}
232+
```
233+
234+
See also:
235+
236+
- https://cloud.google.com/docs/authentication/production
237+
- https://cloud.google.com/sql/docs/postgres/iam-logins
217238

218239
---
219240
**Note**

0 commit comments

Comments
 (0)