From e19e35fc41e762ef6826a6b183b3f04d2aa9bbcd Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Mon, 22 Oct 2018 20:16:26 -0700 Subject: [PATCH 1/8] Add DelegateTokenSource --- google/delegate.go | 117 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 google/delegate.go diff --git a/google/delegate.go b/google/delegate.go new file mode 100644 index 000000000..01d87b627 --- /dev/null +++ b/google/delegate.go @@ -0,0 +1,117 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package google + +import ( + "context" + "fmt" + "strconv" + "sync" + "time" + + "golang.org/x/oauth2" + "google.golang.org/api/iamcredentials/v1" +) + +// DelegateTokenSource allows a TokenSource issued to a user or +// service account to impersonate another. The target service account +// must grant the orginating credential principal the +// "Service Account Token Creator" IAM role: +// https://cloud.google.com/iam/docs/service-accounts#the_service_account_token_creator_role +// +// rootSource (TokenSource): The root TokenSource +// used as to acquire the delegated identity TokenSource. +// rootSource *must* include scopes that includes +// "https://www.googleapis.com/auth/iam" +// principal (string): The service account to impersonate. +// new_scopes ([]string): Scopes to request during the +// authorization grant. +// delegates ([]string): The chained list of delegates required +// to grant the final access_token. +// lifetime (int): Number of seconds the delegated credential should +// be valid for (upto 3600). +// +// Usage: +// principal := "impersonated-account@project.iam.gserviceaccount.com" +// lifetime := 30 +// delegates := []string{} +// newScopes := []string{storage.ScopeReadOnly} +// rootTokenSource, err := google.DefaultTokenSource(ctx, +// "https://www.googleapis.com/auth/iam") +// delegatetokenSource, err := google.DelegateTokenSource(ctx, +// rootTokenSource, +// principal, lifetime, delegates, newScopes) +// storeageClient, _ = storage.NewClient(ctx, +// option.WithTokenSource(delegatetokenSource)) + +// Note that this is not a standard OAuth flow, but rather uses Google Cloud +// IAMCredentials API to exchange one oauth token for an impersonated account +// see: https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/generateAccessToken +func DelegateTokenSource(ctx context.Context, rootSource oauth2.TokenSource, + principal string, lifetime int, delegates []string, + newScopes []string) (oauth2.TokenSource, error) { + + return &delegateTokenSource{ + ctx: ctx, + rootSource: rootSource, + principal: principal, + lifetime: strconv.Itoa(lifetime) + "s", + delegates: delegates, + newScopes: newScopes, + }, nil +} + +type delegateTokenSource struct { + ctx context.Context + rootSource oauth2.TokenSource + principal string + lifetime string + delegates []string + newScopes []string +} + +var ( + mu sync.Mutex + tok *oauth2.Token +) + +func (ts *delegateTokenSource) Token() (*oauth2.Token, error) { + + mu.Lock() + defer mu.Unlock() + + if tok.Valid() { + return tok, nil + } + + client := oauth2.NewClient(context.Background(), ts.rootSource) + + service, err := iamcredentials.New(client) + if err != nil { + return nil, fmt.Errorf("Error creating IAMCredentials: %v", err) + } + name := "projects/-/serviceAccounts/" + ts.principal + tokenRequest := &iamcredentials.GenerateAccessTokenRequest{ + Lifetime: ts.lifetime, + Delegates: ts.delegates, + Scope: ts.newScopes, + } + at, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, tokenRequest).Do() + if err != nil { + return nil, fmt.Errorf("Error calling GenerateAccessToken: %v", err) + } + + expireAt, err := time.Parse(time.RFC3339, at.ExpireTime) + if err != nil { + return nil, fmt.Errorf("Error parsing ExpireTime: %v", err) + } + + tok = &oauth2.Token{ + AccessToken: at.AccessToken, + Expiry: expireAt, + } + + return tok, nil +} From e8ef4a080dcdb41344ee8fa66e6a35fce99a92f3 Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Tue, 23 Oct 2018 01:08:50 -0700 Subject: [PATCH 2/8] update ctx; duration --- google/delegate.go | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/google/delegate.go b/google/delegate.go index 01d87b627..06de0aae4 100644 --- a/google/delegate.go +++ b/google/delegate.go @@ -7,7 +7,6 @@ package google import ( "context" "fmt" - "strconv" "sync" "time" @@ -17,47 +16,49 @@ import ( // DelegateTokenSource allows a TokenSource issued to a user or // service account to impersonate another. The target service account -// must grant the orginating credential principal the +// must grant the orginating principal the // "Service Account Token Creator" IAM role: // https://cloud.google.com/iam/docs/service-accounts#the_service_account_token_creator_role // // rootSource (TokenSource): The root TokenSource -// used as to acquire the delegated identity TokenSource. -// rootSource *must* include scopes that includes +// used as to acquire the target identity TokenSource. +// rootSource *must* include scopes that contains // "https://www.googleapis.com/auth/iam" +// or +// "https://www.googleapis.com/auth/cloud-platform" // principal (string): The service account to impersonate. // new_scopes ([]string): Scopes to request during the // authorization grant. // delegates ([]string): The chained list of delegates required // to grant the final access_token. -// lifetime (int): Number of seconds the delegated credential should +// lifetime (time.Duration): Number of seconds the delegated credential should // be valid for (upto 3600). // // Usage: // principal := "impersonated-account@project.iam.gserviceaccount.com" -// lifetime := 30 +// lifetime := 30 * time.Second // delegates := []string{} // newScopes := []string{storage.ScopeReadOnly} // rootTokenSource, err := google.DefaultTokenSource(ctx, // "https://www.googleapis.com/auth/iam") // delegatetokenSource, err := google.DelegateTokenSource(ctx, // rootTokenSource, -// principal, lifetime, delegates, newScopes) +// principal, lifetime, delegates, newScopes) // storeageClient, _ = storage.NewClient(ctx, // option.WithTokenSource(delegatetokenSource)) - +// // Note that this is not a standard OAuth flow, but rather uses Google Cloud // IAMCredentials API to exchange one oauth token for an impersonated account // see: https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/generateAccessToken func DelegateTokenSource(ctx context.Context, rootSource oauth2.TokenSource, - principal string, lifetime int, delegates []string, + principal string, lifetime time.Duration, delegates []string, newScopes []string) (oauth2.TokenSource, error) { return &delegateTokenSource{ ctx: ctx, rootSource: rootSource, principal: principal, - lifetime: strconv.Itoa(lifetime) + "s", + lifetime: lifetime, delegates: delegates, newScopes: newScopes, }, nil @@ -67,7 +68,7 @@ type delegateTokenSource struct { ctx context.Context rootSource oauth2.TokenSource principal string - lifetime string + lifetime time.Duration delegates []string newScopes []string } @@ -86,26 +87,26 @@ func (ts *delegateTokenSource) Token() (*oauth2.Token, error) { return tok, nil } - client := oauth2.NewClient(context.Background(), ts.rootSource) + client := oauth2.NewClient(ts.ctx, ts.rootSource) service, err := iamcredentials.New(client) if err != nil { - return nil, fmt.Errorf("Error creating IAMCredentials: %v", err) + return nil, fmt.Errorf("google: Error creating IAMCredentials: %v", err) } name := "projects/-/serviceAccounts/" + ts.principal tokenRequest := &iamcredentials.GenerateAccessTokenRequest{ - Lifetime: ts.lifetime, + Lifetime: ts.lifetime.String(), Delegates: ts.delegates, Scope: ts.newScopes, } at, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, tokenRequest).Do() if err != nil { - return nil, fmt.Errorf("Error calling GenerateAccessToken: %v", err) + return nil, fmt.Errorf("google: Error calling iamcredentials.GenerateAccessToken: %v", err) } expireAt, err := time.Parse(time.RFC3339, at.ExpireTime) if err != nil { - return nil, fmt.Errorf("Error parsing ExpireTime: %v", err) + return nil, fmt.Errorf("google: Error parsing ExpireTime from iamcredentials: %v", err) } tok = &oauth2.Token{ From df9bbd8bf52dfa0c0a8b0fd696e43ff0cad4b519 Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Tue, 23 Oct 2018 10:23:56 -0700 Subject: [PATCH 3/8] add input validation --- google/delegate.go | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/google/delegate.go b/google/delegate.go index 06de0aae4..f1eb2a714 100644 --- a/google/delegate.go +++ b/google/delegate.go @@ -7,6 +7,8 @@ package google import ( "context" "fmt" + "regexp" + "strings" "sync" "time" @@ -14,6 +16,13 @@ import ( "google.golang.org/api/iamcredentials/v1" ) +var ( + mu sync.Mutex + tok *oauth2.Token +) + +const emailRegex string = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" + // DelegateTokenSource allows a TokenSource issued to a user or // service account to impersonate another. The target service account // must grant the orginating principal the @@ -25,7 +34,7 @@ import ( // rootSource *must* include scopes that contains // "https://www.googleapis.com/auth/iam" // or -// "https://www.googleapis.com/auth/cloud-platform" +// "https://www.googleapis.com/auth/cloud.platform" // principal (string): The service account to impersonate. // new_scopes ([]string): Scopes to request during the // authorization grant. @@ -54,6 +63,29 @@ func DelegateTokenSource(ctx context.Context, rootSource oauth2.TokenSource, principal string, lifetime time.Duration, delegates []string, newScopes []string) (oauth2.TokenSource, error) { + reEmail := regexp.MustCompile(emailRegex) + scopePrefix := "https://www.googleapis.com/auth/" + + if rootSource == nil { + return nil, fmt.Errorf("oauth2/google: rootSource cannot be nil") + } + if !reEmail.MatchString(principal) { + return nil, fmt.Errorf("oauth2/google: principal must be a serviceAccount email address") + } + if lifetime <= time.Duration(3600) { + return nil, fmt.Errorf("oauth2/google: lifetime must be less than or equal to 3600 seconds") + } + for _, d := range delegates { + if !reEmail.MatchString(d) { + return nil, fmt.Errorf("oauth2/google: delegates must be a serviceAccount email address: %v", d) + } + } + for _, s := range newScopes { + if !strings.HasPrefix(s, scopePrefix) { + return nil, fmt.Errorf("oauth2/google: scopes must be a Google Auth scope url: %v", s) + } + } + return &delegateTokenSource{ ctx: ctx, rootSource: rootSource, @@ -73,11 +105,6 @@ type delegateTokenSource struct { newScopes []string } -var ( - mu sync.Mutex - tok *oauth2.Token -) - func (ts *delegateTokenSource) Token() (*oauth2.Token, error) { mu.Lock() @@ -91,7 +118,7 @@ func (ts *delegateTokenSource) Token() (*oauth2.Token, error) { service, err := iamcredentials.New(client) if err != nil { - return nil, fmt.Errorf("google: Error creating IAMCredentials: %v", err) + return nil, fmt.Errorf("oauth2/google: Error creating IAMCredentials: %v", err) } name := "projects/-/serviceAccounts/" + ts.principal tokenRequest := &iamcredentials.GenerateAccessTokenRequest{ @@ -101,12 +128,12 @@ func (ts *delegateTokenSource) Token() (*oauth2.Token, error) { } at, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, tokenRequest).Do() if err != nil { - return nil, fmt.Errorf("google: Error calling iamcredentials.GenerateAccessToken: %v", err) + return nil, fmt.Errorf("oauth2/google: Error calling iamcredentials.GenerateAccessToken: %v", err) } expireAt, err := time.Parse(time.RFC3339, at.ExpireTime) if err != nil { - return nil, fmt.Errorf("google: Error parsing ExpireTime from iamcredentials: %v", err) + return nil, fmt.Errorf("oauth2/google: Error parsing ExpireTime from iamcredentials: %v", err) } tok = &oauth2.Token{ From 9e74e20ad2ede403d29ee1b0a86368a41d394b31 Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Tue, 23 Oct 2018 12:03:15 -0700 Subject: [PATCH 4/8] Fix time duration logic --- google/delegate.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/google/delegate.go b/google/delegate.go index f1eb2a714..f0fb7b454 100644 --- a/google/delegate.go +++ b/google/delegate.go @@ -72,7 +72,7 @@ func DelegateTokenSource(ctx context.Context, rootSource oauth2.TokenSource, if !reEmail.MatchString(principal) { return nil, fmt.Errorf("oauth2/google: principal must be a serviceAccount email address") } - if lifetime <= time.Duration(3600) { + if lifetime > (3600 * time.Second) { return nil, fmt.Errorf("oauth2/google: lifetime must be less than or equal to 3600 seconds") } for _, d := range delegates { @@ -113,7 +113,6 @@ func (ts *delegateTokenSource) Token() (*oauth2.Token, error) { if tok.Valid() { return tok, nil } - client := oauth2.NewClient(ts.ctx, ts.rootSource) service, err := iamcredentials.New(client) @@ -122,7 +121,7 @@ func (ts *delegateTokenSource) Token() (*oauth2.Token, error) { } name := "projects/-/serviceAccounts/" + ts.principal tokenRequest := &iamcredentials.GenerateAccessTokenRequest{ - Lifetime: ts.lifetime.String(), + Lifetime: fmt.Sprintf("%ds", int(ts.lifetime.Seconds())), Delegates: ts.delegates, Scope: ts.newScopes, } From 6f87b5c0712863db89485e02f09bda60daff7df8 Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Wed, 24 Oct 2018 10:56:33 -0700 Subject: [PATCH 5/8] Remove some validation; principal can be email or uniqueid --- google/delegate.go | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/google/delegate.go b/google/delegate.go index f0fb7b454..bb454b41a 100644 --- a/google/delegate.go +++ b/google/delegate.go @@ -7,8 +7,6 @@ package google import ( "context" "fmt" - "regexp" - "strings" "sync" "time" @@ -21,8 +19,6 @@ var ( tok *oauth2.Token ) -const emailRegex string = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" - // DelegateTokenSource allows a TokenSource issued to a user or // service account to impersonate another. The target service account // must grant the orginating principal the @@ -63,28 +59,12 @@ func DelegateTokenSource(ctx context.Context, rootSource oauth2.TokenSource, principal string, lifetime time.Duration, delegates []string, newScopes []string) (oauth2.TokenSource, error) { - reEmail := regexp.MustCompile(emailRegex) - scopePrefix := "https://www.googleapis.com/auth/" - if rootSource == nil { return nil, fmt.Errorf("oauth2/google: rootSource cannot be nil") } - if !reEmail.MatchString(principal) { - return nil, fmt.Errorf("oauth2/google: principal must be a serviceAccount email address") - } if lifetime > (3600 * time.Second) { return nil, fmt.Errorf("oauth2/google: lifetime must be less than or equal to 3600 seconds") } - for _, d := range delegates { - if !reEmail.MatchString(d) { - return nil, fmt.Errorf("oauth2/google: delegates must be a serviceAccount email address: %v", d) - } - } - for _, s := range newScopes { - if !strings.HasPrefix(s, scopePrefix) { - return nil, fmt.Errorf("oauth2/google: scopes must be a Google Auth scope url: %v", s) - } - } return &delegateTokenSource{ ctx: ctx, @@ -119,7 +99,7 @@ func (ts *delegateTokenSource) Token() (*oauth2.Token, error) { if err != nil { return nil, fmt.Errorf("oauth2/google: Error creating IAMCredentials: %v", err) } - name := "projects/-/serviceAccounts/" + ts.principal + name := fmt.Sprintf("projects/-/serviceAccounts/%s", ts.principal) tokenRequest := &iamcredentials.GenerateAccessTokenRequest{ Lifetime: fmt.Sprintf("%ds", int(ts.lifetime.Seconds())), Delegates: ts.delegates, From e6c872ade0e75fb2fb4ced11fef08ffc73a86ca9 Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Fri, 2 Nov 2018 14:35:37 -0400 Subject: [PATCH 6/8] rename DelegateTokenSource --> ImpersonatedTokenSource --- google/delegate.go | 124 ------------------------------------------ google/impersonate.go | 124 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 124 deletions(-) delete mode 100644 google/delegate.go create mode 100644 google/impersonate.go diff --git a/google/delegate.go b/google/delegate.go deleted file mode 100644 index bb454b41a..000000000 --- a/google/delegate.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package google - -import ( - "context" - "fmt" - "sync" - "time" - - "golang.org/x/oauth2" - "google.golang.org/api/iamcredentials/v1" -) - -var ( - mu sync.Mutex - tok *oauth2.Token -) - -// DelegateTokenSource allows a TokenSource issued to a user or -// service account to impersonate another. The target service account -// must grant the orginating principal the -// "Service Account Token Creator" IAM role: -// https://cloud.google.com/iam/docs/service-accounts#the_service_account_token_creator_role -// -// rootSource (TokenSource): The root TokenSource -// used as to acquire the target identity TokenSource. -// rootSource *must* include scopes that contains -// "https://www.googleapis.com/auth/iam" -// or -// "https://www.googleapis.com/auth/cloud.platform" -// principal (string): The service account to impersonate. -// new_scopes ([]string): Scopes to request during the -// authorization grant. -// delegates ([]string): The chained list of delegates required -// to grant the final access_token. -// lifetime (time.Duration): Number of seconds the delegated credential should -// be valid for (upto 3600). -// -// Usage: -// principal := "impersonated-account@project.iam.gserviceaccount.com" -// lifetime := 30 * time.Second -// delegates := []string{} -// newScopes := []string{storage.ScopeReadOnly} -// rootTokenSource, err := google.DefaultTokenSource(ctx, -// "https://www.googleapis.com/auth/iam") -// delegatetokenSource, err := google.DelegateTokenSource(ctx, -// rootTokenSource, -// principal, lifetime, delegates, newScopes) -// storeageClient, _ = storage.NewClient(ctx, -// option.WithTokenSource(delegatetokenSource)) -// -// Note that this is not a standard OAuth flow, but rather uses Google Cloud -// IAMCredentials API to exchange one oauth token for an impersonated account -// see: https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/generateAccessToken -func DelegateTokenSource(ctx context.Context, rootSource oauth2.TokenSource, - principal string, lifetime time.Duration, delegates []string, - newScopes []string) (oauth2.TokenSource, error) { - - if rootSource == nil { - return nil, fmt.Errorf("oauth2/google: rootSource cannot be nil") - } - if lifetime > (3600 * time.Second) { - return nil, fmt.Errorf("oauth2/google: lifetime must be less than or equal to 3600 seconds") - } - - return &delegateTokenSource{ - ctx: ctx, - rootSource: rootSource, - principal: principal, - lifetime: lifetime, - delegates: delegates, - newScopes: newScopes, - }, nil -} - -type delegateTokenSource struct { - ctx context.Context - rootSource oauth2.TokenSource - principal string - lifetime time.Duration - delegates []string - newScopes []string -} - -func (ts *delegateTokenSource) Token() (*oauth2.Token, error) { - - mu.Lock() - defer mu.Unlock() - - if tok.Valid() { - return tok, nil - } - client := oauth2.NewClient(ts.ctx, ts.rootSource) - - service, err := iamcredentials.New(client) - if err != nil { - return nil, fmt.Errorf("oauth2/google: Error creating IAMCredentials: %v", err) - } - name := fmt.Sprintf("projects/-/serviceAccounts/%s", ts.principal) - tokenRequest := &iamcredentials.GenerateAccessTokenRequest{ - Lifetime: fmt.Sprintf("%ds", int(ts.lifetime.Seconds())), - Delegates: ts.delegates, - Scope: ts.newScopes, - } - at, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, tokenRequest).Do() - if err != nil { - return nil, fmt.Errorf("oauth2/google: Error calling iamcredentials.GenerateAccessToken: %v", err) - } - - expireAt, err := time.Parse(time.RFC3339, at.ExpireTime) - if err != nil { - return nil, fmt.Errorf("oauth2/google: Error parsing ExpireTime from iamcredentials: %v", err) - } - - tok = &oauth2.Token{ - AccessToken: at.AccessToken, - Expiry: expireAt, - } - - return tok, nil -} diff --git a/google/impersonate.go b/google/impersonate.go new file mode 100644 index 000000000..bf6c8a302 --- /dev/null +++ b/google/impersonate.go @@ -0,0 +1,124 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package google + +import ( + "context" + "fmt" + "sync" + "time" + + "golang.org/x/oauth2" + "google.golang.org/api/iamcredentials/v1" +) + +var ( + mu sync.Mutex + tok *oauth2.Token +) + +// ImpersonatedTokenSource allows a TokenSource issued to a user or +// service account to impersonate another. The target service account +// must grant the orginating principal the +// "Service Account Token Creator" IAM role: +// https://cloud.google.com/iam/docs/service-accounts#the_service_account_token_creator_role +// +// rootSource (TokenSource): The root TokenSource +// used as to acquire the target identity TokenSource. +// rootSource *must* include scopes that contains +// "https://www.googleapis.com/auth/iam" +// or +// "https://www.googleapis.com/auth/cloud.platform" +// principal (string): The service account to impersonate. +// new_scopes ([]string): Scopes to request during the +// authorization grant. +// delegates ([]string): The chained list of delegates required +// to grant the final access_token. +// lifetime (time.Duration): Number of seconds the delegated credential should +// be valid for (upto 3600). +// +// Usage: +// principal := "impersonated-account@project.iam.gserviceaccount.com" +// lifetime := 30 * time.Second +// delegates := []string{} +// newScopes := []string{storage.ScopeReadOnly} +// rootTokenSource, err := google.DefaultTokenSource(ctx, +// "https://www.googleapis.com/auth/iam") +// delegatetokenSource, err := google.ImpersonatedTokenSource(ctx, +// rootTokenSource, +// principal, lifetime, delegates, newScopes) +// storeageClient, _ = storage.NewClient(ctx, +// option.WithTokenSource(delegatetokenSource)) +// +// Note that this is not a standard OAuth flow, but rather uses Google Cloud +// IAMCredentials API to exchange one oauth token for an impersonated account +// see: https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/generateAccessToken +func ImpersonatedTokenSource(ctx context.Context, rootSource oauth2.TokenSource, + principal string, lifetime time.Duration, delegates []string, + newScopes []string) (oauth2.TokenSource, error) { + + if rootSource == nil { + return nil, fmt.Errorf("oauth2/google: rootSource cannot be nil") + } + if lifetime > (3600 * time.Second) { + return nil, fmt.Errorf("oauth2/google: lifetime must be less than or equal to 3600 seconds") + } + + return &impersonatedTokenSource{ + ctx: ctx, + rootSource: rootSource, + principal: principal, + lifetime: lifetime, + delegates: delegates, + newScopes: newScopes, + }, nil +} + +type impersonatedTokenSource struct { + ctx context.Context + rootSource oauth2.TokenSource + principal string + lifetime time.Duration + delegates []string + newScopes []string +} + +func (ts *impersonatedTokenSource) Token() (*oauth2.Token, error) { + + mu.Lock() + defer mu.Unlock() + + if tok.Valid() { + return tok, nil + } + client := oauth2.NewClient(ts.ctx, ts.rootSource) + + service, err := iamcredentials.New(client) + if err != nil { + return nil, fmt.Errorf("oauth2/google: Error creating IAMCredentials: %v", err) + } + name := fmt.Sprintf("projects/-/serviceAccounts/%s", ts.principal) + tokenRequest := &iamcredentials.GenerateAccessTokenRequest{ + Lifetime: fmt.Sprintf("%ds", int(ts.lifetime.Seconds())), + Delegates: ts.delegates, + Scope: ts.newScopes, + } + at, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, tokenRequest).Do() + if err != nil { + return nil, fmt.Errorf("oauth2/google: Error calling iamcredentials.GenerateAccessToken: %v", err) + } + + expireAt, err := time.Parse(time.RFC3339, at.ExpireTime) + if err != nil { + return nil, fmt.Errorf("oauth2/google: Error parsing ExpireTime from iamcredentials: %v", err) + } + + tok = &oauth2.Token{ + AccessToken: at.AccessToken, + Expiry: expireAt, + } + + return tok, nil +} From a84066e99b9786a95a3e9f5f04c881eff7a169df Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Fri, 9 Nov 2018 14:23:24 -0800 Subject: [PATCH 7/8] rename to source/target --- google/impersonate.go | 147 ++++++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 69 deletions(-) diff --git a/google/impersonate.go b/google/impersonate.go index bf6c8a302..abf895782 100644 --- a/google/impersonate.go +++ b/google/impersonate.go @@ -5,23 +5,24 @@ package google import ( - "context" - "fmt" - "sync" - "time" + "context" + "fmt" + "sync" + "time" - "golang.org/x/oauth2" - "google.golang.org/api/iamcredentials/v1" + "golang.org/x/oauth2" + "google.golang.org/api/iamcredentials/v1" ) var ( - mu sync.Mutex - tok *oauth2.Token + mu sync.Mutex + tok *oauth2.Token ) // ImpersonatedTokenSource allows a TokenSource issued to a user or -// service account to impersonate another. The target service account -// must grant the orginating principal the +// service account to impersonate another. The source project using +// ImpersonatedTokenSource must enable the "IAMCredentials" API. Also, the +// target service account must grant the orginating principal the // "Service Account Token Creator" IAM role: // https://cloud.google.com/iam/docs/service-accounts#the_service_account_token_creator_role // @@ -31,94 +32,102 @@ var ( // "https://www.googleapis.com/auth/iam" // or // "https://www.googleapis.com/auth/cloud.platform" -// principal (string): The service account to impersonate. -// new_scopes ([]string): Scopes to request during the +// targetPrincipal (string): The service account to impersonate. +// targetScopes ([]string): Scopes to request during the // authorization grant. // delegates ([]string): The chained list of delegates required -// to grant the final access_token. +// to grant the final access_token. If set, the sequence of +// identities must have "Service Account Token Creator" capability +// granted to the preceeding identity. For example, if set to +// [serviceAccountB, serviceAccountC], the source_credential +// must have the Token Creator role on serviceAccountB. +// serviceAccountB must have the Token Creator on serviceAccountC. +// Finally, C must have Token Creator on target_principal. +// If left unset, source_credential must have that role on +// target_principal. // lifetime (time.Duration): Number of seconds the delegated credential should // be valid for (upto 3600). // // Usage: -// principal := "impersonated-account@project.iam.gserviceaccount.com" +// targetPrincipal := "impersonated-account@project.iam.gserviceaccount.com" // lifetime := 30 * time.Second // delegates := []string{} -// newScopes := []string{storage.ScopeReadOnly} +// targetScopes := []string{storage.ScopeReadOnly} // rootTokenSource, err := google.DefaultTokenSource(ctx, // "https://www.googleapis.com/auth/iam") -// delegatetokenSource, err := google.ImpersonatedTokenSource(ctx, +// impersonatedTokenSource, err := google.ImpersonatedTokenSource(ctx, // rootTokenSource, -// principal, lifetime, delegates, newScopes) +// targetPrincipal, lifetime, delegates, targetScopes) // storeageClient, _ = storage.NewClient(ctx, -// option.WithTokenSource(delegatetokenSource)) +// option.WithTokenSource(impersonatedTokenSource)) // // Note that this is not a standard OAuth flow, but rather uses Google Cloud // IAMCredentials API to exchange one oauth token for an impersonated account // see: https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/generateAccessToken func ImpersonatedTokenSource(ctx context.Context, rootSource oauth2.TokenSource, - principal string, lifetime time.Duration, delegates []string, - newScopes []string) (oauth2.TokenSource, error) { + targetPrincipal string, lifetime time.Duration, delegates []string, + targetScopes []string) (oauth2.TokenSource, error) { - if rootSource == nil { - return nil, fmt.Errorf("oauth2/google: rootSource cannot be nil") - } - if lifetime > (3600 * time.Second) { - return nil, fmt.Errorf("oauth2/google: lifetime must be less than or equal to 3600 seconds") - } + if rootSource == nil { + return nil, fmt.Errorf("oauth2/google: rootSource cannot be nil") + } + if lifetime > (3600 * time.Second) { + return nil, fmt.Errorf("oauth2/google: lifetime must be less than or equal to 3600 seconds") + } - return &impersonatedTokenSource{ - ctx: ctx, - rootSource: rootSource, - principal: principal, - lifetime: lifetime, - delegates: delegates, - newScopes: newScopes, - }, nil + return &impersonatedTokenSource{ + ctx: ctx, + rootSource: rootSource, + targetPrincipal: targetPrincipal, + lifetime: lifetime, + delegates: delegates, + targetScopes: targetScopes, + }, nil } type impersonatedTokenSource struct { - ctx context.Context - rootSource oauth2.TokenSource - principal string - lifetime time.Duration - delegates []string - newScopes []string + ctx context.Context + rootSource oauth2.TokenSource + targetPrincipal string + lifetime time.Duration + delegates []string + targetScopes []string } func (ts *impersonatedTokenSource) Token() (*oauth2.Token, error) { - mu.Lock() - defer mu.Unlock() + mu.Lock() + defer mu.Unlock() - if tok.Valid() { - return tok, nil - } - client := oauth2.NewClient(ts.ctx, ts.rootSource) + if tok.Valid() { + return tok, nil + } + client := oauth2.NewClient(ts.ctx, ts.rootSource) - service, err := iamcredentials.New(client) - if err != nil { - return nil, fmt.Errorf("oauth2/google: Error creating IAMCredentials: %v", err) - } - name := fmt.Sprintf("projects/-/serviceAccounts/%s", ts.principal) - tokenRequest := &iamcredentials.GenerateAccessTokenRequest{ - Lifetime: fmt.Sprintf("%ds", int(ts.lifetime.Seconds())), - Delegates: ts.delegates, - Scope: ts.newScopes, - } - at, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, tokenRequest).Do() - if err != nil { - return nil, fmt.Errorf("oauth2/google: Error calling iamcredentials.GenerateAccessToken: %v", err) - } + service, err := iamcredentials.New(client) + if err != nil { + return nil, fmt.Errorf("oauth2/google: Error creating IAMCredentials: %v", err) + } + name := fmt.Sprintf("projects/-/serviceAccounts/%s", ts.targetPrincipal) + tokenRequest := &iamcredentials.GenerateAccessTokenRequest{ + Lifetime: fmt.Sprintf("%ds", int(ts.lifetime.Seconds())), + Delegates: ts.delegates, + Scope: ts.targetScopes, + } + at, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, tokenRequest).Do() + if err != nil { + return nil, fmt.Errorf("oauth2/google: Error calling iamcredentials.GenerateAccessToken: %v", err) + } - expireAt, err := time.Parse(time.RFC3339, at.ExpireTime) - if err != nil { - return nil, fmt.Errorf("oauth2/google: Error parsing ExpireTime from iamcredentials: %v", err) - } + expireAt, err := time.Parse(time.RFC3339, at.ExpireTime) + if err != nil { + return nil, fmt.Errorf("oauth2/google: Error parsing ExpireTime from iamcredentials: %v", err) + } - tok = &oauth2.Token{ - AccessToken: at.AccessToken, - Expiry: expireAt, - } + tok = &oauth2.Token{ + AccessToken: at.AccessToken, + Expiry: expireAt, + } - return tok, nil + return tok, nil } From 556b9b2f660113d25fb86cf89864ef025a10cb8e Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Wed, 14 Nov 2018 19:20:20 -0800 Subject: [PATCH 8/8] address patchset11 comments --- google/example_test.go | 32 +++++++- google/impersonate.go | 169 ++++++++++++++++++++--------------------- 2 files changed, 112 insertions(+), 89 deletions(-) diff --git a/google/example_test.go b/google/example_test.go index 643f50716..bdba9e5fa 100644 --- a/google/example_test.go +++ b/google/example_test.go @@ -5,12 +5,13 @@ package google_test import ( + "context" "fmt" "io/ioutil" "log" "net/http" + "time" - "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "golang.org/x/oauth2/jwt" @@ -160,3 +161,32 @@ func ExampleCredentialsFromJSON() { } _ = creds // TODO: Use creds. } + +func ExampleImpersonatedCredentials() { + ctx := context.Background() + targetPrincipal := "impersonated-account@project.iam.gserviceaccount.com" + lifetime := 30 * time.Second + delegates := []string{} + targetScopes := []string{"https://www.googleapis.com/auth/devstorage.read_only"} + rootTokenSource, err := google.DefaultTokenSource(ctx, + "https://www.googleapis.com/auth/iam") + if err != nil { + log.Fatal(err) + } + impersonatedTokenSource, err := google.ImpersonatedTokenSource( + google.ImpersonatedTokenConfig{ + RootTokenSource: rootTokenSource, + TargetPrincipal: targetPrincipal, + Lifetime: lifetime, + Delegates: delegates, + TargetScopes: targetScopes, + }, + ) + + client := &http.Client{ + Transport: &oauth2.Transport{ + Source: impersonatedTokenSource, + }, + } + client.Get("...") +} diff --git a/google/impersonate.go b/google/impersonate.go index abf895782..80d0fbf92 100644 --- a/google/impersonate.go +++ b/google/impersonate.go @@ -5,22 +5,26 @@ package google import ( - "context" - "fmt" - "sync" - "time" + "context" + "fmt" + "sync" + "time" - "golang.org/x/oauth2" - "google.golang.org/api/iamcredentials/v1" + "golang.org/x/oauth2" + "google.golang.org/api/iamcredentials/v1" ) -var ( - mu sync.Mutex - tok *oauth2.Token -) +// ImpersonatedTokenConfig prameters to start Credential impersonation exchange. +type ImpersonatedTokenConfig struct { + RootTokenSource oauth2.TokenSource + TargetPrincipal string + Lifetime time.Duration + Delegates []string + TargetScopes []string +} -// ImpersonatedTokenSource allows a TokenSource issued to a user or -// service account to impersonate another. The source project using +// ImpersonatedTokenSource returns a TokenSource issued to a user or +// service account to impersonate another. The source project using // ImpersonatedTokenSource must enable the "IAMCredentials" API. Also, the // target service account must grant the orginating principal the // "Service Account Token Creator" IAM role: @@ -36,98 +40,87 @@ var ( // targetScopes ([]string): Scopes to request during the // authorization grant. // delegates ([]string): The chained list of delegates required -// to grant the final access_token. If set, the sequence of +// to grant the final access_token. If set, the sequence of // identities must have "Service Account Token Creator" capability -// granted to the preceeding identity. For example, if set to +// granted to the preceeding identity. For example, if set to // [serviceAccountB, serviceAccountC], the source_credential // must have the Token Creator role on serviceAccountB. // serviceAccountB must have the Token Creator on serviceAccountC. // Finally, C must have Token Creator on target_principal. // If left unset, source_credential must have that role on // target_principal. -// lifetime (time.Duration): Number of seconds the delegated credential should -// be valid for (upto 3600). -// -// Usage: -// targetPrincipal := "impersonated-account@project.iam.gserviceaccount.com" -// lifetime := 30 * time.Second -// delegates := []string{} -// targetScopes := []string{storage.ScopeReadOnly} -// rootTokenSource, err := google.DefaultTokenSource(ctx, -// "https://www.googleapis.com/auth/iam") -// impersonatedTokenSource, err := google.ImpersonatedTokenSource(ctx, -// rootTokenSource, -// targetPrincipal, lifetime, delegates, targetScopes) -// storeageClient, _ = storage.NewClient(ctx, -// option.WithTokenSource(impersonatedTokenSource)) +// lifetime (time.Duration): Number of seconds the impersonated credential should +// be valid for (up to 3600). // // Note that this is not a standard OAuth flow, but rather uses Google Cloud // IAMCredentials API to exchange one oauth token for an impersonated account // see: https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/generateAccessToken -func ImpersonatedTokenSource(ctx context.Context, rootSource oauth2.TokenSource, - targetPrincipal string, lifetime time.Duration, delegates []string, - targetScopes []string) (oauth2.TokenSource, error) { - - if rootSource == nil { - return nil, fmt.Errorf("oauth2/google: rootSource cannot be nil") - } - if lifetime > (3600 * time.Second) { - return nil, fmt.Errorf("oauth2/google: lifetime must be less than or equal to 3600 seconds") - } - - return &impersonatedTokenSource{ - ctx: ctx, - rootSource: rootSource, - targetPrincipal: targetPrincipal, - lifetime: lifetime, - delegates: delegates, - targetScopes: targetScopes, - }, nil +func ImpersonatedTokenSource(tokenConfig ImpersonatedTokenConfig) (oauth2.TokenSource, error) { + + if tokenConfig.RootTokenSource == nil { + return nil, fmt.Errorf("oauth2/google: rootSource cannot be nil") + } + if tokenConfig.Lifetime > (3600 * time.Second) { + return nil, fmt.Errorf("oauth2/google: lifetime must be less than or equal to 3600 seconds") + } + + return &impersonatedTokenSource{ + refreshMutex: &sync.Mutex{}, // guards impersonatedToken; held while fetching or updating it. + impersonatedToken: nil, // Token representing the impersonated identity. Initially nil. + + rootSource: tokenConfig.RootTokenSource, + targetPrincipal: tokenConfig.TargetPrincipal, + lifetime: tokenConfig.Lifetime, + delegates: tokenConfig.Delegates, + targetScopes: tokenConfig.TargetScopes, + }, nil } type impersonatedTokenSource struct { - ctx context.Context - rootSource oauth2.TokenSource - targetPrincipal string - lifetime time.Duration - delegates []string - targetScopes []string + refreshMutex *sync.Mutex // guards impersonatedToken; held while fetching or updating it. + impersonatedToken *oauth2.Token // Token representing the impersonated identity. + + rootSource oauth2.TokenSource + targetPrincipal string + lifetime time.Duration + delegates []string + targetScopes []string } func (ts *impersonatedTokenSource) Token() (*oauth2.Token, error) { - mu.Lock() - defer mu.Unlock() - - if tok.Valid() { - return tok, nil - } - client := oauth2.NewClient(ts.ctx, ts.rootSource) - - service, err := iamcredentials.New(client) - if err != nil { - return nil, fmt.Errorf("oauth2/google: Error creating IAMCredentials: %v", err) - } - name := fmt.Sprintf("projects/-/serviceAccounts/%s", ts.targetPrincipal) - tokenRequest := &iamcredentials.GenerateAccessTokenRequest{ - Lifetime: fmt.Sprintf("%ds", int(ts.lifetime.Seconds())), - Delegates: ts.delegates, - Scope: ts.targetScopes, - } - at, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, tokenRequest).Do() - if err != nil { - return nil, fmt.Errorf("oauth2/google: Error calling iamcredentials.GenerateAccessToken: %v", err) - } - - expireAt, err := time.Parse(time.RFC3339, at.ExpireTime) - if err != nil { - return nil, fmt.Errorf("oauth2/google: Error parsing ExpireTime from iamcredentials: %v", err) - } - - tok = &oauth2.Token{ - AccessToken: at.AccessToken, - Expiry: expireAt, - } - - return tok, nil + ts.refreshMutex.Lock() + defer ts.refreshMutex.Unlock() + + if ts.impersonatedToken.Valid() { + return ts.impersonatedToken, nil + } + client := oauth2.NewClient(context.TODO(), ts.rootSource) + + service, err := iamcredentials.New(client) + if err != nil { + return nil, fmt.Errorf("oauth2/google: Error creating IAMCredentials: %v", err) + } + name := fmt.Sprintf("projects/-/serviceAccounts/%s", ts.targetPrincipal) + tokenRequest := &iamcredentials.GenerateAccessTokenRequest{ + Lifetime: fmt.Sprintf("%ds", int(ts.lifetime.Seconds())), + Delegates: ts.delegates, + Scope: ts.targetScopes, + } + at, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, tokenRequest).Do() + if err != nil { + return nil, fmt.Errorf("oauth2/google: Error calling iamcredentials.GenerateAccessToken: %v", err) + } + + expireAt, err := time.Parse(time.RFC3339, at.ExpireTime) + if err != nil { + return nil, fmt.Errorf("oauth2/google: Error parsing ExpireTime from iamcredentials: %v", err) + } + + ts.impersonatedToken = &oauth2.Token{ + AccessToken: at.AccessToken, + Expiry: expireAt, + } + + return ts.impersonatedToken, nil }