Merge branch '22155-layout-bugs'
[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         return &cachingPermChecker{
29                 ac:         ac,
30                 token:      "-",
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         ac         *arvados.Client
43         token      string
44         cache      map[string]cacheEnt
45         maxCurrent int
46
47         nChecks  uint64
48         nMisses  uint64
49         nInvalid uint64
50 }
51
52 func (pc *cachingPermChecker) SetToken(token string) {
53         if pc.token == token {
54                 return
55         }
56         pc.token = token
57         pc.cache = make(map[string]cacheEnt)
58 }
59
60 func (pc *cachingPermChecker) Check(ctx context.Context, uuid string) (bool, error) {
61         pc.nChecks++
62         logger := ctxlog.FromContext(ctx).
63                 WithField("token", pc.token).
64                 WithField("uuid", uuid)
65         pc.tidy()
66         now := time.Now()
67         if perm, ok := pc.cache[uuid]; ok && now.Sub(perm.Time) < maxPermCacheAge {
68                 logger.WithField("allowed", perm.allowed).Debug("cache hit")
69                 return perm.allowed, nil
70         }
71
72         path, err := pc.ac.PathForUUID("get", uuid)
73         if err != nil {
74                 pc.nInvalid++
75                 return false, err
76         }
77
78         pc.nMisses++
79         ctx = arvados.ContextWithAuthorization(ctx, "Bearer "+pc.token)
80         ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Minute))
81         defer cancel()
82         var buf map[string]interface{}
83         err = pc.ac.RequestAndDecodeContext(ctx, &buf, "GET", path, nil, url.Values{
84                 "include_trash": {"true"},
85                 "select":        {`["uuid"]`},
86         })
87
88         var allowed bool
89         if err == nil {
90                 allowed = true
91         } else if txErr, ok := err.(*arvados.TransactionError); ok && pc.isNotAllowed(txErr.StatusCode) {
92                 allowed = false
93         } else {
94                 // If "context deadline exceeded", "client
95                 // disconnected", HTTP 5xx, network error, etc., don't
96                 // cache the result.
97                 logger.WithError(err).Error("lookup error")
98                 return false, err
99         }
100         logger.WithField("allowed", allowed).Debug("cache miss")
101         pc.cache[uuid] = cacheEnt{Time: now, allowed: allowed}
102         return allowed, nil
103 }
104
105 func (pc *cachingPermChecker) isNotAllowed(status int) bool {
106         switch status {
107         case http.StatusForbidden, http.StatusUnauthorized, http.StatusNotFound:
108                 return true
109         default:
110                 return false
111         }
112 }
113
114 func (pc *cachingPermChecker) tidy() {
115         if len(pc.cache) <= pc.maxCurrent*2 {
116                 return
117         }
118         tooOld := time.Now().Add(-minPermCacheAge)
119         for uuid, t := range pc.cache {
120                 if t.Before(tooOld) {
121                         delete(pc.cache, uuid)
122                 }
123         }
124         pc.maxCurrent = len(pc.cache)
125 }