Tres Seaver
2010-10-01 9a8e60493792285aaa660c6839d739174fb30553
Added a ``login`` method to the ``repoze.who.api.API`` object.

This method is convenience for application-driven login code, which would
otherwise need to use private methods of the API, and reach doen into its
plugins.

5 files modified
227 ■■■■■ changed files
CHANGES.txt 6 ●●●●● patch | view | raw | blame | history
docs/api.rst 65 ●●●●● patch | view | raw | blame | history
repoze/who/api.py 22 ●●●●● patch | view | raw | blame | history
repoze/who/interfaces.py 18 ●●●●● patch | view | raw | blame | history
repoze/who/tests/test_api.py 116 ●●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -1,6 +1,12 @@
repoze.who Changelog
====================
After 2.0a3 (unreleased)
------------------------
- Added a ``login`` method to the ``repoze.who.api.API`` object, as a
  convenience for application-driven login code.
2.0a3 (2010-09030)
------------------
docs/api.rst
@@ -71,6 +71,71 @@
       who_api = context.who_api_factory(request.environ)
.. _writing_custom_login_view:
Writing a Custom Login View
---------------------------
:class:`repoze.who.api.API` provides a helper method to assist developers
who want to control the details of the login view.  The following
BFG example illustrates how this API might be used:
.. code-block:: python
   :linenos:
    def login_view(context, request):
        message = ''
        who_api = get_api(request.environ)
        if 'form.login' in request.POST:
            creds = {}
            creds['login'] = request.POST['login']
            creds['password'] = request.POST['password']
            authenticated, headers = who_api.login(creds)
            if authenticated:
                return HTTPFound(location='/', headers=headers)
            message = 'Invalid login.'
        else:
            # Forcefully forget any existing credentials.
            _, headers = who_api.login({})
        request.response_headerlist = headers
        if 'REMOTE_USER' in request.environ:
            del request.environ['REMOTE_USER']
        return {'message': message}
This application is written as a "hybrid":  the :mod:`repoze.who` middleware
injects the API object into the WSGI enviornment on each request.
- In line 4, this  application extracts the API object from the environ
  using :func:`repoze.who.api:get_api`.
- Lines 6 - 8 fabricate a set of credentials, based on the values the
  user entered in the form.
- In line 9, the application asks the API to authenticate those credentials,
  returning an identity and a set of respones headers.
- Lines 10 and 11 handle the case of successful authentication:  in this
  case, the application redirects to the site root, setting the headers
  returned by the API object, which will "remember" the user across requests.
- Line 13 is reached on failed login.  In this case, the headers returned
  in line 9 will be "forget" headers, clearing any existing cookies or other
  tokens.
- Lines 14 - 16 perform a "fake" login, in order to get the "forget" headers.
- Line 18 sets the "forget" headers to clear any authenticated user for
  subsequent requests.
- Lines 19 - 20 clear any authenticated user for the current request.
- Line 22 returns any message about a failed login to the rendering template.
.. _interfaces:
Interfaces
repoze/who/api.py
@@ -113,6 +113,7 @@
        (self.interface_registry,
         self.name_registry) = make_registries(identifiers, authenticators,
                                               challengers, mdproviders)
        self.identifiers = identifiers
        self.authenticators = authenticators
        self.challengers = challengers
        self.mdproviders = mdproviders
@@ -122,7 +123,7 @@
        classification = self.classification = (request_classifier and
                                                request_classifier(environ))
        logger and logger.info('request classification: %s' % classification)
    def authenticate(self):
        ids = self._identify()
@@ -229,6 +230,25 @@
                                        % (identifier, headers))
        return headers
    def login(self, credentials, identifier_name=None):
        """ See IAPI.
        """
        if identifier_name is not None:
            identifier = self.name_registry[identifier_name]
        else:
            identifier = self.identifiers[0][1]
        # Pretend that the given identifier extracted the identity.
        authenticated = self._authenticate([(identifier, credentials)])
        if authenticated:
            # and therefore can remember it
            rank, plugin, identifier, identity, userid = authenticated[0]
            headers = identifier.remember(self.environ, identity)
            return identity, headers
        else:
            # or forget it
            headers = identifier.forget(self.environ, None)
            return None, headers
    def _identify(self):
        """ See IAPI.
        """
repoze/who/interfaces.py
@@ -47,6 +47,24 @@
        o If 'identity' is not passed, use the identity in the environment.
        """
    def login(credentials, identifier_name=None):
        """ -> (identity, headers)
        o This is an API for browser-based application login forms.
        o If 'identifier_name' is passed, use it to look up the identifier;
          othewise, use the first configured identifier.
        o Attempt to authenticate 'credentials' as though the identifier
          had extracted them.
        o On success, 'identity' will be authenticated mapping, and 'headers'
          will be "remember" headers.
        o On failure, 'identity' will be None, and response_headers will be
          "forget" headers.
        """
class IPlugin(Interface):
    pass
repoze/who/tests/test_api.py
@@ -597,6 +597,122 @@
        self.failUnless(logger._info[1].endswith(repr(HEADERS)))
        self.assertEqual(len(logger._debug), 0)
    def test_login_w_identifier_name_hit(self):
        REMEMBER_HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')]
        FORGET_HEADERS = [('Spam', 'Blah')]
        class _Identifier:
            def identify(self, environ):
                pass
            def remember(self, environ, identity):
                return REMEMBER_HEADERS
            def forget(self, environ, identity):
                return FORGET_HEADERS
        class _BogusIdentifier:
            def identify(self, environ):
                pass
            def remember(self, environ, identity):
                pass
            def forget(self, environ, identity):
                pass
        authenticator = DummyAuthenticator('chrisid')
        environ = self._makeEnviron()
        identifiers = [('bogus', _BogusIdentifier()),
                       ('valid', _Identifier()),
                      ]
        api = self._makeOne(identifiers=identifiers,
                            authenticators=[('authentic', authenticator)],
                            environ=environ)
        identity, headers = api.login({'login': 'chrisid'}, 'valid')
        self.assertEqual(identity['repoze.who.userid'], 'chrisid')
        self.assertEqual(headers, REMEMBER_HEADERS)
    def test_login_wo_identifier_name_hit(self):
        REMEMBER_HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')]
        FORGET_HEADERS = [('Spam', 'Blah')]
        class _Identifier:
            def identify(self, environ):
                pass
            def remember(self, environ, identity):
                return REMEMBER_HEADERS
            def forget(self, environ, identity):
                return FORGET_HEADERS
        class _BogusIdentifier:
            def identify(self, environ):
                pass
            def remember(self, environ, identity):
                pass
            def forget(self, environ, identity):
                pass
        authenticator = DummyAuthenticator('chrisid')
        environ = self._makeEnviron()
        identifiers = [('valid', _Identifier()),
                       ('bogus', _BogusIdentifier()),
                      ]
        api = self._makeOne(identifiers=identifiers,
                            authenticators=[('authentic', authenticator)],
                            environ=environ)
        identity, headers = api.login({'login': 'chrisid'})
        self.failUnless(identity)
        self.assertEqual(headers, REMEMBER_HEADERS)
    def test_login_w_identifier_name_miss(self):
        REMEMBER_HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')]
        FORGET_HEADERS = [('Spam', 'Blah')]
        class _Identifier:
            def identify(self, environ):
                pass
            def remember(self, environ, identity):
                return REMEMBER_HEADERS
            def forget(self, environ, identity):
                return FORGET_HEADERS
        class _BogusIdentifier:
            def identify(self, environ):
                pass
            def remember(self, environ, identity):
                pass
            def forget(self, environ, identity):
                pass
        authenticator = DummyFailAuthenticator()
        environ = self._makeEnviron()
        identifiers = [('bogus', _BogusIdentifier()),
                       ('valid', _Identifier()),
                      ]
        api = self._makeOne(identifiers=identifiers,
                            authenticators=[('authentic', authenticator)],
                            environ=environ)
        identity, headers = api.login({'login': 'notchrisid'}, 'valid')
        self.assertEqual(identity, None)
        self.assertEqual(headers, FORGET_HEADERS)
    def test_login_wo_identifier_name_miss(self):
        REMEMBER_HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')]
        FORGET_HEADERS = [('Spam', 'Blah')]
        class _Identifier:
            def identify(self, environ):
                pass
            def remember(self, environ, identity):
                return REMEMBER_HEADERS
            def forget(self, environ, identity):
                return FORGET_HEADERS
        class _BogusIdentifier:
            def identify(self, environ):
                pass
            def remember(self, environ, identity):
                pass
            def forget(self, environ, identity):
                pass
        authenticator = DummyFailAuthenticator()
        environ = self._makeEnviron()
        identifiers = [('valid', _Identifier()),
                       ('bogus', _BogusIdentifier()),
                      ]
        api = self._makeOne(identifiers=identifiers,
                            authenticators=[('authentic', authenticator)],
                            environ=environ)
        identity, headers = api.login({'login': 'notchrisid'})
        self.assertEqual(identity, None)
        self.assertEqual(headers, FORGET_HEADERS)
    def test__identify_success(self):
        environ = self._makeEnviron()
        credentials = {'login':'chris', 'password':'password'}