class AuthClientPolicy: """ An authentication policy for registered AuthClients Authentication for a request to API routes with HTTP Basic Authentication credentials that represent a registered AuthClient with grant type of ``client_credentials`` in the db. Authentication can be of two types: * The client itself: Some endpoints allow an authenticated auth_client to take action on users within its authority, such as creating a user or adding a user to a group. In this case, assuming credentials are valid, the request will be authenticated, but no ``authenticated_userid`` (and thus no request.user) will be set * A user within the client's associated authority: If an HTTP ``X-Forwarded-User`` header is present, its value will be treated as a ``userid`` and, if the client credentials are valid _and_ the userid represents an extant user within the client's authority, the request will be authenticated as that user. In this case, ``authenticated_userid`` will be set and there will ultimately be a request.user available. Note: To differentiate between request with a Token-authenticated user and a request with an auth_client forwarded user, the latter has an additional principal, ``client:{client_id}@{authority}`` to mark it as being authenticated on behalf of an auth_client """ def __init__(self, check=None): if check is None: check = AuthClientPolicy.check self._basic_auth_policy = BasicAuthAuthenticationPolicy(check=check) def unauthenticated_userid(self, request): """ Return the forwarded userid or the auth_client's id If a forwarded user header is set, return the ``userid`` (its value) Otherwise return the username parsed from the Basic Auth header :return: :py:attr:`h.models.user.User.userid` or :py:attr:`h.models.auth_client.AuthClient.id` :rtype: str """ forwarded_userid = AuthClientPolicy._forwarded_userid(request) if forwarded_userid is not None: return forwarded_userid # username from BasicAuth header return self._basic_auth_policy.unauthenticated_userid(request) def authenticated_userid(self, request): """ Return any forwarded userid or None Rely mostly on :py:meth:`pyramid.authentication.BasicAuthAuthenticationPolicy.authenticated_userid`, but don't actually return a ``userid`` unless there is a forwarded user header set—the auth client itself is not a "user" Although this looks as if it trusts the return value of :py:meth:`pyramid.authentication.BasicAuthAuthenticationPolicy.authenticated_userid` irrationally, rest assured that :py:meth:`~h.auth.policy.AuthClientPolicy.check` will always be called (via :py:meth:`pyramid.authentication.BasicAuthAuthenticationPolicy.callback`) before any non-None value is returned. :rtype: :py:attr:`h.models.user.User.userid` or ``None`` """ forwarded_userid = AuthClientPolicy._forwarded_userid(request) # only evaluate setting an authenticated_userid if forwarded user is present if forwarded_userid is None: return None # username extracted from BasicAuth header auth_userid = self._basic_auth_policy.unauthenticated_userid(request) # authentication of BasicAuth and forwarded user—this will invoke check callback_ok = self._basic_auth_policy.callback(auth_userid, request) if callback_ok is not None: return (forwarded_userid ) # This should always be a userid, not an auth_client id def effective_principals(self, request): """ Return a list of principals for the request This will concatenate the principals returned by :py:meth:`~h.auth.policy.AuthClientPolicy.check` (which is a list or None) with Pyramid's system principal(s). If :py:meth:`~h.auth.policy.AuthClientPolicy.check` returns None—that is, if authentication is unsuccessful—the returned principals will only contain Pyramid's ``system.Everyone`` principal (and Pyramid will not consider the request as authenticated). :rtype: list ``['system.Everyone']`` concatenated with any principals from a successful authentication """ return self._basic_auth_policy.effective_principals(request) def remember(self, request, userid, **kw): """Not implemented for basic auth client policy.""" return [] def forget(self, request): """Not implemented for basic auth client policy.""" return [] @staticmethod def check(username, password, request): """ Return list of appropriate principals or None if authentication is unsuccessful. Validate the basic auth credentials from the request by matching them to an auth_client record in the DB. If an HTTP ``X-Forwarded-User`` header is present in the request, this represents the intent to authenticate "on behalf of" a user within the auth_client's authority. If this header is present, the user indicated by its value (a :py:attr:`h.models.user.User.userid`) _must_ exist and be within the auth_client's authority, or authentication will fail. :param username: username parsed out of Authorization header (Basic) :param password: password parsed out of Authorization header (Basic) :returns: additional principals for the auth_client or None :rtype: list or None """ client_id = username client_secret = password # validate that the credentials in BasicAuth header # match an AuthClient record in the db client = util.verify_auth_client(client_id, client_secret, request.db) if client is None: return None forwarded_userid = AuthClientPolicy._forwarded_userid(request) if (forwarded_userid is None ): # No forwarded user; set principals for basic auth_client return util.principals_for_auth_client(client) user_service = request.find_service(name="user") try: user = user_service.fetch(forwarded_userid) except ValueError: # raised if userid is invalid format return None # invalid user, so we are failing here if user and user.authority == client.authority: return util.principals_for_auth_client_user(user, client) return None @staticmethod def _forwarded_userid(request): """Return forwarded userid or None""" return request.headers.get("X-Forwarded-User", None)
class HybridAuthenticationPolicy(): """ HybridAuthenticationPolicy. Called in the same way as other auth policies, but wraps Basic and AuthTkt. This policy also caches password lookups by remembering them in the request object. """ def __init__(self, secret, realm='Realm', hardcoded=()): """ We need to initialise variables here for both forms of auth which we're planning on using. :param secret: A hashing secret for AuthTkt, which should be generated outside the Pyhton process. :param realm: The Basic Auth realm which is probably set to eos_db. :param hardcoded: Triplets of user:password:group that should not be looked up in the database. """ self.hardcoded = { x[0]: (x[1],x[2]) for x in hardcoded } #DELETE ME #self.check = check # Password check routine passed to the constructor. #self.realm = realm # Basic Auth realm. # Now initialise both Auth Policies. AuthTkt has sha256 specified in # place of the default MD5 in order to suppress warnings about # security. self.bap = BasicAuthAuthenticationPolicy(check=self.passwordcheck, realm=realm) self.tap = AuthTktAuthenticationPolicy(secret=secret, callback=self.groupfinder, cookie_name='auth_tkt', hashalg='sha256') #Utility functions to interact with eos_db.server def groupfinder(self, username, request): """ Return the user group (just one) associated with the user. This uses a server function to check which group a user has been associated with. This provides the standard callback wanted by AuthTktAuthenticationPolicy. An alternative would be to encode the groups in the Tkt. The mapping of groups to actual capabilities is stored in views.PermissionsMap """ group = server.get_user_group(username) if group: return ["group:" + str(group)] def passwordcheck(self, login, password, request): """Password checking callback. """ hc = self.hardcoded if login in hc and hc[login][0] == password: return ['group:' + hc[login][1]] elif server.check_password(login, password): user_group = server.get_user_group(login) log.debug("Found user group %s" % user_group) return ['group:' + user_group] else: log.debug("Password chack failed for user %s" % login) return None def unauthenticated_userid(self, request): """ Return the userid parsed from the auth ticket cookie. If this does not exist, then check the basic auth header, and return that, if it exists. """ #Allow forcing the auth_tkt cookie. Helpful for JS calls. #Maybe move this to a callback so it only ever happens once? if request.headers.get('auth_tkt'): request.cookies['auth_tkt'] = request.headers['auth_tkt'] #Or, surely: return ( self.tap.unauthenticated_userid(request) or self.bap.unauthenticated_userid(request) ) def authenticated_userid(self, request): """ Return the Auth Ticket user ID if that exists. If not, then check for a user ID in Basic Auth. """ try: return request.cached_authenticated_userid except: #Proceed to look-up then pass #Allow forcing the auth_tkt cookie. if request.headers.get('auth_tkt'): request.cookies['auth_tkt'] = request.headers['auth_tkt'] request.cached_authenticated_userid = ( self.tap.unauthenticated_userid(request) or self.bap.unauthenticated_userid(request) ) return request.cached_authenticated_userid def effective_principals(self, request): """ Returns the list of effective principles from the auth policy under which the user is currently authenticated. Auth ticket takes precedence. """ try: return request.cached_effective_principals except: #Proceed to look-up then pass #Allow forcing the auth_tkt cookie. if request.headers.get('auth_tkt'): request.cookies['auth_tkt'] = request.headers['auth_tkt'] userid = self.tap.authenticated_userid(request) if userid: request.cached_effective_principals = self.tap.effective_principals(request) else: request.cached_effective_principals = self.bap.effective_principals(request) return request.cached_effective_principals def remember(self, request, principal, **kw): """Causes the session info to be remembered by passing the appropriate AuthTkt into the response. """ # We always rememeber by creating an AuthTkt, but only if there is something to remember # and if the user was not in the hard-coded list. if principal and principal not in self.hardcoded: return self.tap.remember(request, principal, **kw) else: return () def forget(self, request): """ Forget both sessions. """ return self.bap.forget(request) + self.tap.forget(request) def get_forbidden_view(self, request): """ Fire a 401 when authentication needed. """ # FIXME - this doesn't distinguish between unauthenticated and # unauthorized. Should it? if request.headers.get('auth_tkt'): return HTTPRequestTimeout() #print ("Access Forbidden") response = HTTPUnauthorized() response.headers.extend(self.bap.forget(request)) return response
class HybridAuthenticationPolicy(): """ HybridAuthenticationPolicy. Called in the same way as other auth policies, but wraps Basic and AuthTkt. This policy also caches password lookups by remembering them in the request object. """ def __init__(self, secret, realm='Realm', hardcoded=()): """ We need to initialise variables here for both forms of auth which we're planning on using. :param secret: A hashing secret for AuthTkt, which should be generated outside the Pyhton process. :param realm: The Basic Auth realm which is probably set to eos_db. :param hardcoded: Triplets of user:password:group that should not be looked up in the database. """ self.hardcoded = {x[0]: (x[1], x[2]) for x in hardcoded} #DELETE ME #self.check = check # Password check routine passed to the constructor. #self.realm = realm # Basic Auth realm. # Now initialise both Auth Policies. AuthTkt has sha256 specified in # place of the default MD5 in order to suppress warnings about # security. self.bap = BasicAuthAuthenticationPolicy(check=self.passwordcheck, realm=realm) self.tap = AuthTktAuthenticationPolicy(secret=secret, callback=self.groupfinder, cookie_name='auth_tkt', hashalg='sha256') #Utility functions to interact with eos_db.server def groupfinder(self, username, request): """ Return the user group (just one) associated with the user. This uses a server function to check which group a user has been associated with. This provides the standard callback wanted by AuthTktAuthenticationPolicy. An alternative would be to encode the groups in the Tkt. The mapping of groups to actual capabilities is stored in views.PermissionsMap """ group = server.get_user_group(username) if group: return ["group:" + str(group)] def passwordcheck(self, login, password, request): """Password checking callback. """ hc = self.hardcoded if login in hc and hc[login][0] == password: return ['group:' + hc[login][1]] elif server.check_password(login, password): user_group = server.get_user_group(login) log.debug("Found user group %s" % user_group) return ['group:' + user_group] else: log.debug("Password chack failed for user %s" % login) return None def unauthenticated_userid(self, request): """ Return the userid parsed from the auth ticket cookie. If this does not exist, then check the basic auth header, and return that, if it exists. """ #Allow forcing the auth_tkt cookie. Helpful for JS calls. #Maybe move this to a callback so it only ever happens once? if request.headers.get('auth_tkt'): request.cookies['auth_tkt'] = request.headers['auth_tkt'] #Or, surely: return (self.tap.unauthenticated_userid(request) or self.bap.unauthenticated_userid(request)) def authenticated_userid(self, request): """ Return the Auth Ticket user ID if that exists. If not, then check for a user ID in Basic Auth. """ try: return request.cached_authenticated_userid except: #Proceed to look-up then pass #Allow forcing the auth_tkt cookie. if request.headers.get('auth_tkt'): request.cookies['auth_tkt'] = request.headers['auth_tkt'] request.cached_authenticated_userid = ( self.tap.unauthenticated_userid(request) or self.bap.unauthenticated_userid(request)) return request.cached_authenticated_userid def effective_principals(self, request): """ Returns the list of effective principles from the auth policy under which the user is currently authenticated. Auth ticket takes precedence. """ try: return request.cached_effective_principals except: #Proceed to look-up then pass #Allow forcing the auth_tkt cookie. if request.headers.get('auth_tkt'): request.cookies['auth_tkt'] = request.headers['auth_tkt'] userid = self.tap.authenticated_userid(request) if userid: request.cached_effective_principals = self.tap.effective_principals( request) else: request.cached_effective_principals = self.bap.effective_principals( request) return request.cached_effective_principals def remember(self, request, principal, **kw): """Causes the session info to be remembered by passing the appropriate AuthTkt into the response. """ # We always rememeber by creating an AuthTkt, but only if there is something to remember # and if the user was not in the hard-coded list. if principal and principal not in self.hardcoded: return self.tap.remember(request, principal, **kw) else: return () def forget(self, request): """ Forget both sessions. """ return self.bap.forget(request) + self.tap.forget(request) def get_forbidden_view(self, request): """ Fire a 401 when authentication needed. """ # FIXME - this doesn't distinguish between unauthenticated and # unauthorized. Should it? if request.headers.get('auth_tkt'): return HTTPRequestTimeout() #print ("Access Forbidden") response = HTTPUnauthorized() response.headers.extend(self.bap.forget(request)) return response
class AuthClientPolicy(object): """ An authentication policy for registered AuthClients Authentication for a request to API routes with HTTP Basic Authentication credentials that represent a registered AuthClient with grant type of ``client_credentials`` in the db. Authentication can be of two types: * The client itself: Some endpoints allow an authenticated auth_client to take action on users within its authority, such as creating a user or adding a user to a group. In this case, assuming credentials are valid, the request will be authenticated, but no ``authenticated_userid`` (and thus no request.user) will be set * A user within the client's associated authority: If an HTTP ``X-Forwarded-User`` header is present, its value will be treated as a ``userid`` and, if the client credentials are valid _and_ the userid represents an extant user within the client's authority, the request will be authenticated as that user. In this case, ``authenticated_userid`` will be set and there will ultimately be a request.user available. Note: To differentiate between request with a Token-authenticated user and a request with an auth_client forwarded user, the latter has an additional principal, ``client:{client_id}@{authority}`` to mark it as being authenticated on behalf of an auth_client """ def __init__(self, check=None): if check is None: check = AuthClientPolicy.check self._basic_auth_policy = BasicAuthAuthenticationPolicy(check=check) def unauthenticated_userid(self, request): """ Return the forwarded userid or the auth_client's id If a forwarded user header is set, return the ``userid`` (its value) Otherwise return the username parsed from the Basic Auth header :return: :py:attr:`h.models.user.User.userid` or :py:attr:`h.models.auth_client.AuthClient.id` :rtype: str """ forwarded_userid = AuthClientPolicy._forwarded_userid(request) if forwarded_userid is not None: return forwarded_userid # username from BasicAuth header return self._basic_auth_policy.unauthenticated_userid(request) def authenticated_userid(self, request): """ Return any forwarded userid or None Rely mostly on :py:meth:`pyramid.authentication.BasicAuthAuthenticationPolicy.authenticated_userid`, but don't actually return a ``userid`` unless there is a forwarded user header set—the auth client itself is not a "user" Although this looks as if it trusts the return value of :py:meth:`pyramid.authentication.BasicAuthAuthenticationPolicy.authenticated_userid` irrationally, rest assured that :py:meth:`~h.auth.policy.AuthClientPolicy.check` will always be called (via :py:meth:`pyramid.authentication.BasicAuthAuthenticationPolicy.callback`) before any non-None value is returned. :rtype: :py:attr:`h.models.user.User.userid` or ``None`` """ forwarded_userid = AuthClientPolicy._forwarded_userid(request) # only evaluate setting an authenticated_userid if forwarded user is present if forwarded_userid is None: return None # username extracted from BasicAuth header auth_userid = self._basic_auth_policy.unauthenticated_userid(request) # authentication of BasicAuth and forwarded user—this will invoke check callback_ok = self._basic_auth_policy.callback(auth_userid, request) if callback_ok is not None: return ( forwarded_userid ) # This should always be a userid, not an auth_client id def effective_principals(self, request): """ Return a list of principals for the request This will concatenate the principals returned by :py:meth:`~h.auth.policy.AuthClientPolicy.check` (which is a list or None) with Pyramid's system principal(s). If :py:meth:`~h.auth.policy.AuthClientPolicy.check` returns None—that is, if authentication is unsuccessful—the returned principals will only contain Pyramid's ``system.Everyone`` principal (and Pyramid will not consider the request as authenticated). :rtype: list ``['system.Everyone']`` concatenated with any principals from a successful authentication """ return self._basic_auth_policy.effective_principals(request) def remember(self, request, userid, **kw): """Not implemented for basic auth client policy.""" return [] def forget(self, request): """Not implemented for basic auth client policy.""" return [] @staticmethod def check(username, password, request): """ Return list of appropriate principals or None if authentication is unsuccessful. Validate the basic auth credentials from the request by matching them to an auth_client record in the DB. If an HTTP ``X-Forwarded-User`` header is present in the request, this represents the intent to authenticate "on behalf of" a user within the auth_client's authority. If this header is present, the user indicated by its value (a :py:attr:`h.models.user.User.userid`) _must_ exist and be within the auth_client's authority, or authentication will fail. :param username: username parsed out of Authorization header (Basic) :param password: password parsed out of Authorization header (Basic) :returns: additional principals for the auth_client or None :rtype: list or None """ client_id = username client_secret = password # validate that the credentials in BasicAuth header # match an AuthClient record in the db client = util.verify_auth_client(client_id, client_secret, request.db) if client is None: return None forwarded_userid = AuthClientPolicy._forwarded_userid(request) if ( forwarded_userid is None ): # No forwarded user; set principals for basic auth_client return util.principals_for_auth_client(client) user_service = request.find_service(name="user") try: user = user_service.fetch(forwarded_userid) except ValueError: # raised if userid is invalid format return None # invalid user, so we are failing here if user and user.authority == client.authority: return util.principals_for_auth_client_user(user, client) return None @staticmethod def _forwarded_userid(request): """Return forwarded userid or None""" userid = request.headers.get("X-Forwarded-User", None) if userid is not None: # In Python 2 request header values are byte strings, so we need to # decode them to get unicode. # FIXME: Remove this once we've moved to Python 3. userid = pyramid.compat.text_(userid) return userid