repoze.who-1.0.18/0000755000175000017500000000000011274642513013612 5ustar tseavertseaverrepoze.who-1.0.18/README.txt0000644000175000017500000000143311274373525015315 0ustar tseavertseaver*************************************************** ``repoze.who`` -- WSGI Authentication Middleware *************************************************** ``repoze.who`` is an identification and authentication framework for arbitrary WSGI applications. It acts as WSGI middleware. ``repoze.who`` is inspired by Zope 2's Pluggable Authentication Service (PAS) (but ``repoze.who`` is not dependent on Zope in any way; it is useful for any WSGI application). It provides no facility for authorization (ensuring whether a user can or cannot perform the operation implied by the request). This is considered to be the domain of the WSGI application. See the ``docs`` subdirectory of this package (also available at least provisionally at http://static.repoze.org/whodocs) for more information. repoze.who-1.0.18/repoze.who.egg-info/0000755000175000017500000000000011274642513017404 5ustar tseavertseaverrepoze.who-1.0.18/repoze.who.egg-info/namespace_packages.txt0000644000175000017500000000004511274642513023736 0ustar tseavertseaverrepoze repoze.who repoze.who.plugins repoze.who-1.0.18/repoze.who.egg-info/dependency_links.txt0000644000175000017500000000000111274642513023452 0ustar tseavertseaver repoze.who-1.0.18/repoze.who.egg-info/SOURCES.txt0000644000175000017500000000301011274642513021262 0ustar tseavertseaverCHANGES.txt COPYRIGHT.txt LICENSE.txt README.txt TODO.txt ez_setup.py setup.cfg setup.py docs/Makefile docs/changes.rst docs/conf.py docs/index.rst docs/narr.rst docs/.static/logo_hi.gif docs/.static/repoze.css repoze/__init__.py repoze.who.egg-info/PKG-INFO repoze.who.egg-info/SOURCES.txt repoze.who.egg-info/dependency_links.txt repoze.who.egg-info/entry_points.txt repoze.who.egg-info/namespace_packages.txt repoze.who.egg-info/not-zip-safe repoze.who.egg-info/requires.txt repoze.who.egg-info/test_info.txt repoze.who.egg-info/top_level.txt repoze/who/__init__.py repoze/who/classifiers.py repoze/who/config.py repoze/who/interfaces.py repoze/who/middleware.py repoze/who/restrict.py repoze/who/utils.py repoze/who/plugins/__init__.py repoze/who/plugins/auth_tkt.py repoze/who/plugins/basicauth.py repoze/who/plugins/cookie.py repoze/who/plugins/form.py repoze/who/plugins/htpasswd.py repoze/who/plugins/sql.py repoze/who/plugins/tests/__init__.py repoze/who/plugins/tests/test_authtkt.py repoze/who/plugins/tests/test_basicauth.py repoze/who/plugins/tests/test_cookie.py repoze/who/plugins/tests/test_form.py repoze/who/plugins/tests/test_htpasswd.py repoze/who/plugins/tests/test_sql.py repoze/who/plugins/tests/fixtures/__init__.py repoze/who/plugins/tests/fixtures/form.html repoze/who/plugins/tests/fixtures/test.htpasswd repoze/who/plugins/tests/fixtures/testapp.py repoze/who/tests/__init__.py repoze/who/tests/test_classifiers.py repoze/who/tests/test_config.py repoze/who/tests/test_middleware.py repoze/who/tests/test_restrict.pyrepoze.who-1.0.18/repoze.who.egg-info/test_info.txt0000644000175000017500000000014611274375173022145 0ustar tseavertseavertest_module = None test_suite = repoze.who test_loader = None tests_require = Paste zope.interface repoze.who-1.0.18/repoze.who.egg-info/top_level.txt0000644000175000017500000000000711274642513022133 0ustar tseavertseaverrepoze repoze.who-1.0.18/repoze.who.egg-info/not-zip-safe0000644000175000017500000000000111007074672021631 0ustar tseavertseaver repoze.who-1.0.18/repoze.who.egg-info/requires.txt0000644000175000017500000000003711274642513022004 0ustar tseavertseaverPaste zope.interface setuptoolsrepoze.who-1.0.18/repoze.who.egg-info/PKG-INFO0000644000175000017500000005212311274642513020504 0ustar tseavertseaverMetadata-Version: 1.0 Name: repoze.who Version: 1.0.18 Summary: repoze.who is an identification and authentication framework for WSGI. Home-page: http://www.repoze.org Author: Agendaless Consulting Author-email: repoze-dev@lists.repoze.org License: BSD-derived (http://www.repoze.org/LICENSE.txt) Description: *************************************************** ``repoze.who`` -- WSGI Authentication Middleware *************************************************** ``repoze.who`` is an identification and authentication framework for arbitrary WSGI applications. It acts as WSGI middleware. ``repoze.who`` is inspired by Zope 2's Pluggable Authentication Service (PAS) (but ``repoze.who`` is not dependent on Zope in any way; it is useful for any WSGI application). It provides no facility for authorization (ensuring whether a user can or cannot perform the operation implied by the request). This is considered to be the domain of the WSGI application. See the ``docs`` subdirectory of this package (also available at least provisionally at http://static.repoze.org/whodocs) for more information. repoze.who Changelog ==================== 1.0.18 (2009-11-05) ------------------- - Issue #104: AuthTkt plugin was passing an invalid cookie value in headers from ``forget``, and was not setting the ``Max-Age`` and ``Expires`` attributes of those cookies. 1.0.17 (2009-11-05) ------------------- - Fixed the ``repoze.who.plugins.form.make_plugin`` factory's ``formcallable`` argument handling, to allow passing in a dotted name (e.g., from a config file). 1.0.16 (2009-11-04) ------------------- - Exposed ``formcallable`` argument for ``repoze.who.plugins.form.FormPlugin`` to the callers of the ``repoze.who.plugins.form.make_plugin`` factory. Thanks to Roland Hedburg for the report. - Fixed an issue that caused the following symptom when using the ini configuration parser:: TypeError: _makePlugin() got multiple values for keyword argument 'name' See http://bugs.repoze.org/issue92 for more details. Thanks to vaab for the bug report and initial fix. 1.0.15 (2009-06-25) ------------------- - If the form post value ``max_age`` exists while in the ``identify`` method is handling the ``login_handler_path``, pass the max_age value in the returned identity dictionary as ``max_age``. See the below bullet point for why. - If the ``identity`` dict passed to the ``auth_tkt`` ``remember`` method contains a ``max_age`` key with a string (or integer) value, treat it as a cue to set the ``Max-Age`` and ``Expires`` headers in the returned cookies. The cookie ``Max-Age`` is set to the value and the ``Expires`` is computed from the current time. 1.0.14 (2009-06-17) ------------------- - Fix test breakage on Windows. See http://bugs.repoze.org/issue79 . - Documented issue with using ``include_ip`` setting in the ``auth_tkt`` plugin. See http://bugs.repoze.org/issue81 . - Added 'passthrough_challenge_decider', which avoids re-challenging 401 responses which have been "pre-challenged" by the application. - One-hundred percent unit test coverage. - Add ``timeout`` and ``reissue_time`` arguments to the auth_tkt identifier plugin, courtesty of Paul Johnston. - Add a ``userid_checker`` argument to the auth_tkt identifier plugin, courtesty of Gustavo Narea. If ``userid_checker`` is provided, it must be a dotted Python name that resolves to a function which accepts a userid and returns a boolean True or False, indicating whether that user exists in a database. This is a workaround. Due to a design bug in repoze.who, the only way who can check for user existence is to use one or more IAuthenticator plugin ``authenticate`` methods. If an IAuthenticator's ``authenticate`` method returns true, it means that the user exists. However most IAuthenticator plugins expect *both* a username and a password, and will return False unconditionally if both aren't supplied. This means that an authenticator can't be used to check if the user "only" exists. The identity provided by an auth_tkt does not contain a password to check against. The actual design bug in repoze.who is this: when a user presents credentials from an auth_tkt, he is considered "preauthenticated". IAuthenticator.authenticate is just never called for a "preauthenticated" identity, which works fine, but it means that the user will be considered authenticated even if you deleted the user's record from whatever database you happen to be using. However, if you use a userid_checker, you can ensure that a user exists for the auth_tkt supplied userid. If the userid_checker returns False, the auth_tkt credentials are considered "no good". 1.0.13 (2009-04-24) ------------------- - Added a paragraph to ``IAuthenticator`` docstring, documenting that plugins are allowed to add keys to the ``identity`` dictionary (e.g., to save a second database query in an ``IMetadataProvider`` plugin). - Patch supplied for issue #71 (http://bugs.repoze.org/issue71) whereby a downstream app can return a generator, relying on an upstream component to call start_response. We do this because the challenge decider needs the status and headers to decide what to do. 1.0.12 (2009-04-19) ------------------- - auth_tkt plugin tried to append REMOTE_USER_TOKENS data to existing tokens data returned by auth_tkt.parse_tkt; this was incorrect; just overwrite. - Extended auth_tkt plugin factory to allow passing secret in a separate file from the main config file. See http://bugs.repoze.org/issue40 . 1.0.11 (2009-04-10) ------------------- - Fix auth_tkt plugin; cookie values are now quoted, making it possible to put spaces and other whitespace, etc in usernames. (thanks to Michael Pedersen). - Fix corner case issue of an exception raised when attempting to log when there are no identifiers or authenticators. 1.0.10 (2009-01-23) ------------------- - The RedirectingFormPlugin now passes along SetCookie headers set into the response by the application within the NotFound response (fixes TG2 "flash" issue). 1.0.9 (2008-12-18) ------------------ - The RedirectingFormPlugin now attempts to find a header named ``X-Authentication-Failure-Reason`` among the response headers set by the application when a challenge is issued. If a value for this header exists (and is non-blank), the value is attached to the redirect URL's query string as the ``reason`` parameter (or a user-settable key). This makes it possible for downstream applications to issue a response that initiates a challenge with this header and subsequently display the reason in the login form rendered as a result of the challenge. 1.0.8 (2008-12-13) ------------------ - The ``PluggableAuthenticationMiddleware`` constructor accepts a ``log_stream`` argument, which is typically a file. After this release, it can also be a PEP 333 ``Logger`` instance; if it is a PEP 333 ``Logger`` instance, this logger will be used as the repoze.who logger (instead of one being constructed by the middleware, as was previously always the case). When the ``log_stream`` argument is a PEP 333 Logger object, the ``log_level`` argument is ignored. 1.0.7 (2008-08-28) ------------------ - ``repoze.who`` and ``repoze.who.plugins`` were not added to the ``namespace_packages`` list in setup.py, potentially making 1.0.6 a brownbag release, given that making these packages namespace packages was the only reason for its release. 1.0.6 (2008-08-28) ------------------ - Make repoze.who and repoze.who.plugins into namespace packages mainly so we can allow plugin authors to distribute packages in the repoze.who.plugins namespace. 1.0.5 (2008-08-23) ------------------ - Fix auth_tkt plugin to set the same cookies in its ``remember`` method that it does in its ``forget`` method. Previously, logging out and relogging back in to a site that used auth_tkt identifier plugin was slightly dicey and would only work sometimes. - The FormPlugin plugin has grown a redirect-on-unauthorized feature. Any response from a downstream application that causes a challenge and includes a Location header will cause a redirect to the value of the Location header. 1.0.4 (2008-08-22) ------------------ - Added a key to the '[general]' config section: ``remote_user_key``. If you use this key in the config file, it tells who to 1) not perform any authentication if it exists in the environment during ingress and 2) to set the key in the environment for the downstream app to use as the REMOTE_USER variable. The default is ``REMOTE_USER``. - Using unicode user ids in combination with the auth_tkt plugin would cause problems under mod_wsgi. - Allowed 'cookie_path' argument to InsecureCookiePlugin (and config constructor). Thanks to Gustavo Narea. 1.0.3 (2008-08-16) ------------------ - A bug in the middleware's ``authenticate`` method made it impossible to authenticate a user with a userid that was null (e.g. 0, False), which are valid identifiers. The only invalid userid is now None. - Applied patch from Olaf Conradi which logs an error when an invalid filename is passed to the HTPasswdPlugin. 1.0.2 (2008-06-16) ------------------ - Fix bug found by Chris Perkins: the auth_tkt plugin's "remember" method didn't handle userids which are Python "long" instances properly. Symptom: TypeError: cannot concatenate 'str' and 'long' objects in "paste.auth.auth_tkt". - Added predicate-based "restriction" middleware support (repoze.who.restrict), allowing configuratio-driven authorization as a WSGI filter. One example predicate, 'authenticated_predicate', is supplied, which requires that the user be authenticated either via 'REMOTE_USER' or via 'repoze.who.identity'. To use the filter to restrict access:: [filter:authenticated_only] use = egg:repoze.who#authenticated or:: [filter:some_predicate] use = egg:repoze.who#predicate predicate = my.module:some_predicate some_option = a value 1.0.1 (2008-05-24) ------------------ - Remove dependency-link to dist.repoze.org to prevent easy_install from inserting that path into its search paths (the dependencies are available from PyPI). 1.0 (2008-05-04) ----------------- - The plugin at plugins.form.FormPlugin didn't redirect properly after collecting identification information. Symptom: a downstream app would receive a POST request with a blank body, which would sometimes result in a Bad Request error. - Fixed interface declarations of 'classifiers.default_request_classifier' and 'classifiers.default_password_compare'. - Added actual config-driven middleware factory, 'config.make_middleware_with_config' - Removed fossilized 'who_conf' argument from plugin factory functions. - Added ConfigParser-based WhoConfig, implementing the spec outlined at http://www.plope.com/static/misc/sphinxtest/intro.html#middleware-configuration-via-config-file, with the following changes: - "Bare" plugins (requiring no configuration options) may be specified as either egg entry points (e.g., 'egg:distname#entry_point_name') or as dotted-path-with-colon (e.g., 'dotted.name:object_id'). - Therefore, the separator between a plugin and its classifier is now a semicolon, rather than a colon. E.g.:: [plugins:id_plugin] use = egg:another.package#identify_with_frobnatz frobnatz = baz [identifiers] plugins = egg:my.egg#identify;browser dotted.name:identifier id_plugin 0.9.1 (2008-04-27) ------------------ - Fix auth_tkt plugin to be able to encode and decode integer user ids. 0.9 (2008-04-01) ---------------- - Fix bug introduced in FormPlugin in 0.8 release (rememberer headers not set). - Add PATH_INFO to started and ended log info. - Add a SQLMetadataProviderPlugin (in plugins/sql). - Change constructor of SQLAuthenticatorPlugin: it now accepts only "query", "conn_factory", and "compare_fn". The old constructor accepted a DSN, but some database systems don't use DBAPI DSNs. The new constructor accepts no DSN; the conn_factory is assumed to do all the work to make a connection, including knowing the DSN if one is required. The "conn_factory" should return something that, when called with no arguments, returns a database connection. - The "make_plugin" helper in plugins/sql has been renamed "make_authenticator_plugin". When called, this helper will return a SQLAuthenticatorPlugin. A bit of helper logic in the "make_authenticator_plugin" allows a connection factory to be computed. The top-level callable referred to by conn_factory in this helper should return a function that, when called with no arguments, returns a datbase connection. The top-level callable itself is called with "who_conf" (global who configuration) and any number of non-top-level keyword arguments as they are passed into the helper, to allow for a DSN or URL or whatever to be passed in. - A "make_metatata_plugin" helper has been added to plugins/sql. When called, this will make a SQLMetadataProviderPlugin. See the implementation for details. It is similar to the "make_authenticator_plugin" helper. 0.8 (2008-03-27) ---------------- - Add a RedirectingFormIdentifier plugin. This plugin is willing to redirect to an external (or downstream application) login form to perform identification. The external login form must post to the "login_handler_path" of the plugin (optimally with a "came_from" value to tell the plugin where to redirect the response to if the authentication works properly). The "logout_handler_path" of this plugin can be visited to perform a logout. The "came_from" value also works there. - Identifier plugins are now permitted to set a key in the environment named 'repoze.who.application' on ingress (in 'identify'). If an identifier plugin does so, this application is used instead of the "normal" downstream application. This feature was added to more simply support the redirecting form identifier plugin. 0.7 (2008-03-26) ---------------- - Change the IMetadataProvider interface: this interface used to have a "metadata" method which returned a dictionary. This method is not part of that API anymore. It's been replaced with an "add_metadata" method which has the signature:: def add_metadata(environ, identity): """ Add metadata to the identity (which is a dictionary) """ The return value is ignored. IMetadataProvider plugins are now assumed to be responsible for 'scribbling' directly on the identity that is passed in (it's a dictionary). The user id can always be retrieved from the identity via identity['repoze.who.userid'] for metadata plugins that rely on that value. 0.6 (2008-03-20) ---------------- - Renaming: repoze.pam is now repoze.who - Bump ez_setup.py version. - Add IMetadataProvider plugin type. Chris says 'Whit rules'. 0.5 (2008-03-09) ---------------- - Allow "remote user key" (default: REMOTE_USER) to be overridden (pass in remote_user_key to middleware constructor). - Allow form plugin to override the default form. - API change: IIdentifiers are no longer required to put both 'login' and 'password' in a returned identity dictionary. Instead, an IIdentifier can place arbitrary key/value pairs in the identity dictionary (or return an empty dictionary). - API return value change: the "failure" identity which IIdentifiers return is now None rather than an empty dictionary. - The IAuthenticator interface now specifies that IAuthenticators must not raise an exception when evaluating an identity that does not have "expected" key/value pairs (e.g. when an IAuthenticator that expects login and password inspects an identity returned by an IP-based auth system which only puts the IP address in the identity); instead they fail gracefully by returning None. - Add (cookie) "auth_tkt" identification plugin. - Stamp identity dictionaries with a userid by placing a key named 'repoze.pam.userid' into the identity for each authenticated identity. - If an IIdentifier plugin inserts a 'repoze.pam.userid' key into the identity dictionary, consider this identity "preauthenticated". No authenticator plugins will be asked to authenticate this identity. This is designed for things like the recently added auth_tkt plugin, which embeds the user id into the ticket. This effectively alllows an IIdentifier plugin to become an IAuthenticator plugin when breaking apart the responsibility into two separate plugins is "make-work". Preauthenticated identities will be selected first when deciding which identity to use for any given request. - Insert a 'repoze.pam.identity' key into the WSGI environment on ingress if an identity is found. Its value will be the identity dictionary related to the identity selected by repoze.pam on ingress. Downstream consumers are allowed to mutate this dictionary; this value is passed to "remember" and "forget", so its main use is to do a "credentials reset"; e.g. a user has changed his username or password within the application, but we don't want to force him to log in again after he does so. 0.4 (03-07-2008) ---------------- - Allow plugins to specify a classifiers list per interface (instead of a single classifiers list per plugin). 0.3 (03-05-2008) ---------------- - Make SQLAuthenticatorPlugin's default_password_compare use hexdigest sha instead of base64'ed binary sha for simpler conversion. 0.2 (03-04-2008) ---------------- - Added SQLAuthenticatorPlugin (see plugins/sql.py). 0.1 (02-27-2008) ---------------- - Initial release (no configuration file support yet). Keywords: web application server wsgi zope Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application repoze.who-1.0.18/repoze.who.egg-info/entry_points.txt0000644000175000017500000000044611274642513022706 0ustar tseavertseaver [paste.filter_app_factory] test = repoze.who.middleware:make_test_middleware config = repoze.who.config:make_middleware_with_config predicate = repoze.who.restrict:make_predicate_restriction authenticated = repoze.who.restrict:make_authenticated_restriction repoze.who-1.0.18/ez_setup.py0000644000175000017500000002231311145266212016016 0ustar tseavertseaver#!python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys DEFAULT_VERSION = "0.6c9" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', } import sys, os def _validate_md5(egg_name, data): if egg_name in md5_data: from md5 import md5 digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print >>sys.stderr, ( "md5 validation of %s failed! (Possible download problem?)" % egg_name ) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules def do_download(): egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools; setuptools.bootstrap_install_from = egg try: import pkg_resources except ImportError: return do_download() try: pkg_resources.require("setuptools>="+version); return except pkg_resources.VersionConflict, e: if was_imported: print >>sys.stderr, ( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first, using 'easy_install -U setuptools'." "\n\n(Currently using %r)" ) % (version, e.args[0]) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return do_download() except pkg_resources.DistributionNotFound: return do_download() def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay = 15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2, shutil egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ); from time import sleep; sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto,"wb"); dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0,egg) from setuptools.command.easy_install import main return main(list(argv)+[egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': print >>sys.stderr, ( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ) sys.exit(2) req = "setuptools>="+version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv)+[download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print "Setuptools version",version,"or greater has been installed." print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' def update_md5(filenames): """Update our built-in md5 registry""" import re from md5 import md5 for name in filenames: base = os.path.basename(name) f = open(name,'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb'); src = f.read(); f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print >>sys.stderr, "Internal error!" sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile,'w') f.write(src) f.close() if __name__=='__main__': if len(sys.argv)>2 and sys.argv[1]=='--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:]) repoze.who-1.0.18/CHANGES.txt0000644000175000017500000004064011274642326015431 0ustar tseavertseaverrepoze.who Changelog ==================== 1.0.18 (2009-11-05) ------------------- - Issue #104: AuthTkt plugin was passing an invalid cookie value in headers from ``forget``, and was not setting the ``Max-Age`` and ``Expires`` attributes of those cookies. 1.0.17 (2009-11-05) ------------------- - Fixed the ``repoze.who.plugins.form.make_plugin`` factory's ``formcallable`` argument handling, to allow passing in a dotted name (e.g., from a config file). 1.0.16 (2009-11-04) ------------------- - Exposed ``formcallable`` argument for ``repoze.who.plugins.form.FormPlugin`` to the callers of the ``repoze.who.plugins.form.make_plugin`` factory. Thanks to Roland Hedburg for the report. - Fixed an issue that caused the following symptom when using the ini configuration parser:: TypeError: _makePlugin() got multiple values for keyword argument 'name' See http://bugs.repoze.org/issue92 for more details. Thanks to vaab for the bug report and initial fix. 1.0.15 (2009-06-25) ------------------- - If the form post value ``max_age`` exists while in the ``identify`` method is handling the ``login_handler_path``, pass the max_age value in the returned identity dictionary as ``max_age``. See the below bullet point for why. - If the ``identity`` dict passed to the ``auth_tkt`` ``remember`` method contains a ``max_age`` key with a string (or integer) value, treat it as a cue to set the ``Max-Age`` and ``Expires`` headers in the returned cookies. The cookie ``Max-Age`` is set to the value and the ``Expires`` is computed from the current time. 1.0.14 (2009-06-17) ------------------- - Fix test breakage on Windows. See http://bugs.repoze.org/issue79 . - Documented issue with using ``include_ip`` setting in the ``auth_tkt`` plugin. See http://bugs.repoze.org/issue81 . - Added 'passthrough_challenge_decider', which avoids re-challenging 401 responses which have been "pre-challenged" by the application. - One-hundred percent unit test coverage. - Add ``timeout`` and ``reissue_time`` arguments to the auth_tkt identifier plugin, courtesty of Paul Johnston. - Add a ``userid_checker`` argument to the auth_tkt identifier plugin, courtesty of Gustavo Narea. If ``userid_checker`` is provided, it must be a dotted Python name that resolves to a function which accepts a userid and returns a boolean True or False, indicating whether that user exists in a database. This is a workaround. Due to a design bug in repoze.who, the only way who can check for user existence is to use one or more IAuthenticator plugin ``authenticate`` methods. If an IAuthenticator's ``authenticate`` method returns true, it means that the user exists. However most IAuthenticator plugins expect *both* a username and a password, and will return False unconditionally if both aren't supplied. This means that an authenticator can't be used to check if the user "only" exists. The identity provided by an auth_tkt does not contain a password to check against. The actual design bug in repoze.who is this: when a user presents credentials from an auth_tkt, he is considered "preauthenticated". IAuthenticator.authenticate is just never called for a "preauthenticated" identity, which works fine, but it means that the user will be considered authenticated even if you deleted the user's record from whatever database you happen to be using. However, if you use a userid_checker, you can ensure that a user exists for the auth_tkt supplied userid. If the userid_checker returns False, the auth_tkt credentials are considered "no good". 1.0.13 (2009-04-24) ------------------- - Added a paragraph to ``IAuthenticator`` docstring, documenting that plugins are allowed to add keys to the ``identity`` dictionary (e.g., to save a second database query in an ``IMetadataProvider`` plugin). - Patch supplied for issue #71 (http://bugs.repoze.org/issue71) whereby a downstream app can return a generator, relying on an upstream component to call start_response. We do this because the challenge decider needs the status and headers to decide what to do. 1.0.12 (2009-04-19) ------------------- - auth_tkt plugin tried to append REMOTE_USER_TOKENS data to existing tokens data returned by auth_tkt.parse_tkt; this was incorrect; just overwrite. - Extended auth_tkt plugin factory to allow passing secret in a separate file from the main config file. See http://bugs.repoze.org/issue40 . 1.0.11 (2009-04-10) ------------------- - Fix auth_tkt plugin; cookie values are now quoted, making it possible to put spaces and other whitespace, etc in usernames. (thanks to Michael Pedersen). - Fix corner case issue of an exception raised when attempting to log when there are no identifiers or authenticators. 1.0.10 (2009-01-23) ------------------- - The RedirectingFormPlugin now passes along SetCookie headers set into the response by the application within the NotFound response (fixes TG2 "flash" issue). 1.0.9 (2008-12-18) ------------------ - The RedirectingFormPlugin now attempts to find a header named ``X-Authentication-Failure-Reason`` among the response headers set by the application when a challenge is issued. If a value for this header exists (and is non-blank), the value is attached to the redirect URL's query string as the ``reason`` parameter (or a user-settable key). This makes it possible for downstream applications to issue a response that initiates a challenge with this header and subsequently display the reason in the login form rendered as a result of the challenge. 1.0.8 (2008-12-13) ------------------ - The ``PluggableAuthenticationMiddleware`` constructor accepts a ``log_stream`` argument, which is typically a file. After this release, it can also be a PEP 333 ``Logger`` instance; if it is a PEP 333 ``Logger`` instance, this logger will be used as the repoze.who logger (instead of one being constructed by the middleware, as was previously always the case). When the ``log_stream`` argument is a PEP 333 Logger object, the ``log_level`` argument is ignored. 1.0.7 (2008-08-28) ------------------ - ``repoze.who`` and ``repoze.who.plugins`` were not added to the ``namespace_packages`` list in setup.py, potentially making 1.0.6 a brownbag release, given that making these packages namespace packages was the only reason for its release. 1.0.6 (2008-08-28) ------------------ - Make repoze.who and repoze.who.plugins into namespace packages mainly so we can allow plugin authors to distribute packages in the repoze.who.plugins namespace. 1.0.5 (2008-08-23) ------------------ - Fix auth_tkt plugin to set the same cookies in its ``remember`` method that it does in its ``forget`` method. Previously, logging out and relogging back in to a site that used auth_tkt identifier plugin was slightly dicey and would only work sometimes. - The FormPlugin plugin has grown a redirect-on-unauthorized feature. Any response from a downstream application that causes a challenge and includes a Location header will cause a redirect to the value of the Location header. 1.0.4 (2008-08-22) ------------------ - Added a key to the '[general]' config section: ``remote_user_key``. If you use this key in the config file, it tells who to 1) not perform any authentication if it exists in the environment during ingress and 2) to set the key in the environment for the downstream app to use as the REMOTE_USER variable. The default is ``REMOTE_USER``. - Using unicode user ids in combination with the auth_tkt plugin would cause problems under mod_wsgi. - Allowed 'cookie_path' argument to InsecureCookiePlugin (and config constructor). Thanks to Gustavo Narea. 1.0.3 (2008-08-16) ------------------ - A bug in the middleware's ``authenticate`` method made it impossible to authenticate a user with a userid that was null (e.g. 0, False), which are valid identifiers. The only invalid userid is now None. - Applied patch from Olaf Conradi which logs an error when an invalid filename is passed to the HTPasswdPlugin. 1.0.2 (2008-06-16) ------------------ - Fix bug found by Chris Perkins: the auth_tkt plugin's "remember" method didn't handle userids which are Python "long" instances properly. Symptom: TypeError: cannot concatenate 'str' and 'long' objects in "paste.auth.auth_tkt". - Added predicate-based "restriction" middleware support (repoze.who.restrict), allowing configuratio-driven authorization as a WSGI filter. One example predicate, 'authenticated_predicate', is supplied, which requires that the user be authenticated either via 'REMOTE_USER' or via 'repoze.who.identity'. To use the filter to restrict access:: [filter:authenticated_only] use = egg:repoze.who#authenticated or:: [filter:some_predicate] use = egg:repoze.who#predicate predicate = my.module:some_predicate some_option = a value 1.0.1 (2008-05-24) ------------------ - Remove dependency-link to dist.repoze.org to prevent easy_install from inserting that path into its search paths (the dependencies are available from PyPI). 1.0 (2008-05-04) ----------------- - The plugin at plugins.form.FormPlugin didn't redirect properly after collecting identification information. Symptom: a downstream app would receive a POST request with a blank body, which would sometimes result in a Bad Request error. - Fixed interface declarations of 'classifiers.default_request_classifier' and 'classifiers.default_password_compare'. - Added actual config-driven middleware factory, 'config.make_middleware_with_config' - Removed fossilized 'who_conf' argument from plugin factory functions. - Added ConfigParser-based WhoConfig, implementing the spec outlined at http://www.plope.com/static/misc/sphinxtest/intro.html#middleware-configuration-via-config-file, with the following changes: - "Bare" plugins (requiring no configuration options) may be specified as either egg entry points (e.g., 'egg:distname#entry_point_name') or as dotted-path-with-colon (e.g., 'dotted.name:object_id'). - Therefore, the separator between a plugin and its classifier is now a semicolon, rather than a colon. E.g.:: [plugins:id_plugin] use = egg:another.package#identify_with_frobnatz frobnatz = baz [identifiers] plugins = egg:my.egg#identify;browser dotted.name:identifier id_plugin 0.9.1 (2008-04-27) ------------------ - Fix auth_tkt plugin to be able to encode and decode integer user ids. 0.9 (2008-04-01) ---------------- - Fix bug introduced in FormPlugin in 0.8 release (rememberer headers not set). - Add PATH_INFO to started and ended log info. - Add a SQLMetadataProviderPlugin (in plugins/sql). - Change constructor of SQLAuthenticatorPlugin: it now accepts only "query", "conn_factory", and "compare_fn". The old constructor accepted a DSN, but some database systems don't use DBAPI DSNs. The new constructor accepts no DSN; the conn_factory is assumed to do all the work to make a connection, including knowing the DSN if one is required. The "conn_factory" should return something that, when called with no arguments, returns a database connection. - The "make_plugin" helper in plugins/sql has been renamed "make_authenticator_plugin". When called, this helper will return a SQLAuthenticatorPlugin. A bit of helper logic in the "make_authenticator_plugin" allows a connection factory to be computed. The top-level callable referred to by conn_factory in this helper should return a function that, when called with no arguments, returns a datbase connection. The top-level callable itself is called with "who_conf" (global who configuration) and any number of non-top-level keyword arguments as they are passed into the helper, to allow for a DSN or URL or whatever to be passed in. - A "make_metatata_plugin" helper has been added to plugins/sql. When called, this will make a SQLMetadataProviderPlugin. See the implementation for details. It is similar to the "make_authenticator_plugin" helper. 0.8 (2008-03-27) ---------------- - Add a RedirectingFormIdentifier plugin. This plugin is willing to redirect to an external (or downstream application) login form to perform identification. The external login form must post to the "login_handler_path" of the plugin (optimally with a "came_from" value to tell the plugin where to redirect the response to if the authentication works properly). The "logout_handler_path" of this plugin can be visited to perform a logout. The "came_from" value also works there. - Identifier plugins are now permitted to set a key in the environment named 'repoze.who.application' on ingress (in 'identify'). If an identifier plugin does so, this application is used instead of the "normal" downstream application. This feature was added to more simply support the redirecting form identifier plugin. 0.7 (2008-03-26) ---------------- - Change the IMetadataProvider interface: this interface used to have a "metadata" method which returned a dictionary. This method is not part of that API anymore. It's been replaced with an "add_metadata" method which has the signature:: def add_metadata(environ, identity): """ Add metadata to the identity (which is a dictionary) """ The return value is ignored. IMetadataProvider plugins are now assumed to be responsible for 'scribbling' directly on the identity that is passed in (it's a dictionary). The user id can always be retrieved from the identity via identity['repoze.who.userid'] for metadata plugins that rely on that value. 0.6 (2008-03-20) ---------------- - Renaming: repoze.pam is now repoze.who - Bump ez_setup.py version. - Add IMetadataProvider plugin type. Chris says 'Whit rules'. 0.5 (2008-03-09) ---------------- - Allow "remote user key" (default: REMOTE_USER) to be overridden (pass in remote_user_key to middleware constructor). - Allow form plugin to override the default form. - API change: IIdentifiers are no longer required to put both 'login' and 'password' in a returned identity dictionary. Instead, an IIdentifier can place arbitrary key/value pairs in the identity dictionary (or return an empty dictionary). - API return value change: the "failure" identity which IIdentifiers return is now None rather than an empty dictionary. - The IAuthenticator interface now specifies that IAuthenticators must not raise an exception when evaluating an identity that does not have "expected" key/value pairs (e.g. when an IAuthenticator that expects login and password inspects an identity returned by an IP-based auth system which only puts the IP address in the identity); instead they fail gracefully by returning None. - Add (cookie) "auth_tkt" identification plugin. - Stamp identity dictionaries with a userid by placing a key named 'repoze.pam.userid' into the identity for each authenticated identity. - If an IIdentifier plugin inserts a 'repoze.pam.userid' key into the identity dictionary, consider this identity "preauthenticated". No authenticator plugins will be asked to authenticate this identity. This is designed for things like the recently added auth_tkt plugin, which embeds the user id into the ticket. This effectively alllows an IIdentifier plugin to become an IAuthenticator plugin when breaking apart the responsibility into two separate plugins is "make-work". Preauthenticated identities will be selected first when deciding which identity to use for any given request. - Insert a 'repoze.pam.identity' key into the WSGI environment on ingress if an identity is found. Its value will be the identity dictionary related to the identity selected by repoze.pam on ingress. Downstream consumers are allowed to mutate this dictionary; this value is passed to "remember" and "forget", so its main use is to do a "credentials reset"; e.g. a user has changed his username or password within the application, but we don't want to force him to log in again after he does so. 0.4 (03-07-2008) ---------------- - Allow plugins to specify a classifiers list per interface (instead of a single classifiers list per plugin). 0.3 (03-05-2008) ---------------- - Make SQLAuthenticatorPlugin's default_password_compare use hexdigest sha instead of base64'ed binary sha for simpler conversion. 0.2 (03-04-2008) ---------------- - Added SQLAuthenticatorPlugin (see plugins/sql.py). 0.1 (02-27-2008) ---------------- - Initial release (no configuration file support yet). repoze.who-1.0.18/COPYRIGHT.txt0000644000175000017500000000015511007074523015716 0ustar tseavertseaverCopyright (c) 2007 Agendaless Consulting and Contributors. (http://www.agendaless.com), All Rights Reserved repoze.who-1.0.18/repoze/0000755000175000017500000000000011274642513015116 5ustar tseavertseaverrepoze.who-1.0.18/repoze/__init__.py0000644000175000017500000000011111007074523017212 0ustar tseavertseaver# repoze package __import__('pkg_resources').declare_namespace(__name__) repoze.who-1.0.18/repoze/who/0000755000175000017500000000000011274642513015713 5ustar tseavertseaverrepoze.who-1.0.18/repoze/who/interfaces.py0000644000175000017500000001651311173115704020411 0ustar tseavertseaverfrom zope.interface import Interface class IPlugin(Interface): pass class IRequestClassifier(IPlugin): """ On ingress: classify a request. """ def __call__(environ): """ environ -> request classifier string This interface is responsible for returning a string value representing a request classification. o 'environ' is the WSGI environment. """ class IChallengeDecider(IPlugin): """ On egress: decide whether a challenge needs to be presented to the user. """ def __call__(environ, status, headers): """ args -> True | False o 'environ' is the WSGI environment. o 'status' is the HTTP status as returned by the downstream WSGI application. o 'headers' are the headers returned by the downstream WSGI application. This interface is responsible for returning True if a challenge needs to be presented to the user, False otherwise. """ class IIdentifier(IPlugin): """ On ingress: Extract credentials from the WSGI environment and turn them into an identity. On egress (remember): Conditionally set information in the response headers allowing the remote system to remember this identity. On egress (forget): Conditionally set information in the response headers allowing the remote system to forget this identity (during a challenge). """ def identify(environ): """ On ingress: environ -> { k1 : v1 , ... , kN : vN } | None o 'environ' is the WSGI environment. o If credentials are found, the returned identity mapping will contain an arbitrary set of key/value pairs. If the identity is based on a login and password, the environment is recommended to contain at least 'login' and 'password' keys as this provides compatibility between the plugin and existing authenticator plugins. If the identity can be 'preauthenticated' (e.g. if the userid is embedded in the identity, such as when we're using ticket-based authentication), the plugin should set the userid in the special 'repoze.who.userid' key; no authenticators will be asked to authenticate the identity thereafer. o Return None to indicate that the plugin found no appropriate credentials. o Only IIdentifier plugins which match one of the the current request's classifications will be asked to perform identification. o An identifier plugin is permitted to add a key to the environment named 'repoze.who.application', which should be an arbitrary WSGI application. If an identifier plugin does so, this application is used instead of the downstream application set up within the middleware. This feature is useful for identifier plugins which need to perform redirection to obtain credentials. If two identifier plugins add a 'repoze.who.application' WSGI application to the environment, the last one consulted will"win". """ def remember(environ, identity): """ On egress (no challenge required): args -> [ (header-name, header-value), ...] | None Return a list of headers suitable for allowing the requesting system to remember the identification information (e.g. a Set-Cookie header). Return None if no headers need to be set. These headers will be appended to any headers returned by the downstream application. """ def forget(environ, identity): """ On egress (challenge required): args -> [ (header-name, header-value), ...] | None Return a list of headers suitable for allowing the requesting system to forget the identification information (e.g. a Set-Cookie header with an expires date in the past). Return None if no headers need to be set. These headers will be included in the response provided by the challenge app. """ class IAuthenticator(IPlugin): """ On ingress: validate the identity and return a user id or None. """ def authenticate(environ, identity): """ identity -> 'userid' | None o 'environ' is the WSGI environment. o 'identity' will be a dictionary (with arbitrary keys and values). o The IAuthenticator should return a single user id (optimally a string) if the identity can be authenticated. If the identify cannot be authenticated, the IAuthenticator should return None. Each instance of a registered IAuthenticator plugin that matches the request classifier will be called N times during a single request, where N is the number of identities found by any IIdentifierPlugin instances. An authenticator must not raise an exception if it is provided an identity dictionary that it does not understand (e.g. if it presumes that 'login' and 'password' are keys in the dictionary, it should check for the existence of these keys before attempting to do anything; if they don't exist, it should return None). An authenticator is permitted to add extra keys to the 'identity' dictionary (e.g., to save metadata from a database query, rather than requiring a separate query from an IMetadataProvider plugin). """ class IChallenger(IPlugin): """ On egress: Conditionally initiate a challenge to the user to provide credentials. Only challenge plugins which match one of the the current response's classifications will be asked to perform a challenge. """ def challenge(environ, status, app_headers, forget_headers): """ args -> WSGI application or None o 'environ' is the WSGI environment. o 'status' is the status written into start_response by the downstream application. o 'app_headers' is the headers list written into start_response by the downstream application. o 'forget_headers' is a list of headers which must be passed back in the response in order to perform credentials reset (logout). These come from the 'forget' method of IIdentifier plugin used to do the request's identification. Examine the values passed in and return a WSGI application (a callable which accepts environ and start_response as its two positional arguments, ala PEP 333) which causes a challenge to be performed. Return None to forego performing a challenge. """ class IMetadataProvider(IPlugin): """On ingress: When an identity is authenticated, metadata providers may scribble on the identity dictionary arbitrarily. Return values from metadata providers are ignored. """ def add_metadata(environ, identity): """ Add metadata to the identity (which is a dictionary). One value is always guaranteed to be in the dictionary when add_metadata is called: 'repoze.who.userid', representing the user id of the identity. Availability and composition of other keys will depend on the identifier plugin which created the identity. """ repoze.who-1.0.18/repoze/who/plugins/0000755000175000017500000000000011274642513017374 5ustar tseavertseaverrepoze.who-1.0.18/repoze/who/plugins/__init__.py0000644000175000017500000000015111201101422021455 0ustar tseavertseaver# repoze.who.plugins package __import__('pkg_resources').declare_namespace(__name__) #pragma NO COVERAGE repoze.who-1.0.18/repoze/who/plugins/sql.py0000644000175000017500000001004411201110375020526 0ustar tseavertseaverfrom zope.interface import implements from repoze.who.interfaces import IAuthenticator from repoze.who.interfaces import IMetadataProvider def default_password_compare(cleartext_password, stored_password_hash): try: from hashlib import sha1 except ImportError: # Python < 2.5 #pragma NO COVERAGE from sha import new as sha1 #pragma NO COVERAGE # the stored password is stored as '{SHA}'. # or as a cleartext password (no {SHA} prefix) if stored_password_hash.startswith('{SHA}'): stored_password_hash = stored_password_hash[5:] digest = sha1(cleartext_password).hexdigest() else: digest = cleartext_password if stored_password_hash == digest: return True return False def make_psycopg_conn_factory(**kw): # convenience (I always seem to use Postgres) def conn_factory(): #pragma NO COVERAGE import psycopg2 #pragma NO COVERAGE return psycopg2.connect(kw['repoze.who.dsn']) #pragma NO COVERAGE return conn_factory #pragma NO COVERAGE class SQLAuthenticatorPlugin: implements(IAuthenticator) def __init__(self, query, conn_factory, compare_fn): # statement should be pyformat dbapi binding-style, e.g. # "select user_id, password from users where login=%(login)s" self.query = query self.conn_factory = conn_factory self.compare_fn = compare_fn or default_password_compare self.conn = None # IAuthenticator def authenticate(self, environ, identity): if not 'login' in identity: return None if not self.conn: self.conn = self.conn_factory() curs = self.conn.cursor() curs.execute(self.query, identity) result = curs.fetchone() curs.close() if result: user_id, password = result if self.compare_fn(identity['password'], password): return user_id class SQLMetadataProviderPlugin: implements(IMetadataProvider) def __init__(self, name, query, conn_factory, filter): self.name = name self.query = query self.conn_factory = conn_factory self.filter = filter self.conn = None # IMetadataProvider def add_metadata(self, environ, identity): if self.conn is None: self.conn = self.conn_factory() curs = self.conn.cursor() # can't use dots in names in python string formatting :-( identity['__userid'] = identity['repoze.who.userid'] curs.execute(self.query, identity) result = curs.fetchall() if self.filter: result = self.filter(result) curs.close() del identity['__userid'] identity[self.name] = result def make_authenticator_plugin(query=None, conn_factory=None, compare_fn=None, **kw): from repoze.who.utils import resolveDotted if query is None: raise ValueError('query must be specified') if conn_factory is None: raise ValueError('conn_factory must be specified') try: conn_factory = resolveDotted(conn_factory)(**kw) except Exception, why: raise ValueError('conn_factory could not be resolved: %s' % why) if compare_fn is not None: compare_fn = resolveDotted(compare_fn) return SQLAuthenticatorPlugin(query, conn_factory, compare_fn) def make_metadata_plugin(name=None, query=None, conn_factory=None, filter=None, **kw): from repoze.who.utils import resolveDotted if name is None: raise ValueError('name must be specified') if query is None: raise ValueError('query must be specified') if conn_factory is None: raise ValueError('conn_factory must be specified') try: conn_factory = resolveDotted(conn_factory)(**kw) except Exception, why: raise ValueError('conn_factory could not be resolved: %s' % why) if filter is not None: filter = resolveDotted(filter) return SQLMetadataProviderPlugin(name, query, conn_factory, filter) repoze.who-1.0.18/repoze/who/plugins/auth_tkt.py0000644000175000017500000001743511274642115021601 0ustar tseavertseaverimport datetime from codecs import utf_8_decode from codecs import utf_8_encode import os import time from paste.request import get_cookies from paste.auth import auth_tkt from zope.interface import implements from repoze.who.interfaces import IIdentifier _NOW_TESTING = None # unit tests can replace def _now(): #pragma NO COVERAGE if _NOW_TESTING is not None: return _NOW_TESTING return datetime.datetime.now() class AuthTktCookiePlugin(object): implements(IIdentifier) userid_type_decoders = { 'int':int, 'unicode':lambda x: utf_8_decode(x)[0], } userid_type_encoders = { int: ('int', str), long: ('int', str), unicode: ('unicode', lambda x: utf_8_encode(x)[0]), } def __init__(self, secret, cookie_name='auth_tkt', secure=False, include_ip=False, timeout=None, reissue_time=None, userid_checker=None): self.secret = secret self.cookie_name = cookie_name self.include_ip = include_ip self.secure = secure if timeout and ( (not reissue_time) or (reissue_time > timeout) ): raise ValueError('When timeout is specified, reissue_time must ' 'be set to a lower value') self.timeout = timeout self.reissue_time = reissue_time self.userid_checker = userid_checker # IIdentifier def identify(self, environ): cookies = get_cookies(environ) cookie = cookies.get(self.cookie_name) if cookie is None or not cookie.value: return None if self.include_ip: remote_addr = environ['REMOTE_ADDR'] else: remote_addr = '0.0.0.0' try: timestamp, userid, tokens, user_data = auth_tkt.parse_ticket( self.secret, cookie.value, remote_addr) except auth_tkt.BadTicket: return None if self.userid_checker and not self.userid_checker(userid): return None if self.timeout and ( (timestamp + self.timeout) < time.time() ): return None userid_typename = 'userid_type:' user_data_info = user_data.split('|') for datum in filter(None, user_data_info): if datum.startswith(userid_typename): userid_type = datum[len(userid_typename):] decoder = self.userid_type_decoders.get(userid_type) if decoder: userid = decoder(userid) environ['REMOTE_USER_TOKENS'] = tokens environ['REMOTE_USER_DATA'] = user_data environ['AUTH_TYPE'] = 'cookie' identity = {} identity['timestamp'] = timestamp identity['repoze.who.userid'] = userid identity['tokens'] = tokens identity['userdata'] = user_data return identity def _get_cookies(self, environ, value, max_age=None): if max_age is not None: later = _now() + datetime.timedelta(seconds=int(max_age)) # Wdy, DD-Mon-YY HH:MM:SS GMT expires = later.strftime('%a, %d %b %Y %H:%M:%S') # the Expires header is *required* at least for IE7 (IE7 does # not respect Max-Age) max_age = "; Max-Age=%s; Expires=%s" % (max_age, expires) else: max_age = '' cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME')) wild_domain = '.' + cur_domain cookies = [ ('Set-Cookie', '%s="%s"; Path=/%s' % ( self.cookie_name, value, max_age)), ('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s' % ( self.cookie_name, value, cur_domain, max_age)), ('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s' % ( self.cookie_name, value, wild_domain, max_age)) ] return cookies # IIdentifier def forget(self, environ, identity): # return a set of expires Set-Cookie headers return self._get_cookies(environ, 'INVALID', 0) # IIdentifier def remember(self, environ, identity): if self.include_ip: remote_addr = environ['REMOTE_ADDR'] else: remote_addr = '0.0.0.0' cookies = get_cookies(environ) old_cookie = cookies.get(self.cookie_name) existing = cookies.get(self.cookie_name) old_cookie_value = getattr(existing, 'value', None) max_age = identity.get('max_age', None) timestamp, userid, tokens, userdata = None, '', '', '' if old_cookie_value: try: timestamp,userid,tokens,userdata = auth_tkt.parse_ticket( self.secret, old_cookie_value, remote_addr) except auth_tkt.BadTicket: pass who_userid = identity['repoze.who.userid'] who_tokens = identity.get('tokens', '') who_userdata = identity.get('userdata', '') encoding_data = self.userid_type_encoders.get(type(who_userid)) if encoding_data: encoding, encoder = encoding_data who_userid = encoder(who_userid) who_userdata = 'userid_type:%s' % encoding if not isinstance(tokens, basestring): tokens = ','.join(tokens) if not isinstance(who_tokens, basestring): who_tokens = ','.join(who_tokens) old_data = (userid, tokens, userdata) new_data = (who_userid, who_tokens, who_userdata) if old_data != new_data or (self.reissue_time and ( (timestamp + self.reissue_time) < time.time() )): ticket = auth_tkt.AuthTicket( self.secret, who_userid, remote_addr, tokens=who_tokens, user_data=who_userdata, cookie_name=self.cookie_name, secure=self.secure) new_cookie_value = ticket.cookie_value() cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME')) wild_domain = '.' + cur_domain if old_cookie_value != new_cookie_value: # return a set of Set-Cookie headers return self._get_cookies(environ, new_cookie_value, max_age) def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) #pragma NO COVERAGE def _bool(value): if isinstance(value, basestring): return value.lower() in ('yes', 'true', '1') return value def make_plugin(secret=None, secretfile=None, cookie_name='auth_tkt', secure=False, include_ip=False, timeout=None, reissue_time=None, userid_checker=None, ): from repoze.who.utils import resolveDotted if (secret is None and secretfile is None): raise ValueError("One of 'secret' or 'secretfile' must not be None.") if (secret is not None and secretfile is not None): raise ValueError("Specify only one of 'secret' or 'secretfile'.") if secretfile: secretfile = os.path.abspath(os.path.expanduser(secretfile)) if not os.path.exists(secretfile): raise ValueError("No such 'secretfile': %s" % secretfile) secret = open(secretfile).read().strip() if timeout: timeout = int(timeout) if reissue_time: reissue_time = int(reissue_time) if userid_checker is not None: userid_checker = resolveDotted(userid_checker) plugin = AuthTktCookiePlugin(secret, cookie_name, _bool(secure), _bool(include_ip), timeout, reissue_time, userid_checker, ) return plugin repoze.who-1.0.18/repoze/who/plugins/htpasswd.py0000644000175000017500000000352111201105637021573 0ustar tseavertseaverfrom zope.interface import implements from repoze.who.interfaces import IAuthenticator from repoze.who.utils import resolveDotted class HTPasswdPlugin(object): implements(IAuthenticator) def __init__(self, filename, check): self.filename = filename self.check = check # IAuthenticatorPlugin def authenticate(self, environ, identity): try: login = identity['login'] password = identity['password'] except KeyError: return None if hasattr(self.filename, 'seek'): # assumed to have a readline self.filename.seek(0) f = self.filename else: try: f = open(self.filename, 'r') except IOError: environ['repoze.who.logger'].warn('could not open htpasswd ' 'file %s' % self.filename) return None for line in f: try: username, hashed = line.rstrip().split(':', 1) except ValueError: continue if username == login: if self.check(password, hashed): return username return None def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) #pragma NO COVERAGE def crypt_check(password, hashed): from crypt import crypt salt = hashed[:2] return hashed == crypt(password, salt) def plain_check(password, hashed): return hashed == password def make_plugin(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 HTPasswdPlugin(filename, check) repoze.who-1.0.18/repoze/who/plugins/form.py0000644000175000017500000002130711274635377020726 0ustar tseavertseaverimport urlparse import urllib import cgi from paste.httpheaders import CONTENT_LENGTH from paste.httpheaders import CONTENT_TYPE from paste.httpheaders import LOCATION from paste.httpexceptions import HTTPFound from paste.httpexceptions import HTTPUnauthorized from paste.request import parse_dict_querystring from paste.request import parse_formvars from paste.request import construct_url from paste.response import header_value from zope.interface import implements from repoze.who.config import _resolve from repoze.who.interfaces import IChallenger from repoze.who.interfaces import IIdentifier _DEFAULT_FORM = """ Log In
Log In

User Name
Password
  
""" class FormPluginBase(object): def _get_rememberer(self, environ): rememberer = environ['repoze.who.plugins'][self.rememberer_name] return rememberer # IIdentifier def remember(self, environ, identity): rememberer = self._get_rememberer(environ) return rememberer.remember(environ, identity) # IIdentifier def forget(self, environ, identity): rememberer = self._get_rememberer(environ) return rememberer.forget(environ, identity) def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) #pragma NO COVERAGE class FormPlugin(FormPluginBase): implements(IChallenger, IIdentifier) def __init__(self, login_form_qs, rememberer_name, formbody=None, formcallable=None): self.login_form_qs = login_form_qs # rememberer_name is the name of another configured plugin which # implements IIdentifier, to handle remember and forget duties # (ala a cookie plugin or a session plugin) self.rememberer_name = rememberer_name self.formbody = formbody self.formcallable = formcallable # IIdentifier def identify(self, environ): query = parse_dict_querystring(environ) # If the extractor finds a special query string on any request, # it will attempt to find the values in the input body. if query.get(self.login_form_qs): form = parse_formvars(environ) from StringIO import StringIO # XXX we need to replace wsgi.input because we've read it # this smells funny environ['wsgi.input'] = StringIO() form.update(query) try: login = form['login'] password = form['password'] except KeyError: return None del query[self.login_form_qs] environ['QUERY_STRING'] = urllib.urlencode(query) environ['repoze.who.application'] = HTTPFound( construct_url(environ)) credentials = {'login':login, 'password':password} max_age = form.get('max_age', None) if max_age is not None: credentials['max_age'] = max_age return credentials return None # IChallenger def challenge(self, environ, status, app_headers, forget_headers): if app_headers: location = LOCATION(app_headers) if location: headers = list(app_headers) + list(forget_headers) return HTTPFound(headers = headers) form = self.formbody or _DEFAULT_FORM if self.formcallable is not None: form = self.formcallable(environ) def auth_form(environ, start_response): content_length = CONTENT_LENGTH.tuples(str(len(form))) content_type = CONTENT_TYPE.tuples('text/html') headers = content_length + content_type + forget_headers start_response('200 OK', headers) return [form] return auth_form class RedirectingFormPlugin(FormPluginBase): implements(IChallenger, IIdentifier) def __init__(self, login_form_url, login_handler_path, logout_handler_path, rememberer_name, reason_param='reason'): self.login_form_url = login_form_url self.login_handler_path = login_handler_path self.logout_handler_path = logout_handler_path # rememberer_name is the name of another configured plugin which # implements IIdentifier, to handle remember and forget duties # (ala a cookie plugin or a session plugin) self.rememberer_name = rememberer_name self.reason_param = reason_param # IIdentifier def identify(self, environ): path_info = environ['PATH_INFO'] query = parse_dict_querystring(environ) if path_info == self.logout_handler_path: # we've been asked to perform a logout form = parse_formvars(environ) form.update(query) referer = environ.get('HTTP_REFERER', '/') came_from = form.get('came_from', referer) # set in environ for self.challenge() to find later environ['came_from'] = came_from environ['repoze.who.application'] = HTTPUnauthorized() return None elif path_info == self.login_handler_path: # we've been asked to perform a login form = parse_formvars(environ) form.update(query) try: login = form['login'] password = form['password'] max_age = form.get('max_age', None) credentials = { 'login':form['login'], 'password':form['password'], } except KeyError: credentials = None if credentials is not None: max_age = form.get('max_age', None) if max_age is not None: credentials['max_age'] = max_age referer = environ.get('HTTP_REFERER', '/') came_from = form.get('came_from', referer) environ['repoze.who.application'] = HTTPFound(came_from) return credentials # IChallenger def challenge(self, environ, status, app_headers, forget_headers): reason = header_value(app_headers, 'X-Authorization-Failure-Reason') url_parts = list(urlparse.urlparse(self.login_form_url)) query = url_parts[4] query_elements = cgi.parse_qs(query) came_from = environ.get('came_from', construct_url(environ)) query_elements['came_from'] = came_from if reason: query_elements[self.reason_param] = reason url_parts[4] = urllib.urlencode(query_elements, doseq=True) login_form_url = urlparse.urlunparse(url_parts) headers = [ ('Location', login_form_url) ] cookies = [(h,v) for (h,v) in app_headers if h.lower() == 'set-cookie'] headers = headers + forget_headers + cookies return HTTPFound(headers=headers) def make_plugin(login_form_qs='__do_login', rememberer_name=None, form=None, formcallable=None, ): if rememberer_name is None: raise ValueError( 'must include rememberer key (name of another IIdentifier plugin)') if form is not None: form = open(form).read() if isinstance(formcallable, str): formcallable = _resolve(formcallable) plugin = FormPlugin(login_form_qs, rememberer_name, form, formcallable) return plugin def make_redirecting_plugin(login_form_url=None, login_handler_path='/login_handler', logout_handler_path='/logout_handler', rememberer_name=None): if login_form_url is None: raise ValueError( 'must include login_form_url in configuration') if login_handler_path is None: raise ValueError( 'login_handler_path must not be None') if logout_handler_path is None: raise ValueError( 'logout_handler_path must not be None') if rememberer_name is None: raise ValueError( 'must include rememberer key (name of another IIdentifier plugin)') plugin = RedirectingFormPlugin(login_form_url, login_handler_path, logout_handler_path, rememberer_name) return plugin repoze.who-1.0.18/repoze/who/plugins/tests/0000755000175000017500000000000011274642513020536 5ustar tseavertseaverrepoze.who-1.0.18/repoze/who/plugins/tests/__init__.py0000644000175000017500000000001111201101303022610 0ustar tseavertseaver#package repoze.who-1.0.18/repoze/who/plugins/tests/test_cookie.py0000644000175000017500000000621111201104036023377 0ustar tseavertseaverimport unittest class TestInsecureCookiePlugin(unittest.TestCase): def _getTargetClass(self): from repoze.who.plugins.cookie import InsecureCookiePlugin return InsecureCookiePlugin def _makeOne(self, *arg, **kw): plugin = self._getTargetClass()(*arg, **kw) return plugin def _makeEnviron(self, kw=None): environ = {} environ['wsgi.version'] = (1,0) if kw is not None: environ.update(kw) return environ def test_implements(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IIdentifier klass = self._getTargetClass() verifyClass(IIdentifier, klass) def test_identify_nocookies(self): plugin = self._makeOne('oatmeal') environ = self._makeEnviron() result = plugin.identify(environ) self.assertEqual(result, None) def test_identify_badcookies(self): plugin = self._makeOne('oatmeal') environ = self._makeEnviron({'HTTP_COOKIE':'oatmeal=a'}) result = plugin.identify(environ) self.assertEqual(result, None) def test_identify_badcookies_binascci_but_not_splittable(self): plugin = self._makeOne('oatmeal') auth = 'bogus'.encode('base64').rstrip() environ = self._makeEnviron({'HTTP_COOKIE':'oatmeal=%s' % auth}) result = plugin.identify(environ) self.assertEqual(result, None) def test_identify_success(self): plugin = self._makeOne('oatmeal') auth = 'foo:password'.encode('base64').rstrip() environ = self._makeEnviron({'HTTP_COOKIE':'oatmeal=%s;' % auth}) result = plugin.identify(environ) self.assertEqual(result, {'login':'foo', 'password':'password'}) def test_remember_creds_same(self): plugin = self._makeOne('oatmeal') creds = {'login':'foo', 'password':'password'} auth = 'foo:password'.encode('base64').rstrip() auth = 'oatmeal=%s;' % auth environ = self._makeEnviron({'HTTP_COOKIE':auth}) result = plugin.remember(environ, creds) self.assertEqual(result, None) def test_remember_creds_different(self): plugin = self._makeOne('oatmeal') creds = {'login':'bar', 'password':'password'} auth = 'foo:password'.encode('base64').rstrip() creds_auth = 'bar:password'.encode('base64').rstrip() environ = self._makeEnviron({'HTTP_COOKIE':'oatmeal=%s;' % auth}) result = plugin.remember(environ, creds) expected = 'oatmeal=%s; Path=/;' % creds_auth self.assertEqual(result, [('Set-Cookie', expected)]) def test_factory(self): from repoze.who.plugins.cookie import make_plugin plugin = make_plugin('foo') self.assertEqual(plugin.cookie_name, 'foo') def test_forget(self): plugin = self._makeOne('oatmeal') headers = plugin.forget({}, None) self.assertEqual(len(headers), 1) header = headers[0] name, value = header self.assertEqual(name, 'Set-Cookie') self.assertEqual(value, 'oatmeal=""; Path=/; Expires=Sun, 10-May-1971 11:59:00 GMT') repoze.who-1.0.18/repoze/who/plugins/tests/test_htpasswd.py0000644000175000017500000001210411201106275023770 0ustar tseavertseaverimport unittest class TestHTPasswdPlugin(unittest.TestCase): def _getTargetClass(self): from repoze.who.plugins.htpasswd import HTPasswdPlugin return HTPasswdPlugin def _makeOne(self, *arg, **kw): plugin = self._getTargetClass()(*arg, **kw) return plugin def _makeEnviron(self, kw=None): environ = {} environ['wsgi.version'] = (1,0) if kw is not None: environ.update(kw) return environ def test_implements(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IAuthenticator klass = self._getTargetClass() verifyClass(IAuthenticator, klass) def test_authenticate_nocreds(self): from StringIO import StringIO io = StringIO() plugin = self._makeOne(io, None) environ = self._makeEnviron() creds = {} result = plugin.authenticate(environ, creds) self.assertEqual(result, None) def test_authenticate_nolines(self): from StringIO import StringIO io = StringIO() plugin = self._makeOne(io, None) environ = self._makeEnviron() creds = {'login':'chrism', 'password':'pass'} result = plugin.authenticate(environ, creds) self.assertEqual(result, None) def test_authenticate_nousermatch(self): from StringIO import StringIO io = StringIO('nobody:foo') plugin = self._makeOne(io, None) environ = self._makeEnviron() creds = {'login':'chrism', 'password':'pass'} result = plugin.authenticate(environ, creds) self.assertEqual(result, None) def test_authenticate_match(self): from StringIO import StringIO io = StringIO('chrism:pass') def check(password, hashed): return True plugin = self._makeOne(io, check) environ = self._makeEnviron() creds = {'login':'chrism', 'password':'pass'} result = plugin.authenticate(environ, creds) self.assertEqual(result, 'chrism') def test_authenticate_badline(self): from StringIO import StringIO io = StringIO('badline\nchrism:pass') def check(password, hashed): return True plugin = self._makeOne(io, check) environ = self._makeEnviron() creds = {'login':'chrism', 'password':'pass'} result = plugin.authenticate(environ, creds) self.assertEqual(result, 'chrism') def test_authenticate_filename(self): import os here = os.path.abspath(os.path.dirname(__file__)) htpasswd = os.path.join(here, 'fixtures', 'test.htpasswd') def check(password, hashed): return True plugin = self._makeOne(htpasswd, check) environ = self._makeEnviron() creds = {'login':'chrism', 'password':'pass'} result = plugin.authenticate(environ, creds) self.assertEqual(result, 'chrism') def test_authenticate_bad_filename_logs_to_repoze_who_logger(self): import os here = os.path.abspath(os.path.dirname(__file__)) htpasswd = os.path.join(here, 'fixtures', 'test.htpasswd.nonesuch') def check(password, hashed): return True plugin = self._makeOne(htpasswd, check) environ = self._makeEnviron() class DummyLogger: warnings = [] def warn(self, msg): self.warnings.append(msg) logger = environ['repoze.who.logger'] = DummyLogger() creds = {'login':'chrism', 'password':'pass'} result = plugin.authenticate(environ, creds) self.assertEqual(result, None) self.assertEqual(len(logger.warnings), 1) self.failUnless('could not open htpasswd' in logger.warnings[0]) def test_crypt_check(self): import sys # win32 does not have a crypt library, don't # fail here if "win32" == sys.platform: return from crypt import crypt salt = '123' hashed = crypt('password', salt) from repoze.who.plugins.htpasswd import crypt_check self.assertEqual(crypt_check('password', hashed), True) self.assertEqual(crypt_check('notpassword', hashed), False) def test_plain_check(self): from repoze.who.plugins.htpasswd import plain_check self.failUnless(plain_check('password', 'password')) self.failIf(plain_check('notpassword', 'password')) def test_factory_no_filename_raises(self): from repoze.who.plugins.htpasswd import make_plugin self.assertRaises(ValueError, make_plugin) def test_factory_no_check_fn_raises(self): from repoze.who.plugins.htpasswd import make_plugin self.assertRaises(ValueError, make_plugin, 'foo') def test_factory(self): from repoze.who.plugins.htpasswd import make_plugin from repoze.who.plugins.htpasswd import crypt_check plugin = make_plugin('foo', 'repoze.who.plugins.htpasswd:crypt_check') self.assertEqual(plugin.filename, 'foo') self.assertEqual(plugin.check, crypt_check) repoze.who-1.0.18/repoze/who/plugins/tests/test_form.py0000644000175000017500000006212111274635417023121 0ustar tseavertseaverimport unittest class TestFormPlugin(unittest.TestCase): def _getTargetClass(self): from repoze.who.plugins.form import FormPlugin return FormPlugin def _makeOne(self, login_form_qs='__do_login', rememberer_name='cookie', formbody=None, formcallable=None, ): plugin = self._getTargetClass()(login_form_qs, rememberer_name, formbody, formcallable) return plugin def _makeEnviron(self, login=None, password=None, do_login=False, max_age=None): from StringIO import StringIO fields = [] if login: fields.append(('login', login)) if password: fields.append(('password', password)) if max_age: fields.append(('max_age', max_age)) content_type, body = encode_multipart_formdata(fields) credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) environ = {'wsgi.version': (1,0), 'wsgi.input': StringIO(body), 'wsgi.url_scheme': 'http', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '8080', 'CONTENT_TYPE': content_type, 'CONTENT_LENGTH': len(body), 'REQUEST_METHOD': 'POST', 'repoze.who.plugins': {'cookie':identifier}, 'PATH_INFO': '/protected', 'QUERY_STRING': '', } if do_login: environ['QUERY_STRING'] = '__do_login=true' return environ def test_implements(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IChallenger klass = self._getTargetClass() verifyClass(IIdentifier, klass) verifyClass(IChallenger, klass) def test_identify_noqs(self): plugin = self._makeOne() environ = self._makeEnviron() result = plugin.identify(environ) self.assertEqual(result, None) def test_identify_qs_no_values(self): plugin = self._makeOne() environ = self._makeEnviron(do_login=True) result = plugin.identify(environ) self.assertEqual(result, None) def test_identify_nologin(self): plugin = self._makeOne() environ = self._makeEnviron(do_login=True, login='chris') result = plugin.identify(environ) self.assertEqual(result, None) def test_identify_nopassword(self): plugin = self._makeOne() environ = self._makeEnviron(do_login=True, password='password') result = plugin.identify(environ) self.assertEqual(result, None) def test_identify_success(self): from paste.httpexceptions import HTTPFound plugin = self._makeOne() environ = self._makeEnviron(do_login=True, login='chris', password='password') result = plugin.identify(environ) self.assertEqual(result, {'login':'chris', 'password':'password'}) app = environ['repoze.who.application'] self.failUnless(isinstance(app, HTTPFound)) self.assertEqual(app.location(), 'http://localhost:8080/protected') def test_identify_success_with_max_age(self): from paste.httpexceptions import HTTPFound plugin = self._makeOne() environ = self._makeEnviron(do_login=True, login='chris', password='password', max_age='500') result = plugin.identify(environ) self.assertEqual(result, {'login':'chris', 'password':'password', 'max_age':'500'}) app = environ['repoze.who.application'] self.failUnless(isinstance(app, HTTPFound)) self.assertEqual(app.location(), 'http://localhost:8080/protected') def test_remember(self): plugin = self._makeOne() environ = self._makeEnviron() identity = {} result = plugin.remember(environ, identity) self.assertEqual(result, None) self.assertEqual(environ['repoze.who.plugins']['cookie'].remembered, identity) def test_forget(self): plugin = self._makeOne() environ = self._makeEnviron() identity = {} result = plugin.forget(environ, identity) self.assertEqual(result, None) self.assertEqual(environ['repoze.who.plugins']['cookie'].forgotten, identity ) def test_challenge_defaultform(self): from repoze.who.plugins.form import _DEFAULT_FORM plugin = self._makeOne() environ = self._makeEnviron() app = plugin.challenge(environ, '401 Unauthorized', [], []) sr = DummyStartResponse() result = app(environ, sr) self.assertEqual(''.join(result), _DEFAULT_FORM) self.assertEqual(len(sr.headers), 2) cl = str(len(_DEFAULT_FORM)) self.assertEqual(sr.headers[0], ('Content-Length', cl)) self.assertEqual(sr.headers[1], ('Content-Type', 'text/html')) self.assertEqual(sr.status, '200 OK') def test_challenge_customform(self): import os here = os.path.dirname(__file__) fixtures = os.path.join(here, 'fixtures') form = os.path.join(fixtures, 'form.html') formbody = open(form).read() plugin = self._makeOne(formbody=formbody) environ = self._makeEnviron() app = plugin.challenge(environ, '401 Unauthorized', [], []) sr = DummyStartResponse() result = app(environ, sr) self.assertEqual(''.join(result), formbody) self.assertEqual(len(sr.headers), 2) cl = str(len(formbody)) self.assertEqual(sr.headers[0], ('Content-Length', cl)) self.assertEqual(sr.headers[1], ('Content-Type', 'text/html')) self.assertEqual(sr.status, '200 OK') def test_challenge_formcallable(self): def _formcallable(environ): return 'formcallable' plugin = self._makeOne(formcallable=_formcallable) environ = self._makeEnviron() app = plugin.challenge(environ, '401 Unauthorized', [], []) sr = DummyStartResponse() result = app(environ, sr) self.assertEqual(result, ['formcallable']) def test_challenge_with_location(self): plugin = self._makeOne() environ = self._makeEnviron() app = plugin.challenge(environ, '401 Unauthorized', [('Location', 'http://foo/bar')], [('Set-Cookie', 'a=123')]) sr = DummyStartResponse() app(environ, sr) headers = sorted(sr.headers) self.assertEqual(len(headers), 3) self.assertEqual(headers[0], ('Location', 'http://foo/bar')) self.assertEqual(headers[1], ('Set-Cookie', 'a=123')) self.assertEqual(headers[2], ('content-type', 'text/plain; charset=utf8')) self.assertEqual(sr.status, '302 Found') class Test_make_plugin(unittest.TestCase): def _callFUT(self, *args, **kw): from repoze.who.plugins.form import make_plugin return make_plugin(*args, **kw) def test_no_rememberer_name_raises(self): self.assertRaises(ValueError, self._callFUT) def test_with_form(self): import os here = os.path.dirname(__file__) fixtures = os.path.join(here, 'fixtures') form = os.path.join(fixtures, 'form.html') formbody = open(form).read() plugin = self._callFUT('__login', 'cookie', form) self.assertEqual(plugin.login_form_qs, '__login') self.assertEqual(plugin.rememberer_name, 'cookie') self.assertEqual(plugin.formbody, formbody) self.assertEqual(plugin.formcallable, None) def test_default_form(self): plugin = self._callFUT('__login', 'cookie') self.assertEqual(plugin.login_form_qs, '__login') self.assertEqual(plugin.rememberer_name, 'cookie') self.assertEqual(plugin.formbody, None) self.assertEqual(plugin.formcallable, None) def test_with_formcallable(self): dotted='repoze.who.plugins.tests.test_form:sample_formcallable' plugin = self._callFUT('__login', 'cookie', formcallable=dotted ) self.assertEqual(plugin.formcallable, sample_formcallable) def sample_formcallable(environ): return {'foo': 'bar'} class TestRedirectingFormPlugin(unittest.TestCase): def _getTargetClass(self): from repoze.who.plugins.form import RedirectingFormPlugin return RedirectingFormPlugin def _makeOne(self, login_form_url='http://example.com/login.html', login_handler_path = '/login_handler', logout_handler_path = '/logout_handler', rememberer_name='cookie', reason_param='reason'): plugin = self._getTargetClass()(login_form_url, login_handler_path, logout_handler_path, rememberer_name, reason_param) return plugin def _makeEnviron(self, login=None, password=None, came_from=None, path_info='/', identifier=None, max_age=None): from StringIO import StringIO fields = [] if login: fields.append(('login', login)) if password: fields.append(('password', password)) if came_from: fields.append(('came_from', came_from)) if max_age: fields.append(('max_age', max_age)) if identifier is None: credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) content_type, body = encode_multipart_formdata(fields) environ = {'wsgi.version': (1,0), 'wsgi.input': StringIO(body), 'wsgi.url_scheme':'http', 'SERVER_NAME': 'www.example.com', 'SERVER_PORT': '80', 'CONTENT_TYPE': content_type, 'CONTENT_LENGTH': len(body), 'REQUEST_METHOD': 'POST', 'repoze.who.plugins': {'cookie':identifier}, 'QUERY_STRING': 'default=1', 'PATH_INFO': path_info, } return environ def test_implements(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IChallenger klass = self._getTargetClass() verifyClass(IIdentifier, klass) verifyClass(IChallenger, klass) def test_identify_pathinfo_miss(self): plugin = self._makeOne() environ = self._makeEnviron(path_info='/not_login_handler') result = plugin.identify(environ) self.assertEqual(result, None) self.failIf(environ.get('repoze.who.application')) def test_identify_via_login_handler(self): plugin = self._makeOne() environ = self._makeEnviron(path_info='/login_handler', login='chris', password='password', came_from='http://example.com') result = plugin.identify(environ) self.assertEqual(result, {'login':'chris', 'password':'password'}) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 1) name, value = app.headers[0] self.assertEqual(name, 'location') self.assertEqual(value, 'http://example.com') self.assertEqual(app.code, 302) def test_identify_via_login_handler_max_age(self): plugin = self._makeOne() environ = self._makeEnviron(path_info='/login_handler', login='chris', password='password', came_from='http://example.com', max_age='500') result = plugin.identify(environ) self.assertEqual(result, {'login':'chris', 'password':'password', 'max_age':'500'}) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 1) name, value = app.headers[0] self.assertEqual(name, 'location') self.assertEqual(value, 'http://example.com') self.assertEqual(app.code, 302) def test_identify_via_login_handler_no_username_pass(self): plugin = self._makeOne() environ = self._makeEnviron(path_info='/login_handler') result = plugin.identify(environ) self.assertEqual(result, None) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 1) name, value = app.headers[0] self.assertEqual(name, 'location') self.assertEqual(value, '/') self.assertEqual(app.code, 302) def test_identify_via_login_handler_no_came_from_no_http_referer(self): plugin = self._makeOne() environ = self._makeEnviron(path_info='/login_handler', login='chris', password='password') result = plugin.identify(environ) self.assertEqual(result, {'login':'chris', 'password':'password'}) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 1) name, value = app.headers[0] self.assertEqual(name, 'location') self.assertEqual(value, '/') self.assertEqual(app.code, 302) def test_identify_via_login_handler_no_came_from(self): plugin = self._makeOne() environ = self._makeEnviron(path_info='/login_handler', login='chris', password='password') environ['HTTP_REFERER'] = 'http://foo.bar' result = plugin.identify(environ) self.assertEqual(result, {'login':'chris', 'password':'password'}) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 1) name, value = app.headers[0] self.assertEqual(name, 'location') self.assertEqual(value, 'http://foo.bar') self.assertEqual(app.code, 302) def test_identify_via_logout_handler(self): plugin = self._makeOne() environ = self._makeEnviron(path_info='/logout_handler', login='chris', password='password', came_from='http://example.com') result = plugin.identify(environ) self.assertEqual(result, None) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 0) self.assertEqual(app.code, 401) self.assertEqual(environ['came_from'], 'http://example.com') def test_identify_via_logout_handler_no_came_from_no_http_referer(self): plugin = self._makeOne() environ = self._makeEnviron(path_info='/logout_handler', login='chris', password='password') result = plugin.identify(environ) self.assertEqual(result, None) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 0) self.assertEqual(app.code, 401) self.assertEqual(environ['came_from'], '/') def test_identify_via_logout_handler_no_came_from(self): plugin = self._makeOne() environ = self._makeEnviron(path_info='/logout_handler', login='chris', password='password') environ['HTTP_REFERER'] = 'http://example.com/referer' result = plugin.identify(environ) self.assertEqual(result, None) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 0) self.assertEqual(app.code, 401) self.assertEqual(environ['came_from'], 'http://example.com/referer') def test_remember(self): plugin = self._makeOne() environ = self._makeEnviron() identity = {} result = plugin.remember(environ, identity) self.assertEqual(result, None) self.assertEqual(environ['repoze.who.plugins']['cookie'].remembered, identity) def test_forget(self): plugin = self._makeOne() environ = self._makeEnviron() identity = {} result = plugin.forget(environ, identity) self.assertEqual(result, None) self.assertEqual(environ['repoze.who.plugins']['cookie'].forgotten, identity ) def test_challenge(self): plugin = self._makeOne() environ = self._makeEnviron() app = plugin.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) sr = DummyStartResponse() result = ''.join(app(environ, sr)) self.failUnless(result.startswith('302 Found')) self.assertEqual(len(sr.headers), 3) self.assertEqual(sr.headers[0][0], 'Location') url = sr.headers[0][1] import urlparse import cgi parts = urlparse.urlparse(url) parts_qsl = cgi.parse_qsl(parts[4]) self.assertEqual(len(parts_qsl), 1) came_from_key, came_from_value = parts_qsl[0] self.assertEqual(parts[0], 'http') self.assertEqual(parts[1], 'example.com') self.assertEqual(parts[2], '/login.html') self.assertEqual(parts[3], '') self.assertEqual(came_from_key, 'came_from') self.assertEqual(came_from_value, 'http://www.example.com/?default=1') headers = sr.headers self.assertEqual(len(headers), 3) self.assertEqual(sr.headers[1][0], 'forget') self.assertEqual(sr.headers[1][1], '1') self.assertEqual(sr.headers[2][0], 'content-type') self.assertEqual(sr.headers[2][1], 'text/plain; charset=utf8') self.assertEqual(sr.status, '302 Found') def test_challenge_came_from_in_environ(self): plugin = self._makeOne() environ = self._makeEnviron() environ['came_from'] = 'http://example.com/came_from' app = plugin.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) sr = DummyStartResponse() result = ''.join(app(environ, sr)) self.failUnless(result.startswith('302 Found')) self.assertEqual(len(sr.headers), 3) self.assertEqual(sr.headers[0][0], 'Location') url = sr.headers[0][1] import urlparse import cgi parts = urlparse.urlparse(url) parts_qsl = cgi.parse_qsl(parts[4]) self.assertEqual(len(parts_qsl), 1) came_from_key, came_from_value = parts_qsl[0] self.assertEqual(parts[0], 'http') self.assertEqual(parts[1], 'example.com') self.assertEqual(parts[2], '/login.html') self.assertEqual(parts[3], '') self.assertEqual(came_from_key, 'came_from') self.assertEqual(came_from_value, 'http://example.com/came_from') def test_challenge_with_reason_header(self): plugin = self._makeOne() environ = self._makeEnviron() environ['came_from'] = 'http://example.com/came_from' app = plugin.challenge( environ, '401 Unauthorized', [('X-Authorization-Failure-Reason', 'you are ugly')], [('forget', '1')]) sr = DummyStartResponse() result = ''.join(app(environ, sr)) self.failUnless(result.startswith('302 Found')) self.assertEqual(len(sr.headers), 3) self.assertEqual(sr.headers[0][0], 'Location') url = sr.headers[0][1] import urlparse import cgi parts = urlparse.urlparse(url) parts_qsl = cgi.parse_qsl(parts[4]) self.assertEqual(len(parts_qsl), 2) parts_qsl.sort() came_from_key, came_from_value = parts_qsl[0] reason_key, reason_value = parts_qsl[1] self.assertEqual(parts[0], 'http') self.assertEqual(parts[1], 'example.com') self.assertEqual(parts[2], '/login.html') self.assertEqual(parts[3], '') self.assertEqual(came_from_key, 'came_from') self.assertEqual(came_from_value, 'http://example.com/came_from') self.assertEqual(reason_key, 'reason') self.assertEqual(reason_value, 'you are ugly') def test_challenge_with_reason_and_custom_reason_param(self): plugin = self._makeOne(reason_param='auth_failure') environ = self._makeEnviron() environ['came_from'] = 'http://example.com/came_from' app = plugin.challenge( environ, '401 Unauthorized', [('X-Authorization-Failure-Reason', 'you are ugly')], [('forget', '1')]) sr = DummyStartResponse() result = ''.join(app(environ, sr)) self.failUnless(result.startswith('302 Found')) self.assertEqual(len(sr.headers), 3) self.assertEqual(sr.headers[0][0], 'Location') url = sr.headers[0][1] import urlparse import cgi parts = urlparse.urlparse(url) parts_qsl = cgi.parse_qsl(parts[4]) self.assertEqual(len(parts_qsl), 2) parts_qsl.sort() reason_key, reason_value = parts_qsl[0] came_from_key, came_from_value = parts_qsl[1] self.assertEqual(parts[0], 'http') self.assertEqual(parts[1], 'example.com') self.assertEqual(parts[2], '/login.html') self.assertEqual(parts[3], '') self.assertEqual(came_from_key, 'came_from') self.assertEqual(came_from_value, 'http://example.com/came_from') self.assertEqual(reason_key, 'auth_failure') self.assertEqual(reason_value, 'you are ugly') def test_challenge_with_setcookie_from_app(self): plugin = self._makeOne() environ = self._makeEnviron() app = plugin.challenge( environ, '401 Unauthorized', [('app', '1'), ('set-cookie','a'), ('set-cookie','b')], []) sr = DummyStartResponse() result = ''.join(app(environ, sr)) self.failUnless(result.startswith('302 Found')) self.assertEqual(sr.headers[1][0], 'set-cookie') self.assertEqual(sr.headers[1][1], 'a') self.assertEqual(sr.headers[2][0], 'set-cookie') self.assertEqual(sr.headers[2][1], 'b') class Test_make_redirecting_plugin(unittest.TestCase): def _callFUT(self, *args, **kw): from repoze.who.plugins.form import make_redirecting_plugin return make_redirecting_plugin(*args, **kw) def test_factory_no_login_form_url_raises(self): self.assertRaises(ValueError, self._callFUT, None) def test_factory_no_login_handler_path_raises(self): self.assertRaises(ValueError, self._callFUT, '/go_there', None) def test_factory_no_logout_handler_path_raises(self): self.assertRaises(ValueError, self._callFUT, '/go_there', '/logged_in', None) def test_factory_no_rememberer_name_raises(self): self.assertRaises(ValueError, self._callFUT, '/go_there', '/logged_in', '/logged_out', None) def test_factory_ok(self): plugin = self._callFUT('/go_there', '/logged_in', '/logged_out', 'rememberer') self.assertEqual(plugin.login_form_url, '/go_there') self.assertEqual(plugin.login_handler_path, '/logged_in') self.assertEqual(plugin.logout_handler_path, '/logged_out') self.assertEqual(plugin.rememberer_name, 'rememberer') class DummyIdentifier: forgotten = False remembered = False def __init__(self, credentials=None, remember_headers=None, forget_headers=None, replace_app=None): self.credentials = credentials self.remember_headers = remember_headers self.forget_headers = forget_headers self.replace_app = replace_app def identify(self, environ): if self.replace_app: environ['repoze.who.application'] = self.replace_app return self.credentials def forget(self, environ, identity): self.forgotten = identity return self.forget_headers def remember(self, environ, identity): self.remembered = identity return self.remember_headers class DummyStartResponse: def __call__(self, status, headers, exc_info=None): self.status = status self.headers = headers self.exc_info = exc_info return [] def encode_multipart_formdata(fields): BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' CRLF = '\r\n' L = [] for (key, value) in fields: L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"' % key) L.append('') L.append(value) L.append('--' + BOUNDARY + '--') L.append('') body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body repoze.who-1.0.18/repoze/who/plugins/tests/test_sql.py0000644000175000017500000002165511201110504022733 0ustar tseavertseaverimport unittest class TestSQLAuthenticatorPlugin(unittest.TestCase): def _getTargetClass(self): from repoze.who.plugins.sql import SQLAuthenticatorPlugin return SQLAuthenticatorPlugin def _makeOne(self, *arg, **kw): plugin = self._getTargetClass()(*arg, **kw) return plugin def _makeEnviron(self, kw=None): environ = {} environ['wsgi.version'] = (1,0) if kw is not None: environ.update(kw) return environ def test_implements(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IAuthenticator klass = self._getTargetClass() verifyClass(IAuthenticator, klass, tentative=True) def test_authenticate_noresults(self): dummy_factory = DummyConnectionFactory([]) plugin = self._makeOne('select foo from bar', dummy_factory, compare_succeed) environ = self._makeEnviron() identity = {'login':'foo', 'password':'bar'} result = plugin.authenticate(environ, identity) self.assertEqual(result, None) self.assertEqual(dummy_factory.query, 'select foo from bar') self.assertEqual(dummy_factory.closed, True) def test_authenticate_comparefail(self): dummy_factory = DummyConnectionFactory([ ['userid', 'password'] ]) plugin = self._makeOne('select foo from bar', dummy_factory, compare_fail) environ = self._makeEnviron() identity = {'login':'fred', 'password':'bar'} result = plugin.authenticate(environ, identity) self.assertEqual(result, None) self.assertEqual(dummy_factory.query, 'select foo from bar') self.assertEqual(dummy_factory.closed, True) def test_authenticate_comparesuccess(self): dummy_factory = DummyConnectionFactory([ ['userid', 'password'] ]) plugin = self._makeOne('select foo from bar', dummy_factory, compare_succeed) environ = self._makeEnviron() identity = {'login':'fred', 'password':'bar'} result = plugin.authenticate(environ, identity) self.assertEqual(result, 'userid') self.assertEqual(dummy_factory.query, 'select foo from bar') self.assertEqual(dummy_factory.closed, True) def test_authenticate_nologin(self): dummy_factory = DummyConnectionFactory([ ['userid', 'password'] ]) plugin = self._makeOne('select foo from bar', dummy_factory, compare_succeed) environ = self._makeEnviron() identity = {} result = plugin.authenticate(environ, identity) self.assertEqual(result, None) self.assertEqual(dummy_factory.query, None) self.assertEqual(dummy_factory.closed, False) class TestDefaultPasswordCompare(unittest.TestCase): def _getFUT(self): from repoze.who.plugins.sql import default_password_compare return default_password_compare def _get_sha_hex_digest(self, clear='password'): try: from hashlib import sha1 except ImportError: from sha import new as sha1 return sha1(clear).hexdigest() def test_shaprefix_success(self): stored = '{SHA}' + self._get_sha_hex_digest() compare = self._getFUT() result = compare('password', stored) self.assertEqual(result, True) def test_shaprefix_fail(self): stored = '{SHA}' + self._get_sha_hex_digest() compare = self._getFUT() result = compare('notpassword', stored) self.assertEqual(result, False) def test_noprefix_success(self): stored = 'password' compare = self._getFUT() result = compare('password', stored) self.assertEqual(result, True) def test_noprefix_fail(self): stored = 'password' compare = self._getFUT() result = compare('notpassword', stored) self.assertEqual(result, False) class TestSQLMetadataProviderPlugin(unittest.TestCase): def _getTargetClass(self): from repoze.who.plugins.sql import SQLMetadataProviderPlugin return SQLMetadataProviderPlugin def _makeOne(self, *arg, **kw): klass = self._getTargetClass() return klass(*arg, **kw) def test_implements(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IMetadataProvider klass = self._getTargetClass() verifyClass(IMetadataProvider, klass, tentative=True) def test_add_metadata(self): dummy_factory = DummyConnectionFactory([ [1, 2, 3] ]) def dummy_filter(results): return results plugin = self._makeOne('md', 'select foo from bar', dummy_factory, dummy_filter) environ = {} identity = {'repoze.who.userid':1} plugin.add_metadata(environ, identity) self.assertEqual(dummy_factory.closed, True) self.assertEqual(identity['md'], [ [1,2,3] ]) self.assertEqual(dummy_factory.query, 'select foo from bar') self.failIf(identity.has_key('__userid')) class TestMakeSQLAuthenticatorPlugin(unittest.TestCase): def _getFUT(self): from repoze.who.plugins.sql import make_authenticator_plugin return make_authenticator_plugin def test_noquery(self): f = self._getFUT() self.assertRaises(ValueError, f, None, 'conn', 'compare') def test_no_connfactory(self): f = self._getFUT() self.assertRaises(ValueError, f, 'statement', None, 'compare') def test_bad_connfactory(self): f = self._getFUT() self.assertRaises(ValueError, f, 'statement', 'does.not:exist', None) def test_connfactory_specd(self): f = self._getFUT() plugin = f('statement', 'repoze.who.plugins.tests.test_sql:make_dummy_connfactory', None) self.assertEqual(plugin.query, 'statement') self.assertEqual(plugin.conn_factory, DummyConnFactory) from repoze.who.plugins.sql import default_password_compare self.assertEqual(plugin.compare_fn, default_password_compare) def test_comparefunc_specd(self): f = self._getFUT() plugin = f('statement', 'repoze.who.plugins.tests.test_sql:make_dummy_connfactory', 'repoze.who.plugins.tests.test_sql:make_dummy_connfactory') self.assertEqual(plugin.query, 'statement') self.assertEqual(plugin.conn_factory, DummyConnFactory) self.assertEqual(plugin.compare_fn, make_dummy_connfactory) class TestMakeSQLMetadataProviderPlugin(unittest.TestCase): def _getFUT(self): from repoze.who.plugins.sql import make_metadata_plugin return make_metadata_plugin def test_no_name(self): f = self._getFUT() self.assertRaises(ValueError, f) def test_no_query(self): f = self._getFUT() self.assertRaises(ValueError, f, 'name', None, None) def test_no_connfactory(self): f = self._getFUT() self.assertRaises(ValueError, f, 'name', 'statement', None) def test_bad_connfactory(self): f = self._getFUT() self.assertRaises(ValueError, f, 'name', 'statement', 'does.not:exist', None) def test_connfactory_specd(self): f = self._getFUT() plugin = f('name', 'statement', 'repoze.who.plugins.tests.test_sql:make_dummy_connfactory', None) self.assertEqual(plugin.name, 'name') self.assertEqual(plugin.query, 'statement') self.assertEqual(plugin.conn_factory, DummyConnFactory) self.assertEqual(plugin.filter, None) def test_comparefn_specd(self): f = self._getFUT() plugin = f('name', 'statement', 'repoze.who.plugins.tests.test_sql:make_dummy_connfactory', 'repoze.who.plugins.tests.test_sql:make_dummy_connfactory') self.assertEqual(plugin.name, 'name') self.assertEqual(plugin.query, 'statement') self.assertEqual(plugin.conn_factory, DummyConnFactory) self.assertEqual(plugin.filter, make_dummy_connfactory) class DummyConnectionFactory: # acts as all of: a factory, a connection, and a cursor closed = False query = None def __init__(self, results): self.results = results def __call__(self): return self def cursor(self): return self def execute(self, query, *arg): self.query = query self.bindargs = arg def fetchall(self): return self.results def fetchone(self): if self.results: return self.results[0] return [] def close(self): self.closed = True def compare_fail(cleartext, stored): return False def compare_succeed(cleartext, stored): return True class _DummyConnFactory: pass DummyConnFactory = _DummyConnFactory() def make_dummy_connfactory(**kw): return DummyConnFactory repoze.who-1.0.18/repoze/who/plugins/tests/test_authtkt.py0000644000175000017500000004506311274640713023643 0ustar tseavertseaverimport unittest class TestAuthTktCookiePlugin(unittest.TestCase): tempdir = None _now_testing = None def setUp(self): pass def tearDown(self): if self.tempdir is not None: import shutil shutil.rmtree(self.tempdir) if self._now_testing is not None: self._setNowTesting(self._now_testing) def _getTargetClass(self): from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin return AuthTktCookiePlugin def _makeEnviron(self, kw=None): environ = {'wsgi.version': (1,0)} if kw is not None: environ.update(kw) environ['REMOTE_ADDR'] = '1.1.1.1' environ['SERVER_NAME'] = 'localhost' return environ def _makeOne(self, *arg, **kw): plugin = self._getTargetClass()(*arg, **kw) return plugin def _makeTicket(self, userid='userid', remote_addr='0.0.0.0', tokens = [], userdata='userdata', cookie_name='auth_tkt', secure=False, time=None): from paste.auth import auth_tkt ticket = auth_tkt.AuthTicket( 'secret', userid, remote_addr, tokens=tokens, user_data=userdata, time=time, cookie_name=cookie_name, secure=secure) return ticket.cookie_value() def _setNowTesting(self, value): from repoze.who.plugins import auth_tkt auth_tkt._NOW_TESTING, self._now_testing = value, auth_tkt._NOW_TESTING def test_implements(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IIdentifier klass = self._getTargetClass() verifyClass(IIdentifier, klass) def test_identify_nocookie(self): plugin = self._makeOne('secret') environ = self._makeEnviron() result = plugin.identify(environ) self.assertEqual(result, None) def test_identify_good_cookie_include_ip(self): plugin = self._makeOne('secret', include_ip=True) val = self._makeTicket(remote_addr='1.1.1.1') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) self.assertEqual(result['repoze.who.userid'], 'userid') self.assertEqual(result['userdata'], 'userdata') self.failUnless('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) self.assertEqual(environ['REMOTE_USER_DATA'],'userdata') self.assertEqual(environ['AUTH_TYPE'],'cookie') def test_identify_good_cookie_dont_include_ip(self): plugin = self._makeOne('secret', include_ip=False) val = self._makeTicket() environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) self.assertEqual(result['repoze.who.userid'], 'userid') self.assertEqual(result['userdata'], 'userdata') self.failUnless('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) self.assertEqual(environ['REMOTE_USER_DATA'],'userdata') self.assertEqual(environ['AUTH_TYPE'],'cookie') def test_identify_good_cookie_int_useridtype(self): plugin = self._makeOne('secret', include_ip=False) val = self._makeTicket(userid='1', userdata='userid_type:int') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) self.assertEqual(result['repoze.who.userid'], 1) self.assertEqual(result['userdata'], 'userid_type:int') self.failUnless('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) self.assertEqual(environ['REMOTE_USER_DATA'],'userid_type:int') self.assertEqual(environ['AUTH_TYPE'],'cookie') def test_identify_good_cookie_unknown_useridtype(self): plugin = self._makeOne('secret', include_ip=False) val = self._makeTicket(userid='userid', userdata='userid_type:unknown') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) self.assertEqual(result['repoze.who.userid'], 'userid') self.assertEqual(result['userdata'], 'userid_type:unknown') self.failUnless('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) self.assertEqual(environ['REMOTE_USER_DATA'],'userid_type:unknown') self.assertEqual(environ['AUTH_TYPE'],'cookie') def test_identify_bad_cookie(self): plugin = self._makeOne('secret', include_ip=True) environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=bogus'}) result = plugin.identify(environ) self.assertEqual(result, None) def test_identify_bad_cookie_expired(self): import time plugin = self._makeOne('secret', timeout=2, reissue_time=1) val = self._makeTicket(userid='userid', time=time.time()-3) environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.identify(environ) self.assertEqual(result, None) def test_remember_creds_same(self): plugin = self._makeOne('secret') val = self._makeTicket(userid='userid') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.remember(environ, {'repoze.who.userid':'userid', 'userdata':'userdata'}) self.assertEqual(result, None) def test_remember_creds_different(self): plugin = self._makeOne('secret') old_val = self._makeTicket(userid='userid') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='other', userdata='userdata') result = plugin.remember(environ, {'repoze.who.userid':'other', 'userdata':'userdata'}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; Path=/; Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; Path=/; Domain=.localhost' % new_val)) def test_remember_creds_different_include_ip(self): plugin = self._makeOne('secret', include_ip=True) old_val = self._makeTicket(userid='userid', remote_addr='1.1.1.1') environ = self._makeEnviron({'HTTP_COOKIE': 'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='other', userdata='userdata', remote_addr='1.1.1.1') result = plugin.remember(environ, {'repoze.who.userid':'other', 'userdata':'userdata'}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; Path=/; Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; Path=/; Domain=.localhost' % new_val)) def test_remember_creds_different_bad_old_cookie(self): plugin = self._makeOne('secret') old_val = 'BOGUS' environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='other', userdata='userdata') result = plugin.remember(environ, {'repoze.who.userid':'other', 'userdata':'userdata'}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; Path=/; Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; Path=/; Domain=.localhost' % new_val)) def test_remember_creds_different_with_nonstring_tokens(self): plugin = self._makeOne('secret') old_val = self._makeTicket(userid='userid') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='other', userdata='userdata', tokens='foo,bar', ) result = plugin.remember(environ, {'repoze.who.userid': 'other', 'userdata': 'userdata', 'tokens': ['foo', 'bar'], }) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', 'auth_tkt="%s"; Path=/; Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', 'auth_tkt="%s"; Path=/; Domain=.localhost' % new_val)) def test_remember_creds_different_int_userid(self): plugin = self._makeOne('secret') old_val = self._makeTicket(userid='userid') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='1', userdata='userid_type:int') result = plugin.remember(environ, {'repoze.who.userid':1, 'userdata':''}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; Path=/' % new_val)) def test_remember_creds_different_long_userid(self): plugin = self._makeOne('secret') old_val = self._makeTicket(userid='userid') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='1', userdata='userid_type:int') result = plugin.remember(environ, {'repoze.who.userid':long(1), 'userdata':''}) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; Path=/' % new_val)) def test_remember_creds_different_unicode_userid(self): plugin = self._makeOne('secret') old_val = self._makeTicket(userid='userid') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) userid = unicode('\xc2\xa9', 'utf-8') new_val = self._makeTicket(userid=userid.encode('utf-8'), userdata='userid_type:unicode') result = plugin.remember(environ, {'repoze.who.userid':userid, 'userdata':''}) self.assertEqual(type(result[0][1]), str) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; Path=/' % new_val)) def test_remember_creds_reissue(self): import time plugin = self._makeOne('secret', reissue_time=1) old_val = self._makeTicket(userid='userid', userdata='', time=time.time()-2) environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) new_val = self._makeTicket(userid='userid', userdata='') result = plugin.remember(environ, {'repoze.who.userid':'userid', 'userdata':''}) self.assertEqual(type(result[0][1]), str) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', 'auth_tkt="%s"; Path=/' % new_val)) def test_forget(self): from datetime import datetime now = datetime(2009, 11, 5, 16, 15, 22) self._setNowTesting(now) plugin = self._makeOne('secret') environ = self._makeEnviron() headers = plugin.forget(environ, None) self.assertEqual(len(headers), 3) header = headers[0] name, value = header self.assertEqual(name, 'Set-Cookie') self.assertEqual(value, 'auth_tkt="INVALID"; Path=/; ' 'Max-Age=0; Expires=Thu, 05 Nov 2009 16:15:22' ) header = headers[1] name, value = header self.assertEqual(name, 'Set-Cookie') self.assertEqual(value, 'auth_tkt="INVALID"; Path=/; Domain=localhost; ' 'Max-Age=0; Expires=Thu, 05 Nov 2009 16:15:22' ) header = headers[2] name, value = header self.assertEqual(name, 'Set-Cookie') self.assertEqual(value, 'auth_tkt="INVALID"; Path=/; Domain=.localhost; ' 'Max-Age=0; Expires=Thu, 05 Nov 2009 16:15:22' ) def test_factory_wo_secret_wo_secretfile_raises_ValueError(self): from repoze.who.plugins.auth_tkt import make_plugin self.assertRaises(ValueError, make_plugin) def test_factory_w_secret_w_secretfile_raises_ValueError(self): from repoze.who.plugins.auth_tkt import make_plugin self.assertRaises(ValueError, make_plugin, 'secret', 'secretfile') def test_factory_w_bad_secretfile_raises_ValueError(self): from repoze.who.plugins.auth_tkt import make_plugin self.assertRaises(ValueError, make_plugin, secretfile='nonesuch.txt') def test_factory_w_secret(self): from repoze.who.plugins.auth_tkt import make_plugin plugin = make_plugin('secret') self.assertEqual(plugin.cookie_name, 'auth_tkt') self.assertEqual(plugin.secret, 'secret') self.assertEqual(plugin.include_ip, False) self.assertEqual(plugin.secure, False) def test_factory_w_secretfile(self): import os from tempfile import mkdtemp from repoze.who.plugins.auth_tkt import make_plugin tempdir = self.tempdir = mkdtemp() path = os.path.join(tempdir, 'who.secret') secret = open(path, 'w') secret.write('s33kr1t\n') secret.flush() secret.close() plugin = make_plugin(secretfile=path) self.assertEqual(plugin.secret, 's33kr1t') def test_factory_with_timeout_and_reissue_time(self): from repoze.who.plugins.auth_tkt import make_plugin plugin = make_plugin('secret', timeout=5, reissue_time=1) self.assertEqual(plugin.timeout, 5) self.assertEqual(plugin.reissue_time, 1) def test_factory_with_userid_checker(self): from repoze.who.plugins.auth_tkt import make_plugin plugin = make_plugin( 'secret', userid_checker='repoze.who.plugins.auth_tkt:make_plugin') self.assertEqual(plugin.userid_checker, make_plugin) def test_timeout_no_reissue(self): self.assertRaises(ValueError, self._makeOne, 'userid', timeout=1) def test_timeout_lower_than_reissue(self): self.assertRaises(ValueError, self._makeOne, 'userid', timeout=1, reissue_time=2) def test_identify_with_checker_and_existing_account(self): plugin = self._makeOne('secret', userid_checker=dummy_userid_checker) val = self._makeTicket(userid='existing') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) self.assertEqual(result['repoze.who.userid'], 'existing') self.assertEqual(result['userdata'], 'userdata') self.failUnless('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) self.assertEqual(environ['REMOTE_USER_DATA'],'userdata') self.assertEqual(environ['AUTH_TYPE'],'cookie') def test_identify_with_checker_and_non_existing_account(self): plugin = self._makeOne('secret', userid_checker=dummy_userid_checker) val = self._makeTicket(userid='nonexisting') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % val}) original_environ = environ.copy() result = plugin.identify(environ) self.assertEqual(result, None) # The environ must not have been modified, excuding the paste.cookies # variable: del environ['paste.cookies'] self.assertEqual(environ, original_environ) def test_remember_max_age(self): plugin = self._makeOne('secret') environ = {'HTTP_HOST':'example.com'} tkt = self._makeTicket(userid='chris', userdata='') result = plugin.remember(environ, {'repoze.who.userid':'chris', 'max_age':'500'}) name,value = result.pop(0) self.assertEqual('Set-Cookie', name) self.failUnless( value.startswith('auth_tkt="%s"; Path=/; Max-Age=500' % tkt), value) self.failUnless('; Expires=' in value) name,value = result.pop(0) self.assertEqual('Set-Cookie', name) self.failUnless( value.startswith( 'auth_tkt="%s"; Path=/; Domain=example.com; Max-Age=500' % tkt), value) self.failUnless('; Expires=' in value) name,value = result.pop(0) self.assertEqual('Set-Cookie', name) self.failUnless( value.startswith( 'auth_tkt="%s"; Path=/; Domain=.example.com; Max-Age=500' % tkt), value) self.failUnless('; Expires=' in value) def dummy_userid_checker(userid): return userid == 'existing' repoze.who-1.0.18/repoze/who/plugins/tests/fixtures/0000755000175000017500000000000011274642513022407 5ustar tseavertseaverrepoze.who-1.0.18/repoze/who/plugins/tests/fixtures/__init__.py0000644000175000017500000000002411201077013024477 0ustar tseavertseaver# this is a package repoze.who-1.0.18/repoze/who/plugins/tests/fixtures/form.html0000644000175000017500000000001711201077013024221 0ustar tseavertseaver repoze.who-1.0.18/repoze/who/plugins/tests/fixtures/test.htpasswd0000644000175000017500000000002411201077013025124 0ustar tseavertseaverbadline chrism:pass repoze.who-1.0.18/repoze/who/plugins/tests/fixtures/testapp.py0000644000175000017500000000323711201077013024431 0ustar tseavertseaverdef tack_environ(environ, msg): import pprint penv = pprint.pformat(environ) return msg + '\n\n' + penv def deny(start_response, environ, msg): ct = 'text/plain' msg = tack_environ(environ, msg) cl = str(len(msg)) start_response('401 Unauthorized', [ ('Content-Type', ct), ('Content-Length', cl) ], ) def allow(start_response, environ, msg): ct = 'text/plain' msg = tack_environ(environ, msg) cl = str(len(msg)) start_response('200 OK', [ ('Content-Type', ct), ('Content-Length', cl) ], ) return [msg] def app(environ, start_response): path_info = environ['PATH_INFO'] remote_user = environ.get('REMOTE_USER') if path_info.endswith('/shared'): if not remote_user: return deny(start_response, environ, 'You cant do that') else: return allow(start_response, environ, 'Welcome to the shared area, %s' % remote_user) elif path_info.endswith('/admin'): if remote_user != 'admin': return deny(start_response, environ, 'Only admin can do that') else: return allow(start_response, environ, 'Hello, admin!') elif path_info.endswith('/chris'): if remote_user != 'chris': return deny(start_response, environ, 'Only chris can do that') else: return allow(start_response, environ, 'Hello, chris!') else: return allow(start_response, environ, 'Unprotected page') def make_app(global_config, **kw): return app repoze.who-1.0.18/repoze/who/plugins/tests/test_basicauth.py0000644000175000017500000000747311201077207024115 0ustar tseavertseaverimport unittest class TestBasicAuthPlugin(unittest.TestCase): def _getTargetClass(self): from repoze.who.plugins.basicauth import BasicAuthPlugin return BasicAuthPlugin def _makeOne(self, *arg, **kw): plugin = self._getTargetClass()(*arg, **kw) return plugin def _makeEnviron(self, kw=None): environ = {} environ['wsgi.version'] = (1,0) if kw is not None: environ.update(kw) return environ def test_implements(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IChallenger from repoze.who.interfaces import IIdentifier klass = self._getTargetClass() verifyClass(IChallenger, klass) verifyClass(IIdentifier, klass) def test_challenge(self): plugin = self._makeOne('realm') environ = self._makeEnviron() result = plugin.challenge(environ, '401 Unauthorized', [], []) self.assertNotEqual(result, None) app_iter = result(environ, lambda *arg: None) items = [] for item in app_iter: items.append(item) response = ''.join(items) self.failUnless(response.startswith('401 Unauthorized')) def test_identify_noauthinfo(self): plugin = self._makeOne('realm') environ = self._makeEnviron() creds = plugin.identify(environ) self.assertEqual(creds, None) def test_identify_nonbasic(self): plugin = self._makeOne('realm') environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Digest abc'}) creds = plugin.identify(environ) self.assertEqual(creds, None) def test_identify_basic_badencoding(self): plugin = self._makeOne('realm') environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic abc'}) creds = plugin.identify(environ) self.assertEqual(creds, None) def test_identify_basic_badrepr(self): plugin = self._makeOne('realm') value = 'foo'.encode('base64') environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic %s' % value}) creds = plugin.identify(environ) self.assertEqual(creds, None) def test_identify_basic_ok(self): plugin = self._makeOne('realm') value = 'foo:bar'.encode('base64') environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic %s' % value}) creds = plugin.identify(environ) self.assertEqual(creds, {'login':'foo', 'password':'bar'}) def test_remember(self): plugin = self._makeOne('realm') creds = {} environ = self._makeEnviron() result = plugin.remember(environ, creds) self.assertEqual(result, None) def test_forget(self): plugin = self._makeOne('realm') creds = {'login':'foo', 'password':'password'} environ = self._makeEnviron() result = plugin.forget(environ, creds) self.assertEqual(result, [('WWW-Authenticate', 'Basic realm="realm"')] ) def test_challenge_forgetheaders_includes(self): plugin = self._makeOne('realm') creds = {'login':'foo', 'password':'password'} environ = self._makeEnviron() forget = plugin._get_wwwauth() result = plugin.challenge(environ, '401 Unauthorized', [], forget) self.assertEqual(result.headers, forget) def test_challenge_forgetheaders_omits(self): plugin = self._makeOne('realm') creds = {'login':'foo', 'password':'password'} environ = self._makeEnviron() forget = plugin._get_wwwauth() result = plugin.challenge(environ, '401 Unauthorized', [], []) self.assertEqual(result.headers, forget) def test_factory(self): from repoze.who.plugins.basicauth import make_plugin plugin = make_plugin('realm') self.assertEqual(plugin.realm, 'realm') repoze.who-1.0.18/repoze/who/plugins/cookie.py0000644000175000017500000000375311201104065021210 0ustar tseavertseaverimport binascii from paste.request import get_cookies from zope.interface import implements from repoze.who.interfaces import IIdentifier class InsecureCookiePlugin(object): implements(IIdentifier) def __init__(self, cookie_name, cookie_path='/'): self.cookie_name = cookie_name self.cookie_path = cookie_path # IIdentifier def identify(self, environ): cookies = get_cookies(environ) cookie = cookies.get(self.cookie_name) if cookie is None: return None try: auth = cookie.value.decode('base64') except binascii.Error: # can't decode return None try: login, password = auth.split(':', 1) return {'login':login, 'password':password} except ValueError: # not enough values to unpack return None # IIdentifier def forget(self, environ, identity): # return a expires Set-Cookie header expired = ('%s=""; Path=%s; Expires=Sun, 10-May-1971 11:59:00 GMT' % (self.cookie_name, self.cookie_path)) return [('Set-Cookie', expired)] # IIdentifier def remember(self, environ, identity): cookie_value = '%(login)s:%(password)s' % identity cookie_value = cookie_value.encode('base64').rstrip() cookies = get_cookies(environ) existing = cookies.get(self.cookie_name) value = getattr(existing, 'value', None) if value != cookie_value: # return a Set-Cookie header set_cookie = '%s=%s; Path=%s;' % (self.cookie_name, cookie_value, self.cookie_path) return [('Set-Cookie', set_cookie)] def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) #pragma NO COVERAGE def make_plugin(cookie_name='repoze.who.plugins.cookie', cookie_path='/'): plugin = InsecureCookiePlugin(cookie_name, cookie_path) return plugin repoze.who-1.0.18/repoze/who/plugins/basicauth.py0000644000175000017500000000374611201103572021706 0ustar tseavertseaverimport binascii from paste.httpheaders import WWW_AUTHENTICATE from paste.httpheaders import AUTHORIZATION from paste.httpexceptions import HTTPUnauthorized from zope.interface import implements from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IChallenger class BasicAuthPlugin(object): implements(IIdentifier, IChallenger) def __init__(self, realm): self.realm = realm # IIdentifier def identify(self, environ): authorization = AUTHORIZATION(environ) try: authmeth, auth = authorization.split(' ', 1) except ValueError: # not enough values to unpack return None if authmeth.lower() == 'basic': try: auth = auth.strip().decode('base64') except binascii.Error: # can't decode return None try: login, password = auth.split(':', 1) except ValueError: # not enough values to unpack return None auth = {'login':login, 'password':password} return auth return None # IIdentifier def remember(self, environ, identity): # we need to do nothing here; the browser remembers the basic # auth info as a result of the user typing it in. pass def _get_wwwauth(self): head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm) return head # IIdentifier def forget(self, environ, identity): return self._get_wwwauth() # IChallenger def challenge(self, environ, status, app_headers, forget_headers): head = self._get_wwwauth() if head[0] not in forget_headers: head = head + forget_headers return HTTPUnauthorized(headers=head) def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) #pragma NO COVERAGE def make_plugin(realm='basic'): plugin = BasicAuthPlugin(realm) return plugin repoze.who-1.0.18/repoze/who/__init__.py0000644000175000017500000000014111201070310017774 0ustar tseavertseaver# repoze.who package __import__('pkg_resources').declare_namespace(__name__) #pragma NO COVERAGE repoze.who-1.0.18/repoze/who/middleware.py0000644000175000017500000004270511201074723020402 0ustar tseavertseaverimport logging from StringIO import StringIO import sys from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IAuthenticator from repoze.who.interfaces import IChallenger from repoze.who.interfaces import IMetadataProvider _STARTED = '-- repoze.who request started (%s) --' _ENDED = '-- repoze.who request ended (%s) --' class PluggableAuthenticationMiddleware(object): def __init__(self, app, identifiers, authenticators, challengers, mdproviders, classifier, challenge_decider, log_stream = None, log_level = logging.INFO, remote_user_key = 'REMOTE_USER', ): iregistry, nregistry = make_registries(identifiers, authenticators, challengers, mdproviders) self.registry = iregistry self.name_registry = nregistry self.app = app self.classifier = classifier self.challenge_decider = challenge_decider self.remote_user_key = remote_user_key self.logger = None if isinstance(log_stream, logging.Logger): self.logger = log_stream elif log_stream: handler = logging.StreamHandler(log_stream) fmt = '%(asctime)s %(message)s' formatter = logging.Formatter(fmt) handler.setFormatter(formatter) self.logger = logging.Logger('repoze.who') self.logger.addHandler(handler) self.logger.setLevel(log_level) def __call__(self, environ, start_response): if self.remote_user_key in environ: # act as a pass through if REMOTE_USER (or whatever) is # already set return self.app(environ, start_response) path_info = environ.get('PATH_INFO', None) environ['repoze.who.plugins'] = self.name_registry environ['repoze.who.logger'] = self.logger environ['repoze.who.application'] = self.app logger = self.logger logger and logger.info(_STARTED % path_info) classification = self.classifier(environ) logger and logger.info('request classification: %s' % classification) userid = None identity = None identifier = None ids = self.identify(environ, classification) # ids will be list of tuples: [ (IIdentifier, identity) ] if ids: auth_ids = self.authenticate(environ, classification, ids) # auth_ids will be a list of five-tuples in the form # ( (auth_rank, id_rank), authenticator, identifier, identity, # userid ) # # When sorted, its first element will represent the "best" # identity for this request. if auth_ids: auth_ids.sort() best = auth_ids[0] rank, authenticator, identifier, identity, userid = best identity = Identity(identity) # dont show contents at print # allow IMetadataProvider plugins to scribble on the identity self.add_metadata(environ, classification, identity) # add the identity to the environment; a downstream # application can mutate it to do an 'identity reset' # as necessary, e.g. identity['login'] = 'foo', # identity['password'] = 'bar' environ['repoze.who.identity'] = identity # set the REMOTE_USER environ[self.remote_user_key] = userid else: logger and logger.info('no identities found, not authenticating') # allow identifier plugins to replace the downstream # application (to do redirection and unauthorized themselves # mostly) app = environ.pop('repoze.who.application') if app is not self.app: logger and logger.info( 'static downstream application replaced with %s' % app) wrapper = StartResponseWrapper(start_response) app_iter = app(environ, wrapper.wrap_start_response) # The challenge decider almost(?) always needs information from the # response. The WSGI spec (PEP 333) states that a WSGI application # must call start_response by the iterable's first iteration. If # start_response hasn't been called, we'll wrap it in a way that # triggers that call. if not wrapper.called: app_iter = wrap_generator(app_iter) if self.challenge_decider(environ, wrapper.status, wrapper.headers): logger and logger.info('challenge required') challenge_app = self.challenge( environ, classification, wrapper.status, wrapper.headers, identifier, identity ) if challenge_app is not None: logger and logger.info('executing challenge app') if app_iter: list(app_iter) # unwind the original app iterator # replace the downstream app with the challenge app app_iter = challenge_app(environ, start_response) else: logger and logger.info('configuration error: no challengers') raise RuntimeError('no challengers found') else: logger and logger.info('no challenge required') remember_headers = [] if identifier: remember_headers = identifier.remember(environ, identity) if remember_headers: logger and logger.info('remembering via headers from %s: %s' % (identifier, remember_headers)) wrapper.finish_response(remember_headers) logger and logger.info(_ENDED % path_info) return app_iter def identify(self, environ, classification): logger = self.logger candidates = self.registry.get(IIdentifier, ()) logger and self.logger.info('identifier plugins registered %s' % (candidates,)) plugins = match_classification(IIdentifier, candidates, classification) logger and self.logger.info( 'identifier plugins matched for ' 'classification "%s": %s' % (classification, plugins)) results = [] for plugin in plugins: identity = plugin.identify(environ) if identity is not None: logger and logger.debug( 'identity returned from %s: %s' % (plugin, identity)) results.append((plugin, identity)) else: logger and logger.debug( 'no identity returned from %s (%s)' % (plugin, identity)) logger and logger.debug('identities found: %s' % (results,)) return results def add_metadata(self, environ, classification, identity): candidates = self.registry.get(IMetadataProvider, ()) plugins = match_classification(IMetadataProvider, candidates, classification) for plugin in plugins: plugin.add_metadata(environ, identity) def authenticate(self, environ, classification, identities): logger = self.logger candidates = self.registry.get(IAuthenticator, []) logger and self.logger.info('authenticator plugins registered %s' % candidates) plugins = match_classification(IAuthenticator, candidates, classification) logger and self.logger.info( 'authenticator plugins matched for ' 'classification "%s": %s' % (classification, plugins)) # 'preauthenticated' identities are considered best-ranking identities, results, id_rank_start =self._filter_preauthenticated( identities) auth_rank = 0 for plugin in plugins: identifier_rank = id_rank_start for identifier, identity in identities: userid = plugin.authenticate(environ, identity) if userid is not None: logger and logger.debug( 'userid returned from %s: "%s"' % (plugin, userid)) # stamp the identity with the userid identity['repoze.who.userid'] = userid rank = (auth_rank, identifier_rank) results.append( (rank, plugin, identifier, identity, userid) ) else: logger and logger.debug( 'no userid returned from %s: (%s)' % ( plugin, userid)) identifier_rank += 1 auth_rank += 1 logger and logger.debug('identities authenticated: %s' % (results,)) return results def _filter_preauthenticated(self, identities): logger = self.logger results = [] new_identities = identities[:] identifier_rank = 0 for thing in identities: identifier, identity = thing userid = identity.get('repoze.who.userid') if userid is not None: # the identifier plugin has already authenticated this # user (domain auth, auth ticket, etc) logger and logger.info( 'userid preauthenticated by %s: "%s" ' '(repoze.who.userid set)' % (identifier, userid) ) rank = (0, identifier_rank) results.append( (rank, None, identifier, identity, userid) ) identifier_rank += 1 new_identities.remove(thing) return new_identities, results, identifier_rank def challenge(self, environ, classification, status, app_headers, identifier, identity): # happens on egress logger = self.logger forget_headers = [] if identifier: forget_headers = identifier.forget(environ, identity) if forget_headers is None: forget_headers = [] else: logger and logger.info('forgetting via headers from %s: %s' % (identifier, forget_headers)) candidates = self.registry.get(IChallenger, ()) logger and logger.info('challengers registered: %s' % candidates) plugins = match_classification(IChallenger, candidates, classification) logger and logger.info('challengers matched for ' 'classification "%s": %s' % (classification, plugins)) for plugin in plugins: app = plugin.challenge(environ, status, app_headers, forget_headers) if app is not None: # new WSGI application logger and logger.info( 'challenger plugin %s "challenge" returned an app' % ( plugin)) return app # signifies no challenge logger and logger.info('no challenge app returned') return None def wrap_generator(result): """\ This function returns a generator that behaves exactly the same as the original. It's only difference is it pulls the first iteration off and caches it to trigger any immediate side effects (in a WSGI world, this ensures start_response is called). """ # Neat trick to pull the first iteration only. We need to do this outside # of the generator function to ensure it is called. for iter in result: first = iter break # Wrapper yields the first iteration, then passes result's iterations # directly up. def wrapper(): yield first for iter in result: # We'll let result's StopIteration bubble up directly. yield iter return wrapper() def match_classification(iface, plugins, classification): result = [] for plugin in plugins: plugin_classifications = getattr(plugin, 'classifications', {}) iface_classifications = plugin_classifications.get(iface) if not iface_classifications: # good for any result.append(plugin) continue if classification in iface_classifications: result.append(plugin) return result class StartResponseWrapper(object): def __init__(self, start_response): self.start_response = start_response self.status = None self.headers = [] self.exc_info = None self.buffer = StringIO() # A WSGI app may delay calling start_response until the first iteration # of its generator. We track this so we know whether or not we need to # trigger an iteration before examining the response. self.called = False def wrap_start_response(self, status, headers, exc_info=None): self.headers = headers self.status = status self.exc_info = exc_info # The response has been initiated, so we have a valid code. self.called = True return self.buffer.write def finish_response(self, extra_headers): if not extra_headers: extra_headers = [] headers = self.headers + extra_headers write = self.start_response(self.status, headers, self.exc_info) if write: self.buffer.seek(0) value = self.buffer.getvalue() if value: write(value) if hasattr(write, 'close'): write.close() def make_test_middleware(app, global_conf): """ Functionally equivalent to [plugin:form] use = repoze.who.plugins.form.FormPlugin rememberer_name = cookie login_form_qs=__do_login [plugin:cookie] use = repoze.who.plugins.cookie:InsecureCookiePlugin cookie_name = oatmeal [plugin:basicauth] use = repoze.who.plugins.basicauth.BasicAuthPlugin realm = repoze.who [plugin:htpasswd] use = repoze.who.plugins.htpasswd.HTPasswdPlugin filename = <...> check_fn = repoze.who.plugins.htpasswd:crypt_check [general] request_classifier = repoze.who.classifiers:default_request_classifier challenge_decider = repoze.who.classifiers:default_challenge_decider [identifiers] plugins = form:browser cookie basicauth [authenticators] plugins = htpasswd [challengers] plugins = form:browser basicauth """ # be able to test without a config file from repoze.who.plugins.basicauth import BasicAuthPlugin from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin from repoze.who.plugins.cookie import InsecureCookiePlugin from repoze.who.plugins.form import FormPlugin from repoze.who.plugins.htpasswd import HTPasswdPlugin io = StringIO() salt = 'aa' for name, password in [ ('admin', 'admin'), ('chris', 'chris') ]: io.write('%s:%s\n' % (name, password)) io.seek(0) def cleartext_check(password, hashed): return password == hashed #pragma NO COVERAGE htpasswd = HTPasswdPlugin(io, cleartext_check) basicauth = BasicAuthPlugin('repoze.who') auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt') cookie = InsecureCookiePlugin('oatmeal') form = FormPlugin('__do_login', rememberer_name='auth_tkt') form.classifications = { IIdentifier:['browser'], IChallenger:['browser'] } # only for browser identifiers = [('form', form),('auth_tkt',auth_tkt),('basicauth',basicauth)] authenticators = [('htpasswd', htpasswd)] challengers = [('form',form), ('basicauth',basicauth)] mdproviders = [] from repoze.who.classifiers import default_request_classifier from repoze.who.classifiers import default_challenge_decider log_stream = None import os if os.environ.get('WHO_LOG'): log_stream = sys.stdout middleware = PluggableAuthenticationMiddleware( app, identifiers, authenticators, challengers, mdproviders, default_request_classifier, default_challenge_decider, log_stream = log_stream, log_level = logging.DEBUG ) return middleware def verify(plugin, iface): from zope.interface.verify import verifyObject verifyObject(iface, plugin, tentative=True) def make_registries(identifiers, authenticators, challengers, mdproviders): from zope.interface.verify import BrokenImplementation interface_registry = {} name_registry = {} for supplied, iface in [ (identifiers, IIdentifier), (authenticators, IAuthenticator), (challengers, IChallenger), (mdproviders, IMetadataProvider)]: for name, value in supplied: try: verify(value, iface) except BrokenImplementation, why: why = str(why) raise ValueError(str(name) + ': ' + why) L = interface_registry.setdefault(iface, []) L.append(value) name_registry[name] = value return interface_registry, name_registry class Identity(dict): """ dict subclass that prevents its members from being rendered during print """ def __repr__(self): return '' % id(self) __str__ = __repr__ repoze.who-1.0.18/repoze/who/config.py0000644000175000017500000001270011274372147017535 0ustar tseavertseaver""" Configuration parser """ from ConfigParser import ConfigParser from StringIO import StringIO import logging from pkg_resources import EntryPoint import sys from repoze.who.interfaces import IAuthenticator from repoze.who.interfaces import IChallengeDecider from repoze.who.interfaces import IChallenger from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IMetadataProvider from repoze.who.interfaces import IPlugin from repoze.who.interfaces import IRequestClassifier from repoze.who.middleware import PluggableAuthenticationMiddleware def _resolve(name): if name: return EntryPoint.parse('x=%s' % name).load(False) class WhoConfig: def __init__(self, here): self.here = here self.request_classifier = None self.challenge_decider = None self.plugins = {} self.identifiers = [] self.authenticators = [] self.challengers = [] self.mdproviders = [] self.remote_user_key = 'REMOTE_USER' def _makePlugin(self, name, iface, options=None): if options is None: options = {} obj = _resolve(name) if not iface.providedBy(obj): obj = obj(**options) return obj def _getPlugin(self, name, iface): obj = self.plugins.get(name) if obj is None: obj = self._makePlugin(name, iface) return obj def _parsePluginSequence(self, attr, proptext, iface): lines = proptext.split() for line in lines: if ';' in line: plugin_name, classifier = line.split(';') else: plugin_name = line classifier = None plugin = self._getPlugin(plugin_name, iface) if classifier is not None: classifications = getattr(plugin, 'classifications', None) if classifications is None: classifications = plugin.classifications = {} classifications[iface] = classifier attr.append((plugin_name, plugin)) def parse(self, text): if getattr(text, 'readline', None) is None: text = StringIO(text) cp = ConfigParser(defaults={'here': self.here}) cp.readfp(text) for s_id in [x for x in cp.sections() if x.startswith('plugin:')]: plugin_id = s_id[len('plugin:'):] options = dict(cp.items(s_id)) if 'use' in options: name = options.pop('use') del options['here'] obj = self._makePlugin(name, IPlugin, options) self.plugins[plugin_id] = obj if 'general' in cp.sections(): general = dict(cp.items('general')) rc = general.get('request_classifier') if rc is not None: rc = self._getPlugin(rc, IRequestClassifier) self.request_classifier = rc cd = general.get('challenge_decider') if cd is not None: cd = self._getPlugin(cd, IChallengeDecider) self.challenge_decider = cd ru = general.get('remote_user_key') if ru is not None: self.remote_user_key = ru if 'identifiers' in cp.sections(): identifiers = dict(cp.items('identifiers')) self._parsePluginSequence(self.identifiers, identifiers['plugins'], IIdentifier, ) if 'authenticators' in cp.sections(): authenticators = dict(cp.items('authenticators')) self._parsePluginSequence(self.authenticators, authenticators['plugins'], IAuthenticator, ) if 'challengers' in cp.sections(): challengers = dict(cp.items('challengers')) self._parsePluginSequence(self.challengers, challengers['plugins'], IChallenger, ) if 'mdproviders' in cp.sections(): mdproviders = dict(cp.items('mdproviders')) self._parsePluginSequence(self.mdproviders, mdproviders['plugins'], IMetadataProvider, ) _LEVELS = {'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR, } def make_middleware_with_config(app, global_conf, config_file, log_file=None, log_level=None): parser = WhoConfig(global_conf['here']) parser.parse(open(config_file)) log_stream = None if log_file is not None: if log_file.lower() == 'stdout': log_stream = sys.stdout else: log_stream = open(log_file, 'wb') if log_level is None: log_level = logging.INFO else: log_level = _LEVELS[log_level.lower()] return PluggableAuthenticationMiddleware( app, parser.identifiers, parser.authenticators, parser.challengers, parser.mdproviders, parser.request_classifier, parser.challenge_decider, log_stream, log_level, parser.remote_user_key, ) repoze.who-1.0.18/repoze/who/tests/0000755000175000017500000000000011274642513017055 5ustar tseavertseaverrepoze.who-1.0.18/repoze/who/tests/__init__.py0000644000175000017500000000001111201067452021147 0ustar tseavertseaver#package repoze.who-1.0.18/repoze/who/tests/test_restrict.py0000644000175000017500000001254111201076443022321 0ustar tseavertseaverimport unittest class AuthenticatedPredicateTests(unittest.TestCase): def _getFUT(self): from repoze.who.restrict import authenticated_predicate return authenticated_predicate() def test___call___no_identity_returns_False(self): predicate = self._getFUT() environ = {} self.failIf(predicate(environ)) def test___call___w_REMOTE_AUTH_returns_True(self): predicate = self._getFUT() environ = {'REMOTE_USER': 'fred'} self.failUnless(predicate(environ)) def test___call___w_repoze_who_identity_returns_True(self): predicate = self._getFUT() environ = {'repoze.who.identity': {'login': 'fred'}} self.failUnless(predicate(environ)) class MakeAuthenticatedRestrictionTests(unittest.TestCase): def _getFUT(self): from repoze.who.restrict import make_authenticated_restriction return make_authenticated_restriction def test_enabled(self): from repoze.who.restrict import authenticated_predicate fut = self._getFUT() app = DummyApp() filter = fut(app, {}, enabled=True) self.failUnless(filter.app is app) self.failUnless(filter.enabled) predicate = filter.predicate self.failUnless(predicate({'REMOTE_USER': 'fred'})) self.failUnless(predicate({'repoze.who.identity': {'login': 'fred'}})) class PredicateRestrictionTests(unittest.TestCase): def _getTargetClass(self): from repoze.who.restrict import PredicateRestriction return PredicateRestriction def _makeOne(self, app=None, **kw): if app is None: app = DummyApp() return self._getTargetClass()(app, **kw) def test___call___disabled_predicate_false_calls_app_not_predicate(self): _tested = [] def _factory(): def _predicate(env): _tested.append(env) return False return _predicate _started = [] def _start_response(status, headers): _started.append((status, headers)) environ = {'testing': True} restrict = self._makeOne(predicate=_factory, enabled=False) restrict(environ, _start_response) self.assertEqual(len(_tested), 0) self.assertEqual(len(_started), 0) self.assertEqual(restrict.app.environ, environ) def test___call___enabled_predicate_false_returns_401(self): _tested = [] def _factory(): def _predicate(env): _tested.append(env) return False return _predicate _started = [] def _start_response(status, headers): _started.append((status, headers)) environ = {'testing': True} restrict = self._makeOne(predicate=_factory) restrict(environ, _start_response) self.assertEqual(len(_tested), 1) self.assertEqual(len(_started), 1, _started) self.assertEqual(_started[0][0], '401 Unauthorized') self.assertEqual(restrict.app.environ, None) def test___call___enabled_predicate_true_calls_app(self): _tested = [] def _factory(): def _predicate(env): _tested.append(env) return True return _predicate _started = [] def _start_response(status, headers): _started.append((status, headers)) environ = {'testing': True, 'REMOTE_USER': 'fred'} restrict = self._makeOne(predicate=_factory) restrict(environ, _start_response) self.assertEqual(len(_tested), 1) self.assertEqual(len(_started), 0) self.assertEqual(restrict.app.environ, environ) class MakePredicateRestrictionTests(unittest.TestCase): def _getFUT(self): from repoze.who.restrict import make_predicate_restriction return make_predicate_restriction def test_non_string_predicate_no_args(self): fut = self._getFUT() app = DummyApp() def _predicate(env): return True def _factory(): return _predicate filter = fut(app, {}, predicate=_factory) self.failUnless(filter.app is app) self.failUnless(filter.predicate is _predicate) self.failUnless(filter.enabled) def test_disabled_non_string_predicate_w_args(self): fut = self._getFUT() app = DummyApp() filter = fut(app, {}, predicate=DummyPredicate, enabled=False, foo='Foo') self.failUnless(filter.app is app) self.failUnless(isinstance(filter.predicate, DummyPredicate)) self.assertEqual(filter.predicate.foo, 'Foo') self.failIf(filter.enabled) def test_enabled_string_predicate_w_args(self): fut = self._getFUT() app = DummyApp() filter = fut(app, {}, predicate='repoze.who.tests.test_restrict:DummyPredicate', enabled=True, foo='Foo') self.failUnless(filter.app is app) self.failUnless(isinstance(filter.predicate, DummyPredicate)) self.assertEqual(filter.predicate.foo, 'Foo') self.failUnless(filter.enabled) class DummyApp: environ = None def __call__(self, environ, start_response): self.environ = environ return [] class DummyPredicate: def __init__(self, **kw): self.__dict__.update(kw) def __call__(self, env): return True repoze.who-1.0.18/repoze/who/tests/test_config.py0000644000175000017500000003731411274372146021745 0ustar tseavertseaverimport unittest class TestWhoConfig(unittest.TestCase): def _getTargetClass(self): from repoze.who.config import WhoConfig return WhoConfig def _makeOne(self, here='/', *args, **kw): return self._getTargetClass()(here, *args, **kw) def _getDummyPluginClass(self, iface): from zope.interface import classImplements if not iface.implementedBy(DummyPlugin): classImplements(DummyPlugin, iface) return DummyPlugin def test_defaults_before_parse(self): config = self._makeOne() self.assertEqual(config.request_classifier, None) self.assertEqual(config.challenge_decider, None) self.assertEqual(config.remote_user_key, 'REMOTE_USER') self.assertEqual(len(config.plugins), 0) self.assertEqual(len(config.identifiers), 0) self.assertEqual(len(config.authenticators), 0) self.assertEqual(len(config.challengers), 0) self.assertEqual(len(config.mdproviders), 0) def test_parse_empty_string(self): config = self._makeOne() config.parse('') self.assertEqual(config.request_classifier, None) self.assertEqual(config.challenge_decider, None) self.assertEqual(config.remote_user_key, 'REMOTE_USER') self.assertEqual(len(config.plugins), 0) self.assertEqual(len(config.identifiers), 0) self.assertEqual(len(config.authenticators), 0) self.assertEqual(len(config.challengers), 0) self.assertEqual(len(config.mdproviders), 0) def test_parse_empty_file(self): from StringIO import StringIO config = self._makeOne() config.parse(StringIO()) self.assertEqual(config.request_classifier, None) self.assertEqual(config.challenge_decider, None) self.assertEqual(config.remote_user_key, 'REMOTE_USER') self.assertEqual(len(config.plugins), 0) self.assertEqual(len(config.identifiers), 0) self.assertEqual(len(config.authenticators), 0) self.assertEqual(len(config.challengers), 0) self.assertEqual(len(config.mdproviders), 0) def test_parse_plugins(self): config = self._makeOne() config.parse(PLUGINS_ONLY) self.assertEqual(len(config.plugins), 2) self.failUnless(isinstance(config.plugins['foo'], DummyPlugin)) bar = config.plugins['bar'] self.failUnless(isinstance(bar, DummyPlugin)) self.assertEqual(bar.credentials, 'qux') def test_parse_general_empty(self): config = self._makeOne() config.parse('[general]') self.assertEqual(config.request_classifier, None) self.assertEqual(config.challenge_decider, None) self.assertEqual(config.remote_user_key, 'REMOTE_USER') self.assertEqual(len(config.plugins), 0) def test_parse_general_only(self): from repoze.who.interfaces import IRequestClassifier from repoze.who.interfaces import IChallengeDecider class IDummy(IRequestClassifier, IChallengeDecider): pass PLUGIN_CLASS = self._getDummyPluginClass(IDummy) config = self._makeOne() config.parse(GENERAL_ONLY) self.failUnless(isinstance(config.request_classifier, PLUGIN_CLASS)) self.failUnless(isinstance(config.challenge_decider, PLUGIN_CLASS)) self.assertEqual(config.remote_user_key, 'ANOTHER_REMOTE_USER') self.assertEqual(len(config.plugins), 0) def test_parse_general_with_plugins(self): from repoze.who.interfaces import IRequestClassifier from repoze.who.interfaces import IChallengeDecider class IDummy(IRequestClassifier, IChallengeDecider): pass PLUGIN_CLASS = self._getDummyPluginClass(IDummy) config = self._makeOne() config.parse(GENERAL_WITH_PLUGINS) self.failUnless(isinstance(config.request_classifier, PLUGIN_CLASS)) self.failUnless(isinstance(config.challenge_decider, PLUGIN_CLASS)) def test_parse_identifiers_only(self): from repoze.who.interfaces import IIdentifier PLUGIN_CLASS = self._getDummyPluginClass(IIdentifier) config = self._makeOne() config.parse(IDENTIFIERS_ONLY) identifiers = config.identifiers self.assertEqual(len(identifiers), 2) first, second = identifiers self.assertEqual(first[0], 'repoze.who.tests.test_config:DummyPlugin') self.failUnless(isinstance(first[1], PLUGIN_CLASS)) self.assertEqual(len(first[1].classifications), 1) self.assertEqual(first[1].classifications[IIdentifier], 'klass1') self.assertEqual(second[0], 'repoze.who.tests.test_config:DummyPlugin') self.failUnless(isinstance(second[1], PLUGIN_CLASS)) def test_parse_identifiers_with_plugins(self): from repoze.who.interfaces import IIdentifier PLUGIN_CLASS = self._getDummyPluginClass(IIdentifier) config = self._makeOne() config.parse(IDENTIFIERS_WITH_PLUGINS) identifiers = config.identifiers self.assertEqual(len(identifiers), 2) first, second = identifiers self.assertEqual(first[0], 'foo') self.failUnless(isinstance(first[1], PLUGIN_CLASS)) self.assertEqual(len(first[1].classifications), 1) self.assertEqual(first[1].classifications[IIdentifier], 'klass1') self.assertEqual(second[0], 'bar') self.failUnless(isinstance(second[1], PLUGIN_CLASS)) def test_parse_authenticators_only(self): from repoze.who.interfaces import IAuthenticator PLUGIN_CLASS = self._getDummyPluginClass(IAuthenticator) config = self._makeOne() config.parse(AUTHENTICATORS_ONLY) authenticators = config.authenticators self.assertEqual(len(authenticators), 2) first, second = authenticators self.assertEqual(first[0], 'repoze.who.tests.test_config:DummyPlugin') self.failUnless(isinstance(first[1], PLUGIN_CLASS)) self.assertEqual(len(first[1].classifications), 1) self.assertEqual(first[1].classifications[IAuthenticator], 'klass1') self.assertEqual(second[0], 'repoze.who.tests.test_config:DummyPlugin') self.failUnless(isinstance(second[1], PLUGIN_CLASS)) def test_parse_authenticators_with_plugins(self): from repoze.who.interfaces import IAuthenticator PLUGIN_CLASS = self._getDummyPluginClass(IAuthenticator) config = self._makeOne() config.parse(AUTHENTICATORS_WITH_PLUGINS) authenticators = config.authenticators self.assertEqual(len(authenticators), 2) first, second = authenticators self.assertEqual(first[0], 'foo') self.failUnless(isinstance(first[1], PLUGIN_CLASS)) self.assertEqual(len(first[1].classifications), 1) self.assertEqual(first[1].classifications[IAuthenticator], 'klass1') self.assertEqual(second[0], 'bar') self.failUnless(isinstance(second[1], PLUGIN_CLASS)) def test_parse_challengers_only(self): from repoze.who.interfaces import IChallenger PLUGIN_CLASS = self._getDummyPluginClass(IChallenger) config = self._makeOne() config.parse(CHALLENGERS_ONLY) challengers = config.challengers self.assertEqual(len(challengers), 2) first, second = challengers self.assertEqual(first[0], 'repoze.who.tests.test_config:DummyPlugin') self.failUnless(isinstance(first[1], PLUGIN_CLASS)) self.assertEqual(len(first[1].classifications), 1) self.assertEqual(first[1].classifications[IChallenger], 'klass1') self.assertEqual(second[0], 'repoze.who.tests.test_config:DummyPlugin') self.failUnless(isinstance(second[1], PLUGIN_CLASS)) def test_parse_challengers_with_plugins(self): from repoze.who.interfaces import IChallenger PLUGIN_CLASS = self._getDummyPluginClass(IChallenger) config = self._makeOne() config.parse(CHALLENGERS_WITH_PLUGINS) challengers = config.challengers self.assertEqual(len(challengers), 2) first, second = challengers self.assertEqual(first[0], 'foo') self.failUnless(isinstance(first[1], PLUGIN_CLASS)) self.assertEqual(len(first[1].classifications), 1) self.assertEqual(first[1].classifications[IChallenger], 'klass1') self.assertEqual(second[0], 'bar') self.failUnless(isinstance(second[1], PLUGIN_CLASS)) def test_parse_mdproviders_only(self): from repoze.who.interfaces import IMetadataProvider PLUGIN_CLASS = self._getDummyPluginClass(IMetadataProvider) config = self._makeOne() config.parse(MDPROVIDERS_ONLY) mdproviders = config.mdproviders self.assertEqual(len(mdproviders), 2) first, second = mdproviders self.assertEqual(first[0], 'repoze.who.tests.test_config:DummyPlugin') self.failUnless(isinstance(first[1], PLUGIN_CLASS)) self.assertEqual(len(first[1].classifications), 1) self.assertEqual(first[1].classifications[IMetadataProvider], 'klass1') self.assertEqual(second[0], 'repoze.who.tests.test_config:DummyPlugin') self.failUnless(isinstance(second[1], PLUGIN_CLASS)) def test_parse_mdproviders_with_plugins(self): from repoze.who.interfaces import IMetadataProvider PLUGIN_CLASS = self._getDummyPluginClass(IMetadataProvider) config = self._makeOne() config.parse(MDPROVIDERS_WITH_PLUGINS) mdproviders = config.mdproviders self.assertEqual(len(mdproviders), 2) first, second = mdproviders self.assertEqual(first[0], 'foo') self.failUnless(isinstance(first[1], PLUGIN_CLASS)) self.assertEqual(len(first[1].classifications), 1) self.assertEqual(first[1].classifications[IMetadataProvider], 'klass1') self.assertEqual(second[0], 'bar') self.failUnless(isinstance(second[1], PLUGIN_CLASS)) def test_parse_make_plugin_names(self): # see http://bugs.repoze.org/issue92 config = self._makeOne() config.parse(MAKE_PLUGIN_ARG_NAMES) self.assertEqual(len(config.plugins), 1) foo = config.plugins['foo'] self.failUnless(isinstance(foo, DummyPlugin)) self.assertEqual(foo.iface, 'iface') self.assertEqual(foo.name, 'name') class DummyPlugin: def __init__(self, **kw): self.__dict__.update(kw) PLUGINS_ONLY = """\ [plugin:foo] use = repoze.who.tests.test_config:DummyPlugin [plugin:bar] use = repoze.who.tests.test_config:DummyPlugin credentials = qux """ GENERAL_ONLY = """\ [general] request_classifier = repoze.who.tests.test_config:DummyPlugin challenge_decider = repoze.who.tests.test_config:DummyPlugin remote_user_key = ANOTHER_REMOTE_USER """ GENERAL_WITH_PLUGINS = """\ [general] request_classifier = classifier challenge_decider = decider [plugin:classifier] use = repoze.who.tests.test_config:DummyPlugin [plugin:decider] use = repoze.who.tests.test_config:DummyPlugin """ IDENTIFIERS_ONLY = """\ [identifiers] plugins = repoze.who.tests.test_config:DummyPlugin;klass1 repoze.who.tests.test_config:DummyPlugin """ IDENTIFIERS_WITH_PLUGINS = """\ [identifiers] plugins = foo;klass1 bar [plugin:foo] use = repoze.who.tests.test_config:DummyPlugin [plugin:bar] use = repoze.who.tests.test_config:DummyPlugin """ AUTHENTICATORS_ONLY = """\ [authenticators] plugins = repoze.who.tests.test_config:DummyPlugin;klass1 repoze.who.tests.test_config:DummyPlugin """ AUTHENTICATORS_WITH_PLUGINS = """\ [authenticators] plugins = foo;klass1 bar [plugin:foo] use = repoze.who.tests.test_config:DummyPlugin [plugin:bar] use = repoze.who.tests.test_config:DummyPlugin """ CHALLENGERS_ONLY = """\ [challengers] plugins = repoze.who.tests.test_config:DummyPlugin;klass1 repoze.who.tests.test_config:DummyPlugin """ CHALLENGERS_WITH_PLUGINS = """\ [challengers] plugins = foo;klass1 bar [plugin:foo] use = repoze.who.tests.test_config:DummyPlugin [plugin:bar] use = repoze.who.tests.test_config:DummyPlugin """ MDPROVIDERS_ONLY = """\ [mdproviders] plugins = repoze.who.tests.test_config:DummyPlugin;klass1 repoze.who.tests.test_config:DummyPlugin """ MDPROVIDERS_WITH_PLUGINS = """\ [mdproviders] plugins = foo;klass1 bar [plugin:foo] use = repoze.who.tests.test_config:DummyPlugin [plugin:bar] use = repoze.who.tests.test_config:DummyPlugin """ MAKE_PLUGIN_ARG_NAMES = """\ [plugin:foo] use = repoze.who.tests.test_config:DummyPlugin name = name iface = iface """ class TestConfigMiddleware(unittest.TestCase): tempdir = None def setUp(self): pass def tearDown(self): if self.tempdir is not None: import shutil shutil.rmtree(self.tempdir) def _getFactory(self): from repoze.who.config import make_middleware_with_config return make_middleware_with_config def _getTempfile(self, text): import os import tempfile tempdir = self.tempdir = tempfile.mkdtemp() path = os.path.join(tempdir, 'who.ini') config = open(path, 'w') config.write(text) config.flush() config.close() return path def test_sample_config(self): import logging from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IAuthenticator from repoze.who.interfaces import IChallenger app = DummyApp() factory = self._getFactory() path = self._getTempfile(SAMPLE_CONFIG) global_conf = {'here': '/'} middleware = factory(app, global_conf, config_file=path, log_file='STDOUT', log_level='debug') self.assertEqual(len(middleware.registry[IIdentifier]), 3) self.assertEqual(len(middleware.registry[IAuthenticator]), 1) self.assertEqual(len(middleware.registry[IChallenger]), 2) self.failUnless(middleware.logger, middleware.logger) self.assertEqual(middleware.logger.getEffectiveLevel(), logging.DEBUG) def test_sample_config_no_log_level(self): import logging app = DummyApp() factory = self._getFactory() path = self._getTempfile(SAMPLE_CONFIG) global_conf = {'here': '/'} middleware = factory(app, global_conf, config_file=path, log_file='STDOUT') self.assertEqual(middleware.logger.getEffectiveLevel(), logging.INFO) def test_sample_config_w_log_file(self): import logging import os app = DummyApp() factory = self._getFactory() path = self._getTempfile(SAMPLE_CONFIG) logfile = os.path.join(self.tempdir, 'who.log') global_conf = {'here': '/'} middleware = factory(app, global_conf, config_file=path, log_file=logfile) self.assertEqual(middleware.logger.getEffectiveLevel(), logging.INFO) logging.shutdown() SAMPLE_CONFIG = """\ [plugin:form] use = repoze.who.plugins.form:make_plugin login_form_qs = __do_login rememberer_name = auth_tkt [plugin:auth_tkt] use = repoze.who.plugins.auth_tkt:make_plugin secret = s33kr1t cookie_name = oatmeal secure = False include_ip = True [plugin:basicauth] use = repoze.who.plugins.basicauth:make_plugin realm = 'sample' [plugin:htpasswd] use = repoze.who.plugins.htpasswd:make_plugin filename = %(here)s/etc/passwd check_fn = repoze.who.plugins.htpasswd:crypt_check [general] request_classifier = repoze.who.classifiers:default_request_classifier challenge_decider = repoze.who.classifiers:default_challenge_decider [identifiers] plugins = form;browser auth_tkt basicauth [authenticators] plugins = htpasswd [challengers] plugins = form;browser basicauth [mdproviders] plugins = """ class DummyApp: environ = None def __call__(self, environ, start_response): self.environ = environ return [] repoze.who-1.0.18/repoze/who/tests/test_classifiers.py0000644000175000017500000000571411201127521022767 0ustar tseavertseaverimport unittest class TestDefaultRequestClassifier(unittest.TestCase): def _getFUT(self): from repoze.who.classifiers import default_request_classifier return default_request_classifier def _makeEnviron(self, kw=None): environ = {} environ['wsgi.version'] = (1,0) if kw is not None: environ.update(kw) return environ def test_classify_dav_method(self): classifier = self._getFUT() environ = self._makeEnviron({'REQUEST_METHOD':'COPY'}) result = classifier(environ) self.assertEqual(result, 'dav') def test_classify_dav_useragent(self): classifier = self._getFUT() environ = self._makeEnviron({'HTTP_USER_AGENT':'WebDrive'}) result = classifier(environ) self.assertEqual(result, 'dav') def test_classify_xmlpost(self): classifier = self._getFUT() environ = self._makeEnviron({'CONTENT_TYPE':'text/xml', 'REQUEST_METHOD':'POST'}) result = classifier(environ) self.assertEqual(result, 'xmlpost') def test_classify_browser(self): classifier = self._getFUT() environ = self._makeEnviron({'CONTENT_TYPE':'text/xml', 'REQUEST_METHOD':'GET'}) result = classifier(environ) self.assertEqual(result, 'browser') class TestDefaultChallengeDecider(unittest.TestCase): def _getFUT(self): from repoze.who.classifiers import default_challenge_decider return default_challenge_decider def _makeEnviron(self, kw=None): environ = {} environ['wsgi.version'] = (1,0) if kw is not None: environ.update(kw) return environ def test_challenges_on_401(self): decider = self._getFUT() self.failUnless(decider({}, '401 Unauthorized', [])) def test_doesnt_challenges_on_non_401(self): decider = self._getFUT() self.failIf(decider({}, '200 Ok', [])) class TestPassthroughChallengeDecider(unittest.TestCase): def _getFUT(self): from repoze.who.classifiers import passthrough_challenge_decider return passthrough_challenge_decider def _makeEnviron(self, kw=None): environ = {} environ['wsgi.version'] = (1,0) if kw is not None: environ.update(kw) return environ def test_challenges_on_bare_401(self): decider = self._getFUT() self.failUnless(decider({}, '401 Unauthorized', [])) def test_doesnt_challenges_on_non_401(self): decider = self._getFUT() self.failIf(decider({}, '200 Ok', [])) def test_doesnt_challenges_on_401_with_WWW_Authenticate(self): decider = self._getFUT() self.failIf(decider({}, '401 Ok', [('WWW-Authenticate', 'xxx')])) def test_doesnt_challenges_on_401_with_text_html(self): decider = self._getFUT() self.failIf(decider({}, '401 Ok', [('Content-Type', 'text/html')])) repoze.who-1.0.18/repoze/who/tests/test_middleware.py0000644000175000017500000012503511201074623022600 0ustar tseavertseaverimport unittest class TestMiddleware(unittest.TestCase): def _getTargetClass(self): from repoze.who.middleware import PluggableAuthenticationMiddleware return PluggableAuthenticationMiddleware def _makeOne(self, app=None, identifiers=None, authenticators=None, challengers=None, classifier=None, mdproviders=None, challenge_decider=None, log_stream=None, log_level=None, ): if app is None: app = DummyApp() if identifiers is None: identifiers = [] if authenticators is None: authenticators = [] if challengers is None: challengers = [] if classifier is None: classifier = DummyRequestClassifier() if mdproviders is None: mdproviders = [] if challenge_decider is None: challenge_decider = DummyChallengeDecider() if log_level is None: import logging log_level = logging.DEBUG mw = self._getTargetClass()(app, identifiers, authenticators, challengers, mdproviders, classifier, challenge_decider, log_stream, log_level=logging.DEBUG) return mw def _makeEnviron(self, kw=None): environ = {} environ['wsgi.version'] = (1,0) if kw is not None: environ.update(kw) return environ def test_accepts_logger(self): import logging logger = logging.Logger('something') logger.setLevel(logging.INFO) mw = self._makeOne(log_stream=logger) self.assertEqual(logger, mw.logger) def test_identify_success(self): environ = self._makeEnviron() credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identifiers = [ ('i', identifier) ] mw = self._makeOne(identifiers=identifiers) results = mw.identify(environ, None) self.assertEqual(len(results), 1) new_identifier, identity = results[0] self.assertEqual(new_identifier, identifier) self.assertEqual(identity['login'], 'chris') self.assertEqual(identity['password'], 'password') def test_identify_success_empty_identity(self): environ = self._makeEnviron() identifier = DummyIdentifier({}) identifiers = [ ('i', identifier) ] mw = self._makeOne(identifiers=identifiers) results = mw.identify(environ, None) self.assertEqual(len(results), 1) new_identifier, identity = results[0] self.assertEqual(new_identifier, identifier) self.assertEqual(identity, {}) def test_identify_fail(self): environ = self._makeEnviron() plugin = DummyNoResultsIdentifier() plugins = [ ('dummy', plugin) ] mw = self._makeOne(identifiers=plugins) results = mw.identify(environ, None) self.assertEqual(len(results), 0) def test_identify_success_skip_noresults(self): environ = self._makeEnviron() mw = self._makeOne() plugin1 = DummyNoResultsIdentifier() credentials = {'login':'chris', 'password':'password'} plugin2 = DummyIdentifier(credentials) plugins = [ ('identifier1', plugin1), ('identifier2', plugin2) ] mw = self._makeOne(identifiers=plugins) results = mw.identify(environ, None) self.assertEqual(len(results), 1) new_identifier, identity = results[0] self.assertEqual(new_identifier, plugin2) self.assertEqual(identity['login'], 'chris') self.assertEqual(identity['password'], 'password') def test_identify_success_multiresults(self): environ = self._makeEnviron() mw = self._makeOne() plugin1 = DummyIdentifier({'login':'fred','password':'fred'}) plugin2 = DummyIdentifier({'login':'bob','password':'bob'}) plugins = [ ('identifier1', plugin1), ('identifier2', plugin2) ] mw = self._makeOne(identifiers=plugins) results = mw.identify(environ, None) self.assertEqual(len(results), 2) new_identifier, identity = results[0] self.assertEqual(new_identifier, plugin1) self.assertEqual(identity['login'], 'fred') self.assertEqual(identity['password'], 'fred') new_identifier, identity = results[1] self.assertEqual(new_identifier, plugin2) self.assertEqual(identity['login'], 'bob') self.assertEqual(identity['password'], 'bob') def test_identify_find_implicit_classifier(self): environ = self._makeEnviron() mw = self._makeOne() plugin1 = DummyIdentifier({'login':'fred','password':'fred'}) from repoze.who.interfaces import IIdentifier plugin1.classifications = {IIdentifier:['nomatch']} plugin2 = DummyIdentifier({'login':'bob','password':'bob'}) plugins = [ ('identifier1', plugin1), ('identifier2', plugin2) ] mw = self._makeOne(identifiers=plugins) results = mw.identify(environ, 'match') self.assertEqual(len(results), 1) plugin, creds = results[0] self.assertEqual(creds['login'], 'bob') self.assertEqual(creds['password'], 'bob') self.assertEqual(plugin, plugin2) def test_identify_find_explicit_classifier(self): environ = self._makeEnviron() from repoze.who.interfaces import IIdentifier plugin1 = DummyIdentifier({'login':'fred','password':'fred'}) plugin1.classifications = {IIdentifier:['nomatch']} plugin2 = DummyIdentifier({'login':'bob','password':'bob'}) plugin2.classifications = {IIdentifier:['match']} plugins= [ ('identifier1', plugin1), ('identifier2', plugin2) ] mw = self._makeOne(identifiers=plugins) results = mw.identify(environ, 'match') self.assertEqual(len(results), 1) plugin, creds = results[0] self.assertEqual(creds['login'], 'bob') self.assertEqual(creds['password'], 'bob') self.assertEqual(plugin, plugin2) def test_authenticate_success(self): environ = self._makeEnviron() plugin1 = DummyAuthenticator('a') plugins = [ ('identifier1', plugin1) ] mw = self._makeOne(authenticators=plugins) identities = [ (None, {'login':'chris', 'password':'password'}) ] results = mw.authenticate(environ, None, identities) self.assertEqual(len(results), 1) result = results[0] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (0,0)) self.assertEqual(authenticator, plugin1) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'a') def test_authenticate_fail(self): environ = self._makeEnviron() mw = self._makeOne() # no authenticators identities = [ (None, {'login':'chris', 'password':'password'}) ] result = mw.authenticate(environ, None, identities) self.assertEqual(len(result), 0) def test_authenticate_success_skip_fail(self): environ = self._makeEnviron() mw = self._makeOne() plugin1 = DummyFailAuthenticator() plugin2 = DummyAuthenticator() plugins = [ ('dummy1', plugin1), ('dummy2', plugin2) ] mw = self._makeOne(authenticators=plugins) creds = {'login':'chris', 'password':'password'} identities = [ (None, {'login':'chris', 'password':'password'}) ] results = mw.authenticate(environ, None, identities) self.assertEqual(len(results), 1) result = results[0] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (1,0)) self.assertEqual(authenticator, plugin2) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'chris') def test_authenticate_success_multiresult(self): environ = self._makeEnviron() mw = self._makeOne() plugin1 = DummyAuthenticator('chris_id1') plugin2 = DummyAuthenticator('chris_id2') plugins = [ ('dummy1',plugin1), ('dummy2',plugin2) ] mw = self._makeOne(authenticators=plugins) creds = {'login':'chris', 'password':'password'} identities = [ (None, {'login':'chris', 'password':'password'}) ] results = mw.authenticate(environ, None, identities) self.assertEqual(len(results), 2) result = results[0] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (0,0,)) self.assertEqual(authenticator, plugin1) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'chris_id1') result = results[1] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (1,0)) self.assertEqual(authenticator, plugin2) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'chris_id2') def test_authenticate_find_implicit_classifier(self): environ = self._makeEnviron() mw = self._makeOne() plugin1 = DummyAuthenticator('chris_id1') from repoze.who.interfaces import IAuthenticator plugin1.classifications = {IAuthenticator:['nomatch']} plugin2 = DummyAuthenticator('chris_id2') plugins = [ ('auth1', plugin1), ('auth2', plugin2) ] mw = self._makeOne(authenticators = plugins) identities = [ (None, {'login':'chris', 'password':'password'}) ] results = mw.authenticate(environ, 'match', identities) self.assertEqual(len(results), 1) result = results[0] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (0,0)) self.assertEqual(authenticator, plugin2) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'chris_id2') def test_authenticate_find_explicit_classifier(self): environ = self._makeEnviron() mw = self._makeOne() from repoze.who.interfaces import IAuthenticator plugin1 = DummyAuthenticator('chris_id1') plugin1.classifications = {IAuthenticator:['nomatch']} plugin2 = DummyAuthenticator('chris_id2') plugin2.classifications = {IAuthenticator:['match']} plugins = [ ('auth1', plugin1), ('auth2', plugin2) ] mw = self._makeOne(authenticators = plugins) identities = [ (None, {'login':'chris', 'password':'password'}) ] results = mw.authenticate(environ, 'match', identities) self.assertEqual(len(results), 1) result = results[0] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (0, 0)) self.assertEqual(authenticator, plugin2) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'chris_id2') def test_authenticate_user_null_but_not_none(self): environ = self._makeEnviron() plugin1 = DummyAuthenticator(0) plugins = [ ('identifier1', plugin1) ] mw = self._makeOne(authenticators=plugins) identities = [ (None, {'login':'chris', 'password':'password'}) ] results = mw.authenticate(environ, None, identities) self.assertEqual(len(results), 1) result = results[0] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (0,0)) self.assertEqual(authenticator, plugin1) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 0) def test_authenticate_success_multiresult_one_preauthenticated(self): environ = self._makeEnviron() mw = self._makeOne() preauth = DummyIdentifier({'repoze.who.userid':'preauthenticated'}) plugin1 = DummyAuthenticator('chris_id1') plugin2 = DummyAuthenticator('chris_id2') plugins = [ ('dummy1',plugin1), ('dummy2',plugin2) ] mw = self._makeOne(authenticators=plugins) creds = {'login':'chris', 'password':'password'} identities = [ (None, {'login':'chris', 'password':'password'}), (preauth, preauth.credentials) ] results = mw.authenticate(environ, None, identities) self.assertEqual(len(results), 3) result = results[0] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (0,0,)) self.assertEqual(authenticator, None) self.assertEqual(identifier, preauth) self.assertEqual(creds['repoze.who.userid'], 'preauthenticated') self.assertEqual(userid, 'preauthenticated') result = results[1] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (0,1)) self.assertEqual(authenticator, plugin1) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'chris_id1') result = results[2] rank, authenticator, identifier, creds, userid = result self.assertEqual(rank, (1,1)) self.assertEqual(authenticator, plugin2) self.assertEqual(identifier, None) self.assertEqual(creds['login'], 'chris') self.assertEqual(creds['password'], 'password') self.assertEqual(userid, 'chris_id2') def test_challenge_noidentifier_noapp(self): environ = self._makeEnviron() challenger = DummyChallenger() plugins = [ ('challenge', challenger) ] mw = self._makeOne(challengers = plugins) identity = {'login':'chris', 'password':'password'} app = mw.challenge(environ, 'match', '401 Unauthorized', [], None, identity) self.assertEqual(app, None) self.assertEqual(environ['challenged'], app) def test_challenge_noidentifier_withapp(self): environ = self._makeEnviron() app = DummyApp() challenger = DummyChallenger(app) plugins = [ ('challenge', challenger) ] mw = self._makeOne(challengers = plugins) identity = {'login':'chris', 'password':'password'} result = mw.challenge(environ, 'match', '401 Unauthorized', [], None, identity) self.assertEqual(result, app) self.assertEqual(environ['challenged'], app) def test_challenge_identifier_noapp(self): environ = self._makeEnviron() challenger = DummyChallenger() credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) plugins = [ ('challenge', challenger) ] mw = self._makeOne(challengers = plugins) identity = {'login':'chris', 'password':'password'} result = mw.challenge(environ, 'match', '401 Unauthorized', [], identifier, identity) self.assertEqual(result, None) self.assertEqual(environ['challenged'], None) self.assertEqual(identifier.forgotten, identity) def test_challenge_identifier_app(self): environ = self._makeEnviron() app = DummyApp() challenger = DummyChallenger(app) credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) plugins = [ ('challenge', challenger) ] mw = self._makeOne(challengers = plugins) identity = {'login':'chris', 'password':'password'} result = mw.challenge(environ, 'match', '401 Unauthorized', [], identifier, identity) self.assertEqual(result, app) self.assertEqual(environ['challenged'], app) self.assertEqual(identifier.forgotten, identity) def test_challenge_identifier_forget_headers(self): FORGET_HEADERS = [('X-testing-forget', 'Oubliez!')] environ = self._makeEnviron() app = DummyApp() challenger = DummyChallenger(app) credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials, forget_headers=FORGET_HEADERS) plugins = [ ('challenge', challenger) ] mw = self._makeOne(challengers = plugins) identity = {'login':'chris', 'password':'password'} result = mw.challenge(environ, 'match', '401 Unauthorized', [], identifier, identity) def test_multi_challenge_firstwins(self): environ = self._makeEnviron() app1 = DummyApp() app2 = DummyApp() challenger1 = DummyChallenger(app1) challenger2 = DummyChallenger(app2) credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) plugins = [ ('challenge1', challenger1), ('challenge2', challenger2) ] mw = self._makeOne(challengers = plugins) identity = {'login':'chris', 'password':'password'} result = mw.challenge(environ, 'match', '401 Unauthorized', [], identifier, identity) self.assertEqual(result, app1) self.assertEqual(environ['challenged'], app1) self.assertEqual(identifier.forgotten, identity) def test_multi_challenge_skipnomatch_findimplicit(self): environ = self._makeEnviron() app1 = DummyApp() app2 = DummyApp() from repoze.who.interfaces import IChallenger challenger1 = DummyChallenger(app1) challenger1.classifications = {IChallenger:['nomatch']} challenger2 = DummyChallenger(app2) challenger2.classifications = {IChallenger:None} credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) plugins = [ ('challenge1', challenger1), ('challenge2', challenger2) ] mw = self._makeOne(challengers = plugins) identity = {'login':'chris', 'password':'password'} result = mw.challenge(environ, 'match', '401 Unauthorized', [], identifier, identity) self.assertEqual(result, app2) self.assertEqual(environ['challenged'], app2) self.assertEqual(identifier.forgotten, identity) def test_multi_challenge_skipnomatch_findexplicit(self): environ = self._makeEnviron() app1 = DummyApp() app2 = DummyApp() from repoze.who.interfaces import IChallenger challenger1 = DummyChallenger(app1) challenger1.classifications = {IChallenger:['nomatch']} challenger2 = DummyChallenger(app2) challenger2.classifications = {IChallenger:['match']} credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) plugins = [ ('challenge1', challenger1), ('challenge2', challenger2) ] mw = self._makeOne(challengers = plugins) identity = {'login':'chris', 'password':'password'} result = mw.challenge(environ, 'match', '401 Unauthorized', [], identifier, identity) self.assertEqual(result, app2) self.assertEqual(environ['challenged'], app2) self.assertEqual(identifier.forgotten, identity) def test_add_metadata(self): environ = self._makeEnviron() plugin1 = DummyMDProvider({'foo':'bar'}) plugin2 = DummyMDProvider({'fuz':'baz'}) plugins = [ ('meta1', plugin1), ('meta2', plugin2) ] mw = self._makeOne(mdproviders=plugins) classification = '' identity = {} results = mw.add_metadata(environ, classification, identity) self.assertEqual(identity['foo'], 'bar') self.assertEqual(identity['fuz'], 'baz') def test_add_metadata_w_classification(self): environ = self._makeEnviron() plugin1 = DummyMDProvider({'foo':'bar'}) plugin2 = DummyMDProvider({'fuz':'baz'}) from repoze.who.interfaces import IMetadataProvider plugin2.classifications = {IMetadataProvider:['foo']} plugins = [ ('meta1', plugin1), ('meta2', plugin2) ] mw = self._makeOne(mdproviders=plugins) classification = 'monkey' identity = {} mw.add_metadata(environ, classification, identity) self.assertEqual(identity['foo'], 'bar') self.assertEqual(identity.get('fuz'), None) def test_call_remoteuser_already_set(self): environ = self._makeEnviron({'REMOTE_USER':'admin'}) mw = self._makeOne() result = mw(environ, None) self.assertEqual(mw.app.environ, environ) self.assertEqual(result, []) def test_call_200_no_plugins(self): environ = self._makeEnviron() headers = [('a', '1')] app = DummyWorkingApp('200 OK', headers) mw = self._makeOne(app=app) start_response = DummyStartResponse() result = mw(environ, start_response) self.assertEqual(mw.app.environ, environ) self.assertEqual(result, ['body']) self.assertEqual(start_response.status, '200 OK') self.assertEqual(start_response.headers, headers) def test_call_401_no_challengers(self): environ = self._makeEnviron() headers = [('a', '1')] app = DummyWorkingApp('401 Unauthorized', headers) mw = self._makeOne(app=app) start_response = DummyStartResponse() self.assertRaises(RuntimeError, mw, environ, start_response) def test_call_200_no_challengers(self): environ = self._makeEnviron() headers = [('a', '1')] app = DummyWorkingApp('200 OK', headers) credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identifiers = [ ('identifier', identifier) ] mw = self._makeOne(app=app, identifiers=identifiers) start_response = DummyStartResponse() result = mw(environ, start_response) self.assertEqual(mw.app.environ, environ) self.assertEqual(result, ['body']) self.assertEqual(start_response.status, '200 OK') self.assertEqual(start_response.headers, headers) def test_call_401_no_identifiers(self): environ = self._makeEnviron() headers = [('a', '1')] app = DummyWorkingApp('401 Unauthorized', headers) from paste.httpexceptions import HTTPUnauthorized challenge_app = HTTPUnauthorized() challenge = DummyChallenger(challenge_app) challengers = [ ('challenge', challenge) ] mw = self._makeOne(app=app, challengers=challengers) start_response = DummyStartResponse() result = mw(environ, start_response) self.assertEqual(environ['challenged'], challenge_app) self.failUnless(result[0].startswith('401 Unauthorized\r\n')) def test_call_401_challenger_and_identifier_no_authenticator(self): environ = self._makeEnviron() headers = [('a', '1')] app = DummyWorkingApp('401 Unauthorized', headers) from paste.httpexceptions import HTTPUnauthorized challenge_app = HTTPUnauthorized() challenge = DummyChallenger(challenge_app) challengers = [ ('challenge', challenge) ] credentials = {'login':'a', 'password':'b'} identifier = DummyIdentifier(credentials) identifiers = [ ('identifier', identifier) ] mw = self._makeOne(app=app, challengers=challengers, identifiers=identifiers) start_response = DummyStartResponse() result = mw(environ, start_response) self.assertEqual(environ['challenged'], challenge_app) self.failUnless(result[0].startswith('401 Unauthorized\r\n')) self.assertEqual(identifier.forgotten, False) self.assertEqual(environ.get('REMOTE_USER'), None) def test_call_401_challenger_and_identifier_and_authenticator(self): environ = self._makeEnviron() headers = [('a', '1')] app = DummyWorkingApp('401 Unauthorized', headers) from paste.httpexceptions import HTTPUnauthorized challenge_app = HTTPUnauthorized() challenge = DummyChallenger(challenge_app) challengers = [ ('challenge', challenge) ] credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identifiers = [ ('identifier', identifier) ] authenticator = DummyAuthenticator() authenticators = [ ('authenticator', authenticator) ] mw = self._makeOne(app=app, challengers=challengers, identifiers=identifiers, authenticators=authenticators) start_response = DummyStartResponse() result = mw(environ, start_response) self.assertEqual(environ['challenged'], challenge_app) self.failUnless(result[0].startswith('401 Unauthorized\r\n')) # @@ unfuck ## self.assertEqual(identifier.forgotten, identifier.credentials) self.assertEqual(environ['REMOTE_USER'], 'chris') ## self.assertEqual(environ['repoze.who.identity'], identifier.credentials) def test_call_200_challenger_and_identifier_and_authenticator(self): environ = self._makeEnviron() headers = [('a', '1')] app = DummyWorkingApp('200 OK', headers) from paste.httpexceptions import HTTPUnauthorized challenge_app = HTTPUnauthorized() challenge = DummyChallenger(challenge_app) challengers = [ ('challenge', challenge) ] credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identifiers = [ ('identifier', identifier) ] authenticator = DummyAuthenticator() authenticators = [ ('authenticator', authenticator) ] mw = self._makeOne(app=app, challengers=challengers, identifiers=identifiers, authenticators=authenticators) start_response = DummyStartResponse() result = mw(environ, start_response) self.assertEqual(environ.get('challenged'), None) self.assertEqual(identifier.forgotten, False) # @@ figure out later ## self.assertEqual(dict(identifier.remembered)['login'], dict(identifier.credentials)['login']) ## self.assertEqual(dict(identifier.remembered)['password'], dict(identifier.credentials)['password']) self.assertEqual(environ['REMOTE_USER'], 'chris') ## self.assertEqual(environ['repoze.who.identity'], identifier.credentials) def test_call_200_identity_reset(self): environ = self._makeEnviron() headers = [('a', '1')] new_identity = {'user_id':'foo', 'password':'bar'} app = DummyIdentityResetApp('200 OK', headers, new_identity) from paste.httpexceptions import HTTPUnauthorized challenge_app = HTTPUnauthorized() challenge = DummyChallenger(challenge_app) challengers = [ ('challenge', challenge) ] credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identifiers = [ ('identifier', identifier) ] authenticator = DummyAuthenticator() authenticators = [ ('authenticator', authenticator) ] mw = self._makeOne(app=app, challengers=challengers, identifiers=identifiers, authenticators=authenticators) start_response = DummyStartResponse() result = mw(environ, start_response) self.assertEqual(environ.get('challenged'), None) self.assertEqual(identifier.forgotten, False) new_credentials = identifier.credentials.copy() new_credentials['login'] = 'fred' new_credentials['password'] = 'schooled' # @@ unfuck ## self.assertEqual(identifier.remembered, new_credentials) self.assertEqual(environ['REMOTE_USER'], 'chris') ## self.assertEqual(environ['repoze.who.identity'], new_credentials) def test_call_200_with_metadata(self): environ = self._makeEnviron() headers = [('a', '1')] app = DummyWorkingApp('200 OK', headers) from paste.httpexceptions import HTTPUnauthorized challenge_app = HTTPUnauthorized() challenge = DummyChallenger(challenge_app) challengers = [ ('challenge', challenge) ] credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identifiers = [ ('identifier', identifier) ] authenticator = DummyAuthenticator() authenticators = [ ('authenticator', authenticator) ] mdprovider = DummyMDProvider({'foo':'bar'}) mdproviders = [ ('mdprovider', mdprovider) ] mw = self._makeOne(app=app, challengers=challengers, identifiers=identifiers, authenticators=authenticators, mdproviders=mdproviders) start_response = DummyStartResponse() result = mw(environ, start_response) # metadata self.assertEqual(environ['repoze.who.identity']['foo'], 'bar') def test_call_ingress_plugin_replaces_application(self): environ = self._makeEnviron() headers = [('a', '1')] app = DummyWorkingApp('200 OK', headers) challengers = [] credentials = {'login':'chris', 'password':'password'} from paste.httpexceptions import HTTPFound identifier = DummyIdentifier( credentials, remember_headers=[('a', '1')], replace_app = HTTPFound('http://example.com/redirect') ) identifiers = [ ('identifier', identifier) ] authenticator = DummyAuthenticator() authenticators = [ ('authenticator', authenticator) ] mdproviders = [] mw = self._makeOne(app=app, challengers=challengers, identifiers=identifiers, authenticators=authenticators, mdproviders=mdproviders) start_response = DummyStartResponse() result = ''.join(mw(environ, start_response)) self.failUnless(result.startswith('302 Found')) self.assertEqual(start_response.status, '302 Found') headers = start_response.headers self.assertEqual(len(headers), 3) self.assertEqual(headers[0], ('location', 'http://example.com/redirect')) self.assertEqual(headers[1], ('content-type', 'text/plain; charset=utf8')) self.assertEqual(headers[2], ('a', '1')) self.assertEqual(start_response.exc_info, None) self.failIf(environ.has_key('repoze.who.application')) def test_call_app_doesnt_call_start_response(self): environ = self._makeEnviron() headers = [('a', '1')] app = DummyGeneratorApp('200 OK', headers) from paste.httpexceptions import HTTPUnauthorized challenge_app = HTTPUnauthorized() challenge = DummyChallenger(challenge_app) challengers = [ ('challenge', challenge) ] credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) identifiers = [ ('identifier', identifier) ] authenticator = DummyAuthenticator() authenticators = [ ('authenticator', authenticator) ] mdprovider = DummyMDProvider({'foo':'bar'}) mdproviders = [ ('mdprovider', mdprovider) ] mw = self._makeOne(app=app, challengers=challengers, identifiers=identifiers, authenticators=authenticators, mdproviders=mdproviders) start_response = DummyStartResponse() result = mw(environ, start_response) # metadata self.assertEqual(environ['repoze.who.identity']['foo'], 'bar') # XXX need more call tests: # - auth_id sorting class TestMatchClassification(unittest.TestCase): def _getFUT(self): from repoze.who.middleware import match_classification return match_classification def test_match_classification(self): f = self._getFUT() from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IChallenger from repoze.who.interfaces import IAuthenticator multi1 = DummyMultiPlugin() multi2 = DummyMultiPlugin() multi1.classifications = {IIdentifier:('foo', 'bar'), IChallenger:('buz',), IAuthenticator:None} multi2.classifications = {IIdentifier:('foo', 'baz', 'biz')} plugins = (multi1, multi2) # specific self.assertEqual(f(IIdentifier, plugins, 'foo'), [multi1, multi2]) self.assertEqual(f(IIdentifier, plugins, 'bar'), [multi1]) self.assertEqual(f(IIdentifier, plugins, 'biz'), [multi2]) # any for multi2 self.assertEqual(f(IChallenger, plugins, 'buz'), [multi1, multi2]) # any for either self.assertEqual(f(IAuthenticator, plugins, 'buz'), [multi1, multi2]) class TestStartResponseWrapper(unittest.TestCase): def _getTargetClass(self): from repoze.who.middleware import StartResponseWrapper return StartResponseWrapper def _makeOne(self, *arg, **kw): plugin = self._getTargetClass()(*arg, **kw) return plugin def test_ctor(self): wrapper = self._makeOne(None) self.assertEqual(wrapper.start_response, None) self.assertEqual(wrapper.headers, []) self.failUnless(wrapper.buffer) def test_finish_response(self): statuses = [] headerses = [] datases = [] closededs = [] from StringIO import StringIO def write(data): datases.append(data) def close(): closededs.append(True) write.close = close def start_response(status, headers, exc_info=None): statuses.append(status) headerses.append(headers) return write wrapper = self._makeOne(start_response) wrapper.status = '401 Unauthorized' wrapper.headers = [('a', '1')] wrapper.buffer = StringIO('written') extra_headers = [('b', '2')] result = wrapper.finish_response(extra_headers) self.assertEqual(result, None) self.assertEqual(headerses[0], wrapper.headers + extra_headers) self.assertEqual(statuses[0], wrapper.status) self.assertEqual(datases[0], 'written') self.assertEqual(closededs[0], True) class TestMakeRegistries(unittest.TestCase): def _getFUT(self): from repoze.who.middleware import make_registries return make_registries def test_empty(self): fn = self._getFUT() iface_reg, name_reg = fn([], [], [], []) self.assertEqual(iface_reg, {}) self.assertEqual(name_reg, {}) def test_brokenimpl(self): fn = self._getFUT() self.assertRaises(ValueError, fn, [(None, DummyApp())], [], [], []) def test_ok(self): fn = self._getFUT() credentials1 = {'login':'chris', 'password':'password'} dummy_id1 = DummyIdentifier(credentials1) credentials2 = {'login':'chris', 'password':'password'} dummy_id2 = DummyIdentifier(credentials2) identifiers = [ ('id1', dummy_id1), ('id2', dummy_id2) ] dummy_auth = DummyAuthenticator(None) authenticators = [ ('auth', dummy_auth) ] dummy_challenger = DummyChallenger(None) challengers = [ ('challenger', dummy_challenger) ] dummy_mdprovider = DummyMDProvider() mdproviders = [ ('mdproviders', dummy_mdprovider) ] iface_reg, name_reg = fn(identifiers, authenticators, challengers, mdproviders) from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IAuthenticator from repoze.who.interfaces import IChallenger self.assertEqual(iface_reg[IIdentifier], [dummy_id1, dummy_id2]) self.assertEqual(iface_reg[IAuthenticator], [dummy_auth]) self.assertEqual(iface_reg[IChallenger], [dummy_challenger]) self.assertEqual(name_reg['id1'], dummy_id1) self.assertEqual(name_reg['id2'], dummy_id2) self.assertEqual(name_reg['auth'], dummy_auth) self.assertEqual(name_reg['challenger'], dummy_challenger) class TestIdentityDict(unittest.TestCase): def _getTargetClass(self): from repoze.who.middleware import Identity return Identity def _makeOne(self, **kw): klass = self._getTargetClass() return klass(**kw) def test_str(self): identity = self._makeOne(foo=1) self.failUnless(str(identity).startswith('' where is one of" @echo " html to make standalone HTML files" @echo " pickle to make pickle files (usable by e.g. sphinx-web)" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview over all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" clean: -rm -rf .build/* html: mkdir -p .build/html .build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) .build/html @echo @echo "Build finished. The HTML pages are in .build/html." pickle: mkdir -p .build/pickle .build/doctrees $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) .build/pickle @echo @echo "Build finished; now you can process the pickle files or run" @echo " sphinx-web .build/pickle" @echo "to start the sphinx-web server." web: pickle htmlhelp: mkdir -p .build/htmlhelp .build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) .build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in .build/htmlhelp." latex: mkdir -p .build/latex .build/doctrees $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex @echo @echo "Build finished; the LaTeX files are in .build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: mkdir -p .build/changes .build/doctrees $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) .build/changes @echo @echo "The overview file is in .build/changes." linkcheck: mkdir -p .build/linkcheck .build/doctrees $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) .build/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in .build/linkcheck/output.txt." repoze.who-1.0.18/docs/narr.rst0000644000175000017500000014155211274372147016251 0ustar tseavertseaverMiddleware Responsibilities =========================== :mod:`repoze.who` as middleware has one major function on ingress: it conditionally places identification and authentication information (including a ``REMOTE_USER`` value) into the WSGI environment and allows the request to continue to a downstream WSGI application. :mod:`repoze.who` as middleware has one major function on egress: it examines the headers set by the downstream application, the WSGI environment, or headers supplied by other plugins and conditionally challenges for credentials. Configuration Points ==================== Classifiers ----------- :mod:`repoze.who` "classifies" the request on middleware ingress. Request classification happens before identification and authentication. A request from a browser might be classified a different way than a request from an XML-RPC client. :mod:`repoze.who` uses request classifiers to decide which other components to consult during subsequent identification, authentication, and challenge steps. Plugins are free to advertise themselves as willing to participate in identification and authorization for a request based on this classification. The request classification system is pluggable. :mod:`repoze.who` provides a default classifier that you may use. You may extend the classification system by making :mod:`repoze.who` aware of a different request classifier implementation. Challenge Deciders ------------------ :mod:`repoze.who` uses a "challenge decider" to decide whether the response returned from a downstream application requires a challenge plugin to fire. When using the default challenge decider, only the status is used (if it starts with ``401``, a challenge is required). :mod:`repoze.who` also provides an alternate challenge decider, ``repoze.who.classifiers.passthrough_challenge_decider``, which avoids challenging ``401`` responses which have been "pre-challenged" by the application. You may supply a different challenge decider as necessary. Plugins ------- :mod:`repoze.who` has core functionality designed around the concept of plugins. Plugins are instances that are willing to perform one or more identification- and/or authentication-related duties. Each plugin can be configured arbitrarily. :mod:`repoze.who` consults the set of configured plugins when it intercepts a WSGI request, and gives some subset of them a chance to influence what :mod:`repoze.who` does for the current request. .. note:: As of :mod:`repoze.who` 1.0.7, the ``repoze.who.plugins`` package is a namespace package, intended to make it possible for people to ship eggs which are who plugins as, e.g. ``repoze.who.plugins.mycoolplugin``. Lifecycle of a Request ====================== :mod:`repoze.who` performs duties both on middleware "ingress" and on middleware "egress". Request (Ingress) Stages ------------------------ :mod:`repoze.who` performs the following operations in the following order during middleware ingress: 1. Request Classification The WSGI environment is examined and the request is classified into one "type" of request. The callable named as the ``classifer`` argument to the :mod:`repoze.who` middleware constructor is used to perform the classification. It returns a value that is considered to be the request classification (a single string). 2. Identification Identifiers 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 credentials data from the environment. For example, a basic auth identifier might use the ``HTTP_AUTHORIZATION`` header to find login and password information. Identifiers are also responsible for providing header information to set and remove authentication information in the response during egress. 3. Authentication Authenticators which nominate themselves as willing to authenticate for a particular class of request will be consulted to compare information provided by the identification plugins that returned credentials. For example, an htpasswd authenticator might look in a file for a user record matching any of the identities. If it finds one, and if the password listed in the record matches the password provided by an identity, the userid of the user would be returned (which would be the same as the login name). 4. Metadata Provision The identity of the authenticated user found during the authentication step can be augmented with arbitrary metadata. For example, a metadata provider plugin might augment the identity with first, middle and last names, or a more specialized metadata provider might augment the identity with a list of role or group names. Response (Egress) Stages ------------------------ :mod:`repoze.who` performs the following operations in the following order during middleware egress: #. Challenge Decision The WSGI environment and the status and headers returned by the downstream application may be examined to determine whether a challenge is required. Typically, only the status is used (if it starts with ``401``, a challenge is required, and the challenge decider returns True). This behavior is pluggable. It is replaced by changing the ``challenge_decider`` argument to the middleware. If a challenge is required, the challenge decider will return True; if it's not, it will return False. #. Challenge If the challenge decider returns True, challengers which nominate themselves as willing to execute a challenge for a particular class of request (as provided by the classifier) will be consulted, and one will be chosen to perform a challenge. A challenger plugin can use application-returned headers, the WSGI environment, and other items to determine what sort of operation should be performed to actuate the challenge. Note that :mod:`repoze.who` defers to the identifier plugin which provided the identity (if any) to reset credentials at challenge time; this is not the responsibility of the challenger. This is known as "forgetting" credentials. #. Remember The identifier plugin that the "best" set of credentials came from (if any) will be consulted to "remember" these credentials if the challenge decider returns False. Plugin Types ============ Identifier Plugins ------------------ You can register a plugin as willing to act as an "identifier". An identifier examines the WSGI environment and attempts to extract credentials from the environment. These credentials are used by authenticator plugins to perform authentication. In some cases, an identification plugin can "preauthenticate" an identity (and can thus act as an authenticator plugin). Authenticator Plugins --------------------- You may register a plugin as willing to act as an "authenticator". Authenticator plugins are responsible for resolving a set of credentials provided by an identifier plugin into a user id. Typically, authenticator plugins will perform a lookup into a database or some other persistent store, check the provided credentials against the stored data, and return a user id if the credentials can be validated. The user id provided by an authenticator is eventually passed to downstream WSGI applications in the "REMOTE_USER' environment variable. Additionally, the "identity" of the user (as provided by the identifier from whence the identity came) is passed along to downstream application in the ``repoze.who.identity`` environment variable. Metadata Provider Plugins ------------------------- You may register a plugin as willing to act as a "metadata provider" (aka mdprovider). Metadata provider plugins are responsible for adding arbitrary information to the identity dictionary for consumption by downstream applications. For instance, a metadata provider plugin may add "group" information to the the identity. Challenger Plugins ------------------ You may register a plugin as willing to act as a "challenger". Challenger plugins are responsible for initiating a challenge to the requesting user. Challenger plugins are invoked by :mod:`repoze.who` when it decides a challenge is necessary. A challenge might consist of displaying a form or presenting the user with a basic or digest authentication dialog. Default Plugin Implementations ============================== :mod:`repoze.who` ships with a variety of default plugins that do authentication, identification, challenge and metadata provision. .. module:: repoze.who.plugins.auth_tkt .. class:: AuthTktCookiePlugin(secret [, secretfile=None, [, cookie_name='auth_tkt' [, secure=False [, include_ip=False [, timeout=None [, reissue_time=None [, userid_checker=None]]]]]]]) An :class:`AuthTktCookiePlugin` is an ``IIdentifier`` plugin which remembers its identity state in a client-side cookie. This plugin uses the ``paste.auth.auth_tkt``"auth ticket" protocol. It should be instantiated passing a *secret*, which is used to encrypt the cookie on the client side and decrypt the cookie on the server side. The cookie name used to store the cookie value can be specified using the *cookie_name* parameter. If *secure* is False, the cookie will be sent across any HTTP or HTTPS connection; if it is True, the cookie will be sent only across an HTTPS connection. If *include_ip* is True, the ``REMOTE_ADDR`` of the WSGI environment will be placed in the cookie. If *timeout* is specfied, it is the maximum age in seconds allowed for a cookie. If *reissue_time* is specified, when we encounter a cookie that is older than the reissue time (in seconds), but younger that the timeout, a new cookie will be issued. If *timeout* is specified, you must also set *reissue_time* to a lower value. If ``userid_checker`` is provided, it must be a dotted Python name that resolves to a function which accepts a userid and returns a boolean True or False, indicating whether that user exists in a database. This is a workaround. Due to a design bug in repoze.who, the only way who can check for user existence is to use one or more IAuthenticator plugin ``authenticate`` methods. If an IAuthenticator's ``authenticate`` method returns true, it means that the user exists. However most IAuthenticator plugins expect *both* a username and a password, and will return False unconditionally if both aren't supplied. This means that an authenticator can't be used to check if the user "only" exists. The identity provided by an auth_tkt does not contain a password to check against. The actual design bug in repoze.who is this: when a user presents credentials from an auth_tkt, he is considered "preauthenticated". IAuthenticator.authenticate is just never called for a "preauthenticated" identity, which works fine, but it means that the user will be considered authenticated even if you deleted the user's record from whatever database you happen to be using. However, if you use a userid_checker, you can ensure that a user exists for the auth_tkt supplied userid. If the userid_checker returns False, the auth_tkt credentials are considered "no good". .. note:: Using the *include_ip* setting for public-facing applications may cause problems for some users. `One study `_ reports that as many as 3% of users change their IP addresses legitimately during a session. .. module:: repoze.who.plugins.basicauth .. class:: BasicAuthPlugin(realm) A :class:`BasicAuthPlugin` plugin is both an ``IIdentifier`` and ``IChallenger`` plugin that implements the Basic Access Authentication scheme described in :rfc:`2617`. It looks for credentials within the ``HTTP-Authorization`` header sent by browsers. It challenges by sending an ``WWW-Authenticate`` header to the browser. The single argument *realm* indicates the basic auth realm that should be sent in the ``WWW-Authenticate`` header. .. module:: repoze.who.plugins.cookie .. class:: InsecureCookiePlugin(cookie_name) A :class:`InsecureCookiePlugin` is an ``IIdentifier`` plugin. It stores identification information in an insecure form (the base64 value of the username and password separated by a colon) in a client-side cookie. It accepts a single argument named *cookie_name*. This is the cookie name of the cookie used to store the identification information. .. module:: repoze.who.plugins.form .. class:: FormPlugin(login_form_qs, rememberer_name [, formbody=None [, formcallable=None]]) A :class:`FormPlugin` is both an ``IIdentifier`` and ``IChallenger`` plugin. It intercepts form POSTs to gather identification at ingress and conditionally displays a login form at egress if challenge is required. *login_form_qs* is a query string name used to denote that a form POST is destined for the form plugin (anything unique is fine), *rememberer_name* is the "configuration name" of another ``IIdentifier`` plugin that will be used to perform ``remember`` and ``forget`` duties for the FormPlugin (it does not do these itself). For example, if you have a cookie identification plugin named ``cookie`` defined in your middleware configuration, you might set *rememberer_name* to ``cookie``. *formbody* is a literal string that should be displayed as the form body. *formcallable* is a callable that will return a form body if *formbody* is None. If both *formbody* and *formcallable* are None, a default form is used. .. class:: RedirectingFormPlugin(login_form_url, login_handler_path, logout_handler_path, rememberer_name) A :class:`RedirectingFormPlugin` is both an ``IIdentifier`` and ``IChallenger`` plugin. It intercepts form POSTs to gather identification at ingress and conditionally redirects a login form at egress if challenge is required (as opposed to the :class:`FormPlugin`, it does not handle its own form generation). *login_form_url* is a URL that should be redirected to when a challenge is required. *login_handler_path* is the path that the form will POST to, signifying that the plugin should gather credentials. *logout_handler_path* is a path that can be called to log the current user out when visited. *rememberer_name* is the configuration name of another ``IIdentifier`` plugin that will be used to perform ``remember`` and ``forget`` duties for the RedirectingFormPlugin (it does not do these itself). For example, if you have a cookie identification plugin named ``cookie`` defined in your middleware configuration, you might set *rememberer_name* to ``cookie``. .. module:: repoze.who.plugins.htpasswd .. class:: HTPasswdPlugin(filename, check) A :class:`HTPasswdPlugin` is an ``IAuthenticator`` implementation which compares identity information against an Apache-style htpasswd file. The *filename* argument should be an absolute path to the htpasswd file' the *check* argument is a callable which takes two arguments: "password" and "hashed", where the "password" argument is the unencrypted password provided by the identifier plugin, and the hashed value is the value stored in the htpasswd file. If the hashed value of the password matches the hash, this callable should return True. A default implementation named ``crypt_check`` is available for use as a check function (on UNIX) as ``repoze.who.plugins.htpasswd:crypt_check``; it assumes the values in the htpasswd file are encrypted with the UNIX ``crypt`` function. .. module:: repoze.who.plugins.sql .. class:: SQLAuthenticatorPlugin(query, conn_factory, compare_fn) A :class:`SQLAuthenticatorPlugin` is an ``IAuthenticator`` implementation which compares login-password identity information against data in an arbitrary SQL database. The *query* argument should be a SQL query that returns two columns in a single row considered to be the user id and the password respectively. The SQL query should contain Python-DBAPI style substitution values for ``%(login)``, e.g. ``SELECT user_id, password FROM users WHERE login = %(login)``. The *conn_factory* argument should be a callable that returns a DBAPI database connection. The *compare_fn* argument should be a callable that accepts two arguments: ``cleartext`` and ``stored_password_hash``. It should compare the hashed version of cleartext and return True if it matches the stored password hash, otherwise it should return False. A comparison function named ``default_password_compare`` exists in the ``repoze.who.plugins.sql`` module demonstrating this. The :class:`SQLAuthenticatorPlugin`\'s ``authenticate`` method will return the user id of the user unchanged to :mod:`repoze.who`. .. class:: SQLMetadataProviderPlugin(name, query, conn_factory, filter) A :class:`SQLMetatadaProviderPlugin` is an ``IMetadataProvider`` implementation which adds arbitrary metadata to the identity on ingress using data from an arbitrary SQL database. The *name* argument should be a string. It will be used as a key in the identity dictionary. The *query* argument should be a SQL query that returns arbitrary data from the database in a form that accepts Python-binding style DBAPI arguments. It should expect that a ``__userid`` value will exist in the dictionary that is bound. The SQL query should contain Python-DBAPI style substitution values for (at least) ``%(__userid)``, e.g. ``SELECT group FROM groups WHERE user_id = %(__userid)``. The *conn_factory* argument should be a callable that returns a DBAPI database connection. The *filter* argument should be a callable that accepts the result of the DBAPI ``fetchall`` based on the SQL query. It should massage the data into something that will be set in the environment under the *name* key. Middleware Configuration via Python Code ======================================== .. module:: repoze.who.middleware .. class:: PluggableAuthenticationMiddleware(app, identifiers, challengers, mdproviders, classifier, challenge_decider [, log_stream=None [, log_level=logging.INFO[, remote_user_key='REMOTE_USER']]]) The primary method of configuring the :mod:`repoze.who` middleware is to use straight Python code, meant to be consumed by frameworks which construct and compose middleware pipelines without using a configuration file. In the middleware constructor: *app* is the "next" application in the WSGI pipeline. *identifiers* is a sequence of ``IIdentifier`` plugins, *challengers* is a sequence of ``IChallenger`` plugins, *mdproviders* is a sequence of ``IMetadataProvider`` plugins. Any of these can be specified as the empty sequence. *classifier* is a request classifier callable, *challenge_decider* is a challenge decision callable. *log_stream* is a stream object (an object with a ``write`` method) *or* a ``logging.Logger`` object, *log_level* is a numeric value that maps to the ``logging`` module's notion of log levels, *remote_user_key* is the key in which the ``REMOTE_USER`` (userid) value should be placed in the WSGI environment for consumption by downstream applications. An example configuration which uses the default plugins follows:: from repoze.who.middleware import PluggableAuthenticationMiddleware from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IChallenger from repoze.who.plugins.basicauth import BasicAuthPlugin from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin from repoze.who.plugins.cookie import InsecureCookiePlugin from repoze.who.plugins.form import FormPlugin from repoze.who.plugins.htpasswd import HTPasswdPlugin io = StringIO() salt = 'aa' for name, password in [ ('admin', 'admin'), ('chris', 'chris') ]: io.write('%s:%s\n' % (name, password)) io.seek(0) def cleartext_check(password, hashed): return password == hashed htpasswd = HTPasswdPlugin(io, cleartext_check) basicauth = BasicAuthPlugin('repoze.who') auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt') form = FormPlugin('__do_login', rememberer_name='auth_tkt') form.classifications = { IIdentifier:['browser'], IChallenger:['browser'] } # only for browser identifiers = [('form', form),('auth_tkt',auth_tkt),('basicauth',basicauth)] authenticators = [('htpasswd', htpasswd)] challengers = [('form',form), ('basicauth',basicauth)] mdproviders = [] from repoze.who.classifiers import default_request_classifier from repoze.who.classifiers import default_challenge_decider log_stream = None import os if os.environ.get('WHO_LOG'): log_stream = sys.stdout middleware = PluggableAuthenticationMiddleware( app, identifiers, authenticators, challengers, mdproviders, default_request_classifier, default_challenge_decider, log_stream = log_stream, log_level = logging.DEBUG ) The above example configures the repoze.who middleware with: - Three ``IIdentifier`` plugins (form auth, auth_tkt cookie, and a basic auth plugin). The form auth plugin is set up to fire only when the request is a ``browser`` request (as per the combination of the request classifier returning ``browser`` and the framework checking against the *classifications* attribute of the plugin, which limits ``IIdentifier`` and ``IChallenger`` to the ``browser`` classification only). In this setup, when "identification" needs to be performed, the form auth plugin will be checked first (if the request is a browser request), then the auth_tkt cookie plugin, then the basic auth plugin. - One ``IAuthenticator`` plugin: an htpasswd one. This htpasswd plugin is configured with two valid username/password combinations: chris/chris, and admin/admin. When an username and password is found via any identifier, it will be checked against this authenticator. - Two ``IChallenger`` plugins: the form plugin, then the basic auth plugin. The form auth will fire if the request is a ``browser`` request, otherwise the basic auth plugin will fire. The rest of the middleware configuration is for values like logging and the classifier and decider implementations. These use the "stock" implementations. .. note:: The ``app`` referred to in the example is the "downstream" WSGI application that who is wrapping. Middleware Configuration via Config File ======================================== :mod:`repoze.who` may be configured using a ConfigParser-style .INI file. The configuration file has five main types of sections: plugin sections, a general section, an identifiers section, an authenticators section, and a challengers section. Each "plugin" section defines a configuration for a particular plugin. The identifiers, authenticators, and challengers sections refer to these plugins to form a site configuration. The general section is general middleware configuration. To configure :mod:`repoze.who` in Python, using an .INI file, call the `make_middleware_with_config` entry point, passing the right-hand application and the path to the confi file :: from repoze.who.config import make_middleware_with_config who = make_middleware_with_config(app, '/path/to/who.ini') :mod:`repoze.who`'s configuration file can be pointed to within a PasteDeploy configuration file :: [filter:who] use = egg:repoze.who#config config_file = %(here)s/who.ini log_file = stdout log_level = debug Below is an example of a configuration file (what ``config_file`` might point at above ) that might be used to configure the :mod:`repoze.who` middleware. A set of plugins are defined, and they are referred to by following non-plugin sections. In the below configuration, five plugins are defined. The form, and basicauth plugins are nominated to act as challenger plugins. The form, cookie, and basicauth plugins are nominated to act as identification plugins. The htpasswd and sqlusers plugins are nominated to act as authenticator plugins. :: [plugin:form] # identificaion and challenge use = repoze.who.plugins.form:make_plugin login_form_qs = __do_login rememberer_name = auth_tkt form = %(here)s/login_form.html [plugin:auth_tkt] # identification use = repoze.who.plugins.auth_tkt:make_plugin secret = s33kr1t cookie_name = oatmeal secure = False include_ip = False [plugin:basicauth] # identification and challenge use = repoze.who.plugins.basicauth:make_plugin realm = 'sample' [plugin:htpasswd] # authentication use = repoze.who.plugins.htpasswd:make_plugin filename = %(here)s/passwd check_fn = repoze.who.plugins.htpasswd:crypt_check [plugin:sqlusers] # authentication use = repoze.who.plugins.sql:make_authenticator_plugin query = "SELECT userid, password FROM users where login = %(login)s;" conn_factory = repoze.who.plugins.sql:make_psycopg_conn_factory compare_fn = repoze.who.plugins.sql:default_password_compare [plugin:sqlproperties] name = properties use = repoze.who.plugins.sql:make_metadata_plugin query = "SELECT firstname, lastname FROM users where userid = %(__userid)s;" filter = my.package:filter_propmd conn_factory = repoze.who.plugins.sql:make_psycopg_conn_factory [general] request_classifier = repoze.who.classifiers:default_request_classifier challenge_decider = repoze.who.classifiers:default_challenge_decider remote_user_key = REMOTE_USER [identifiers] # plugin_name;classifier_name:.. or just plugin_name (good for any) plugins = form;browser auth_tkt basicauth [authenticators] # plugin_name;classifier_name.. or just plugin_name (good for any) plugins = htpasswd sqlusers [challengers] # plugin_name;classifier_name:.. or just plugin_name (good for any) plugins = form;browser basicauth [mdproviders] plugins = sqlproperties The basicauth section configures a plugin that does identification and challenge for basic auth credentials. The form section configures a plugin that does identification and challenge (its implementation defers to the cookie plugin for identification "forget" and "remember" duties, thus the "identifier_impl_name" key; this is looked up at runtime). The auth_tkt section configures a plugin that does identification for cookie auth credentials. The htpasswd plugin obtains its user info from a file. The sqlusers plugin obtains its user info from a Postgres database. The identifiers section provides an ordered list of plugins that are willing to provide identification capability. These will be consulted in the defined order. The tokens on each line of the ``plugins=`` key are in the form "plugin_name:requestclassifier_name:..." (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 form plugin (if the request is classified as a browser request), then the cookie identifier (unconditionally), 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. 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. Each authenticator is called with each set of identities found by the identifier plugins. The first identity that can be authenticated is used to set ``REMOTE_USER``. The mdproviders section provides an ordered list of plugins that provide metadata provider capability. These will be consulted in the defined order. Each will have a chance (on ingress) to provide add metadata to the authenticated identity. Our example mdproviders section shows one plugin configured: "sqlproperties". The sqlproperties plugin will add information related to user properties (e.g. first name and last name) to the identity dictionary. The challengers section provides an ordered list of plugins that provide challenger capability. These will be consulted in the defined order, so the system will consult the cookie auth plugin first, then the basic auth plugin. Each will have a chance to initiate a challenge. The above configuration indicates that the form challenger will fire if it's a browser request, and the basic auth challenger will fire if it's not (fallback). Writing :mod:`repoze.who` Plugins ================================= :mod:`repoze.who` can be extended arbitrarily through the creation of plugins. Plugins are of one of four types: identifier plugins, authenticator plugins, metadata provider plugins, and challenge plugins. Writing An Identifier Plugin ---------------------------- An identifier plugin (aka an ``IIdentifier`` plugin) must do three things: extract credentials from the request and turn them into an "identity", "remember" credentials, and "forget" credentials. Here's a simple cookie identification plugin that does these three things :: class InsecureCookiePlugin(object): def __init__(self, cookie_name): self.cookie_name = cookie_name def identify(self, environ): cookies = get_cookies(environ) cookie = cookies.get(self.cookie_name) if cookie is None: return None import binascii try: auth = cookie.value.decode('base64') except binascii.Error: # can't decode return None try: login, password = auth.split(':', 1) return {'login':login, 'password':password} except ValueError: # not enough values to unpack return None def remember(self, environ, identity): cookie_value = '%(login)s:%(password)s' % identity cookie_value = cookie_value.encode('base64').rstrip() from paste.request import get_cookies cookies = get_cookies(environ) existing = cookies.get(self.cookie_name) value = getattr(existing, 'value', None) if value != cookie_value: # return a Set-Cookie header set_cookie = '%s=%s; Path=/;' % (self.cookie_name, cookie_value) return [('Set-Cookie', set_cookie)] def forget(self, environ, identity): # return a expires Set-Cookie header expired = ('%s=""; Path=/; Expires=Sun, 10-May-1971 11:59:00 GMT' % self.cookie_name) return [('Set-Cookie', expired)] def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) .identify ~~~~~~~~~ The ``identify`` method of our InsecureCookiePlugin accepts a single argument "environ". This will be the WSGI environment dictionary. Our plugin attempts to grub through the cookies sent by the client, trying to find one that matches our cookie name. If it finds one that matches, it attempts to decode it and turn it into a login and a password, which it returns as values in a dictionary. This dictionary is thereafter known as an "identity". If it finds no credentials in cookies, it returns None (which is not considered an identity). More generally, the ``identify`` method of an ``IIdentifier`` plugin is called once on WSGI request "ingress", and it is expected to grub arbitrarily through the WSGI environment looking for credential information. In our above plugin, the credential information is expected to be in a cookie but credential information could be in a cookie, a form field, basic/digest auth information, a header, a WSGI environment variable set by some upstream middleware or whatever else someone might use to stash authentication information. If the plugin finds credentials in the request, it's expected to return an "identity": this must be a dictionary. The dictionary is not required to have any particular keys or value composition, although it's wise if the identification plugin looks for both a login name and a password information to return at least {'login':login_name, 'password':password}, as some authenticator plugins may depend on presence of the names "login" and "password" (e.g. the htpasswd and sql ``IAuthenticator`` plugins). If an ``IIdentifier`` plugin finds no credentials, it is expected to return None. An ``IIdentifier`` plugin is also permitted to "preauthenticate" an identity. If the identifier plugin knows that the identity is "good" (e.g. in the case of ticket-based authentication where the userid is embedded into the ticket), it can insert a special key into the identity dictionary: ``repoze.who.userid``. If this key is present in the identity dictionary, no authenticators will be asked to authenticate the identity. This effectively allows an ``IIdentifier`` plugin to become an ``IAuthenticator`` plugin when breaking apart the responsibility into two separate plugins is "make-work". Preauthenticated identities will be selected first when deciding which identity to use for any given request. Our cookie plugin doesn't use this feature. .remember ~~~~~~~~~ If we've passed a REMOTE_USER to the WSGI application during ingress (as a result of providing an identity that could be authenticated), and the downstream application doesn't kick back with an unauthorized response, on egress we want the requesting client to "remember" the identity we provided if there's some way to do that and if he hasn't already, in order to ensure he will pass it back to us on subsequent requests without requiring another login. The remember method of an ``IIdentifier`` plugin is called for each non-unauthenticated response. It is the responsibility of the ``IIdentifier`` plugin to conditionally return HTTP headers that will cause the client to remember the credentials implied by "identity". Our InsecureCookiePlugin implements the "remember" method by returning headers which set a cookie if and only if one is not already set with the same name and value in the WSGI environment. These headers will be tacked on to the response headers provided by the downstream application during the response. When you write a remember method, most of the work involved is determining *whether or not* you need to return headers. It's typical to see remember methods that compute an "old state" and a "new state" and compare the two against each other in order to determine if headers need to be returned. In our example InsecureCookiePlugin, the "old state" is ``cookie_value`` and the "new state" is ``value``. .forget ~~~~~~~ Eventually the WSGI application we're serving will issue a "401 Unauthorized" or another status signifying that the request could not be authorized. :mod:`repoze.who` intercepts this status and calls ``IIdentifier`` plugins asking them to "forget" the credentials implied by the identity. It is the "forget" method's job at this point to return HTTP headers that will effectively clear any credentials on the requesting client implied by the "identity" argument. Our InsecureCookiePlugin implements the "forget" method by returning a header which resets the cookie that was set earlier by the remember method to one that expires in the past (on my birthday, in fact). This header will be tacked onto the response headers provided by the downstream application. Writing an Authenticator Plugin ------------------------------- An authenticator plugin (aka an ``IAuthenticator`` plugin) must do only one thing (on "ingress"): accept an identity and check if the identity is "good". If the identity is good, it should return a "user id". This user id may or may not be the same as the "login" provided by the user. An ``IAuthenticator`` plugin will be called for each identity found during the identification phase (there may be multiple identities for a single request, as there may be multiple ``IIdentifier`` plugins active at any given time), so it may be called multiple times in the same request. Here's a simple authenticator plugin that attempts to match an identity against ones defined in an "htpasswd" file that does just that:: class SimpleHTPasswdPlugin(object): def __init__(self, filename): self.filename = filename # IAuthenticatorPlugin def authenticate(self, environ, identity): try: login = identity['login'] password = identity['password'] except KeyError: return None f = open(self.filename, 'r') for line in f: try: username, hashed = line.rstrip().split(':', 1) except ValueError: continue if username == login: if crypt_check(password, hashed): return username return None def crypt_check(password, hashed): from crypt import crypt salt = hashed[:2] return hashed == crypt(password, salt) An ``IAuthenticator`` plugin implements one "interface" method: "authentictate". The formal specification for the arguments and return values expected from these methods are available in the ``interfaces.py`` file in :mod:`repoze.who` as the ``IAuthenticator`` interface, but let's examine this method here less formally. .authenticate ~~~~~~~~~~~~~ The ``authenticate`` method accepts two arguments: the WSGI environment and an identity. Our SimpleHTPasswdPlugin ``authenticate`` implementation grabs the login and password out of the identity and attempts to find the login in the htpasswd file. If it finds it, it compares the crypted version of the password provided by the user to the crypted version stored in the htpasswd file, and finally, if they match, it returns the login. If they do not match, it returns None. .. note:: Our plugin's ``authenticate`` method does not assume that the keys ``login`` or ``password`` exist in the identity; although it requires them to do "real work" it returns None if they are not present instead of raising an exception. This is required by the ``IAuthenticator`` interface specification. Writing a Challenger Plugin --------------------------- A challenger plugin (aka an ``IChallenger`` plugin) must do only one thing on "egress": return a WSGI application which performs a "challenge". A WSGI application is a callable that accepts an "environ" and a "start_response" as its parameters; see "PEP 333" for further definition of what a WSGI application is. A challenge asks the user for credentials. Here's an example of a simple challenger plugin:: from paste.httpheaders import WWW_AUTHENTICATE from paste.httpexceptions import HTTPUnauthorized class BasicAuthChallengerPlugin(object): def __init__(self, realm): self.realm = realm # IChallenger def challenge(self, environ, status, app_headers, forget_headers): head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm) if head[0] not in forget_headers: head = head + forget_headers return HTTPUnauthorized(headers=head) Note that the plugin implements a single "interface" method: "challenge". The formal specification for the arguments and return values expected from this method is available in the "interfaces.py" file in :mod:`repoze.who` as the ``IChallenger`` interface. This method is called when :mod:`repoze.who` determines that the application has returned an "unauthorized" response (e.g. a 401). Only one challenger will be consulted during "egress" as necessary (the first one to return a non-None response). .challenge ~~~~~~~~~~ The challenge method takes environ (the WSGI environment), 'status' (the status as set by the downstream application), the "app_headers" (headers returned by the application), and the "forget_headers" (headers returned by all participating ``IIdentifier`` plugins whom were asked to "forget" this user). Our BasicAuthChallengerPlugin takes advantage of the fact that the HTTPUnauthorized exception imported from paste.httpexceptions can be used as a WSGI application. It first makes sure that we don't repeat headers if an identification plugin has already set a "WWW-Authenticate" header like ours, then it returns an instance of HTTPUnauthorized, passing in merged headers. This will cause a basic authentication dialog to be presented to the user. Writing a Metadata Provider Plugin ---------------------------------- A metadata provider plugin (aka an ``IMetadataProvider`` plugin) must do only one thing (on "ingress"): "scribble" on the identity dictionary provided to it when it is called. An ``IMetadataProvider`` plugin will be called with the final "best" identity found during the authentication phase, or not at all if no "best" identity could be authenticated. Thus, each ``IMetadataProvider`` plugin will be called exactly zero or one times during a request. Here's a simple metadata provider plugin that provides "property" information from a dictionary:: _DATA = { 'chris': {'first_name':'Chris', 'last_name':'McDonough'} , 'whit': {'first_name':'Whit', 'last_name':'Morriss'} } class SimpleMetadataProvider(object): def add_metadata(self, environ, identity): userid = identity.get('repoze.who.userid') info = _DATA.get(userid) if info is not None: identity.update(info) .add_metadata ~~~~~~~~~~~~~ Arbitrarily add information to the identity dict based in other data in the environment or identity. Our plugin adds ``first_name`` and ``last_name`` values to the identity if the userid matches ``chris`` or ``whit``. Interfaces ========== .. module:: repoze.who.interfaces .. code-block:: python class IPlugin(Interface): pass class IRequestClassifier(IPlugin): """ On ingress: classify a request. """ def __call__(environ): """ environ -> request classifier string This interface is responsible for returning a string value representing a request classification. o 'environ' is the WSGI environment. """ class IChallengeDecider(IPlugin): """ On egress: decide whether a challenge needs to be presented to the user. """ def __call__(environ, status, headers): """ args -> True | False o 'environ' is the WSGI environment. o 'status' is the HTTP status as returned by the downstream WSGI application. o 'headers' are the headers returned by the downstream WSGI application. This interface is responsible for returning True if a challenge needs to be presented to the user, False otherwise. """ class IIdentifier(IPlugin): """ On ingress: Extract credentials from the WSGI environment and turn them into an identity. On egress (remember): Conditionally set information in the response headers allowing the remote system to remember this identity. On egress (forget): Conditionally set information in the response headers allowing the remote system to forget this identity (during a challenge). """ def identify(environ): """ On ingress: environ -> { k1 : v1 , ... , kN : vN } | None o 'environ' is the WSGI environment. o If credentials are found, the returned identity mapping will contain an arbitrary set of key/value pairs. If the identity is based on a login and password, the environment is recommended to contain at least 'login' and 'password' keys as this provides compatibility between the plugin and existing authenticator plugins. If the identity can be 'preauthenticated' (e.g. if the userid is embedded in the identity, such as when we're using ticket-based authentication), the plugin should set the userid in the special 'repoze.who.userid' key; no authenticators will be asked to authenticate the identity thereafer. o Return None to indicate that the plugin found no appropriate credentials. o Only IIdentifier plugins which match one of the the current request's classifications will be asked to perform identification. o An identifier plugin is permitted to add a key to the environment named 'repoze.who.application', which should be an arbitrary WSGI application. If an identifier plugin does so, this application is used instead of the downstream application set up within the middleware. This feature is useful for identifier plugins which need to perform redirection to obtain credentials. If two identifier plugins add a 'repoze.who.application' WSGI application to the environment, the last one consulted will"win". """ def remember(environ, identity): """ On egress (no challenge required): args -> [ (header-name, header-value), ...] | None Return a list of headers suitable for allowing the requesting system to remember the identification information (e.g. a Set-Cookie header). Return None if no headers need to be set. These headers will be appended to any headers returned by the downstream application. """ def forget(environ, identity): """ On egress (challenge required): args -> [ (header-name, header-value), ...] | None Return a list of headers suitable for allowing the requesting system to forget the identification information (e.g. a Set-Cookie header with an expires date in the past). Return None if no headers need to be set. These headers will be included in the response provided by the challenge app. """ class IAuthenticator(IPlugin): """ On ingress: validate the identity and return a user id or None. """ def authenticate(environ, identity): """ identity -> 'userid' | None o 'environ' is the WSGI environment. o 'identity' will be a dictionary (with arbitrary keys and values). o The IAuthenticator should return a single user id (optimally a string) if the identity can be authenticated. If the identify cannot be authenticated, the IAuthenticator should return None. Each instance of a registered IAuthenticator plugin that matches the request classifier will be called N times during a single request, where N is the number of identities found by any IIdentifierPlugin instances. An authenticator must not raise an exception if it is provided an identity dictionary that it does not understand (e.g. if it presumes that 'login' and 'password' are keys in the dictionary, it should check for the existence of these keys before attempting to do anything; if they don't exist, it should return None). """ class IChallenger(IPlugin): """ On egress: Conditionally initiate a challenge to the user to provide credentials. Only challenge plugins which match one of the the current response's classifications will be asked to perform a challenge. """ def challenge(environ, status, app_headers, forget_headers): """ args -> WSGI application or None o 'environ' is the WSGI environment. o 'status' is the status written into start_response by the downstream application. o 'app_headers' is the headers list written into start_response by the downstream application. o 'forget_headers' is a list of headers which must be passed back in the response in order to perform credentials reset (logout). These come from the 'forget' method of IIdentifier plugin used to do the request's identification. Examine the values passed in and return a WSGI application (a callable which accepts environ and start_response as its two positional arguments, ala PEP 333) which causes a challenge to be performed. Return None to forego performing a challenge. """ class IMetadataProvider(IPlugin): """On ingress: When an identity is authenticated, metadata providers may scribble on the identity dictionary arbitrarily. Return values from metadata providers are ignored. """ def add_metadata(environ, identity): """ Add metadata to the identity (which is a dictionary). One value is always guaranteed to be in the dictionary when add_metadata is called: 'repoze.who.userid', representing the user id of the identity. Availability and composition of other keys will depend on the identifier plugin which created the identity. """ repoze.who-1.0.18/docs/changes.rst0000644000175000017500000000003411137401442016671 0ustar tseavertseaver.. include:: ../CHANGES.txt repoze.who-1.0.18/docs/.static/0000755000175000017500000000000011274642513016107 5ustar tseavertseaverrepoze.who-1.0.18/docs/.static/repoze.css0000644000175000017500000000047411133213520020114 0ustar tseavertseaver@import url('default.css'); body { background-color: #006339; } div.document { background-color: #dad3bd; } div.sphinxsidebar h3,h4,h5,li,a { color: #127c56 !important; } div.related { color: #dad3bd !important; background-color: #00744a; } div.related a { color: #dad3bd !important; } repoze.who-1.0.18/docs/.static/logo_hi.gif0000644000175000017500000000777611076121561020232 0ustar tseavertseaverGIF89a4polܵEEE999VVUŪ؉ǼʤჂ򠞙dcaRRQ]\\{zx???MLL444333!,4@pH,Ȥrl:Ш@Zجvzxl-z|~su2WfoTe`UXboef :7=Óʋ<Ϥ5)ݳ߼˿ڶܾՎ΢55=C!ѷm+M>o6dtu._ݘUn>؟3:XfC`[0/h uCP![ W d(]"Xr_ 9"#2dwHe.zآ0]Z}7L PeI8tO aiCÃ WI -*蠄ozXv@5 @d "^0e80ݪ&p"|Cnk, 06,w)>jjwiɂ< ɂ:p +[8th k(8,#h,,/U"CPCg Z6 滲˙2宐A:Ё|{J/ B ؁Pp ZP p MK C,( g l| !p'| T)Prª P t5:z|pBX*R3( D-,. A'G, @8 @A*H@>P ql+0 䯀t)T>P`&(0,=#@V`?-;h_S܃ŭqWuo @/S^&  gZUG >B^EZUmyUk-}T0]w~;ܙ9%pquxO0 h07]- K-[^`q< ]rCyPb6Gy$a>p(%%@$g- }ǀ;%y'?rgy<2Tk=>Ng~'@S2D1\4l#px(w\i׀eBxx42-%eT\?05Ph>(}`(&u$`x/"r(^@6†Vz#ehy)iVhKng(`NHz!wB.  gx%d؉mȆl\e-7p_}4z!<`P#K`h~v,0\e `74(;0H9Ȇ<8،<@1#@։T9l(؎8mȆ $ LC9'J(.`KP hs3`"#׎yS:Y: 92`X`Yh ن0i2i 0m20UTYX`FC3Xo:HXyLB7. QI׋ 0dYfy)B gpr9tYtȗ~)|Xr(RV؄ 2xMxu2yuWɈZə捔ق闫W)B8^hBy)iXyiZHz¨I py1|?P雿Iٙy Ti PTK#p:ZP`a)zzEɚ  p?2UĢâ-j-ꓮB2/:1:= pq)@9>)U.ZFKKʣ>:0ڢ,60%ʥ#$b1P3Хɥ]٦Ӷ>ijkm 0 `U+mJ  l*djjr*A7ॆj v DGiZ$b905- :J** jJ*KfK jm2x=ښrʬJšjZ ׮*:ʩk8گʮ#Gڊ{7P0hzʮ:zK = 4 y<2=7U, M9P1:˳> +@+;KE۳@;LۮO۴NS[L[P;\XO+W;efac[\K]kR Y;repoze.who-1.0.18/docs/index.rst0000644000175000017500000000356011274372147016412 0ustar tseavertseaver.. _index: *************************************************** :mod:`repoze.who` -- WSGI Authentication Middleware *************************************************** :Author: Chris McDonough :Version: |version| .. module:: repoze.who :synopsis: WSGI authentication middleware .. topic:: Overview :mod:`repoze.who` is an identification and authentication framework for arbitrary WSGI applications. It acts as WSGI middleware. :mod:`repoze.who` is inspired by Zope 2's Pluggable Authentication Service (PAS) (but :mod:`repoze.who` is not dependent on Zope in any way; it is useful for any WSGI application). It provides no facility for authorization (ensuring whether a user can or cannot perform the operation implied by the request). This is considered to be the domain of the WSGI application. It attempts to reuse implementations from ``paste.auth`` for some of its functionality. Sections ======== .. toctree:: :maxdepth: 2 narr Change History ============== .. toctree:: :maxdepth: 2 changes Support and Development ======================= To report bugs, use the `Repoze bug tracker `_. If you've got questions that aren't answered by this documentation, contact the `Repoze-dev maillist `_ or join the `#repoze IRC channel `_. Browse and check out tagged and trunk versions of :mod:`repoze.who` via the `Repoze Subversion repository `_. To check out the trunk via Subversion, use this command:: svn co http://svn.repoze.org/repoze.who/trunk repoze.who To find out how to become a contributor to :mod:`repoze.who`, please see the `contributor's page `_. Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` repoze.who-1.0.18/docs/conf.py0000644000175000017500000001361211274372147016047 0ustar tseavertseaver# -*- coding: utf-8 -*- # # repoze.who documentation build configuration file, created by # sphinx-quickstart on Wed Jul 16 13:18:14 2008. # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import sys, os # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. parent = os.path.dirname(os.path.dirname(__file__)) sys.path.append(os.path.abspath(parent)) wd = os.getcwd() os.chdir(parent) os.system('%s setup.py test -q' % sys.executable) os.chdir(wd) for item in os.listdir(parent): if item.endswith('.egg'): sys.path.append(os.path.join(parent, item)) # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest'] # Add any paths that contain templates here, relative to this directory. templates_path = ['.templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = 'repoze.who' copyright = '2008, Agendaless Consulting' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = '1.0.16dev' # The full version, including alpha/beta/rc tags. release = '1.0.16dev' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directories, that shouldn't be searched # for source files. #exclude_dirs = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. html_style = 'repoze.css' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (within the static path) to place at the top of # the sidebar. html_logo = '.static/logo_hi.gif' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['.static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. #html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'repozebfgdoc' # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ ('index', 'repozebfg.tex', 'repoze.who Documentation', 'Agendaless Consulting', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. latex_logo = '.static/logo_hi.gif' # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True