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:
parent
3eea16872d
commit
ee6e68db3a
4
Makefile
4
Makefile
|
@ -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
|
||||
|
|
32
README.md
32
README.md
|
@ -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`.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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/#")
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
user some-user
|
||||
topic #
|
|
@ -0,0 +1,4 @@
|
|||
user some-user
|
||||
topic read clients/topic
|
||||
|
||||
pattern write clients/%c
|
Loading…
Reference in New Issue