Kaynağa Gözat

feature: persist local sessions across restart (#522)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
James Read 1 yıl önce
ebeveyn
işleme
7788f58aac

+ 10 - 0
internal/config/config_helpers.go

@@ -43,6 +43,16 @@ func (cfg *Config) FindAcl(aclTitle string) *AccessControlList {
 	return nil
 }
 
+func (cfg *Config) FindUserByUsername(searchUsername string) *LocalUser {
+	for _, user := range cfg.AuthLocalUsers.Users {
+		if user.Username == searchUsername {
+			return user
+		}
+	}
+
+	return nil
+}
+
 func (cfg *Config) SetDir(dir string) {
 	cfg.usedConfigDir = dir
 }

+ 1 - 1
internal/executor/executor.go

@@ -179,7 +179,7 @@ func (e *Executor) GetLogTrackingIds(startOffset int64, pageCount int64) ([]*Int
 		"total":       totalLogCount,
 		"startIndex":  startIndex,
 		"endIndex":    endIndex,
-	}).Infof("GetLogTrackingIds")
+	}).Tracef("GetLogTrackingIds")
 
 	trackingIds := make([]*InternalLogEntry, 0, pageCount)
 

+ 38 - 0
internal/filehelper/file_write.go

@@ -0,0 +1,38 @@
+package filehelper
+
+import (
+	log "github.com/sirupsen/logrus"
+	"os"
+	"sync"
+)
+
+var writeFileMutex sync.Mutex
+
+func WriteFile(filename string, out []byte) {
+	writeFileMutex.Lock()
+
+	defer writeFileMutex.Unlock()
+
+	if _, err := os.Stat(filename); os.IsNotExist(err) {
+		handle, err := os.Create(filename)
+		handle.Close()
+
+		if err != nil {
+			log.WithFields(log.Fields{
+				"error": err,
+			}).Errorf("Failed to create %v", filename)
+
+			return
+		}
+
+	}
+
+	err := os.WriteFile(filename, out, 0600)
+
+	if err != nil {
+		log.WithFields(log.Fields{
+			"error": err,
+		}).Errorf("Failed to write session to %v", filename)
+		return
+	}
+}

+ 9 - 3
internal/httpservers/restapi.go

@@ -107,12 +107,16 @@ func forwardResponseHandlerLogout(md metadata.MD, w http.ResponseWriter) {
 		http.SetCookie(
 			w,
 			&http.Cookie{
-				Name:  "olivetin-sid-oauth",
-				Value: "",
+				Name:     "olivetin-sid-oauth",
+				MaxAge:   31556952, // 1 year
+				Value:    "",
+				HttpOnly: true,
+				Path:     "/",
 			},
 		)
 
-		delete(localUserSessions, sid)
+		deleteLocalUserSession("local", sid)
+
 		http.SetCookie(
 			w,
 			&http.Cookie{
@@ -147,6 +151,8 @@ func SetGlobalRestConfig(config *config.Config) {
 func startRestAPIServer(globalConfig *config.Config) error {
 	cfg = globalConfig
 
+	loadUserSessions()
+
 	log.WithFields(log.Fields{
 		"address": cfg.ListenAddressRestActions,
 	}).Info("Starting REST API")

+ 177 - 0
internal/httpservers/restapi_auth.go

@@ -0,0 +1,177 @@
+package httpservers
+
+import (
+	"github.com/OliveTin/OliveTin/internal/config"
+	"github.com/OliveTin/OliveTin/internal/filehelper"
+	log "github.com/sirupsen/logrus"
+	"gopkg.in/yaml.v3"
+	"os"
+	"path/filepath"
+	"sync"
+	"time"
+)
+
+var sessionStorageMutex sync.Mutex
+
+type UserSession struct {
+	Username string
+	Expiry   int64
+}
+
+type SessionProvider struct {
+	Sessions map[string]*UserSession
+}
+
+type SessionStorage struct {
+	Providers map[string]*SessionProvider
+}
+
+var (
+	sessionStorage *SessionStorage
+)
+
+func registerSessionProviders() {
+	sessionStorage = &SessionStorage{
+		Providers: make(map[string]*SessionProvider),
+	}
+
+	registerSessionProvider("local")
+	registerSessionProvider("oauth2")
+}
+
+func registerSessionProvider(provider string) {
+	sessionStorage.Providers[provider] = &SessionProvider{
+		Sessions: make(map[string]*UserSession),
+	}
+}
+
+func deleteLocalUserSession(provider string, sid string) {
+	sessionStorageMutex.Lock()
+
+	deleteLocalUserSessionBatch(provider, sid)
+
+	sessionStorageMutex.Unlock()
+
+	saveUserSessions()
+}
+
+func deleteLocalUserSessionBatch(provider string, sid string) {
+	log.WithFields(log.Fields{
+		"sid":      sid,
+		"provider": provider,
+	}).Debug("Deleting user session")
+
+	if _, ok := sessionStorage.Providers[provider]; !ok {
+		return
+	}
+
+	delete(sessionStorage.Providers[provider].Sessions, sid)
+}
+
+func registerUserSession(provider string, sid string, username string) {
+	sessionStorageMutex.Lock()
+	sessionStorage.Providers[provider].Sessions[sid] = &UserSession{
+		Username: username,
+		Expiry:   time.Now().Unix() + 31556952, // 1 year
+	}
+	sessionStorageMutex.Unlock()
+
+	saveUserSessions()
+}
+
+func saveUserSessions() {
+	sessionStorageMutex.Lock()
+	defer sessionStorageMutex.Unlock()
+
+	filename := filepath.Join(cfg.GetDir(), "sessions.db.yaml")
+
+	out, err := yaml.Marshal(sessionStorage)
+
+	if err != nil {
+		log.WithFields(log.Fields{
+			"error": err,
+		}).Errorf("Failed to marshal session data to %v", filename)
+		return
+	}
+
+	filehelper.WriteFile(filename, out)
+}
+
+func loadUserSessions() {
+	registerSessionProviders()
+
+	filename := filepath.Join(cfg.GetDir(), "sessions.db.yaml")
+
+	if _, err := os.Stat(filename); os.IsNotExist(err) {
+		return
+	}
+
+	data, err := os.ReadFile(filename)
+
+	if err != nil {
+		log.WithFields(log.Fields{
+			"error": err,
+		}).Errorf("Failed to read %v", filename)
+		return
+	}
+
+	err = yaml.Unmarshal(data, &sessionStorage)
+
+	if err != nil {
+		log.WithFields(log.Fields{
+			"error": err,
+		}).Error("Failed to unmarshal sessions.local.db")
+		return
+	}
+
+	deleteExpiredSessions()
+}
+
+func deleteExpiredSessions() {
+	sessionStorageMutex.Lock()
+
+	for provider, sessions := range sessionStorage.Providers {
+		for sid, session := range sessions.Sessions {
+			if session.Expiry < time.Now().Unix() {
+				deleteLocalUserSessionBatch(provider, sid)
+			}
+		}
+	}
+
+	sessionStorageMutex.Unlock()
+
+	saveUserSessions()
+}
+
+func getUserFromSession(providerName string, sid string) *config.LocalUser {
+	provider, ok := sessionStorage.Providers[providerName]
+
+	if !ok {
+		log.WithFields(log.Fields{
+			"provider": providerName,
+		}).Warnf("Provider not found")
+		return nil
+	}
+
+	session, ok := provider.Sessions[sid]
+
+	if !ok {
+		log.WithFields(log.Fields{
+			"sid":      sid,
+			"provider": providerName,
+		}).Warnf("Stale session")
+		return nil
+	}
+
+	user := cfg.FindUserByUsername(session.Username)
+
+	if user == nil {
+		log.WithFields(log.Fields{
+			"sid":      sid,
+			"provider": providerName,
+		}).Warnf("User not found")
+		return nil
+	}
+
+	return user
+}

+ 4 - 24
internal/httpservers/restapi_auth_local.go

@@ -4,13 +4,7 @@ import (
 	"google.golang.org/grpc/metadata"
 	"net/http"
 
-	"github.com/OliveTin/OliveTin/internal/config"
 	"github.com/google/uuid"
-	log "github.com/sirupsen/logrus"
-)
-
-var (
-	localUserSessions = make(map[string]*config.LocalUser)
 )
 
 func parseLocalUserCookie(req *http.Request) (string, string, string) {
@@ -22,41 +16,27 @@ func parseLocalUserCookie(req *http.Request) (string, string, string) {
 
 	cookieValue := cookie.Value
 
-	user, ok := localUserSessions[cookieValue]
+	user := getUserFromSession("local", cookieValue)
 
-	if !ok {
-		log.WithFields(log.Fields{
-			"sid":      cookieValue,
-			"provider": "local",
-		}).Warnf("Stale session")
+	if user == nil {
 		return "", "", ""
 	}
 
 	return user.Username, user.Usergroup, cookie.Value
 }
 
-func findUserByUsername(searchUsername string) *config.LocalUser {
-	for _, user := range cfg.AuthLocalUsers.Users {
-		if user.Username == searchUsername {
-			return user
-		}
-	}
-
-	return nil
-}
-
 func forwardResponseHandlerLoginLocalUser(md metadata.MD, w http.ResponseWriter) error {
 	setUsername := getMetadataKeyOrEmpty(md, "set-username")
 
 	if setUsername != "" {
-		user := findUserByUsername(setUsername)
+		user := cfg.FindUserByUsername(setUsername)
 
 		if user == nil {
 			return nil
 		}
 
 		sid := uuid.NewString()
-		localUserSessions[sid] = user
+		registerUserSession("local", sid, user.Username)
 
 		http.SetCookie(
 			w,