Files
authentik/internal/outpost/proxyv2/filesystemstore/filesystemstore.go
Dominic R 795a025af9 outpost/proxyv2: postgresstore: db/pool/misc cleanup and enhancement (#17511)
* wip

* Update internal/outpost/proxyv2/application/session_postgres_test.go

Signed-off-by: Dominic R <dominic@sdko.org>

* Update refresh.go

Co-authored-by: Jens L. <jens@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-10-20 16:25:13 +02:00

207 lines
5.8 KiB
Go

package filesystemstore
import (
"context"
"errors"
"os"
"path"
"strings"
"sync"
"syscall"
"time"
"github.com/gorilla/sessions"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/outpost/proxyv2/sessionstore"
)
const (
SessionCleanupInterval = 5 * time.Minute
SessionCleanupLockFileName = "session-cleanup.lock"
SessionFilePrefix = "session_"
SessionTestFile = SessionFilePrefix + "write_test"
)
var (
ErrSessionCleanupAlreadyRunning = errors.New("session cleanup is already running by another instance")
ErrSessionStoreNoPermission = errors.New("path is not writable")
ErrSessionStorePathNotExist = errors.New("path does not exist")
)
type Store struct {
*sessions.FilesystemStore
storePath string
log *log.Entry
cleanupManager *sessionstore.CleanupManager
}
// NewStore checks if the specified store path exists, is writable and creates a new filesystem session store.
func NewStore(storePath string, keyPairs ...[]byte) (*Store, error) {
if storePath == "" {
storePath = os.TempDir()
}
// check if path exists
_, err := os.ReadDir(storePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrSessionStorePathNotExist
}
return nil, err
}
// check if path is writable
testPath := path.Join(storePath, SessionTestFile)
testFile, err := os.OpenFile(testPath, os.O_CREATE, 0600)
if err != nil {
if errors.Is(err, os.ErrPermission) {
return nil, ErrSessionStoreNoPermission
}
return nil, err
}
if err = testFile.Close(); err != nil {
return nil, err
}
if err = os.Remove(testPath); err != nil {
return nil, err
}
store := &Store{
FilesystemStore: sessions.NewFilesystemStore(storePath, keyPairs...),
storePath: storePath,
log: log.WithField("logger", "authentik.outpost.proxyv2.filesystemstore"),
}
return store, nil
}
// CleanupExpired implements the CleanupStore interface for use with CleanupManager
func (s *Store) CleanupExpired(ctx context.Context) error {
return s.SessionCleanup(ctx)
}
// SessionCleanup acquires a file lock to ensure only one instance runs at a time,
// then checks and deletes expired session files from the filesystem session store.
// It supports context-based cancellation to allow graceful shutdowns or timeouts.
func (s *Store) SessionCleanup(ctx context.Context) error {
s.log.Info("Starting session cleanup")
lockPath := path.Join(s.storePath, SessionCleanupLockFileName)
lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return err
}
defer func() {
if closeErr := lockFile.Close(); closeErr != nil {
s.log.WithError(closeErr).Warn("failed to close lock file")
}
}()
err = syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EWOULDBLOCK {
return ErrSessionCleanupAlreadyRunning
}
return err
}
defer func() {
if flockErr := syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN); flockErr != nil {
s.log.WithError(flockErr).Warn("failed to unlock file")
}
if removeErr := os.Remove(lockPath); removeErr != nil {
s.log.WithError(removeErr).Warn("failed to remove lock file")
}
}()
return s.sessionCleanup(ctx)
}
// sessionCleanup checks the modification time of all session files and removes them
// when they reach the configured maximum age in the session store.
// Since the FilesystemStore from Gorilla does not have a session cleanup function,
// it is only necessary for the filesystem session store.
func (s *Store) sessionCleanup(ctx context.Context) error {
files, err := os.ReadDir(s.storePath)
if err != nil {
return err
}
var errs []error
for _, file := range files {
select {
case <-ctx.Done():
s.log.Warn("session cleanup interrupted during file processing")
return ctx.Err()
default:
}
if !strings.HasPrefix(file.Name(), SessionFilePrefix) {
continue
}
fullPath := path.Join(s.storePath, file.Name())
stat, err := os.Lstat(fullPath)
if err != nil {
s.log.WithError(err).WithField("path", fullPath).Warning("failed to read stats from file")
errs = append(errs, err)
continue
}
modTime := stat.ModTime()
if time.Since(modTime) <= time.Duration(s.Options.MaxAge)*time.Second {
s.log.WithField("max-age", s.Options.MaxAge).WithField("modified", modTime.String()).Debug("session still valid")
continue
}
s.log.WithField("path", fullPath).WithField("modified", modTime.String()).Info("cleanup expired session")
if err = os.Remove(fullPath); err != nil {
s.log.WithError(err).WithField("path", fullPath).Warn("failed to delete session")
errs = append(errs, err)
continue
}
}
return errors.Join(errs...)
}
var (
globalStore *Store
mu sync.Mutex
)
// GetPersistentStore creates a new filesystem store if it is the first time the function has been called,
// or if the path string has changed. It then stores this in the globalStore variable.
// If the function is called multiple times, the store from the variable is returned to ensure that only one instance is running.
func GetPersistentStore(path string) (*Store, error) {
mu.Lock()
defer mu.Unlock()
if globalStore == nil || globalStore.storePath != path {
if globalStore != nil && globalStore.cleanupManager != nil {
globalStore.cleanupManager.Stop()
}
store, err := NewStore(path)
if err != nil {
return nil, err
}
globalStore = store
// Initialize cleanup manager
globalStore.cleanupManager = sessionstore.NewCleanupManager(
globalStore,
globalStore.log,
)
globalStore.cleanupManager.Start()
}
return globalStore, nil
}
// StopPersistentStore stops the cleanup background job and clears the globalStore variable.
func StopPersistentStore() {
mu.Lock()
defer mu.Unlock()
if globalStore != nil && globalStore.cleanupManager != nil {
globalStore.cleanupManager.Stop()
}
globalStore = nil
}