Expand allowedUsers email field to support comma-separated and domains (#9)

* Expand allowedUsers email field to support comma-separated and domains

Closes #8

* Refactor AuthFetch() to return AuthUser struct

Also, this breaks out a parseLine() function which can be easily tested.

* Ignore empty addrs after splitting commas

This ignores a trailing comma

* Add tests for auth parseLine()

* Update documentation in smtprelay.ini

* Fix bug where addrAllowed() was incorrectly case-sensitive

* Update allowedUsers allowed domain format to require leading @

This disambiguates a local user ('john.smith') from a domain ('example.com')
This commit is contained in:
Jonathon Reinhart
2021-02-14 16:16:18 -05:00
committed by GitHub
parent 5c2e28ac36
commit 0e8986ca79
5 changed files with 278 additions and 23 deletions

62
auth.go
View File

@@ -13,6 +13,12 @@ var (
filename string
)
type AuthUser struct {
username string
passwordHash string
allowedAddresses []string
}
func AuthLoadFile(file string) error {
f, err := os.Open(file)
if err != nil {
@@ -28,50 +34,66 @@ func AuthReady() bool {
return (filename != "")
}
// Returns bcrypt-hash, email
// email can be empty in which case it is not checked
func AuthFetch(username string) (string, string, error) {
// Split a string and ignore empty results
// https://stackoverflow.com/a/46798310/119527
func splitstr(s string, sep rune) []string {
return strings.FieldsFunc(s, func(c rune) bool { return c == sep })
}
func parseLine(line string) *AuthUser {
parts := strings.Fields(line)
if len(parts) < 2 || len(parts) > 3 {
return nil
}
user := AuthUser{
username: parts[0],
passwordHash: parts[1],
allowedAddresses: nil,
}
if len(parts) >= 3 {
user.allowedAddresses = splitstr(parts[2], ',')
}
return &user
}
func AuthFetch(username string) (*AuthUser, error) {
if !AuthReady() {
return "", "", errors.New("Authentication file not specified. Call LoadFile() first")
return nil, errors.New("Authentication file not specified. Call LoadFile() first")
}
file, err := os.Open(filename)
if err != nil {
return "", "", err
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
parts := strings.Fields(scanner.Text())
if len(parts) < 2 || len(parts) > 3 {
user := parseLine(scanner.Text())
if user == nil {
continue
}
if strings.ToLower(username) != strings.ToLower(parts[0]) {
if strings.ToLower(username) != strings.ToLower(user.username) {
continue
}
hash := parts[1]
email := ""
if len(parts) >= 3 {
email = parts[2]
}
return hash, email, nil
return user, nil
}
return "", "", errors.New("User not found")
return nil, errors.New("User not found")
}
func AuthCheckPassword(username string, secret string) error {
hash, _, err := AuthFetch(username)
user, err := AuthFetch(username)
if err != nil {
return err
}
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(secret)) == nil {
if bcrypt.CompareHashAndPassword([]byte(user.passwordHash), []byte(secret)) == nil {
return nil
}
return errors.New("Password invalid")