google/internal/externalaccount: allow impersonation lifetime changes

This commit is contained in:
Ryan Kohler 2022-07-11 15:06:45 -07:00
parent 2104d58473
commit 7e0ea92c8e
4 changed files with 104 additions and 48 deletions

View File

@ -122,6 +122,7 @@ type credentialsFile struct {
TokenURLExternal string `json:"token_url"` TokenURLExternal string `json:"token_url"`
TokenInfoURL string `json:"token_info_url"` TokenInfoURL string `json:"token_info_url"`
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
ServiceAccountImpersonation serviceAccountImpersonationInfo `json:"service_account_impersonation"`
Delegates []string `json:"delegates"` Delegates []string `json:"delegates"`
CredentialSource externalaccount.CredentialSource `json:"credential_source"` CredentialSource externalaccount.CredentialSource `json:"credential_source"`
QuotaProjectID string `json:"quota_project_id"` QuotaProjectID string `json:"quota_project_id"`
@ -131,6 +132,10 @@ type credentialsFile struct {
SourceCredentials *credentialsFile `json:"source_credentials"` SourceCredentials *credentialsFile `json:"source_credentials"`
} }
type serviceAccountImpersonationInfo struct {
TokenLifetimeSeconds int `json:"token_lifetime_seconds"`
}
func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config { func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config {
cfg := &jwt.Config{ cfg := &jwt.Config{
Email: f.ClientEmail, Email: f.ClientEmail,
@ -178,6 +183,7 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
TokenURL: f.TokenURLExternal, TokenURL: f.TokenURLExternal,
TokenInfoURL: f.TokenInfoURL, TokenInfoURL: f.TokenInfoURL,
ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL, ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL,
ServiceAccountImpersonationLifetimeSeconds: f.ServiceAccountImpersonation.TokenLifetimeSeconds,
ClientSecret: f.ClientSecret, ClientSecret: f.ClientSecret,
ClientID: f.ClientID, ClientID: f.ClientID,
CredentialSource: f.CredentialSource, CredentialSource: f.CredentialSource,

View File

@ -39,6 +39,9 @@ type Config struct {
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only // ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
// required for workload identity pools when APIs to be accessed have not integrated with UberMint. // required for workload identity pools when APIs to be accessed have not integrated with UberMint.
ServiceAccountImpersonationURL string ServiceAccountImpersonationURL string
// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
// token will be valid for.
ServiceAccountImpersonationLifetimeSeconds int
// ClientSecret is currently only required if token_info endpoint also // ClientSecret is currently only required if token_info endpoint also
// needs to be called with the generated GCP access token. When provided, STS will be // needs to be called with the generated GCP access token. When provided, STS will be
// called with additional basic authentication using client_id as username and client_secret as password. // called with additional basic authentication using client_id as username and client_secret as password.
@ -145,6 +148,7 @@ func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Re
URL: c.ServiceAccountImpersonationURL, URL: c.ServiceAccountImpersonationURL,
Scopes: scopes, Scopes: scopes,
Ts: oauth2.ReuseTokenSource(nil, ts), Ts: oauth2.ReuseTokenSource(nil, ts),
TokenLifetimeSeconds: c.ServiceAccountImpersonationLifetimeSeconds,
} }
return oauth2.ReuseTokenSource(nil, imp), nil return oauth2.ReuseTokenSource(nil, imp), nil
} }

View File

@ -48,12 +48,19 @@ type ImpersonateTokenSource struct {
// Each service account must be granted roles/iam.serviceAccountTokenCreator // Each service account must be granted roles/iam.serviceAccountTokenCreator
// on the next service account in the chain. Optional. // on the next service account in the chain. Optional.
Delegates []string Delegates []string
// TokenLifetimeSeconds is the number of seconds the impersonation token will
// be valid for.
TokenLifetimeSeconds int
} }
// Token performs the exchange to get a temporary service account token to allow access to GCP. // Token performs the exchange to get a temporary service account token to allow access to GCP.
func (its ImpersonateTokenSource) Token() (*oauth2.Token, error) { func (its ImpersonateTokenSource) Token() (*oauth2.Token, error) {
lifetimeString := "3600s"
if its.TokenLifetimeSeconds != 0 {
lifetimeString = fmt.Sprintf("%ds", its.TokenLifetimeSeconds)
}
reqBody := generateAccessTokenReq{ reqBody := generateAccessTokenReq{
Lifetime: "3600s", Lifetime: lifetimeString,
Scope: its.Scopes, Scope: its.Scopes,
Delegates: its.Delegates, Delegates: its.Delegates,
} }

View File

@ -13,28 +13,18 @@ import (
"testing" "testing"
) )
var testImpersonateConfig = Config{
Audience: "32555940559.apps.googleusercontent.com",
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
ClientSecret: "notsosecret",
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
CredentialSource: testBaseCredSource,
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
}
var ( var (
baseImpersonateCredsReqBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" baseImpersonateCredsReqBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt"
baseImpersonateCredsRespBody = `{"accessToken":"Second.Access.Token","expireTime":"2020-12-28T15:01:23Z"}` baseImpersonateCredsRespBody = `{"accessToken":"Second.Access.Token","expireTime":"2020-12-28T15:01:23Z"}`
) )
func TestImpersonation(t *testing.T) { func createImpersonationServer(urlWanted, authWanted, bodyWanted, response string, t *testing.T) *httptest.Server {
impersonateServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.URL.String(), "/"; got != want { if got, want := r.URL.String(), urlWanted; got != want {
t.Errorf("URL.String(): got %v but want %v", got, want) t.Errorf("URL.String(): got %v but want %v", got, want)
} }
headerAuth := r.Header.Get("Authorization") headerAuth := r.Header.Get("Authorization")
if got, want := headerAuth, "Bearer Sample.Access.Token"; got != want { if got, want := headerAuth, authWanted; got != want {
t.Errorf("got %v but want %v", got, want) t.Errorf("got %v but want %v", got, want)
} }
headerContentType := r.Header.Get("Content-Type") headerContentType := r.Header.Get("Content-Type")
@ -45,14 +35,16 @@ func TestImpersonation(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Failed reading request body: %v.", err) t.Fatalf("Failed reading request body: %v.", err)
} }
if got, want := string(body), "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}"; got != want { if got, want := string(body), bodyWanted; got != want {
t.Errorf("Unexpected impersonation payload: got %v but want %v", got, want) t.Errorf("Unexpected impersonation payload: got %v but want %v", got, want)
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Write([]byte(baseImpersonateCredsRespBody)) w.Write([]byte(response))
})) }))
testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL }
targetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
func createTargetServer(t *testing.T) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.URL.String(), "/"; got != want { if got, want := r.URL.String(), "/"; got != want {
t.Errorf("URL.String(): got %v but want %v", got, want) t.Errorf("URL.String(): got %v but want %v", got, want)
} }
@ -74,9 +66,54 @@ func TestImpersonation(t *testing.T) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Write([]byte(baseCredsResponseBody)) w.Write([]byte(baseCredsResponseBody))
})) }))
defer targetServer.Close() }
var impersonationTests = []struct {
name string
config Config
expectedImpersonationBody string
}{
{
name: "Base Impersonation",
config: Config{
Audience: "32555940559.apps.googleusercontent.com",
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
ClientSecret: "notsosecret",
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
CredentialSource: testBaseCredSource,
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
},
expectedImpersonationBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
},
{
name: "With TokenLifetime Set",
config: Config{
Audience: "32555940559.apps.googleusercontent.com",
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
ClientSecret: "notsosecret",
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
CredentialSource: testBaseCredSource,
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
ServiceAccountImpersonationLifetimeSeconds: 10000,
},
expectedImpersonationBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
},
}
func TestImpersonation(t *testing.T) {
for _, tt := range impersonationTests {
t.Run(tt.name, func(t *testing.T) {
testImpersonateConfig := tt.config
impersonateServer := createImpersonationServer("/", "Bearer Sample.Access.Token", tt.expectedImpersonationBody, baseImpersonateCredsRespBody, t)
defer impersonateServer.Close()
testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL
targetServer := createTargetServer(t)
defer targetServer.Close()
testImpersonateConfig.TokenURL = targetServer.URL testImpersonateConfig.TokenURL = targetServer.URL
allURLs := regexp.MustCompile(".+") allURLs := regexp.MustCompile(".+")
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), []*regexp.Regexp{allURLs}, []*regexp.Regexp{allURLs}, "http") ourTS, err := testImpersonateConfig.tokenSource(context.Background(), []*regexp.Regexp{allURLs}, []*regexp.Regexp{allURLs}, "http")
if err != nil { if err != nil {
@ -97,4 +134,6 @@ func TestImpersonation(t *testing.T) {
if got, want := tok.TokenType, "Bearer"; got != want { if got, want := tok.TokenType, "Bearer"; got != want {
t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want) t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want)
} }
})
}
} }