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