Naive rewrite of the Authenticator methods to avoid the mystic __call__ stuff, improve semantic

This commit is contained in:
Alexandre Aubin 2021-06-15 17:57:41 +02:00
parent 4345081e6f
commit f792166581
3 changed files with 116 additions and 147 deletions

View file

@ -32,7 +32,33 @@ class BaseAuthenticator(object):
# Virtual methods # Virtual methods
# Each authenticator classes must implement these methods. # Each authenticator classes must implement these methods.
def authenticate(self, credentials=None): def authenticate_credentials(self, credentials=None, store_session=False):
try:
# Attempt to authenticate
self.authenticate(credentials)
except MoulinetteError:
raise
except Exception as e:
logger.exception(f"authentication {self.name} failed because '{e}'")
raise MoulinetteAuthenticationError("unable_authenticate")
# Store session for later using the provided (new) token if any
if store_session:
try:
s_id = random_ascii()
s_token = random_ascii()
self._store_session(s_id, s_token)
except Exception as e:
import traceback
traceback.print_exc()
logger.exception(f"unable to store session because {e}")
else:
logger.debug("session has been stored")
def _authenticate_credentials(self, credentials=None):
"""Attempt to authenticate """Attempt to authenticate
Attempt to authenticate with given credentials. It should raise an Attempt to authenticate with given credentials. It should raise an
@ -46,84 +72,6 @@ class BaseAuthenticator(object):
"derived class '%s' must override this method" % self.__class__.__name__ "derived class '%s' must override this method" % self.__class__.__name__
) )
# Authentication methods
def __call__(self, credentials=None, token=None):
"""Attempt to authenticate
Attempt to authenticate either with credentials or with session
token if 'credentials' is None. If the authentication succeed, the
instance is returned and the session is registered for the token
if 'token' and 'credentials' are given.
The token is composed by the session identifier and a session
hash (the "true token") - to use for encryption - as a 2-tuple.
Keyword arguments:
- credentials -- A string containing the credentials to be used by the authenticator
- token -- The session token in the form of (id, hash)
"""
if hasattr(self, "is_authenticated"):
return self.is_authenticated
is_authenticated = False
#
# Authenticate using the credentials
#
if credentials:
try:
# Attempt to authenticate
self.authenticate(credentials)
except MoulinetteError:
raise
except Exception as e:
logger.exception(f"authentication {self.name} failed because '{e}'")
raise MoulinetteAuthenticationError("unable_authenticate")
else:
is_authenticated = True
# Store session for later using the provided (new) token if any
if token:
try:
s_id, s_token = token
self._store_session(s_id, s_token)
except Exception as e:
import traceback
traceback.print_exc()
logger.exception(f"unable to store session because {e}")
else:
logger.debug("session has been stored")
#
# Authenticate using the token provided
#
elif token:
try:
s_id, s_token = token
# Attempt to authenticate
self._authenticate_session(s_id, s_token)
except MoulinetteError:
raise
except Exception as e:
logger.exception(f"authentication {self.name} failed because '{e}'")
raise MoulinetteAuthenticationError("unable_authenticate")
else:
is_authenticated = True
#
# No credentials given, can't authenticate
#
else:
raise MoulinetteAuthenticationError("unable_authenticate")
self.is_authenticated = is_authenticated
return is_authenticated
# Private methods
def _open_sessionfile(self, session_id, mode="r"): def _open_sessionfile(self, session_id, mode="r"):
"""Open a session file for this instance in given mode""" """Open a session file for this instance in given mode"""
return open_cachefile( return open_cachefile(
@ -143,6 +91,16 @@ class BaseAuthenticator(object):
with self._open_sessionfile(session_id, "w") as f: with self._open_sessionfile(session_id, "w") as f:
f.write(hash_) f.write(hash_)
def authenticate_session(s_id, s_token):
try:
# Attempt to authenticate
self._authenticate_session(s_id, s_token)
except MoulinetteError:
raise
except Exception as e:
logger.exception(f"authentication {self.name} failed because '{e}'")
raise MoulinetteAuthenticationError("unable_authenticate")
def _authenticate_session(self, session_id, session_token): def _authenticate_session(self, session_id, session_token):
"""Checks session and token against the stored session token""" """Checks session and token against the stored session token"""
if not self._session_exists(session_id): if not self._session_exists(session_id):

View file

@ -359,49 +359,67 @@ class _ActionsMapPlugin(object):
- profile -- The authenticator profile name to log in - profile -- The authenticator profile name to log in
""" """
# Retrieve session values authenticator = self.actionsmap.get_authenticator(profile)
try:
s_id = request.get_cookie("session.id") or random_ascii() ##################################################################
except: # Case 1 : credentials were provided #
# Super rare case where there are super weird cookie / cache issue # We want to validate that the credentials are right #
# Previous line throws a CookieError that creates a 500 error ... # Then save the session id/token, and return then in the cookies #
# So let's catch it and just use a fresh ID then... ##################################################################
s_id = random_ascii()
try: try:
s_secret = self.secrets[s_id] s_id, s_token = authenticator.authenticate_credentials(credentials, store_session=True)
except KeyError:
s_tokens = {}
else:
try:
s_tokens = request.get_cookie("session.tokens", secret=s_secret) or {}
except:
# Same as for session.id a few lines before
s_tokens = {}
s_new_token = random_ascii()
try:
# Attempt to authenticate
authenticator = self.actionsmap.get_authenticator(profile)
authenticator(credentials, token=(s_id, s_new_token))
except MoulinetteError as e: except MoulinetteError as e:
if len(s_tokens) > 0: try:
try: self.logout(profile)
self.logout(profile) except Exception:
except: pass
pass # FIXME : replace with MoulinetteAuthenticationError !?
raise HTTPResponse(e.strerror, 401) raise HTTPResponse(e.strerror, 401)
else: else:
# Update dicts with new values # Save session id and token
s_tokens[profile] = s_new_token
# Create and save (in RAM) new cookie secret used to secure(=sign?) the cookie
self.secrets[s_id] = s_secret = random_ascii() self.secrets[s_id] = s_secret = random_ascii()
# Fetch current token per profile
try:
s_tokens = request.get_cookie("session.tokens", secret=s_secret) or {}
except Exception:
# Same as for session.id a few lines before
s_tokens = {}
# Update dicts with new values
s_tokens[profile] = s_token
response.set_cookie("session.id", s_id, secure=True) response.set_cookie("session.id", s_id, secure=True)
response.set_cookie( response.set_cookie(
"session.tokens", s_tokens, secure=True, secret=s_secret "session.tokens", {""}, secure=True, secret=s_secret
) )
return m18n.g("logged_in") return m18n.g("logged_in")
# This is called before each time a route is going to be processed
def _do_authenticate(self, authenticator):
"""Process the authentication
Handle the core.MoulinetteSignals.authenticate signal.
"""
s_id = request.get_cookie("session.id")
try:
s_secret = self.secrets[s_id]
s_token = request.get_cookie("session.tokens", secret=s_secret, default={})[
authenticator.name
]
except KeyError:
msg = m18n.g("authentication_required")
raise HTTPResponse(msg, 401)
else:
authenticator.authenticate_session(s_id, s_token)
def logout(self, profile): def logout(self, profile):
"""Log out from an authenticator profile """Log out from an authenticator profile
@ -412,25 +430,35 @@ class _ActionsMapPlugin(object):
- profile -- The authenticator profile name to log out - profile -- The authenticator profile name to log out
""" """
s_id = request.get_cookie("session.id") # Retrieve session values
# We check that there's a (signed) session.hash available
# for additional security ?
# (An attacker could not craft such signed hashed ? (FIXME : need to make sure of this))
try: try:
s_secret = self.secrets[s_id] s_id = request.get_cookie("session.id") or None
except KeyError: except:
s_secret = {} # Super rare case where there are super weird cookie / cache issue
if profile not in request.get_cookie( # Previous line throws a CookieError that creates a 500 error ...
"session.tokens", secret=s_secret, default={} # So let's catch it and just use None...
): s_id = None
raise HTTPResponse(m18n.g("not_logged_in"), 401)
else: if s_id is not None:
del self.secrets[s_id]
authenticator = self.actionsmap.get_authenticator(profile) # We check that there's a (signed) session.hash available
authenticator._clean_session(s_id) # for additional security ?
# TODO: Clean the session for profile only # (An attacker could not craft such signed hashed ? (FIXME : need to make sure of this))
# Delete cookie and clean the session try:
response.set_cookie("session.tokens", "", max_age=-1) s_secret = self.secrets[s_id]
except KeyError:
s_secret = {}
if profile not in request.get_cookie(
"session.tokens", secret=s_secret, default={}
):
raise HTTPResponse(m18n.g("not_logged_in"), 401)
else:
del self.secrets[s_id]
authenticator = self.actionsmap.get_authenticator(profile)
authenticator._clean_session(s_id)
# TODO: Clean the session for profile only
# Delete cookie and clean the session
response.set_cookie("session.tokens", "", max_age=-1)
return m18n.g("logged_out") return m18n.g("logged_out")
def messages(self): def messages(self):
@ -510,24 +538,6 @@ class _ActionsMapPlugin(object):
# Signals handlers # Signals handlers
def _do_authenticate(self, authenticator):
"""Process the authentication
Handle the core.MoulinetteSignals.authenticate signal.
"""
s_id = request.get_cookie("session.id")
try:
s_secret = self.secrets[s_id]
s_token = request.get_cookie("session.tokens", secret=s_secret, default={})[
authenticator.name
]
except KeyError:
msg = m18n.g("authentication_required")
raise HTTPResponse(msg, 401)
else:
return authenticator(token=(s_id, s_token))
def _do_display(self, message, style): def _do_display(self, message, style):
"""Display a message """Display a message

View file

@ -538,7 +538,8 @@ class Interface(BaseInterface):
# moulinette is used to create a CLI for non-root user that needs to # moulinette is used to create a CLI for non-root user that needs to
# auth somehow but hmpf -.- # auth somehow but hmpf -.-
msg = m18n.g("password") msg = m18n.g("password")
return authenticator(credentials=self._do_prompt(msg, True, False, color="yellow")) credentials = self._do_prompt(msg, True, False, color="yellow")
return authenticator.authenticate_credentials(credentials=credentials)
def _do_prompt(self, message, is_password, confirm, color="blue"): def _do_prompt(self, message, is_password, confirm, color="blue"):
"""Prompt for a value """Prompt for a value