mirror of
https://github.com/project-zot/zot.git
synced 2026-06-19 22:27:58 +08:00
feat(sessions): add support for remote redis session store (#3345)
Description ==================== zot currently stores session cookies in memory or in a local directory. For cases where the session cookies should be independent of the instance where they were created such as multiple instances of zot, or a fully stateless zot instance, there is a need to support a remote session storage. This change adds support for using Redis and Redis-compatible services as a remote session driver as well as introduces a new configuration option for it. What has changed ======================= - New config added under Auth config to specify configuration for the session driver. - Examples README updated with details of the new Auth config. - The config supports only 2 drivers in this change - local and redis - Using the local driver is backwards compatible and behaves the same way that zot currently works for local session storage. - Omitting this config does not result in an error. In this case, zot behaves as it normally does for local session storage. - When configured, zot can use redis for persisting cookie information for zot UI. - The cookie in the store is deleted on logout or after the max expiry time for the cookie. - Configuration for the redis session driver accepts the same configuration values as that of the remote meta cache. - A separate connection is established for the session driver. An existing connection for meta cache will not be re-used for the session driver. - A key prefix is configurable for the redis session driver. The value will be converted into a string for use. If no value is provided, a default prefix of "zotsession" will be used. - Redis sessions does not support hash key or encryption in this change. - New BATS test added to verify zot behavior with Redis session store. - Github workflow updated to install valkey-tools dependency for BATS. Signed-off-by: Vishwas Rajashekar <dev@vrajashkr.com>
This commit is contained in:
committed by
GitHub
parent
cbbd39745c
commit
86af38abfc
+161
-27
@@ -15,11 +15,14 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
guuid "github.com/gofrs/uuid"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
"github.com/project-zot/mockoidc"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/resty.v1"
|
||||
|
||||
zerr "zotregistry.dev/zot/errors"
|
||||
"zotregistry.dev/zot/pkg/api"
|
||||
"zotregistry.dev/zot/pkg/api/config"
|
||||
"zotregistry.dev/zot/pkg/api/constants"
|
||||
@@ -934,45 +937,65 @@ func TestCookiestoreCleanup(t *testing.T) {
|
||||
log := log.NewTestLogger()
|
||||
metrics := monitoring.NewMetricsServer(true, log)
|
||||
|
||||
Convey("Test cookiestore cleanup works", t, func() {
|
||||
taskScheduler := scheduler.NewScheduler(config.New(), metrics, log)
|
||||
taskScheduler.RateLimit = 50 * time.Millisecond
|
||||
taskScheduler.RunScheduler()
|
||||
authCfgTestCases := []struct {
|
||||
name string
|
||||
cfg config.AuthConfig
|
||||
}{
|
||||
{
|
||||
"empty Auth config",
|
||||
config.AuthConfig{},
|
||||
},
|
||||
{
|
||||
"local session driver",
|
||||
config.AuthConfig{
|
||||
SessionDriver: map[string]any{
|
||||
"name": "local",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rootDir := t.TempDir()
|
||||
for _, testCase := range authCfgTestCases {
|
||||
Convey("Test cookiestore cleanup works with "+testCase.name, t, func() {
|
||||
taskScheduler := scheduler.NewScheduler(config.New(), metrics, log)
|
||||
taskScheduler.RateLimit = 50 * time.Millisecond
|
||||
taskScheduler.RunScheduler()
|
||||
|
||||
err := os.MkdirAll(path.Join(rootDir, "_sessions"), storageConstants.DefaultDirPerms)
|
||||
So(err, ShouldBeNil)
|
||||
rootDir := t.TempDir()
|
||||
|
||||
sessionPath := path.Join(rootDir, "_sessions", "session_1234")
|
||||
err := os.MkdirAll(path.Join(rootDir, "_sessions"), storageConstants.DefaultDirPerms)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = os.WriteFile(sessionPath, []byte("session"), storageConstants.DefaultFilePerms)
|
||||
So(err, ShouldBeNil)
|
||||
sessionPath := path.Join(rootDir, "_sessions", "session_1234")
|
||||
|
||||
changeTime := time.Now().Add(-4 * time.Hour)
|
||||
err = os.WriteFile(sessionPath, []byte("session"), storageConstants.DefaultFilePerms)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = os.Chtimes(sessionPath, changeTime, changeTime)
|
||||
So(err, ShouldBeNil)
|
||||
changeTime := time.Now().Add(-4 * time.Hour)
|
||||
|
||||
imgStore := local.NewImageStore(rootDir, false, false, log, metrics, nil, nil, nil, nil)
|
||||
err = os.Chtimes(sessionPath, changeTime, changeTime)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
storeController := storage.StoreController{
|
||||
DefaultStore: imgStore,
|
||||
}
|
||||
imgStore := local.NewImageStore(rootDir, false, false, log, metrics, nil, nil, nil, nil)
|
||||
|
||||
cookieStore, err := api.NewCookieStore(storeController, nil, nil)
|
||||
So(err, ShouldBeNil)
|
||||
storeController := storage.StoreController{
|
||||
DefaultStore: imgStore,
|
||||
}
|
||||
|
||||
cookieStore.RunSessionCleaner(taskScheduler)
|
||||
cookieStore, err := api.NewCookieStore(&testCase.cfg, storeController, log)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
cookieStore.RunSessionCleaner(taskScheduler)
|
||||
|
||||
taskScheduler.Shutdown()
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// make sure session is removed
|
||||
_, err = os.Stat(sessionPath)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
taskScheduler.Shutdown()
|
||||
|
||||
// make sure session is removed
|
||||
_, err = os.Stat(sessionPath)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
}
|
||||
|
||||
Convey("Test cookiestore cleanup without permissions on rootDir", t, func() {
|
||||
taskScheduler := scheduler.NewScheduler(config.New(), metrics, log)
|
||||
@@ -995,7 +1018,11 @@ func TestCookiestoreCleanup(t *testing.T) {
|
||||
DefaultStore: imgStore,
|
||||
}
|
||||
|
||||
cookieStore, err := api.NewCookieStore(storeController, []byte("secret"), nil)
|
||||
authCfg := config.AuthConfig{
|
||||
SessionHashKey: []byte("secret"),
|
||||
}
|
||||
|
||||
cookieStore, err := api.NewCookieStore(&authCfg, storeController, log)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = os.Chmod(rootDir, 0o000)
|
||||
@@ -1085,6 +1112,113 @@ func TestCookiestoreCleanup(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedisCookieStore(t *testing.T) {
|
||||
log := log.Logger{}
|
||||
|
||||
testRedis := miniredis.RunT(t)
|
||||
|
||||
storeController := storage.StoreController{
|
||||
DefaultStore: &mocks.MockedImageStore{
|
||||
GetImageManifestFn: func(repo string, reference string) ([]byte, godigest.Digest, string, error) {
|
||||
return []byte{}, "", "", zerr.ErrRepoBadVersion
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
testName string
|
||||
shouldErrBeNil bool
|
||||
expectedErrStr string
|
||||
inputCfg *config.AuthConfig
|
||||
}{
|
||||
{
|
||||
"Cookie store creation should not fail if the driver is local",
|
||||
true,
|
||||
"",
|
||||
&config.AuthConfig{
|
||||
SessionDriver: map[string]any{
|
||||
"name": "local",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Cookie store creation should fail if the driver is unsupported",
|
||||
false,
|
||||
"invalid server config: sessiondriver unknowndriver not supported",
|
||||
&config.AuthConfig{
|
||||
SessionDriver: map[string]any{
|
||||
"name": "unknowndriver",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Cookie store creation should not fail if the keyPrefix for Redis is not a string",
|
||||
true,
|
||||
"",
|
||||
&config.AuthConfig{
|
||||
SessionDriver: map[string]any{
|
||||
"name": "redis",
|
||||
"keyprefix": 8,
|
||||
"url": "redis://" + testRedis.Addr(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Cookie store creation should not fail if the SessionDriver Config is nil",
|
||||
true,
|
||||
"",
|
||||
&config.AuthConfig{},
|
||||
},
|
||||
{
|
||||
"Cookie store creation and use should succeed with valid configuration",
|
||||
true,
|
||||
"",
|
||||
&config.AuthConfig{
|
||||
SessionDriver: map[string]any{
|
||||
"name": "redis",
|
||||
"url": "redis://" + testRedis.Addr(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Cookie store creation should fail if the url for Redis is incorrect",
|
||||
false,
|
||||
"dial tcp: lookup unknown on 127.0.0.53:53: server misbehaving",
|
||||
&config.AuthConfig{
|
||||
SessionDriver: map[string]any{
|
||||
"name": "redis",
|
||||
"url": "redis://unknown:1000",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Cookie store creation should fail if the url for Redis has an invalid value",
|
||||
false,
|
||||
"invalid server config: cachedriver map[name:redis url:%!s(int=100)] has invalid value for url",
|
||||
&config.AuthConfig{
|
||||
SessionDriver: map[string]any{
|
||||
"name": "redis",
|
||||
"url": 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
Convey(testCase.testName, t, func() {
|
||||
cookieStore, err := api.NewCookieStore(testCase.inputCfg, storeController, log)
|
||||
if testCase.shouldErrBeNil {
|
||||
So(err, ShouldBeNil)
|
||||
So(cookieStore, ShouldNotBeNil)
|
||||
} else {
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldEqual, testCase.expectedErrStr)
|
||||
So(cookieStore, ShouldBeNil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockUUIDGenerator struct {
|
||||
guuid.Generator
|
||||
succeedAttempts int
|
||||
|
||||
Reference in New Issue
Block a user