Merge branch '8784-dir-listings'
[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                 "select": {`["uuid"]`},
78         })
79
80         var allowed bool
81         if err == nil {
82                 allowed = true
83         } else if txErr, ok := err.(*arvados.TransactionError); ok && pc.isNotAllowed(txErr.StatusCode) {
84                 allowed = false
85         } else {
86                 logger.WithError(err).Error("lookup error")
87                 return false, err
88         }
89         logger.WithField("allowed", allowed).Debug("cache miss")
90         pc.cache[uuid] = cacheEnt{Time: now, allowed: allowed}
91         return allowed, nil
92 }
93
94 func (pc *cachingPermChecker) isNotAllowed(status int) bool {
95         switch status {
96         case http.StatusForbidden, http.StatusUnauthorized, http.StatusNotFound:
97                 return true
98         default:
99                 return false
100         }
101 }
102
103 func (pc *cachingPermChecker) tidy() {
104         if len(pc.cache) <= pc.maxCurrent*2 {
105                 return
106         }
107         tooOld := time.Now().Add(-minPermCacheAge)
108         for uuid, t := range pc.cache {
109                 if t.Before(tooOld) {
110                         delete(pc.cache, uuid)
111                 }
112         }
113         pc.maxCurrent = len(pc.cache)
114 }