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