diff options
89 files changed, 1797 insertions, 1138 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index c8a87f625..51a1e457d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,14 +1,82 @@ -unreleased -========== +1.9a2 (2017-05-09) +================== -Features --------- +Backward Incompatibilities +-------------------------- + +- ``request.exception`` and ``request.exc_info`` will only be set if the + response was generated by the EXCVIEW tween. This is to avoid any confusion + where a response was generated elsewhere in the pipeline and not in + direct relation to the original exception. If anyone upstream wants to + catch and render responses for exceptions they should set + ``request.exception`` and ``request.exc_info`` themselves to indicate + the exception that was squashed when generating the response. + + Similar behavior occurs with ``request.invoke_exception_view`` in which + the exception properties are set to reflect the exception if a response + is successfully generated by the method. + + This is a very minor incompatibility. Most tweens right now would give + priority to the raised exception and ignore ``request.exception``. This + change just improves and clarifies that bookkeeping by trying to be + more clear about the relationship between the response and its squashed + exception. See https://github.com/Pylons/pyramid/pull/3029 and + https://github.com/Pylons/pyramid/pull/3031 + +1.9a1 (2017-05-01) +================== + +Major Features +-------------- + +- The file format used by all ``p*`` command line scripts such as ``pserve`` + and ``pshell``, as well as the ``pyramid.paster.bootstrap`` function + is now replaceable thanks to a new dependency on + `plaster <http://docs.pylonsproject.org/projects/plaster/en/latest/>`_. + + For now, Pyramid is still shipping with integrated support for the + PasteDeploy INI format by depending on the + `plaster_pastedeploy <https://github.com/Pylons/plaster_pastedeploy>`_ + binding library. This may change in the future. + + See https://github.com/Pylons/pyramid/pull/2985 - Added an execution policy hook to the request pipeline. An execution policy has the ability to control creation and execution of the request - objects before they enter rest of the pipeline. This means for a given - request that the policy may create more than one request for retry - purposes. See https://github.com/Pylons/pyramid/pull/2964 + objects before they enter the rest of the pipeline. This means for a single + request environ the policy may create more than one request object. + + The first library to use this feature is + `pyramid_retry + <http://docs.pylonsproject.org/projects/pyramid-retry/en/latest/>`_. + + See https://github.com/Pylons/pyramid/pull/2964 + +- CSRF support has been refactored out of sessions and into its own + independent API in the ``pyramid.csrf`` module. It supports a pluggable + ``pyramid.interfaces.ICSRFStoragePolicy`` which can be used to define your + own mechanism for generating and validating CSRF tokens. By default, + Pyramid continues to use the ``pyramid.csrf.LegacySessionCSRFStoragePolicy`` + that uses the ``request.session.get_csrf_token`` and + ``request.session.new_csrf_token`` APIs under the hood to preserve + compatibility. Two new policies are shipped as well, + ``pyramid.csrf.SessionCSRFStoragePolicy`` and + ``pyramid.csrf.CookieCSRFStoragePolicy`` which will store the CSRF tokens + in the session and in a standalone cookie, respectively. The storage policy + can be changed by using the new + ``pyramid.config.Configurator.set_csrf_storage_policy`` config directive. + + CSRF tokens should be used via the new ``pyramid.csrf.get_csrf_token``, + ``pyramid.csrf.new_csrf_token`` and ``pyramid.csrf.check_csrf_token`` APIs + in order to continue working if the storage policy is changed. Also, the + ``pyramid.csrf.get_csrf_token`` function is injected into templates to be + used conveniently in UI code. + + See https://github.com/Pylons/pyramid/pull/2854 and + https://github.com/Pylons/pyramid/pull/3019 + +Minor Features +-------------- - Support an ``open_url`` config setting in the ``pserve`` section of the config file. This url is used to open a web browser when ``pserve --browser`` @@ -18,11 +86,18 @@ Features requirement that the server is being run in this format so it may fail. See https://github.com/Pylons/pyramid/pull/2984 +- The ``pyramid.config.Configurator`` can now be used as a context manager + which will automatically push/pop threadlocals (similar to + ``config.begin()`` and ``config.end()``). It will also automatically perform + a ``config.commit()`` and thus it is only recommended to be used at the + top-level of your app. See https://github.com/Pylons/pyramid/pull/2874 + - The threadlocals are now available inside any function invoked via ``config.include``. This means the only config-time code that cannot rely on threadlocals is code executed from non-actions inside the main. This can be alleviated by invoking ``config.begin()`` and ``config.end()`` - appropriately. See https://github.com/Pylons/pyramid/pull/2989 + appropriately or using the new context manager feature of the configurator. + See https://github.com/Pylons/pyramid/pull/2989 Bug Fixes --------- @@ -45,8 +120,27 @@ Bug Fixes Deprecations ------------ -Backward Incompatibilities --------------------------- +- Pyramid currently depends on ``plaster_pastedeploy`` to simplify the + transition to ``plaster`` by maintaining integrated support for INI files. + This dependency on ``plaster_pastedeploy`` should be considered subject to + Pyramid's deprecation policy and may be removed in the future. + Applications should depend on the appropriate plaster binding to satisfy + their needs. + +- Retrieving CSRF token from the session has been deprecated in favor of + equivalent methods in the ``pyramid.csrf`` module. The CSRF methods + (``ISession.get_csrf_token`` and ``ISession.new_csrf_token``) are no longer + required on the ``ISession`` interface except when using the default + ``pyramid.csrf.LegacySessionCSRFStoragePolicy``. + + Also, ``pyramid.session.check_csrf_token`` is now located at + ``pyramid.csrf.check_csrf_token``. + + See https://github.com/Pylons/pyramid/pull/2854 and + https://github.com/Pylons/pyramid/pull/3019 Documentation Changes --------------------- + +- Added the execution policy to the routing diagram in the Request Processing + chapter. See https://github.com/Pylons/pyramid/pull/2993 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 542b85f8e..2e49a193a 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -291,6 +291,8 @@ Contributors - Mikko Ohtamaa, 2016/12/6 +- Jure Cerjak, 2016/12/7 + - Martin Frlin, 2016/12/7 - Kirill Kuzminykh, 2017/03/01 diff --git a/README.rst b/README.rst index f05dd8bbf..5f42115df 100644 --- a/README.rst +++ b/README.rst @@ -1,16 +1,16 @@ Pyramid ======= -.. image:: https://travis-ci.org/Pylons/pyramid.png?branch=master +.. image:: https://travis-ci.org/Pylons/pyramid.png?branch=1.9-branch :target: https://travis-ci.org/Pylons/pyramid :alt: master Travis CI Status -.. image:: https://readthedocs.org/projects/pyramid/badge/?version=master - :target: http://docs.pylonsproject.org/projects/pyramid/en/master/ +.. image:: https://readthedocs.org/projects/pyramid/badge/?version=1.9-branch + :target: http://docs.pylonsproject.org/projects/pyramid/en/1.9-branch/ :alt: Master Documentation Status -.. image:: https://readthedocs.org/projects/pyramid/badge/?version=latest - :target: http://docs.pylonsproject.org/projects/pyramid/en/latest/ +.. image:: https://readthedocs.org/projects/pyramid/badge/?version=1.9-branch + :target: http://docs.pylonsproject.org/projects/pyramid/en/1.9-branch/ :alt: Latest Documentation Status .. image:: https://img.shields.io/badge/irc-freenode-blue.svg @@ -31,10 +31,10 @@ and deployment more fun, more predictable, and more productive. return Response('Hello %(name)s!' % request.matchdict) if __name__ == '__main__': - config = Configurator() - config.add_route('hello', '/hello/{name}') - config.add_view(hello_world, route_name='hello') - app = config.make_wsgi_app() + with Configurator() as config: + config.add_route('hello', '/hello/{name}') + config.add_view(hello_world, route_name='hello') + app = config.make_wsgi_app() server = make_server('0.0.0.0', 8080, app) server.serve_forever() diff --git a/RELEASING.txt b/RELEASING.txt index b9e5f4a6c..58ebb2fb3 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -108,10 +108,8 @@ Update previous version (final releases only) Marketing and communications ---------------------------- -- Edit Pylons/trypyramid.com/src/templates/resources.html for major releases - only. - -- Edit Pylons/pylonsrtd/pylonsrtd/docs/pyramid.rst for major releases only. +- Edit Pylons/trypyramid.com/src/templates/resources.html for major releases, + pre-releases, and once pre-releases are final. - Edit `http://wiki.python.org/moin/WebFrameworks <http://wiki.python.org/moin/WebFrameworks>`_. diff --git a/contributing.md b/contributing.md index 9deeee035..82f60e7b8 100644 --- a/contributing.md +++ b/contributing.md @@ -26,6 +26,8 @@ listed below. * [master](https://github.com/Pylons/pyramid/) - The branch on which further development takes place. The default branch on GitHub. +* [1.9-branch](https://github.com/Pylons/pyramid/tree/1.9-branch) - The branch + classified as "alpha". * [1.8-branch](https://github.com/Pylons/pyramid/tree/1.8-branch) - The branch classified as "stable" or "latest". * [1.7-branch](https://github.com/Pylons/pyramid/tree/1.7-branch) - The oldest diff --git a/docs/api/config.rst b/docs/api/config.rst index c76d3d5ff..a785b64ad 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -37,6 +37,7 @@ .. automethod:: set_authentication_policy .. automethod:: set_authorization_policy .. automethod:: set_default_csrf_options + .. automethod:: set_csrf_storage_policy .. automethod:: set_default_permission .. automethod:: add_permission diff --git a/docs/api/csrf.rst b/docs/api/csrf.rst new file mode 100644 index 000000000..38501546e --- /dev/null +++ b/docs/api/csrf.rst @@ -0,0 +1,23 @@ +.. _csrf_module: + +:mod:`pyramid.csrf` +------------------- + +.. automodule:: pyramid.csrf + + .. autoclass:: LegacySessionCSRFStoragePolicy + :members: + + .. autoclass:: SessionCSRFStoragePolicy + :members: + + .. autoclass:: CookieCSRFStoragePolicy + :members: + + .. autofunction:: get_csrf_token + + .. autofunction:: new_csrf_token + + .. autofunction:: check_csrf_origin + + .. autofunction:: check_csrf_token diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index a212ba7a9..e542a6be0 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -44,6 +44,9 @@ Other Interfaces .. autointerface:: IRoutePregenerator :members: + .. autointerface:: ICSRFStoragePolicy + :members: + .. autointerface:: ISession :members: diff --git a/docs/api/paster.rst b/docs/api/paster.rst index 27bc81a1f..f0784d0f8 100644 --- a/docs/api/paster.rst +++ b/docs/api/paster.rst @@ -7,8 +7,8 @@ .. autofunction:: bootstrap - .. autofunction:: get_app(config_uri, name=None, options=None) + .. autofunction:: get_app - .. autofunction:: get_appsettings(config_uri, name=None, options=None) + .. autofunction:: get_appsettings - .. autofunction:: setup_logging(config_uri, global_conf=None) + .. autofunction:: setup_logging diff --git a/docs/api/session.rst b/docs/api/session.rst index 56c4f52d7..53bae7c52 100644 --- a/docs/api/session.rst +++ b/docs/api/session.rst @@ -9,10 +9,6 @@ .. autofunction:: signed_deserialize - .. autofunction:: check_csrf_origin - - .. autofunction:: check_csrf_token - .. autofunction:: SignedCookieSessionFactory .. autofunction:: UnencryptedCookieSessionFactoryConfig diff --git a/docs/conf.py b/docs/conf.py index df58064e5..e63019c63 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -67,9 +67,10 @@ intersphinx_mapping = { 'cookiecutter': ('https://cookiecutter.readthedocs.io/en/latest/', None), 'deform': ('http://docs.pylonsproject.org/projects/deform/en/latest', None), 'jinja2': ('http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/', None), + 'plaster': ('http://docs.pylonsproject.org/projects/plaster/en/latest/', None), 'pylonswebframework': ('http://docs.pylonsproject.org/projects/pylons-webframework/en/latest/', None), 'python': ('https://docs.python.org/3', None), - 'pytest': ('http://pytest.org/latest/', None), + 'pytest': ('https://pytest.org/en/latest/', None), 'sphinx': ('http://www.sphinx-doc.org/en/latest', None), 'sqla': ('http://docs.sqlalchemy.org/en/latest', None), 'tm': ('http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/', None), diff --git a/docs/glossary.rst b/docs/glossary.rst index 0a46fac3b..2e5276554 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -366,6 +366,14 @@ Glossary :term:`WSGI` components together declaratively within an ``.ini`` file. It was developed by Ian Bicking. + plaster + `plaster <http://docs.pylonsproject.org/projects/plaster/en/latest/>`_ is + a library used by :app:`Pyramid` which acts as an abstraction between + command-line scripts and the file format used to load the :term:`WSGI` + components and application settings. By default :app:`Pyramid` ships + with the ``plaster_pastedeploy`` library installed which provides + integrated support for loading a :term:`PasteDeploy` INI file. + Chameleon `chameleon <https://chameleon.readthedocs.org/en/latest/>`_ is an attribute language template compiler which supports the :term:`ZPT` @@ -891,6 +899,11 @@ Glossary :meth:`pyramid.config.Configurator.set_session_factory` for more information. + CSRF storage policy + A utility that implements :class:`pyramid.interfaces.ICSRFStoragePolicy` + which is responsible for allocating CSRF tokens to a user and verifying + that a provided token is acceptable. + Mako `Mako <http://www.makotemplates.org/>`_ is a template language which refines the familiar ideas of componentized layout and inheritance diff --git a/docs/index.rst b/docs/index.rst index ed5b458ea..7d3393548 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -185,6 +185,7 @@ Change History .. toctree:: :maxdepth: 1 + whatsnew-1.9 whatsnew-1.8 whatsnew-1.7 whatsnew-1.6 diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst index 87682158b..9cc5b4ed8 100644 --- a/docs/narr/logging.rst +++ b/docs/narr/logging.rst @@ -16,7 +16,7 @@ to send log messages to loggers that you've configured. cookiecutter which does not create these files, the configuration information in this chapter may not be applicable. -.. index: +.. index:: pair: settings; logging pair: .ini; logging pair: logging; configuration diff --git a/docs/narr/myproject/README.txt b/docs/narr/myproject/README.txt index 41ef0ff91..2ffc0acba 100644 --- a/docs/narr/myproject/README.txt +++ b/docs/narr/myproject/README.txt @@ -1,5 +1,5 @@ MyProject -=============================== +========= Getting Started --------------- diff --git a/docs/narr/paste.rst b/docs/narr/paste.rst index 2d4e76e24..26cb1bfa5 100644 --- a/docs/narr/paste.rst +++ b/docs/narr/paste.rst @@ -26,12 +26,7 @@ documentation, see http://pythonpaste.org/deploy/. PasteDeploy ----------- -:term:`PasteDeploy` is the system that Pyramid uses to allow :term:`deployment -settings` to be specified using an ``.ini`` configuration file format. It also -allows the ``pserve`` command to work. Its configuration format provides a -convenient place to define application :term:`deployment settings` and WSGI -server settings, and its server runner allows you to stop and start a Pyramid -application easily. +:term:`plaster` is the system that Pyramid uses to load settings from configuration files. The most common format for these files is an ``.ini`` format structured in a way defined by :term:`PasteDeploy`. The format supports mechanisms to define WSGI app :term:`deployment settings`, WSGI server settings and logging. This allows the ``pserve`` command to work, allowing you to stop and start a Pyramid application easily. .. _pastedeploy_entry_points: @@ -96,3 +91,8 @@ applications, servers, and :term:`middleware` defined within the configuration file. The values in a ``[DEFAULT]`` section will be passed to your application's ``main`` function as ``global_config`` (see the reference to the ``main`` function in :ref:`init_py`). + +Alternative Configuration File Formats +-------------------------------------- + +It is possible to use different file formats with :app:`Pyramid` if you do not like :term:`PasteDeploy`. Under the hood all command-line scripts such as ``pserve`` and ``pshell`` pass the ``config_uri`` (e.g. ``development.ini`` or ``production.ini``) to the :term:`plaster` library which performs a lookup for an appropriate parser. For ``.ini`` files it uses PasteDeploy but you can register your own configuration formats that plaster will find instead. diff --git a/docs/narr/project.rst b/docs/narr/project.rst index ad27290f3..a150afc6b 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -94,7 +94,7 @@ If prompted for the first item, accept the default ``yes`` by hitting return. You've cloned ~/.cookiecutters/pyramid-cookiecutter-starter before. Is it okay to delete and re-clone it? [yes]: yes project_name [Pyramid Scaffold]: myproject - repo_name [scaffold]: myproject + repo_name [myproject]: myproject Select template_language: 1 - jinja2 2 - chameleon diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 77e7fd707..3a6bfa5e5 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -146,7 +146,7 @@ For example, the following view declaration protects the view named # config is an instance of pyramid.config.Configurator config.add_view('mypackage.views.blog_entry_add_view', - name='add_entry.html', + name='add_entry.html', context='mypackage.resources.Blog', permission='add') @@ -725,7 +725,7 @@ object that implements the following interface: """ Return ``True`` if any of the ``principals`` is allowed the ``permission`` in the current ``context``, else return ``False`` """ - + def principals_allowed_by_permission(self, context, permission): """ Return a set of principal identifiers allowed by the ``permission`` in ``context``. This behavior is optional; if you @@ -765,3 +765,215 @@ which would allow the attacker to control the content of the payload. Re-using a secret across two different subsystems might drop the security of signing to zero. Keys should not be re-used across different contexts where an attacker has the possibility of providing a chosen plaintext. + +.. index:: + single: preventing cross-site request forgery attacks + single: cross-site request forgery attacks, prevention + +Preventing Cross-Site Request Forgery Attacks +--------------------------------------------- + +`Cross-site request forgery +<https://en.wikipedia.org/wiki/Cross-site_request_forgery>`_ attacks are a +phenomenon whereby a user who is logged in to your website might inadvertantly +load a URL because it is linked from, or embedded in, an attacker's website. +If the URL is one that may modify or delete data, the consequences can be dire. + +You can avoid most of these attacks by issuing a unique token to the browser +and then requiring that it be present in all potentially unsafe requests. +:app:`Pyramid` provides facilities to create and check CSRF tokens. + +By default :app:`Pyramid` comes with a session-based CSRF implementation +:class:`pyramid.csrf.SessionCSRFStoragePolicy`. To use it, you must first enable +a :term:`session factory` as described in +:ref:`using_the_default_session_factory` or +:ref:`using_alternate_session_factories`. Alternatively, you can use +a cookie-based implementation :class:`pyramid.csrf.CookieCSRFStoragePolicy` which gives +some additional flexibility as it does not require a session for each user. +You can also define your own implementation of +:class:`pyramid.interfaces.ICSRFStoragePolicy` and register it with the +:meth:`pyramid.config.Configurator.set_csrf_storage_policy` directive. + +For example: + +.. code-block:: python + + from pyramid.config import Configurator + + config = Configurator() + config.set_csrf_storage_policy(MyCustomCSRFPolicy()) + +.. index:: + single: csrf.get_csrf_token + +Using the ``csrf.get_csrf_token`` Method +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To get the current CSRF token, use the +:data:`pyramid.csrf.get_csrf_token` method. + +.. code-block:: python + + from pyramid.csrf import get_csrf_token + token = get_csrf_token(request) + +The ``get_csrf_token()`` method accepts a single argument: the request. It +returns a CSRF *token* string. If ``get_csrf_token()`` or ``new_csrf_token()`` +was invoked previously for this user, then the existing token will be returned. +If no CSRF token previously existed for this user, then a new token will be set +into the session and returned. The newly created token will be opaque and +randomized. + +.. _get_csrf_token_in_templates: + +Using the ``get_csrf_token`` global in templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Templates have a ``get_csrf_token()`` method inserted into their globals, which +allows you to get the current token without modifying the view code. This +method takes no arguments and returns a CSRF token string. You can use the +returned token as the value of a hidden field in a form that posts to a method +that requires elevated privileges, or supply it as a request header in AJAX +requests. + +For example, include the CSRF token as a hidden field: + +.. code-block:: html + + <form method="post" action="/myview"> + <input type="hidden" name="csrf_token" value="${get_csrf_token()}"> + <input type="submit" value="Delete Everything"> + </form> + +Or include it as a header in a jQuery AJAX request: + +.. code-block:: javascript + + var csrfToken = "${get_csrf_token()}"; + $.ajax({ + type: "POST", + url: "/myview", + headers: { 'X-CSRF-Token': csrfToken } + }).done(function() { + alert("Deleted"); + }); + +The handler for the URL that receives the request should then require that the +correct CSRF token is supplied. + +.. index:: + single: csrf.new_csrf_token + +Using the ``csrf.new_csrf_token`` Method +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To explicitly create a new CSRF token, use the ``csrf.new_csrf_token()`` +method. This differs only from ``csrf.get_csrf_token()`` inasmuch as it +clears any existing CSRF token, creates a new CSRF token, sets the token into +the user, and returns the token. + +.. code-block:: python + + from pyramid.csrf import get_csrf_token + token = new_csrf_token() + +.. note:: + + It is not possible to force a new CSRF token from a template. If you + want to regenerate your CSRF token then do it in the view code and return + the new token as part of the context. + +Checking CSRF Tokens Manually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In request handling code, you can check the presence and validity of a CSRF +token with :func:`pyramid.csrf.check_csrf_token`. If the token is valid, it +will return ``True``, otherwise it will raise ``HTTPBadRequest``. Optionally, +you can specify ``raises=False`` to have the check return ``False`` instead of +raising an exception. + +By default, it checks for a POST parameter named ``csrf_token`` or a header +named ``X-CSRF-Token``. + +.. code-block:: python + + from pyramid.csrf import check_csrf_token + + def myview(request): + # Require CSRF Token + check_csrf_token(request) + + # ... + +.. _auto_csrf_checking: + +Checking CSRF Tokens Automatically +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.7 + +:app:`Pyramid` supports automatically checking CSRF tokens on requests with an +unsafe method as defined by RFC2616. Any other request may be checked manually. +This feature can be turned on globally for an application using the +:meth:`pyramid.config.Configurator.set_default_csrf_options` directive. +For example: + +.. code-block:: python + + from pyramid.config import Configurator + + config = Configurator() + config.set_default_csrf_options(require_csrf=True) + +CSRF checking may be explicitly enabled or disabled on a per-view basis using +the ``require_csrf`` view option. A value of ``True`` or ``False`` will +override the default set by ``set_default_csrf_options``. For example: + +.. code-block:: python + + @view_config(route_name='hello', require_csrf=False) + def myview(request): + # ... + +When CSRF checking is active, the token and header used to find the +supplied CSRF token will be ``csrf_token`` and ``X-CSRF-Token``, respectively, +unless otherwise overridden by ``set_default_csrf_options``. The token is +checked against the value in ``request.POST`` which is the submitted form body. +If this value is not present, then the header will be checked. + +In addition to token based CSRF checks, if the request is using HTTPS then the +automatic CSRF checking will also check the referrer of the request to ensure +that it matches one of the trusted origins. By default the only trusted origin +is the current host, however additional origins may be configured by setting +``pyramid.csrf_trusted_origins`` to a list of domain names (and ports if they +are non-standard). If a host in the list of domains starts with a ``.`` then +that will allow all subdomains as well as the domain without the ``.``. + +If CSRF checks fail then a :class:`pyramid.exceptions.BadCSRFToken` or +:class:`pyramid.exceptions.BadCSRFOrigin` exception will be raised. This +exception may be caught and handled by an :term:`exception view` but, by +default, will result in a ``400 Bad Request`` response being sent to the +client. + +Checking CSRF Tokens with a View Predicate +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 1.7 + Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead + to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised. + +A convenient way to require a valid CSRF token for a particular view is to +include ``check_csrf=True`` as a view predicate. See +:meth:`pyramid.config.Configurator.add_view`. + +.. code-block:: python + + @view_config(request_method='POST', check_csrf=True, ...) + def myview(request): + ... + +.. note:: + A mismatch of a CSRF token is treated like any other predicate miss, and the + predicate system, when it doesn't find a view, raises ``HTTPNotFound`` + instead of ``HTTPBadRequest``, so ``check_csrf=True`` behavior is different + from calling :func:`pyramid.csrf.check_csrf_token`. diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index 5b24201a9..7e2469d54 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -12,8 +12,7 @@ application. This chapter describes how to configure sessions, what session implementations :app:`Pyramid` provides out of the box, how to store and retrieve data from -sessions, and two session-specific features: flash messages, and cross-site -request forgery attack prevention. +sessions, and a session-specific feature: flash messages. .. index:: single: session factory (default) @@ -316,183 +315,3 @@ flash storage. ['info message'] >>> request.session.peek_flash() [] - -.. index:: - single: preventing cross-site request forgery attacks - single: cross-site request forgery attacks, prevention - -Preventing Cross-Site Request Forgery Attacks ---------------------------------------------- - -`Cross-site request forgery -<https://en.wikipedia.org/wiki/Cross-site_request_forgery>`_ attacks are a -phenomenon whereby a user who is logged in to your website might inadvertantly -load a URL because it is linked from, or embedded in, an attacker's website. -If the URL is one that may modify or delete data, the consequences can be dire. - -You can avoid most of these attacks by issuing a unique token to the browser -and then requiring that it be present in all potentially unsafe requests. -:app:`Pyramid` sessions provide facilities to create and check CSRF tokens. - -To use CSRF tokens, you must first enable a :term:`session factory` as -described in :ref:`using_the_default_session_factory` or -:ref:`using_alternate_session_factories`. - -.. index:: - single: session.get_csrf_token - -Using the ``session.get_csrf_token`` Method -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To get the current CSRF token from the session, use the -``session.get_csrf_token()`` method. - -.. code-block:: python - - token = request.session.get_csrf_token() - -The ``session.get_csrf_token()`` method accepts no arguments. It returns a -CSRF *token* string. If ``session.get_csrf_token()`` or -``session.new_csrf_token()`` was invoked previously for this session, then the -existing token will be returned. If no CSRF token previously existed for this -session, then a new token will be set into the session and returned. The newly -created token will be opaque and randomized. - -You can use the returned token as the value of a hidden field in a form that -posts to a method that requires elevated privileges, or supply it as a request -header in AJAX requests. - -For example, include the CSRF token as a hidden field: - -.. code-block:: html - - <form method="post" action="/myview"> - <input type="hidden" name="csrf_token" value="${request.session.get_csrf_token()}"> - <input type="submit" value="Delete Everything"> - </form> - -Or include it as a header in a jQuery AJAX request: - -.. code-block:: javascript - - var csrfToken = ${request.session.get_csrf_token()}; - $.ajax({ - type: "POST", - url: "/myview", - headers: { 'X-CSRF-Token': csrfToken } - }).done(function() { - alert("Deleted"); - }); - -The handler for the URL that receives the request should then require that the -correct CSRF token is supplied. - -.. index:: - single: session.new_csrf_token - -Using the ``session.new_csrf_token`` Method -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To explicitly create a new CSRF token, use the ``session.new_csrf_token()`` -method. This differs only from ``session.get_csrf_token()`` inasmuch as it -clears any existing CSRF token, creates a new CSRF token, sets the token into -the session, and returns the token. - -.. code-block:: python - - token = request.session.new_csrf_token() - -Checking CSRF Tokens Manually -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In request handling code, you can check the presence and validity of a CSRF -token with :func:`pyramid.session.check_csrf_token`. If the token is valid, it -will return ``True``, otherwise it will raise ``HTTPBadRequest``. Optionally, -you can specify ``raises=False`` to have the check return ``False`` instead of -raising an exception. - -By default, it checks for a POST parameter named ``csrf_token`` or a header -named ``X-CSRF-Token``. - -.. code-block:: python - - from pyramid.session import check_csrf_token - - def myview(request): - # Require CSRF Token - check_csrf_token(request) - - # ... - -.. _auto_csrf_checking: - -Checking CSRF Tokens Automatically -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 1.7 - -:app:`Pyramid` supports automatically checking CSRF tokens on requests with an -unsafe method as defined by RFC2616. Any other request may be checked manually. -This feature can be turned on globally for an application using the -:meth:`pyramid.config.Configurator.set_default_csrf_options` directive. -For example: - -.. code-block:: python - - from pyramid.config import Configurator - - config = Configurator() - config.set_default_csrf_options(require_csrf=True) - -CSRF checking may be explicitly enabled or disabled on a per-view basis using -the ``require_csrf`` view option. A value of ``True`` or ``False`` will -override the default set by ``set_default_csrf_options``. For example: - -.. code-block:: python - - @view_config(route_name='hello', require_csrf=False) - def myview(request): - # ... - -When CSRF checking is active, the token and header used to find the -supplied CSRF token will be ``csrf_token`` and ``X-CSRF-Token``, respectively, -unless otherwise overridden by ``set_default_csrf_options``. The token is -checked against the value in ``request.POST`` which is the submitted form body. -If this value is not present, then the header will be checked. - -In addition to token based CSRF checks, if the request is using HTTPS then the -automatic CSRF checking will also check the referrer of the request to ensure -that it matches one of the trusted origins. By default the only trusted origin -is the current host, however additional origins may be configured by setting -``pyramid.csrf_trusted_origins`` to a list of domain names (and ports if they -are non standard). If a host in the list of domains starts with a ``.`` then -that will allow all subdomains as well as the domain without the ``.``. - -If CSRF checks fail then a :class:`pyramid.exceptions.BadCSRFToken` or -:class:`pyramid.exceptions.BadCSRFOrigin` exception will be raised. This -exception may be caught and handled by an :term:`exception view` but, by -default, will result in a ``400 Bad Request`` response being sent to the -client. - -Checking CSRF Tokens with a View Predicate -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 1.7 - Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead - to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised. - -A convenient way to require a valid CSRF token for a particular view is to -include ``check_csrf=True`` as a view predicate. See -:meth:`pyramid.config.Configurator.add_view`. - -.. code-block:: python - - @view_config(request_method='POST', check_csrf=True, ...) - def myview(request): - ... - -.. note:: - A mismatch of a CSRF token is treated like any other predicate miss, and the - predicate system, when it doesn't find a view, raises ``HTTPNotFound`` - instead of ``HTTPBadRequest``, so ``check_csrf=True`` behavior is different - from calling :func:`pyramid.session.check_csrf_token`. diff --git a/docs/narr/startup.rst b/docs/narr/startup.rst index 27a2f1919..5e7c7c871 100644 --- a/docs/narr/startup.rst +++ b/docs/narr/startup.rst @@ -38,7 +38,14 @@ Here's a high-level time-ordered overview of what happens when you press begin to run and serve an application using the information contained within the ``development.ini`` file. -#. The framework finds a section named either ``[app:main]``, +#. ``pserve`` passes the ``development.ini`` path to :term:`plaster` which + finds an available configuration loader that recognizes the ``ini`` format. + +#. :term:`plaster` finds the ``plaster_pastedeploy`` library which binds + the :term:`PasteDeploy` library and returns a parser that can understand + the format. + +#. The :term:`PasteDeploy` finds a section named either ``[app:main]``, ``[pipeline:main]``, or ``[composite:main]`` in the ``.ini`` file. This section represents the configuration of a :term:`WSGI` application that will be served. If you're using a simple application (e.g., ``[app:main]``), the diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index 6b3b5fcce..4eadbd2f0 100644 --- a/docs/narr/templates.rst +++ b/docs/narr/templates.rst @@ -228,6 +228,10 @@ These values are provided to the template: provided if the template is rendered as the result of a ``renderer=`` argument to the view configuration being used. +``get_csrf_token()`` + A convenience function to access the current CSRF token. See + :ref:`get_csrf_token_in_templates` for more information. + ``renderer_name`` The renderer name used to perform the rendering, e.g., ``mypackage:templates/foo.pt``. diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index cd1598a1c..de896939a 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -519,7 +519,7 @@ If prompted for the first item, accept the default ``yes`` by hitting return. You've cloned ~/.cookiecutters/pyramid-cookiecutter-starter before. Is it okay to delete and re-clone it? [yes]: yes project_name [Pyramid Scaffold]: hello_world - repo_name [scaffold]: hello_world + repo_name [hello_world]: hello_world Select template_language: 1 - jinja2 2 - chameleon @@ -875,7 +875,7 @@ If prompted for the first item, accept the default ``yes`` by hitting return. You've cloned ~/.cookiecutters/pyramid-cookiecutter-alchemy before. Is it okay to delete and re-clone it? [yes]: yes project_name [Pyramid Scaffold]: sqla_demo - repo_name [scaffold]: sqla_demo + repo_name [sqla_demo]: sqla_demo We then run through the following commands as before. diff --git a/docs/quick_tour/logging/README.txt b/docs/quick_tour/logging/README.txt index fb7bde0a7..ff70a1354 100644 --- a/docs/quick_tour/logging/README.txt +++ b/docs/quick_tour/logging/README.txt @@ -1,5 +1,5 @@ hello_world -=============================== +=========== Getting Started --------------- diff --git a/docs/quick_tour/package/README.txt b/docs/quick_tour/package/README.txt index fb7bde0a7..ff70a1354 100644 --- a/docs/quick_tour/package/README.txt +++ b/docs/quick_tour/package/README.txt @@ -1,5 +1,5 @@ hello_world -=============================== +=========== Getting Started --------------- diff --git a/docs/quick_tour/sessions/README.txt b/docs/quick_tour/sessions/README.txt index fb7bde0a7..ff70a1354 100644 --- a/docs/quick_tour/sessions/README.txt +++ b/docs/quick_tour/sessions/README.txt @@ -1,5 +1,5 @@ hello_world -=============================== +=========== Getting Started --------------- diff --git a/docs/quick_tour/sqla_demo/README.txt b/docs/quick_tour/sqla_demo/README.txt index 1659e47ab..27bbff5a7 100644 --- a/docs/quick_tour/sqla_demo/README.txt +++ b/docs/quick_tour/sqla_demo/README.txt @@ -1,5 +1,5 @@ sqla_demo -=============================== +========= Getting Started --------------- diff --git a/docs/quick_tutorial/cookiecutters.rst b/docs/quick_tutorial/cookiecutters.rst index edfd8cd69..337a5c535 100644 --- a/docs/quick_tutorial/cookiecutters.rst +++ b/docs/quick_tutorial/cookiecutters.rst @@ -37,7 +37,7 @@ Steps You've cloned ~/.cookiecutters/pyramid-cookiecutter-starter before. Is it okay to delete and re-clone it? [yes]: yes project_name [Pyramid Scaffold]: cc_starter - repo_name [scaffold]: cc_starter + repo_name [cc_starter]: cc_starter Select template_language: 1 - jinja2 2 - chameleon diff --git a/docs/quick_tutorial/cookiecutters/README.txt b/docs/quick_tutorial/cookiecutters/README.txt index 4b1f31bf3..55c5dcec6 100644 --- a/docs/quick_tutorial/cookiecutters/README.txt +++ b/docs/quick_tutorial/cookiecutters/README.txt @@ -1,5 +1,5 @@ cc_starter -=============================== +========== Getting Started --------------- diff --git a/docs/quick_tutorial/unit_testing.rst b/docs/quick_tutorial/unit_testing.rst index 7c85d5289..002c62fde 100644 --- a/docs/quick_tutorial/unit_testing.rst +++ b/docs/quick_tutorial/unit_testing.rst @@ -29,7 +29,7 @@ broken the code. As you're writing your code, you might find this more convenient than changing to your browser constantly and clicking reload. We'll also leave discussion of `pytest-cov -<http://pytest-cov.readthedocs.org/en/latest/>`_ for another section. +<http://pytest-cov.readthedocs.io/en/latest/>`_ for another section. Objectives diff --git a/docs/tutorials/modwsgi/index.rst b/docs/tutorials/modwsgi/index.rst index 690266586..170f2ebc8 100644 --- a/docs/tutorials/modwsgi/index.rst +++ b/docs/tutorials/modwsgi/index.rst @@ -48,7 +48,7 @@ specific path information for commands and files. You've cloned ~/.cookiecutters/pyramid-cookiecutter-starter before. Is it okay to delete and re-clone it? [yes]: yes project_name [Pyramid Scaffold]: myproject - repo_name [scaffold]: myproject + repo_name [myproject]: myproject Select template_language: 1 - jinja2 2 - chameleon diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst index 6be826395..de057b1cc 100644 --- a/docs/tutorials/wiki/installation.rst +++ b/docs/tutorials/wiki/installation.rst @@ -50,7 +50,7 @@ If prompted for the first item, accept the default ``yes`` by hitting return. You've cloned ~/.cookiecutters/pyramid-cookiecutter-zodb before. Is it okay to delete and re-clone it? [yes]: yes project_name [Pyramid Scaffold]: myproj - repo_name [scaffold]: tutorial + repo_name [myproj]: tutorial Change directory into your newly created project ------------------------------------------------ diff --git a/docs/tutorials/wiki/src/authorization/README.txt b/docs/tutorials/wiki/src/authorization/README.txt index 98683bf8c..5ec53bf9d 100644 --- a/docs/tutorials/wiki/src/authorization/README.txt +++ b/docs/tutorials/wiki/src/authorization/README.txt @@ -1,5 +1,5 @@ myproj -=============================== +====== Getting Started --------------- diff --git a/docs/tutorials/wiki/src/basiclayout/README.txt b/docs/tutorials/wiki/src/basiclayout/README.txt index 98683bf8c..5ec53bf9d 100644 --- a/docs/tutorials/wiki/src/basiclayout/README.txt +++ b/docs/tutorials/wiki/src/basiclayout/README.txt @@ -1,5 +1,5 @@ myproj -=============================== +====== Getting Started --------------- diff --git a/docs/tutorials/wiki/src/installation/README.txt b/docs/tutorials/wiki/src/installation/README.txt index 98683bf8c..5ec53bf9d 100644 --- a/docs/tutorials/wiki/src/installation/README.txt +++ b/docs/tutorials/wiki/src/installation/README.txt @@ -1,5 +1,5 @@ myproj -=============================== +====== Getting Started --------------- diff --git a/docs/tutorials/wiki/src/models/README.txt b/docs/tutorials/wiki/src/models/README.txt index 98683bf8c..5ec53bf9d 100644 --- a/docs/tutorials/wiki/src/models/README.txt +++ b/docs/tutorials/wiki/src/models/README.txt @@ -1,5 +1,5 @@ myproj -=============================== +====== Getting Started --------------- diff --git a/docs/tutorials/wiki/src/tests/README.txt b/docs/tutorials/wiki/src/tests/README.txt index 98683bf8c..5ec53bf9d 100644 --- a/docs/tutorials/wiki/src/tests/README.txt +++ b/docs/tutorials/wiki/src/tests/README.txt @@ -1,5 +1,5 @@ myproj -=============================== +====== Getting Started --------------- diff --git a/docs/tutorials/wiki/src/views/README.txt b/docs/tutorials/wiki/src/views/README.txt index 98683bf8c..5ec53bf9d 100644 --- a/docs/tutorials/wiki/src/views/README.txt +++ b/docs/tutorials/wiki/src/views/README.txt @@ -1,5 +1,5 @@ myproj -=============================== +====== Getting Started --------------- diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 9eeb1711d..c61d4360d 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -62,7 +62,7 @@ If prompted for the first item, accept the default ``yes`` by hitting return. You've cloned ~/.cookiecutters/pyramid-cookiecutter-alchemy before. Is it okay to delete and re-clone it? [yes]: yes project_name [Pyramid Scaffold]: myproj - repo_name [scaffold]: tutorial + repo_name [myproj]: tutorial Change directory into your newly created project ------------------------------------------------ diff --git a/docs/tutorials/wiki2/src/authentication/README.txt b/docs/tutorials/wiki2/src/authentication/README.txt index 5e21b8aa4..81102a869 100644 --- a/docs/tutorials/wiki2/src/authentication/README.txt +++ b/docs/tutorials/wiki2/src/authentication/README.txt @@ -1,5 +1,5 @@ myproj -=============================== +====== Getting Started --------------- diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py index 1b071434c..2d058d874 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py @@ -1,4 +1,4 @@ -import cgi +from pyramid.compat import escape import re from docutils.core import publish_parts @@ -32,10 +32,10 @@ def view_page(request): exists = request.dbsession.query(Page).filter_by(name=word).all() if exists: view_url = request.route_url('view_page', pagename=word) - return '<a href="%s">%s</a>' % (view_url, cgi.escape(word)) + return '<a href="%s">%s</a>' % (view_url, escape(word)) else: add_url = request.route_url('add_page', pagename=word) - return '<a href="%s">%s</a>' % (add_url, cgi.escape(word)) + return '<a href="%s">%s</a>' % (add_url, escape(word)) content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(add_link, content) diff --git a/docs/tutorials/wiki2/src/authorization/README.txt b/docs/tutorials/wiki2/src/authorization/README.txt index 5e21b8aa4..81102a869 100644 --- a/docs/tutorials/wiki2/src/authorization/README.txt +++ b/docs/tutorials/wiki2/src/authorization/README.txt @@ -1,5 +1,5 @@ myproj -=============================== +====== Getting Started --------------- diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py index 9358993ea..65c12ed3b 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -1,4 +1,4 @@ -import cgi +from pyramid.compat import escape import re from docutils.core import publish_parts @@ -25,10 +25,10 @@ def view_page(request): exists = request.dbsession.query(Page).filter_by(name=word).all() if exists: view_url = request.route_url('view_page', pagename=word) - return '<a href="%s">%s</a>' % (view_url, cgi.escape(word)) + return '<a href="%s">%s</a>' % (view_url, escape(word)) else: add_url = request.route_url('add_page', pagename=word) - return '<a href="%s">%s</a>' % (add_url, cgi.escape(word)) + return '<a href="%s">%s</a>' % (add_url, escape(word)) content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(add_link, content) diff --git a/docs/tutorials/wiki2/src/basiclayout/README.txt b/docs/tutorials/wiki2/src/basiclayout/README.txt index 5e21b8aa4..81102a869 100644 --- a/docs/tutorials/wiki2/src/basiclayout/README.txt +++ b/docs/tutorials/wiki2/src/basiclayout/README.txt @@ -1,5 +1,5 @@ myproj -=============================== +====== Getting Started --------------- diff --git a/docs/tutorials/wiki2/src/installation/README.txt b/docs/tutorials/wiki2/src/installation/README.txt index 5e21b8aa4..81102a869 100644 --- a/docs/tutorials/wiki2/src/installation/README.txt +++ b/docs/tutorials/wiki2/src/installation/README.txt @@ -1,5 +1,5 @@ myproj -=============================== +====== Getting Started --------------- diff --git a/docs/tutorials/wiki2/src/models/README.txt b/docs/tutorials/wiki2/src/models/README.txt index 5e21b8aa4..81102a869 100644 --- a/docs/tutorials/wiki2/src/models/README.txt +++ b/docs/tutorials/wiki2/src/models/README.txt @@ -1,5 +1,5 @@ myproj -=============================== +====== Getting Started --------------- diff --git a/docs/tutorials/wiki2/src/tests/README.txt b/docs/tutorials/wiki2/src/tests/README.txt index 5e21b8aa4..81102a869 100644 --- a/docs/tutorials/wiki2/src/tests/README.txt +++ b/docs/tutorials/wiki2/src/tests/README.txt @@ -1,5 +1,5 @@ myproj -=============================== +====== Getting Started --------------- diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py index 9358993ea..65c12ed3b 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py @@ -1,4 +1,4 @@ -import cgi +from pyramid.compat import escape import re from docutils.core import publish_parts @@ -25,10 +25,10 @@ def view_page(request): exists = request.dbsession.query(Page).filter_by(name=word).all() if exists: view_url = request.route_url('view_page', pagename=word) - return '<a href="%s">%s</a>' % (view_url, cgi.escape(word)) + return '<a href="%s">%s</a>' % (view_url, escape(word)) else: add_url = request.route_url('add_page', pagename=word) - return '<a href="%s">%s</a>' % (add_url, cgi.escape(word)) + return '<a href="%s">%s</a>' % (add_url, escape(word)) content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(add_link, content) diff --git a/docs/tutorials/wiki2/src/views/README.txt b/docs/tutorials/wiki2/src/views/README.txt index 5e21b8aa4..81102a869 100644 --- a/docs/tutorials/wiki2/src/views/README.txt +++ b/docs/tutorials/wiki2/src/views/README.txt @@ -1,5 +1,5 @@ myproj -=============================== +====== Getting Started --------------- diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py index bb6300b75..3b95e0f59 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -1,4 +1,4 @@ -import cgi +from pyramid.compat import escape import re from docutils.core import publish_parts @@ -31,10 +31,10 @@ def view_page(request): exists = request.dbsession.query(Page).filter_by(name=word).all() if exists: view_url = request.route_url('view_page', pagename=word) - return '<a href="%s">%s</a>' % (view_url, cgi.escape(word)) + return '<a href="%s">%s</a>' % (view_url, escape(word)) else: add_url = request.route_url('add_page', pagename=word) - return '<a href="%s">%s</a>' % (add_url, cgi.escape(word)) + return '<a href="%s">%s</a>' % (add_url, escape(word)) content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(add_link, content) diff --git a/docs/whatsnew-1.9.rst b/docs/whatsnew-1.9.rst new file mode 100644 index 000000000..0ba29625c --- /dev/null +++ b/docs/whatsnew-1.9.rst @@ -0,0 +1,61 @@ +What's New in Pyramid 1.9 +========================= + +This article explains the new features in :app:`Pyramid` version 1.9 as compared to its predecessor, :app:`Pyramid` 1.8. It also documents backwards incompatibilities between the two versions and deprecations added to :app:`Pyramid` 1.9, as well as software dependency changes and notable documentation additions. + +Major Feature Additions +----------------------- + +- The file format used by all ``p*`` command line scripts such as ``pserve`` and ``pshell``, as well as the :func:`pyramid.paster.bootstrap` function is now replaceable thanks to a new dependency on `plaster <http://docs.pylonsproject.org/projects/plaster/en/latest/>`_. + + For now, Pyramid is still shipping with integrated support for the PasteDeploy INI format by depending on the `plaster_pastedeploy <https://github.com/Pylons/plaster_pastedeploy>`_ binding library. This may change in the future so it is recommended for applications to start depending on the appropriate plaster binding for their needs. + + See https://github.com/Pylons/pyramid/pull/2985 + +- Added an :term:`execution policy` hook to the request pipeline. An execution policy has the ability to control creation and execution of the request objects before they enter the rest of the pipeline. This means for a single request environ the policy may create more than one request object. + + The execution policy can be replaced using the new :meth:`pyramid.config.Configurator.set_execution_policy` config directive. + + The first library to use this feature is `pyramid_retry <http://docs.pylonsproject.org/projects/pyramid-retry/en/latest/>`_. + + See https://github.com/Pylons/pyramid/pull/2964 + +- CSRF support has been refactored out of sessions and into its own independent API in the :mod:`pyramid.csrf` module. It supports a pluggable :class:`pyramid.interfaces.ICSRFStoragePolicy` which can be used to define your own mechanism for generating and validating CSRF tokens. By default, Pyramid continues to use the :class:`pyramid.csrf.LegacySessionCSRFStoragePolicy` that uses the ``request.session.get_csrf_token`` and ``request.session.new_csrf_token`` APIs under the hood to preserve compatibility with older Pyramid applications. Two new policies are shipped as well, :class:`pyramid.csrf.SessionCSRFStoragePolicy` and :class:`pyramid.csrf.CookieCSRFStoragePolicy` which will store the CSRF tokens in the session and in a standalone cookie, respectively. The storage policy can be changed by using the new :meth:`pyramid.config.Configurator.set_csrf_storage_policy` config directive. + + CSRF tokens should be used via the new :func:`pyramid.csrf.get_csrf_token`, :func:`pyramid.csrf.new_csrf_token` and :func:`pyramid.csrf.check_csrf_token` APIs in order to continue working if the storage policy is changed. Also, the :func:`pyramid.csrf.get_csrf_token` function is now injected into templates to be used conveniently in UI code. + + See https://github.com/Pylons/pyramid/pull/2854 and https://github.com/Pylons/pyramid/pull/3019 + +Minor Feature Additions +----------------------- + +- Support an ``open_url`` config setting in the ``pserve`` section of the config file. This url is used to open a web browser when ``pserve --browser`` is invoked. When this setting is unavailable the ``pserve`` script will attempt to guess the port the server is using from the ``server:<server_name>`` section of the config file but there is no requirement that the server is being run in this format so it may fail. See https://github.com/Pylons/pyramid/pull/2984 + +- The :class:`pyramid.config.Configurator` can now be used as a context manager which will automatically push/pop threadlocals (similar to :meth:`pyramid.config.Configurator.begin` and :meth:`pyramid.config.Configurator.end`). It will also automatically perform a :meth:`pyramid.config.Configurator.commit` at the end and thus it is only recommended to be used at the top-level of your app. See https://github.com/Pylons/pyramid/pull/2874 + +- The threadlocals are now available inside any function invoked via :meth:`pyramid.config.Configurator.include`. This means the only config-time code that cannot rely on threadlocals is code executed from non-actions inside the main. This can be alleviated by invoking :meth:`pyramid.config.Configurator.begin` and :meth:`pyramid.config.Configurator.end` appropriately or using the new context manager feature of the configurator. See https://github.com/Pylons/pyramid/pull/2989 + +Deprecations +------------ + +- Pyramid currently depends on ``plaster_pastedeploy`` to simplify the transition to ``plaster`` by maintaining integrated support for INI files. This dependency on ``plaster_pastedeploy`` should be considered subject to Pyramid's deprecation policy and may be removed in the future. Applications should depend on the appropriate plaster binding to satisfy their needs. + +- Retrieving CSRF token from the session has been deprecated in favor of equivalent methods in the :mod:`pyramid.csrf` module. The CSRF methods (``ISession.get_csrf_token`` and ``ISession.new_csrf_token``) are no longer required on the :class:`pyramid.interfaces.ISession` interface except when using the default :class:`pyramid.csrf.LegacySessionCSRFStoragePolicy`. + + Also, ``pyramid.session.check_csrf_token`` is now located at :func:`pyramid.csrf.check_csrf_token`. + + See https://github.com/Pylons/pyramid/pull/2854 and https://github.com/Pylons/pyramid/pull/3019 + +Backward Incompatibilities +-------------------------- + +- ``request.exception`` and ``request.exc_info`` will only be set if the response was generated by the EXCVIEW tween. This is to avoid any confusion where a response was generated elsewhere in the pipeline and not in direct relation to the original exception. If anyone upstream wants to catch and render responses for exceptions they should set ``request.exception`` and ``request.exc_info`` themselves to indicate the exception that was squashed when generating the response. + + Similar behavior occurs with :meth:`pyramid.request.Request.invoke_exception_view` in which the exception properties are set to reflect the exception if a response is successfully generated by the method. + + This is a very minor incompatibility. Most tweens right now would give priority to the raised exception and ignore ``request.exception``. This change just improves and clarifies that bookkeeping by trying to be more clear about the relationship between the response and its squashed exception. See https://github.com/Pylons/pyramid/pull/3029 and https://github.com/Pylons/pyramid/pull/3031 + +Documentation Enhancements +-------------------------- + +- Added the :term:`execution policy` to the routing diagram in :ref:`router_chapter`. See https://github.com/Pylons/pyramid/pull/2993 diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 6c661aa59..a34f0b4db 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -110,6 +110,17 @@ class Configurator( A Configurator is used to configure a :app:`Pyramid` :term:`application registry`. + The Configurator lifecycle can be managed by using a context manager to + automatically handle calling :meth:`pyramid.config.Configurator.begin` and + :meth:`pyramid.config.Configurator.end` as well as + :meth:`pyramid.config.Configurator.commit`. + + .. code-block:: python + + with Configurator(settings=settings) as config: + config.add_route('home', '/') + app = config.make_wsgi_app() + If the ``registry`` argument is not ``None``, it must be an instance of the :class:`pyramid.registry.Registry` class representing the registry to configure. If ``registry`` is ``None``, the @@ -265,6 +276,11 @@ class Configurator( .. versionadded:: 1.6 The ``root_package`` argument. The ``response_factory`` argument. + + .. versionadded:: 1.9 + The ability to use the configurator as a context manager with the + ``with``-statement to make threadlocal configuration available for + further configuration with an implicit commit. """ manager = manager # for testing injection venusian = venusian # for testing injection @@ -380,6 +396,7 @@ class Configurator( self.add_default_view_derivers() self.add_default_route_predicates() self.add_default_tweens() + self.add_default_security() if exceptionresponse_view is not None: exceptionresponse_view = self.maybe_dotted(exceptionresponse_view) @@ -646,12 +663,22 @@ class Configurator( _ctx = action_state # bw compat def commit(self): - """ Commit any pending configuration actions. If a configuration + """ + Commit any pending configuration actions. If a configuration conflict is detected in the pending configuration actions, this method will raise a :exc:`ConfigurationConflictError`; within the traceback of this error will be information about the source of the conflict, usually including file names and line numbers of the cause of the - configuration conflicts.""" + configuration conflicts. + + .. warning:: + You should think very carefully before manually invoking + ``commit()``. Especially not as part of any reusable configuration + methods. Normally it should only be done by an application author at + the end of configuration in order to override certain aspects of an + addon. + + """ self.begin() try: self.action_state.execute_actions(introspector=self.introspector) @@ -933,6 +960,16 @@ class Configurator( """ return self.manager.pop() + def __enter__(self): + self.begin() + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + self.end() + + if exc_value is None: + self.commit() + # this is *not* an action method (uses caller_package) def scan(self, package=None, categories=None, onerror=None, ignore=None, **kw): diff --git a/pyramid/config/security.py b/pyramid/config/security.py index 1d4bbe890..20b816161 100644 --- a/pyramid/config/security.py +++ b/pyramid/config/security.py @@ -3,17 +3,24 @@ from zope.interface import implementer from pyramid.interfaces import ( IAuthorizationPolicy, IAuthenticationPolicy, + ICSRFStoragePolicy, IDefaultCSRFOptions, IDefaultPermission, PHASE1_CONFIG, PHASE2_CONFIG, ) +from pyramid.csrf import LegacySessionCSRFStoragePolicy from pyramid.exceptions import ConfigurationError from pyramid.util import action_method from pyramid.util import as_sorted_tuple + class SecurityConfiguratorMixin(object): + + def add_default_security(self): + self.set_csrf_storage_policy(LegacySessionCSRFStoragePolicy()) + @action_method def set_authentication_policy(self, policy): """ Override the :app:`Pyramid` :term:`authentication policy` in the @@ -223,9 +230,31 @@ class SecurityConfiguratorMixin(object): intr['header'] = header intr['safe_methods'] = as_sorted_tuple(safe_methods) intr['callback'] = callback + self.action(IDefaultCSRFOptions, register, order=PHASE1_CONFIG, introspectables=(intr,)) + @action_method + def set_csrf_storage_policy(self, policy): + """ + Set the :term:`CSRF storage policy` used by subsequent view + registrations. + + ``policy`` is a class that implements the + :meth:`pyramid.interfaces.ICSRFStoragePolicy` interface and defines + how to generate and persist CSRF tokens. + + """ + def register(): + self.registry.registerUtility(policy, ICSRFStoragePolicy) + intr = self.introspectable('csrf storage policy', + None, + policy, + 'csrf storage policy') + intr['policy'] = policy + self.action(ICSRFStoragePolicy, register, introspectables=(intr,)) + + @implementer(IDefaultCSRFOptions) class DefaultCSRFOptions(object): def __init__(self, require_csrf, token, header, safe_methods, callback): diff --git a/pyramid/config/views.py b/pyramid/config/views.py index dd8e9e787..48c4e3437 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -641,18 +641,22 @@ class ViewsConfiguratorMixin(object): 'check name'. If the value provided is ``True``, ``csrf_token`` will be used as the check name. - If CSRF checking is performed, the checked value will be the value - of ``request.params[check_name]``. This value will be compared - against the value of ``request.session.get_csrf_token()``, and the - check will pass if these two values are the same. If the check - passes, the associated view will be permitted to execute. If the + If CSRF checking is performed, the checked value will be the value of + ``request.params[check_name]``. This value will be compared against + the value of ``policy.get_csrf_token()`` (where ``policy`` is an + implementation of :meth:`pyramid.interfaces.ICSRFStoragePolicy`), and the + check will pass if these two values are the same. If the check + passes, the associated view will be permitted to execute. If the check fails, the associated view will not be permitted to execute. - Note that using this feature requires a :term:`session factory` to - have been configured. - .. versionadded:: 1.4a2 + .. versionchanged:: 1.9 + This feature requires either a :term:`session factory` to have been + configured, or a :term:`CSRF storage policy` other than the default + to be in use. + + physical_path If specified, this value should be a string or a tuple representing @@ -972,7 +976,7 @@ class ViewsConfiguratorMixin(object): def register_view(classifier, request_iface, derived_view): # A multiviews is a set of views which are registered for # exactly the same context type/request type/name triad. Each - # consituent view in a multiview differs only by the + # constituent view in a multiview differs only by the # predicates which it possesses. # To find a previously registered view for a context diff --git a/pyramid/csrf.py b/pyramid/csrf.py new file mode 100644 index 000000000..7c836e5ad --- /dev/null +++ b/pyramid/csrf.py @@ -0,0 +1,332 @@ +import uuid + +from webob.cookies import CookieProfile +from zope.interface import implementer + + +from pyramid.authentication import _SimpleSerializer + +from pyramid.compat import ( + bytes_, + urlparse, + text_, +) +from pyramid.exceptions import ( + BadCSRFOrigin, + BadCSRFToken, +) +from pyramid.interfaces import ICSRFStoragePolicy +from pyramid.settings import aslist +from pyramid.util import ( + is_same_domain, + strings_differ +) + + +@implementer(ICSRFStoragePolicy) +class LegacySessionCSRFStoragePolicy(object): + """ A CSRF storage policy that defers control of CSRF storage to the + session. + + This policy maintains compatibility with legacy ISession implementations + that know how to manage CSRF tokens themselves via + ``ISession.new_csrf_token`` and ``ISession.get_csrf_token``. + + Note that using this CSRF implementation requires that + a :term:`session factory` is configured. + + .. versionadded:: 1.9 + + """ + def new_csrf_token(self, request): + """ Sets a new CSRF token into the session and returns it. """ + return request.session.new_csrf_token() + + def get_csrf_token(self, request): + """ Returns the currently active CSRF token from the session, + generating a new one if needed.""" + return request.session.get_csrf_token() + + def check_csrf_token(self, request, supplied_token): + """ Returns ``True`` if the ``supplied_token`` is valid.""" + expected_token = self.get_csrf_token(request) + return not strings_differ( + bytes_(expected_token), bytes_(supplied_token)) + + +@implementer(ICSRFStoragePolicy) +class SessionCSRFStoragePolicy(object): + """ A CSRF storage policy that persists the CSRF token in the session. + + Note that using this CSRF implementation requires that + a :term:`session factory` is configured. + + ``key`` + + The session key where the CSRF token will be stored. + Default: `_csrft_`. + + .. versionadded:: 1.9 + + """ + _token_factory = staticmethod(lambda: text_(uuid.uuid4().hex)) + + def __init__(self, key='_csrft_'): + self.key = key + + def new_csrf_token(self, request): + """ Sets a new CSRF token into the session and returns it. """ + token = self._token_factory() + request.session[self.key] = token + return token + + def get_csrf_token(self, request): + """ Returns the currently active CSRF token from the session, + generating a new one if needed.""" + token = request.session.get(self.key, None) + if not token: + token = self.new_csrf_token(request) + return token + + def check_csrf_token(self, request, supplied_token): + """ Returns ``True`` if the ``supplied_token`` is valid.""" + expected_token = self.get_csrf_token(request) + return not strings_differ( + bytes_(expected_token), bytes_(supplied_token)) + + +@implementer(ICSRFStoragePolicy) +class CookieCSRFStoragePolicy(object): + """ An alternative CSRF implementation that stores its information in + unauthenticated cookies, known as the 'Double Submit Cookie' method in the + `OWASP CSRF guidelines <https://www.owasp.org/index.php/ + Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet# + Double_Submit_Cookie>`_. This gives some additional flexibility with + regards to scaling as the tokens can be generated and verified by a + front-end server. + + .. versionadded:: 1.9 + + """ + _token_factory = staticmethod(lambda: text_(uuid.uuid4().hex)) + + def __init__(self, cookie_name='csrf_token', secure=False, httponly=False, + domain=None, max_age=None, path='/'): + serializer = _SimpleSerializer() + self.cookie_profile = CookieProfile( + cookie_name=cookie_name, + secure=secure, + max_age=max_age, + httponly=httponly, + path=path, + domains=[domain], + serializer=serializer + ) + self.cookie_name = cookie_name + + def new_csrf_token(self, request): + """ Sets a new CSRF token into the request and returns it. """ + token = self._token_factory() + request.cookies[self.cookie_name] = token + def set_cookie(request, response): + self.cookie_profile.set_cookies( + response, + token, + ) + request.add_response_callback(set_cookie) + return token + + def get_csrf_token(self, request): + """ Returns the currently active CSRF token by checking the cookies + sent with the current request.""" + bound_cookies = self.cookie_profile.bind(request) + token = bound_cookies.get_value() + if not token: + token = self.new_csrf_token(request) + return token + + def check_csrf_token(self, request, supplied_token): + """ Returns ``True`` if the ``supplied_token`` is valid.""" + expected_token = self.get_csrf_token(request) + return not strings_differ( + bytes_(expected_token), bytes_(supplied_token)) + + +def get_csrf_token(request): + """ Get the currently active CSRF token for the request passed, generating + a new one using ``new_csrf_token(request)`` if one does not exist. This + calls the equivalent method in the chosen CSRF protection implementation. + + .. versionadded :: 1.9 + + """ + registry = request.registry + csrf = registry.getUtility(ICSRFStoragePolicy) + return csrf.get_csrf_token(request) + + +def new_csrf_token(request): + """ Generate a new CSRF token for the request passed and persist it in an + implementation defined manner. This calls the equivalent method in the + chosen CSRF protection implementation. + + .. versionadded :: 1.9 + + """ + registry = request.registry + csrf = registry.getUtility(ICSRFStoragePolicy) + return csrf.new_csrf_token(request) + + +def check_csrf_token(request, + token='csrf_token', + header='X-CSRF-Token', + raises=True): + """ Check the CSRF token returned by the + :class:`pyramid.interfaces.ICSRFStoragePolicy` implementation against the + value in ``request.POST.get(token)`` (if a POST request) or + ``request.headers.get(header)``. If a ``token`` keyword is not supplied to + this function, the string ``csrf_token`` will be used to look up the token + in ``request.POST``. If a ``header`` keyword is not supplied to this + function, the string ``X-CSRF-Token`` will be used to look up the token in + ``request.headers``. + + If the value supplied by post or by header cannot be verified by the + :class:`pyramid.interfaces.ICSRFStoragePolicy`, and ``raises`` is + ``True``, this function will raise an + :exc:`pyramid.exceptions.BadCSRFToken` exception. If the values differ + and ``raises`` is ``False``, this function will return ``False``. If the + CSRF check is successful, this function will return ``True`` + unconditionally. + + See :ref:`auto_csrf_checking` for information about how to secure your + application automatically against CSRF attacks. + + .. versionadded:: 1.4a2 + + .. versionchanged:: 1.7a1 + A CSRF token passed in the query string of the request is no longer + considered valid. It must be passed in either the request body or + a header. + + .. versionchanged:: 1.9 + Moved from :mod:`pyramid.session` to :mod:`pyramid.csrf` and updated + to use the configured :class:`pyramid.interfaces.ICSRFStoragePolicy` to + verify the CSRF token. + + """ + supplied_token = "" + # We first check the headers for a csrf token, as that is significantly + # cheaper than checking the POST body + if header is not None: + supplied_token = request.headers.get(header, "") + + # If this is a POST/PUT/etc request, then we'll check the body to see if it + # has a token. We explicitly use request.POST here because CSRF tokens + # should never appear in an URL as doing so is a security issue. We also + # explicitly check for request.POST here as we do not support sending form + # encoded data over anything but a request.POST. + if supplied_token == "" and token is not None: + supplied_token = request.POST.get(token, "") + + policy = request.registry.getUtility(ICSRFStoragePolicy) + if not policy.check_csrf_token(request, text_(supplied_token)): + if raises: + raise BadCSRFToken('check_csrf_token(): Invalid token') + return False + return True + + +def check_csrf_origin(request, trusted_origins=None, raises=True): + """ + Check the ``Origin`` of the request to see if it is a cross site request or + not. + + If the value supplied by the ``Origin`` or ``Referer`` header isn't one of the + trusted origins and ``raises`` is ``True``, this function will raise a + :exc:`pyramid.exceptions.BadCSRFOrigin` exception, but if ``raises`` is + ``False``, this function will return ``False`` instead. If the CSRF origin + checks are successful this function will return ``True`` unconditionally. + + Additional trusted origins may be added by passing a list of domain (and + ports if non-standard like ``['example.com', 'dev.example.com:8080']``) in + with the ``trusted_origins`` parameter. If ``trusted_origins`` is ``None`` + (the default) this list of additional domains will be pulled from the + ``pyramid.csrf_trusted_origins`` setting. + + Note that this function will do nothing if ``request.scheme`` is not + ``https``. + + .. versionadded:: 1.7 + + .. versionchanged:: 1.9 + Moved from :mod:`pyramid.session` to :mod:`pyramid.csrf` + + """ + def _fail(reason): + if raises: + raise BadCSRFOrigin(reason) + else: + return False + + if request.scheme == "https": + # Suppose user visits http://example.com/ + # An active network attacker (man-in-the-middle, MITM) sends a + # POST form that targets https://example.com/detonate-bomb/ and + # submits it via JavaScript. + # + # The attacker will need to provide a CSRF cookie and token, but + # that's no problem for a MITM when we cannot make any assumptions + # about what kind of session storage is being used. So the MITM can + # circumvent the CSRF protection. This is true for any HTTP connection, + # but anyone using HTTPS expects better! For this reason, for + # https://example.com/ we need additional protection that treats + # http://example.com/ as completely untrusted. Under HTTPS, + # Barth et al. found that the Referer header is missing for + # same-domain requests in only about 0.2% of cases or less, so + # we can use strict Referer checking. + + # Determine the origin of this request + origin = request.headers.get("Origin") + if origin is None: + origin = request.referrer + + # Fail if we were not able to locate an origin at all + if not origin: + return _fail("Origin checking failed - no Origin or Referer.") + + # Parse our origin so we we can extract the required information from + # it. + originp = urlparse.urlparse(origin) + + # Ensure that our Referer is also secure. + if originp.scheme != "https": + return _fail( + "Referer checking failed - Referer is insecure while host is " + "secure." + ) + + # Determine which origins we trust, which by default will include the + # current origin. + if trusted_origins is None: + trusted_origins = aslist( + request.registry.settings.get( + "pyramid.csrf_trusted_origins", []) + ) + + if request.host_port not in set(["80", "443"]): + trusted_origins.append("{0.domain}:{0.host_port}".format(request)) + else: + trusted_origins.append(request.domain) + + # Actually check to see if the request's origin matches any of our + # trusted origins. + if not any(is_same_domain(originp.netloc, host) + for host in trusted_origins): + reason = ( + "Referer checking failed - {0} does not match any trusted " + "origins." + ) + return _fail(reason.format(origin)) + + return True diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index bbb4754e4..ab83813c8 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -927,6 +927,13 @@ class ISession(IDict): usually accessed via ``request.session``. Keys and values of a session must be pickleable. + + .. versionchanged:: 1.9 + + Sessions are no longer required to implement ``get_csrf_token`` and + ``new_csrf_token``. CSRF token support was moved to the pluggable + :class:`pyramid.interfaces.ICSRFStoragePolicy` configuration hook. + """ # attributes @@ -981,19 +988,39 @@ class ISession(IDict): :meth:`pyramid.interfaces.ISession.flash` """ - def new_csrf_token(): - """ Create and set into the session a new, random cross-site request - forgery protection token. Return the token. It will be a string.""" - def get_csrf_token(): - """ Return a random cross-site request forgery protection token. It - will be a string. If a token was previously added to the session via - ``new_csrf_token``, that token will be returned. If no CSRF token - was previously set into the session, ``new_csrf_token`` will be +class ICSRFStoragePolicy(Interface): + """ An object that offers the ability to verify CSRF tokens and generate + new ones.""" + + def new_csrf_token(request): + """ Create and return a new, random cross-site request forgery + protection token. The token will be an ascii-compatible unicode + string. + + """ + + def get_csrf_token(request): + """ Return a cross-site request forgery protection token. It + will be an ascii-compatible unicode string. If a token was previously + set for this user via ``new_csrf_token``, that token will be returned. + If no CSRF token was previously set, ``new_csrf_token`` will be called, which will create and set a token, and this token will be returned. + + """ + + def check_csrf_token(request, token): + """ Determine if the supplied ``token`` is valid. Most implementations + should simply compare the ``token`` to the current value of + ``get_csrf_token`` but it is possible to verify the token using + any mechanism necessary using this method. + + Returns ``True`` if the ``token`` is valid, otherwise ``False``. + """ + class IIntrospector(Interface): def get(category_name, discriminator, default=None): """ Get the IIntrospectable related to the category_name and the diff --git a/pyramid/paster.py b/pyramid/paster.py index 5429a7860..f7544f0c5 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -1,14 +1,17 @@ -import os +from pyramid.scripting import prepare +from pyramid.scripts.common import get_config_loader -from paste.deploy import ( - loadapp, - appconfig, - ) +def setup_logging(config_uri, global_conf=None): + """ + Set up Python logging with the filename specified via ``config_uri`` + (a string in the form ``filename#sectionname``). -from pyramid.scripting import prepare -from pyramid.scripts.common import setup_logging # noqa, api + Extra defaults can optionally be specified as a dict in ``global_conf``. + """ + loader = get_config_loader(config_uri) + loader.setup_logging(global_conf) -def get_app(config_uri, name=None, options=None, loadapp=loadapp): +def get_app(config_uri, name=None, options=None): """ Return the WSGI application named ``name`` in the PasteDeploy config file specified by ``config_uri``. @@ -18,20 +21,13 @@ def get_app(config_uri, name=None, options=None, loadapp=loadapp): If the ``name`` is None, this will attempt to parse the name from the ``config_uri`` string expecting the format ``inifile#name``. - If no name is found, the name will default to "main".""" - path, section = _getpathsec(config_uri, name) - config_name = 'config:%s' % path - here_dir = os.getcwd() + If no name is found, the name will default to "main". - app = loadapp( - config_name, - name=section, - relative_to=here_dir, - global_conf=options) - - return app + """ + loader = get_config_loader(config_uri) + return loader.get_wsgi_app(name, options) -def get_appsettings(config_uri, name=None, options=None, appconfig=appconfig): +def get_appsettings(config_uri, name=None, options=None): """ Return a dictionary representing the key/value pairs in an ``app`` section within the file represented by ``config_uri``. @@ -41,24 +37,11 @@ def get_appsettings(config_uri, name=None, options=None, appconfig=appconfig): If the ``name`` is None, this will attempt to parse the name from the ``config_uri`` string expecting the format ``inifile#name``. - If no name is found, the name will default to "main".""" - path, section = _getpathsec(config_uri, name) - config_name = 'config:%s' % path - here_dir = os.getcwd() - return appconfig( - config_name, - name=section, - relative_to=here_dir, - global_conf=options) - -def _getpathsec(config_uri, name): - if '#' in config_uri: - path, section = config_uri.split('#', 1) - else: - path, section = config_uri, 'main' - if name: - section = name - return path, section + If no name is found, the name will default to "main". + + """ + loader = get_config_loader(config_uri) + return loader.get_wsgi_app_settings(name, options) def bootstrap(config_uri, request=None, options=None): """ Load a WSGI application from the PasteDeploy config file specified diff --git a/pyramid/predicates.py b/pyramid/predicates.py index 7c3a778ca..3d7bb1b4b 100644 --- a/pyramid/predicates.py +++ b/pyramid/predicates.py @@ -4,7 +4,7 @@ from pyramid.exceptions import ConfigurationError from pyramid.compat import is_nonstr_iter -from pyramid.session import check_csrf_token +from pyramid.csrf import check_csrf_token from pyramid.traversal import ( find_interface, traversal_path, diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 47705d5d9..6019f50fb 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -1,3 +1,4 @@ +from functools import partial import json import os import re @@ -19,6 +20,7 @@ from pyramid.compat import ( text_type, ) +from pyramid.csrf import get_csrf_token from pyramid.decorator import reify from pyramid.events import BeforeRender @@ -428,6 +430,7 @@ class RendererHelper(object): 'context':context, 'request':request, 'req':request, + 'get_csrf_token':partial(get_csrf_token, request), } return self.render_to_response(response, system, request=request) @@ -441,13 +444,13 @@ class RendererHelper(object): 'context':getattr(request, 'context', None), 'request':request, 'req':request, + 'get_csrf_token':partial(get_csrf_token, request), } system_values = BeforeRender(system_values, value) registry = self.registry registry.notify(system_values) - result = renderer(value, system_values) return result diff --git a/pyramid/scripts/common.py b/pyramid/scripts/common.py index fc141f6e2..f4b8027db 100644 --- a/pyramid/scripts/common.py +++ b/pyramid/scripts/common.py @@ -1,6 +1,4 @@ -import os -from pyramid.compat import configparser -from logging.config import fileConfig +import plaster def parse_vars(args): """ @@ -17,26 +15,9 @@ def parse_vars(args): result[name] = value return result -def setup_logging(config_uri, global_conf=None, - fileConfig=fileConfig, - configparser=configparser): +def get_config_loader(config_uri): """ - Set up logging via :func:`logging.config.fileConfig` with the filename - specified via ``config_uri`` (a string in the form - ``filename#sectionname``). + Find a ``plaster.ILoader`` object supporting the "wsgi" protocol. - ConfigParser defaults are specified for the special ``__file__`` - and ``here`` variables, similar to PasteDeploy config loading. - Extra defaults can optionally be specified as a dict in ``global_conf``. """ - path = config_uri.split('#', 1)[0] - parser = configparser.ConfigParser() - parser.read([path]) - if parser.has_section('loggers'): - config_file = os.path.abspath(path) - full_global_conf = dict( - __file__=config_file, - here=os.path.dirname(config_file)) - if global_conf: - full_global_conf.update(global_conf) - return fileConfig(config_file, full_global_conf) + return plaster.get_loader(config_uri, protocols=['wsgi']) diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py index 66feff624..f0681afd7 100644 --- a/pyramid/scripts/prequest.py +++ b/pyramid/scripts/prequest.py @@ -5,9 +5,8 @@ import textwrap from pyramid.compat import url_unquote from pyramid.request import Request -from pyramid.paster import get_app +from pyramid.scripts.common import get_config_loader from pyramid.scripts.common import parse_vars -from pyramid.scripts.common import setup_logging def main(argv=sys.argv, quiet=False): command = PRequestCommand(argv, quiet) @@ -110,7 +109,7 @@ class PRequestCommand(object): "passed here.", ) - get_app = staticmethod(get_app) + _get_config_loader = staticmethod(get_config_loader) stdin = sys.stdin def __init__(self, argv, quiet=False): @@ -121,17 +120,18 @@ class PRequestCommand(object): if not self.quiet: print(msg) - def configure_logging(self, app_spec): - setup_logging(app_spec) - def run(self): if not self.args.config_uri or not self.args.path_info: self.out('You must provide at least two arguments') return 2 - app_spec = self.args.config_uri + config_uri = self.args.config_uri + config_vars = parse_vars(self.args.config_vars) path = self.args.path_info - self.configure_logging(app_spec) + loader = self._get_config_loader(config_uri) + loader.setup_logging(config_vars) + + app = loader.get_wsgi_app(self.args.app_name, config_vars) if not path.startswith('/'): path = '/' + path @@ -158,9 +158,6 @@ class PRequestCommand(object): name, value = item.split(':', 1) headers[name] = value.strip() - app = self.get_app(app_spec, self.args.app_name, - options=parse_vars(self.args.config_vars)) - request_method = (self.args.method or 'GET').upper() environ = { diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index 80c8238a2..69d61ae8f 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -7,10 +7,11 @@ import re from zope.interface import Interface from pyramid.paster import bootstrap -from pyramid.compat import (string_types, configparser) +from pyramid.compat import string_types from pyramid.interfaces import IRouteRequest from pyramid.config import not_ +from pyramid.scripts.common import get_config_loader from pyramid.scripts.common import parse_vars from pyramid.static import static_view from pyramid.view import _find_views @@ -175,7 +176,6 @@ def get_route_data(route, registry): (route.name, route_intr['external_url'], UNKNOWN_KEY, ANY_KEY) ] - route_request_methods = route_intr['request_methods'] view_intr = registry.introspector.related(route_intr) @@ -245,9 +245,9 @@ class PRoutesCommand(object): will be assumed. Example: 'proutes myapp.ini'. """ - bootstrap = (bootstrap,) + bootstrap = staticmethod(bootstrap) # testing + get_config_loader = staticmethod(get_config_loader) # testing stdout = sys.stdout - ConfigParser = configparser.ConfigParser # testing parser = argparse.ArgumentParser( description=textwrap.dedent(description), formatter_class=argparse.RawDescriptionHelpFormatter, @@ -308,18 +308,12 @@ class PRoutesCommand(object): return True - def proutes_file_config(self, filename): - config = self.ConfigParser() - config.read(filename) - try: - items = config.items('proutes') - for k, v in items: - if 'format' == k: - cols = re.split(r'[,|\s\n]+', v) - self.column_format = [x.strip() for x in cols] - - except configparser.NoSectionError: - return + def proutes_file_config(self, loader, global_conf=None): + settings = loader.get_settings('proutes', global_conf) + format = settings.get('format') + if format: + cols = re.split(r'[,|\s\n]+', format) + self.column_format = [x.strip() for x in cols] def out(self, msg): # pragma: no cover if not self.quiet: @@ -336,12 +330,15 @@ class PRoutesCommand(object): return 2 config_uri = self.args.config_uri - env = self.bootstrap[0](config_uri, options=parse_vars(self.args.config_vars)) + config_vars = parse_vars(self.args.config_vars) + loader = self.get_config_loader(config_uri) + loader.setup_logging(config_vars) + self.proutes_file_config(loader, config_vars) + + env = self.bootstrap(config_uri, options=config_vars) registry = env['registry'] mapper = self._get_mapper(registry) - self.proutes_file_config(config_uri) - if self.args.format: columns = self.args.format.split(',') self.column_format = [x.strip() for x in columns] diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index c469dde04..f7d094980 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -18,16 +18,11 @@ import time import webbrowser import hupper -from paste.deploy import ( - loadapp, - loadserver, -) from pyramid.compat import PY2 -from pyramid.compat import configparser +from pyramid.scripts.common import get_config_loader from pyramid.scripts.common import parse_vars -from pyramid.scripts.common import setup_logging from pyramid.path import AssetResolver from pyramid.settings import aslist @@ -113,9 +108,7 @@ class PServeCommand(object): "passed here.", ) - ConfigParser = configparser.ConfigParser # testing - loadapp = staticmethod(loadapp) # testing - loadserver = staticmethod(loadserver) # testing + _get_config_loader = staticmethod(get_config_loader) # for testing open_url = None @@ -133,26 +126,14 @@ class PServeCommand(object): if self.args.verbose > 0: print(msg) - def get_config_vars(self): - restvars = self.args.config_vars - return parse_vars(restvars) + def get_config_path(self, loader): + return os.path.abspath(loader.uri.path) - def pserve_file_config(self, filename, global_conf=None): - here = os.path.abspath(os.path.dirname(filename)) - defaults = {} - if global_conf: - defaults.update(global_conf) - defaults['here'] = here - - config = self.ConfigParser(defaults=defaults) - config.optionxform = str - config.read(filename) - try: - items = dict(config.items('pserve')) - except configparser.NoSectionError: - return - - watch_files = aslist(items.get('watch_files', ''), flatten=False) + def pserve_file_config(self, loader, global_conf=None): + settings = loader.get_settings('pserve', global_conf) + config_path = self.get_config_path(loader) + here = os.path.dirname(config_path) + watch_files = aslist(settings.get('watch_files', ''), flatten=False) # track file paths relative to the ini file resolver = AssetResolver(package=None) @@ -164,45 +145,30 @@ class PServeCommand(object): self.watch_files.add(os.path.abspath(file)) # attempt to determine the url of the server - open_url = items.get('open_url') + open_url = settings.get('open_url') if open_url: self.open_url = open_url - def _guess_server_url(self, filename, server_name, - global_conf=None): # pragma: no cover + def guess_server_url(self, loader, server_name, global_conf=None): server_name = server_name or 'main' - here = os.path.abspath(os.path.dirname(filename)) - defaults = {} - if global_conf: - defaults.update(global_conf) - defaults['here'] = here - - config = self.ConfigParser(defaults=defaults) - config.optionxform = str - config.read(filename) - try: - items = dict(config.items('server:' + server_name)) - except configparser.NoSectionError: - return - - if 'port' in items: - return 'http://127.0.0.1:{port}'.format(**items) + settings = loader.get_settings('server:' + server_name, global_conf) + if 'port' in settings: + return 'http://127.0.0.1:{port}'.format(**settings) def run(self): # pragma: no cover if not self.args.config_uri: self.out('You must give a config file') return 2 + config_uri = self.args.config_uri + config_vars = parse_vars(self.args.config_vars) app_spec = self.args.config_uri - - vars = self.get_config_vars() app_name = self.args.app_name - base = os.getcwd() - if not self._scheme_re.search(app_spec): - config_path = os.path.join(base, app_spec) - app_spec = 'config:' + app_spec - else: - config_path = None + loader = self._get_config_loader(config_uri) + loader.setup_logging(config_vars) + + self.pserve_file_config(loader, global_conf=config_vars) + server_name = self.args.server_name if self.args.server: server_spec = 'egg:pyramid' @@ -211,15 +177,17 @@ class PServeCommand(object): else: server_spec = app_spec + server_loader = loader + if server_spec != app_spec: + server_loader = self.get_config_loader(server_spec) + # do not open the browser on each reload so check hupper first if self.args.browser and not hupper.is_active(): - self.pserve_file_config(config_path, global_conf=vars) url = self.open_url - # do not guess the url if the server is sourced from a different - # location than the config_path - if not url and server_spec == app_spec: - url = self._guess_server_url(config_path, server_name, vars) + if not url: + url = self.guess_server_url( + server_loader, server_name, config_vars) if not url: self.out('WARNING: could not determine the server\'s url to ' @@ -246,20 +214,19 @@ class PServeCommand(object): ) return 0 - if config_path: - setup_logging(config_path, global_conf=vars) - self.pserve_file_config(config_path, global_conf=vars) - self.watch_files.add(config_path) + config_path = self.get_config_path(loader) + self.watch_files.add(config_path) + + server_path = self.get_config_path(server_loader) + self.watch_files.add(server_path) if hupper.is_active(): reloader = hupper.get_reloader() reloader.watch_files(list(self.watch_files)) - server = self.loadserver( - server_spec, name=server_name, relative_to=base, global_conf=vars) + server = server_loader.get_wsgi_server(server_name, config_vars) - app = self.loadapp( - app_spec, name=app_name, relative_to=base, global_conf=vars) + app = loader.get_wsgi_app(app_name, config_vars) if self.args.verbose > 0: if hasattr(os, 'getpid'): diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py index 83e640c32..bb201dbc2 100644 --- a/pyramid/scripts/pshell.py +++ b/pyramid/scripts/pshell.py @@ -5,15 +5,14 @@ import sys import textwrap import pkg_resources -from pyramid.compat import configparser from pyramid.compat import exec_ from pyramid.util import DottedNameResolver from pyramid.paster import bootstrap from pyramid.settings import aslist +from pyramid.scripts.common import get_config_loader from pyramid.scripts.common import parse_vars -from pyramid.scripts.common import setup_logging def main(argv=sys.argv, quiet=False): command = PShellCommand(argv, quiet) @@ -41,7 +40,8 @@ class PShellCommand(object): than one Pyramid application within it, the loader will use the last one. """ - bootstrap = (bootstrap,) # for testing + bootstrap = staticmethod(bootstrap) # for testing + get_config_loader = staticmethod(get_config_loader) # for testing pkg_resources = pkg_resources # for testing parser = argparse.ArgumentParser( @@ -78,7 +78,6 @@ class PShellCommand(object): "passed here.", ) - ConfigParser = configparser.ConfigParser # testing default_runner = python_shell_runner # testing loaded_objects = {} @@ -91,20 +90,13 @@ class PShellCommand(object): self.quiet = quiet self.args = self.parser.parse_args(argv[1:]) - def pshell_file_config(self, filename): - config = self.ConfigParser() - config.optionxform = str - config.read(filename) - try: - items = config.items('pshell') - except configparser.NoSectionError: - return - + def pshell_file_config(self, loader, defaults): + settings = loader.get_settings('pshell', defaults) resolver = DottedNameResolver(None) self.loaded_objects = {} self.object_help = {} self.setup = None - for k, v in items: + for k, v in settings.items(): if k == 'setup': self.setup = v elif k == 'default_shell': @@ -124,13 +116,12 @@ class PShellCommand(object): self.out('Requires a config file argument') return 2 config_uri = self.args.config_uri - config_file = config_uri.split('#', 1)[0] - setup_logging(config_file) - self.pshell_file_config(config_file) + config_vars = parse_vars(self.args.config_vars) + loader = self.get_config_loader(config_uri) + loader.setup_logging(config_vars) + self.pshell_file_config(loader, config_vars) - # bootstrap the environ - env = self.bootstrap[0](config_uri, - options=parse_vars(self.args.config_vars)) + env = self.bootstrap(config_uri, options=config_vars) # remove the closer from the env self.closer = env.pop('closer') diff --git a/pyramid/scripts/ptweens.py b/pyramid/scripts/ptweens.py index 5ca77e52a..d5cbebe12 100644 --- a/pyramid/scripts/ptweens.py +++ b/pyramid/scripts/ptweens.py @@ -7,6 +7,7 @@ from pyramid.interfaces import ITweens from pyramid.tweens import MAIN from pyramid.tweens import INGRESS from pyramid.paster import bootstrap +from pyramid.paster import setup_logging from pyramid.scripts.common import parse_vars def main(argv=sys.argv, quiet=False): @@ -47,7 +48,8 @@ class PTweensCommand(object): ) stdout = sys.stdout - bootstrap = (bootstrap,) # testing + bootstrap = staticmethod(bootstrap) # testing + setup_logging = staticmethod(setup_logging) # testing def __init__(self, argv, quiet=False): self.quiet = quiet @@ -76,7 +78,9 @@ class PTweensCommand(object): self.out('Requires a config file argument') return 2 config_uri = self.args.config_uri - env = self.bootstrap[0](config_uri, options=parse_vars(self.args.config_vars)) + config_vars = parse_vars(self.args.config_vars) + self.setup_logging(config_uri, global_conf=config_vars) + env = self.bootstrap(config_uri, options=config_vars) registry = env['registry'] tweens = self._get_tweens(registry) if tweens is not None: diff --git a/pyramid/scripts/pviews.py b/pyramid/scripts/pviews.py index 4d3312917..c0df2f078 100644 --- a/pyramid/scripts/pviews.py +++ b/pyramid/scripts/pviews.py @@ -4,6 +4,7 @@ import textwrap from pyramid.interfaces import IMultiView from pyramid.paster import bootstrap +from pyramid.paster import setup_logging from pyramid.request import Request from pyramid.scripts.common import parse_vars from pyramid.view import _find_views @@ -51,7 +52,8 @@ class PViewsCommand(object): ) - bootstrap = (bootstrap,) # testing + bootstrap = staticmethod(bootstrap) # testing + setup_logging = staticmethod(setup_logging) # testing def __init__(self, argv, quiet=False): self.quiet = quiet @@ -252,13 +254,15 @@ class PViewsCommand(object): self.out('Command requires a config file arg and a url arg') return 2 config_uri = self.args.config_uri + config_vars = parse_vars(self.args.config_vars) url = self.args.url + self.setup_logging(config_uri, global_conf=config_vars) + if not url.startswith('/'): url = '/%s' % url request = Request.blank(url) - env = self.bootstrap[0](config_uri, options=parse_vars(self.args.config_vars), - request=request) + env = self.bootstrap(config_uri, options=config_vars, request=request) view = self._find_view(request) self.out('') self.out("URL = %s" % url) diff --git a/pyramid/session.py b/pyramid/session.py index 47b80f617..33119343b 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -16,19 +16,15 @@ from pyramid.compat import ( text_, bytes_, native_, - urlparse, ) - -from pyramid.exceptions import ( - BadCSRFOrigin, - BadCSRFToken, +from pyramid.csrf import ( + check_csrf_origin, + check_csrf_token, ) + from pyramid.interfaces import ISession -from pyramid.settings import aslist -from pyramid.util import ( - is_same_domain, - strings_differ, -) +from pyramid.util import strings_differ + def manage_accessed(wrapped): """ Decorator which causes a cookie to be renewed when an accessor @@ -109,149 +105,6 @@ def signed_deserialize(serialized, secret, hmac=hmac): return pickle.loads(pickled) -def check_csrf_origin(request, trusted_origins=None, raises=True): - """ - Check the Origin of the request to see if it is a cross site request or - not. - - If the value supplied by the Origin or Referer header isn't one of the - trusted origins and ``raises`` is ``True``, this function will raise a - :exc:`pyramid.exceptions.BadCSRFOrigin` exception but if ``raises`` is - ``False`` this function will return ``False`` instead. If the CSRF origin - checks are successful this function will return ``True`` unconditionally. - - Additional trusted origins may be added by passing a list of domain (and - ports if nonstandard like `['example.com', 'dev.example.com:8080']`) in - with the ``trusted_origins`` parameter. If ``trusted_origins`` is ``None`` - (the default) this list of additional domains will be pulled from the - ``pyramid.csrf_trusted_origins`` setting. - - Note that this function will do nothing if request.scheme is not https. - - .. versionadded:: 1.7 - """ - def _fail(reason): - if raises: - raise BadCSRFOrigin(reason) - else: - return False - - if request.scheme == "https": - # Suppose user visits http://example.com/ - # An active network attacker (man-in-the-middle, MITM) sends a - # POST form that targets https://example.com/detonate-bomb/ and - # submits it via JavaScript. - # - # The attacker will need to provide a CSRF cookie and token, but - # that's no problem for a MITM when we cannot make any assumptions - # about what kind of session storage is being used. So the MITM can - # circumvent the CSRF protection. This is true for any HTTP connection, - # but anyone using HTTPS expects better! For this reason, for - # https://example.com/ we need additional protection that treats - # http://example.com/ as completely untrusted. Under HTTPS, - # Barth et al. found that the Referer header is missing for - # same-domain requests in only about 0.2% of cases or less, so - # we can use strict Referer checking. - - # Determine the origin of this request - origin = request.headers.get("Origin") - if origin is None: - origin = request.referrer - - # Fail if we were not able to locate an origin at all - if not origin: - return _fail("Origin checking failed - no Origin or Referer.") - - # Parse our origin so we we can extract the required information from - # it. - originp = urlparse.urlparse(origin) - - # Ensure that our Referer is also secure. - if originp.scheme != "https": - return _fail( - "Referer checking failed - Referer is insecure while host is " - "secure." - ) - - # Determine which origins we trust, which by default will include the - # current origin. - if trusted_origins is None: - trusted_origins = aslist( - request.registry.settings.get( - "pyramid.csrf_trusted_origins", []) - ) - - if request.host_port not in set(["80", "443"]): - trusted_origins.append("{0.domain}:{0.host_port}".format(request)) - else: - trusted_origins.append(request.domain) - - # Actually check to see if the request's origin matches any of our - # trusted origins. - if not any(is_same_domain(originp.netloc, host) - for host in trusted_origins): - reason = ( - "Referer checking failed - {0} does not match any trusted " - "origins." - ) - return _fail(reason.format(origin)) - - return True - - -def check_csrf_token(request, - token='csrf_token', - header='X-CSRF-Token', - raises=True): - """ Check the CSRF token in the request's session against the value in - ``request.POST.get(token)`` (if a POST request) or - ``request.headers.get(header)``. If a ``token`` keyword is not supplied to - this function, the string ``csrf_token`` will be used to look up the token - in ``request.POST``. If a ``header`` keyword is not supplied to this - function, the string ``X-CSRF-Token`` will be used to look up the token in - ``request.headers``. - - If the value supplied by post or by header doesn't match the value - supplied by ``request.session.get_csrf_token()``, and ``raises`` is - ``True``, this function will raise an - :exc:`pyramid.exceptions.BadCSRFToken` exception. - If the values differ and ``raises`` is ``False``, this function will - return ``False``. If the CSRF check is successful, this function will - return ``True`` unconditionally. - - Note that using this function requires that a :term:`session factory` is - configured. - - See :ref:`auto_csrf_checking` for information about how to secure your - application automatically against CSRF attacks. - - .. versionadded:: 1.4a2 - - .. versionchanged:: 1.7a1 - A CSRF token passed in the query string of the request is no longer - considered valid. It must be passed in either the request body or - a header. - """ - supplied_token = "" - # If this is a POST/PUT/etc request, then we'll check the body to see if it - # has a token. We explicitly use request.POST here because CSRF tokens - # should never appear in an URL as doing so is a security issue. We also - # explicitly check for request.POST here as we do not support sending form - # encoded data over anything but a request.POST. - if token is not None: - supplied_token = request.POST.get(token, "") - - # If we were unable to locate a CSRF token in a request body, then we'll - # check to see if there are any headers that have a value for us. - if supplied_token == "" and header is not None: - supplied_token = request.headers.get(header, "") - - expected_token = request.session.get_csrf_token() - if strings_differ(bytes_(expected_token), bytes_(supplied_token)): - if raises: - raise BadCSRFToken('check_csrf_token(): Invalid token') - return False - return True class PickleSerializer(object): """ A serializer that uses the pickle protocol to dump Python @@ -759,3 +612,13 @@ def SignedCookieSessionFactory( reissue_time=reissue_time, set_on_exception=set_on_exception, ) + +check_csrf_origin = check_csrf_origin # api +deprecated('check_csrf_origin', + 'pyramid.session.check_csrf_origin is deprecated as of Pyramid ' + '1.9. Use pyramid.csrf.check_csrf_origin instead.') + +check_csrf_token = check_csrf_token # api +deprecated('check_csrf_token', + 'pyramid.session.check_csrf_token is deprecated as of Pyramid ' + '1.9. Use pyramid.csrf.check_csrf_token instead.') diff --git a/pyramid/testing.py b/pyramid/testing.py index 877b351db..69b30e83f 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -479,6 +479,7 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True, config.add_default_view_derivers() config.add_default_route_predicates() config.add_default_tweens() + config.add_default_security() config.commit() global have_zca try: diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 53c601537..ab584cc3d 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -141,6 +141,22 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(manager.pushed, pushed) self.assertEqual(manager.popped, True) + def test_context_manager(self): + from pyramid.config import Configurator + config = Configurator() + manager = DummyThreadLocalManager() + config.manager = manager + view = lambda r: None + with config as ctx: + self.assertTrue(config is ctx) + self.assertEqual(manager.pushed, + {'registry': config.registry, 'request': None}) + self.assertFalse(manager.popped) + config.add_view(view) + self.assertTrue(manager.popped) + config.add_view(view) # did not raise a conflict because of commit + config.commit() + def test_ctor_with_package_registry(self): import sys from pyramid.config import Configurator diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 211632730..0816d9958 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -18,6 +18,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): def _makeOne(self, *arg, **kw): from pyramid.config import Configurator config = Configurator(*arg, **kw) + config.set_default_csrf_options(require_csrf=False) return config def _getViewCallable(self, config, ctx_iface=None, exc_iface=None, @@ -2373,7 +2374,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): view = lambda r: 'OK' config.set_default_csrf_options(require_csrf=True) config.add_view(view, context=Exception, renderer=null_renderer) - view_intr = introspector.introspectables[1] + view_intr = introspector.introspectables[-1] self.assertTrue(view_intr.type_name, 'view') self.assertEqual(view_intr['callable'], view) derived_view = view_intr['derived_callable'] diff --git a/pyramid/tests/test_csrf.py b/pyramid/tests/test_csrf.py new file mode 100644 index 000000000..f01780ad8 --- /dev/null +++ b/pyramid/tests/test_csrf.py @@ -0,0 +1,406 @@ +import unittest + +from pyramid import testing +from pyramid.config import Configurator + + +class TestLegacySessionCSRFStoragePolicy(unittest.TestCase): + class MockSession(object): + def __init__(self, current_token='02821185e4c94269bdc38e6eeae0a2f8'): + self.current_token = current_token + + def new_csrf_token(self): + self.current_token = 'e5e9e30a08b34ff9842ff7d2b958c14b' + return self.current_token + + def get_csrf_token(self): + return self.current_token + + def _makeOne(self): + from pyramid.csrf import LegacySessionCSRFStoragePolicy + return LegacySessionCSRFStoragePolicy() + + def test_register_session_csrf_policy(self): + from pyramid.csrf import LegacySessionCSRFStoragePolicy + from pyramid.interfaces import ICSRFStoragePolicy + + config = Configurator() + config.set_csrf_storage_policy(self._makeOne()) + config.commit() + + policy = config.registry.queryUtility(ICSRFStoragePolicy) + + self.assertTrue(isinstance(policy, LegacySessionCSRFStoragePolicy)) + + def test_session_csrf_implementation_delegates_to_session(self): + policy = self._makeOne() + request = DummyRequest(session=self.MockSession()) + + self.assertEqual( + policy.get_csrf_token(request), + '02821185e4c94269bdc38e6eeae0a2f8' + ) + self.assertEqual( + policy.new_csrf_token(request), + 'e5e9e30a08b34ff9842ff7d2b958c14b' + ) + + def test_check_csrf_token(self): + request = DummyRequest(session=self.MockSession('foo')) + + policy = self._makeOne() + self.assertTrue(policy.check_csrf_token(request, 'foo')) + self.assertFalse(policy.check_csrf_token(request, 'bar')) + + +class TestSessionCSRFStoragePolicy(unittest.TestCase): + def _makeOne(self, **kw): + from pyramid.csrf import SessionCSRFStoragePolicy + return SessionCSRFStoragePolicy(**kw) + + def test_register_session_csrf_policy(self): + from pyramid.csrf import SessionCSRFStoragePolicy + from pyramid.interfaces import ICSRFStoragePolicy + + config = Configurator() + config.set_csrf_storage_policy(self._makeOne()) + config.commit() + + policy = config.registry.queryUtility(ICSRFStoragePolicy) + + self.assertTrue(isinstance(policy, SessionCSRFStoragePolicy)) + + def test_it_creates_a_new_token(self): + request = DummyRequest(session={}) + + policy = self._makeOne() + policy._token_factory = lambda: 'foo' + self.assertEqual(policy.get_csrf_token(request), 'foo') + + def test_get_csrf_token_returns_the_new_token(self): + request = DummyRequest(session={'_csrft_': 'foo'}) + + policy = self._makeOne() + self.assertEqual(policy.get_csrf_token(request), 'foo') + + token = policy.new_csrf_token(request) + self.assertNotEqual(token, 'foo') + self.assertEqual(token, policy.get_csrf_token(request)) + + def test_check_csrf_token(self): + request = DummyRequest(session={}) + + policy = self._makeOne() + self.assertFalse(policy.check_csrf_token(request, 'foo')) + + request.session = {'_csrft_': 'foo'} + self.assertTrue(policy.check_csrf_token(request, 'foo')) + self.assertFalse(policy.check_csrf_token(request, 'bar')) + + +class TestCookieCSRFStoragePolicy(unittest.TestCase): + def _makeOne(self, **kw): + from pyramid.csrf import CookieCSRFStoragePolicy + return CookieCSRFStoragePolicy(**kw) + + def test_register_cookie_csrf_policy(self): + from pyramid.csrf import CookieCSRFStoragePolicy + from pyramid.interfaces import ICSRFStoragePolicy + + config = Configurator() + config.set_csrf_storage_policy(self._makeOne()) + config.commit() + + policy = config.registry.queryUtility(ICSRFStoragePolicy) + + self.assertTrue(isinstance(policy, CookieCSRFStoragePolicy)) + + def test_get_cookie_csrf_with_no_existing_cookie_sets_cookies(self): + response = MockResponse() + request = DummyRequest() + + policy = self._makeOne() + token = policy.get_csrf_token(request) + request.response_callback(request, response) + self.assertEqual( + response.headerlist, + [('Set-Cookie', 'csrf_token={}; Path=/'.format(token))] + ) + + def test_existing_cookie_csrf_does_not_set_cookie(self): + request = DummyRequest() + request.cookies = {'csrf_token': 'e6f325fee5974f3da4315a8ccf4513d2'} + + policy = self._makeOne() + token = policy.get_csrf_token(request) + + self.assertEqual( + token, + 'e6f325fee5974f3da4315a8ccf4513d2' + ) + self.assertIsNone(request.response_callback) + + def test_new_cookie_csrf_with_existing_cookie_sets_cookies(self): + request = DummyRequest() + request.cookies = {'csrf_token': 'e6f325fee5974f3da4315a8ccf4513d2'} + + policy = self._makeOne() + token = policy.new_csrf_token(request) + + response = MockResponse() + request.response_callback(request, response) + self.assertEqual( + response.headerlist, + [('Set-Cookie', 'csrf_token={}; Path=/'.format(token))] + ) + + def test_get_csrf_token_returns_the_new_token(self): + request = DummyRequest() + request.cookies = {'csrf_token': 'foo'} + + policy = self._makeOne() + self.assertEqual(policy.get_csrf_token(request), 'foo') + + token = policy.new_csrf_token(request) + self.assertNotEqual(token, 'foo') + self.assertEqual(token, policy.get_csrf_token(request)) + + def test_check_csrf_token(self): + request = DummyRequest() + + policy = self._makeOne() + self.assertFalse(policy.check_csrf_token(request, 'foo')) + + request.cookies = {'csrf_token': 'foo'} + self.assertTrue(policy.check_csrf_token(request, 'foo')) + self.assertFalse(policy.check_csrf_token(request, 'bar')) + +class Test_get_csrf_token(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def _callFUT(self, *args, **kwargs): + from pyramid.csrf import get_csrf_token + return get_csrf_token(*args, **kwargs) + + def test_no_override_csrf_utility_registered(self): + request = testing.DummyRequest() + self._callFUT(request) + + def test_success(self): + self.config.set_csrf_storage_policy(DummyCSRF()) + request = testing.DummyRequest() + + csrf_token = self._callFUT(request) + + self.assertEquals(csrf_token, '02821185e4c94269bdc38e6eeae0a2f8') + + +class Test_new_csrf_token(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def _callFUT(self, *args, **kwargs): + from pyramid.csrf import new_csrf_token + return new_csrf_token(*args, **kwargs) + + def test_no_override_csrf_utility_registered(self): + request = testing.DummyRequest() + self._callFUT(request) + + def test_success(self): + self.config.set_csrf_storage_policy(DummyCSRF()) + request = testing.DummyRequest() + + csrf_token = self._callFUT(request) + + self.assertEquals(csrf_token, 'e5e9e30a08b34ff9842ff7d2b958c14b') + + +class Test_check_csrf_token(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + # set up CSRF + self.config.set_default_csrf_options(require_csrf=False) + + def _callFUT(self, *args, **kwargs): + from ..csrf import check_csrf_token + return check_csrf_token(*args, **kwargs) + + def test_success_token(self): + request = testing.DummyRequest() + request.method = "POST" + request.POST = {'csrf_token': request.session.get_csrf_token()} + self.assertEqual(self._callFUT(request, token='csrf_token'), True) + + def test_success_header(self): + request = testing.DummyRequest() + request.headers['X-CSRF-Token'] = request.session.get_csrf_token() + self.assertEqual(self._callFUT(request, header='X-CSRF-Token'), True) + + def test_success_default_token(self): + request = testing.DummyRequest() + request.method = "POST" + request.POST = {'csrf_token': request.session.get_csrf_token()} + self.assertEqual(self._callFUT(request), True) + + def test_success_default_header(self): + request = testing.DummyRequest() + request.headers['X-CSRF-Token'] = request.session.get_csrf_token() + self.assertEqual(self._callFUT(request), True) + + def test_failure_raises(self): + from pyramid.exceptions import BadCSRFToken + request = testing.DummyRequest() + self.assertRaises(BadCSRFToken, self._callFUT, request, + 'csrf_token') + + def test_failure_no_raises(self): + request = testing.DummyRequest() + result = self._callFUT(request, 'csrf_token', raises=False) + self.assertEqual(result, False) + + +class Test_check_csrf_token_without_defaults_configured(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def _callFUT(self, *args, **kwargs): + from ..csrf import check_csrf_token + return check_csrf_token(*args, **kwargs) + + def test_success_token(self): + request = testing.DummyRequest() + request.method = "POST" + request.POST = {'csrf_token': request.session.get_csrf_token()} + self.assertEqual(self._callFUT(request, token='csrf_token'), True) + + def test_failure_raises(self): + from pyramid.exceptions import BadCSRFToken + request = testing.DummyRequest() + self.assertRaises(BadCSRFToken, self._callFUT, request, + 'csrf_token') + + def test_failure_no_raises(self): + request = testing.DummyRequest() + result = self._callFUT(request, 'csrf_token', raises=False) + self.assertEqual(result, False) + + +class Test_check_csrf_origin(unittest.TestCase): + def _callFUT(self, *args, **kwargs): + from ..csrf import check_csrf_origin + return check_csrf_origin(*args, **kwargs) + + def test_success_with_http(self): + request = testing.DummyRequest() + request.scheme = "http" + self.assertTrue(self._callFUT(request)) + + def test_success_with_https_and_referrer(self): + request = testing.DummyRequest() + request.scheme = "https" + request.host = "example.com" + request.host_port = "443" + request.referrer = "https://example.com/login/" + request.registry.settings = {} + self.assertTrue(self._callFUT(request)) + + def test_success_with_https_and_origin(self): + request = testing.DummyRequest() + request.scheme = "https" + request.host = "example.com" + request.host_port = "443" + request.headers = {"Origin": "https://example.com/"} + request.referrer = "https://not-example.com/" + request.registry.settings = {} + self.assertTrue(self._callFUT(request)) + + def test_success_with_additional_trusted_host(self): + request = testing.DummyRequest() + request.scheme = "https" + request.host = "example.com" + request.host_port = "443" + request.referrer = "https://not-example.com/login/" + request.registry.settings = { + "pyramid.csrf_trusted_origins": ["not-example.com"], + } + self.assertTrue(self._callFUT(request)) + + def test_success_with_nonstandard_port(self): + request = testing.DummyRequest() + request.scheme = "https" + request.host = "example.com:8080" + request.host_port = "8080" + request.referrer = "https://example.com:8080/login/" + request.registry.settings = {} + self.assertTrue(self._callFUT(request)) + + def test_fails_with_wrong_host(self): + from pyramid.exceptions import BadCSRFOrigin + request = testing.DummyRequest() + request.scheme = "https" + request.host = "example.com" + request.host_port = "443" + request.referrer = "https://not-example.com/login/" + request.registry.settings = {} + self.assertRaises(BadCSRFOrigin, self._callFUT, request) + self.assertFalse(self._callFUT(request, raises=False)) + + def test_fails_with_no_origin(self): + from pyramid.exceptions import BadCSRFOrigin + request = testing.DummyRequest() + request.scheme = "https" + request.referrer = None + self.assertRaises(BadCSRFOrigin, self._callFUT, request) + self.assertFalse(self._callFUT(request, raises=False)) + + def test_fails_when_http_to_https(self): + from pyramid.exceptions import BadCSRFOrigin + request = testing.DummyRequest() + request.scheme = "https" + request.host = "example.com" + request.host_port = "443" + request.referrer = "http://example.com/evil/" + request.registry.settings = {} + self.assertRaises(BadCSRFOrigin, self._callFUT, request) + self.assertFalse(self._callFUT(request, raises=False)) + + def test_fails_with_nonstandard_port(self): + from pyramid.exceptions import BadCSRFOrigin + request = testing.DummyRequest() + request.scheme = "https" + request.host = "example.com:8080" + request.host_port = "8080" + request.referrer = "https://example.com/login/" + request.registry.settings = {} + self.assertRaises(BadCSRFOrigin, self._callFUT, request) + self.assertFalse(self._callFUT(request, raises=False)) + + +class DummyRequest(object): + registry = None + session = None + response_callback = None + + def __init__(self, registry=None, session=None): + self.registry = registry + self.session = session + self.cookies = {} + + def add_response_callback(self, callback): + self.response_callback = callback + + +class MockResponse(object): + def __init__(self): + self.headerlist = [] + + +class DummyCSRF(object): + def new_csrf_token(self, request): + return 'e5e9e30a08b34ff9842ff7d2b958c14b' + + def get_csrf_token(self, request): + return '02821185e4c94269bdc38e6eeae0a2f8' diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index 22a5cde3d..784458647 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -1,58 +1,32 @@ import os import unittest +from pyramid.tests.test_scripts.dummy import DummyLoader here = os.path.dirname(__file__) class Test_get_app(unittest.TestCase): - def _callFUT(self, config_file, section_name, **kw): - from pyramid.paster import get_app - return get_app(config_file, section_name, **kw) + def _callFUT(self, config_file, section_name, options=None, _loader=None): + import pyramid.paster + old_loader = pyramid.paster.get_config_loader + try: + if _loader is not None: + pyramid.paster.get_config_loader = _loader + return pyramid.paster.get_app(config_file, section_name, + options=options) + finally: + pyramid.paster.get_config_loader = old_loader def test_it(self): app = DummyApp() - loadapp = DummyLoadWSGI(app) - result = self._callFUT('/foo/bar/myapp.ini', 'myapp', loadapp=loadapp) - self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini') - self.assertEqual(loadapp.section_name, 'myapp') - self.assertEqual(loadapp.relative_to, os.getcwd()) - self.assertEqual(result, app) - - def test_it_with_hash(self): - app = DummyApp() - loadapp = DummyLoadWSGI(app) + loader = DummyLoader(app=app) result = self._callFUT( - '/foo/bar/myapp.ini#myapp', None, loadapp=loadapp - ) - self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini') - self.assertEqual(loadapp.section_name, 'myapp') - self.assertEqual(loadapp.relative_to, os.getcwd()) - self.assertEqual(result, app) - - def test_it_with_hash_and_name_override(self): - app = DummyApp() - loadapp = DummyLoadWSGI(app) - result = self._callFUT( - '/foo/bar/myapp.ini#myapp', 'yourapp', loadapp=loadapp - ) - self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini') - self.assertEqual(loadapp.section_name, 'yourapp') - self.assertEqual(loadapp.relative_to, os.getcwd()) - self.assertEqual(result, app) - - def test_it_with_options(self): - app = DummyApp() - loadapp = DummyLoadWSGI(app) - options = {'a':1} - result = self._callFUT( - '/foo/bar/myapp.ini#myapp', - 'yourapp', - loadapp=loadapp, - options=options, - ) - self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini') - self.assertEqual(loadapp.section_name, 'yourapp') - self.assertEqual(loadapp.relative_to, os.getcwd()) - self.assertEqual(loadapp.kw, {'global_conf':options}) + '/foo/bar/myapp.ini', 'myapp', options={'a': 'b'}, + _loader=loader) + self.assertEqual(loader.uri.path, '/foo/bar/myapp.ini') + self.assertEqual(len(loader.calls), 1) + self.assertEqual(loader.calls[0]['op'], 'app') + self.assertEqual(loader.calls[0]['name'], 'myapp') + self.assertEqual(loader.calls[0]['defaults'], {'a': 'b'}) self.assertEqual(result, app) def test_it_with_dummyapp_requiring_options(self): @@ -63,38 +37,28 @@ class Test_get_app(unittest.TestCase): self.assertEqual(app.settings['foo'], 'baz') class Test_get_appsettings(unittest.TestCase): - def _callFUT(self, config_file, section_name, **kw): - from pyramid.paster import get_appsettings - return get_appsettings(config_file, section_name, **kw) + def _callFUT(self, config_file, section_name, options=None, _loader=None): + import pyramid.paster + old_loader = pyramid.paster.get_config_loader + try: + if _loader is not None: + pyramid.paster.get_config_loader = _loader + return pyramid.paster.get_appsettings(config_file, section_name, + options=options) + finally: + pyramid.paster.get_config_loader = old_loader def test_it(self): - values = {'a':1} - appconfig = DummyLoadWSGI(values) - result = self._callFUT('/foo/bar/myapp.ini', 'myapp', - appconfig=appconfig) - self.assertEqual(appconfig.config_name, 'config:/foo/bar/myapp.ini') - self.assertEqual(appconfig.section_name, 'myapp') - self.assertEqual(appconfig.relative_to, os.getcwd()) - self.assertEqual(result, values) - - def test_it_with_hash(self): - values = {'a':1} - appconfig = DummyLoadWSGI(values) - result = self._callFUT('/foo/bar/myapp.ini#myapp', None, - appconfig=appconfig) - self.assertEqual(appconfig.config_name, 'config:/foo/bar/myapp.ini') - self.assertEqual(appconfig.section_name, 'myapp') - self.assertEqual(appconfig.relative_to, os.getcwd()) - self.assertEqual(result, values) - - def test_it_with_hash_and_name_override(self): - values = {'a':1} - appconfig = DummyLoadWSGI(values) - result = self._callFUT('/foo/bar/myapp.ini#myapp', 'yourapp', - appconfig=appconfig) - self.assertEqual(appconfig.config_name, 'config:/foo/bar/myapp.ini') - self.assertEqual(appconfig.section_name, 'yourapp') - self.assertEqual(appconfig.relative_to, os.getcwd()) + values = {'a': 1} + loader = DummyLoader(app_settings=values) + result = self._callFUT( + '/foo/bar/myapp.ini', 'myapp', options={'a': 'b'}, + _loader=loader) + self.assertEqual(loader.uri.path, '/foo/bar/myapp.ini') + self.assertEqual(len(loader.calls), 1) + self.assertEqual(loader.calls[0]['op'], 'app_settings') + self.assertEqual(loader.calls[0]['name'], 'myapp') + self.assertEqual(loader.calls[0]['defaults'], {'a': 'b'}) self.assertEqual(result, values) def test_it_with_dummyapp_requiring_options(self): @@ -105,40 +69,39 @@ class Test_get_appsettings(unittest.TestCase): self.assertEqual(result['foo'], 'baz') class Test_setup_logging(unittest.TestCase): - def _callFUT(self, config_file, global_conf=None): - from pyramid.paster import setup_logging - dummy_cp = DummyConfigParserModule - return setup_logging( - config_uri=config_file, - global_conf=global_conf, - fileConfig=self.fileConfig, - configparser=dummy_cp, - ) + def _callFUT(self, config_file, global_conf=None, _loader=None): + import pyramid.paster + old_loader = pyramid.paster.get_config_loader + try: + if _loader is not None: + pyramid.paster.get_config_loader = _loader + return pyramid.paster.setup_logging(config_file, global_conf) + finally: + pyramid.paster.get_config_loader = old_loader def test_it_no_global_conf(self): - config_file, dict = self._callFUT('/abc') - # os.path.abspath is a sop to Windows - self.assertEqual(config_file, os.path.abspath('/abc')) - self.assertEqual(dict['__file__'], os.path.abspath('/abc')) - self.assertEqual(dict['here'], os.path.abspath('/')) + loader = DummyLoader() + self._callFUT('/abc.ini', _loader=loader) + self.assertEqual(loader.uri.path, '/abc.ini') + self.assertEqual(len(loader.calls), 1) + self.assertEqual(loader.calls[0]['op'], 'logging') + self.assertEqual(loader.calls[0]['defaults'], None) def test_it_global_conf_empty(self): - config_file, dict = self._callFUT('/abc', global_conf={}) - # os.path.abspath is a sop to Windows - self.assertEqual(config_file, os.path.abspath('/abc')) - self.assertEqual(dict['__file__'], os.path.abspath('/abc')) - self.assertEqual(dict['here'], os.path.abspath('/')) + loader = DummyLoader() + self._callFUT('/abc.ini', global_conf={}, _loader=loader) + self.assertEqual(loader.uri.path, '/abc.ini') + self.assertEqual(len(loader.calls), 1) + self.assertEqual(loader.calls[0]['op'], 'logging') + self.assertEqual(loader.calls[0]['defaults'], {}) def test_it_global_conf_not_empty(self): - config_file, dict = self._callFUT('/abc', global_conf={'key': 'val'}) - # os.path.abspath is a sop to Windows - self.assertEqual(config_file, os.path.abspath('/abc')) - self.assertEqual(dict['__file__'], os.path.abspath('/abc')) - self.assertEqual(dict['here'], os.path.abspath('/')) - self.assertEqual(dict['key'], 'val') - - def fileConfig(self, config_file, dict): - return config_file, dict + loader = DummyLoader() + self._callFUT('/abc.ini', global_conf={'key': 'val'}, _loader=loader) + self.assertEqual(loader.uri.path, '/abc.ini') + self.assertEqual(len(loader.calls), 1) + self.assertEqual(loader.calls[0]['op'], 'logging') + self.assertEqual(loader.calls[0]['defaults'], {'key': 'val'}) class Test_bootstrap(unittest.TestCase): def _callFUT(self, config_uri, request=None): @@ -187,17 +150,6 @@ class DummyRegistry(object): dummy_registry = DummyRegistry() -class DummyLoadWSGI: - def __init__(self, result): - self.result = result - - def __call__(self, config_name, name=None, relative_to=None, **kw): - self.config_name = config_name - self.section_name = name - self.relative_to = relative_to - self.kw = kw - return self.result - class DummyApp: def __init__(self): self.registry = dummy_registry @@ -214,13 +166,3 @@ class DummyRequest: def __init__(self, environ): self.environ = environ self.matchdict = {} - -class DummyConfigParser(object): - def read(self, x): - pass - - def has_section(self, name): - return True - -class DummyConfigParserModule(object): - ConfigParser = DummyConfigParser diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 65bfa5582..86d8b582a 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -203,6 +203,7 @@ class TestRendererHelper(unittest.TestCase): self.assertEqual(helper.get_renderer(), factory.respond) def test_render_view(self): + import pyramid.csrf self._registerRendererFactory() self._registerResponseFactory() request = Dummy() @@ -212,6 +213,9 @@ class TestRendererHelper(unittest.TestCase): request = testing.DummyRequest() response = 'response' response = helper.render_view(request, response, view, context) + get_csrf = response.app_iter[1].pop('get_csrf_token') + self.assertEqual(get_csrf.args, (request, )) + self.assertEqual(get_csrf.func, pyramid.csrf.get_csrf_token) self.assertEqual(response.app_iter[0], 'response') self.assertEqual(response.app_iter[1], {'renderer_info': helper, @@ -242,12 +246,16 @@ class TestRendererHelper(unittest.TestCase): self.assertEqual(reg.event.__class__.__name__, 'BeforeRender') def test_render_system_values_is_None(self): + import pyramid.csrf self._registerRendererFactory() request = Dummy() context = Dummy() request.context = context helper = self._makeOne('loo.foo') result = helper.render('values', None, request=request) + get_csrf = result[1].pop('get_csrf_token') + self.assertEqual(get_csrf.args, (request, )) + self.assertEqual(get_csrf.func, pyramid.csrf.get_csrf_token) system = {'request':request, 'context':context, 'renderer_name':'loo.foo', diff --git a/pyramid/tests/test_scripts/dummy.py b/pyramid/tests/test_scripts/dummy.py index ced09d0b0..2d2b0549f 100644 --- a/pyramid/tests/test_scripts/dummy.py +++ b/pyramid/tests/test_scripts/dummy.py @@ -81,29 +81,6 @@ class DummyMultiView(object): self.views = [(None, view, None) for view in views] self.__request_attrs__ = attrs -class DummyConfigParser(object): - def __init__(self, result, defaults=None): - self.result = result - self.defaults = defaults - - def read(self, filename): - self.filename = filename - - def items(self, section): - self.section = section - if self.result is None: - from pyramid.compat import configparser - raise configparser.NoSectionError(section) - return self.result - -class DummyConfigParserFactory(object): - items = None - - def __call__(self, defaults=None): - self.defaults = defaults - self.parser = DummyConfigParser(self.items, defaults) - return self.parser - class DummyCloser(object): def __call__(self): self.called = True @@ -162,3 +139,50 @@ class DummyPkgResources(object): def iter_entry_points(self, name): return self.entry_points + + +class dummy_setup_logging(object): + def __call__(self, config_uri, global_conf): + self.config_uri = config_uri + self.defaults = global_conf + + +class DummyLoader(object): + def __init__(self, settings=None, app_settings=None, app=None, server=None): + if not settings: + settings = {} + if not app_settings: + app_settings = {} + self.settings = settings + self.app_settings = app_settings + self.app = app + self.server = server + self.calls = [] + + def __call__(self, uri): + import plaster + self.uri = plaster.parse_uri(uri) + return self + + def add_call(self, op, name, defaults): + self.calls.append({'op': op, 'name': name, 'defaults': defaults}) + + def get_settings(self, name=None, defaults=None): + self.add_call('settings', name, defaults) + return self.settings.get(name, {}) + + def get_wsgi_app(self, name=None, defaults=None): + self.add_call('app', name, defaults) + return self.app + + def get_wsgi_app_settings(self, name=None, defaults=None): + self.add_call('app_settings', name, defaults) + return self.app_settings + + def get_wsgi_server(self, name=None, defaults=None): + self.add_call('server', name, defaults) + return self.server + + def setup_logging(self, defaults): + self.add_call('logging', None, defaults) + self.defaults = defaults diff --git a/pyramid/tests/test_scripts/test_prequest.py b/pyramid/tests/test_scripts/test_prequest.py index 45db0dbaf..75d5cc198 100644 --- a/pyramid/tests/test_scripts/test_prequest.py +++ b/pyramid/tests/test_scripts/test_prequest.py @@ -1,4 +1,5 @@ import unittest +from pyramid.tests.test_scripts import dummy class TestPRequestCommand(unittest.TestCase): def _getTargetClass(self): @@ -7,23 +8,17 @@ class TestPRequestCommand(unittest.TestCase): def _makeOne(self, argv, headers=None): cmd = self._getTargetClass()(argv) - cmd.get_app = self.get_app - self._headers = headers or [] - self._out = [] - cmd.out = self.out - return cmd - - def get_app(self, spec, app_name=None, options=None): - self._spec = spec - self._app_name = app_name - self._options = options or {} def helloworld(environ, start_request): self._environ = environ self._path_info = environ['PATH_INFO'] - start_request('200 OK', self._headers) + start_request('200 OK', headers or []) return [b'abc'] - return helloworld + self.loader = dummy.DummyLoader(app=helloworld) + self._out = [] + cmd._get_config_loader = self.loader + cmd.out = self.out + return cmd def out(self, msg): self._out.append(msg) @@ -38,8 +33,10 @@ class TestPRequestCommand(unittest.TestCase): [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._path_info, '/') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) + self.assertEqual(self.loader.uri.path, 'development.ini') + self.assertEqual(self.loader.calls[0]['op'], 'logging') + self.assertEqual(self.loader.calls[1]['op'], 'app') + self.assertEqual(self.loader.calls[1]['name'], None) self.assertEqual(self._out, ['abc']) def test_command_path_doesnt_start_with_slash(self): @@ -47,8 +44,7 @@ class TestPRequestCommand(unittest.TestCase): [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._path_info, '/abc') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) + self.assertEqual(self.loader.uri.path, 'development.ini') self.assertEqual(self._out, ['abc']) def test_command_has_bad_config_header(self): @@ -67,8 +63,6 @@ class TestPRequestCommand(unittest.TestCase): command.run() self.assertEqual(self._environ['HTTP_NAME'], 'value') self.assertEqual(self._path_info, '/') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) self.assertEqual(self._out, ['abc']) def test_command_w_basic_auth(self): @@ -81,8 +75,6 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._environ['HTTP_AUTHORIZATION'], 'Basic dXNlcjpwYXNzd29yZA==') self.assertEqual(self._path_info, '/') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) self.assertEqual(self._out, ['abc']) def test_command_has_content_type_header_var(self): @@ -92,8 +84,6 @@ class TestPRequestCommand(unittest.TestCase): command.run() self.assertEqual(self._environ['CONTENT_TYPE'], 'app/foo') self.assertEqual(self._path_info, '/') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) self.assertEqual(self._out, ['abc']) def test_command_has_multiple_header_vars(self): @@ -109,8 +99,6 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._environ['HTTP_NAME'], 'value') self.assertEqual(self._environ['HTTP_NAME2'], 'value2') self.assertEqual(self._path_info, '/') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) self.assertEqual(self._out, ['abc']) def test_command_method_get(self): @@ -119,8 +107,6 @@ class TestPRequestCommand(unittest.TestCase): command.run() self.assertEqual(self._environ['REQUEST_METHOD'], 'GET') self.assertEqual(self._path_info, '/') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) self.assertEqual(self._out, ['abc']) def test_command_method_post(self): @@ -134,8 +120,6 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._environ['CONTENT_LENGTH'], '-1') self.assertEqual(self._environ['wsgi.input'], stdin) self.assertEqual(self._path_info, '/') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) self.assertEqual(self._out, ['abc']) def test_command_method_put(self): @@ -149,8 +133,6 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._environ['CONTENT_LENGTH'], '-1') self.assertEqual(self._environ['wsgi.input'], stdin) self.assertEqual(self._path_info, '/') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) self.assertEqual(self._out, ['abc']) def test_command_method_patch(self): @@ -164,8 +146,6 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._environ['CONTENT_LENGTH'], '-1') self.assertEqual(self._environ['wsgi.input'], stdin) self.assertEqual(self._path_info, '/') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) self.assertEqual(self._out, ['abc']) def test_command_method_propfind(self): @@ -178,8 +158,6 @@ class TestPRequestCommand(unittest.TestCase): command.run() self.assertEqual(self._environ['REQUEST_METHOD'], 'PROPFIND') self.assertEqual(self._path_info, '/') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) self.assertEqual(self._out, ['abc']) def test_command_method_options(self): @@ -192,8 +170,6 @@ class TestPRequestCommand(unittest.TestCase): command.run() self.assertEqual(self._environ['REQUEST_METHOD'], 'OPTIONS') self.assertEqual(self._path_info, '/') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) self.assertEqual(self._out, ['abc']) def test_command_with_query_string(self): @@ -202,8 +178,6 @@ class TestPRequestCommand(unittest.TestCase): command.run() self.assertEqual(self._environ['QUERY_STRING'], 'a=1&b=2&c') self.assertEqual(self._path_info, '/abc') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) self.assertEqual(self._out, ['abc']) def test_command_display_headers(self): @@ -212,8 +186,6 @@ class TestPRequestCommand(unittest.TestCase): [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._path_info, '/') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) self.assertEqual( self._out, ['200 OK', 'Content-Type: text/html; charset=UTF-8', 'abc']) @@ -223,21 +195,13 @@ class TestPRequestCommand(unittest.TestCase): headers=[('Content-Type', 'image/jpeg')]) command.run() self.assertEqual(self._path_info, '/') - self.assertEqual(self._spec, 'development.ini') - self.assertEqual(self._app_name, None) self.assertEqual(self._out, [b'abc']) def test_command_method_configures_logging(self): command = self._makeOne(['', 'development.ini', '/']) - called_args = [] - - def configure_logging(app_spec): - called_args.append(app_spec) - - command.configure_logging = configure_logging command.run() - self.assertEqual(called_args, ['development.ini']) + self.assertEqual(self.loader.calls[0]['op'], 'logging') class Test_main(unittest.TestCase): diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index 74293a112..fab5e163e 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -19,7 +19,8 @@ class TestPRoutesCommand(unittest.TestCase): def _makeOne(self): cmd = self._getTargetClass()([]) - cmd.bootstrap = (dummy.DummyBootstrap(),) + cmd.bootstrap = dummy.DummyBootstrap() + cmd.get_config_loader = dummy.DummyLoader() cmd.args.config_uri = '/foo/bar/myapp.ini#myapp' return cmd @@ -37,14 +38,15 @@ class TestPRoutesCommand(unittest.TestCase): def test_good_args(self): cmd = self._getTargetClass()([]) - cmd.bootstrap = (dummy.DummyBootstrap(),) + cmd.bootstrap = dummy.DummyBootstrap() + cmd.get_config_loader = dummy.DummyLoader() cmd.args.config_uri = '/foo/bar/myapp.ini#myapp' cmd.args.config_args = ('a=1',) route = dummy.DummyRoute('a', '/a') mapper = dummy.DummyMapper(route) cmd._get_mapper = lambda *arg: mapper registry = self._makeRegistry() - cmd.bootstrap = (dummy.DummyBootstrap(registry=registry),) + cmd.bootstrap = dummy.DummyBootstrap(registry=registry) L = [] cmd.out = lambda msg: L.append(msg) cmd.run() @@ -52,7 +54,8 @@ class TestPRoutesCommand(unittest.TestCase): def test_bad_args(self): cmd = self._getTargetClass()([]) - cmd.bootstrap = (dummy.DummyBootstrap(),) + cmd.bootstrap = dummy.DummyBootstrap() + cmd.get_config_loader = dummy.DummyLoader() cmd.args.config_uri = '/foo/bar/myapp.ini#myapp' cmd.args.config_vars = ('a',) route = dummy.DummyRoute('a', '/a') @@ -86,7 +89,7 @@ class TestPRoutesCommand(unittest.TestCase): mapper = dummy.DummyMapper(route) command._get_mapper = lambda *arg: mapper registry = self._makeRegistry() - command.bootstrap = (dummy.DummyBootstrap(registry=registry),) + command.bootstrap = dummy.DummyBootstrap(registry=registry) L = [] command.out = L.append @@ -103,7 +106,7 @@ class TestPRoutesCommand(unittest.TestCase): L = [] command.out = L.append registry = self._makeRegistry() - command.bootstrap = (dummy.DummyBootstrap(registry=registry),) + command.bootstrap = dummy.DummyBootstrap(registry=registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -124,7 +127,7 @@ class TestPRoutesCommand(unittest.TestCase): command._get_mapper = lambda *arg: mapper L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=registry),) + command.bootstrap = dummy.DummyBootstrap(registry=registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -150,7 +153,7 @@ class TestPRoutesCommand(unittest.TestCase): command._get_mapper = lambda *arg: mapper L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=registry),) + command.bootstrap = dummy.DummyBootstrap(registry=registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -190,7 +193,7 @@ class TestPRoutesCommand(unittest.TestCase): command._get_mapper = lambda *arg: mapper L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=registry),) + command.bootstrap = dummy.DummyBootstrap(registry=registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -218,7 +221,7 @@ class TestPRoutesCommand(unittest.TestCase): command = self._makeOne() L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -252,7 +255,7 @@ class TestPRoutesCommand(unittest.TestCase): command._get_mapper = lambda *arg: mapper L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=registry),) + command.bootstrap = dummy.DummyBootstrap(registry=registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -288,7 +291,7 @@ class TestPRoutesCommand(unittest.TestCase): command._get_mapper = lambda *arg: mapper L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=registry),) + command.bootstrap = dummy.DummyBootstrap(registry=registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -327,7 +330,7 @@ class TestPRoutesCommand(unittest.TestCase): command = self._makeOne() L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -354,7 +357,7 @@ class TestPRoutesCommand(unittest.TestCase): command = self._makeOne() L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -382,7 +385,7 @@ class TestPRoutesCommand(unittest.TestCase): command = self._makeOne() L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -410,7 +413,7 @@ class TestPRoutesCommand(unittest.TestCase): command = self._makeOne() L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -436,7 +439,7 @@ class TestPRoutesCommand(unittest.TestCase): command = self._makeOne() L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 5) @@ -461,7 +464,7 @@ class TestPRoutesCommand(unittest.TestCase): command = self._makeOne() L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -491,7 +494,7 @@ class TestPRoutesCommand(unittest.TestCase): command = self._makeOne() L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config2.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config2.registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -521,7 +524,7 @@ class TestPRoutesCommand(unittest.TestCase): command = self._makeOne() L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -551,7 +554,7 @@ class TestPRoutesCommand(unittest.TestCase): command = self._makeOne() L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -592,7 +595,7 @@ class TestPRoutesCommand(unittest.TestCase): L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -624,7 +627,7 @@ class TestPRoutesCommand(unittest.TestCase): command.args.format = 'method,name' L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) @@ -654,7 +657,7 @@ class TestPRoutesCommand(unittest.TestCase): command.args.format = 'predicates,name,pattern' L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) expected = ( "You provided invalid formats ['predicates'], " "Available formats are ['name', 'pattern', 'view', 'method']" @@ -682,10 +685,9 @@ class TestPRoutesCommand(unittest.TestCase): L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) - config_factory = dummy.DummyConfigParserFactory() - command.ConfigParser = config_factory - config_factory.items = [('format', 'method\nname')] + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + command.get_config_loader = dummy.DummyLoader( + {'proutes': {'format': 'method\nname'}}) result = command.run() self.assertEqual(result, 0) @@ -715,10 +717,9 @@ class TestPRoutesCommand(unittest.TestCase): L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) - config_factory = dummy.DummyConfigParserFactory() - command.ConfigParser = config_factory - config_factory.items = [('format', 'method name')] + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + command.get_config_loader = dummy.DummyLoader( + {'proutes': {'format': 'method name'}}) result = command.run() self.assertEqual(result, 0) @@ -748,10 +749,9 @@ class TestPRoutesCommand(unittest.TestCase): L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) - config_factory = dummy.DummyConfigParserFactory() - command.ConfigParser = config_factory - config_factory.items = [('format', 'method,name')] + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + command.get_config_loader = dummy.DummyLoader( + {'proutes': {'format': 'method,name'}}) result = command.run() self.assertEqual(result, 0) @@ -771,7 +771,7 @@ class TestPRoutesCommand(unittest.TestCase): command = self._makeOne() L = [] command.out = L.append - command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py index d5578b3ea..485cf38cb 100644 --- a/pyramid/tests/test_scripts/test_pserve.py +++ b/pyramid/tests/test_scripts/test_pserve.py @@ -10,16 +10,10 @@ class TestPServeCommand(unittest.TestCase): def setUp(self): from pyramid.compat import NativeIO self.out_ = NativeIO() - self.config_factory = dummy.DummyConfigParserFactory() def out(self, msg): self.out_.write(msg) - def _get_server(*args, **kwargs): - def server(app): - return '' - return server - def _getTargetClass(self): from pyramid.scripts.pserve import PServeCommand return PServeCommand @@ -29,7 +23,8 @@ class TestPServeCommand(unittest.TestCase): effargs.extend(args) cmd = self._getTargetClass()(effargs) cmd.out = self.out - cmd.ConfigParser = self.config_factory + self.loader = dummy.DummyLoader() + cmd._get_config_loader = self.loader return cmd def test_run_no_args(self): @@ -38,41 +33,33 @@ class TestPServeCommand(unittest.TestCase): self.assertEqual(result, 2) self.assertEqual(self.out_.getvalue(), 'You must give a config file') - def test_config_vars_no_command(self): - inst = self._makeOne() - inst.args.config_uri = 'foo' - inst.args.config_vars = ['a=1', 'b=2'] - result = inst.get_config_vars() - self.assertEqual(result, {'a': '1', 'b': '2'}) - def test_parse_vars_good(self): inst = self._makeOne('development.ini', 'a=1', 'b=2') - inst.loadserver = self._get_server - app = dummy.DummyApp() - def get_app(*args, **kwargs): - app.global_conf = kwargs.get('global_conf', None) + def get_app(name, global_conf): + app.name = name + app.global_conf = global_conf + return app + self.loader.get_wsgi_app = get_app + self.loader.server = lambda x: x - inst.loadapp = get_app inst.run() self.assertEqual(app.global_conf, {'a': '1', 'b': '2'}) def test_parse_vars_bad(self): inst = self._makeOne('development.ini', 'a') - inst.loadserver = self._get_server self.assertRaises(ValueError, inst.run) def test_config_file_finds_watch_files(self): inst = self._makeOne('development.ini') - self.config_factory.items = [( - 'watch_files', - 'foo\n/baz\npyramid.tests.test_scripts:*.py', - )] - inst.pserve_file_config('/base/path.ini', global_conf={'a': '1'}) - self.assertEqual(self.config_factory.defaults, { + loader = self.loader('/base/path.ini') + loader.settings = {'pserve': { + 'watch_files': 'foo\n/baz\npyramid.tests.test_scripts:*.py', + }} + inst.pserve_file_config(loader, global_conf={'a': '1'}) + self.assertEqual(loader.calls[0]['defaults'], { 'a': '1', - 'here': os.path.abspath('/base'), }) self.assertEqual(inst.watch_files, set([ os.path.abspath('/base/foo'), @@ -82,28 +69,26 @@ class TestPServeCommand(unittest.TestCase): def test_config_file_finds_open_url(self): inst = self._makeOne('development.ini') - self.config_factory.items = [( - 'open_url', 'http://127.0.0.1:8080/', - )] - inst.pserve_file_config('/base/path.ini', global_conf={'a': '1'}) - self.assertEqual(self.config_factory.defaults, { + loader = self.loader('/base/path.ini') + loader.settings = {'pserve': { + 'open_url': 'http://127.0.0.1:8080/', + }} + inst.pserve_file_config(loader, global_conf={'a': '1'}) + self.assertEqual(loader.calls[0]['defaults'], { 'a': '1', - 'here': os.path.abspath('/base'), }) self.assertEqual(inst.open_url, 'http://127.0.0.1:8080/') - def test__guess_server_url(self): + def test_guess_server_url(self): inst = self._makeOne('development.ini') - self.config_factory.items = [( - 'port', '8080', - )] - url = inst._guess_server_url( - '/base/path.ini', 'main', global_conf={'a': '1'}) - self.assertEqual(self.config_factory.defaults, { + loader = self.loader('/base/path.ini') + loader.settings = {'server:foo': { + 'port': '8080', + }} + url = inst.guess_server_url(loader, 'foo', global_conf={'a': '1'}) + self.assertEqual(loader.calls[0]['defaults'], { 'a': '1', - 'here': os.path.abspath('/base'), }) - self.assertEqual(self.config_factory.parser.section, 'server:main') self.assertEqual(url, 'http://127.0.0.1:8080') def test_reload_call_hupper_with_correct_args(self): diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py index 303964b2b..ca9eb7af2 100644 --- a/pyramid/tests/test_scripts/test_pshell.py +++ b/pyramid/tests/test_scripts/test_pshell.py @@ -8,16 +8,16 @@ class TestPShellCommand(unittest.TestCase): from pyramid.scripts.pshell import PShellCommand return PShellCommand - def _makeOne(self, patch_bootstrap=True, patch_config=True, + def _makeOne(self, patch_bootstrap=True, patch_loader=True, patch_args=True, patch_options=True): cmd = self._getTargetClass()([]) if patch_bootstrap: self.bootstrap = dummy.DummyBootstrap() - cmd.bootstrap = (self.bootstrap,) - if patch_config: - self.config_factory = dummy.DummyConfigParserFactory() - cmd.ConfigParser = self.config_factory + cmd.bootstrap = self.bootstrap + if patch_loader: + self.loader = dummy.DummyLoader() + cmd.get_config_loader = self.loader if patch_args: class Args(object): pass self.args = Args() @@ -46,9 +46,6 @@ class TestPShellCommand(unittest.TestCase): command.default_runner = shell command.run() - self.assertTrue(self.config_factory.parser) - self.assertEqual(self.config_factory.parser.filename, - '/foo/bar/myapp.ini') self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') self.assertEqual(shell.env, { 'app':self.bootstrap.app, 'root':self.bootstrap.root, @@ -79,9 +76,6 @@ class TestPShellCommand(unittest.TestCase): self.assertEqual( out_calls, ['could not find a shell named "unknown_python_shell"'] ) - self.assertTrue(self.config_factory.parser) - self.assertEqual(self.config_factory.parser.filename, - '/foo/bar/myapp.ini') self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') self.assertTrue(self.bootstrap.closer.called) @@ -100,9 +94,6 @@ class TestPShellCommand(unittest.TestCase): command.args.python_shell = 'ipython' command.run() - self.assertTrue(self.config_factory.parser) - self.assertEqual(self.config_factory.parser.filename, - '/foo/bar/myapp.ini') self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') self.assertEqual(shell.env, { 'app':self.bootstrap.app, 'root':self.bootstrap.root, @@ -199,12 +190,9 @@ class TestPShellCommand(unittest.TestCase): command = self._makeOne() model = dummy.Dummy() user = dummy.Dummy() - self.config_factory.items = [('m', model), ('User', user)] + self.loader.settings = {'pshell': {'m': model, 'User': user}} shell = dummy.DummyShell() command.run(shell) - self.assertTrue(self.config_factory.parser) - self.assertEqual(self.config_factory.parser.filename, - '/foo/bar/myapp.ini') self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') self.assertEqual(shell.env, { 'app':self.bootstrap.app, 'root':self.bootstrap.root, @@ -223,12 +211,9 @@ class TestPShellCommand(unittest.TestCase): env['a'] = 1 env['root'] = 'root override' env['none'] = None - self.config_factory.items = [('setup', setup)] + self.loader.settings = {'pshell': {'setup': setup}} shell = dummy.DummyShell() command.run(shell) - self.assertTrue(self.config_factory.parser) - self.assertEqual(self.config_factory.parser.filename, - '/foo/bar/myapp.ini') self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') self.assertEqual(shell.env, { 'app':self.bootstrap.app, 'root':'root override', @@ -252,12 +237,9 @@ class TestPShellCommand(unittest.TestCase): 'python': dshell, } ) - self.config_factory.items = [ - ('default_shell', 'bpython python\nipython')] + self.loader.settings = {'pshell': { + 'default_shell': 'bpython python\nipython'}} command.run() - self.assertTrue(self.config_factory.parser) - self.assertEqual(self.config_factory.parser.filename, - '/foo/bar/myapp.ini') self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') self.assertTrue(dshell.called) @@ -268,12 +250,9 @@ class TestPShellCommand(unittest.TestCase): env['a'] = 1 env['m'] = 'model override' env['root'] = 'root override' - self.config_factory.items = [('setup', setup), ('m', model)] + self.loader.settings = {'pshell': {'setup': setup, 'm': model}} shell = dummy.DummyShell() command.run(shell) - self.assertTrue(self.config_factory.parser) - self.assertEqual(self.config_factory.parser.filename, - '/foo/bar/myapp.ini') self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') self.assertEqual(shell.env, { 'app':self.bootstrap.app, 'root':'root override', @@ -291,14 +270,10 @@ class TestPShellCommand(unittest.TestCase): env['a'] = 1 env['root'] = 'root override' model = dummy.Dummy() - self.config_factory.items = [('setup', 'abc'), - ('m', model)] + self.loader.settings = {'pshell': {'setup': 'abc', 'm': model}} command.args.setup = setup shell = dummy.DummyShell() command.run(shell) - self.assertTrue(self.config_factory.parser) - self.assertEqual(self.config_factory.parser.filename, - '/foo/bar/myapp.ini') self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') self.assertEqual(shell.env, { 'app':self.bootstrap.app, 'root':'root override', @@ -313,13 +288,11 @@ class TestPShellCommand(unittest.TestCase): def test_command_custom_section_override(self): command = self._makeOne() dummy_ = dummy.Dummy() - self.config_factory.items = [('app', dummy_), ('root', dummy_), - ('registry', dummy_), ('request', dummy_)] + self.loader.settings = {'pshell': { + 'app': dummy_, 'root': dummy_, 'registry': dummy_, + 'request': dummy_}} shell = dummy.DummyShell() command.run(shell) - self.assertTrue(self.config_factory.parser) - self.assertEqual(self.config_factory.parser.filename, - '/foo/bar/myapp.ini') self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') self.assertEqual(shell.env, { 'app':dummy_, 'root':dummy_, 'registry':dummy_, 'request':dummy_, diff --git a/pyramid/tests/test_scripts/test_ptweens.py b/pyramid/tests/test_scripts/test_ptweens.py index f63069fed..6907b858d 100644 --- a/pyramid/tests/test_scripts/test_ptweens.py +++ b/pyramid/tests/test_scripts/test_ptweens.py @@ -8,7 +8,8 @@ class TestPTweensCommand(unittest.TestCase): def _makeOne(self): cmd = self._getTargetClass()([]) - cmd.bootstrap = (dummy.DummyBootstrap(),) + cmd.bootstrap = dummy.DummyBootstrap() + cmd.setup_logging = dummy.dummy_setup_logging() cmd.args.config_uri = '/foo/bar/myapp.ini#myapp' return cmd diff --git a/pyramid/tests/test_scripts/test_pviews.py b/pyramid/tests/test_scripts/test_pviews.py index 7bdab5804..6ec9defbd 100644 --- a/pyramid/tests/test_scripts/test_pviews.py +++ b/pyramid/tests/test_scripts/test_pviews.py @@ -8,7 +8,8 @@ class TestPViewsCommand(unittest.TestCase): def _makeOne(self, registry=None): cmd = self._getTargetClass()([]) - cmd.bootstrap = (dummy.DummyBootstrap(registry=registry),) + cmd.bootstrap = dummy.DummyBootstrap(registry=registry) + cmd.setup_logging = dummy.dummy_setup_logging() cmd.args.config_uri = '/foo/bar/myapp.ini#myapp' return cmd diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 3a308d08b..ade602799 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -659,144 +659,6 @@ class Test_signed_deserialize(unittest.TestCase): result = self._callFUT(serialized, secret.decode('latin-1')) self.assertEqual(result, '123') -class Test_check_csrf_token(unittest.TestCase): - def _callFUT(self, *args, **kwargs): - from ..session import check_csrf_token - return check_csrf_token(*args, **kwargs) - - def test_success_token(self): - request = testing.DummyRequest() - request.method = "POST" - request.POST = {'csrf_token': request.session.get_csrf_token()} - self.assertEqual(self._callFUT(request, token='csrf_token'), True) - - def test_success_header(self): - request = testing.DummyRequest() - request.headers['X-CSRF-Token'] = request.session.get_csrf_token() - self.assertEqual(self._callFUT(request, header='X-CSRF-Token'), True) - - def test_success_default_token(self): - request = testing.DummyRequest() - request.method = "POST" - request.POST = {'csrf_token': request.session.get_csrf_token()} - self.assertEqual(self._callFUT(request), True) - - def test_success_default_header(self): - request = testing.DummyRequest() - request.headers['X-CSRF-Token'] = request.session.get_csrf_token() - self.assertEqual(self._callFUT(request), True) - - def test_failure_raises(self): - from pyramid.exceptions import BadCSRFToken - request = testing.DummyRequest() - self.assertRaises(BadCSRFToken, self._callFUT, request, - 'csrf_token') - - def test_failure_no_raises(self): - request = testing.DummyRequest() - result = self._callFUT(request, 'csrf_token', raises=False) - self.assertEqual(result, False) - - def test_token_differing_types(self): - from pyramid.compat import text_ - request = testing.DummyRequest() - request.method = "POST" - request.session['_csrft_'] = text_('foo') - request.POST = {'csrf_token': b'foo'} - self.assertEqual(self._callFUT(request, token='csrf_token'), True) - - -class Test_check_csrf_origin(unittest.TestCase): - - def _callFUT(self, *args, **kwargs): - from ..session import check_csrf_origin - return check_csrf_origin(*args, **kwargs) - - def test_success_with_http(self): - request = testing.DummyRequest() - request.scheme = "http" - self.assertTrue(self._callFUT(request)) - - def test_success_with_https_and_referrer(self): - request = testing.DummyRequest() - request.scheme = "https" - request.host = "example.com" - request.host_port = "443" - request.referrer = "https://example.com/login/" - request.registry.settings = {} - self.assertTrue(self._callFUT(request)) - - def test_success_with_https_and_origin(self): - request = testing.DummyRequest() - request.scheme = "https" - request.host = "example.com" - request.host_port = "443" - request.headers = {"Origin": "https://example.com/"} - request.referrer = "https://not-example.com/" - request.registry.settings = {} - self.assertTrue(self._callFUT(request)) - - def test_success_with_additional_trusted_host(self): - request = testing.DummyRequest() - request.scheme = "https" - request.host = "example.com" - request.host_port = "443" - request.referrer = "https://not-example.com/login/" - request.registry.settings = { - "pyramid.csrf_trusted_origins": ["not-example.com"], - } - self.assertTrue(self._callFUT(request)) - - def test_success_with_nonstandard_port(self): - request = testing.DummyRequest() - request.scheme = "https" - request.host = "example.com:8080" - request.host_port = "8080" - request.referrer = "https://example.com:8080/login/" - request.registry.settings = {} - self.assertTrue(self._callFUT(request)) - - def test_fails_with_wrong_host(self): - from pyramid.exceptions import BadCSRFOrigin - request = testing.DummyRequest() - request.scheme = "https" - request.host = "example.com" - request.host_port = "443" - request.referrer = "https://not-example.com/login/" - request.registry.settings = {} - self.assertRaises(BadCSRFOrigin, self._callFUT, request) - self.assertFalse(self._callFUT(request, raises=False)) - - def test_fails_with_no_origin(self): - from pyramid.exceptions import BadCSRFOrigin - request = testing.DummyRequest() - request.scheme = "https" - request.referrer = None - self.assertRaises(BadCSRFOrigin, self._callFUT, request) - self.assertFalse(self._callFUT(request, raises=False)) - - def test_fails_when_http_to_https(self): - from pyramid.exceptions import BadCSRFOrigin - request = testing.DummyRequest() - request.scheme = "https" - request.host = "example.com" - request.host_port = "443" - request.referrer = "http://example.com/evil/" - request.registry.settings = {} - self.assertRaises(BadCSRFOrigin, self._callFUT, request) - self.assertFalse(self._callFUT(request, raises=False)) - - def test_fails_with_nonstandard_port(self): - from pyramid.exceptions import BadCSRFOrigin - request = testing.DummyRequest() - request.scheme = "https" - request.host = "example.com:8080" - request.host_port = "8080" - request.referrer = "https://example.com/login/" - request.registry.settings = {} - self.assertRaises(BadCSRFOrigin, self._callFUT, request) - self.assertFalse(self._callFUT(request, raises=False)) - class DummySerializer(object): def dumps(self, value): diff --git a/pyramid/tests/test_tweens.py b/pyramid/tests/test_tweens.py index c8eada34c..2e74ad7cf 100644 --- a/pyramid/tests/test_tweens.py +++ b/pyramid/tests/test_tweens.py @@ -22,6 +22,8 @@ class Test_excview_tween_factory(unittest.TestCase): request = DummyRequest() result = tween(request) self.assertTrue(result is dummy_response) + self.assertIsNone(request.exception) + self.assertIsNone(request.exc_info) def test_it_catches_notfound(self): from pyramid.request import Request @@ -31,8 +33,11 @@ class Test_excview_tween_factory(unittest.TestCase): raise HTTPNotFound tween = self._makeOne(handler) request = Request.blank('/') + request.registry = self.config.registry result = tween(request) self.assertEqual(result.status, '404 Not Found') + self.assertIsInstance(request.exception, HTTPNotFound) + self.assertEqual(request.exception, request.exc_info[1]) def test_it_catches_with_predicate(self): from pyramid.request import Request @@ -44,8 +49,11 @@ class Test_excview_tween_factory(unittest.TestCase): raise ValueError tween = self._makeOne(handler) request = Request.blank('/') + request.registry = self.config.registry result = tween(request) self.assertTrue(b'foo' in result.body) + self.assertIsInstance(request.exception, ValueError) + self.assertEqual(request.exception, request.exc_info[1]) def test_it_reraises_on_mismatch(self): from pyramid.request import Request @@ -55,8 +63,11 @@ class Test_excview_tween_factory(unittest.TestCase): raise ValueError tween = self._makeOne(handler) request = Request.blank('/') + request.registry = self.config.registry request.method = 'POST' self.assertRaises(ValueError, lambda: tween(request)) + self.assertIsNone(request.exception) + self.assertIsNone(request.exc_info) def test_it_reraises_on_no_match(self): from pyramid.request import Request @@ -64,10 +75,14 @@ class Test_excview_tween_factory(unittest.TestCase): raise ValueError tween = self._makeOne(handler) request = Request.blank('/') + request.registry = self.config.registry self.assertRaises(ValueError, lambda: tween(request)) + self.assertIsNone(request.exception) + self.assertIsNone(request.exc_info) class DummyRequest: - pass + exception = None + exc_info = None class DummyResponse: pass diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index bbf6103f4..d64f0a73f 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -369,12 +369,16 @@ class Test_strings_differ(unittest.TestCase): from pyramid.util import strings_differ return strings_differ(*args, **kw) - def test_it(self): + def test_it_bytes(self): self.assertFalse(self._callFUT(b'foo', b'foo')) self.assertTrue(self._callFUT(b'123', b'345')) self.assertTrue(self._callFUT(b'1234', b'123')) self.assertTrue(self._callFUT(b'123', b'1234')) + def test_it_native_str(self): + self.assertFalse(self._callFUT('123', '123')) + self.assertTrue(self._callFUT('123', '1234')) + def test_it_with_internal_comparator(self): result = self._callFUT(b'foo', b'foo', compare_digest=None) self.assertFalse(result) diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index cab42cf48..2061515b3 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -778,11 +778,11 @@ class TestViewMethodsMixin(unittest.TestCase): orig_response = request.response = DummyResponse(b'foo') try: raise RuntimeError - except RuntimeError: + except RuntimeError as ex: response = request.invoke_exception_view() self.assertEqual(response.app_iter, [b'bar']) - self.assertTrue(request.exception is orig_exc) - self.assertTrue(request.exc_info is orig_exc_info) + self.assertTrue(request.exception is ex) + self.assertTrue(request.exc_info[1] is ex) self.assertTrue(request.response is orig_response) else: # pragma: no cover self.fail() diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py index 51d0bd367..6b81cc1e5 100644 --- a/pyramid/tests/test_viewderivers.py +++ b/pyramid/tests/test_viewderivers.py @@ -12,6 +12,7 @@ class TestDeriveView(unittest.TestCase): def setUp(self): self.config = testing.setUp() + self.config.set_default_csrf_options(require_csrf=False) def tearDown(self): self.config = None diff --git a/pyramid/tweens.py b/pyramid/tweens.py index a842b1133..740b6961c 100644 --- a/pyramid/tweens.py +++ b/pyramid/tweens.py @@ -1,66 +1,44 @@ import sys from pyramid.compat import reraise -from pyramid.exceptions import PredicateMismatch -from pyramid.interfaces import ( - IExceptionViewClassifier, - IRequest, - ) +from pyramid.httpexceptions import HTTPNotFound -from zope.interface import providedBy -from pyramid.view import _call_view +def _error_handler(request, exc): + # NOTE: we do not need to delete exc_info because this function + # should never be in the call stack of the exception + exc_info = sys.exc_info() + + try: + response = request.invoke_exception_view(exc_info) + except HTTPNotFound: + # re-raise the original exception as no exception views were + # able to handle the error + reraise(*exc_info) + + return response def excview_tween_factory(handler, registry): """ A :term:`tween` factory which produces a tween that catches an exception raised by downstream tweens (or the main Pyramid request handler) and, if possible, converts it into a Response using an - :term:`exception view`.""" + :term:`exception view`. + + .. versionchanged:: 1.9 + The ``request.response`` will be remain unchanged even if the tween + handles an exception. Previously it was deleted after handling an + exception. + + Also, ``request.exception`` and ``request.exc_info`` are only set if + the tween handles an exception and returns a response otherwise they + are left at their original values. + + """ def excview_tween(request): - attrs = request.__dict__ try: response = handler(request) except Exception as exc: - # WARNING: do not assign the result of sys.exc_info() to a local - # var here, doing so will cause a leak. We used to actually - # explicitly delete both "exception" and "exc_info" from ``attrs`` - # in a ``finally:`` clause below, but now we do not because these - # attributes are useful to upstream tweens. This actually still - # apparently causes a reference cycle, but it is broken - # successfully by the garbage collector (see - # https://github.com/Pylons/pyramid/issues/1223). - attrs['exc_info'] = sys.exc_info() - attrs['exception'] = exc - # clear old generated request.response, if any; it may - # have been mutated by the view, and its state is not - # sane (e.g. caching headers) - if 'response' in attrs: - del attrs['response'] - # we use .get instead of .__getitem__ below due to - # https://github.com/Pylons/pyramid/issues/700 - request_iface = attrs.get('request_iface', IRequest) - provides = providedBy(exc) - try: - response = _call_view( - registry, - request, - exc, - provides, - '', - view_classifier=IExceptionViewClassifier, - request_iface=request_iface.combined - ) - - # if views matched but did not pass predicates, squash the error - # and re-raise the original exception - except PredicateMismatch: - response = None - - # re-raise the original exception as no exception views were - # able to handle the error - if response is None: - reraise(*attrs['exc_info']) - + response = _error_handler(request, exc) return response return excview_tween diff --git a/pyramid/view.py b/pyramid/view.py index 498bdde45..0c1b8cd97 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -657,8 +657,19 @@ class ViewMethodsMixin(object): This method returns a :term:`response` object or raises :class:`pyramid.httpexceptions.HTTPNotFound` if a matching view cannot - be found.""" + be found. + If a response is generated then ``request.exception`` and + ``request.exc_info`` will be left at the values used to render the + response. Otherwise the previous values for ``request.exception`` and + ``request.exc_info`` will be restored. + + .. versionchanged:: 1.9 + The ``request.exception`` and ``request.exc_info`` properties will + reflect the exception used to render the response where previously + they were reset to the values prior to invoking the method. + + """ if request is None: request = self registry = getattr(request, 'registry', None) @@ -673,7 +684,7 @@ class ViewMethodsMixin(object): # clear old generated request.response, if any; it may # have been mutated by the view, and its state is not # sane (e.g. caching headers) - with hide_attrs(request, 'exception', 'exc_info', 'response'): + with hide_attrs(request, 'response', 'exc_info', 'exception'): attrs['exception'] = exc attrs['exc_info'] = exc_info # we use .get instead of .__getitem__ below due to @@ -690,6 +701,11 @@ class ViewMethodsMixin(object): secure=secure, request_iface=request_iface.combined, ) - if response is None: - raise HTTPNotFound - return response + + if response is None: + raise HTTPNotFound + + # successful response, overwrite exception/exc_info + attrs['exception'] = exc + attrs['exc_info'] = exc_info + return response diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py index 4eb0ce704..d2869b162 100644 --- a/pyramid/viewderivers.py +++ b/pyramid/viewderivers.py @@ -6,7 +6,7 @@ from zope.interface import ( ) from pyramid.security import NO_PERMISSION_REQUIRED -from pyramid.session import ( +from pyramid.csrf import ( check_csrf_origin, check_csrf_token, ) @@ -13,6 +13,9 @@ docs = develop easy_install pyramid[docs] [bdist_wheel] universal = 1 +[metadata] +license_file = LICENSE.txt + [flake8] ignore = # E121: continuation line under-indented for hanging indent @@ -34,6 +34,8 @@ install_requires = [ 'venusian >= 1.0a3', # ``ignore`` 'translationstring >= 0.4', # py3 compat 'PasteDeploy >= 1.5.0', # py3 compat + 'plaster', + 'plaster_pastedeploy', 'hupper', ] @@ -59,7 +61,7 @@ testing_extras = tests_require + [ ] setup(name='pyramid', - version='1.9.dev0', + version='1.9a2', description='The Pyramid Web Framework, a Pylons project', long_description=README + '\n\n' + CHANGES, classifiers=[ |
