5824: Merge branch 'master' into 5824-go-sdk
[arvados.git] / services / arv-git-httpd / auth_handler.go
1 package main
2
3 import (
4         "log"
5         "net/http"
6         "net/http/cgi"
7         "os"
8         "strings"
9         "time"
10
11         "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
12         "git.curoverse.com/arvados.git/sdk/go/auth"
13         "git.curoverse.com/arvados.git/sdk/go/httpserver"
14 )
15
16 var clientPool = arvadosclient.MakeClientPool()
17
18 type authHandler struct {
19         handler *cgi.Handler
20 }
21
22 func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
23         var statusCode int
24         var statusText string
25         var apiToken string
26         var repoName string
27         var validApiToken bool
28
29         w := httpserver.WrapResponseWriter(wOrig)
30
31         defer func() {
32                 if w.WroteStatus() == 0 {
33                         // Nobody has called WriteHeader yet: that
34                         // must be our job.
35                         w.WriteHeader(statusCode)
36                         w.Write([]byte(statusText))
37                 }
38
39                 // If the given password is a valid token, log the first 10 characters of the token.
40                 // Otherwise: log the string <invalid> if a password is given, else an empty string.
41                 passwordToLog := ""
42                 if !validApiToken {
43                         if len(apiToken) > 0 {
44                                 passwordToLog = "<invalid>"
45                         }
46                 } else {
47                         passwordToLog = apiToken[0:10]
48                 }
49
50                 httpserver.Log(r.RemoteAddr, passwordToLog, w.WroteStatus(), statusText, repoName, r.Method, r.URL.Path)
51         }()
52
53         creds := auth.NewCredentialsFromHTTPRequest(r)
54         if len(creds.Tokens) == 0 {
55                 statusCode, statusText = http.StatusUnauthorized, "no credentials provided"
56                 w.Header().Add("WWW-Authenticate", "Basic realm=\"git\"")
57                 return
58         }
59         apiToken = creds.Tokens[0]
60
61         // Access to paths "/foo/bar.git/*" and "/foo/bar/.git/*" are
62         // protected by the permissions on the repository named
63         // "foo/bar".
64         pathParts := strings.SplitN(r.URL.Path[1:], ".git/", 2)
65         if len(pathParts) != 2 {
66                 statusCode, statusText = http.StatusBadRequest, "bad request"
67                 return
68         }
69         repoName = pathParts[0]
70         repoName = strings.TrimRight(repoName, "/")
71
72         arv := clientPool.Get()
73         if arv == nil {
74                 statusCode, statusText = http.StatusInternalServerError, "connection pool failed: "+clientPool.Err().Error()
75                 return
76         }
77         defer clientPool.Put(arv)
78
79         // Ask API server whether the repository is readable using
80         // this token (by trying to read it!)
81         arv.ApiToken = apiToken
82         reposFound := arvadosclient.Dict{}
83         if err := arv.List("repositories", arvadosclient.Dict{
84                 "filters": [][]string{{"name", "=", repoName}},
85         }, &reposFound); err != nil {
86                 statusCode, statusText = http.StatusInternalServerError, err.Error()
87                 return
88         }
89         validApiToken = true
90         if avail, ok := reposFound["items_available"].(float64); !ok {
91                 statusCode, statusText = http.StatusInternalServerError, "bad list response from API"
92                 return
93         } else if avail < 1 {
94                 statusCode, statusText = http.StatusNotFound, "not found"
95                 return
96         } else if avail > 1 {
97                 statusCode, statusText = http.StatusInternalServerError, "name collision"
98                 return
99         }
100
101         repoUUID := reposFound["items"].([]interface{})[0].(map[string]interface{})["uuid"].(string)
102
103         isWrite := strings.HasSuffix(r.URL.Path, "/git-receive-pack")
104         if !isWrite {
105                 statusText = "read"
106         } else {
107                 err := arv.Update("repositories", repoUUID, arvadosclient.Dict{
108                         "repository": arvadosclient.Dict{
109                                 "modified_at": time.Now().String(),
110                         },
111                 }, &arvadosclient.Dict{})
112                 if err != nil {
113                         statusCode, statusText = http.StatusForbidden, err.Error()
114                         return
115                 }
116                 statusText = "write"
117         }
118
119         // Regardless of whether the client asked for "/foo.git" or
120         // "/foo/.git", we choose whichever variant exists in our repo
121         // root, and we try {uuid}.git and {uuid}/.git first. If none
122         // of these exist, we 404 even though the API told us the repo
123         // _should_ exist (presumably this means the repo was just
124         // created, and gitolite sync hasn't run yet).
125         rewrittenPath := ""
126         tryDirs := []string{
127                 "/" + repoUUID + ".git",
128                 "/" + repoUUID + "/.git",
129                 "/" + repoName + ".git",
130                 "/" + repoName + "/.git",
131         }
132         for _, dir := range tryDirs {
133                 if fileInfo, err := os.Stat(theConfig.Root + dir); err != nil {
134                         if !os.IsNotExist(err) {
135                                 statusCode, statusText = http.StatusInternalServerError, err.Error()
136                                 return
137                         }
138                 } else if fileInfo.IsDir() {
139                         rewrittenPath = dir + "/" + pathParts[1]
140                         break
141                 }
142         }
143         if rewrittenPath == "" {
144                 log.Println("WARNING:", repoUUID,
145                         "git directory not found in", theConfig.Root, tryDirs)
146                 // We say "content not found" to disambiguate from the
147                 // earlier "API says that repo does not exist" error.
148                 statusCode, statusText = http.StatusNotFound, "content not found"
149                 return
150         }
151         r.URL.Path = rewrittenPath
152
153         handlerCopy := *h.handler
154         handlerCopy.Env = append(handlerCopy.Env, "REMOTE_USER="+r.RemoteAddr) // Should be username
155         handlerCopy.ServeHTTP(&w, r)
156 }