feature: persist local sessions across restart (#522)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
James Read 2025-02-21 22:26:15 +00:00 committed by GitHub
parent f3bc82311d
commit 7788f58aac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 239 additions and 28 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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")

View File

@ -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
}

View File

@ -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,