mirror of
https://github.com/goauthentik/authentik
synced 2026-04-27 09:57:31 +02:00
* 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>
207 lines
5.8 KiB
Go
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
|
|
}
|