-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
This package is used for creating an impersonated TokenSource. This TokenSource can then be passed into any client or API that accepts a client option. This package provides support for three types of impersonation: 1. A service account impersonating another service account. 2. A service account impersonating another service account as an admin user -- a pattern used with domain wide delegation. 3. A service account creating an impersonated ID token.
- Loading branch information
Showing
11 changed files
with
1,132 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// Copyright 2021 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 impersonate is used to impersonate Google Credentials. | ||
// | ||
// Required IAM roles | ||
// | ||
// In order to impersonate a service account the base service account must have | ||
// the Service Account Token Creator role, roles/iam.serviceAccountTokenCreator, | ||
// on the service account being impersonated. See | ||
// https://cloud.google.com/iam/docs/understanding-service-accounts. | ||
// | ||
// Optionally, delegates can be used during impersonation if the base service | ||
// account lacks the token creator role on the target. When using delegates, | ||
// each service account must be granted roles/iam.serviceAccountTokenCreator | ||
// on the next service account in the delgation chain. | ||
// | ||
// For example, if a base service account of SA1 is trying to impersonate target | ||
// service account SA2 while using delegate service accounts DSA1 and DSA2, | ||
// the following must be true: | ||
// | ||
// 1. Base service account SA1 has roles/iam.serviceAccountTokenCreator on | ||
// DSA1. | ||
// 2. DSA1 has roles/iam.serviceAccountTokenCreator on DSA2. | ||
// 3. DSA2 has roles/iam.serviceAccountTokenCreator on target SA2. | ||
// | ||
// If the base credential is an authorized user and not a service account, or if | ||
// the option WithQuotaProject is set, the target service account must have a | ||
// role that grants the serviceusage.services.use permission such as | ||
// roles/serviceusage.serviceUsageConsumer. | ||
package impersonate |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// Copyright 2021 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 impersonate_test | ||
|
||
import ( | ||
"context" | ||
"log" | ||
|
||
admin "google.golang.org/api/admin/directory/v1" | ||
"google.golang.org/api/impersonate" | ||
"google.golang.org/api/option" | ||
"google.golang.org/api/secretmanager/v1" | ||
"google.golang.org/api/transport" | ||
) | ||
|
||
func ExampleCredentialsTokenSource_serviceAccount() { | ||
ctx := context.Background() | ||
|
||
// Base credentials sourced from ADC or provided client options. | ||
ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ | ||
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com", | ||
Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, | ||
// Optionally supply delegates. | ||
Delegates: []string{"bar@project-id.iam.gserviceaccount.com"}, | ||
}) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
// Pass an impersonated credential to any function that takes client | ||
// options. | ||
client, err := secretmanager.NewService(ctx, option.WithTokenSource(ts)) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
// Use your client that is authenticated with impersonated credentials to | ||
// make requests. | ||
client.Projects.Secrets.Get("...") | ||
} | ||
|
||
func ExampleCredentialsTokenSource_adminUser() { | ||
ctx := context.Background() | ||
|
||
// Base credentials sourced from ADC or provided client options. | ||
ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ | ||
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com", | ||
Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, | ||
// Optionally supply delegates. | ||
Delegates: []string{"bar@project-id.iam.gserviceaccount.com"}, | ||
// Specify user to impersonate | ||
Subject: "admin@example.com", | ||
}) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
// Pass an impersonated credential to any function that takes client | ||
// options. | ||
client, err := admin.NewService(ctx, option.WithTokenSource(ts)) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
// Use your client that is authenticated with impersonated credentials to | ||
// make requests. | ||
client.Groups.Delete("...") | ||
} | ||
|
||
func ExampleIDTokenSource() { | ||
ctx := context.Background() | ||
|
||
// Base credentials sourced from ADC or provided client options. | ||
ts, err := impersonate.IDTokenSource(ctx, impersonate.IDTokenConfig{ | ||
Audience: "http://example.com/", | ||
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com", | ||
IncludeEmail: true, | ||
// Optionally supply delegates. | ||
Delegates: []string{"bar@project-id.iam.gserviceaccount.com"}, | ||
}) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
// Pass an impersonated credential to any function that takes client | ||
// options. | ||
client, _, err := transport.NewHTTPClient(ctx, option.WithTokenSource(ts)) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
// Use your client that is authenticated with impersonated credentials to | ||
// make requests. | ||
client.Get("http://example.com/") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
// Copyright 2021 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 impersonate | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"net/http" | ||
"time" | ||
|
||
"golang.org/x/oauth2" | ||
"google.golang.org/api/option" | ||
htransport "google.golang.org/api/transport/http" | ||
) | ||
|
||
// IDTokenConfig for generating an impersonated ID token. | ||
type IDTokenConfig struct { | ||
// Audience is the `aud` field for the token, such as an API endpoint the | ||
// token will grant access to. Required. | ||
Audience string | ||
// TargetPrincipal is the email address of the service account to | ||
// impersonate. Required. | ||
TargetPrincipal string | ||
// IncludeEmail includes the service account's email in the token. The | ||
// resulting token will include both an `email` and `email_verified` | ||
// claim. | ||
IncludeEmail bool | ||
// 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 | ||
} | ||
|
||
// IDTokenSource creates an impersonated TokenSource that returns ID tokens | ||
// configured with the provided config and using credentials loaded from | ||
// Application Default Credentials as the base credentials. The tokens provided | ||
// by the source are valid for one hour and are automatically refreshed. | ||
func IDTokenSource(ctx context.Context, config IDTokenConfig, opts ...option.ClientOption) (oauth2.TokenSource, error) { | ||
if config.Audience == "" { | ||
return nil, fmt.Errorf("impersonate: an audience must be provided") | ||
} | ||
if config.TargetPrincipal == "" { | ||
return nil, fmt.Errorf("impersonate: a target service account must be provided") | ||
} | ||
|
||
clientOpts := append(defaultClientOptions(), opts...) | ||
client, _, err := htransport.NewClient(ctx, clientOpts...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
its := impersonatedIDTokenSource{ | ||
client: client, | ||
targetPrincipal: config.TargetPrincipal, | ||
audience: config.Audience, | ||
includeEmail: config.IncludeEmail, | ||
} | ||
for _, v := range config.Delegates { | ||
its.delegates = append(its.delegates, formatIAMServiceAccountName(v)) | ||
} | ||
return oauth2.ReuseTokenSource(nil, its), nil | ||
} | ||
|
||
type generateIDTokenRequest struct { | ||
Audience string `json:"audience"` | ||
IncludeEmail bool `json:"includeEmail"` | ||
Delegates []string `json:"delegates,omitempty"` | ||
} | ||
|
||
type generateIDTokenResponse struct { | ||
Token string `json:"token"` | ||
} | ||
|
||
type impersonatedIDTokenSource struct { | ||
client *http.Client | ||
|
||
targetPrincipal string | ||
audience string | ||
includeEmail bool | ||
delegates []string | ||
} | ||
|
||
func (i impersonatedIDTokenSource) Token() (*oauth2.Token, error) { | ||
now := time.Now() | ||
genIDTokenReq := generateIDTokenRequest{ | ||
Audience: i.audience, | ||
IncludeEmail: i.includeEmail, | ||
Delegates: i.delegates, | ||
} | ||
bodyBytes, err := json.Marshal(genIDTokenReq) | ||
if err != nil { | ||
return nil, fmt.Errorf("impersonate: unable to marshal request: %v", err) | ||
} | ||
|
||
url := fmt.Sprintf("%s/v1/%s:generateIdToken", iamCredentailsEndpoint, formatIAMServiceAccountName(i.targetPrincipal)) | ||
req, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes)) | ||
if err != nil { | ||
return nil, fmt.Errorf("impersonate: unable to create request: %v", err) | ||
} | ||
req.Header.Set("Content-Type", "application/json") | ||
resp, err := i.client.Do(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("impersonate: unable to generate ID token: %v", err) | ||
} | ||
defer resp.Body.Close() | ||
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) | ||
if err != nil { | ||
return nil, fmt.Errorf("impersonate: unable to read body: %v", err) | ||
} | ||
if c := resp.StatusCode; c < 200 || c > 299 { | ||
return nil, fmt.Errorf("impersonate: status code %d: %s", c, body) | ||
} | ||
|
||
var generateIDTokenResp generateIDTokenResponse | ||
if err := json.Unmarshal(body, &generateIDTokenResp); err != nil { | ||
return nil, fmt.Errorf("impersonate: unable to parse response: %v", err) | ||
} | ||
return &oauth2.Token{ | ||
AccessToken: generateIDTokenResp.Token, | ||
// Generated ID tokens are good for one hour. | ||
Expiry: now.Add(1 * time.Hour), | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
// Copyright 2021 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 impersonate | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"io/ioutil" | ||
"net/http" | ||
"testing" | ||
|
||
"google.golang.org/api/option" | ||
) | ||
|
||
func TestIDTokenSource(t *testing.T) { | ||
ctx := context.Background() | ||
tests := []struct { | ||
name string | ||
aud string | ||
targetPrincipal string | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "missing aud", | ||
targetPrincipal: "foo@project-id.iam.gserviceaccount.com", | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "missing targetPrincipal", | ||
aud: "http://example.com/", | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "works", | ||
aud: "http://example.com/", | ||
targetPrincipal: "foo@project-id.iam.gserviceaccount.com", | ||
wantErr: false, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
name := tt.name | ||
t.Run(name, func(t *testing.T) { | ||
idTok := "id-token" | ||
client := &http.Client{ | ||
Transport: RoundTripFn(func(req *http.Request) *http.Response { | ||
resp := generateIDTokenResponse{ | ||
Token: idTok, | ||
} | ||
b, err := json.Marshal(&resp) | ||
if err != nil { | ||
t.Fatalf("unable to marshal response: %v", err) | ||
} | ||
return &http.Response{ | ||
StatusCode: 200, | ||
Body: ioutil.NopCloser(bytes.NewReader(b)), | ||
Header: make(http.Header), | ||
} | ||
}), | ||
} | ||
ts, err := IDTokenSource(ctx, IDTokenConfig{ | ||
Audience: tt.aud, | ||
TargetPrincipal: tt.targetPrincipal, | ||
}, option.WithHTTPClient(client)) | ||
if tt.wantErr && err != nil { | ||
return | ||
} | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
tok, err := ts.Token() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if tok.AccessToken != idTok { | ||
t.Fatalf("got %q, want %q", tok.AccessToken, idTok) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.