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