forked from Mirrors/oauth2
google/internal/externalaccount: allow impersonation lifetime changes
This commit is contained in:
parent
2104d58473
commit
7e0ea92c8e
|
@ -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,12 +183,13 @@ 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,
|
||||||
ClientSecret: f.ClientSecret,
|
ServiceAccountImpersonationLifetimeSeconds: f.ServiceAccountImpersonation.TokenLifetimeSeconds,
|
||||||
ClientID: f.ClientID,
|
ClientSecret: f.ClientSecret,
|
||||||
CredentialSource: f.CredentialSource,
|
ClientID: f.ClientID,
|
||||||
QuotaProjectID: f.QuotaProjectID,
|
CredentialSource: f.CredentialSource,
|
||||||
Scopes: params.Scopes,
|
QuotaProjectID: f.QuotaProjectID,
|
||||||
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
|
Scopes: params.Scopes,
|
||||||
|
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
|
||||||
}
|
}
|
||||||
return cfg.TokenSource(ctx)
|
return cfg.TokenSource(ctx)
|
||||||
case impersonatedServiceAccount:
|
case impersonatedServiceAccount:
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -141,10 +144,11 @@ func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Re
|
||||||
scopes := c.Scopes
|
scopes := c.Scopes
|
||||||
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
|
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
|
||||||
imp := ImpersonateTokenSource{
|
imp := ImpersonateTokenSource{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,27 +66,74 @@ 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()
|
}
|
||||||
|
|
||||||
testImpersonateConfig.TokenURL = targetServer.URL
|
var impersonationTests = []struct {
|
||||||
allURLs := regexp.MustCompile(".+")
|
name string
|
||||||
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), []*regexp.Regexp{allURLs}, []*regexp.Regexp{allURLs}, "http")
|
config Config
|
||||||
if err != nil {
|
expectedImpersonationBody string
|
||||||
t.Fatalf("Failed to create TokenSource: %v", err)
|
}{
|
||||||
}
|
{
|
||||||
|
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\"]}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
oldNow := now
|
func TestImpersonation(t *testing.T) {
|
||||||
defer func() { now = oldNow }()
|
for _, tt := range impersonationTests {
|
||||||
now = testNow
|
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
|
||||||
|
|
||||||
tok, err := ourTS.Token()
|
targetServer := createTargetServer(t)
|
||||||
if err != nil {
|
defer targetServer.Close()
|
||||||
t.Fatalf("Unexpected error: %e", err)
|
testImpersonateConfig.TokenURL = targetServer.URL
|
||||||
}
|
|
||||||
if got, want := tok.AccessToken, "Second.Access.Token"; got != want {
|
allURLs := regexp.MustCompile(".+")
|
||||||
t.Errorf("Unexpected access token: got %v, but wanted %v", got, want)
|
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), []*regexp.Regexp{allURLs}, []*regexp.Regexp{allURLs}, "http")
|
||||||
}
|
if err != nil {
|
||||||
if got, want := tok.TokenType, "Bearer"; got != want {
|
t.Fatalf("Failed to create TokenSource: %v", err)
|
||||||
t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want)
|
}
|
||||||
|
|
||||||
|
oldNow := now
|
||||||
|
defer func() { now = oldNow }()
|
||||||
|
now = testNow
|
||||||
|
|
||||||
|
tok, err := ourTS.Token()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %e", err)
|
||||||
|
}
|
||||||
|
if got, want := tok.AccessToken, "Second.Access.Token"; got != want {
|
||||||
|
t.Errorf("Unexpected access token: got %v, but wanted %v", got, want)
|
||||||
|
}
|
||||||
|
if got, want := tok.TokenType, "Bearer"; got != want {
|
||||||
|
t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue