From b3e67bad75d023a0febe7d3c4b2572a3fffd788e Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 16 Nov 2025 23:08:20 +0000 Subject: [PATCH] doc: Add config tool to help support people --- Makefile | 3 + service/cmd/config-tool/main.go | 154 +++++++++++++++++++++++ service/internal/api/local_user_login.go | 9 +- service/internal/config/sanitize.go | 12 +- 4 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 service/cmd/config-tool/main.go diff --git a/Makefile b/Makefile index d06002f..aa70614 100644 --- a/Makefile +++ b/Makefile @@ -56,4 +56,7 @@ clean: $(call delete-files,reports) $(call delete-files,gen) +config-tool: + cd service && go run cmd/config-tool/main.go + .PHONY: proto service diff --git a/service/cmd/config-tool/main.go b/service/cmd/config-tool/main.go new file mode 100644 index 0000000..2a92633 --- /dev/null +++ b/service/cmd/config-tool/main.go @@ -0,0 +1,154 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strconv" + + "github.com/OliveTin/OliveTin/internal/api" + config "github.com/OliveTin/OliveTin/internal/config" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" + log "github.com/sirupsen/logrus" +) + +func printPwd() { + pwd, err := os.Getwd() + if err != nil { + log.Fatalf("Error getting working directory: %v", err) + } + log.Infof("Working directory: %s", pwd) +} + +func main() { + resetPasswords := flag.Bool("passwords", true, "Reset passwords") + flag.Parse() + + log.Info("Config tool started") + + printPwd() + + k := koanf.New(".") + + configPath, err := filepath.Abs("../config.yaml") + if err != nil { + log.Fatalf("Error getting absolute config path: %v", err) + } + + log.Infof("Loading config from %s", configPath) + + backupOriginalConfig(configPath) + + err = k.Load(file.Provider(configPath), yaml.Parser()) + + if err != nil { + log.Fatalf("Error loading config: %v", err) + } + + cfg := &config.Config{} + + config.AppendSource(cfg, k, configPath) + + if *resetPasswords { + resetAllPasswords(k, cfg) + } + + saveConfig(k) +} + +func backupOriginalConfig(configPath string) { + originalConfigPath := filepath.Join(filepath.Dir(configPath), "config.original.yaml") + data, err := os.ReadFile(configPath) + if err != nil { + log.Fatalf("Error reading config for backup: %v", err) + } + err = os.WriteFile(originalConfigPath, data, 0644) + if err != nil { + log.Fatalf("Error writing backup config: %v", err) + } + log.Infof("Original config backed up to %s", originalConfigPath) +} + +func resetAllPasswords(k *koanf.Koanf, cfg *config.Config) { + if !cfg.AuthLocalUsers.Enabled || len(cfg.AuthLocalUsers.Users) == 0 { + log.Info("No local users found, skipping password reset") + return + } + + hashedPassword, err := api.CreateHash("password") + if err != nil { + log.Fatalf("Error creating password hash: %v", err) + } + + usersSlice := k.Get("authLocalUsers.users") + usersSliceTyped, ok := usersSlice.([]interface{}) + + if ok && len(usersSliceTyped) > 0 { + newUsersSlice := make([]interface{}, len(usersSliceTyped)) + for index, userValue := range usersSliceTyped { + userMap, ok := userValue.(map[string]interface{}) + if !ok { + log.Warnf("User entry at index %d is not a map, skipping", index) + newUsersSlice[index] = userValue + continue + } + + oldPassword, _ := userMap["password"].(string) + username, _ := userMap["username"].(string) + if username == "" { + username = fmt.Sprintf("user[%d]", index) + } + + newUserMap := make(map[string]interface{}) + for k, v := range userMap { + newUserMap[k] = v + } + newUserMap["password"] = hashedPassword + newUsersSlice[index] = newUserMap + + oldHashPreview := oldPassword + if len(oldPassword) > 20 { + oldHashPreview = oldPassword[:20] + } + log.Infof("Reset password for user '%s' (old hash: %s...)", username, oldHashPreview) + } + k.Set("authLocalUsers.users", newUsersSlice) + } else { + for index, user := range cfg.AuthLocalUsers.Users { + key := "authLocalUsers.users." + strconv.Itoa(index) + ".password" + k.Set(key, hashedPassword) + + oldHashPreview := user.Password + if len(oldHashPreview) > 20 { + oldHashPreview = oldHashPreview[:20] + } + log.Infof("Reset password for user '%s' (old hash: %s...)", user.Username, oldHashPreview) + } + } + + log.Infof("Reset %d password(s) to 'password'", len(cfg.AuthLocalUsers.Users)) +} + +func saveConfig(k *koanf.Koanf) { + pwd, err := os.Getwd() + if err != nil { + log.Fatalf("Error getting working directory: %v", err) + } + fullPath := filepath.Join(filepath.Dir(filepath.Dir(filepath.Dir(pwd))), "config.yaml") + + out, err := k.Marshal(yaml.Parser()) + + if err != nil { + log.Fatalf("Error marshalling config: %v", err) + } + + err = os.WriteFile(fullPath, out, 0644) + if err != nil { + log.Fatalf("Error saving config: %v", err) + } + + log.Infof("Config saved to %s", fullPath) +} diff --git a/service/internal/api/local_user_login.go b/service/internal/api/local_user_login.go index c6d8442..db6e99e 100644 --- a/service/internal/api/local_user_login.go +++ b/service/internal/api/local_user_login.go @@ -1,10 +1,11 @@ package api import ( + "runtime" + config "github.com/OliveTin/OliveTin/internal/config" "github.com/alexedwards/argon2id" log "github.com/sirupsen/logrus" - "runtime" ) var defaultParams = argon2id.Params{ @@ -15,7 +16,7 @@ var defaultParams = argon2id.Params{ KeyLength: 32, } -func createHash(password string) (string, error) { +func CreateHash(password string) (string, error) { hash, err := argon2id.CreateHash(password, &defaultParams) if err != nil { @@ -26,6 +27,10 @@ func createHash(password string) (string, error) { return hash, nil } +func createHash(password string) (string, error) { + return CreateHash(password) +} + func comparePasswordAndHash(password, hash string) bool { match, err := argon2id.ComparePasswordAndHash(password, hash) diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index 2a93759..9040ade 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -1,15 +1,18 @@ package config import ( + "strings" + "github.com/google/uuid" log "github.com/sirupsen/logrus" - "strings" ) // Sanitize will look for common configuration issues, and fix them. For example, // populating undefined fields - name -> title, etc. func (cfg *Config) Sanitize() { cfg.sanitizeLogLevel() + cfg.sanitizeAuthRequireGuestsToLogin() + cfg.sanitizeLogHistoryPageSize() // log.Infof("cfg %p", cfg) @@ -41,12 +44,9 @@ func (action *Action) sanitize(cfg *Config) { for idx := range action.Arguments { action.Arguments[idx].sanitize() } - - sanitizeAuthRequireGuestsToLogin(cfg) - sanitizeLogHistoryPageSize(cfg) } -func sanitizeAuthRequireGuestsToLogin(cfg *Config) { +func (cfg *Config) sanitizeAuthRequireGuestsToLogin() { if cfg.AuthRequireGuestsToLogin { log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false") @@ -56,7 +56,7 @@ func sanitizeAuthRequireGuestsToLogin(cfg *Config) { } } -func sanitizeLogHistoryPageSize(cfg *Config) { +func (cfg *Config) sanitizeLogHistoryPageSize() { if cfg.LogHistoryPageSize < 10 { log.Warnf("LogsHistoryLimit is too low, setting it to 10") cfg.LogHistoryPageSize = 10