mirror of
https://github.com/versity/versitygw.git
synced 2026-09-21 07:24:29 +00:00
The cached GetUserAccount keeps serving a stale entry for the cache TTL after the backing IAM service changes, which delays credential revocation by up to the configured expiry. Add GetUserAccountFresh to the IAM cache, which reads directly from the underlying service and refreshes the cached entry with the result. Callers that need revocations to take effect immediately can use it instead of the cached path. Signed-off-by: Jihyeon Gim <potatogim@potatogim.net>
227 lines
5.4 KiB
Go
227 lines
5.4 KiB
Go
// Copyright 2023 Versity Software
|
|
// This file is licensed under the Apache License, Version 2.0
|
|
// (the "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// IAMCache is an in memory cache of the IAM accounts
|
|
// with expiration. This helps to alleviate the load on
|
|
// the real IAM service if the gateway is handling
|
|
// many requests. This forwards account updates to the
|
|
// underlying service, and returns cached results while
|
|
// the in memory account is not expired.
|
|
type IAMCache struct {
|
|
service IAMService
|
|
iamcache *icache
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
var _ IAMService = &IAMCache{}
|
|
|
|
type item struct {
|
|
value Account
|
|
exp time.Time
|
|
}
|
|
|
|
type icache struct {
|
|
sync.RWMutex
|
|
expire time.Duration
|
|
items map[string]item
|
|
}
|
|
|
|
func (i *icache) set(k string, v Account) {
|
|
cpy := v
|
|
i.Lock()
|
|
i.items[k] = item{
|
|
exp: time.Now().Add(i.expire),
|
|
value: cpy,
|
|
}
|
|
i.Unlock()
|
|
}
|
|
|
|
func (i *icache) get(k string) (Account, bool) {
|
|
i.RLock()
|
|
v, ok := i.items[k]
|
|
i.RUnlock()
|
|
if !ok || !v.exp.After(time.Now()) {
|
|
return Account{}, false
|
|
}
|
|
return v.value, true
|
|
}
|
|
|
|
func (i *icache) update(k string, props MutableProps) {
|
|
i.Lock()
|
|
defer i.Unlock()
|
|
|
|
item, found := i.items[k]
|
|
if found {
|
|
updateAcc(&item.value, props)
|
|
|
|
// refresh the expiration date
|
|
item.exp = time.Now().Add(i.expire)
|
|
|
|
i.items[k] = item
|
|
}
|
|
}
|
|
|
|
func (i *icache) Delete(k string) {
|
|
i.Lock()
|
|
delete(i.items, k)
|
|
i.Unlock()
|
|
}
|
|
|
|
func (i *icache) gcCache(ctx context.Context, interval time.Duration) {
|
|
for {
|
|
if ctx.Err() != nil {
|
|
break
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
i.Lock()
|
|
// prune expired entries
|
|
for k, v := range i.items {
|
|
if now.After(v.exp) {
|
|
delete(i.items, k)
|
|
}
|
|
}
|
|
i.Unlock()
|
|
|
|
// sleep for the clean interval or context cancelation,
|
|
// whichever comes first
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-time.After(interval):
|
|
}
|
|
}
|
|
}
|
|
|
|
// NewCache initializes an IAM cache for the provided service. The expireTime
|
|
// is the duration a cache entry can be valid, and the cleanupInterval is
|
|
// how often to scan cache and cleanup expired entries.
|
|
func NewCache(service IAMService, expireTime, cleanupInterval time.Duration) *IAMCache {
|
|
i := &IAMCache{
|
|
service: service,
|
|
iamcache: &icache{
|
|
items: make(map[string]item),
|
|
expire: expireTime,
|
|
},
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
go i.iamcache.gcCache(ctx, cleanupInterval)
|
|
i.cancel = cancel
|
|
|
|
return i
|
|
}
|
|
|
|
// CreateAccount send create to IAM service and creates an account cache entry
|
|
func (c *IAMCache) CreateAccount(account Account) error {
|
|
err := c.service.CreateAccount(account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// we need a copy of account to be able to store beyond the
|
|
// lifetime of the request, otherwise Fiber will reuse and corrupt
|
|
// these entries
|
|
acct := Account{
|
|
Access: strings.Clone(account.Access),
|
|
Secret: strings.Clone(account.Secret),
|
|
Role: Role(strings.Clone(string(account.Role))),
|
|
}
|
|
|
|
c.iamcache.set(acct.Access, acct)
|
|
return nil
|
|
}
|
|
|
|
// GetUserAccount retrieves the cache account if it is in the cache and not
|
|
// expired. Otherwise retrieves from underlying IAM service and caches
|
|
// result for the expire duration.
|
|
func (c *IAMCache) GetUserAccount(access string) (Account, error) {
|
|
acct, found := c.iamcache.get(access)
|
|
if found {
|
|
return acct, nil
|
|
}
|
|
|
|
a, err := c.service.GetUserAccount(access)
|
|
if err != nil {
|
|
return Account{}, err
|
|
}
|
|
|
|
c.iamcache.set(access, a)
|
|
return a, nil
|
|
}
|
|
|
|
// ResolveAccounts returns the subset of accessKeyIDs that do not exist. It
|
|
// loops over the cache's own GetUserAccount so lookups benefit from caching
|
|
// the same way a single-account check would.
|
|
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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.iamcache.Delete(access)
|
|
return nil
|
|
}
|
|
|
|
func (c *IAMCache) UpdateUserAccount(access string, props MutableProps) error {
|
|
err := c.service.UpdateUserAccount(access, props)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.iamcache.update(access, props)
|
|
return nil
|
|
}
|
|
|
|
// ListUserAccounts is a passthrough to the underlying service and
|
|
// does not make use of the cache
|
|
func (c *IAMCache) ListUserAccounts() ([]Account, error) {
|
|
return c.service.ListUserAccounts()
|
|
}
|
|
|
|
// Shutdown graceful termination of service
|
|
func (c *IAMCache) Shutdown() error {
|
|
c.cancel()
|
|
return nil
|
|
}
|