package cache import ( "encoding/json" "errors" "fmt" "time" "github.com/golang/glog" "github.com/hashicorp/golang-lru" kapi "k8s.io/kubernetes/pkg/api" kerrs "k8s.io/kubernetes/pkg/api/errors" utilruntime "k8s.io/kubernetes/pkg/util/runtime" "k8s.io/kubernetes/pkg/util/sets" authorizationapi "github.com/openshift/origin/pkg/authorization/api" "github.com/openshift/origin/pkg/authorization/authorizer" ) type CacheAuthorizer struct { authorizer authorizer.Authorizer authorizeCache *lru.Cache allowedSubjectsCache *lru.Cache ttl time.Duration now func() time.Time } type authorizeCacheRecord struct { created time.Time allowed bool reason string err error } type allowedSubjectsCacheRecord struct { created time.Time users sets.String groups sets.String } // NewAuthorizer returns an authorizer that caches the results of the given authorizer func NewAuthorizer(a authorizer.Authorizer, ttl time.Duration, cacheSize int) (authorizer.Authorizer, error) { authorizeCache, err := lru.New(cacheSize) if err != nil { return nil, err } allowedSubjectsCache, err := lru.New(cacheSize) if err != nil { return nil, err } return &CacheAuthorizer{ authorizer: a, authorizeCache: authorizeCache, allowedSubjectsCache: allowedSubjectsCache, ttl: ttl, now: time.Now, }, nil } func (c *CacheAuthorizer) Authorize(ctx kapi.Context, a authorizer.Action) (allowed bool, reason string, err error) { key, err := cacheKey(ctx, a) if err != nil { glog.V(5).Infof("could not build cache key for %#v: %v", a, err) return c.authorizer.Authorize(ctx, a) } if value, hit := c.authorizeCache.Get(key); hit { switch record := value.(type) { case *authorizeCacheRecord: if record.created.Add(c.ttl).After(c.now()) { return record.allowed, record.reason, record.err } else { glog.V(5).Infof("cache record expired for %s", key) c.authorizeCache.Remove(key) } default: utilruntime.HandleError(fmt.Errorf("invalid cache record type for key %s: %#v", key, record)) } } allowed, reason, err = c.authorizer.Authorize(ctx, a) // Don't cache results if there was an error unrelated to authorization // TODO: figure out a better way to determine this if err == nil || kerrs.IsForbidden(err) { c.authorizeCache.Add(key, &authorizeCacheRecord{created: c.now(), allowed: allowed, reason: reason, err: err}) } return allowed, reason, err } func (c *CacheAuthorizer) GetAllowedSubjects(ctx kapi.Context, attributes authorizer.Action) (sets.String, sets.String, error) { key, err := cacheKey(ctx, attributes) if err != nil { glog.V(5).Infof("could not build cache key for %#v: %v", attributes, err) return c.authorizer.GetAllowedSubjects(ctx, attributes) } if value, hit := c.allowedSubjectsCache.Get(key); hit { switch record := value.(type) { case *allowedSubjectsCacheRecord: if record.created.Add(c.ttl).After(c.now()) { return record.users, record.groups, nil } else { glog.V(5).Infof("cache record expired for %s", key) c.allowedSubjectsCache.Remove(key) } default: utilruntime.HandleError(fmt.Errorf("invalid cache record type for key %s: %#v", key, record)) } } users, groups, err := c.authorizer.GetAllowedSubjects(ctx, attributes) // Don't cache results if there was an error if err == nil { c.allowedSubjectsCache.Add(key, &allowedSubjectsCacheRecord{created: c.now(), users: users, groups: groups}) } return users, groups, err } func cacheKey(ctx kapi.Context, a authorizer.Action) (string, error) { if a.GetRequestAttributes() != nil { // TODO: see if we can serialize this? return "", errors.New("cannot cache request attributes") } keyData := map[string]interface{}{ "verb": a.GetVerb(), "apiVersion": a.GetAPIVersion(), "apiGroup": a.GetAPIGroup(), "resource": a.GetResource(), "resourceName": a.GetResourceName(), "nonResourceURL": a.IsNonResourceURL(), "url": a.GetURL(), } if namespace, ok := kapi.NamespaceFrom(ctx); ok { keyData["namespace"] = namespace } if user, ok := kapi.UserFrom(ctx); ok { keyData["user"] = user.GetName() keyData["groups"] = user.GetGroups() keyData["scopes"] = user.GetExtra()[authorizationapi.ScopesKey] } key, err := json.Marshal(keyData) return string(key), err }