From d8a2d6253cacd82fdd5702476a42ff8fbe2082e1 Mon Sep 17 00:00:00 2001 From: Bogdan Bodnar <bogdan.bodnar@nic.cz> Date: Fri, 8 Mar 2019 15:15:34 +0100 Subject: [PATCH 1/6] Add filesystem authentication. --- foris_ws/__main__.py | 9 +++- foris_ws/authentication/filesystem.py | 67 +++++++++++++++++++++++++++ setup.py | 1 + 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 foris_ws/authentication/filesystem.py diff --git a/foris_ws/__main__.py b/foris_ws/__main__.py index 37bafcb..0a32a4d 100755 --- a/foris_ws/__main__.py +++ b/foris_ws/__main__.py @@ -56,11 +56,16 @@ def main() -> typing.NoReturn: parser = argparse.ArgumentParser(prog="foris-ws") parser.add_argument("-d", "--debug", dest="debug", action="store_true", default=False) parser.add_argument('--version', action='version', version=__version__) + + auth_choices = ["filesystem", "none"] + if "ubus" in available_buses: + auth_choices.append("ubus") parser.add_argument( "-a", "--authentication", type=str, - choices=["ubus", "none"] if "ubus" in available_buses else ["none"], + choices=auth_choices, help="Which authentication method should be used", required=True ) + parser.add_argument( "--host", type=str, help="Hostname of the websocket server.", required=True ) @@ -122,6 +127,8 @@ def main() -> typing.NoReturn: if options.authentication == "ubus": from foris_ws.authentication.ubus import authenticate + elif options.authentication == "filesystem": + from foris_ws.authentication.filesystem import authenticate elif options.authentication == "none": from foris_ws.authentication.none import authenticate diff --git a/foris_ws/authentication/filesystem.py b/foris_ws/authentication/filesystem.py new file mode 100644 index 0000000..f0e6ff0 --- /dev/null +++ b/foris_ws/authentication/filesystem.py @@ -0,0 +1,67 @@ +# +# foris-ws +# Copyright (C) 2019 CZ.NIC, z.s.p.o. (http://www.nic.cz/) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +import logging +import re + +from http import HTTPStatus +from typing import Optional, Tuple +from websockets.http import Headers +from werkzeug.contrib.cache import FileSystemCache + +logger = logging.getLogger(__name__) + +SESSIONS_DIR = "/tmp/foris-sessions" + + +def authenticate(path: str, request_headers: Headers) -> Optional[Tuple[int, Headers, bytes]]: + """ Performs an authentication based on authentication token placed in cookie + and session saved by Flask to filesystem. + + :param message: should contain clients initial request + :rtype: bool + """ + + logger.debug("Logging using authentication cookie of the filesystem session.") + + if "Cookie" not in request_headers: + logger.debug("Missing cookie.") + return HTTPStatus.FORBIDDEN, Headers([]), b'Missing Cookie' + + foris_ws_session_re = re.search(r'session=([^;\s]*)', request_headers["Cookie"]) + if not foris_ws_session_re: + logger.debug("Missing foris.ws.session in cookie.") + return HTTPStatus.FORBIDDEN, Headers([]), b'Missing foris.ws.session in cookie' + + session_id = foris_ws_session_re.group(1) + logger.debug("Using session id %s" % session_id) + + fs_cache = FileSystemCache(SESSIONS_DIR) + data = fs_cache.get('session:' + session_id) + + if data is None: + logger.debug("Session '%s' not found." % session_id) + return HTTPStatus.FORBIDDEN, Headers([]), b'Session not found' + + if not data.get('logged', None): + logger.debug("Session '%s' found but not logged." % session_id) + return HTTPStatus.FORBIDDEN, Headers([]), b'Session not logged' + + logger.debug("Connection granted.") + return None diff --git a/setup.py b/setup.py index ce85b46..8bcddf3 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,7 @@ setup( extras_require={ 'ubus': ["ubus"], 'mqtt': ["paho-mqtt"], + 'fs_auth': ["werkzeug"], }, tests_require=[ 'pytest', -- GitLab From 083eb23556cb5d6020a9254bbe8d2f99ddf807f5 Mon Sep 17 00:00:00 2001 From: Bogdan Bodnar <bogdan.bodnar@nic.cz> Date: Fri, 8 Mar 2019 15:16:02 +0100 Subject: [PATCH 2/6] Add simple tests of filesystem auth. --- tests/test_auth_fs.py | 77 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/test_auth_fs.py diff --git a/tests/test_auth_fs.py b/tests/test_auth_fs.py new file mode 100644 index 0000000..08d3578 --- /dev/null +++ b/tests/test_auth_fs.py @@ -0,0 +1,77 @@ +# +# foris-ws +# Copyright (C) 2018 CZ.NIC, z.s.p.o. (http://www.nic.cz/) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +import json +from werkzeug.contrib.cache import FileSystemCache + +import pytest +import websocket + +from .fixtures import mqtt_ws + +SESSIONS_DIR = "/tmp/foris-sessions" +SESSIONS_ID = "some-testing-sessions-id" + + +def test_fail(mqtt_ws): + _, _, host, port = mqtt_ws + ws = websocket.WebSocket() + + with pytest.raises(websocket.WebSocketBadStatusException): + ws.connect( + "ws://%s:%d/" % ("[%s]" % host if ":" in host else host, port), + cookie="session=not-existed-session", + ) + ws.close() + + +def test_logged(mqtt_ws): + fs_cache = FileSystemCache(SESSIONS_DIR) + fs_cache.add('session:%s' % SESSIONS_ID, {'logged': True}) + + _, _, host, port = mqtt_ws + ws = websocket.WebSocket() + + ws.connect( + "ws://%s:%d/" % ("[%s]" % host if ":" in host else host, port), + cookie="session=%s" % SESSIONS_ID, + ) + + ws.send(b'{"action": "subscribe", "params": ["testd"]}') + res = ws.recv() + assert json.loads(res)["result"] + + ws.close() + fs_cache.clear() + + +def test_not_logged(mqtt_ws): + fs_cache = FileSystemCache(SESSIONS_DIR) + fs_cache.add('session:%s' % SESSIONS_ID, {'logged': True}) + + _, _, host, port = mqtt_ws + ws = websocket.WebSocket() + + with pytest.raises(websocket.WebSocketBadStatusException): + ws.connect( + "ws://%s:%d/" % ("[%s]" % host if ":" in host else host, port), + cookie="session=%s" % SESSIONS_ID, + ) + ws.close() + fs_cache.clear() -- GitLab From c87e6bd90873b8e7a99e7b15ae411ca901ecc285 Mon Sep 17 00:00:00 2001 From: Bogdan Bodnar <bogdan.bodnar@nic.cz> Date: Fri, 8 Mar 2019 15:34:55 +0100 Subject: [PATCH 3/6] Fix auth fs tests. --- setup.py | 1 + tests/test_auth_fs.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 8bcddf3..b347bd1 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ setup( 'foris-controller', 'ubus', 'paho-mqtt', + 'werkzeug' ], entry_points={ "console_scripts": [ diff --git a/tests/test_auth_fs.py b/tests/test_auth_fs.py index 08d3578..858d448 100644 --- a/tests/test_auth_fs.py +++ b/tests/test_auth_fs.py @@ -23,12 +23,16 @@ from werkzeug.contrib.cache import FileSystemCache import pytest import websocket -from .fixtures import mqtt_ws +from .fixtures import ( + authentication, mosquitto_test, rpcd, ubusd_test, + address_family, mqtt_ws, mqtt_controller, ws_client, mqtt_notify, +) SESSIONS_DIR = "/tmp/foris-sessions" SESSIONS_ID = "some-testing-sessions-id" +@pytest.mark.parametrize("authentication", ["filesystem"], ids=["auth_fs"], indirect=True, scope="function") def test_fail(mqtt_ws): _, _, host, port = mqtt_ws ws = websocket.WebSocket() @@ -41,6 +45,7 @@ def test_fail(mqtt_ws): ws.close() +@pytest.mark.parametrize("authentication", ["filesystem"], ids=["auth_fs"], indirect=True, scope="function") def test_logged(mqtt_ws): fs_cache = FileSystemCache(SESSIONS_DIR) fs_cache.add('session:%s' % SESSIONS_ID, {'logged': True}) @@ -61,9 +66,10 @@ def test_logged(mqtt_ws): fs_cache.clear() +@pytest.mark.parametrize("authentication", ["filesystem"], ids=["auth_fs"], indirect=True, scope="function") def test_not_logged(mqtt_ws): fs_cache = FileSystemCache(SESSIONS_DIR) - fs_cache.add('session:%s' % SESSIONS_ID, {'logged': True}) + fs_cache.add('session:%s' % SESSIONS_ID, {}) _, _, host, port = mqtt_ws ws = websocket.WebSocket() -- GitLab From bef120b4694b192388305c230f401b1ab4a9a9b5 Mon Sep 17 00:00:00 2001 From: Stepan Henek <stepan.henek@nic.cz> Date: Mon, 11 Mar 2019 14:57:24 +0100 Subject: [PATCH 4/6] make mqtt really optional --- foris_ws/bus_listener.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/foris_ws/bus_listener.py b/foris_ws/bus_listener.py index b854f75..b1e4629 100644 --- a/foris_ws/bus_listener.py +++ b/foris_ws/bus_listener.py @@ -19,9 +19,8 @@ import logging -from typing import Type, Optional, Tuple +from typing import Type, Dict, Any from foris_client.buses.base import BaseListener -from foris_client.buses.mqtt import MqttListener from .connection import connections @@ -34,6 +33,7 @@ def handler(notification: dict, controller_id: str): :param notification: notification to be sent :param controller_id: id of the controller from which the notification came """ + logger.debug("Handling bus notification from %s: %s", controller_id, notification) connections.publish_notification(controller_id, notification["module"], notification) logger.debug("Handling finished: %s - %s", controller_id, notification) @@ -41,25 +41,15 @@ def handler(notification: dict, controller_id: str): def make_bus_listener( listener_class: Type[BaseListener], - socket_path: Optional[str] = None, - host: Optional[str] = None, - port: Optional[int] = None, - credentials: Optional[Tuple[str]] = None, + **listener_kwargs: Dict[str, Any], ) -> BaseListener: """ Prepares a new foris notification listener :param listener_class: listener class to be used (UbusListener, UnixSocketListner, ...) - :param socket_path: path to socket - :param host: mqtt host - :param port: mqtt port - :param credentils: path to mqtt passwd file + :param listener_kwargs: argument for the listener :returns: instantiated listener """ - if listener_class is MqttListener: - logger.debug("Initializing bus listener (%s:%d)", host, port) - listener = listener_class(host, port, handler, credentials=credentials) - else: - logger.debug("Initializing bus listener (%s)", socket_path) - listener = listener_class(socket_path, handler) + logger.debug("Initializing bus listener (%s: %s)", listener_class, listener_kwargs) + listener = listener_class(**dict(handler=handler), **listener_kwargs) return listener -- GitLab From 3776b2751014b893eff6711d4176b1ffa6c04351 Mon Sep 17 00:00:00 2001 From: Stepan Henek <stepan.henek@nic.cz> Date: Mon, 11 Mar 2019 15:20:58 +0100 Subject: [PATCH 5/6] tests: auth_fs session cleanup --- tests/test_auth_fs.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/test_auth_fs.py b/tests/test_auth_fs.py index 858d448..4315312 100644 --- a/tests/test_auth_fs.py +++ b/tests/test_auth_fs.py @@ -18,6 +18,7 @@ # import json +import shutil from werkzeug.contrib.cache import FileSystemCache import pytest @@ -32,8 +33,18 @@ SESSIONS_DIR = "/tmp/foris-sessions" SESSIONS_ID = "some-testing-sessions-id" +@pytest.fixture(scope="function") +def fs_cache(): + + cache = FileSystemCache(SESSIONS_DIR) + + yield cache + + shutil.rmtree(SESSIONS_DIR, ignore_errors=False) + + @pytest.mark.parametrize("authentication", ["filesystem"], ids=["auth_fs"], indirect=True, scope="function") -def test_fail(mqtt_ws): +def test_fail(mqtt_ws, fs_cache): _, _, host, port = mqtt_ws ws = websocket.WebSocket() @@ -46,8 +57,7 @@ def test_fail(mqtt_ws): @pytest.mark.parametrize("authentication", ["filesystem"], ids=["auth_fs"], indirect=True, scope="function") -def test_logged(mqtt_ws): - fs_cache = FileSystemCache(SESSIONS_DIR) +def test_logged(mqtt_ws, fs_cache): fs_cache.add('session:%s' % SESSIONS_ID, {'logged': True}) _, _, host, port = mqtt_ws @@ -67,8 +77,7 @@ def test_logged(mqtt_ws): @pytest.mark.parametrize("authentication", ["filesystem"], ids=["auth_fs"], indirect=True, scope="function") -def test_not_logged(mqtt_ws): - fs_cache = FileSystemCache(SESSIONS_DIR) +def test_not_logged(mqtt_ws, fs_cache): fs_cache.add('session:%s' % SESSIONS_ID, {}) _, _, host, port = mqtt_ws -- GitLab From 3183174f55c601168a63054242d9d3c79e6b3a81 Mon Sep 17 00:00:00 2001 From: Stepan Henek <stepan.henek@nic.cz> Date: Mon, 11 Mar 2019 15:32:12 +0100 Subject: [PATCH 6/6] foris-ws: nicer handling for optinal imports --- foris_ws/__main__.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/foris_ws/__main__.py b/foris_ws/__main__.py index 0a32a4d..8544c4e 100755 --- a/foris_ws/__main__.py +++ b/foris_ws/__main__.py @@ -25,6 +25,7 @@ import typing import re import signal import websockets +import importlib from . import __version__ @@ -34,21 +35,22 @@ from .ws_handling import connection_handler as ws_connection_handler logger = logging.getLogger(__name__) -available_buses: typing.List[str] = ['unix-socket'] +def _extend_choices(names: typing.List[str], module_name: str, target_list: typing.List[str]): + try: + if importlib.util.find_spec(module_name): + target_list.extend(names) + except ModuleNotFoundError: + pass -try: - __import__("ubus") - available_buses.append("ubus") -except ModuleNotFoundError: - pass +available_buses: typing.List[str] = ['unix-socket'] +_extend_choices(["ubus"], "ubus", available_buses) +_extend_choices(["mqtt"], "paho.mqtt.client", available_buses) -try: - __import__("paho.mqtt.client") - available_buses.append("mqtt") -except ModuleNotFoundError: - pass +auth_choices: typing.List[str] = ['none'] +_extend_choices(["ubus"], "ubus", auth_choices) +_extend_choices(["filesystem"], "werkzeug.contrib.cache", auth_choices) def main() -> typing.NoReturn: @@ -57,9 +59,6 @@ def main() -> typing.NoReturn: parser.add_argument("-d", "--debug", dest="debug", action="store_true", default=False) parser.add_argument('--version', action='version', version=__version__) - auth_choices = ["filesystem", "none"] - if "ubus" in available_buses: - auth_choices.append("ubus") parser.add_argument( "-a", "--authentication", type=str, choices=auth_choices, -- GitLab