Ejemplo n.º 1
0
Archivo: policy.py Proyecto: y3g0r/h
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)
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
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