diff -Nru python-repoze.who-1.0.18/.bzrignore python-repoze.who-2.2/.bzrignore --- python-repoze.who-1.0.18/.bzrignore 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/.bzrignore 2011-05-18 16:35:37.000000000 +0000 @@ -0,0 +1,3 @@ +.coverage +*.egg-info +docs/.build/* diff -Nru python-repoze.who-1.0.18/CHANGES.rst python-repoze.who-2.2/CHANGES.rst --- python-repoze.who-1.0.18/CHANGES.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/CHANGES.rst 2013-05-17 22:01:10.000000000 +0000 @@ -0,0 +1,670 @@ +repoze.who Changelog +==================== + +2.2 (2013-05-17) +---------------- + +- Parse INI-file configuration using ``SafeConfigParser``: allows + escaping the ``'%'`` so that e.g. a query template using for a DB-API + connection using ``pyformat`` preserves the template. + +- Added support for Python 3.3, PyPy. + + +2.1 (2013-03-20) +---------------- + +- ``_compat`` module: tolerate missing ``CONTENT_TYPE`` key in the WSGI + environment. Thanks to Dag Hoidal for the patch. + +- ``htpasswd`` plugin: add a ``sha1_check`` checker function (the ``crypt`` + module is not available on Windows). Thanks to Chandrashekar Jayaraman + for the patch. + +- Documentation typo fixes from Carlos de la Guardia and Atsushi Odagiri. + + +2.1b1 (2012-11-05) +------------------ + +- Ported to Py3k using the "compatible subset" mode. + - Dropped support for Python < 2.6.x. + - Dropped dependency on Paste (forking some code from it). + - Added dependency on WebOb instead. + Thanks to Atsushi Odagiri (aodag) for the initial effort. + + +2.0 (2011-09-28) +---------------- + +- ``auth_tkt`` plugin: strip any port number from the 'Domain' of generated + cookies. http://bugs.repoze.org/issue66 + +- Further harden middleware, calling ``close()`` on the iterable even if + raising an exception for a missing challenger. + http://bugs.repoze.org/issue174 + + +2.0b1 (2011-05-24) +------------------ + +- Enabled standard use of logging module's configuration mechanism. + See http://docs.python.org/dev/howto/logging.html#configuring-logging-for-a-library + Thanks to jgoldsmith for the patch: http://bugs.repoze.org/issue178 + + +- ``repoze.who.plugins.htpasswd``: defend against timing-based attacks. + + +2.0a4 (2011-02-02) +------------------ + +- Ensure that the middleware calls ``close()`` (if it exists) on the + iterable returned from thw wrapped application, as required by PEP 333. + http://bugs.repoze.org/issue174 + +- Make ``make_api_factory_with_config`` tolerant of invalid filenames / + content for the config file: in such cases, the API factory will have + *no* configured plugins or policies: it will only be useful for retrieving + the API from an environment populated by middleware. + +- Fix bug in ``repoze.who.api`` where the ``remember()`` or ``forget()`` + methods could return a None if the identifier plugin returned a None. + +- Fix ``auth_tkt`` plugin to not hand over tokens as strings to paste. See + http://lists.repoze.org/pipermail/repoze-dev/2010-November/003680.html + +- Fix ``auth_tkt`` plugin to add "secure" and "HttpOnly" to cookies when + configured with ``secure=True``: these attributes prevent the browser from + sending cookies over insecure channels, which could be vulnerable to some + XSS attacks. + +- Avoid propagating unicode 'max_age' value into cookie headers. See + https://bugs.launchpad.net/bugs/674123 . + +- Added a single-file example BFG application demonstrating the use of + the new 'login' and 'logout' methods of the API object. + +- Add ``login`` and ``logout`` methods to the ``repoze.who.api.API`` object, + as a convenience for application-driven login / logout code, which would + otherwise need to use private methods of the API, and reach down into + its plugins. + + +2.0a3 (2010-09-30) +------------------ + +- Deprecated the following plugins, moving their modules, tests, and docs + to a new project, ``repoze.who.deprecatedplugins``: + + - ``repoze.who.plugins.cookie.InsecureCookiePlugin`` + + - ``repoze.who.plugins.form.FormPlugin`` + + - ``repoze.who.plugins.form.RedirectingFormPlugin`` + +- Made the ``repoze.who.plugins.cookie.InsecureCookiePlugin`` take a + ``charset`` argument, and use to to encode / decode login and password. + See http://bugs.repoze.org/issue155 + +- Updated ``repoze.who.restrict`` to return headers as a list, to keep + ``wsgiref`` from complaining. + +- Helped default request classifier cope with xml submissions with an + explicit charset defined: http://bugs.repoze.org/issue145 (Lorenzo + M. Catucci) + +- Corrected the handling of type and subtype when matching an XML post + to ``xmlpost`` in the default classifier, which, according to RFC + 2045, must be matched case-insensitively: + http://bugs.repoze.org/issue145 (Lorenzo M. Catucci) + +- Added ``repoze.who.config:make_api_factory_with_config``, a convenience + method for applications which want to set up their own API Factory from + a configuration file. + +- Fixed example call to ``repoze.who.config:make_middleware_with_config`` + (added missing ``global_config`` argument). See + http://bugs.repoze.org/issue114 + + +2.0a2 (2010-03-25) +------------------ + +Bugs Fixed +~~~~~~~~~~ + +- Fixed failure to pass substution values in log message string formatting + for ``repoze.who.api:API.challenge``. Fix included adding tests for all + logging done by the API object. See http://bugs.repoze.org/issue122 + +Backward Incompatibilities +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Adjusted logging level for some lower-level details from ``info`` + to ``debug``. + + + +2.0a1 (2010-02-24) +------------------ + +Features +~~~~~~~~ + +- Restored the ability to create the middleware using the old ``classifier`` + argument. That argument is now a deprecated-but-will-work-forever alias for + ``request_classifier``. + +- The ``auth_tkt`` plugin now implements the ``IAuthenticator`` interface, + and should normally be used both as an ``IIdentifier`` and an + ``IAuthenticator``. + +- Factored out the API of the middleware object to make it useful from + within the application. Applications using ``repoze.who``` now fall into + one of three catgeories: + + - "middleware-only" applications are configured with middleware, and + use either ``REMOTE_USER`` or ``repoze.who.identity`` from the environment + to determing the authenticated user. + + - "bare metal" applications use no ``repoze.who`` middleware at all: + instead, they configure and an ``APIFactory`` object at startup, and + use it to create an ``API`` object when needed on a per-request basis. + + - "hybrid" applications are configured with ``repoze.who`` middleware, + but use a new library function to fetch the ``API`` object from the + environ, e.g. to permit calling ``remember`` after a signup or successful + login. + +Bugs Fixed +~~~~~~~~~~ + +- Fix http://bugs.repoze.org/issue102: when no challengers existed, + logging would cause an exception. + +- Remove ``ez_setup.py`` and dependency on it in setup.py (support + distribute). + +Backward Incompatibilities +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- The middleware used to allow identifier plugins to "pre-authenticate" + an identity. This feature is no longer supported: the ``auth_tkt`` + plugin, which used to use the feature, is now configured to work as + an authenticator plugin (as well as an identifier). + +- The ``repoze.who.middleware:PluggableAuthenticationMiddleware`` class + no longer has the following (non-API) methods (now made API methods + of the ``repoze.who.api:API`` class): + + - ``add_metadata`` + - ``authenticate`` + - ``challenge`` + - ``identify`` + +- The following (non-API) functions moved from ``repoze.who.middleware`` to + ``repoze.who.api``: + + - ``make_registries`` + - ``match_classification`` + - ``verify`` + + + +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). diff -Nru python-repoze.who-1.0.18/CHANGES.txt python-repoze.who-2.2/CHANGES.txt --- python-repoze.who-1.0.18/CHANGES.txt 2009-11-05 21:34:14.000000000 +0000 +++ python-repoze.who-2.2/CHANGES.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,455 +0,0 @@ -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). diff -Nru python-repoze.who-1.0.18/CONTRIBUTORS.txt python-repoze.who-2.2/CONTRIBUTORS.txt --- python-repoze.who-1.0.18/CONTRIBUTORS.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/CONTRIBUTORS.txt 2012-11-09 01:04:06.000000000 +0000 @@ -0,0 +1,108 @@ +Repoze Project Contributor Agreement +==================================== + +The submitter agrees by adding his or her name within the section below named +"Contributors" and submitting the resulting modified document to the +canonical shared repository location for this software project (whether +directly, as a user with "direct commit access", or via a "pull request"), he +or she is signing a contract electronically. The submitter becomes a +Contributor after a) he or she signs this document by adding their name +beneath the "Contributors" section below, and b) the resulting document is +accepted into the canonical version control repository. + +Treatment of Account +--------------------- + +Contributor will not allow anyone other than the Contributor to use his or +her username or source repository login to submit code to a Repoze Project +source repository. Should Contributor become aware of any such use, +Contributor will immediately by notifying Agendaless Consulting. +Notification must be performed by sending an email to +webmaster@agendaless.com. Until such notice is received, Contributor will be +presumed to have taken all actions made through Contributor's account. If the +Contributor has direct commit access, Agendaless Consulting will have +complete control and discretion over capabilities assigned to Contributor's +account, and may disable Contributor's account for any reason at any time. + +Legal Effect of Contribution +---------------------------- + +Upon submitting a change or new work to a Repoze Project source Repository (a +"Contribution"), you agree to assign, and hereby do assign, a one-half +interest of all right, title and interest in and to copyright and other +intellectual property rights with respect to your new and original portions +of the Contribution to Agendaless Consulting. You and Agendaless Consulting +each agree that the other shall be free to exercise any and all exclusive +rights in and to the Contribution, without accounting to one another, +including without limitation, the right to license the Contribution to others +under the Repoze Public License. This agreement shall run with title to the +Contribution. Agendaless Consulting does not convey to you any right, title +or interest in or to the Program or such portions of the Contribution that +were taken from the Program. Your transmission of a submission to the Repoze +Project source Repository and marks of identification concerning the +Contribution itself constitute your intent to contribute and your assignment +of the work in accordance with the provisions of this Agreement. + +License Terms +------------- + +Code committed to the Repoze Project source repository (Committed Code) must +be governed by the Repoze Public License (http://repoze.org/LICENSE.txt, aka +"the RPL") or another license acceptable to Agendaless Consulting. Until +Agendaless Consulting declares in writing an acceptable license other than +the RPL, only the RPL shall be used. A list of exceptions is detailed within +the "Licensing Exceptions" section of this document, if one exists. + +Representations, Warranty, and Indemnification +---------------------------------------------- + +Contributor represents and warrants that the Committed Code does not violate +the rights of any person or entity, and that the Contributor has legal +authority to enter into this Agreement and legal authority over Contributed +Code. Further, Contributor indemnifies Agendaless Consulting against +violations. + +Cryptography +------------ + +Contributor understands that cryptographic code may be subject to government +regulations with which Agendaless Consulting and/or entities using Committed +Code must comply. Any code which contains any of the items listed below must +not be checked-in until Agendaless Consulting staff has been notified and has +approved such contribution in writing. + +- Cryptographic capabilities or features + +- Calls to cryptographic features + +- User interface elements which provide context relating to cryptography + +- Code which may, under casual inspection, appear to be cryptographic. + +Notices +------- + +Contributor confirms that any notices required will be included in any +Committed Code. + +Licensing Exceptions +==================== + +None. + +List of Contributors +==================== + +The below-signed are contributors to a code repository that is part of the +project named "repoze.who". + +Each below-signed contributor has read, understand and agrees to the terms +above in the section within this document entitled "Repoze Project Contributor +Agreement" as of the date beside his or her name. + +Contributors +------------ + +- Tres Seaver, 2011/02/22 +- Atsushi Odagiri, 2012/03/22 +- Chandrashekar Jayaraman, 2012/11/09 diff -Nru python-repoze.who-1.0.18/debian/changelog python-repoze.who-2.2/debian/changelog --- python-repoze.who-1.0.18/debian/changelog 2015-12-16 12:02:17.000000000 +0000 +++ python-repoze.who-2.2/debian/changelog 2015-11-13 10:14:44.000000000 +0000 @@ -1,18 +1,57 @@ -python-repoze.who (1.0.18-4) unstable; urgency=medium +python-repoze.who (2.2-3) unstable; urgency=medium - * QA upload. - * Switch to debhelper 9. - * Switch to dh_python2. + * Added reproducibility patch from Chris Lamb (Closes: #790697). + * Fixed watch file. - -- Dimitri John Ledkov Sun, 20 Apr 2014 14:15:19 +0100 + -- Thomas Goirand Fri, 13 Nov 2015 10:06:13 +0000 -python-repoze.who (1.0.18-3) unstable; urgency=medium +python-repoze.who (2.2-2) unstable; urgency=medium - * QA upload. - * Move call to dh_link in debian/rules to a proper debian/links file, - so that debhelper just does the right thing (Closes: #738395) + * Uploading to unstable. + * Adopting the package, as per the email of Florian that the package is + currently unmaintained. + * Switch the package to PKG OpenStack as I have no write access to the DPMT. + * Bump to debhelper/compat 9. + + -- Thomas Goirand Sat, 31 Oct 2015 06:49:20 +0100 + +python-repoze.who (2.2-1) experimental; urgency=low + + [ Thomas Goirand ] + * Team upload. + * Switched to format 3.0 (quilt). + * Ran wrap-and-sort. + * Added dh-python as build-depends. + * Added Python3 support. + * Added debian/source/options to ignore change to the egg-info. + * debian/copyright is now using parseable format 1.0. + + [ Bernhard Reiter ] + * New upstream release + * debian/control + - Add python-repoze.sphinx.autointerface to Build-Depends + - Bump Standards-Version to 3.9.1 (no changes needed) + - Add myself to Uploaders + - Replace python-dev build dependency with python + (package is pure Python) + * debian/source/format: Create (with 1.0 inside) + * debian/doc-base: s/pytyon-repoze-who/python-repoze.who/ + * debian/watch: uversionmangle alpha part of version number + + [ Jakub Wilk ] + * Use canonical URIs for Vcs-* fields. + + [ Florian Zavatzki ] + * New upstream release + * debian/control: + - Add myself to Uploaders + - Add python-webob to Depends + * debian/rules: Change rules format - -- Jonathan Wiltshire Sun, 23 Mar 2014 12:58:05 +0000 + [ Piotr Ożarowski ] + * Do not install useless .pth files + + -- Thomas Goirand Fri, 24 Apr 2015 07:58:17 +0000 python-repoze.who (1.0.18-2) unstable; urgency=low diff -Nru python-repoze.who-1.0.18/debian/control python-repoze.who-2.2/debian/control --- python-repoze.who-1.0.18/debian/control 2015-12-16 12:02:17.000000000 +0000 +++ python-repoze.who-2.2/debian/control 2015-11-13 10:14:44.000000000 +0000 @@ -1,33 +1,41 @@ Source: python-repoze.who Section: python Priority: optional -Maintainer: Debian QA Group -Build-Depends: - debhelper (>= 9), - cdbs (>= 0.4.90~), - python-dev, - dh-python, - python-setuptools, - python-sphinx, - python-zope.interface, - python-paste -Standards-Version: 3.8.3 +Maintainer: PKG OpenStack +Uploaders: Thomas Goirand , +Build-Depends: debhelper (>= 9), + dh-python, + python-all, + python-setuptools, + python-sphinx, + python3-all, + python3-setuptools, +Build-Depends-Indep: python-nose, + python-paste, + python-repoze.sphinx.autointerface, + python-webob, + python-zope.interface, + python3-nose, + python3-paste, + python3-webob, + python3-zope.interface, +Standards-Version: 3.9.6 Homepage: http://www.repoze.org/ -Vcs-Svn: svn://svn.debian.org/python-modules/packages/python-repoze.who/trunk/ -Vcs-Browser: http://svn.debian.org/viewsvn/python-modules/packages/python-repoze.who/trunk/ +Vcs-Git: git://anonscm.debian.org/openstack/python-repoze.who.git +Vcs-Browser: https://anonscm.debian.org/cgit/openstack/python-repoze.who.git Package: python-repoze.who Architecture: all -Depends: - ${misc:Depends}, - ${python:Depends}, - python-zope.interface, - python-pkg-resources, - python-paste -Suggests: - python-psycopg2, - libjs-jquery -Description: identification and authentication framework for Python WSGI applications +Depends: python-paste, + python-pkg-resources, + python-sphinx, + python-webob, + python-zope.interface, + ${misc:Depends}, + ${python:Depends}, +Suggests: libjs-jquery, + python-psycopg2, +Description: ident and auth framework for Python WSGI applications - Python 2.x repoze.who is an identification and authentication framework for arbitrary Python WSGI applications; it acts as WSGI middleware. . @@ -38,4 +46,30 @@ 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. + . + This package contains the Python 2.x module. +Package: python3-repoze.who +Architecture: all +Depends: python3-paste, + python3-pkg-resources, + python3-sphinx, + python3-webob, + python3-zope.interface, + ${misc:Depends}, + ${python3:Depends}, +Suggests: libjs-jquery, + python3-psycopg2, +Description: ident and auth framework for Python WSGI applications - Python 3.x + repoze.who is an identification and authentication framework for + arbitrary Python WSGI applications; it acts as WSGI middleware. + . + repoze.who is inspired by Zope 2's Pluggable Authentication Service + (PAS), but 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. + . + This package contains the Python 3.x module. diff -Nru python-repoze.who-1.0.18/debian/copyright python-repoze.who-2.2/debian/copyright --- python-repoze.who-1.0.18/debian/copyright 2015-12-16 12:02:17.000000000 +0000 +++ python-repoze.who-2.2/debian/copyright 2015-11-13 10:14:44.000000000 +0000 @@ -1,53 +1,54 @@ -This package was debianized by Stefano Zacchiroli on -Fri, 29 May 2009 22:36:57 +0200. +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: repoze.who +Upstream-Contact: Agendaless Consulting +Source: https://pypi.python.org/pypi/repoze.who -Author: Agendaless Consulting +Files: debian/* +Copyright: (c) 2009, Stefano Zacchiroli + (c) 2015, Florian Zavatzki + (c) 2015, Bernhard Reiter + (c) 2009, 2015, Piotr Ożarowski + (c) 2015, Thomas Goirand +License: GPL-3+ + On Debian systems the full text of the GNU General Public License can + be found in the `/usr/share/common-licenses/GPL-3' file. Files: * -Copyright: Copyright © 2007 Agendaless Consulting and Contributors -License: - A copyright notice accompanies this license document that identifies - the copyright holders. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are - met: - +Copyright: (c) 2007, Agendaless Consulting and Contributors +License: BSD-like + A copyright notice accompanies this license document that identifies + the copyright holders. + . + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + . 1. Redistributions in source code must retain the accompanying copyright notice, this list of conditions, and the following disclaimer. - + . 2. Redistributions in binary form must reproduce the accompanying copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. - + . 3. Names of the copyright holders must not be used to endorse or promote products derived from this software without prior written permission from the copyright holders. - + . 4. If any files are modified, you must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. - + . Disclaimer - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND - ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR - TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF - THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - SUCH DAMAGE. - -Files: debian/* -Copyright: Copyright © 2009 Stefano Zacchiroli -License: GPL-3+ - On Debian systems the full text of the GNU General Public License can - be found in the `/usr/share/common-licenses/GPL-3' file. - + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESSED + OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff -Nru python-repoze.who-1.0.18/debian/doc-base python-repoze.who-2.2/debian/doc-base --- python-repoze.who-1.0.18/debian/doc-base 2015-12-16 12:02:17.000000000 +0000 +++ python-repoze.who-2.2/debian/doc-base 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -Document: pytyon-repoze-who -Title: Documentation for repoze.who -Abstract: Documentation for repoze.who: identification and authentication framework for Python WSGI applications -Section: Programming/Python - -Format: HTML -Index: /usr/share/doc/python-repoze.who/html/index.html -Files: /usr/share/doc/python-repoze.who/html/* diff -Nru python-repoze.who-1.0.18/debian/docs python-repoze.who-2.2/debian/docs --- python-repoze.who-1.0.18/debian/docs 2015-12-16 12:02:17.000000000 +0000 +++ python-repoze.who-2.2/debian/docs 2015-11-13 10:14:44.000000000 +0000 @@ -1,3 +1,2 @@ -README.txt +README.rst TODO.txt -docs/.build/html/ diff -Nru python-repoze.who-1.0.18/debian/gbp.conf python-repoze.who-2.2/debian/gbp.conf --- python-repoze.who-1.0.18/debian/gbp.conf 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/debian/gbp.conf 2015-11-13 10:14:44.000000000 +0000 @@ -0,0 +1,10 @@ +[DEFAULT] +upstream-branch = upstream +debian-branch = master +pristine-tar = True + +[buildpackage] +export-dir = ../build-area/ + +[git-import-orig] +dch = False diff -Nru python-repoze.who-1.0.18/debian/.git-dpm python-repoze.who-2.2/debian/.git-dpm --- python-repoze.who-1.0.18/debian/.git-dpm 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/debian/.git-dpm 2015-11-13 10:14:44.000000000 +0000 @@ -0,0 +1,11 @@ +# see git-dpm(1) from git-dpm package +0e7f1abb4744569a43ff3af928dbc2889871332f +0e7f1abb4744569a43ff3af928dbc2889871332f +0e7f1abb4744569a43ff3af928dbc2889871332f +0e7f1abb4744569a43ff3af928dbc2889871332f +python-repoze.who_2.2.orig.tar.gz +e3b966e4d11a12b06e7e799c9726fa888f6437d9 +308593 +debianTag="debian/%e%v" +patchedTag="patched/%e%v" +upstreamTag="upstream/%e%u" diff -Nru python-repoze.who-1.0.18/debian/links python-repoze.who-2.2/debian/links --- python-repoze.who-1.0.18/debian/links 2015-12-16 12:02:17.000000000 +0000 +++ python-repoze.who-2.2/debian/links 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -/usr/share/javascript/jquery/jquery.js /usr/share/doc/python-repoze.who/html/_static/jquery.js diff -Nru python-repoze.who-1.0.18/debian/python-repoze.who-doc.doc-base python-repoze.who-2.2/debian/python-repoze.who-doc.doc-base --- python-repoze.who-1.0.18/debian/python-repoze.who-doc.doc-base 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/debian/python-repoze.who-doc.doc-base 2015-11-13 10:14:44.000000000 +0000 @@ -0,0 +1,8 @@ +Document: python-repoze.who +Title: Documentation for repoze.who +Abstract: Documentation for repoze.who: identification and authentication framework for Python WSGI applications +Section: Programming/Python + +Format: HTML +Index: /usr/share/doc/python-repoze.who/html/index.html +Files: /usr/share/doc/python-repoze.who/html/* diff -Nru python-repoze.who-1.0.18/debian/rules python-repoze.who-2.2/debian/rules --- python-repoze.who-1.0.18/debian/rules 2015-12-16 12:02:17.000000000 +0000 +++ python-repoze.who-2.2/debian/rules 2015-11-13 10:14:44.000000000 +0000 @@ -1,17 +1,41 @@ #!/usr/bin/make -f -include /usr/share/cdbs/1/rules/debhelper.mk -include /usr/share/cdbs/1/class/python-distutils.mk -PKG = python-repoze.who -DEB_PYTHON_INSTALL_ARGS_ALL += --single-version-externally-managed -DEB_COMPRESS_EXCLUDE += .js - -build/$(PKG):: - $(MAKE) -C docs/ html - -cleanbuilddir/$(PKG):: - $(MAKE) -C docs/ clean - -install/$(PKG):: - find $(CURDIR)/debian/$(PKG) -name requires.txt -delete - find $(CURDIR)/debian/$(PKG) -name '*.pth' -delete +PYTHONS:=$(shell pyversions -vr) +PYTHON3S:=$(shell py3versions -vr) + +BUILD_DATE = $(shell dpkg-parsechangelog --show-field Date) +BUILD_DATE_FORMATTED = $(shell LC_ALL=C date --utc --date="$(BUILD_DATE)" "+%B %d, %Y") + +%: + dh $@ --buildsystem=python_distutils --with python2,python3,sphinxdoc + +override_dh_install: + set -e ; for pyvers in $(PYTHONS); do \ + python$$pyvers setup.py install --install-layout=deb \ + --root $(CURDIR)/debian/python-repoze.who; \ + done + set -e ; for pyvers in $(PYTHON3S); do \ + python$$pyvers setup.py install --install-layout=deb \ + --root $(CURDIR)/debian/python3-repoze.who; \ + done + rm -rf $(CURDIR)/debian/python*/usr/lib/python*/dist-packages/*.pth + + find $(CURDIR)/debian/python-repoze.who -iname __pycache__ -exec rm -rf {} \; + +override_dh_clean: + dh_clean -O--buildsystem=python_distutils + rm -rf docs/.build build + +override_dh_auto_build: + PYTHONPATH=. sphinx-build -b html -D today="$(BUILD_DATE_FORMATTED)" docs $(CURDIR)/debian/python-repoze.who/usr/share/doc/python-repoze.who/html + +override_dh_auto_install: + dh_auto_install + find debian/ -name '*.pth' -delete + + +override_dh_auto_test: +ifeq (,$(findstring nocheck, $(DEB_BUILD_OPTIONS))) + nosetests + nosetests3 +endif diff -Nru python-repoze.who-1.0.18/debian/source/format python-repoze.who-2.2/debian/source/format --- python-repoze.who-1.0.18/debian/source/format 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/debian/source/format 2015-11-13 10:14:44.000000000 +0000 @@ -0,0 +1 @@ +3.0 (quilt) diff -Nru python-repoze.who-1.0.18/debian/source/options python-repoze.who-2.2/debian/source/options --- python-repoze.who-1.0.18/debian/source/options 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/debian/source/options 2015-11-13 10:14:44.000000000 +0000 @@ -0,0 +1 @@ +extend-diff-ignore = "^[^/]*[.]egg-info/" diff -Nru python-repoze.who-1.0.18/debian/watch python-repoze.who-2.2/debian/watch --- python-repoze.who-1.0.18/debian/watch 2015-12-16 12:02:17.000000000 +0000 +++ python-repoze.who-2.2/debian/watch 2015-11-13 10:14:44.000000000 +0000 @@ -1,2 +1,3 @@ version=3 -http://pypi.python.org/packages/source/r/repoze.who/repoze.who-([0-9a-z.]*)\.tar\.gz +opts="uversionmangle=s/a/~a/" \ +http://pypi.debian.net/repoze.who/repoze.who-(.*).tar.gz diff -Nru python-repoze.who-1.0.18/docs/api.rst python-repoze.who-2.2/docs/api.rst --- python-repoze.who-1.0.18/docs/api.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/docs/api.rst 2012-03-24 21:17:18.000000000 +0000 @@ -0,0 +1,171 @@ +.. _api_narrative: + +Using the :mod:`repoze.who` Application Programming Interface (API) +=================================================================== + +.. _without_middleware: + +Using :mod:`repoze.who` without Middleware +------------------------------------------ + +An application which does not use the :mod:`repoze.who` middleware needs +to perform two separate tasks to use :mod:`repoze.who` machinery: + +- At application startup, it must create an :class:`repoze.who.api:APIFactory` + instance, populating it with a request classifier, a challenge decider, + and a set of plugins. It can do this process imperatively + (see :ref:`imperative_configuration`), or using a declarative + configuration file (see :ref:`declarative_configuration`). For the latter + case, there is a convenience function, + :func:`repoze.who.config.make_api_factory_with_config`: + +.. code-block:: python + + # myapp/run.py + from repoze.who.config import make_api_factory_with_config + who_api_factory = None + def startup(global_conf): + global who_api_factory + who_api_factory = make_api_factory_with_config(global_conf, + '/path/to/who.config') + +- When it needs to use the API, it must call the ``APIFactory``, passing + the WSGI environment to it. The ``APIFactory`` returns an object + implementing the :class:`repoze.who.interfaces:IRepozeWhoAPI` interface. + +.. code-block:: python + + # myapp/views.py + from myapp.run import who_api_factory + def my_view(context, request): + who_api = who_api_factory(request.environ) + +- Calling the ``APIFactory`` multiple times within the same request is + allowed, and should be very cheap (the API object is cached in the + request environment). + + +.. _middleware_api_hybrid: + +Mixed Use of :mod:`repoze.who` Middleware and API +------------------------------------------------- + +An application which uses the :mod:`repoze.who` middleware may still need +to interact directly with the ``IRepozeWhoAPI`` object for some purposes. +In such cases, it should call :func:`repoze.who.api:get_api`, passing +the WSGI environment. + +.. code-block:: python + + from repoze.who.api import get_api + def my_view(context, request): + who_api = get_api(request.environ) + +Alternately, the application might configure the ``APIFactory`` at startup, +as above, and then use it to find the API object, or create it if it was +not already created for the current request (e.g. perhaps by the middleware): + +.. code-block:: python + + def my_view(context, request): + who_api = context.who_api_factory(request.environ) + + +.. _writing_custom_login_view: + +Writing a Custom Login View +--------------------------- + +:class:`repoze.who.api.API` provides a helper method to assist developers +who want to control the details of the login view. The following +BFG example illustrates how this API might be used: + +.. code-block:: python + :linenos: + + def login_view(context, request): + message = '' + + who_api = get_api(request.environ) + if 'form.login' in request.POST: + creds = {} + creds['login'] = request.POST['login'] + creds['password'] = request.POST['password'] + authenticated, headers = who_api.login(creds) + if authenticated: + return HTTPFound(location='/', headers=headers) + + message = 'Invalid login.' + else: + # Forcefully forget any existing credentials. + _, headers = who_api.login({}) + + request.response_headerlist = headers + if 'REMOTE_USER' in request.environ: + del request.environ['REMOTE_USER'] + + return {'message': message} + +This application is written as a "hybrid": the :mod:`repoze.who` middleware +injects the API object into the WSGI enviornment on each request. + +- In line 4, this application extracts the API object from the environ + using :func:`repoze.who.api:get_api`. + +- Lines 6 - 8 fabricate a set of credentials, based on the values the + user entered in the form. + +- In line 9, the application asks the API to authenticate those credentials, + returning an identity and a set of respones headers. + +- Lines 10 and 11 handle the case of successful authentication: in this + case, the application redirects to the site root, setting the headers + returned by the API object, which will "remember" the user across requests. + +- Line 13 is reached on failed login. In this case, the headers returned + in line 9 will be "forget" headers, clearing any existing cookies or other + tokens. + +- Lines 14 - 16 perform a "fake" login, in order to get the "forget" headers. + +- Line 18 sets the "forget" headers to clear any authenticated user for + subsequent requests. + +- Lines 19 - 20 clear any authenticated user for the current request. + +- Line 22 returns any message about a failed login to the rendering template. + + +.. _interfaces: + +Interfaces +---------- + +.. automodule:: repoze.who.interfaces + + .. autointerface:: IAPIFactory + :members: + + .. autointerface:: IAPI + :members: + + .. autointerface:: IPlugin + :members: + + .. autointerface:: IRequestClassifier + :members: + + .. autointerface:: IChallengeDecider + :members: + + .. autointerface:: IIdentifier + :members: + + .. autointerface:: IAuthenticator + :members: + + .. autointerface:: IChallenger + :members: + + .. autointerface:: IMetadataProvider + :members: diff -Nru python-repoze.who-1.0.18/docs/changes.rst python-repoze.who-2.2/docs/changes.rst --- python-repoze.who-1.0.18/docs/changes.rst 2009-01-26 18:38:26.000000000 +0000 +++ python-repoze.who-2.2/docs/changes.rst 2013-04-05 16:02:59.000000000 +0000 @@ -1 +1 @@ -.. include:: ../CHANGES.txt +.. include:: ../CHANGES.rst diff -Nru python-repoze.who-1.0.18/docs/configuration.rst python-repoze.who-2.2/docs/configuration.rst --- python-repoze.who-1.0.18/docs/configuration.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/docs/configuration.rst 2013-04-26 12:49:37.000000000 +0000 @@ -0,0 +1,319 @@ +.. _configuration_points: + +Configuring :mod:`repoze.who` +============================= + +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``. + + +.. _imperative_configuration: + +Configuring :mod:`repoze.who` via Python Code +--------------------------------------------- + +.. module:: repoze.who.middleware + +.. class:: PluggableAuthenticationMiddleware(app, identifiers, challengers, authenticators, 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.redirector import RedirectorPlugin + 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') + redirector = RedirectorPlugin('/login.html') + redirector.classifications = {IChallenger:['browser'],} # only for browser + identifiers = [('auth_tkt', auth_tkt), + ('basicauth', basicauth)] + authenticators = [('auth_tkt', auth_tkt), + ('htpasswd', htpasswd)] + challengers = [('redirector', redirector), + ('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: + +- Two ``IIdentifier`` plugins (auth_tkt cookie, and a + basic auth plugin). In this setup, when "identification" needs to + be performed, the auth_tkt plugin will be checked first, then + the basic auth plugin. The application is responsible for handling + login via a form: this view would use the API (via :method:`remember`) + to generate apprpriate response headers. + +- Two ``IAuthenticator`` plugins: the auth_tkt plugin and an htpasswd plugin. + The auth_tkt plugin performs both ``IIdentifier`` and ``IAuthenticator`` + functions. The 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 redirector plugin, then the basic auth + plugin. The redirector 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. + + +.. _declarative_configuration: + +Configuring :mod:`repoze.who` 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, the global configuration dictionary, and the path to the +config file :: + + from repoze.who.config import make_middleware_with_config + who = make_middleware_with_config(app, global_conf, '/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:redirector] + # identificaion and challenge + use = repoze.who.plugins.redirector:make_plugin + login_url = /login.html + + [plugin:auth_tkt] + # identification and authentication + 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 + # Note the double %%: we have to escape it from the config parser in + # order to preserve it as a template for the psycopg2, whose 'paramstyle' + # is 'pyformat'. + 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 + # Note the double %%: we have to escape it from the config parser in + # order to preserve it as a template for the psycopg2, whose 'paramstyle' + # is 'pyformat'. + 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 = + auth_tkt + basicauth + + [authenticators] + # plugin_name;classifier_name.. or just plugin_name (good for any) + plugins = + auth_tkt + htpasswd + sqlusers + + [challengers] + # plugin_name;classifier_name:.. or just plugin_name (good for any) + plugins = + redirector;browser + basicauth + + [mdproviders] + plugins = + sqlproperties + +The basicauth section configures a plugin that does identification and +challenge for basic auth credentials. The redirector section configures a +plugin that does challenges. The auth_tkt section configures a plugin that +does identification for cookie auth credentials, as well as authenticating +them. 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 auth_tkt 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 redirector challenger +will fire if it's a browser request, and the basic auth challenger +will fire if it's not (fallback). diff -Nru python-repoze.who-1.0.18/docs/conf.py python-repoze.who-2.2/docs/conf.py --- python-repoze.who-1.0.18/docs/conf.py 2009-11-04 21:38:47.000000000 +0000 +++ python-repoze.who-2.2/docs/conf.py 2012-03-24 21:17:18.000000000 +0000 @@ -32,7 +32,11 @@ # 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'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'repoze.sphinx.autointerface', +] # Add any paths that contain templates here, relative to this directory. templates_path = ['.templates'] @@ -51,9 +55,10 @@ # other places throughout the built documents. # # The short X.Y version. -version = '1.0.16dev' + +version = '2.0a4' # The full version, including alpha/beta/rc tags. -release = '1.0.16dev' +release = version # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff -Nru python-repoze.who-1.0.18/docs/examples/examples.ini python-repoze.who-2.2/docs/examples/examples.ini --- python-repoze.who-1.0.18/docs/examples/examples.ini 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/docs/examples/examples.ini 2011-02-22 14:58:18.000000000 +0000 @@ -0,0 +1,11 @@ +[application:login_no_who] +paste.app_factory = standalone_login_no_who:main + +[application:login_w_who] +paste.app_factory = standalone_login:main + +[server:main] +use = egg:PasteScript#cherrypy +host = 127.0.0.1 +port = 5552 +numthreads = 4 diff -Nru python-repoze.who-1.0.18/docs/examples/hybrid/example.py python-repoze.who-2.2/docs/examples/hybrid/example.py --- python-repoze.who-1.0.18/docs/examples/hybrid/example.py 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/docs/examples/hybrid/example.py 2011-02-22 14:58:18.000000000 +0000 @@ -0,0 +1,217 @@ +""" Simple BFG application demonstrating use of repoze.who in "hybrid" mode. + +- repoze.who middleware intercepts and validates existing request credentials, + leaving 'REMOTE_USER' in the WSGI environ if they are OK. + +- Application handles login / logout directly, using the repoze.who API + to validate credentials and set headers. +""" +import logging +import os +import sys +from StringIO import StringIO + +from paste.httpserver import serve +from repoze.bfg.authentication import RemoteUserAuthenticationPolicy +from repoze.bfg.authorization import ACLAuthorizationPolicy +from repoze.bfg.configuration import Configurator +from repoze.bfg.security import Allow +from repoze.bfg.security import Authenticated +from repoze.bfg.security import DENY_ALL +from repoze.bfg.security import Everyone +from repoze.who.api import get_api +from repoze.who.interfaces import IChallenger +from repoze.who.middleware import PluggableAuthenticationMiddleware as PAM +from repoze.who.plugins.basicauth import BasicAuthPlugin +from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin +from repoze.who.plugins.redirector import RedirectorPlugin +from repoze.who.plugins.htpasswd import HTPasswdPlugin +from repoze.who.classifiers import default_request_classifier +from repoze.who.classifiers import default_challenge_decider +from webob import Response +from webob.exc import HTTPFound + +LINK = '

%(title)s

' + +ACTIONS = { + 'root': {'url': '/', 'title': 'Root'}, + 'protected': {'url': '/protected.html', 'title': 'Protected'}, + 'login': {'url': '/login.html', 'title': 'Login'}, + 'logout': {'url': '/logout.html', 'title': 'Logout'}, +} + +def _actions(request): + names = ['root'] + if 'REMOTE_USER' in request.environ: + names.append('protected') + names.append('logout') + else: + names.append('login') + return '\n'.join([LINK % ACTIONS[x] for x in names]) + + +PAGE = """\ + + +

%(page_title)s

+%(actions)s + + +""" + +def unprotected(request): + return Response(PAGE % {'page_title': 'Unprotected Page', + 'actions': _actions(request), + }) + +def protected(request): + return Response(PAGE % {'page_title': 'protected Page', + 'actions': _actions(request), + }) + + +LOGIN_FORM = """\ + + +

Log In

+
+ %(came_from)s +

%(message)s

+

Login name:

+

Password:

+

+
+ + +""" + +def login(request): + message = '' + info = {} + + # Remember any 'came_from', for redirection on succcesful login. + came_from = request.params.get('came_from') + if came_from is not None: + info['came_from'] = ( + '' % came_from) + else: + info['came_from'] = '' + + who_api = get_api(request.environ) + if 'form.login' in request.POST: + # Validate credentials. + creds = {} + creds['login'] = request.POST['login'] + creds['password'] = request.POST['password'] + authenticated, headers = who_api.login(creds) + + if authenticated: + # Redirect to 'came_from', or to root. + # headers here are "remember" headers, setting the + # auth_tkt cookies. + return HTTPFound(location=came_from or '/', + headers=headers) + + else: + message = 'Invalid login.' + else: + # Forcefully forget any existing credentials. + _, headers = who_api.login({}) + + # Headers here are "forget" headers, clearing the auth_tkt cookies. + request.response_headerlist = headers + + if 'REMOTE_USER' in request.environ: + del request.environ['REMOTE_USER'] + + info['message'] = message + + return Response(LOGIN_FORM % info) + + +def logout(request): + # Use repoze.who API to get "forget" headers. + who_api = get_api(request.environ) + return HTTPFound(location='/', headers=who_api.logout()) + + +class Root(object): + __acl__ = [(Allow, Authenticated, ('view_protected',)), + (Allow, Everyone, ('view',)), + DENY_ALL, + ] + +def get_root(*args, **kw): + return Root() + + +if __name__ == '__main__': + # Configure the BFG application + + ## Set up security policies, root object, etc. + authentication_policy=RemoteUserAuthenticationPolicy() + authorization_policy=ACLAuthorizationPolicy() + config = Configurator( + root_factory=get_root, + default_permission='view', + authentication_policy=authentication_policy, + authorization_policy=authorization_policy, + ) + config.begin() + + ## Configure views + config.add_view(unprotected) + config.add_view(protected, 'protected.html', permission='view_protected') + config.add_view(login, 'login.html') + config.add_view(logout, 'logout.html') + config.end() + + ## Create the app object. + app = config.make_wsgi_app() + + # Configure the repoze.who middleware: + + ## fake .htpasswd authentication source + io = StringIO() + for name, password in [('admin', 'admin'), + ('user', 'user')]: + io.write('%s:%s\n' % (name, password)) + io.seek(0) + def cleartext_check(password, hashed): + return password == hashed + htpasswd = HTPasswdPlugin(io, cleartext_check) + + ## other plugins + basicauth = BasicAuthPlugin('repoze.who') + auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt') + redirector = RedirectorPlugin(login_url='/login.html') + redirector.classifications = {IChallenger:['browser'] } # only for browser + + ## group / order plugins by function + identifiers = [('auth_tkt', auth_tkt), + ('basicauth', basicauth)] + authenticators = [('auth_tkt', auth_tkt), + ('htpasswd', htpasswd)] + challengers = [('redirector', redirector), + ('basicauth', basicauth)] + mdproviders = [] + + ## set up who logging, if desired + log_stream = None + if os.environ.get('WHO_LOG'): + log_stream = sys.stdout + + # Wrap the middleware around the application. + middleware = PAM(app, + identifiers, + authenticators, + challengers, + mdproviders, + default_request_classifier, + default_challenge_decider, + log_stream = log_stream, + log_level = logging.DEBUG + ) + + # Serve up the WSGI stack. + serve(middleware, host='0.0.0.0') diff -Nru python-repoze.who-1.0.18/docs/examples/standalone_login_no_who.py python-repoze.who-2.2/docs/examples/standalone_login_no_who.py --- python-repoze.who-1.0.18/docs/examples/standalone_login_no_who.py 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/docs/examples/standalone_login_no_who.py 2011-02-22 14:58:18.000000000 +0000 @@ -0,0 +1,97 @@ +# Standalone login application for demo SSO: +# N.B.: this version does *not* use repoze.who at all, but should produce +# a cookie which repoze.who.plugin.authtkt can use. +import datetime + +from paste.auth import auth_tkt +from webob import Request + +LOGIN_FORM_TEMPLATE = """\ + + + Demo SSO Login + + +

Demo SSO Login

+

%(message)s

+
+ +
+ + +
+
+ + +
+ +
+ + +""" + +# oh emacs python-mode, you disappoint me """ + +# Clients have to know about these values out-of-band +SECRET = 's33kr1t' +COOKIE_NAME = 'auth_cookie' + +MAX_AGE = '3600' # seconds + +AUTH = { + 'phred': 'y4bb3d4bb4d00', + 'bharney': 'b3dr0ck', +} + +def _validate(login_name, password): + # Your application's logic goes here + return AUTH.get(login_name) == password + +def _get_cookies(environ, value): + + later = (datetime.datetime.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) + tail = "; Max-Age=%s; Expires=%s" % (MAX_AGE, expires) + + cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME')) + wild_domain = '.' + cur_domain + + return [('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s' + % (COOKIE_NAME, value, wild_domain, tail)), + ] + +def login(environ, start_response): + request = Request(environ) + message = '' + if 'form.submitted' in request.POST: + came_from = request.POST['came_from'] + login_name = request.POST['login_name'] + password = request.POST['password'] + remote_addr = environ['REMOTE_ADDR'] + if _validate(login_name, password): + headers = [('Location', came_from)] + ticket = auth_tkt.AuthTicket(SECRET, login_name, remote_addr, + cookie_name=COOKIE_NAME, secure=True) + headers = _get_cookies(environ, ticket.cookie_value()) + headers.append(('Location', came_from)) + start_response('302 Found', headers) + return [] + message = 'Authentication failed' + else: + came_from = request.GET.get('came_from', '') + login_name = '' + + body = LOGIN_FORM_TEMPLATE % {'message': message, + 'came_from': came_from, + 'login_name': login_name, + } + start_response('200 OK', []) + return [body] + + +def main(global_config, **local_config): + return login diff -Nru python-repoze.who-1.0.18/docs/examples/standalone_login.py python-repoze.who-2.2/docs/examples/standalone_login.py --- python-repoze.who-1.0.18/docs/examples/standalone_login.py 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/docs/examples/standalone_login.py 2011-02-22 14:58:18.000000000 +0000 @@ -0,0 +1,118 @@ +# Login application for demo SSO: using the repoze.who API. +from repoze.who.api import APIFactory +from repoze.who.config import WhoConfig +from webob import Request + +LOGIN_FORM_TEMPLATE = """\ + + + Demo SSO Login + + +

Demo SSO Login

+

%(message)s

+
+ +
+ + +
+
+ + +
+ +
+ + +""" + +MAX_AGE = '3600' # seconds + +AUTH = { + 'phred': 'y4bb3d4bb4d00', + 'bharney': 'b3dr0ck', +} + +# This config would normally be in a separate file: inlined here for +# didactic purposes. +WHO_CONFIG = """\ +[plugin:auth_tkt] +# identification + authorization +use = repoze.who.plugins.auth_tkt:make_plugin +secret = s33kr1t +cookie_name = auth_cookie +secure = True +include_ip = True + +[general] +request_classifier = repoze.who.classifiers:default_request_classifier +challenge_decider = repoze.who.classifiers:default_challenge_decider +remote_user_key = REMOTE_USER + +[identifiers] +plugins = + auth_tkt + +[authenticators] +plugins = + auth_tkt + +[challengers] +plugins = + +[mdproviders] +plugins = +""" + +# oh emacs python-mode, you disappoint me """ + +api_factory = None + +def _configure_api_factory(): + global api_factory + if api_factory is None: + config = WhoConfig(here='/tmp') # XXX config file location + config.parse(WHO_CONFIG) + api_factory = APIFactory(identifiers=config.identifiers, + authenticators=config.authenticators, + challengers=config.challengers, + mdproviders=config.mdproviders, + request_classifier=config.request_classifier, + challenge_decider=config.challenge_decider, + ) + return api_factory + +def _validate(login_name, password): + # Your application's logic goes here + return AUTH.get(login_name) == password + +def login(environ, start_response): + request = Request(environ) + message = '' + if 'form.submitted' in request.POST: + came_from = request.POST['came_from'] + login_name = request.POST['login_name'] + password = request.POST['password'] + remote_addr = environ['REMOTE_ADDR'] + tokens = userdata = '' + if _validate(login_name, password): + api = _configure_api_factory()(environ) + headers = [('Location', came_from)] + headers.extend(api.remember(login_name)) + start_response('302 Found', headers) + return [] + message = 'Authentication failed' + else: + came_from = request.GET.get('came_from') + login_name = '' + + body = LOGIN_FORM_TEMPLATE % {'message': message, + 'came_from': came_from, + 'login_name': login_name, + } + start_response('200 OK', []) + return [body] + +def main(global_config, **local_config): + return login diff -Nru python-repoze.who-1.0.18/docs/index.rst python-repoze.who-2.2/docs/index.rst --- python-repoze.who-1.0.18/docs/index.rst 2009-11-04 21:38:47.000000000 +0000 +++ python-repoze.who-2.2/docs/index.rst 2012-03-24 21:17:18.000000000 +0000 @@ -4,7 +4,7 @@ :mod:`repoze.who` -- WSGI Authentication Middleware *************************************************** -:Author: Chris McDonough +:Author: Chris McDonough / Tres Seaver :Version: |version| .. module:: repoze.who @@ -13,7 +13,8 @@ .. topic:: Overview :mod:`repoze.who` is an identification and authentication framework - for arbitrary WSGI applications. It acts as WSGI middleware. + for arbitrary WSGI applications. It can be used as WSGI middleware, + or as an API from within a WSGI application. :mod:`repoze.who` is inspired by Zope 2's Pluggable Authentication Service (PAS) (but :mod:`repoze.who` is not dependent on Zope in any @@ -32,6 +33,11 @@ :maxdepth: 2 narr + use_cases + middleware + api + configuration + plugins Change History ============== diff -Nru python-repoze.who-1.0.18/docs/middleware.rst python-repoze.who-2.2/docs/middleware.rst --- python-repoze.who-1.0.18/docs/middleware.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/docs/middleware.rst 2011-05-13 16:43:51.000000000 +0000 @@ -0,0 +1,159 @@ +.. _using_middleware: + +Using :mod:`repoze.who` Middleware +================================== + +.. _middleware_responsibilities: + +Middleware 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. + + +.. _request_lifecycle: + +Lifecycle of a Request +---------------------- + +:mod:`repoze.who` performs duties both on middleware "ingress" and on +middleware "egress". The following graphic outlines where it sits in the context +of the request and its response: + +.. image:: .static/request-lifecycle.png + + +.. _ingress_stages: + +Request (Ingress) Stages +++++++++++++++++++++++++ + +.. image:: .static/ingress.png + +:mod:`repoze.who` performs the following operations in the following +order during middleware ingress: + +#. Environment Setup + + The middleware adds a number of keys to the WSGI environment: + + ``repoze.who.plugins`` + A reference to the configured plugin set. + + ``repoze.who.logger`` + A reference to the logger configured into the middleware. + + ``repoze.who.application`` + A refererence to the "right-hand" application. The plugins + consulted during request classification / identification / + authentication may replace this application with another + WSGI application, which will be used for the remainer of the + current request. + +#. Request Classification + + The middleware hands the WSGI environment to the configured ``classifier`` + plugin, which is responsible for classifying the request into a single + "type". This plugin must return a single string value classifying the + request, e.g., "browser", "xml-rpc", "webdav", etc. + + This classification may serve to filter out plugins consulted later in + the request. For instance, a plugin which issued a challenge as an + HTML form would be inappropriate for use in requests from an XML-RPC + or WebDAV client. + +#. Identification + + Each plugin configured as an identifier for a particular class of + request is called to extract identity data ("credentials") from the + WSGI environment. + + For example, a basic auth identifier might use + the ``HTTP_AUTHORIZATION`` header to find login and password + information. Each configured identifier plugin is consulted in turn, + and any non-None identities returned are collected into a list to be + authenticated. + + Identifiers are also responsible for providing header information used + to set and remove authentication information in the response during + egress (to "remember" or "forget" the currently-authenticated user). + +#. Authentication + + The middlware consults each plugin configured as an authenticators for + a particular class of request, to compare credentials extracted by the + identification plugins to a given policy, or set of valid credentials. + + For example, an htpasswd authenticator might look in a file for a user + record matching any of the extracted credentials. If it finds one, and + if the password listed in the record matches the password in the + identity, the userid of the user would be returned (which would + be the same as the login name). Successfully-authenticated ndenties are + "weighted", with the highest weight identity governing the remainder of + the request. + +#. Metadata Assignment + + After identifying and authenticating a user, :mod:`repoze.who` consults + plugins configured as metadata providers, which may augmented the + authenticated identity with arbitrary metadata. + + For example, a metadata provider plugin might add the user's first, + middle and last names to the identity. A more specialized metadata + provider might augment the identity with a list of role or group names + assigned to the user. + + +.. _egress_stages: + +Response (Egress) Stages +++++++++++++++++++++++++ + +:mod:`repoze.who` performs the following operations in the following +order during middleware egress: + +#. Challenge Decision + + The middleare examines the WSGI environment and the status and headers + returned by the downstream application 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 can be replaced by configuring a different + ``challenge_decider`` plugin for the middleware. + + If a challenge is required, the challenge decider returns True; otherwise, + it returns False. + +#. Credentials reset, AKA "forgetting" + + If the challenge decider returns True, the middleware first delegates + to the identifier plugin which provided the currently-authenticated + identity to "forget" the identity, by adding response headers (e.g., to + expire a cookie). + +#. Challenge + + The plugin then consults each of the set of plugins configured as + challengers for the current request classification: the first plugin + which returns a non-None WSGI application will be used perform a + challenge. + + Challenger plugins may use application-returned headers, the WSGI + environment, and other items to determine what sort of operation + should be performed to actuate the challenge. + +#. 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. diff -Nru python-repoze.who-1.0.18/docs/narr.rst python-repoze.who-2.2/docs/narr.rst --- python-repoze.who-1.0.18/docs/narr.rst 2009-11-04 21:38:47.000000000 +0000 +++ python-repoze.who-2.2/docs/narr.rst 2011-02-22 14:58:18.000000000 +0000 @@ -1,1152 +1,54 @@ -Middleware Responsibilities -=========================== +:mod:`repoze.who` Narrative Documentation +========================================= -: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. +Using :mod:`repoze.who` as WSGI Middleware +------------------------------------------ -: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. +:mod:`repoze.who` was originally developed for use as authentication +middleware in a WSGI pipeline, for use by applications which only +needed to obtain an "authenticated user" to enforce a given security +policy. -Configuration Points -==================== +See :ref:`middleware_responsibilities` for a description of this use case. -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. +Using :mod:`repoze.who` without WSGI Middleware +----------------------------------------------- -You may extend the classification system by making :mod:`repoze.who` aware -of a different request classifier implementation. +Some applications might want to use a configured set of +:mod:`repoze.who` plugins to do identification and authentication for +a request, outside the context of using :mod:`repoze.who` middleware. +For example, a performance-sensitive application might wish to defer +the effort of identifying and authenticating a user until the point at +which authorization is required, knowing that some code paths will not +need to do the work. -Challenge Deciders ------------------- +See :ref:`api_narrative` for a description of this use case. -: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. +Mixing Middleware and API Uses +------------------------------ -You may supply a different challenge decider as necessary. +Some applications might use the :mod:`repoze.who` middleware for most +authentication purposes, but need to participate more directly in the +mechanics of identification and authorization for some portions of the +application. For example, consider a system which allows users to +sign up online for membrship in a site: once the user completes +registration, such an application might wish to log the user in +transparently, and thus needs to interact with the configured +:mod:`repoze.who` middleware to generate response headers, ensuring +that the user's next request is properly authenticated. -Plugins -------- +See :ref:`middleware_api_hybrid` for a description of this use case. -: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. - """ +Configuring :mod:`repoze.who` +----------------------------- +Developers and integrators can configure :mod:`repoze.who` using either +imperative Python code (see :ref:`imperative_configuration`) or using an +INI-style declarative configuration file (see :ref:`declarative_configuration`). +In either case, the result of the configuration will be a +:class:`repoze.who.api:APIFactory` instance, complete with a request +classifier, a challenge decider, and a set of plugins for each plugin +interface. diff -Nru python-repoze.who-1.0.18/docs/plugins.rst python-repoze.who-2.2/docs/plugins.rst --- python-repoze.who-1.0.18/docs/plugins.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/docs/plugins.rst 2012-11-05 22:31:00.000000000 +0000 @@ -0,0 +1,634 @@ +.. _about_plugins: + +About :mod:`repoze.who` Plugins +=============================== + +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. + + +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_plugins: + +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 [, cookie_name='auth_tkt' [, secure=False [, include_ip=False]]]) + + An :class:`AuthTktCookiePlugin` is an ``IIdentifier`` and ``IAuthenticator`` + 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. + + Normally, using the plugin as an identifier requires also using it as + an authenticator. + +.. 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.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.redirector + +.. class:: RedirectorPlugin(login_url, came_from_param, reason_param, reason_header) + + A :class:`RedirectorPlugin` is an ``IChallenger`` plugin. + It redirects to a configured login URL at egress if a challenge is + required . + *login_url* is the URL that should be redirected to when a + challenge is required. *came_from_param* is the name of an optional + query string parameter: if configured, the plugin provides the current + request URL in the redirected URL's query string, using the supplied + parameter name. *reason_param* is the name of an optional + query string parameter: if configured, and the application supplies + a header matching *reason_header* (defaulting to + ``X-Authorization-Failure-Reason``), the plugin includes that reason in + the query string of the redirected URL, using the supplied parameter name. + *reason_header* is an optional parameter overriding the default response + header name (``X-Authorization-Failure-Reason``) which + the plugin checks to find the application-supplied reason for the challenge. + *reason_header* cannot be set unless *reason_param* is also set. + +.. 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. + + +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): + from paste.request import get_cookies + 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. + + +.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``. + + +Known Plugins for :mod:`repoze.who` +=================================== + + +Plugins shipped with :mod:`repoze.who` +-------------------------------------- + +See :ref:`default_plugins`. + + +Deprecated plugins +------------------ + +The :mod:`repoze.who.deprecatedplugins` distribution bundles the following +plugin implementations which were shipped with :mod:`repoze.who` prior +to version 2.0a3. These plugins are deprecated, and should only be used +while migrating an existing deployment to replacement versions. + +:class:`repoze.who.plugins.cookie.InsecureCookiePlugin` + An ``IIdentifier`` plugin which stores identification information in an + insecure form (the base64 value of the username and password separated by + a colon) in a client-side cookie. Please use the + :class:`AuthTktCookiePlugin` instead. + +:class:`repoze.who.plugins.form.FormPlugin` + + An ``IIdentifier`` and ``IChallenger`` plugin, which intercepts form POSTs + to gather identification at ingress and conditionally displays a login form + at egress if challenge is required. + + Applications should supply their + own login form, and use :class:`repoze.who.api.API` to authenticate + and remember users. To replace the challenger role, please use + :class:`repoze.who.plugins.redirector.RedirectorPlugin`, configured with + the URL of your application's login form. + +:class:`repoze.who.plugins.form.RedirectingFormPlugin` + + An ``IIdentifier`` and ``IChallenger`` plugin, which intercepts form POSTs + to gather identification at ingress and conditionally redirects a login form + at egress if challenge is required. + + Applications should supply their + own login form, and use :class:`repoze.who.api.API` to authenticate + and remember users. To replace the challenger role, please use + :class:`repoze.who.plugins.redirector.RedirectorPlugin`, configured with + the URL of your application's login form. + + +Third-party Plugins +------------------- + +:class:`repoze.who.plugins.zodb.ZODBPlugin` + This class implements the :class:`repoze.who.interfaces.IAuthenticator` + and :class:`repoze.who.interfaces.IMetadataProvider` plugin interfaces + using ZODB database lookups. See + http://pypi.python.org/pypi/repoze.whoplugins.zodb/ + +:class:`repoze.who.plugins.ldap.LDAPAuthenticatorPlugin` + This class implements the :class:`repoze.who.interfaces.IAuthenticator` + plugin interface using the :mod:`python-ldap` library to query an LDAP + database. See http://code.gustavonarea.net/repoze.who.plugins.ldap/ + +:class:`repoze.who.plugins.ldap.LDAPAttributesPlugin` + This class implements the :class:`repoze.who.interfaces.IMetadataProvider` + plugin interface using the :mod:`python-ldap` library to query an LDAP + database. See http://code.gustavonarea.net/repoze.who.plugins.ldap/ + +:class:`repoze.who.plugins.friendlyform.FriendlyFormPlugin` + This class implements the :class:`repoze.who.interfaces.IIdentifier` and + :class:`repoze.who.interfaces.IChallenger` plugin interfaces. It is + similar to :class:`repoze.who.plugins.form.RedirectingFormPlugin`, + bt with with additional features: + + - Users are not challenged on logout, unless the referrer URL is a + private one (but that’s up to the application). + + - Developers may define post-login and/or post-logout pages. + + - In the login URL, the amount of failed logins is available in the + environ. It’s also increased by one on every login try. This counter + will allow developers not using a post-login page to handle logins that + fail/succeed. + + See http://code.gustavonarea.net/repoze.who-friendlyform/ + +:func:`repoze.who.plugins.openid.identifiers.OpenIdIdentificationPlugin` + This class implements the :class:`repoze.who.interfaces.IIdentifier`, + :class:`repoze.who.interfaces.IAuthenticator`, and + :class:`repoze.who.interfaces.IChallenger` plugin interfaces using OpenId. + See http://quantumcore.org/docs/repoze.who.plugins.openid/ + +:func:`repoze.who.plugins.openid.classifiers.openid_challenge_decider` + This function provides the :class:`repoze.who.interfaces.IChallengeDecider` + interface using OpenId. See + http://quantumcore.org/docs/repoze.who.plugins.openid/ + +:class:`repoze.who.plugins.use_beaker.UseBeakerPlugin` + This packkage provids a :class:`repoze.who.interfaces.IIdentifier` plugin + using :mod:`beaker.session` cache. See + http://pypi.python.org/pypi/repoze.who-use_beaker/ + +:class:`repoze.who.plugins.cas.main_plugin.CASChallengePlugin` + This class implements the :class:`repoze.who.interfaces.IIdentifier` + :class:`repoze.who.interfaces.IAuthenticator`, and + :class:`repoze.who.interfaces.IChallenger` plugin interfaces using CAS. + See http://pypi.python.org/pypi/repoze.who.plugins.cas + +:class:`repoze.who.plugins.cas.challenge_decider.my_challenge_decider` + This function provides the :class:`repoze.who.interfaces.IChallengeDecider` + interface using CAS. See + http://pypi.python.org/pypi/repoze.who.plugins.cas/ + +:class:`repoze.who.plugins.recaptcha.captcha.RecaptchaPlugin` + This class implements the :class:`repoze.who.interfaces.IAuthenticator` + plugin interface, using the recaptch API. + See http://pypi.python.org/pypi/repoze.who.plugins.recaptcha/ + +:class:`repoze.who.plugins.sa.SQLAlchemyUserChecker` + User existence checker for + :class:`repoze.who.plugins.auth_tkt.AuthTktCookiePlugin`, based on + the SQLAlchemy ORM. See http://pypi.python.org/pypi/repoze.who.plugins.sa/ + +:class:`repoze.who.plugins.sa.SQLAlchemyAuthenticatorPlugin` + This class implements the :class:`repoze.who.interfaces.IAuthenticator` + plugin interface, using the the SQLAlchemy ORM. + See http://pypi.python.org/pypi/repoze.who.plugins.sa/ + +:class:`repoze.who.plugins.sa.SQLAlchemyUserMDPlugin` + This class implements the :class:`repoze.who.interfaces.IMetadataProvider` + plugin interface, using the the SQLAlchemy ORM. + See http://pypi.python.org/pypi/repoze.who.plugins.sa/ + +:class:`repoze.who.plugins.formcookie.CookieRedirectingFormPlugin` + This class implements the :class:`repoze.who.interfaces.IIdentifier` and + :class:`repoze.who.interfaces.IChallenger` plugin interfaces, similar + to :class:`repoze.who.plugins.form.RedirectingFormPlugin`. The + plugin tracks the ``came_from`` URL via a cookie, rather than the query + string. See http://pypi.python.org/pypi/repoze.who.plugins.formcookie/ Binary files /tmp/tmpQFE8JS/44sw4xWb8J/python-repoze.who-1.0.18/docs/.static/ingress.png and /tmp/tmpQFE8JS/GxJw2cwJXv/python-repoze.who-2.2/docs/.static/ingress.png differ diff -Nru python-repoze.who-1.0.18/docs/.static/repoze.css python-repoze.who-2.2/docs/.static/repoze.css --- python-repoze.who-1.0.18/docs/.static/repoze.css 2009-01-13 22:36:00.000000000 +0000 +++ python-repoze.who-2.2/docs/.static/repoze.css 2012-03-24 21:17:18.000000000 +0000 @@ -19,3 +19,11 @@ div.related a { color: #dad3bd !important; } + +ol li { + color: Black !important; +} + +ul.simple li { + color: Black !important; +} Binary files /tmp/tmpQFE8JS/44sw4xWb8J/python-repoze.who-1.0.18/docs/.static/request-lifecycle.png and /tmp/tmpQFE8JS/GxJw2cwJXv/python-repoze.who-2.2/docs/.static/request-lifecycle.png differ diff -Nru python-repoze.who-1.0.18/docs/use_cases.rst python-repoze.who-2.2/docs/use_cases.rst --- python-repoze.who-1.0.18/docs/use_cases.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/docs/use_cases.rst 2011-02-22 14:58:18.000000000 +0000 @@ -0,0 +1,156 @@ +:mod:`repoze.who` Use Cases +=========================== + +How should an application interact with :mod:`repoze.who`? There are three +main scenarios: + +Middleware-Only Use Cases +------------------------- + +Examples of using the :mod:`repoze.who` middleware, without explicitly +using its API. + + +Simple: Bug Tracker with ``REMOTE_USER`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This application expects the ``REMOTE_USER`` variable to be set by +the middleware for authenticated requests. It allows the middleware to +handle challenging the user when needed. + +In protected views, such as those which allow creating or following up +to bug reports: + +- Check ``environ['REMOTE_USER']`` to get the authenticated user, and apply + any application-specific policy (who is allowed to edit). + + - If the access check fails because the user is not yet authenticated, + return an 401 Unauthorized response. + + - If the access check fails for authenticated users, return a + 403 Forbidden response. + +Note that the application here doesn't depend on :mod:`repoze.who` at +all: it would work identically if run behind Apache's ``mod_auth``. The +``Trac`` application works exactly this way. + +The middleware can be configured to suit the policy required for the +site, e.g.: + +- challenge / identify using HTTP basic authentication + +- authorize via an ``.htaccces``-style file. + + +More complex: Wiki with ``repoze.who.identity`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This application use the ``repoze.who.identity`` variable set in the +WSGI environment by the middleware for authenticated requests. The application +still allows the middleware to handle challenging the user when needed. + +The only difference from the previous example is that protected views, +such as those which allow adding or editing wiki pages, can use the extra +metadata stored inside ``environ['repoze.who.identity']`` (a mapping) to +make authorization decisions: such metadata might include groups or roles +mapped by the middleware onto the user. + + +API-Only Use Cases +------------------ + +Examples of using the :mod:`repoze.who` API without its middleware. + + +Simple: Wiki with its own login and logout views. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This application uses the :mod:`repoze.who` API to compute the authenticated +user, as well as using its ``remember`` API to set headers for cookie-based +authentication. + +In each view: + +- Call ``api.authenticate`` to get the authenticated user. + +- Show a ``login`` link for non-authenticated requests. + +- Show a ``logout`` link for authenticated requests. + +- Don't show "protected" links for non-authenticated requests. + +In protected views, such as those which allow adding or editing +wiki pages: + +- Call ``api.authenticate`` to get the authenticated user; check + the metadata about the user (e.g., any appropriate roles or groups) + to verify access. + + - If the access check fails because the user is not yet authenticated, + redirect to the ``login`` view, with a ``came_from`` value of the + current URL. + + - If the access check fails for authenticated users, return a + 403 Forbidden response. + +In the login view: + +- For ``GET`` requests, show the login form. + +- For ``POST`` requests, validate the login and password from the form. + If successful, call ``api.remember``, and append the returned headers to + your response, which may also contain, e.g., a ``Location`` header for + a redirect to the ``came_from`` URL. In this case, there will be + no authenticator plugin which knows about the login / password at all. + +In the logout view: + +- Call ``api.forget`` and append the headers to your response, which may + also contain, e.g., a ``Location`` header for a redirect to the + ``came_from`` URL after logging out. + + +More complex: multiple applications with "single sign-on" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this scenario, authentication is "federated" across multiple applications, +which delegate to a central "login application." This application verifies +credentials from the user, and then uses headers or other tokens to +communicate the verified identity to the delegating application. + +In the login application: + +- The SSO login application works just like the login view described above: + the difference is that the configured identifier plugins must emit + headers from ``remember`` which can be recognized by their counterparts + in the other apps. + +In the non-login applications: + +- Challenge plugins here must be configured to implement the specific + SSO protocol, e.g. redirect to the login app with information in the + query string (other protocols might differ). + +- Identifer plugins must be able to "crack" / consume whatever tokens are + returned by the SSO login app. + +- Authenticators will normally be no-ops (e.g., the ``auth_tkt`` plugin + used as an authenticator). + +Hybrid Use Cases +---------------- + +Examples of using the :mod:`repoze.who` API in conjuntion with its middleware. + +Most complex: integrate Trac and the wiki behind SSO +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This example extends the previous one, but adds into the mix the +requirement that one or more of the non-login applications (e.g., Trac) +be used "off the shelf," without modifying them. Such applications can +be plugged into the same SSO regime, with the addition of the +:mod:``repoze.who`` middleware as an adapter to bridge the gap (e.g., +to turn the SSO tokens into the ``REMOTE_USER`` required by Trac). + +In this scenario, the middleware would be configured identically to the +API used in applications which do not need the middleware shim. diff -Nru python-repoze.who-1.0.18/ez_setup.py python-repoze.who-2.2/ez_setup.py --- python-repoze.who-1.0.18/ez_setup.py 2009-02-13 12:50:18.000000000 +0000 +++ python-repoze.who-2.2/ez_setup.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,272 +0,0 @@ -#!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:]) - - - - - diff -Nru python-repoze.who-1.0.18/.gitignore python-repoze.who-2.2/.gitignore --- python-repoze.who-1.0.18/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/.gitignore 2013-04-05 16:05:38.000000000 +0000 @@ -0,0 +1,8 @@ +*.pyc +*.egg-info +.coverage +docs/.build +.tox +coverage.xml +nosetests.xml +docs/_build diff -Nru python-repoze.who-1.0.18/PKG-INFO python-repoze.who-2.2/PKG-INFO --- python-repoze.who-1.0.18/PKG-INFO 2009-11-05 21:36:11.000000000 +0000 +++ python-repoze.who-2.2/PKG-INFO 2013-05-18 16:07:39.000000000 +0000 @@ -1,17 +1,20 @@ Metadata-Version: 1.0 Name: repoze.who -Version: 1.0.18 +Version: 2.2 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 - *************************************************** +Description: ``repoze.who`` -- WSGI Authentication Middleware / API + ====================================================== + + Overview + -------- ``repoze.who`` is an identification and authentication framework - for arbitrary WSGI applications. It acts as WSGI middleware. + for arbitrary WSGI applications. ``repoze.who`` can be configured + either as WSGI middleware or as an API for use by an application. ``repoze.who`` is inspired by Zope 2's Pluggable Authentication Service (PAS) (but ``repoze.who`` is not dependent on Zope in any @@ -19,59 +22,273 @@ 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 ==================== + 2.2 (2013-05-17) + ---------------- + + - Parse INI-file configuration using ``SafeConfigParser``: allows + escaping the ``'%'`` so that e.g. a query template using for a DB-API + connection using ``pyformat`` preserves the template. + + - Added support for Python 3.3, PyPy. + + + 2.1 (2013-03-20) + ---------------- + + - ``_compat`` module: tolerate missing ``CONTENT_TYPE`` key in the WSGI + environment. Thanks to Dag Hoidal for the patch. + + - ``htpasswd`` plugin: add a ``sha1_check`` checker function (the ``crypt`` + module is not available on Windows). Thanks to Chandrashekar Jayaraman + for the patch. + + - Documentation typo fixes from Carlos de la Guardia and Atsushi Odagiri. + + + 2.1b1 (2012-11-05) + ------------------ + + - Ported to Py3k using the "compatible subset" mode. + - Dropped support for Python < 2.6.x. + - Dropped dependency on Paste (forking some code from it). + - Added dependency on WebOb instead. + Thanks to Atsushi Odagiri (aodag) for the initial effort. + + + 2.0 (2011-09-28) + ---------------- + + - ``auth_tkt`` plugin: strip any port number from the 'Domain' of generated + cookies. http://bugs.repoze.org/issue66 + + - Further harden middleware, calling ``close()`` on the iterable even if + raising an exception for a missing challenger. + http://bugs.repoze.org/issue174 + + + 2.0b1 (2011-05-24) + ------------------ + + - Enabled standard use of logging module's configuration mechanism. + See http://docs.python.org/dev/howto/logging.html#configuring-logging-for-a-library + Thanks to jgoldsmith for the patch: http://bugs.repoze.org/issue178 + + + - ``repoze.who.plugins.htpasswd``: defend against timing-based attacks. + + + 2.0a4 (2011-02-02) + ------------------ + + - Ensure that the middleware calls ``close()`` (if it exists) on the + iterable returned from thw wrapped application, as required by PEP 333. + http://bugs.repoze.org/issue174 + + - Make ``make_api_factory_with_config`` tolerant of invalid filenames / + content for the config file: in such cases, the API factory will have + *no* configured plugins or policies: it will only be useful for retrieving + the API from an environment populated by middleware. + + - Fix bug in ``repoze.who.api`` where the ``remember()`` or ``forget()`` + methods could return a None if the identifier plugin returned a None. + + - Fix ``auth_tkt`` plugin to not hand over tokens as strings to paste. See + http://lists.repoze.org/pipermail/repoze-dev/2010-November/003680.html + + - Fix ``auth_tkt`` plugin to add "secure" and "HttpOnly" to cookies when + configured with ``secure=True``: these attributes prevent the browser from + sending cookies over insecure channels, which could be vulnerable to some + XSS attacks. + + - Avoid propagating unicode 'max_age' value into cookie headers. See + https://bugs.launchpad.net/bugs/674123 . + + - Added a single-file example BFG application demonstrating the use of + the new 'login' and 'logout' methods of the API object. + + - Add ``login`` and ``logout`` methods to the ``repoze.who.api.API`` object, + as a convenience for application-driven login / logout code, which would + otherwise need to use private methods of the API, and reach down into + its plugins. + + + 2.0a3 (2010-09-30) + ------------------ + + - Deprecated the following plugins, moving their modules, tests, and docs + to a new project, ``repoze.who.deprecatedplugins``: + + - ``repoze.who.plugins.cookie.InsecureCookiePlugin`` + + - ``repoze.who.plugins.form.FormPlugin`` + + - ``repoze.who.plugins.form.RedirectingFormPlugin`` + + - Made the ``repoze.who.plugins.cookie.InsecureCookiePlugin`` take a + ``charset`` argument, and use to to encode / decode login and password. + See http://bugs.repoze.org/issue155 + + - Updated ``repoze.who.restrict`` to return headers as a list, to keep + ``wsgiref`` from complaining. + + - Helped default request classifier cope with xml submissions with an + explicit charset defined: http://bugs.repoze.org/issue145 (Lorenzo + M. Catucci) + + - Corrected the handling of type and subtype when matching an XML post + to ``xmlpost`` in the default classifier, which, according to RFC + 2045, must be matched case-insensitively: + http://bugs.repoze.org/issue145 (Lorenzo M. Catucci) + + - Added ``repoze.who.config:make_api_factory_with_config``, a convenience + method for applications which want to set up their own API Factory from + a configuration file. + + - Fixed example call to ``repoze.who.config:make_middleware_with_config`` + (added missing ``global_config`` argument). See + http://bugs.repoze.org/issue114 + + + 2.0a2 (2010-03-25) + ------------------ + + Bugs Fixed + ~~~~~~~~~~ + + - Fixed failure to pass substution values in log message string formatting + for ``repoze.who.api:API.challenge``. Fix included adding tests for all + logging done by the API object. See http://bugs.repoze.org/issue122 + + Backward Incompatibilities + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + - Adjusted logging level for some lower-level details from ``info`` + to ``debug``. + + + + 2.0a1 (2010-02-24) + ------------------ + + Features + ~~~~~~~~ + + - Restored the ability to create the middleware using the old ``classifier`` + argument. That argument is now a deprecated-but-will-work-forever alias for + ``request_classifier``. + + - The ``auth_tkt`` plugin now implements the ``IAuthenticator`` interface, + and should normally be used both as an ``IIdentifier`` and an + ``IAuthenticator``. + + - Factored out the API of the middleware object to make it useful from + within the application. Applications using ``repoze.who``` now fall into + one of three catgeories: + + - "middleware-only" applications are configured with middleware, and + use either ``REMOTE_USER`` or ``repoze.who.identity`` from the environment + to determing the authenticated user. + + - "bare metal" applications use no ``repoze.who`` middleware at all: + instead, they configure and an ``APIFactory`` object at startup, and + use it to create an ``API`` object when needed on a per-request basis. + + - "hybrid" applications are configured with ``repoze.who`` middleware, + but use a new library function to fetch the ``API`` object from the + environ, e.g. to permit calling ``remember`` after a signup or successful + login. + + Bugs Fixed + ~~~~~~~~~~ + + - Fix http://bugs.repoze.org/issue102: when no challengers existed, + logging would cause an exception. + + - Remove ``ez_setup.py`` and dependency on it in setup.py (support + distribute). + + Backward Incompatibilities + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + - The middleware used to allow identifier plugins to "pre-authenticate" + an identity. This feature is no longer supported: the ``auth_tkt`` + plugin, which used to use the feature, is now configured to work as + an authenticator plugin (as well as an identifier). + + - The ``repoze.who.middleware:PluggableAuthenticationMiddleware`` class + no longer has the following (non-API) methods (now made API methods + of the ``repoze.who.api:API`` class): + + - ``add_metadata`` + - ``authenticate`` + - ``challenge`` + - ``identify`` + + - The following (non-API) functions moved from ``repoze.who.middleware`` to + ``repoze.who.api``: + + - ``make_registries`` + - ``match_classification`` + - ``verify`` + + + 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. + 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). + 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. + 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:: + ini configuration parser:: - TypeError: _makePlugin() got multiple values for keyword argument 'name' + 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. + 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. + 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. + 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) @@ -80,322 +297,322 @@ - 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 . + 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. + 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. + identifier plugin, courtesty of Paul Johnston. - Add a ``userid_checker`` argument to the auth_tkt identifier plugin, - courtesty of Gustavo Narea. + 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". + 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). + 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. + 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. + 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 . + 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). + 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. + 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). + 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. + ``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. + ``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. + ``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. + 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. + 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. + 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``. + 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. + cause problems under mod_wsgi. - Allowed 'cookie_path' argument to InsecureCookiePlugin (and config - constructor). Thanks to Gustavo Narea. + 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. + 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. + 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". + 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 + (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). + 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. + 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'. + 'classifiers.default_request_classifier' and + 'classifiers.default_password_compare'. - Added actual config-driven middleware factory, - 'config.make_middleware_with_config' + '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: + 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 + - "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. + ids. 0.9 (2008-04-01) ---------------- - Fix bug introduced in FormPlugin in 0.8 release (rememberer headers - not set). + 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. + "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. + "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. + 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. + 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. + 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. + 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) @@ -412,63 +629,63 @@ ---------------- - Allow "remote user key" (default: REMOTE_USER) to be overridden - (pass in remote_user_key to middleware constructor). + (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). + 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. + 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. + 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. + '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. + 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. + 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). + 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. + sha instead of base64'ed binary sha for simpler conversion. 0.2 (03-04-2008) @@ -485,7 +702,14 @@ Keywords: web application server wsgi zope Platform: UNKNOWN Classifier: Intended Audience :: Developers -Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Internet :: WWW/HTTP :: WSGI diff -Nru python-repoze.who-1.0.18/README.rst python-repoze.who-2.2/README.rst --- python-repoze.who-1.0.18/README.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/README.rst 2012-03-24 21:17:18.000000000 +0000 @@ -0,0 +1,20 @@ +``repoze.who`` -- WSGI Authentication Middleware / API +====================================================== + +Overview +-------- + +``repoze.who`` is an identification and authentication framework +for arbitrary WSGI applications. ``repoze.who`` can be configured +either as WSGI middleware or as an API for use by an application. + +``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. diff -Nru python-repoze.who-1.0.18/README.txt python-repoze.who-2.2/README.txt --- python-repoze.who-1.0.18/README.txt 2009-11-04 21:51:17.000000000 +0000 +++ python-repoze.who-2.2/README.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,18 +0,0 @@ -*************************************************** -``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. - diff -Nru python-repoze.who-1.0.18/repoze/who/api.py python-repoze.who-2.2/repoze/who/api.py --- python-repoze.who-1.0.18/repoze/who/api.py 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/api.py 2012-03-19 19:42:14.000000000 +0000 @@ -0,0 +1,374 @@ +from zope.interface import implementer + +from repoze.who.interfaces import IAPI +from repoze.who.interfaces import IAPIFactory +from repoze.who.interfaces import IIdentifier +from repoze.who.interfaces import IAuthenticator +from repoze.who.interfaces import IChallenger +from repoze.who.interfaces import IMetadataProvider + + +def get_api(environ): + return environ.get('repoze.who.api') + + +@implementer(IAPIFactory) +class APIFactory(object): + + def __init__(self, + identifiers=(), + authenticators=(), + challengers=(), + mdproviders=(), + request_classifier=None, + challenge_decider=None, + remote_user_key = 'REMOTE_USER', + logger=None, + ): + self.identifiers = identifiers + self.authenticators = authenticators + self.challengers = challengers + self.mdproviders = mdproviders + self.request_classifier = request_classifier + self.challenge_decider = challenge_decider + self.remote_user_key = remote_user_key + self.logger = logger + + def __call__(self, environ): + """ See IAPIFactory. + """ + api = environ.get('repoze.who.api') + if api is None: + api = environ['repoze.who.api'] = API(environ, + self.identifiers, + self.authenticators, + self.challengers, + self.mdproviders, + self.request_classifier, + self.challenge_decider, + self.remote_user_key, + self.logger, + ) + return api + + +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 as 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 + + +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 + + +@implementer(IAPI) +class API(object): + + def __init__(self, + environ, + identifiers, + authenticators, + challengers, + mdproviders, + request_classifier, + challenge_decider, + remote_user_key, + logger, + ): + self.environ = environ + (self.interface_registry, + self.name_registry) = make_registries(identifiers, authenticators, + challengers, mdproviders) + self.identifiers = identifiers + self.authenticators = authenticators + self.challengers = challengers + self.mdproviders = mdproviders + self.challenge_decider = challenge_decider + self.remote_user_key = remote_user_key + self.logger = logger + classification = self.classification = (request_classifier and + request_classifier(environ)) + logger and logger.info('request classification: %s' % classification) + + def authenticate(self): + + ids = self._identify() + + # ids will be list of tuples: [ (IIdentifier, identity) ] + if ids: + auth_ids = self._authenticate(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 + identity['authenticator'] = authenticator + identity['identifier'] = identifier + + # allow IMetadataProvider plugins to scribble on the identity + self._add_metadata(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' + self.environ['repoze.who.identity'] = identity + # set the REMOTE_USER + self.environ[self.remote_user_key] = userid + return identity + + self.logger and self.logger.info( + 'no identities found, not authenticating') + + def challenge(self, status='403 Forbidden', app_headers=()): + """ See IAPI. + """ + identity = self.environ.get('repoze.who.identity', {}) + identifier = identity.get('identifier') + + logger = self.logger + + forget_headers = [] + + if identifier: + id_forget_headers = identifier.forget(self.environ, identity) + if id_forget_headers is not None: + forget_headers.extend(id_forget_headers) + logger and logger.info('forgetting via headers from %s: %s' + % (identifier, forget_headers)) + + candidates = self.interface_registry.get(IChallenger, ()) + logger and logger.debug('challengers registered: %s' % repr(candidates)) + plugins = match_classification(IChallenger, candidates, + self.classification) + logger and logger.debug('challengers matched for ' + 'classification "%s": %s' + % (self.classification, plugins)) + for plugin in plugins: + app = plugin.challenge(self.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 remember(self, identity=None): + """ See IAPI. + """ + headers = () + if identity is None: + identity = self.environ.get('repoze.who.identity', {}) + identifier = identity.get('identifier') + if identifier: + got_headers = identifier.remember(self.environ, identity) + if got_headers: + headers = got_headers + logger = self.logger + logger and logger.info('remembering via headers from %s: %s' + % (identifier, headers)) + return headers + + def forget(self, identity=None): + """ See IAPI. + """ + headers = () + if identity is None: + identity = self.environ.get('repoze.who.identity', {}) + identifier = identity.get('identifier') + if identifier: + got_headers = identifier.forget(self.environ, identity) + if got_headers: + headers = got_headers + logger = self.logger + logger and logger.info('forgetting via headers from %s: %s' + % (identifier, headers)) + return headers + + def login(self, credentials, identifier_name=None): + """ See IAPI. + """ + authenticated = identity = plugin = None + headers = [] + + # Filter identifiers using 'identifier_name', if provided. + if identifier_name is not None: + identifiers = [(name, plugin) for name, plugin in self.identifiers + if name == identifier_name] + else: + identifiers = self.identifiers + + # First pass: for each identifier, pretend that it was the source + # of the credentials, and try to authenticate. + for name, identifier in identifiers: + authenticated = self._authenticate([(identifier, credentials)]) + + if authenticated: # and therefore can remember it + rank, plugin, identifier, identity, userid = authenticated[0] + break + + # Second pass to allow identifiers which passed on auth to participate + # in remember / forget. + for name, identifier in identifiers: + if identity is not None: + i_headers = identifier.remember(self.environ, identity) + else: + i_headers = identifier.forget(self.environ, None) + if i_headers is not None: + headers.extend(i_headers) + + return identity, headers + + def logout(self, identifier_name=None): + """ See IAPI. + """ + authenticated = None + headers = [] + # Filter identifiers using 'identifier_name', if provided. + if identifier_name is not None: + identifiers = [(name, plugin) for name, plugin in self.identifiers + if name == identifier_name] + else: + identifiers = self.identifiers + + for name, identifier in identifiers: + headers.extend(identifier.forget(self.environ, None)) + + # we need to remove the identity for hybrid middleware/api usages to + # work correctly: middleware calls ``remember`` unconditionally "on + # the way out", and if an identity is found, competing login headers + # will be set. + if 'repoze.who.identity' in self.environ: + del self.environ['repoze.who.identity'] + + return headers + + def _identify(self): + """ See IAPI. + """ + logger = self.logger + candidates = self.interface_registry.get(IIdentifier, ()) + logger and self.logger.debug('identifier plugins registered: %s' % + (candidates,)) + plugins = match_classification(IIdentifier, candidates, + self.classification) + logger and self.logger.debug( + 'identifier plugins matched for ' + 'classification "%s": %s' % (self.classification, plugins)) + + results = [] + for plugin in plugins: + identity = plugin.identify(self.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 _authenticate(self, identities): + """ See IAPI. + """ + logger = self.logger + candidates = self.interface_registry.get(IAuthenticator, []) + logger and self.logger.debug('authenticator plugins registered: %s' % + candidates) + plugins = match_classification(IAuthenticator, candidates, + self.classification) + logger and self.logger.debug( + 'authenticator plugins matched for ' + 'classification "%s": %s' % (self.classification, plugins)) + + auth_rank = 0 + results = [] + + for plugin in plugins: + identifier_rank = 0 + for identifier, identity in identities: + userid = plugin.authenticate(self.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 _add_metadata(self, identity): + """ See IAPI. + """ + candidates = self.interface_registry.get(IMetadataProvider, ()) + plugins = match_classification(IMetadataProvider, candidates, + self.classification) + for plugin in plugins: + plugin.add_metadata(self.environ, identity) + +class Identity(dict): + """ dict subclass: prevent members from being rendered during print + """ + def __repr__(self): + return '' % id(self) + __str__ = __repr__ diff -Nru python-repoze.who-1.0.18/repoze/who/_auth_tkt.py python-repoze.who-2.2/repoze/who/_auth_tkt.py --- python-repoze.who-1.0.18/repoze/who/_auth_tkt.py 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/_auth_tkt.py 2012-03-19 19:42:14.000000000 +0000 @@ -0,0 +1,208 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +########################################################################## +# +# Copyright (c) 2005 Imaginary Landscape LLC and Contributors. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +########################################################################## +""" +Implementation of cookie signing as done in `mod_auth_tkt +`_. + +mod_auth_tkt is an Apache module that looks for these signed cookies +and sets ``REMOTE_USER``, ``REMOTE_USER_TOKENS`` (a comma-separated +list of groups) and ``REMOTE_USER_DATA`` (arbitrary string data). + +This module is an alternative to the ``paste.auth.cookie`` module; +it's primary benefit is compatibility with mod_auth_tkt, which in turn +makes it possible to use the same authentication process with +non-Python code run under Apache. +""" +from hashlib import md5 +import time as time_mod + +from repoze.who._compat import encodestring +from repoze.who._compat import SimpleCookie +from repoze.who._compat import url_quote +from repoze.who._compat import url_unquote + + +class AuthTicket(object): + + """ + This class represents an authentication token. You must pass in + the shared secret, the userid, and the IP address. Optionally you + can include tokens (a list of strings, representing role names), + 'user_data', which is arbitrary data available for your own use in + later scripts. Lastly, you can override the cookie name and + timestamp. + + Once you provide all the arguments, use .cookie_value() to + generate the appropriate authentication ticket. .cookie() + generates a Cookie object, the str() of which is the complete + cookie header to be sent. + + CGI usage:: + + token = auth_tkt.AuthTick('sharedsecret', 'username', + os.environ['REMOTE_ADDR'], tokens=['admin']) + print 'Status: 200 OK' + print 'Content-type: text/html' + print token.cookie() + print + ... redirect HTML ... + + Webware usage:: + + token = auth_tkt.AuthTick('sharedsecret', 'username', + self.request().environ()['REMOTE_ADDR'], tokens=['admin']) + self.response().setCookie('auth_tkt', token.cookie_value()) + + Be careful not to do an HTTP redirect after login; use meta + refresh or Javascript -- some browsers have bugs where cookies + aren't saved when set on a redirect. + """ + + def __init__(self, secret, userid, ip, tokens=(), user_data='', + time=None, cookie_name='auth_tkt', + secure=False): + self.secret = secret + self.userid = userid + self.ip = ip + self.tokens = ','.join(tokens) + self.user_data = user_data + if time is None: + self.time = time_mod.time() + else: + self.time = time + self.cookie_name = cookie_name + self.secure = secure + + def digest(self): + return calculate_digest( + self.ip, self.time, self.secret, self.userid, self.tokens, + self.user_data) + + def cookie_value(self): + v = '%s%08x%s!' % (self.digest(), int(self.time), + url_quote(self.userid)) + if self.tokens: + v += self.tokens + '!' + v += self.user_data + return v + + def cookie(self): + c = SimpleCookie() + c_val = encodestring(self.cookie_value()) + c_val = c_val.strip().replace('\n', '') + c[self.cookie_name] = c_val + c[self.cookie_name]['path'] = '/' + if self.secure: + c[self.cookie_name]['secure'] = 'true' + return c + + +class BadTicket(Exception): + """ + Exception raised when a ticket can't be parsed. If we get + far enough to determine what the expected digest should have + been, expected is set. This should not be shown by default, + but can be useful for debugging. + """ + def __init__(self, msg, expected=None): + self.expected = expected + Exception.__init__(self, msg) + + +def parse_ticket(secret, ticket, ip): + """ + Parse the ticket, returning (timestamp, userid, tokens, user_data). + + If the ticket cannot be parsed, ``BadTicket`` will be raised with + an explanation. + """ + ticket = ticket.strip('"') + digest = ticket[:32] + try: + timestamp = int(ticket[32:40], 16) + except ValueError as e: + raise BadTicket('Timestamp is not a hex integer: %s' % e) + try: + userid, data = ticket[40:].split('!', 1) + except ValueError: + raise BadTicket('userid is not followed by !') + userid = url_unquote(userid) + if '!' in data: + tokens, user_data = data.split('!', 1) + else: + # @@: Is this the right order? + tokens = '' + user_data = data + + expected = calculate_digest(ip, timestamp, secret, + userid, tokens, user_data) + + if expected != digest: + raise BadTicket('Digest signature is not correct', + expected=(expected, digest)) + + tokens = tokens.split(',') + + return (timestamp, userid, tokens, user_data) + + +def calculate_digest(ip, timestamp, secret, userid, tokens, user_data): + secret = maybe_encode(secret) + userid = maybe_encode(userid) + tokens = maybe_encode(tokens) + user_data = maybe_encode(user_data) + digest0 = md5( + encode_ip_timestamp(ip, timestamp) + secret + userid + b'\0' + + tokens + b'\0' + user_data).hexdigest() + digest = md5(maybe_encode(digest0) + secret).hexdigest() + return digest + + +if type(chr(1)) == type(b''): #pragma NO COVER Python < 3.0 + def ints2bytes(ints): + return b''.join(map(chr, ints)) +else: #pragma NO COVER Python >= 3.0 + def ints2bytes(ints): + return bytes(ints) + +def encode_ip_timestamp(ip, timestamp): + ip_chars = ints2bytes(map(int, ip.split('.'))) + t = int(timestamp) + ts = ((t & 0xff000000) >> 24, + (t & 0xff0000) >> 16, + (t & 0xff00) >> 8, + t & 0xff) + ts_chars = ints2bytes(ts) + return ip_chars + ts_chars + + +def maybe_encode(s, encoding='utf8'): + if not isinstance(s, type(b'')): + s = s.encode(encoding) + return s + + +# Original Paste AuthTktMiddleware stripped: we don't have a use for it. diff -Nru python-repoze.who-1.0.18/repoze/who/classifiers.py python-repoze.who-2.2/repoze/who/classifiers.py --- python-repoze.who-1.0.18/repoze/who/classifiers.py 2009-05-08 22:17:00.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/classifiers.py 2013-03-20 17:19:17.000000000 +0000 @@ -1,9 +1,8 @@ -from paste.httpheaders import REQUEST_METHOD -from paste.httpheaders import CONTENT_TYPE -from paste.httpheaders import USER_AGENT -from paste.httpheaders import WWW_AUTHENTICATE +from repoze.who._compat import CONTENT_TYPE +from repoze.who._compat import REQUEST_METHOD +from repoze.who._compat import USER_AGENT -import zope.interface +from zope.interface import directlyProvides from repoze.who.interfaces import IRequestClassifier from repoze.who.interfaces import IChallengeDecider @@ -33,8 +32,14 @@ ) def default_request_classifier(environ): - """ Returns one of the classifiers 'dav', 'xmlpost', or 'browser', - depending on the imperative logic below""" + """Return one of the following classifiers: + + 'dav': the request comes from a WebDAV agent. + + 'xmlpost': the request is a POST of XML data. + + 'browser': the request comes from a normal browser (default). + """ request_method = REQUEST_METHOD(environ) if request_method in _DAV_METHODS: return 'dav' @@ -44,14 +49,14 @@ if useragent.find(agent) != -1: return 'dav' if request_method == 'POST': - if CONTENT_TYPE(environ) == 'text/xml': + if CONTENT_TYPE(environ).lower().startswith('text/xml'): return 'xmlpost' return 'browser' -zope.interface.directlyProvides(default_request_classifier, IRequestClassifier) +directlyProvides(default_request_classifier, IRequestClassifier) def default_challenge_decider(environ, status, headers): return status.startswith('401 ') -zope.interface.directlyProvides(default_challenge_decider, IChallengeDecider) +directlyProvides(default_challenge_decider, IChallengeDecider) def passthrough_challenge_decider(environ, status, headers): """ Don't challenge for pre-challenged responses. @@ -68,5 +73,4 @@ if ct is not None: return not ct.startswith('text/html') return True -zope.interface.directlyProvides(passthrough_challenge_decider, - IChallengeDecider) +directlyProvides(passthrough_challenge_decider, IChallengeDecider) diff -Nru python-repoze.who-1.0.18/repoze/who/_compat.py python-repoze.who-2.2/repoze/who/_compat.py --- python-repoze.who-1.0.18/repoze/who/_compat.py 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/_compat.py 2013-04-26 12:30:59.000000000 +0000 @@ -0,0 +1,133 @@ +try: + STRING_TYPES = (str, unicode) +except NameError: #pragma NO COVER Python >= 3.0 + STRING_TYPES = (str,) + +try: + u = unicode +except NameError: #pragma NO COVER Python >= 3.0 + u = str + b = bytes +else: #pragma NO COVER Python < 3.0 + b = str + +import base64 +if 'decodebytes' in base64.__dict__: #pragma NO COVER Python >= 3.0 + decodebytes = base64.decodebytes + encodebytes = base64.encodebytes + def decodestring(value): + return base64.decodebytes(bytes(value, 'ascii')).decode('ascii') + def encodestring(value): + return base64.encodebytes(bytes(value, 'ascii')).decode('ascii') +else: #pragma NO COVER Python < 3.0 + decodebytes = base64.decodestring + encodebytes = base64.encodestring + decodestring = base64.decodestring + encodestring = base64.encodestring + +try: + from urllib.parse import parse_qs +except ImportError: #pragma NO COVER Python < 3.0 + from cgi import parse_qs + from cgi import parse_qsl +else: #pragma NO COVER Python >= 3.0 + from urllib.parse import parse_qsl + +try: + from ConfigParser import SafeConfigParser +except ImportError: #pragma NO COVER Python >= 3.0 + from configparser import SafeConfigParser + from configparser import ParsingError +else: #pragma NO COVER Python < 3.0 + from ConfigParser import ParsingError + +try: + from Cookie import SimpleCookie +except ImportError: #pragma NO COVER Python >= 3.0 + from http.cookies import SimpleCookie + from http.cookies import CookieError +else: #pragma NO COVER Python < 3.0 + from Cookie import CookieError + +try: + from itertools import izip_longest +except ImportError: #pragma NO COVER Python >= 3.0 + from itertools import zip_longest as izip_longest + +try: + from StringIO import StringIO +except ImportError: #pragma NO COVER Python >= 3.0 + from io import StringIO + +try: + from urllib import urlencode +except ImportError: #pragma NO COVER Python >= 3.0 + from urllib.parse import urlencode + from urllib.parse import quote as url_quote + from urllib.parse import unquote as url_unquote +else: #pragma NO COVER Python < 3.0 + from urllib import quote as url_quote + from urllib import unquote as url_unquote + +try: + from urlparse import urlparse +except ImportError: #pragma NO COVER Python >= 3.0 + from urllib.parse import urlparse + from urllib.parse import urlunparse +else: #pragma NO COVER Python < 3.0 + from urlparse import urlunparse + +import wsgiref.util +import wsgiref.headers + +def REQUEST_METHOD(environ): + return environ['REQUEST_METHOD'] + +def CONTENT_TYPE(environ): + return environ.get('CONTENT_TYPE', '') + +def USER_AGENT(environ): + return environ.get('HTTP_USER_AGENT') + +def AUTHORIZATION(environ): + return environ.get('HTTP_AUTHORIZATION', '') + +def get_cookies(environ): + header = environ.get('HTTP_COOKIE', '') + if 'paste.cookies' in environ: + cookies, check_header = environ['paste.cookies'] + if check_header == header: + return cookies + cookies = SimpleCookie() + try: + cookies.load(header) + except CookieError: #pragma NO COVER (can't see how to provoke this) + pass + environ['paste.cookies'] = (cookies, header) + return cookies + +def construct_url(environ): + return wsgiref.util.request_uri(environ) + +def header_value(environ, key): + headers = wsgiref.headers.Headers(environ) + values = headers.get(key) + if not values: + return "" + if isinstance(values, list): #pragma NO COVER can't be true under Py3k. + return ",".join(values) + else: + return values + +def must_decode(value): + if type(value) is b: + try: + return value.decode('utf-8') + except UnicodeDecodeError: + return value.decode('latin1') + return value + +def must_encode(value): + if type(value) is u: + return value.encode('utf-8') + return value diff -Nru python-repoze.who-1.0.18/repoze/who/config.py python-repoze.who-2.2/repoze/who/config.py --- python-repoze.who-1.0.18/repoze/who/config.py 2009-11-04 21:38:47.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/config.py 2013-04-26 12:31:02.000000000 +0000 @@ -1,11 +1,11 @@ """ Configuration parser """ -from ConfigParser import ConfigParser -from StringIO import StringIO import logging from pkg_resources import EntryPoint import sys +import warnings +from repoze.who.api import APIFactory from repoze.who.interfaces import IAuthenticator from repoze.who.interfaces import IChallengeDecider from repoze.who.interfaces import IChallenger @@ -14,6 +14,9 @@ from repoze.who.interfaces import IPlugin from repoze.who.interfaces import IRequestClassifier from repoze.who.middleware import PluggableAuthenticationMiddleware +from repoze.who._compat import StringIO +from repoze.who._compat import SafeConfigParser +from repoze.who._compat import ParsingError def _resolve(name): if name: @@ -68,8 +71,11 @@ def parse(self, text): if getattr(text, 'readline', None) is None: text = StringIO(text) - cp = ConfigParser(defaults={'here': self.here}) - cp.readfp(text) + cp = SafeConfigParser(defaults={'here': self.here}) + try: + cp.read_file(text) + except AttributeError: #pragma NO COVER Python < 3.0 + cp.readfp(text) for s_id in [x for x in cp.sections() if x.startswith('plugin:')]: plugin_id = s_id[len('plugin:'):] @@ -126,28 +132,79 @@ ) +class NullHandler(logging.Handler): + def emit(self, record): + pass + + _LEVELS = {'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR, } +def make_api_factory_with_config(global_conf, + config_file, + remote_user_key = 'REMOTE_USER', + logger=None, + ): + identifiers = authenticators = challengers = mdproviders = () + request_classifier = None + challenge_decider = None + parser = WhoConfig(global_conf['here']) + try: + opened = open(config_file) + except IOError: + warnings.warn('Non-existent who config file: %s' % config_file, + stacklevel=2) + else: + try: + try: + parser.parse(opened) + except ParsingError: + warnings.warn('Invalid who config file: %s' % config_file, + stacklevel=2) + else: + identifiers = parser.identifiers + authenticators = parser.authenticators + challengers = parser.challengers + mdproviders = parser.mdproviders + request_classifier = parser.request_classifier + challenge_decider = parser.challenge_decider + finally: + opened.close() + + return APIFactory(identifiers, + authenticators, + challengers, + mdproviders, + request_classifier, + challenge_decider, + remote_user_key, + logger, + ) + 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)) + with open(config_file) as f: + parser.parse(f) log_stream = None + if log_level is None: + log_level = logging.INFO + elif not isinstance(log_level, int): + log_level = _LEVELS[log_level.lower()] + 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()] + log_stream = logging.getLogger('repoze.who') + log_stream.addHandler(NullHandler()) + log_stream.setLevel(log_level or 0) return PluggableAuthenticationMiddleware( app, diff -Nru python-repoze.who-1.0.18/repoze/who/interfaces.py python-repoze.who-2.2/repoze/who/interfaces.py --- python-repoze.who-1.0.18/repoze/who/interfaces.py 2009-04-20 15:59:00.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/interfaces.py 2011-02-22 14:58:18.000000000 +0000 @@ -1,8 +1,86 @@ from zope.interface import Interface + +class IAPIFactory(Interface): + def __call__(environ): + """ environ -> IRepozeWhoAPI + """ + + +class IAPI(Interface): + """ Facade for stateful invocation of underlying plugins. + """ + def authenticate(): + """ -> {identity} + + o Return an authenticated identity mapping, extracted from the + request environment. + + o If no identity can be authenticated, return None. + + o Identity will include at least a 'repoze.who.userid' key, + as well as any keys added by metadata plugins. + """ + + def challenge(status='403 Forbidden', app_headers=()): + """ -> wsgi application + + o Return a WSGI application which represents a "challenge" + (request for credentials) in response to the current request. + """ + + def remember(identity=None): + """ -> [headers] + + O Return a sequence of response headers which suffice to remember + the given identity. + + o If 'identity' is not passed, use the identity in the environment. + """ + + def forget(identity=None): + """ -> [headers] + + O Return a sequence of response headers which suffice to destroy + any credentials used to establish an identity. + + o If 'identity' is not passed, use the identity in the environment. + """ + + def login(credentials, identifier_name=None): + """ -> (identity, headers) + + o This is an API for browser-based application login forms. + + o If 'identifier_name' is passed, use it to look up the identifier; + othewise, use the first configured identifier. + + o Attempt to authenticate 'credentials' as though the identifier + had extracted them. + + o On success, 'identity' will be authenticated mapping, and 'headers' + will be "remember" headers. + + o On failure, 'identity' will be None, and response_headers will be + "forget" headers. + """ + + def logout(identifier_name=None): + """ -> (headers) + + o This is an API for browser-based application logout. + + o If 'identifier_name' is passed, use it to look up the identifier; + othewise, use the first configured identifier. + + o Returned headers will be "forget" headers. + """ + + class IPlugin(Interface): pass + class IRequestClassifier(IPlugin): """ On ingress: classify a request. """ @@ -15,6 +93,7 @@ o 'environ' is the WSGI environment. """ + class IChallengeDecider(IPlugin): """ On egress: decide whether a challenge needs to be presented to the user. @@ -34,6 +113,7 @@ a challenge needs to be presented to the user, False otherwise. """ + class IIdentifier(IPlugin): """ @@ -112,6 +192,7 @@ included in the response provided by the challenge app. """ + class IAuthenticator(IPlugin): """ On ingress: validate the identity and return a user id or None. @@ -147,6 +228,7 @@ than requiring a separate query from an IMetadataProvider plugin). """ + class IChallenger(IPlugin): """ On egress: Conditionally initiate a challenge to the user to diff -Nru python-repoze.who-1.0.18/repoze/who/middleware.py python-repoze.who-2.2/repoze/who/middleware.py --- python-repoze.who-1.0.18/repoze/who/middleware.py 2009-05-08 18:28:35.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/middleware.py 2012-03-19 19:42:14.000000000 +0000 @@ -1,46 +1,61 @@ import logging -from StringIO import StringIO import sys -from repoze.who.interfaces import IIdentifier -from repoze.who.interfaces import IAuthenticator +from repoze.who.api import APIFactory from repoze.who.interfaces import IChallenger -from repoze.who.interfaces import IMetadataProvider +from repoze.who._compat import StringIO _STARTED = '-- repoze.who request started (%s) --' _ENDED = '-- repoze.who request ended (%s) --' class PluggableAuthenticationMiddleware(object): - def __init__(self, app, + def __init__(self, + app, identifiers, authenticators, challengers, mdproviders, - classifier, - challenge_decider, + request_classifier = None, + challenge_decider = None, log_stream = None, log_level = logging.INFO, remote_user_key = 'REMOTE_USER', + classifier = None ): - iregistry, nregistry = make_registries(identifiers, authenticators, - challengers, mdproviders) - self.registry = iregistry - self.name_registry = nregistry + if challenge_decider is None: + raise ValueError('challenge_decider is required') + if request_classifier is not None and classifier is not None: + raise ValueError( + 'Only one of request_classifier and classifier is allowed') + if request_classifier is None: + if classifier is None: + raise ValueError( + 'Either request_classifier or classifier is required') + request_classifier = classifier self.app = app - self.classifier = classifier - self.challenge_decider = challenge_decider - self.remote_user_key = remote_user_key - self.logger = None + logger = self.logger = None if isinstance(log_stream, logging.Logger): - self.logger = log_stream + 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) + logger = self.logger = logging.Logger('repoze.who') + logger.addHandler(handler) + logger.setLevel(log_level) + self.remote_user_key = remote_user_key + + self.api_factory = APIFactory(identifiers, + authenticators, + challengers, + mdproviders, + request_classifier, + challenge_decider, + remote_user_key, + logger + ) + def __call__(self, environ, start_response): if self.remote_user_key in environ: @@ -48,52 +63,17 @@ # already set return self.app(environ, start_response) - path_info = environ.get('PATH_INFO', None) + api = self.api_factory(environ) - environ['repoze.who.plugins'] = self.name_registry + environ['repoze.who.plugins'] = api.name_registry # BBB? environ['repoze.who.logger'] = self.logger environ['repoze.who.application'] = self.app logger = self.logger + path_info = environ.get('PATH_INFO', None) 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') + identity = api.authenticate() # allow identifier plugins to replace the downstream # application (to do redirection and unauthorized themselves @@ -114,170 +94,34 @@ if not wrapper.called: app_iter = wrap_generator(app_iter) - if self.challenge_decider(environ, wrapper.status, wrapper.headers): + if api.challenge_decider(environ, wrapper.status, wrapper.headers): logger and logger.info('challenge required') + close = getattr(app_iter, 'close', _no_op) - challenge_app = self.challenge( - environ, - classification, - wrapper.status, - wrapper.headers, - identifier, - identity - ) + challenge_app = api.challenge(wrapper.status, wrapper.headers) if challenge_app is not None: logger and logger.info('executing challenge app') if app_iter: list(app_iter) # unwind the original app iterator + # PEP 333 requires that we call the original iterator's + # 'close' method, if it exists, before releasing it. + close() # replace the downstream app with the challenge app app_iter = challenge_app(environ, start_response) else: logger and logger.info('configuration error: no challengers') + close() 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)) + remember_headers = api.remember(identity) 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 _no_op(): + pass def wrap_generator(result): """\ @@ -286,6 +130,9 @@ caches it to trigger any immediate side effects (in a WSGI world, this ensures start_response is called). """ + # PEP 333 requires that we call the original iterator's + # 'close' method, if it exists, before releasing it. + close = getattr(result, 'close', lambda: None) # 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: @@ -299,22 +146,9 @@ for iter in result: # We'll let result's StopIteration bubble up directly. yield iter + close() 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 @@ -351,13 +185,13 @@ 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 + [plugin:redirector] + use = repoze.who.plugins.redirector.RedirectorPlugin + login_url = /login.html + + [plugin:auth_tkt] + use = repoze.who.plugins.auth_tkt:AuthTktCookiePlugin + secret = SEEKRIT cookie_name = oatmeal [plugin:basicauth] @@ -374,22 +208,20 @@ challenge_decider = repoze.who.classifiers:default_challenge_decider [identifiers] - plugins = form:browser cookie basicauth + plugins = authtkt basicauth [authenticators] - plugins = htpasswd + plugins = authtkt htpasswd [challengers] - plugins = form:browser basicauth + plugins = redirector: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.redirector import RedirectorPlugin 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) @@ -398,13 +230,14 @@ 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)] + redirector = RedirectorPlugin('/login.html') + redirector.classifications = {IChallenger: ['browser']} # only for browser + identifiers = [('auth_tkt', auth_tkt), + ('basicauth', basicauth), + ] authenticators = [('htpasswd', htpasswd)] - challengers = [('form',form), ('basicauth',basicauth)] + challengers = [('redirector', redirector), + ('basicauth', basicauth)] mdproviders = [] from repoze.who.classifiers import default_request_classifier from repoze.who.classifiers import default_challenge_decider @@ -424,38 +257,3 @@ 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__ - - diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/auth_tkt.py python-repoze.who-2.2/repoze/who/plugins/auth_tkt.py --- python-repoze.who-1.0.18/repoze/who/plugins/auth_tkt.py 2009-11-05 21:31:57.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/auth_tkt.py 2012-03-19 19:42:14.000000000 +0000 @@ -4,12 +4,14 @@ import os import time -from paste.request import get_cookies -from paste.auth import auth_tkt - -from zope.interface import implements +from zope.interface import implementer from repoze.who.interfaces import IIdentifier +from repoze.who.interfaces import IAuthenticator +from repoze.who._compat import get_cookies +import repoze.who._auth_tkt as auth_tkt +from repoze.who._compat import STRING_TYPES +from repoze.who._compat import u _NOW_TESTING = None # unit tests can replace def _now(): #pragma NO COVERAGE @@ -17,21 +19,26 @@ return _NOW_TESTING return datetime.datetime.now() -class AuthTktCookiePlugin(object): - implements(IIdentifier) +@implementer(IIdentifier, IAuthenticator) +class AuthTktCookiePlugin(object): - 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]), - } - + userid_type_decoders = {'int': int, + 'unicode': lambda x: utf_8_decode(x)[0], + } + + userid_type_encoders = {int: ('int', str), + } + try: + userid_type_encoders[long] = ('int', str) + except NameError: #pragma NO COVER Python >= 3.0 + pass + try: + userid_type_encoders[unicode] = ('unicode', + lambda x: utf_8_encode(x)[0]) + except NameError: #pragma NO COVER Python >= 3.0 + pass + def __init__(self, secret, cookie_name='auth_tkt', secure=False, include_ip=False, timeout=None, reissue_time=None, userid_checker=None): @@ -65,9 +72,6 @@ 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 @@ -86,34 +90,11 @@ identity = {} identity['timestamp'] = timestamp - identity['repoze.who.userid'] = userid + identity['repoze.who.plugins.auth_tkt.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 @@ -132,7 +113,7 @@ old_cookie_value = getattr(existing, 'value', None) max_age = identity.get('max_age', None) - timestamp, userid, tokens, userdata = None, '', '', '' + timestamp, userid, tokens, userdata = None, '', (), '' if old_cookie_value: try: @@ -140,21 +121,19 @@ self.secret, old_cookie_value, remote_addr) except auth_tkt.BadTicket: pass + tokens = tuple(tokens) who_userid = identity['repoze.who.userid'] - who_tokens = identity.get('tokens', '') + who_tokens = tuple(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) + # XXX we are discarding the userdata passed in the identity? 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) @@ -170,18 +149,55 @@ 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) + # IAuthenticator + def authenticate(self, environ, identity): + userid = identity.get('repoze.who.plugins.auth_tkt.userid') + if userid is None: + return None + if self.userid_checker and not self.userid_checker(userid): + return None + identity['repoze.who.userid'] = userid + return userid + + def _get_cookies(self, environ, value, max_age=None): + if max_age is not None: + max_age = int(max_age) + later = _now() + datetime.timedelta(seconds=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 = '' + + secure = '' + if self.secure: + secure = '; secure; HttpOnly' + + cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME')) + cur_domain = cur_domain.split(':')[0] # drop port + wild_domain = '.' + cur_domain + cookies = [ + ('Set-Cookie', '%s="%s"; Path=/%s%s' % ( + self.cookie_name, value, max_age, secure)), + ('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s%s' % ( + self.cookie_name, value, cur_domain, max_age, secure)), + ('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s%s' % ( + self.cookie_name, value, wild_domain, max_age, secure)) + ] + return cookies + def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) #pragma NO COVERAGE def _bool(value): - if isinstance(value, basestring): + if isinstance(value, STRING_TYPES): return value.lower() in ('yes', 'true', '1') return value @@ -203,7 +219,8 @@ 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() + with open(secretfile) as f: + secret = f.read().strip() if timeout: timeout = int(timeout) if reissue_time: diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/basicauth.py python-repoze.who-2.2/repoze/who/plugins/basicauth.py --- python-repoze.who-1.0.18/repoze/who/plugins/basicauth.py 2009-05-08 19:26:50.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/basicauth.py 2012-03-19 19:42:14.000000000 +0000 @@ -1,38 +1,42 @@ import binascii -from paste.httpheaders import WWW_AUTHENTICATE -from paste.httpheaders import AUTHORIZATION -from paste.httpexceptions import HTTPUnauthorized - -from zope.interface import implements +from webob.exc import HTTPUnauthorized +from zope.interface import implementer from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IChallenger +from repoze.who._compat import AUTHORIZATION +from repoze.who._compat import decodebytes +from repoze.who._compat import must_decode +@implementer(IIdentifier, IChallenger) class BasicAuthPlugin(object): - implements(IIdentifier, IChallenger) - def __init__(self, realm): self.realm = realm # IIdentifier def identify(self, environ): authorization = AUTHORIZATION(environ) + if type(authorization) != type(b''): + # this header *must* be base64-encoded ASCII + authorization = authorization.encode('ascii') try: - authmeth, auth = authorization.split(' ', 1) + authmeth, auth = authorization.split(b' ', 1) except ValueError: # not enough values to unpack return None - if authmeth.lower() == 'basic': + if authmeth.lower() == b'basic': try: - auth = auth.strip().decode('base64') + auth = auth.strip() + auth = decodebytes(auth) except binascii.Error: # can't decode return None try: - login, password = auth.split(':', 1) + login, password = auth.split(b':', 1) except ValueError: # not enough values to unpack return None - auth = {'login':login, 'password':password} + auth = {'login': must_decode(login), + 'password': must_decode(password)} return auth return None @@ -44,7 +48,7 @@ pass def _get_wwwauth(self): - head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm) + head = [('WWW-Authenticate', 'Basic realm="%s"' % self.realm)] return head # IIdentifier diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/cookie.py python-repoze.who-2.2/repoze/who/plugins/cookie.py --- python-repoze.who-1.0.18/repoze/who/plugins/cookie.py 2009-05-08 19:29:57.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/cookie.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,63 +0,0 @@ -import 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 - diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/form.py python-repoze.who-2.2/repoze/who/plugins/form.py --- python-repoze.who-1.0.18/repoze/who/plugins/form.py 2009-11-05 20:52:15.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/form.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,246 +0,0 @@ -import 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 - diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/htpasswd.py python-repoze.who-2.2/repoze/who/plugins/htpasswd.py --- python-repoze.who-1.0.18/repoze/who/plugins/htpasswd.py 2009-05-08 19:44:31.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/htpasswd.py 2012-11-09 01:45:20.000000000 +0000 @@ -1,11 +1,19 @@ -from zope.interface import implements +import itertools + +from zope.interface import implementer from repoze.who.interfaces import IAuthenticator from repoze.who.utils import resolveDotted +from repoze.who._compat import izip_longest + + +def _padding_for_file_lines(): + yield 'aaaaaa:bbbbbb' + +@implementer(IAuthenticator) class HTPasswdPlugin(object): - implements(IAuthenticator) def __init__(self, filename, check): self.filename = filename @@ -13,6 +21,13 @@ # IAuthenticatorPlugin def authenticate(self, environ, identity): + # NOW HEAR THIS!!! + # + # This method is *intentionally* slower than would be ideal because + # it is trying to avoid leaking information via timing attacks + # (number of users, length of user IDs, length of passwords, etc.). + # + # Do *not* try to optimize anything away here. try: login = identity['login'] password = identity['password'] @@ -23,35 +38,74 @@ # assumed to have a readline self.filename.seek(0) f = self.filename + must_close = False else: try: f = open(self.filename, 'r') + must_close = True except IOError: environ['repoze.who.logger'].warn('could not open htpasswd ' 'file %s' % self.filename) return None - for line in f: + result = None + maybe_user = None + to_check = 'ABCDEF0123456789' + + # Try not to reveal how many users we have. + # XXX: the max count here should be configurable ;( + lines = itertools.chain(f, _padding_for_file_lines()) + for line in itertools.islice(lines, 0, 1000): try: username, hashed = line.rstrip().split(':', 1) except ValueError: continue - if username == login: - if self.check(password, hashed): - return username - return None + if _same_string(username, login): + # Don't bail early: leaks information!! + maybe_user = username + to_check = hashed + + if must_close: + f.close() + + # Check *something* here, to mitigate a timing attack. + password_ok = self.check(password, to_check) + + # Check our flags: if both are OK, we found a match. + if password_ok and maybe_user: + result = maybe_user + + return result def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) #pragma NO COVERAGE +PADDING = ' ' * 1000 + +def _same_string(x, y): + # Attempt at isochronous string comparison. + mismatches = filter(None, [a != b for a, b, ignored + in izip_longest(x, y, PADDING)]) + if type(mismatches) != list: #pragma NO COVER Python >= 3.0 + mismatches = list(mismatches) + return len(mismatches) == 0 + def crypt_check(password, hashed): from crypt import crypt salt = hashed[:2] - return hashed == crypt(password, salt) + return _same_string(hashed, crypt(password, salt)) + +def sha1_check(password, hashed): + from hashlib import sha1 + from base64 import standard_b64encode + from repoze.who._compat import must_encode + encrypted_string = standard_b64encode(sha1(must_encode(password)).digest()) + return _same_string(hashed, "%s%s" % ("{SHA}", encrypted_string)) def plain_check(password, hashed): - return hashed == password + return _same_string(password, hashed) + def make_plugin(filename=None, check_fn=None): if filename is None: @@ -60,5 +114,3 @@ raise ValueError('check_fn must be specified') check = resolveDotted(check_fn) return HTPasswdPlugin(filename, check) - - diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/redirector.py python-repoze.who-2.2/repoze/who/plugins/redirector.py --- python-repoze.who-1.0.18/repoze/who/plugins/redirector.py 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/redirector.py 2012-03-19 19:42:14.000000000 +0000 @@ -0,0 +1,77 @@ +from webob.exc import HTTPFound +from zope.interface import implementer + +from repoze.who.interfaces import IChallenger +from repoze.who._compat import construct_url +from repoze.who._compat import header_value +from repoze.who._compat import parse_qs +from repoze.who._compat import u +from repoze.who._compat import urlencode +from repoze.who._compat import urlparse +from repoze.who._compat import urlunparse + +@implementer(IChallenger) +class RedirectorPlugin(object): + """ Plugin for issuing challenges as redirects to a configured URL. + + o If the ``reason_param`` option is configured, and the application has + supplied an ``X-Authorization-Failure-Reason`` header, the plugin + includes that reason in the query string of the redirected URL. + """ + + def __init__(self, + login_url, + came_from_param='came_from', + reason_param='reason', + reason_header='X-Authorization-Failure-Reason', + ): + self.login_url = login_url + self.came_from_param = came_from_param + if ((reason_param is None and reason_header is not None) or + (reason_param is not None and reason_header is None)): + raise ValueError( + "Must supply both 'reason_header' and 'reason_param', " + "or neither one.") + self.reason_param = reason_param + self.reason_header = reason_header + self._login_url_parts = list(urlparse(login_url)) + + # IChallenger + def challenge(self, environ, status, app_headers, forget_headers): + if self.reason_param is not None or self.came_from_param is not None: + url_parts = self._login_url_parts[:] + query = url_parts[4] + query_elements = parse_qs(query) + if self.reason_param is not None: + reason = header_value(app_headers, self.reason_header) + if reason: + query_elements[self.reason_param] = reason + if self.came_from_param is not None: + query_elements[self.came_from_param] = construct_url(environ) + url_parts[4] = urlencode(query_elements, doseq=True) + login_url = urlunparse(url_parts) + else: + login_url = self.login_url + headers = [('Location', login_url)] + forget_headers + cookies = [(h,v) for (h,v) in app_headers if h.lower() == 'set-cookie'] + headers += cookies + return HTTPFound(headers=headers) + +def make_plugin(login_url, + came_from_param=None, + reason_param=None, + reason_header=None, + ): + if login_url in (u(''), b'', None): + raise ValueError("No 'login_url'") + if reason_header is not None and reason_param is None: + raise Exception("Can't set 'reason_header' without 'reason_param'.") + + if reason_header is None and reason_param is not None: + reason_header='X-Authorization-Failure-Reason' + + return RedirectorPlugin(login_url, + came_from_param=came_from_param, + reason_param=reason_param, + reason_header=reason_header, + ) diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/sql.py python-repoze.who-2.2/repoze/who/plugins/sql.py --- python-repoze.who-1.0.18/repoze/who/plugins/sql.py 2009-05-08 20:07:25.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/sql.py 2012-03-19 19:42:14.000000000 +0000 @@ -1,4 +1,4 @@ -from zope.interface import implements +from zope.interface import implementer from repoze.who.interfaces import IAuthenticator from repoze.who.interfaces import IMetadataProvider @@ -14,6 +14,8 @@ if stored_password_hash.startswith('{SHA}'): stored_password_hash = stored_password_hash[5:] + if not isinstance(cleartext_password, type(b'')): + cleartext_password = cleartext_password.encode('utf-8') digest = sha1(cleartext_password).hexdigest() else: digest = cleartext_password @@ -30,9 +32,9 @@ return psycopg2.connect(kw['repoze.who.dsn']) #pragma NO COVERAGE return conn_factory #pragma NO COVERAGE +@implementer(IAuthenticator) 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" @@ -56,9 +58,9 @@ if self.compare_fn(identity['password'], password): return user_id +@implementer(IMetadataProvider) class SQLMetadataProviderPlugin: - implements(IMetadataProvider) - + def __init__(self, name, query, conn_factory, filter): self.name = name self.query = query @@ -90,7 +92,7 @@ raise ValueError('conn_factory must be specified') try: conn_factory = resolveDotted(conn_factory)(**kw) - except Exception, why: + except Exception as why: raise ValueError('conn_factory could not be resolved: %s' % why) if compare_fn is not None: compare_fn = resolveDotted(compare_fn) @@ -107,7 +109,7 @@ raise ValueError('conn_factory must be specified') try: conn_factory = resolveDotted(conn_factory)(**kw) - except Exception, why: + except Exception as why: raise ValueError('conn_factory could not be resolved: %s' % why) if filter is not None: filter = resolveDotted(filter) diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/tests/fixtures/form.html python-repoze.who-2.2/repoze/who/plugins/tests/fixtures/form.html --- python-repoze.who-1.0.18/repoze/who/plugins/tests/fixtures/form.html 2009-05-08 18:46:35.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/tests/fixtures/form.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ - - diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/tests/test_authtkt.py python-repoze.who-2.2/repoze/who/plugins/tests/test_authtkt.py --- python-repoze.who-1.0.18/repoze/who/plugins/tests/test_authtkt.py 2009-11-05 21:21:15.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/tests/test_authtkt.py 2012-03-19 19:42:14.000000000 +0000 @@ -1,5 +1,6 @@ import unittest + class TestAuthTktCookiePlugin(unittest.TestCase): tempdir = None _now_testing = None @@ -14,27 +15,36 @@ if self._now_testing is not None: self._setNowTesting(self._now_testing) + def failUnless(self, predicate, message=''): + self.assertTrue(predicate, message) # Nannies go home! + + def failIf(self, predicate, message=''): + self.assertFalse(predicate, message) # Nannies go home! + def _getTargetClass(self): from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin return AuthTktCookiePlugin def _makeEnviron(self, kw=None): - environ = {'wsgi.version': (1,0)} + from wsgiref.util import setup_testing_defaults + environ = {} + setup_testing_defaults(environ) if kw is not None: environ.update(kw) environ['REMOTE_ADDR'] = '1.1.1.1' - environ['SERVER_NAME'] = 'localhost' + environ['HTTP_HOST'] = 'localhost' return environ - def _makeOne(self, *arg, **kw): - plugin = self._getTargetClass()(*arg, **kw) + def _makeOne(self, secret='s33kr3t', *arg, **kw): + plugin = self._getTargetClass()(secret, *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 + #from paste.auth import auth_tkt + import repoze.who._auth_tkt as auth_tkt ticket = auth_tkt.AuthTicket( 'secret', userid, @@ -50,12 +60,37 @@ from repoze.who.plugins import auth_tkt auth_tkt._NOW_TESTING, self._now_testing = value, auth_tkt._NOW_TESTING - def test_implements(self): + def test_class_conforms_to_IIdentifier(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IIdentifier klass = self._getTargetClass() verifyClass(IIdentifier, klass) + def test_instance_conforms_to_IIdentifier(self): + from zope.interface.verify import verifyObject + from repoze.who.interfaces import IIdentifier + klass = self._getTargetClass() + verifyObject(IIdentifier, self._makeOne()) + + def test_class_conforms_to_IAuthenticator(self): + from zope.interface.verify import verifyClass + from repoze.who.interfaces import IAuthenticator + klass = self._getTargetClass() + verifyClass(IAuthenticator, klass) + + def test_instance_conforms_to_IAuthenticator(self): + from zope.interface.verify import verifyObject + from repoze.who.interfaces import IAuthenticator + klass = self._getTargetClass() + verifyObject(IAuthenticator, self._makeOne()) + + 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_nocookie(self): plugin = self._makeOne('secret') environ = self._makeEnviron() @@ -69,7 +104,7 @@ result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) - self.assertEqual(result['repoze.who.userid'], 'userid') + self.assertEqual(result['repoze.who.plugins.auth_tkt.userid'], 'userid') self.assertEqual(result['userdata'], 'userdata') self.failUnless('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) @@ -83,7 +118,7 @@ result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) - self.assertEqual(result['repoze.who.userid'], 'userid') + self.assertEqual(result['repoze.who.plugins.auth_tkt.userid'], 'userid') self.assertEqual(result['userdata'], 'userdata') self.failUnless('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) @@ -97,7 +132,7 @@ result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) - self.assertEqual(result['repoze.who.userid'], 1) + self.assertEqual(result['repoze.who.plugins.auth_tkt.userid'], 1) self.assertEqual(result['userdata'], 'userid_type:int') self.failUnless('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) @@ -111,7 +146,7 @@ result = plugin.identify(environ) self.assertEqual(len(result), 4) self.assertEqual(result['tokens'], ['']) - self.assertEqual(result['repoze.who.userid'], 'userid') + self.assertEqual(result['repoze.who.plugins.auth_tkt.userid'], 'userid') self.assertEqual(result['userdata'], 'userid_type:unknown') self.failUnless('timestamp' in result) self.assertEqual(environ['REMOTE_USER_TOKENS'], ['']) @@ -132,6 +167,20 @@ result = plugin.identify(environ) self.assertEqual(result, None) + 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.plugins.auth_tkt.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_remember_creds_same(self): plugin = self._makeOne('secret') val = self._makeTicket(userid='userid') @@ -140,6 +189,34 @@ 'userdata':'userdata'}) self.assertEqual(result, None) + def test_remember_creds_secure(self): + plugin = self._makeOne('secret', secure=True) + val = self._makeTicket(userid='userid', secure=True) + environ = self._makeEnviron() + result = plugin.remember(environ, {'repoze.who.userid':'userid', + 'userdata':'userdata'}) + self.assertEqual(len(result), 3) + self.assertEqual(result[0], + ('Set-Cookie', + 'auth_tkt="%s"; ' + 'Path=/; ' + 'secure; ' + 'HttpOnly' % val)) + self.assertEqual(result[1], + ('Set-Cookie', + 'auth_tkt="%s"; ' + 'Path=/; ' + 'Domain=localhost; ' + 'secure; HttpOnly' + % val)) + self.assertEqual(result[2], + ('Set-Cookie', + 'auth_tkt="%s"; ' + 'Path=/; ' + 'Domain=.localhost; ' + 'secure; HttpOnly' + % val)) + def test_remember_creds_different(self): plugin = self._makeOne('secret') old_val = self._makeTicket(userid='userid') @@ -150,14 +227,46 @@ self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', - 'auth_tkt="%s"; Path=/' % new_val)) + '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_strips_port(self): + plugin = self._makeOne('secret') + old_val = self._makeTicket(userid='userid') + environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val, + 'HTTP_HOST': 'localhost:8080', + }) + 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' + 'auth_tkt="%s"; ' + 'Path=/; ' + 'Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', - 'auth_tkt="%s"; Path=/; Domain=.localhost' + 'auth_tkt="%s"; ' + 'Path=/; ' + 'Domain=.localhost' % new_val)) def test_remember_creds_different_include_ip(self): @@ -172,14 +281,19 @@ self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', - 'auth_tkt="%s"; Path=/' % new_val)) + 'auth_tkt="%s"; ' + 'Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', - 'auth_tkt="%s"; Path=/; Domain=localhost' + 'auth_tkt="%s"; ' + 'Path=/; ' + 'Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', - 'auth_tkt="%s"; Path=/; Domain=.localhost' + 'auth_tkt="%s"; ' + 'Path=/; ' + 'Domain=.localhost' % new_val)) def test_remember_creds_different_bad_old_cookie(self): @@ -192,39 +306,78 @@ self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', - 'auth_tkt="%s"; Path=/' % new_val)) + 'auth_tkt="%s"; ' + 'Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', - 'auth_tkt="%s"; Path=/; Domain=localhost' + 'auth_tkt="%s"; ' + 'Path=/; ' + 'Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', - 'auth_tkt="%s"; Path=/; Domain=.localhost' + 'auth_tkt="%s"; ' + 'Path=/; ' + 'Domain=.localhost' % new_val)) - def test_remember_creds_different_with_nonstring_tokens(self): + def test_remember_creds_different_with_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', + new_val = self._makeTicket(userid='userid', userdata='userdata', - tokens='foo,bar', + tokens=['foo', 'bar'], ) - result = plugin.remember(environ, {'repoze.who.userid': 'other', + result = plugin.remember(environ, {'repoze.who.userid': 'userid', 'userdata': 'userdata', 'tokens': ['foo', 'bar'], }) self.assertEqual(len(result), 3) self.assertEqual(result[0], ('Set-Cookie', - 'auth_tkt="%s"; Path=/' % new_val)) + 'auth_tkt="%s"; ' + 'Path=/' % new_val)) self.assertEqual(result[1], ('Set-Cookie', - 'auth_tkt="%s"; Path=/; Domain=localhost' + 'auth_tkt="%s"; ' + 'Path=/; Domain=localhost' % new_val)) self.assertEqual(result[2], ('Set-Cookie', - 'auth_tkt="%s"; Path=/; Domain=.localhost' + 'auth_tkt="%s"; ' + 'Path=/; ' + 'Domain=.localhost' + % new_val)) + + def test_remember_creds_different_with_tuple_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='userid', + userdata='userdata', + tokens=['foo', 'bar'], + ) + result = plugin.remember(environ, {'repoze.who.userid': 'userid', + '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): @@ -241,6 +394,10 @@ 'auth_tkt="%s"; Path=/' % new_val)) def test_remember_creds_different_long_userid(self): + try: + long + except NameError: #pragma NO COVER Python >= 3.0 + return plugin = self._makeOne('secret') old_val = self._makeTicket(userid='userid') environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=%s' % old_val}) @@ -256,9 +413,13 @@ 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') + userid = b'\xc2\xa9'.decode('utf-8') + if type(b'') == type(''): + userdata = 'userid_type:unicode' + else: # XXX + userdata = '' new_val = self._makeTicket(userid=userid.encode('utf-8'), - userdata='userid_type:unicode') + userdata=userdata) result = plugin.remember(environ, {'repoze.who.userid':userid, 'userdata':''}) self.assertEqual(type(result[0][1]), str) @@ -282,6 +443,37 @@ ('Set-Cookie', 'auth_tkt="%s"; Path=/' % new_val)) + 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 test_forget(self): from datetime import datetime now = datetime(2009, 11, 5, 16, 15, 22) @@ -312,6 +504,25 @@ 'Max-Age=0; Expires=Thu, 05 Nov 2009 16:15:22' ) + def test_authenticate_non_auth_tkt_credentials(self): + plugin = self._makeOne() + self.assertEqual(plugin.authenticate(environ={}, identity={}), None) + + def test_authenticate_without_checker(self): + plugin = self._makeOne() + identity = {'repoze.who.plugins.auth_tkt.userid': 'phred'} + self.assertEqual(plugin.authenticate({}, identity), 'phred') + + def test_authenticate_with_checker_and_non_existing_account(self): + plugin = self._makeOne('secret', userid_checker=dummy_userid_checker) + identity = {'repoze.who.plugins.auth_tkt.userid': 'phred'} + self.assertEqual(plugin.authenticate({}, identity), None) + + def test_authenticate_with_checker_and_existing_account(self): + plugin = self._makeOne('secret', userid_checker=dummy_userid_checker) + identity = {'repoze.who.plugins.auth_tkt.userid': 'existing'} + self.assertEqual(plugin.authenticate({}, identity), 'existing') + def test_factory_wo_secret_wo_secretfile_raises_ValueError(self): from repoze.who.plugins.auth_tkt import make_plugin self.assertRaises(ValueError, make_plugin) @@ -358,52 +569,19 @@ 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): + def test_remember_max_age_unicode(self): + from repoze.who._compat import u 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) + result = plugin.remember(environ, {'repoze.who.userid': 'chris', + 'max_age': u('500')}) + name, value = result.pop(0) self.assertEqual('Set-Cookie', name) + self.failUnless(isinstance(value, str)) self.failUnless( value.startswith('auth_tkt="%s"; Path=/; Max-Age=500' % tkt), - value) + (value, tkt)) self.failUnless('; Expires=' in value) name,value = result.pop(0) diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/tests/test_basicauth.py python-repoze.who-2.2/repoze/who/plugins/tests/test_basicauth.py --- python-repoze.who-1.0.18/repoze/who/plugins/tests/test_basicauth.py 2009-05-08 18:48:39.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/tests/test_basicauth.py 2012-03-19 19:42:14.000000000 +0000 @@ -10,9 +10,16 @@ plugin = self._getTargetClass()(*arg, **kw) return plugin + def failUnless(self, predicate, message=''): + self.assertTrue(predicate, message) # Nannies go home! + + def failIf(self, predicate, message=''): + self.assertFalse(predicate, message) # Nannies go home! + def _makeEnviron(self, kw=None): + from wsgiref.util import setup_testing_defaults environ = {} - environ['wsgi.version'] = (1,0) + setup_testing_defaults(environ) if kw is not None: environ.update(kw) return environ @@ -34,9 +41,9 @@ items = [] for item in app_iter: items.append(item) - response = ''.join(items) + response = b''.join(items).decode('utf-8') self.failUnless(response.startswith('401 Unauthorized')) - + def test_identify_noauthinfo(self): plugin = self._makeOne('realm') environ = self._makeEnviron() @@ -56,19 +63,43 @@ self.assertEqual(creds, None) def test_identify_basic_badrepr(self): + from repoze.who._compat import encodebytes plugin = self._makeOne('realm') - value = 'foo'.encode('base64') + value = encodebytes(b'foo').decode('ascii') environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic %s' % value}) creds = plugin.identify(environ) self.assertEqual(creds, None) def test_identify_basic_ok(self): + from repoze.who._compat import encodebytes plugin = self._makeOne('realm') - value = 'foo:bar'.encode('base64') + value = encodebytes(b'foo:bar').decode('ascii') environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic %s' % value}) creds = plugin.identify(environ) self.assertEqual(creds, {'login':'foo', 'password':'bar'}) + def test_identify_basic_ok_utf8_values(self): + from repoze.who._compat import encodebytes + LOGIN = b'b\xc3\xa2tard' + PASSWD = b'l\xc3\xa0 demain' + plugin = self._makeOne('realm') + value = encodebytes(b':'.join((LOGIN, PASSWD))).decode('ascii') + environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic %s' % value}) + creds = plugin.identify(environ) + self.assertEqual(creds, {'login': LOGIN.decode('utf-8'), + 'password': PASSWD.decode('utf-8')}) + + def test_identify_basic_ok_latin1_values(self): + from repoze.who._compat import encodebytes + LOGIN = b'b\xe2tard' + PASSWD = b'l\xe0 demain' + plugin = self._makeOne('realm') + value = encodebytes(b':'.join((LOGIN, PASSWD))).decode('ascii') + environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic %s' % value}) + creds = plugin.identify(environ) + self.assertEqual(creds, {'login': LOGIN.decode('latin1'), + 'password': PASSWD.decode('latin1')}) + def test_remember(self): plugin = self._makeOne('realm') creds = {} @@ -89,16 +120,15 @@ environ = self._makeEnviron() forget = plugin._get_wwwauth() result = plugin.challenge(environ, '401 Unauthorized', [], forget) - self.assertEqual(result.headers, forget) - + self.assertTrue(forget[0] in result.headers.items()) + 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) - + self.assertTrue(forget[0] in result.headers.items()) def test_factory(self): from repoze.who.plugins.basicauth import make_plugin diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/tests/test_cookie.py python-repoze.who-2.2/repoze/who/plugins/tests/test_cookie.py --- python-repoze.who-1.0.18/repoze/who/plugins/tests/test_cookie.py 2009-05-08 19:29:34.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/tests/test_cookie.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,84 +0,0 @@ -import 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') diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/tests/test_form.py python-repoze.who-2.2/repoze/who/plugins/tests/test_form.py --- python-repoze.who-1.0.18/repoze/who/plugins/tests/test_form.py 2009-11-05 20:52:31.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/tests/test_form.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,620 +0,0 @@ -import 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 diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/tests/test_htpasswd.py python-repoze.who-2.2/repoze/who/plugins/tests/test_htpasswd.py --- python-repoze.who-1.0.18/repoze/who/plugins/tests/test_htpasswd.py 2009-05-08 19:49:17.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/tests/test_htpasswd.py 2012-11-09 01:45:33.000000000 +0000 @@ -1,5 +1,6 @@ import unittest + class TestHTPasswdPlugin(unittest.TestCase): def _getTargetClass(self): @@ -17,6 +18,12 @@ environ.update(kw) return environ + def failUnless(self, predicate, message=''): + self.assertTrue(predicate, message) # Nannies go home! + + def failIf(self, predicate, message=''): + self.assertFalse(predicate, message) # Nannies go home! + def test_implements(self): from zope.interface.verify import verifyClass from repoze.who.interfaces import IAuthenticator @@ -24,34 +31,38 @@ verifyClass(IAuthenticator, klass) def test_authenticate_nocreds(self): - from StringIO import StringIO + from repoze.who._compat 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 + from repoze.who._compat import StringIO io = StringIO() - plugin = self._makeOne(io, None) + 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, None) - + def test_authenticate_nousermatch(self): - from StringIO import StringIO + from repoze.who._compat import StringIO io = StringIO('nobody:foo') - plugin = self._makeOne(io, None) + 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, None) def test_authenticate_match(self): - from StringIO import StringIO + from repoze.who._compat import StringIO io = StringIO('chrism:pass') def check(password, hashed): return True @@ -62,7 +73,7 @@ self.assertEqual(result, 'chrism') def test_authenticate_badline(self): - from StringIO import StringIO + from repoze.who._compat import StringIO io = StringIO('badline\nchrism:pass') def check(password, hashed): return True @@ -117,6 +128,19 @@ self.assertEqual(crypt_check('password', hashed), True) self.assertEqual(crypt_check('notpassword', hashed), False) + def test_sha1_check(self): + from base64 import standard_b64encode + from hashlib import sha1 + from repoze.who._compat import must_encode + from repoze.who.plugins.htpasswd import sha1_check + + encrypted_string = standard_b64encode(sha1( + must_encode("password")).digest()) + self.assertEqual(sha1_check('password', + "%s%s" % ("{SHA}", encrypted_string)), True) + self.assertEqual(sha1_check('notpassword', + "%s%s" % ("{SHA}", encrypted_string)), False) + def test_plain_check(self): from repoze.who.plugins.htpasswd import plain_check self.failUnless(plain_check('password', 'password')) diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/tests/test_redirector.py python-repoze.who-2.2/repoze/who/plugins/tests/test_redirector.py --- python-repoze.who-1.0.18/repoze/who/plugins/tests/test_redirector.py 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/tests/test_redirector.py 2012-03-19 19:42:14.000000000 +0000 @@ -0,0 +1,388 @@ +import unittest + +class _Base(unittest.TestCase): + + def failUnless(self, predicate, message=''): + self.assertTrue(predicate, message) # Nannies go home! + + def failIf(self, predicate, message=''): + self.assertFalse(predicate, message) # Nannies go home! + + +class TestRedirectorPlugin(_Base): + + def _getTargetClass(self): + from repoze.who.plugins.redirector import RedirectorPlugin + return RedirectorPlugin + + def _makeOne(self, + login_url='http://example.com/login.html', + came_from_param=None, + reason_param=None, + reason_header=None, + ): + return self._getTargetClass()(login_url, + came_from_param=came_from_param, + reason_param=reason_param, + reason_header=reason_header) + + def _makeEnviron(self, login=None, password=None, came_from=None, + path_info='/', identifier=None, max_age=None): + from repoze.who._compat 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_class_conforms_to_IChallenger(self): + from zope.interface.verify import verifyClass + from repoze.who.interfaces import IChallenger + verifyClass(IChallenger, self._getTargetClass()) + + def test_instance_conforms_to_IChallenger(self): + from zope.interface.verify import verifyObject + from repoze.who.interfaces import IChallenger + verifyObject(IChallenger, self._makeOne()) + + def test_ctor_w_reason_param_wo_reason_header(self): + self.assertRaises(ValueError, self._makeOne, + reason_param='reason', + reason_header=None) + + def test_ctor_wo_reason_param_w_reason_header(self): + self.assertRaises(ValueError, self._makeOne, + reason_param=None, + reason_header='X-Reason') + + def test_challenge(self): + from ..._compat import parse_qsl + from ..._compat import urlparse + plugin = self._makeOne(came_from_param='came_from', + reason_param='reason', + reason_header='X-Authorization-Failure-Reason', + ) + environ = self._makeEnviron() + app = plugin.challenge(environ, '401 Unauthorized', [('app', '1')], + [('forget', '1')]) + sr = DummyStartResponse() + result = b''.join(app(environ, sr)).decode('ascii') + self.failUnless(result.startswith('302 Found')) + self.assertEqual(sr.headers[0][0], 'forget') + self.assertEqual(sr.headers[0][1], '1') + self.assertEqual(sr.headers[1][0], 'Location') + url = sr.headers[1][1] + parts = urlparse(url) + parts_qsl = 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(sr.headers[2][0], 'Content-Length') + self.assertEqual(sr.headers[2][1], '165') + self.assertEqual(sr.headers[3][0], 'Content-Type') + self.assertEqual(sr.headers[3][1], 'text/plain; charset=UTF-8') + self.assertEqual(sr.status, '302 Found') + + def test_challenge_with_reason_header(self): + from ..._compat import parse_qsl + from ..._compat import urlparse + plugin = self._makeOne(came_from_param='came_from', + reason_param='reason', + reason_header='X-Authorization-Failure-Reason', + ) + environ = self._makeEnviron() + app = plugin.challenge( + environ, '401 Unauthorized', + [('X-Authorization-Failure-Reason', 'you are ugly')], + [('forget', '1')]) + sr = DummyStartResponse() + result = b''.join(app(environ, sr)).decode('ascii') + self.failUnless(result.startswith('302 Found')) + self.assertEqual(sr.headers[1][0], 'Location') + url = sr.headers[1][1] + parts = urlparse(url) + parts_qsl = 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://www.example.com/?default=1') + self.assertEqual(reason_key, 'reason') + self.assertEqual(reason_value, 'you are ugly') + + def test_challenge_with_custom_reason_header(self): + from ..._compat import parse_qsl + from ..._compat import urlparse + plugin = self._makeOne(came_from_param='came_from', + reason_param='reason', + reason_header='X-Custom-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 = b''.join(app(environ, sr)).decode('ascii') + self.failUnless(result.startswith('302 Found')) + self.assertEqual(sr.headers[1][0], 'Location') + url = sr.headers[1][1] + parts = urlparse(url) + parts_qsl = 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') + + def test_challenge_w_reason_no_reason_param_no_came_from_param(self): + from ..._compat import parse_qsl + from ..._compat import urlparse + plugin = self._makeOne() + environ = self._makeEnviron() + app = plugin.challenge( + environ, '401 Unauthorized', + [('X-Authorization-Failure-Reason', 'you are ugly')], + [('forget', '1')]) + sr = DummyStartResponse() + result = b''.join(app(environ, sr)).decode('ascii') + self.failUnless(result.startswith('302 Found')) + self.assertEqual(sr.headers[0][0], "forget") + self.assertEqual(sr.headers[0][1], "1") + self.assertEqual(sr.headers[1][0], 'Location') + url = sr.headers[1][1] + parts = urlparse(url) + parts_qsl = parse_qsl(parts[4]) + self.assertEqual(len(parts_qsl), 0) + self.assertEqual(parts[0], 'http') + self.assertEqual(parts[1], 'example.com') + self.assertEqual(parts[2], '/login.html') + self.assertEqual(parts[3], '') + + def test_challenge_w_reason_no_reason_param_w_came_from_param(self): + from ..._compat import parse_qsl + from ..._compat import urlparse + plugin = self._makeOne(came_from_param='came_from', + ) + 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 = b''.join(app(environ, sr)).decode('ascii') + self.failUnless(result.startswith('302 Found')) + self.assertEqual(sr.headers[1][0], 'Location') + url = sr.headers[1][1] + parts = urlparse(url) + parts_qsl = 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') + + def test_challenge_with_reason_and_custom_reason_param(self): + from ..._compat import parse_qsl + from ..._compat import urlparse + plugin = self._makeOne(came_from_param='came_from', + reason_param='auth_failure', + reason_header='X-Custom-Auth-Failure', + ) + environ = self._makeEnviron() + app = plugin.challenge( + environ, '401 Unauthorized', + [('X-Authorization-Failure-Reason', 'wrong reason'), + ('X-Custom-Auth-Failure', 'you are ugly')], + [('forget', '1')]) + sr = DummyStartResponse() + result = b''.join(app(environ, sr)).decode('ascii') + self.failUnless(result.startswith('302 Found')) + self.assertEqual(sr.headers[1][0], 'Location') + url = sr.headers[1][1] + parts = urlparse(url) + parts_qsl = 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://www.example.com/?default=1') + self.assertEqual(reason_key, 'auth_failure') + self.assertEqual(reason_value, 'you are ugly') + + def test_challenge_wo_reason_w_came_from_param(self): + from ..._compat import parse_qsl + from ..._compat import urlparse + plugin = self._makeOne(came_from_param='came_from') + environ = self._makeEnviron() + app = plugin.challenge( + environ, '401 Unauthorized', + [], + [('forget', '1')]) + sr = DummyStartResponse() + result = b''.join(app(environ, sr)).decode('ascii') + self.failUnless(result.startswith('302 Found')) + self.assertEqual(sr.headers[1][0], 'Location') + url = sr.headers[1][1] + parts = urlparse(url) + parts_qsl = 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') + + def test_challenge_with_setcookie_from_app(self): + plugin = self._makeOne(came_from_param='came_from', + reason_param='reason', + reason_header='X-Authorization-Failure-Reason', + ) + environ = self._makeEnviron() + app = plugin.challenge( + environ, + '401 Unauthorized', + [('app', '1'), ('set-cookie','a'), ('set-cookie','b')], + []) + sr = DummyStartResponse() + result = b''.join(app(environ, sr)).decode('ascii') + self.failUnless(result.startswith('302 Found')) + self.assertEqual(sr.headers[0][0], 'set-cookie') + self.assertEqual(sr.headers[0][1], 'a') + self.assertEqual(sr.headers[1][0], 'set-cookie') + self.assertEqual(sr.headers[1][1], 'b') + +class Test_make_redirecting_plugin(_Base): + + def _callFUT(self, *args, **kw): + from repoze.who.plugins.redirector import make_plugin + return make_plugin(*args, **kw) + + def test_no_login_url_raises(self): + self.assertRaises(ValueError, self._callFUT, None) + + def test_defaults(self): + plugin = self._callFUT('/go_there') + self.assertEqual(plugin.login_url, '/go_there') + self.assertEqual(plugin.came_from_param, None) + self.assertEqual(plugin.reason_param, None) + self.assertEqual(plugin.reason_header, None) + + def test_explicit_came_from_param(self): + plugin = self._callFUT('/go_there', came_from_param='whence') + self.assertEqual(plugin.login_url, '/go_there') + self.assertEqual(plugin.came_from_param, 'whence') + self.assertEqual(plugin.reason_param, None) + self.assertEqual(plugin.reason_header, None) + + def test_explicit_reason_param(self): + plugin = self._callFUT('/go_there', reason_param='why') + self.assertEqual(plugin.login_url, '/go_there') + self.assertEqual(plugin.came_from_param, None) + self.assertEqual(plugin.reason_param, 'why') + self.assertEqual(plugin.reason_header, 'X-Authorization-Failure-Reason') + + def test_explicit_reason_header_param_no_reason_param_raises(self): + self.assertRaises(Exception, self._callFUT, '/go_there', + reason_header='X-Reason') + + def test_explicit_reason_header_param(self): + plugin = self._callFUT('/go_there', reason_param='why', + reason_header='X-Reason') + self.assertEqual(plugin.login_url, '/go_there') + self.assertEqual(plugin.came_from_param, None) + self.assertEqual(plugin.reason_param, 'why') + self.assertEqual(plugin.reason_header, 'X-Reason') + +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 diff -Nru python-repoze.who-1.0.18/repoze/who/plugins/tests/test_sql.py python-repoze.who-2.2/repoze/who/plugins/tests/test_sql.py --- python-repoze.who-1.0.18/repoze/who/plugins/tests/test_sql.py 2009-05-08 20:08:36.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/plugins/tests/test_sql.py 2012-03-19 19:42:14.000000000 +0000 @@ -1,6 +1,14 @@ import unittest -class TestSQLAuthenticatorPlugin(unittest.TestCase): +class _Base(unittest.TestCase): + + def failUnless(self, predicate, message=''): + self.assertTrue(predicate, message) # Nannies go home! + + def failIf(self, predicate, message=''): + self.assertFalse(predicate, message) # Nannies go home! + +class TestSQLAuthenticatorPlugin(_Base): def _getTargetClass(self): from repoze.who.plugins.sql import SQLAuthenticatorPlugin @@ -67,7 +75,7 @@ self.assertEqual(dummy_factory.query, None) self.assertEqual(dummy_factory.closed, False) -class TestDefaultPasswordCompare(unittest.TestCase): +class TestDefaultPasswordCompare(_Base): def _getFUT(self): from repoze.who.plugins.sql import default_password_compare @@ -78,6 +86,8 @@ from hashlib import sha1 except ImportError: from sha import new as sha1 + if not isinstance(clear, type(b'')): + clear = clear.encode('utf-8') return sha1(clear).hexdigest() def test_shaprefix_success(self): @@ -86,6 +96,13 @@ result = compare('password', stored) self.assertEqual(result, True) + def test_shaprefix_w_unicode_cleartext(self): + from repoze.who._compat import u + stored = '{SHA}' + self._get_sha_hex_digest() + compare = self._getFUT() + result = compare(u('password'), stored) + self.assertEqual(result, True) + def test_shaprefix_fail(self): stored = '{SHA}' + self._get_sha_hex_digest() compare = self._getFUT() @@ -104,7 +121,7 @@ result = compare('notpassword', stored) self.assertEqual(result, False) -class TestSQLMetadataProviderPlugin(unittest.TestCase): +class TestSQLMetadataProviderPlugin(_Base): def _getTargetClass(self): from repoze.who.plugins.sql import SQLMetadataProviderPlugin @@ -132,9 +149,9 @@ 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')) + self.failIf('__userid' in identity) -class TestMakeSQLAuthenticatorPlugin(unittest.TestCase): +class TestMakeSQLAuthenticatorPlugin(_Base): def _getFUT(self): from repoze.who.plugins.sql import make_authenticator_plugin @@ -171,7 +188,7 @@ self.assertEqual(plugin.conn_factory, DummyConnFactory) self.assertEqual(plugin.compare_fn, make_dummy_connfactory) -class TestMakeSQLMetadataProviderPlugin(unittest.TestCase): +class TestMakeSQLMetadataProviderPlugin(_Base): def _getFUT(self): from repoze.who.plugins.sql import make_metadata_plugin diff -Nru python-repoze.who-1.0.18/repoze/who/restrict.py python-repoze.who-2.2/repoze/who/restrict.py --- python-repoze.who-1.0.18/repoze/who/restrict.py 2009-05-08 18:38:09.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/restrict.py 2012-03-19 19:42:14.000000000 +0000 @@ -1,6 +1,8 @@ # Authorization middleware from pkg_resources import EntryPoint +from repoze.who._compat import STRING_TYPES + def authenticated_predicate(): def _predicate(environ): return 'REMOTE_USER' in environ or 'repoze.who.identity' in environ @@ -17,7 +19,7 @@ def __call__(self, environ, start_response): if self.enabled: if not self.predicate(environ): - start_response('401 Unauthorized', ()) + start_response('401 Unauthorized', []) return [] return self.app(environ, start_response) @@ -26,6 +28,6 @@ def make_predicate_restriction(app, global_config, predicate, enabled=True, **kw): - if isinstance(predicate, basestring): + if isinstance(predicate, STRING_TYPES): predicate = EntryPoint.parse('x=%s' % predicate).load(False) return PredicateRestriction(app, predicate, enabled, **kw) diff -Nru python-repoze.who-1.0.18/repoze/who/tests/test_api.py python-repoze.who-2.2/repoze/who/tests/test_api.py --- python-repoze.who-1.0.18/repoze/who/tests/test_api.py 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/tests/test_api.py 2012-03-19 19:42:14.000000000 +0000 @@ -0,0 +1,1253 @@ +import unittest + +class _Base(unittest.TestCase): + + def failUnless(self, predicate, message=''): + self.assertTrue(predicate, message) # Nannies go home! + + def failIf(self, predicate, message=''): + self.assertFalse(predicate, message) # Nannies go home! + +class Test_get_api(_Base): + + def _callFUT(self, environ): + from repoze.who.api import get_api + return get_api(environ) + + def test___call___empty_environ(self): + environ = {} + api = self._callFUT(environ) + self.failUnless(api is None) + + def test___call___w_api_in_environ(self): + expected = object() + environ = {'repoze.who.api': expected} + api = self._callFUT(environ) + self.failUnless(api is expected) + +class APIFactoryTests(_Base): + + def _getTargetClass(self): + from repoze.who.api import APIFactory + return APIFactory + + def _makeOne(self, + plugins=None, + identifiers=None, + authenticators=None, + challengers=None, + mdproviders=None, + request_classifier=None, + challenge_decider=None, + remote_user_key=None, + logger=None, + ): + if plugins is None: + plugins = {} + if identifiers is None: + identifiers = () + if authenticators is None: + authenticators = () + if challengers is None: + challengers = () + if mdproviders is None: + mdproviders = () + return self._getTargetClass()(identifiers, + authenticators, + challengers, + mdproviders, + request_classifier, + challenge_decider, + remote_user_key, + logger, + ) + + def test_class_conforms_to_IAPIFactory(self): + from zope.interface.verify import verifyClass + from repoze.who.interfaces import IAPIFactory + verifyClass(IAPIFactory, self._getTargetClass()) + + def test_instance_conforms_to_IAPIFactory(self): + from zope.interface.verify import verifyObject + from repoze.who.interfaces import IAPIFactory + verifyObject(IAPIFactory, self._makeOne()) + + def test_ctor_defaults(self): + factory = self._makeOne() + self.assertEqual(len(factory.identifiers), 0) + self.assertEqual(len(factory.authenticators), 0) + self.assertEqual(len(factory.challengers), 0) + self.assertEqual(len(factory.mdproviders), 0) + self.assertEqual(factory.request_classifier, None) + self.assertEqual(factory.challenge_decider, None) + self.assertEqual(factory.logger, None) + + def test___call___empty_environ(self): + from repoze.who.api import API + environ = {} + factory = self._makeOne() + api = factory(environ) + self.failUnless(isinstance(api, API)) + self.failUnless(environ['repoze.who.api'] is api) + + def test___call___w_api_in_environ(self): + expected = object() + environ = {'repoze.who.api': expected} + factory = self._makeOne() + api = factory(environ) + self.failUnless(api is expected) + + +class TestMakeRegistries(_Base): + + def _callFUT(self, identifiers, authenticators, challengers, mdproviders): + from repoze.who.api import make_registries + return make_registries(identifiers, authenticators, + challengers, mdproviders) + + def test_empty(self): + iface_reg, name_reg = self._callFUT([], [], [], []) + self.assertEqual(iface_reg, {}) + self.assertEqual(name_reg, {}) + + def test_brokenimpl(self): + self.assertRaises(ValueError, self._callFUT, + [(None, object())], [], [], []) + + def test_ok(self): + from repoze.who.interfaces import IIdentifier + from repoze.who.interfaces import IAuthenticator + from repoze.who.interfaces import IChallenger + from repoze.who.interfaces import IMetadataProvider + 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 = [ ('mdprovider', dummy_mdprovider) ] + iface_reg, name_reg = self._callFUT(identifiers, authenticators, + challengers, mdproviders) + 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(iface_reg[IMetadataProvider], [dummy_mdprovider]) + 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) + self.assertEqual(name_reg['mdprovider'], dummy_mdprovider) + +class TestMatchClassification(_Base): + + def _getFUT(self): + from repoze.who.api 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 APITests(_Base): + + def _getTargetClass(self): + from repoze.who.api import API + return API + + def _makeOne(self, + environ=None, + identifiers=None, + authenticators=None, + challengers=None, + request_classifier=None, + mdproviders=None, + challenge_decider=None, + remote_user_key=None, + logger=None + ): + if environ is None: + environ = {} + if identifiers is None: + identifiers = [] + if authenticators is None: + authenticators = [] + if challengers is None: + challengers = [] + if request_classifier is None: + request_classifier = DummyRequestClassifier() + if mdproviders is None: + mdproviders = [] + if challenge_decider is None: + challenge_decider = DummyChallengeDecider() + api = self._getTargetClass()(environ, + identifiers, + authenticators, + challengers, + mdproviders, + request_classifier, + challenge_decider, + remote_user_key, + logger, + ) + return api + + def _makeEnviron(self): + from wsgiref.util import setup_testing_defaults + environ = {} + setup_testing_defaults(environ) + return environ + + def test_class_conforms_to_IAPI(self): + from zope.interface.verify import verifyClass + from repoze.who.interfaces import IAPI + verifyClass(IAPI, self._getTargetClass()) + + def test_ctor_accepts_logger_instance(self): + logger = DummyLogger() + api = self._makeOne(logger=logger) + self.assertEqual(len(logger._info), 1) + self.assertEqual(logger._info[0], 'request classification: browser') + self.assertEqual(len(logger._debug), 0) + + def test_authenticate_no_identities(self): + logger = DummyLogger() + environ = self._makeEnviron() + plugin = DummyNoResultsIdentifier() + plugins = [ ('dummy', plugin) ] + api = self._makeOne(environ=environ, + identifiers=plugins, + logger=logger) + identity = api.authenticate() + self.assertEqual(identity, None) + self.assertEqual(len(logger._info), 2) + self.assertEqual(logger._info[0], 'request classification: browser') + self.assertEqual(logger._info[1], 'no identities found, ' + 'not authenticating') + + def test_authenticate_w_identities_no_authenticators(self): + logger = DummyLogger() + environ = self._makeEnviron() + credentials = {'login':'chris', 'password':'password'} + identifier = DummyIdentifier(credentials) + identifiers = [ ('i', identifier) ] + api = self._makeOne(environ=environ, + identifiers=identifiers, logger=logger) + identity = api.authenticate() + self.assertEqual(identity, None) + self.assertEqual(len(logger._info), 2) + self.assertEqual(logger._info[0], 'request classification: browser') + # Hmm, should this message distinguish "none found" from + # "none authenticated"? + self.assertEqual(logger._info[1], 'no identities found, ' + 'not authenticating') + + #def test_authenticate_w_identities_w_authenticators_miss(self): + def test_authenticate_w_identities_w_authenticators_hit(self): + logger = DummyLogger() + environ = self._makeEnviron() + credentials = {'login':'chris', 'password':'password'} + identifier = DummyIdentifier(credentials) + identifiers = [ ('i', identifier) ] + authenticator = DummyAuthenticator('chrisid') + authenticators = [ ('a', authenticator) ] + api = self._makeOne(environ=environ, + identifiers=identifiers, + authenticators=authenticators, + logger=logger) + identity = api.authenticate() + self.assertEqual(identity['repoze.who.userid'], 'chrisid') + self.failUnless(identity['identifier'] is identifier) + self.failUnless(identity['authenticator'] is authenticator) + + self.assertEqual(len(logger._info), 1) + self.assertEqual(logger._info[0], 'request classification: browser') + + def test_challenge_noidentifier_noapp(self): + logger = DummyLogger() + identity = {'login':'chris', 'password':'password'} + environ = self._makeEnviron() + environ['repoze.who.identity'] = identity + challenger = DummyChallenger() + plugins = [ ('challenge', challenger) ] + api = self._makeOne(environ=environ, + challengers=plugins, + request_classifier=lambda environ: 'match', + logger=logger, + ) + app = api.challenge('401 Unauthorized', []) + self.assertEqual(app, None) + self.assertEqual(environ['challenged'], None) + self.assertEqual(len(logger._info), 2) + self.assertEqual(logger._info[0], 'request classification: match') + self.assertEqual(logger._info[1], 'no challenge app returned') + self.assertEqual(len(logger._debug), 2) + self.failUnless(logger._debug[0].startswith( + 'challengers registered: [')) + self.failUnless(logger._debug[1].startswith( + 'challengers matched for ' + 'classification "match": [')) + + def test_challenge_noidentifier_with_app(self): + logger = DummyLogger() + identity = {'login':'chris', 'password':'password'} + environ = self._makeEnviron() + environ['repoze.who.identity'] = identity + app = DummyApp() + challenger = DummyChallenger(app) + plugins = [ ('challenge', challenger) ] + api = self._makeOne(environ=environ, + challengers=plugins, + request_classifier=lambda environ: 'match', + logger=logger, + ) + result = api.challenge('401 Unauthorized', []) + self.assertEqual(result, app) + self.assertEqual(environ['challenged'], app) + self.assertEqual(len(logger._info), 2) + self.assertEqual(logger._info[0], 'request classification: match') + self.failUnless(logger._info[1].startswith('challenger plugin ')) + self.failUnless(logger._info[1].endswith( + '"challenge" returned an app')) + self.assertEqual(len(logger._debug), 2) + self.failUnless(logger._debug[0].startswith( + 'challengers registered: [')) + self.failUnless(logger._debug[1].startswith( + 'challengers matched for ' + 'classification "match": [')) + + def test_challenge_identifier_no_app_no_forget_headers(self): + logger = DummyLogger() + credentials = {'login':'chris', 'password':'password'} + identifier = DummyIdentifier(credentials) + identity = {'login':'chris', + 'password':'password', + 'identifier': identifier} + environ = self._makeEnviron() + environ['repoze.who.identity'] = identity + challenger = DummyChallenger() + plugins = [ ('challenge', challenger) ] + api = self._makeOne(environ=environ, + challengers=plugins, + request_classifier=lambda environ: 'match', + logger=logger, + ) + result = api.challenge('401 Unauthorized', []) + self.assertEqual(result, None) + self.assertEqual(environ['challenged'], None) + self.assertEqual(identifier.forgotten, identity) + self.assertEqual(len(logger._info), 2) + self.assertEqual(logger._info[0], 'request classification: match') + self.assertEqual(logger._info[1], 'no challenge app returned') + self.assertEqual(len(logger._debug), 2) + self.failUnless(logger._debug[0].startswith( + 'challengers registered: [')) + self.failUnless(logger._debug[1].startswith( + 'challengers matched for ' + 'classification "match": [')) + + def test_challenge_identifier_app_no_forget_headers(self): + logger = DummyLogger() + credentials = {'login':'chris', 'password':'password'} + identifier = DummyIdentifier(credentials) + identity = {'login':'chris', + 'password':'password', + 'identifier': identifier} + environ = self._makeEnviron() + environ['repoze.who.identity'] = identity + app = DummyApp() + challenger = DummyChallenger(app) + plugins = [ ('challenge', challenger) ] + api = self._makeOne(environ=environ, + challengers=plugins, + request_classifier=lambda environ: 'match', + logger=logger, + ) + result = api.challenge('401 Unauthorized', []) + self.assertEqual(result, app) + self.assertEqual(environ['challenged'], app) + self.assertEqual(identifier.forgotten, identity) + self.assertEqual(len(logger._info), 2) + self.assertEqual(logger._info[0], 'request classification: match') + self.failUnless(logger._info[1].startswith('challenger plugin ')) + self.failUnless(logger._info[1].endswith( + '"challenge" returned an app')) + self.assertEqual(len(logger._debug), 2) + self.failUnless(logger._debug[0].startswith( + 'challengers registered: [')) + self.failUnless(logger._debug[1].startswith( + 'challengers matched for ' + 'classification "match": [')) + + def test_challenge_identifier_no_app_forget_headers(self): + FORGET_HEADERS = [('X-testing-forget', 'Oubliez!')] + logger = DummyLogger() + credentials = {'login':'chris', 'password':'password'} + identifier = DummyIdentifier(credentials, + forget_headers=FORGET_HEADERS) + identity = {'login':'chris', + 'password':'password', + 'identifier': identifier} + environ = self._makeEnviron() + environ['repoze.who.identity'] = identity + app = DummyApp() + challenger = DummyChallenger(app) + plugins = [ ('challenge', challenger) ] + api = self._makeOne(environ=environ, + challengers=plugins, + request_classifier=lambda environ: 'match', + logger=logger, + ) + result = api.challenge('401 Unauthorized', []) + self.assertEqual(result, app) + self.assertEqual(environ['challenged'], app) + self.assertEqual(challenger._challenged_with[3], FORGET_HEADERS) + self.assertEqual(len(logger._info), 3) + self.assertEqual(logger._info[0], 'request classification: match') + self.failUnless(logger._info[1].startswith( + 'forgetting via headers from')) + self.failUnless(logger._info[1].endswith(repr(FORGET_HEADERS))) + self.failUnless(logger._info[2].startswith('challenger plugin ')) + self.failUnless(logger._info[2].endswith( + '"challenge" returned an app')) + self.assertEqual(len(logger._debug), 2) + self.failUnless(logger._debug[0].startswith( + 'challengers registered: [')) + self.failUnless(logger._debug[1].startswith( + 'challengers matched for ' + 'classification "match": [')) + + def test_multi_challenge_firstwins(self): + credentials = {'login':'chris', 'password':'password'} + identifier = DummyIdentifier(credentials) + identity = {'login':'chris', + 'password':'password', + 'identifier': identifier} + environ = self._makeEnviron() + environ['repoze.who.identity'] = identity + app1 = DummyApp() + app2 = DummyApp() + challenger1 = DummyChallenger(app1) + challenger2 = DummyChallenger(app2) + plugins = [ ('challenge1', challenger1), ('challenge2', challenger2) ] + api = self._makeOne(environ=environ, challengers=plugins, + request_classifier=lambda environ: 'match') + result = api.challenge('401 Unauthorized', []) + self.assertEqual(result, app1) + self.assertEqual(environ['challenged'], app1) + self.assertEqual(identifier.forgotten, identity) + + def test_multi_challenge_skipnomatch_findimplicit(self): + from repoze.who.interfaces import IChallenger + credentials = {'login':'chris', 'password':'password'} + identifier = DummyIdentifier(credentials) + identity = {'login':'chris', + 'password':'password', + 'identifier': identifier} + environ = self._makeEnviron() + environ['repoze.who.identity'] = identity + app1 = DummyApp() + app2 = DummyApp() + challenger1 = DummyChallenger(app1) + challenger1.classifications = {IChallenger:['nomatch']} + challenger2 = DummyChallenger(app2) + challenger2.classifications = {IChallenger:None} + plugins = [ ('challenge1', challenger1), ('challenge2', challenger2) ] + api = self._makeOne(environ=environ, challengers=plugins, + request_classifier=lambda environ: 'match') + result = api.challenge('401 Unauthorized', []) + self.assertEqual(result, app2) + self.assertEqual(environ['challenged'], app2) + self.assertEqual(identifier.forgotten, identity) + + def test_multi_challenge_skipnomatch_findexplicit(self): + from repoze.who.interfaces import IChallenger + credentials = {'login':'chris', 'password':'password'} + identifier = DummyIdentifier(credentials) + identity = {'login':'chris', + 'password':'password', + 'identifier': identifier} + environ = self._makeEnviron() + environ['repoze.who.identity'] = identity + app1 = DummyApp() + app2 = DummyApp() + challenger1 = DummyChallenger(app1) + challenger1.classifications = {IChallenger:['nomatch']} + challenger2 = DummyChallenger(app2) + challenger2.classifications = {IChallenger:['match']} + plugins = [ ('challenge1', challenger1), ('challenge2', challenger2) ] + api = self._makeOne(environ=environ, challengers=plugins, + request_classifier=lambda environ: 'match') + result = api.challenge('401 Unauthorized', []) + self.assertEqual(result, app2) + self.assertEqual(environ['challenged'], app2) + self.assertEqual(identifier.forgotten, identity) + + def test_remember_identifier_plugin_returns_none(self): + class _Identifier: + def identify(self, environ): + return None + def remember(self, environ, identity): + return () + def forget(self, environ, identity): + return () + identity = {'identifier': _Identifier()} + api = self._makeOne() + headers = api.remember(identity=identity) + self.assertEqual(tuple(headers), ()) + + def test_remember_no_identity_passed_or_in_environ(self): + logger = DummyLogger() + environ = self._makeEnviron() + api = self._makeOne(environ=environ) + self.assertEqual(len(api.remember()), 0) + self.assertEqual(len(logger._info), 0) + self.assertEqual(len(logger._debug), 0) + + def test_remember_no_identity_passed_but_in_environ(self): + HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] + logger = DummyLogger() + class _Identifier: + def remember(self, environ, identity): + return HEADERS + environ = self._makeEnviron() + environ['repoze.who.identity'] = {'identifier': _Identifier()} + api = self._makeOne(environ=environ, logger=logger) + self.assertEqual(api.remember(), HEADERS) + self.assertEqual(len(logger._info), 2) + self.assertEqual(logger._info[0], 'request classification: browser') + self.failUnless(logger._info[1].startswith( + 'remembering via headers from')) + self.failUnless(logger._info[1].endswith(repr(HEADERS))) + self.assertEqual(len(logger._debug), 0) + + def test_remember_w_identity_passed_no_identifier(self): + logger = DummyLogger() + environ = self._makeEnviron() + api = self._makeOne(environ=environ, logger=logger) + identity = {} + self.assertEqual(len(api.remember(identity)), 0) + self.assertEqual(len(logger._info), 1) + self.assertEqual(logger._info[0], 'request classification: browser') + self.assertEqual(len(logger._debug), 0) + + def test_remember_w_identity_passed_w_identifier(self): + HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] + logger = DummyLogger() + class _Identifier: + def remember(self, environ, identity): + return HEADERS + environ = self._makeEnviron() + api = self._makeOne(environ=environ, logger=logger) + identity = {'identifier': _Identifier()} + self.assertEqual(api.remember(identity), HEADERS) + self.assertEqual(len(logger._info), 2) + self.assertEqual(logger._info[0], 'request classification: browser') + self.failUnless(logger._info[1].startswith( + 'remembering via headers from')) + self.failUnless(logger._info[1].endswith(repr(HEADERS))) + self.assertEqual(len(logger._debug), 0) + + def test_forget_identifier_plugin_returns_none(self): + class _Identifier: + def identify(self, environ): + return None + def remember(self, environ, identity): + return () + def forget(self, environ, identity): + return () + identity = {'identifier': _Identifier()} + api = self._makeOne() + headers = api.forget(identity=identity) + self.assertEqual(tuple(headers), ()) + + def test_forget_no_identity_passed_or_in_environ(self): + logger = DummyLogger() + environ = self._makeEnviron() + api = self._makeOne(environ=environ, logger=logger) + self.assertEqual(len(api.forget()), 0) + self.assertEqual(len(logger._info), 1) + self.assertEqual(logger._info[0], 'request classification: browser') + self.assertEqual(len(logger._debug), 0) + + def test_forget_no_identity_passed_but_in_environ(self): + HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] + logger = DummyLogger() + class _Identifier: + def forget(self, environ, identity): + return HEADERS + environ = self._makeEnviron() + environ['repoze.who.identity'] = {'identifier': _Identifier()} + api = self._makeOne(environ=environ, logger=logger) + self.assertEqual(api.forget(), HEADERS) + self.assertEqual(len(logger._info), 2) + self.assertEqual(logger._info[0], 'request classification: browser') + self.failUnless(logger._info[1].startswith( + 'forgetting via headers from')) + self.failUnless(logger._info[1].endswith(repr(HEADERS))) + self.assertEqual(len(logger._debug), 0) + + def test_forget_w_identity_passed_no_identifier(self): + environ = self._makeEnviron() + logger = DummyLogger() + api = self._makeOne(environ=environ, logger=logger) + identity = {} + self.assertEqual(len(api.forget(identity)), 0) + self.assertEqual(len(logger._info), 1) + self.assertEqual(logger._info[0], 'request classification: browser') + self.assertEqual(len(logger._debug), 0) + + def test_forget_w_identity_passed_w_identifier(self): + HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] + logger = DummyLogger() + class _Identifier: + def forget(self, environ, identity): + return HEADERS + environ = self._makeEnviron() + api = self._makeOne(environ=environ, logger=logger) + identity = {'identifier': _Identifier()} + self.assertEqual(api.forget(identity), HEADERS) + self.assertEqual(len(logger._info), 2) + self.assertEqual(logger._info[0], 'request classification: browser') + self.failUnless(logger._info[1].startswith( + 'forgetting via headers from')) + self.failUnless(logger._info[1].endswith(repr(HEADERS))) + self.assertEqual(len(logger._debug), 0) + + def test_login_w_identifier_name_hit(self): + REMEMBER_HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] + FORGET_HEADERS = [('Spam', 'Blah')] + class _Identifier: + def identify(self, environ): + pass + def remember(self, environ, identity): + return REMEMBER_HEADERS[1:] + def forget(self, environ, identity): + return FORGET_HEADERS + class _BogusIdentifier: + def identify(self, environ): + pass + def remember(self, environ, identity): + return REMEMBER_HEADERS[:1] + def forget(self, environ, identity): + pass + authenticator = DummyAuthenticator('chrisid') + environ = self._makeEnviron() + identifiers = [('bogus', _BogusIdentifier()), + ('valid', _Identifier()), + ] + api = self._makeOne(identifiers=identifiers, + authenticators=[('authentic', authenticator)], + environ=environ) + identity, headers = api.login({'login': 'chrisid'}, 'valid') + self.assertEqual(identity['repoze.who.userid'], 'chrisid') + self.assertEqual(headers, REMEMBER_HEADERS[1:]) + + def test_login_wo_identifier_name_hit(self): + REMEMBER_HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] + FORGET_HEADERS = [('Spam', 'Blah')] + class _Identifier: + def identify(self, environ): + pass + def remember(self, environ, identity): + return REMEMBER_HEADERS[1:] + def forget(self, environ, identity): + return FORGET_HEADERS + class _BogusIdentifier: + def identify(self, environ): + pass + def remember(self, environ, identity): + return REMEMBER_HEADERS[:1] + def forget(self, environ, identity): + pass + authenticator = DummyAuthenticator('chrisid') + environ = self._makeEnviron() + identifiers = [('bogus', _BogusIdentifier()), + ('valid', _Identifier()), + ] + api = self._makeOne(identifiers=identifiers, + authenticators=[('authentic', authenticator)], + environ=environ) + identity, headers = api.login({'login': 'chrisid'}) + self.assertEqual(identity['repoze.who.userid'], 'chrisid') + self.assertEqual(headers, REMEMBER_HEADERS) + + def test_login_w_identifier_name_miss(self): + REMEMBER_HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] + FORGET_HEADERS = [('Spam', 'Blah')] + class _Identifier: + def identify(self, environ): + pass + def remember(self, environ, identity): + return REMEMBER_HEADERS + def forget(self, environ, identity): + return FORGET_HEADERS + class _BogusIdentifier: + def identify(self, environ): + pass + def remember(self, environ, identity): + return () + def forget(self, environ, identity): + return () + authenticator = DummyFailAuthenticator() + environ = self._makeEnviron() + identifiers = [('bogus', _BogusIdentifier()), + ('valid', _Identifier()), + ] + api = self._makeOne(identifiers=identifiers, + authenticators=[('authentic', authenticator)], + environ=environ) + identity, headers = api.login({'login': 'notchrisid'}, 'valid') + self.assertEqual(identity, None) + self.assertEqual(headers, FORGET_HEADERS) + + def test_logout_wo_identifier_name_miss(self): + FORGET_HEADERS = [('Spam', 'Blah')] + class _Identifier: + def identify(self, environ): + pass + def remember(self, environ, identity): + return () + def forget(self, environ, identity): + return FORGET_HEADERS[:1] + class _BogusIdentifier: + def identify(self, environ): + pass + def remember(self, environ, identity): + return () + def forget(self, environ, identity): + return FORGET_HEADERS[1:] + environ = self._makeEnviron() + identifiers = [('valid', _Identifier()), + ('bogus', _BogusIdentifier()), + ] + api = self._makeOne(identifiers=identifiers, + environ=environ) + headers = api.logout() + self.assertEqual(headers, FORGET_HEADERS) + + def test_logout_w_identifier_name(self): + FORGET_HEADERS = [('Spam', 'Blah')] + class _Identifier: + def identify(self, environ): + pass + def remember(self, environ, identity): + return () + def forget(self, environ, identity): + return FORGET_HEADERS + class _BogusIdentifier: + def identify(self, environ): + pass + def remember(self, environ, identity): + return () + def forget(self, environ, identity): + return () + environ = self._makeEnviron() + identifiers = [('bogus', _BogusIdentifier()), + ('valid', _Identifier()), + ] + api = self._makeOne(identifiers=identifiers, + environ=environ) + headers = api.logout('valid') + self.assertEqual(headers, FORGET_HEADERS) + + def test_logout_wo_identifier_name(self): + REMEMBER_HEADERS = [('Foo', 'Bar'), ('Baz', 'Qux')] + FORGET_HEADERS = [('Spam', 'Blah')] + class _Identifier: + def identify(self, environ): + pass + def remember(self, environ, identity): + return REMEMBER_HEADERS + def forget(self, environ, identity): + return FORGET_HEADERS + class _BogusIdentifier: + def identify(self, environ): + pass + def remember(self, environ, identity): + return () + def forget(self, environ, identity): + return () + authenticator = DummyFailAuthenticator() + environ = self._makeEnviron() + identifiers = [('valid', _Identifier()), + ('bogus', _BogusIdentifier()), + ] + api = self._makeOne(identifiers=identifiers, + authenticators=[('authentic', authenticator)], + environ=environ) + headers = api.logout() + self.assertEqual(headers, FORGET_HEADERS) + + def test_logout_removes_repoze_who_identity(self): + class _Identifier: + def identify(self, environ): + pass + def forget(self, environ, identity): + return () + def remember(self, environ, identity): + return () + authenticator = DummyFailAuthenticator() + environ = self._makeEnviron() + environ['repoze.who.identity'] = 'identity' + identifiers = [('valid', _Identifier())] + api = self._makeOne(identifiers=identifiers, + authenticators=[('authentic', authenticator)], + environ=environ) + api.logout() + self.failIf('repoze.who.identity' in environ) + + def test__identify_success(self): + environ = self._makeEnviron() + credentials = {'login':'chris', 'password':'password'} + identifier = DummyIdentifier(credentials) + identifiers = [ ('i', identifier) ] + api = self._makeOne(environ=environ, identifiers=identifiers) + results = api._identify() + 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) ] + api = self._makeOne(environ=environ, identifiers=identifiers) + results = api._identify() + self.assertEqual(len(results), 1) + new_identifier, identity = results[0] + self.assertEqual(new_identifier, identifier) + self.assertEqual(identity, {}) + + def test__identify_fail(self): + logger = DummyLogger() + environ = self._makeEnviron() + plugin = DummyNoResultsIdentifier() + plugins = [ ('dummy', plugin) ] + api = self._makeOne(environ=environ, + identifiers=plugins, + logger=logger) + results = api._identify() + self.assertEqual(len(results), 0) + self.assertEqual(len(logger._info), 1) + self.assertEqual(logger._info[0], 'request classification: browser') + self.assertEqual(len(logger._debug), 4) + self.failUnless(logger._debug[0].startswith( + 'identifier plugins registered: [')) + self.failUnless(logger._debug[1].startswith( + 'identifier plugins matched for ' + 'classification "browser": [')) + self.failUnless(logger._debug[2].startswith( + 'no identity returned from <')) + self.failUnless(logger._debug[2].endswith('> (None)')) + self.assertEqual(logger._debug[3], 'identities found: []') + + def test__identify_success_skip_noresults(self): + environ = self._makeEnviron() + api = self._makeOne() + plugin1 = DummyNoResultsIdentifier() + credentials = {'login':'chris', 'password':'password'} + plugin2 = DummyIdentifier(credentials) + plugins = [ ('identifier1', plugin1), ('identifier2', plugin2) ] + api = self._makeOne(environ=environ, identifiers=plugins) + results = api._identify() + 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() + api = self._makeOne() + plugin1 = DummyIdentifier({'login':'fred','password':'fred'}) + plugin2 = DummyIdentifier({'login':'bob','password':'bob'}) + plugins = [ ('identifier1', plugin1), ('identifier2', plugin2) ] + api = self._makeOne(environ=environ, identifiers=plugins) + results = api._identify() + 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() + api = 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) ] + api = self._makeOne(environ=environ, identifiers=plugins, + request_classifier=lambda environ: 'match') + results = api._identify() + 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) ] + api = self._makeOne(environ=environ, identifiers=plugins, + request_classifier=lambda environ: 'match') + results = api._identify() + 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) ] + api = self._makeOne(environ=environ, authenticators=plugins) + identities = [ (None, {'login':'chris', 'password':'password'}) ] + results = api._authenticate(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): + logger = DummyLogger() + environ = self._makeEnviron() + # no authenticators + api = self._makeOne(environ=environ, logger=logger) + identities = [ (None, {'login':'chris', 'password':'password'}) ] + result = api._authenticate(identities) + self.assertEqual(len(result), 0) + self.assertEqual(len(logger._info), 1) + self.assertEqual(logger._info[0], 'request classification: browser') + self.assertEqual(len(logger._debug), 3) + self.assertEqual(logger._debug[0], 'authenticator plugins ' + 'registered: []') + self.assertEqual(logger._debug[1], 'authenticator plugins matched ' + 'for classification "browser": []') + self.assertEqual(logger._debug[2], 'identities authenticated: []') + + def test__authenticate_success_skip_fail(self): + logger = DummyLogger() + environ = self._makeEnviron() + plugin1 = DummyFailAuthenticator() + plugin2 = DummyAuthenticator() + plugins = [ ('dummy1', plugin1), ('dummy2', plugin2) ] + api = self._makeOne(authenticators=plugins, logger=logger) + creds = {'login':'chris', 'password':'password'} + identities = [ (None, {'login':'chris', 'password':'password'}) ] + results = api._authenticate(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') + + self.assertEqual(len(logger._info), 1) + self.assertEqual(logger._info[0], 'request classification: browser') + self.assertEqual(len(logger._debug), 5) + self.failUnless(logger._debug[0].startswith( + 'authenticator plugins registered: [')) + self.failUnless(logger._debug[1].startswith( + 'authenticator plugins matched for ' + 'classification "browser": [')) + self.failUnless(logger._debug[2].startswith('no userid returned from')) + self.failUnless(logger._debug[3].startswith('userid returned from')) + self.failUnless(logger._debug[3].endswith('"chris"')) + self.failUnless(logger._debug[4].startswith( + 'identities authenticated: [((1, 0),')) + + def test__authenticate_success_multiresult(self): + logger = DummyLogger() + environ = self._makeEnviron() + plugin1 = DummyAuthenticator('chris_id1') + plugin2 = DummyAuthenticator('chris_id2') + plugins = [ ('dummy1',plugin1), ('dummy2',plugin2) ] + api = self._makeOne(environ=environ, + authenticators=plugins, logger=logger) + creds = {'login':'chris', 'password':'password'} + identities = [ (None, {'login':'chris', 'password':'password'}) ] + results = api._authenticate(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') + + self.assertEqual(len(logger._info), 1) + self.assertEqual(logger._info[0], 'request classification: browser') + self.assertEqual(len(logger._debug), 5) + self.failUnless(logger._debug[0].startswith( + 'authenticator plugins registered: [')) + self.failUnless(logger._debug[1].startswith( + 'authenticator plugins matched for ' + 'classification "browser": [')) + self.failUnless(logger._debug[2].startswith('userid returned from')) + self.failUnless(logger._debug[2].endswith('"chris_id1"')) + self.failUnless(logger._debug[3].startswith('userid returned from')) + self.failUnless(logger._debug[3].endswith('"chris_id2"')) + self.failUnless(logger._debug[4].startswith( + 'identities authenticated: [((0, 0),') + ) + + def test__authenticate_find_implicit_classifier(self): + from repoze.who.interfaces import IAuthenticator + environ = self._makeEnviron() + plugin1 = DummyAuthenticator('chris_id1') + plugin1.classifications = {IAuthenticator:['nomatch']} + plugin2 = DummyAuthenticator('chris_id2') + plugins = [ ('auth1', plugin1), ('auth2', plugin2) ] + api = self._makeOne(environ=environ, authenticators=plugins, + request_classifier=lambda environ: 'match') + identities = [ (None, {'login':'chris', 'password':'password'}) ] + results = api._authenticate(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): + from repoze.who.interfaces import IAuthenticator + environ = self._makeEnviron() + plugin1 = DummyAuthenticator('chris_id1') + plugin1.classifications = {IAuthenticator:['nomatch']} + plugin2 = DummyAuthenticator('chris_id2') + plugin2.classifications = {IAuthenticator:['match']} + plugins = [ ('auth1', plugin1), ('auth2', plugin2) ] + api = self._makeOne(environ=environ, authenticators=plugins, + request_classifier=lambda environ: 'match') + identities = [ (None, {'login':'chris', 'password':'password'}) ] + results = api._authenticate(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) ] + api = self._makeOne(environ=environ, authenticators=plugins) + identities = [ (None, {'login':'chris', 'password':'password'}) ] + results = api._authenticate(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__add_metadata(self): + environ = self._makeEnviron() + plugin1 = DummyMDProvider({'foo':'bar'}) + plugin2 = DummyMDProvider({'fuz':'baz'}) + plugins = [ ('meta1', plugin1), ('meta2', plugin2) ] + api = self._makeOne(environ=environ, mdproviders=plugins) + classification = '' + identity = {} + results = api._add_metadata(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) ] + api = self._makeOne(environ=environ, mdproviders=plugins) + classification = 'monkey' + identity = {} + api._add_metadata(identity) + self.assertEqual(identity['foo'], 'bar') + self.assertEqual(identity.get('fuz'), None) + + +class TestIdentityDict(_Base): + + def _getTargetClass(self): + from repoze.who.api 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('BBBB', 1, 2, 3, 4)) + + def test_encode_ip_timestamp(self): + from struct import pack + from .._auth_tkt import encode_ip_timestamp + self.assertEqual(encode_ip_timestamp('1.2.3.4', _WHEN), + pack('>BBBBL', 1, 2, 3, 4, _WHEN)) + + def test_maybe_encode_bytes(self): + from .._auth_tkt import maybe_encode + foo = b'foo' + self.failUnless(maybe_encode(foo) is foo) + + def test_maybe_encode_native_string(self): + from .._auth_tkt import maybe_encode + foo = 'foo' + self.assertEqual(maybe_encode(foo), b'foo') + + def test_maybe_encode_unicode(self): + from .._auth_tkt import maybe_encode + from .._compat import u + foo = u('foo') + self.assertEqual(maybe_encode(foo), b'foo') + + +_WHEN = 1234567 +class _Timemod(object): + @staticmethod + def time(): + return _WHEN + + +class _Monkey(object): + + def __init__(self, module, **replacements): + self.module = module + self.orig = {} + self.replacements = replacements + + def __enter__(self): + for k, v in self.replacements.items(): + orig = getattr(self.module, k, self) + if orig is not self: + self.orig[k] = orig + setattr(self.module, k, v) + + def __exit__(self, *exc_info): + for k, v in self.replacements.items(): + if k in self.orig: + setattr(self.module, k, self.orig[k]) + else: #pragma NO COVERSGE + delattr(self.module, k) diff -Nru python-repoze.who-1.0.18/repoze/who/tests/test_classifiers.py python-repoze.who-2.2/repoze/who/tests/test_classifiers.py --- python-repoze.who-1.0.18/repoze/who/tests/test_classifiers.py 2009-05-08 22:16:49.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/tests/test_classifiers.py 2012-03-19 19:42:14.000000000 +0000 @@ -1,18 +1,31 @@ import unittest -class TestDefaultRequestClassifier(unittest.TestCase): +class _Base(unittest.TestCase): + + def failUnless(self, predicate, message=''): + self.assertTrue(predicate, message) # Nannies go home! + + def failIf(self, predicate, message=''): + self.assertFalse(predicate, message) # Nannies go home! + +class TestDefaultRequestClassifier(_Base): def _getFUT(self): from repoze.who.classifiers import default_request_classifier return default_request_classifier def _makeEnviron(self, kw=None): + from wsgiref.util import setup_testing_defaults environ = {} - environ['wsgi.version'] = (1,0) + setup_testing_defaults(environ) if kw is not None: environ.update(kw) return environ + def test_conforms_to_IRequestClassifier(self): + from repoze.who.interfaces import IRequestClassifier + self.failUnless(IRequestClassifier.providedBy(self._getFUT())) + def test_classify_dav_method(self): classifier = self._getFUT() environ = self._makeEnviron({'REQUEST_METHOD':'COPY'}) @@ -32,6 +45,24 @@ result = classifier(environ) self.assertEqual(result, 'xmlpost') + def test_classify_xmlpost_uppercase(self): + """RFC 2045, Sec. 5.1: The type, subtype, and parameter names + are not case sensitive""" + classifier = self._getFUT() + environ = self._makeEnviron({'CONTENT_TYPE':'TEXT/XML', + 'REQUEST_METHOD':'POST'}) + result = classifier(environ) + self.assertEqual(result, 'xmlpost') + + def test_classify_rich_xmlpost(self): + """RFC 2046, sec. 4.1.2: A critical parameter that may be specified + in the Content-Type field for "text/plain" data is the character set.""" + classifier = self._getFUT() + environ = self._makeEnviron({'CONTENT_TYPE':'text/xml; charset=UTF-8 (some comment)', + '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', @@ -40,7 +71,7 @@ self.assertEqual(result, 'browser') -class TestDefaultChallengeDecider(unittest.TestCase): +class TestDefaultChallengeDecider(_Base): def _getFUT(self): from repoze.who.classifiers import default_challenge_decider @@ -53,6 +84,10 @@ environ.update(kw) return environ + def test_conforms_to_IChallengeDecider(self): + from repoze.who.interfaces import IChallengeDecider + self.failUnless(IChallengeDecider.providedBy(self._getFUT())) + def test_challenges_on_401(self): decider = self._getFUT() self.failUnless(decider({}, '401 Unauthorized', [])) @@ -61,7 +96,7 @@ decider = self._getFUT() self.failIf(decider({}, '200 Ok', [])) -class TestPassthroughChallengeDecider(unittest.TestCase): +class TestPassthroughChallengeDecider(_Base): def _getFUT(self): from repoze.who.classifiers import passthrough_challenge_decider @@ -74,6 +109,10 @@ environ.update(kw) return environ + def test_conforms_to_IChallengeDecider(self): + from repoze.who.interfaces import IChallengeDecider + self.failUnless(IChallengeDecider.providedBy(self._getFUT())) + def test_challenges_on_bare_401(self): decider = self._getFUT() self.failUnless(decider({}, '401 Unauthorized', [])) diff -Nru python-repoze.who-1.0.18/repoze/who/tests/test__compat.py python-repoze.who-2.2/repoze/who/tests/test__compat.py --- python-repoze.who-1.0.18/repoze/who/tests/test__compat.py 1970-01-01 00:00:00.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/tests/test__compat.py 2013-03-20 17:18:26.000000000 +0000 @@ -0,0 +1,136 @@ +import unittest + +class CompatTests(unittest.TestCase): + + def failUnless(self, predicate, message=''): + self.assertTrue(predicate, message) # Nannies go home! + + def failIf(self, predicate, message=''): + self.assertFalse(predicate, message) # Nannies go home! + + def test_REQUEST_METHOD_miss(self): + # PEP 3333 says CONTENT_TYPE is mandatory + from .._compat import REQUEST_METHOD + self.assertRaises(KeyError, REQUEST_METHOD, {}) + + def test_REQUEST_METHOD_hit(self): + from .._compat import REQUEST_METHOD + self.assertEqual(REQUEST_METHOD({'REQUEST_METHOD': 'FOO'}), 'FOO') + + def test_CONTENT_TYPE_miss(self): + # PEP 3333 says CONTENT_TYPE is optional + from .._compat import CONTENT_TYPE + self.assertEqual(CONTENT_TYPE({}), '') + + def test_CONTENT_TYPE_hit(self): + from .._compat import CONTENT_TYPE + self.assertEqual(CONTENT_TYPE({'CONTENT_TYPE': 'text/html'}), + 'text/html') + + def test_USER_AGENT_miss(self): + from .._compat import USER_AGENT + self.assertEqual(USER_AGENT({}), None) + + def test_USER_AGENT_hit(self): + from .._compat import USER_AGENT + self.assertEqual(USER_AGENT({'HTTP_USER_AGENT': 'FOO'}), 'FOO') + + def test_AUTHORIZATION_miss(self): + from .._compat import AUTHORIZATION + self.assertEqual(AUTHORIZATION({}), '') + + def test_AUTHORIZATION_hit(self): + from .._compat import AUTHORIZATION + self.assertEqual(AUTHORIZATION({'HTTP_AUTHORIZATION': 'FOO'}), 'FOO') + + def test_get_cookies_no_cache_ok_header_value(self): + from .._compat import get_cookies + from .._compat import SimpleCookie + environ = {'HTTP_COOKIE': 'qux=spam'} + cookies = get_cookies(environ) + self.failUnless(isinstance(cookies, SimpleCookie)) + self.assertEqual(len(cookies), 1) + self.assertEqual(cookies['qux'].value, 'spam') + self.assertEqual(environ['paste.cookies'], (cookies, 'qux=spam')) + + def test_get_cookies_w_cache_miss(self): + from .._compat import get_cookies + from .._compat import SimpleCookie + environ = {'HTTP_COOKIE': 'qux=spam', + 'paste.cookies': (object(), 'foo=bar'), + } + cookies = get_cookies(environ) + self.failUnless(isinstance(cookies, SimpleCookie)) + self.assertEqual(len(cookies), 1) + self.assertEqual(cookies['qux'].value, 'spam') + self.assertEqual(environ['paste.cookies'], (cookies, 'qux=spam')) + + def test_get_cookies_w_cache_hit(self): + from .._compat import get_cookies + from .._compat import SimpleCookie + existing = SimpleCookie() + existing['foo'] = 'bar' + environ = {'HTTP_COOKIE': 'qux=spam', + 'paste.cookies': (existing, 'qux=spam'), + } + cookies = get_cookies(environ) + self.failUnless(cookies is existing) + + def test_construct_url(self): + from .._compat import construct_url + environ = {'wsgi.url_scheme': 'http', + 'HTTP_HOST': 'example.com', + } + self.assertEqual(construct_url(environ), 'http://example.com/') + + def test_header_value_miss(self): + from .._compat import header_value + self.assertEqual(header_value([], 'nonesuch'), '') + + def test_header_value_simple(self): + from .._compat import header_value + self.assertEqual(header_value([('simple', 'SIMPLE')], 'simple'), + 'SIMPLE') + + def test_must_decode_non_string(self): + from .._compat import must_decode + foo = object() + self.failUnless(must_decode(foo) is foo) + + def test_must_decode_unicode(self): + from .._compat import must_decode + from .._compat import u + foo = u('foo') + self.failUnless(must_decode(foo) is foo) + + def test_must_decode_utf8(self): + from .._compat import must_decode + foo = b'b\xc3\xa2tard' + self.assertEqual(must_decode(foo), foo.decode('utf-8')) + + def test_must_decode_latin1(self): + from .._compat import must_decode + foo = b'b\xe2tard' + self.assertEqual(must_decode(foo), foo.decode('latin1')) + + def test_must_encode_non_string(self): + from .._compat import must_encode + foo = object() + self.failUnless(must_encode(foo) is foo) + + def test_must_encode_unicode(self): + from .._compat import must_encode + from .._compat import u + foo = u('foo') + self.assertEqual(must_encode(foo), foo.encode('utf-8')) + + def test_must_encode_utf8(self): + from .._compat import must_encode + foo = b'b\xc3\xa2tard' + self.failUnless(must_encode(foo) is foo) + + def test_must_encode_latin1(self): + from .._compat import must_encode + foo = b'b\xe2tard' + self.failUnless(must_encode(foo) is foo) + diff -Nru python-repoze.who-1.0.18/repoze/who/tests/test_config.py python-repoze.who-2.2/repoze/who/tests/test_config.py --- python-repoze.who-1.0.18/repoze/who/tests/test_config.py 2009-11-04 21:38:46.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/tests/test_config.py 2013-04-26 12:41:14.000000000 +0000 @@ -1,6 +1,14 @@ import unittest -class TestWhoConfig(unittest.TestCase): +class _Base(unittest.TestCase): + + def failUnless(self, predicate, message=''): + self.assertTrue(predicate, message) # Nannies go home! + + def failIf(self, predicate, message=''): + self.assertFalse(predicate, message) # Nannies go home! + +class TestWhoConfig(_Base): def _getTargetClass(self): from repoze.who.config import WhoConfig @@ -39,7 +47,7 @@ self.assertEqual(len(config.mdproviders), 0) def test_parse_empty_file(self): - from StringIO import StringIO + from repoze.who._compat import StringIO config = self._makeOne() config.parse(StringIO()) self.assertEqual(config.request_classifier, None) @@ -222,6 +230,9 @@ self.failUnless(isinstance(foo, DummyPlugin)) self.assertEqual(foo.iface, 'iface') self.assertEqual(foo.name, 'name') + self.assertEqual(foo.template, '%(template)s') + self.assertEqual(foo.template_with_eq, + 'template_with_eq = %(template_with_eq)s') class DummyPlugin: def __init__(self, **kw): @@ -340,18 +351,20 @@ use = repoze.who.tests.test_config:DummyPlugin name = name iface = iface +template = %%(template)s +template_with_eq = template_with_eq = %%(template_with_eq)s """ -class TestConfigMiddleware(unittest.TestCase): - tempdir = None +class TestConfigMiddleware(_Base): + _tempdir = None def setUp(self): pass def tearDown(self): - if self.tempdir is not None: + if self._tempdir is not None: import shutil - shutil.rmtree(self.tempdir) + shutil.rmtree(self._tempdir) def _getFactory(self): from repoze.who.config import make_middleware_with_config @@ -360,7 +373,7 @@ def _getTempfile(self, text): import os import tempfile - tempdir = self.tempdir = tempfile.mkdtemp() + tempdir = self._tempdir = tempfile.mkdtemp() path = os.path.join(tempdir, 'who.ini') config = open(path, 'w') config.write(text) @@ -370,18 +383,17 @@ 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) + api_factory = middleware.api_factory + self.assertEqual(len(api_factory.identifiers), 2) + self.assertEqual(len(api_factory.authenticators), 1) + self.assertEqual(len(api_factory.challengers), 2) + self.assertEqual(len(api_factory.mdproviders), 0) self.failUnless(middleware.logger, middleware.logger) self.assertEqual(middleware.logger.getEffectiveLevel(), logging.DEBUG) @@ -401,18 +413,150 @@ app = DummyApp() factory = self._getFactory() path = self._getTempfile(SAMPLE_CONFIG) - logfile = os.path.join(self.tempdir, 'who.log') + logfile = os.path.join(self._tempdir, 'who.log') global_conf = {'here': '/'} middleware = factory(app, global_conf, config_file=path, - log_file=logfile) + log_file=logfile, log_level=logging.WARN) + self.assertEqual(middleware.logger.getEffectiveLevel(), logging.WARN) + handlers = middleware.logger.handlers + self.assertEqual(len(handlers), 1) + self.failUnless(isinstance(handlers[0], logging.StreamHandler)) + self.assertEqual(handlers[0].stream.name, logfile) + logging.shutdown() + handlers[0].stream.close() + + def test_sample_config_wo_log_file(self): + import logging + from repoze.who.config import NullHandler + app = DummyApp() + factory = self._getFactory() + path = self._getTempfile(SAMPLE_CONFIG) + global_conf = {'here': '/'} + middleware = factory(app, global_conf, config_file=path) self.assertEqual(middleware.logger.getEffectiveLevel(), logging.INFO) + handlers = middleware.logger.handlers + self.assertEqual(len(handlers), 1) + self.failUnless(isinstance(handlers[0], NullHandler)) logging.shutdown() +class NullHandlerTests(_Base): + + def _getTargetClass(self): + from repoze.who.config import NullHandler + return NullHandler + + def _makeOne(self): + return self._getTargetClass()() + + def test_inheritance(self): + import logging + handler = self._makeOne() + self.failUnless(isinstance(handler, logging.Handler)) + + def test_emit_doesnt_raise_NotImplementedError(self): + handler = self._makeOne() + handler.emit(object()) + +class Test_make_api_factory_with_config(_Base): + _tempdir = None + _warning_filters = None + + def setUp(self): + pass + + def tearDown(self): + if self._tempdir is not None: + import shutil + shutil.rmtree(self._tempdir) + if self._warning_filters is not None: + import warnings + warnings.filters[:] = self._warning_filters + + def _getFactory(self): + from repoze.who.config import make_api_factory_with_config + return make_api_factory_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_bad_config_filename(self): + import warnings + with warnings.catch_warnings(record=True) as warned: + factory = self._getFactory() + path = '/nonesuch/file/should/exist' + global_conf = {'here': '/'} + api_factory = factory(global_conf, config_file=path) + self.assertEqual(len(api_factory.identifiers), 0) + self.assertEqual(len(api_factory.authenticators), 0) + self.assertEqual(len(api_factory.challengers), 0) + self.assertEqual(len(api_factory.mdproviders), 0) + self.assertEqual(api_factory.remote_user_key, 'REMOTE_USER') + self.failUnless(api_factory.logger is None) + self.failUnless(warned) + + def test_bad_config_content(self): + import warnings + with warnings.catch_warnings(record=True) as warned: + factory = self._getFactory() + path = self._getTempfile('this is not an INI file') + global_conf = {'here': '/'} + api_factory = factory(global_conf, config_file=path) + self.assertEqual(len(api_factory.identifiers), 0) + self.assertEqual(len(api_factory.authenticators), 0) + self.assertEqual(len(api_factory.challengers), 0) + self.assertEqual(len(api_factory.mdproviders), 0) + self.assertEqual(api_factory.remote_user_key, 'REMOTE_USER') + self.failUnless(api_factory.logger is None) + self.failUnless(warned) + + def test_sample_config_no_logger(self): + factory = self._getFactory() + path = self._getTempfile(SAMPLE_CONFIG) + global_conf = {'here': '/'} + api_factory = factory(global_conf, config_file=path) + self.assertEqual(len(api_factory.identifiers), 2) + self.assertEqual(len(api_factory.authenticators), 1) + self.assertEqual(len(api_factory.challengers), 2) + self.assertEqual(len(api_factory.mdproviders), 0) + self.assertEqual(api_factory.remote_user_key, 'REMOTE_USER') + self.failUnless(api_factory.logger is None) + + def test_sample_config_w_remote_user_key(self): + factory = self._getFactory() + path = self._getTempfile(SAMPLE_CONFIG) + global_conf = {'here': '/'} + api_factory = factory(global_conf, config_file=path, + remote_user_key = 'X-OTHER-USER') + self.assertEqual(len(api_factory.identifiers), 2) + self.assertEqual(len(api_factory.authenticators), 1) + self.assertEqual(len(api_factory.challengers), 2) + self.assertEqual(len(api_factory.mdproviders), 0) + self.assertEqual(api_factory.remote_user_key, 'X-OTHER-USER') + + def test_sample_config_w_logger(self): + factory = self._getFactory() + path = self._getTempfile(SAMPLE_CONFIG) + global_conf = {'here': '/'} + logger = object() + api_factory = factory(global_conf, config_file=path, logger=logger) + self.assertEqual(len(api_factory.identifiers), 2) + self.assertEqual(len(api_factory.authenticators), 1) + self.assertEqual(len(api_factory.challengers), 2) + self.assertEqual(len(api_factory.mdproviders), 0) + self.failUnless(api_factory.logger is logger) + SAMPLE_CONFIG = """\ -[plugin:form] -use = repoze.who.plugins.form:make_plugin -login_form_qs = __do_login -rememberer_name = auth_tkt +[plugin:redirector] +use = repoze.who.plugins.redirector:make_plugin +login_url = /login.html [plugin:auth_tkt] use = repoze.who.plugins.auth_tkt:make_plugin @@ -436,7 +580,6 @@ [identifiers] plugins = - form;browser auth_tkt basicauth @@ -445,7 +588,7 @@ [challengers] plugins = - form;browser + redirector;browser basicauth [mdproviders] diff -Nru python-repoze.who-1.0.18/repoze/who/tests/test_middleware.py python-repoze.who-2.2/repoze/who/tests/test_middleware.py --- python-repoze.who-1.0.18/repoze/who/tests/test_middleware.py 2009-05-08 18:27:31.000000000 +0000 +++ python-repoze.who-2.2/repoze/who/tests/test_middleware.py 2012-03-19 19:42:14.000000000 +0000 @@ -1,6 +1,14 @@ import unittest -class TestMiddleware(unittest.TestCase): +class _Base(unittest.TestCase): + + def failUnless(self, predicate, message=''): + self.assertTrue(predicate, message) # Nannies go home! + + def failIf(self, predicate, message=''): + self.assertFalse(predicate, message) # Nannies go home! + +class TestMiddleware(_Base): def _getTargetClass(self): from repoze.who.middleware import PluggableAuthenticationMiddleware @@ -11,11 +19,12 @@ identifiers=None, authenticators=None, challengers=None, - classifier=None, + request_classifier=None, mdproviders=None, challenge_decider=None, log_stream=None, log_level=None, + remote_user_key='REMOTE_USER', ): if app is None: app = DummyApp() @@ -25,8 +34,8 @@ authenticators = [] if challengers is None: challengers = [] - if classifier is None: - classifier = DummyRequestClassifier() + if request_classifier is None: + request_classifier = DummyRequestClassifier() if mdproviders is None: mdproviders = [] if challenge_decider is None: @@ -39,440 +48,146 @@ authenticators, challengers, mdproviders, - classifier, + request_classifier, challenge_decider, log_stream, - log_level=logging.DEBUG) + log_level=logging.DEBUG, + remote_user_key=remote_user_key, + ) return mw def _makeEnviron(self, kw=None): + from wsgiref.util import setup_testing_defaults environ = {} - environ['wsgi.version'] = (1,0) + setup_testing_defaults(environ) 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_ctor_positional_args(self): + klass = self._getTargetClass() + app = DummyApp() + identifiers = [] + authenticators = [] + challengers = [] + request_classifier = DummyRequestClassifier() + mdproviders = [] + challenge_decider = DummyChallengeDecider() + mw = klass(app, + identifiers, + authenticators, + challengers, + mdproviders, + request_classifier, + challenge_decider, + ) + self.assertEqual(mw.app, app) + af = mw.api_factory + self.assertEqual(af.identifiers, identifiers) + self.assertEqual(af.authenticators, authenticators) + self.assertEqual(af.challengers, challengers) + self.assertEqual(af.mdproviders, mdproviders) + self.assertEqual(af.request_classifier, request_classifier) + self.assertEqual(af.challenge_decider, challenge_decider) - def test_challenge_noidentifier_withapp(self): - environ = self._makeEnviron() + def test_ctor_wo_request_classifier_or_classifier_raises(self): + # BBB for old argument name + klass = self._getTargetClass() 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) + identifiers = [] + authenticators = [] + challengers = [] + mdproviders = [] + challenge_decider = DummyChallengeDecider() + self.assertRaises(ValueError, + klass, + app, + identifiers, + authenticators, + challengers, + mdproviders, + challenge_decider = challenge_decider, + ) - 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_ctor_w_request_classifier_and_classifier_raises(self): + # BBB for old argument name + klass = self._getTargetClass() + app = DummyApp() + identifiers = [] + authenticators = [] + challengers = [] + request_classifier = DummyRequestClassifier() + mdproviders = [] + challenge_decider = DummyChallengeDecider() + self.assertRaises(ValueError, + klass, + app, + identifiers, + authenticators, + challengers, + mdproviders, + request_classifier, + challenge_decider, + classifier = object() + ) - def test_challenge_identifier_app(self): - environ = self._makeEnviron() + def test_ctor_wo_challenge_decider_raises(self): + # BBB for old argument name + klass = self._getTargetClass() 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) + identifiers = [] + authenticators = [] + challengers = [] + request_classifier = DummyRequestClassifier() + mdproviders = [] + self.assertRaises(ValueError, + klass, + app, + identifiers, + authenticators, + challengers, + mdproviders, + classifier = request_classifier, + ) - def test_challenge_identifier_forget_headers(self): - FORGET_HEADERS = [('X-testing-forget', 'Oubliez!')] - environ = self._makeEnviron() + def test_ctor_w_classifier(self): + # BBB for old argument name + klass = self._getTargetClass() 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) + identifiers = [] + authenticators = [] + challengers = [] + request_classifier = DummyRequestClassifier() + mdproviders = [] + challenge_decider = DummyChallengeDecider() + mw = klass(app, + identifiers, + authenticators, + challengers, + mdproviders, + classifier = request_classifier, + challenge_decider = challenge_decider, + ) + self.assertEqual(mw.app, app) + af = mw.api_factory + self.assertEqual(af.identifiers, identifiers) + self.assertEqual(af.authenticators, authenticators) + self.assertEqual(af.challengers, challengers) + self.assertEqual(af.mdproviders, mdproviders) + self.assertEqual(af.request_classifier, request_classifier) + self.assertEqual(af.challenge_decider, challenge_decider) + + def test_ctor_accepts_logger(self): + import logging + restore = logging.raiseExceptions + logging.raiseExceptions = 0 + try: + logger = logging.Logger('something') + logger.setLevel(logging.INFO) + mw = self._makeOne(log_stream=logger) + self.assertEqual(logger, mw.logger) + finally: + logging.raiseExceptions = restore def test_call_remoteuser_already_set(self): environ = self._makeEnviron({'REMOTE_USER':'admin'}) @@ -517,24 +232,24 @@ self.assertEqual(start_response.headers, headers) def test_call_401_no_identifiers(self): + from webob.exc import HTTPUnauthorized 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) + result = b''.join(mw(environ, start_response)).decode('ascii') self.assertEqual(environ['challenged'], challenge_app) - self.failUnless(result[0].startswith('401 Unauthorized\r\n')) + self.failUnless(result.startswith('401 Unauthorized')) def test_call_401_challenger_and_identifier_no_authenticator(self): + from webob.exc import HTTPUnauthorized 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) ] @@ -545,17 +260,17 @@ identifiers=identifiers) start_response = DummyStartResponse() - result = mw(environ, start_response) + result = b''.join(mw(environ, start_response)).decode('ascii') self.assertEqual(environ['challenged'], challenge_app) - self.failUnless(result[0].startswith('401 Unauthorized\r\n')) + self.failUnless(result.startswith('401 Unauthorized')) self.assertEqual(identifier.forgotten, False) self.assertEqual(environ.get('REMOTE_USER'), None) def test_call_401_challenger_and_identifier_and_authenticator(self): + from webob.exc import HTTPUnauthorized 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) ] @@ -568,19 +283,19 @@ identifiers=identifiers, authenticators=authenticators) start_response = DummyStartResponse() - result = mw(environ, start_response) + result = b''.join(mw(environ, start_response)).decode('ascii') self.assertEqual(environ['challenged'], challenge_app) - self.failUnless(result[0].startswith('401 Unauthorized\r\n')) + self.failUnless(result.startswith('401 Unauthorized')) # @@ 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): + from webob.exc import HTTPUnauthorized 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) ] @@ -604,11 +319,11 @@ def test_call_200_identity_reset(self): + from webob.exc import HTTPUnauthorized 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) ] @@ -633,10 +348,10 @@ ## self.assertEqual(environ['repoze.who.identity'], new_credentials) def test_call_200_with_metadata(self): + from webob.exc import HTTPUnauthorized 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) ] @@ -657,12 +372,12 @@ self.assertEqual(environ['repoze.who.identity']['foo'], 'bar') def test_call_ingress_plugin_replaces_application(self): + from webob.exc import HTTPFound 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')], @@ -678,25 +393,25 @@ authenticators=authenticators, mdproviders=mdproviders) start_response = DummyStartResponse() - result = ''.join(mw(environ, start_response)) + result = b''.join(mw(environ, start_response)).decode('ascii') 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(len(headers), 3, headers) + #self.assertEqual(headers[0], + # ('Location', 'http://example.com/redirect')) self.assertEqual(headers[2], + ('Content-Type', 'text/plain; charset=UTF-8')) + self.assertEqual(headers[3], ('a', '1')) self.assertEqual(start_response.exc_info, None) - self.failIf(environ.has_key('repoze.who.application')) + self.failIf('repoze.who.application' in environ) def test_call_app_doesnt_call_start_response(self): + from webob.exc import HTTPUnauthorized 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) ] @@ -716,37 +431,54 @@ # metadata self.assertEqual(environ['repoze.who.identity']['foo'], 'bar') - # XXX need more call tests: - # - auth_id sorting + def test_call_w_challenge_closes_iterable(self): + from webob.exc import HTTPUnauthorized + environ = self._makeEnviron() + headers = [('a', '1')] + app = DummyIterableWithCloseApp('401 Unauthorized', headers) + 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 = b''.join(mw(environ, start_response)).decode('ascii') + self.failUnless(result.startswith('401 Unauthorized')) + self.failUnless(app._iterable._closed) -class TestMatchClassification(unittest.TestCase): + def test_call_w_challenge_but_no_challenger_still_closes_iterable(self): + environ = self._makeEnviron() + headers = [('a', '1')] + app = DummyIterableWithCloseApp('401 Unauthorized', headers) + challengers = [] + 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() + self.assertRaises(RuntimeError, mw, environ, start_response) + self.failUnless(app._iterable._closed) - 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]) + # XXX need more call tests: + # - auth_id sorting -class TestStartResponseWrapper(unittest.TestCase): +class TestStartResponseWrapper(_Base): def _getTargetClass(self): from repoze.who.middleware import StartResponseWrapper @@ -763,11 +495,11 @@ self.failUnless(wrapper.buffer) def test_finish_response(self): + from repoze.who._compat import StringIO statuses = [] headerses = [] datases = [] closededs = [] - from StringIO import StringIO def write(data): datases.append(data) def close(): @@ -791,86 +523,33 @@ 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('