Chris McDonough
2008-02-24 d85ba6e4ef1ef25fb7b6325d31553f0852149558
Middleware implementation (ingress only).

Better interfaces and classifier configuration.

2 files added
6 files modified
858 ■■■■ changed files
README.txt 197 ●●●●● patch | view | raw | blame | history
repoze/pam/classifiers.py 59 ●●●●● patch | view | raw | blame | history
repoze/pam/etc/sample-config.ini 39 ●●●●● patch | view | raw | blame | history
repoze/pam/interfaces.py 91 ●●●●● patch | view | raw | blame | history
repoze/pam/middleware.py 125 ●●●●● patch | view | raw | blame | history
repoze/pam/plugins/basicauth.py 11 ●●●● patch | view | raw | blame | history
repoze/pam/plugins/htpasswd.py 10 ●●●● patch | view | raw | blame | history
repoze/pam/tests.py 326 ●●●●● patch | view | raw | blame | history
README.txt
@@ -18,8 +18,8 @@
  operation implied by the request).  This is also the domain of the
  WSGI application.
 
  It attemtps to reuse implementations from AuthKit for some of its
  functionality.
  It attemtps to reuse implementations from AuthKit and paste.auth for
  some of its functionality.
Middleware Responsibilities
@@ -29,8 +29,8 @@
  the request to continue to a downstream WSGI application.
  repoze.pam's middleware has one major function on egress: it
  examines the WSGI environment (or catches an exception) and
  conditionally challenges for credentials.
  examines the headers set by the downstream application or the WSGI
  environment and conditionally challenges for credentials.
PasteDeploy Configuration
@@ -40,6 +40,33 @@
    [filter:pam]
    use = egg:repoze.pam#pam
    config_file = %(here)s/pam.ini
Classifiers
  repoze.pam "classifies" both the request (on middleware ingress) and
  the response (on middleware egress).
  Request classification happens on middleware ingress, before
  extraction and authentication.  A request from a browser might be
  classified a different way that a request from an XML-RPC client.
  repoze.pam uses request classifiers to decide which other components
  to consult during subsequent identification, authorization, and
  challenge steps.  Extraction and authenticator plugins are free to
  advertise themselves as willing to participate in identification and
  authorization for a request based on this classification.
  Response classification happens on middleware egress, before
  challenge.  A response from a an application can be classified
  arbitrarily.  repoze.pam uses response classifiers to decide which
  challenge plugins are willing to examine the response, and
  potentially actuate a challenge.  Challenge plugins are free to
  advertise themselves as willing to participate based on the response
  classification.
  The classification system is pluggable.  repoze.pam provides a set
  of default classifiers that you may use.  You may extend the
  classification system by making repoze.pam aware of different
  classifier implementations.
Plugins
@@ -55,22 +82,67 @@
  a WSGI request, and gives some subset of them a chance to influence
  what is added to the WSGI environment.
Ingress Stages
  repoze.pam performs the following operations in the following order
  during request ingress:
  1.  Request Classification
      The WSGI environment is examined and the request is classified
      into one "type" of request.  The callable named as
      'request_classifer=' within the '[classifiers]' section is used
      to perform the classification.  It returns a value that is
      considered to be the request classification.
  2.  Extraction
      Extractors which nominate themselves as willing to extract data
      for a particular class of request (as provided by the request
      classifier) will be consulted to retrieve login and password
      data from the environment.  For example, a basic auth extractor
      might use the WWW-Authenticate header to find login and password
      information.
  3.  Authentication
      Authenticators which nominate themselves as willing to
      authenticate for a particular class of request will be consulted
      to compare login and password information provided by the
      extraction plugin that returned a set of credentials.  For
      example, an htpasswd authenticator might look in a file for a
      user record matching the login.  If it finds one, and if the
      password listed in the record matches the password found by the
      extractor, the userid of the user would be returned (which would
      be the same as the login name).
Egress Stages
  repoze.pam performs the following operations in the following order
  during request egress:
  1.  Response Classification
      The WSGI environment and the headers returned by the downstream
      application are examined and the request is classified into one
      "type" of request.  The callable named as 'response_classifer='
      within the '[classifiers]' section is used to perform the
      classification.  It returns a value that is considered the
      classification.
  2.  Challenge
      Challengers which nominate themselves as willing to execute a
      challenge for a particular class of request (as provided by the
      response classifier) will be consulted.  The challenger plugins
      can use application-returned headers and the WSGI environment to
      determine what sort of operation should be performed to actuate
      the challenge.  For example, if the application sets a 401
      Unauthorized header in the response headers, a challenge plugin
      might redirect the user to a login page by setting additional
      headers in the response headers.
Plugin Types
  Classification Plugins
    repoze.pam "classifies" each request.  For example, a request from
    a browser might be classified a different way that a request from
    an XML-RPC client.  repoze.pam uses request classifiers to decide
    which other components to consult during subsequent identification
    and authorization steps.  Other components advertise themselves as
    willing to participate in identification and authorization for a
    request based on this classification.
    The classification system is pluggable.  repoze.pam provides a
    number of default classifiers that you may use.  You may extend
    the classification system by making repoze.pam aware of new
    classifier implementations.
  Extractor Plugins
@@ -105,12 +177,11 @@
Configuration File Example
  repoze.pam is configured using a ConfigParser-style .INI file.  The
  configuration file has five main types of sections: plugin sections,
  a classifiers section, an authenticators section, a challengers
  section, and an extractors section.  Each "plugin" section defines a
  configuration for a particular plugin.  The classifiers,
  authenticators, challengers, and extractors sections refer to these
  plugins to form a site configuration.
  configuration file has four main types of sections: plugin sections,
  an authenticators section, a challengers section, and an extractors
  section.  Each "plugin" section defines a configuration for a
  particular plugin.  The classifiers, authenticators, and extractors
  sections refer to these plugins to form a site configuration.
Example repoze.pam Configuration File
@@ -118,98 +189,86 @@
  configure the repoze.pam middleware.  A set of plugins are defined,
  and they are referred to by following non-plugin sections.
  In the below configuration, seven plugins are defined.  The
  In the below configuration, four plugins are defined.  The
  cookieauth and basicauth plugins are nominated to act as both
  challenger and extractor plugins.  The filusers and sqlusers plugins
  are nominated to act as authenticator plugins.  The browser, dav,
  and xmlrpc plugins are nominated to act as classifier plugins::
  are nominated to act as authenticator plugins.
    [plugin:basicauth]
    # challenge and extraction
    use = egg:repoze.pam#basicauth
    # challenge
    realm = repoze
    requests =
        browser
        dav
        xmrpc
    [plugin:cookieauth]
    # extraction, challenge, credentials update, credentials reset
    # challenge and extraction
    use = egg:repoze.pam#cookieauth
    # challenge
    requests = browser
    login_path = /login_form
    # extraction
    cookie_name = repoze.pam.auth
    form_name_name = __ac_name
    form_password_name = __ac_password
    [plugin:fileusers]
    use = egg:repoze.pam#fileusersource
    # authentication
    use = egg:repoze.pam#fileusersource
    filename = %(here)s/users.txt
    encryptpwd = egg:repoze.pam#shaencrypt
    [plugin:sqlusers]
    use = egg:repoze.pam#squsersource
    # authentication
    use = egg:repoze.pam#squsersource
    db = sqlite://database?user=foo&pass=bar
    get_userinfo = select id, password from users
    encryptpwd = egg:repoze.pam#shaencrypt
    [plugin:browser]
    use = egg:repoze.pam#browserchooser
    [plugin:dav]
    dav = egg:repoze.pam#davchooser
    [plugin:xmlrpc]
    xmlrpc = egg:repoze.pam#xmlrpcchooser
    [classifiers]
    plugins =
          browser
          dav
          xmlrpc
    ingress_classifier = egg:repoze.pam#defaultingressclassifier
    egress_classifier = egg:repoze.pam#defaultegressclassifier
    [extractors]
    # plugin_name:ingressclassifier_name:.. or just plugin_name (good for any)
    plugins =
          cookieauth
          cookieauth:browser
          basicauth
    [authenticators]
    # plugin_name (ingress classifiers ignored)
    plugins =
          fileusers
          fileusers
          sqlusers
    [challengers]
    # plugin_name:egressclassifier_name:.. or just plugin_name (good for any)
    plugins =
          cookieauth
          cookieauth:browser
          basicauth
Further Description of Example Config
  The basicauth plugin configuration nominates itself as willing to
  participate in requests classified as "browser", "dav", and "xmlrpc"
  (via the "requests" key).  The cookieauth plugin configuration
  nominates itself as willing to participate in requests classified as
  "browser".
  The basicauth section configures a plugin that does extraction and
  challenge for basic auth credentials.  The cookieauth section
  configures a plugin that does extraction and challenge for cookie
  auth credentials.  The fileusers plugin obtains its user info from a
  file.  The sqlusers plugin obtains its user info from a sqlite
  database.
  The fileusers plugin obtains its user info from a file.  The
  sqlusers plugin obtains its user info from a sqlite database.
  The classifiers section indicates that the classifiers named in the
  plugins = line each has a chance to classify the request.
  The extractors seciton provides an ordered list of plugins that are
  The extractors section provides an ordered list of plugins that are
  willing to provide extraction capability.  These will be consulted
  in the defined order, so the system will look for credentials using
  the cookie auth plugin, then the basic auth plugin.
  in the defined order.  The tokens on each line of the plugin= key
  are in the form "plugin_name:classifier" (or just "plugin_name" if
  the plugin can be consulted regardless of the classification of the
  request).  The configuration above indicates that the system will
  look for credentials using the cookie auth plugin (if the request is
  classified as a browser request), then the basic auth plugin
  (unconditionally).
  The authenticators section provides an ordered list of plugins that
  provide authenticator capability.  These will be consulted in the
  defined order, so the system will look for users in the file, then
  in the sql database when attempting to validate credentials.
  in the sql database when attempting to validate credentials.  No
  classification prefixes are given to restrict which of the two
  plugins are used, so both plugins are consulted regardless of the
  classification of the request.
  The challengers section provides an ordered list of plugins that
  provide challenger capability.  These will be consulted in the
repoze/pam/classifiers.py
New file
@@ -0,0 +1,59 @@
from paste.httpheaders import USER_AGENT
from paste.httpheaders import REQUEST_METHOD
from paste.httpheaders import CONTENT_TYPE
from zope.interface import implements
from repoze.pam.interfaces import IRequestClassifier
from repoze.pam.interfaces import IResponseClassifier
class DefaultRequestClassifier(object):
    implements(IRequestClassifier)
    _DAV_METHODS = (
        'OPTIONS',
        'PROPFIND',
        'PROPPATCH',
        'MKCOL',
        'LOCK',
        'UNLOCK',
        'TRACE',
        'DELETE',
        'COPY',
        'MOVE'
        )
    _DAV_USERAGENTS = ( # convenience, override as necessary
        'Microsoft Data Access Internet Publishing Provider',
        'WebDrive',
        'Zope External Editor',
        'WebDAVFS',
        'Goliath',
        'neon',
        'davlib',
        'wsAPI',
        'Microsoft-WebDAV'
        )
    def __call__(self, environ):
        """ Returns one of the classifiers 'dav', 'xmlpost', or
        'browser', depending on the imperative logic below"""
        request_method = REQUEST_METHOD(environ)
        if request_method in self._DAV_METHODS:
            return 'dav'
        useragent = USER_AGENT(environ)
        if useragent:
            for agent in self._DAV_USERAGENTS:
                if useragent.find(agent) != -1:
                    return 'dav'
        if request_method == 'POST':
            if CONTENT_TYPE(environ) == 'text/xml':
                return 'xmlpost'
        return 'browser'
class DefaultResponseClassifier(object):
    implements(IResponseClassifier)
    def __call__(self, environ, request_classification, headers, exception):
        """ By default, return the request classification """
        return request_classification
repoze/pam/etc/sample-config.ini
@@ -1,64 +1,49 @@
[plugin:basicauth]
# challenge and extraction
# the basicauth plugin performs challenge and extraction
use = egg:repoze.pam#basicauth
# challenge
realm = repoze
requests =
    browser
    dav
    xmrpc
[plugin:cookieauth]
# extraction, challenge, credentials update, credentials reset
# the cookieauth plugin performs challenge and extraction
use = egg:repoze.pam#cookieauth
# challenge
requests = browser
login_path = /login_form
# extraction
cookie_name = repoze.pam.auth
form_name_name = __ac_name
form_password_name = __ac_password
[plugin:fileusers]
# the fileusers plugin performs authentication
use = egg:repoze.pam#fileusersource
# authentication
filename = %(here)s/users.txt
encryptpwd = egg:repoze.pam#shaencrypt
[plugin:sqlusers]
# the sqlusers plugin performs authentication
use = egg:repoze.pam#squsersource
# authentication
db = sqlite://database?user=foo&pass=bar
get_userinfo = select id, password from users
encryptpwd = egg:repoze.pam#shaencrypt
[plugin:browser]
use = egg:repoze.pam#browserchooser
[plugin:dav]
dav = egg:repoze.pam#davchooser
[plugin:xmlrpc]
xmlrpc = egg:repoze.pam#xmlrpcchooser
[classifiers]
plugins =
      browser
      dav
      xmlrpc
request_classifier = egg:repoze.pam#defaultrequestclassifier
response_classifier = egg:repoze.pam#defaultresponseclassifier
[extractors]
# plugin_name:requestclassifier_result:... or just plugin_name (good for any)
plugins =
      cookieauth
      cookieauth:browser
      basicauth
[authenticators]
# plugin_name:requestclassifier_result:... or just plugin_name (good for any)
plugins =
      fileusers
      fileusers
      sqlusers
[challengers]
# plugin_name:responseclassifier_result:.. or just plugin_name (good for any)
plugins =
      cookieauth
      cookieauth:browser
      basicauth
repoze/pam/interfaces.py
@@ -1,5 +1,29 @@
from zope.interface import Interface
class IRequestClassifier(Interface):
    """ On ingress: classify a request.
    This interface is responsible for returning a string representing
    a classification name based on introspection of the WSGI
    environment (environ).
    """
    def __call__(environ):
        """ Return a string representing the classification of this
        request. """
class IResponseClassifier(Interface):
    """ On egress: classify a response.
    This interface is responsible for returning a string representing
    a classification name based on introspection of the ingress
    classification, the WSGI environment (environ), the headers
    returned in the response (headers), or the exception raised by a
    downstream application.
    """
    def __call__(environ, request_classification, headers, exception):
        """ Return a string representing the classification of this
        request. """
class IExtractorPlugin(Interface):
    """ On ingress: Extract credentials from the WSGI environment.
@@ -20,6 +44,9 @@
        o Return an empty mapping to indicate that the plugin found no
          appropriate credentials.
        o Only extraction plugins which match one of the the current
          request's classifications will be asked to perform extraction.
        """
    
class IAuthenticatorPlugin(Interface):
@@ -38,72 +65,26 @@
          be the value placed into the REMOTE_USER key in the environ
          to be used by downstream applications.
        o If the credentials cannot be authenticated, return None.
        o If the credentials cannot be authenticated, None will be returned.
        """
class IChallengerPlugin(Interface):
    """ On ingress: Initiate a challenge to the user to provide credentials.
    """ On egress: Initiate a challenge to the user to provide credentials.
        o 'environ' is the WSGI environment.
        o Challenge plugins have an attribute 'protocols' representing
          the protocols the plugin operates under, defaulting to None.
        o Only challenge plugins which match the current request's
          protocol will be asked to perform a challenge.
        o If no challenge plugins satisfy the current request's
          protocol, a default exception will be raised.
        o If no challenge plugins themselves raise an exception, a
          default exception will be raised.
        o Only challenge plugins which match one of the the current
          request's classifications will be asked to perform a
          challenge.
    """
    def challenge(environ):
    def challenge(environ, request_classifier, headers, exception):
        """ Examine the environ and perform one of the following two
        actions:
        - Raise an exception which can be interpreted by
          left-hand-side middleware should gather credentials
          (present a form, show a basic auth dialog).
        - Do nothing.
        """ Examine the values passed in and perform an arbitrary action
        (usually mutating environ or raising an exception) to cause a
        challenge to be raised to the user.
        The return value of this method is ignored.
        """
class ICredentialsUpdaterPlugin(Interface):
    """ On egress:  user has changed her password.
    This interface is not responsible for the actual password change,
    it is used after a successful password change event in a
    downstream application.
    It is called when the repoze.pam middleware intercepts a
    'repoze.pam.update' key in the WSGI environ during egress.
    """
    def update(environ, login, new_password):
        """ Scribble as appropriate.
        """
class ILogoutPlugin(Interface):
    """ On egress:  user has logged out.
    It is called when the repoze.pam middleware intercepts an
    ResetCredentialsException from downstream middleware.
    It is called when the repoze.pam middleware intercepts a
    'repoze.pam.reset' key in the WSGI environ during egress.
    """
    def logout(environ):
        """ Scribble as appropriate.
        """
repoze/pam/middleware.py
New file
@@ -0,0 +1,125 @@
from repoze.pam.interfaces import IAuthenticatorPlugin
from repoze.pam.interfaces import IExtractorPlugin
from repoze.pam.interfaces import IChallengerPlugin
class PluggableAuthenticationMiddleware(object):
    def __init__(self, app, registry, request_classifier, response_classifier,
                 add_credentials=True):
        self.registry = registry
        self.app = app
        self.request_classifier = request_classifier
        self.response_classififer = response_classifier
        self.add_credentials = add_credentials
    def __call__(self, environ, start_response):
        request_classification = self.on_ingress(environ)
        return self.app(environ, start_response)
        # XXX on_egress
    def on_ingress(self, environ):
        classification = self.request_classifier(environ)
        credentials = self.extract(environ, classification)
        if credentials:
            userid = self.authenticate(environ, credentials, classification)
        if self.add_credentials:
            credentials['userid'] = userid
            environ['repoze.pam.credentials'] = credentials
        if userid:
            environ['REMOTE_USER'] = userid
        return classification
    def on_egress(self, environ, request_classifier, headers, exception):
        self.challenge(environ, request_classifier, headers, exception)
    def extract(self, environ, classifier):
        extractor_candidates = self.registry.get(IExtractorPlugin)
        extractors = self._match_classifier(extractor_candidates, classifier)
        for extractor in extractors:
            creds = extractor.extract(environ)
            if creds:
                # XXX PAS returns all credentials (it fully iterates over all
                # extraction plugins)
                return creds
        return {}
    def authenticate(self, environ, credentials, classification):
        # on ingress
        userid = None
        auth_candidates = self.registry.get(IAuthenticatorPlugin)
        authenticators = self._match_classifier(auth_candidates, classification)
        for authenticator in authenticators:
            userid = authenticator.authenticate(environ, credentials)
            if userid:
                # XXX PAS calls all authenticators (it fully iterates over all
                # authenticator plugins)
                break
        return userid
    def challenge(self, environ, request_classification, headers, exception):
        # on egress
        classification = self.response_classifier(environ,
                                                  request_classification,
                                                  headers,
                                                  exception)
        challenger_candidates = self.registry.get(IChallengerPlugin)
        challengers = self._match_classifier(challenger_candidates,
                                              classification)
        for challenger in challengers:
            new_headers, new_status = challengers.challenge(environ)
    def _match_classifier(self, plugins, classifier):
        result = []
        for plugin in plugins:
            plugin_classifiers = getattr(plugin, 'classifiers', set())
            if not plugin_classifiers: # good for any
                result.append(plugin)
                continue
            if classifier in plugin_classifiers:
                result.append(plugin)
        return result
def make_middleware(app, global_conf, config_file=None):
    if config_file is None:
        raise ValueError('config_file must be specified')
    return PluggableAuthenticationMiddleware(app)
def make_test_middleware(app, global_conf):
    # no config file required
    from repoze.pam.plugins.basicauth import BasicAuthPlugin
    from repoze.pam.plugins.htpasswd import HTPasswdPlugin
    basicauth = BasicAuthPlugin('repoze.pam')
    basicauth.classifiers = set() # good for any
    from StringIO import StringIO
    io = StringIO('chrism:aajfMKNH1hTm2\n')
    htpasswd = HTPasswdPlugin(io)
    htpasswd.classifiers = set() # good for any
    registry = make_registry((htpasswd,), (basicauth,), (basicauth,))
    class DummyClassifier:
        def classify(self, *arg, **kw):
            return None
    classifier = DummyClassifier()
    middleware = PluggableAuthenticationMiddleware(app, registry,
                                                   classifier, classifier)
    return middleware
def verify(plugins, iface):
    from zope.interface.verify import verifyObject
    for plugin in plugins:
        verifyObject(iface, plugin, tentative=True)
def make_registry(extractors, authenticators, challengers):
    registry = {}
    verify(extractors, IExtractorPlugin)
    registry[IExtractorPlugin] = extractors
    verify(authenticators, IAuthenticatorPlugin)
    registry[IAuthenticatorPlugin] = authenticators
    verify(challengers, IChallengerPlugin)
    registry[IChallengerPlugin] = challengers
    return registry
repoze/pam/plugins/basicauth.py
@@ -13,12 +13,11 @@
    implements(IChallengerPlugin, IExtractorPlugin)
    
    def __init__(self, realm, requests):
    def __init__(self, realm):
        self.realm = realm
        self.requests = requests
    # IChallengerPlugin
    def challenge(self, environ):
    def challenge(self, environ, request_classifier, headers, exception):
        head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
        raise HTTPUnauthorized(headers=head)
@@ -46,5 +45,7 @@
        return {}
def make_plugin(pam_conf, realm, requests):
    return BasicAuthPlugin(realm, requests)
def make_plugin(pam_conf, realm='basic'):
    plugin = BasicAuthPlugin(realm)
    return plugin
repoze/pam/plugins/htpasswd.py
@@ -3,7 +3,7 @@
from repoze.pam.interfaces import IAuthenticatorPlugin
from repoze.pam.utils import resolveDotted
class HTPasswdAuthenticator(object):
class HTPasswdPlugin(object):
    implements(IAuthenticatorPlugin)
@@ -43,8 +43,12 @@
    salt = hashed[:2]
    return hashed == crypt(password, salt)
def make_plugin(pam_conf, filename, check_fn):
def make_plugin(pam_conf, filename=None, check_fn=None):
    if filename is None:
        raise ValueError('filename must be specified')
    if check_fn is None:
        raise ValueError('check_fn must be specified')
    check = resolveDotted(check_fn)
    return HTPasswdAuthenticator(filename, check)
    return HTPasswdPlugin(filename, check)
    
repoze/pam/tests.py
@@ -11,6 +11,204 @@
            environ.update(kw)
        return environ
class TestMiddleware(Base):
    def _getTargetClass(self):
        from repoze.pam.middleware import PluggableAuthenticationMiddleware
        return PluggableAuthenticationMiddleware
    def _makeOne(self,
                 app=None,
                 registry=None,
                 request_classifier=None,
                 response_classifier=None,
                 add_credentials=True
                 ):
        if registry is None:
            registry = {}
            from repoze.pam.interfaces import IAuthenticatorPlugin
            from repoze.pam.interfaces import IExtractorPlugin
            from repoze.pam.interfaces import IChallengerPlugin
            registry[IExtractorPlugin] = [ DummyExtractor() ]
            registry[IAuthenticatorPlugin] = [ DummyAuthenticator() ]
            registry[IChallengerPlugin] = [ DummyChallenger() ]
        if app is None:
            app = DummyApp()
        if request_classifier is None:
            request_classifier = DummyRequestClassifier()
        if response_classifier is None:
            response_classifier = DummyResponseClassifier()
        mw = self._getTargetClass()(app, registry, request_classifier,
                                    response_classifier, add_credentials)
        return mw
    def test_extract_success(self):
        environ = self._makeEnviron()
        mw = self._makeOne()
        creds = mw.extract(environ, None)
        self.assertEqual(creds['login'], 'chris')
        self.assertEqual(creds['password'], 'password')
    def test_extract_fail(self):
        environ = self._makeEnviron()
        from repoze.pam.interfaces import IExtractorPlugin
        registry = {
            IExtractorPlugin:[DummyNoResultsExtractor()]
            }
        mw = self._makeOne(registry=registry)
        creds = mw.extract(environ, None)
        self.assertEqual(creds, {})
    def test_extract_success_skip_noresults(self):
        environ = self._makeEnviron()
        mw = self._makeOne()
        from repoze.pam.interfaces import IExtractorPlugin
        registry = {
            IExtractorPlugin:[DummyNoResultsExtractor(), DummyExtractor()]
            }
        mw = self._makeOne(registry=registry)
        creds = mw.extract(environ, None)
        self.assertEqual(creds['login'], 'chris')
        self.assertEqual(creds['password'], 'password')
    def test_extract_success_firstwins(self):
        environ = self._makeEnviron()
        mw = self._makeOne()
        from repoze.pam.interfaces import IExtractorPlugin
        extractor1 = DummyExtractor({'login':'fred','password':'fred'})
        extractor2 = DummyExtractor({'login':'bob','password':'bob'})
        registry = {
            IExtractorPlugin:[extractor1, extractor2]
            }
        mw = self._makeOne(registry=registry)
        creds = mw.extract(environ, None)
        self.assertEqual(creds['login'], 'fred')
        self.assertEqual(creds['password'], 'fred')
    def test_extract_find_implicit_classifier(self):
        environ = self._makeEnviron()
        mw = self._makeOne()
        from repoze.pam.interfaces import IExtractorPlugin
        extractor1 = DummyExtractor({'login':'fred','password':'fred'})
        extractor1.classifiers = set(['nomatch'])
        extractor2 = DummyExtractor({'login':'bob','password':'bob'})
        registry = {
            IExtractorPlugin:[extractor1, extractor2]
            }
        mw = self._makeOne(registry=registry)
        creds = mw.extract(environ, None)
        self.assertEqual(creds['login'], 'bob')
        self.assertEqual(creds['password'], 'bob')
    def test_extract_find_explicit_classifier(self):
        environ = self._makeEnviron()
        mw = self._makeOne()
        from repoze.pam.interfaces import IExtractorPlugin
        extractor1 = DummyExtractor({'login':'fred','password':'fred'})
        extractor1.classifiers = set(['nomatch'])
        extractor2 = DummyExtractor({'login':'bob','password':'bob'})
        extractor2.classifiers = set(['match'])
        registry = {
            IExtractorPlugin:[extractor1, extractor2]
            }
        mw = self._makeOne(registry=registry)
        creds = mw.extract(environ, 'match')
        self.assertEqual(creds['login'], 'bob')
        self.assertEqual(creds['password'], 'bob')
    def test_authenticate_success(self):
        environ = self._makeEnviron()
        mw = self._makeOne()
        creds = {'login':'chris', 'password':'password'}
        userid = mw.authenticate(environ, creds, None)
        self.assertEqual(userid, 'chris')
    def test_authenticate_fail(self):
        environ = self._makeEnviron()
        mw = self._makeOne()
        creds = {'login':'chris', 'password':'password'}
        from repoze.pam.interfaces import IAuthenticatorPlugin
        registry = {
            IAuthenticatorPlugin:[DummyFailAuthenticator()]
            }
        mw = self._makeOne(registry=registry)
        userid = mw.authenticate(environ, creds, None)
        self.assertEqual(userid, None)
    def test_authenticate_success_skip_fail(self):
        environ = self._makeEnviron()
        mw = self._makeOne()
        from repoze.pam.interfaces import IAuthenticatorPlugin
        registry = {
            IAuthenticatorPlugin:[DummyFailAuthenticator(),DummyAuthenticator()]
            }
        mw = self._makeOne(registry=registry)
        creds = {'login':'chris', 'password':'password'}
        userid = mw.authenticate(environ, creds, None)
        self.assertEqual(userid, 'chris')
    def test_authenticate_success_firstwins(self):
        environ = self._makeEnviron()
        mw = self._makeOne()
        from repoze.pam.interfaces import IAuthenticatorPlugin
        registry = {
            IAuthenticatorPlugin:[DummyAuthenticator('chris_id1'),
                                  DummyAuthenticator('chris_id2')]
            }
        mw = self._makeOne(registry=registry)
        creds = {'login':'chris', 'password':'password'}
        userid = mw.authenticate(environ, creds, None)
        self.assertEqual(userid, 'chris_id1')
    def test_authenticate_find_implicit_classifier(self):
        environ = self._makeEnviron()
        mw = self._makeOne()
        from repoze.pam.interfaces import IAuthenticatorPlugin
        plugin1 = DummyAuthenticator('chris_id1')
        plugin1.classifiers = set(['nomatch'])
        plugin2 = DummyAuthenticator('chris_id2')
        registry = {
            IAuthenticatorPlugin:[plugin1, plugin2]
            }
        mw = self._makeOne(registry=registry)
        creds = {'login':'chris', 'password':'password'}
        userid = mw.authenticate(environ, creds, None)
        self.assertEqual(userid, 'chris_id2')
    def test_authenticate_find_explicit_classifier(self):
        environ = self._makeEnviron()
        mw = self._makeOne()
        from repoze.pam.interfaces import IAuthenticatorPlugin
        plugin1 = DummyAuthenticator('chris_id1')
        plugin1.classifiers = set(['nomatch'])
        plugin2 = DummyAuthenticator('chris_id2')
        plugin2.classifiers = set(['match'])
        registry = {
            IAuthenticatorPlugin:[plugin1, plugin2]
            }
        mw = self._makeOne(registry=registry)
        creds = {'login':'chris', 'password':'password'}
        userid = mw.authenticate(environ, creds, 'match')
        self.assertEqual(userid, 'chris_id2')
    def test_on_ingress_success_addcredentials(self):
        environ = self._makeEnviron()
        mw = self._makeOne()
        classification = mw.on_ingress(environ)
        self.assertEqual(classification, 'browser')
        self.assertEqual(environ['REMOTE_USER'], 'chris')
        self.assertEqual(environ['repoze.pam.credentials'],
                     {'login':'chris','password':'password','userid':'chris'})
    def test_on_ingress_success_noaddcredentials(self):
        environ = self._makeEnviron()
        mw = self._makeOne()
        mw.add_credentials = False
        classification = mw.on_ingress(environ)
        self.assertEqual(classification, 'browser')
        self.assertEqual(environ['REMOTE_USER'], 'chris')
        self.failIf(environ.has_key('repoze.pam.credentials'))
class TestBasicAuthPlugin(Base):
    def _getTargetClass(self):
        from repoze.pam.plugins.basicauth import BasicAuthPlugin
@@ -29,44 +227,45 @@
        verifyClass(IExtractorPlugin, klass)
    def test_challenge(self):
        plugin = self._makeOne('realm', [])
        plugin = self._makeOne('realm')
        environ = self._makeEnviron()
        from paste.httpexceptions import HTTPUnauthorized
        self.assertRaises(HTTPUnauthorized, plugin.challenge, environ)
        self.assertRaises(HTTPUnauthorized, plugin.challenge, environ,
                          None, None, None)
        
    def test_extract_noauthinfo(self):
        plugin = self._makeOne('realm', [])
        plugin = self._makeOne('realm')
        environ = self._makeEnviron()
        result = plugin.extract(environ)
        self.assertEqual(result, {})
    def test_extract_nonbasic(self):
        plugin = self._makeOne('realm', [])
        plugin = self._makeOne('realm')
        environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Digest abc'})
        result = plugin.extract(environ)
        self.assertEqual(result, {})
    def test_extract_nonbasic(self):
        plugin = self._makeOne('realm', [])
        plugin = self._makeOne('realm')
        environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Digest abc'})
        result = plugin.extract(environ)
        self.assertEqual(result, {})
    def test_extract_basic_badencoding(self):
        plugin = self._makeOne('realm', [])
        plugin = self._makeOne('realm')
        environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic abc'})
        result = plugin.extract(environ)
        self.assertEqual(result, {})
    def test_extract_basic_badrepr(self):
        plugin = self._makeOne('realm', [])
        plugin = self._makeOne('realm')
        value = 'foo'.encode('base64')
        environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic %s' % value})
        result = plugin.extract(environ)
        self.assertEqual(result, {})
    def test_extract_basic_ok(self):
        plugin = self._makeOne('realm', [])
        plugin = self._makeOne('realm')
        value = 'foo:bar'.encode('base64')
        environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic %s' % value})
        result = plugin.extract(environ)
@@ -74,14 +273,13 @@
    def test_factory(self):
        from repoze.pam.plugins.basicauth import make_plugin
        plugin = make_plugin({}, 'realm', ['a', 'b'])
        plugin = make_plugin({}, 'realm')
        self.assertEqual(plugin.realm, 'realm')
        self.assertEqual(plugin.requests, ['a', 'b'])
        
class TestHTPasswdAuthenticator(Base):
class TestHTPasswdPlugin(Base):
    def _getTargetClass(self):
        from repoze.pam.plugins.htpasswd import HTPasswdAuthenticator
        return HTPasswdAuthenticator
        from repoze.pam.plugins.htpasswd import HTPasswdPlugin
        return HTPasswdPlugin
    def _makeOne(self, *arg, **kw):
        plugin = self._getTargetClass()(*arg, **kw)
@@ -168,3 +366,105 @@
        self.assertEqual(plugin.filename, 'foo')
        self.assertEqual(plugin.check, check_crypted)
        
class TestDefaultRequestClassifier(Base):
    def _getTargetClass(self):
        from repoze.pam.classifiers import DefaultRequestClassifier
        return DefaultRequestClassifier
    def _makeOne(self, *arg, **kw):
        classifier = self._getTargetClass()(*arg, **kw)
        return classifier
    def test_implements(self):
        from zope.interface.verify import verifyClass
        from repoze.pam.interfaces import IRequestClassifier
        klass = self._getTargetClass()
        verifyClass(IRequestClassifier, klass)
    def test_classify_dav_method(self):
        classifier = self._makeOne()
        environ = self._makeEnviron({'REQUEST_METHOD':'COPY'})
        result = classifier(environ)
        self.assertEqual(result, 'dav')
    def test_classify_dav_useragent(self):
        classifier = self._makeOne()
        environ = self._makeEnviron({'HTTP_USER_AGENT':'WebDrive'})
        result = classifier(environ)
        self.assertEqual(result, 'dav')
    def test_classify_xmlpost(self):
        classifier = self._makeOne()
        environ = self._makeEnviron({'CONTENT_TYPE':'text/xml',
                                     'REQUEST_METHOD':'POST'})
        result = classifier(environ)
        self.assertEqual(result, 'xmlpost')
    def test_classify_browser(self):
        classifier = self._makeOne()
        environ = self._makeEnviron({'CONTENT_TYPE':'text/xml',
                                     'REQUEST_METHOD':'GET'})
        result = classifier(environ)
        self.assertEqual(result, 'browser')
class TestDefaultResponseClassifier(Base):
    def _getTargetClass(self):
        from repoze.pam.classifiers import DefaultResponseClassifier
        return DefaultResponseClassifier
    def _makeOne(self, *arg, **kw):
        classifier = self._getTargetClass()(*arg, **kw)
        return classifier
    def test_implements(self):
        from zope.interface.verify import verifyClass
        from repoze.pam.interfaces import IResponseClassifier
        klass = self._getTargetClass()
        verifyClass(IResponseClassifier, klass)
    def test_classify(self):
        classifier = self._makeOne()
        result = classifier(None, 'dav', None, None)
        self.assertEqual(result, 'dav')
class DummyApp:
    def __call__(self, environ, start_response):
        return ['a']
class DummyRequestClassifier:
    def __call__(self, environ):
        return 'browser'
class DummyResponseClassifier:
    def __call__(self, environ, request_classification, headers, exception):
        return request_classification
class DummyExtractor:
    def __init__(self, credentials=None):
        if credentials is None:
            credentials = {'login':'chris', 'password':'password'}
        self.credentials = credentials
    def extract(self, environ):
        return self.credentials
class DummyNoResultsExtractor:
    def extract(self, environ):
        return {}
class DummyAuthenticator:
    def __init__(self, userid=None):
        self.userid = userid
    def authenticate(self, environ, credentials):
        if self.userid is None:
            return credentials['login']
        return self.userid
class DummyFailAuthenticator:
    def authenticate(self, environ, credentials):
        return None
class DummyChallenger:
    def challenge(self, environ, request_classifier, headers, exception):
        environ['challenged'] = True