mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
Naive rewrite of the Authenticator methods to avoid the mystic __call__ stuff, improve semantic
This commit is contained in:
parent
4345081e6f
commit
f792166581
3 changed files with 116 additions and 147 deletions
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue