From a8e26b9ca42882fab141b70a957c66c33ade0eee Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Wed, 9 Jun 2021 10:46:53 -0700 Subject: [PATCH 01/27] oauth2/google: implement support for token downscoping to allow for restricted permissions --- google/downscoped/downscoping.go | 116 ++++++++++++++++++++++++++ google/downscoped/downscoping_test.go | 55 ++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 google/downscoped/downscoping.go create mode 100644 google/downscoped/downscoping_test.go diff --git a/google/downscoped/downscoping.go b/google/downscoped/downscoping.go new file mode 100644 index 000000000..5d2ce6ec2 --- /dev/null +++ b/google/downscoped/downscoping.go @@ -0,0 +1,116 @@ +package downscoped + +import ( + "encoding/json" + "fmt" + "golang.org/x/oauth2" + "io/ioutil" + "net/http" + "net/url" + "time" +) + +const ( + IDENTITY_BINDING_ENDPOINT = "https://sts.googleapis.com/v1beta/token" +) + +// Defines an upper bound of permissions available for a GCP credential for one or more resources +type AccessBoundary struct { + AccessBoundaryRules []AccessBoundaryRule `json:"accessBoundaryRules"` +} + +func NewAccessBoundary() AccessBoundary { + return AccessBoundary{make([]AccessBoundaryRule, 0)} +} + +type AvailabilityCondition struct { + Title string `json:"title,omitempty"` + Expression string `json:"expression"` + Description string `json:"description,omitempty"` +} + +type AccessBoundaryRule struct { + AvailableResource string `json:"availableResource"` + AvailablePermissions []string `json:"availablePermissions"` + Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"` +} + +type DownScopedTokenResponse struct { + AccessToken string `json:"access_token"` + IssuedTokenType string `json:"issued_token_type"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` +} + +type DownscopingConfig struct { + RootSource oauth2.TokenSource + CredentialAccessBoundary AccessBoundary +} + +func DownscopedTokenWithEndpoint(config DownscopingConfig, endpoint string) (oauth2.TokenSource, error) { + if config.RootSource == nil { + return nil, fmt.Errorf("oauth2/google: rootTokenSource cannot be nil") + } + if len(config.CredentialAccessBoundary.AccessBoundaryRules) == 0 { + return nil, fmt.Errorf("oauth2/google: length of AccessBoundaryRules must be at least 1") + } + + downscopedOptions := struct { + Boundary AccessBoundary `json:"accessBoundary"` + }{ + Boundary: config.CredentialAccessBoundary, + } + + tok, err := config.RootSource.Token() + if err != nil { + return nil, fmt.Errorf("oauth2/google: unable to refresh root token %v", err) + } + + b, err := json.Marshal(downscopedOptions) // TODO: make sure that this marshals properly! + if err != nil { + return nil, fmt.Errorf("oauth2/google: Unable to marshall AccessBoundary payload %v", err) + } + + form := url.Values{} + form.Add("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") + form.Add("subject_token_type", "urn:ietf:params:oauth:token-type:access_token") + form.Add("requested_token_type", "urn:ietf:params:oauth:token-type:access_token") + form.Add("subject_token", tok.AccessToken) + form.Add("options", url.QueryEscape(string(b))) + + resp, err := http.PostForm(endpoint, form) + defer resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("unable to generate POST Request %v", err) + } + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := ioutil.ReadAll(resp.Body) + return nil, fmt.Errorf("unable to exchange token %v", string(bodyBytes)) + } + + tresp := DownScopedTokenResponse{} + json.NewDecoder(resp.Body).Decode(&tresp) + + // an exchanged token that is derived from a service account (2LO) has an expired_in value + // a token derived from a users token (3LO) does not. + // The following code uses the time remaining on rootToken for a user as the value for the + // derived token's lifetime + var expiry_time time.Time + if tresp.ExpiresIn > 0 { + expiry_time = time.Now().Add(time.Duration(time.Duration(tresp.ExpiresIn) * time.Second)) + } else { + expiry_time = tok.Expiry + } + + newToken := &oauth2.Token{ + AccessToken: tresp.AccessToken, + TokenType: tresp.TokenType, + Expiry: expiry_time, + } + return oauth2.StaticTokenSource(newToken), nil +} + +func NewDownscopedTokenSource(config DownscopingConfig) (oauth2.TokenSource, error) { + return DownscopedTokenWithEndpoint(config, IDENTITY_BINDING_ENDPOINT) +} diff --git a/google/downscoped/downscoping_test.go b/google/downscoped/downscoping_test.go new file mode 100644 index 000000000..fca160108 --- /dev/null +++ b/google/downscoped/downscoping_test.go @@ -0,0 +1,55 @@ +package downscoped + +import ( + "golang.org/x/oauth2" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +var ( + standardReqBody = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=%257B%2522accessBoundary%2522%253A%257B%2522accessBoundaryRules%2522%253A%255B%257B%2522availableResource%2522%253A%2522test1%2522%252C%2522availablePermissions%2522%253A%255B%2522Perm1%252C%2Bperm2%2522%255D%257D%255D%257D%257D&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&subject_token=Mellon&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token" + standardRespBody = `{"access_token":"Open Sesame","expires_in":432,"issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer"}` +) + +func Test_NewAccessBoundary(t *testing.T) { + got := NewAccessBoundary() + want := AccessBoundary{nil} + if got.AccessBoundaryRules == nil || len(got.AccessBoundaryRules) != 0 { + t.Errorf("NewAccessBoundary() = %v; want %v", got, want) + } +} + +func Test_DownscopedTokenSource(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Errorf("Unexpected request method, %v is found", r.Method) + } + if r.URL.String() != "/" { //TODO: Will this work, or do I need to redirect this to this test server instead? + t.Errorf("Unexpected request URL, %v is found", r.URL) + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatalf("Failed to read request body: %v", err) + } + if got, want := string(body), standardReqBody; got != want { + t.Errorf("Unexpected exchange payload: got %v but want %v,", got, want) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(standardRespBody)) + + })) + new := NewAccessBoundary() + new.AccessBoundaryRules = append(new.AccessBoundaryRules, AccessBoundaryRule{"test1", []string{"Perm1, perm2"}, nil}) + myTok := oauth2.Token{AccessToken: "Mellon"} + tmpSrc := oauth2.StaticTokenSource(&myTok) + out, err := DownscopedTokenWithEndpoint(DownscopingConfig{tmpSrc, new}, ts.URL) + if err != nil { + t.Fatalf("NewDownscopedTokenSource failed with error: %v", err) + } + _, err = out.Token() + if err != nil { + t.Fatalf("Token() call failed with error %v", err) + } +} From 52684dc091dcf33ac80e51f219a52803356a128a Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Wed, 9 Jun 2021 14:25:06 -0700 Subject: [PATCH 02/27] First set of comment changes. --- google/downscoped/downscoping.go | 43 +++++++++++++-------------- google/downscoped/downscoping_test.go | 9 +++--- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/google/downscoped/downscoping.go b/google/downscoped/downscoping.go index 5d2ce6ec2..80677d4f6 100644 --- a/google/downscoped/downscoping.go +++ b/google/downscoped/downscoping.go @@ -1,28 +1,25 @@ package downscoped import ( + "context" "encoding/json" "fmt" "golang.org/x/oauth2" - "io/ioutil" "net/http" "net/url" "time" ) const ( - IDENTITY_BINDING_ENDPOINT = "https://sts.googleapis.com/v1beta/token" + identityBindingEndpoint = "https://sts.googleapis.com/v1beta/token" ) // Defines an upper bound of permissions available for a GCP credential for one or more resources type AccessBoundary struct { + // One or more AccessBoundaryRules are required to define permissions for the new downscoped token AccessBoundaryRules []AccessBoundaryRule `json:"accessBoundaryRules"` } -func NewAccessBoundary() AccessBoundary { - return AccessBoundary{make([]AccessBoundaryRule, 0)} -} - type AvailabilityCondition struct { Title string `json:"title,omitempty"` Expression string `json:"expression"` @@ -35,7 +32,7 @@ type AccessBoundaryRule struct { Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"` } -type DownScopedTokenResponse struct { +type downscopedTokenResponse struct { AccessToken string `json:"access_token"` IssuedTokenType string `json:"issued_token_type"` TokenType string `json:"token_type"` @@ -47,12 +44,12 @@ type DownscopingConfig struct { CredentialAccessBoundary AccessBoundary } -func DownscopedTokenWithEndpoint(config DownscopingConfig, endpoint string) (oauth2.TokenSource, error) { +func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, endpoint string) (oauth2.TokenSource, error) { if config.RootSource == nil { - return nil, fmt.Errorf("oauth2/google: rootTokenSource cannot be nil") + return nil, fmt.Errorf("oauth2/google/downscoped: rootTokenSource cannot be nil") } if len(config.CredentialAccessBoundary.AccessBoundaryRules) == 0 { - return nil, fmt.Errorf("oauth2/google: length of AccessBoundaryRules must be at least 1") + return nil, fmt.Errorf("oauth2/google/downscoped: length of AccessBoundaryRules must be at least 1") } downscopedOptions := struct { @@ -63,12 +60,12 @@ func DownscopedTokenWithEndpoint(config DownscopingConfig, endpoint string) (oau tok, err := config.RootSource.Token() if err != nil { - return nil, fmt.Errorf("oauth2/google: unable to refresh root token %v", err) + return nil, fmt.Errorf("oauth2/google/downscoped: unable to refresh root token %v", err) } - b, err := json.Marshal(downscopedOptions) // TODO: make sure that this marshals properly! + b, err := json.Marshal(downscopedOptions) if err != nil { - return nil, fmt.Errorf("oauth2/google: Unable to marshall AccessBoundary payload %v", err) + return nil, fmt.Errorf("oauth2/google/downscoped: Unable to marshall AccessBoundary payload %v", err) } form := url.Values{} @@ -78,20 +75,22 @@ func DownscopedTokenWithEndpoint(config DownscopingConfig, endpoint string) (oau form.Add("subject_token", tok.AccessToken) form.Add("options", url.QueryEscape(string(b))) - resp, err := http.PostForm(endpoint, form) - defer resp.Body.Close() + myClient := oauth2.NewClient(ctx, nil) + resp, err := myClient.PostForm(endpoint, form) if err != nil { return nil, fmt.Errorf("unable to generate POST Request %v", err) } + defer resp.Body.Close() + var tresp downscopedTokenResponse + err = json.NewDecoder(resp.Body).Decode(&tresp) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal response body: %v", err) + } if resp.StatusCode != http.StatusOK { - bodyBytes, _ := ioutil.ReadAll(resp.Body) - return nil, fmt.Errorf("unable to exchange token %v", string(bodyBytes)) + return nil, fmt.Errorf("unable to exchange token %v", tresp) } - tresp := DownScopedTokenResponse{} - json.NewDecoder(resp.Body).Decode(&tresp) - // an exchanged token that is derived from a service account (2LO) has an expired_in value // a token derived from a users token (3LO) does not. // The following code uses the time remaining on rootToken for a user as the value for the @@ -111,6 +110,6 @@ func DownscopedTokenWithEndpoint(config DownscopingConfig, endpoint string) (oau return oauth2.StaticTokenSource(newToken), nil } -func NewDownscopedTokenSource(config DownscopingConfig) (oauth2.TokenSource, error) { - return DownscopedTokenWithEndpoint(config, IDENTITY_BINDING_ENDPOINT) +func NewDownscopedTokenSource(ctx context.Context, config DownscopingConfig) (oauth2.TokenSource, error) { + return downscopedTokenWithEndpoint(ctx, config, identityBindingEndpoint) } diff --git a/google/downscoped/downscoping_test.go b/google/downscoped/downscoping_test.go index fca160108..d25d2e0ab 100644 --- a/google/downscoped/downscoping_test.go +++ b/google/downscoped/downscoping_test.go @@ -1,6 +1,7 @@ package downscoped import ( + "context" "golang.org/x/oauth2" "io/ioutil" "net/http" @@ -14,7 +15,7 @@ var ( ) func Test_NewAccessBoundary(t *testing.T) { - got := NewAccessBoundary() + got := AccessBoundary{make([]AccessBoundaryRule, 0)} want := AccessBoundary{nil} if got.AccessBoundaryRules == nil || len(got.AccessBoundaryRules) != 0 { t.Errorf("NewAccessBoundary() = %v; want %v", got, want) @@ -26,7 +27,7 @@ func Test_DownscopedTokenSource(t *testing.T) { if r.Method != "POST" { t.Errorf("Unexpected request method, %v is found", r.Method) } - if r.URL.String() != "/" { //TODO: Will this work, or do I need to redirect this to this test server instead? + if r.URL.String() != "/" { t.Errorf("Unexpected request URL, %v is found", r.URL) } body, err := ioutil.ReadAll(r.Body) @@ -40,11 +41,11 @@ func Test_DownscopedTokenSource(t *testing.T) { w.Write([]byte(standardRespBody)) })) - new := NewAccessBoundary() + new := AccessBoundary{make([]AccessBoundaryRule, 0)} new.AccessBoundaryRules = append(new.AccessBoundaryRules, AccessBoundaryRule{"test1", []string{"Perm1, perm2"}, nil}) myTok := oauth2.Token{AccessToken: "Mellon"} tmpSrc := oauth2.StaticTokenSource(&myTok) - out, err := DownscopedTokenWithEndpoint(DownscopingConfig{tmpSrc, new}, ts.URL) + out, err := downscopedTokenWithEndpoint(context.Background(), DownscopingConfig{tmpSrc, new}, ts.URL) if err != nil { t.Fatalf("NewDownscopedTokenSource failed with error: %v", err) } From eb57311a00818ed2053b762606d5254d305371fc Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 10 Jun 2021 14:11:50 -0700 Subject: [PATCH 03/27] Add some validity testing for AccessBoundaryRules and add documentation. --- .../{downscoped => downscope}/downscoping.go | 64 ++++++++++++++++--- .../downscoping_test.go | 2 +- 2 files changed, 56 insertions(+), 10 deletions(-) rename google/{downscoped => downscope}/downscoping.go (50%) rename google/{downscoped => downscope}/downscoping_test.go (99%) diff --git a/google/downscoped/downscoping.go b/google/downscope/downscoping.go similarity index 50% rename from google/downscoped/downscoping.go rename to google/downscope/downscoping.go index 80677d4f6..2b9d113e2 100644 --- a/google/downscoped/downscoping.go +++ b/google/downscope/downscoping.go @@ -1,4 +1,9 @@ -package downscoped +/* +Package downscope implements the ability to downwcope, or restrict, the +Identity and AccessManagement permissions that a short-lived Token +can use. Please note that only Google Cloud Storage supports this feature. + */ +package downscope import ( "context" @@ -16,19 +21,40 @@ const ( // Defines an upper bound of permissions available for a GCP credential for one or more resources type AccessBoundary struct { - // One or more AccessBoundaryRules are required to define permissions for the new downscoped token + // One or more AccessBoundaryRules are required to define permissions + // for the new downscoped token. Each one defines an access (or set of accesses) + // that the new token has to a given resource. AccessBoundaryRules []AccessBoundaryRule `json:"accessBoundaryRules"` } +// An AvailabilityCondition restricts access to a given Resource. type AvailabilityCondition struct { - Title string `json:"title,omitempty"` + // A condition expression that specifies the Cloud Storage objects where + // permissions are available. For further documentation, see + // https://cloud.google.com/iam/docs/conditions-overview Expression string `json:"expression"` + // Optional. A short string that identifies the purpose of the condition. + Title string `json:"title,omitempty"` + // Optional. Details about the purpose of the condition. Description string `json:"description,omitempty"` } +// Sets the permissions (and optionally conditions) that the new +// token has on given resource. type AccessBoundaryRule struct { + // AvailableResource is the full resource name of the Cloud Storage bucket that the rule applies to. + // Use the format //storage.googleapis.com/projects/_/buckets/bucket-name. AvailableResource string `json:"availableResource"` + // AvailablePermissions is a list that defines the upper bound on the available permissions + // for the resource. Each value is the identifier for an IAM predefined role or custom role, + // with the prefix inRole:. For example: inRole:roles/storage.objectViewer. + // Only the permissions in these roles will be available. AvailablePermissions []string `json:"availablePermissions"` + // An optional Condition that restricts the availability of permissions + // to specific Cloud Storage objects. + // + // Use this field if you want to make permissions available for specific objects, + // rather than all objects in a Cloud Storage bucket. Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"` } @@ -39,17 +65,33 @@ type downscopedTokenResponse struct { ExpiresIn int `json:"expires_in"` } +// Specifies the information necessary to request a downscoped token. type DownscopingConfig struct { + // RootSource is the TokenSource used to create the downscoped token. + // The downscoped token therefore has some subset of the accesses of + // the original RootSource. RootSource oauth2.TokenSource + // CredentialAccessBoundary defines the accesses held by the new + // downscoped Token. CredentialAccessBoundary AccessBoundary } +// downscopedTokenWithEndpoint is a helper function used for unit testing +// purposes, as it allows us to pass in a locally mocked endpoint. func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, endpoint string) (oauth2.TokenSource, error) { if config.RootSource == nil { - return nil, fmt.Errorf("oauth2/google/downscoped: rootTokenSource cannot be nil") + return nil, fmt.Errorf("downscope: rootTokenSource cannot be nil") } if len(config.CredentialAccessBoundary.AccessBoundaryRules) == 0 { - return nil, fmt.Errorf("oauth2/google/downscoped: length of AccessBoundaryRules must be at least 1") + return nil, fmt.Errorf("downscope: length of AccessBoundaryRules must be at least 1") + } + for _, val := range config.CredentialAccessBoundary.AccessBoundaryRules { + if val.AvailableResource == "" { + return nil, fmt.Errorf("downscope: all rules must have a nonempty AvailableResource: %+v", val) + } + if len(val.AvailablePermissions) == 0 { + return nil, fmt.Errorf("downscope: all rules must provide at least one permission: %+v", val) + } } downscopedOptions := struct { @@ -60,12 +102,12 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, tok, err := config.RootSource.Token() if err != nil { - return nil, fmt.Errorf("oauth2/google/downscoped: unable to refresh root token %v", err) + return nil, fmt.Errorf("downscope: unable to obtain root token: %v", err) } b, err := json.Marshal(downscopedOptions) if err != nil { - return nil, fmt.Errorf("oauth2/google/downscoped: Unable to marshall AccessBoundary payload %v", err) + return nil, fmt.Errorf("downscope: Unable to marshall AccessBoundary payload %v", err) } form := url.Values{} @@ -88,7 +130,7 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, return nil, fmt.Errorf("unable to unmarshal response body: %v", err) } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unable to exchange token %v", tresp) + return nil, fmt.Errorf("unable to exchange token; %v", resp.StatusCode) } // an exchanged token that is derived from a service account (2LO) has an expired_in value @@ -110,6 +152,10 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, return oauth2.StaticTokenSource(newToken), nil } -func NewDownscopedTokenSource(ctx context.Context, config DownscopingConfig) (oauth2.TokenSource, error) { +// NewTokenSource takes a root TokenSource and returns a downscoped TokenSource +// with a subset of the permissions held by the root source. The +// CredentialAccessBoundary in the config defines the permissions held +// by the new TokenSource. +func NewTokenSource(ctx context.Context, config DownscopingConfig) (oauth2.TokenSource, error) { return downscopedTokenWithEndpoint(ctx, config, identityBindingEndpoint) } diff --git a/google/downscoped/downscoping_test.go b/google/downscope/downscoping_test.go similarity index 99% rename from google/downscoped/downscoping_test.go rename to google/downscope/downscoping_test.go index d25d2e0ab..ee7563250 100644 --- a/google/downscoped/downscoping_test.go +++ b/google/downscope/downscoping_test.go @@ -1,4 +1,4 @@ -package downscoped +package downscope import ( "context" From add980136395b64876f296b9bc36b534781fd45e Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 10 Jun 2021 14:55:41 -0700 Subject: [PATCH 04/27] Add exmaple showing how NewTokenSource should be called. --- google/downscope/downscoping.go | 5 +++- google/downscope/downscoping_test.go | 41 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 2b9d113e2..343d6b8d3 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -155,7 +155,10 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, // NewTokenSource takes a root TokenSource and returns a downscoped TokenSource // with a subset of the permissions held by the root source. The // CredentialAccessBoundary in the config defines the permissions held -// by the new TokenSource. +// by the new TokenSource. Do note that the returned TokenSource is +// an oauth2.StaticTokenSource. If you wish to refresh this token automatically, +// then initialize a locally defined TokenSource struct with the Token held +// by the StaticTokenSource and wrap that TokenSource in an oauth2.ReuseTokenSource. func NewTokenSource(ctx context.Context, config DownscopingConfig) (oauth2.TokenSource, error) { return downscopedTokenWithEndpoint(ctx, config, identityBindingEndpoint) } diff --git a/google/downscope/downscoping_test.go b/google/downscope/downscoping_test.go index ee7563250..40eaba6e7 100644 --- a/google/downscope/downscoping_test.go +++ b/google/downscope/downscoping_test.go @@ -2,8 +2,11 @@ package downscope import ( "context" + "fmt" "golang.org/x/oauth2" + "golang.org/x/oauth2/google" "io/ioutil" + "log" "net/http" "net/http/httptest" "testing" @@ -54,3 +57,41 @@ func Test_DownscopedTokenSource(t *testing.T) { t.Fatalf("Token() call failed with error %v", err) } } + +func Example() { + ctx := context.Background() + availableResource := "//storage.googleapis.com/projects/_/buckets/foo" + availablePermissions := []string{"inRole:roles/storage.objectViewer"} + + + // Initializes an accessBoundary + myBoundary := AccessBoundary{make([]AccessBoundaryRule, 0)} + + // Add a new rule to the AccessBoundary + myBoundary.AccessBoundaryRules = append(myBoundary.AccessBoundaryRules, AccessBoundaryRule{availableResource, availablePermissions, nil}) + + // Get the token source for Application Default Credentials (DefaultTokenSource is a shorthand + // for is a shortcut for FindDefaultCredentials(ctx, scope).TokenSource. + // This example assumes that you've defined the GOOGLE_APPLICATION_CREDENTIALS environment variable + rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform") + if err != nil { + log.Fatalf("failed to generate root token source; %v", err) + return + } + myTokenSource, err := NewTokenSource(context.Background(), DownscopingConfig{rootSource, myBoundary}) + //myTokenSource, err := NewSource(rootSource, myBoundary) + if err != nil { + log.Fatalf("failed to generate downscoped token source: %v", err) + return + } + fmt.Printf("%+v\n", myTokenSource) + // You can now use the token held in myTokenSource to make + // Google Cloud Storage calls. A short example follows. + + // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(myTokenSource)) + // bkt := storageClient.Bucket(bucketName) + // obj := bkt.Object(objectName) + // rc, err := obj.NewReader(ctx) + // data, err := ioutil.ReadAll(rc) + return +} \ No newline at end of file From e035bf9673ef6ab1b5468c36c3ef9d326aef19c8 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 10 Jun 2021 14:55:56 -0700 Subject: [PATCH 05/27] go fmt --- google/downscope/downscoping.go | 14 +++++++------- google/downscope/downscoping_test.go | 3 +-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 343d6b8d3..4f89e3ecf 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -2,7 +2,7 @@ Package downscope implements the ability to downwcope, or restrict, the Identity and AccessManagement permissions that a short-lived Token can use. Please note that only Google Cloud Storage supports this feature. - */ +*/ package downscope import ( @@ -32,9 +32,9 @@ type AvailabilityCondition struct { // A condition expression that specifies the Cloud Storage objects where // permissions are available. For further documentation, see // https://cloud.google.com/iam/docs/conditions-overview - Expression string `json:"expression"` + Expression string `json:"expression"` // Optional. A short string that identifies the purpose of the condition. - Title string `json:"title,omitempty"` + Title string `json:"title,omitempty"` // Optional. Details about the purpose of the condition. Description string `json:"description,omitempty"` } @@ -44,18 +44,18 @@ type AvailabilityCondition struct { type AccessBoundaryRule struct { // AvailableResource is the full resource name of the Cloud Storage bucket that the rule applies to. // Use the format //storage.googleapis.com/projects/_/buckets/bucket-name. - AvailableResource string `json:"availableResource"` + AvailableResource string `json:"availableResource"` // AvailablePermissions is a list that defines the upper bound on the available permissions // for the resource. Each value is the identifier for an IAM predefined role or custom role, // with the prefix inRole:. For example: inRole:roles/storage.objectViewer. // Only the permissions in these roles will be available. - AvailablePermissions []string `json:"availablePermissions"` + AvailablePermissions []string `json:"availablePermissions"` // An optional Condition that restricts the availability of permissions // to specific Cloud Storage objects. // // Use this field if you want to make permissions available for specific objects, // rather than all objects in a Cloud Storage bucket. - Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"` + Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"` } type downscopedTokenResponse struct { @@ -70,7 +70,7 @@ type DownscopingConfig struct { // RootSource is the TokenSource used to create the downscoped token. // The downscoped token therefore has some subset of the accesses of // the original RootSource. - RootSource oauth2.TokenSource + RootSource oauth2.TokenSource // CredentialAccessBoundary defines the accesses held by the new // downscoped Token. CredentialAccessBoundary AccessBoundary diff --git a/google/downscope/downscoping_test.go b/google/downscope/downscoping_test.go index 40eaba6e7..1897a1e13 100644 --- a/google/downscope/downscoping_test.go +++ b/google/downscope/downscoping_test.go @@ -63,7 +63,6 @@ func Example() { availableResource := "//storage.googleapis.com/projects/_/buckets/foo" availablePermissions := []string{"inRole:roles/storage.objectViewer"} - // Initializes an accessBoundary myBoundary := AccessBoundary{make([]AccessBoundaryRule, 0)} @@ -94,4 +93,4 @@ func Example() { // rc, err := obj.NewReader(ctx) // data, err := ioutil.ReadAll(rc) return -} \ No newline at end of file +} From 69736ff94be58a576375f5785e88733b5a584bba Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Fri, 11 Jun 2021 11:40:45 -0700 Subject: [PATCH 06/27] downscope: make changes related to comments, including adding another example and touching up comment formatting. --- google/downscope/downscoping.go | 59 +++++++++-------- google/downscope/downscoping_test.go | 94 +++++++++++++++++----------- 2 files changed, 89 insertions(+), 64 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 4f89e3ecf..dd6f7deb7 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -1,7 +1,11 @@ +// 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 downscope implements the ability to downwcope, or restrict, the +Package downscope implements the ability to downscope, or restrict, the Identity and AccessManagement permissions that a short-lived Token -can use. Please note that only Google Cloud Storage supports this feature. +can use. Please note that only Google Cloud Storage supports this feature. */ package downscope @@ -9,21 +13,18 @@ import ( "context" "encoding/json" "fmt" - "golang.org/x/oauth2" "net/http" "net/url" "time" + + "golang.org/x/oauth2" ) const ( identityBindingEndpoint = "https://sts.googleapis.com/v1beta/token" ) -// Defines an upper bound of permissions available for a GCP credential for one or more resources -type AccessBoundary struct { - // One or more AccessBoundaryRules are required to define permissions - // for the new downscoped token. Each one defines an access (or set of accesses) - // that the new token has to a given resource. +type accessBoundary struct { AccessBoundaryRules []AccessBoundaryRule `json:"accessBoundaryRules"` } @@ -33,9 +34,9 @@ type AvailabilityCondition struct { // permissions are available. For further documentation, see // https://cloud.google.com/iam/docs/conditions-overview Expression string `json:"expression"` - // Optional. A short string that identifies the purpose of the condition. + // Title is short string that identifies the purpose of the condition. Optional. Title string `json:"title,omitempty"` - // Optional. Details about the purpose of the condition. + // Description details about the purpose of the condition. Optional. Description string `json:"description,omitempty"` } @@ -46,12 +47,12 @@ type AccessBoundaryRule struct { // Use the format //storage.googleapis.com/projects/_/buckets/bucket-name. AvailableResource string `json:"availableResource"` // AvailablePermissions is a list that defines the upper bound on the available permissions - // for the resource. Each value is the identifier for an IAM predefined role or custom role, + // for the resource. Each value is the identifier for an IAM predefined role or custom role, // with the prefix inRole:. For example: inRole:roles/storage.objectViewer. // Only the permissions in these roles will be available. AvailablePermissions []string `json:"availablePermissions"` - // An optional Condition that restricts the availability of permissions - // to specific Cloud Storage objects. + // An Condition restricts the availability of permissions + // to specific Cloud Storage objects. Optional. // // Use this field if you want to make permissions available for specific objects, // rather than all objects in a Cloud Storage bucket. @@ -65,15 +66,17 @@ type downscopedTokenResponse struct { ExpiresIn int `json:"expires_in"` } -// Specifies the information necessary to request a downscoped token. +// DownscopingConfigSpecifies the information necessary to request a downscoped token type DownscopingConfig struct { // RootSource is the TokenSource used to create the downscoped token. // The downscoped token therefore has some subset of the accesses of // the original RootSource. RootSource oauth2.TokenSource - // CredentialAccessBoundary defines the accesses held by the new - // downscoped Token. - CredentialAccessBoundary AccessBoundary + // Rules defines the accesses held by the new + // downscoped Token. One or more AccessBoundaryRules are required to + // define permissions for the new downscoped token. Each one defines an + // access (or set of accesses) that the new token has to a given resource. + Rules []AccessBoundaryRule } // downscopedTokenWithEndpoint is a helper function used for unit testing @@ -82,10 +85,10 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, if config.RootSource == nil { return nil, fmt.Errorf("downscope: rootTokenSource cannot be nil") } - if len(config.CredentialAccessBoundary.AccessBoundaryRules) == 0 { + if len(config.Rules) == 0 { return nil, fmt.Errorf("downscope: length of AccessBoundaryRules must be at least 1") } - for _, val := range config.CredentialAccessBoundary.AccessBoundaryRules { + for _, val := range config.Rules { if val.AvailableResource == "" { return nil, fmt.Errorf("downscope: all rules must have a nonempty AvailableResource: %+v", val) } @@ -95,9 +98,11 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, } downscopedOptions := struct { - Boundary AccessBoundary `json:"accessBoundary"` + Boundary accessBoundary `json:"accessBoundary"` }{ - Boundary: config.CredentialAccessBoundary, + Boundary: accessBoundary{ + AccessBoundaryRules: config.Rules, + }, } tok, err := config.RootSource.Token() @@ -107,7 +112,7 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, b, err := json.Marshal(downscopedOptions) if err != nil { - return nil, fmt.Errorf("downscope: Unable to marshall AccessBoundary payload %v", err) + return nil, fmt.Errorf("downscope: unable to marshall AccessBoundary payload %v", err) } form := url.Values{} @@ -127,10 +132,10 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, var tresp downscopedTokenResponse err = json.NewDecoder(resp.Body).Decode(&tresp) if err != nil { - return nil, fmt.Errorf("unable to unmarshal response body: %v", err) + return nil, fmt.Errorf("downscope: unable to unmarshal response body: %v", err) } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unable to exchange token; %v", resp.StatusCode) + return nil, fmt.Errorf("downscope: unable to exchange token; %v", resp.StatusCode) } // an exchanged token that is derived from a service account (2LO) has an expired_in value @@ -153,10 +158,10 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, } // NewTokenSource takes a root TokenSource and returns a downscoped TokenSource -// with a subset of the permissions held by the root source. The +// with a subset of the permissions held by the root source. The // CredentialAccessBoundary in the config defines the permissions held -// by the new TokenSource. Do note that the returned TokenSource is -// an oauth2.StaticTokenSource. If you wish to refresh this token automatically, +// by the new TokenSource. Do note that the returned TokenSource is +// an oauth2.StaticTokenSource. If you wish to refresh this token automatically, // then initialize a locally defined TokenSource struct with the Token held // by the StaticTokenSource and wrap that TokenSource in an oauth2.ReuseTokenSource. func NewTokenSource(ctx context.Context, config DownscopingConfig) (oauth2.TokenSource, error) { diff --git a/google/downscope/downscoping_test.go b/google/downscope/downscoping_test.go index 1897a1e13..95ae5ba7f 100644 --- a/google/downscope/downscoping_test.go +++ b/google/downscope/downscoping_test.go @@ -1,15 +1,18 @@ +// 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 downscope import ( "context" - "fmt" - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" "io/ioutil" "log" "net/http" "net/http/httptest" "testing" + + "golang.org/x/oauth2" ) var ( @@ -17,14 +20,6 @@ var ( standardRespBody = `{"access_token":"Open Sesame","expires_in":432,"issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer"}` ) -func Test_NewAccessBoundary(t *testing.T) { - got := AccessBoundary{make([]AccessBoundaryRule, 0)} - want := AccessBoundary{nil} - if got.AccessBoundaryRules == nil || len(got.AccessBoundaryRules) != 0 { - t.Errorf("NewAccessBoundary() = %v; want %v", got, want) - } -} - func Test_DownscopedTokenSource(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { @@ -44,8 +39,12 @@ func Test_DownscopedTokenSource(t *testing.T) { w.Write([]byte(standardRespBody)) })) - new := AccessBoundary{make([]AccessBoundaryRule, 0)} - new.AccessBoundaryRules = append(new.AccessBoundaryRules, AccessBoundaryRule{"test1", []string{"Perm1, perm2"}, nil}) + new := []AccessBoundaryRule{ + AccessBoundaryRule{ + AvailableResource: "test1", + AvailablePermissions: []string{"Perm1", "Perm2"}, + }, + } myTok := oauth2.Token{AccessToken: "Mellon"} tmpSrc := oauth2.StaticTokenSource(&myTok) out, err := downscopedTokenWithEndpoint(context.Background(), DownscopingConfig{tmpSrc, new}, ts.URL) @@ -58,39 +57,60 @@ func Test_DownscopedTokenSource(t *testing.T) { } } -func Example() { +func ExampleNewTokenSource() { ctx := context.Background() - availableResource := "//storage.googleapis.com/projects/_/buckets/foo" - availablePermissions := []string{"inRole:roles/storage.objectViewer"} + // Initializes an accessBoundary with one Rule + accessBoundary := []AccessBoundaryRule{ + AccessBoundaryRule{ + AvailableResource: "//storage.googleapis.com/projects/_/buckets/foo", + AvailablePermissions: []string{"inRole:roles/storage.objectViewer"}, + }, + } - // Initializes an accessBoundary - myBoundary := AccessBoundary{make([]AccessBoundaryRule, 0)} + var rootSource oauth2.TokenSource + // This Source can be initialized using Application Default Credentials as follows: - // Add a new rule to the AccessBoundary - myBoundary.AccessBoundaryRules = append(myBoundary.AccessBoundaryRules, AccessBoundaryRule{availableResource, availablePermissions, nil}) + // rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform") - // Get the token source for Application Default Credentials (DefaultTokenSource is a shorthand - // for is a shortcut for FindDefaultCredentials(ctx, scope).TokenSource. - // This example assumes that you've defined the GOOGLE_APPLICATION_CREDENTIALS environment variable - rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform") - if err != nil { - log.Fatalf("failed to generate root token source; %v", err) - return - } - myTokenSource, err := NewTokenSource(context.Background(), DownscopingConfig{rootSource, myBoundary}) + myTokenSource, err := NewTokenSource(ctx, DownscopingConfig{RootSource: rootSource, Rules: accessBoundary}) //myTokenSource, err := NewSource(rootSource, myBoundary) if err != nil { log.Fatalf("failed to generate downscoped token source: %v", err) - return } - fmt.Printf("%+v\n", myTokenSource) + _ = myTokenSource // You can now use the token held in myTokenSource to make - // Google Cloud Storage calls. A short example follows. + // Google Cloud Storage calls, as follows: + + // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(myTokenSource)) +} + +type localTokenSource struct { + tokenBrokerURL string + tokenSourceForBroker oauth2.TokenSource +} + +func (lts localTokenSource) Token() (*oauth2.Token, error) { + // Make a call to a remote token broker, which runs downscope.NewTokenSource + // to generate a downscoped version of a token it holds. Return + var tok oauth2.Token + return &tok, nil +} + +// ExampleRefreshableToken provices a sample of how a token consumer would +// construct a refreshable token by wrapping a method that requests a +// downscoped token from a token broker in an oauth2.ReuseTokenSource +func ExampleRefreshableToken() { + var myCredentials oauth2.TokenSource + // This Source contains the credentials that the token consumer uses to + // authenticate itself to the token broker from which it is requesting + // a downscoped token. + myTokenSource := localTokenSource{ + tokenBrokerURL: "www.foo.bar", + tokenSourceForBroker: myCredentials, + } + downscopedToken := oauth2.ReuseTokenSource(nil, myTokenSource) + // downscopedToken can now be used as a refreshable token for Google Cloud Storage calls // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(myTokenSource)) - // bkt := storageClient.Bucket(bucketName) - // obj := bkt.Object(objectName) - // rc, err := obj.NewReader(ctx) - // data, err := ioutil.ReadAll(rc) - return + _ = downscopedToken } From be467eec8b1c6b84a1faf10a119f834d8927400f Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Fri, 11 Jun 2021 11:43:37 -0700 Subject: [PATCH 07/27] downscope: update comment formatting --- google/downscope/downscoping.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index dd6f7deb7..194286c55 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -6,6 +6,7 @@ Package downscope implements the ability to downscope, or restrict, the Identity and AccessManagement permissions that a short-lived Token can use. Please note that only Google Cloud Storage supports this feature. +For complete documentation, see https://cloud.google.com/iam/docs/downscoping-short-lived-credentials */ package downscope @@ -30,7 +31,7 @@ type accessBoundary struct { // An AvailabilityCondition restricts access to a given Resource. type AvailabilityCondition struct { - // A condition expression that specifies the Cloud Storage objects where + // A Expression specifies the Cloud Storage objects where // permissions are available. For further documentation, see // https://cloud.google.com/iam/docs/conditions-overview Expression string `json:"expression"` @@ -40,8 +41,8 @@ type AvailabilityCondition struct { Description string `json:"description,omitempty"` } -// Sets the permissions (and optionally conditions) that the new -// token has on given resource. +// An AccessBoundaryRule Sets the permissions (and optionally conditions) +// that the new token has on given resource. type AccessBoundaryRule struct { // AvailableResource is the full resource name of the Cloud Storage bucket that the rule applies to. // Use the format //storage.googleapis.com/projects/_/buckets/bucket-name. @@ -66,7 +67,7 @@ type downscopedTokenResponse struct { ExpiresIn int `json:"expires_in"` } -// DownscopingConfigSpecifies the information necessary to request a downscoped token +// DownscopingConfig specifies the information necessary to request a downscoped token type DownscopingConfig struct { // RootSource is the TokenSource used to create the downscoped token. // The downscoped token therefore has some subset of the accesses of From c4c64d51bccff3d92333d9233f5f92b886c9b9b4 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Mon, 14 Jun 2021 10:55:04 -0700 Subject: [PATCH 08/27] downscope: add some context to returned error --- google/downscope/downscoping.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 194286c55..660e5454d 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -14,6 +14,7 @@ import ( "context" "encoding/json" "fmt" + "io/ioutil" "net/http" "net/url" "time" @@ -129,15 +130,19 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, return nil, fmt.Errorf("unable to generate POST Request %v", err) } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("downscope: unable to exchange token; %v. Failed to read response body: %v", resp.StatusCode, err) + } + return nil, fmt.Errorf("downscope: unable to exchange token; %v. Server responsed: %v", resp.StatusCode, string(b)) + } var tresp downscopedTokenResponse err = json.NewDecoder(resp.Body).Decode(&tresp) if err != nil { return nil, fmt.Errorf("downscope: unable to unmarshal response body: %v", err) } - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("downscope: unable to exchange token; %v", resp.StatusCode) - } // an exchanged token that is derived from a service account (2LO) has an expired_in value // a token derived from a users token (3LO) does not. From 776a9ed8a346db676f05a4deef8b056eba887dc5 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Mon, 14 Jun 2021 13:51:56 -0700 Subject: [PATCH 09/27] downscope: move example files to a separate file & package --- google/downscope/downscoping_test.go | 61 +------------------------ google/downscope/example_test.go | 67 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 60 deletions(-) create mode 100644 google/downscope/example_test.go diff --git a/google/downscope/downscoping_test.go b/google/downscope/downscoping_test.go index 95ae5ba7f..84e53c3e8 100644 --- a/google/downscope/downscoping_test.go +++ b/google/downscope/downscoping_test.go @@ -7,7 +7,6 @@ package downscope import ( "context" "io/ioutil" - "log" "net/http" "net/http/httptest" "testing" @@ -16,7 +15,7 @@ import ( ) var ( - standardReqBody = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=%257B%2522accessBoundary%2522%253A%257B%2522accessBoundaryRules%2522%253A%255B%257B%2522availableResource%2522%253A%2522test1%2522%252C%2522availablePermissions%2522%253A%255B%2522Perm1%252C%2Bperm2%2522%255D%257D%255D%257D%257D&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&subject_token=Mellon&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token" + standardReqBody = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=%257B%2522accessBoundary%2522%253A%257B%2522accessBoundaryRules%2522%253A%255B%257B%2522availableResource%2522%253A%2522test1%2522%252C%2522availablePermissions%2522%253A%255B%2522Perm1%2522%252C%2522Perm2%2522%255D%257D%255D%257D%257D&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&subject_token=Mellon&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token" standardRespBody = `{"access_token":"Open Sesame","expires_in":432,"issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer"}` ) @@ -56,61 +55,3 @@ func Test_DownscopedTokenSource(t *testing.T) { t.Fatalf("Token() call failed with error %v", err) } } - -func ExampleNewTokenSource() { - ctx := context.Background() - // Initializes an accessBoundary with one Rule - accessBoundary := []AccessBoundaryRule{ - AccessBoundaryRule{ - AvailableResource: "//storage.googleapis.com/projects/_/buckets/foo", - AvailablePermissions: []string{"inRole:roles/storage.objectViewer"}, - }, - } - - var rootSource oauth2.TokenSource - // This Source can be initialized using Application Default Credentials as follows: - - // rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform") - - myTokenSource, err := NewTokenSource(ctx, DownscopingConfig{RootSource: rootSource, Rules: accessBoundary}) - //myTokenSource, err := NewSource(rootSource, myBoundary) - if err != nil { - log.Fatalf("failed to generate downscoped token source: %v", err) - } - _ = myTokenSource - // You can now use the token held in myTokenSource to make - // Google Cloud Storage calls, as follows: - - // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(myTokenSource)) -} - -type localTokenSource struct { - tokenBrokerURL string - tokenSourceForBroker oauth2.TokenSource -} - -func (lts localTokenSource) Token() (*oauth2.Token, error) { - // Make a call to a remote token broker, which runs downscope.NewTokenSource - // to generate a downscoped version of a token it holds. Return - var tok oauth2.Token - return &tok, nil -} - -// ExampleRefreshableToken provices a sample of how a token consumer would -// construct a refreshable token by wrapping a method that requests a -// downscoped token from a token broker in an oauth2.ReuseTokenSource -func ExampleRefreshableToken() { - var myCredentials oauth2.TokenSource - // This Source contains the credentials that the token consumer uses to - // authenticate itself to the token broker from which it is requesting - // a downscoped token. - myTokenSource := localTokenSource{ - tokenBrokerURL: "www.foo.bar", - tokenSourceForBroker: myCredentials, - } - - downscopedToken := oauth2.ReuseTokenSource(nil, myTokenSource) - // downscopedToken can now be used as a refreshable token for Google Cloud Storage calls - // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(myTokenSource)) - _ = downscopedToken -} diff --git a/google/downscope/example_test.go b/google/downscope/example_test.go new file mode 100644 index 000000000..ed2e317cf --- /dev/null +++ b/google/downscope/example_test.go @@ -0,0 +1,67 @@ +package downscope_test + +import ( + "context" + "log" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/google/downscope" +) + +func ExampleNewTokenSource() { + ctx := context.Background() + // Initializes an accessBoundary with one Rule + accessBoundary := []downscope.AccessBoundaryRule{ + downscope.AccessBoundaryRule{ + AvailableResource: "//storage.googleapis.com/projects/_/buckets/foo", + AvailablePermissions: []string{"inRole:roles/storage.objectViewer"}, + }, + } + + var rootSource oauth2.TokenSource + // This Source can be initialized using Application Default Credentials as follows: + + // rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform") + + myTokenSource, err := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary}) + //myTokenSource, err := NewSource(rootSource, myBoundary) + if err != nil { + log.Fatalf("failed to generate downscoped token source: %v", err) + } + _ = myTokenSource + // You can now use the token held in myTokenSource to make + // Google Cloud Storage calls, as follows: + + // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(myTokenSource)) +} + +type localTokenSource struct { + tokenBrokerURL string + tokenSourceForBroker oauth2.TokenSource +} + +func (lts localTokenSource) Token() (*oauth2.Token, error) { + // Make a call to a remote token broker, which runs downscope.NewTokenSource + // to generate a downscoped version of a token it holds. Return + var tok oauth2.Token + return &tok, nil +} + +// ExampleRefreshableToken provices a sample of how a token consumer would +// construct a refreshable token by wrapping a method that requests a +// downscoped token from a token broker in an oauth2.ReuseTokenSource +func ExampleRefreshableToken() { + var myCredentials oauth2.TokenSource + // This Source contains the credentials that the token consumer uses to + // authenticate itself to the token broker from which it is requesting + // a downscoped token. + myTokenSource := localTokenSource{ + tokenBrokerURL: "www.foo.bar", + tokenSourceForBroker: myCredentials, + } + + downscopedToken := oauth2.ReuseTokenSource(nil, myTokenSource) + // downscopedToken can now be used as a refreshable token for Google Cloud Storage calls + // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(myTokenSource)) + _ = downscopedToken +} From b594a6032e9e45a4e69bf725e474853210a6f0a9 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Wed, 16 Jun 2021 12:14:24 -0700 Subject: [PATCH 10/27] downscope: minor tweaks --- google/downscope/downscoping.go | 12 ++++++------ google/downscope/downscoping_test.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 660e5454d..70e0b215c 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -23,7 +23,7 @@ import ( ) const ( - identityBindingEndpoint = "https://sts.googleapis.com/v1beta/token" + identityBindingEndpoint = "https://sts.googleapis.com/v1/token" ) type accessBoundary struct { @@ -122,7 +122,7 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, form.Add("subject_token_type", "urn:ietf:params:oauth:token-type:access_token") form.Add("requested_token_type", "urn:ietf:params:oauth:token-type:access_token") form.Add("subject_token", tok.AccessToken) - form.Add("options", url.QueryEscape(string(b))) + form.Add("options", string(b)) myClient := oauth2.NewClient(ctx, nil) resp, err := myClient.PostForm(endpoint, form) @@ -148,17 +148,17 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, // a token derived from a users token (3LO) does not. // The following code uses the time remaining on rootToken for a user as the value for the // derived token's lifetime - var expiry_time time.Time + var expiryTime time.Time if tresp.ExpiresIn > 0 { - expiry_time = time.Now().Add(time.Duration(time.Duration(tresp.ExpiresIn) * time.Second)) + expiryTime = time.Now().Add(time.Duration(tresp.ExpiresIn) * time.Second) } else { - expiry_time = tok.Expiry + expiryTime = tok.Expiry } newToken := &oauth2.Token{ AccessToken: tresp.AccessToken, TokenType: tresp.TokenType, - Expiry: expiry_time, + Expiry: expiryTime, } return oauth2.StaticTokenSource(newToken), nil } diff --git a/google/downscope/downscoping_test.go b/google/downscope/downscoping_test.go index 84e53c3e8..eb5bbca6d 100644 --- a/google/downscope/downscoping_test.go +++ b/google/downscope/downscoping_test.go @@ -15,7 +15,7 @@ import ( ) var ( - standardReqBody = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=%257B%2522accessBoundary%2522%253A%257B%2522accessBoundaryRules%2522%253A%255B%257B%2522availableResource%2522%253A%2522test1%2522%252C%2522availablePermissions%2522%253A%255B%2522Perm1%2522%252C%2522Perm2%2522%255D%257D%255D%257D%257D&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&subject_token=Mellon&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token" + standardReqBody = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=%7B%22accessBoundary%22%3A%7B%22accessBoundaryRules%22%3A%5B%7B%22availableResource%22%3A%22test1%22%2C%22availablePermissions%22%3A%5B%22Perm1%22%2C%22Perm2%22%5D%7D%5D%7D%7D&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&subject_token=Mellon&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token" standardRespBody = `{"access_token":"Open Sesame","expires_in":432,"issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer"}` ) From cbbc5061280d2fcd107593ac8f98f8431649172c Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 17 Jun 2021 11:06:20 -0700 Subject: [PATCH 11/27] downscope: fixing nits and renaming --- google/downscope/downscoping_test.go | 2 +- google/downscope/example_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/google/downscope/downscoping_test.go b/google/downscope/downscoping_test.go index eb5bbca6d..fe59ce318 100644 --- a/google/downscope/downscoping_test.go +++ b/google/downscope/downscoping_test.go @@ -39,7 +39,7 @@ func Test_DownscopedTokenSource(t *testing.T) { })) new := []AccessBoundaryRule{ - AccessBoundaryRule{ + { AvailableResource: "test1", AvailablePermissions: []string{"Perm1", "Perm2"}, }, diff --git a/google/downscope/example_test.go b/google/downscope/example_test.go index ed2e317cf..411223490 100644 --- a/google/downscope/example_test.go +++ b/google/downscope/example_test.go @@ -41,8 +41,8 @@ type localTokenSource struct { } func (lts localTokenSource) Token() (*oauth2.Token, error) { - // Make a call to a remote token broker, which runs downscope.NewTokenSource - // to generate a downscoped version of a token it holds. Return + // Make a call to a remote token broker, which runs downscope.NewTokenSource() + // to generate a downscoped version of a token it holds. Returns said token. var tok oauth2.Token return &tok, nil } @@ -50,7 +50,7 @@ func (lts localTokenSource) Token() (*oauth2.Token, error) { // ExampleRefreshableToken provices a sample of how a token consumer would // construct a refreshable token by wrapping a method that requests a // downscoped token from a token broker in an oauth2.ReuseTokenSource -func ExampleRefreshableToken() { +func ExampleNewTokenSource_refresh() { var myCredentials oauth2.TokenSource // This Source contains the credentials that the token consumer uses to // authenticate itself to the token broker from which it is requesting From 1d9ea0c8e4ac72a6e037dfa3c7abaee343738393 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 17 Jun 2021 14:58:26 -0700 Subject: [PATCH 12/27] downscope: refactor main functionality into a method on a tokenSource, update examples accordingly, and test for maximum boundary rule violations --- google/downscope/downscoping.go | 33 ++++++++++++++++-------- google/downscope/example_test.go | 43 +++----------------------------- 2 files changed, 26 insertions(+), 50 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 70e0b215c..65f609846 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -78,18 +78,31 @@ type DownscopingConfig struct { // downscoped Token. One or more AccessBoundaryRules are required to // define permissions for the new downscoped token. Each one defines an // access (or set of accesses) that the new token has to a given resource. + // There can be a maximum of 10 AccessBoundaryRules. Rules []AccessBoundaryRule } +// A DownscopingTokenSource is used to retrieve a downscoped token with restricted +// permissions compared to the root Token that is used to generate it. +type DownscopingTokenSource struct { + // Ctx is the context used to query the API to retrieve a downscoped Token. + Ctx context.Context + // Config holds the information necessary to generate a downscoped Token. + Config DownscopingConfig +} + // downscopedTokenWithEndpoint is a helper function used for unit testing // purposes, as it allows us to pass in a locally mocked endpoint. -func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, endpoint string) (oauth2.TokenSource, error) { +func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, endpoint string) (*oauth2.Token, error) { if config.RootSource == nil { return nil, fmt.Errorf("downscope: rootTokenSource cannot be nil") } if len(config.Rules) == 0 { return nil, fmt.Errorf("downscope: length of AccessBoundaryRules must be at least 1") } + if len(config.Rules) > 10 { + return nil, fmt.Errorf("downscope: length of AccessBoundaryRules may not be greater than 10") + } for _, val := range config.Rules { if val.AvailableResource == "" { return nil, fmt.Errorf("downscope: all rules must have a nonempty AvailableResource: %+v", val) @@ -160,16 +173,14 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, TokenType: tresp.TokenType, Expiry: expiryTime, } - return oauth2.StaticTokenSource(newToken), nil + return newToken, nil } -// NewTokenSource takes a root TokenSource and returns a downscoped TokenSource -// with a subset of the permissions held by the root source. The -// CredentialAccessBoundary in the config defines the permissions held -// by the new TokenSource. Do note that the returned TokenSource is -// an oauth2.StaticTokenSource. If you wish to refresh this token automatically, -// then initialize a locally defined TokenSource struct with the Token held -// by the StaticTokenSource and wrap that TokenSource in an oauth2.ReuseTokenSource. -func NewTokenSource(ctx context.Context, config DownscopingConfig) (oauth2.TokenSource, error) { - return downscopedTokenWithEndpoint(ctx, config, identityBindingEndpoint) +// Token() uses a DownscopingTokenSource to generate an oauth2 Token. +// Do note that the returned TokenSource is an oauth2.StaticTokenSource. If you wish +// to refresh this token automatically, then initialize a locally defined +// TokenSource struct with the Token held by the StaticTokenSource and wrap +// that TokenSource in an oauth2.ReuseTokenSource. +func (dts DownscopingTokenSource) Token() (*oauth2.Token, error) { + return downscopedTokenWithEndpoint(dts.Ctx, dts.Config, identityBindingEndpoint) } diff --git a/google/downscope/example_test.go b/google/downscope/example_test.go index 411223490..35d3d83b9 100644 --- a/google/downscope/example_test.go +++ b/google/downscope/example_test.go @@ -2,7 +2,6 @@ package downscope_test import ( "context" - "log" "golang.org/x/oauth2" "golang.org/x/oauth2/google/downscope" @@ -19,49 +18,15 @@ func ExampleNewTokenSource() { } var rootSource oauth2.TokenSource - // This Source can be initialized using Application Default Credentials as follows: + // This Source can be initialized in multiple ways; the following example uses + // Application Default Credentials. // rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform") - myTokenSource, err := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary}) - //myTokenSource, err := NewSource(rootSource, myBoundary) - if err != nil { - log.Fatalf("failed to generate downscoped token source: %v", err) - } - _ = myTokenSource + dts := downscope.DownscopingTokenSource{ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary}} + _ = dts // You can now use the token held in myTokenSource to make // Google Cloud Storage calls, as follows: // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(myTokenSource)) } - -type localTokenSource struct { - tokenBrokerURL string - tokenSourceForBroker oauth2.TokenSource -} - -func (lts localTokenSource) Token() (*oauth2.Token, error) { - // Make a call to a remote token broker, which runs downscope.NewTokenSource() - // to generate a downscoped version of a token it holds. Returns said token. - var tok oauth2.Token - return &tok, nil -} - -// ExampleRefreshableToken provices a sample of how a token consumer would -// construct a refreshable token by wrapping a method that requests a -// downscoped token from a token broker in an oauth2.ReuseTokenSource -func ExampleNewTokenSource_refresh() { - var myCredentials oauth2.TokenSource - // This Source contains the credentials that the token consumer uses to - // authenticate itself to the token broker from which it is requesting - // a downscoped token. - myTokenSource := localTokenSource{ - tokenBrokerURL: "www.foo.bar", - tokenSourceForBroker: myCredentials, - } - - downscopedToken := oauth2.ReuseTokenSource(nil, myTokenSource) - // downscopedToken can now be used as a refreshable token for Google Cloud Storage calls - // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(myTokenSource)) - _ = downscopedToken -} From a362f28044bbe386c475b1ada2347d837af66397 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 17 Jun 2021 15:11:54 -0700 Subject: [PATCH 13/27] downscope: fix grammar and punctuation. --- google/downscope/downscoping.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 65f609846..87ea3efdb 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -32,7 +32,7 @@ type accessBoundary struct { // An AvailabilityCondition restricts access to a given Resource. type AvailabilityCondition struct { - // A Expression specifies the Cloud Storage objects where + // An Expression specifies the Cloud Storage objects where // permissions are available. For further documentation, see // https://cloud.google.com/iam/docs/conditions-overview Expression string `json:"expression"` @@ -68,7 +68,7 @@ type downscopedTokenResponse struct { ExpiresIn int `json:"expires_in"` } -// DownscopingConfig specifies the information necessary to request a downscoped token +// DownscopingConfig specifies the information necessary to request a downscoped token. type DownscopingConfig struct { // RootSource is the TokenSource used to create the downscoped token. // The downscoped token therefore has some subset of the accesses of From 304d28ba9e71ed200d9e9051fe3b1f8ef696bd19 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Tue, 22 Jun 2021 09:25:17 -0700 Subject: [PATCH 14/27] downscope: further updates and nits --- google/downscope/downscoping.go | 25 +++++++++++++++---------- google/downscope/downscoping_test.go | 6 +----- google/downscope/example_test.go | 10 +++++++--- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 87ea3efdb..19954287b 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -56,7 +56,7 @@ type AccessBoundaryRule struct { // An Condition restricts the availability of permissions // to specific Cloud Storage objects. Optional. // - // Use this field if you want to make permissions available for specific objects, + // A Condition can be used to make permissions available for specific objects, // rather than all objects in a Cloud Storage bucket. Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"` } @@ -82,13 +82,18 @@ type DownscopingConfig struct { Rules []AccessBoundaryRule } -// A DownscopingTokenSource is used to retrieve a downscoped token with restricted +// A downscopingTokenSource is used to retrieve a downscoped token with restricted // permissions compared to the root Token that is used to generate it. -type DownscopingTokenSource struct { - // Ctx is the context used to query the API to retrieve a downscoped Token. - Ctx context.Context - // Config holds the information necessary to generate a downscoped Token. - Config DownscopingConfig +type downscopingTokenSource struct { + // ctx is the context used to query the API to retrieve a downscoped Token. + ctx context.Context + // config holds the information necessary to generate a downscoped Token. + config DownscopingConfig +} + +// NewTokenSource returns an empty downscopingTokenSource. +func NewTokenSource(ctx context.Context, conf DownscopingConfig) downscopingTokenSource { + return downscopingTokenSource{ctx: ctx, config: conf} } // downscopedTokenWithEndpoint is a helper function used for unit testing @@ -176,11 +181,11 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, return newToken, nil } -// Token() uses a DownscopingTokenSource to generate an oauth2 Token. +// Token() uses a downscopingTokenSource to generate an oauth2 Token. // Do note that the returned TokenSource is an oauth2.StaticTokenSource. If you wish // to refresh this token automatically, then initialize a locally defined // TokenSource struct with the Token held by the StaticTokenSource and wrap // that TokenSource in an oauth2.ReuseTokenSource. -func (dts DownscopingTokenSource) Token() (*oauth2.Token, error) { - return downscopedTokenWithEndpoint(dts.Ctx, dts.Config, identityBindingEndpoint) +func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { + return downscopedTokenWithEndpoint(dts.ctx, dts.config, identityBindingEndpoint) } diff --git a/google/downscope/downscoping_test.go b/google/downscope/downscoping_test.go index fe59ce318..e79edc8fd 100644 --- a/google/downscope/downscoping_test.go +++ b/google/downscope/downscoping_test.go @@ -46,12 +46,8 @@ func Test_DownscopedTokenSource(t *testing.T) { } myTok := oauth2.Token{AccessToken: "Mellon"} tmpSrc := oauth2.StaticTokenSource(&myTok) - out, err := downscopedTokenWithEndpoint(context.Background(), DownscopingConfig{tmpSrc, new}, ts.URL) + _, err := downscopedTokenWithEndpoint(context.Background(), DownscopingConfig{tmpSrc, new}, ts.URL) if err != nil { t.Fatalf("NewDownscopedTokenSource failed with error: %v", err) } - _, err = out.Token() - if err != nil { - t.Fatalf("Token() call failed with error %v", err) - } } diff --git a/google/downscope/example_test.go b/google/downscope/example_test.go index 35d3d83b9..cffd4ff02 100644 --- a/google/downscope/example_test.go +++ b/google/downscope/example_test.go @@ -1,3 +1,7 @@ +// 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 downscope_test import ( @@ -9,9 +13,9 @@ import ( func ExampleNewTokenSource() { ctx := context.Background() - // Initializes an accessBoundary with one Rule + // Initializes an accessBoundary with one Rule. accessBoundary := []downscope.AccessBoundaryRule{ - downscope.AccessBoundaryRule{ + { AvailableResource: "//storage.googleapis.com/projects/_/buckets/foo", AvailablePermissions: []string{"inRole:roles/storage.objectViewer"}, }, @@ -23,7 +27,7 @@ func ExampleNewTokenSource() { // rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform") - dts := downscope.DownscopingTokenSource{ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary}} + dts := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary}) _ = dts // You can now use the token held in myTokenSource to make // Google Cloud Storage calls, as follows: From 1024258a24930fd94e79808dcbaabad443b5c0cb Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Tue, 22 Jun 2021 13:14:47 -0700 Subject: [PATCH 15/27] downscope: refactor some code to remove an extraneous function and instead run that code inside of Token() --- google/downscope/downscoping.go | 36 ++++++++++++---------------- google/downscope/downscoping_test.go | 4 +++- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 19954287b..c1a6a8ffb 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -22,7 +22,7 @@ import ( "golang.org/x/oauth2" ) -const ( +var ( identityBindingEndpoint = "https://sts.googleapis.com/v1/token" ) @@ -96,19 +96,22 @@ func NewTokenSource(ctx context.Context, conf DownscopingConfig) downscopingToke return downscopingTokenSource{ctx: ctx, config: conf} } -// downscopedTokenWithEndpoint is a helper function used for unit testing -// purposes, as it allows us to pass in a locally mocked endpoint. -func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, endpoint string) (*oauth2.Token, error) { - if config.RootSource == nil { +// Token() uses a downscopingTokenSource to generate an oauth2 Token. +// Do note that the returned TokenSource is an oauth2.StaticTokenSource. If you wish +// to refresh this token automatically, then initialize a locally defined +// TokenSource struct with the Token held by the StaticTokenSource and wrap +// that TokenSource in an oauth2.ReuseTokenSource. +func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { + if dts.config.RootSource == nil { return nil, fmt.Errorf("downscope: rootTokenSource cannot be nil") } - if len(config.Rules) == 0 { + if len(dts.config.Rules) == 0 { return nil, fmt.Errorf("downscope: length of AccessBoundaryRules must be at least 1") } - if len(config.Rules) > 10 { + if len(dts.config.Rules) > 10 { return nil, fmt.Errorf("downscope: length of AccessBoundaryRules may not be greater than 10") } - for _, val := range config.Rules { + for _, val := range dts.config.Rules { if val.AvailableResource == "" { return nil, fmt.Errorf("downscope: all rules must have a nonempty AvailableResource: %+v", val) } @@ -121,11 +124,11 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, Boundary accessBoundary `json:"accessBoundary"` }{ Boundary: accessBoundary{ - AccessBoundaryRules: config.Rules, + AccessBoundaryRules: dts.config.Rules, }, } - tok, err := config.RootSource.Token() + tok, err := dts.config.RootSource.Token() if err != nil { return nil, fmt.Errorf("downscope: unable to obtain root token: %v", err) } @@ -142,8 +145,8 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, form.Add("subject_token", tok.AccessToken) form.Add("options", string(b)) - myClient := oauth2.NewClient(ctx, nil) - resp, err := myClient.PostForm(endpoint, form) + myClient := oauth2.NewClient(dts.ctx, nil) + resp, err := myClient.PostForm(identityBindingEndpoint, form) if err != nil { return nil, fmt.Errorf("unable to generate POST Request %v", err) } @@ -180,12 +183,3 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, } return newToken, nil } - -// Token() uses a downscopingTokenSource to generate an oauth2 Token. -// Do note that the returned TokenSource is an oauth2.StaticTokenSource. If you wish -// to refresh this token automatically, then initialize a locally defined -// TokenSource struct with the Token held by the StaticTokenSource and wrap -// that TokenSource in an oauth2.ReuseTokenSource. -func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { - return downscopedTokenWithEndpoint(dts.ctx, dts.config, identityBindingEndpoint) -} diff --git a/google/downscope/downscoping_test.go b/google/downscope/downscoping_test.go index e79edc8fd..d5adda19c 100644 --- a/google/downscope/downscoping_test.go +++ b/google/downscope/downscoping_test.go @@ -46,7 +46,9 @@ func Test_DownscopedTokenSource(t *testing.T) { } myTok := oauth2.Token{AccessToken: "Mellon"} tmpSrc := oauth2.StaticTokenSource(&myTok) - _, err := downscopedTokenWithEndpoint(context.Background(), DownscopingConfig{tmpSrc, new}, ts.URL) + dts := downscopingTokenSource{context.Background(), DownscopingConfig{tmpSrc, new}} + identityBindingEndpoint = ts.URL + _, err := dts.Token() if err != nil { t.Fatalf("NewDownscopedTokenSource failed with error: %v", err) } From 1888dba9b13085cf11e11fe4e41ca1bfc131995d Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Wed, 23 Jun 2021 10:17:59 -0700 Subject: [PATCH 16/27] downscope: change return type of NewTokenSource --- google/downscope/downscoping.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index c1a6a8ffb..52d21d220 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -92,7 +92,7 @@ type downscopingTokenSource struct { } // NewTokenSource returns an empty downscopingTokenSource. -func NewTokenSource(ctx context.Context, conf DownscopingConfig) downscopingTokenSource { +func NewTokenSource(ctx context.Context, conf DownscopingConfig) oauth2.TokenSource { return downscopingTokenSource{ctx: ctx, config: conf} } From fec7137f21a3d93d4bac9f9be553bb5200a28e79 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 24 Jun 2021 11:42:43 -0700 Subject: [PATCH 17/27] downscope: fix some nits --- google/downscope/downscoping.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 52d21d220..48ae8f279 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -103,7 +103,7 @@ func NewTokenSource(ctx context.Context, conf DownscopingConfig) oauth2.TokenSou // that TokenSource in an oauth2.ReuseTokenSource. func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { if dts.config.RootSource == nil { - return nil, fmt.Errorf("downscope: rootTokenSource cannot be nil") + return nil, fmt.Errorf("downscope: rootSource cannot be nil") } if len(dts.config.Rules) == 0 { return nil, fmt.Errorf("downscope: length of AccessBoundaryRules must be at least 1") @@ -135,7 +135,7 @@ func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { b, err := json.Marshal(downscopedOptions) if err != nil { - return nil, fmt.Errorf("downscope: unable to marshall AccessBoundary payload %v", err) + return nil, fmt.Errorf("downscope: unable to marshal AccessBoundary payload %v", err) } form := url.Values{} @@ -151,6 +151,10 @@ func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { return nil, fmt.Errorf("unable to generate POST Request %v", err) } defer resp.Body.Close() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("downscope: unable to read reaponse body: %v", err) + } if resp.StatusCode != http.StatusOK { b, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -160,7 +164,8 @@ func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { } var tresp downscopedTokenResponse - err = json.NewDecoder(resp.Body).Decode(&tresp) + + err = json.Unmarshal(respBody, &tresp) if err != nil { return nil, fmt.Errorf("downscope: unable to unmarshal response body: %v", err) } From 941cf10a8ebe14d2b03bf7253731134629fc7f80 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 24 Jun 2021 16:24:14 -0700 Subject: [PATCH 18/27] downscope: move validation checks --- google/downscope/downscoping.go | 28 ++++++++++++++-------------- google/downscope/example_test.go | 6 ++++-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 48ae8f279..2d74c3749 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -92,26 +92,17 @@ type downscopingTokenSource struct { } // NewTokenSource returns an empty downscopingTokenSource. -func NewTokenSource(ctx context.Context, conf DownscopingConfig) oauth2.TokenSource { - return downscopingTokenSource{ctx: ctx, config: conf} -} - -// Token() uses a downscopingTokenSource to generate an oauth2 Token. -// Do note that the returned TokenSource is an oauth2.StaticTokenSource. If you wish -// to refresh this token automatically, then initialize a locally defined -// TokenSource struct with the Token held by the StaticTokenSource and wrap -// that TokenSource in an oauth2.ReuseTokenSource. -func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { - if dts.config.RootSource == nil { +func NewTokenSource(ctx context.Context, conf DownscopingConfig) (oauth2.TokenSource, error) { + if conf.RootSource == nil { return nil, fmt.Errorf("downscope: rootSource cannot be nil") } - if len(dts.config.Rules) == 0 { + if len(conf.Rules) == 0 { return nil, fmt.Errorf("downscope: length of AccessBoundaryRules must be at least 1") } - if len(dts.config.Rules) > 10 { + if len(conf.Rules) > 10 { return nil, fmt.Errorf("downscope: length of AccessBoundaryRules may not be greater than 10") } - for _, val := range dts.config.Rules { + for _, val := range conf.Rules { if val.AvailableResource == "" { return nil, fmt.Errorf("downscope: all rules must have a nonempty AvailableResource: %+v", val) } @@ -119,6 +110,15 @@ func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { return nil, fmt.Errorf("downscope: all rules must provide at least one permission: %+v", val) } } + return downscopingTokenSource{ctx: ctx, config: conf}, nil +} + +// Token() uses a downscopingTokenSource to generate an oauth2 Token. +// Do note that the returned TokenSource is an oauth2.StaticTokenSource. If you wish +// to refresh this token automatically, then initialize a locally defined +// TokenSource struct with the Token held by the StaticTokenSource and wrap +// that TokenSource in an oauth2.ReuseTokenSource. +func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { downscopedOptions := struct { Boundary accessBoundary `json:"accessBoundary"` diff --git a/google/downscope/example_test.go b/google/downscope/example_test.go index cffd4ff02..061cf5784 100644 --- a/google/downscope/example_test.go +++ b/google/downscope/example_test.go @@ -27,8 +27,10 @@ func ExampleNewTokenSource() { // rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform") - dts := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary}) - _ = dts + dts, err := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary}) + if err != nil { + _ = dts + } // You can now use the token held in myTokenSource to make // Google Cloud Storage calls, as follows: From c9764790d5acaff59f2035fe87db6f85387f7116 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 29 Jul 2021 13:38:18 -0700 Subject: [PATCH 19/27] downscope: update documentation --- google/downscope/downscoping.go | 28 ++++++++++++++++++++++++++-- google/downscope/example_test.go | 11 ++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 2d74c3749..97ab80173 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -4,9 +4,33 @@ /* Package downscope implements the ability to downscope, or restrict, the -Identity and AccessManagement permissions that a short-lived Token +Identity and Access Management permissions that a short-lived Token can use. Please note that only Google Cloud Storage supports this feature. For complete documentation, see https://cloud.google.com/iam/docs/downscoping-short-lived-credentials + +To downscope permissions of a source credential, you need to define +a Credential Access Boundary. Said Boundary specifies which resources +the newly created credential can access, an upper bound on the permissions +it has over those resources, and optionally attribute-base conditional +access to the aforementioned resources. For more information on IAM +Credentials, see https://cloud.google.com/iam/docs/conditions-overview + +This functionality would typically be used to provide a third party with +limited access to and permissions on resources held by the owner of the root +credential or internally in conjunction with the principle of least privilege +to ensure that internal services only hold the minimum necessary privileges +for their function. + +For example, a token broker can be set up on a server in a private network. +Various workloads (token consumers) in the same network will send authenticated +requests to that broker for downscoped tokens to access or modify specific google +cloud storage buckets. See the NewTokenSource example for an example of how a +token broker would use this package. + +The broker will use the functionality in this package to generate a downscoped +token with the requested configuration, and then pass it back to the token +consumer. These downscoped access tokens can then be used to access Google +Storage resources. */ package downscope @@ -91,7 +115,7 @@ type downscopingTokenSource struct { config DownscopingConfig } -// NewTokenSource returns an empty downscopingTokenSource. +// NewTokenSource returns an configured downscopingTokenSource. func NewTokenSource(ctx context.Context, conf DownscopingConfig) (oauth2.TokenSource, error) { if conf.RootSource == nil { return nil, fmt.Errorf("downscope: rootSource cannot be nil") diff --git a/google/downscope/example_test.go b/google/downscope/example_test.go index 061cf5784..93972832d 100644 --- a/google/downscope/example_test.go +++ b/google/downscope/example_test.go @@ -6,11 +6,16 @@ package downscope_test import ( "context" + "fmt" "golang.org/x/oauth2" "golang.org/x/oauth2/google/downscope" ) +func Example() { + +} + func ExampleNewTokenSource() { ctx := context.Background() // Initializes an accessBoundary with one Rule. @@ -29,8 +34,12 @@ func ExampleNewTokenSource() { dts, err := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary}) if err != nil { - _ = dts + fmt.Printf("failed to generate downscoped token source: %v", err) } + + // Enables automatic token refreshing + _ := oauth2.ReuseTokenSource(nil, dts) + // You can now use the token held in myTokenSource to make // Google Cloud Storage calls, as follows: From e4ec8cdba17dcb0556c47de22c3b211350d8b9a1 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 29 Jul 2021 14:44:16 -0700 Subject: [PATCH 20/27] Removed some code that's not yet finished --- google/downscope/example_test.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/google/downscope/example_test.go b/google/downscope/example_test.go index 93972832d..475efc2b2 100644 --- a/google/downscope/example_test.go +++ b/google/downscope/example_test.go @@ -12,10 +12,6 @@ import ( "golang.org/x/oauth2/google/downscope" ) -func Example() { - -} - func ExampleNewTokenSource() { ctx := context.Background() // Initializes an accessBoundary with one Rule. @@ -38,7 +34,7 @@ func ExampleNewTokenSource() { } // Enables automatic token refreshing - _ := oauth2.ReuseTokenSource(nil, dts) + _ = oauth2.ReuseTokenSource(nil, dts) // You can now use the token held in myTokenSource to make // Google Cloud Storage calls, as follows: From 0bd54f59194d1f75f6fae18392acfda6c0edeb30 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Sat, 31 Jul 2021 00:29:48 -0700 Subject: [PATCH 21/27] downscope: documentation tweaks --- google/downscope/downscoping.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 97ab80173..5d9ad5302 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -11,9 +11,9 @@ For complete documentation, see https://cloud.google.com/iam/docs/downscoping-sh To downscope permissions of a source credential, you need to define a Credential Access Boundary. Said Boundary specifies which resources the newly created credential can access, an upper bound on the permissions -it has over those resources, and optionally attribute-base conditional +it has over those resources, and optionally attribute-based conditional access to the aforementioned resources. For more information on IAM -Credentials, see https://cloud.google.com/iam/docs/conditions-overview +Conditions, see https://cloud.google.com/iam/docs/conditions-overview. This functionality would typically be used to provide a third party with limited access to and permissions on resources held by the owner of the root @@ -30,7 +30,8 @@ token broker would use this package. The broker will use the functionality in this package to generate a downscoped token with the requested configuration, and then pass it back to the token consumer. These downscoped access tokens can then be used to access Google -Storage resources. +Storage resources. For instance, you can create a NewClient from the +"cloud.google.com/go/storage" package and pass in option.WithTokenSource(yourTokenSource)) */ package downscope @@ -115,7 +116,7 @@ type downscopingTokenSource struct { config DownscopingConfig } -// NewTokenSource returns an configured downscopingTokenSource. +// NewTokenSource returns a configured downscopingTokenSource. func NewTokenSource(ctx context.Context, conf DownscopingConfig) (oauth2.TokenSource, error) { if conf.RootSource == nil { return nil, fmt.Errorf("downscope: rootSource cannot be nil") From e1c4f0152871c4f916746c61ba18b091b8ee377e Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Mon, 2 Aug 2021 11:39:22 -0700 Subject: [PATCH 22/27] downscope: add new examples and update existing ones. --- .../{example_test.go => tokenbroker_test.go} | 21 +++++++-- google/downscope/tokenconsumer_test.go | 44 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) rename google/downscope/{example_test.go => tokenbroker_test.go} (64%) create mode 100644 google/downscope/tokenconsumer_test.go diff --git a/google/downscope/example_test.go b/google/downscope/tokenbroker_test.go similarity index 64% rename from google/downscope/example_test.go rename to google/downscope/tokenbroker_test.go index 475efc2b2..ebe03d8af 100644 --- a/google/downscope/example_test.go +++ b/google/downscope/tokenbroker_test.go @@ -7,12 +7,17 @@ package downscope_test import ( "context" "fmt" + "golang.org/x/oauth2/google" "golang.org/x/oauth2" "golang.org/x/oauth2/google/downscope" ) + + func ExampleNewTokenSource() { + // This shows how to generate a downscoped token. This code would be run on the + // token broker, which holds the root token used to generate the downscoped token. ctx := context.Background() // Initializes an accessBoundary with one Rule. accessBoundary := []downscope.AccessBoundaryRule{ @@ -26,18 +31,26 @@ func ExampleNewTokenSource() { // This Source can be initialized in multiple ways; the following example uses // Application Default Credentials. - // rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform") + rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform") dts, err := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary}) if err != nil { fmt.Printf("failed to generate downscoped token source: %v", err) + return } - // Enables automatic token refreshing - _ = oauth2.ReuseTokenSource(nil, dts) + tok, err := dts.Token() + if err != nil { + fmt.Printf("failed to generate token: %v", err) + return + } + _ = tok + // You can now pass tok to a token consumer however you wish, such as exposing + // a REST API and sending it over HTTP. - // You can now use the token held in myTokenSource to make + // You can instead use the token held in myTokenSource to make // Google Cloud Storage calls, as follows: // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(myTokenSource)) + } diff --git a/google/downscope/tokenconsumer_test.go b/google/downscope/tokenconsumer_test.go new file mode 100644 index 000000000..913b5b589 --- /dev/null +++ b/google/downscope/tokenconsumer_test.go @@ -0,0 +1,44 @@ +package downscope_test + +import ( + "golang.org/x/oauth2" +) + +type localTokenSource struct { + requestedPerms []string + requestedObject string + brokerURL string +} + +func (localTokenSource) Token() (*oauth2.Token, error){ + var remoteToken oauth2.Token + // retrieve remoteToken, an oauth2.Token, from token broker + return &remoteToken, nil +} + + +func Example() { + // A token consumer should define their own tokenSource. In the Token() method, + // it should send a query to a token broker requesting a downscoped token. + // The token broker holds the root credential that is used to generate the + // downscoped token. + + thisTokenSource := localTokenSource{ + requestedPerms: []string{"inRole:roles/storage.objectViewer"}, + requestedObject: "//storage.googleapis.com/projects/_/buckets/foo", + brokerURL: "yourURL.com/internal/broker", + } + + // Wrap the TokenSource in an oauth2.ReuseTokenSource to enable automatic refreshing + refreshableTS := oauth2.ReuseTokenSource(nil, thisTokenSource) + + + // You can now use the token source to access Google Cloud Storage resources as follows. + + // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(refreshableTS)) + // bkt := storageClient.Bucket(bucketName) + // obj := bkt.Object(objectName) + // rc, err := obj.NewReader(ctx) + // defer rc.Close() + // data, err := ioutil.ReadAll(rc) +} \ No newline at end of file From db8a13916cf1e5a92d8889b871d9ee57431bea55 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Wed, 4 Aug 2021 14:38:56 -0700 Subject: [PATCH 23/27] downscope: update examples --- google/downscope/downscoping.go | 10 +++++----- google/downscope/tokenbroker_test.go | 9 ++++----- google/downscope/tokenconsumer_test.go | 16 ++++++---------- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 5d9ad5302..5ef211ccf 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -24,13 +24,13 @@ for their function. For example, a token broker can be set up on a server in a private network. Various workloads (token consumers) in the same network will send authenticated requests to that broker for downscoped tokens to access or modify specific google -cloud storage buckets. See the NewTokenSource example for an example of how a +cloud storage buckets. See the NewTokenSource example for an example of how a token broker would use this package. The broker will use the functionality in this package to generate a downscoped token with the requested configuration, and then pass it back to the token -consumer. These downscoped access tokens can then be used to access Google -Storage resources. For instance, you can create a NewClient from the +consumer. These downscoped access tokens can then be used to access Google +Storage resources. For instance, you can create a NewClient from the "cloud.google.com/go/storage" package and pass in option.WithTokenSource(yourTokenSource)) */ package downscope @@ -81,7 +81,7 @@ type AccessBoundaryRule struct { // An Condition restricts the availability of permissions // to specific Cloud Storage objects. Optional. // - // A Condition can be used to make permissions available for specific objects, + // A Condition can be used to make permissions available for specific objects, // rather than all objects in a Cloud Storage bucket. Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"` } @@ -183,7 +183,7 @@ func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { if resp.StatusCode != http.StatusOK { b, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("downscope: unable to exchange token; %v. Failed to read response body: %v", resp.StatusCode, err) + return nil, fmt.Errorf("downscope: unable to exchange token; %v. Failed to read response body: %v", resp.StatusCode, err) } return nil, fmt.Errorf("downscope: unable to exchange token; %v. Server responsed: %v", resp.StatusCode, string(b)) } diff --git a/google/downscope/tokenbroker_test.go b/google/downscope/tokenbroker_test.go index ebe03d8af..4ba370b08 100644 --- a/google/downscope/tokenbroker_test.go +++ b/google/downscope/tokenbroker_test.go @@ -7,16 +7,15 @@ package downscope_test import ( "context" "fmt" + "golang.org/x/oauth2/google" "golang.org/x/oauth2" "golang.org/x/oauth2/google/downscope" ) - - func ExampleNewTokenSource() { - // This shows how to generate a downscoped token. This code would be run on the + // This shows how to generate a downscoped token. This code would be run on the // token broker, which holds the root token used to generate the downscoped token. ctx := context.Background() // Initializes an accessBoundary with one Rule. @@ -48,9 +47,9 @@ func ExampleNewTokenSource() { // You can now pass tok to a token consumer however you wish, such as exposing // a REST API and sending it over HTTP. - // You can instead use the token held in myTokenSource to make + // You can instead use the token held in dts to make // Google Cloud Storage calls, as follows: - // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(myTokenSource)) + // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(dts)) } diff --git a/google/downscope/tokenconsumer_test.go b/google/downscope/tokenconsumer_test.go index 913b5b589..998e9e005 100644 --- a/google/downscope/tokenconsumer_test.go +++ b/google/downscope/tokenconsumer_test.go @@ -5,40 +5,36 @@ import ( ) type localTokenSource struct { - requestedPerms []string requestedObject string - brokerURL string + brokerURL string } -func (localTokenSource) Token() (*oauth2.Token, error){ +func (localTokenSource) Token() (*oauth2.Token, error) { var remoteToken oauth2.Token // retrieve remoteToken, an oauth2.Token, from token broker return &remoteToken, nil } - func Example() { - // A token consumer should define their own tokenSource. In the Token() method, + // A token consumer should define their own tokenSource. In the Token() method, // it should send a query to a token broker requesting a downscoped token. // The token broker holds the root credential that is used to generate the // downscoped token. thisTokenSource := localTokenSource{ - requestedPerms: []string{"inRole:roles/storage.objectViewer"}, requestedObject: "//storage.googleapis.com/projects/_/buckets/foo", - brokerURL: "yourURL.com/internal/broker", + brokerURL: "yourURL.com/internal/broker", } // Wrap the TokenSource in an oauth2.ReuseTokenSource to enable automatic refreshing refreshableTS := oauth2.ReuseTokenSource(nil, thisTokenSource) - // You can now use the token source to access Google Cloud Storage resources as follows. // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(refreshableTS)) - // bkt := storageClient.Bucket(bucketName) + // bkt := storageClient.Bucket("foo"") // obj := bkt.Object(objectName) // rc, err := obj.NewReader(ctx) // defer rc.Close() // data, err := ioutil.ReadAll(rc) -} \ No newline at end of file +} From 63894e56810431f8a45d381f4ffb123da1a1b8e0 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Wed, 4 Aug 2021 14:43:05 -0700 Subject: [PATCH 24/27] Update example_test.go --- google/downscope/example_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/google/downscope/example_test.go b/google/downscope/example_test.go index 475efc2b2..138b9f470 100644 --- a/google/downscope/example_test.go +++ b/google/downscope/example_test.go @@ -31,6 +31,7 @@ func ExampleNewTokenSource() { dts, err := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary}) if err != nil { fmt.Printf("failed to generate downscoped token source: %v", err) + return } // Enables automatic token refreshing From b74b0944f513b47371999cea7bb0ebf762df300d Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 5 Aug 2021 10:27:23 -0700 Subject: [PATCH 25/27] more nits --- google/downscope/downscoping.go | 10 +++++----- google/downscope/tokenbroker_test.go | 4 +++- google/downscope/tokenconsumer_test.go | 6 +++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 5ef211ccf..ac5c00da1 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -9,13 +9,13 @@ can use. Please note that only Google Cloud Storage supports this feature. For complete documentation, see https://cloud.google.com/iam/docs/downscoping-short-lived-credentials To downscope permissions of a source credential, you need to define -a Credential Access Boundary. Said Boundary specifies which resources +a Credential Access Boundary. Said Boundary specifies which resources the newly created credential can access, an upper bound on the permissions -it has over those resources, and optionally attribute-based conditional -access to the aforementioned resources. For more information on IAM +it has over those resources, and optionally attribute-based conditional +access to the aforementioned resources. For more information on IAM Conditions, see https://cloud.google.com/iam/docs/conditions-overview. -This functionality would typically be used to provide a third party with +This functionality can be used to provide a third party with limited access to and permissions on resources held by the owner of the root credential or internally in conjunction with the principle of least privilege to ensure that internal services only hold the minimum necessary privileges @@ -185,7 +185,7 @@ func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { if err != nil { return nil, fmt.Errorf("downscope: unable to exchange token; %v. Failed to read response body: %v", resp.StatusCode, err) } - return nil, fmt.Errorf("downscope: unable to exchange token; %v. Server responsed: %v", resp.StatusCode, string(b)) + return nil, fmt.Errorf("downscope: unable to exchange token; %v. Server responsed: %v", resp.StatusCode, string(b)) } var tresp downscopedTokenResponse diff --git a/google/downscope/tokenbroker_test.go b/google/downscope/tokenbroker_test.go index 4ba370b08..cb168785f 100644 --- a/google/downscope/tokenbroker_test.go +++ b/google/downscope/tokenbroker_test.go @@ -18,7 +18,9 @@ func ExampleNewTokenSource() { // This shows how to generate a downscoped token. This code would be run on the // token broker, which holds the root token used to generate the downscoped token. ctx := context.Background() - // Initializes an accessBoundary with one Rule. + // Initializes an accessBoundary with one Rule which restricts the downscoped + // token to only be able to access the bucket "foo" and only grants it the + // permission "storage.objectViewer". accessBoundary := []downscope.AccessBoundaryRule{ { AvailableResource: "//storage.googleapis.com/projects/_/buckets/foo", diff --git a/google/downscope/tokenconsumer_test.go b/google/downscope/tokenconsumer_test.go index 998e9e005..548b6eb95 100644 --- a/google/downscope/tokenconsumer_test.go +++ b/google/downscope/tokenconsumer_test.go @@ -11,7 +11,7 @@ type localTokenSource struct { func (localTokenSource) Token() (*oauth2.Token, error) { var remoteToken oauth2.Token - // retrieve remoteToken, an oauth2.Token, from token broker + // Retrieve remoteToken, an oauth2.Token, from token broker return &remoteToken, nil } @@ -26,13 +26,13 @@ func Example() { brokerURL: "yourURL.com/internal/broker", } - // Wrap the TokenSource in an oauth2.ReuseTokenSource to enable automatic refreshing + // Wrap the TokenSource in an oauth2.ReuseTokenSource to enable automatic refreshing. refreshableTS := oauth2.ReuseTokenSource(nil, thisTokenSource) // You can now use the token source to access Google Cloud Storage resources as follows. // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(refreshableTS)) - // bkt := storageClient.Bucket("foo"") + // bkt := storageClient.Bucket("foo") // obj := bkt.Object(objectName) // rc, err := obj.NewReader(ctx) // defer rc.Close() From c56d6183bb2dbf86d433c8d1ec7574ad547194e3 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 5 Aug 2021 10:44:14 -0700 Subject: [PATCH 26/27] Removed extraneous newline. --- google/downscope/tokenconsumer_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/google/downscope/tokenconsumer_test.go b/google/downscope/tokenconsumer_test.go index 548b6eb95..071d4f529 100644 --- a/google/downscope/tokenconsumer_test.go +++ b/google/downscope/tokenconsumer_test.go @@ -11,7 +11,7 @@ type localTokenSource struct { func (localTokenSource) Token() (*oauth2.Token, error) { var remoteToken oauth2.Token - // Retrieve remoteToken, an oauth2.Token, from token broker + // Retrieve remoteToken, an oauth2.Token, from token broker. return &remoteToken, nil } @@ -20,7 +20,6 @@ func Example() { // it should send a query to a token broker requesting a downscoped token. // The token broker holds the root credential that is used to generate the // downscoped token. - thisTokenSource := localTokenSource{ requestedObject: "//storage.googleapis.com/projects/_/buckets/foo", brokerURL: "yourURL.com/internal/broker", From 2149795f02d502661402819ac4dcb23cee5e7b8b Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Mon, 9 Aug 2021 13:33:14 -0700 Subject: [PATCH 27/27] Removed tokenconsumer example file; will add the examples elsewhere. --- google/downscope/tokenconsumer_test.go | 39 -------------------------- 1 file changed, 39 deletions(-) delete mode 100644 google/downscope/tokenconsumer_test.go diff --git a/google/downscope/tokenconsumer_test.go b/google/downscope/tokenconsumer_test.go deleted file mode 100644 index 071d4f529..000000000 --- a/google/downscope/tokenconsumer_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package downscope_test - -import ( - "golang.org/x/oauth2" -) - -type localTokenSource struct { - requestedObject string - brokerURL string -} - -func (localTokenSource) Token() (*oauth2.Token, error) { - var remoteToken oauth2.Token - // Retrieve remoteToken, an oauth2.Token, from token broker. - return &remoteToken, nil -} - -func Example() { - // A token consumer should define their own tokenSource. In the Token() method, - // it should send a query to a token broker requesting a downscoped token. - // The token broker holds the root credential that is used to generate the - // downscoped token. - thisTokenSource := localTokenSource{ - requestedObject: "//storage.googleapis.com/projects/_/buckets/foo", - brokerURL: "yourURL.com/internal/broker", - } - - // Wrap the TokenSource in an oauth2.ReuseTokenSource to enable automatic refreshing. - refreshableTS := oauth2.ReuseTokenSource(nil, thisTokenSource) - - // You can now use the token source to access Google Cloud Storage resources as follows. - - // storageClient, err := storage.NewClient(ctx, option.WithTokenSource(refreshableTS)) - // bkt := storageClient.Bucket("foo") - // obj := bkt.Object(objectName) - // rc, err := obj.NewReader(ctx) - // defer rc.Close() - // data, err := ioutil.ReadAll(rc) -}