Skip to content

Commit 4cdeb75

Browse files
modular-magicianc2thornsalrashid123
authored
New data source for oidc tokens (#3739) (#2269)
Co-authored-by: Cameron Thornton <[email protected]> Co-authored-by: salmaan rashid <[email protected]> Signed-off-by: Modular Magician <[email protected]> Co-authored-by: Cameron Thornton <[email protected]> Co-authored-by: salmaan rashid <[email protected]>
1 parent 24bde3c commit 4cdeb75

File tree

8 files changed

+376
-7
lines changed

8 files changed

+376
-7
lines changed

.changelog/3739.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:new-datasource
2+
`google_service_account_id_token`
3+
```

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/sirupsen/logrus v1.2.0 // indirect
2121
github.com/stoewer/go-strcase v1.0.1
2222
github.com/terraform-providers/terraform-provider-random v0.0.0-20190925211435-95c131714b03
23+
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
2324
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
2425
google.golang.org/api v0.26.0
2526
)

google-beta/config.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -734,37 +734,60 @@ func (c *Config) synchronousTimeout() time.Duration {
734734
}
735735

736736
func (c *Config) getTokenSource(clientScopes []string) (oauth2.TokenSource, error) {
737+
creds, err := c.GetCredentials(clientScopes)
738+
if err != nil {
739+
return nil, fmt.Errorf("%s", err)
740+
}
741+
return creds.TokenSource, nil
742+
}
743+
744+
// staticTokenSource is used to be able to identify static token sources without reflection.
745+
type staticTokenSource struct {
746+
oauth2.TokenSource
747+
}
748+
749+
func (c *Config) GetCredentials(clientScopes []string) (googleoauth.Credentials, error) {
737750
if c.AccessToken != "" {
738751
contents, _, err := pathorcontents.Read(c.AccessToken)
739752
if err != nil {
740-
return nil, fmt.Errorf("Error loading access token: %s", err)
753+
return googleoauth.Credentials{}, fmt.Errorf("Error loading access token: %s", err)
741754
}
742755

743756
log.Printf("[INFO] Authenticating using configured Google JSON 'access_token'...")
744757
log.Printf("[INFO] -- Scopes: %s", clientScopes)
745758
token := &oauth2.Token{AccessToken: contents}
746-
return oauth2.StaticTokenSource(token), nil
759+
760+
return googleoauth.Credentials{
761+
TokenSource: staticTokenSource{oauth2.StaticTokenSource(token)},
762+
}, nil
747763
}
748764

749765
if c.Credentials != "" {
750766
contents, _, err := pathorcontents.Read(c.Credentials)
751767
if err != nil {
752-
return nil, fmt.Errorf("Error loading credentials: %s", err)
768+
return googleoauth.Credentials{}, fmt.Errorf("error loading credentials: %s", err)
753769
}
754770

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

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

765781
log.Printf("[INFO] Authenticating using DefaultClient...")
766782
log.Printf("[INFO] -- Scopes: %s", clientScopes)
767-
return googleoauth.DefaultTokenSource(context.Background(), clientScopes...)
783+
784+
defaultTS, err := googleoauth.DefaultTokenSource(context.Background(), clientScopes...)
785+
if err != nil {
786+
return googleoauth.Credentials{}, fmt.Errorf("Error loading Default TokenSource: %s", err)
787+
}
788+
return googleoauth.Credentials{
789+
TokenSource: defaultTS,
790+
}, err
768791
}
769792

770793
// Remove the `/{{version}}/` from a base path if present.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package google
2+
3+
import (
4+
"time"
5+
6+
"fmt"
7+
"strings"
8+
9+
iamcredentials "google.golang.org/api/iamcredentials/v1"
10+
"google.golang.org/api/idtoken"
11+
"google.golang.org/api/option"
12+
13+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
14+
"golang.org/x/net/context"
15+
)
16+
17+
const (
18+
userInfoScope = "https://www.googleapis.com/auth/userinfo.email"
19+
)
20+
21+
func dataSourceGoogleServiceAccountIdToken() *schema.Resource {
22+
23+
return &schema.Resource{
24+
Read: dataSourceGoogleServiceAccountIdTokenRead,
25+
Schema: map[string]*schema.Schema{
26+
"target_audience": {
27+
Type: schema.TypeString,
28+
Required: true,
29+
},
30+
"target_service_account": {
31+
Type: schema.TypeString,
32+
Optional: true,
33+
ValidateFunc: validateRegexp("(" + strings.Join(PossibleServiceAccountNames, "|") + ")"),
34+
},
35+
"delegates": {
36+
Type: schema.TypeSet,
37+
Optional: true,
38+
Elem: &schema.Schema{
39+
Type: schema.TypeString,
40+
ValidateFunc: validateRegexp(ServiceAccountLinkRegex),
41+
},
42+
},
43+
"include_email": {
44+
Type: schema.TypeBool,
45+
Optional: true,
46+
Default: false,
47+
},
48+
// Not used currently
49+
// https://github.com/googleapis/google-api-go-client/issues/542
50+
// "format": {
51+
// Type: schema.TypeString,
52+
// Optional: true,
53+
// ValidateFunc: validation.StringInSlice([]string{
54+
// "FULL", "STANDARD"}, true),
55+
// Default: "STANDARD",
56+
// },
57+
"id_token": {
58+
Type: schema.TypeString,
59+
Sensitive: true,
60+
Computed: true,
61+
},
62+
},
63+
}
64+
}
65+
66+
func dataSourceGoogleServiceAccountIdTokenRead(d *schema.ResourceData, meta interface{}) error {
67+
68+
config := meta.(*Config)
69+
targetAudience := d.Get("target_audience").(string)
70+
creds, err := config.GetCredentials([]string{userInfoScope})
71+
if err != nil {
72+
return fmt.Errorf("error calling getCredentials(): %v", err)
73+
}
74+
75+
ts := creds.TokenSource
76+
77+
// If the source token is just an access_token, all we can do is use the iamcredentials api to get an id_token
78+
if _, ok := ts.(staticTokenSource); ok {
79+
// Use
80+
// https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateIdToken
81+
service := config.clientIamCredentials
82+
name := fmt.Sprintf("projects/-/serviceAccounts/%s", d.Get("target_service_account").(string))
83+
tokenRequest := &iamcredentials.GenerateIdTokenRequest{
84+
Audience: targetAudience,
85+
IncludeEmail: d.Get("include_email").(bool),
86+
Delegates: convertStringSet(d.Get("delegates").(*schema.Set)),
87+
}
88+
at, err := service.Projects.ServiceAccounts.GenerateIdToken(name, tokenRequest).Do()
89+
if err != nil {
90+
return fmt.Errorf("error calling iamcredentials.GenerateIdToken: %v", err)
91+
}
92+
93+
d.SetId(time.Now().UTC().String())
94+
d.Set("id_token", at.Token)
95+
96+
return nil
97+
}
98+
99+
tok, err := ts.Token()
100+
if err != nil {
101+
return fmt.Errorf("unable to get Token() from tokenSource: %v", err)
102+
}
103+
104+
// only user-credential TokenSources have refreshTokens
105+
if tok.RefreshToken != "" {
106+
return fmt.Errorf("unsupported Credential Type supplied. Use serviceAccount credentials")
107+
}
108+
ctx := context.Background()
109+
co := []option.ClientOption{}
110+
if creds.JSON != nil {
111+
co = append(co, idtoken.WithCredentialsJSON(creds.JSON))
112+
}
113+
114+
idTokenSource, err := idtoken.NewTokenSource(ctx, targetAudience, co...)
115+
if err != nil {
116+
return fmt.Errorf("unable to retrieve TokenSource: %v", err)
117+
}
118+
idToken, err := idTokenSource.Token()
119+
if err != nil {
120+
return fmt.Errorf("unable to retrieve Token: %v", err)
121+
}
122+
123+
d.SetId(time.Now().UTC().String())
124+
d.Set("id_token", idToken.AccessToken)
125+
126+
return nil
127+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package google
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"fmt"
8+
9+
"google.golang.org/api/idtoken"
10+
11+
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
12+
"github.com/hashicorp/terraform-plugin-sdk/terraform"
13+
)
14+
15+
const targetAudience = "https://foo.bar/"
16+
17+
func testAccCheckServiceAccountIdTokenValue(name, audience string) resource.TestCheckFunc {
18+
return func(s *terraform.State) error {
19+
ms := s.RootModule()
20+
21+
rs, ok := ms.Resources[name]
22+
if !ok {
23+
return fmt.Errorf("can't find %s in state", name)
24+
}
25+
26+
v, ok := rs.Primary.Attributes["id_token"]
27+
if !ok {
28+
return fmt.Errorf("id_token not found")
29+
}
30+
31+
_, err := idtoken.Validate(context.Background(), v, audience)
32+
if err != nil {
33+
return fmt.Errorf("token validation failed: %v", err)
34+
}
35+
36+
return nil
37+
}
38+
}
39+
40+
func TestAccDataSourceGoogleServiceAccountIdToken_basic(t *testing.T) {
41+
t.Parallel()
42+
43+
resourceName := "data.google_service_account_id_token.default"
44+
45+
resource.Test(t, resource.TestCase{
46+
PreCheck: func() { testAccPreCheck(t) },
47+
Providers: testAccProviders,
48+
Steps: []resource.TestStep{
49+
{
50+
Config: testAccCheckGoogleServiceAccountIdToken_basic(targetAudience),
51+
Check: resource.ComposeTestCheckFunc(
52+
resource.TestCheckResourceAttr(resourceName, "target_audience", targetAudience),
53+
testAccCheckServiceAccountIdTokenValue(resourceName, targetAudience),
54+
),
55+
},
56+
},
57+
})
58+
}
59+
60+
func testAccCheckGoogleServiceAccountIdToken_basic(targetAudience string) string {
61+
62+
return fmt.Sprintf(`
63+
data "google_service_account_id_token" "default" {
64+
target_audience = "%s"
65+
}
66+
`, targetAudience)
67+
}
68+
69+
func TestAccDataSourceGoogleServiceAccountIdToken_impersonation(t *testing.T) {
70+
t.Parallel()
71+
72+
resourceName := "data.google_service_account_id_token.default"
73+
serviceAccount := getTestServiceAccountFromEnv(t)
74+
targetServiceAccountEmail := BootstrapServiceAccount(t, getTestProjectFromEnv(), serviceAccount)
75+
76+
resource.Test(t, resource.TestCase{
77+
PreCheck: func() { testAccPreCheck(t) },
78+
Providers: testAccProviders,
79+
Steps: []resource.TestStep{
80+
{
81+
Config: testAccCheckGoogleServiceAccountIdToken_impersonation_datasource(targetAudience, targetServiceAccountEmail),
82+
Check: resource.ComposeTestCheckFunc(
83+
resource.TestCheckResourceAttr(resourceName, "target_audience", targetAudience),
84+
testAccCheckServiceAccountIdTokenValue(resourceName, targetAudience),
85+
),
86+
},
87+
},
88+
})
89+
}
90+
91+
func testAccCheckGoogleServiceAccountIdToken_impersonation_datasource(targetAudience string, targetServiceAccount string) string {
92+
93+
return fmt.Sprintf(`
94+
data "google_service_account_access_token" "default" {
95+
target_service_account = "%s"
96+
scopes = ["userinfo-email", "https://www.googleapis.com/auth/cloud-platform"]
97+
lifetime = "30s"
98+
}
99+
100+
provider google {
101+
alias = "impersonated"
102+
access_token = data.google_service_account_access_token.default.access_token
103+
}
104+
105+
data "google_service_account_id_token" "default" {
106+
provider = google.impersonated
107+
target_service_account = "%s"
108+
target_audience = "%s"
109+
}
110+
`, targetServiceAccount, targetServiceAccount, targetAudience)
111+
}

google-beta/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,7 @@ func Provider() terraform.ResourceProvider {
656656
"google_secret_manager_secret_version": dataSourceSecretManagerSecretVersion(),
657657
"google_service_account": dataSourceGoogleServiceAccount(),
658658
"google_service_account_access_token": dataSourceGoogleServiceAccountAccessToken(),
659+
"google_service_account_id_token": dataSourceGoogleServiceAccountIdToken(),
659660
"google_service_account_key": dataSourceGoogleServiceAccountKey(),
660661
"google_sql_ca_certs": dataSourceGoogleSQLCaCerts(),
661662
"google_storage_bucket_object": dataSourceGoogleStorageBucketObject(),

0 commit comments

Comments
 (0)