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