From 7592010c7ae2f7f4a7338641bf34e32b754613b1 Mon Sep 17 00:00:00 2001
From: Jerry Sun <jerry.sun@windriver.com>
Date: Fri, 15 Jan 2021 15:57:14 -0500
Subject: [PATCH] Registry Token Server Credentials Caching

This commit implements simple in-memory caching of keystone
credentials. Authentication requests are expensive and can create
high CPU usage, for example, when deploying an application on many
subclouds. This commit implements caching of the keystone credentials.
If the credentials in a registry authentication request matches cached
credentials, no authentication request is made to keystone. The cache
is invalidated every 10 minutes to remove stale entries.

Change-Id: Ia3d701d4955cfb1870b31665426a6af3b54f2520
Story: 2007267
Task: 41605
Signed-off-by: Jerry Sun <jerry.sun@windriver.com>
---
 registry-token-server/src/keystone/access.go | 88 ++++++++++++++++++--
 1 file changed, 80 insertions(+), 8 deletions(-)

diff --git a/registry-token-server/src/keystone/access.go b/registry-token-server/src/keystone/access.go
index ddeb8c2..dbb8575 100644
--- a/registry-token-server/src/keystone/access.go
+++ b/registry-token-server/src/keystone/access.go
@@ -14,6 +14,7 @@ import (
         "context"
 	"fmt"
 	"net/http"
+	"time"
 
 	dcontext "github.com/docker/distribution/context"
 	"github.com/docker/distribution/registry/auth"
@@ -21,6 +22,71 @@ import (
 	"github.com/gophercloud/gophercloud/openstack"
 )
 
+type credentials struct {
+	username, password string
+}
+
+var credentialsCache = make([]credentials, 0)
+var cacheInvalidateInterval = time.Duration(10) * time.Minute
+var lastCacheInvalidation = time.Now()
+const cacheSize = 20
+
+// add the username and password pair into the cache
+// if the cache is already full, the oldest entry is removed
+// if the username already exist, update the password
+func cacheStore(username string, password string) {
+	// invalidate cache every <interval>
+	currentTime := time.Now()
+	if currentTime.Sub(lastCacheInvalidation) > cacheInvalidateInterval {
+		credentialsCache = make([]credentials, 0)
+		lastCacheInvalidation = time.Now()
+	}
+
+	for i, cacheEntry := range credentialsCache {
+		if cacheEntry.username == username {
+			credentialsCache[i].password = password
+			return
+		}
+	}
+
+	// credentials does not exist in the cache
+	if len(credentialsCache) >= cacheSize {
+		credentialsCache = credentialsCache[:cacheSize - 1]
+	}
+	newCredentials := credentials{
+		username: username,
+		password: password,
+	}
+	credentialsCache = append(credentialsCache, newCredentials)
+}
+
+// check if the username password pair exist in the cache
+// if the user exists, move them to the top of the cache
+func cacheCheck(username string, password string) bool {
+	// invalidate cache every <interval>
+	currentTime := time.Now()
+	if currentTime.Sub(lastCacheInvalidation) > cacheInvalidateInterval {
+		credentialsCache = make([]credentials, 0)
+		lastCacheInvalidation = time.Now()
+	}
+	for i, cacheEntry := range credentialsCache {
+		if cacheEntry.username == username && cacheEntry.password == password{
+			// move the entry to the top if it is not at the top already
+			if i != 0 {
+				temp := credentials{
+					username: username,
+					password: password,
+				}
+				credentialsCache = append(credentialsCache[:i], credentialsCache[i+1:]...)
+				credentialsCache = append([]credentials{temp}, credentialsCache...)
+			}
+			return true
+		}
+	}
+	// entry not found
+	return false
+}
+
 type accessController struct {
 	realm    string
 	endpoint string
@@ -63,12 +129,15 @@ func (ac *accessController) Authorized(ctx context.Context, accessRecords ...aut
 		DomainID:         "default",
 	}
 
-	if _, err := openstack.AuthenticatedClient(opts); err != nil {
-		dcontext.GetLogger(ctx).Errorf("error authenticating user %q: %v", username, err)
-		return nil, &challenge{
-			realm: ac.realm,
-			err:   auth.ErrAuthenticationFailure,
+	if !cacheCheck(username, password){
+		if _, err := openstack.AuthenticatedClient(opts); err != nil {
+			dcontext.GetLogger(ctx).Errorf("error authenticating user %q: %v", username, err)
+			return nil, &challenge{
+				realm: ac.realm,
+				err:   auth.ErrAuthenticationFailure,
+			}
 		}
+		cacheStore(username, password)
 	}
 
 	return auth.WithUser(ctx, auth.UserInfo{Name: username}), nil
@@ -85,9 +154,12 @@ func (ac *accessController) AuthenticateUser(username string, password string) e
 		DomainID:         "default",
 	}
 
-	if _, err := openstack.AuthenticatedClient(opts); err != nil {
-		dcontext.GetLogger(context.Background()).Errorf("error authenticating user %q: %v", username, err)
-		return auth.ErrAuthenticationFailure
+        if !cacheCheck(username, password){
+		if _, err := openstack.AuthenticatedClient(opts); err != nil {
+			dcontext.GetLogger(context.Background()).Errorf("error authenticating user %q: %v", username, err)
+			return auth.ErrAuthenticationFailure
+		}
+                cacheStore(username, password)
 	}
 
 	return nil