Add "ntfy user hash"
This commit is contained in:
@@ -543,8 +543,8 @@ func parseProvisionUsers(usersRaw []string) ([]*user.User, error) {
|
|||||||
role := user.Role(strings.TrimSpace(parts[2]))
|
role := user.Role(strings.TrimSpace(parts[2]))
|
||||||
if !user.AllowedUsername(username) {
|
if !user.AllowedUsername(username) {
|
||||||
return nil, fmt.Errorf("invalid auth-provision-users: %s, username invalid", userLine)
|
return nil, fmt.Errorf("invalid auth-provision-users: %s, username invalid", userLine)
|
||||||
} else if passwordHash == "" {
|
} else if err := user.AllowedPasswordHash(passwordHash); err != nil {
|
||||||
return nil, fmt.Errorf("invalid auth-provision-users: %s, password hash cannot be empty", userLine)
|
return nil, fmt.Errorf("invalid auth-provision-users: %s, %s", userLine, err.Error())
|
||||||
} else if !user.AllowedRole(role) {
|
} else if !user.AllowedRole(role) {
|
||||||
return nil, fmt.Errorf("invalid auth-provision-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role)
|
return nil, fmt.Errorf("invalid auth-provision-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role)
|
||||||
}
|
}
|
||||||
|
|||||||
33
cmd/user.go
33
cmd/user.go
@@ -133,6 +133,22 @@ as messages per day, attachment file sizes, etc.
|
|||||||
Example:
|
Example:
|
||||||
ntfy user change-tier phil pro # Change tier to "pro" for user "phil"
|
ntfy user change-tier phil pro # Change tier to "pro" for user "phil"
|
||||||
ntfy user change-tier phil - # Remove tier from user "phil" entirely
|
ntfy user change-tier phil - # Remove tier from user "phil" entirely
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hash",
|
||||||
|
Usage: "Create password hash for a predefined user",
|
||||||
|
UsageText: "ntfy user hash",
|
||||||
|
Action: execUserHash,
|
||||||
|
Description: `Asks for a password and creates a bcrypt password hash.
|
||||||
|
|
||||||
|
This command is useful to create a password hash for a user, which can then be used
|
||||||
|
for predefined users in the server config file, in auth-provision-users.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
$ ntfy user hash
|
||||||
|
(asks for password and confirmation)
|
||||||
|
$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -289,6 +305,23 @@ func execUserChangeRole(c *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func execUserHash(c *cli.Context) error {
|
||||||
|
manager, err := createUserManager(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
password, err := readPasswordAndConfirm(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hash, err := manager.HashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to hash password: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(c.App.Writer, "%s\n", string(hash))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func execUserChangeTier(c *cli.Context) error {
|
func execUserChangeTier(c *cli.Context) error {
|
||||||
username := c.Args().Get(0)
|
username := c.Args().Get(0)
|
||||||
tier := c.Args().Get(1)
|
tier := c.Args().Get(1)
|
||||||
|
|||||||
@@ -981,12 +981,15 @@ func (a *Manager) addUserTx(tx *sql.Tx, username, password string, role Role, ha
|
|||||||
if !AllowedUsername(username) || !AllowedRole(role) {
|
if !AllowedUsername(username) || !AllowedRole(role) {
|
||||||
return ErrInvalidArgument
|
return ErrInvalidArgument
|
||||||
}
|
}
|
||||||
var hash []byte
|
var hash string
|
||||||
var err error = nil
|
var err error = nil
|
||||||
if hashed {
|
if hashed {
|
||||||
hash = []byte(password)
|
hash = password
|
||||||
|
if err := AllowedPasswordHash(hash); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost)
|
hash, err = a.HashPassword(password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1328,12 +1331,15 @@ func (a *Manager) ChangePassword(username, password string, hashed bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Manager) changePasswordTx(tx *sql.Tx, username, password string, hashed bool) error {
|
func (a *Manager) changePasswordTx(tx *sql.Tx, username, password string, hashed bool) error {
|
||||||
var hash []byte
|
var hash string
|
||||||
var err error
|
var err error
|
||||||
if hashed {
|
if hashed {
|
||||||
hash = []byte(password)
|
hash = password
|
||||||
|
if err := AllowedPasswordHash(hash); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost)
|
hash, err = a.HashPassword(password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1640,6 +1646,15 @@ func (a *Manager) readTier(rows *sql.Rows) (*Tier, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HashPassword hashes the given password using bcrypt with the configured cost
|
||||||
|
func (a *Manager) HashPassword(password string) (string, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Close closes the underlying database
|
// Close closes the underlying database
|
||||||
func (a *Manager) Close() error {
|
func (a *Manager) Close() error {
|
||||||
return a.db.Close()
|
return a.db.Close()
|
||||||
@@ -1681,7 +1696,7 @@ func (a *Manager) maybeProvisionUsersAndAccess() error {
|
|||||||
if err := a.addUserTx(tx, user.Name, user.Hash, user.Role, true, true); err != nil && !errors.Is(err, ErrUserExists) {
|
if err := a.addUserTx(tx, user.Name, user.Hash, user.Role, true, true); err != nil && !errors.Is(err, ErrUserExists) {
|
||||||
return fmt.Errorf("failed to add provisioned user %s: %v", user.Name, err)
|
return fmt.Errorf("failed to add provisioned user %s: %v", user.Name, err)
|
||||||
}
|
}
|
||||||
} else if existingUser.Hash != user.Hash || existingUser.Role != user.Role {
|
} else if existingUser.Provisioned && (existingUser.Hash != user.Hash || existingUser.Role != user.Role) {
|
||||||
log.Tag(tag).Info("Updating provisioned user %s", user.Name)
|
log.Tag(tag).Info("Updating provisioned user %s", user.Name)
|
||||||
if err := a.changePasswordTx(tx, user.Name, user.Hash, true); err != nil {
|
if err := a.changePasswordTx(tx, user.Name, user.Hash, true); err != nil {
|
||||||
return fmt.Errorf("failed to change password for provisioned user %s: %v", user.Name, err)
|
return fmt.Errorf("failed to change password for provisioned user %s: %v", user.Name, err)
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ func TestManager_UserManagement(t *testing.T) {
|
|||||||
func TestManager_ChangePassword(t *testing.T) {
|
func TestManager_ChangePassword(t *testing.T) {
|
||||||
a := newTestManager(t, PermissionDenyAll)
|
a := newTestManager(t, PermissionDenyAll)
|
||||||
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false))
|
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false))
|
||||||
require.Nil(t, a.AddUser("jane", "$2b$10$OyqU72muEy7VMd1SAU2Iru5IbeSMgrtCGHu/fWLmxL1MwlijQXWbG", RoleUser, true))
|
require.Nil(t, a.AddUser("jane", "$2a$10$OyqU72muEy7VMd1SAU2Iru5IbeSMgrtCGHu/fWLmxL1MwlijQXWbG", RoleUser, true))
|
||||||
|
|
||||||
_, err := a.Authenticate("phil", "phil")
|
_, err := a.Authenticate("phil", "phil")
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
@@ -354,7 +354,7 @@ func TestManager_ChangePassword(t *testing.T) {
|
|||||||
_, err = a.Authenticate("phil", "newpass")
|
_, err = a.Authenticate("phil", "newpass")
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
require.Nil(t, a.ChangePassword("jane", "$2b$10$CNaCW.q1R431urlbQ5Drh.zl48TiiOeJSmZgfcswkZiPbJGQ1ApSS", true))
|
require.Nil(t, a.ChangePassword("jane", "$2a$10$CNaCW.q1R431urlbQ5Drh.zl48TiiOeJSmZgfcswkZiPbJGQ1ApSS", true))
|
||||||
_, err = a.Authenticate("jane", "jane")
|
_, err = a.Authenticate("jane", "jane")
|
||||||
require.Equal(t, ErrUnauthenticated, err)
|
require.Equal(t, ErrUnauthenticated, err)
|
||||||
_, err = a.Authenticate("jane", "newpass")
|
_, err = a.Authenticate("jane", "newpass")
|
||||||
|
|||||||
@@ -274,6 +274,14 @@ func AllowedTier(tier string) bool {
|
|||||||
return allowedTierRegex.MatchString(tier)
|
return allowedTierRegex.MatchString(tier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllowedPasswordHash checks if the given password hash is a valid bcrypt hash
|
||||||
|
func AllowedPasswordHash(hash string) error {
|
||||||
|
if !strings.HasPrefix(hash, "$2a$") && !strings.HasPrefix(hash, "$2b$") && !strings.HasPrefix(hash, "$2y$") {
|
||||||
|
return ErrPasswordHashInvalid
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Error constants used by the package
|
// Error constants used by the package
|
||||||
var (
|
var (
|
||||||
ErrUnauthenticated = errors.New("unauthenticated")
|
ErrUnauthenticated = errors.New("unauthenticated")
|
||||||
@@ -281,6 +289,7 @@ var (
|
|||||||
ErrInvalidArgument = errors.New("invalid argument")
|
ErrInvalidArgument = errors.New("invalid argument")
|
||||||
ErrUserNotFound = errors.New("user not found")
|
ErrUserNotFound = errors.New("user not found")
|
||||||
ErrUserExists = errors.New("user already exists")
|
ErrUserExists = errors.New("user already exists")
|
||||||
|
ErrPasswordHashInvalid = errors.New("password hash but be a bcrypt hash, use 'ntfy user hash' to generate")
|
||||||
ErrTierNotFound = errors.New("tier not found")
|
ErrTierNotFound = errors.New("tier not found")
|
||||||
ErrTokenNotFound = errors.New("token not found")
|
ErrTokenNotFound = errors.New("token not found")
|
||||||
ErrPhoneNumberNotFound = errors.New("phone number not found")
|
ErrPhoneNumberNotFound = errors.New("phone number not found")
|
||||||
|
|||||||
Reference in New Issue
Block a user