diff --git a/mmv1/third_party/terraform/fwmodels/provider_model.go.tmpl b/mmv1/third_party/terraform/fwmodels/provider_model.go.tmpl index 61bc8d3fe57e..0260f6cfdd7f 100644 --- a/mmv1/third_party/terraform/fwmodels/provider_model.go.tmpl +++ b/mmv1/third_party/terraform/fwmodels/provider_model.go.tmpl @@ -5,10 +5,17 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +type ExternalCredentialsModel struct { + Audience types.String `tfsdk:"audience"` + ServiceAccountEmail types.String `tfsdk:"service_account_email"` + IdentityTokenFile types.String `tfsdk:"identity_token_file"` +} + // ProviderModel maps provider schema data to a Go type. // When the plugin-framework provider is configured, the Configure function receives data about // the provider block in the configuration. That data is used to populate this struct. type ProviderModel struct { + ExternalCredentials []ExternalCredentialsModel `tfsdk:"external_credentials"` Credentials types.String `tfsdk:"credentials"` AccessToken types.String `tfsdk:"access_token"` ImpersonateServiceAccount types.String `tfsdk:"impersonate_service_account"` diff --git a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl index 47db6d12ad56..36c254543427 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl +++ b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl @@ -251,6 +251,30 @@ func (p *FrameworkProvider) Schema(_ context.Context, _ provider.SchemaRequest, }, }, }, + }, + "external_credentials": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "audience": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + fwvalidators.NonEmptyStringValidator(), + }, + }, + "service_account_email": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + fwvalidators.NonEmptyStringValidator(), + }, + }, + "identity_token_file": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + fwvalidators.NonEmptyStringValidator(), + }, + }, + }, + }, }, }, } diff --git a/mmv1/third_party/terraform/provider/provider.go.tmpl b/mmv1/third_party/terraform/provider/provider.go.tmpl index 066592474b49..683e4311474a 100644 --- a/mmv1/third_party/terraform/provider/provider.go.tmpl +++ b/mmv1/third_party/terraform/provider/provider.go.tmpl @@ -37,14 +37,37 @@ func Provider() *schema.Provider { Type: schema.TypeString, Optional: true, ValidateFunc: ValidateCredentials, - ConflictsWith: []string{"access_token"}, + ConflictsWith: []string{"access_token", "external_credentials"}, }, "access_token": { Type: schema.TypeString, Optional: true, ValidateFunc: ValidateEmptyStrings, - ConflictsWith: []string{"credentials"}, + ConflictsWith: []string{"credentials", "external_credentials"}, + }, + + "external_credentials": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ConflictsWith: []string{"credentials", "access_token"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "audience": { + Type: schema.TypeString, + Required: true, // TODO: validate audience is non-empty string + }, + "service_account_email": { + Type: schema.TypeString, // TODO: validate service_account_email is non-empty string that is a valid email address + Required: true, + }, + "identity_token_file": { + Type: schema.TypeString, // TODO: validate identity_token is non-empty string that is a valid JWT + Required: true, + }, + }, + }, }, "impersonate_service_account": { @@ -267,6 +290,15 @@ func ProviderConfigure(ctx context.Context, d *schema.ResourceData, p *schema.Pr config.Credentials = v.(string) } + + if v, ok := d.GetOk("external_credentials"); ok { + externalCredentials, err := transport_tpg.ExpandExternalCredentials(v) + if err != nil { + return nil, diag.FromErr(err) + } + config.ExternalCredentials = externalCredentials + } + // only check environment variables if neither value was set in config- this // means config beats env var in all cases. if config.AccessToken == "" && config.Credentials == "" { diff --git a/mmv1/third_party/terraform/transport/config.go.tmpl b/mmv1/third_party/terraform/transport/config.go.tmpl index a3e7f323358a..89a93c50b9e6 100644 --- a/mmv1/third_party/terraform/transport/config.go.tmpl +++ b/mmv1/third_party/terraform/transport/config.go.tmpl @@ -30,6 +30,7 @@ import ( "golang.org/x/oauth2" "google.golang.org/grpc" googleoauth "golang.org/x/oauth2/google" + googleoauthexternalaccount "golang.org/x/oauth2/google/externalaccount" appengine "google.golang.org/api/appengine/v1" "google.golang.org/api/bigquery/v2" "google.golang.org/api/bigtableadmin/v2" @@ -182,6 +183,7 @@ type Config struct { DCLConfig AccessToken string Credentials string + ExternalCredentials *googleoauthexternalaccount.Config ImpersonateServiceAccount string ImpersonateServiceAccountDelegates []string Project string @@ -507,6 +509,25 @@ func (c *Config) LoadAndValidate(ctx context.Context) error { return nil } +func ExpandExternalCredentials(v interface{}) (*googleoauthexternalaccount.Config, error) { + ls := v.([]interface{}) + if len(ls) == 0 || ls[0] == nil { + return nil, fmt.Errorf("external_credentials must be a list with at least one element") + } + cfgV := ls[0].(map[string]interface{}) + externalCredentialsConfig := &googleoauthexternalaccount.Config{ + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", + TokenURL: "https://sts.googleapis.com/v1/token", + Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, + Audience: cfgV["audience"].(string), + ServiceAccountImpersonationURL: fmt.Sprintf("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken", cfgV["service_account_email"].(string)), + CredentialSource: &googleoauthexternalaccount.CredentialSource{ + File: cfgV["identity_token_file"].(string), + }, + } + return externalCredentialsConfig, nil +} + func ExpandProviderBatchingConfig(v interface{}) (*BatchingConfig, error) { config := &BatchingConfig{ SendAfter: time.Second * DefaultBatchSendIntervalSec, @@ -594,6 +615,16 @@ func (c *Config) logGoogleIdentities() error { // Get a TokenSource based on the Google Credentials configured. // If initialCredentialsOnly is true, don't follow the impersonation settings and return the initial set of creds. func (c *Config) getTokenSource(clientScopes []string, initialCredentialsOnly bool) (oauth2.TokenSource, error) { + + if c.ExternalCredentials != nil { + log.Printf("[INFO] Using external credentials") + creds, err := googleoauthexternalaccount.NewTokenSource(c.Context, *c.ExternalCredentials) + if err != nil { + return nil, fmt.Errorf("error creating token source from external credentials: %s", err) + } + return creds, nil + } + creds, err := c.GetCredentials(clientScopes, initialCredentialsOnly) if err != nil { return nil, fmt.Errorf("%s", err)