From 2addea1e08d4a722f96ead2c9aebe7b172199852 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 23 Aug 2019 02:37:57 +0200 Subject: [PATCH] Implement basic tests for auth mechanism --- moulinette/authenticators/dummy.py | 28 +++++++++ test/actionsmap/moulitest.yml | 42 +++++++++++++ test/conftest.py | 3 +- test/locales/en.json | 3 + test/src/__init__.py | 0 test/src/testauth.py | 10 +++ test/test_auth.py | 99 ++++++++++++++++++++++++++++++ 7 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 moulinette/authenticators/dummy.py create mode 100644 test/actionsmap/moulitest.yml create mode 100644 test/locales/en.json create mode 100644 test/src/__init__.py create mode 100644 test/src/testauth.py create mode 100644 test/test_auth.py diff --git a/moulinette/authenticators/dummy.py b/moulinette/authenticators/dummy.py new file mode 100644 index 00000000..ea05da35 --- /dev/null +++ b/moulinette/authenticators/dummy.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +import logging +from moulinette.core import MoulinetteError +from moulinette.authenticators import BaseAuthenticator + +logger = logging.getLogger('moulinette.authenticator.dummy') + +# Dummy authenticator implementation + + +class Authenticator(BaseAuthenticator): + + """Dummy authenticator used for tests + """ + + vendor = 'dummy' + + def __init__(self, name, vendor, parameters, extra): + logger.debug("initialize authenticator '%s") + super(Authenticator, self).__init__(name) + + def authenticate(self, password): + + if not password == "Yoloswag": + raise MoulinetteError("Invalid password!") + + return self diff --git a/test/actionsmap/moulitest.yml b/test/actionsmap/moulitest.yml new file mode 100644 index 00000000..a14bd67a --- /dev/null +++ b/test/actionsmap/moulitest.yml @@ -0,0 +1,42 @@ + +############################# +# Global parameters # +############################# +_global: + configuration: + authenticate: + - all + authenticator: + default: + vendor: dummy + help: Dummy Password + yoloswag: + vendor: dummy + help: Dummy Yoloswag Password + +############################# +# Test Actions # +############################# +testauth: + actions: + + none: + api: GET /test-auth/none + configuration: + authenticate: false + + default: + api: GET /test-auth/default + configuration: + authenticate: all + authenticator: default + +# only-api: +# api: GET /test-auth/only-api +# configuration: +# authenticate: api +# + other-profile: + api: GET /test-auth/other-profile + configuration: + authenticator: yoloswag diff --git a/test/conftest.py b/test/conftest.py index 4cf10815..c67331ae 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -92,7 +92,6 @@ def patch_logging(moulinette): } - @pytest.fixture(scope='session', autouse=True) def moulinette(tmp_path_factory): import moulinette @@ -100,8 +99,10 @@ def moulinette(tmp_path_factory): # Can't call the namespace just 'test' because # that would lead to some "import test" not importing the right stuff namespace = "moulitest" + tmp_cache = str(tmp_path_factory.mktemp("cache")) tmp_data = str(tmp_path_factory.mktemp("data")) tmp_lib = str(tmp_path_factory.mktemp("lib")) + os.environ['MOULINETTE_CACHE_DIR'] = tmp_cache os.environ['MOULINETTE_DATA_DIR'] = tmp_data os.environ['MOULINETTE_LIB_DIR'] = tmp_lib shutil.copytree("./test/actionsmap", "%s/actionsmap" % tmp_data) diff --git a/test/locales/en.json b/test/locales/en.json new file mode 100644 index 00000000..e63d37b6 --- /dev/null +++ b/test/locales/en.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} diff --git a/test/src/__init__.py b/test/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/src/testauth.py b/test/src/testauth.py new file mode 100644 index 00000000..0a28f9c4 --- /dev/null +++ b/test/src/testauth.py @@ -0,0 +1,10 @@ +def testauth_none(): + return "some_data_from_none" + + +def testauth_default(): + return "some_data_from_default" + + +def testauth_other_profile(): + return "some_data_from_other_profile" diff --git a/test/test_auth.py b/test/test_auth.py new file mode 100644 index 00000000..128316ea --- /dev/null +++ b/test/test_auth.py @@ -0,0 +1,99 @@ +import os +import requests + + +def login(webapi, cookies=None, csrf=False, profile=None): + + data = {"password": "Yoloswag"} + if profile: + data["profile"] = profile + + return requests.post(webapi + "/login", + cookies=cookies, + data=data, + headers=None if csrf else {"X-Requested-With": ""}) + + +def test_request_no_auth_needed(monkeypatch, tmp_path, moulinette_webapi): + + r = requests.get(moulinette_webapi + "/test-auth/none") + + assert r.status_code == 200 + assert r.text == '"some_data_from_none"' + + +def test_request_with_auth_but_not_logged(monkeypatch, tmp_path, moulinette_webapi): + + r = requests.get(moulinette_webapi + "/test-auth/default") + + assert r.status_code == 401 + assert r.text == "Authentication required" + + +def test_login(monkeypatch, moulinette_webapi): + + r = login(moulinette_webapi) + + assert r.status_code == 200 + assert r.text == "Logged in" + assert "session.id" in r.cookies + assert "session.tokens" in r.cookies + + cache_session_default = os.environ['MOULINETTE_CACHE_DIR'] + "/session/default/" + assert r.cookies["session.id"] + ".asc" in os.listdir(cache_session_default) + + +def test_login_csrf_attempt(moulinette_webapi): + + # C.f. + # https://security.stackexchange.com/a/58308 + # https://stackoverflow.com/a/22533680 + + r = login(moulinette_webapi, csrf=True) + + assert r.status_code == 403 + assert "session.id" not in r.cookies + assert "session.tokens" not in r.cookies + assert "CSRF protection" in r.text + + +def test_login_then_legit_request_without_cookies(moulinette_webapi): + + login(moulinette_webapi) + + r = requests.get(moulinette_webapi + "/test-auth/default") + + assert r.status_code == 401 + assert r.text == "Authentication required" + + +def test_login_then_legit_request(moulinette_webapi): + + r_login = login(moulinette_webapi) + + r = requests.get(moulinette_webapi + "/test-auth/default", + cookies={"session.id": r_login.cookies["session.id"], + "session.tokens": r_login.cookies["session.tokens"], }) + + assert r.status_code == 200 + assert r.text == '"some_data_from_default"' + + +def test_login_then_logout(moulinette_webapi): + + r_login = login(moulinette_webapi) + + r = requests.get(moulinette_webapi + "/logout", + cookies={"session.id": r_login.cookies["session.id"], + "session.tokens": r_login.cookies["session.tokens"], }) + + assert r.status_code == 200 + cache_session_default = os.environ['MOULINETTE_CACHE_DIR'] + "/session/default/" + assert not r_login.cookies["session.id"] + ".asc" in os.listdir(cache_session_default) + + r = requests.get(moulinette_webapi + "/test-auth/default", + cookies={"session.id": r_login.cookies["session.id"], + "session.tokens": r_login.cookies["session.tokens"], }) + + assert r.status_code == 401 + assert r.text == "Authentication required"