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

Add oidc token support #5670

Closed
wants to merge 15 commits into from
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ require (
github.com/sirupsen/logrus v1.2.0 // indirect
github.com/stoewer/go-strcase v1.0.2
github.com/terraform-providers/terraform-provider-random v0.0.0-20190925200408-30dac3233094
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
google.golang.org/api v0.25.0
google.golang.org/api v0.26.0
)

go 1.14
4 changes: 3 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,8 @@ google.golang.org/api v0.23.0 h1:YlvGEOq2NA2my8cZ/9V8BcEO9okD48FlJcdqN0xJL3s=
google.golang.org/api v0.23.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.25.0 h1:LodzhlzZEUfhXzNUMIfVlf9Gr6Ua5MMtoFWh7+f47qA=
google.golang.org/api v0.25.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.26.0 h1:VJZ8h6E8ip82FRpQl848c5vAadxlTXrUh8RzQzSRm08=
google.golang.org/api v0.26.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
Expand Down Expand Up @@ -749,4 +751,4 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
40 changes: 31 additions & 9 deletions google/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -694,37 +694,59 @@ 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) {
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
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/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
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
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/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) {
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
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/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,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