diff --git a/model/user.go b/model/user.go index ff64a3b20..4fb152808 100644 --- a/model/user.go +++ b/model/user.go @@ -7,13 +7,17 @@ type User struct { UserName string `json:"userName"` Name string `json:"name"` Email string `json:"email"` - Password string `json:"-"` IsAdmin bool `json:"isAdmin"` LastLoginAt *time.Time `json:"lastLoginAt"` LastAccessAt *time.Time `json:"lastAccessAt"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` - NewPassword string `json:"password,omitempty"` + + // This is only available on the backend, and it is never sent over the wire + Password string `json:"-"` + // This is used to set or change a password when calling Put. If it is empty, the password is not changed. + // It is received from the UI with the name "password" + NewPassword string `json:"password,omitempty"` } type Users []User diff --git a/persistence/user_repository_test.go b/persistence/user_repository_test.go index 87e2aec58..315ce0001 100644 --- a/persistence/user_repository_test.go +++ b/persistence/user_repository_test.go @@ -19,12 +19,12 @@ var _ = Describe("UserRepository", func() { Describe("Put/Get/FindByUsername", func() { usr := model.User{ - ID: "123", - UserName: "AdMiN", - Name: "Admin", - Email: "admin@admin.com", - Password: "wordpass", - IsAdmin: true, + ID: "123", + UserName: "AdMiN", + Name: "Admin", + Email: "admin@admin.com", + NewPassword: "wordpass", + IsAdmin: true, } It("saves the user to the DB", func() { Expect(repo.Put(&usr)).To(BeNil()) @@ -33,6 +33,7 @@ var _ = Describe("UserRepository", func() { actual, err := repo.Get("123") Expect(err).ToNot(HaveOccurred()) Expect(actual.Name).To(Equal("Admin")) + Expect(actual.Password).To(Equal("wordpass")) }) It("find the user by case-insensitive username", func() { actual, err := repo.FindByUsername("aDmIn") diff --git a/server/initial_setup.go b/server/initial_setup.go index b74dcb786..8f916b807 100644 --- a/server/initial_setup.go +++ b/server/initial_setup.go @@ -47,12 +47,12 @@ func createInitialAdminUser(ds model.DataStore, initialPassword string) error { log.Warn("Creating initial admin user. This should only be used for development purposes!!", "user", consts.DevInitialUserName, "password", initialPassword, "id", id) initialUser := model.User{ - ID: id, - UserName: consts.DevInitialUserName, - Name: consts.DevInitialName, - Email: "", - Password: initialPassword, - IsAdmin: true, + ID: id, + UserName: consts.DevInitialUserName, + Name: consts.DevInitialName, + Email: "", + NewPassword: initialPassword, + IsAdmin: true, } err := users.Put(&initialUser) if err != nil { diff --git a/server/initial_setup_test.go b/server/initial_setup_test.go new file mode 100644 index 000000000..ce52c2165 --- /dev/null +++ b/server/initial_setup_test.go @@ -0,0 +1,36 @@ +package server + +import ( + "context" + + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/tests" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("initial_setup", func() { + var ds model.DataStore + + BeforeEach(func() { + ds = &tests.MockDataStore{} + }) + + Describe("createInitialAdminUser", func() { + It("creates a new admin user with specified password if User table is empty", func() { + Expect(createInitialAdminUser(ds, "pass123")).To(BeNil()) + ur := ds.User(context.TODO()) + admin, err := ur.FindByUsername("admin") + Expect(err).To(BeNil()) + Expect(admin.Password).To(Equal("pass123")) + }) + + It("does not create a new admin user if User table is not empty", func() { + Expect(createInitialAdminUser(ds, "first")).To(BeNil()) + ur := ds.User(context.TODO()) + Expect(ur.CountAll()).To(Equal(int64(1))) + Expect(createInitialAdminUser(ds, "second")).To(BeNil()) + Expect(ur.CountAll()).To(Equal(int64(1))) + }) + }) +}) diff --git a/server/subsonic/middlewares_test.go b/server/subsonic/middlewares_test.go index 3d40d94f7..9388309e7 100644 --- a/server/subsonic/middlewares_test.go +++ b/server/subsonic/middlewares_test.go @@ -36,10 +36,12 @@ func newPostRequest(queryParam string, formFields ...string) *http.Request { var _ = Describe("Middlewares", func() { var next *mockHandler var w *httptest.ResponseRecorder + var ds model.DataStore BeforeEach(func() { next = &mockHandler{} w = httptest.NewRecorder() + ds = &tests.MockDataStore{} }) Describe("ParsePostForm", func() { @@ -115,11 +117,13 @@ var _ = Describe("Middlewares", func() { }) Describe("Authenticate", func() { - var ds model.DataStore BeforeEach(func() { - ds = &tests.MockDataStore{} + ur := ds.User(context.TODO()) + _ = ur.Put(&model.User{ + UserName: "admin", + NewPassword: "wordpass", + }) }) - It("passes authentication with correct credentials", func() { r := newGetRequest("u=admin", "p=wordpass") cp := authenticate(ds)(next) @@ -220,16 +224,18 @@ var _ = Describe("Middlewares", func() { }) Describe("validateUser", func() { - var ds model.DataStore BeforeEach(func() { - ds = &tests.MockDataStore{} + ur := ds.User(context.TODO()) + _ = ur.Put(&model.User{ + UserName: "admin", + NewPassword: "wordpass", + }) }) - Context("Plaintext password", func() { It("authenticates with plaintext password ", func() { usr, err := validateUser(context.TODO(), ds, "admin", "wordpass", "", "", "") Expect(err).NotTo(HaveOccurred()) - Expect(usr).To(Equal(&model.User{UserName: "admin", Password: "wordpass"})) + Expect(usr.UserName).To(Equal("admin")) }) It("fails authentication with wrong password", func() { @@ -242,7 +248,7 @@ var _ = Describe("Middlewares", func() { It("authenticates with simple encoded password ", func() { usr, err := validateUser(context.TODO(), ds, "admin", "enc:776f726470617373", "", "", "") Expect(err).NotTo(HaveOccurred()) - Expect(usr).To(Equal(&model.User{UserName: "admin", Password: "wordpass"})) + Expect(usr.UserName).To(Equal("admin")) }) }) @@ -250,7 +256,7 @@ var _ = Describe("Middlewares", func() { It("authenticates with token based authentication", func() { usr, err := validateUser(context.TODO(), ds, "admin", "", "23b342970e25c7928831c3317edd0b67", "retnlmjetrymazgkt", "") Expect(err).NotTo(HaveOccurred()) - Expect(usr).To(Equal(&model.User{UserName: "admin", Password: "wordpass"})) + Expect(usr.UserName).To(Equal("admin")) }) It("fails if salt is missing", func() { @@ -273,7 +279,7 @@ var _ = Describe("Middlewares", func() { usr, err := validateUser(context.TODO(), ds, "admin", "", "", "", validToken) Expect(err).NotTo(HaveOccurred()) - Expect(usr).To(Equal(&model.User{UserName: "admin", Password: "wordpass"})) + Expect(usr.UserName).To(Equal("admin")) }) It("fails if JWT token is invalid", func() { diff --git a/tests/mock_persistence.go b/tests/mock_persistence.go index 789cdd56d..f42f14171 100644 --- a/tests/mock_persistence.go +++ b/tests/mock_persistence.go @@ -2,6 +2,8 @@ package tests import ( "context" + "encoding/base64" + "strings" "github.com/navidrome/navidrome/model" ) @@ -95,15 +97,29 @@ func (db *MockDataStore) GC(ctx context.Context, rootFolder string) error { type mockedUserRepo struct { model.UserRepository + data map[string]*model.User +} + +func (u *mockedUserRepo) CountAll(qo ...model.QueryOptions) (int64, error) { + return int64(len(u.data)), nil +} + +func (u *mockedUserRepo) Put(usr *model.User) error { + if u.data == nil { + u.data = make(map[string]*model.User) + } + if usr.ID == "" { + usr.ID = base64.StdEncoding.EncodeToString([]byte(usr.UserName)) + } + usr.Password = usr.NewPassword + u.data[strings.ToLower(usr.UserName)] = usr + return nil } func (u *mockedUserRepo) FindByUsername(username string) (*model.User, error) { - if username != "admin" { + usr, ok := u.data[strings.ToLower(username)] + if !ok { return nil, model.ErrNotFound } - return &model.User{UserName: "admin", Password: "wordpass"}, nil -} - -func (u *mockedUserRepo) UpdateLastAccessAt(id string) error { - return nil + return usr, nil }