11002: Merge branch 'master' into 11002-arvput-crash-fix
[arvados.git] / services / ws / permission.go
1 package main
2
3 import (
4         "net/http"
5         "net/url"
6         "time"
7
8         "git.curoverse.com/arvados.git/sdk/go/arvados"
9 )
10
11 const (
12         maxPermCacheAge = time.Hour
13         minPermCacheAge = 5 * time.Minute
14 )
15
16 type permChecker interface {
17         SetToken(token string)
18         Check(uuid string) (bool, error)
19 }
20
21 func newPermChecker(ac arvados.Client) permChecker {
22         ac.AuthToken = ""
23         return &cachingPermChecker{
24                 Client:     &ac,
25                 cache:      make(map[string]cacheEnt),
26                 maxCurrent: 16,
27         }
28 }
29
30 type cacheEnt struct {
31         time.Time
32         allowed bool
33 }
34
35 type cachingPermChecker struct {
36         *arvados.Client
37         cache      map[string]cacheEnt
38         maxCurrent int
39 }
40
41 func (pc *cachingPermChecker) SetToken(token string) {
42         pc.Client.AuthToken = token
43 }
44
45 func (pc *cachingPermChecker) Check(uuid string) (bool, error) {
46         logger := logger(nil).
47                 WithField("token", pc.Client.AuthToken).
48                 WithField("uuid", uuid)
49         pc.tidy()
50         now := time.Now()
51         if perm, ok := pc.cache[uuid]; ok && now.Sub(perm.Time) < maxPermCacheAge {
52                 logger.WithField("allowed", perm.allowed).Debug("cache hit")
53                 return perm.allowed, nil
54         }
55         var buf map[string]interface{}
56         path, err := pc.PathForUUID("get", uuid)
57         if err != nil {
58                 return false, err
59         }
60         err = pc.RequestAndDecode(&buf, "GET", path, nil, url.Values{
61                 "select": {`["uuid"]`},
62         })
63
64         var allowed bool
65         if err == nil {
66                 allowed = true
67         } else if txErr, ok := err.(*arvados.TransactionError); ok && txErr.StatusCode == http.StatusNotFound {
68                 allowed = false
69         } else if txErr.StatusCode == http.StatusForbidden {
70                 // Some requests are expressly forbidden for reasons
71                 // other than "you aren't allowed to know whether this
72                 // UUID exists" (404).
73                 allowed = false
74         } else {
75                 logger.WithError(err).Error("lookup error")
76                 return false, err
77         }
78         logger.WithField("allowed", allowed).Debug("cache miss")
79         pc.cache[uuid] = cacheEnt{Time: now, allowed: allowed}
80         return allowed, nil
81 }
82
83 func (pc *cachingPermChecker) tidy() {
84         if len(pc.cache) <= pc.maxCurrent*2 {
85                 return
86         }
87         tooOld := time.Now().Add(-minPermCacheAge)
88         for uuid, t := range pc.cache {
89                 if t.Before(tooOld) {
90                         delete(pc.cache, uuid)
91                 }
92         }
93         pc.maxCurrent = len(pc.cache)
94 }