mosquitto-go-auth/backends/files.go

347 lines
8.8 KiB
Go

package backends
import (
"bufio"
"fmt"
"os"
"strings"
log "github.com/sirupsen/logrus"
"github.com/pkg/errors"
"github.com/iegomez/mosquitto-go-auth/common"
)
// saltSize defines the salt size
const saltSize = 16
// HashIterations defines the number of hash iterations.
var HashIterations = 100000
//FileUer keeps a user password and acl records.
type FileUser struct {
Password string
AclRecords []AclRecord
}
//AclRecord holds a topic and access privileges.
type AclRecord struct {
Topic string
Acc byte //None 0x00, Read 0x01, Write 0x02, ReadWrite: Read | Write : 0x03
}
//FileBE holds paths to files, list of file users and general (no user or pattern) acl records.
type Files struct {
PasswordPath string
AclPath string
CheckAcls bool
Users map[string]*FileUser //Users keeps a registry of username/FileUser pairs, holding a user's password and Acl records.
AclRecords []AclRecord
}
//NewFiles initializes a files backend.
func NewFiles(authOpts map[string]string, logLevel log.Level) (Files, error) {
log.SetLevel(logLevel)
var files = Files{
PasswordPath: "",
AclPath: "",
CheckAcls: false,
Users: make(map[string]*FileUser),
AclRecords: make([]AclRecord, 0, 0),
}
if passwordPath, ok := authOpts["password_path"]; ok {
files.PasswordPath = passwordPath
} else {
return files, errors.New("Files backend error: no password path given.\n")
}
if aclPath, ok := authOpts["acl_path"]; ok {
files.AclPath = aclPath
files.CheckAcls = true
} else {
files.CheckAcls = false
log.Info("Acls won't be checked.\n")
}
//Now initialize FileUsers by reading from password and acl files.
uCount, uErr := files.readPasswords()
if uErr != nil {
return files, errors.Errorf("Fatal: %s\n", uErr)
} else {
log.Infof("Got %d users from passwords file.\n", uCount)
}
//Only read acls if path was given.
if files.CheckAcls {
aclCount, aclErr := files.readAcls()
if aclErr != nil {
return files, errors.Errorf("Fatal: %s\n", aclErr)
} else {
log.Infof("Got %d lines from acl file.\n", aclCount)
}
}
return files, nil
}
//ReadPasswords read file and populates FileUsers. Return amount of users seen and possile error.
func (o Files) readPasswords() (int, error) {
usersCount := 0
file, fErr := os.Open(o.PasswordPath)
defer file.Close()
if fErr != nil {
return usersCount, fmt.Errorf("Files backend error: couldn't open passwords file: %s\n", fErr)
}
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
index := 0
//Read line by line
for scanner.Scan() {
index++
//Check comment or empty line to skip them.
if checkCommentOrEmpty(scanner.Text()) {
continue
}
lineArr := strings.Split(scanner.Text(), ":")
if len(lineArr) != 2 {
log.Errorf("Read passwords error: line %d is not well formatted.\n", index)
continue
}
//Create user if it doesn't exist and save password; override password if user existed.
var fileUser *FileUser
var ok bool
fileUser, ok = o.Users[lineArr[0]]
if ok {
fileUser.Password = lineArr[1]
} else {
usersCount++
fileUser = &FileUser{
Password: lineArr[1],
AclRecords: make([]AclRecord, 0, 0),
}
o.Users[lineArr[0]] = fileUser
}
}
return usersCount, nil
}
//ReadAcls reads the Acl file and associates them to existing users. It omits any non existing users.
func (o *Files) readAcls() (int, error) {
linesCount := 0
//Set currentUser as empty string
currentUser := ""
file, fErr := os.Open(o.AclPath)
defer file.Close()
if fErr != nil {
return linesCount, errors.Errorf("Files backend error: couldn't open acl file: %s\n", fErr)
}
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
index := 0
for scanner.Scan() {
index++
line := scanner.Text()
//Check comment or empty line to skip them.
if checkCommentOrEmpty(scanner.Text()) {
continue
}
//If we see a user line, change the current user.
if strings.Contains(line, "user") {
//Try to get username
lineArr := strings.Fields(line)
//Check format
if len(lineArr) == 2 && lineArr[0] == "user" {
_, ok := o.Users[lineArr[1]]
//Check that user exists
if !ok {
return 0, errors.Errorf("Files backend error: user %s does not exist for acl at line %d\n", lineArr[1], index)
}
currentUser = lineArr[1]
} else {
return 0, errors.Errorf("Files backend error: wrong acl format at line %d\n", index)
}
} else if strings.Contains(line, "topic") {
//Split and check for read, write or empty (readwwrite) privileges.
lineArr := strings.Fields(line)
if (len(lineArr) == 2 || len(lineArr) == 3) && lineArr[0] == "topic" {
var aclRecord = AclRecord{
Topic: "",
Acc: MOSQ_ACL_NONE,
}
//If len is 2, then we assume ReadWrite privileges.
if len(lineArr) == 2 {
aclRecord.Topic = lineArr[1]
aclRecord.Acc = MOSQ_ACL_READWRITE
} else {
aclRecord.Topic = lineArr[2]
if lineArr[1] == "read" {
aclRecord.Acc = MOSQ_ACL_READ
} else if lineArr[1] == "write" {
aclRecord.Acc = MOSQ_ACL_WRITE
} else if lineArr[1] == "readwrite" {
aclRecord.Acc = MOSQ_ACL_READWRITE
} else if lineArr[1] == "subscribe" {
aclRecord.Acc = MOSQ_ACL_SUBSCRIBE
} else {
return 0, errors.Errorf("Files backend error: wrong acl format at line %d\n", index)
}
}
//Append to user or general depending on currentUser.
if currentUser != "" {
fUser, _ := o.Users[currentUser]
fUser.AclRecords = append(fUser.AclRecords, aclRecord)
} else {
o.AclRecords = append(o.AclRecords, aclRecord)
}
linesCount++
} else {
return 0, errors.Errorf("Files backend error: wrong acl format at line %d\n", index)
}
} else if strings.Contains(line, "pattern") {
//Split and check for read, write or empty (readwwrite) privileges.
lineArr := strings.Fields(line)
if (len(lineArr) == 2 || len(lineArr) == 3) && lineArr[0] == "pattern" {
var aclRecord = AclRecord{
Topic: "",
Acc: MOSQ_ACL_NONE,
}
//If len is 2, then we assume ReadWrite privileges.
if len(lineArr) == 2 {
aclRecord.Topic = lineArr[1]
aclRecord.Acc = MOSQ_ACL_READWRITE
} else {
aclRecord.Topic = lineArr[2]
if lineArr[1] == "read" {
aclRecord.Acc = MOSQ_ACL_READ
} else if lineArr[1] == "write" {
aclRecord.Acc = MOSQ_ACL_WRITE
} else if lineArr[1] == "readwrite" {
aclRecord.Acc = MOSQ_ACL_READWRITE
} else if lineArr[1] == "subscribe" {
aclRecord.Acc = MOSQ_ACL_SUBSCRIBE
} else {
return 0, errors.Errorf("Files backend error: wrong acl format at line %d\n", index)
}
}
//Append to general acls.
o.AclRecords = append(o.AclRecords, aclRecord)
linesCount++
} else {
return 0, errors.Errorf("Files backend error: wrong acl format at line %d\n", index)
}
}
}
return linesCount, nil
}
func checkCommentOrEmpty(line string) bool {
if len(strings.Replace(line, " ", "", -1)) == 0 || line[0:1] == "#" {
return true
}
return false
}
//GetUser checks that user exists and password is correct.
func (o Files) GetUser(username, password, clientid string) bool {
fileUser, ok := o.Users[username]
if !ok {
return false
}
if common.HashCompare(password, fileUser.Password) {
return true
}
log.Warnf("wrong password for user %s\n", username)
return false
}
//GetSuperuser returns false for files backend.
func (o Files) GetSuperuser(username string) bool {
return false
}
//CheckAcl checks that the topic may be read/written by the given user/clientid.
func (o Files) CheckAcl(username, topic, clientid string, acc int32) bool {
//If there are no acls, all access is allowed.
if !o.CheckAcls {
return true
}
fileUser, ok := o.Users[username]
//If user exists, check against his acls and common ones. If not, check against common acls only.
if ok {
for _, aclRecord := range fileUser.AclRecords {
if common.TopicsMatch(aclRecord.Topic, topic) && (acc == int32(aclRecord.Acc) || int32(aclRecord.Acc) == MOSQ_ACL_READWRITE || (acc == MOSQ_ACL_SUBSCRIBE && topic != "#" && (int32(aclRecord.Acc) == MOSQ_ACL_READ || int32(aclRecord.Acc) == MOSQ_ACL_SUBSCRIBE))) {
return true
}
}
}
for _, aclRecord := range o.AclRecords {
//Replace all occurrences of %c for clientid and %u for username
aclTopic := strings.Replace(aclRecord.Topic, "%c", clientid, -1)
aclTopic = strings.Replace(aclTopic, "%u", username, -1)
if common.TopicsMatch(aclTopic, topic) && (acc == int32(aclRecord.Acc) || int32(aclRecord.Acc) == MOSQ_ACL_READWRITE || (acc == MOSQ_ACL_SUBSCRIBE && topic != "#" && (int32(aclRecord.Acc) == MOSQ_ACL_READ || int32(aclRecord.Acc) == MOSQ_ACL_SUBSCRIBE))) {
return true
}
}
return false
}
//GetName returns the backend's name
func (o Files) GetName() string {
return "Files"
}
//Halt does nothing for files as there's no cleanup needed.
func (o Files) Halt() {
//Do nothing
}