/* * 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, Expiration: a.Result.Credentials.Expiration, SignerType: SignatureV4, }, nil }