272 lines
6.2 KiB
Go
272 lines
6.2 KiB
Go
package backends
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"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"
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"go.mongodb.org/mongo-driver/mongo"
|
|
"go.mongodb.org/mongo-driver/mongo/options"
|
|
)
|
|
|
|
type Mongo struct {
|
|
Host string
|
|
Port string
|
|
Username string
|
|
Password string
|
|
SaltEncoding string
|
|
DBName string
|
|
AuthSource string
|
|
UsersCollection string
|
|
AclsCollection string
|
|
Conn *mongo.Client
|
|
disableSuperuser bool
|
|
hasher hashing.HashComparer
|
|
withTLS bool
|
|
insecureSkipVerify bool
|
|
}
|
|
|
|
type MongoAcl struct {
|
|
Topic string `bson:"topic"`
|
|
Acc int32 `bson:"acc"`
|
|
}
|
|
|
|
type MongoUser struct {
|
|
Username string `bson:"username"`
|
|
PasswordHash string `bson:"password"`
|
|
Superuser bool `bson:"superuser"`
|
|
Acls []MongoAcl `bson:"acls"`
|
|
}
|
|
|
|
func NewMongo(authOpts map[string]string, logLevel log.Level, hasher hashing.HashComparer) (Mongo, error) {
|
|
|
|
log.SetLevel(logLevel)
|
|
|
|
var m = Mongo{
|
|
Host: "localhost",
|
|
Port: "27017",
|
|
Username: "",
|
|
Password: "",
|
|
DBName: "mosquitto",
|
|
AuthSource: "",
|
|
UsersCollection: "users",
|
|
AclsCollection: "acls",
|
|
hasher: hasher,
|
|
withTLS: false,
|
|
insecureSkipVerify: false,
|
|
}
|
|
|
|
if authOpts["mongo_disable_superuser"] == "true" {
|
|
m.disableSuperuser = true
|
|
}
|
|
|
|
if mongoHost, ok := authOpts["mongo_host"]; ok {
|
|
m.Host = mongoHost
|
|
}
|
|
|
|
if mongoPort, ok := authOpts["mongo_port"]; ok {
|
|
m.Port = mongoPort
|
|
}
|
|
|
|
if mongoUsername, ok := authOpts["mongo_username"]; ok {
|
|
m.Username = mongoUsername
|
|
}
|
|
|
|
if mongoPassword, ok := authOpts["mongo_password"]; ok {
|
|
m.Password = mongoPassword
|
|
}
|
|
|
|
if mongoDBName, ok := authOpts["mongo_dbname"]; ok {
|
|
m.DBName = mongoDBName
|
|
}
|
|
|
|
if mongoAuthSource, ok := authOpts["mongo_authsource"]; ok {
|
|
m.AuthSource = mongoAuthSource
|
|
}
|
|
|
|
if usersCollection, ok := authOpts["mongo_users"]; ok {
|
|
m.UsersCollection = usersCollection
|
|
}
|
|
|
|
if aclsCollection, ok := authOpts["mongo_acls"]; ok {
|
|
m.AclsCollection = aclsCollection
|
|
}
|
|
|
|
if authOpts["mongo_use_tls"] == "true" {
|
|
m.withTLS = true
|
|
}
|
|
|
|
if authOpts["mongo_insecure_skip_verify"] == "true" {
|
|
m.insecureSkipVerify = true
|
|
}
|
|
|
|
addr := fmt.Sprintf("mongodb://%s:%s", m.Host, m.Port)
|
|
|
|
to := 60 * time.Second
|
|
|
|
opts := options.ClientOptions{
|
|
ConnectTimeout: &to,
|
|
}
|
|
|
|
if m.withTLS {
|
|
opts.TLSConfig = &tls.Config{}
|
|
}
|
|
|
|
opts.ApplyURI(addr)
|
|
|
|
if m.Username != "" && m.Password != "" {
|
|
opts.Auth = &options.Credential{
|
|
AuthSource: m.DBName,
|
|
Username: m.Username,
|
|
Password: m.Password,
|
|
PasswordSet: true,
|
|
}
|
|
// Set custom AuthSource db if supplied in config
|
|
if m.AuthSource != "" {
|
|
opts.Auth.AuthSource = m.AuthSource
|
|
log.Infof("mongo backend: set authentication db to: %s", m.AuthSource)
|
|
}
|
|
}
|
|
|
|
client, err := mongo.Connect(context.TODO(), &opts)
|
|
if err != nil {
|
|
return m, errors.Errorf("couldn't start mongo backend: %s", err)
|
|
}
|
|
|
|
m.Conn = client
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
//GetUser checks that the username exists and the given password hashes to the same password.
|
|
func (o Mongo) GetUser(username, password, clientid string) (bool, error) {
|
|
|
|
uc := o.Conn.Database(o.DBName).Collection(o.UsersCollection)
|
|
|
|
var user MongoUser
|
|
|
|
err := uc.FindOne(context.TODO(), bson.M{"username": username}).Decode(&user)
|
|
if err != nil {
|
|
if err == mongo.ErrNoDocuments {
|
|
// avoid leaking the fact that user exists or not though error.
|
|
return false, nil
|
|
}
|
|
|
|
log.Debugf("Mongo get user error: %s", err)
|
|
return false, err
|
|
}
|
|
|
|
if o.hasher.Compare(password, user.PasswordHash) {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
//GetSuperuser checks that the key username:su exists and has value "true".
|
|
func (o Mongo) GetSuperuser(username string) (bool, error) {
|
|
|
|
if o.disableSuperuser {
|
|
return false, nil
|
|
}
|
|
|
|
uc := o.Conn.Database(o.DBName).Collection(o.UsersCollection)
|
|
|
|
var user MongoUser
|
|
|
|
err := uc.FindOne(context.TODO(), bson.M{"username": username}).Decode(&user)
|
|
if err != nil {
|
|
if err == mongo.ErrNoDocuments {
|
|
// avoid leaking the fact that user exists or not though error.
|
|
return false, nil
|
|
}
|
|
|
|
log.Debugf("Mongo get superuser error: %s", err)
|
|
return false, err
|
|
}
|
|
|
|
return user.Superuser, nil
|
|
|
|
}
|
|
|
|
//CheckAcl gets all acls for the username and tries to match against topic, acc, and username/clientid if needed.
|
|
func (o Mongo) CheckAcl(username, topic, clientid string, acc int32) (bool, error) {
|
|
|
|
//Get user and check his acls.
|
|
uc := o.Conn.Database(o.DBName).Collection(o.UsersCollection)
|
|
|
|
var user MongoUser
|
|
|
|
err := uc.FindOne(context.TODO(), bson.M{"username": username}).Decode(&user)
|
|
if err != nil {
|
|
if err == mongo.ErrNoDocuments {
|
|
// avoid leaking the fact that user exists or not though error.
|
|
return false, nil
|
|
}
|
|
|
|
log.Debugf("Mongo get superuser error: %s", err)
|
|
return false, err
|
|
}
|
|
|
|
for _, acl := range user.Acls {
|
|
// 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
|
|
}
|
|
}
|
|
|
|
//Now check common acls.
|
|
|
|
ac := o.Conn.Database(o.DBName).Collection(o.AclsCollection)
|
|
cur, err := ac.Find(context.TODO(), bson.M{"acc": bson.M{"$in": []int32{acc, 3}}})
|
|
|
|
if err != nil {
|
|
log.Debugf("Mongo check acl error: %s", err)
|
|
return false, err
|
|
}
|
|
|
|
defer cur.Close(context.TODO())
|
|
|
|
for cur.Next(context.TODO()) {
|
|
var acl MongoAcl
|
|
err = cur.Decode(&acl)
|
|
if err == nil {
|
|
aclTopic := strings.Replace(acl.Topic, "%c", clientid, -1)
|
|
aclTopic = strings.Replace(aclTopic, "%u", username, -1)
|
|
if topics.Match(aclTopic, topic) {
|
|
return true, nil
|
|
}
|
|
} else {
|
|
log.Errorf("mongo cursor decode error: %s", err)
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
//GetName returns the backend's name
|
|
func (o Mongo) GetName() string {
|
|
return "Mongo"
|
|
}
|
|
|
|
//Halt closes the mongo session.
|
|
func (o Mongo) Halt() {
|
|
if o.Conn != nil {
|
|
err := o.Conn.Disconnect(context.TODO())
|
|
if err != nil {
|
|
log.Errorf("mongo halt: %s", err)
|
|
}
|
|
}
|
|
}
|