iam_aws.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. /*
  2. * Minio Go Library for Amazon S3 Compatible Cloud Storage
  3. * Copyright 2017 Minio, Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. package credentials
  18. import (
  19. "bufio"
  20. "encoding/json"
  21. "errors"
  22. "net/http"
  23. "net/url"
  24. "path"
  25. "time"
  26. )
  27. // DefaultExpiryWindow - Default expiry window.
  28. // ExpiryWindow will allow the credentials to trigger refreshing
  29. // prior to the credentials actually expiring. This is beneficial
  30. // so race conditions with expiring credentials do not cause
  31. // request to fail unexpectedly due to ExpiredTokenException exceptions.
  32. const DefaultExpiryWindow = time.Second * 10 // 10 secs
  33. // A IAM retrieves credentials from the EC2 service, and keeps track if
  34. // those credentials are expired.
  35. type IAM struct {
  36. Expiry
  37. // Required http Client to use when connecting to IAM metadata service.
  38. Client *http.Client
  39. // Custom endpoint to fetch IAM role credentials.
  40. endpoint string
  41. }
  42. // IAM Roles for Amazon EC2
  43. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  44. const (
  45. defaultIAMRoleEndpoint = "http://169.254.169.254"
  46. defaultIAMSecurityCredsPath = "/latest/meta-data/iam/security-credentials"
  47. )
  48. // NewIAM returns a pointer to a new Credentials object wrapping
  49. // the IAM. Takes a ConfigProvider to create a EC2Metadata client.
  50. // The ConfigProvider is satisfied by the session.Session type.
  51. func NewIAM(endpoint string) *Credentials {
  52. if endpoint == "" {
  53. endpoint = defaultIAMRoleEndpoint
  54. }
  55. p := &IAM{
  56. Client: &http.Client{
  57. Transport: http.DefaultTransport,
  58. },
  59. endpoint: endpoint,
  60. }
  61. return New(p)
  62. }
  63. // Retrieve retrieves credentials from the EC2 service.
  64. // Error will be returned if the request fails, or unable to extract
  65. // the desired
  66. func (m *IAM) Retrieve() (Value, error) {
  67. roleCreds, err := getCredentials(m.Client, m.endpoint)
  68. if err != nil {
  69. return Value{}, err
  70. }
  71. // Expiry window is set to 10secs.
  72. m.SetExpiration(roleCreds.Expiration, DefaultExpiryWindow)
  73. return Value{
  74. AccessKeyID: roleCreds.AccessKeyID,
  75. SecretAccessKey: roleCreds.SecretAccessKey,
  76. SessionToken: roleCreds.Token,
  77. SignerType: SignatureV4,
  78. }, nil
  79. }
  80. // A ec2RoleCredRespBody provides the shape for unmarshaling credential
  81. // request responses.
  82. type ec2RoleCredRespBody struct {
  83. // Success State
  84. Expiration time.Time
  85. AccessKeyID string
  86. SecretAccessKey string
  87. Token string
  88. // Error state
  89. Code string
  90. Message string
  91. // Unused params.
  92. LastUpdated time.Time
  93. Type string
  94. }
  95. // Get the final IAM role URL where the request will
  96. // be sent to fetch the rolling access credentials.
  97. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  98. func getIAMRoleURL(endpoint string) (*url.URL, error) {
  99. if endpoint == "" {
  100. endpoint = defaultIAMRoleEndpoint
  101. }
  102. u, err := url.Parse(endpoint)
  103. if err != nil {
  104. return nil, err
  105. }
  106. u.Path = defaultIAMSecurityCredsPath
  107. return u, nil
  108. }
  109. // listRoleNames lists of credential role names associated
  110. // with the current EC2 service. If there are no credentials,
  111. // or there is an error making or receiving the request.
  112. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  113. func listRoleNames(client *http.Client, u *url.URL) ([]string, error) {
  114. req, err := http.NewRequest("GET", u.String(), nil)
  115. if err != nil {
  116. return nil, err
  117. }
  118. resp, err := client.Do(req)
  119. if err != nil {
  120. return nil, err
  121. }
  122. defer resp.Body.Close()
  123. if resp.StatusCode != http.StatusOK {
  124. return nil, errors.New(resp.Status)
  125. }
  126. credsList := []string{}
  127. s := bufio.NewScanner(resp.Body)
  128. for s.Scan() {
  129. credsList = append(credsList, s.Text())
  130. }
  131. if err := s.Err(); err != nil {
  132. return nil, err
  133. }
  134. return credsList, nil
  135. }
  136. // getCredentials - obtains the credentials from the IAM role name associated with
  137. // the current EC2 service.
  138. //
  139. // If the credentials cannot be found, or there is an error
  140. // reading the response an error will be returned.
  141. func getCredentials(client *http.Client, endpoint string) (ec2RoleCredRespBody, error) {
  142. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  143. u, err := getIAMRoleURL(endpoint)
  144. if err != nil {
  145. return ec2RoleCredRespBody{}, err
  146. }
  147. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  148. roleNames, err := listRoleNames(client, u)
  149. if err != nil {
  150. return ec2RoleCredRespBody{}, err
  151. }
  152. if len(roleNames) == 0 {
  153. return ec2RoleCredRespBody{}, errors.New("No IAM roles attached to this EC2 service")
  154. }
  155. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  156. // - An instance profile can contain only one IAM role. This limit cannot be increased.
  157. roleName := roleNames[0]
  158. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  159. // The following command retrieves the security credentials for an
  160. // IAM role named `s3access`.
  161. //
  162. // $ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/s3access
  163. //
  164. u.Path = path.Join(u.Path, roleName)
  165. req, err := http.NewRequest("GET", u.String(), nil)
  166. if err != nil {
  167. return ec2RoleCredRespBody{}, err
  168. }
  169. resp, err := client.Do(req)
  170. if err != nil {
  171. return ec2RoleCredRespBody{}, err
  172. }
  173. defer resp.Body.Close()
  174. if resp.StatusCode != http.StatusOK {
  175. return ec2RoleCredRespBody{}, errors.New(resp.Status)
  176. }
  177. respCreds := ec2RoleCredRespBody{}
  178. if err := json.NewDecoder(resp.Body).Decode(&respCreds); err != nil {
  179. return ec2RoleCredRespBody{}, err
  180. }
  181. if respCreds.Code != "Success" {
  182. // If an error code was returned something failed requesting the role.
  183. return ec2RoleCredRespBody{}, errors.New(respCreds.Message)
  184. }
  185. return respCreds, nil
  186. }