diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 4f89e3e..dd6f7de 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 1897a1e..95ae5ba 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 - 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 + // Initializes an accessBoundary with one Rule + accessBoundary := []AccessBoundaryRule{ + AccessBoundaryRule{ + AvailableResource: "//storage.googleapis.com/projects/_/buckets/foo", + AvailablePermissions: []string{"inRole:roles/storage.objectViewer"}, + }, } - myTokenSource, err := NewTokenSource(context.Background(), DownscopingConfig{rootSource, myBoundary}) + + 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) - 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)) - // bkt := storageClient.Bucket(bucketName) - // obj := bkt.Object(objectName) - // rc, err := obj.NewReader(ctx) - // data, err := ioutil.ReadAll(rc) - return +} + +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 }