11557: Merge branch 'master' into 11557-acr-output-col-perms
[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         nChecks  uint64
41         nMisses  uint64
42         nInvalid uint64
43 }
44
45 func (pc *cachingPermChecker) SetToken(token string) {
46         if pc.Client.AuthToken == token {
47                 return
48         }
49         pc.Client.AuthToken = token
50         pc.cache = make(map[string]cacheEnt)
51 }
52
53 func (pc *cachingPermChecker) Check(uuid string) (bool, error) {
54         pc.nChecks++
55         logger := logger(nil).
56                 WithField("token", pc.Client.AuthToken).
57                 WithField("uuid", uuid)
58         pc.tidy()
59         now := time.Now()
60         if perm, ok := pc.cache[uuid]; ok && now.Sub(perm.Time) < maxPermCacheAge {
61                 logger.WithField("allowed", perm.allowed).Debug("cache hit")
62                 return perm.allowed, nil
63         }
64         var buf map[string]interface{}
65         path, err := pc.PathForUUID("get", uuid)
66         if err != nil {
67                 pc.nInvalid++
68                 return false, err
69         }
70
71         pc.nMisses++
72         err = pc.RequestAndDecode(&buf, "GET", path, nil, url.Values{
73                 "select": {`["uuid"]`},
74         })
75
76         var allowed bool
77         if err == nil {
78                 allowed = true
79         } else if txErr, ok := err.(*arvados.TransactionError); ok && pc.isNotAllowed(txErr.StatusCode) {
80                 allowed = false
81         } else {
82                 logger.WithError(err).Error("lookup error")
83                 return false, err
84         }
85         logger.WithField("allowed", allowed).Debug("cache miss")
86         pc.cache[uuid] = cacheEnt{Time: now, allowed: allowed}
87         return allowed, nil
88 }
89
90 func (pc *cachingPermChecker) isNotAllowed(status int) bool {
91         switch status {
92         case http.StatusForbidden, http.StatusUnauthorized, http.StatusNotFound:
93                 return true
94         default:
95                 return false
96         }
97 }
98
99 func (pc *cachingPermChecker) tidy() {
100         if len(pc.cache) <= pc.maxCurrent*2 {
101                 return
102         }
103         tooOld := time.Now().Add(-minPermCacheAge)
104         for uuid, t := range pc.cache {
105                 if t.Before(tooOld) {
106                         delete(pc.cache, uuid)
107                 }
108         }
109         pc.maxCurrent = len(pc.cache)
110 }