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