Imported Upstream version 2.4.1
[scm/test.git] / lfsapi / auth.go
1 package lfsapi
2
3 import (
4         "encoding/base64"
5         "fmt"
6         "net"
7         "net/http"
8         "net/url"
9         "os"
10         "strings"
11
12         "github.com/bgentry/go-netrc/netrc"
13         "github.com/git-lfs/git-lfs/errors"
14         "github.com/rubyist/tracerx"
15 )
16
17 var (
18         defaultCredentialHelper = &commandCredentialHelper{}
19         defaultNetrcFinder      = &noFinder{}
20         defaultEndpointFinder   = NewEndpointFinder(nil)
21 )
22
23 // DoWithAuth sends an HTTP request to get an HTTP response. It attempts to add
24 // authentication from netrc or git's credential helpers if necessary,
25 // supporting basic and ntlm authentication.
26 func (c *Client) DoWithAuth(remote string, req *http.Request) (*http.Response, error) {
27         req.Header = c.extraHeadersFor(req)
28
29         apiEndpoint, access, credHelper, credsURL, creds, err := c.getCreds(remote, req)
30         if err != nil {
31                 return nil, err
32         }
33
34         res, err := c.doWithCreds(req, credHelper, creds, credsURL, access)
35         if err != nil {
36                 if errors.IsAuthError(err) {
37                         newAccess := getAuthAccess(res)
38                         if newAccess != access {
39                                 c.Endpoints.SetAccess(apiEndpoint.Url, newAccess)
40                         }
41
42                         if creds != nil || (access == NoneAccess && len(req.Header.Get("Authorization")) == 0) {
43                                 tracerx.Printf("api: http response indicates %q authentication. Resubmitting...", newAccess)
44                                 if creds != nil {
45                                         req.Header.Del("Authorization")
46                                         credHelper.Reject(creds)
47                                 }
48                                 return c.DoWithAuth(remote, req)
49                         }
50                 }
51         }
52
53         if res != nil && res.StatusCode < 300 && res.StatusCode > 199 {
54                 credHelper.Approve(creds)
55         }
56
57         return res, err
58 }
59
60 func (c *Client) doWithCreds(req *http.Request, credHelper CredentialHelper, creds Creds, credsURL *url.URL, access Access) (*http.Response, error) {
61         if access == NTLMAccess {
62                 return c.doWithNTLM(req, credHelper, creds, credsURL)
63         }
64         return c.do(req)
65 }
66
67 // getCreds fills the authorization header for the given request if possible,
68 // from the following sources:
69 //
70 // 1. NTLM access is handled elsewhere.
71 // 2. Existing Authorization or ?token query tells LFS that the request is ready.
72 // 3. Netrc based on the hostname.
73 // 4. URL authentication on the Endpoint URL or the Git Remote URL.
74 // 5. Git Credential Helper, potentially prompting the user.
75 //
76 // There are three URLs in play, that make this a little confusing.
77 //
78 // 1. The request URL, which should be something like "https://git.com/repo.git/info/lfs/objects/batch"
79 // 2. The LFS API URL, which should be something like "https://git.com/repo.git/info/lfs"
80 //    This URL used for the "lfs.URL.access" git config key, which determines
81 //    what kind of auth the LFS server expects. Could be BasicAccess, NTLMAccess,
82 //    or NoneAccess, in which the Git Credential Helper step is skipped. We do
83 //    not want to prompt the user for a password to fetch public repository data.
84 // 3. The Git Remote URL, which should be something like "https://git.com/repo.git"
85 //    This URL is used for the Git Credential Helper. This way existing https
86 //    Git remote credentials can be re-used for LFS.
87 func (c *Client) getCreds(remote string, req *http.Request) (Endpoint, Access, CredentialHelper, *url.URL, Creds, error) {
88         ef := c.Endpoints
89         if ef == nil {
90                 ef = defaultEndpointFinder
91         }
92
93         netrcFinder := c.Netrc
94         if netrcFinder == nil {
95                 netrcFinder = defaultNetrcFinder
96         }
97
98         operation := getReqOperation(req)
99         apiEndpoint := ef.Endpoint(operation, remote)
100         access := ef.AccessFor(apiEndpoint.Url)
101
102         if access != NTLMAccess {
103                 if requestHasAuth(req) || setAuthFromNetrc(netrcFinder, req) || access == NoneAccess {
104                         return apiEndpoint, access, nullCreds, nil, nil, nil
105                 }
106
107                 credsURL, err := getCredURLForAPI(ef, operation, remote, apiEndpoint, req)
108                 if err != nil {
109                         return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds")
110                 }
111
112                 if credsURL == nil {
113                         return apiEndpoint, access, nullCreds, nil, nil, nil
114                 }
115
116                 credHelper, creds, err := c.getGitCreds(ef, req, credsURL)
117                 if err == nil {
118                         tracerx.Printf("Filled credentials for %s", credsURL)
119                         setRequestAuth(req, creds["username"], creds["password"])
120                 }
121                 return apiEndpoint, access, credHelper, credsURL, creds, err
122         }
123
124         // NTLM ONLY
125
126         credsURL, err := url.Parse(apiEndpoint.Url)
127         if err != nil {
128                 return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds")
129         }
130
131         if netrcMachine := getAuthFromNetrc(netrcFinder, req); netrcMachine != nil {
132                 creds := Creds{
133                         "protocol": credsURL.Scheme,
134                         "host":     credsURL.Host,
135                         "username": netrcMachine.Login,
136                         "password": netrcMachine.Password,
137                         "source":   "netrc",
138                 }
139
140                 return apiEndpoint, access, nullCreds, credsURL, creds, nil
141         }
142
143         // NTLM uses creds to create the session
144         credHelper, creds, err := c.getGitCreds(ef, req, credsURL)
145         return apiEndpoint, access, credHelper, credsURL, creds, err
146 }
147
148 func (c *Client) getGitCreds(ef EndpointFinder, req *http.Request, u *url.URL) (CredentialHelper, Creds, error) {
149         credHelper, input := c.getCredentialHelper(u)
150         creds, err := credHelper.Fill(input)
151         if creds == nil || len(creds) < 1 {
152                 errmsg := fmt.Sprintf("Git credentials for %s not found", u)
153                 if err != nil {
154                         errmsg = errmsg + ":\n" + err.Error()
155                 } else {
156                         errmsg = errmsg + "."
157                 }
158                 err = errors.New(errmsg)
159         }
160
161         return credHelper, creds, err
162 }
163
164 func getAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) *netrc.Machine {
165         hostname := req.URL.Host
166         var host string
167
168         if strings.Contains(hostname, ":") {
169                 var err error
170                 host, _, err = net.SplitHostPort(hostname)
171                 if err != nil {
172                         tracerx.Printf("netrc: error parsing %q: %s", hostname, err)
173                         return nil
174                 }
175         } else {
176                 host = hostname
177         }
178
179         return netrcFinder.FindMachine(host)
180 }
181
182 func setAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) bool {
183         if machine := getAuthFromNetrc(netrcFinder, req); machine != nil {
184                 setRequestAuth(req, machine.Login, machine.Password)
185                 return true
186         }
187
188         return false
189 }
190
191 func getCredURLForAPI(ef EndpointFinder, operation, remote string, apiEndpoint Endpoint, req *http.Request) (*url.URL, error) {
192         apiURL, err := url.Parse(apiEndpoint.Url)
193         if err != nil {
194                 return nil, err
195         }
196
197         // if the LFS request doesn't match the current LFS url, don't bother
198         // attempting to set the Authorization header from the LFS or Git remote URLs.
199         if req.URL.Scheme != apiURL.Scheme ||
200                 req.URL.Host != apiURL.Host {
201                 return req.URL, nil
202         }
203
204         if setRequestAuthFromURL(req, apiURL) {
205                 return nil, nil
206         }
207
208         if len(remote) > 0 {
209                 if u := ef.GitRemoteURL(remote, operation == "upload"); u != "" {
210                         schemedUrl, _ := prependEmptySchemeIfAbsent(u)
211
212                         gitRemoteURL, err := url.Parse(schemedUrl)
213                         if err != nil {
214                                 return nil, err
215                         }
216
217                         if gitRemoteURL.Scheme == apiURL.Scheme &&
218                                 gitRemoteURL.Host == apiURL.Host {
219
220                                 if setRequestAuthFromURL(req, gitRemoteURL) {
221                                         return nil, nil
222                                 }
223
224                                 return gitRemoteURL, nil
225                         }
226                 }
227         }
228
229         return apiURL, nil
230 }
231
232 // prependEmptySchemeIfAbsent prepends an empty scheme "//" if none was found in
233 // the URL in order to satisfy RFC 3986 §3.3, and `net/url.Parse()`.
234 //
235 // It returns a string parse-able with `net/url.Parse()` and a boolean whether
236 // or not an empty scheme was added.
237 func prependEmptySchemeIfAbsent(u string) (string, bool) {
238         if hasScheme(u) {
239                 return u, false
240         }
241
242         colon := strings.Index(u, ":")
243         slash := strings.Index(u, "/")
244
245         if colon >= 0 && (slash < 0 || colon < slash) {
246                 // First path segment has a colon, assumed that it's a
247                 // scheme-less URL. Append an empty scheme on top to
248                 // satisfy RFC 3986 §3.3, and `net/url.Parse()`.
249                 return fmt.Sprintf("//%s", u), true
250         }
251         return u, true
252 }
253
254 var (
255         // supportedSchemes is the list of URL schemes the `lfsapi` package
256         // supports.
257         supportedSchemes = []string{"ssh", "http", "https"}
258 )
259
260 // hasScheme returns whether or not a given string (taken to represent a RFC
261 // 3986 URL) has a scheme that is supported by the `lfsapi` package.
262 func hasScheme(what string) bool {
263         for _, scheme := range supportedSchemes {
264                 if strings.HasPrefix(what, fmt.Sprintf("%s://", scheme)) {
265                         return true
266                 }
267         }
268
269         return false
270 }
271
272 func requestHasAuth(req *http.Request) bool {
273         // The "Authorization" string constant is safe, since we assume that all
274         // request headers have been canonicalized.
275         if len(req.Header.Get("Authorization")) > 0 {
276                 return true
277         }
278
279         return len(req.URL.Query().Get("token")) > 0
280 }
281
282 func setRequestAuthFromURL(req *http.Request, u *url.URL) bool {
283         if u.User == nil {
284                 return false
285         }
286
287         if pass, ok := u.User.Password(); ok {
288                 fmt.Fprintln(os.Stderr, "warning: current Git remote contains credentials")
289                 setRequestAuth(req, u.User.Username(), pass)
290                 return true
291         }
292
293         return false
294 }
295
296 func setRequestAuth(req *http.Request, user, pass string) {
297         // better not be NTLM!
298         if len(user) == 0 && len(pass) == 0 {
299                 return
300         }
301
302         token := fmt.Sprintf("%s:%s", user, pass)
303         auth := "Basic " + strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(token)))
304         req.Header.Set("Authorization", auth)
305 }
306
307 func getReqOperation(req *http.Request) string {
308         operation := "download"
309         if req.Method == "POST" || req.Method == "PUT" {
310                 operation = "upload"
311         }
312         return operation
313 }
314
315 var (
316         authenticateHeaders = []string{"Lfs-Authenticate", "Www-Authenticate"}
317 )
318
319 func getAuthAccess(res *http.Response) Access {
320         for _, headerName := range authenticateHeaders {
321                 for _, auth := range res.Header[headerName] {
322                         pieces := strings.SplitN(strings.ToLower(auth), " ", 2)
323                         if len(pieces) == 0 {
324                                 continue
325                         }
326
327                         switch Access(pieces[0]) {
328                         case NegotiateAccess, NTLMAccess:
329                                 // When server sends Www-Authentication: Negotiate, it supports both Kerberos and NTLM.
330                                 // Since git-lfs current does not support Kerberos, we will return NTLM in this case.
331                                 return NTLMAccess
332                         }
333                 }
334         }
335
336         return BasicAccess
337 }