From 34a2a660272c4501d96276acc2b19e4251b3f87f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 14 Jul 2023 19:09:00 +0200 Subject: [PATCH 1/4] Fix boring login API expecting a weird form/multiparam thing instead of classic JSON for credentials ... --- moulinette/interfaces/api.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py index 34b51143..bf3b0c7f 100644 --- a/moulinette/interfaces/api.py +++ b/moulinette/interfaces/api.py @@ -353,11 +353,18 @@ class _ActionsMapPlugin: """ - if "credentials" not in request.params: - raise HTTPResponse("Missing credentials parameter", 400) - credentials = request.params["credentials"] + if request.get_header("Content-Type") == "application/json": + if "credentials" not in request.json: + raise HTTPResponse("Missing credentials parameter", 400) + credentials = request.json["credentials"] + profile = request.json.get("profile", self.actionsmap.default_authentication) + else: + if "credentials" not in request.params: + raise HTTPResponse("Missing credentials parameter", 400) + credentials = request.params["credentials"] + + profile = request.params.get("profile", self.actionsmap.default_authentication) - profile = request.params.get("profile", self.actionsmap.default_authentication) authenticator = self.actionsmap.get_authenticator(profile) try: @@ -732,7 +739,7 @@ class Interface: def wrapper(*args, **kwargs): try: locale = request.params.pop("locale") - except KeyError: + except (KeyError, ValueError): locale = m18n.default_locale m18n.set_locale(locale) return callback(*args, **kwargs) From 4104704a87b97daa96985c1c48c3f6d6f29131c9 Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 28 Jul 2023 17:17:51 +0200 Subject: [PATCH 2/4] allow json requests --- moulinette/interfaces/api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py index bf3b0c7f..c0e18be8 100644 --- a/moulinette/interfaces/api.py +++ b/moulinette/interfaces/api.py @@ -312,6 +312,9 @@ class _ActionsMapPlugin: return value def wrapper(*args, **kwargs): + if request.get_header("Content-Type") == "application/json": + return callback((request.method, context.rule), request.json) + params = kwargs # Format boolean params for a in args: From 328107c94699074cd00692451fd55589c96f16f2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 29 Jul 2023 19:09:52 +0200 Subject: [PATCH 3/4] api: Add a proper mechanism to allow specific, configurable CORS origins --- moulinette/__init__.py | 3 ++- moulinette/interfaces/api.py | 27 +++++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/moulinette/__init__.py b/moulinette/__init__.py index 6df5abef..92b7e691 100755 --- a/moulinette/__init__.py +++ b/moulinette/__init__.py @@ -53,7 +53,7 @@ class Moulinette: # Easy access to interfaces -def api(host="localhost", port=80, routes={}, actionsmap=None, locales_dir=None): +def api(host="localhost", port=80, routes={}, actionsmap=None, locales_dir=None, allowed_cors_origins=[]): """Web server (API) interface Run a HTTP server with the moulinette for an API usage. @@ -73,6 +73,7 @@ def api(host="localhost", port=80, routes={}, actionsmap=None, locales_dir=None) Api( routes=routes, actionsmap=actionsmap, + allowed_cors_origins=allowed_cors_origins, ).run(host, port) except MoulinetteError as e: import logging diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py index c0e18be8..32383977 100644 --- a/moulinette/interfaces/api.py +++ b/moulinette/interfaces/api.py @@ -717,9 +717,11 @@ class Interface: type = "api" - def __init__(self, routes={}, actionsmap=None): + def __init__(self, routes={}, actionsmap=None, allowed_cors_origins=[]): actionsmap = ActionsMap(actionsmap, ActionsMapParser()) + self.allowed_cors_origins = allowed_cors_origins + # Attempt to retrieve log queues from an APIQueueHandler handler = log.getHandlersByClass(APIQueueHandler, limit=1) if handler: @@ -729,11 +731,18 @@ class Interface: # TODO: Return OK to 'OPTIONS' xhr requests (l173) app = Bottle(autojson=True) - # Wrapper which sets proper header - def apiheader(callback): + def cors(callback): def wrapper(*args, **kwargs): - response.set_header("Access-Control-Allow-Origin", "*") - return callback(*args, **kwargs) + r = callback(*args, **kwargs) + origin = request.headers.environ.get("HTTP_ORIGIN", "") + if origin and origin in self.allowed_cors_origins: + resp = r if isinstance(r, HTTPResponse) else response + resp.headers['Access-Control-Allow-Origin'] = origin + resp.headers['Access-Control-Allow-Methods'] = 'GET, HEAD, POST, PUT, OPTIONS, DELETE' + resp.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token' + resp.headers['Access-Control-Allow-Credentials'] = 'true' + + return r return wrapper @@ -751,7 +760,7 @@ class Interface: # Install plugins app.install(filter_csrf) - app.install(apiheader) + app.install(cors) app.install(api18n) actionsmapplugin = _ActionsMapPlugin(actionsmap, log_queues) app.install(actionsmapplugin) @@ -760,6 +769,12 @@ class Interface: self.display = actionsmapplugin.display self.prompt = actionsmapplugin.prompt + def handle_options(): + return HTTPResponse("", 204) + + app.route('/<:re:.*>', method="OPTIONS", + callback=handle_options, skip=["actionsmap"]) + # Append additional routes # TODO: Add optional authentication to those routes? for (m, p), c in routes.items(): From a6c7e55d1dcfc26c94cc97713b270037d236b30a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 29 Jul 2023 19:10:47 +0200 Subject: [PATCH 4/4] api: fix authentication failure not deleting expired cookies --- moulinette/interfaces/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py index 32383977..ac632ec0 100644 --- a/moulinette/interfaces/api.py +++ b/moulinette/interfaces/api.py @@ -387,6 +387,7 @@ class _ActionsMapPlugin: try: session_infos = authenticator.get_session_cookie() except Exception: + authenticator.delete_session_cookie() msg = m18n.g("authentication_required") raise HTTPResponse(msg, 401)