diff --git a/auth/iam_cache.go b/auth/iam_cache.go index 35e67022..e1e68f1e 100644 --- a/auth/iam_cache.go +++ b/auth/iam_cache.go @@ -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) diff --git a/auth/iam_cache_test.go b/auth/iam_cache_test.go new file mode 100644 index 00000000..74cbc4fb --- /dev/null +++ b/auth/iam_cache_test.go @@ -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) + } +}