Add JWT files mode. Now both JWT and Files may use the generally available strict files checker.

Files should be able to check ACLs only.
Clen setPrefixes method.
Fix test-backends by building custom plugin too, fix files only acls by checking if a user was seen before creating a general record.
This commit is contained in:
Ignacio Gómez 2021-04-03 19:21:29 -03:00
parent 3eea16872d
commit ee6e68db3a
No known key found for this signature in database
GPG Key ID: 15A77C6BEC604B06
29 changed files with 1143 additions and 784 deletions

View File

@ -14,10 +14,14 @@ all:
go build pw-gen/pw.go
test:
cd plugin && make
go test ./backends ./cache ./hashing -v -count=1
rm plugin/*.so
test-backends:
cd plugin && make
go test ./backends -v -failfast -count=1
rm plugin/*.so
test-cache:
go test ./cache -v -failfast -count=1

View File

@ -498,8 +498,8 @@ Usage of ./pw:
For this backend `passwords` and `acls` file paths must be given:
```
auth_opt_password_path /path/to/password_file
auth_opt_acl_path /path/to/acl_file
auth_opt_files_password_path /path/to/password_file
auth_opt_files_acl_path /path/to/acl_file
```
The following are correctly formatted examples of password and acl files:
@ -824,11 +824,11 @@ There are no requirements, as the tests create (and later delete) the DB and tab
### JWT
The `jwt` backend is for auth with a JWT remote API, a local DB or a JavaScript VM interpreter. Global otions for JWT are:
The `jwt` backend is for auth with a JWT remote API, a local DB, a JavaScript VM interpreter or an ACL file. Global otions for JWT are:
| Option | default | Mandatory | Meaning |
| ------------------------- | ----------------- | :---------: | ------------------------------------------------------- |
| jwt_mode | | Y | local, remote, js |
| jwt_mode | | Y | local, remote, js, files |
| jwt_parse_token | false | N | Parse token in remote/js modes |
| jwt_secret | | Y/N | JWT secret, required for local mode, optional otherwise |
| jwt_userfield | | N | When `Username`, expect `username` as part of claims |
@ -1002,7 +1002,7 @@ Since local JWT follows the underlying DB backend's way of working, both of thes
#### JS mode
The last mode for this backend is JS mode, which allows to run a JavaScript interpreter VM to conduct checks. Options for this mode are:
When set to `js` JWT will act in JS mode, which allows to run a JavaScript interpreter VM to conduct checks. Options for this mode are:
| Option | default | Mandatory | Meaning |
| ------------------------------| --------------- | :---------: | ----------------------------------------------------- |
@ -1049,6 +1049,28 @@ With `auth_opt_jwt_parse_token` the signature would be `function checkAcl(token,
Finally, this mode uses [otto](https://github.com/robertkrimen/otto) under the hood to run the scripts. Please check their documentation for supported features and known limitations.
#### Files mode
When set to `files` JWT will run in Files mode, which allows to check user ACLs from a given file.
These ACLs follow the exact same syntax and semantics as those from the [Files](#files) backend.
Options for this mode are:
| Option | default | Mandatory | Meaning |
| ------------------------------| --------------- | :---------: | --------------------- |
| jwt_files_acl_path | | Y | Path to ACL files |
Notice there's no `passwords` file option since usernames come from parsing the JWT token and no password check is required.
Thus, you should be careful about general ACL rules and prefer to explicitly set rules for each valid user.
If this shows to be a pain, I'm open to add a file that sets valid `users`,
i.e. like the `passwords` file for regular `Files` backend but without actual passwords.
If you run into the case where you want to grant some general access but only to valid registered users,
and find that duplicating rules for each of them in ACLs file is really a pain, please open an issue for discussion.
#### Password hashing
Since JWT needs not to check passwords, there's no need to configure a `hasher`.

View File

@ -257,12 +257,8 @@ func (b *Backends) setCheckers(authOpts map[string]string) error {
}
}
if len(b.userCheckers) == 0 {
return errors.New("no backend registered user checks")
}
if len(b.aclCheckers) == 0 {
return errors.New("no backend registered ACL checks")
if len(b.userCheckers) == 0 && len(b.aclCheckers) == 0 {
return errors.New("no backends registered")
}
return nil
@ -270,31 +266,38 @@ func (b *Backends) setCheckers(authOpts map[string]string) error {
// setPrefixes sets options for prefixes handling.
func (b *Backends) setPrefixes(authOpts map[string]string, backends []string) {
if checkPrefix, ok := authOpts["check_prefix"]; ok && strings.Replace(checkPrefix, " ", "", -1) == "true" {
// Check that backends match prefixes.
if prefixesStr, ok := authOpts["prefixes"]; ok {
prefixes := strings.Split(strings.Replace(prefixesStr, " ", "", -1), ",")
if len(prefixes) == len(backends) {
// Set prefixes
// (I know some people find this type of comments useless, even harmful,
// but I find them helpful for quick code navigation on a project I don't work on daily, so screw them).
for i, backend := range backends {
b.prefixes[prefixes[i]] = backend
}
log.Infof("prefixes enabled for backends %s with prefixes %s.", authOpts["backends"], authOpts["prefixes"])
b.checkPrefix = true
} else {
log.Errorf("Error: got %d backends and %d prefixes, defaulting to prefixes disabled.", len(backends), len(prefixes))
b.checkPrefix = false
}
checkPrefix, ok := authOpts["check_prefix"]
} else {
log.Warn("Error: prefixes enabled but no options given, defaulting to prefixes disabled.")
b.checkPrefix = false
}
} else {
if !ok || strings.Replace(checkPrefix, " ", "", -1) != "true" {
b.checkPrefix = false
return
}
prefixesStr, ok := authOpts["prefixes"]
if !ok {
log.Warn("Error: prefixes enabled but no options given, defaulting to prefixes disabled.")
b.checkPrefix = false
return
}
prefixes := strings.Split(strings.Replace(prefixesStr, " ", "", -1), ",")
if len(prefixes) != len(backends) {
log.Errorf("Error: got %d backends and %d prefixes, defaulting to prefixes disabled.", len(backends), len(prefixes))
b.checkPrefix = false
return
}
for i, backend := range backends {
b.prefixes[prefixes[i]] = backend
}
log.Infof("prefixes enabled for backends %s with prefixes %s.", authOpts["backends"], authOpts["prefixes"])
b.checkPrefix = true
}
// checkPrefix checks if a username contains a valid prefix. If so, returns ok and the suitable backend name; else, !ok and empty string.

View File

@ -30,8 +30,8 @@ func TestBackends(t *testing.T) {
pwPath, _ := filepath.Abs("../test-files/passwords")
aclPath, _ := filepath.Abs("../test-files/acls")
authOpts["password_path"] = pwPath
authOpts["acl_path"] = aclPath
authOpts["files_password_path"] = pwPath
authOpts["files_acl_path"] = aclPath
authOpts["redis_host"] = "localhost"
authOpts["redis_port"] = "6379"
@ -65,24 +65,6 @@ func TestBackends(t *testing.T) {
So(err.Error(), ShouldEqual, "unknown backend unknown")
})
Convey("On initialization, lacking user/acl checkers should result in an error", t, func() {
authOpts["backends"] = "files, redis"
authOpts["files_register"] = "user"
authOpts["redis_register"] = "user"
_, err := Initialize(authOpts, log.DebugLevel)
So(err, ShouldNotBeNil)
So(err.Error(), ShouldEqual, "no backend registered ACL checks")
authOpts["backends"] = "files, redis"
authOpts["files_register"] = "acl"
authOpts["redis_register"] = "acl"
_, err = Initialize(authOpts, log.DebugLevel)
So(err, ShouldNotBeNil)
So(err.Error(), ShouldEqual, "no backend registered user checks")
})
Convey("On initialization, unknown checkers should result in an error", t, func() {
authOpts["backends"] = "files, redis"
authOpts["files_register"] = "user"

View File

@ -1,12 +0,0 @@
package backends
//Mosquitto 1.5 introduces a new acc, MOSQ_ACL_SUBSCRIBE. Kept the names, so don't mind the linter.
//In almost any case, subscribe should be the same as read, except if you want to deny access to # by preventing it on subscribe.
const (
MOSQ_ACL_NONE = 0x00
MOSQ_ACL_READ = 0x01
MOSQ_ACL_WRITE = 0x02
MOSQ_ACL_READWRITE = 0x03
MOSQ_ACL_SUBSCRIBE = 0x04
MOSQ_ACL_DENY = 0x11
)

View File

@ -0,0 +1,12 @@
package constants
// Mosquitto 1.5 introduces a new acc, MOSQ_ACL_SUBSCRIBE. Kept the names, so don't mind the linter.
// In almost any case, subscribe should be the same as read, except if you want to deny access to # by preventing it on subscribe.
const (
MOSQ_ACL_NONE = 0x00
MOSQ_ACL_READ = 0x01
MOSQ_ACL_WRITE = 0x02
MOSQ_ACL_READWRITE = 0x03
MOSQ_ACL_SUBSCRIBE = 0x04
MOSQ_ACL_DENY = 0x11
)

View File

@ -20,7 +20,7 @@ type CustomPlugin struct {
func NewCustomPlugin(authOpts map[string]string, logLevel log.Level) (*CustomPlugin, error) {
plug, err := plugin.Open(authOpts["plugin_path"])
if err != nil {
return nil, fmt.Errorf("Could not init custom plugin: %s", err)
return nil, fmt.Errorf("could not init custom plugin: %s", err)
}
customPlugin := &CustomPlugin{
@ -31,14 +31,14 @@ func NewCustomPlugin(authOpts map[string]string, logLevel log.Level) (*CustomPlu
plInit, err := plug.Lookup("Init")
if err != nil {
return nil, fmt.Errorf("Couldn't find func Init in plugin: %s", err)
return nil, fmt.Errorf("couldn't find func Init in plugin: %s", err)
}
initFunc := plInit.(func(authOpts map[string]string, logLevel log.Level) error)
err = initFunc(authOpts, logLevel)
if err != nil {
return nil, fmt.Errorf("Couldn't init plugin: %s", err)
return nil, fmt.Errorf("couldn't init plugin: %s", err)
}
customPlugin.init = initFunc
@ -46,7 +46,7 @@ func NewCustomPlugin(authOpts map[string]string, logLevel log.Level) (*CustomPlu
plName, err := plug.Lookup("GetName")
if err != nil {
return nil, fmt.Errorf("Couldn't find func GetName in plugin: %s", err)
return nil, fmt.Errorf("couldn't find func GetName in plugin: %s", err)
}
nameFunc := plName.(func() string)
@ -101,7 +101,7 @@ func NewCustomPlugin(authOpts map[string]string, logLevel log.Level) (*CustomPlu
plHalt, err := plug.Lookup("Halt")
if err != nil {
return nil, fmt.Errorf("Couldn't find func Halt in plugin: %s", err)
return nil, fmt.Errorf("couldn't find func Halt in plugin: %s", err)
}
haltFunc := plHalt.(func())

View File

@ -1,422 +1,69 @@
package backends
import (
"bufio"
"fmt"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"github.com/iegomez/mosquitto-go-auth/backends/files"
"github.com/iegomez/mosquitto-go-auth/hashing"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
const (
read = "read"
write = "write"
readwrite = "readwrite"
subscribe = "subscribe"
deny = "deny"
)
var permissions = map[string]byte{
read: MOSQ_ACL_READ,
write: MOSQ_ACL_WRITE,
readwrite: MOSQ_ACL_READWRITE,
subscribe: MOSQ_ACL_SUBSCRIBE,
deny: MOSQ_ACL_DENY,
}
//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, Subscribe 0x04, Deny 0x11
}
//FileBE holds paths to files, list of file users and general (no user or pattern) acl records.
// Files hols a static failes checker.
type Files struct {
sync.Mutex
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
filesOnly bool
hasher hashing.HashComparer
signals chan os.Signal
checker *files.Checker
}
//NewFiles initializes a files backend.
// NewFiles initializes a files backend.
func NewFiles(authOpts map[string]string, logLevel log.Level, hasher hashing.HashComparer) (*Files, error) {
log.SetLevel(logLevel)
var files = &Files{
PasswordPath: "",
AclPath: "",
CheckAcls: false,
Users: make(map[string]*FileUser),
AclRecords: make([]AclRecord, 0),
filesOnly: true,
hasher: hasher,
signals: make(chan os.Signal, 1),
/*
It is an error for the Files backend not to have a passwords file, but it is not for the underlying
static files checker since it may be used in JWT. Thus, we need to check for the option here before
building our checker.
*/
pwRegistered := strings.Contains(authOpts["files_register"], "user")
pwPath, ok := authOpts["files_password_path"]
if pwRegistered && (!ok || pwPath == "") {
return nil, errors.New("missing passwords file path")
}
if len(strings.Split(strings.Replace(authOpts["backends"], " ", "", -1), ",")) > 1 {
files.filesOnly = false
}
if passwordPath, ok := authOpts["password_path"]; ok {
files.PasswordPath = passwordPath
} else {
return nil, errors.New("Files backend error: no password path given")
}
if aclPath, ok := authOpts["acl_path"]; ok {
files.AclPath = aclPath
files.CheckAcls = true
} else {
files.CheckAcls = false
log.Info("Acls won't be checked")
}
err := files.loadFiles()
var checker, err = files.NewChecker(authOpts["backends"], authOpts["files_password_path"], authOpts["files_acl_path"], logLevel, hasher)
if err != nil {
return nil, err
}
go files.watchSignals()
return files, nil
return &Files{
checker: checker,
}, nil
}
func (o *Files) watchSignals() {
signal.Notify(o.signals, syscall.SIGHUP)
for {
select {
case sig := <-o.signals:
if sig == syscall.SIGHUP {
log.Debugln("Got SIGHUP, reloading files.")
o.loadFiles()
}
}
}
}
func (o *Files) loadFiles() error {
o.Lock()
defer o.Unlock()
count, err := o.readPasswords()
if err != nil {
return errors.Errorf("read passwords: %s", err)
}
log.Debugf("got %d users from passwords file", count)
//Only read acls if path was given.
if o.CheckAcls {
count, err := o.readAcls()
if err != nil {
return errors.Errorf("read acls: %s", err)
}
log.Debugf("got %d lines from acl file", count)
}
return nil
}
//ReadPasswords read file and populates FileUsers. Return amount of users seen and possile error.
func (o *Files) readPasswords() (int, error) {
usersCount := 0
file, err := os.Open(o.PasswordPath)
if err != nil {
return usersCount, fmt.Errorf("Files backend error: couldn't open passwords file: %s", err)
}
defer file.Close()
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", 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),
}
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
currentUser := ""
userExists := false
file, err := os.Open(o.AclPath)
if err != nil {
return linesCount, errors.Errorf("Files backend error: couldn't open acl file: %s", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
index := 0
for scanner.Scan() {
index++
if checkCommentOrEmpty(scanner.Text()) {
continue
}
line := strings.TrimSpace(scanner.Text())
lineArr := strings.Fields(line)
prefix := lineArr[0]
if prefix == "user" {
// Since there may be more than one consecutive space in the username, we have to remove the prefix and trim to get the username.
username, err := removeAndTrim(prefix, line, index)
if err != nil {
return 0, err
}
_, ok := o.Users[username]
if !ok {
log.Warnf("user %s doesn't exist, skipping acls", username)
// Flag username to skip topics later.
userExists = false
continue
}
userExists = true
currentUser = username
} else if prefix == "topic" || prefix == "pattern" {
var aclRecord = AclRecord{
Topic: "",
Acc: MOSQ_ACL_NONE,
}
/* If len is 2, then we assume ReadWrite privileges.
Notice that Mosquitto docs prevent whitespaces in the topic when there's no explicit access given:
"The access type is controlled using "read", "write", "readwrite" or "deny". This parameter is optional (unless <topic> includes a space character)"
https://mosquitto.org/man/mosquitto-conf-5.html
When access is given, then the topic may contain whitespaces.
Nevertheless, there may be white spaces between topic/pattern and the permission or the topic itself.
Fields captures the case in which there's only topic/pattern and the given topic because it trims extra spaces between them.
*/
if len(lineArr) == 2 {
aclRecord.Topic = lineArr[1]
aclRecord.Acc = MOSQ_ACL_READWRITE
} else {
// There may be more than one space between topic/pattern and the permission, as well as between the latter and the topic itself.
// Hence, we remove the prefix, trim the line and split on white space to get the permission.
line, err = removeAndTrim(prefix, line, index)
if err != nil {
return 0, err
}
lineArr = strings.Split(line, " ")
permission := lineArr[0]
// Again, there may be more than one space between the permission and the topic, so we'll trim what's left after removing it and that'll be the topic.
topic, err := removeAndTrim(permission, line, index)
if err != nil {
return 0, err
}
switch permission {
case read, write, readwrite, subscribe, deny:
aclRecord.Acc = permissions[permission]
default:
return 0, errors.Errorf("Files backend error: wrong acl format at line %d", index)
}
aclRecord.Topic = topic
}
if prefix == "topic" {
if currentUser != "" {
// Skip topic when user was not found.
if !userExists {
continue
}
fUser, ok := o.Users[currentUser]
if !ok {
return 0, errors.Errorf("Files backend error: user does not exist for acl at line %d", index)
}
fUser.AclRecords = append(fUser.AclRecords, aclRecord)
} else {
o.AclRecords = append(o.AclRecords, aclRecord)
}
} else {
o.AclRecords = append(o.AclRecords, aclRecord)
}
linesCount++
} else {
return 0, errors.Errorf("Files backend error: wrong acl format at line %d", index)
}
}
return linesCount, nil
}
func removeAndTrim(prefix, line string, index int) (string, error) {
if len(line)-len(prefix) < 1 {
return "", errors.Errorf("Files backend error: wrong acl format at line %d", index)
}
newLine := strings.TrimSpace(line[len(prefix):])
return newLine, 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.
// GetUser checks that user exists and password is correct.
func (o *Files) GetUser(username, password, clientid string) (bool, error) {
fileUser, ok := o.Users[username]
if !ok {
return false, nil
}
if o.hasher.Compare(password, fileUser.Password) {
return true, nil
}
log.Warnf("wrong password for user %s", username)
return false, nil
return o.checker.GetUser(username, password, clientid)
}
//GetSuperuser returns false for files backend.
// GetSuperuser returns false for files backend.
func (o *Files) GetSuperuser(username string) (bool, error) {
return false, nil
}
//CheckAcl checks that the topic may be read/written by the given user/clientid.
// 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, error) {
//If there are no acls and Files is the only backend, all access is allowed.
//If there are other backends, then we can't blindly grant access.
if !o.CheckAcls {
return o.filesOnly, nil
}
fileUser, ok := o.Users[username]
// Check if the topic was explicitly denied and refuse to authorize if so.
if ok {
for _, aclRecord := range fileUser.AclRecords {
match := TopicsMatch(aclRecord.Topic, topic)
if match {
if aclRecord.Acc == MOSQ_ACL_DENY {
return false, nil
}
}
}
}
for _, aclRecord := range o.AclRecords {
aclTopic := strings.Replace(aclRecord.Topic, "%c", clientid, -1)
aclTopic = strings.Replace(aclTopic, "%u", username, -1)
match := TopicsMatch(aclTopic, topic)
if match {
if aclRecord.Acc == MOSQ_ACL_DENY {
return false, nil
}
}
}
// No denials, check against user's acls and common ones. If not authorized, check against pattern acls.
if ok {
for _, aclRecord := range fileUser.AclRecords {
match := TopicsMatch(aclRecord.Topic, topic)
if match {
if 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, nil
}
}
}
}
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)
match := TopicsMatch(aclTopic, topic)
if match {
if 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, nil
}
}
}
return false, nil
return o.checker.CheckAcl(username, topic, clientid, acc)
}
//GetName returns the backend's name
// GetName returns the backend's name
func (o *Files) GetName() string {
return "Files"
}
//Halt does nothing for files as there's no cleanup needed.
// Halt cleans up Files backend.
func (o *Files) Halt() {
//Do nothing
o.checker.Halt()
}

438
backends/files/files.go Normal file
View File

@ -0,0 +1,438 @@
package files
import (
"bufio"
"fmt"
"os"
"os/signal"
"strings"
"sync"
"syscall"
. "github.com/iegomez/mosquitto-go-auth/backends/constants"
"github.com/iegomez/mosquitto-go-auth/backends/topics"
"github.com/iegomez/mosquitto-go-auth/hashing"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
const (
read = "read"
write = "write"
readwrite = "readwrite"
subscribe = "subscribe"
deny = "deny"
)
var permissions = map[string]byte{
read: MOSQ_ACL_READ,
write: MOSQ_ACL_WRITE,
readwrite: MOSQ_ACL_READWRITE,
subscribe: MOSQ_ACL_SUBSCRIBE,
deny: MOSQ_ACL_DENY,
}
// StaticFileUer keeps a user password and acl records.
type staticFileUser 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, Subscribe 0x04, Deny 0x11
}
// Checker holds paths to static files, list of file users and general (no user or pattern) acl records.
type Checker struct {
sync.Mutex
pwPath string
aclPath string
checkACLs bool
checkUsers bool
users map[string]*staticFileUser //users keeps a registry of username/staticFileUser pairs, holding a user's password and Acl records.
aclRecords []aclRecord
staticFilesOnly bool
hasher hashing.HashComparer
signals chan os.Signal
}
// NewCheckers initializes a static files checker.
func NewChecker(backends, passwordPath, aclPath string, logLevel log.Level, hasher hashing.HashComparer) (*Checker, error) {
log.SetLevel(logLevel)
var checker = &Checker{
pwPath: passwordPath,
aclPath: aclPath,
checkACLs: true,
users: make(map[string]*staticFileUser),
aclRecords: make([]aclRecord, 0),
staticFilesOnly: true,
hasher: hasher,
signals: make(chan os.Signal, 1),
checkUsers: true,
}
if checker.pwPath == "" {
checker.checkUsers = false
log.Infoln("[StaticFiles] passwords won't be checked")
}
if checker.aclPath == "" {
checker.checkACLs = false
log.Infoln("[StaticFiles] acls won't be checked")
}
if len(strings.Split(strings.Replace(backends, " ", "", -1), ",")) > 1 {
checker.staticFilesOnly = false
}
err := checker.loadStaticFiles()
if err != nil {
return nil, err
}
go checker.watchSignals()
return checker, nil
}
func (o *Checker) watchSignals() {
signal.Notify(o.signals, syscall.SIGHUP)
for {
select {
case sig := <-o.signals:
if sig == syscall.SIGHUP {
log.Debugln("[StaticFiles] got SIGHUP, reloading static files")
o.loadStaticFiles()
}
}
}
}
func (o *Checker) loadStaticFiles() error {
o.Lock()
defer o.Unlock()
if o.checkUsers {
count, err := o.readPasswords()
if err != nil {
return errors.Errorf("read passwords: %s", err)
}
log.Debugf("got %d users from passwords file", count)
}
if o.checkACLs {
count, err := o.readAcls()
if err != nil {
return errors.Errorf("read acls: %s", err)
}
log.Debugf("got %d lines from acl file", count)
}
return nil
}
// ReadPasswords reads passwords file and populates static file users. Returns amount of users seen and possile error.
func (o *Checker) readPasswords() (int, error) {
usersCount := 0
file, err := os.Open(o.pwPath)
if err != nil {
return usersCount, fmt.Errorf("[StaticFiles] error: couldn't open passwords file: %s", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
index := 0
for scanner.Scan() {
index++
text := scanner.Text()
if checkCommentOrEmpty(text) {
continue
}
lineArr := strings.Split(text, ":")
if len(lineArr) != 2 {
log.Errorf("Read passwords error: line %d is not well formatted", index)
continue
}
var fileUser *staticFileUser
var ok bool
fileUser, ok = o.users[lineArr[0]]
if ok {
fileUser.password = lineArr[1]
} else {
usersCount++
fileUser = &staticFileUser{
password: lineArr[1],
aclRecords: make([]aclRecord, 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 *Checker) readAcls() (int, error) {
linesCount := 0
currentUser := ""
userExists := false
userSeen := false
file, err := os.Open(o.aclPath)
if err != nil {
return linesCount, errors.Errorf("StaticFiles backend error: couldn't open acl file: %s", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
index := 0
for scanner.Scan() {
index++
text := scanner.Text()
if checkCommentOrEmpty(text) {
continue
}
line := strings.TrimSpace(text)
lineArr := strings.Fields(line)
prefix := lineArr[0]
if prefix == "user" {
// Flag that a user has been seen so no topic coming after is addigned to general ones.
userSeen = true
// Since there may be more than one consecutive space in the username, we have to remove the prefix and trim to get the username.
username, err := removeAndTrim(prefix, line, index)
if err != nil {
return 0, err
}
_, ok := o.users[username]
if !ok {
if o.checkUsers {
log.Warnf("user %s doesn't exist, skipping acls", username)
// Flag username to skip topics later.
userExists = false
continue
}
o.users[username] = &staticFileUser{
password: "",
aclRecords: make([]aclRecord, 0),
}
}
userExists = true
currentUser = username
} else if prefix == "topic" || prefix == "pattern" {
var aclRecord = aclRecord{
topic: "",
acc: MOSQ_ACL_NONE,
}
/* If len is 2, then we assume ReadWrite privileges.
Notice that Mosquitto docs prevent whitespaces in the topic when there's no explicit access given:
"The access type is controlled using "read", "write", "readwrite" or "deny". This parameter is optional (unless <topic> includes a space character)"
https://mosquitto.org/man/mosquitto-conf-5.html
When access is given, then the topic may contain whitespaces.
Nevertheless, there may be white spaces between topic/pattern and the permission or the topic itself.
Fields captures the case in which there's only topic/pattern and the given topic because it trims extra spaces between them.
*/
if len(lineArr) == 2 {
aclRecord.topic = lineArr[1]
aclRecord.acc = MOSQ_ACL_READWRITE
} else {
// There may be more than one space between topic/pattern and the permission, as well as between the latter and the topic itself.
// Hence, we remove the prefix, trim the line and split on white space to get the permission.
line, err = removeAndTrim(prefix, line, index)
if err != nil {
return 0, err
}
lineArr = strings.Split(line, " ")
permission := lineArr[0]
// Again, there may be more than one space between the permission and the topic, so we'll trim what's left after removing it and that'll be the topic.
topic, err := removeAndTrim(permission, line, index)
if err != nil {
return 0, err
}
switch permission {
case read, write, readwrite, subscribe, deny:
aclRecord.acc = permissions[permission]
default:
return 0, errors.Errorf("StaticFiles backend error: wrong acl format at line %d", index)
}
aclRecord.topic = topic
}
if prefix == "topic" {
if currentUser != "" {
// Skip topic when user was not found.
if !userExists {
continue
}
fUser, ok := o.users[currentUser]
if !ok {
return 0, errors.Errorf("StaticFiles backend error: user does not exist for acl at line %d", index)
}
fUser.aclRecords = append(fUser.aclRecords, aclRecord)
} else {
// Only append to general topics when no user has been processed.
if !userSeen {
o.aclRecords = append(o.aclRecords, aclRecord)
}
}
} else {
o.aclRecords = append(o.aclRecords, aclRecord)
}
linesCount++
} else {
return 0, errors.Errorf("StaticFiles backend error: wrong acl format at line %d", index)
}
}
return linesCount, nil
}
func removeAndTrim(prefix, line string, index int) (string, error) {
if len(line)-len(prefix) < 1 {
return "", errors.Errorf("StaticFiles backend error: wrong acl format at line %d", index)
}
newLine := strings.TrimSpace(line[len(prefix):])
return newLine, nil
}
func checkCommentOrEmpty(line string) bool {
if len(strings.Replace(line, " ", "", -1)) == 0 || line[0:1] == "#" {
return true
}
return false
}
func (o *Checker) Users() map[string]*staticFileUser {
return o.users
}
// GetUser checks that user exists and password is correct.
func (o *Checker) GetUser(username, password, clientid string) (bool, error) {
fileUser, ok := o.users[username]
if !ok {
return false, nil
}
if o.hasher.Compare(password, fileUser.password) {
return true, nil
}
log.Warnf("wrong password for user %s", username)
return false, nil
}
// GetSuperuser returns false as there are no files superusers.
func (o *Checker) GetSuperuser(username string) (bool, error) {
return false, nil
}
// CheckAcl checks that the topic may be read/written by the given user/clientid.
func (o *Checker) CheckAcl(username, topic, clientid string, acc int32) (bool, error) {
// If there are no acls and StaticFiles is the only backend, all access is allowed.
// If there are other backends, then we can't blindly grant access.
if !o.checkACLs {
return o.staticFilesOnly, nil
}
fileUser, ok := o.users[username]
// Check if the topic was explicitly denied and refuse to authorize if so.
if ok {
for _, aclRecord := range fileUser.aclRecords {
match := topics.Match(aclRecord.topic, topic)
if match {
if aclRecord.acc == MOSQ_ACL_DENY {
return false, nil
}
}
}
}
for _, aclRecord := range o.aclRecords {
aclTopic := strings.Replace(aclRecord.topic, "%c", clientid, -1)
aclTopic = strings.Replace(aclTopic, "%u", username, -1)
match := topics.Match(aclTopic, topic)
if match {
if aclRecord.acc == MOSQ_ACL_DENY {
return false, nil
}
}
}
// No denials, check against user's acls and common ones. If not authorized, check against pattern acls.
if ok {
for _, aclRecord := range fileUser.aclRecords {
match := topics.Match(aclRecord.topic, topic)
if match {
if 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, nil
}
}
}
}
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)
match := topics.Match(aclTopic, topic)
if match {
if 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, nil
}
}
}
return false, nil
}
// Halt does nothing for static files as there's no cleanup needed.
func (o *Checker) Halt() {
// NO-OP
}

View File

@ -0,0 +1,372 @@
package files
import (
"fmt"
"os"
"path/filepath"
"syscall"
"testing"
"time"
. "github.com/iegomez/mosquitto-go-auth/backends/constants"
"github.com/iegomez/mosquitto-go-auth/hashing"
log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"
)
func TestFiles(t *testing.T) {
authOpts := make(map[string]string)
Convey("Given empty opts NewChecker should fail", t, func() {
files, err := NewChecker("", "", "", log.DebugLevel, hashing.NewHasher(authOpts, "files"))
So(err, ShouldBeError)
files.Halt()
})
Convey("Given valid params NewChecker should return a new checker instance", t, func() {
backendsOpt := "files"
pwPath, err := filepath.Abs("test-files/passwords")
So(err, ShouldBeNil)
aclPath, err := filepath.Abs("test-files/acls")
So(err, ShouldBeNil)
clientID := "test_client"
files, err := NewChecker(backendsOpt, pwPath, aclPath, log.DebugLevel, hashing.NewHasher(authOpts, "files"))
So(err, ShouldBeNil)
/*
ACL file looks like this:
topic test/general
topic deny test/general_denied
user test1
topic write test/topic/1
topic read test/topic/2
user test2
topic read test/topic/+
user test3
topic read test/#
topic deny test/denied
user test with space
topic test/space
topic read test/multiple spaces in/topic
topic read test/lots of spaces in/topic and borders
user not_present
topic read test/not_present
pattern read test/%u
pattern read test/%c
*/
// passwords are the same as users,
// except for user4 that's not present in passwords and should be skipped when reading acls
user1 := "test1"
user2 := "test2"
user3 := "test3"
user4 := "not_present"
elton := "test with space" // You know, because he's a rocket man. Ok, I'll let myself out.
generalTopic := "test/general"
generalDeniedTopic := "test/general_denied"
Convey("All users but not present ones should have a record", func() {
_, ok := files.users[user1]
So(ok, ShouldBeTrue)
_, ok = files.users[user2]
So(ok, ShouldBeTrue)
_, ok = files.users[user3]
So(ok, ShouldBeTrue)
_, ok = files.users[user4]
So(ok, ShouldBeFalse)
_, ok = files.users[elton]
So(ok, ShouldBeTrue)
})
Convey("All users should be able to read the general topic", func() {
authenticated, err := files.CheckAcl(user1, generalTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
authenticated, err = files.CheckAcl(user2, generalTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
authenticated, err = files.CheckAcl(user3, generalTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
authenticated, err = files.CheckAcl(elton, generalTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
})
Convey("No user should be able to read the general denied topic", func() {
authenticated, err := files.CheckAcl(user1, generalDeniedTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeFalse)
authenticated, err = files.CheckAcl(user2, generalDeniedTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeFalse)
authenticated, err = files.CheckAcl(user3, generalDeniedTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeFalse)
authenticated, err = files.CheckAcl(elton, generalDeniedTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeFalse)
})
Convey("Given a username and a correct password, it should correctly authenticate it", func() {
authenticated, err := files.GetUser(user1, user1, clientID)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
authenticated, err = files.GetUser(user2, user2, clientID)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
authenticated, err = files.GetUser(user3, user3, clientID)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
authenticated, err = files.GetUser(elton, elton, clientID)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
})
Convey("Given a username and an incorrect password, it should not authenticate it", func() {
authenticated, err := files.GetUser(user1, user2, clientID)
So(err, ShouldBeNil)
So(authenticated, ShouldBeFalse)
})
Convey("Given a wrong username, it should not authenticate it and not return error", func() {
authenticated, err := files.GetUser(user4, "whatever_password", "")
So(err, ShouldBeNil)
So(authenticated, ShouldBeFalse)
})
//There are no superusers for files
Convey("For any user superuser should return false", func() {
superuser, err := files.GetSuperuser(user1)
So(err, ShouldBeNil)
So(superuser, ShouldBeFalse)
Convey("Including non-present username", func() {
superuser, err := files.GetSuperuser(user4)
So(err, ShouldBeNil)
So(superuser, ShouldBeFalse)
})
})
testTopic1 := `test/topic/1`
testTopic2 := `test/topic/2`
testTopic3 := `test/other/1`
testTopic4 := `other/1`
readWriteTopic := "readwrite/topic"
spaceTopic := "test/space"
multiSpaceTopic := "test/multiple spaces in/topic"
lotsOfSpacesTopic := "test/lots of spaces in/topic and borders"
deniedTopic := "test/denied"
Convey("Topics for non existing users should be ignored when there's a passwords file", func() {
for record := range files.aclRecords {
So(record, ShouldNotEqual, "test/not_present")
}
for _, user := range files.users {
for record := range user.aclRecords {
So(record, ShouldNotEqual, "test/not_present")
}
}
})
Convey("Topics for users should be honored when there's no passwords file", func() {
tt, err := files.CheckAcl("not_present", "test/not_present", clientID, 1)
So(err, ShouldBeNil)
So(tt, ShouldBeTrue)
})
Convey("User 1 should be able to publish and not subscribe to test topic 1, and only subscribe but not publish to topic 2", func() {
tt1, err1 := files.CheckAcl(user1, testTopic1, clientID, 2)
tt2, err2 := files.CheckAcl(user1, testTopic1, clientID, 1)
tt3, err3 := files.CheckAcl(user1, testTopic2, clientID, 2)
tt4, err4 := files.CheckAcl(user1, testTopic2, clientID, 1)
So(err1, ShouldBeNil)
So(err2, ShouldBeNil)
So(err3, ShouldBeNil)
So(err4, ShouldBeNil)
So(tt1, ShouldBeTrue)
So(tt2, ShouldBeFalse)
So(tt3, ShouldBeFalse)
So(tt4, ShouldBeTrue)
})
Convey("User 1 should be able to subscribe or publish to a readwrite topic rule", func() {
tt1, err1 := files.CheckAcl(user1, readWriteTopic, clientID, 2)
tt2, err2 := files.CheckAcl(user1, readWriteTopic, clientID, 1)
So(err1, ShouldBeNil)
So(err2, ShouldBeNil)
So(tt1, ShouldBeTrue)
So(tt2, ShouldBeTrue)
})
Convey("User 2 should be able to read any test/topic/X but not any/other", func() {
tt1, err1 := files.CheckAcl(user2, testTopic1, clientID, 1)
tt2, err2 := files.CheckAcl(user2, testTopic2, clientID, 1)
tt3, err3 := files.CheckAcl(user2, testTopic3, clientID, 1)
So(err1, ShouldBeNil)
So(err2, ShouldBeNil)
So(err3, ShouldBeNil)
So(tt1, ShouldBeTrue)
So(tt2, ShouldBeTrue)
So(tt3, ShouldBeFalse)
})
Convey("User 3 should be able to read any test/X but not other/... nor test/denied\n\n", func() {
tt1, err1 := files.CheckAcl(user3, testTopic1, clientID, 1)
tt2, err2 := files.CheckAcl(user3, testTopic2, clientID, 1)
tt3, err3 := files.CheckAcl(user3, testTopic3, clientID, 1)
tt4, err4 := files.CheckAcl(user3, testTopic4, clientID, 1)
tt5, err5 := files.CheckAcl(user3, deniedTopic, clientID, 1)
So(err1, ShouldBeNil)
So(err2, ShouldBeNil)
So(err3, ShouldBeNil)
So(err4, ShouldBeNil)
So(err5, ShouldBeNil)
So(tt1, ShouldBeTrue)
So(tt2, ShouldBeTrue)
So(tt3, ShouldBeTrue)
So(tt4, ShouldBeFalse)
So(tt5, ShouldBeFalse)
})
Convey("User 4 should not be able to read since it's not in the passwords file", func() {
tt1, err1 := files.CheckAcl(user4, testTopic1, clientID, 1)
So(err1, ShouldBeNil)
So(tt1, ShouldBeFalse)
})
Convey("Elton Bowie should be able to read and write to `test/space`, and only read from other topics", func() {
tt1, err1 := files.CheckAcl(elton, spaceTopic, clientID, 2)
tt2, err2 := files.CheckAcl(elton, multiSpaceTopic, clientID, 1)
tt3, err3 := files.CheckAcl(elton, multiSpaceTopic, clientID, 2)
tt4, err4 := files.CheckAcl(elton, lotsOfSpacesTopic, clientID, 1)
tt5, err5 := files.CheckAcl(elton, lotsOfSpacesTopic, clientID, 2)
So(err1, ShouldBeNil)
So(err2, ShouldBeNil)
So(err3, ShouldBeNil)
So(err4, ShouldBeNil)
So(err5, ShouldBeNil)
So(tt1, ShouldBeTrue)
So(tt2, ShouldBeTrue)
So(tt3, ShouldBeFalse)
So(tt4, ShouldBeTrue)
So(tt5, ShouldBeFalse)
})
//Now check against patterns.
Convey("Given a topic that mentions username, acl check should pass", func() {
tt1, err1 := files.CheckAcl(user1, "test/test1", clientID, 1)
So(err1, ShouldBeNil)
So(tt1, ShouldBeTrue)
tt2, err2 := files.CheckAcl(elton, "test/test with space", clientID, 1)
So(err2, ShouldBeNil)
So(tt2, ShouldBeTrue)
})
Convey("Given a topic that mentions clientid, acl check should pass", func() {
tt1, err1 := files.CheckAcl(user1, "test/test_client", clientID, 1)
So(err1, ShouldBeNil)
So(tt1, ShouldBeTrue)
})
//Halt files
files.Halt()
})
Convey("On SIGHUP files should be reloaded", t, func() {
pwFile, err := os.Create("test-files/test-passwords")
So(err, ShouldBeNil)
aclFile, err := os.Create("test-files/test-acls")
So(err, ShouldBeNil)
pwPath, err := filepath.Abs("test-files/test-passwords")
So(err, ShouldBeNil)
aclPath, err := filepath.Abs("test-files/test-acls")
So(err, ShouldBeNil)
defer os.Remove(pwPath)
defer os.Remove(aclPath)
hasher := hashing.NewHasher(authOpts, "files")
user1 := "test1"
user2 := "test2"
pw1, err := hasher.Hash(user1)
So(err, ShouldBeNil)
pw2, err := hasher.Hash(user2)
So(err, ShouldBeNil)
pwFile.WriteString(fmt.Sprintf("\n%s:%s\n", user1, pw1))
aclFile.WriteString("\nuser test1")
aclFile.WriteString("\ntopic read test/#")
pwFile.Sync()
aclFile.Sync()
backendsOpt := "files"
files, err := NewChecker(backendsOpt, pwPath, aclPath, log.DebugLevel, hasher)
So(err, ShouldBeNil)
user, ok := files.users[user1]
So(ok, ShouldBeTrue)
record := user.aclRecords[0]
So(record.acc, ShouldEqual, MOSQ_ACL_READ)
So(record.topic, ShouldEqual, "test/#")
_, ok = files.users[user2]
So(ok, ShouldBeFalse)
// Now add second user and reload.
pwFile.WriteString(fmt.Sprintf("\n%s:%s\n", user2, pw2))
aclFile.WriteString("\nuser test2")
aclFile.WriteString("\ntopic write test/#")
files.signals <- syscall.SIGHUP
time.Sleep(200 * time.Millisecond)
user, ok = files.users[user2]
So(ok, ShouldBeTrue)
record = user.aclRecords[0]
So(record.acc, ShouldEqual, MOSQ_ACL_WRITE)
So(record.topic, ShouldEqual, "test/#")
})
}

View File

@ -1,361 +1,102 @@
package backends
import (
"fmt"
"os"
"path/filepath"
"syscall"
"testing"
"time"
"github.com/iegomez/mosquitto-go-auth/hashing"
log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"
)
func TestFiles(t *testing.T) {
//Initialize Files with mock password and acl files.
func TestFilesBackend(t *testing.T) {
// The bulk of files testing is done in the internal files checker, we'll just check obvious initialization and defaults.
authOpts := make(map[string]string)
logLevel := log.DebugLevel
hasher := hashing.NewHasher(authOpts, "files")
Convey("Given empty opts NewFiles should fail", t, func() {
files, err := NewFiles(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, "files"))
So(err, ShouldBeError)
Convey("When files backend is set, missing passwords path should make NewFiles fail when registered to check users", t, func() {
authOpts["backends"] = "files"
authOpts["files_register"] = "user"
files.Halt()
_, err := NewFiles(authOpts, logLevel, hasher)
So(err, ShouldNotBeNil)
})
pwPath, _ := filepath.Abs("../test-files/passwords")
aclPath, _ := filepath.Abs("../test-files/acls")
authOpts["password_path"] = pwPath
authOpts["acl_path"] = aclPath
clientID := "test_client"
Convey("When files backend is set, missing passwords path should not make NewFiles fail when not registered to check users", t, func() {
authOpts["backends"] = "files"
delete(authOpts, "files_register")
Convey("Given valid params NewFiles should return a new files backend instance", t, func() {
files, err := NewFiles(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, "files"))
_, err := NewFiles(authOpts, logLevel, hasher)
So(err, ShouldBeNil)
/*
ACL file looks like this:
topic test/general
topic deny test/general_denied
user test1
topic write test/topic/1
topic read test/topic/2
user test2
topic read test/topic/+
user test3
topic read test/#
topic deny test/denied
user test with space
topic test/space
topic read test/multiple spaces in/topic
topic read test/lots of spaces in/topic and borders
user not_present
topic read test/not_present
pattern read test/%u
pattern read test/%c
*/
// passwords are the same as users,
// except for user4 that's not present in passwords and should be skipped when reading acls
user1 := "test1"
user2 := "test2"
user3 := "test3"
user4 := "not_present"
elton := "test with space" // You know, because he's a rocket man. Ok, I'll let myself out.
generalTopic := "test/general"
generalDeniedTopic := "test/general_denied"
Convey("All users but not present ones should have a record", func() {
_, ok := files.Users[user1]
So(ok, ShouldBeTrue)
_, ok = files.Users[user2]
So(ok, ShouldBeTrue)
_, ok = files.Users[user3]
So(ok, ShouldBeTrue)
_, ok = files.Users[user4]
So(ok, ShouldBeFalse)
_, ok = files.Users[elton]
So(ok, ShouldBeTrue)
})
Convey("All users should be able to read the general topic", func() {
authenticated, err := files.CheckAcl(user1, generalTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
authenticated, err = files.CheckAcl(user2, generalTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
authenticated, err = files.CheckAcl(user3, generalTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
authenticated, err = files.CheckAcl(elton, generalTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
})
Convey("No user should be able to read the general denied topic", func() {
authenticated, err := files.CheckAcl(user1, generalDeniedTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeFalse)
authenticated, err = files.CheckAcl(user2, generalDeniedTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeFalse)
authenticated, err = files.CheckAcl(user3, generalDeniedTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeFalse)
authenticated, err = files.CheckAcl(elton, generalDeniedTopic, clientID, 1)
So(err, ShouldBeNil)
So(authenticated, ShouldBeFalse)
})
Convey("Given a username and a correct password, it should correctly authenticate it", func() {
authenticated, err := files.GetUser(user1, user1, clientID)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
authenticated, err = files.GetUser(user2, user2, clientID)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
authenticated, err = files.GetUser(user3, user3, clientID)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
authenticated, err = files.GetUser(elton, elton, clientID)
So(err, ShouldBeNil)
So(authenticated, ShouldBeTrue)
})
Convey("Given a username and an incorrect password, it should not authenticate it", func() {
authenticated, err := files.GetUser(user1, user2, clientID)
So(err, ShouldBeNil)
So(authenticated, ShouldBeFalse)
})
Convey("Given a wrong username, it should not authenticate it and not return error", func() {
authenticated, err := files.GetUser(user4, "whatever_password", "")
So(err, ShouldBeNil)
So(authenticated, ShouldBeFalse)
})
//There are no superusers for files
Convey("For any user superuser should return false", func() {
superuser, err := files.GetSuperuser(user1)
So(err, ShouldBeNil)
So(superuser, ShouldBeFalse)
Convey("Including non-present username", func() {
superuser, err := files.GetSuperuser(user4)
So(err, ShouldBeNil)
So(superuser, ShouldBeFalse)
})
})
testTopic1 := `test/topic/1`
testTopic2 := `test/topic/2`
testTopic3 := `test/other/1`
testTopic4 := `other/1`
readWriteTopic := "readwrite/topic"
spaceTopic := "test/space"
multiSpaceTopic := "test/multiple spaces in/topic"
lotsOfSpacesTopic := "test/lots of spaces in/topic and borders"
deniedTopic := "test/denied"
Convey("Topics for non existing users should be ignored", func() {
for record := range files.AclRecords {
So(record, ShouldNotEqual, "test/not_present")
}
for _, user := range files.Users {
for record := range user.AclRecords {
So(record, ShouldNotEqual, "test/not_present")
}
}
})
Convey("User 1 should be able to publish and not subscribe to test topic 1, and only subscribe but not publish to topic 2", func() {
tt1, err1 := files.CheckAcl(user1, testTopic1, clientID, 2)
tt2, err2 := files.CheckAcl(user1, testTopic1, clientID, 1)
tt3, err3 := files.CheckAcl(user1, testTopic2, clientID, 2)
tt4, err4 := files.CheckAcl(user1, testTopic2, clientID, 1)
So(err1, ShouldBeNil)
So(err2, ShouldBeNil)
So(err3, ShouldBeNil)
So(err4, ShouldBeNil)
So(tt1, ShouldBeTrue)
So(tt2, ShouldBeFalse)
So(tt3, ShouldBeFalse)
So(tt4, ShouldBeTrue)
})
Convey("User 1 should be able to subscribe or publish to a readwrite topic rule", func() {
tt1, err1 := files.CheckAcl(user1, readWriteTopic, clientID, 2)
tt2, err2 := files.CheckAcl(user1, readWriteTopic, clientID, 1)
So(err1, ShouldBeNil)
So(err2, ShouldBeNil)
So(tt1, ShouldBeTrue)
So(tt2, ShouldBeTrue)
})
Convey("User 2 should be able to read any test/topic/X but not any/other", func() {
tt1, err1 := files.CheckAcl(user2, testTopic1, clientID, 1)
tt2, err2 := files.CheckAcl(user2, testTopic2, clientID, 1)
tt3, err3 := files.CheckAcl(user2, testTopic3, clientID, 1)
So(err1, ShouldBeNil)
So(err2, ShouldBeNil)
So(err3, ShouldBeNil)
So(tt1, ShouldBeTrue)
So(tt2, ShouldBeTrue)
So(tt3, ShouldBeFalse)
})
Convey("User 3 should be able to read any test/X but not other/... nor test/denied\n\n", func() {
fmt.Printf("\n\nUser 3 acls: %#v", files.Users[user3].AclRecords)
tt1, err1 := files.CheckAcl(user3, testTopic1, clientID, 1)
tt2, err2 := files.CheckAcl(user3, testTopic2, clientID, 1)
tt3, err3 := files.CheckAcl(user3, testTopic3, clientID, 1)
tt4, err4 := files.CheckAcl(user3, testTopic4, clientID, 1)
tt5, err5 := files.CheckAcl(user3, deniedTopic, clientID, 1)
So(err1, ShouldBeNil)
So(err2, ShouldBeNil)
So(err3, ShouldBeNil)
So(err4, ShouldBeNil)
So(err5, ShouldBeNil)
So(tt1, ShouldBeTrue)
So(tt2, ShouldBeTrue)
So(tt3, ShouldBeTrue)
So(tt4, ShouldBeFalse)
So(tt5, ShouldBeFalse)
})
Convey("User 4 should not be able to read since it's not in the passwords file", func() {
tt1, err1 := files.CheckAcl(user4, testTopic1, clientID, 1)
So(err1, ShouldBeNil)
So(tt1, ShouldBeFalse)
})
Convey("Elton Bowie should be able to read and write to `test/space`, and only read from other topics", func() {
tt1, err1 := files.CheckAcl(elton, spaceTopic, clientID, 2)
tt2, err2 := files.CheckAcl(elton, multiSpaceTopic, clientID, 1)
tt3, err3 := files.CheckAcl(elton, multiSpaceTopic, clientID, 2)
tt4, err4 := files.CheckAcl(elton, lotsOfSpacesTopic, clientID, 1)
tt5, err5 := files.CheckAcl(elton, lotsOfSpacesTopic, clientID, 2)
So(err1, ShouldBeNil)
So(err2, ShouldBeNil)
So(err3, ShouldBeNil)
So(err4, ShouldBeNil)
So(err5, ShouldBeNil)
So(tt1, ShouldBeTrue)
So(tt2, ShouldBeTrue)
So(tt3, ShouldBeFalse)
So(tt4, ShouldBeTrue)
So(tt5, ShouldBeFalse)
})
//Now check against patterns.
Convey("Given a topic that mentions username, acl check should pass", func() {
tt1, err1 := files.CheckAcl(user1, "test/test1", clientID, 1)
So(err1, ShouldBeNil)
So(tt1, ShouldBeTrue)
tt2, err2 := files.CheckAcl(elton, "test/test with space", clientID, 1)
So(err2, ShouldBeNil)
So(tt2, ShouldBeTrue)
})
Convey("Given a topic that mentions clientid, acl check should pass", func() {
tt1, err1 := files.CheckAcl(user1, "test/test_client", clientID, 1)
So(err1, ShouldBeNil)
So(tt1, ShouldBeTrue)
})
//Halt files
files.Halt()
})
Convey("On SIGHUP files should be reloaded", t, func() {
pwFile, err := os.Create("../test-files/test-passwords")
So(err, ShouldBeNil)
aclFile, err := os.Create("../test-files/test-acls")
Convey("When passwords path is given, NewFiles should succeed", t, func() {
pwPath, err := filepath.Abs("../test-files/passwords")
So(err, ShouldBeNil)
defer os.Remove(pwFile.Name())
defer os.Remove(aclFile.Name())
authOpts["backends"] = "files"
authOpts["files_register"] = "user"
authOpts["files_password_path"] = pwPath
hasher := hashing.NewHasher(authOpts, "files")
_, err = NewFiles(authOpts, logLevel, hasher)
So(err, ShouldBeNil)
})
user1 := "test1"
user2 := "test2"
pw1, err := hasher.Hash(user1)
Convey("When Files is only registered to check acls and there are no rules for the tested user", t, func() {
aclPath, err := filepath.Abs("../test-files/acls-only")
So(err, ShouldBeNil)
pw2, err := hasher.Hash(user2)
authOpts["backends"] = "files"
authOpts["files_register"] = "acl"
authOpts["files_acl_path"] = aclPath
delete(authOpts, "files_password_path")
f, err := NewFiles(authOpts, logLevel, hasher)
So(err, ShouldBeNil)
pwFile.WriteString(fmt.Sprintf("\n%s:%s\n", user1, pw1))
granted, err := f.CheckAcl("some-user", "any/topic", "client-id", 1)
So(err, ShouldBeNil)
So(granted, ShouldBeTrue)
aclFile.WriteString("\nuser test1")
aclFile.WriteString("\ntopic read test/#")
granted, err = f.CheckAcl("test1", "any/topic", "client-id", 1)
So(err, ShouldBeNil)
So(granted, ShouldBeFalse)
})
pwFile.Sync()
aclFile.Sync()
authOpts["password_path"] = pwFile.Name()
authOpts["acl_path"] = aclFile.Name()
files, err := NewFiles(authOpts, log.DebugLevel, hasher)
Convey("With acls only test case", t, func() {
aclPath, err := filepath.Abs("../test-files/acls-read-only")
So(err, ShouldBeNil)
user, ok := files.Users[user1]
So(ok, ShouldBeTrue)
So(err, ShouldBeNil)
record := user.AclRecords[0]
So(record.Acc, ShouldEqual, MOSQ_ACL_READ)
So(record.Topic, ShouldEqual, "test/#")
authOpts["backends"] = "files"
authOpts["files_register"] = "acl"
authOpts["files_acl_path"] = aclPath
delete(authOpts, "files_password_path")
_, ok = files.Users[user2]
So(ok, ShouldBeFalse)
f, err := NewFiles(authOpts, logLevel, hasher)
So(err, ShouldBeNil)
// Now add second user and reload.
pwFile.WriteString(fmt.Sprintf("\n%s:%s\n", user2, pw2))
granted, err := f.CheckAcl("some-user", "clients/wrong-topic", "client-id", 1)
So(err, ShouldBeNil)
So(granted, ShouldBeFalse)
aclFile.WriteString("\nuser test2")
aclFile.WriteString("\ntopic write test/#")
granted, err = f.CheckAcl("some-user", "clients/wrong-topic", "client-id", 2)
So(err, ShouldBeNil)
So(granted, ShouldBeFalse)
files.signals <- syscall.SIGHUP
granted, err = f.CheckAcl("some-user", "clients/topic", "client-id", 2)
So(err, ShouldBeNil)
So(granted, ShouldBeFalse)
time.Sleep(200 * time.Millisecond)
granted, err = f.CheckAcl("some-user", "clients/topic", "client-id", 1)
So(err, ShouldBeNil)
So(granted, ShouldBeTrue)
user, ok = files.Users[user2]
So(ok, ShouldBeTrue)
record = user.AclRecords[0]
So(record.Acc, ShouldEqual, MOSQ_ACL_WRITE)
So(record.Topic, ShouldEqual, "test/#")
granted, err = f.CheckAcl("some-user", "clients/client-id", "client-id", 2)
So(err, ShouldBeNil)
So(granted, ShouldBeTrue)
})
}

View File

@ -9,6 +9,7 @@ import (
"strings"
"testing"
. "github.com/iegomez/mosquitto-go-auth/backends/constants"
log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"
)

View File

@ -39,6 +39,7 @@ const (
remoteMode = "remote"
localMode = "local"
jsMode = "js"
filesMode = "files"
)
func NewJWT(authOpts map[string]string, logLevel log.Level, hasher hashing.HashComparer) (*JWT, error) {
@ -83,6 +84,9 @@ func NewJWT(authOpts map[string]string, logLevel log.Level, hasher hashing.HashC
case remoteMode:
jwt.mode = remoteMode
checker, err = NewRemoteJWTChecker(authOpts, options)
case filesMode:
jwt.mode = filesMode
checker, err = NewFilesJWTChecker(authOpts, logLevel, hasher, options)
default:
err = errors.New("unknown JWT mode")
}

59
backends/jwt_files.go Normal file
View File

@ -0,0 +1,59 @@
package backends
import (
"github.com/iegomez/mosquitto-go-auth/backends/files"
"github.com/iegomez/mosquitto-go-auth/hashing"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
type filesJWTChecker struct {
checker *files.Checker
options tokenOptions
}
func NewFilesJWTChecker(authOpts map[string]string, logLevel log.Level, hasher hashing.HashComparer, options tokenOptions) (jwtChecker, error) {
log.SetLevel(logLevel)
/* We could ask for a file listing available users with no password, but that gives very little value
versus just assuming users in the ACL file are valid ones, while general rules apply to any user.
Thus, padswords file makes no sense for JWT, we only need to check ACLs.
*/
aclPath, ok := authOpts["jwt_acl_path"]
if !ok || aclPath == "" {
return nil, errors.New("missing acl file path")
}
var checker, err = files.NewChecker(authOpts["backends"], "", aclPath, logLevel, hasher)
if err != nil {
return nil, err
}
return &filesJWTChecker{
checker: checker,
options: options,
}, nil
}
func (o *filesJWTChecker) GetUser(token string) (bool, error) {
return false, nil
}
func (o *filesJWTChecker) GetSuperuser(token string) (bool, error) {
return false, nil
}
func (o *filesJWTChecker) CheckAcl(token, topic, clientid string, acc int32) (bool, error) {
username, err := getUsernameForToken(o.options, token, o.options.skipACLExpiration)
if err != nil {
log.Printf("jwt get user error: %s", err)
return false, err
}
return o.checker.CheckAcl(username, topic, clientid, acc)
}
func (o *filesJWTChecker) Halt() {
// NO-OP
}

View File

@ -5,12 +5,14 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
. "github.com/iegomez/mosquitto-go-auth/backends/constants"
"github.com/iegomez/mosquitto-go-auth/hashing"
log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"
@ -18,7 +20,7 @@ import (
var username = "test"
//Hash generated by the pw utility
// Hash generated by the pw utility
var userPassHash = "PBKDF2$sha512$100000$os24lcPr9cJt2QDVWssblQ==$BK1BQ2wbwU1zNxv3Ml3wLuu5//hPop3/LvaPYjjCwdBvnpwusnukJPpcXQzyyjOlZdieXTx6sXAcX4WnZRZZnw=="
var jwtSecret = "some_jwt_secret"
@ -55,6 +57,15 @@ var expiredToken = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"username": username,
})
var notPresentJwtToken = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": "jwt-test",
"aud": "jwt-test",
"nbf": nowSecondsSinceEpoch,
"exp": expSecondsSinceEpoch,
"sub": "user",
"username": "not_present",
})
var tkOptions = tokenOptions{
secret: jwtSecret,
userField: "Username",
@ -137,7 +148,7 @@ func TestJsJWTChecker(t *testing.T) {
aclResponse, err = checker.CheckAcl("incorrect", "test/topic", "id", 1)
So(err, ShouldBeNil)
So(userResponse, ShouldBeFalse)
So(aclResponse, ShouldBeFalse)
aclResponse, err = checker.CheckAcl("correct", "bad/topic", "id", 1)
So(err, ShouldBeNil)
@ -173,6 +184,58 @@ func TestJsJWTChecker(t *testing.T) {
})
}
func TestFilesJWTChecker(t *testing.T) {
// The bulk of files testing is done in the internal files checker.
// Neverthelss, we'll check that tokens are effectively parsed and correct usernames get the expected access.
authOpts := make(map[string]string)
logLevel := log.DebugLevel
hasher := hashing.NewHasher(authOpts, "files")
Convey("Given empty opts NewFilesJWTChecker should fail", t, func() {
_, err := NewFilesJWTChecker(authOpts, logLevel, hasher, tkOptions)
So(err, ShouldNotBeNil)
})
Convey("When files backend is set, missing acl path should make NewFilesJWTChecker fail", t, func() {
authOpts["backends"] = "files"
_, err := NewFilesJWTChecker(authOpts, logLevel, hasher, tkOptions)
So(err, ShouldNotBeNil)
})
Convey("When acl path is given, NewFilesJWTChecker should succeed", t, func() {
pwPath, err := filepath.Abs("../test-files/acls")
So(err, ShouldBeNil)
authOpts["backends"] = "files"
authOpts["jwt_acl_path"] = pwPath
filesChecker, err := NewFilesJWTChecker(authOpts, logLevel, hasher, tkOptions)
So(err, ShouldBeNil)
token, err := notPresentJwtToken.SignedString([]byte(jwtSecret))
So(err, ShouldBeNil)
Convey("Access should be granted for ACL mentioned users", func() {
tt, err := filesChecker.CheckAcl(token, "test/not_present", "id", 1)
So(err, ShouldBeNil)
So(tt, ShouldBeTrue)
})
Convey("Access should be granted for general ACL rules on non mentioned users", func() {
tt1, err1 := filesChecker.CheckAcl(token, "test/general", "id", 1)
tt2, err2 := filesChecker.CheckAcl(token, "test/general_denied", "id", 1)
So(err1, ShouldBeNil)
So(tt1, ShouldBeTrue)
So(err2, ShouldBeNil)
So(tt2, ShouldBeFalse)
})
})
}
func TestLocalPostgresJWT(t *testing.T) {
Convey("Creating a token should return a nil error", t, func() {

View File

@ -6,6 +6,8 @@ import (
"strings"
"time"
. "github.com/iegomez/mosquitto-go-auth/backends/constants"
"github.com/iegomez/mosquitto-go-auth/backends/topics"
"github.com/iegomez/mosquitto-go-auth/hashing"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
@ -199,7 +201,8 @@ func (o Mongo) CheckAcl(username, topic, clientid string, acc int32) (bool, erro
}
for _, acl := range user.Acls {
if (acl.Acc == acc || acl.Acc == 3) && TopicsMatch(acl.Topic, topic) {
// TODO: needs fixing since it's bypassing MOSQ_ACL_SUBSCRIBE.
if (acl.Acc == acc || acl.Acc == MOSQ_ACL_READWRITE) && topics.Match(acl.Topic, topic) {
return true, nil
}
}
@ -222,7 +225,7 @@ func (o Mongo) CheckAcl(username, topic, clientid string, acc int32) (bool, erro
if err == nil {
aclTopic := strings.Replace(acl.Topic, "%c", clientid, -1)
aclTopic = strings.Replace(aclTopic, "%u", username, -1)
if TopicsMatch(aclTopic, topic) {
if topics.Match(aclTopic, topic) {
return true, nil
}
} else {

View File

@ -4,6 +4,7 @@ import (
"context"
"testing"
. "github.com/iegomez/mosquitto-go-auth/backends/constants"
"github.com/iegomez/mosquitto-go-auth/hashing"
log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"

View File

@ -10,6 +10,7 @@ import (
"strings"
mq "github.com/go-sql-driver/mysql"
"github.com/iegomez/mosquitto-go-auth/backends/topics"
"github.com/iegomez/mosquitto-go-auth/hashing"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
@ -296,7 +297,7 @@ func (o Mysql) CheckAcl(username, topic, clientid string, acc int32) (bool, erro
for _, acl := range acls {
aclTopic := strings.Replace(acl, "%c", clientid, -1)
aclTopic = strings.Replace(aclTopic, "%u", username, -1)
if TopicsMatch(aclTopic, topic) {
if topics.Match(aclTopic, topic) {
return true, nil
}
}

View File

@ -3,6 +3,7 @@ package backends
import (
"testing"
. "github.com/iegomez/mosquitto-go-auth/backends/constants"
"github.com/iegomez/mosquitto-go-auth/hashing"
log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"

View File

@ -6,6 +6,7 @@ import (
"strconv"
"strings"
"github.com/iegomez/mosquitto-go-auth/backends/topics"
"github.com/iegomez/mosquitto-go-auth/hashing"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
@ -242,7 +243,7 @@ func (o Postgres) CheckAcl(username, topic, clientid string, acc int32) (bool, e
for _, acl := range acls {
aclTopic := strings.Replace(acl, "%c", clientid, -1)
aclTopic = strings.Replace(aclTopic, "%u", username, -1)
if TopicsMatch(aclTopic, topic) {
if topics.Match(aclTopic, topic) {
return true, nil
}
}

View File

@ -3,6 +3,7 @@ package backends
import (
"testing"
. "github.com/iegomez/mosquitto-go-auth/backends/constants"
"github.com/iegomez/mosquitto-go-auth/hashing"
log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"

View File

@ -9,6 +9,8 @@ import (
"time"
goredis "github.com/go-redis/redis/v8"
. "github.com/iegomez/mosquitto-go-auth/backends/constants"
"github.com/iegomez/mosquitto-go-auth/backends/topics"
"github.com/iegomez/mosquitto-go-auth/hashing"
log "github.com/sirupsen/logrus"
)
@ -347,7 +349,7 @@ func (o Redis) checkAcl(username, topic, clientid string, acc int32) (bool, erro
//Now loop through acls looking for a match.
for _, acl := range acls {
if TopicsMatch(acl, topic) {
if topics.Match(acl, topic) {
return true, nil
}
}
@ -355,7 +357,7 @@ func (o Redis) checkAcl(username, topic, clientid string, acc int32) (bool, erro
for _, acl := range commonAcls {
aclTopic := strings.Replace(acl, "%c", clientid, -1)
aclTopic = strings.Replace(aclTopic, "%u", username, -1)
if TopicsMatch(aclTopic, topic) {
if topics.Match(aclTopic, topic) {
return true, nil
}
}

View File

@ -4,6 +4,7 @@ import (
"context"
"testing"
. "github.com/iegomez/mosquitto-go-auth/backends/constants"
"github.com/iegomez/mosquitto-go-auth/hashing"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"

View File

@ -5,6 +5,7 @@ import (
"strconv"
"strings"
"github.com/iegomez/mosquitto-go-auth/backends/topics"
"github.com/iegomez/mosquitto-go-auth/hashing"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
@ -175,7 +176,7 @@ func (o Sqlite) CheckAcl(username, topic, clientid string, acc int32) (bool, err
for _, acl := range acls {
aclTopic := strings.Replace(acl, "%c", clientid, -1)
aclTopic = strings.Replace(aclTopic, "%u", username, -1)
if TopicsMatch(aclTopic, topic) {
if topics.Match(aclTopic, topic) {
return true, nil
}
}

View File

@ -4,6 +4,7 @@ import (
"os"
"testing"
. "github.com/iegomez/mosquitto-go-auth/backends/constants"
"github.com/iegomez/mosquitto-go-auth/hashing"
log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"

View File

@ -1,11 +1,15 @@
package backends
package topics
import "strings"
func TopicsMatch(savedTopic, givenTopic string) bool {
// Match tells if givenTopic matches savedTopic's pattern.
func Match(savedTopic, givenTopic string) bool {
return givenTopic == savedTopic || match(strings.Split(savedTopic, "/"), strings.Split(givenTopic, "/"))
}
// TODO: I've always trusted this function does the right thing,
// and it's kind of been proven by use and indirect testing of backends,
// but it should really have tests of its own.
func match(route []string, topic []string) bool {
switch {
case len(route) == 0:

View File

@ -4,8 +4,8 @@ auth_opt_log_level debug
auth_opt_backends files
auth_opt_check_prefix false
auth_opt_password_path /etc/mosquitto/auth/passwords
auth_opt_acl_path /etc/mosquitto/auth/acls
auth_opt_files_password_path /etc/mosquitto/auth/passwords
auth_opt_files_acl_path /etc/mosquitto/auth/acls
auth_opt_cache_host redis
auth_opt_cache true

2
test-files/acls-only Normal file
View File

@ -0,0 +1,2 @@
user some-user
topic #

View File

@ -0,0 +1,4 @@
user some-user
topic read clients/topic
pattern write clients/%c