Merge pull request #2325 from potatogim/feat/iam-cache-fresh

auth: add GetUserAccountFresh to bypass the IAM cache
This commit is contained in:
Ben McClelland
2026-08-31 12:37:57 -07:00
committed by GitHub
2 changed files with 85 additions and 0 deletions
+15
View File
@@ -177,6 +177,21 @@ func (c *IAMCache) ResolveAccounts(accessKeyIDs []string) ([]string, error) {
return resolveAccountsByLookup(accessKeyIDs, c.GetUserAccount)
}
// GetUserAccountFresh bypasses the in-memory cache and fetches the
// account directly from the underlying IAM service, then refreshes
// the cached entry with the result. This gives callers that need
// revocation to take effect immediately (rather than after the
// cache TTL) a way to observe the backing store state.
func (c *IAMCache) GetUserAccountFresh(access string) (Account, error) {
a, err := c.service.GetUserAccount(access)
if err != nil {
return Account{}, err
}
c.iamcache.set(access, a)
return a, nil
}
// DeleteUserAccount deletes account from IAM service and cache
func (c *IAMCache) DeleteUserAccount(access string) error {
err := c.service.DeleteUserAccount(access)
+70
View File
@@ -0,0 +1,70 @@
package auth
import (
"testing"
"time"
)
type freshTestService struct {
IAMService
accounts map[string]Account
fetches int
}
func (s *freshTestService) GetUserAccount(access string) (Account, error) {
s.fetches++
a, ok := s.accounts[access]
if !ok {
return Account{}, ErrNoSuchUser
}
return a, nil
}
// GetUserAccountFresh must bypass a cached-but-stale entry and
// reflect the backing service state, refreshing the cache.
func TestIAMCacheGetUserAccountFresh(t *testing.T) {
svc := &freshTestService{accounts: map[string]Account{
"acct": {Access: "acct", Secret: "old", Role: RoleUser},
}}
c := NewCache(svc, time.Minute, time.Hour)
defer c.Shutdown()
// Prime the cache with the old secret.
if _, err := c.GetUserAccount("acct"); err != nil {
t.Fatalf("prime: %v", err)
}
// Change the backing store behind the cache.
svc.accounts["acct"] = Account{Access: "acct", Secret: "new", Role: RoleAdmin}
// Cached read still serves the stale entry.
got, err := c.GetUserAccount("acct")
if err != nil {
t.Fatalf("cached read: %v", err)
}
if got.Secret != "old" {
t.Fatalf("cached read: expected stale secret, got %q", got.Secret)
}
// Fresh read must observe the update.
got, err = c.GetUserAccountFresh("acct")
if err != nil {
t.Fatalf("fresh read: %v", err)
}
if got.Secret != "new" || got.Role != RoleAdmin {
t.Fatalf("fresh read: got %+v", got)
}
// And the cache is refreshed as a side effect.
got, err = c.GetUserAccount("acct")
if err != nil {
t.Fatalf("post-fresh cached read: %v", err)
}
if got.Secret != "new" {
t.Fatalf("post-fresh cached read: cache not refreshed, got %q", got.Secret)
}
if svc.fetches != 2 {
t.Fatalf("expected 2 backing fetches (prime + fresh), got %d", svc.fetches)
}
}