From a3dd13af1f779bb22895e48926bd80c46f03c7f3 Mon Sep 17 00:00:00 2001 From: guillaume blaquiere Date: Mon, 23 Aug 2021 10:43:18 +0200 Subject: [PATCH 1/7] feat: add service account impersonation support The impersonated_service_account type in the JSON configuration file is taken into account. source_credentials field in the JSON is now supported. Fixes #515 --- google/google.go | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/google/google.go b/google/google.go index 422ff1f..eeeb4e1 100644 --- a/google/google.go +++ b/google/google.go @@ -92,9 +92,10 @@ func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) { // JSON key file types. const ( - serviceAccountKey = "service_account" - userCredentialsKey = "authorized_user" - externalAccountKey = "external_account" + serviceAccountKey = "service_account" + userCredentialsKey = "authorized_user" + externalAccountKey = "external_account" + impersonatedServiceAccount = "impersonated_service_account" ) // credentialsFile is the unmarshalled representation of a credentials file. @@ -123,6 +124,9 @@ type credentialsFile struct { ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` CredentialSource externalaccount.CredentialSource `json:"credential_source"` QuotaProjectID string `json:"quota_project_id"` + + // Service account impersonation + SourceCredentials *credentialsFile `json:"source_credentials"` } func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config { @@ -178,6 +182,23 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar Scopes: params.Scopes, } return cfg.TokenSource(ctx) + case impersonatedServiceAccount: + if f.SourceCredentials == nil { + return nil, errors.New("missing 'source_credentials' field in credentials") + } + + sourceToken, err := f.SourceCredentials.tokenSource(ctx, params) + if err != nil { + return nil, err + } + imp := externalaccount.ImpersonateTokenSource{ + Ctx: ctx, + Url: f.ServiceAccountImpersonationURL, + Scopes: params.Scopes, + Ts: oauth2.ReuseTokenSource(nil, sourceToken), + // Delegates?? -> I don't know how to manage and how to use them here + } + return oauth2.ReuseTokenSource(nil, imp), nil case "": return nil, errors.New("missing 'type' field in credentials") default: From 121de73b38f0d33d9d04cceb7ea076c302dd6d5e Mon Sep 17 00:00:00 2001 From: guillaume blaquiere Date: Mon, 23 Aug 2021 10:52:40 +0200 Subject: [PATCH 2/7] refactor: make impersonateTokenSource struct public to impersonate the service account from the JSON config file, the existing impersonation mechanism can be reused. Updates #515 --- .../externalaccount/basecredentials.go | 10 +++++----- .../internal/externalaccount/impersonate.go | 20 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index dab917f..002b7d8 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -124,11 +124,11 @@ func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Re } scopes := c.Scopes ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"} - imp := impersonateTokenSource{ - ctx: ctx, - url: c.ServiceAccountImpersonationURL, - scopes: scopes, - ts: oauth2.ReuseTokenSource(nil, ts), + imp := ImpersonateTokenSource{ + Ctx: ctx, + Url: c.ServiceAccountImpersonationURL, + Scopes: scopes, + Ts: oauth2.ReuseTokenSource(nil, ts), } return oauth2.ReuseTokenSource(nil, imp), nil } diff --git a/google/internal/externalaccount/impersonate.go b/google/internal/externalaccount/impersonate.go index 64edb56..ee50ad8 100644 --- a/google/internal/externalaccount/impersonate.go +++ b/google/internal/externalaccount/impersonate.go @@ -29,30 +29,30 @@ type impersonateTokenResponse struct { ExpireTime string `json:"expireTime"` } -type impersonateTokenSource struct { - ctx context.Context - ts oauth2.TokenSource +type ImpersonateTokenSource struct { + Ctx context.Context + Ts oauth2.TokenSource - url string - scopes []string + Url string + Scopes []string } // 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) { reqBody := generateAccessTokenReq{ Lifetime: "3600s", - Scope: its.scopes, + Scope: its.Scopes, } b, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("oauth2/google: unable to marshal request: %v", err) } - client := oauth2.NewClient(its.ctx, its.ts) - req, err := http.NewRequest("POST", its.url, bytes.NewReader(b)) + client := oauth2.NewClient(its.Ctx, its.Ts) + req, err := http.NewRequest("POST", its.Url, bytes.NewReader(b)) if err != nil { return nil, fmt.Errorf("oauth2/google: unable to create impersonation request: %v", err) } - req = req.WithContext(its.ctx) + req = req.WithContext(its.Ctx) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) From 2fca3adf8de7e0eb2c3303024336bb6499f3cc29 Mon Sep 17 00:00:00 2001 From: guillaume blaquiere Date: Mon, 23 Aug 2021 11:01:37 +0200 Subject: [PATCH 3/7] docs: ImpersonateTokenSource description The refactor made ImpersonateTokenSource public. Short explanation of each fields. Updates #515 --- google/internal/externalaccount/impersonate.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/google/internal/externalaccount/impersonate.go b/google/internal/externalaccount/impersonate.go index ee50ad8..cea9458 100644 --- a/google/internal/externalaccount/impersonate.go +++ b/google/internal/externalaccount/impersonate.go @@ -29,11 +29,17 @@ type impersonateTokenResponse struct { ExpireTime string `json:"expireTime"` } +// ImpersonateTokenSource uses a source credential, stored in Ts, to request an access token to the provided Url +// Scopes can be defined when the access token is requested. type ImpersonateTokenSource struct { + // execution context Ctx context.Context - Ts oauth2.TokenSource + // source credential + Ts oauth2.TokenSource - Url string + // impersonation url to request an access token + Url string + // scopes to include in the access token request Scopes []string } From c2f6109f1ce4eb5d20411a529112d774c7658dfa Mon Sep 17 00:00:00 2001 From: guillaume blaquiere Date: Sat, 4 Sep 2021 21:11:51 +0200 Subject: [PATCH 4/7] fix: Add delegates support Get the delegates from the input JSON and use them in the refreshToken requests. Updates #515 --- google/google.go | 11 ++++++----- google/internal/externalaccount/impersonate.go | 7 +++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/google/google.go b/google/google.go index eeeb4e1..15f8f32 100644 --- a/google/google.go +++ b/google/google.go @@ -122,6 +122,7 @@ type credentialsFile struct { TokenURLExternal string `json:"token_url"` TokenInfoURL string `json:"token_info_url"` ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` + Delegates []string `json:"delegates"` CredentialSource externalaccount.CredentialSource `json:"credential_source"` QuotaProjectID string `json:"quota_project_id"` @@ -192,11 +193,11 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar return nil, err } imp := externalaccount.ImpersonateTokenSource{ - Ctx: ctx, - Url: f.ServiceAccountImpersonationURL, - Scopes: params.Scopes, - Ts: oauth2.ReuseTokenSource(nil, sourceToken), - // Delegates?? -> I don't know how to manage and how to use them here + Ctx: ctx, + Url: f.ServiceAccountImpersonationURL, + Scopes: params.Scopes, + Ts: oauth2.ReuseTokenSource(nil, sourceToken), + Delegates: f.Delegates, } return oauth2.ReuseTokenSource(nil, imp), nil case "": diff --git a/google/internal/externalaccount/impersonate.go b/google/internal/externalaccount/impersonate.go index cea9458..0d476d1 100644 --- a/google/internal/externalaccount/impersonate.go +++ b/google/internal/externalaccount/impersonate.go @@ -41,13 +41,16 @@ type ImpersonateTokenSource struct { Url string // scopes to include in the access token request Scopes []string + // Delegates for impersonation to include in the access token request + Delegates []string } // Token performs the exchange to get a temporary service account token to allow access to GCP. func (its ImpersonateTokenSource) Token() (*oauth2.Token, error) { reqBody := generateAccessTokenReq{ - Lifetime: "3600s", - Scope: its.Scopes, + Lifetime: "3600s", + Scope: its.Scopes, + Delegates: its.Delegates, } b, err := json.Marshal(reqBody) if err != nil { From 1c896197e869769bedaff2f93c941c72414cc2bf Mon Sep 17 00:00:00 2001 From: Guillaume Blaquiere Date: Wed, 15 Sep 2021 10:15:15 +0200 Subject: [PATCH 5/7] chore: rename variable and improve documentation. --- google/google.go | 6 +++--- .../externalaccount/basecredentials.go | 2 +- .../internal/externalaccount/impersonate.go | 21 ++++++++++++------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/google/google.go b/google/google.go index 15f8f32..0e5b53d 100644 --- a/google/google.go +++ b/google/google.go @@ -188,15 +188,15 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar return nil, errors.New("missing 'source_credentials' field in credentials") } - sourceToken, err := f.SourceCredentials.tokenSource(ctx, params) + ts, err := f.SourceCredentials.tokenSource(ctx, params) if err != nil { return nil, err } imp := externalaccount.ImpersonateTokenSource{ Ctx: ctx, - Url: f.ServiceAccountImpersonationURL, + URL: f.ServiceAccountImpersonationURL, Scopes: params.Scopes, - Ts: oauth2.ReuseTokenSource(nil, sourceToken), + Ts: oauth2.ReuseTokenSource(nil, ts), Delegates: f.Delegates, } return oauth2.ReuseTokenSource(nil, imp), nil diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index 002b7d8..fd0a02b 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -126,7 +126,7 @@ func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Re ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"} imp := ImpersonateTokenSource{ Ctx: ctx, - Url: c.ServiceAccountImpersonationURL, + URL: c.ServiceAccountImpersonationURL, Scopes: scopes, Ts: oauth2.ReuseTokenSource(nil, ts), } diff --git a/google/internal/externalaccount/impersonate.go b/google/internal/externalaccount/impersonate.go index 0d476d1..8251fc8 100644 --- a/google/internal/externalaccount/impersonate.go +++ b/google/internal/externalaccount/impersonate.go @@ -29,19 +29,24 @@ type impersonateTokenResponse struct { ExpireTime string `json:"expireTime"` } -// ImpersonateTokenSource uses a source credential, stored in Ts, to request an access token to the provided Url +// ImpersonateTokenSource uses a source credential, stored in Ts, to request an access token to the provided URL. // Scopes can be defined when the access token is requested. type ImpersonateTokenSource struct { - // execution context + // Ctx is the execution context of the impersonation process + // used to perform http call to the URL. Required Ctx context.Context - // source credential + // Ts is the source credential used to generate a token on the + // impersonated service account. Required. Ts oauth2.TokenSource - // impersonation url to request an access token - Url string - // scopes to include in the access token request + // URL is the endpoint to call to generate a token + // on behalf the service account. Required. + URL string + // Scopes that the impersonated credential should have. Required. Scopes []string - // Delegates for impersonation to include in the access token request + // Delegates are the service account email addresses in a delegation chain. + // Each service account must be granted roles/iam.serviceAccountTokenCreator + // on the next service account in the chain. Optional. Delegates []string } @@ -57,7 +62,7 @@ func (its ImpersonateTokenSource) Token() (*oauth2.Token, error) { return nil, fmt.Errorf("oauth2/google: unable to marshal request: %v", err) } client := oauth2.NewClient(its.Ctx, its.Ts) - req, err := http.NewRequest("POST", its.Url, bytes.NewReader(b)) + req, err := http.NewRequest("POST", its.URL, bytes.NewReader(b)) if err != nil { return nil, fmt.Errorf("oauth2/google: unable to create impersonation request: %v", err) } From 2a8fe80698a9dcd57358a3f522eba2c45160a4f6 Mon Sep 17 00:00:00 2001 From: Guillaume Blaquiere Date: Wed, 15 Sep 2021 10:16:09 +0200 Subject: [PATCH 6/7] fix: add ImpersonationURL check --- google/google.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/google.go b/google/google.go index 0e5b53d..c4fe932 100644 --- a/google/google.go +++ b/google/google.go @@ -184,7 +184,7 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar } return cfg.TokenSource(ctx) case impersonatedServiceAccount: - if f.SourceCredentials == nil { + if f.ServiceAccountImpersonationURL != "" && f.SourceCredentials == nil { return nil, errors.New("missing 'source_credentials' field in credentials") } From 07d40c05972bb05ae2e4a002fcc33b1d88ab9834 Mon Sep 17 00:00:00 2001 From: Guillaume Blaquiere Date: Mon, 27 Sep 2021 21:47:14 +0200 Subject: [PATCH 7/7] fix: wrong if condition, improve error message. --- google/google.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/google/google.go b/google/google.go index c4fe932..a1f6671 100644 --- a/google/google.go +++ b/google/google.go @@ -184,8 +184,8 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar } return cfg.TokenSource(ctx) case impersonatedServiceAccount: - if f.ServiceAccountImpersonationURL != "" && f.SourceCredentials == nil { - return nil, errors.New("missing 'source_credentials' field in credentials") + if f.ServiceAccountImpersonationURL != "" || f.SourceCredentials == nil { + return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials") } ts, err := f.SourceCredentials.tokenSource(ctx, params)