From 1e24809ed6cf0f25f359af818b9c89cae8d873cd Mon Sep 17 00:00:00 2001 From: selfhoster1312 <121760708+selfhoster1312@users.noreply.github.com> Date: Wed, 25 Jan 2023 02:18:10 +0100 Subject: [PATCH] Create accounts automatically when authenticating from HTTP header (#2087) * Create accounts automatically when authenticating from HTTP header * Disable password check when header auth is enabled * Formatting * Password change is valid when no password (old or new) is provided * Test suite runs with header auth disabled (mock config) Prevents nil pointer access (panic) while testing password validating logic * Use a constant prefix for autogenerated passwords (header auth case) * Add tests * Add context to log messages Co-authored-by: Deluan --- consts/consts.go | 1 + persistence/user_repository.go | 15 ++++++++++----- persistence/user_repository_test.go | 30 +++++++++++++++++++++++++++++ server/auth.go | 21 ++++++++++++++++++-- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/consts/consts.go b/consts/consts.go index c391be5e..f1483f47 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -25,6 +25,7 @@ const ( // Never ever change this! Or it will break all Navidrome installations that don't set the config option DefaultEncryptionKey = "just for obfuscation" PasswordsEncryptedKey = "PasswordsEncryptedKey" + PasswordAutogenPrefix = "__NAVIDROME_AUTOGEN__" //nolint:gosec DevInitialUserName = "admin" DevInitialName = "Dev Admin" diff --git a/persistence/user_repository.go b/persistence/user_repository.go index 08555c03..0acbaae2 100644 --- a/persistence/user_repository.go +++ b/persistence/user_repository.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "errors" "fmt" + "strings" "sync" "time" @@ -206,12 +207,16 @@ func validatePasswordChange(newUser *model.User, logged *model.User) error { if logged.IsAdmin && newUser.ID != logged.ID { return nil } - if newUser.NewPassword != "" && newUser.CurrentPassword == "" { - err.Errors["currentPassword"] = "ra.validation.required" + if newUser.NewPassword == "" { + if newUser.CurrentPassword == "" { + return nil + } + err.Errors["password"] = "ra.validation.required" } - if newUser.CurrentPassword != "" { - if newUser.NewPassword == "" { - err.Errors["password"] = "ra.validation.required" + + if !strings.HasPrefix(logged.Password, consts.PasswordAutogenPrefix) { + if newUser.CurrentPassword == "" { + err.Errors["currentPassword"] = "ra.validation.required" } if newUser.CurrentPassword != logged.Password { err.Errors["currentPassword"] = "ra.validation.passwordDoesNotMatch" diff --git a/persistence/user_repository_test.go b/persistence/user_repository_test.go index 2cb98429..d592b86d 100644 --- a/persistence/user_repository_test.go +++ b/persistence/user_repository_test.go @@ -6,6 +6,8 @@ import ( "github.com/beego/beego/v2/client/orm" "github.com/deluan/rest" + "github.com/google/uuid" + "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/tests" @@ -81,6 +83,34 @@ var _ = Describe("UserRepository", func() { Expect(err).To(BeNil()) }) + Context("Autogenerated password (used with Reverse Proxy Authentication)", func() { + var user model.User + BeforeEach(func() { + loggedUser.IsAdmin = false + loggedUser.Password = consts.PasswordAutogenPrefix + uuid.NewString() + }) + It("does nothing if passwords are not specified", func() { + user = *loggedUser + err := validatePasswordChange(&user, loggedUser) + Expect(err).To(BeNil()) + }) + It("does not requires currentPassword for regular user", func() { + user = *loggedUser + user.CurrentPassword = "" + user.NewPassword = "new" + err := validatePasswordChange(&user, loggedUser) + Expect(err).ToNot(HaveOccurred()) + }) + It("does not requires currentPassword for admin", func() { + loggedUser.IsAdmin = true + user = *loggedUser + user.CurrentPassword = "" + user.NewPassword = "new" + err := validatePasswordChange(&user, loggedUser) + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("Logged User is admin", func() { BeforeEach(func() { loggedUser.IsAdmin = true diff --git a/server/auth.go b/server/auth.go index c4c23ed0..cc59fd19 100644 --- a/server/auth.go +++ b/server/auth.go @@ -285,8 +285,25 @@ func handleLoginFromHeaders(ds model.DataStore, r *http.Request) map[string]inte userRepo := ds.User(r.Context()) user, err := userRepo.FindByUsernameWithPassword(username) if user == nil || err != nil { - log.Warn(r, "User passed in header not found", "user", username) - return nil + log.Info(r, "User passed in header not found", "user", username) + newUser := model.User{ + ID: uuid.NewString(), + UserName: username, + Name: username, + Email: "", + NewPassword: consts.PasswordAutogenPrefix + uuid.NewString(), + IsAdmin: false, + } + err := userRepo.Put(&newUser) + if err != nil { + log.Error(r, "Could not create new user", "user", username, err) + return nil + } + user, err = userRepo.FindByUsernameWithPassword(username) + if user == nil || err != nil { + log.Error(r, "Created user but failed to fetch it", "user", username) + return nil + } } err = userRepo.UpdateLastLoginAt(user.ID)