binwiederhier 7 месяцев назад
Родитель
Сommit
f99801a2e6
5 измененных файлов с 68 добавлено и 11 удалено
  1. 2 2
      cmd/serve.go
  2. 33 0
      cmd/user.go
  3. 22 7
      user/manager.go
  4. 2 2
      user/manager_test.go
  5. 9 0
      user/types.go

+ 2 - 2
cmd/serve.go

@@ -543,8 +543,8 @@ func parseProvisionUsers(usersRaw []string) ([]*user.User, error) {
 		role := user.Role(strings.TrimSpace(parts[2]))
 		if !user.AllowedUsername(username) {
 			return nil, fmt.Errorf("invalid auth-provision-users: %s, username invalid", userLine)
-		} else if passwordHash == "" {
-			return nil, fmt.Errorf("invalid auth-provision-users: %s, password hash cannot be empty", userLine)
+		} else if err := user.AllowedPasswordHash(passwordHash); err != nil {
+			return nil, fmt.Errorf("invalid auth-provision-users: %s, %s", userLine, err.Error())
 		} 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)
 		}

+ 33 - 0
cmd/user.go

@@ -133,6 +133,22 @@ as messages per day, attachment file sizes, etc.
 Example:
   ntfy user change-tier phil pro   # Change tier to "pro" for user "phil"  
   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
 }
 
+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 {
 	username := c.Args().Get(0)
 	tier := c.Args().Get(1)

+ 22 - 7
user/manager.go

@@ -981,12 +981,15 @@ func (a *Manager) addUserTx(tx *sql.Tx, username, password string, role Role, ha
 	if !AllowedUsername(username) || !AllowedRole(role) {
 		return ErrInvalidArgument
 	}
-	var hash []byte
+	var hash string
 	var err error = nil
 	if hashed {
-		hash = []byte(password)
+		hash = password
+		if err := AllowedPasswordHash(hash); err != nil {
+			return err
+		}
 	} else {
-		hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost)
+		hash, err = a.HashPassword(password)
 		if err != nil {
 			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 {
-	var hash []byte
+	var hash string
 	var err error
 	if hashed {
-		hash = []byte(password)
+		hash = password
+		if err := AllowedPasswordHash(hash); err != nil {
+			return err
+		}
 	} else {
-		hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost)
+		hash, err = a.HashPassword(password)
 		if err != nil {
 			return err
 		}
@@ -1640,6 +1646,15 @@ func (a *Manager) readTier(rows *sql.Rows) (*Tier, error) {
 	}, 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
 func (a *Manager) Close() error {
 	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) {
 					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)
 				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)

+ 2 - 2
user/manager_test.go

@@ -340,7 +340,7 @@ func TestManager_UserManagement(t *testing.T) {
 func TestManager_ChangePassword(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
 	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")
 	require.Nil(t, err)
@@ -354,7 +354,7 @@ func TestManager_ChangePassword(t *testing.T) {
 	_, err = a.Authenticate("phil", "newpass")
 	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")
 	require.Equal(t, ErrUnauthenticated, err)
 	_, err = a.Authenticate("jane", "newpass")

+ 9 - 0
user/types.go

@@ -274,6 +274,14 @@ func AllowedTier(tier string) bool {
 	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
 var (
 	ErrUnauthenticated     = errors.New("unauthenticated")
@@ -281,6 +289,7 @@ var (
 	ErrInvalidArgument     = errors.New("invalid argument")
 	ErrUserNotFound        = errors.New("user not found")
 	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")
 	ErrTokenNotFound       = errors.New("token not found")
 	ErrPhoneNumberNotFound = errors.New("phone number not found")