Go · 2166 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package policy
4
5 import (
6 "context"
7 "sync"
8 )
9
10 // cacheKey is the per-(actor, repo) composite the cache memoizes
11 // effective role lookups against. The cache is request-scoped — created
12 // in WithCache, dropped when the context goes out of scope.
13 type cacheKey struct {
14 actorUserID int64
15 repoID int64
16 }
17
18 // requestCache holds the memo. The lock is held across DB lookups so
19 // concurrent goroutines reading the same key (rare in practice — a
20 // single request pipeline is typically sequential) coalesce into one
21 // query.
22 type requestCache struct {
23 mu sync.Mutex
24 roles map[cacheKey]Role
25 }
26
27 type cacheCtxKey struct{}
28
29 // WithCache returns a derived context that carries a per-request memo.
30 // Wire this into the request middleware once, and every Can() call on
31 // downstream handlers benefits from de-duplication. Calling Can with a
32 // context that has no cache is supported — the lookup just runs every
33 // time.
34 func WithCache(ctx context.Context) context.Context {
35 c := &requestCache{roles: make(map[cacheKey]Role, 4)}
36 return context.WithValue(ctx, cacheCtxKey{}, c)
37 }
38
39 // cacheFromContext returns the cache or nil. Internal helper — Can()
40 // degrades gracefully when nil.
41 func cacheFromContext(ctx context.Context) *requestCache {
42 c, _ := ctx.Value(cacheCtxKey{}).(*requestCache)
43 return c
44 }
45
46 // InvalidateRepo drops cached entries for one repo. Call this from
47 // handlers that mutate collaborator state and then re-check policy
48 // inside the same request.
49 func InvalidateRepo(ctx context.Context, repoID int64) {
50 c := cacheFromContext(ctx)
51 if c == nil {
52 return
53 }
54 c.mu.Lock()
55 defer c.mu.Unlock()
56 for k := range c.roles {
57 if k.repoID == repoID {
58 delete(c.roles, k)
59 }
60 }
61 }
62
63 // cacheGet returns (role, true) when present.
64 func cacheGet(c *requestCache, k cacheKey) (Role, bool) {
65 if c == nil {
66 return RoleNone, false
67 }
68 c.mu.Lock()
69 defer c.mu.Unlock()
70 r, ok := c.roles[k]
71 return r, ok
72 }
73
74 // cachePut stores a role. Safe to call with c == nil.
75 func cachePut(c *requestCache, k cacheKey, r Role) {
76 if c == nil {
77 return
78 }
79 c.mu.Lock()
80 defer c.mu.Unlock()
81 c.roles[k] = r
82 }
83