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

New data source for oidc tokens #2269

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/3739.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-datasource
`google_service_account_id_token`
```
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/sirupsen/logrus v1.2.0 // indirect
github.com/stoewer/go-strcase v1.0.1
github.com/terraform-providers/terraform-provider-random v0.0.0-20190925211435-95c131714b03
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
google.golang.org/api v0.26.0
)
Expand Down
37 changes: 30 additions & 7 deletions google-beta/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -734,37 +734,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
127 changes: 127 additions & 0 deletions google-beta/data_source_google_service_account_id_token.go
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
}
111 changes: 111 additions & 0 deletions google-beta/data_source_google_service_account_id_token_test.go
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)
}
1 change: 1 addition & 0 deletions google-beta/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,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