api: Move cookie session management logic to the authenticator for more flexibility

This commit is contained in:
Alexandre Aubin 2021-12-22 19:06:33 +01:00
parent 9fcc9630bd
commit 2fc9611b53
2 changed files with 31 additions and 59 deletions

View file

@ -10,6 +10,7 @@ from typing import List, Optional
from time import time from time import time
from collections import OrderedDict from collections import OrderedDict
from importlib import import_module from importlib import import_module
from functools import cache
from moulinette import m18n, Moulinette from moulinette import m18n, Moulinette
from moulinette.core import ( from moulinette.core import (
@ -456,6 +457,7 @@ class ActionsMap(object):
self.extraparser = ExtraArgumentParser(top_parser.interface) self.extraparser = ExtraArgumentParser(top_parser.interface)
self.parser = self._construct_parser(actionsmap, top_parser) self.parser = self._construct_parser(actionsmap, top_parser)
@cache
def get_authenticator(self, auth_method): def get_authenticator(self, auth_method):
if auth_method == "default": if auth_method == "default":
@ -616,7 +618,6 @@ class ActionsMap(object):
_global = actionsmap.pop("_global", {}) _global = actionsmap.pop("_global", {})
self.namespace = _global["namespace"] self.namespace = _global["namespace"]
self.cookie_name = _global["cookie_name"]
self.default_authentication = _global["authentication"][ self.default_authentication = _global["authentication"][
interface_type interface_type
] ]

View file

@ -85,9 +85,15 @@ class APIQueueHandler(logging.Handler):
def __init__(self): def __init__(self):
logging.Handler.__init__(self) logging.Handler.__init__(self)
self.queues = LogQueues() self.queues = LogQueues()
# actionsmap is actually set during the interface's init ...
self.actionsmap = None
def emit(self, record): def emit(self, record):
s_id = Session.get_infos(raise_if_no_session_exists=False)["id"]
profile = request.params.get("profile", self.actionsmap.default_authentication)
authenticator = self.actionsmap.get_authenticator(profile)
s_id = authenticator.get_session_cookie(raise_if_no_session_exists=False)["id"]
try: try:
queue = self.queues[s_id] queue = self.queues[s_id]
except KeyError: except KeyError:
@ -234,47 +240,6 @@ class _HTTPArgumentParser(object):
raise MoulinetteValidationError(message, raw_msg=True) raise MoulinetteValidationError(message, raw_msg=True)
class Session:
secret = random_ascii()
cookie_name = None # This is later set to the actionsmap name
def set_infos(infos):
assert isinstance(infos, dict)
response.set_cookie(
f"session.{Session.cookie_name}",
infos,
secure=True,
secret=Session.secret,
httponly=True,
# samesite="strict", # Bottle 0.12 doesn't support samesite, to be added in next versions
)
def get_infos(raise_if_no_session_exists=True):
try:
infos = request.get_cookie(
f"session.{Session.cookie_name}", secret=Session.secret, default={}
)
except Exception:
if not raise_if_no_session_exists:
return {"id": random_ascii()}
raise MoulinetteAuthenticationError("unable_authenticate")
if "id" not in infos:
infos["id"] = random_ascii()
return infos
@staticmethod
def delete_infos():
response.set_cookie(f"session.{Session.cookie_name}", "", max_age=-1)
response.delete_cookie(f"session.{Session.cookie_name}")
class _ActionsMapPlugin(object): class _ActionsMapPlugin(object):
"""Actions map Bottle Plugin """Actions map Bottle Plugin
@ -294,7 +259,6 @@ class _ActionsMapPlugin(object):
self.actionsmap = actionsmap self.actionsmap = actionsmap
self.log_queues = log_queues self.log_queues = log_queues
Session.cookie_name = actionsmap.cookie_name
def setup(self, app): def setup(self, app):
"""Setup plugin on the application """Setup plugin on the application
@ -398,13 +362,10 @@ class _ActionsMapPlugin(object):
credentials = request.params["credentials"] 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) authenticator = self.actionsmap.get_authenticator(profile)
try: try:
auth_info = authenticator.authenticate_credentials(credentials) auth_infos = authenticator.authenticate_credentials(credentials)
session_infos = Session.get_infos(raise_if_no_session_exists=False)
session_infos[profile] = auth_info
except MoulinetteError as e: except MoulinetteError as e:
try: try:
self.logout() self.logout()
@ -412,18 +373,14 @@ class _ActionsMapPlugin(object):
pass pass
raise HTTPResponse(e.strerror, 401) raise HTTPResponse(e.strerror, 401)
else: else:
Session.set_infos(session_infos) authenticator.set_session_cookie(auth_infos)
return m18n.g("logged_in") return m18n.g("logged_in")
# This is called before each time a route is going to be processed # This is called before each time a route is going to be processed
def authenticate(self, authenticator): def authenticate(self, authenticator):
try: try:
session_infos = Session.get_infos()[authenticator.name] session_infos = authenticator.get_session_cookie()
# Here, maybe we want to re-authenticate the session via the authenticator
# For example to check that the username authenticated is still in the admin group...
except Exception: except Exception:
msg = m18n.g("authentication_required") msg = m18n.g("authentication_required")
raise HTTPResponse(msg, 401) raise HTTPResponse(msg, 401)
@ -431,13 +388,17 @@ class _ActionsMapPlugin(object):
return session_infos return session_infos
def logout(self): def logout(self):
profile = request.params.get("profile", self.actionsmap.default_authentication)
authenticator = self.actionsmap.get_authenticator(profile)
try: try:
Session.get_infos() authenticator.get_session_cookie()
except KeyError: except KeyError:
raise HTTPResponse(m18n.g("not_logged_in"), 401) raise HTTPResponse(m18n.g("not_logged_in"), 401)
else: else:
# Delete cookie and clean the session # Delete cookie and clean the session
Session.delete_infos() authenticator.delete_session_cookie()
return m18n.g("logged_out") return m18n.g("logged_out")
def messages(self): def messages(self):
@ -446,7 +407,11 @@ class _ActionsMapPlugin(object):
Retrieve the WebSocket stream and send to it each messages displayed by Retrieve the WebSocket stream and send to it each messages displayed by
the display method. They are JSON encoded as a dict { style: message }. the display method. They are JSON encoded as a dict { style: message }.
""" """
s_id = Session.get_infos()["id"]
profile = request.params.get("profile", self.actionsmap.default_authentication)
authenticator = self.actionsmap.get_authenticator(profile)
s_id = authenticator.get_session_cookie()["id"]
try: try:
queue = self.log_queues[s_id] queue = self.log_queues[s_id]
except KeyError: except KeyError:
@ -514,8 +479,10 @@ class _ActionsMapPlugin(object):
UPLOAD_DIR = None UPLOAD_DIR = None
# Close opened WebSocket by putting StopIteration in the queue # Close opened WebSocket by putting StopIteration in the queue
profile = request.params.get("profile", self.actionsmap.default_authentication)
authenticator = self.actionsmap.get_authenticator(profile)
try: try:
s_id = Session.get_infos()["id"] s_id = authenticator.get_session_cookie()["id"]
queue = self.log_queues[s_id] queue = self.log_queues[s_id]
except KeyError: except KeyError:
pass pass
@ -524,7 +491,10 @@ class _ActionsMapPlugin(object):
def display(self, message, style="info"): def display(self, message, style="info"):
s_id = Session.get_infos(raise_if_no_session_exists=False)["id"] profile = request.params.get("profile", self.actionsmap.default_authentication)
authenticator = self.actionsmap.get_authenticator(profile)
s_id = authenticator.get_session_cookie(raise_if_no_session_exists=False)["id"]
try: try:
queue = self.log_queues[s_id] queue = self.log_queues[s_id]
except KeyError: except KeyError:
@ -742,6 +712,7 @@ class Interface:
handler = log.getHandlersByClass(APIQueueHandler, limit=1) handler = log.getHandlersByClass(APIQueueHandler, limit=1)
if handler: if handler:
log_queues = handler.queues log_queues = handler.queues
handler.actionsmap = actionsmap
# TODO: Return OK to 'OPTIONS' xhr requests (l173) # TODO: Return OK to 'OPTIONS' xhr requests (l173)
app = Bottle(autojson=True) app = Bottle(autojson=True)