From 77ccb4ddf8c5a26fe7e498c9c6d01f16a8642687 Mon Sep 17 00:00:00 2001 From: Saman Mahdanian Date: Mon, 29 Jan 2024 18:11:48 +0330 Subject: [PATCH] New Arch almost done --- api/v1alpha1/accesstoken_types.go | 3 - api/v1alpha1/webservice_types.go | 20 +- api/v1alpha1/webserviceaccessbinding_types.go | 2 +- api/v1alpha1/zz_generated.deepcopy.go | 17 +- pkg/auth/authenticator.go | 194 +++++++++---- pkg/auth/authenticator_cache.go | 273 ++++++++++++++---- pkg/auth/authenticator_test.go | 118 ++++---- 7 files changed, 443 insertions(+), 184 deletions(-) diff --git a/api/v1alpha1/accesstoken_types.go b/api/v1alpha1/accesstoken_types.go index 3852c4f..dfce608 100644 --- a/api/v1alpha1/accesstoken_types.go +++ b/api/v1alpha1/accesstoken_types.go @@ -17,7 +17,6 @@ limitations under the License. package v1alpha1 import ( - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -40,8 +39,6 @@ const ( SuspendedState AccessTokenState = "Suspended" ) -type WebserviceReference corev1.SecretReference - // AccessTokenSpec defines the desired state of AccessToken type AccessTokenSpec struct { // State shows the state of the token (whether you use token or it's just a draft) diff --git a/api/v1alpha1/webservice_types.go b/api/v1alpha1/webservice_types.go index 88c6bb2..97b2927 100644 --- a/api/v1alpha1/webservice_types.go +++ b/api/v1alpha1/webservice_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -100,10 +101,27 @@ type WebServiceList struct { Items []WebService `json:"items"` } -func (w WebService) encodedName() string { +type LocalWebserviceReference corev1.LocalObjectReference +type WebserviceReference corev1.SecretReference + +func (w WebserviceReference) LocalName() string { return w.Namespace + "/" + w.Name } +func (w LocalWebserviceReference) LocalName(ns string) string { + return WebserviceReference{ + Name: w.Name, + Namespace: ns, + }.LocalName() +} + +func (w WebService) LocalName() string { + return WebserviceReference{ + Name: w.Name, + Namespace: w.Namespace, + }.LocalName() +} + func init() { SchemeBuilder.Register(&WebService{}, &WebServiceList{}) } diff --git a/api/v1alpha1/webserviceaccessbinding_types.go b/api/v1alpha1/webserviceaccessbinding_types.go index 8f2c8e1..f37f466 100644 --- a/api/v1alpha1/webserviceaccessbinding_types.go +++ b/api/v1alpha1/webserviceaccessbinding_types.go @@ -29,7 +29,7 @@ type WebserviceAccessBindingSpec struct { Subjects []string `json:"subjects,omitempty"` // WebServices are the target service accesses - Webservices []string `json:"webservices,omitempty"` + Webservices []LocalWebserviceReference `json:"webservices,omitempty"` } // WebserviceAccessBindingStatus defines the observed state of WebserviceAccessBinding diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c98e410..91170f2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -135,6 +135,21 @@ func (in *AccessTokenStatus) DeepCopy() *AccessTokenStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalWebserviceReference) DeepCopyInto(out *LocalWebserviceReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalWebserviceReference. +func (in *LocalWebserviceReference) DeepCopy() *LocalWebserviceReference { + if in == nil { + return nil + } + out := new(LocalWebserviceReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UpstreamHttpAuthService) DeepCopyInto(out *UpstreamHttpAuthService) { *out = *in @@ -314,7 +329,7 @@ func (in *WebserviceAccessBindingSpec) DeepCopyInto(out *WebserviceAccessBinding } if in.Webservices != nil { in, out := &in.Webservices, &out.Webservices - *out = make([]string, len(*in)) + *out = make([]LocalWebserviceReference, len(*in)) copy(*out, *in) } } diff --git a/pkg/auth/authenticator.go b/pkg/auth/authenticator.go index a7cd168..8faf178 100644 --- a/pkg/auth/authenticator.go +++ b/pkg/auth/authenticator.go @@ -13,6 +13,7 @@ import ( "github.com/asaskevich/govalidator" "github.com/go-logr/logr" + "github.com/snapp-incubator/Cerberus/api/v1alpha1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -28,7 +29,7 @@ type Authenticator struct { httpClient *http.Client accessTokensCache *AccessTokensCache - servicesCache *ServicesCache + webservicesCache *WebservicesCache cacheLock sync.RWMutex updateLock sync.Mutex @@ -91,6 +92,14 @@ const ( // the request context is not listed by Cerberus CerberusReasonWebserviceNotFound CerberusReason = "webservice-notfound" + // CerberusReasonWebserviceEmpty means that given webservice in + // the request context is empty or it's not given at all + CerberusReasonWebserviceEmpty CerberusReason = "webservice-empty" + + // CerberusReasonWebserviceNamespaceEmpty means that given namespace of webservice in + // the request context is empty or it's not given at all + CerberusReasonWebserviceNamespaceEmpty CerberusReason = "webservice-namespace-empty" + // CerberusReasonInvalidUpstreamAddress means that requested webservice // has an invalid upstream address in it's manifest CerberusReasonInvalidUpstreamAddress CerberusReason = "invalid-auth-upstream" @@ -148,11 +157,11 @@ const ( // TestAccess will check if given AccessToken (identified by raw token in the request) // has access to given Webservice (identified by its name) and returns proper CerberusReason -func (a *Authenticator) TestAccess(request *Request, wsvc ServicesCacheEntry) (bool, CerberusReason, CerberusExtraHeaders) { - newExtraHeaders := make(CerberusExtraHeaders) - ok, reason, token := a.readToken(request, wsvc) - if !ok { - return false, reason, newExtraHeaders +func (a *Authenticator) TestAccess(request *Request, wsvc WebservicesCacheEntry) (reason CerberusReason, newExtraHeaders CerberusExtraHeaders) { + newExtraHeaders = make(CerberusExtraHeaders) + reason, token := a.readToken(request, wsvc) + if reason != "" { + return } a.cacheLock.RLock() @@ -161,23 +170,53 @@ func (a *Authenticator) TestAccess(request *Request, wsvc ServicesCacheEntry) (b defer cacheReaders.Dec() if token == "" { - return false, CerberusReasonTokenEmpty, newExtraHeaders + return } - ac, ok := (*a.accessCache)[token] + ac, ok := a.accessTokensCache.ReadAccesstoken(token) if !ok { - return false, CerberusReasonTokenNotFound, newExtraHeaders + return } - priority := (*a.accessCache)[token].Spec.Priority + + newExtraHeaders.set(CerberusHeaderAccessToken, ac.ObjectMeta.Name) + + reason, h := a.testPriority(ac, wsvc) + newExtraHeaders.merge(h) + if reason != "" { + return + } + + reason, h = a.testIPAccess(ac, wsvc, request) + newExtraHeaders.merge(h) + if reason != "" { + return + } + + reason, h = a.testDomainAccess(ac, wsvc, request) + newExtraHeaders.merge(h) + + if !ac.TestAccess(wsvc.Name) { + return + } + reason = CerberusReasonOK + return +} + +func (a *Authenticator) testPriority(ac AccessTokensCacheEntry, wsvc WebservicesCacheEntry) (CerberusReason, CerberusExtraHeaders) { + newExtraHeaders := make(CerberusExtraHeaders) + priority := ac.Spec.Priority minPriority := wsvc.Spec.MinimumTokenPriority if priority < minPriority { newExtraHeaders[CerberusHeaderAccessLimitReason] = TokenPriorityLowerThanServiceMinAccessLimit newExtraHeaders[CerberusHeaderTokenPriority] = fmt.Sprint(priority) newExtraHeaders[CerberusHeaderWebServiceMinPriority] = fmt.Sprint(minPriority) - return false, CerberusReasonAccessLimited, newExtraHeaders + return CerberusReasonAccessLimited, newExtraHeaders } + return "", newExtraHeaders +} - var referrer string +func (a *Authenticator) testIPAccess(ac AccessTokensCacheEntry, wsvc WebservicesCacheEntry, request *Request) (CerberusReason, CerberusExtraHeaders) { + newExtraHeaders := make(CerberusExtraHeaders) if len(ac.Spec.AllowedIPs) > 0 { ipList := make([]string, 0) @@ -187,16 +226,15 @@ func (a *Authenticator) TestAccess(request *Request, wsvc ServicesCacheEntry) (b ips := strings.Split(xForwardedFor, ", ") ipList = append(ipList, ips...) } - referrer = request.Request.Header.Get("referrer") // Retrieve "remoteAddr" from the request remoteAddr := request.Request.RemoteAddr host, _, err := net.SplitHostPort(remoteAddr) if err != nil { - return false, CerberusReasonInvalidSourceIp, newExtraHeaders + return CerberusReasonInvalidSourceIp, newExtraHeaders } if net.ParseIP(host) == nil { - return false, CerberusReasonEmptySourceIp, newExtraHeaders + return CerberusReasonEmptySourceIp, newExtraHeaders } ipList = append(ipList, host) @@ -204,58 +242,55 @@ func (a *Authenticator) TestAccess(request *Request, wsvc ServicesCacheEntry) (b if !wsvc.Spec.IgnoreIP { ipAllowed, err := checkIP(ipList, ac.Spec.AllowedIPs) if err != nil { - return false, CerberusReasonBadIpList, newExtraHeaders + return CerberusReasonBadIpList, newExtraHeaders } if !ipAllowed { - return false, CerberusReasonIpNotAllowed, newExtraHeaders + return CerberusReasonIpNotAllowed, newExtraHeaders } } } + return "", newExtraHeaders +} + +func (a *Authenticator) testDomainAccess(ac AccessTokensCacheEntry, wsvc WebservicesCacheEntry, request *Request) (CerberusReason, CerberusExtraHeaders) { + newExtraHeaders := make(CerberusExtraHeaders) + var referrer string + referrer = request.Request.Header.Get("referrer") // Check if IgnoreDomain is true, skip domain list check if !wsvc.Spec.IgnoreDomain && len(ac.Spec.AllowedDomains) > 0 && referrer != "" { domainAllowed, err := CheckDomain(referrer, ac.Spec.AllowedDomains) if err != nil { - return false, CerberusReasonBadDomainList, newExtraHeaders + return CerberusReasonBadDomainList, newExtraHeaders } if !domainAllowed { - return false, CerberusReasonDomainNotAllowed, newExtraHeaders + return CerberusReasonDomainNotAllowed, newExtraHeaders } } - - newExtraHeaders[CerberusHeaderAccessToken] = ac.ObjectMeta.Name - - if _, ok := (*a.accessCache)[token].allowedServices[wsvc.Name]; !ok { - return false, CerberusReasonUnauthorized, newExtraHeaders - } - return true, CerberusReasonOK, newExtraHeaders + return "", newExtraHeaders } // readToken reads token from given Request object and // will return error if it not exists at expected header -func (a *Authenticator) readToken(request *Request, wsvc ServicesCacheEntry) (bool, CerberusReason, string) { +func (a *Authenticator) readToken(request *Request, wsvc WebservicesCacheEntry) (CerberusReason, string) { if wsvc.Spec.LookupHeader == "" { - return false, CerberusReasonLookupIdentifierEmpty, "" + return CerberusReasonLookupIdentifierEmpty, "" } token := request.Request.Header.Get(wsvc.Spec.LookupHeader) - return true, "", token + return "", token } // readService reads requested webservice from cache and // will return error if the object would not be found in cache -func (a *Authenticator) readService(wsvc string) (bool, CerberusReason, ServicesCacheEntry) { +func (a *Authenticator) readService(wsvc string) (bool, CerberusReason, WebservicesCacheEntry) { a.cacheLock.RLock() cacheReaders.Inc() defer a.cacheLock.RUnlock() defer cacheReaders.Dec() - if wsvc == "" { - return false, CerberusReasonLookupEmpty, ServicesCacheEntry{} - } - - res, ok := (*a.servicesCache)[wsvc] + res, ok := a.webservicesCache.ReadWebservice(wsvc) if !ok { - return false, CerberusReasonWebserviceNotFound, ServicesCacheEntry{} + return false, CerberusReasonWebserviceNotFound, WebservicesCacheEntry{} } return true, "", res } @@ -270,50 +305,49 @@ func toExtraHeaders(headers CerberusExtraHeaders) ExtraHeaders { // Check is the function which is used to Authenticate and Respond to gRPC envoy.CheckRequest func (a *Authenticator) Check(ctx context.Context, request *Request) (*Response, error) { + wsvc, ns, reason := readRequestContext(request) + if reason != "" { + return generateResponse(false, reason, nil), nil + } + wsvc = v1alpha1.WebserviceReference{ + Name: wsvc, + Namespace: ns, + }.LocalName() - wsvc := request.Context["webservice"] request.Context[HasUpstreamAuth] = "false" var extraHeaders ExtraHeaders - var httpStatusCode int ok, reason, wsvcCacheEntry := a.readService(wsvc) if ok { var cerberusExtraHeaders CerberusExtraHeaders - ok, reason, cerberusExtraHeaders = a.TestAccess(request, wsvcCacheEntry) + reason, cerberusExtraHeaders = a.TestAccess(request, wsvcCacheEntry) extraHeaders = toExtraHeaders(cerberusExtraHeaders) - if ok && hasUpstreamAuth(wsvcCacheEntry) { + if reason == CerberusReasonOK && hasUpstreamAuth(wsvcCacheEntry) { request.Context[HasUpstreamAuth] = "true" ok, reason = a.checkServiceUpstreamAuth(wsvcCacheEntry, request, &extraHeaders, ctx) } } - if ok { - httpStatusCode = http.StatusOK - } else { - httpStatusCode = http.StatusUnauthorized + var err error + if reason == CerberusReasonUpstreamAuthTimeout || reason == CerberusReasonUpstreamAuthFailed { + err = status.Error(codes.DeadlineExceeded, "Timeout exceeded") } - response := http.Response{ - StatusCode: httpStatusCode, - Header: http.Header{ - ExternalAuthHandlerHeader: {"cerberus"}, - CerberusHeaderReasonHeader: {string(reason)}, - }, - } + return generateResponse(ok, reason, extraHeaders), err +} - for key, value := range extraHeaders { - response.Header.Add(string(key), value) +func readRequestContext(request *Request) (wsvc string, ns string, reason CerberusReason) { + wsvc = request.Context["webservice"] + if wsvc == "" { + return "", "", CerberusReasonWebserviceEmpty } - var err error - if reason == CerberusReasonUpstreamAuthTimeout || reason == CerberusReasonUpstreamAuthFailed { - err = status.Error(codes.DeadlineExceeded, "Timeout exceeded") + ns = request.Context["namespace"] + if ns == "" { + return "", "", CerberusReasonWebserviceNamespaceEmpty } - return &Response{ - Allow: ok, - Response: response, - }, err + return } // NewAuthenticator creates new Authenticator object with given logger. @@ -366,7 +400,7 @@ func CheckDomain(domain string, domainAllowedList []string) (bool, error) { // checkServiceUpstreamAuth function is designed to validate the request through // the upstream authentication for a given webservice -func (a *Authenticator) checkServiceUpstreamAuth(service ServicesCacheEntry, request *Request, extraHeaders *ExtraHeaders, ctx context.Context) (bool, CerberusReason) { +func (a *Authenticator) checkServiceUpstreamAuth(service WebservicesCacheEntry, request *Request, extraHeaders *ExtraHeaders, ctx context.Context) (bool, CerberusReason) { downstreamDeadline, hasDownstreamDeadline := ctx.Deadline() serviceUpstreamAuthCalls.With(AddWithDownstreamDeadline(nil, hasDownstreamDeadline)).Inc() @@ -434,6 +468,42 @@ func (a *Authenticator) checkServiceUpstreamAuth(service ServicesCacheEntry, req // hasUpstreamAuth evaluates whether the provided webservice // upstreamauth instance is considered empty or not -func hasUpstreamAuth(service ServicesCacheEntry) bool { +func hasUpstreamAuth(service WebservicesCacheEntry) bool { return service.Spec.UpstreamHttpAuth.Address != "" } + +func generateResponse(ok bool, reason CerberusReason, extraHeaders ExtraHeaders) *Response { + var httpStatusCode int + if ok { + httpStatusCode = http.StatusOK + } else { + httpStatusCode = http.StatusUnauthorized + } + + response := http.Response{ + StatusCode: httpStatusCode, + Header: http.Header{ + ExternalAuthHandlerHeader: {"cerberus"}, + CerberusHeaderReasonHeader: {string(reason)}, + }, + } + + for key, value := range extraHeaders { + response.Header.Add(string(key), value) + } + + return &Response{ + Allow: ok, + Response: response, + } +} + +func (ch CerberusExtraHeaders) merge(h CerberusExtraHeaders) { + for key, value := range h { + ch[key] = value + } +} + +func (ch CerberusExtraHeaders) set(key CerberusHeaderName, value string) { + ch[key] = value +} diff --git a/pkg/auth/authenticator_cache.go b/pkg/auth/authenticator_cache.go index 7073f48..1ec5e57 100644 --- a/pkg/auth/authenticator_cache.go +++ b/pkg/auth/authenticator_cache.go @@ -2,12 +2,13 @@ package auth import ( "context" + "fmt" "reflect" + "strings" "time" "github.com/snapp-incubator/Cerberus/api/v1alpha1" - cerberusv1alpha1 "github.com/snapp-incubator/Cerberus/api/v1alpha1" - v1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -24,19 +25,18 @@ type WebservicesCache map[string]WebservicesCacheEntry // and it also holds a map[string]struct{} which holds name of Webservices // which the given token has access to. type AccessTokensCacheEntry struct { - cerberusv1alpha1.AccessToken - rawToken string + v1alpha1.AccessToken allowedWebservicesCache AllowedWebservicesCache } // AllowedWebserviceCache will hold which Webservices in which Namespaces does the AccessToken -// has access to (e.g AccessCache["ns-123"]["wsvc-123"] is present +// has access to (e.g AccessCache["ns-123/wsvc-123"] is present // if the corresponding AccessToken has access to in namespace ) -type AllowedWebservicesCache map[string]map[string]struct{} +type AllowedWebservicesCache map[string]struct{} // WebservicesCacheEntry will hold all datas included in Webservice manifest type WebservicesCacheEntry struct { - cerberusv1alpha1.WebService + v1alpha1.WebService allowedNamespacesCache AllowedNamespacesCache } @@ -63,30 +63,28 @@ func (a *Authenticator) UpdateCache(c client.Client, ctx context.Context, readOn a.updateLock.Lock() defer a.updateLock.Unlock() - tokens, err := retrieveObjects[*cerberusv1alpha1.AccessTokenList](c, ctx) + tokens, err := retrieveObjects[*v1alpha1.AccessTokenList](c, ctx) if err != nil { return } - secrets, err := retrieveObjects[*v1.SecretList](c, ctx) + secrets, err := retrieveObjects[*corev1.SecretList](c, ctx) if err != nil { return } - bindings, err := retrieveObjects[*cerberusv1alpha1.WebserviceAccessBindingList](c, ctx) + bindings, err := retrieveObjects[*v1alpha1.WebserviceAccessBindingList](c, ctx) if err != nil { return } - webservices, err := retrieveObjects[*cerberusv1alpha1.WebServiceList](c, ctx) + webservices, err := retrieveObjects[*v1alpha1.WebServiceList](c, ctx) if err != nil { return } - buildNewWebservicesCache(webservices, bindings) - - // TODO: remove this line - a.logger.Info("new access cache", "accessCache", newAccessCache, "servicesCache", newServicesCache) + newWebservicesCache := a.buildNewWebservicesCache(webservices, bindings) + newAccessCache := a.buildNewAccessTokensCache(tokens, secrets, newWebservicesCache) cacheWriteLockRequestStartTime := time.Now() a.cacheLock.Lock() @@ -94,8 +92,8 @@ func (a *Authenticator) UpdateCache(c client.Client, ctx context.Context, readOn defer a.cacheLock.Unlock() cacheWriteStartTime := time.Now() - a.accessCache = &newAccessCache - a.servicesCache = &newServicesCache + a.accessTokensCache = newAccessCache + a.webservicesCache = newWebservicesCache cacheWriteTime.Observe(time.Since(cacheWriteStartTime).Seconds()) return nil } @@ -114,7 +112,7 @@ func retrieveObjects[K client.ObjectList]( var result K elemType := reflect.TypeOf(result).Elem() newInstance := reflect.New(elemType).Elem() - reflect.ValueOf(result).Elem().Set(newInstance) + // reflect.ValueOf(result).Elem().Set(newInstance) metricsLabel := reflect.TypeOf(newInstance).String() err := c.List(ctx, result) @@ -122,69 +120,240 @@ func retrieveObjects[K client.ObjectList]( return result, err } -func buildNewAccessTokensCache( - tokens *v1alpha1.AccessTokenList, - secrets *v1.SecretList, +// buildNewWebservicesCache creates WebservicesCacheEntry for each webservice and then it +// fills allowedNamespaces for every given webservice over given bindings +// later this allowedNamespaces will be used in building/verifing AccessTokensCache +func (a *Authenticator) buildNewWebservicesCache( + webservices *v1alpha1.WebServiceList, bindings *v1alpha1.WebserviceAccessBindingList, -) *AccessTokensCache { - secretValues := getSecretRawTokenMap(secrets) +) *WebservicesCache { + newWebservicesCache := make(WebservicesCache) + for _, webservice := range webservices.Items { + if webservice.Namespace == "" { + a.logger.Info("webservice namespace is empty", + "webservice", webservice.Name, + ) + continue + } + newWebservicesCache[webservice.LocalName()] = WebservicesCacheEntry{ + WebService: webservice, + allowedNamespacesCache: make(AllowedNamespacesCache), + } + } + webserviceCacheEntries.Set(float64(len(newWebservicesCache))) + + ignoredBindings := newWebservicesCache.buildAllowedNamespacesCache(bindings) + for name, wsvcs := range ignoredBindings { + a.logger.Info("ignored some webservices over binding", + "binding", name, "webservices", strings.Join(wsvcs, ","), + ) + } + return &newWebservicesCache +} +// buildNewAccessTokensCache creates AccessTokensCache and validated given access for each AccessToken +func (a *Authenticator) buildNewAccessTokensCache( + tokens *v1alpha1.AccessTokenList, + secrets *corev1.SecretList, + newWebservicesCache *WebservicesCache, +) *AccessTokensCache { + secretValues := getSecretRawTokenMap(secrets) // Secret.Name -> Secret.Data.token newAccessTokensCache := make(AccessTokensCache) - rawToken := make(map[string]string) for _, token := range tokens.Items { - if t, ok := secretValues[token.Namespace+"."+token.Name]; ok { - rawToken[token.Name] = t - newAccessTokensCache[t] = AccessTokensCacheEntry{ - AccessToken: token, - allowedWebservicesCache: make(AllowedWebservicesCache), - rawToken: t, - } + tokenInfoLogger := a.logger.WithValues("accesstoken", token.Name, "namespace", token.Namespace) + if strings.Contains(token.Name, ".") { + // TODO: update AccessToken status and report the error + tokenInfoLogger.Info("dot character is not allowed in AccessToken name") + continue + } + if strings.Contains(token.Namespace, ".") { + // TODO: update AccessToken status and report the error + tokenInfoLogger.Info("dot character is not allowed in AccessToken namespace") + continue + } + + tokenRawValue, ok := secretValues[token.Namespace+"."+token.Name] + if !ok { + tokenInfoLogger.Info("unable to find secret for accesstoken") + continue + } + if tokenRawValue == "token-field-not-found" { + tokenInfoLogger.Info("corresponding secret for accesstoken does not contain token field") + continue + } + + newAccessTokensCache[tokenRawValue] = AccessTokensCacheEntry{ + AccessToken: token, + allowedWebservicesCache: make(AllowedWebservicesCache), } } accessCacheEntries.Set(float64(len(newAccessTokensCache))) - newAccessTokensCache.buildAllowedWebservicesCache() + + ignoredRequestedAccesses := newAccessTokensCache.buildAllowedWebservicesCache(newWebservicesCache) + for at, ignoredWebservices := range ignoredRequestedAccesses { + result := make([]string, 0) + for _, wr := range ignoredWebservices { + result = append(result, wr.LocalName()) + } + name, namespace := decodeLocalName(at) + a.logger.Info("some allowed webservices for token are ignored", + "accesstoken", name, "namespace", namespace, "ignored", strings.Join(result, ","), + ) + } + return &newAccessTokensCache } -func buildNewWebservicesCache(webservices *v1alpha1.WebServiceList, bindings *v1alpha1.WebserviceAccessBindingList) *WebservicesCache { - newWebservicesCache := make(WebservicesCache) - for _, webservice := range webservices.Items { - newWebservicesCache[webservice.Name] = WebservicesCacheEntry{ - WebService: webservice, - allowedNamespacesCache: make(AllowedNamespacesCache), +// buildAllowedNamespacesCache builds allowedNamespacesCache for the WebservicesCache over given bindings +// it also allows requests from same namespace to webservice +func (c *WebservicesCache) buildAllowedNamespacesCache(bindings *v1alpha1.WebserviceAccessBindingList) map[string][]string { + for _, wsvc := range *c { + wsvc.allowNamespace(wsvc.Namespace) + } + + ignoredBindings := make(map[string][]string) + for _, binding := range bindings.Items { + for _, wsvc := range binding.Spec.Webservices { + for _, ns := range binding.Spec.Subjects { + err := c.allowWebserviceCallsFromNamespace(wsvc.LocalName(binding.Namespace), ns) + if err != nil { + eb := binding.Namespace + "/" + binding.Name + if _, ok := ignoredBindings[eb]; !ok { + ignoredBindings[eb] = make([]string, 0) + } + ignoredBindings[eb] = append(ignoredBindings[eb], wsvc.LocalName(binding.Namespace)) + } + } } } - webserviceCacheEntries.Set(float64(len(newWebservicesCache))) + return ignoredBindings +} - newWebservicesCache.buildAllowedNamespacesCache(bindings) - return &newWebservicesCache +// allowWebserviceCallsFromNamespace adds given namespace to allowed namespaces for given webservice +// (it adds namespace to webservice.allowedNamespacesCache) +// will return error if wsvc not exists in cache webservices +func (c *WebservicesCache) allowWebserviceCallsFromNamespace(wsvc string, ns string) error { + if err := c.validateWebservice(wsvc); err != nil { + return err + } + (*c)[wsvc].allowNamespace(ns) + return nil +} + +// checkAccess returns true if given namespace has access to given webservice +func (c *WebservicesCache) checkAccess(wsvc string, ns string) (bool, error) { + if err := c.validateWebservice(wsvc); err != nil { + return false, err + } + return (*c)[wsvc].checkAccessFrom(ns), nil +} + +// validateWebservice raises a proper error if wsvc is not present in cache +func (c *WebservicesCache) validateWebservice(wsvc string) (err error) { + if _, ok := (*c)[wsvc]; !ok { + err = fmt.Errorf("webservice not found in webservices cache") + } + return } -func (c *WebservicesCache) buildAllowedNamespacesCache(bindings *v1alpha1.WebserviceAccessBindingList) { - // for _, binding := range bindings.I +// allowNamespace adds given namespace to given cache entry for a webservice +func (c WebservicesCacheEntry) allowNamespace(ns string) { + c.allowedNamespacesCache.add(ns) +} + +// checkAccessFrom returns true if given namespace has access to correspondig webservice +func (c WebservicesCacheEntry) checkAccessFrom(ns string) bool { + _, ok := c.allowedNamespacesCache[ns] + return ok +} + +// add inserts given namespace as a key into underlying map behind AllowedNamespacesCache +func (c AllowedNamespacesCache) add(ns string) { + c[ns] = struct{}{} } // getSecretRawTokenMap converts a secret list to a map from secret name to it's // token field value for faster access to token values by secret name -func getSecretRawTokenMap(secrets *v1.SecretList) map[string]string { +func getSecretRawTokenMap(secrets *corev1.SecretList) map[string]string { result := make(map[string]string) for _, secret := range secrets.Items { if t, ok := secret.Data["token"]; ok { result[secret.Name] = string(t) + } else { + result[secret.Name] = "token-field-not-found" } } return result } -func (c *AccessTokensCache) buildAllowedWebservicesCache(bindings *v1alpha1.WebserviceAccessBindingList) { - for _, binding := range bindings.Items { - for _, subject := range binding.Spec.Subjects { - for _, webservice := range binding.Spec.Webservices { - if t, ok := rawToken[subject]; ok { - newAccessTokensCache[t].allowedWebservicesCache[webservice] = struct{}{} - } - } +// buildAllowedWebservicesCache builds actual access cache for each token and returns all ignored entries +// per accesstoken names encoded in [Namespace/Name]->[]*wsvcRefs model +func (c *AccessTokensCache) buildAllowedWebservicesCache(newWebservicesCache *WebservicesCache) map[string][]*v1alpha1.WebserviceReference { + ignoredEntries := make(map[string][]*v1alpha1.WebserviceReference) + for tRaw, token := range *c { + ignoredEntriesForToken := (*c)[tRaw].buildAllowedWebservicesCache(newWebservicesCache) + ignoredEntries[encodeLocalName(token)] = ignoredEntriesForToken + } + return ignoredEntries +} + +// buildAllowedWebservicesCache adds valid AllowedWebservices from AccessToken.Spec to actual cache +// and returns ignores WebserviceReferences +func (t AccessTokensCacheEntry) buildAllowedWebservicesCache(newWebservicesCache *WebservicesCache) []*v1alpha1.WebserviceReference { + ignoredEntries := make([]*v1alpha1.WebserviceReference, 0) + for _, webserviceRef := range t.Spec.AllowedWebservices { + if webserviceRef.Namespace == "" { + webserviceRef.Namespace = t.Namespace + } + + if ok, err := newWebservicesCache.checkAccess( + webserviceRef.LocalName(), t.Namespace, + ); !ok || err != nil { + ignoredEntries = append(ignoredEntries, webserviceRef) + continue } + t.allowedWebservicesCache.add(webserviceRef.LocalName()) + } + return ignoredEntries +} + +// add inserted a local webservice name to actual access cache for a token +func (c AllowedWebservicesCache) add(wsvc string) { + c[wsvc] = struct{}{} +} + +// encodeAccessTokenLocalName encodes AccessTokensCacheEntry name and namespace into a single string +// (concats them with a "/" in between). use decodeAccessTokenLocalName to retrive Name and Namespace +// mainly used for trace and log returning in functions +func encodeLocalName(at AccessTokensCacheEntry) string { + return at.Namespace + "/" + at.Name +} + +// decodeAccessTokenLocalName decodes encodeAccessTokenLocalName result into corresponding Name and Namespace +func decodeLocalName(at string) (name, namespace string) { + s := strings.Split(at, "/") + name = s[0] + if len(s) > 1 { + namespace = s[1] } + return +} + +// ReadWebservice returns cache entry for service name +func (c *WebservicesCache) ReadWebservice(wsvc string) (WebservicesCacheEntry, bool) { + r, ok := (*c)[wsvc] + return r, ok +} + +// ReadAccesstoken +func (c *AccessTokensCache) ReadAccesstoken(rawToken string) (AccessTokensCacheEntry, bool) { + r, ok := (*c)[rawToken] + return r, ok +} + +// TestAccess +func (c *AccessTokensCacheEntry) TestAccess(wsvc string) bool { + _, ok := c.allowedWebservicesCache[wsvc] + return ok } diff --git a/pkg/auth/authenticator_test.go b/pkg/auth/authenticator_test.go index 57ce7e2..3634087 100644 --- a/pkg/auth/authenticator_test.go +++ b/pkg/auth/authenticator_test.go @@ -62,11 +62,11 @@ func generateSubjects(subjectCount int) []string { return subject } -func generateWebservices(webserviceCount int) []string { - webservice := make([]string, webserviceCount) +func generateWebservices(webserviceCount int) []cerberusv1alpha1.LocalWebserviceReference { + webservice := make([]cerberusv1alpha1.LocalWebserviceReference, webserviceCount) for i := 0; i < webserviceCount; i++ { - webservice[i] = fmt.Sprintf("webservice-%d", i+1) + webservice[i].Name = fmt.Sprintf("webservice-%d", i+1) } return webservice @@ -139,8 +139,8 @@ func TestCheckDomainComplex(t *testing.T) { func TestReadService(t *testing.T) { authenticator := &Authenticator{ - accessCache: &AccessCache{}, - servicesCache: &ServicesCache{}, + accessTokensCache: &AccessTokensCache{}, + webservicesCache: &WebservicesCache{}, } request := &Request{ @@ -153,16 +153,15 @@ func TestReadService(t *testing.T) { } // Create a test WebserviceCacheEntry - webservice := ServicesCacheEntry{ - cerberusv1alpha1.WebService{ - + webservice := WebservicesCacheEntry{ + WebService: cerberusv1alpha1.WebService{ Spec: cerberusv1alpha1.WebServiceSpec{ LookupHeader: "X-Cerberus-Token", }, }, } - (*authenticator.servicesCache)["SampleWebService"] = webservice + (*authenticator.webservicesCache)["SampleWebService"] = webservice request.Request.Header.Set("X-Cerberus-Token", "test-token") @@ -172,10 +171,10 @@ func TestReadService(t *testing.T) { wsvc string expectedOk bool expectedReason CerberusReason - expectedCacheEntry ServicesCacheEntry + expectedCacheEntry WebservicesCacheEntry }{ {wsvc, true, "", webservice}, - {"nonexistent_service", false, CerberusReasonWebserviceNotFound, ServicesCacheEntry{}}, + {"nonexistent_service", false, CerberusReasonWebserviceNotFound, WebservicesCacheEntry{}}, } for _, tc := range testCases { @@ -205,8 +204,8 @@ func TestReadToken(t *testing.T) { } // Create a test WebserviceCacheEntry - webservice := ServicesCacheEntry{ - cerberusv1alpha1.WebService{ + webservice := WebservicesCacheEntry{ + WebService: cerberusv1alpha1.WebService{ Spec: cerberusv1alpha1.WebServiceSpec{ LookupHeader: string(CerberusHeaderAccessToken), }, @@ -215,11 +214,7 @@ func TestReadToken(t *testing.T) { request.Request.Header.Set(string(CerberusHeaderAccessToken), "test-token") - ok, reason, token := authenticator.readToken(request, webservice) - - if !ok { - t.Errorf("Expected token to be read successfully.") - } + reason, token := authenticator.readToken(request, webservice) if reason != "" { t.Errorf("Expected reason to be empty.") @@ -257,21 +252,21 @@ func TestUpdateCache(t *testing.T) { func TestTestAccessValidToken(t *testing.T) { authenticator := &Authenticator{ - accessCache: &AccessCache{}, - servicesCache: &ServicesCache{}, + accessTokensCache: &AccessTokensCache{}, + webservicesCache: &WebservicesCache{}, } - tokenEntry := AccessCacheEntry{ + tokenEntry := AccessTokensCacheEntry{ AccessToken: cerberusv1alpha1.AccessToken{ ObjectMeta: metav1.ObjectMeta{ Name: "valid-token", }, }, - allowedServices: map[string]struct{}{ + allowedWebservicesCache: map[string]struct{}{ "SampleWebService": {}, }, } - (*authenticator.accessCache)["valid-token"] = tokenEntry + (*authenticator.accessTokensCache)["valid-token"] = tokenEntry headers := http.Header{} headers.Set(string(CerberusHeaderAccessToken), "valid-token") @@ -285,8 +280,8 @@ func TestTestAccessValidToken(t *testing.T) { }, } - webservice := ServicesCacheEntry{ - cerberusv1alpha1.WebService{ + webservice := WebservicesCacheEntry{ + WebService: cerberusv1alpha1.WebService{ ObjectMeta: metav1.ObjectMeta{ Name: "SampleWebService", }, @@ -295,19 +290,18 @@ func TestTestAccessValidToken(t *testing.T) { }, }, } - (*authenticator.servicesCache)["SampleWebService"] = webservice + (*authenticator.webservicesCache)["SampleWebService"] = webservice - ok, reason, extraHeaders := authenticator.TestAccess(request, webservice) + reason, extraHeaders := authenticator.TestAccess(request, webservice) - assert.True(t, ok, "Expected access to be granted") assert.Equal(t, CerberusReasonOK, reason, "Expected reason to be OK") assert.Equal(t, "valid-token", extraHeaders[CerberusHeaderAccessToken], "Expected token in extraHeaders") } func TestTestAccessInvalidToken(t *testing.T) { authenticator := &Authenticator{ - accessCache: &AccessCache{}, - servicesCache: &ServicesCache{}, + accessTokensCache: &AccessTokensCache{}, + webservicesCache: &WebservicesCache{}, } headers := http.Header{} @@ -322,8 +316,8 @@ func TestTestAccessInvalidToken(t *testing.T) { }, } - webservice := ServicesCacheEntry{ - cerberusv1alpha1.WebService{ + webservice := WebservicesCacheEntry{ + WebService: cerberusv1alpha1.WebService{ ObjectMeta: metav1.ObjectMeta{ Name: "SampleWebService", }, @@ -333,19 +327,18 @@ func TestTestAccessInvalidToken(t *testing.T) { }, } - (*authenticator.servicesCache)["SampleWebService"] = webservice + (*authenticator.webservicesCache)["SampleWebService"] = webservice - ok, reason, extraHeaders := authenticator.TestAccess(request, webservice) + reason, extraHeaders := authenticator.TestAccess(request, webservice) - assert.False(t, ok, "Expected access to be denied") assert.Equal(t, CerberusReasonTokenNotFound, reason, "Expected reason to be TokenNotFound") assert.Empty(t, extraHeaders, "Expected no extra headers for invalid token") } func TestTestAccessEmptyToken(t *testing.T) { authenticator := &Authenticator{ - accessCache: &AccessCache{}, - servicesCache: &ServicesCache{}, + accessTokensCache: &AccessTokensCache{}, + webservicesCache: &WebservicesCache{}, } headers := http.Header{} @@ -360,8 +353,8 @@ func TestTestAccessEmptyToken(t *testing.T) { }, } - webservice := ServicesCacheEntry{ - cerberusv1alpha1.WebService{ + webservice := WebservicesCacheEntry{ + WebService: cerberusv1alpha1.WebService{ ObjectMeta: metav1.ObjectMeta{ Name: "SampleWebService", }, @@ -371,22 +364,21 @@ func TestTestAccessEmptyToken(t *testing.T) { }, } - (*authenticator.servicesCache)["SampleWebService"] = webservice + (*authenticator.webservicesCache)["SampleWebService"] = webservice - ok, reason, extraHeaders := authenticator.TestAccess(request, webservice) + reason, extraHeaders := authenticator.TestAccess(request, webservice) - assert.False(t, ok, "Expected access to be denied") assert.Equal(t, CerberusReasonTokenEmpty, reason, "Expected reason to be TokenEmpty") assert.Empty(t, extraHeaders, "Expected no extra headers for empty token") } func TestTestAccessBadIPList(t *testing.T) { authenticator := &Authenticator{ - accessCache: &AccessCache{}, - servicesCache: &ServicesCache{}, + accessTokensCache: &AccessTokensCache{}, + webservicesCache: &WebservicesCache{}, } - tokenEntry := AccessCacheEntry{ + tokenEntry := AccessTokensCacheEntry{ AccessToken: cerberusv1alpha1.AccessToken{ ObjectMeta: metav1.ObjectMeta{ Name: "valid-token", @@ -395,11 +387,11 @@ func TestTestAccessBadIPList(t *testing.T) { AllowedIPs: []string{"192.168.1.1", "192.168.1.2"}, }, }, - allowedServices: map[string]struct{}{ + allowedWebservicesCache: map[string]struct{}{ "SampleWebService": {}, }, } - (*authenticator.accessCache)["valid-token"] = tokenEntry + (*authenticator.accessTokensCache)["valid-token"] = tokenEntry // Assuming an IP not in the allow list headers := http.Header{} @@ -416,8 +408,8 @@ func TestTestAccessBadIPList(t *testing.T) { }, } - webservice := ServicesCacheEntry{ - cerberusv1alpha1.WebService{ + webservice := WebservicesCacheEntry{ + WebService: cerberusv1alpha1.WebService{ ObjectMeta: metav1.ObjectMeta{ Name: "SampleWebService", }, @@ -427,23 +419,22 @@ func TestTestAccessBadIPList(t *testing.T) { }, } - (*authenticator.servicesCache)["SampleWebService"] = webservice + (*authenticator.webservicesCache)["SampleWebService"] = webservice - ok, reason, extraHeaders := authenticator.TestAccess(request, webservice) + reason, extraHeaders := authenticator.TestAccess(request, webservice) - assert.False(t, ok, "Expected access to be denied") assert.Equal(t, CerberusReasonBadIpList, reason, "Expected reason to be BadIpList") assert.Empty(t, extraHeaders, "Expected no extra headers for invalid IP") } func TestTestAccessLimited(t *testing.T) { authenticator := &Authenticator{ - accessCache: &AccessCache{}, - servicesCache: &ServicesCache{}, + accessTokensCache: &AccessTokensCache{}, + webservicesCache: &WebservicesCache{}, } // Assuming an a token with lower Priority than WebService threshold - tokenEntry := AccessCacheEntry{ + tokenEntry := AccessTokensCacheEntry{ AccessToken: cerberusv1alpha1.AccessToken{ ObjectMeta: metav1.ObjectMeta{ Name: "valid-token", @@ -452,11 +443,11 @@ func TestTestAccessLimited(t *testing.T) { Priority: 50, }, }, - allowedServices: map[string]struct{}{ + allowedWebservicesCache: map[string]struct{}{ "SampleWebService": {}, }, } - (*authenticator.accessCache)["valid-token"] = tokenEntry + (*authenticator.accessTokensCache)["valid-token"] = tokenEntry headers := http.Header{} headers.Set(string(CerberusHeaderAccessToken), "valid-token") @@ -470,8 +461,8 @@ func TestTestAccessLimited(t *testing.T) { }, } - webservice := ServicesCacheEntry{ - cerberusv1alpha1.WebService{ + webservice := WebservicesCacheEntry{ + WebService: cerberusv1alpha1.WebService{ ObjectMeta: metav1.ObjectMeta{ Name: "SampleWebService", }, @@ -482,11 +473,10 @@ func TestTestAccessLimited(t *testing.T) { }, } - (*authenticator.servicesCache)["SampleWebService"] = webservice + (*authenticator.webservicesCache)["SampleWebService"] = webservice - ok, reason, extraHeaders := authenticator.TestAccess(request, webservice) + reason, extraHeaders := authenticator.TestAccess(request, webservice) - assert.False(t, ok, "Expected access to be denied") assert.Equal(t, CerberusReasonAccessLimited, reason, "Expected reason to be AccessLimited") assert.Equal(t, extraHeaders[CerberusHeaderAccessLimitReason], TokenPriorityLowerThanServiceMinAccessLimit) assert.Equal(t, extraHeaders[CerberusHeaderTokenPriority], fmt.Sprint(tokenEntry.Spec.Priority)) @@ -620,6 +610,6 @@ func assertCachesPopulated(t *testing.T, authenticator *Authenticator) { defer authenticator.cacheLock.RUnlock() //TODO: check this error - //assert.NotEmpty(t, authenticator.accessCache) - assert.NotEmpty(t, authenticator.servicesCache) + //assert.NotEmpty(t, authenticator.accessTokensCache) + assert.NotEmpty(t, authenticator.webservicesCache) }