1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
16 "git.arvados.org/arvados.git/sdk/go/arvados"
17 "git.arvados.org/arvados.git/sdk/go/arvadosclient"
18 "git.arvados.org/arvados.git/sdk/go/auth"
19 "git.arvados.org/arvados.git/sdk/go/httpserver"
20 "github.com/sirupsen/logrus"
23 type authHandler struct {
25 clientPool *arvadosclient.ClientPool
26 cluster *arvados.Cluster
29 func (h *authHandler) CheckHealth() error {
33 func (h *authHandler) Done() <-chan struct{} {
37 func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
42 w := httpserver.WrapResponseWriter(wOrig)
44 if r.Method == "OPTIONS" {
45 method := r.Header.Get("Access-Control-Request-Method")
46 if method != "GET" && method != "POST" {
47 w.WriteHeader(http.StatusMethodNotAllowed)
50 w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
51 w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
52 w.Header().Set("Access-Control-Allow-Origin", "*")
53 w.Header().Set("Access-Control-Max-Age", "86400")
54 w.WriteHeader(http.StatusOK)
58 if r.Header.Get("Origin") != "" {
59 // Allow simple cross-origin requests without user
60 // credentials ("user credentials" as defined by CORS,
61 // i.e., cookies, HTTP authentication, and client-side
62 // SSL certificates. See
63 // http://www.w3.org/TR/cors/#user-credentials).
64 w.Header().Set("Access-Control-Allow-Origin", "*")
68 if w.WroteStatus() == 0 {
69 // Nobody has called WriteHeader yet: that
71 w.WriteHeader(statusCode)
72 if statusCode >= 400 {
73 w.Write([]byte(statusText))
78 creds := auth.CredentialsFromRequest(r)
79 if len(creds.Tokens) == 0 {
80 statusCode, statusText = http.StatusUnauthorized, "no credentials provided"
81 w.Header().Add("WWW-Authenticate", "Basic realm=\"git\"")
84 apiToken = creds.Tokens[0]
86 // Access to paths "/foo/bar.git/*" and "/foo/bar/.git/*" are
87 // protected by the permissions on the repository named
89 pathParts := strings.SplitN(r.URL.Path[1:], ".git/", 2)
90 if len(pathParts) != 2 {
91 statusCode, statusText = http.StatusNotFound, "not found"
94 repoName := pathParts[0]
95 repoName = strings.TrimRight(repoName, "/")
96 httpserver.SetResponseLogFields(r.Context(), logrus.Fields{
100 arv := h.clientPool.Get()
102 statusCode, statusText = http.StatusInternalServerError, "connection pool failed: "+h.clientPool.Err().Error()
105 defer h.clientPool.Put(arv)
107 // Log the UUID if the supplied token is a v2 token, otherwise
108 // just the last five characters.
109 httpserver.SetResponseLogFields(r.Context(), logrus.Fields{
110 "tokenUUID": func() string {
111 if strings.HasPrefix(apiToken, "v2/") && strings.IndexRune(apiToken[3:], '/') == 27 {
112 // UUID part of v2 token
113 return apiToken[3:30]
114 } else if len(apiToken) > 5 {
115 return "[...]" + apiToken[len(apiToken)-5:]
122 // Ask API server whether the repository is readable using
123 // this token (by trying to read it!)
124 arv.ApiToken = apiToken
125 repoUUID, err := h.lookupRepo(arv, repoName)
127 statusCode, statusText = http.StatusInternalServerError, err.Error()
131 statusCode, statusText = http.StatusNotFound, "not found"
135 isWrite := strings.HasSuffix(r.URL.Path, "/git-receive-pack")
139 err := arv.Update("repositories", repoUUID, arvadosclient.Dict{
140 "repository": arvadosclient.Dict{
141 "modified_at": time.Now().String(),
143 }, &arvadosclient.Dict{})
145 statusCode, statusText = http.StatusForbidden, err.Error()
151 // Regardless of whether the client asked for "/foo.git" or
152 // "/foo/.git", we choose whichever variant exists in our repo
153 // root, and we try {uuid}.git and {uuid}/.git first. If none
154 // of these exist, we 404 even though the API told us the repo
155 // _should_ exist (presumably this means the repo was just
156 // created, and gitolite sync hasn't run yet).
159 "/" + repoUUID + ".git",
160 "/" + repoUUID + "/.git",
161 "/" + repoName + ".git",
162 "/" + repoName + "/.git",
164 for _, dir := range tryDirs {
165 if fileInfo, err := os.Stat(h.cluster.Git.Repositories + dir); err != nil {
166 if !os.IsNotExist(err) {
167 statusCode, statusText = http.StatusInternalServerError, err.Error()
170 } else if fileInfo.IsDir() {
171 rewrittenPath = dir + "/" + pathParts[1]
175 if rewrittenPath == "" {
176 log.Println("WARNING:", repoUUID,
177 "git directory not found in", h.cluster.Git.Repositories, tryDirs)
178 // We say "content not found" to disambiguate from the
179 // earlier "API says that repo does not exist" error.
180 statusCode, statusText = http.StatusNotFound, "content not found"
183 r.URL.Path = rewrittenPath
185 h.handler.ServeHTTP(w, r)
188 var uuidRegexp = regexp.MustCompile(`^[0-9a-z]{5}-s0uqq-[0-9a-z]{15}$`)
190 func (h *authHandler) lookupRepo(arv *arvadosclient.ArvadosClient, repoName string) (string, error) {
191 reposFound := arvadosclient.Dict{}
193 if uuidRegexp.MatchString(repoName) {
198 err := arv.List("repositories", arvadosclient.Dict{
199 "filters": [][]string{{column, "=", repoName}},
203 } else if avail, ok := reposFound["items_available"].(float64); !ok {
204 return "", errors.New("bad list response from API")
205 } else if avail < 1 {
207 } else if avail > 1 {
208 return "", errors.New("name collision")
210 return reposFound["items"].([]interface{})[0].(map[string]interface{})["uuid"].(string), nil