feature: persist local sessions across restart (#522)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
parent
f3bc82311d
commit
7788f58aac
|
|
@ -43,6 +43,16 @@ func (cfg *Config) FindAcl(aclTitle string) *AccessControlList {
|
||||||
return nil
|
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) {
|
func (cfg *Config) SetDir(dir string) {
|
||||||
cfg.usedConfigDir = dir
|
cfg.usedConfigDir = dir
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ func (e *Executor) GetLogTrackingIds(startOffset int64, pageCount int64) ([]*Int
|
||||||
"total": totalLogCount,
|
"total": totalLogCount,
|
||||||
"startIndex": startIndex,
|
"startIndex": startIndex,
|
||||||
"endIndex": endIndex,
|
"endIndex": endIndex,
|
||||||
}).Infof("GetLogTrackingIds")
|
}).Tracef("GetLogTrackingIds")
|
||||||
|
|
||||||
trackingIds := make([]*InternalLogEntry, 0, pageCount)
|
trackingIds := make([]*InternalLogEntry, 0, pageCount)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -107,12 +107,16 @@ func forwardResponseHandlerLogout(md metadata.MD, w http.ResponseWriter) {
|
||||||
http.SetCookie(
|
http.SetCookie(
|
||||||
w,
|
w,
|
||||||
&http.Cookie{
|
&http.Cookie{
|
||||||
Name: "olivetin-sid-oauth",
|
Name: "olivetin-sid-oauth",
|
||||||
Value: "",
|
MaxAge: 31556952, // 1 year
|
||||||
|
Value: "",
|
||||||
|
HttpOnly: true,
|
||||||
|
Path: "/",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
delete(localUserSessions, sid)
|
deleteLocalUserSession("local", sid)
|
||||||
|
|
||||||
http.SetCookie(
|
http.SetCookie(
|
||||||
w,
|
w,
|
||||||
&http.Cookie{
|
&http.Cookie{
|
||||||
|
|
@ -147,6 +151,8 @@ func SetGlobalRestConfig(config *config.Config) {
|
||||||
func startRestAPIServer(globalConfig *config.Config) error {
|
func startRestAPIServer(globalConfig *config.Config) error {
|
||||||
cfg = globalConfig
|
cfg = globalConfig
|
||||||
|
|
||||||
|
loadUserSessions()
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"address": cfg.ListenAddressRestActions,
|
"address": cfg.ListenAddressRestActions,
|
||||||
}).Info("Starting REST API")
|
}).Info("Starting REST API")
|
||||||
|
|
|
||||||
|
|
@ -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,13 +4,7 @@ import (
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/OliveTin/OliveTin/internal/config"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
localUserSessions = make(map[string]*config.LocalUser)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseLocalUserCookie(req *http.Request) (string, string, string) {
|
func parseLocalUserCookie(req *http.Request) (string, string, string) {
|
||||||
|
|
@ -22,41 +16,27 @@ func parseLocalUserCookie(req *http.Request) (string, string, string) {
|
||||||
|
|
||||||
cookieValue := cookie.Value
|
cookieValue := cookie.Value
|
||||||
|
|
||||||
user, ok := localUserSessions[cookieValue]
|
user := getUserFromSession("local", cookieValue)
|
||||||
|
|
||||||
if !ok {
|
if user == nil {
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"sid": cookieValue,
|
|
||||||
"provider": "local",
|
|
||||||
}).Warnf("Stale session")
|
|
||||||
return "", "", ""
|
return "", "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return user.Username, user.Usergroup, cookie.Value
|
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 {
|
func forwardResponseHandlerLoginLocalUser(md metadata.MD, w http.ResponseWriter) error {
|
||||||
setUsername := getMetadataKeyOrEmpty(md, "set-username")
|
setUsername := getMetadataKeyOrEmpty(md, "set-username")
|
||||||
|
|
||||||
if setUsername != "" {
|
if setUsername != "" {
|
||||||
user := findUserByUsername(setUsername)
|
user := cfg.FindUserByUsername(setUsername)
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sid := uuid.NewString()
|
sid := uuid.NewString()
|
||||||
localUserSessions[sid] = user
|
registerUserSession("local", sid, user.Username)
|
||||||
|
|
||||||
http.SetCookie(
|
http.SetCookie(
|
||||||
w,
|
w,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue