Skip to content

Commit

Permalink
New data source for oidc tokens (#3739)
Browse files Browse the repository at this point in the history
Co-authored-by: Cameron Thornton <[email protected]>
Co-authored-by: salmaan rashid <[email protected]>
  • Loading branch information
3 people authored Jul 14, 2020
1 parent 076c3c2 commit 5cc4d2f
Show file tree
Hide file tree
Showing 5 changed files with 368 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package google

import (
"time"

"fmt"
"strings"

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

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"golang.org/x/net/context"
)

const (
userInfoScope = "https://www.googleapis.com/auth/userinfo.email"
)

func dataSourceGoogleServiceAccountIdToken() *schema.Resource {

return &schema.Resource{
Read: dataSourceGoogleServiceAccountIdTokenRead,
Schema: map[string]*schema.Schema{
"target_audience": {
Type: schema.TypeString,
Required: true,
},
"target_service_account": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateRegexp("(" + strings.Join(PossibleServiceAccountNames, "|") + ")"),
},
"delegates": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validateRegexp(ServiceAccountLinkRegex),
},
},
"include_email": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
// Not used currently
// https://github.com/googleapis/google-api-go-client/issues/542
// "format": {
// Type: schema.TypeString,
// Optional: true,
// ValidateFunc: validation.StringInSlice([]string{
// "FULL", "STANDARD"}, true),
// Default: "STANDARD",
// },
"id_token": {
Type: schema.TypeString,
Sensitive: true,
Computed: true,
},
},
}
}

func dataSourceGoogleServiceAccountIdTokenRead(d *schema.ResourceData, meta interface{}) error {

config := meta.(*Config)
targetAudience := d.Get("target_audience").(string)
creds, err := config.GetCredentials([]string{userInfoScope})
if err != nil {
return fmt.Errorf("error calling getCredentials(): %v", err)
}

ts := creds.TokenSource

// If the source token is just an access_token, all we can do is use the iamcredentials api to get an id_token
if _, ok := ts.(staticTokenSource); ok {
// Use
// https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateIdToken
service := config.clientIamCredentials
name := fmt.Sprintf("projects/-/serviceAccounts/%s", d.Get("target_service_account").(string))
tokenRequest := &iamcredentials.GenerateIdTokenRequest{
Audience: targetAudience,
IncludeEmail: d.Get("include_email").(bool),
Delegates: convertStringSet(d.Get("delegates").(*schema.Set)),
}
at, err := service.Projects.ServiceAccounts.GenerateIdToken(name, tokenRequest).Do()
if err != nil {
return fmt.Errorf("error calling iamcredentials.GenerateIdToken: %v", err)
}

d.SetId(time.Now().UTC().String())
d.Set("id_token", at.Token)

return nil
}

tok, err := ts.Token()
if err != nil {
return fmt.Errorf("unable to get Token() from tokenSource: %v", err)
}

// only user-credential TokenSources have refreshTokens
if tok.RefreshToken != "" {
return fmt.Errorf("unsupported Credential Type supplied. Use serviceAccount 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 {
return fmt.Errorf("unable to retrieve TokenSource: %v", err)
}
idToken, err := idTokenSource.Token()
if err != nil {
return fmt.Errorf("unable to retrieve Token: %v", err)
}

d.SetId(time.Now().UTC().String())
d.Set("id_token", idToken.AccessToken)

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

import (
"context"
"testing"

"fmt"

"google.golang.org/api/idtoken"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
)

const targetAudience = "https://foo.bar/"

func testAccCheckServiceAccountIdTokenValue(name, audience string) resource.TestCheckFunc {
return func(s *terraform.State) error {
ms := s.RootModule()

rs, ok := ms.Resources[name]
if !ok {
return fmt.Errorf("can't find %s in state", name)
}

v, ok := rs.Primary.Attributes["id_token"]
if !ok {
return fmt.Errorf("id_token not found")
}

_, err := idtoken.Validate(context.Background(), v, audience)
if err != nil {
return fmt.Errorf("token validation failed: %v", err)
}

return nil
}
}

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

resourceName := "data.google_service_account_id_token.default"

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckGoogleServiceAccountIdToken_basic(targetAudience),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "target_audience", targetAudience),
testAccCheckServiceAccountIdTokenValue(resourceName, targetAudience),
),
},
},
})
}

func testAccCheckGoogleServiceAccountIdToken_basic(targetAudience string) string {

return fmt.Sprintf(`
data "google_service_account_id_token" "default" {
target_audience = "%s"
}
`, targetAudience)
}

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

resourceName := "data.google_service_account_id_token.default"
serviceAccount := getTestServiceAccountFromEnv(t)
targetServiceAccountEmail := BootstrapServiceAccount(t, getTestProjectFromEnv(), serviceAccount)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckGoogleServiceAccountIdToken_impersonation_datasource(targetAudience, targetServiceAccountEmail),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "target_audience", targetAudience),
testAccCheckServiceAccountIdTokenValue(resourceName, targetAudience),
),
},
},
})
}

func testAccCheckGoogleServiceAccountIdToken_impersonation_datasource(targetAudience string, targetServiceAccount 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"
}
provider google {
alias = "impersonated"
access_token = data.google_service_account_access_token.default.access_token
}
data "google_service_account_id_token" "default" {
provider = google.impersonated
target_service_account = "%s"
target_audience = "%s"
}
`, targetServiceAccount, targetServiceAccount, targetAudience)
}
37 changes: 30 additions & 7 deletions third_party/terraform/utils/config.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -638,37 +638,60 @@ func (c *Config) synchronousTimeout() time.Duration {
}

func (c *Config) getTokenSource(clientScopes []string) (oauth2.TokenSource, error) {
creds, err := c.GetCredentials(clientScopes)
if err != nil {
return nil, fmt.Errorf("%s", err)
}
return creds.TokenSource, nil
}

// staticTokenSource is used to be able to identify static token sources without reflection.
type staticTokenSource struct {
oauth2.TokenSource
}

func (c *Config) GetCredentials(clientScopes []string) (googleoauth.Credentials, error) {
if c.AccessToken != "" {
contents, _, err := pathorcontents.Read(c.AccessToken)
if err != nil {
return nil, fmt.Errorf("Error loading access token: %s", err)
return googleoauth.Credentials{}, fmt.Errorf("Error loading access token: %s", err)
}

log.Printf("[INFO] Authenticating using configured Google JSON 'access_token'...")
log.Printf("[INFO] -- Scopes: %s", clientScopes)
token := &oauth2.Token{AccessToken: contents}
return oauth2.StaticTokenSource(token), nil

return googleoauth.Credentials{
TokenSource: staticTokenSource{oauth2.StaticTokenSource(token)},
}, nil
}

if c.Credentials != "" {
contents, _, err := pathorcontents.Read(c.Credentials)
if err != nil {
return nil, fmt.Errorf("Error loading credentials: %s", err)
return googleoauth.Credentials{}, fmt.Errorf("error loading credentials: %s", err)
}

creds, err := googleoauth.CredentialsFromJSON(context.Background(), []byte(contents), clientScopes...)
creds, err := googleoauth.CredentialsFromJSON(c.context, []byte(contents), clientScopes...)
if err != nil {
return nil, fmt.Errorf("Unable to parse credentials: %s", err)
return googleoauth.Credentials{}, fmt.Errorf("unable to parse credentials from '%s': %s", contents, err)
}

log.Printf("[INFO] Authenticating using configured Google JSON 'credentials'...")
log.Printf("[INFO] -- Scopes: %s", clientScopes)
return creds.TokenSource, nil
return *creds, nil
}

log.Printf("[INFO] Authenticating using DefaultClient...")
log.Printf("[INFO] -- Scopes: %s", clientScopes)
return googleoauth.DefaultTokenSource(context.Background(), clientScopes...)

defaultTS, err := googleoauth.DefaultTokenSource(context.Background(), clientScopes...)
if err != nil {
return googleoauth.Credentials{}, fmt.Errorf("Error loading Default TokenSource: %s", err)
}
return googleoauth.Credentials{
TokenSource: defaultTS,
}, err
}

// Remove the `/{{version}}/` from a base path if present.
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 @@ -210,6 +210,7 @@ func Provider() terraform.ResourceProvider {
"google_secret_manager_secret_version": dataSourceSecretManagerSecretVersion(),
"google_service_account": dataSourceGoogleServiceAccount(),
"google_service_account_access_token": dataSourceGoogleServiceAccountAccessToken(),
"google_service_account_id_token": dataSourceGoogleServiceAccountIdToken(),
"google_service_account_key": dataSourceGoogleServiceAccountKey(),
"google_sql_ca_certs": dataSourceGoogleSQLCaCerts(),
"google_storage_bucket_object": dataSourceGoogleStorageBucketObject(),
Expand Down
Loading

0 comments on commit 5cc4d2f

Please sign in to comment.