Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sessions): periodically cleanup expired sessions #1939

Merged
merged 1 commit into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 0 additions & 35 deletions pkg/api/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ import (
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/gob"
"errors"
"fmt"
"net"
"net/http"
"os"
"path"
"strconv"
"strings"
"time"
Expand All @@ -39,7 +37,6 @@ import (
zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/log"
reqCtx "zotregistry.io/zot/pkg/requestcontext"
storageConstants "zotregistry.io/zot/pkg/storage/constants"
)

const (
Expand Down Expand Up @@ -260,38 +257,6 @@ func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun

delay := ctlr.Config.HTTP.Auth.FailDelay

// setup sessions cookie store used to preserve logged in user in web sessions
if ctlr.Config.IsBasicAuthnEnabled() {
// To store custom types in our cookies
// we must first register them using gob.Register
gob.Register(map[string]interface{}{})

cookieStoreHashKey := securecookie.GenerateRandomKey(64)
if cookieStoreHashKey == nil {
panic(zerr.ErrHashKeyNotCreated)
}

// if storage is filesystem then use zot's rootDir to store sessions
if ctlr.Config.Storage.StorageDriver == nil {
sessionsDir := path.Join(ctlr.Config.Storage.RootDirectory, "_sessions")
if err := os.MkdirAll(sessionsDir, storageConstants.DefaultDirPerms); err != nil {
panic(err)
}

cookieStore := sessions.NewFilesystemStore(sessionsDir, cookieStoreHashKey)

cookieStore.MaxAge(cookiesMaxAge)

ctlr.CookieStore = cookieStore
} else {
cookieStore := sessions.NewCookieStore(cookieStoreHashKey)

cookieStore.MaxAge(cookiesMaxAge)

ctlr.CookieStore = cookieStore
}
}

// ldap and htpasswd based authN
if ctlr.Config.IsLdapAuthEnabled() {
ldapConfig := ctlr.Config.HTTP.Auth.LDAP
Expand Down
26 changes: 23 additions & 3 deletions pkg/api/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/zitadel/oidc/pkg/client/rp"

"zotregistry.io/zot/errors"
Expand All @@ -35,7 +34,6 @@
const (
idleTimeout = 120 * time.Second
readHeaderTimeout = 5 * time.Second
cookiesMaxAge = 86400 // seconds
)

type Controller struct {
Expand All @@ -50,7 +48,7 @@
CveScanner ext.CveScanner
SyncOnDemand SyncOnDemand
RelyingParties map[string]rp.RelyingParty
CookieStore sessions.Store
CookieStore *CookieStore
// runtime params
chosenPort int // kernel-chosen port
}
Expand Down Expand Up @@ -96,6 +94,10 @@
}

func (c *Controller) Run(reloadCtx context.Context) error {
if err := c.initCookieStore(); err != nil {
return err
}

Check warning on line 99 in pkg/api/controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controller.go#L98-L99

Added lines #L98 - L99 were not covered by tests

c.StartBackgroundTasks(reloadCtx)

// setup HTTP API router
Expand Down Expand Up @@ -259,6 +261,20 @@
return nil
}

func (c *Controller) initCookieStore() error {
// setup sessions cookie store used to preserve logged in user in web sessions
if c.Config.IsBasicAuthnEnabled() {
cookieStore, err := NewCookieStore(c.StoreController)
if err != nil {
return err
}

Check warning on line 270 in pkg/api/controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controller.go#L269-L270

Added lines #L269 - L270 were not covered by tests

c.CookieStore = cookieStore
}

return nil
}

func (c *Controller) InitMetaDB(reloadCtx context.Context) error {
// init metaDB if search is enabled or we need to store user profiles, api keys or signatures
if c.Config.IsSearchEnabled() || c.Config.IsBasicAuthnEnabled() || c.Config.IsImageTrustEnabled() {
Expand Down Expand Up @@ -395,6 +411,10 @@
c.SyncOnDemand = syncOnDemand
}

if c.CookieStore != nil {
c.CookieStore.RunSessionCleaner(taskScheduler)
}

// we can later move enabling the other scheduled tasks inside the call below
ext.EnableScheduledTasks(c.Config, taskScheduler, c.MetaDB, c.Log) //nolint: contextcheck
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/api/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3409,7 +3409,7 @@ func TestAuthnSessionErrors(t *testing.T) {
session.IsNew = false
session.Values["authStatus"] = true

cookieStore, ok := ctlr.CookieStore.(*sessions.FilesystemStore)
cookieStore, ok := ctlr.CookieStore.Store.(*sessions.FilesystemStore)
So(ok, ShouldBeTrue)

// first encode sessionID
Expand Down Expand Up @@ -3450,7 +3450,7 @@ func TestAuthnSessionErrors(t *testing.T) {
session.Values["authStatus"] = false
session.Values["username"] = username

cookieStore, ok := ctlr.CookieStore.(*sessions.FilesystemStore)
cookieStore, ok := ctlr.CookieStore.Store.(*sessions.FilesystemStore)
So(ok, ShouldBeTrue)

// first encode sessionID
Expand Down
159 changes: 159 additions & 0 deletions pkg/api/cookiestore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package api

import (
"context"
"encoding/gob"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"time"

"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"

zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/scheduler"
"zotregistry.io/zot/pkg/storage"
storageConstants "zotregistry.io/zot/pkg/storage/constants"
)

const cookiesMaxAge = 7200 // 2h

type CookieStore struct {
needsCleanup bool // if store should be periodically cleaned
rootDir string
sessions.Store
}

func (c *CookieStore) RunSessionCleaner(sch *scheduler.Scheduler) {
if c.needsCleanup {
sch.SubmitGenerator(&SessionCleanup{rootDir: c.rootDir}, cookiesMaxAge, scheduler.LowPriority)
}
}

func NewCookieStore(storeController storage.StoreController) (*CookieStore, error) {
// To store custom types in our cookies
// we must first register them using gob.Register
gob.Register(map[string]interface{}{})

hashKey, err := getHashKey()
if err != nil {
return &CookieStore{}, err
}

Check warning on line 44 in pkg/api/cookiestore.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/cookiestore.go#L43-L44

Added lines #L43 - L44 were not covered by tests

var store sessions.Store

var sessionsDir string

var needsCleanup bool

if storeController.DefaultStore.Name() == storageConstants.LocalStorageDriverName {
sessionsDir = path.Join(storeController.DefaultStore.RootDir(), "_sessions")
if err := os.MkdirAll(sessionsDir, storageConstants.DefaultDirPerms); err != nil {
return &CookieStore{}, err
}

Check warning on line 56 in pkg/api/cookiestore.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/cookiestore.go#L55-L56

Added lines #L55 - L56 were not covered by tests

localStore := sessions.NewFilesystemStore(sessionsDir, hashKey)

localStore.MaxAge(cookiesMaxAge)

store = localStore
needsCleanup = true
} else {
memStore := sessions.NewCookieStore(hashKey)

memStore.MaxAge(cookiesMaxAge)

store = memStore
}

return &CookieStore{
Store: store,
rootDir: sessionsDir,
needsCleanup: needsCleanup,
}, nil
}

func getHashKey() ([]byte, error) {
hashKey := securecookie.GenerateRandomKey(64)
if hashKey == nil {
return nil, zerr.ErrHashKeyNotCreated
}

Check warning on line 83 in pkg/api/cookiestore.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/cookiestore.go#L82-L83

Added lines #L82 - L83 were not covered by tests

return hashKey, nil
}

func getExpiredSessions(dir string) ([]string, error) {
sessions := make([]string, 0)

err := filepath.WalkDir(dir, func(_ string, dirEntry fs.DirEntry, err error) error {
if err != nil {
return err
}

Check warning on line 94 in pkg/api/cookiestore.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/cookiestore.go#L93-L94

Added lines #L93 - L94 were not covered by tests

fileInfo, err := dirEntry.Info()
if err != nil { // may have been deleted in the meantime
return nil //nolint: nilerr
}

Check warning on line 99 in pkg/api/cookiestore.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/cookiestore.go#L98-L99

Added lines #L98 - L99 were not covered by tests

if !strings.HasPrefix(fileInfo.Name(), "session_") {
return nil
}

if fileInfo.ModTime().Add(cookiesMaxAge * time.Second).Before(time.Now()) {
sessions = append(sessions, path.Join(dir, fileInfo.Name()))
}

Check warning on line 107 in pkg/api/cookiestore.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/cookiestore.go#L105-L107

Added lines #L105 - L107 were not covered by tests

return nil

Check warning on line 109 in pkg/api/cookiestore.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/cookiestore.go#L109

Added line #L109 was not covered by tests
})

return sessions, err
}

type SessionCleanup struct {
rootDir string
done bool
}

func (gen *SessionCleanup) Next() (scheduler.Task, error) {
sessions, err := getExpiredSessions(gen.rootDir)
if err != nil {
return nil, err
}

Check warning on line 124 in pkg/api/cookiestore.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/cookiestore.go#L123-L124

Added lines #L123 - L124 were not covered by tests

if len(sessions) == 0 {
gen.done = true

return nil, nil
}

return &CleanTask{sessions: sessions}, nil

Check warning on line 132 in pkg/api/cookiestore.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/cookiestore.go#L132

Added line #L132 was not covered by tests
}

func (gen *SessionCleanup) IsDone() bool {
return gen.done
}

func (gen *SessionCleanup) IsReady() bool {
return true
}

func (gen *SessionCleanup) Reset() {
gen.done = false
}

type CleanTask struct {
sessions []string
}

func (cleanTask *CleanTask) DoWork(ctx context.Context) error {
for _, session := range cleanTask.sessions {
if err := os.Remove(session); err != nil {
return err
}

Check warning on line 155 in pkg/api/cookiestore.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/cookiestore.go#L151-L155

Added lines #L151 - L155 were not covered by tests
}

return nil

Check warning on line 158 in pkg/api/cookiestore.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/cookiestore.go#L158

Added line #L158 was not covered by tests
}
4 changes: 4 additions & 0 deletions pkg/storage/imagestore/imagestore.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ type ImageStore struct {
commit bool
}

func (is *ImageStore) Name() string {
return is.storeDriver.Name()
}

func (is *ImageStore) RootDir() string {
return is.rootDir
}
Expand Down
1 change: 1 addition & 0 deletions pkg/storage/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type StoreController interface {
}

type ImageStore interface { //nolint:interfacebloat
Name() string
DirExists(d string) bool
RootDir() string
RLock(*time.Time)
Expand Down
9 changes: 9 additions & 0 deletions pkg/test/mocks/image_store_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
)

type MockedImageStore struct {
NameFn func() string
DirExistsFn func(d string) bool
RootDirFn func() string
InitRepoFn func(name string) error
Expand Down Expand Up @@ -68,6 +69,14 @@ func (is MockedImageStore) RUnlock(t *time.Time) {
func (is MockedImageStore) RLock(t *time.Time) {
}

func (is MockedImageStore) Name() string {
if is.NameFn != nil {
return is.NameFn()
}

return ""
}

func (is MockedImageStore) DirExists(d string) bool {
if is.DirExistsFn != nil {
return is.DirExistsFn(d)
Expand Down
Loading