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 <deluan@navidrome.org>
This commit is contained in:
selfhoster1312 2023-01-25 02:18:10 +01:00 committed by GitHub
parent 9721ef8974
commit 1e24809ed6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 60 additions and 7 deletions

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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)