14860: Exposes the bug with a new test.
[arvados.git] / services / ws / permission.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "net/http"
9         "net/url"
10         "time"
11
12         "git.curoverse.com/arvados.git/sdk/go/arvados"
13 )
14
15 const (
16         maxPermCacheAge = time.Hour
17         minPermCacheAge = 5 * time.Minute
18 )
19
20 type permChecker interface {
21         SetToken(token string)
22         Check(uuid string) (bool, error)
23 }
24
25 func newPermChecker(ac arvados.Client) permChecker {
26         ac.AuthToken = ""
27         return &cachingPermChecker{
28                 Client:     &ac,
29                 cache:      make(map[string]cacheEnt),
30                 maxCurrent: 16,
31         }
32 }
33
34 type cacheEnt struct {
35         time.Time
36         allowed bool
37 }
38
39 type cachingPermChecker struct {
40         *arvados.Client
41         cache      map[string]cacheEnt
42         maxCurrent int
43
44         nChecks  uint64
45         nMisses  uint64
46         nInvalid uint64
47 }
48
49 func (pc *cachingPermChecker) SetToken(token string) {
50         if pc.Client.AuthToken == token {
51                 return
52         }
53         pc.Client.AuthToken = token
54         pc.cache = make(map[string]cacheEnt)
55 }
56
57 func (pc *cachingPermChecker) Check(uuid string) (bool, error) {
58         pc.nChecks++
59         logger := logger(nil).
60                 WithField("token", pc.Client.AuthToken).
61                 WithField("uuid", uuid)
62         pc.tidy()
63         now := time.Now()
64         if perm, ok := pc.cache[uuid]; ok && now.Sub(perm.Time) < maxPermCacheAge {
65                 logger.WithField("allowed", perm.allowed).Debug("cache hit")
66                 return perm.allowed, nil
67         }
68         var buf map[string]interface{}
69         path, err := pc.PathForUUID("get", uuid)
70         if err != nil {
71                 pc.nInvalid++
72                 return false, err
73         }
74
75         pc.nMisses++
76         err = pc.RequestAndDecode(&buf, "GET", path, nil, url.Values{
77                 "include_trash": {"true"},
78                 "select":        {`["uuid"]`},
79         })
80
81         var allowed bool
82         if err == nil {
83                 allowed = true
84         } else if txErr, ok := err.(*arvados.TransactionError); ok && pc.isNotAllowed(txErr.StatusCode) {
85                 allowed = false
86         } else {
87                 logger.WithError(err).Error("lookup error")
88                 return false, err
89         }
90         logger.WithField("allowed", allowed).Debug("cache miss")
91         pc.cache[uuid] = cacheEnt{Time: now, allowed: allowed}
92         return allowed, nil
93 }
94
95 func (pc *cachingPermChecker) isNotAllowed(status int) bool {
96         switch status {
97         case http.StatusForbidden, http.StatusUnauthorized, http.StatusNotFound:
98                 return true
99         default:
100                 return false
101         }
102 }
103
104 func (pc *cachingPermChecker) tidy() {
105         if len(pc.cache) <= pc.maxCurrent*2 {
106                 return
107         }
108         tooOld := time.Now().Add(-minPermCacheAge)
109         for uuid, t := range pc.cache {
110                 if t.Before(tooOld) {
111                         delete(pc.cache, uuid)
112                 }
113         }
114         pc.maxCurrent = len(pc.cache)
115 }