Skip to content

Commit

Permalink
Add google_impersonated_credential datasource (#1597)
Browse files Browse the repository at this point in the history
Merged PR #1597.
  • Loading branch information
chrisst authored and modular-magician committed Apr 2, 2019
1 parent 65c466e commit e582f0a
Show file tree
Hide file tree
Showing 10 changed files with 247 additions and 9 deletions.
2 changes: 1 addition & 1 deletion build/terraform
2 changes: 1 addition & 1 deletion build/terraform-beta
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package google

import (
"fmt"
"log"

"strings"
"time"

"github.com/hashicorp/terraform/helper/schema"
iamcredentials "google.golang.org/api/iamcredentials/v1"
)

func dataSourceGoogleServiceAccountAccessToken() *schema.Resource {

return &schema.Resource{
Read: dataSourceGoogleServiceAccountAccessTokenRead,
Schema: map[string]*schema.Schema{
"target_service_account": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validateRegexp("(" + strings.Join(PossibleServiceAccountNames, "|") + ")"),
},
"access_token": {
Type: schema.TypeString,
Sensitive: true,
Computed: true,
},
"scopes": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
StateFunc: func(v interface{}) string {
return canonicalizeServiceScope(v.(string))
},
},
// ValidateFunc is not yet supported on lists or sets.
},
"delegates": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validateRegexp(ServiceAccountLinkRegex),
},
},
"lifetime": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateDuration(), // duration <=3600s; TODO: support validteDuration(min,max)
Default: "3600s",
},
},
}
}

func dataSourceGoogleServiceAccountAccessTokenRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
log.Printf("[INFO] Acquire Service Account AccessToken for %s", d.Get("target_service_account").(string))

service := config.clientIamCredentials

name := fmt.Sprintf("projects/-/serviceAccounts/%s", d.Get("target_service_account").(string))
tokenRequest := &iamcredentials.GenerateAccessTokenRequest{
Lifetime: d.Get("lifetime").(string),
Delegates: convertStringSet(d.Get("delegates").(*schema.Set)),
Scope: canonicalizeServiceScopes(convertStringSet(d.Get("scopes").(*schema.Set))),
}
at, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, tokenRequest).Do()
if err != nil {
return err
}

d.SetId(time.Now().UTC().String())
d.Set("access_token", at.AccessToken)

return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package google

import (
"testing"

"fmt"

"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

func testAccCheckServiceAccountAccessTokenValue(name, value string) resource.TestCheckFunc {
return func(s *terraform.State) error {
ms := s.RootModule()
rs, ok := ms.Outputs[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}

// TODO: validate the token belongs to the service account
if rs.Value == "" {
return fmt.Errorf("%s Cannot be empty", name)
}

return nil
}
}

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

resourceName := "data.google_service_account_access_token.default"

targetServiceAccountEmail := getTestServiceAccountFromEnv(t)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckGoogleServiceAccountAccessToken_datasource(targetServiceAccountEmail),
Destroy: true,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "target_service_account", targetServiceAccountEmail),
testAccCheckServiceAccountAccessTokenValue("access_token", targetServiceAccountEmail),
),
},
},
})
}

func testAccCheckGoogleServiceAccountAccessToken_datasource(targetServiceAccountID string) string {

return fmt.Sprintf(`
data "google_service_account_access_token" "default" {
target_service_account = "%s"
scopes = ["userinfo-email", "https://www.googleapis.com/auth/cloud-platform"]
lifetime = "30s"
}
output "access_token" {
value = "${data.google_service_account_access_token.default.access_token}"
}
`, targetServiceAccountID)
}
9 changes: 9 additions & 0 deletions third_party/terraform/utils/config.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
dnsBeta "google.golang.org/api/dns/v1beta2"
file "google.golang.org/api/file/v1beta1"
"google.golang.org/api/iam/v1"
iamcredentials "google.golang.org/api/iamcredentials/v1"
cloudlogging "google.golang.org/api/logging/v2"
"google.golang.org/api/pubsub/v1"
"google.golang.org/api/runtimeconfig/v1beta1"
Expand Down Expand Up @@ -81,6 +82,7 @@ type Config struct {
clientDns *dns.Service
clientDnsBeta *dnsBeta.Service
clientFilestore *file.Service
clientIamCredentials *iamcredentials.Service
clientKms *cloudkms.Service
clientLogging *cloudlogging.Service
clientPubsub *pubsub.Service
Expand Down Expand Up @@ -255,6 +257,13 @@ func (c *Config) LoadAndValidate() error {
}
c.clientIAM.UserAgent = userAgent

log.Printf("[INFO] Instantiating Google Cloud IAMCredentials Client...")
c.clientIamCredentials, err = iamcredentials.New(client)
if err != nil {
return err
}
c.clientIamCredentials.UserAgent = userAgent

log.Printf("[INFO] Instantiating Google Cloud Service Management Client...")
c.clientServiceMan, err = servicemanagement.New(client)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions third_party/terraform/utils/provider.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ func Provider() terraform.ResourceProvider {
"google_project_organization_policy": dataSourceGoogleProjectOrganizationPolicy(),
"google_project_services": dataSourceGoogleProjectServices(),
"google_service_account": dataSourceGoogleServiceAccount(),
"google_service_account_access_token": dataSourceGoogleServiceAccountAccessToken(),
"google_service_account_key": dataSourceGoogleServiceAccountKey(),
"google_storage_bucket_object": dataSourceGoogleStorageBucketObject(),
"google_storage_object_signed_url": dataSourceGoogleSignedUrl(),
Expand Down
9 changes: 5 additions & 4 deletions third_party/terraform/utils/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import (

const (
// Copied from the official Google Cloud auto-generated client.
ProjectRegex = "(?:(?:[-a-z0-9]{1,63}\\.)*(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?):)?(?:[0-9]{1,19}|(?:[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?))"
RegionRegex = "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?"
SubnetworkRegex = "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?"
ProjectRegex = "(?:(?:[-a-z0-9]{1,63}\\.)*(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?):)?(?:[0-9]{1,19}|(?:[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?))"
ProjectRegexWildCard = "(?:(?:[-a-z0-9]{1,63}\\.)*(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?):)?(?:[0-9]{1,19}|(?:[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?)|-)"
RegionRegex = "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?"
SubnetworkRegex = "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?"

SubnetworkLinkRegex = "projects/(" + ProjectRegex + ")/regions/(" + RegionRegex + ")/subnetworks/(" + SubnetworkRegex + ")$"

Expand All @@ -35,7 +36,7 @@ var (
// 4 and 28 since the first and last character are excluded.
ServiceAccountNameRegex = fmt.Sprintf(RFC1035NameTemplate, 4, 28)

ServiceAccountLinkRegexPrefix = "projects/" + ProjectRegex + "/serviceAccounts/"
ServiceAccountLinkRegexPrefix = "projects/" + ProjectRegexWildCard + "/serviceAccounts/"
PossibleServiceAccountNames = []string{
AppEngineServiceAccountNameRegex,
ComputeServiceAccountNameRegex,
Expand Down
7 changes: 5 additions & 2 deletions third_party/terraform/website-compiled/google.erb
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@
<li<%%= sidebar_current("docs-google-datasource-service-account") %>>
<a href="/docs/providers/google/d/datasource_google_service_account.html">google_service_account</a>
</li>
<li<%%= sidebar_current("docs-google-datasource-service-account-access-token") %>>
<a href="/docs/providers/google/d/datasource_google_service_account_access_token.html">google_service_account_access_token</a>
</li>
<li<%%= sidebar_current("docs-google-datasource-service-account-key") %>>
<a href="/docs/providers/google/d/datasource_google_service_account_key.html">google_service_account_key</a>
</li>
Expand Down Expand Up @@ -630,12 +633,12 @@
<li<%%= sidebar_current("docs-google-dns-managed-zone") %>>
<a href="/docs/providers/google/r/dns_managed_zone.html">google_dns_managed_zone</a>
</li>

<% unless version == 'ga' %>
<li<%%= sidebar_current("docs-google-dns-policy") %>>
<a href="/docs/providers/google/r/dns_policy.html">google_dns_policy</a>
</li>

<% end -%>
<li<%%= sidebar_current("docs-google-dns-record-set") %>>
<a href="/docs/providers/google/r/dns_record_set.html">google_dns_record_set</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
layout: "google"
page_title: "Google: google_service_account_access_token"
sidebar_current: "docs-google-service-account-access-token"
description: |-
Produces access_token for impersonated service accounts
---

# google\_service\_account\_access\_token

This data source 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`

```sh
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"
}
data "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 = "${data.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. `["storage-ro", "cloud-platform"]`)
* `delegates` (Optional) - Deegate 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.

0 comments on commit e582f0a

Please sign in to comment.