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 ephemeral_google_service_account_access_token #8720

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/12140.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:none

```
2 changes: 1 addition & 1 deletion google-beta/fwprovider/framework_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,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.GoogleEphemeralServiceAccountAccessToken,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package resourcemanager

import (
"context"
"fmt"
"time"

"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-beta/google-beta/fwtransport"
"github.com/hashicorp/terraform-provider-google-beta/google-beta/fwutils"
"github.com/hashicorp/terraform-provider-google-beta/google-beta/fwvalidators"
"github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource"
"google.golang.org/api/iamcredentials/v1"
)

var _ ephemeral.EphemeralResource = &googleEphemeralServiceAccountAccessToken{}

func GoogleEphemeralServiceAccountAccessToken() ephemeral.EphemeralResource {
return &googleEphemeralServiceAccountAccessToken{}
}

type googleEphemeralServiceAccountAccessToken struct {
providerConfig *fwtransport.FrameworkProviderConfig
}

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

type ephemeralServiceAccountAccessTokenModel struct {
TargetServiceAccount types.String `tfsdk:"target_service_account"`
AccessToken types.String `tfsdk:"access_token"`
Scopes types.Set `tfsdk:"scopes"`
Delegates types.Set `tfsdk:"delegates"`
Lifetime types.String `tfsdk:"lifetime"`
}

func (p *googleEphemeralServiceAccountAccessToken) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
resp.Schema.Description = "This ephemeral resource provides a google oauth2 access_token for a different service account than the one initially running the script."
resp.Schema.MarkdownDescription = "This ephemeral resource provides a google oauth2 access_token for a different service account than the one initially running the script."

resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"target_service_account": schema.StringAttribute{
Description: "The service account to impersonate (e.g. `[email protected]`)",
Required: true,
Validators: []validator.String{
fwvalidators.ServiceAccountEmailValidator{},
},
},
"access_token": schema.StringAttribute{
Description: "The `access_token` representing the new generated identity.",
Sensitive: true,
Computed: true,
},
"lifetime": schema.StringAttribute{
Description: "Lifetime of the impersonated token (defaults to its max: `3600s`)",
Optional: true,
Computed: true,
Validators: []validator.String{
fwvalidators.BoundedDuration{
MinDuration: 0,
MaxDuration: 3600 * time.Second,
},
},
},
"scopes": schema.SetAttribute{
Description: "The scopes the new credential should have (e.g. `['cloud-platform']`)",
Required: true,
ElementType: types.StringType,
},
"delegates": schema.SetAttribute{
Description: "Delegate chain of approvals needed to perform full impersonation. Specify the fully qualified service account name. (e.g. `['projects/-/serviceAccounts/[email protected]']`)",
Optional: true,
ElementType: types.StringType,
Validators: []validator.Set{
setvalidator.ValueStringsAre(fwvalidators.ServiceAccountEmailValidator{}),
},
},
},
}
}

func (p *googleEphemeralServiceAccountAccessToken) 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 *googleEphemeralServiceAccountAccessToken) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
var data ephemeralServiceAccountAccessTokenModel

resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

// This is the default value for the lifetime of the access token
// Both ephemeral resources and data sources do not allow you to set a value for this attribute in the schema
if data.Lifetime.IsNull() {
data.Lifetime = types.StringValue("3600s")
}

service := p.providerConfig.NewIamCredentialsClient(p.providerConfig.UserAgent)
name := fmt.Sprintf("projects/-/serviceAccounts/%s", data.TargetServiceAccount.ValueString())

ScopesSetValue, diags := data.Scopes.ToSetValue(ctx)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

var delegates []string
if !data.Delegates.IsNull() {
delegates = fwutils.StringSet(data.Delegates)
}

tokenRequest := &iamcredentials.GenerateAccessTokenRequest{
Lifetime: data.Lifetime.ValueString(),
Delegates: delegates,
Scope: tpgresource.CanonicalizeServiceScopes(fwutils.StringSet(ScopesSetValue)),
}

at, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, tokenRequest).Do()
if err != nil {
resp.Diagnostics.AddError(
"Error generating access token",
fmt.Sprintf("Error generating access token: %s", err),
)
return
}

data.AccessToken = types.StringValue(at.AccessToken)
resp.Diagnostics.Append(resp.Result.Set(ctx, data)...)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package resourcemanager_test

import (
"fmt"
"testing"

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

func TestAccEphemeralServiceAccountToken_basic(t *testing.T) {
t.Parallel()

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

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

func TestAccEphemeralServiceAccountToken_withDelegates(t *testing.T) {
t.Parallel()

project := envvar.GetTestProjectFromEnv()
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) },
ExternalProviders: map[string]resource.ExternalProvider{
"time": {},
},
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testAccEphemeralServiceAccountToken_withDelegates(initialServiceAccount, delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo, targetServiceAccountEmail, project),
},
},
})
}

func TestAccEphemeralServiceAccountToken_withCustomLifetime(t *testing.T) {
t.Parallel()

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

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

func testAccEphemeralServiceAccountToken_basic(serviceAccountEmail string) string {
return fmt.Sprintf(`
ephemeral "google_service_account_access_token" "token" {
target_service_account = "%s"
scopes = ["https://www.googleapis.com/auth/cloud-platform"]
}
`, serviceAccountEmail)
}

func testAccEphemeralServiceAccountToken_withDelegates(initialServiceAccountEmail, delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo, targetServiceAccountEmail, project string) string {
return fmt.Sprintf(`
ephemeral "google_service_account_access_token" "test" {
target_service_account = "%s"
delegates = [
"%s",
"%s",
]
scopes = ["https://www.googleapis.com/auth/cloud-platform"]
lifetime = "3600s"
}

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

func testAccEphemeralServiceAccountToken_withCustomLifetime(serviceAccountEmail string) string {
return fmt.Sprintf(`
ephemeral "google_service_account_access_token" "token" {
target_service_account = "%s"
scopes = ["https://www.googleapis.com/auth/cloud-platform"]
lifetime = "3600s"
}
`, serviceAccountEmail)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
subcategory: "Cloud Platform"
description: |-
Produces access_token for impersonated service accounts
---

# google_service_account_access_token

This ephemeral resource provides a google `oauth2` `access_token` for a different service account than the one initially running the script.

For more information see
[the official documentation](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials) as well as [iamcredentials.generateAccessToken()](https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/generateAccessToken)

## Example Usage

To allow `service_A` to impersonate `service_B`, grant the [Service Account Token Creator](https://cloud.google.com/iam/docs/service-accounts#the_service_account_token_creator_role) on B to A.

In the IAM policy below, `service_A` is given the Token Creator role impersonate `service_B`

```hcl
resource "google_service_account_iam_binding" "token-creator-iam" {
service_account_id = "projects/-/serviceAccounts/[email protected]"
role = "roles/iam.serviceAccountTokenCreator"
members = [
"serviceAccount:[email protected]",
]
}
```

Once the IAM permissions are set, you can apply the new token to a provider bootstrapped with it. Any resources that references the aliased provider will run as the new identity.

In the example below, `google_project` will run as `service_B`.

```hcl
provider "google" {
}

data "google_client_config" "default" {
provider = google
}

ephemeral "google_service_account_access_token" "default" {
provider = google
target_service_account = "[email protected]"
scopes = ["userinfo-email", "cloud-platform"]
lifetime = "300s"
}

provider "google" {
alias = "impersonated"
access_token = ephemeral.google_service_account_access_token.default.access_token
}

data "google_client_openid_userinfo" "me" {
provider = google.impersonated
}

output "target-email" {
value = data.google_client_openid_userinfo.me.email
}
```

> *Note*: the generated token is non-refreshable and can have a maximum `lifetime` of `3600` seconds.

## Argument Reference

The following arguments are supported:

* `target_service_account` (Required) - The service account _to_ impersonate (e.g. `[email protected]`)
* `scopes` (Required) - The scopes the new credential should have (e.g. `["cloud-platform"]`)
* `delegates` (Optional) - Delegate chain of approvals needed to perform full impersonation. Specify the fully qualified service account name. (e.g. `["projects/-/serviceAccounts/[email protected]"]`)
* `lifetime` (Optional) Lifetime of the impersonated token (defaults to its max: `3600s`).

## Attributes Reference

The following attribute is exported:

* `access_token` - The `access_token` representing the new generated identity.