diff --git a/internal/config/config_helpers.go b/internal/config/config_helpers.go index 84769d6..fcf2233 100644 --- a/internal/config/config_helpers.go +++ b/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 } diff --git a/internal/executor/executor.go b/internal/executor/executor.go index e596e7c..df110cb 100644 --- a/internal/executor/executor.go +++ b/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) diff --git a/internal/filehelper/file_write.go b/internal/filehelper/file_write.go new file mode 100644 index 0000000..001c29e --- /dev/null +++ b/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 + } +} diff --git a/internal/httpservers/restapi.go b/internal/httpservers/restapi.go index 5779bb6..158f81b 100644 --- a/internal/httpservers/restapi.go +++ b/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") diff --git a/internal/httpservers/restapi_auth.go b/internal/httpservers/restapi_auth.go new file mode 100644 index 0000000..d11eaf3 --- /dev/null +++ b/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 +} diff --git a/internal/httpservers/restapi_auth_local.go b/internal/httpservers/restapi_auth_local.go index 21e5c7d..9988349 100644 --- a/internal/httpservers/restapi_auth_local.go +++ b/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,