Move stuff to util.go
This commit is contained in:
@@ -550,7 +550,7 @@ func parseUsers(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-users: %s, username invalid", userLine)
|
return nil, fmt.Errorf("invalid auth-users: %s, username invalid", userLine)
|
||||||
} else if err := user.AllowedPasswordHash(passwordHash); err != nil {
|
} else if err := user.ValidPasswordHash(passwordHash); err != nil {
|
||||||
return nil, fmt.Errorf("invalid auth-users: %s, %s", userLine, err.Error())
|
return nil, fmt.Errorf("invalid auth-users: %s, %s", userLine, err.Error())
|
||||||
} else if !user.AllowedRole(role) {
|
} else if !user.AllowedRole(role) {
|
||||||
return nil, fmt.Errorf("invalid auth-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role)
|
return nil, fmt.Errorf("invalid auth-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role)
|
||||||
@@ -625,7 +625,7 @@ func parseTokens(users []*user.User, tokensRaw []string) (map[string][]*user.Tok
|
|||||||
return nil, fmt.Errorf("invalid auth-tokens: %s, username %s invalid", tokenLine, username)
|
return nil, fmt.Errorf("invalid auth-tokens: %s, username %s invalid", tokenLine, username)
|
||||||
}
|
}
|
||||||
token := strings.TrimSpace(parts[1])
|
token := strings.TrimSpace(parts[1])
|
||||||
if !user.AllowedToken(token) {
|
if !user.ValidToken(token) {
|
||||||
return nil, fmt.Errorf("invalid auth-tokens: %s, token %s invalid, use 'ntfy token generate' to generate a random token", tokenLine, token)
|
return nil, fmt.Errorf("invalid auth-tokens: %s, token %s invalid, use 'ntfy token generate' to generate a random token", tokenLine, token)
|
||||||
}
|
}
|
||||||
var label string
|
var label string
|
||||||
|
|||||||
@@ -222,6 +222,6 @@ func execTokenList(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func execTokenGenerate(c *cli.Context) error {
|
func execTokenGenerate(c *cli.Context) error {
|
||||||
fmt.Println(user.GenerateToken())
|
fmt.Fprintln(c.App.Writer, user.GenerateToken())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -306,19 +306,15 @@ func execUserChangeRole(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func execUserHash(c *cli.Context) error {
|
func execUserHash(c *cli.Context) error {
|
||||||
manager, err := createUserManager(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
password, err := readPasswordAndConfirm(c)
|
password, err := readPasswordAndConfirm(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
hash, err := manager.HashPassword(password)
|
hash, err := user.HashPassword(password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to hash password: %w", err)
|
return fmt.Errorf("failed to hash password: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(c.App.Writer, "%s\n", string(hash))
|
fmt.Fprintln(c.App.Writer, hash)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1015,11 +1015,11 @@ func (a *Manager) addUserTx(tx *sql.Tx, username, password string, role Role, ha
|
|||||||
var err error = nil
|
var err error = nil
|
||||||
if hashed {
|
if hashed {
|
||||||
hash = password
|
hash = password
|
||||||
if err := AllowedPasswordHash(hash); err != nil {
|
if err := ValidPasswordHash(hash); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hash, err = a.HashPassword(password)
|
hash, err = hashPassword(password, a.config.BcryptCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1365,11 +1365,11 @@ func (a *Manager) changePasswordTx(tx *sql.Tx, username, password string, hashed
|
|||||||
var err error
|
var err error
|
||||||
if hashed {
|
if hashed {
|
||||||
hash = password
|
hash = password
|
||||||
if err := AllowedPasswordHash(hash); err != nil {
|
if err := ValidPasswordHash(hash); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hash, err = a.HashPassword(password)
|
hash, err = hashPassword(password, a.config.BcryptCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1697,15 +1697,6 @@ 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()
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/stripe/stripe-go/v74"
|
"github.com/stripe/stripe-go/v74"
|
||||||
"heckel.io/ntfy/v2/log"
|
"heckel.io/ntfy/v2/log"
|
||||||
"heckel.io/ntfy/v2/util"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -244,58 +242,6 @@ const (
|
|||||||
everyoneID = "u_everyone"
|
everyoneID = "u_everyone"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
allowedUsernameRegex = regexp.MustCompile(`^[-_.+@a-zA-Z0-9]+$`) // Does not include Everyone (*)
|
|
||||||
allowedTopicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No '*'
|
|
||||||
allowedTopicPatternRegex = regexp.MustCompile(`^[-_*A-Za-z0-9]{1,64}$`) // Adds '*' for wildcards!
|
|
||||||
allowedTierRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)
|
|
||||||
allowedTokenRegex = regexp.MustCompile(`^tk_[-_A-Za-z0-9]{29}$`) // Must be tokenLength-len(tokenPrefix)
|
|
||||||
)
|
|
||||||
|
|
||||||
// AllowedRole returns true if the given role can be used for new users
|
|
||||||
func AllowedRole(role Role) bool {
|
|
||||||
return role == RoleUser || role == RoleAdmin
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllowedUsername returns true if the given username is valid
|
|
||||||
func AllowedUsername(username string) bool {
|
|
||||||
return allowedUsernameRegex.MatchString(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllowedTopic returns true if the given topic name is valid
|
|
||||||
func AllowedTopic(topic string) bool {
|
|
||||||
return allowedTopicRegex.MatchString(topic)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllowedTopicPattern returns true if the given topic pattern is valid; this includes the wildcard character (*)
|
|
||||||
func AllowedTopicPattern(topic string) bool {
|
|
||||||
return allowedTopicPatternRegex.MatchString(topic)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllowedTier returns true if the given tier name is valid
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllowedToken returns true if the given token matches the naming convention
|
|
||||||
func AllowedToken(token string) bool {
|
|
||||||
return allowedTokenRegex.MatchString(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateToken generates a new token with a prefix and a fixed length
|
|
||||||
// Lowercase only to support "<topic>+<token>@<domain>" email addresses
|
|
||||||
func GenerateToken() string {
|
|
||||||
return util.RandomLowerStringPrefix(tokenPrefix, tokenLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error constants used by the package
|
// Error constants used by the package
|
||||||
var (
|
var (
|
||||||
ErrUnauthenticated = errors.New("unauthenticated")
|
ErrUnauthenticated = errors.New("unauthenticated")
|
||||||
|
|||||||
73
user/util.go
Normal file
73
user/util.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"heckel.io/ntfy/v2/util"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
allowedUsernameRegex = regexp.MustCompile(`^[-_.+@a-zA-Z0-9]+$`) // Does not include Everyone (*)
|
||||||
|
allowedTopicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No '*'
|
||||||
|
allowedTopicPatternRegex = regexp.MustCompile(`^[-_*A-Za-z0-9]{1,64}$`) // Adds '*' for wildcards!
|
||||||
|
allowedTierRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)
|
||||||
|
allowedTokenRegex = regexp.MustCompile(`^tk_[-_A-Za-z0-9]{29}$`) // Must be tokenLength-len(tokenPrefix)
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllowedRole returns true if the given role can be used for new users
|
||||||
|
func AllowedRole(role Role) bool {
|
||||||
|
return role == RoleUser || role == RoleAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowedUsername returns true if the given username is valid
|
||||||
|
func AllowedUsername(username string) bool {
|
||||||
|
return allowedUsernameRegex.MatchString(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowedTopic returns true if the given topic name is valid
|
||||||
|
func AllowedTopic(topic string) bool {
|
||||||
|
return allowedTopicRegex.MatchString(topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowedTopicPattern returns true if the given topic pattern is valid; this includes the wildcard character (*)
|
||||||
|
func AllowedTopicPattern(topic string) bool {
|
||||||
|
return allowedTopicPatternRegex.MatchString(topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowedTier returns true if the given tier name is valid
|
||||||
|
func AllowedTier(tier string) bool {
|
||||||
|
return allowedTierRegex.MatchString(tier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidPasswordHash checks if the given password hash is a valid bcrypt hash
|
||||||
|
func ValidPasswordHash(hash string) error {
|
||||||
|
if !strings.HasPrefix(hash, "$2a$") && !strings.HasPrefix(hash, "$2b$") && !strings.HasPrefix(hash, "$2y$") {
|
||||||
|
return ErrPasswordHashInvalid
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidToken returns true if the given token matches the naming convention
|
||||||
|
func ValidToken(token string) bool {
|
||||||
|
return allowedTokenRegex.MatchString(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken generates a new token with a prefix and a fixed length
|
||||||
|
// Lowercase only to support "<topic>+<token>@<domain>" email addresses
|
||||||
|
func GenerateToken() string {
|
||||||
|
return util.RandomLowerStringPrefix(tokenPrefix, tokenLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPassword hashes the given password using bcrypt with the configured cost
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
return hashPassword(password, DefaultUserPasswordBcryptCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashPassword(password string, cost int) (string, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), cost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(hash), nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user