mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
api: Move cookie session management logic to the authenticator for more flexibility
This commit is contained in:
parent
9fcc9630bd
commit
2fc9611b53
2 changed files with 31 additions and 59 deletions
|
@ -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
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue