diff options
46 files changed, 855 insertions, 511 deletions
diff --git a/.travis.yml b/.travis.yml index 9d4324ff8..bc82c8faf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: - 2.7 - pypy - 3.2 + - 3.3 script: python setup.py test -q diff --git a/CHANGES.txt b/CHANGES.txt index bf1c1ea01..d6f5ea792 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,25 @@ Unreleased Features -------- +- An authorization API has been added as a method of the + request: ``request.has_permission``. + + ``request.has_permission`` is a method-based alternative to the + ``pyramid.security.has_permission`` API and works exactly the same. The + older API is now deprecated. + +- Property API attributes have been added to the request for easier access to + authentication data: ``request.authenticated_userid``, + ``request.unauthenticated_userid``, and ``request.effective_principals``. + + These are analogues, respectively, of + ``pyramid.security.authenticated_userid``, + ``pyramid.security.unauthenticated_userid``, and + ``pyramid.security.effective_principals``. They operate exactly the same, + except they are attributes of the request instead of functions accepting a + request. They are properties, so they cannot be assigned to. The older + function-based APIs are now deprecated. + - Pyramid's console scripts (``pserve``, ``pviews``, etc) can now be run directly, allowing custom arguments to be sent to the python interpreter at runtime. For example:: @@ -108,6 +127,21 @@ Deprecations the SignedCookieSessionFactory are not. See https://github.com/Pylons/pyramid/pull/1142 +- The ``pyramid.security.has_permission`` API is now deprecated. Instead, use + the newly-added ``has_permission`` method of the request object. + +- The ``pyramid.security.effective_principals`` API is now deprecated. + Instead, use the newly-added ``effective_principals`` attribute of the + request object. + +- The ``pyramid.security.authenticated_userid`` API is now deprecated. + Instead, use the newly-added ``authenticated_userid`` attribute of the + request object. + +- The ``pyramid.security.unauthenticated_userid`` API is now deprecated. + Instead, use the newly-added ``unauthenticated_userid`` attribute of the + request object. + 1.5a2 (2013-09-22) ================== diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index bfe22e540..63528e662 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -224,3 +224,7 @@ Contributors - Doug Hellmann, 2013/09/06 - Karl O. Pinc, 2013/09/27 + +- Matthew Russell, 2013/10/14 + +- Antti Haapala, 2013/11/15 diff --git a/HACKING.txt b/HACKING.txt index b32a8a957..12f2d68e2 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -6,9 +6,9 @@ Here are some guidelines about hacking on Pyramid. Using a Development Checkout ---------------------------- -You'll have to create a development environment to hack on Pyramid, using a -Pyramid checkout. You can either do this by hand or, if you have ``tox`` -installed (it's on PyPI), you can (ab)use tox to get a working development +You'll have to create a development environment to hack on Pyramid, using a +Pyramid checkout. You can either do this by hand or, if you have ``tox`` +installed (it's on PyPI), you can (ab)use tox to get a working development environment. Each installation method is described below. By Hand @@ -25,15 +25,15 @@ By Hand $ cd ~/hack-on-pyramid $ virtualenv -ppython2.7 env - Note that very old versions of virtualenv (virtualenv versions below, say, + Note that very old versions of virtualenv (virtualenv versions below, say, 1.10 or thereabouts) require you to pass a ``--no-site-packages`` flag to get a completely isolated environment. - You can choose which Python version you want to use by passing a ``-p`` + You can choose which Python version you want to use by passing a ``-p`` flag to ``virtualenv``. For example, ``virtualenv -ppython2.7`` chooses the Python 2.7 interpreter to be installed. - From here on in within these instructions, the ``~/hack-on-pyramid/env`` + From here on in within these instructions, the ``~/hack-on-pyramid/env`` virtual environment you created above will be referred to as ``$VENV``. To use the instructions in the steps that follow literally, use the ``export VENV=~/hack-on-pyramid/env`` command. @@ -132,7 +132,7 @@ Coding Style - PEP8 compliance. Whitespace rules are relaxed: not necessary to put 2 newlines between classes. But 80-column lines, in particular, are - mandatory. See + mandatory. See http://docs.pylonsproject.org/en/latest/community/codestyle.html for more information. @@ -142,14 +142,14 @@ Coding Style Running Tests -------------- -- To run all tests for Pyramid on a single Python version, run ``nosetests`` +- To run all tests for Pyramid on a single Python version, run ``nosetests`` from your development virtualenv (See *Using a Development Checkout* above). - To run individual tests (i.e. during development) you can use a regular expression with the ``-t`` parameter courtesy of the `nose-selecttests - <https://pypi.python.org/pypi/nose-selecttests/>`_ plugin that's been - installed (along with nose itself) via ``python setup.py dev``. The - easiest usage is to simply provide the verbatim name of the test you're + <https://pypi.python.org/pypi/nose-selecttests/>`_ plugin that's been + installed (along with nose itself) via ``python setup.py dev``. The + easiest usage is to simply provide the verbatim name of the test you're working on. - To run the full set of Pyramid tests on all platforms, install ``tox`` @@ -191,8 +191,8 @@ or adds the feature. To build and review docs (where ``$VENV`` refers to the virtualenv you're using to develop Pyramid): -1. After following the steps above in "Using a Development Checkout", cause - Sphinx and all development requirements to be installed in your +1. After following the steps above in "Using a Development Checkout", cause + Sphinx and all development requirements to be installed in your virtualenv:: $ cd ~/hack-on-pyramid @@ -212,9 +212,9 @@ using to develop Pyramid): $ cd ~/hack-on-pyramid/pyramid/docs $ make clean html SPHINXBUILD=$VENV/bin/sphinx-build - The ``SPHINXBUILD=...`` hair is there in order to tell it to use the - virtualenv Python, which will have both Sphinx and Pyramid (for API - documentation generation) installed. + The ``SPHINXBUILD=...`` argument tells Sphinx to use the virtualenv Python, + which will have both Sphinx and Pyramid (for API documentation generation) + installed. 4. Open the ``docs/_build/html/index.html`` file to see the resulting HTML rendering. diff --git a/docs/api/request.rst b/docs/api/request.rst index 72abddb68..b7604020e 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -11,7 +11,9 @@ :exclude-members: add_response_callback, add_finished_callback, route_url, route_path, current_route_url, current_route_path, static_url, static_path, - model_url, resource_url, set_property + model_url, resource_url, set_property, + effective_principals, authenticated_userid, + unauthenticated_userid, has_permission .. attribute:: context @@ -161,6 +163,42 @@ request, the value of this attribute will be ``None``. See :ref:`matched_route`. + .. attribute:: authenticated_userid + + .. versionadded:: 1.5 + + A property which returns the userid of the currently authenticated user + or ``None`` if there is no :term:`authentication policy` in effect or + there is no currently authenticated user. This differs from + :attr:`~pyramid.request.Request.unauthenticated_userid`, because the + effective authentication policy will have ensured that a record + associated with the userid exists in persistent storage; if it has + not, this value will be ``None``. + + .. attribute:: unauthenticated_userid + + .. versionadded:: 1.5 + + A property which returns a value which represents the *claimed* (not + verified) user id of the credentials present in the request. ``None`` if + there is no :term:`authentication policy` in effect or there is no user + data associated with the current request. This differs from + :attr:`~pyramid.request.Request.authenticated_userid`, because the + effective authentication policy will not ensure that a record associated + with the userid exists in persistent storage. Even if the userid + does not exist in persistent storage, this value will be the value + of the userid *claimed* by the request data. + + .. attribute:: effective_principals + + .. versionadded:: 1.5 + + A property which returns the list of 'effective' :term:`principal` + identifiers for this request. This will include the userid of the + currently authenticated user if a user is currently authenticated. If no + :term:`authentication policy` is in effect, this will return a sequence + containing only the :attr:`pyramid.security.Everyone` principal. + .. method:: invoke_subrequest(request, use_tweens=False) .. versionadded:: 1.4a1 @@ -215,6 +253,8 @@ request provided by e.g. the ``pshell`` environment. For more information, see :ref:`subrequest_chapter`. + .. automethod:: has_permission + .. automethod:: add_response_callback .. automethod:: add_finished_callback diff --git a/docs/designdefense.rst b/docs/designdefense.rst index bbce3e29c..2f3c14881 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1078,7 +1078,7 @@ The contents of ``app2.py``: The contents of ``config.py``: .. code-block:: python - :linenos: + :linenos: L = [] diff --git a/docs/narr/events.rst b/docs/narr/events.rst index 2accb3dbe..50484761d 100644 --- a/docs/narr/events.rst +++ b/docs/narr/events.rst @@ -172,7 +172,7 @@ track of the information that subscribers will need. Here are some example custom event classes: .. code-block:: python - :linenos: + :linenos: class DocCreated(object): def __init__(self, doc, request): @@ -196,7 +196,7 @@ also use custom events with :ref:`subscriber predicates event with a decorator: .. code-block:: python - :linenos: + :linenos: from pyramid.events import subscriber from .events import DocCreated @@ -215,7 +215,7 @@ To fire your custom events use the accessed as ``request.registry.notify``. For example: .. code-block:: python - :linenos: + :linenos: from .events import DocCreated diff --git a/docs/narr/extending.rst b/docs/narr/extending.rst index a60a49fea..8462a9da7 100644 --- a/docs/narr/extending.rst +++ b/docs/narr/extending.rst @@ -234,7 +234,7 @@ For example, if the original application has the following ``configure_views`` configuration method: .. code-block:: python - :linenos: + :linenos: def configure_views(config): config.add_view('theoriginalapp.views.theview', name='theview') diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 0c450fad7..f2542f1d7 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -363,7 +363,7 @@ and modify the set of :term:`renderer globals` before they are passed to a that can be used for this purpose. For example: .. code-block:: python - :linenos: + :linenos: from pyramid.events import subscriber from pyramid.events import BeforeRender @@ -963,8 +963,8 @@ For full details, please read the `Venusian documentation .. _registering_tweens: -Registering "Tweens" --------------------- +Registering Tweens +------------------ .. versionadded:: 1.2 Tweens @@ -976,26 +976,80 @@ feature that may be used by Pyramid framework extensions, to provide, for example, Pyramid-specific view timing support bookkeeping code that examines exceptions before they are returned to the upstream WSGI application. Tweens behave a bit like :term:`WSGI` :term:`middleware` but they have the benefit of -running in a context in which they have access to the Pyramid -:term:`application registry` as well as the Pyramid rendering machinery. +running in a context in which they have access to the Pyramid :term:`request`, +:term:`response` and :term:`application registry` as well as the Pyramid +rendering machinery. -Creating a Tween Factory -~~~~~~~~~~~~~~~~~~~~~~~~ +Creating a Tween +~~~~~~~~~~~~~~~~ -To make use of tweens, you must construct a "tween factory". A tween factory +To create a tween, you must write a "tween factory". A tween factory must be a globally importable callable which accepts two arguments: ``handler`` and ``registry``. ``handler`` will be the either the main Pyramid request handling function or another tween. ``registry`` will be the Pyramid :term:`application registry` represented by this Configurator. A -tween factory must return a tween when it is called. +tween factory must return the tween (a callable object) when it is called. -A tween is a callable which accepts a :term:`request` object and returns -a :term:`response` object. +A tween is called with a single argument, ``request``, which is the +:term:`request` created by Pyramid's router when it receives a WSGI request. +A tween should return a :term:`response`, usually the one generated by the +downstream Pyramid application. -Here's an example of a tween factory: +You can write the tween factory as a simple closure-returning function: .. code-block:: python - :linenos: + :linenos: + + def simple_tween_factory(handler, registry): + # one-time configuration code goes here + + def simple_tween(request): + # code to be executed for each request before + # the actual application code goes here + + response = handler(request) + + # code to be executed for each request after + # the actual application code goes here + + return response + + return simple_tween + +Alternatively, the tween factory can be a class with the ``__call__`` magic +method: + +.. code-block:: python + :linenos: + + class simple_tween_factory(object): + def __init__(handler, registry): + self.handler = handler + self.registry = registry + + # one-time configuration code goes here + + def __call__(self, request): + # code to be executed for each request before + # the actual application code goes here + + response = self.handler(request) + + # code to be executed for each request after + # the actual application code goes here + + return response + +The closure style performs slightly better and enables you to conditionally +omit the tween from the request processing pipeline (see the following timing +tween example), whereas the class style makes it easier to have shared mutable +state, and it allows subclassing. + +Here's a complete example of a tween that logs the time spent processing each +request: + +.. code-block:: python + :linenos: # in a module named myapp.tweens @@ -1022,12 +1076,6 @@ Here's an example of a tween factory: # handler return handler -If you remember, a tween is an object which accepts a :term:`request` object -and which returns a :term:`response` argument. The ``request`` argument to a -tween will be the request created by Pyramid's router when it receives a WSGI -request. The response object will be generated by the downstream Pyramid -application and it should be returned by the tween. - In the above example, the tween factory defines a ``timing_tween`` tween and returns it if ``asbool(registry.settings.get('do_timing'))`` is true. It otherwise simply returns the handler it was given. The ``registry.settings`` @@ -1053,7 +1101,7 @@ Here's an example of registering a tween factory as an "implicit" tween in a Pyramid application: .. code-block:: python - :linenos: + :linenos: from pyramid.config import Configurator config = Configurator() @@ -1087,7 +1135,7 @@ chain (the tween generated by the very last tween factory added) as its request handler function. For example: .. code-block:: python - :linenos: + :linenos: from pyramid.config import Configurator @@ -1132,8 +1180,10 @@ Allowable values for ``under`` or ``over`` (or both) are: fallbacks if the desired tween is not included, as well as compatibility with multiple other tweens. -Effectively, ``under`` means "closer to the main Pyramid application than", -``over`` means "closer to the request ingress than". +Effectively, ``over`` means "closer to the request ingress than" and +``under`` means "closer to the main Pyramid application than". +You can think of an onion with outer layers over the inner layers, +the application being under all the layers at the center. For example, the following call to :meth:`~pyramid.config.Configurator.add_tween` will attempt to place the @@ -1329,7 +1379,7 @@ route predicate factory is most often a class with a constructor method. For example: .. code-block:: python - :linenos: + :linenos: class ContentTypePredicate(object): def __init__(self, val, config): @@ -1392,7 +1442,7 @@ with a subscriber that subscribes to the :class:`pyramid.events.NewRequest` event type. .. code-block:: python - :linenos: + :linenos: class RequestPathStartsWith(object): def __init__(self, val, config): @@ -1421,7 +1471,7 @@ previously registered ``request_path_startswith`` predicate in a call to :meth:`~pyramid.config.Configurator.add_subscriber`: .. code-block:: python - :linenos: + :linenos: # define a subscriber in your code @@ -1437,7 +1487,7 @@ Here's the same subscriber/predicate/event-type combination used via :class:`~pyramid.events.subscriber`. .. code-block:: python - :linenos: + :linenos: from pyramid.events import subscriber diff --git a/docs/narr/hybrid.rst b/docs/narr/hybrid.rst index a29ccb2ac..4a3258d35 100644 --- a/docs/narr/hybrid.rst +++ b/docs/narr/hybrid.rst @@ -63,7 +63,7 @@ An application that uses only traversal will have view configuration declarations that look like this: .. code-block:: python - :linenos: + :linenos: # config is an instance of pyramid.config.Configurator diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index b62c16ff0..c9b782c08 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -309,7 +309,7 @@ In particular, add the ``Babel`` and ``lingua`` distributions to the application's ``setup.py`` file: .. code-block:: python - :linenos: + :linenos: setup(name="mypackage", # ... @@ -370,7 +370,7 @@ file of a ``pcreate`` -generated :app:`Pyramid` application has stanzas in it that look something like the following: .. code-block:: ini - :linenos: + :linenos: [compile_catalog] directory = myproject/locale @@ -398,7 +398,7 @@ that you'd like the domain of your translations to be ``mydomain`` instead, change the ``setup.cfg`` file stanzas to look like so: .. code-block:: ini - :linenos: + :linenos: [compile_catalog] directory = myproject/locale @@ -1041,7 +1041,7 @@ if no locale can be determined. Here's an implementation of a simple locale negotiator: .. code-block:: python - :linenos: + :linenos: def my_locale_negotiator(request): locale_name = request.params.get('my_locale') diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index a9c5fdfbd..8acbab3a0 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -336,7 +336,7 @@ For example, instead of returning a ``Response`` object from a ``render_to_response`` call: .. code-block:: python - :linenos: + :linenos: from pyramid.renderers import render_to_response @@ -347,7 +347,7 @@ For example, instead of returning a ``Response`` object from a You can return a Python dictionary: .. code-block:: python - :linenos: + :linenos: from pyramid.view import view_config @@ -827,7 +827,7 @@ Here's an example of using Pyramid's introspector from within a view callable: .. code-block:: python - :linenos: + :linenos: from pyramid.view import view_config from pyramid.response import Response diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index 3c0a6744f..a7bde4cf7 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -24,7 +24,7 @@ Here's an example of using Pyramid's introspector from within a view callable: .. code-block:: python - :linenos: + :linenos: from pyramid.view import view_config from pyramid.response import Response @@ -100,7 +100,7 @@ its ``__getitem__``, ``get``, ``keys``, ``values``, or ``items`` methods. For example: .. code-block:: python - :linenos: + :linenos: route_intr = introspector.get('routes', 'edit_user') pattern = route_intr['pattern'] diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst index b3bfb8a1e..75428d513 100644 --- a/docs/narr/logging.rst +++ b/docs/narr/logging.rst @@ -179,7 +179,7 @@ file, simply create a logger object using the ``__name__`` builtin and call methods on it. .. code-block:: python - :linenos: + :linenos: import logging log = logging.getLogger(__name__) diff --git a/docs/narr/paste.rst b/docs/narr/paste.rst index 3427b6d53..f1fb70869 100644 --- a/docs/narr/paste.rst +++ b/docs/narr/paste.rst @@ -87,7 +87,7 @@ configuration object and *returns* an instance of our application. .. _defaults_section_of_pastedeploy_file: -``[DEFAULTS]`` Section of a PasteDeploy ``.ini`` File +``[DEFAULT]`` Section of a PasteDeploy ``.ini`` File ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can add a ``[DEFAULT]`` section to your PasteDeploy ``.ini`` file. Such diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index b1bb611e5..f3ff1dc4c 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -83,7 +83,7 @@ works against resource instances. Here's a sample resource tree, represented by a variable named ``root``: .. code-block:: python - :linenos: + :linenos: class Resource(dict): pass @@ -201,7 +201,7 @@ location-aware resources. These APIs include (but are not limited to) :func:`~pyramid.traversal.resource_path`, :func:`~pyramid.traversal.resource_path_tuple`, or :func:`~pyramid.traversal.traverse`, :func:`~pyramid.traversal.virtual_root`, -and (usually) :func:`~pyramid.security.has_permission` and +and (usually) :meth:`~pyramid.request.Request.has_permission` and :func:`~pyramid.security.principals_allowed_by_permission`. In general, since so much :app:`Pyramid` infrastructure depends on @@ -695,10 +695,10 @@ The APIs provided by :ref:`location_module` are used against resources. These can be used to walk down a resource tree, or conveniently locate one resource "inside" another. -Some APIs in :ref:`security_module` accept a resource object as a parameter. -For example, the :func:`~pyramid.security.has_permission` API accepts a +Some APIs on the :class:`pyramid.request.Request` accept a resource object as a parameter. +For example, the :meth:`~pyramid.request.Request.has_permission` API accepts a resource object as one of its arguments; the ACL is obtained from this -resource or one of its ancestors. Other APIs in the :mod:`pyramid.security` -module also accept :term:`context` as an argument, and a context is always a -resource. +resource or one of its ancestors. Other security related APIs on the +:class:`pyramid.request.Request` class also accept :term:`context` as an argument, +and a context is always a resource. diff --git a/docs/narr/scaffolding.rst b/docs/narr/scaffolding.rst index 534b2caf4..f924d0d62 100644 --- a/docs/narr/scaffolding.rst +++ b/docs/narr/scaffolding.rst @@ -39,9 +39,9 @@ named ``__init__.py`` with something like the following: from pyramid.scaffolds import PyramidTemplate - class CoolExtensionTemplate(PyramidTemplate): - _template_dir = 'coolextension_scaffold' - summary = 'My cool extension' + class CoolExtensionTemplate(PyramidTemplate): + _template_dir = 'coolextension_scaffold' + summary = 'My cool extension' Once this is done, within the ``scaffolds`` directory, create a template directory. Our example used a template directory named @@ -89,7 +89,7 @@ For example: [pyramid.scaffold] coolextension=coolextension.scaffolds:CoolExtensionTemplate """ - ) + ) Run your distribution's ``setup.py develop`` or ``setup.py install`` command. After that, you should be able to see your scaffolding template @@ -112,7 +112,7 @@ want to have extension scaffolds that can work across Pyramid 1.0.X, 1.1.X, defining your scaffold template: .. code-block:: python - :linenos: + :linenos: try: # pyramid 1.0.X # "pyramid.paster.paste_script..." doesn't exist past 1.0.X diff --git a/docs/narr/security.rst b/docs/narr/security.rst index e85ed823a..9e6fb6c82 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -550,7 +550,7 @@ also contain security debugging information in its body. Debugging Imperative Authorization Failures ------------------------------------------- -The :func:`pyramid.security.has_permission` API is used to check +The :meth:`pyramid.request.Request.has_permission` API is used to check security within view functions imperatively. It returns instances of objects that are effectively booleans. But these objects are not raw ``True`` or ``False`` objects, and have information attached to them @@ -563,7 +563,7 @@ one of :data:`pyramid.security.ACLAllowed`, ``msg`` attribute, which is a string indicating why the permission was denied or allowed. Introspecting this information in the debugger or via print statements when a call to -:func:`~pyramid.security.has_permission` fails is often useful. +:meth:`~pyramid.request.Request.has_permission` fails is often useful. .. index:: single: authentication policy (creating) diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index 88d6904c7..5a5bf8fad 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -214,11 +214,10 @@ function. .. code-block:: python :linenos: - from pyramid.security import has_permission from pyramid.httpexceptions import HTTPForbidden def view_fn(request): - if not has_permission('edit', request.context, request): + if request.has_permission('edit'): raise HTTPForbidden return {'greeting':'hello'} @@ -229,15 +228,16 @@ function. otherwise it would fail when run normally. Without doing anything special during a unit test, the call to -:func:`~pyramid.security.has_permission` in this view function will always -return a ``True`` value. When a :app:`Pyramid` application starts normally, -it will populate a :term:`application registry` using :term:`configuration -declaration` calls made against a :term:`Configurator`. But if this -application registry is not created and populated (e.g. by initializing the -configurator with an authorization policy), like when you invoke application -code via a unit test, :app:`Pyramid` API functions will tend to either fail -or return default results. So how do you test the branch of the code in this -view function that raises :exc:`~pyramid.httpexceptions.HTTPForbidden`? +:meth:`~pyramid.request.Request.has_permission` in this view function will +always return a ``True`` value. When a :app:`Pyramid` application starts +normally, it will populate a :term:`application registry` using +:term:`configuration declaration` calls made against a :term:`Configurator`. +But if this application registry is not created and populated (e.g. by +initializing the configurator with an authorization policy), like when you +invoke application code via a unit test, :app:`Pyramid` API functions will tend +to either fail or return default results. So how do you test the branch of the +code in this view function that raises +:exc:`~pyramid.httpexceptions.HTTPForbidden`? The testing API provided by :app:`Pyramid` allows you to simulate various application registry registrations for use under a unit testing framework @@ -287,12 +287,12 @@ Its third line registers a "dummy" "non-permissive" authorization policy using the :meth:`~pyramid.config.Configurator.testing_securitypolicy` method, which is a special helper method for unit testing. -We then create a :class:`pyramid.testing.DummyRequest` object which simulates -a WebOb request object API. A :class:`pyramid.testing.DummyRequest` is a -request object that requires less setup than a "real" :app:`Pyramid` request. -We call the function being tested with the manufactured request. When the -function is called, :func:`pyramid.security.has_permission` will call the -"dummy" authentication policy we've registered through +We then create a :class:`pyramid.testing.DummyRequest` object which simulates a +WebOb request object API. A :class:`pyramid.testing.DummyRequest` is a request +object that requires less setup than a "real" :app:`Pyramid` request. We call +the function being tested with the manufactured request. When the function is +called, :meth:`pyramid.request.Request.has_permission` will call the "dummy" +authentication policy we've registered through :meth:`~pyramid.config.Configurator.testing_securitypolicy`, which denies access. We check that the view function raises a :exc:`~pyramid.httpexceptions.HTTPForbidden` error. diff --git a/docs/narr/threadlocals.rst b/docs/narr/threadlocals.rst index a90ee4905..afe56de3e 100644 --- a/docs/narr/threadlocals.rst +++ b/docs/narr/threadlocals.rst @@ -29,17 +29,16 @@ of a thread local or a global is usually just a way to avoid passing some value around between functions, which is itself usually a very bad idea, at least if code readability counts as an important concern. -For historical reasons, however, thread local variables are indeed -consulted by various :app:`Pyramid` API functions. For example, -the implementation of the :mod:`pyramid.security` function named -:func:`~pyramid.security.authenticated_userid` retrieves the thread -local :term:`application registry` as a matter of course to find an +For historical reasons, however, thread local variables are indeed consulted by +various :app:`Pyramid` API functions. For example, the implementation of the +:mod:`pyramid.security` function named +:func:`~pyramid.security.authenticated_userid` (deprecated as of 1.5) retrieves +the thread local :term:`application registry` as a matter of course to find an :term:`authentication policy`. It uses the -:func:`pyramid.threadlocal.get_current_registry` function to -retrieve the application registry, from which it looks up the -authentication policy; it then uses the authentication policy to -retrieve the authenticated user id. This is how :app:`Pyramid` -allows arbitrary authentication policies to be "plugged in". +:func:`pyramid.threadlocal.get_current_registry` function to retrieve the +application registry, from which it looks up the authentication policy; it then +uses the authentication policy to retrieve the authenticated user id. This is +how :app:`Pyramid` allows arbitrary authentication policies to be "plugged in". When they need to do so, :app:`Pyramid` internals use two API functions to retrieve the :term:`request` and :term:`application diff --git a/docs/narr/upgrading.rst b/docs/narr/upgrading.rst index 64343ca3e..eb3194a65 100644 --- a/docs/narr/upgrading.rst +++ b/docs/narr/upgrading.rst @@ -137,7 +137,7 @@ In the above case, it's line #3 in the ``myproj.views`` module (``from pyramid.view import static``) that is causing the problem: .. code-block:: python - :linenos: + :linenos: from pyramid.view import view_config @@ -148,7 +148,7 @@ The deprecation warning tells me how to fix it, so I can change the code to do things the newer way: .. code-block:: python - :linenos: + :linenos: from pyramid.view import view_config diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 61849c3c0..96ee5758e 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -492,7 +492,7 @@ The simplest route declaration which configures a route match to *directly* result in a particular view callable being invoked: .. code-block:: python - :linenos: + :linenos: config.add_route('idea', 'site/{id}') config.add_view('mypackage.views.site_view', route_name='idea') @@ -901,7 +901,7 @@ Details of the route matching decision for a particular request to the which you started the application from. For example: .. code-block:: text - :linenos: + :linenos: $ PYRAMID_DEBUG_ROUTEMATCH=true $VENV/bin/pserve development.ini Starting server in PID 13586. @@ -1060,7 +1060,7 @@ A custom route predicate may also *modify* the ``match`` dictionary. For instance, a predicate might do some type conversion of values: .. code-block:: python - :linenos: + :linenos: def integers(*segment_names): def predicate(info, request): @@ -1086,7 +1086,7 @@ To avoid the try/except uncertainty, the route pattern can contain regular expressions specifying requirements for that marker. For instance: .. code-block:: python - :linenos: + :linenos: def integers(*segment_names): def predicate(info, request): @@ -1128,7 +1128,7 @@ name. The ``pattern`` attribute is the route pattern. An example of using the route in a set of route predicates: .. code-block:: python - :linenos: + :linenos: def twenty_ten(info, request): if info['route'].name in ('ymd', 'ym', 'y'): diff --git a/docs/narr/vhosting.rst b/docs/narr/vhosting.rst index d37518052..53f6888b3 100644 --- a/docs/narr/vhosting.rst +++ b/docs/narr/vhosting.rst @@ -109,7 +109,7 @@ An example of an Apache ``mod_proxy`` configuration that will host the is below: .. code-block:: apache - :linenos: + :linenos: NameVirtualHost *:80 @@ -130,7 +130,7 @@ For a :app:`Pyramid` application running under :term:`mod_wsgi`, the same can be achieved using ``SetEnv``: .. code-block:: apache - :linenos: + :linenos: <Location /> SetEnv HTTP_X_VHM_ROOT /cms diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index 7c76116f7..e5a2c1ade 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -435,7 +435,7 @@ configured view. If specified, this value should be a :term:`principal` identifier or a sequence of principal identifiers. If the - :func:`pyramid.security.effective_principals` method indicates that every + :meth:`pyramid.request.Request.effective_principals` method indicates that every principal named in the argument list is present in the current request, this predicate will return True; otherwise it will return False. For example: ``effective_principals=pyramid.security.Authenticated`` or diff --git a/docs/narr/views.rst b/docs/narr/views.rst index b2dd549ce..a746eb043 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -536,7 +536,7 @@ The following types work as view callables in this style: e.g.: .. code-block:: python - :linenos: + :linenos: from pyramid.response import Response diff --git a/docs/quick_tutorial/authentication/tutorial/views.py b/docs/quick_tutorial/authentication/tutorial/views.py index 3038b6d9b..ab46eb2dd 100644 --- a/docs/quick_tutorial/authentication/tutorial/views.py +++ b/docs/quick_tutorial/authentication/tutorial/views.py @@ -2,8 +2,8 @@ from pyramid.httpexceptions import HTTPFound from pyramid.security import ( remember, forget, - authenticated_userid ) + from pyramid.view import ( view_config, view_defaults @@ -16,7 +16,7 @@ from .security import USERS class TutorialViews: def __init__(self, request): self.request = request - self.logged_in = authenticated_userid(request) + self.logged_in = request.authenticated_userid @view_config(route_name='home') def home(self): diff --git a/docs/quick_tutorial/authorization/tutorial/views.py b/docs/quick_tutorial/authorization/tutorial/views.py index 92c1946ba..43d14455a 100644 --- a/docs/quick_tutorial/authorization/tutorial/views.py +++ b/docs/quick_tutorial/authorization/tutorial/views.py @@ -2,8 +2,8 @@ from pyramid.httpexceptions import HTTPFound from pyramid.security import ( remember, forget, - authenticated_userid ) + from pyramid.view import ( view_config, view_defaults, @@ -17,7 +17,7 @@ from .security import USERS class TutorialViews: def __init__(self, request): self.request = request - self.logged_in = authenticated_userid(request) + self.logged_in = request.authenticated_userid @view_config(route_name='home') def home(self): diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 460a852e0..93cd0c18e 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -56,10 +56,10 @@ returns one of these values: return ``None``. For example, ``groupfinder('editor', request )`` returns ``['group:editor']``, -``groupfinder('viewer', request)`` returns ``[]``, and ``groupfinder('admin', request)`` -returns ``None``. We will use ``groupfinder()`` as an :term:`authentication policy` -"callback" that will provide the :term:`principal` or principals -for a user. +``groupfinder('viewer', request)`` returns ``[]``, and ``groupfinder('admin', +request)`` returns ``None``. We will use ``groupfinder()`` as an +:term:`authentication policy` "callback" that will provide the +:term:`principal` or principals for a user. In a production system, user and group data will most often come from a database, but here we use "dummy" @@ -149,8 +149,8 @@ to the ``@view_config`` decorator for ``add_page()`` and ``edit_page()``, for example: .. code-block:: python - :linenos: - :emphasize-lines: 3 + :linenos: + :emphasize-lines: 3 @view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt', @@ -251,18 +251,6 @@ in ``views.py``. Return a logged_in flag to the renderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add the following line to the import at the head of -``tutorial/tutorial/views.py``: - -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 11-15 - :linenos: - :emphasize-lines: 4 - :language: python - -(Only the highlighted line and a trailing comma on the preceding -line need to be added.) - Add a ``logged_in`` parameter to the return value of ``view_page()``, ``edit_page()`` and ``add_page()``, like this: @@ -274,14 +262,13 @@ like this: return dict(page = page, content = content, edit_url = edit_url, - logged_in = authenticated_userid(request)) + logged_in = request.authenticated_userid) (Only the highlighted line and a trailing comma on the preceding line need to be added.) -:meth:`~pyramid.security.authenticated_userid()` will return ``None`` -if the user is not authenticated, or a user id if the user is -authenticated. +The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if +the user is not authenticated, or a user id if the user is authenticated. Add a "Logout" link when logged in ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index 77956b1e3..62e96e0e7 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -11,9 +11,9 @@ from pyramid.view import ( from pyramid.security import ( remember, forget, - authenticated_userid, ) + from .security import USERS from .models import Page @@ -45,7 +45,7 @@ def view_page(context, request): edit_url = request.resource_url(context, 'edit_page') return dict(page = context, content = content, edit_url = edit_url, - logged_in = authenticated_userid(request)) + logged_in = request.authenticated_userid) @view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt', @@ -65,7 +65,7 @@ def add_page(context, request): page.__parent__ = context return dict(page=page, save_url=save_url, - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(name='edit_page', context='.models.Page', renderer='templates/edit.pt', @@ -77,7 +77,7 @@ def edit_page(context, request): return dict(page=context, save_url=request.resource_url(context, 'edit_page'), - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(context='.models.Wiki', name='login', renderer='templates/login.pt') diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py index 77956b1e3..62e96e0e7 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki/src/tests/tutorial/views.py @@ -11,9 +11,9 @@ from pyramid.view import ( from pyramid.security import ( remember, forget, - authenticated_userid, ) + from .security import USERS from .models import Page @@ -45,7 +45,7 @@ def view_page(context, request): edit_url = request.resource_url(context, 'edit_page') return dict(page = context, content = content, edit_url = edit_url, - logged_in = authenticated_userid(request)) + logged_in = request.authenticated_userid) @view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt', @@ -65,7 +65,7 @@ def add_page(context, request): page.__parent__ = context return dict(page=page, save_url=save_url, - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(name='edit_page', context='.models.Page', renderer='templates/edit.pt', @@ -77,7 +77,7 @@ def edit_page(context, request): return dict(page=context, save_url=request.resource_url(context, 'edit_page'), - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(context='.models.Wiki', name='login', renderer='templates/login.pt') diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index cf20db6d7..1e5d0dcbf 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -221,7 +221,7 @@ Add the following import statements to the head of ``tutorial/tutorial/views.py``: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 9-16,18,24-25 + :lines: 9-19 :linenos: :emphasize-lines: 3,6-9,11 :language: python @@ -274,17 +274,6 @@ added to ``views.py``. Return a logged_in flag to the renderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add the following line to the import at the head of -``tutorial/tutorial/views.py``: - -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 14-18 - :linenos: - :emphasize-lines: 4 - :language: python - -(Only the highlighted line needs to be added.) - Add a ``logged_in`` parameter to the return value of ``view_page()``, ``edit_page()`` and ``add_page()``, like this: @@ -296,12 +285,12 @@ like this: return dict(page = page, content = content, edit_url = edit_url, - logged_in = authenticated_userid(request)) + logged_in = request.authenticated_userid) (Only the highlighted line needs to be added.) -The :meth:`~pyramid.security.authenticated_userid` method will return None -if the user is not authenticated. +The :meth:`~pyramid.request.Request.authenticated_userid` property will be +``None`` if the user is not authenticated. Add a "Logout" link when logged in ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -349,7 +338,7 @@ when we're done: .. literalinclude:: src/authorization/tutorial/views.py :linenos: - :emphasize-lines: 11,14-18,25,31,37,58,61,73,76,88,91-117,119-123 + :emphasize-lines: 11,14-19,25,31,37,58,61,73,76,88,91-117,119-123 :language: python (Only the highlighted lines need to be added.) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py index b6dbbf5f6..e954d5a31 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -14,15 +14,15 @@ from pyramid.view import ( from pyramid.security import ( remember, forget, - authenticated_userid, ) +from .security import USERS + from .models import ( DBSession, Page, ) -from .security import USERS # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @@ -55,7 +55,7 @@ def view_page(request): content = wikiwords.sub(check, content) edit_url = request.route_url('edit_page', pagename=pagename) return dict(page=page, content=content, edit_url=edit_url, - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(route_name='add_page', renderer='templates/edit.pt', permission='edit') @@ -70,7 +70,7 @@ def add_page(request): save_url = request.route_url('add_page', pagename=pagename) page = Page(name='', data='') return dict(page=page, save_url=save_url, - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(route_name='edit_page', renderer='templates/edit.pt', permission='edit') @@ -84,8 +84,8 @@ def edit_page(request): pagename=pagename)) return dict( page=page, - save_url = request.route_url('edit_page', pagename=pagename), - logged_in=authenticated_userid(request), + save_url=request.route_url('edit_page', pagename=pagename), + logged_in=request.authenticated_userid ) @view_config(route_name='login', renderer='templates/login.pt') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests.py b/docs/tutorials/wiki2/src/tests/tutorial/tests.py index 4ee30685e..c50e05b6d 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests.py @@ -26,27 +26,6 @@ def _registerRoutes(config): config.add_route('add_page', 'add_page/{pagename}') -class PageModelTests(unittest.TestCase): - - def setUp(self): - self.session = _initTestingDB() - - def tearDown(self): - self.session.remove() - - def _getTargetClass(self): - from tutorial.models import Page - return Page - - def _makeOne(self, name='SomeName', data='some data'): - return self._getTargetClass()(name, data) - - def test_constructor(self): - instance = self._makeOne() - self.assertEqual(instance.name, 'SomeName') - self.assertEqual(instance.data, 'some data') - - class ViewWikiTests(unittest.TestCase): def setUp(self): self.config = testing.setUp() diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py index b6dbbf5f6..41bea4785 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views.py @@ -14,15 +14,15 @@ from pyramid.view import ( from pyramid.security import ( remember, forget, - authenticated_userid, ) +from .security import USERS + from .models import ( DBSession, Page, ) -from .security import USERS # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @@ -55,7 +55,7 @@ def view_page(request): content = wikiwords.sub(check, content) edit_url = request.route_url('edit_page', pagename=pagename) return dict(page=page, content=content, edit_url=edit_url, - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(route_name='add_page', renderer='templates/edit.pt', permission='edit') @@ -70,7 +70,7 @@ def add_page(request): save_url = request.route_url('add_page', pagename=pagename) page = Page(name='', data='') return dict(page=page, save_url=save_url, - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(route_name='edit_page', renderer='templates/edit.pt', permission='edit') @@ -84,8 +84,8 @@ def edit_page(request): pagename=pagename)) return dict( page=page, - save_url = request.route_url('edit_page', pagename=pagename), - logged_in=authenticated_userid(request), + save_url=request.route_url('edit_page', pagename=pagename), + logged_in=request.authenticated_userid ) @view_config(route_name='login', renderer='templates/login.pt') @@ -121,4 +121,3 @@ def logout(request): headers = forget(request) return HTTPFound(location = request.route_url('view_wiki'), headers = headers) - diff --git a/docs/whatsnew-1.5.rst b/docs/whatsnew-1.5.rst index 57f93cbff..23613896a 100644 --- a/docs/whatsnew-1.5.rst +++ b/docs/whatsnew-1.5.rst @@ -316,6 +316,48 @@ The feature additions in Pyramid 1.5 follow. - :func:`pyramid.path.package_name` no longer thows an exception when resolving the package name for namespace packages that have no ``__file__`` attribute. +- An authorization API has been added as a method of the request: + :meth:`pyramid.request.Request.has_permission`. It is a method-based + alternative to the :func:`pyramid.security.has_permission` API and works + exactly the same. The older API is now deprecated. + +- Property API attributes have been added to the request for easier access to + authentication data: :attr:`pyramid.request.Request.authenticated_userid`, + :attr:`pyramid.request.Request.unauthenticated_userid`, and + :attr:`pyramid.request.Request.effective_principals`. These are analogues, + respectively, of :func:`pyramid.security.authenticated_userid`, + :func:`pyramid.security.unauthenticated_userid`, and + :func:`pyramid.security.effective_principals`. They operate exactly the + same, except they are attributes of the request instead of functions + accepting a request. They are properties, so they cannot be assigned to. + The older function-based APIs are now deprecated. + +- Pyramid's console scripts (``pserve``, ``pviews``, etc) can now be run + directly, allowing custom arguments to be sent to the python interpreter + at runtime. For example:: + + python -3 -m pyramid.scripts.pserve development.ini + +- Added a specific subclass of :class:`pyramid.httpexceptions.HTTPBadRequest` + named :class:`pyramid.exceptions.BadCSRFToken` which will now be raised in + response to failures in the ``check_csrf_token`` view predicate. See + https://github.com/Pylons/pyramid/pull/1149 + +- Added a new ``SignedCookieSessionFactory`` which is very similar to the + ``UnencryptedCookieSessionFactoryConfig`` but with a clearer focus on + signing content. The custom serializer arguments to this function should + only focus on serializing, unlike its predecessor which required the + serializer to also perform signing. + See https://github.com/Pylons/pyramid/pull/1142 + +- Added a new ``BaseCookieSessionFactory`` which acts as a generic cookie + factory that can be used by framework implementors to create their own + session implementations. It provides a reusable API which focuses strictly + on providing a dictionary-like object that properly handles renewals, + timeouts, and conformance with the ``ISession`` API. + See https://github.com/Pylons/pyramid/pull/1142 + + Other Backwards Incompatibilities --------------------------------- @@ -404,6 +446,13 @@ Other Backwards Incompatibilities Pyramid narrative documentation instead of providing renderer globals values to the configurator. +- The key/values in the ``_query`` parameter of + :meth:`pyramid.request.Request.route_url` and the ``query`` parameter of + :meth:`pyramid.request.Request.resource_url` (and their variants), used to + encode a value of ``None`` as the string ``'None'``, leaving the resulting + query string to be ``a=b&key=None``. The value is now dropped in this + situation, leaving a query string of ``a=b&key=``. See + https://github.com/Pylons/pyramid/issues/1119 Deprecations ------------ @@ -417,12 +466,36 @@ Deprecations a deprecation warning when used. It had been docs-deprecated in 1.4 but did not issue a deprecation warning when used. +- :func:`pyramid.security.has_permission` is now deprecated in favor of using + :meth:`pyramid.request.Request.has_permission`. + +- The :func:`pyramid.security.authenticated_userid`, + :func:`pyramid.security.unauthenticated_userid`, and + :func:`pyramid.security.effective_principals` functions have been + deprecated. Use :attr:`pyramid.request.Request.authenticated_userid`, + :attr:`pyramid.request.Request.unauthenticated_userid` and + :attr:`pyramid.request.Request.effective_principals` instead. + +- Deprecate the ``pyramid.interfaces.ITemplateRenderer`` interface. It was + ill-defined and became unused when Mako and Chameleon template bindings were + split into their own packages. + +- The ``pyramid.session.UnencryptedCookieSessionFactoryConfig`` API has been + deprecated and is superseded by the + ``pyramid.session.SignedCookieSessionFactory``. Note that while the cookies + generated by the ``UnencryptedCookieSessionFactoryConfig`` + are compatible with cookies generated by old releases, cookies generated by + the SignedCookieSessionFactory are not. See + https://github.com/Pylons/pyramid/pull/1142 + Documentation Enhancements -------------------------- - A new documentation chapter named :ref:`quick_tour` was added. It describes starting out with Pyramid from a high level. +- Added a :ref:`quick_tutorial` to go with the Quick Tour + - Many other enhancements. diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index c8f66e83d..967f2eeee 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -13,7 +13,6 @@ from pyramid.traversal import ( from pyramid.urldispatch import _compile_route from pyramid.util import object_description from pyramid.session import check_csrf_token -from pyramid.security import effective_principals from .util import as_sorted_tuple @@ -288,7 +287,7 @@ class EffectivePrincipalsPredicate(object): phash = text def __call__(self, context, request): - req_principals = effective_principals(request) + req_principals = request.effective_principals if is_nonstr_iter(req_principals): rpset = set(req_principals) if self.val.issubset(rpset): diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 4de4663a8..4fd207600 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -237,10 +237,10 @@ class RoutesConfiguratorMixin(object): If specified, this value should be a :term:`principal` identifier or a sequence of principal identifiers. If the - :func:`pyramid.security.effective_principals` method indicates that - every principal named in the argument list is present in the current - request, this predicate will return True; otherwise it will return - False. For example: + :attr:`pyramid.request.Request.effective_principals` property + indicates that every principal named in the argument list is present + in the current request, this predicate will return True; otherwise it + will return False. For example: ``effective_principals=pyramid.security.Authenticated`` or ``effective_principals=('fred', 'group:admins')``. diff --git a/pyramid/config/testing.py b/pyramid/config/testing.py index 2ab85b1f5..5df726a31 100644 --- a/pyramid/config/testing.py +++ b/pyramid/config/testing.py @@ -47,14 +47,14 @@ class TestingConfiguratorMixin(object): ``groupids`` argument. The authentication policy will return the userid identifier implied by the ``userid`` argument and the group ids implied by the ``groupids`` argument when the - :func:`pyramid.security.authenticated_userid` or - :func:`pyramid.security.effective_principals` APIs are + :attr:`pyramid.request.Request.authenticated_userid` or + :attr:`pyramid.request.Request.effective_principals` APIs are used. This function is most useful when testing code that uses - the APIs named :func:`pyramid.security.has_permission`, - :func:`pyramid.security.authenticated_userid`, - :func:`pyramid.security.effective_principals`, and + the APIs named :meth:`pyramid.request.Request.has_permission`, + :attr:`pyramid.request.Request.authenticated_userid`, + :attr:`pyramid.request.Request.effective_principals`, and :func:`pyramid.security.principals_allowed_by_permission`. .. versionadded:: 1.4 diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 0165f96f1..72dc3f414 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1024,10 +1024,10 @@ class ViewsConfiguratorMixin(object): If specified, this value should be a :term:`principal` identifier or a sequence of principal identifiers. If the - :func:`pyramid.security.effective_principals` method indicates that - every principal named in the argument list is present in the current - request, this predicate will return True; otherwise it will return - False. For example: + :attr:`pyramid.request.Request.effective_principals` property + indicates that every principal named in the argument list is present + in the current request, this predicate will return True; otherwise it + will return False. For example: ``effective_principals=pyramid.security.Authenticated`` or ``effective_principals=('fred', 'group:admins')``. diff --git a/pyramid/i18n.py b/pyramid/i18n.py index cdedbc877..6ffd93e8f 100644 --- a/pyramid/i18n.py +++ b/pyramid/i18n.py @@ -107,7 +107,8 @@ def default_locale_negotiator(request): - First, the negotiator looks for the ``_LOCALE_`` attribute of the request object (possibly set by a view or a listener for an - :term:`event`). + :term:`event`). If the attribute exists and it is not ``None``, + its value will be used. - Then it looks for the ``request.params['_LOCALE_']`` value. diff --git a/pyramid/request.py b/pyramid/request.py index 2cf0613f7..188e968ac 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -21,6 +21,10 @@ from pyramid.compat import ( from pyramid.decorator import reify from pyramid.i18n import LocalizerRequestMixin from pyramid.response import Response +from pyramid.security import ( + AuthenticationAPIMixin, + AuthorizationAPIMixin, + ) from pyramid.url import URLMethodsMixin from pyramid.util import InstancePropertyMixin @@ -136,8 +140,15 @@ class CallbackMethodsMixin(object): callback(self) @implementer(IRequest) -class Request(BaseRequest, URLMethodsMixin, CallbackMethodsMixin, - InstancePropertyMixin, LocalizerRequestMixin): +class Request( + BaseRequest, + URLMethodsMixin, + CallbackMethodsMixin, + InstancePropertyMixin, + LocalizerRequestMixin, + AuthenticationAPIMixin, + AuthorizationAPIMixin, + ): """ A subclass of the :term:`WebOb` Request class. An instance of this class is created by the :term:`router` and is provided to a diff --git a/pyramid/security.py b/pyramid/security.py index 3e25f9b2f..58fa9332a 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -1,3 +1,4 @@ +from zope.deprecation import deprecated from zope.interface import providedBy from pyramid.interfaces import ( @@ -30,79 +31,143 @@ DENY_ALL = (Deny, Everyone, ALL_PERMISSIONS) NO_PERMISSION_REQUIRED = '__no_permission_required__' -def has_permission(permission, context, request): - """ Provided a permission (a string or unicode object), a context - (a :term:`resource` instance) and a request object, return an - instance of :data:`pyramid.security.Allowed` if the permission - is granted in this context to the user implied by the - request. Return an instance of :mod:`pyramid.security.Denied` - if this permission is not granted in this context to this user. - This function delegates to the current authentication and - authorization policies. Return - :data:`pyramid.security.Allowed` unconditionally if no - authentication policy has been configured in this application.""" +def _get_registry(request): try: reg = request.registry except AttributeError: reg = get_current_registry() # b/c - authn_policy = reg.queryUtility(IAuthenticationPolicy) - if authn_policy is None: - return Allowed('No authentication policy in use.') + return reg - authz_policy = reg.queryUtility(IAuthorizationPolicy) - if authz_policy is None: - raise ValueError('Authentication policy registered without ' - 'authorization policy') # should never happen - principals = authn_policy.effective_principals(request) - return authz_policy.permits(context, principals, permission) +def _get_authentication_policy(request): + registry = _get_registry(request) + return registry.queryUtility(IAuthenticationPolicy) -def authenticated_userid(request): - """ Return the userid of the currently authenticated user or - ``None`` if there is no :term:`authentication policy` in effect or - there is no currently authenticated user.""" - try: - reg = request.registry - except AttributeError: - reg = get_current_registry() # b/c +def has_permission(permission, context, request): + """ + A function that calls + :meth:`pyramid.request.Request.has_permission` and returns its result. + + .. deprecated:: 1.5 + Use :meth:`pyramid.request.Request.has_permission` instead. + + .. versionchanged:: 1.5a3 + If context is None, then attempt to use the context attribute + of self, if not set then the AttributeError is propergated. + """ + return request.has_permission(permission, context) + +deprecated( + 'has_permission', + 'As of Pyramid 1.5 the "pyramid.security.has_permission" API is now ' + 'deprecated. It will be removed in Pyramd 1.8. Use the ' + '"has_permission" method of the Pyramid request instead.' + ) - policy = reg.queryUtility(IAuthenticationPolicy) - if policy is None: - return None - return policy.authenticated_userid(request) + +def authenticated_userid(request): + """ + A function that returns the value of the property + :attr:`pyramid.request.Request.authenticated_userid`. + + .. deprecated:: 1.5 + Use :attr:`pyramid.request.Request.authenticated_userid` instead. + """ + return request.authenticated_userid + +deprecated( + 'authenticated_userid', + 'As of Pyramid 1.5 the "pyramid.security.authenticated_userid" API is now ' + 'deprecated. It will be removed in Pyramd 1.8. Use the ' + '"authenticated_userid" attribute of the Pyramid request instead.' + ) def unauthenticated_userid(request): - """ Return an object which represents the *claimed* (not verified) user - id of the credentials present in the request. ``None`` if there is no - :term:`authentication policy` in effect or there is no user data - associated with the current request. This differs from - :func:`~pyramid.security.authenticated_userid`, because the effective - authentication policy will not ensure that a record associated with the - userid exists in persistent storage.""" - try: - reg = request.registry - except AttributeError: - reg = get_current_registry() # b/c + """ + A function that returns the value of the property + :attr:`pyramid.request.Request.unauthenticated_userid`. + + .. deprecated:: 1.5 + Use :attr:`pyramid.request.Request.unauthenticated_userid` instead. + """ + return request.unauthenticated_userid + +deprecated( + 'unauthenticated_userid', + 'As of Pyramid 1.5 the "pyramid.security.unauthenticated_userid" API is ' + 'now deprecated. It will be removed in Pyramd 1.8. Use the ' + '"unauthenticated_userid" attribute of the Pyramid request instead.' + ) + +def effective_principals(request): + """ + A function that returns the value of the property + :attr:`pyramid.request.Request.effective_principals`. + + .. deprecated:: 1.5 + Use :attr:`pyramid.request.Request.effective_principals` instead. + """ + return request.effective_principals + +deprecated( + 'effective_principals', + 'As of Pyramid 1.5 the "pyramid.security.effective_principals" API is ' + 'now deprecated. It will be removed in Pyramd 1.8. Use the ' + '"effective_principals" attribute of the Pyramid request instead.' + ) + +def remember(request, principal, **kw): + """ + Returns a sequence of header tuples (e.g. ``[('Set-Cookie', 'foo=abc')]``) + on this request's response. + These headers are suitable for 'remembering' a set of credentials + implied by the data passed as ``principal`` and ``*kw`` using the + current :term:`authentication policy`. Common usage might look + like so within the body of a view function (``response`` is + assumed to be a :term:`WebOb` -style :term:`response` object + computed previously by the view code):: - policy = reg.queryUtility(IAuthenticationPolicy) + .. code-block:: python + + from pyramid.security import remember + headers = remember(request, 'chrism', password='123', max_age='86400') + response = request.response + response.headerlist.extend(headers) + return response + + If no :term:`authentication policy` is in use, this function will + always return an empty sequence. If used, the composition and + meaning of ``**kw`` must be agreed upon by the calling code and + the effective authentication policy. + """ + policy = _get_authentication_policy(request) if policy is None: - return None - return policy.unauthenticated_userid(request) + return [] + return policy.remember(request, principal, **kw) -def effective_principals(request): - """ Return the list of 'effective' :term:`principal` identifiers - for the ``request``. This will include the userid of the - currently authenticated user if a user is currently - authenticated. If no :term:`authentication policy` is in effect, - this will return an empty sequence.""" - try: - reg = request.registry - except AttributeError: - reg = get_current_registry() # b/c +def forget(request): + """ + Return a sequence of header tuples (e.g. ``[('Set-Cookie', + 'foo=abc')]``) suitable for 'forgetting' the set of credentials + possessed by the currently authenticated user. A common usage + might look like so within the body of a view function + (``response`` is assumed to be an :term:`WebOb` -style + :term:`response` object computed previously by the view code):: + + from pyramid.security import forget + headers = forget(request) + response.headerlist.extend(headers) + return response - policy = reg.queryUtility(IAuthenticationPolicy) + If no :term:`authentication policy` is in use, this function will + always return an empty sequence. + + .. deprecated:: 1.5 + Use :meth:`pyramid.request.Request.get_logout_headers` instead. + """ + policy = _get_authentication_policy(request) if policy is None: - return [Everyone] - return policy.effective_principals(request) + return [] + return policy.forget(request) def principals_allowed_by_permission(context, permission): """ Provided a ``context`` (a resource object), and a ``permission`` @@ -140,10 +205,7 @@ def view_execution_permitted(context, request, name=''): An exception is raised if no view is found. """ - try: - reg = request.registry - except AttributeError: - reg = get_current_registry() # b/c + reg = _get_registry(request) provides = [IViewClassifier] + map_(providedBy, (request, context)) view = reg.adapters.lookup(provides, ISecuredView, name=name) if view is None: @@ -157,58 +219,6 @@ def view_execution_permitted(context, request, name=''): (name, context)) return view.__permitted__(context, request) -def remember(request, principal, **kw): - """ Return a sequence of header tuples (e.g. ``[('Set-Cookie', - 'foo=abc')]``) suitable for 'remembering' a set of credentials - implied by the data passed as ``principal`` and ``*kw`` using the - current :term:`authentication policy`. Common usage might look - like so within the body of a view function (``response`` is - assumed to be a :term:`WebOb` -style :term:`response` object - computed previously by the view code):: - - from pyramid.security import remember - headers = remember(request, 'chrism', password='123', max_age='86400') - response.headerlist.extend(headers) - return response - - If no :term:`authentication policy` is in use, this function will - always return an empty sequence. If used, the composition and - meaning of ``**kw`` must be agreed upon by the calling code and - the effective authentication policy.""" - try: - reg = request.registry - except AttributeError: - reg = get_current_registry() # b/c - policy = reg.queryUtility(IAuthenticationPolicy) - if policy is None: - return [] - else: - return policy.remember(request, principal, **kw) - -def forget(request): - """ Return a sequence of header tuples (e.g. ``[('Set-Cookie', - 'foo=abc')]``) suitable for 'forgetting' the set of credentials - possessed by the currently authenticated user. A common usage - might look like so within the body of a view function - (``response`` is assumed to be an :term:`WebOb` -style - :term:`response` object computed previously by the view code):: - - from pyramid.security import forget - headers = forget(request) - response.headerlist.extend(headers) - return response - - If no :term:`authentication policy` is in use, this function will - always return an empty sequence.""" - try: - reg = request.registry - except AttributeError: - reg = get_current_registry() # b/c - policy = reg.queryUtility(IAuthenticationPolicy) - if policy is None: - return [] - else: - return policy.forget(request) class PermitsResult(int): def __new__(cls, s, *args): @@ -294,3 +304,89 @@ class ACLAllowed(ACLPermitsResult): summary is available as the ``msg`` attribute.""" boolval = 1 +class AuthenticationAPIMixin(object): + + def _get_authentication_policy(self): + reg = _get_registry(self) + return reg.queryUtility(IAuthenticationPolicy) + + @property + def authenticated_userid(self): + """ Return the userid of the currently authenticated user or + ``None`` if there is no :term:`authentication policy` in effect or + there is no currently authenticated user. + + .. versionadded:: 1.5 + """ + policy = self._get_authentication_policy() + if policy is None: + return None + return policy.authenticated_userid(self) + + @property + def unauthenticated_userid(self): + """ Return an object which represents the *claimed* (not verified) user + id of the credentials present in the request. ``None`` if there is no + :term:`authentication policy` in effect or there is no user data + associated with the current request. This differs from + :attr:`~pyramid.request.Request.authenticated_userid`, because the + effective authentication policy will not ensure that a record + associated with the userid exists in persistent storage. + + .. versionadded:: 1.5 + """ + policy = self._get_authentication_policy() + if policy is None: + return None + return policy.unauthenticated_userid(self) + + @property + def effective_principals(self): + """ Return the list of 'effective' :term:`principal` identifiers + for the ``request``. This will include the userid of the + currently authenticated user if a user is currently + authenticated. If no :term:`authentication policy` is in effect, + this will return an empty sequence. + + .. versionadded:: 1.5 + """ + policy = self._get_authentication_policy() + if policy is None: + return [Everyone] + return policy.effective_principals(self) + +class AuthorizationAPIMixin(object): + + def has_permission(self, permission, context=None): + """ Given a permission and an optional context, returns an instance of + :data:`pyramid.security.Allowed` if the permission is granted to this + request with the provided context, or the context already associated + with the request. Otherwise, returns an instance of + :data:`pyramid.security.Denied`. This method delegates to the current + authentication and authorization policies. Returns + :data:`pyramid.security.Allowed` unconditionally if no authentication + policy has been registered for this request. If ``context`` is not + supplied or is supplied as ``None``, the context used is the + ``request.context`` attribute. + + :param permission: Does this request have the given permission? + :type permission: unicode, str + :param context: A resource object or ``None`` + :type context: object + :returns: `pyramid.security.PermitsResult` + + .. versionadded:: 1.5 + + """ + if context is None: + context = self.context + reg = _get_registry(self) + authn_policy = reg.queryUtility(IAuthenticationPolicy) + if authn_policy is None: + return Allowed('No authentication policy in use.') + authz_policy = reg.queryUtility(IAuthorizationPolicy) + if authz_policy is None: + raise ValueError('Authentication policy registered without ' + 'authorization policy') # should never happen + principals = authn_policy.effective_principals(self) + return authz_policy.permits(context, principals, permission) diff --git a/pyramid/testing.py b/pyramid/testing.py index 4590c55f8..b3460d8aa 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -27,6 +27,8 @@ from pyramid.registry import Registry from pyramid.security import ( Authenticated, Everyone, + AuthenticationAPIMixin, + AuthorizationAPIMixin, ) from pyramid.threadlocal import ( @@ -280,10 +282,15 @@ class DummySession(dict): token = self.new_csrf_token() return token - @implementer(IRequest) -class DummyRequest(URLMethodsMixin, CallbackMethodsMixin, InstancePropertyMixin, - LocalizerRequestMixin): +class DummyRequest( + URLMethodsMixin, + CallbackMethodsMixin, + InstancePropertyMixin, + LocalizerRequestMixin, + AuthenticationAPIMixin, + AuthorizationAPIMixin, + ): """ A DummyRequest object (incompletely) imitates a :term:`request` object. The ``params``, ``environ``, ``headers``, ``path``, and diff --git a/pyramid/tests/test_config/test_testing.py b/pyramid/tests/test_config/test_testing.py index 1089f09fc..05561bfe9 100644 --- a/pyramid/tests/test_config/test_testing.py +++ b/pyramid/tests/test_config/test_testing.py @@ -1,6 +1,7 @@ import unittest from pyramid.compat import text_ +from pyramid.security import AuthenticationAPIMixin, AuthorizationAPIMixin from pyramid.tests.test_config import IDummy class TestingConfiguratorMixinTests(unittest.TestCase): @@ -196,13 +197,9 @@ from zope.interface import implementer class DummyEvent: pass -class DummyRequest: - subpath = () - matchdict = None +class DummyRequest(AuthenticationAPIMixin, AuthorizationAPIMixin): def __init__(self, environ=None): if environ is None: environ = {} self.environ = environ - self.params = {} - self.cookies = {} - + diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 6cd72fc59..ed41b62ff 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -6,9 +6,10 @@ from pyramid.compat import ( text_, bytes_, native_, - iteritems_, - iterkeys_, - itervalues_, + ) +from pyramid.security import ( + AuthenticationAPIMixin, + AuthorizationAPIMixin, ) class TestRequest(unittest.TestCase): @@ -53,6 +54,11 @@ class TestRequest(unittest.TestCase): cls = self._getTargetClass() self.assertEqual(cls.ResponseClass, Response) + def test_implements_security_apis(self): + apis = (AuthenticationAPIMixin, AuthorizationAPIMixin) + r = self._makeOne() + self.assertTrue(isinstance(r, apis)) + def test_charset_defaults_to_utf8(self): r = self._makeOne({'PATH_INFO':'/'}) self.assertEqual(r.charset, 'UTF-8') diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py index e530e33ca..6f08a100c 100644 --- a/pyramid/tests/test_security.py +++ b/pyramid/tests/test_security.py @@ -1,14 +1,13 @@ import unittest -from pyramid.testing import cleanUp - +from pyramid import testing class TestAllPermissionsList(unittest.TestCase): def setUp(self): - cleanUp() + testing.setUp() def tearDown(self): - cleanUp() + testing.tearDown() def _getTargetClass(self): from pyramid.security import AllPermissionsList @@ -103,13 +102,101 @@ class TestACLDenied(unittest.TestCase): self.assertTrue('<ACLDenied instance at ' in repr(denied)) self.assertTrue("with msg %r>" % msg in repr(denied)) -class TestViewExecutionPermitted(unittest.TestCase): +class TestPrincipalsAllowedByPermission(unittest.TestCase): def setUp(self): - cleanUp() + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, *arg): + from pyramid.security import principals_allowed_by_permission + return principals_allowed_by_permission(*arg) + + def test_no_authorization_policy(self): + from pyramid.security import Everyone + context = DummyContext() + result = self._callFUT(context, 'view') + self.assertEqual(result, [Everyone]) + + def test_with_authorization_policy(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + _registerAuthorizationPolicy(registry, 'yo') + context = DummyContext() + result = self._callFUT(context, 'view') + self.assertEqual(result, 'yo') + +class TestRemember(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, *arg): + from pyramid.security import remember + return remember(*arg) + + def test_no_authentication_policy(self): + request = _makeRequest() + result = self._callFUT(request, 'me') + self.assertEqual(result, []) + def test_with_authentication_policy(self): + request = _makeRequest() + registry = request.registry + _registerAuthenticationPolicy(registry, 'yo') + result = self._callFUT(request, 'me') + self.assertEqual(result, [('X-Pyramid-Test', 'me')]) + + def test_with_authentication_policy_no_reg_on_request(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + request = _makeRequest() + del request.registry + _registerAuthenticationPolicy(registry, 'yo') + result = self._callFUT(request, 'me') + self.assertEqual(result, [('X-Pyramid-Test', 'me')]) + +class TestForget(unittest.TestCase): + def setUp(self): + testing.setUp() + def tearDown(self): - cleanUp() + testing.tearDown() + def _callFUT(self, *arg): + from pyramid.security import forget + return forget(*arg) + + def test_no_authentication_policy(self): + request = _makeRequest() + result = self._callFUT(request) + self.assertEqual(result, []) + + def test_with_authentication_policy(self): + request = _makeRequest() + _registerAuthenticationPolicy(request.registry, 'yo') + result = self._callFUT(request) + self.assertEqual(result, [('X-Pyramid-Test', 'logout')]) + + def test_with_authentication_policy_no_reg_on_request(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + request = _makeRequest() + del request.registry + _registerAuthenticationPolicy(registry, 'yo') + result = self._callFUT(request) + self.assertEqual(result, [('X-Pyramid-Test', 'logout')]) + +class TestViewExecutionPermitted(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + def _callFUT(self, *arg, **kw): from pyramid.security import view_execution_permitted return view_execution_permitted(*arg, **kw) @@ -140,7 +227,7 @@ class TestViewExecutionPermitted(unittest.TestCase): reg = get_current_registry() reg.registerUtility(settings, ISettings) context = DummyContext() - request = DummyRequest({}) + request = testing.DummyRequest({}) class DummyView(object): pass view = DummyView() @@ -159,7 +246,7 @@ class TestViewExecutionPermitted(unittest.TestCase): reg = get_current_registry() reg.registerUtility(settings, ISettings) context = DummyContext() - request = DummyRequest({}) + request = testing.DummyRequest({}) self.assertRaises(TypeError, self._callFUT, context, request, '') def test_with_permission(self): @@ -171,232 +258,197 @@ class TestViewExecutionPermitted(unittest.TestCase): context = DummyContext() directlyProvides(context, IContext) self._registerSecuredView('', True) - request = DummyRequest({}) + request = testing.DummyRequest({}) directlyProvides(request, IRequest) result = self._callFUT(context, request, '') - self.assertTrue(result is True) - -class TestHasPermission(unittest.TestCase): - def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() - - def _callFUT(self, *arg): - from pyramid.security import has_permission - return has_permission(*arg) - - def test_no_authentication_policy(self): - request = _makeRequest() - result = self._callFUT('view', None, request) - self.assertEqual(result, True) - self.assertEqual(result.msg, 'No authentication policy in use.') - - def test_authentication_policy_no_authorization_policy(self): - request = _makeRequest() - _registerAuthenticationPolicy(request.registry, None) - self.assertRaises(ValueError, self._callFUT, 'view', None, request) - - def test_authn_and_authz_policies_registered(self): - request = _makeRequest() - _registerAuthenticationPolicy(request.registry, None) - _registerAuthorizationPolicy(request.registry, 'yo') - self.assertEqual(self._callFUT('view', None, request), 'yo') - - def test_no_registry_on_request(self): - from pyramid.threadlocal import get_current_registry - request = DummyRequest({}) - registry = get_current_registry() - _registerAuthenticationPolicy(registry, None) - _registerAuthorizationPolicy(registry, 'yo') - self.assertEqual(self._callFUT('view', None, request), 'yo') + self.assertTrue(result) class TestAuthenticatedUserId(unittest.TestCase): def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() + testing.setUp() - def _callFUT(self, request): - from pyramid.security import authenticated_userid - return authenticated_userid(request) + def tearDown(self): + testing.tearDown() + + def test_backward_compat_delegates_to_mixin(self): + from zope.deprecation import __show__ + try: + __show__.off() + request = _makeFakeRequest() + from pyramid.security import authenticated_userid + self.assertEqual( + authenticated_userid(request), + 'authenticated_userid' + ) + finally: + __show__.on() def test_no_authentication_policy(self): request = _makeRequest() - result = self._callFUT(request) - self.assertEqual(result, None) + self.assertEqual(request.authenticated_userid, None) def test_with_authentication_policy(self): request = _makeRequest() _registerAuthenticationPolicy(request.registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + self.assertEqual(request.authenticated_userid, 'yo') def test_with_authentication_policy_no_reg_on_request(self): from pyramid.threadlocal import get_current_registry - request = DummyRequest({}) registry = get_current_registry() + request = _makeRequest() + del request.registry _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + self.assertEqual(request.authenticated_userid, 'yo') -class TestUnauthenticatedUserId(unittest.TestCase): +class TestUnAuthenticatedUserId(unittest.TestCase): def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() + testing.setUp() - def _callFUT(self, request): - from pyramid.security import unauthenticated_userid - return unauthenticated_userid(request) + def tearDown(self): + testing.tearDown() + + def test_backward_compat_delegates_to_mixin(self): + from zope.deprecation import __show__ + try: + __show__.off() + request = _makeFakeRequest() + from pyramid.security import unauthenticated_userid + self.assertEqual( + unauthenticated_userid(request), + 'unauthenticated_userid', + ) + finally: + __show__.on() def test_no_authentication_policy(self): request = _makeRequest() - result = self._callFUT(request) - self.assertEqual(result, None) + self.assertEqual(request.unauthenticated_userid, None) def test_with_authentication_policy(self): request = _makeRequest() _registerAuthenticationPolicy(request.registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + self.assertEqual(request.unauthenticated_userid, 'yo') def test_with_authentication_policy_no_reg_on_request(self): from pyramid.threadlocal import get_current_registry - request = DummyRequest({}) registry = get_current_registry() + request = _makeRequest() + del request.registry _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + self.assertEqual(request.unauthenticated_userid, 'yo') class TestEffectivePrincipals(unittest.TestCase): def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() + testing.setUp() - def _callFUT(self, request): - from pyramid.security import effective_principals - return effective_principals(request) + def tearDown(self): + testing.tearDown() + + def test_backward_compat_delegates_to_mixin(self): + request = _makeFakeRequest() + from zope.deprecation import __show__ + try: + __show__.off() + from pyramid.security import effective_principals + self.assertEqual( + effective_principals(request), + 'effective_principals' + ) + finally: + __show__.on() def test_no_authentication_policy(self): from pyramid.security import Everyone request = _makeRequest() - result = self._callFUT(request) - self.assertEqual(result, [Everyone]) + self.assertEqual(request.effective_principals, [Everyone]) def test_with_authentication_policy(self): request = _makeRequest() _registerAuthenticationPolicy(request.registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + self.assertEqual(request.effective_principals, 'yo') def test_with_authentication_policy_no_reg_on_request(self): from pyramid.threadlocal import get_current_registry registry = get_current_registry() - request = DummyRequest({}) + request = _makeRequest() + del request.registry _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + self.assertEqual(request.effective_principals, 'yo') -class TestPrincipalsAllowedByPermission(unittest.TestCase): - def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() - - def _callFUT(self, *arg): - from pyramid.security import principals_allowed_by_permission - return principals_allowed_by_permission(*arg) - - def test_no_authorization_policy(self): - from pyramid.security import Everyone - context = DummyContext() - result = self._callFUT(context, 'view') - self.assertEqual(result, [Everyone]) - - def test_with_authorization_policy(self): - from pyramid.threadlocal import get_current_registry - registry = get_current_registry() - _registerAuthorizationPolicy(registry, 'yo') - context = DummyContext() - result = self._callFUT(context, 'view') - self.assertEqual(result, 'yo') - -class TestRemember(unittest.TestCase): +class TestHasPermission(unittest.TestCase): def setUp(self): - cleanUp() + testing.setUp() def tearDown(self): - cleanUp() + testing.tearDown() - def _callFUT(self, *arg): - from pyramid.security import remember - return remember(*arg) + def _makeOne(self): + from pyramid.security import AuthorizationAPIMixin + from pyramid.registry import Registry + mixin = AuthorizationAPIMixin() + mixin.registry = Registry() + mixin.context = object() + return mixin + + def test_delegates_to_mixin(self): + from zope.deprecation import __show__ + try: + __show__.off() + mixin = self._makeOne() + from pyramid.security import has_permission + self.called_has_permission = False + + def mocked_has_permission(*args, **kw): + self.called_has_permission = True + + mixin.has_permission = mocked_has_permission + has_permission('view', object(), mixin) + self.assertTrue(self.called_has_permission) + finally: + __show__.on() def test_no_authentication_policy(self): - request = _makeRequest() - result = self._callFUT(request, 'me') - self.assertEqual(result, []) + request = self._makeOne() + result = request.has_permission('view') + self.assertTrue(result) + self.assertEqual(result.msg, 'No authentication policy in use.') - def test_with_authentication_policy(self): - request = _makeRequest() - registry = request.registry - _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request, 'me') - self.assertEqual(result, 'yo') + def test_with_no_authorization_policy(self): + request = self._makeOne() + _registerAuthenticationPolicy(request.registry, None) + self.assertRaises(ValueError, + request.has_permission, 'view', context=None) - def test_with_authentication_policy_no_reg_on_request(self): + def test_with_authn_and_authz_policies_registered(self): + request = self._makeOne() + _registerAuthenticationPolicy(request.registry, None) + _registerAuthorizationPolicy(request.registry, 'yo') + self.assertEqual(request.has_permission('view', context=None), 'yo') + + def test_with_no_reg_on_request(self): from pyramid.threadlocal import get_current_registry registry = get_current_registry() - request = DummyRequest({}) - _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request, 'me') - self.assertEqual(result, 'yo') - -class TestForget(unittest.TestCase): - def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() - - def _callFUT(self, *arg): - from pyramid.security import forget - return forget(*arg) + request = self._makeOne() + del request.registry + _registerAuthenticationPolicy(registry, None) + _registerAuthorizationPolicy(registry, 'yo') + self.assertEqual(request.has_permission('view'), 'yo') - def test_no_authentication_policy(self): - request = _makeRequest() - result = self._callFUT(request) - self.assertEqual(result, []) + def test_with_no_context_passed(self): + request = self._makeOne() + self.assertTrue(request.has_permission('view')) - def test_with_authentication_policy(self): - request = _makeRequest() - _registerAuthenticationPolicy(request.registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + def test_with_no_context_passed_or_on_request(self): + request = self._makeOne() + del request.context + self.assertRaises(AttributeError, request.has_permission, 'view') - def test_with_authentication_policy_no_reg_on_request(self): - from pyramid.threadlocal import get_current_registry - registry = get_current_registry() - request = DummyRequest({}) - _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') +_TEST_HEADER = 'X-Pyramid-Test' class DummyContext: def __init__(self, *arg, **kw): self.__dict__.update(kw) -class DummyRequest: - def __init__(self, environ): - self.environ = environ - class DummyAuthenticationPolicy: def __init__(self, result): self.result = result @@ -411,10 +463,14 @@ class DummyAuthenticationPolicy: return self.result def remember(self, request, principal, **kw): - return self.result + headers = [(_TEST_HEADER, principal)] + self._header_remembered = headers[0] + return headers def forget(self, request): - return self.result + headers = [(_TEST_HEADER, 'logout')] + self._header_forgotten = headers[0] + return headers class DummyAuthorizationPolicy: def __init__(self, result): @@ -440,8 +496,24 @@ def _registerAuthorizationPolicy(reg, result): def _makeRequest(): from pyramid.registry import Registry - request = DummyRequest({}) + request = testing.DummyRequest(environ={}) request.registry = Registry() + request.context = object() return request +def _makeFakeRequest(): + class FakeRequest(testing.DummyRequest): + @property + def authenticated_userid(req): + return 'authenticated_userid' + + @property + def unauthenticated_userid(req): + return 'unauthenticated_userid' + + @property + def effective_principals(req): + return 'effective_principals' + + return FakeRequest({}) |
