...

Source file src/cloud.google.com/go/auth/credentials/downscope/downscope.go

Documentation: cloud.google.com/go/auth/credentials/downscope

     1  // Copyright 2023 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package downscope
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"net/http"
    22  	"net/url"
    23  	"strings"
    24  	"time"
    25  
    26  	"cloud.google.com/go/auth"
    27  	"cloud.google.com/go/auth/internal"
    28  )
    29  
    30  const (
    31  	universeDomainPlaceholder       = "UNIVERSE_DOMAIN"
    32  	identityBindingEndpointTemplate = "https://sts.UNIVERSE_DOMAIN/v1/token"
    33  )
    34  
    35  // Options for configuring [NewCredentials].
    36  type Options struct {
    37  	// Credentials is the [cloud.google.com/go/auth.Credentials] used to
    38  	// create the downscoped credentials. Required.
    39  	Credentials *auth.Credentials
    40  	// Rules defines the accesses held by the new downscoped credentials. One or
    41  	// more AccessBoundaryRules are required to define permissions for the new
    42  	// downscoped credentials. Each one defines an access (or set of accesses)
    43  	//that the new credentials has to a given resource. There can be a maximum
    44  	// of 10 AccessBoundaryRules. Required.
    45  	Rules []AccessBoundaryRule
    46  	// Client configures the underlying client used to make network requests
    47  	// when fetching tokens. Optional.
    48  	Client *http.Client
    49  	// UniverseDomain is the default service domain for a given Cloud universe.
    50  	// The default value is "googleapis.com". Optional.
    51  	UniverseDomain string
    52  }
    53  
    54  func (o *Options) client() *http.Client {
    55  	if o.Client != nil {
    56  		return o.Client
    57  	}
    58  	return internal.CloneDefaultClient()
    59  }
    60  
    61  // identityBindingEndpoint returns the identity binding endpoint with the
    62  // configured universe domain.
    63  func (o *Options) identityBindingEndpoint() string {
    64  	if o.UniverseDomain == "" {
    65  		return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, internal.DefaultUniverseDomain, 1)
    66  	}
    67  	return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, o.UniverseDomain, 1)
    68  }
    69  
    70  // An AccessBoundaryRule Sets the permissions (and optionally conditions) that
    71  // the new token has on given resource.
    72  type AccessBoundaryRule struct {
    73  	// AvailableResource is the full resource name of the Cloud Storage bucket
    74  	// that the rule applies to. Use the format
    75  	// //storage.googleapis.com/projects/_/buckets/bucket-name.
    76  	AvailableResource string `json:"availableResource"`
    77  	// AvailablePermissions is a list that defines the upper bound on the available permissions
    78  	// for the resource. Each value is the identifier for an IAM predefined role or custom role,
    79  	// with the prefix inRole:. For example: inRole:roles/storage.objectViewer.
    80  	// Only the permissions in these roles will be available.
    81  	AvailablePermissions []string `json:"availablePermissions"`
    82  	// An Condition restricts the availability of permissions
    83  	// to specific Cloud Storage objects. Optional.
    84  	//
    85  	// A Condition can be used to make permissions available for specific objects,
    86  	// rather than all objects in a Cloud Storage bucket.
    87  	Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"`
    88  }
    89  
    90  // An AvailabilityCondition restricts access to a given Resource.
    91  type AvailabilityCondition struct {
    92  	// An Expression specifies the Cloud Storage objects where
    93  	// permissions are available. For further documentation, see
    94  	// https://cloud.google.com/iam/docs/conditions-overview. Required.
    95  	Expression string `json:"expression"`
    96  	// Title is short string that identifies the purpose of the condition. Optional.
    97  	Title string `json:"title,omitempty"`
    98  	// Description details about the purpose of the condition. Optional.
    99  	Description string `json:"description,omitempty"`
   100  }
   101  
   102  // NewCredentials returns a [cloud.google.com/go/auth.Credentials] that is
   103  // more restrictive than [Options.Credentials] provided. The new credentials
   104  // will delegate to the base credentials for all non-token activity.
   105  func NewCredentials(opts *Options) (*auth.Credentials, error) {
   106  	if opts == nil {
   107  		return nil, fmt.Errorf("downscope: providing opts is required")
   108  	}
   109  	if opts.Credentials == nil {
   110  		return nil, fmt.Errorf("downscope: Credentials cannot be nil")
   111  	}
   112  	if len(opts.Rules) == 0 {
   113  		return nil, fmt.Errorf("downscope: length of AccessBoundaryRules must be at least 1")
   114  	}
   115  	if len(opts.Rules) > 10 {
   116  		return nil, fmt.Errorf("downscope: length of AccessBoundaryRules may not be greater than 10")
   117  	}
   118  	for _, val := range opts.Rules {
   119  		if val.AvailableResource == "" {
   120  			return nil, fmt.Errorf("downscope: all rules must have a nonempty AvailableResource")
   121  		}
   122  		if len(val.AvailablePermissions) == 0 {
   123  			return nil, fmt.Errorf("downscope: all rules must provide at least one permission")
   124  		}
   125  	}
   126  	return auth.NewCredentials(&auth.CredentialsOptions{
   127  		TokenProvider: &downscopedTokenProvider{
   128  			Options:                 opts,
   129  			Client:                  opts.client(),
   130  			identityBindingEndpoint: opts.identityBindingEndpoint(),
   131  		},
   132  		ProjectIDProvider:      auth.CredentialsPropertyFunc(opts.Credentials.ProjectID),
   133  		QuotaProjectIDProvider: auth.CredentialsPropertyFunc(opts.Credentials.QuotaProjectID),
   134  		UniverseDomainProvider: internal.StaticCredentialsProperty(opts.UniverseDomain),
   135  	}), nil
   136  }
   137  
   138  // downscopedTokenProvider is used to retrieve a downscoped tokens.
   139  type downscopedTokenProvider struct {
   140  	Options *Options
   141  	Client  *http.Client
   142  	// identityBindingEndpoint is the identity binding endpoint with the
   143  	// configured universe domain.
   144  	identityBindingEndpoint string
   145  }
   146  
   147  type downscopedOptions struct {
   148  	Boundary accessBoundary `json:"accessBoundary"`
   149  }
   150  
   151  type accessBoundary struct {
   152  	AccessBoundaryRules []AccessBoundaryRule `json:"accessBoundaryRules"`
   153  }
   154  
   155  type downscopedTokenResponse struct {
   156  	AccessToken     string `json:"access_token"`
   157  	IssuedTokenType string `json:"issued_token_type"`
   158  	TokenType       string `json:"token_type"`
   159  	ExpiresIn       int    `json:"expires_in"`
   160  }
   161  
   162  func (dts *downscopedTokenProvider) Token(ctx context.Context) (*auth.Token, error) {
   163  	downscopedOptions := downscopedOptions{
   164  		Boundary: accessBoundary{
   165  			AccessBoundaryRules: dts.Options.Rules,
   166  		},
   167  	}
   168  
   169  	tok, err := dts.Options.Credentials.Token(ctx)
   170  	if err != nil {
   171  		return nil, fmt.Errorf("downscope: unable to obtain root token: %w", err)
   172  	}
   173  	b, err := json.Marshal(downscopedOptions)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	form := url.Values{}
   179  	form.Add("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
   180  	form.Add("subject_token_type", "urn:ietf:params:oauth:token-type:access_token")
   181  	form.Add("requested_token_type", "urn:ietf:params:oauth:token-type:access_token")
   182  	form.Add("subject_token", tok.Value)
   183  	form.Add("options", string(b))
   184  
   185  	resp, err := dts.Client.PostForm(dts.identityBindingEndpoint, form)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	defer resp.Body.Close()
   190  	respBody, err := internal.ReadAll(resp.Body)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	if resp.StatusCode != http.StatusOK {
   195  		return nil, fmt.Errorf("downscope: unable to exchange token, %v: %s", resp.StatusCode, respBody)
   196  	}
   197  
   198  	var tresp downscopedTokenResponse
   199  	err = json.Unmarshal(respBody, &tresp)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  
   204  	// An exchanged token that is derived from a service account (2LO) has an
   205  	// expired_in value a token derived from a users token (3LO) does not.
   206  	// The following code uses the time remaining on rootToken for a user as the
   207  	// value for the derived token's lifetime.
   208  	var expiryTime time.Time
   209  	if tresp.ExpiresIn > 0 {
   210  		expiryTime = time.Now().Add(time.Duration(tresp.ExpiresIn) * time.Second)
   211  	} else {
   212  		expiryTime = tok.Expiry
   213  	}
   214  	return &auth.Token{
   215  		Value:  tresp.AccessToken,
   216  		Type:   tresp.TokenType,
   217  		Expiry: expiryTime,
   218  	}, nil
   219  }
   220  

View as plain text