aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/minio/minio-go/v7/pkg/credentials/assume_role.go
blob: 800c4a294a5d485cd5fbf638ef821f2756599cc7 (plain) (blame)
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
/*
 * MinIO Go Library for Amazon S3 Compatible Cloud Storage
 * Copyright 2020 MinIO, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package credentials

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/xml"
	"errors"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/minio/minio-go/v7/pkg/signer"
)

// AssumeRoleResponse contains the result of successful AssumeRole request.
type AssumeRoleResponse struct {
	XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleResponse" json:"-"`

	Result           AssumeRoleResult `xml:"AssumeRoleResult"`
	ResponseMetadata struct {
		RequestID string `xml:"RequestId,omitempty"`
	} `xml:"ResponseMetadata,omitempty"`
}

// AssumeRoleResult - Contains the response to a successful AssumeRole
// request, including temporary credentials that can be used to make
// MinIO API requests.
type AssumeRoleResult struct {
	// The identifiers for the temporary security credentials that the operation
	// returns.
	AssumedRoleUser AssumedRoleUser `xml:",omitempty"`

	// The temporary security credentials, which include an access key ID, a secret
	// access key, and a security (or session) token.
	//
	// Note: The size of the security token that STS APIs return is not fixed. We
	// strongly recommend that you make no assumptions about the maximum size. As
	// of this writing, the typical size is less than 4096 bytes, but that can vary.
	// Also, future updates to AWS might require larger sizes.
	Credentials struct {
		AccessKey    string    `xml:"AccessKeyId" json:"accessKey,omitempty"`
		SecretKey    string    `xml:"SecretAccessKey" json:"secretKey,omitempty"`
		Expiration   time.Time `xml:"Expiration" json:"expiration,omitempty"`
		SessionToken string    `xml:"SessionToken" json:"sessionToken,omitempty"`
	} `xml:",omitempty"`

	// A percentage value that indicates the size of the policy in packed form.
	// The service rejects any policy with a packed size greater than 100 percent,
	// which means the policy exceeded the allowed space.
	PackedPolicySize int `xml:",omitempty"`
}

// A STSAssumeRole retrieves credentials from MinIO service, and keeps track if
// those credentials are expired.
type STSAssumeRole struct {
	Expiry

	// Required http Client to use when connecting to MinIO STS service.
	Client *http.Client

	// STS endpoint to fetch STS credentials.
	STSEndpoint string

	// various options for this request.
	Options STSAssumeRoleOptions
}

// STSAssumeRoleOptions collection of various input options
// to obtain AssumeRole credentials.
type STSAssumeRoleOptions struct {
	// Mandatory inputs.
	AccessKey string
	SecretKey string

	SessionToken string // Optional if the first request is made with temporary credentials.
	Policy       string // Optional to assign a policy to the assumed role

	Location        string // Optional commonly needed with AWS STS.
	DurationSeconds int    // Optional defaults to 1 hour.

	// Optional only valid if using with AWS STS
	RoleARN         string
	RoleSessionName string
	ExternalID      string
}

// NewSTSAssumeRole returns a pointer to a new
// Credentials object wrapping the STSAssumeRole.
func NewSTSAssumeRole(stsEndpoint string, opts STSAssumeRoleOptions) (*Credentials, error) {
	if stsEndpoint == "" {
		return nil, errors.New("STS endpoint cannot be empty")
	}
	if opts.AccessKey == "" || opts.SecretKey == "" {
		return nil, errors.New("AssumeRole credentials access/secretkey is mandatory")
	}
	return New(&STSAssumeRole{
		Client: &http.Client{
			Transport: http.DefaultTransport,
		},
		STSEndpoint: stsEndpoint,
		Options:     opts,
	}), nil
}

const defaultDurationSeconds = 3600

// closeResponse close non nil response with any response Body.
// convenient wrapper to drain any remaining data on response body.
//
// Subsequently this allows golang http RoundTripper
// to re-use the same connection for future requests.
func closeResponse(resp *http.Response) {
	// Callers should close resp.Body when done reading from it.
	// If resp.Body is not closed, the Client's underlying RoundTripper
	// (typically Transport) may not be able to re-use a persistent TCP
	// connection to the server for a subsequent "keep-alive" request.
	if resp != nil && resp.Body != nil {
		// Drain any remaining Body and then close the connection.
		// Without this closing connection would disallow re-using
		// the same connection for future uses.
		//  - http://stackoverflow.com/a/17961593/4465767
		io.Copy(io.Discard, resp.Body)
		resp.Body.Close()
	}
}

func getAssumeRoleCredentials(clnt *http.Client, endpoint string, opts STSAssumeRoleOptions) (AssumeRoleResponse, error) {
	v := url.Values{}
	v.Set("Action", "AssumeRole")
	v.Set("Version", STSVersion)
	if opts.RoleARN != "" {
		v.Set("RoleArn", opts.RoleARN)
	}
	if opts.RoleSessionName != "" {
		v.Set("RoleSessionName", opts.RoleSessionName)
	}
	if opts.DurationSeconds > defaultDurationSeconds {
		v.Set("DurationSeconds", strconv.Itoa(opts.DurationSeconds))
	} else {
		v.Set("DurationSeconds", strconv.Itoa(defaultDurationSeconds))
	}
	if opts.Policy != "" {
		v.Set("Policy", opts.Policy)
	}
	if opts.ExternalID != "" {
		v.Set("ExternalId", opts.ExternalID)
	}

	u, err := url.Parse(endpoint)
	if err != nil {
		return AssumeRoleResponse{}, err
	}
	u.Path = "/"

	postBody := strings.NewReader(v.Encode())
	hash := sha256.New()
	if _, err = io.Copy(hash, postBody); err != nil {
		return AssumeRoleResponse{}, err
	}
	postBody.Seek(0, 0)

	req, err := http.NewRequest(http.MethodPost, u.String(), postBody)
	if err != nil {
		return AssumeRoleResponse{}, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(hash.Sum(nil)))
	if opts.SessionToken != "" {
		req.Header.Set("X-Amz-Security-Token", opts.SessionToken)
	}
	req = signer.SignV4STS(*req, opts.AccessKey, opts.SecretKey, opts.Location)

	resp, err := clnt.Do(req)
	if err != nil {
		return AssumeRoleResponse{}, err
	}
	defer closeResponse(resp)
	if resp.StatusCode != http.StatusOK {
		var errResp ErrorResponse
		buf, err := io.ReadAll(resp.Body)
		if err != nil {
			return AssumeRoleResponse{}, err
		}
		_, err = xmlDecodeAndBody(bytes.NewReader(buf), &errResp)
		if err != nil {
			var s3Err Error
			if _, err = xmlDecodeAndBody(bytes.NewReader(buf), &s3Err); err != nil {
				return AssumeRoleResponse{}, err
			}
			errResp.RequestID = s3Err.RequestID
			errResp.STSError.Code = s3Err.Code
			errResp.STSError.Message = s3Err.Message
		}
		return AssumeRoleResponse{}, errResp
	}

	a := AssumeRoleResponse{}
	if _, err = xmlDecodeAndBody(resp.Body, &a); err != nil {
		return AssumeRoleResponse{}, err
	}
	return a, nil
}

// Retrieve retrieves credentials from the MinIO service.
// Error will be returned if the request fails.
func (m *STSAssumeRole) Retrieve() (Value, error) {
	a, err := getAssumeRoleCredentials(m.Client, m.STSEndpoint, m.Options)
	if err != nil {
		return Value{}, err
	}

	// Expiry window is set to 10secs.
	m.SetExpiration(a.Result.Credentials.Expiration, DefaultExpiryWindow)

	return Value{
		AccessKeyID:     a.Result.Credentials.AccessKey,
		SecretAccessKey: a.Result.Credentials.SecretKey,
		SessionToken:    a.Result.Credentials.SessionToken,
		SignerType:      SignatureV4,
	}, nil
}