diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 29071c7..4f8b862 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -243,6 +243,11 @@ jobs: GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }} GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }} run: | + # SignPath may name the signed zip *.zip.zip (one compression level, bad Content-Disposition). + if [[ -f signed-windows-zip/OliveTin-windows-amd64.zip.zip ]]; then + mv signed-windows-zip/OliveTin-windows-amd64.zip.zip \ + signed-windows-zip/OliveTin-windows-amd64.zip + fi zip_path="$(find signed-windows-zip -type f -name 'OliveTin-windows-amd64.zip' | head -n 1)" msi_path="$(find signed-windows-msi -type f -name 'OliveTin-windows-amd64.msi' | head -n 1)" if [[ -z "${zip_path}" || -z "${msi_path}" ]]; then diff --git a/service/cmd/config-tool/main.go b/service/cmd/config-tool/main.go index 4a9ebf3..e714e64 100644 --- a/service/cmd/config-tool/main.go +++ b/service/cmd/config-tool/main.go @@ -82,8 +82,90 @@ func backupOriginalConfig(configPath string) { log.Infof("Original config backed up to %s", originalConfigPath) } +func passwordHashPreview(password string) string { + if len(password) > 20 { + return password[:20] + } + + return password +} + +func userDisplayName(username string, index int) string { + if username == "" { + return fmt.Sprintf("user[%d]", index) + } + + return username +} + +func copyUserMapWithPassword(userMap map[string]interface{}, hashedPassword string) map[string]interface{} { + newUserMap := make(map[string]interface{}, len(userMap)+1) + for key, value := range userMap { + newUserMap[key] = value + } + newUserMap["password"] = hashedPassword + + return newUserMap +} + +func resetPasswordInUserMap(userValue interface{}, index int, hashedPassword string) interface{} { + userMap, ok := userValue.(map[string]interface{}) + if !ok { + log.Warnf("User entry at index %d is not a map, skipping", index) + return userValue + } + + oldPassword, _ := userMap["password"].(string) + username, _ := userMap["username"].(string) + log.Infof("Reset password for user '%s' (old hash: %s...)", userDisplayName(username, index), passwordHashPreview(oldPassword)) + + return copyUserMapWithPassword(userMap, hashedPassword) +} + +func resetPasswordsFromSlice(k *koanf.Koanf, usersSliceTyped []interface{}, hashedPassword string) { + newUsersSlice := make([]interface{}, len(usersSliceTyped)) + for index, userValue := range usersSliceTyped { + newUsersSlice[index] = resetPasswordInUserMap(userValue, index, hashedPassword) + } + + err := k.Set("authLocalUsers.users", newUsersSlice) + if err != nil { + log.WithFields(log.Fields{ + "error": err, + }).Fatalf("Error setting users") + } +} + +func resetPasswordsFromConfig(k *koanf.Koanf, cfg *config.Config, hashedPassword string) { + for index, user := range cfg.AuthLocalUsers.Users { + key := "authLocalUsers.users." + strconv.Itoa(index) + ".password" + err := k.Set(key, hashedPassword) + if err != nil { + log.WithFields(log.Fields{ + "error": err, + }).Fatalf("Error setting user password") + } + + log.Infof("Reset password for user '%s' (old hash: %s...)", user.Username, passwordHashPreview(user.Password)) + } +} + +func hasLocalUsers(cfg *config.Config) bool { + return cfg.AuthLocalUsers.Enabled && len(cfg.AuthLocalUsers.Users) > 0 +} + +func applyPasswordResets(k *koanf.Koanf, cfg *config.Config, hashedPassword string) { + usersSliceTyped, ok := k.Get("authLocalUsers.users").([]interface{}) + if ok && len(usersSliceTyped) > 0 { + resetPasswordsFromSlice(k, usersSliceTyped, hashedPassword) + return + } + + resetPasswordsFromConfig(k, cfg, hashedPassword) +} + func resetAllPasswords(k *koanf.Koanf, cfg *config.Config) { - if !cfg.AuthLocalUsers.Enabled || len(cfg.AuthLocalUsers.Users) == 0 { + if !hasLocalUsers(cfg) { log.Info("No local users found, skipping password reset") return } @@ -93,64 +175,7 @@ func resetAllPasswords(k *koanf.Koanf, cfg *config.Config) { 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) - } - err = k.Set("authLocalUsers.users", newUsersSlice) - - if err != nil { - log.WithFields(log.Fields{ - "error": err, - }).Fatalf("Error setting users") - } - } else { - for index, user := range cfg.AuthLocalUsers.Users { - key := "authLocalUsers.users." + strconv.Itoa(index) + ".password" - err = k.Set(key, hashedPassword) - - if err != nil { - log.WithFields(log.Fields{ - "error": err, - }).Fatalf("Error setting user password") - } - - oldHashPreview := user.Password - if len(oldHashPreview) > 20 { - oldHashPreview = oldHashPreview[:20] - } - log.Infof("Reset password for user '%s' (old hash: %s...)", user.Username, oldHashPreview) - } - } - + applyPasswordResets(k, cfg, hashedPassword) log.Infof("Reset %d password(s) to 'password'", len(cfg.AuthLocalUsers.Users)) } diff --git a/service/main.go b/service/main.go index 8700692..b5c7134 100644 --- a/service/main.go +++ b/service/main.go @@ -129,92 +129,97 @@ func getConfigPath(directory string) string { return configPath } +func configSearchDirectories(configDir string) []string { + directories := []string{configDir} + + // Only load additional configs if not in integration test mode + absConfigDir, _ := filepath.Abs(configDir) + if strings.Contains(absConfigDir, "integration-tests") { + return directories + } + + return append(directories, + servicehost.GetConfigFilePath(), + "/config", // For containers. + "/etc/OliveTin/", + ) +} + +func configPathExists(configPath string) bool { + _, err := os.Stat(configPath) + found := err == nil + + log.WithFields(log.Fields{ + "configPath": configPath, + "found": found, + }).Debug("Checking base config path") + + return found +} + +func watchConfigFile(k *koanf.Koanf, f *file.File, configPath string) { + err := f.Watch(func(evt interface{}, err error) { + log.Infof("config file changed: %v", evt) + + errLoad := k.Load(f, yaml.Parser()) + if errLoad != nil { + log.WithFields(log.Fields{ + "error": errLoad, + }).Fatalf("Error loading config file") + } + + config.AppendSource(cfg, k, configPath) + }) + + if err != nil { + log.WithFields(log.Fields{ + "error": err, + }).Fatalf("Error watching config file") + } +} + +func loadAndWatchConfig(k *koanf.Koanf, configPath string) { + log.WithFields(log.Fields{ + "configPath": configPath, + }).Info("Loading config from path") + + f := file.Provider(configPath) + + if err := k.Load(f, yaml.Parser()); err != nil { + log.Fatalf("error loading config from %s: %v", configPath, err) + } + + watchConfigFile(k, f, configPath) +} + +func findAndLoadBaseConfig(k *koanf.Koanf, directories []string) string { + for _, directory := range directories { + configPath := getConfigPath(directory) + if !configPathExists(configPath) { + continue + } + + loadAndWatchConfig(k, configPath) + return configPath + } + + return "" +} + func initConfig(configDir string) { k := koanf.New(".") err := k.Load(env.Provider(".", ".", nil), nil) - if err != nil { log.WithFields(log.Fields{ "error": err, }).Fatalf("Error loading environment variables") } - directories := []string{ - configDir, - } - - // Only load additional configs if not in integration test mode - absConfigDir, _ := filepath.Abs(configDir) - if !strings.Contains(absConfigDir, "integration-tests") { - directories = append(directories, - servicehost.GetConfigFilePath(), - "/config", // For containers. - "/etc/OliveTin/", - ) - } - - var baseConfigPath string - - for _, directory := range directories { - configPath := getConfigPath(directory) - - found := true - if _, err := os.Stat(configPath); err != nil { - found = false - } - - log.WithFields(log.Fields{ - "configPath": configPath, - "found": found, - }).Debug("Checking base config path") - - if !found { - continue - } - - if baseConfigPath == "" { - baseConfigPath = configPath - } - - log.WithFields(log.Fields{ - "configPath": configPath, - }).Info("Loading config from path") - - f := file.Provider(configPath) - - if err := k.Load(f, yaml.Parser()); err != nil { - log.Fatalf("error loading config from %s: %v", configPath, err) - os.Exit(1) - } - - err := f.Watch(func(evt interface{}, err error) { - log.Infof("config file changed: %v", evt) - - errLoad := k.Load(f, yaml.Parser()) - - if errLoad != nil { - log.WithFields(log.Fields{ - "error": errLoad, - }).Fatalf("Error loading config file") - } - - config.AppendSource(cfg, k, configPath) - }) - - if err != nil { - log.WithFields(log.Fields{ - "error": err, - }).Fatalf("Error watching config file") - } - - break - } - + baseConfigPath := findAndLoadBaseConfig(k, configSearchDirectories(configDir)) cfg = config.DefaultConfigWithBasePort(getBasePort()) if baseConfigPath == "" { log.Fatalf("No base config file found") - os.Exit(1) } config.AppendSource(cfg, k, baseConfigPath)