mosquitto-go-auth/go-auth.go

768 lines
21 KiB
Go

package main
import "C"
import (
"fmt"
"os"
"strconv"
"strings"
"time"
log "github.com/sirupsen/logrus"
b64 "encoding/base64"
"plugin"
goredis "github.com/go-redis/redis"
bes "github.com/iegomez/mosquitto-go-auth/backends"
)
type Backend interface {
GetUser(username, password, clientid string) bool
GetSuperuser(username string) bool
CheckAcl(username, topic, clientId string, acc int32) bool
GetName() string
Halt()
}
type CommonData struct {
Backends map[string]Backend
Plugin *plugin.Plugin
PInit func(map[string]string, log.Level) error
PGetName func() string
PGetUser func(username, password string) bool
PGetSuperuser func(username string) bool
PCheckAcl func(username, topic, clientid string, acc int) bool
PHalt func()
Superusers []string
AclCacheSeconds int64
AuthCacheSeconds int64
UseCache bool
RedisCache *goredis.Client
CheckPrefix bool
Prefixes map[string]string
LogLevel log.Level
LogDest string
LogFile string
}
//Cache stores necessary values for Redis cache
type Cache struct {
Host string
Port string
Password string
DB int32
}
var allowedBackends = map[string]bool{
"postgres": true,
"jwt": true,
"redis": true,
"http": true,
"files": true,
"mysql": true,
"sqlite": true,
"mongo": true,
"plugin": true,
"grpc": true,
}
var backends []string //List of selected backends.
var authOpts map[string]string //Options passed by mosquitto.
var cache Cache //Cache conf.
var commonData CommonData //General struct with options and conf.
//export AuthPluginInit
func AuthPluginInit(keys []string, values []string, authOptsNum int) {
//Initialize Cache with default values
cache = Cache{
Host: "localhost",
Port: "6379",
Password: "",
DB: 3,
}
log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
})
superusers := make([]string, 10, 10)
cmbackends := make(map[string]Backend)
//Initialize common struct with default and given values
commonData = CommonData{
Superusers: superusers,
AclCacheSeconds: 30,
AuthCacheSeconds: 30,
CheckPrefix: false,
Prefixes: make(map[string]string),
LogLevel: log.InfoLevel,
}
//First, get backends
backendsOk := false
authOpts = make(map[string]string)
for i := 0; i < authOptsNum; i++ {
if keys[i] == "backends" {
backends = strings.Split(strings.Replace(values[i], " ", "", -1), ",")
if len(backends) > 0 {
backendsCheck := true
for _, backend := range backends {
if _, ok := allowedBackends[backend]; !ok {
backendsCheck = false
log.Errorf("backend not allowed: %s", backend)
}
}
backendsOk = backendsCheck
}
} else {
authOpts[keys[i]] = values[i]
}
}
//Log and end program if backends are wrong
if !backendsOk {
log.Fatal("\nbackends error\n")
}
//Check if log level is given. Set level if any valid option is given.
if logLevel, ok := authOpts["log_level"]; ok {
logLevel = strings.Replace(logLevel, " ", "", -1)
switch logLevel {
case "debug":
commonData.LogLevel = log.DebugLevel
case "info":
commonData.LogLevel = log.InfoLevel
case "warn":
commonData.LogLevel = log.WarnLevel
case "error":
commonData.LogLevel = log.ErrorLevel
case "fatal":
commonData.LogLevel = log.FatalLevel
case "panic":
commonData.LogLevel = log.PanicLevel
default:
log.Info("log_level unkwown, using default info level")
}
}
if logDest, ok := authOpts["log_dest"]; ok {
switch logDest {
case "stdout":
log.SetOutput(os.Stdout)
case "file":
if logFile, ok := authOpts["log_file"]; ok {
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err == nil {
log.SetOutput(file)
} else {
log.Errorf("failed to log to file, using default stderr: %s", err)
}
}
default:
log.Info("log_dest unknown, using default stderr")
}
}
//Initialize backends
for _, bename := range backends {
var beIface Backend
var bErr error
if bename == "plugin" {
plug, plErr := plugin.Open(authOpts["plugin_path"])
if plErr != nil {
log.Errorf("Could not init custom plugin: %s", plErr)
commonData.Plugin = nil
} else {
commonData.Plugin = plug
plInit, piErr := commonData.Plugin.Lookup("Init")
if piErr != nil {
log.Errorf("Couldn't find func Init in plugin: %s", plErr)
commonData.Plugin = nil
continue
}
initFunc := plInit.(func(authOpts map[string]string, logLevel log.Level) error)
ipErr := initFunc(authOpts, commonData.LogLevel)
if ipErr != nil {
log.Errorf("Couldn't init plugin: %s", ipErr)
commonData.Plugin = nil
continue
}
commonData.PInit = initFunc
plName, gErr := commonData.Plugin.Lookup("GetName")
if gErr != nil {
log.Errorf("Couldn't find func GetName in plugin: %s", gErr)
commonData.Plugin = nil
continue
}
nameFunc := plName.(func() string)
commonData.PGetName = nameFunc
plGetUser, pgErr := commonData.Plugin.Lookup("GetUser")
if pgErr != nil {
log.Errorf("Couldn't find func GetUser in plugin: %s", pgErr)
commonData.Plugin = nil
continue
}
getUserFunc := plGetUser.(func(username, password string) bool)
commonData.PGetUser = getUserFunc
if pgErr != nil {
log.Errorf("Couldn't find func GetUser in plugin: %s", pgErr)
commonData.Plugin = nil
continue
}
plGetSuperuser, psErr := commonData.Plugin.Lookup("GetSuperuser")
if psErr != nil {
log.Errorf("Couldn't find func GetSuperuser in plugin: %s", psErr)
commonData.Plugin = nil
continue
}
getSuperuserFunc := plGetSuperuser.(func(username string) bool)
commonData.PGetSuperuser = getSuperuserFunc
plCheckAcl, pcErr := commonData.Plugin.Lookup("CheckAcl")
if pcErr != nil {
log.Errorf("Couldn't find func CheckAcl in plugin: %s", pcErr)
commonData.Plugin = nil
continue
}
checkAclFunc := plCheckAcl.(func(username, topic, clientid string, acc int) bool)
commonData.PCheckAcl = checkAclFunc
plHalt, phErr := commonData.Plugin.Lookup("Halt")
if phErr != nil {
log.Errorf("Couldn't find func Halt in plugin: %s", phErr)
commonData.Plugin = nil
continue
}
haltFunc := plHalt.(func())
commonData.PHalt = haltFunc
log.Infof("Backend registered: %s", commonData.PGetName())
}
} else {
switch bename {
case "postgres":
beIface, bErr = bes.NewPostgres(authOpts, commonData.LogLevel)
if bErr != nil {
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, bErr)
} else {
log.Infof("Backend registered: %s", beIface.GetName())
cmbackends["postgres"] = beIface.(bes.Postgres)
}
case "jwt":
beIface, bErr = bes.NewJWT(authOpts, commonData.LogLevel)
if bErr != nil {
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, bErr)
} else {
log.Infof("Backend registered: %s", beIface.GetName())
cmbackends["jwt"] = beIface.(bes.JWT)
}
case "files":
beIface, bErr = bes.NewFiles(authOpts, commonData.LogLevel)
if bErr != nil {
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, bErr)
} else {
log.Infof("Backend registered: %s", beIface.GetName())
cmbackends["files"] = beIface.(bes.Files)
}
case "redis":
beIface, bErr = bes.NewRedis(authOpts, commonData.LogLevel)
if bErr != nil {
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, bErr)
} else {
log.Infof("Backend registered: %s", beIface.GetName())
cmbackends["redis"] = beIface.(bes.Redis)
}
case "mysql":
beIface, bErr = bes.NewMysql(authOpts, commonData.LogLevel)
if bErr != nil {
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, bErr)
} else {
log.Infof("Backend registered: %s", beIface.GetName())
cmbackends["mysql"] = beIface.(bes.Mysql)
}
case "http":
beIface, bErr = bes.NewHTTP(authOpts, commonData.LogLevel)
if bErr != nil {
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, bErr)
} else {
log.Infof("Backend registered: %s", beIface.GetName())
cmbackends["http"] = beIface.(bes.HTTP)
}
case "sqlite":
beIface, bErr = bes.NewSqlite(authOpts, commonData.LogLevel)
if bErr != nil {
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, bErr)
} else {
log.Infof("Backend registered: %s", beIface.GetName())
cmbackends["sqlite"] = beIface.(bes.Sqlite)
}
case "mongo":
beIface, bErr = bes.NewMongo(authOpts, commonData.LogLevel)
if bErr != nil {
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, bErr)
} else {
log.Infof("Backend registered: %s", beIface.GetName())
cmbackends["mongo"] = beIface.(bes.Mongo)
}
case "grpc":
beIface, bErr = bes.NewGRPC(authOpts, commonData.LogLevel)
if bErr != nil {
log.Fatalf("Backend register error: couldn't initialize %s backend with error %s.", bename, bErr)
} else {
log.Infof("Backend registered: %s", beIface.GetName())
cmbackends["grpc"] = beIface.(bes.GRPC)
}
}
}
}
if cache, ok := authOpts["cache"]; ok && strings.Replace(cache, " ", "", -1) == "true" {
log.Info("Cache activated")
commonData.UseCache = true
} else {
log.Info("No cache set.")
commonData.UseCache = false
}
if commonData.UseCache {
if cacheHost, ok := authOpts["cache_host"]; ok {
cache.Host = cacheHost
}
if cachePort, ok := authOpts["cache_port"]; ok {
cache.Port = cachePort
}
if cachePassword, ok := authOpts["cache_password"]; ok {
cache.Password = cachePassword
}
if cacheDB, ok := authOpts["cache_db"]; ok {
db, err := strconv.ParseInt(cacheDB, 10, 32)
if err == nil {
cache.DB = int32(db)
} else {
log.Warningf("couldn't parse cache db (err: %s), defaulting to %d", err, cache.DB)
}
}
if authCacheSec, ok := authOpts["auth_cache_seconds"]; ok {
authSec, err := strconv.ParseInt(authCacheSec, 10, 64)
if err == nil {
commonData.AuthCacheSeconds = authSec
} else {
log.Warningf("couldn't parse AuthCacheSeconds (err: %s), defaulting to %d", err, commonData.AuthCacheSeconds)
}
}
if aclCacheSec, ok := authOpts["acl_cache_seconds"]; ok {
aclSec, err := strconv.ParseInt(aclCacheSec, 10, 64)
if err == nil {
commonData.AclCacheSeconds = aclSec
} else {
log.Warningf("couldn't parse AclCacheSeconds (err: %s), defaulting to %d", err, commonData.AclCacheSeconds)
}
}
addr := fmt.Sprintf("%s:%s", cache.Host, cache.Port)
//If cache is on, try to start redis.
goredisClient := goredis.NewClient(&goredis.Options{
Addr: addr,
Password: cache.Password, // no password set
DB: int(cache.DB), // use default DB
})
_, err := goredisClient.Ping().Result()
if err != nil {
log.Errorf("couldn't start Redis, defaulting to no cache. error: %s", err)
commonData.UseCache = false
} else {
commonData.RedisCache = goredisClient
log.Infof("started cache redis client on DB %d", cache.DB)
//Check if cache must be reset
if cacheReset, ok := authOpts["cache_reset"]; ok && cacheReset == "true" {
commonData.RedisCache.FlushDB()
log.Infof("flushed cache")
}
}
}
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
for i, backend := range backends {
commonData.Prefixes[prefixes[i]] = backend
}
log.Infof("Prefixes enabled for backends %s with prefixes %s.", authOpts["backends"], authOpts["prefixes"])
commonData.CheckPrefix = true
} else {
log.Errorf("Error: got %d backends and %d prefixes, defaulting to prefixes disabled.", len(backends), len(prefixes))
commonData.CheckPrefix = false
}
} else {
log.Warn("Error: prefixes enabled but no options given, defaulting to prefixes disabled.")
commonData.CheckPrefix = false
}
} else {
commonData.CheckPrefix = false
}
commonData.Backends = cmbackends
}
//export AuthUnpwdCheck
func AuthUnpwdCheck(username, password, clientid string) bool {
authenticated := false
var cached = false
var granted = false
if commonData.UseCache {
log.Debugf("checking auth cache for %s", username)
cached, granted = CheckAuthCache(username, password)
if cached {
log.Debugf("found in cache: %s", username)
return granted
}
}
//If prefixes are enabled, checkt if username has a valid prefix and use the correct backend if so.
if commonData.CheckPrefix {
validPrefix, bename := CheckPrefix(username)
if validPrefix {
if bename == "plugin" {
authenticated = CheckPluginAuth(username, password, clientid)
} else {
var backend = commonData.Backends[bename]
if backend.GetUser(username, password, clientid) {
authenticated = true
log.Debugf("user %s authenticated with backend %s", username, backend.GetName())
}
}
} else {
//If there's no valid prefix, check all backends.
authenticated = CheckBackendsAuth(username, password, clientid)
//If not authenticated, check for a present plugin
if !authenticated {
authenticated = CheckPluginAuth(username, password, clientid)
}
}
} else {
authenticated = CheckBackendsAuth(username, password, clientid)
//If not authenticated, check for a present plugin
if !authenticated {
authenticated = CheckPluginAuth(username, password, clientid)
}
}
if commonData.UseCache {
authGranted := "false"
if authenticated {
authGranted = "true"
}
log.Debugf("setting auth cache for %s", username)
SetAuthCache(username, password, authGranted)
}
return authenticated
}
//export AuthAclCheck
func AuthAclCheck(clientid, username, topic string, acc int) bool {
aclCheck := false
var cached = false
var granted = false
if commonData.UseCache {
log.Debugf("checking acl cache for %s", username)
cached, granted = CheckAclCache(username, topic, clientid, acc)
if cached {
log.Debugf("found in cache: %s", username)
return granted
}
}
//If prefixes are enabled, checkt if username has a valid prefix and use the correct backend if so.
//Else, check all backends.
if commonData.CheckPrefix {
validPrefix, bename := CheckPrefix(username)
if validPrefix {
if bename == "plugin" {
aclCheck = CheckPluginAcl(username, topic, clientid, acc)
} else {
var backend = commonData.Backends[bename]
log.Debugf("Superuser check with backend %s", backend.GetName())
if backend.GetSuperuser(username) {
log.Debugf("superuser %s acl authenticated with backend %s", username, backend.GetName())
aclCheck = true
}
//If not superuser, check acl.
if !aclCheck {
log.Debugf("Acl check with backend %s", backend.GetName())
if backend.CheckAcl(username, topic, clientid, int32(acc)) {
log.Debugf("user %s acl authenticated with backend %s", username, backend.GetName())
aclCheck = true
}
}
}
} else {
//If there's no valid prefix, check all backends.
aclCheck = CheckBackendsAcl(username, topic, clientid, acc)
//If acl hasn't passed, check for plugin.
if !aclCheck {
aclCheck = CheckPluginAcl(username, topic, clientid, acc)
}
}
} else {
aclCheck = CheckBackendsAcl(username, topic, clientid, acc)
//If acl hasn't passed, check for plugin.
if !aclCheck {
aclCheck = CheckPluginAcl(username, topic, clientid, acc)
}
}
if commonData.UseCache {
authGranted := "false"
if aclCheck {
authGranted = "true"
}
log.Debugf("setting acl cache (granted = %s) for %s", authGranted, username)
SetAclCache(username, topic, clientid, acc, authGranted)
}
log.Debugf("Acl is %t for user %s", aclCheck, username)
return aclCheck
}
//export AuthPskKeyGet
func AuthPskKeyGet() bool {
return true
}
//CheckAuthCache checks if the username/password pair is present in the cache. Return if it's present and, if so, if it was granted privileges.
func CheckAuthCache(username, password string) (bool, bool) {
pair := b64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("auth%s%s", username, password)))
val, err := commonData.RedisCache.Get(pair).Result()
if err != nil {
return false, false
}
//refresh expiration
commonData.RedisCache.Expire(pair, time.Duration(commonData.AuthCacheSeconds)*time.Second)
if val == "true" {
return true, true
}
return true, false
}
//SetAuthCache sets a pair, granted option and expiration time.
func SetAuthCache(username, password string, granted string) error {
pair := b64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("auth%s%s", username, password)))
err := commonData.RedisCache.Set(pair, granted, time.Duration(commonData.AuthCacheSeconds)*time.Second).Err()
if err != nil {
return err
}
return nil
}
//CheckAclCache checks if the username/topic/clientid/acc mix is present in the cache. Return if it's present and, if so, if it was granted privileges.
func CheckAclCache(username, topic, clientid string, acc int) (bool, bool) {
pair := b64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("acl%s%s%s%d", username, topic, clientid, acc)))
val, err := commonData.RedisCache.Get(pair).Result()
if err != nil {
return false, false
}
//refresh expiration
commonData.RedisCache.Expire(pair, time.Duration(commonData.AclCacheSeconds)*time.Second)
if val == "true" {
return true, true
}
return true, false
}
//SetAclCache sets a mix, granted option and expiration time.
func SetAclCache(username, topic, clientid string, acc int, granted string) error {
pair := b64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("acl%s%s%s%d", username, topic, clientid, acc)))
err := commonData.RedisCache.Set(pair, granted, time.Duration(commonData.AclCacheSeconds)*time.Second).Err()
if err != nil {
return err
}
return nil
}
//CheckPrefix checks if a username contains a valid prefix. If so, returns ok and the suitable backend name; else, !ok and empty string.
func CheckPrefix(username string) (bool, string) {
if strings.Index(username, "_") > 0 {
userPrefix := username[0:strings.Index(username, "_")]
if prefix, ok := commonData.Prefixes[userPrefix]; ok {
log.Debugf("Found prefix for user %s, using backend %s.", username, prefix)
return true, prefix
}
}
return false, ""
}
//CheckBackendsAuth checks for all backends if a username is authenticated and sets the authenticated param.
func CheckBackendsAuth(username, password, clientid string) bool {
authenticated := false
for _, bename := range backends {
if bename == "plugin" {
continue
}
var backend = commonData.Backends[bename]
log.Debugf("checking user %s with backend %s", username, backend.GetName())
if backend.GetUser(username, password, clientid) {
authenticated = true
log.Debugf("user %s authenticated with backend %s", username, backend.GetName())
break
}
}
return authenticated
}
//CheckBackendsAcl checks for all backends if a username is superuser or has acl rights and sets the aclCheck param.
func CheckBackendsAcl(username, topic, clientid string, acc int) bool {
//Check superusers first
aclCheck := false
for _, bename := range backends {
if bename == "plugin" {
continue
}
var backend = commonData.Backends[bename]
log.Debugf("Superuser check with backend %s", backend.GetName())
if backend.GetSuperuser(username) {
log.Debugf("superuser %s acl authenticated with backend %s", username, backend.GetName())
aclCheck = true
break
}
}
if !aclCheck {
for _, bename := range backends {
if bename == "plugin" {
continue
}
var backend = commonData.Backends[bename]
log.Debugf("Acl check with backend %s", backend.GetName())
if backend.CheckAcl(username, topic, clientid, int32(acc)) {
log.Debugf("user %s acl authenticated with backend %s", username, backend.GetName())
aclCheck = true
break
}
}
}
return aclCheck
}
//CheckPluginAuth checks that the plugin is not nil and returns the plugins auth response.
func CheckPluginAuth(username, password, clientid string) bool {
if commonData.Plugin == nil {
return false
}
return commonData.PGetUser(username, password)
}
//CheckPluginAcl checks that the plugin is not nil and returns the superuser/acl response.
func CheckPluginAcl(username, topic, clientid string, acc int) bool {
if commonData.Plugin == nil {
return false
}
//If superuser, authorize it.
if commonData.PGetSuperuser(username) {
return true
}
//Check against the plugin's check acl function.
return commonData.PCheckAcl(username, topic, clientid, acc)
}
//export AuthPluginCleanup
func AuthPluginCleanup() {
log.Info("Cleaning up plugin")
//If cache is set, close cache connection.
if commonData.RedisCache != nil {
commonData.RedisCache.Close()
}
//Halt every registered backend.
for _, v := range commonData.Backends {
v.Halt()
}
if commonData.Plugin != nil {
commonData.PHalt()
}
}
func main() {}