...

Source file src/golang.org/x/oauth2/google/downscope/downscoping.go

Documentation: golang.org/x/oauth2/google/downscope

     1  // Copyright 2021 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  /*
     6  Package downscope implements the ability to downscope, or restrict, the
     7  Identity and Access Management permissions that a short-lived Token
     8  can use. Please note that only Google Cloud Storage supports this feature.
     9  For complete documentation, see https://cloud.google.com/iam/docs/downscoping-short-lived-credentials
    10  
    11  To downscope permissions of a source credential, you need to define
    12  a Credential Access Boundary. Said Boundary specifies which resources
    13  the newly created credential can access, an upper bound on the permissions
    14  it has over those resources, and optionally attribute-based conditional
    15  access to the aforementioned resources. For more information on IAM
    16  Conditions, see https://cloud.google.com/iam/docs/conditions-overview.
    17  
    18  This functionality can be used to provide a third party with
    19  limited access to and permissions on resources held by the owner of the root
    20  credential or internally in conjunction with the principle of least privilege
    21  to ensure that internal services only hold the minimum necessary privileges
    22  for their function.
    23  
    24  For example, a token broker can be set up on a server in a private network.
    25  Various workloads (token consumers) in the same network will send authenticated
    26  requests to that broker for downscoped tokens to access or modify specific google
    27  cloud storage buckets. See the NewTokenSource example for an example of how a
    28  token broker would use this package.
    29  
    30  The broker will use the functionality in this package to generate a downscoped
    31  token with the requested configuration, and then pass it back to the token
    32  consumer. These downscoped access tokens can then be used to access Google
    33  Storage resources. For instance, you can create a NewClient from the
    34  "cloud.google.com/go/storage" package and pass in option.WithTokenSource(yourTokenSource))
    35  */
    36  package downscope
    37  
    38  import (
    39  	"context"
    40  	"encoding/json"
    41  	"fmt"
    42  	"io/ioutil"
    43  	"net/http"
    44  	"net/url"
    45  	"strings"
    46  	"time"
    47  
    48  	"golang.org/x/oauth2"
    49  )
    50  
    51  const (
    52  	universeDomainPlaceholder       = "UNIVERSE_DOMAIN"
    53  	identityBindingEndpointTemplate = "https://sts.UNIVERSE_DOMAIN/v1/token"
    54  	defaultUniverseDomain           = "googleapis.com"
    55  )
    56  
    57  type accessBoundary struct {
    58  	AccessBoundaryRules []AccessBoundaryRule `json:"accessBoundaryRules"`
    59  }
    60  
    61  // An AvailabilityCondition restricts access to a given Resource.
    62  type AvailabilityCondition struct {
    63  	// An Expression specifies the Cloud Storage objects where
    64  	// permissions are available. For further documentation, see
    65  	// https://cloud.google.com/iam/docs/conditions-overview
    66  	Expression string `json:"expression"`
    67  	// Title is short string that identifies the purpose of the condition. Optional.
    68  	Title string `json:"title,omitempty"`
    69  	// Description details about the purpose of the condition. Optional.
    70  	Description string `json:"description,omitempty"`
    71  }
    72  
    73  // An AccessBoundaryRule Sets the permissions (and optionally conditions)
    74  // that the new token has on given resource.
    75  type AccessBoundaryRule struct {
    76  	// AvailableResource is the full resource name of the Cloud Storage bucket that the rule applies to.
    77  	// Use the format //storage.googleapis.com/projects/_/buckets/bucket-name.
    78  	AvailableResource string `json:"availableResource"`
    79  	// AvailablePermissions is a list that defines the upper bound on the available permissions
    80  	// for the resource. Each value is the identifier for an IAM predefined role or custom role,
    81  	// with the prefix inRole:. For example: inRole:roles/storage.objectViewer.
    82  	// Only the permissions in these roles will be available.
    83  	AvailablePermissions []string `json:"availablePermissions"`
    84  	// An Condition restricts the availability of permissions
    85  	// to specific Cloud Storage objects. Optional.
    86  	//
    87  	// A Condition can be used to make permissions available for specific objects,
    88  	// rather than all objects in a Cloud Storage bucket.
    89  	Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"`
    90  }
    91  
    92  type downscopedTokenResponse struct {
    93  	AccessToken     string `json:"access_token"`
    94  	IssuedTokenType string `json:"issued_token_type"`
    95  	TokenType       string `json:"token_type"`
    96  	ExpiresIn       int    `json:"expires_in"`
    97  }
    98  
    99  // DownscopingConfig specifies the information necessary to request a downscoped token.
   100  type DownscopingConfig struct {
   101  	// RootSource is the TokenSource used to create the downscoped token.
   102  	// The downscoped token therefore has some subset of the accesses of
   103  	// the original RootSource.
   104  	RootSource oauth2.TokenSource
   105  	// Rules defines the accesses held by the new
   106  	// downscoped Token. One or more AccessBoundaryRules are required to
   107  	// define permissions for the new downscoped token. Each one defines an
   108  	// access (or set of accesses) that the new token has to a given resource.
   109  	// There can be a maximum of 10 AccessBoundaryRules.
   110  	Rules []AccessBoundaryRule
   111  	// UniverseDomain is the default service domain for a given Cloud universe.
   112  	// The default value is "googleapis.com". Optional.
   113  	UniverseDomain string
   114  }
   115  
   116  // identityBindingEndpoint returns the identity binding endpoint with the
   117  // configured universe domain.
   118  func (dc *DownscopingConfig) identityBindingEndpoint() string {
   119  	if dc.UniverseDomain == "" {
   120  		return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, defaultUniverseDomain, 1)
   121  	}
   122  	return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, dc.UniverseDomain, 1)
   123  }
   124  
   125  // A downscopingTokenSource is used to retrieve a downscoped token with restricted
   126  // permissions compared to the root Token that is used to generate it.
   127  type downscopingTokenSource struct {
   128  	// ctx is the context used to query the API to retrieve a downscoped Token.
   129  	ctx context.Context
   130  	// config holds the information necessary to generate a downscoped Token.
   131  	config DownscopingConfig
   132  	// identityBindingEndpoint is the identity binding endpoint with the
   133  	// configured universe domain.
   134  	identityBindingEndpoint string
   135  }
   136  
   137  // NewTokenSource returns a configured downscopingTokenSource.
   138  func NewTokenSource(ctx context.Context, conf DownscopingConfig) (oauth2.TokenSource, error) {
   139  	if conf.RootSource == nil {
   140  		return nil, fmt.Errorf("downscope: rootSource cannot be nil")
   141  	}
   142  	if len(conf.Rules) == 0 {
   143  		return nil, fmt.Errorf("downscope: length of AccessBoundaryRules must be at least 1")
   144  	}
   145  	if len(conf.Rules) > 10 {
   146  		return nil, fmt.Errorf("downscope: length of AccessBoundaryRules may not be greater than 10")
   147  	}
   148  	for _, val := range conf.Rules {
   149  		if val.AvailableResource == "" {
   150  			return nil, fmt.Errorf("downscope: all rules must have a nonempty AvailableResource: %+v", val)
   151  		}
   152  		if len(val.AvailablePermissions) == 0 {
   153  			return nil, fmt.Errorf("downscope: all rules must provide at least one permission: %+v", val)
   154  		}
   155  	}
   156  	return downscopingTokenSource{
   157  		ctx:                     ctx,
   158  		config:                  conf,
   159  		identityBindingEndpoint: conf.identityBindingEndpoint(),
   160  	}, nil
   161  }
   162  
   163  // Token() uses a downscopingTokenSource to generate an oauth2 Token.
   164  // Do note that the returned TokenSource is an oauth2.StaticTokenSource. If you wish
   165  // to refresh this token automatically, then initialize a locally defined
   166  // TokenSource struct with the Token held by the StaticTokenSource and wrap
   167  // that TokenSource in an oauth2.ReuseTokenSource.
   168  func (dts downscopingTokenSource) Token() (*oauth2.Token, error) {
   169  
   170  	downscopedOptions := struct {
   171  		Boundary accessBoundary `json:"accessBoundary"`
   172  	}{
   173  		Boundary: accessBoundary{
   174  			AccessBoundaryRules: dts.config.Rules,
   175  		},
   176  	}
   177  
   178  	tok, err := dts.config.RootSource.Token()
   179  	if err != nil {
   180  		return nil, fmt.Errorf("downscope: unable to obtain root token: %v", err)
   181  	}
   182  
   183  	b, err := json.Marshal(downscopedOptions)
   184  	if err != nil {
   185  		return nil, fmt.Errorf("downscope: unable to marshal AccessBoundary payload %v", err)
   186  	}
   187  
   188  	form := url.Values{}
   189  	form.Add("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
   190  	form.Add("subject_token_type", "urn:ietf:params:oauth:token-type:access_token")
   191  	form.Add("requested_token_type", "urn:ietf:params:oauth:token-type:access_token")
   192  	form.Add("subject_token", tok.AccessToken)
   193  	form.Add("options", string(b))
   194  
   195  	myClient := oauth2.NewClient(dts.ctx, nil)
   196  	resp, err := myClient.PostForm(dts.identityBindingEndpoint, form)
   197  	if err != nil {
   198  		return nil, fmt.Errorf("unable to generate POST Request %v", err)
   199  	}
   200  	defer resp.Body.Close()
   201  	respBody, err := ioutil.ReadAll(resp.Body)
   202  	if err != nil {
   203  		return nil, fmt.Errorf("downscope: unable to read response body: %v", err)
   204  	}
   205  	if resp.StatusCode != http.StatusOK {
   206  		return nil, fmt.Errorf("downscope: unable to exchange token; %v. Server responded: %s", resp.StatusCode, respBody)
   207  	}
   208  
   209  	var tresp downscopedTokenResponse
   210  
   211  	err = json.Unmarshal(respBody, &tresp)
   212  	if err != nil {
   213  		return nil, fmt.Errorf("downscope: unable to unmarshal response body: %v", err)
   214  	}
   215  
   216  	// an exchanged token that is derived from a service account (2LO) has an expired_in value
   217  	// a token derived from a users token (3LO) does not.
   218  	// The following code uses the time remaining on rootToken for a user as the value for the
   219  	// derived token's lifetime
   220  	var expiryTime time.Time
   221  	if tresp.ExpiresIn > 0 {
   222  		expiryTime = time.Now().Add(time.Duration(tresp.ExpiresIn) * time.Second)
   223  	} else {
   224  		expiryTime = tok.Expiry
   225  	}
   226  
   227  	newToken := &oauth2.Token{
   228  		AccessToken: tresp.AccessToken,
   229  		TokenType:   tresp.TokenType,
   230  		Expiry:      expiryTime,
   231  	}
   232  	return newToken, nil
   233  }
   234  

View as plain text