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

ephemeral: add google_service_account_id_token #12141

Open
wants to merge 10 commits into
base: FEATURE-BRANCH-ephemeral-resource
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,6 @@ func (p *FrameworkProvider) Functions(_ context.Context) []func() function.Funct
// EphemeralResources defines the resources that are of ephemeral type implemented in the provider.
func (p *FrameworkProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource {
return []func() ephemeral.EphemeralResource{
// TODO!
resourcemanager.GoogleEphemeralServiceAccountIdToken,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package resourcemanager

import (
"context"
"fmt"

"google.golang.org/api/idtoken"
"google.golang.org/api/option"

"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-provider-google/google/fwmodels"
"github.com/hashicorp/terraform-provider-google/google/fwtransport"
"github.com/hashicorp/terraform-provider-google/google/fwutils"
"github.com/hashicorp/terraform-provider-google/google/fwvalidators"
"google.golang.org/api/iamcredentials/v1"
)

var _ ephemeral.EphemeralResource = &googleEphemeralServiceAccountIdToken{}

func GoogleEphemeralServiceAccountIdToken() ephemeral.EphemeralResource {
return &googleEphemeralServiceAccountIdToken{}
}

type googleEphemeralServiceAccountIdToken struct {
providerConfig *fwtransport.FrameworkProviderConfig
}

func (p *googleEphemeralServiceAccountIdToken) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_service_account_id_token"
}

type ephemeralServiceAccountIdTokenModel struct {
TargetAudience types.String `tfsdk:"target_audience"`
TargetServiceAccount types.String `tfsdk:"target_service_account"`
Delegates types.Set `tfsdk:"delegates"`
IncludeEmail types.Bool `tfsdk:"include_email"`
IdToken types.String `tfsdk:"id_token"`
}

func (p *googleEphemeralServiceAccountIdToken) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
SarahFrench marked this conversation as resolved.
Show resolved Hide resolved
"target_audience": schema.StringAttribute{
Required: true,
},
"target_service_account": schema.StringAttribute{
Optional: true,
Validators: []validator.String{
fwvalidators.ServiceAccountEmailValidator{},
},
},
"delegates": schema.SetAttribute{
Optional: true,
ElementType: types.StringType,
Validators: []validator.Set{
setvalidator.ValueStringsAre(fwvalidators.ServiceAccountEmailValidator{}),
},
},
"include_email": schema.BoolAttribute{
Optional: true,
Computed: true,
},
"id_token": schema.StringAttribute{
Computed: true,
Sensitive: true,
},
},
}
}

func (p *googleEphemeralServiceAccountIdToken) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

pd, ok := req.ProviderData.(*fwtransport.FrameworkProviderConfig)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *fwtransport.FrameworkProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}

// Required for accessing userAgent and passing as an argument into a util function
p.providerConfig = pd
}

func (p *googleEphemeralServiceAccountIdToken) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
var data ephemeralServiceAccountIdTokenModel

resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)

targetAudience := data.TargetAudience.ValueString()
creds := fwtransport.GetCredentials(ctx, fwmodels.ProviderModel{}, false, &resp.Diagnostics)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This'll be something that'll need refactoring after the muxing fixes are merged; the GetCredentials on the (SDK) Config struct is different to the version implemented on the FrameworkProviderConfig struct.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is gross, but necessary to make sure the framework version of GetCredentials receives the data it needs:

Suggested change
creds := fwtransport.GetCredentials(ctx, fwmodels.ProviderModel{}, false, &resp.Diagnostics)
model := fwmodels.ProviderModel{
Credentials: p.providerConfig.Credentials,
AccessToken: p.providerConfig.AccessToken,
ImpersonateServiceAccount: p.providerConfig.ImpersonateServiceAccount,
ImpersonateServiceAccountDelegates: p.providerConfig.ImpersonateServiceAccountDelegates,
Project: p.providerConfig.Project,
BillingProject: p.providerConfig.BillingProject,
Scopes: p.providerConfig.Scopes,
UniverseDomain: p.providerConfig.UniverseDomain,
}
creds := fwtransport.GetCredentials(ctx, model, false, &resp.Diagnostics)

fwmodels.ProviderModel is the struct that is populated with data from the provider block when the provider is being configured. The fwtransport.GetCredentials function is written in a way that makes it very coupled with being used in the context of a provider being configured, whereas the SDK version of GetCredentials is a method on the Config struct and can be run outside the context of a provider being configured.

Just more evidence that the original muxing was poorly-planned and 💩


targetServiceAccount := data.TargetServiceAccount.ValueString()
// If a target service account is provided, use the API to generate the idToken
if targetServiceAccount != "" {
BBBmau marked this conversation as resolved.
Show resolved Hide resolved
service := p.providerConfig.NewIamCredentialsClient(p.providerConfig.UserAgent)
name := fmt.Sprintf("projects/-/serviceAccounts/%s", targetServiceAccount)
DelegatesSetValue, _ := data.Delegates.ToSetValue(ctx)
BBBmau marked this conversation as resolved.
Show resolved Hide resolved
tokenRequest := &iamcredentials.GenerateIdTokenRequest{
Audience: targetAudience,
IncludeEmail: data.IncludeEmail.ValueBool(),
Delegates: fwutils.StringSet(DelegatesSetValue),
}
at, err := service.Projects.ServiceAccounts.GenerateIdToken(name, tokenRequest).Do()
if err != nil {
resp.Diagnostics.AddError(
"Error calling iamcredentials.GenerateIdToken",
err.Error(),
)
return
}

data.IdToken = types.StringValue(at.Token)
resp.Diagnostics.Append(resp.Result.Set(ctx, data)...)
return
}

// If no target service account, use the default credentials
ctx = context.Background()
co := []option.ClientOption{}
if creds.JSON != nil {
co = append(co, idtoken.WithCredentialsJSON(creds.JSON))
}

idTokenSource, err := idtoken.NewTokenSource(ctx, targetAudience, co...)
if err != nil {
resp.Diagnostics.AddError(
"Unable to retrieve TokenSource",
err.Error(),
)
return
}
idToken, err := idTokenSource.Token()
if err != nil {
resp.Diagnostics.AddError(
"Unable to retrieve Token",
err.Error(),
)
return
}

data.IdToken = types.StringValue(idToken.AccessToken)
resp.Diagnostics.Append(resp.Result.Set(ctx, data)...)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package resourcemanager_test

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-provider-google/google/acctest"
"github.com/hashicorp/terraform-provider-google/google/envvar"
)

func TestEphemeralServiceAccountIdToken_basic(t *testing.T) {
SarahFrench marked this conversation as resolved.
Show resolved Hide resolved
t.Parallel()

serviceAccount := envvar.GetTestServiceAccountFromEnv(t)
targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "idtoken", serviceAccount)

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
ExternalProviders: map[string]resource.ExternalProvider{
"time": {},
},
Steps: []resource.TestStep{
{
Config: testAccEphemeralServiceAccountIdToken_basic(targetServiceAccountEmail),
},
},
})
}

func TestEphemeralServiceAccountIdToken_withDelegates(t *testing.T) {
BBBmau marked this conversation as resolved.
Show resolved Hide resolved
SarahFrench marked this conversation as resolved.
Show resolved Hide resolved
t.Parallel()

initialServiceAccount := envvar.GetTestServiceAccountFromEnv(t)
delegateServiceAccountEmailOne := acctest.BootstrapServiceAccount(t, "delegate1", initialServiceAccount) // SA_2
delegateServiceAccountEmailTwo := acctest.BootstrapServiceAccount(t, "delegate2", delegateServiceAccountEmailOne) // SA_3
targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "target", delegateServiceAccountEmailTwo) // SA_4

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
ExternalProviders: map[string]resource.ExternalProvider{
"time": {},
},
Steps: []resource.TestStep{
{
Config: testAccEphemeralServiceAccountIdToken_withDelegates(delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo, targetServiceAccountEmail),
},
},
})
}

func TestEphemeralServiceAccountIdToken_withIncludeEmail(t *testing.T) {
SarahFrench marked this conversation as resolved.
Show resolved Hide resolved
t.Parallel()

serviceAccount := envvar.GetTestServiceAccountFromEnv(t)
targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "idtoken-email", serviceAccount)

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
ExternalProviders: map[string]resource.ExternalProvider{
"time": {},
},
Steps: []resource.TestStep{
{
Config: testAccEphemeralServiceAccountIdToken_withIncludeEmail(targetServiceAccountEmail),
},
},
})
}

func testAccEphemeralServiceAccountIdToken_basic(serviceAccountEmail string) string {
return fmt.Sprintf(`
ephemeral "google_service_account_id_token" "token" {
target_service_account = "%s"
target_audience = "https://example.com"
}
`, serviceAccountEmail)
}

func testAccEphemeralServiceAccountIdToken_withDelegates(delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo, targetServiceAccountEmail string) string {
return fmt.Sprintf(`
ephemeral "google_service_account_id_token" "token" {
target_service_account = "%s"
delegates = [
"%s",
"%s",
]
target_audience = "https://example.com"
}

# The delegation chain is:
# SA_1 (initialServiceAccountEmail) -> SA_2 (delegateServiceAccountEmailOne) -> SA_3 (delegateServiceAccountEmailTwo) -> SA_4 (targetServiceAccountEmail)
`, targetServiceAccountEmail, delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo)
}

func testAccEphemeralServiceAccountIdToken_withIncludeEmail(serviceAccountEmail string) string {
return fmt.Sprintf(`
ephemeral "google_service_account_id_token" "token" {
target_service_account = "%s"
target_audience = "https://example.com"
include_email = true
}
`, serviceAccountEmail)
}
Loading