Key Access Gate positive cache by user and Required Groups set.

Avoids reusing an allow decision after the configured group list changes on a live gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Konrad Neitzel
2026-08-27 17:14:07 +02:00
co-authored by Cursor
parent 3980263146
commit 92d8efea47
2 changed files with 51 additions and 16 deletions
+22 -16
View File
@@ -81,8 +81,9 @@ func (g *AccessGate) Check(r *http.Request) CheckResult {
}
func (g *AccessGate) memberOfRequired(userID string) (bool, error) {
key := g.cacheKey(userID)
if g.CacheTTL > 0 {
if g.cachedAllowed(userID) {
if g.cachedAllowed(key) {
return true, nil
}
}
@@ -94,7 +95,7 @@ func (g *AccessGate) memberOfRequired(userID string) (bool, error) {
for _, have := range groups {
if have == need {
if g.CacheTTL > 0 {
g.storeAllowed(userID)
g.storeAllowed(key)
}
return true, nil
}
@@ -103,6 +104,10 @@ func (g *AccessGate) memberOfRequired(userID string) (bool, error) {
return false, nil
}
func (g *AccessGate) cacheKey(userID string) string {
return userID + "\x00" + strings.Join(g.Groups, "\x00")
}
func (g *AccessGate) fetchUserGroups(userID string) ([]string, error) {
ocs := g.ocsClient(userID)
path := "cloud/users/" + url.PathEscape(userID) + "/groups"
@@ -139,24 +144,24 @@ func decodeUserGroups(raw []byte) ([]string, error) {
return parsed.OCS.Data.Groups, nil
}
func (g *AccessGate) cachedAllowed(userID string) bool {
func (g *AccessGate) cachedAllowed(key string) bool {
g.mu.Lock()
defer g.mu.Unlock()
ent, ok := g.cache[userID]
ent, ok := g.cache[key]
if !ok {
return false
}
if g.Now().After(ent.until) {
delete(g.cache, userID)
delete(g.cache, key)
return false
}
return true
}
func (g *AccessGate) storeAllowed(userID string) {
func (g *AccessGate) storeAllowed(key string) {
g.mu.Lock()
defer g.mu.Unlock()
g.cache[userID] = cacheEntry{until: g.Now().Add(g.CacheTTL)}
g.cache[key] = cacheEntry{until: g.Now().Add(g.CacheTTL)}
}
func (g AccessGate) normalized() *AccessGate {
@@ -185,27 +190,28 @@ func acceptsHTML(accept string) bool {
}
func (g *AccessGate) shouldSkip(path string) bool {
path = strings.TrimSuffix(path, "/")
if path == "" {
path = "/"
}
path = normalizeGatePath(path)
for _, p := range defaultSkipPaths {
if path == p {
return true
}
}
for _, p := range g.ExtraSkipPaths {
p = strings.TrimSuffix(p, "/")
if p == "" {
p = "/"
}
if path == p {
if path == normalizeGatePath(p) {
return true
}
}
return false
}
func normalizeGatePath(path string) string {
path = strings.TrimSuffix(path, "/")
if path == "" {
return "/"
}
return path
}
var defaultSkipPaths = []string{"/heartbeat", "/enabled", "/init"}
var deniedHTML = []byte(`<!DOCTYPE html>