README.txt | ●●●●● patch | view | raw | blame | history | |
repoze/pam/classifiers.py | ●●●●● patch | view | raw | blame | history | |
repoze/pam/etc/sample-config.ini | ●●●●● patch | view | raw | blame | history | |
repoze/pam/interfaces.py | ●●●●● patch | view | raw | blame | history | |
repoze/pam/middleware.py | ●●●●● patch | view | raw | blame | history | |
repoze/pam/plugins/basicauth.py | ●●●●● patch | view | raw | blame | history | |
repoze/pam/plugins/htpasswd.py | ●●●●● patch | view | raw | blame | history | |
repoze/pam/tests.py | ●●●●● 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