Add mqtt file auth #74

Merged
bmartin5692 merged 2 commits from add-MQTTFileAuth into master 2019-12-30 17:43:45 +01:00
3 changed files with 252 additions and 31 deletions

View file

@ -203,10 +203,12 @@ class ConfServer:
mq_sessions = [] mq_sessions = []
for sess in mqttserver._sessions: for sess in mqttserver._sessions:
tmpsess = [] tmpsess = []
tmpsess.append({"client_id": mqttserver._sessions[sess][0].client_id}) tmpsess.append({
tmpsess.append( "username": mqttserver._sessions[sess][0].username,
{"state": mqttserver._sessions[sess][0].transitions.state} "client_id": mqttserver._sessions[sess][0].client_id,
) "state": mqttserver._sessions[sess][0].transitions.state,
})
mq_sessions.append(tmpsess) mq_sessions.append(tmpsess)
all = { all = {
"bots": bots, "bots": bots,

View file

@ -13,6 +13,7 @@ import bumper
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
import bumper import bumper
from passlib.apps import custom_app_context as pwd_context
helperbotlog = logging.getLogger("helperbot") helperbotlog = logging.getLogger("helperbot")
boterrorlog = logging.getLogger("boterror") boterrorlog = logging.getLogger("boterror")
@ -253,11 +254,11 @@ class MQTTServer:
}, },
"sys_interval": 10, "sys_interval": 10,
"auth": { "auth": {
"allow-anonymous": False, "allow-anonymous": False, # Set to True to allow anonymous authentication
"password-file": os.path.join( "password-file": os.path.join(
os.path.join(bumper.data_dir, "passwd") os.path.join(bumper.data_dir, "passwd")
), ), # For file auth, set user:hash in passwd file see (https://hbmqtt.readthedocs.io/en/latest/references/hbmqtt.html#configuration-example)
"plugins": ["bumper"], # No plugins == no auth "plugins": ["bumper"], # Bumper plugin provides auth and handling of bots/clients connecting
}, },
"topic-check": {"enabled": False}, "topic-check": {"enabled": False},
} }
@ -271,6 +272,8 @@ class BumperMQTTServer_Plugin:
self.context = context self.context = context
try: try:
self.auth_config = self.context.config["auth"] self.auth_config = self.context.config["auth"]
self._users = dict()
self._read_password_file()
except KeyError: except KeyError:
self.context.logger.warning( self.context.logger.warning(
@ -280,6 +283,7 @@ class BumperMQTTServer_Plugin:
mqttserverlog.exception("{}".format(e)) mqttserverlog.exception("{}".format(e))
async def authenticate(self, *args, **kwargs): async def authenticate(self, *args, **kwargs):
authenticated = False
if not self.auth_config: if not self.auth_config:
# auth config section not found # auth config section not found
self.context.logger.warning( self.context.logger.warning(
@ -287,19 +291,14 @@ class BumperMQTTServer_Plugin:
) )
return False return False
allow_anonymous = self.auth_config.get(
"allow-anonymous", True
) # allow anonymous by default
if allow_anonymous:
authenticated = True
self.context.logger.debug("Authentication success: config allows anonymous")
else:
try:
session = kwargs.get("session", None)
username = session.username
password = session.password
client_id = session.client_id
try:
session = kwargs.get("session", None)
username = session.username
password = session.password
client_id = session.client_id
if "@" in client_id:
didsplit = str(client_id).split("@") didsplit = str(client_id).split("@")
if not ( # if ecouser or bumper aren't in details it is a bot if not ( # if ecouser or bumper aren't in details it is a bot
"ecouser" in didsplit[1] or "bumper" in didsplit[1] "ecouser" in didsplit[1] or "bumper" in didsplit[1]
@ -312,10 +311,7 @@ class BumperMQTTServer_Plugin:
tmpbotdetail[1], tmpbotdetail[1],
"eco-ng", "eco-ng",
) )
mqttserverlog.info(f"Bumper Authentication Success - Bot - SN: {username} - DID: {didsplit[0]} - Class: {tmpbotdetail[0]}")
mqttserverlog.info(
"bot authenticated SN: {} DID: {}".format(username, didsplit[0])
)
authenticated = True authenticated = True
else: else:
@ -325,6 +321,7 @@ class BumperMQTTServer_Plugin:
resource = tmpclientdetail[1] resource = tmpclientdetail[1]
if userid == "helper1": if userid == "helper1":
mqttserverlog.info(f"Bumper Authentication Success - Helperbot: {client_id}")
authenticated = True authenticated = True
else: else:
auth = False auth = False
@ -335,20 +332,60 @@ class BumperMQTTServer_Plugin:
if auth: if auth:
bumper.client_add(userid, realm, resource) bumper.client_add(userid, realm, resource)
mqttserverlog.info("client authenticated {}".format(userid)) mqttserverlog.info(f"Bumper Authentication Success - Client - Username: {username} - ClientID: {client_id}")
authenticated = True authenticated = True
else: else:
authenticated = False authenticated = False
except Exception as e: # Check for File Auth
mqttserverlog.exception( if username and not authenticated: # If there is a username and it isn't already authenticated
"Session: {} - {}".format((kwargs.get("session", None)), e) hash = self._users.get(username, None)
) if hash: # If there is a matching entry in passwd, check hash
authenticated = False authenticated = pwd_context.verify(password, hash)
if authenticated:
mqttserverlog.info(f"File Authentication Success - Username: {username} - ClientID: {client_id}")
else:
mqttserverlog.info(f"File Authentication Failed - Username: {username} - ClientID: {client_id}")
else:
mqttserverlog.info(f"File Authentication Failed - No Entry for Username: {username} - ClientID: {client_id}")
except Exception as e:
mqttserverlog.exception(
"Session: {} - {}".format((kwargs.get("session", None)), e)
)
authenticated = False
# Check for allow anonymous
allow_anonymous = self.auth_config.get(
"allow-anonymous", True
)
if allow_anonymous and not authenticated: # If anonymous auth is allowed and it isn't already authenticated
authenticated = True
self.context.logger.debug(f"Anonymous Authentication Success: config allows anonymous - Username: {username}")
mqttserverlog.info(f"Anonymous Authentication Success: config allows anonymous - Username: {username}")
return authenticated return authenticated
def _read_password_file(self):
password_file = self.auth_config.get('password-file', None)
if password_file:
try:
with open(password_file) as f:
self.context.logger.debug(f"Reading user database from {password_file}")
for l in f:
line = l.strip()
if not line.startswith('#'): # Allow comments in files
(username, pwd_hash) = line.split(sep=":", maxsplit=3)
if username:
self._users[username] = pwd_hash
self.context.logger.debug(f"user: {username} - hash: {pwd_hash}")
self.context.logger.debug(f"{(len(self._users))} user(s) read from file {password_file}")
except FileNotFoundError:
self.context.logger.warning(f"Password file {password_file} not found")
else:
self.context.logger.debug("Configuration parameter 'password_file' not found")
async def on_broker_client_connected(self, client_id): async def on_broker_client_connected(self, client_id):
didsplit = str(client_id).split("@") didsplit = str(client_id).split("@")
@ -358,7 +395,6 @@ class BumperMQTTServer_Plugin:
bumper.bot_set_mqtt(bot["did"], True) bumper.bot_set_mqtt(bot["did"], True)
return return
# clientuserid = didsplit[0]
clientresource = didsplit[1].split("/")[1] clientresource = didsplit[1].split("/")[1]
client = bumper.client_get(clientresource) client = bumper.client_get(clientresource)
if client: if client:
@ -374,7 +410,6 @@ class BumperMQTTServer_Plugin:
bumper.bot_set_mqtt(bot["did"], False) bumper.bot_set_mqtt(bot["did"], False)
return return
# clientuserid = didsplit[0]
clientresource = didsplit[1].split("/")[1] clientresource = didsplit[1].split("/")[1]
client = bumper.client_get(clientresource) client = bumper.client_get(clientresource)
if client: if client:

View file

@ -379,6 +379,26 @@ async def test_mqttserver():
mqtt_address = ("127.0.0.1", 8883) mqtt_address = ("127.0.0.1", 8883)
mqtt_server = bumper.MQTTServer(mqtt_address) mqtt_server = bumper.MQTTServer(mqtt_address)
mqtt_server.default_config = {
"listeners": {
"default": {"type": "tcp"},
"tls1": {
"bind": "{}:{}".format(mqtt_address[0], mqtt_address[1]),
"ssl": "on",
"certfile": bumper.server_cert,
"keyfile": bumper.server_key,
},
},
"sys_interval": 10,
"auth": {
"allow-anonymous": True, # Set to True to allow anonymous authentication
"password-file": "tests/passwd", # For file auth, set user:hash in passwd file see (https://hbmqtt.readthedocs.io/en/latest/references/hbmqtt.html#configuration-example)
"plugins": ["bumper"], # Bumper plugin provides auth and handling of bots/clients connecting
},
"topic-check": {"enabled": False},
}
await mqtt_server.broker_coro() await mqtt_server.broker_coro()
# Test helperbot connect # Test helperbot connect
@ -420,6 +440,170 @@ async def test_mqttserver():
) # Check fake_bot is connected ) # Check fake_bot is connected
await fake_bot.Client.disconnect() await fake_bot.Client.disconnect()
# Test file auth client connect
test_client = bumper.MQTTHelperBot(mqtt_address)
test_client.client_id = "test-file-auth"
# await test_client.start_helper_bot()
test_client.Client = hbmqtt.client.MQTTClient(
client_id=test_client.client_id, config={"check_hostname": False, "auto_reconnect": False, "reconnect_retries": 1}
)
# good user/pass
await test_client.Client.connect(
f"mqtts://test-client:abc123!@{test_client.address[0]}:{test_client.address[1]}/",
cafile=bumper.ca_cert, cleansession=True
)
assert (
test_client.Client._connected_state._value == True
) # Check client is connected
await test_client.Client.disconnect()
assert (
test_client.Client._connected_state._value == False
) # Check client is disconnected
# bad password
try:
await test_client.Client.connect(
f"mqtts://test-client:notvalid!@{test_client.address[0]}:{test_client.address[1]}/",
cafile=bumper.ca_cert, cleansession=True
)
assert (
test_client.Client._connected_state._value == False
) # Check client is connected
except Exception as ae:
pass
# no username in file
try:
await test_client.Client.connect(
f"mqtts://test-client-noexist:notvalid!@{test_client.address[0]}:{test_client.address[1]}/",
cafile=bumper.ca_cert, cleansession=True
)
assert (
test_client.Client._connected_state._value == False
) # Check client is connected
except Exception as ae:
pass
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
await mqtt_server.broker.shutdown() await mqtt_server.broker.shutdown()
await asyncio.sleep(0.1)
async def test_passwordfile_badhash_mqttserver():
mqtt_address = ("127.0.0.1", 8883)
mqtt_server = bumper.MQTTServer(mqtt_address)
mqtt_server.default_config = {
"listeners": {
"default": {"type": "tcp"},
"tls1": {
"bind": "{}:{}".format(mqtt_address[0], mqtt_address[1]),
"ssl": "on",
"certfile": bumper.server_cert,
"keyfile": bumper.server_key,
},
},
"sys_interval": 10,
"auth": {
"allow-anonymous": True, # Set to True to allow anonymous authentication
"password-file": "tests/passwd_bad", # For file auth, set user:hash in passwd file see (https://hbmqtt.readthedocs.io/en/latest/references/hbmqtt.html#configuration-example)
"plugins": ["bumper"], # Bumper plugin provides auth and handling of bots/clients connecting
},
"topic-check": {"enabled": False},
}
await mqtt_server.broker_coro()
await asyncio.sleep(0.1)
# bad password
try:
test_client = bumper.MQTTHelperBot(mqtt_address)
await test_client.Client.connect(
f"mqtts://test-client:notvalid!@{test_client.address[0]}:{test_client.address[1]}/",
cafile=bumper.ca_cert, cleansession=True
)
assert (
test_client.Client._connected_state._value == False
) # Check client is connected
except Exception as ae:
pass
await mqtt_server.broker.shutdown()
await asyncio.sleep(0.1)
async def test_nofileauth_mqttserver():
try:
mqtt_address = ("127.0.0.1", 8883)
mqtt_server = bumper.MQTTServer(mqtt_address)
mqtt_server.default_config = {
"listeners": {
"default": {"type": "tcp"},
"tls1": {
"bind": "{}:{}".format(mqtt_address[0], mqtt_address[1]),
"ssl": "on",
"certfile": bumper.server_cert,
"keyfile": bumper.server_key,
},
},
"sys_interval": 10,
"auth": {
"allow-anonymous": True, # Set to True to allow anonymous authentication
"password-file": "tests/passwd-notfound", # For file auth, set user:hash in passwd file see (https://hbmqtt.readthedocs.io/en/latest/references/hbmqtt.html#configuration-example)
"plugins": ["bumper"], # Bumper plugin provides auth and handling of bots/clients connecting
},
"topic-check": {"enabled": False},
}
await mqtt_server.broker_coro()
except:
pass
async def test_passwordfile_opt_missing_mqttserver():
try:
mqtt_address = ("127.0.0.1", 8883)
mqtt_server = bumper.MQTTServer(mqtt_address)
mqtt_server.default_config = {
"listeners": {
"default": {"type": "tcp"},
"tls1": {
"bind": "{}:{}".format(mqtt_address[0], mqtt_address[1]),
"ssl": "on",
"certfile": bumper.server_cert,
"keyfile": bumper.server_key,
},
},
"sys_interval": 10,
"auth": {
"allow-anonymous": True, # Set to True to allow anonymous authentication
"plugins": ["bumper"], # Bumper plugin provides auth and handling of bots/clients connecting
},
"topic-check": {"enabled": False},
}
await mqtt_server.broker_coro()
await mqtt_server.broker.shutdown()
except:
pass