1
2
3
4
5
6
7
8
9
10
11
12
13
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
36 type Options struct {
37
38
39 Credentials *auth.Credentials
40
41
42
43
44
45 Rules []AccessBoundaryRule
46
47
48 Client *http.Client
49
50
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
62
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
71
72 type AccessBoundaryRule struct {
73
74
75
76 AvailableResource string `json:"availableResource"`
77
78
79
80
81 AvailablePermissions []string `json:"availablePermissions"`
82
83
84
85
86
87 Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"`
88 }
89
90
91 type AvailabilityCondition struct {
92
93
94
95 Expression string `json:"expression"`
96
97 Title string `json:"title,omitempty"`
98
99 Description string `json:"description,omitempty"`
100 }
101
102
103
104
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
139 type downscopedTokenProvider struct {
140 Options *Options
141 Client *http.Client
142
143
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
205
206
207
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