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