authorizer.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. /*
  2. Copyright The containerd Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package docker
  14. import (
  15. "context"
  16. "encoding/base64"
  17. "encoding/json"
  18. "fmt"
  19. "io"
  20. "io/ioutil"
  21. "net/http"
  22. "net/url"
  23. "strings"
  24. "sync"
  25. "time"
  26. "github.com/containerd/containerd/errdefs"
  27. "github.com/containerd/containerd/log"
  28. "github.com/pkg/errors"
  29. "github.com/sirupsen/logrus"
  30. "golang.org/x/net/context/ctxhttp"
  31. )
  32. type dockerAuthorizer struct {
  33. credentials func(string) (string, string, error)
  34. client *http.Client
  35. mu sync.Mutex
  36. auth map[string]string
  37. }
  38. // NewAuthorizer creates a Docker authorizer using the provided function to
  39. // get credentials for the token server or basic auth.
  40. func NewAuthorizer(client *http.Client, f func(string) (string, string, error)) Authorizer {
  41. if client == nil {
  42. client = http.DefaultClient
  43. }
  44. return &dockerAuthorizer{
  45. credentials: f,
  46. client: client,
  47. auth: map[string]string{},
  48. }
  49. }
  50. func (a *dockerAuthorizer) Authorize(ctx context.Context, req *http.Request) error {
  51. // TODO: Lookup matching challenge and scope rather than just host
  52. if auth := a.getAuth(req.URL.Host); auth != "" {
  53. req.Header.Set("Authorization", auth)
  54. }
  55. return nil
  56. }
  57. func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.Response) error {
  58. last := responses[len(responses)-1]
  59. host := last.Request.URL.Host
  60. for _, c := range parseAuthHeader(last.Header) {
  61. if c.scheme == bearerAuth {
  62. if err := invalidAuthorization(c, responses); err != nil {
  63. // TODO: Clear token
  64. a.setAuth(host, "")
  65. return err
  66. }
  67. // TODO(dmcg): Store challenge, not token
  68. // Move token fetching to authorize
  69. if err := a.setTokenAuth(ctx, host, c.parameters); err != nil {
  70. return err
  71. }
  72. return nil
  73. } else if c.scheme == basicAuth {
  74. // TODO: Resolve credentials on authorize
  75. username, secret, err := a.credentials(host)
  76. if err != nil {
  77. return err
  78. }
  79. if username != "" && secret != "" {
  80. auth := username + ":" + secret
  81. a.setAuth(host, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(auth))))
  82. return nil
  83. }
  84. }
  85. }
  86. return errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme")
  87. }
  88. func (a *dockerAuthorizer) getAuth(host string) string {
  89. a.mu.Lock()
  90. defer a.mu.Unlock()
  91. return a.auth[host]
  92. }
  93. func (a *dockerAuthorizer) setAuth(host string, auth string) bool {
  94. a.mu.Lock()
  95. defer a.mu.Unlock()
  96. changed := a.auth[host] != auth
  97. a.auth[host] = auth
  98. return changed
  99. }
  100. func (a *dockerAuthorizer) setTokenAuth(ctx context.Context, host string, params map[string]string) error {
  101. realm, ok := params["realm"]
  102. if !ok {
  103. return errors.New("no realm specified for token auth challenge")
  104. }
  105. realmURL, err := url.Parse(realm)
  106. if err != nil {
  107. return errors.Wrap(err, "invalid token auth challenge realm")
  108. }
  109. to := tokenOptions{
  110. realm: realmURL.String(),
  111. service: params["service"],
  112. }
  113. to.scopes = getTokenScopes(ctx, params)
  114. if len(to.scopes) == 0 {
  115. return errors.Errorf("no scope specified for token auth challenge")
  116. }
  117. if a.credentials != nil {
  118. to.username, to.secret, err = a.credentials(host)
  119. if err != nil {
  120. return err
  121. }
  122. }
  123. var token string
  124. if to.secret != "" {
  125. // Credential information is provided, use oauth POST endpoint
  126. token, err = a.fetchTokenWithOAuth(ctx, to)
  127. if err != nil {
  128. return errors.Wrap(err, "failed to fetch oauth token")
  129. }
  130. } else {
  131. // Do request anonymously
  132. token, err = a.fetchToken(ctx, to)
  133. if err != nil {
  134. return errors.Wrap(err, "failed to fetch anonymous token")
  135. }
  136. }
  137. a.setAuth(host, fmt.Sprintf("Bearer %s", token))
  138. return nil
  139. }
  140. type tokenOptions struct {
  141. realm string
  142. service string
  143. scopes []string
  144. username string
  145. secret string
  146. }
  147. type postTokenResponse struct {
  148. AccessToken string `json:"access_token"`
  149. RefreshToken string `json:"refresh_token"`
  150. ExpiresIn int `json:"expires_in"`
  151. IssuedAt time.Time `json:"issued_at"`
  152. Scope string `json:"scope"`
  153. }
  154. func (a *dockerAuthorizer) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
  155. form := url.Values{}
  156. form.Set("scope", strings.Join(to.scopes, " "))
  157. form.Set("service", to.service)
  158. // TODO: Allow setting client_id
  159. form.Set("client_id", "containerd-client")
  160. if to.username == "" {
  161. form.Set("grant_type", "refresh_token")
  162. form.Set("refresh_token", to.secret)
  163. } else {
  164. form.Set("grant_type", "password")
  165. form.Set("username", to.username)
  166. form.Set("password", to.secret)
  167. }
  168. resp, err := ctxhttp.PostForm(ctx, a.client, to.realm, form)
  169. if err != nil {
  170. return "", err
  171. }
  172. defer resp.Body.Close()
  173. // Registries without support for POST may return 404 for POST /v2/token.
  174. // As of September 2017, GCR is known to return 404.
  175. // As of February 2018, JFrog Artifactory is known to return 401.
  176. if (resp.StatusCode == 405 && to.username != "") || resp.StatusCode == 404 || resp.StatusCode == 401 {
  177. return a.fetchToken(ctx, to)
  178. } else if resp.StatusCode < 200 || resp.StatusCode >= 400 {
  179. b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 64000)) // 64KB
  180. log.G(ctx).WithFields(logrus.Fields{
  181. "status": resp.Status,
  182. "body": string(b),
  183. }).Debugf("token request failed")
  184. // TODO: handle error body and write debug output
  185. return "", errors.Errorf("unexpected status: %s", resp.Status)
  186. }
  187. decoder := json.NewDecoder(resp.Body)
  188. var tr postTokenResponse
  189. if err = decoder.Decode(&tr); err != nil {
  190. return "", fmt.Errorf("unable to decode token response: %s", err)
  191. }
  192. return tr.AccessToken, nil
  193. }
  194. type getTokenResponse struct {
  195. Token string `json:"token"`
  196. AccessToken string `json:"access_token"`
  197. ExpiresIn int `json:"expires_in"`
  198. IssuedAt time.Time `json:"issued_at"`
  199. RefreshToken string `json:"refresh_token"`
  200. }
  201. // getToken fetches a token using a GET request
  202. func (a *dockerAuthorizer) fetchToken(ctx context.Context, to tokenOptions) (string, error) {
  203. req, err := http.NewRequest("GET", to.realm, nil)
  204. if err != nil {
  205. return "", err
  206. }
  207. reqParams := req.URL.Query()
  208. if to.service != "" {
  209. reqParams.Add("service", to.service)
  210. }
  211. for _, scope := range to.scopes {
  212. reqParams.Add("scope", scope)
  213. }
  214. if to.secret != "" {
  215. req.SetBasicAuth(to.username, to.secret)
  216. }
  217. req.URL.RawQuery = reqParams.Encode()
  218. resp, err := ctxhttp.Do(ctx, a.client, req)
  219. if err != nil {
  220. return "", err
  221. }
  222. defer resp.Body.Close()
  223. if resp.StatusCode < 200 || resp.StatusCode >= 400 {
  224. // TODO: handle error body and write debug output
  225. return "", errors.Errorf("unexpected status: %s", resp.Status)
  226. }
  227. decoder := json.NewDecoder(resp.Body)
  228. var tr getTokenResponse
  229. if err = decoder.Decode(&tr); err != nil {
  230. return "", fmt.Errorf("unable to decode token response: %s", err)
  231. }
  232. // `access_token` is equivalent to `token` and if both are specified
  233. // the choice is undefined. Canonicalize `access_token` by sticking
  234. // things in `token`.
  235. if tr.AccessToken != "" {
  236. tr.Token = tr.AccessToken
  237. }
  238. if tr.Token == "" {
  239. return "", ErrNoToken
  240. }
  241. return tr.Token, nil
  242. }
  243. func invalidAuthorization(c challenge, responses []*http.Response) error {
  244. errStr := c.parameters["error"]
  245. if errStr == "" {
  246. return nil
  247. }
  248. n := len(responses)
  249. if n == 1 || (n > 1 && !sameRequest(responses[n-2].Request, responses[n-1].Request)) {
  250. return nil
  251. }
  252. return errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr)
  253. }
  254. func sameRequest(r1, r2 *http.Request) bool {
  255. if r1.Method != r2.Method {
  256. return false
  257. }
  258. if *r1.URL != *r2.URL {
  259. return false
  260. }
  261. return true
  262. }