513 lines
15 KiB
Go
513 lines
15 KiB
Go
package backends
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/iegomez/mosquitto-go-auth/hashing"
|
|
"github.com/pkg/errors"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type Backend interface {
|
|
GetUser(username, password, clientid string) (bool, error)
|
|
GetSuperuser(username string) (bool, error)
|
|
CheckAcl(username, topic, clientId string, acc int32) (bool, error)
|
|
GetName() string
|
|
Halt()
|
|
}
|
|
|
|
type Backends struct {
|
|
backends map[string]Backend
|
|
|
|
aclCheckers []string
|
|
userCheckers []string
|
|
superuserCheckers []string
|
|
|
|
checkPrefix bool
|
|
stripPrefix bool
|
|
prefixes map[string]string
|
|
|
|
disableSuperuser bool
|
|
}
|
|
|
|
const (
|
|
// backends
|
|
postgresBackend = "postgres"
|
|
jwtBackend = "jwt"
|
|
redisBackend = "redis"
|
|
httpBackend = "http"
|
|
filesBackend = "files"
|
|
mysqlBackend = "mysql"
|
|
sqliteBackend = "sqlite"
|
|
mongoBackend = "mongo"
|
|
pluginBackend = "plugin"
|
|
grpcBackend = "grpc"
|
|
jsBackend = "js"
|
|
|
|
// checks
|
|
aclCheck = "acl"
|
|
userCheck = "user"
|
|
superuserCheck = "superuser"
|
|
|
|
// other constants
|
|
defaultUserAgent = "mosquitto"
|
|
)
|
|
|
|
// AllowedBackendsOptsPrefix serves as a check for allowed backends and a map from backend to expected opts prefix.
|
|
var allowedBackendsOptsPrefix = map[string]string{
|
|
postgresBackend: "pg",
|
|
jwtBackend: "jwt",
|
|
redisBackend: "redis",
|
|
httpBackend: "http",
|
|
filesBackend: "files",
|
|
mysqlBackend: "mysql",
|
|
sqliteBackend: "sqlite",
|
|
mongoBackend: "mongo",
|
|
pluginBackend: "plugin",
|
|
grpcBackend: "grpc",
|
|
jsBackend: "js",
|
|
}
|
|
|
|
// Initialize sets general options, tries to build the backends and register their checkers.
|
|
func Initialize(authOpts map[string]string, logLevel log.Level, version string) (*Backends, error) {
|
|
|
|
b := &Backends{
|
|
backends: make(map[string]Backend),
|
|
aclCheckers: make([]string, 0),
|
|
userCheckers: make([]string, 0),
|
|
superuserCheckers: make([]string, 0),
|
|
prefixes: make(map[string]string),
|
|
}
|
|
|
|
//Disable superusers for all backends if option is set.
|
|
if authOpts["disable_superuser"] == "true" {
|
|
b.disableSuperuser = true
|
|
|
|
}
|
|
|
|
backendsOpt, ok := authOpts["backends"]
|
|
if !ok || backendsOpt == "" {
|
|
return nil, fmt.Errorf("missing or blank option backends")
|
|
}
|
|
|
|
backends := strings.Split(strings.Replace(backendsOpt, " ", "", -1), ",")
|
|
if len(backends) < 1 {
|
|
return nil, fmt.Errorf("missing or blank option backends")
|
|
}
|
|
|
|
for _, backend := range backends {
|
|
if _, ok := allowedBackendsOptsPrefix[backend]; !ok {
|
|
return nil, fmt.Errorf("unknown backend %s", backend)
|
|
}
|
|
}
|
|
|
|
err := b.addBackends(authOpts, logLevel, backends, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = b.setCheckers(authOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b.setPrefixes(authOpts, backends)
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func (b *Backends) addBackends(authOpts map[string]string, logLevel log.Level, backends []string, version string) error {
|
|
for _, bename := range backends {
|
|
var beIface Backend
|
|
var err error
|
|
|
|
hasher := hashing.NewHasher(authOpts, allowedBackendsOptsPrefix[bename])
|
|
switch bename {
|
|
case postgresBackend:
|
|
beIface, err = NewPostgres(authOpts, logLevel, hasher)
|
|
if err != nil {
|
|
log.Fatalf("backend register error: couldn't initialize %s backend with error %s.", bename, err)
|
|
} else {
|
|
log.Infof("backend registered: %s", beIface.GetName())
|
|
b.backends[postgresBackend] = beIface.(Postgres)
|
|
}
|
|
case jwtBackend:
|
|
beIface, err = NewJWT(authOpts, logLevel, hasher, version)
|
|
if err != nil {
|
|
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, err)
|
|
} else {
|
|
log.Infof("Backend registered: %s", beIface.GetName())
|
|
b.backends[jwtBackend] = beIface.(*JWT)
|
|
}
|
|
case filesBackend:
|
|
beIface, err = NewFiles(authOpts, logLevel, hasher)
|
|
if err != nil {
|
|
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, err)
|
|
} else {
|
|
log.Infof("Backend registered: %s", beIface.GetName())
|
|
b.backends[filesBackend] = beIface.(*Files)
|
|
}
|
|
case redisBackend:
|
|
beIface, err = NewRedis(authOpts, logLevel, hasher)
|
|
if err != nil {
|
|
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, err)
|
|
} else {
|
|
log.Infof("Backend registered: %s", beIface.GetName())
|
|
b.backends[redisBackend] = beIface.(Redis)
|
|
}
|
|
case mysqlBackend:
|
|
beIface, err = NewMysql(authOpts, logLevel, hasher)
|
|
if err != nil {
|
|
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, err)
|
|
} else {
|
|
log.Infof("Backend registered: %s", beIface.GetName())
|
|
b.backends[mysqlBackend] = beIface.(Mysql)
|
|
}
|
|
case httpBackend:
|
|
beIface, err = NewHTTP(authOpts, logLevel, version)
|
|
if err != nil {
|
|
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, err)
|
|
} else {
|
|
log.Infof("Backend registered: %s", beIface.GetName())
|
|
b.backends[httpBackend] = beIface.(HTTP)
|
|
}
|
|
case sqliteBackend:
|
|
beIface, err = NewSqlite(authOpts, logLevel, hasher)
|
|
if err != nil {
|
|
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, err)
|
|
} else {
|
|
log.Infof("Backend registered: %s", beIface.GetName())
|
|
b.backends[sqliteBackend] = beIface.(Sqlite)
|
|
}
|
|
case mongoBackend:
|
|
beIface, err = NewMongo(authOpts, logLevel, hasher)
|
|
if err != nil {
|
|
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, err)
|
|
} else {
|
|
log.Infof("Backend registered: %s", beIface.GetName())
|
|
b.backends[mongoBackend] = beIface.(Mongo)
|
|
}
|
|
case grpcBackend:
|
|
beIface, err = NewGRPC(authOpts, logLevel)
|
|
if err != nil {
|
|
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, err)
|
|
} else {
|
|
log.Infof("Backend registered: %s", beIface.GetName())
|
|
b.backends[grpcBackend] = beIface.(*GRPC)
|
|
}
|
|
case jsBackend:
|
|
beIface, err = NewJavascript(authOpts, logLevel)
|
|
if err != nil {
|
|
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, err)
|
|
} else {
|
|
log.Infof("Backend registered: %s", beIface.GetName())
|
|
b.backends[jsBackend] = beIface.(*Javascript)
|
|
}
|
|
case pluginBackend:
|
|
beIface, err = NewCustomPlugin(authOpts, logLevel)
|
|
if err != nil {
|
|
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, err)
|
|
} else {
|
|
log.Infof("Backend registered: %s", beIface.GetName())
|
|
b.backends[pluginBackend] = beIface.(*CustomPlugin)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unkown backend %s", bename)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Backends) setCheckers(authOpts map[string]string) error {
|
|
// We'll register which plugins will perform checks for user, superuser and acls.
|
|
// At least one backend must be registered for user and acl checks.
|
|
// When option auth_opt_backend_register is missing for the backend, we register all checks.
|
|
for name := range b.backends {
|
|
opt := fmt.Sprintf("%s_register", allowedBackendsOptsPrefix[name])
|
|
options, ok := authOpts[opt]
|
|
|
|
if ok {
|
|
checkers := strings.Split(strings.Replace(options, " ", "", -1), ",")
|
|
for _, check := range checkers {
|
|
switch check {
|
|
case aclCheck:
|
|
b.aclCheckers = append(b.aclCheckers, name)
|
|
log.Infof("registered acl checker: %s", name)
|
|
case userCheck:
|
|
b.userCheckers = append(b.userCheckers, name)
|
|
log.Infof("registered user checker: %s", name)
|
|
case superuserCheck:
|
|
if !b.disableSuperuser {
|
|
b.superuserCheckers = append(b.superuserCheckers, name)
|
|
log.Infof("registered superuser checker: %s", name)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported check %s found for backend %s", check, name)
|
|
}
|
|
}
|
|
} else {
|
|
b.aclCheckers = append(b.aclCheckers, name)
|
|
log.Infof("registered acl checker: %s", name)
|
|
b.userCheckers = append(b.userCheckers, name)
|
|
log.Infof("registered user checker: %s", name)
|
|
|
|
if !b.disableSuperuser {
|
|
b.superuserCheckers = append(b.superuserCheckers, name)
|
|
log.Infof("registered superuser checker: %s", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(b.userCheckers) == 0 && len(b.aclCheckers) == 0 {
|
|
return errors.New("no backends registered")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setPrefixes sets options for prefixes handling.
|
|
func (b *Backends) setPrefixes(authOpts map[string]string, backends []string) {
|
|
checkPrefix, ok := authOpts["check_prefix"]
|
|
|
|
if !ok || strings.Replace(checkPrefix, " ", "", -1) != "true" {
|
|
b.checkPrefix = false
|
|
b.stripPrefix = false
|
|
|
|
return
|
|
}
|
|
|
|
prefixesStr, ok := authOpts["prefixes"]
|
|
|
|
if !ok {
|
|
log.Warn("Error: prefixes enabled but no options given, defaulting to prefixes disabled.")
|
|
b.checkPrefix = false
|
|
b.stripPrefix = 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
|
|
b.stripPrefix = false
|
|
|
|
return
|
|
}
|
|
|
|
if authOpts["strip_prefix"] == "true" {
|
|
b.stripPrefix = true
|
|
}
|
|
|
|
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.
|
|
func (b *Backends) lookupPrefix(username string) (bool, string) {
|
|
if strings.Index(username, "_") > 0 {
|
|
userPrefix := username[0:strings.Index(username, "_")]
|
|
if prefix, ok := b.prefixes[userPrefix]; ok {
|
|
log.Debugf("Found prefix for user %s, using backend %s.", username, prefix)
|
|
return true, prefix
|
|
}
|
|
}
|
|
return false, ""
|
|
}
|
|
|
|
// getPrefixForBackend retrieves the user provided prefix for a given backend.
|
|
func (b *Backends) getPrefixForBackend(backend string) string {
|
|
for k, v := range b.prefixes {
|
|
if v == backend {
|
|
return k
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func checkRegistered(bename string, checkers []string) bool {
|
|
for _, b := range checkers {
|
|
if b == bename {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// AuthUnpwdCheck checks user authentication.
|
|
func (b *Backends) AuthUnpwdCheck(username, password, clientid string) (bool, error) {
|
|
var authenticated bool
|
|
var err error
|
|
|
|
// If prefixes are enabled, check if username has a valid prefix and use the correct backend if so.
|
|
if !b.checkPrefix {
|
|
return b.checkAuth(username, password, clientid)
|
|
}
|
|
|
|
validPrefix, bename := b.lookupPrefix(username)
|
|
|
|
if !validPrefix {
|
|
return b.checkAuth(username, password, clientid)
|
|
}
|
|
|
|
if !checkRegistered(bename, b.userCheckers) {
|
|
return false, fmt.Errorf("backend %s not registered to check users", bename)
|
|
}
|
|
|
|
// If the backend is JWT and the token was prefixed, then strip the token.
|
|
// If the token was passed without a prefix it will be handled in the common case.
|
|
// Also strip the prefix if the strip_prefix option was set.
|
|
if bename == jwtBackend || b.stripPrefix {
|
|
prefix := b.getPrefixForBackend(bename)
|
|
username = strings.TrimPrefix(username, prefix+"_")
|
|
}
|
|
var backend = b.backends[bename]
|
|
|
|
authenticated, err = backend.GetUser(username, password, clientid)
|
|
if authenticated && err == nil {
|
|
log.Debugf("user %s authenticated with backend %s", username, backend.GetName())
|
|
}
|
|
|
|
return authenticated, err
|
|
}
|
|
|
|
func (b *Backends) checkAuth(username, password, clientid string) (bool, error) {
|
|
var err error
|
|
authenticated := false
|
|
|
|
for _, bename := range b.userCheckers {
|
|
var backend = b.backends[bename]
|
|
|
|
log.Debugf("checking user %s with backend %s", username, backend.GetName())
|
|
|
|
if ok, getUserErr := backend.GetUser(username, password, clientid); ok && getUserErr == nil {
|
|
authenticated = true
|
|
log.Debugf("user %s authenticated with backend %s", username, backend.GetName())
|
|
break
|
|
} else if getUserErr != nil && err == nil {
|
|
err = getUserErr
|
|
}
|
|
}
|
|
|
|
// If authenticated is true, it means at least one backend didn't fail and
|
|
// accepted the user. If so, honor the backend and clear the error.
|
|
if authenticated {
|
|
err = nil
|
|
}
|
|
|
|
return authenticated, err
|
|
}
|
|
|
|
// AuthAclCheck checks user/topic/acc authorization.
|
|
func (b *Backends) AuthAclCheck(clientid, username, topic string, acc int) (bool, error) {
|
|
var aclCheck bool
|
|
var err error
|
|
|
|
// If prefixes are enabled, check if username has a valid prefix and use the correct backend if so.
|
|
// Else, check all backends.
|
|
if !b.checkPrefix {
|
|
return b.checkAcl(username, topic, clientid, acc)
|
|
}
|
|
|
|
validPrefix, bename := b.lookupPrefix(username)
|
|
|
|
if !validPrefix {
|
|
return b.checkAcl(username, topic, clientid, acc)
|
|
}
|
|
|
|
// If the backend is JWT and the token was prefixed, then strip the token.
|
|
// If the token was passed without a prefix then let it be handled in the common case.
|
|
// Also strip the prefix if the strip_prefix option was set.
|
|
if bename == jwtBackend || b.stripPrefix {
|
|
prefix := b.getPrefixForBackend(bename)
|
|
username = strings.TrimPrefix(username, prefix+"_")
|
|
}
|
|
var backend = b.backends[bename]
|
|
|
|
// Short circuit checks when superusers are disabled.
|
|
if !b.disableSuperuser && checkRegistered(bename, b.superuserCheckers) {
|
|
log.Debugf("Superuser check with backend %s", backend.GetName())
|
|
|
|
aclCheck, err = backend.GetSuperuser(username)
|
|
|
|
if aclCheck && err == nil {
|
|
log.Debugf("superuser %s acl authenticated with backend %s", username, backend.GetName())
|
|
}
|
|
}
|
|
// If not superuser, check acl.
|
|
if !aclCheck {
|
|
if !checkRegistered(bename, b.aclCheckers) {
|
|
return false, fmt.Errorf("backend %s not registered to check acls", bename)
|
|
}
|
|
|
|
log.Debugf("Acl check with backend %s", backend.GetName())
|
|
if ok, checkACLErr := backend.CheckAcl(username, topic, clientid, int32(acc)); ok && checkACLErr == nil {
|
|
aclCheck = true
|
|
log.Debugf("user %s acl authenticated with backend %s", username, backend.GetName())
|
|
} else if checkACLErr != nil && err == nil {
|
|
err = checkACLErr
|
|
}
|
|
}
|
|
|
|
log.Debugf("Acl is %t for user %s", aclCheck, username)
|
|
return aclCheck, err
|
|
}
|
|
|
|
func (b *Backends) checkAcl(username, topic, clientid string, acc int) (bool, error) {
|
|
// Check superusers first
|
|
var err error
|
|
aclCheck := false
|
|
if !b.disableSuperuser {
|
|
for _, bename := range b.superuserCheckers {
|
|
var backend = b.backends[bename]
|
|
|
|
log.Debugf("Superuser check with backend %s", backend.GetName())
|
|
if ok, getSuperuserErr := backend.GetSuperuser(username); ok && getSuperuserErr == nil {
|
|
log.Debugf("superuser %s acl authenticated with backend %s", username, backend.GetName())
|
|
aclCheck = true
|
|
break
|
|
} else if getSuperuserErr != nil && err == nil {
|
|
err = getSuperuserErr
|
|
}
|
|
}
|
|
}
|
|
|
|
if !aclCheck {
|
|
for _, bename := range b.aclCheckers {
|
|
var backend = b.backends[bename]
|
|
|
|
log.Debugf("Acl check with backend %s", backend.GetName())
|
|
if ok, checkACLErr := backend.CheckAcl(username, topic, clientid, int32(acc)); ok && checkACLErr == nil {
|
|
log.Debugf("user %s acl authenticated with backend %s", username, backend.GetName())
|
|
aclCheck = true
|
|
break
|
|
} else if checkACLErr != nil && err == nil {
|
|
err = checkACLErr
|
|
}
|
|
}
|
|
}
|
|
|
|
// If aclCheck is true, it means at least one backend didn't fail and
|
|
// accepted the access. In this case trust this backend and clear the error.
|
|
if aclCheck {
|
|
err = nil
|
|
}
|
|
|
|
return aclCheck, err
|
|
}
|
|
|
|
func (b *Backends) Halt() {
|
|
// Halt every registered backend.
|
|
for _, v := range b.backends {
|
|
v.Halt()
|
|
}
|
|
}
|