mirror of
https://github.com/binwiederhier/ntfy.git
synced 2025-07-20 10:04:08 +00:00
Allow adding/changing user with password hash via v1/users API
This commit is contained in:
parent
9edab24d4c
commit
4dc3b38c95
4 changed files with 87 additions and 16 deletions
|
@ -42,8 +42,8 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
|
||||||
req, err := readJSONWithLimit[apiUserAddOrUpdateRequest](r.Body, jsonBodyBytesLimit, false)
|
req, err := readJSONWithLimit[apiUserAddOrUpdateRequest](r.Body, jsonBodyBytesLimit, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !user.AllowedUsername(req.Username) || req.Password == "" {
|
} else if !user.AllowedUsername(req.Username) || (req.Password == "" && req.Hash == "") {
|
||||||
return errHTTPBadRequest.Wrap("username invalid, or password missing")
|
return errHTTPBadRequest.Wrap("username invalid, or password/password_hash missing")
|
||||||
}
|
}
|
||||||
u, err := s.userManager.User(req.Username)
|
u, err := s.userManager.User(req.Username)
|
||||||
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
|
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
|
||||||
|
@ -60,7 +60,11 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser, false); err != nil {
|
password, hashed := req.Password, false
|
||||||
|
if req.Hash != "" {
|
||||||
|
password, hashed = req.Hash, true
|
||||||
|
}
|
||||||
|
if err := s.userManager.AddUser(req.Username, password, user.RoleUser, hashed); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if tier != nil {
|
if tier != nil {
|
||||||
|
@ -77,8 +81,8 @@ func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Request, v *vi
|
||||||
return err
|
return err
|
||||||
} else if !user.AllowedUsername(req.Username) {
|
} else if !user.AllowedUsername(req.Username) {
|
||||||
return errHTTPBadRequest.Wrap("username invalid")
|
return errHTTPBadRequest.Wrap("username invalid")
|
||||||
} else if req.Password == "" && req.Tier == "" {
|
} else if req.Password == "" && req.Hash == "" && req.Tier == "" {
|
||||||
return errHTTPBadRequest.Wrap("need to provide at least one of \"password\" or \"tier\"")
|
return errHTTPBadRequest.Wrap("need to provide at least one of \"password\", \"password_hash\" or \"tier\"")
|
||||||
}
|
}
|
||||||
u, err := s.userManager.User(req.Username)
|
u, err := s.userManager.User(req.Username)
|
||||||
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
|
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
|
||||||
|
@ -87,13 +91,21 @@ func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Request, v *vi
|
||||||
if u.IsAdmin() {
|
if u.IsAdmin() {
|
||||||
return errHTTPForbidden
|
return errHTTPForbidden
|
||||||
}
|
}
|
||||||
if req.Password != "" {
|
if req.Hash != "" {
|
||||||
if err := s.userManager.ChangePassword(req.Username, req.Password); err != nil {
|
if err := s.userManager.ChangePassword(req.Username, req.Hash, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if req.Password != "" {
|
||||||
|
if err := s.userManager.ChangePassword(req.Username, req.Password, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser); err != nil {
|
password, hashed := req.Password, false
|
||||||
|
if req.Hash != "" {
|
||||||
|
password, hashed = req.Hash, true
|
||||||
|
}
|
||||||
|
if err := s.userManager.AddUser(req.Username, password, user.RoleUser, hashed); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,12 +65,41 @@ func TestUser_AddRemove(t *testing.T) {
|
||||||
require.Equal(t, 400, rr.Code)
|
require.Equal(t, 400, rr.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUser_AddWithPasswordHash(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||||
|
defer s.closeDatabases()
|
||||||
|
|
||||||
|
// Create admin
|
||||||
|
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||||
|
|
||||||
|
// Create user via API
|
||||||
|
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("phil", "phil"),
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
// Check that user can login with password
|
||||||
|
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("ben", "ben"),
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
// Check users
|
||||||
|
users, err := s.userManager.Users()
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, 3, len(users))
|
||||||
|
require.Equal(t, "phil", users[0].Name)
|
||||||
|
require.Equal(t, user.RoleAdmin, users[0].Role)
|
||||||
|
require.Equal(t, "ben", users[1].Name)
|
||||||
|
require.Equal(t, user.RoleUser, users[1].Role)
|
||||||
|
}
|
||||||
|
|
||||||
func TestUser_ChangeUserPassword(t *testing.T) {
|
func TestUser_ChangeUserPassword(t *testing.T) {
|
||||||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||||
defer s.closeDatabases()
|
defer s.closeDatabases()
|
||||||
|
|
||||||
// Create admin
|
// Create admin
|
||||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||||
|
|
||||||
// Create user via API
|
// Create user via API
|
||||||
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password": "ben"}`, map[string]string{
|
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password": "ben"}`, map[string]string{
|
||||||
|
@ -108,7 +137,7 @@ func TestUser_ChangeUserTier(t *testing.T) {
|
||||||
defer s.closeDatabases()
|
defer s.closeDatabases()
|
||||||
|
|
||||||
// Create admin, tier
|
// Create admin, tier
|
||||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||||
Code: "tier1",
|
Code: "tier1",
|
||||||
}))
|
}))
|
||||||
|
@ -148,7 +177,7 @@ func TestUser_ChangeUserPasswordAndTier(t *testing.T) {
|
||||||
defer s.closeDatabases()
|
defer s.closeDatabases()
|
||||||
|
|
||||||
// Create admin, tier
|
// Create admin, tier
|
||||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||||
Code: "tier1",
|
Code: "tier1",
|
||||||
}))
|
}))
|
||||||
|
@ -195,13 +224,45 @@ func TestUser_ChangeUserPasswordAndTier(t *testing.T) {
|
||||||
require.Equal(t, "tier2", users[1].Tier.Code)
|
require.Equal(t, "tier2", users[1].Tier.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUser_ChangeUserPasswordWithHash(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||||
|
defer s.closeDatabases()
|
||||||
|
|
||||||
|
// Create admin
|
||||||
|
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||||
|
|
||||||
|
// Create user with tier via API
|
||||||
|
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"not-ben"}`, map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("phil", "phil"),
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
// Try to login with first password
|
||||||
|
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("ben", "not-ben"),
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
// Change user password and tier via API
|
||||||
|
rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("phil", "phil"),
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
// Try to login with second password
|
||||||
|
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("ben", "ben"),
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func TestUser_DontChangeAdminPassword(t *testing.T) {
|
func TestUser_DontChangeAdminPassword(t *testing.T) {
|
||||||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||||
defer s.closeDatabases()
|
defer s.closeDatabases()
|
||||||
|
|
||||||
// Create admin
|
// Create admin
|
||||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||||
require.Nil(t, s.userManager.AddUser("admin", "admin", user.RoleAdmin))
|
require.Nil(t, s.userManager.AddUser("admin", "admin", user.RoleAdmin, false))
|
||||||
|
|
||||||
// Try to change password via API
|
// Try to change password via API
|
||||||
rr := request(t, s, "PUT", "/v1/users", `{"username": "admin", "password": "admin-new"}`, map[string]string{
|
rr := request(t, s, "PUT", "/v1/users", `{"username": "admin", "password": "admin-new"}`, map[string]string{
|
||||||
|
|
|
@ -256,6 +256,7 @@ type apiStatsResponse struct {
|
||||||
type apiUserAddOrUpdateRequest struct {
|
type apiUserAddOrUpdateRequest struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
Tier string `json:"tier"`
|
Tier string `json:"tier"`
|
||||||
// Do not add 'role' here. We don't want to add admins via the API.
|
// Do not add 'role' here. We don't want to add admins via the API.
|
||||||
}
|
}
|
||||||
|
|
|
@ -868,10 +868,8 @@ func (a *Manager) AddUser(username, password string, role Role, hashed bool) err
|
||||||
if !AllowedUsername(username) || !AllowedRole(role) {
|
if !AllowedUsername(username) || !AllowedRole(role) {
|
||||||
return ErrInvalidArgument
|
return ErrInvalidArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
var hash []byte
|
var hash []byte
|
||||||
var err error = nil
|
var err error = nil
|
||||||
|
|
||||||
if hashed {
|
if hashed {
|
||||||
hash = []byte(password)
|
hash = []byte(password)
|
||||||
} else {
|
} else {
|
||||||
|
@ -880,7 +878,6 @@ func (a *Manager) AddUser(username, password string, role Role, hashed bool) err
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userID := util.RandomStringPrefix(userIDPrefix, userIDLength)
|
userID := util.RandomStringPrefix(userIDPrefix, userIDLength)
|
||||||
syncTopic, now := util.RandomStringPrefix(syncTopicPrefix, syncTopicLength), time.Now().Unix()
|
syncTopic, now := util.RandomStringPrefix(syncTopicPrefix, syncTopicLength), time.Now().Unix()
|
||||||
if _, err = a.db.Exec(insertUserQuery, userID, username, hash, role, syncTopic, now); err != nil {
|
if _, err = a.db.Exec(insertUserQuery, userID, username, hash, role, syncTopic, now); err != nil {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue