From 3c2f95e8049bbd45b144d454daa68005361828b2 Mon Sep 17 00:00:00 2001 From: Matt Russell Date: Thu, 24 Oct 2013 23:52:42 +0100 Subject: Security APIs on pyramid.request.Request The pyramid.security Authorization API function has_permission is made available on the request. The pyramid.security Authentication API functions are now available as properties (unauthenticated_userid, authenticated_userid, effective_principals) and methods (remember_userid, forget_userid) on pyramid.request.Request. Backwards compatibility: For each of the APIs moved to request method or property, the original API in the pyramid.security module proxies to the request. Reworked tests to check module level b/c wrappers call through to mixins for each API. Tests that check no reg on request now do the right thing. Use a response callback to set the request headers for forget_userid and remember_userid. Update docs. Attempt to improve a documentation section referencing the pyramid.security.has_permission function in docs/narr/resources.rst Ensures backwards compatiblity for `pyramid.security.forget` and `pyramid.security.remember`. --- CHANGES.txt | 8 + CONTRIBUTORS.txt | 2 + docs/narr/resources.rst | 12 +- docs/narr/security.rst | 4 +- docs/narr/testing.rst | 2 +- docs/narr/viewconfig.rst | 2 +- docs/tutorials/wiki/authorization.rst | 4 +- docs/tutorials/wiki2/authorization.rst | 4 +- pyramid/config/routes.py | 2 +- pyramid/config/testing.py | 10 +- pyramid/config/views.py | 2 +- pyramid/request.py | 10 +- pyramid/security.py | 291 +++++++++++++---------- pyramid/testing.py | 11 +- pyramid/tests/test_config/test_testing.py | 33 ++- pyramid/tests/test_request.py | 12 +- pyramid/tests/test_security.py | 369 +++++++++++++++++------------- 17 files changed, 465 insertions(+), 313 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 895dc572f..61f3b63f7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,14 @@ Unreleased Features -------- +- The :mod:``pyramid.security`` authentication API methods should now be + accessed via the request. The ``pyramid.security`` authoriztion API function + :meth:`has_permission` should now be accessed via the request. + The methods :meth:``pyramid.request.Request.forget_userid``, + meth:``pyramid.request.Request.remember_userid`` now automatically + set the headers on the response, as returned by the corrosponding + method of the current request's :term:``authentication policy``. + - 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:: diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index bfe22e540..6dba1076e 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -224,3 +224,5 @@ Contributors - Doug Hellmann, 2013/09/06 - Karl O. Pinc, 2013/09/27 + +- Matthew Russell, 2013/10/14 diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index b1bb611e5..34d75f2cc 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -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/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..3f5d5ae6c 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -229,7 +229,7 @@ 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 +: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 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/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 460a852e0..2bd8c1f1c 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -207,8 +207,8 @@ need to be added.) :meth:`~pyramid.view.forbidden_view_config` will be used to customize the default 403 Forbidden page. -:meth:`~pyramid.security.remember` and -:meth:`~pyramid.security.forget` help to create and +:meth:`~pyramid.request.Request.remember_userid` and +:meth:`~pyramid.request.Request.forget_userid` help to create and expire an auth ticket cookie. Now add the ``login`` and ``logout`` views: diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index cf20db6d7..2b4263610 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -230,8 +230,8 @@ head of ``tutorial/tutorial/views.py``: :meth:`~pyramid.view.forbidden_view_config` will be used to customize the default 403 Forbidden page. -:meth:`~pyramid.security.remember` and -:meth:`~pyramid.security.forget` help to create and +:meth:`~pyramid.request.Request.remember_userid` and +:meth:`~pyramid.request.Request.forget_userid` help to create and expire an auth ticket cookie. Now add the ``login`` and ``logout`` views: diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 4de4663a8..5a671c819 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -237,7 +237,7 @@ 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 + :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: diff --git a/pyramid/config/testing.py b/pyramid/config/testing.py index 2ab85b1f5..a006c4767 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 + :meth:`pyramid.request.Request.authenticated_userid` or + :meth:`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`, + :meth:`pyramid.request.Request.authenticated_userid`, + :meth:`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 69f68e422..b0cd785f5 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1017,7 +1017,7 @@ 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 + :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: diff --git a/pyramid/request.py b/pyramid/request.py index 2cf0613f7..da640ea7d 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -21,6 +21,7 @@ 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 +137,13 @@ 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..b5e0a2c78 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -30,79 +30,64 @@ 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 + +# b/c +def has_permission(permission, context, request): + """ Backwards compatible wrapper. - 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) + Delegates to the :meth:``pyramid.request.Request.has_permission`` method. + """ + return request.has_permission(permission, context) +# b/c 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 + """ Backwards compatible wrapper. - policy = reg.queryUtility(IAuthenticationPolicy) - if policy is None: - return None - return policy.authenticated_userid(request) + Delegates to the + :meth:``pyramid.request.Request.authenticated_userid`` method. + """ + return request.authenticated_userid +# b/c 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 + """ Backwards compatible wrapper. - policy = reg.queryUtility(IAuthenticationPolicy) - if policy is None: - return None - return policy.unauthenticated_userid(request) + Delegates to the + :meth:``pyramid.request.Request.unauthenticated_userid`` method. + """ + return request.unauthenticated_userid +# b/c 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 + """ Backwards compatible wrapper. + + Delegates to the + :meth:``pyramid.request.Request.effective_principals`` method. + """ + return request.effective_principals + +# b/c +def remember(request, principal, **kw): + """ Backwards compatible wrapper. + + Delegates to the :meth:``pyramid.request.Request.remember_userid`` method. + """ + return request._remember_userid(principal, **kw) + +# b/c +def forget(request): + """ Backwards compatible wrapper. + + Delegates to the :meth:``pyramid.request.Request.forget_userid`` method. + """ + return request._forget_userid() - policy = reg.queryUtility(IAuthenticationPolicy) - if policy is None: - return [Everyone] - return policy.effective_principals(request) def principals_allowed_by_permission(context, permission): """ Provided a ``context`` (a resource object), and a ``permission`` @@ -140,10 +125,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 +139,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 +224,134 @@ 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.""" + 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 + :func:`~pyramid.security.authenticated_userid`, because the effective + authentication policy will not ensure that a record associated with the + userid exists in persistent storage.""" + 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.""" + policy = self._get_authentication_policy() + if policy is None: + return [Everyone] + return policy.effective_principals(self) + + # b/c + def _remember_userid(self, principal, **kw): + policy = self._get_authentication_policy() + if policy is None: + return + return policy.remember(self, principal, **kw) + + def remember_userid(self, principal, **kw): + """ Sets 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):: + + .. code-block:: python + + request.remember_userid('chrism', password='123', max_age='86400') + + If no :term:`authentication policy` is in use, this function will + do nothing. If used, the composition and + meaning of ``**kw`` must be agreed upon by the calling code and + the effective authentication policy.""" + headers = self._remember_userid(principal, **kw) + callback = lambda req, response: response.headerlist.extend(headers) + self.add_response_callback(callback) + + # b/c + def _forget_userid(self): + policy = self._get_authentication_policy() + if policy is None: + return + return policy.forget(self) + + def forget_userid(self): + """ Sets a sequence of header tuples (e.g. ``[('Set-Cookie', + 'foo=abc')]``) suitable for 'forgetting' the set of credentials + possessed by the currently authenticated user on the response. + 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):: + + .. code-block:: python + + request.forget_userid() + + If no :term:`authentication policy` is in use, this function will + be a noop.""" + headers = self._forget_userid() + callback = lambda req, response: response.headerlist.extend(headers) + self.add_response_callback(callback) + +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. + + .. versionchanged:: 1.5a3 + If context is None, then attempt to use the context attribute + of self, if not set then the AttributeError is propergated. + + :param permission: Does this request have the given permission? + :type permission: unicode, str + :param context: Typically a resource of a regsitered type. + :type context: object + :returns: `pyramid.security.PermitsResult` + """ + 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..2416c7b34 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,13 @@ 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..d13cb9285 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): @@ -24,28 +25,31 @@ class TestingConfiguratorMixinTests(unittest.TestCase): self.assertEqual(ut.permissive, False) def test_testing_securitypolicy_remember_result(self): - from pyramid.security import remember config = self._makeOne(autocommit=True) pol = config.testing_securitypolicy( 'user', ('group1', 'group2'), - permissive=False, remember_result=True) + permissive=False, + remember_result=[('X-Pyramid-Test', True)]) request = DummyRequest() request.registry = config.registry - val = remember(request, 'fred') + request.remember_userid('fred') self.assertEqual(pol.remembered, 'fred') + val = dict(request.response.headerlist).get('X-Pyramid-Test') self.assertEqual(val, True) def test_testing_securitypolicy_forget_result(self): - from pyramid.security import forget config = self._makeOne(autocommit=True) pol = config.testing_securitypolicy( 'user', ('group1', 'group2'), - permissive=False, forget_result=True) + permissive=False, + forget_result=[('X-Pyramid-Test', True)]) request = DummyRequest() request.registry = config.registry - val = forget(request) + request.response = DummyResponse() + request.forget_userid() self.assertEqual(pol.forgotten, True) - self.assertEqual(val, True) + val = dict(request.response.headerlist).get('X-Pyramid-Test') + self.assertTrue(val) def test_testing_resources(self): from pyramid.traversal import find_resource @@ -196,7 +200,15 @@ from zope.interface import implementer class DummyEvent: pass -class DummyRequest: +class DummyResponse(object): + def __init__(self): + self.headers = [] + + @property + def headerlist(self): + return self.headers + +class DummyRequest(AuthenticationAPIMixin, AuthorizationAPIMixin): subpath = () matchdict = None def __init__(self, environ=None): @@ -205,4 +217,7 @@ class DummyRequest: self.environ = environ self.params = {} self.cookies = {} - + self.response = DummyResponse() + + def add_response_callback(self, callback): + callback(self, self.response) 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..4b40feaf3 100644 --- a/pyramid/tests/test_security.py +++ b/pyramid/tests/test_security.py @@ -1,7 +1,8 @@ import unittest -from pyramid.testing import cleanUp +from pyramid.testing import cleanUp, DummyRequest +_TEST_HEADER = 'X-Pyramid-Test' class TestAllPermissionsList(unittest.TestCase): def setUp(self): @@ -103,13 +104,38 @@ class TestACLDenied(unittest.TestCase): self.assertTrue('" % msg in repr(denied)) -class TestViewExecutionPermitted(unittest.TestCase): +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 TestViewExecutionPermitted(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + def _callFUT(self, *arg, **kw): from pyramid.security import view_execution_permitted return view_execution_permitted(*arg, **kw) @@ -174,229 +200,258 @@ class TestViewExecutionPermitted(unittest.TestCase): request = DummyRequest({}) directlyProvides(request, IRequest) result = self._callFUT(context, request, '') - self.assertTrue(result is True) + self.assertTrue(result) -class TestHasPermission(unittest.TestCase): +class AuthenticationAPIMixinTest(object): 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') - -class TestAuthenticatedUserId(unittest.TestCase): - def setUp(self): - cleanUp() - def tearDown(self): cleanUp() - def _callFUT(self, request): + def _makeOne(self): + from pyramid.registry import Registry + from pyramid.security import AuthenticationAPIMixin + request = DummyRequest(environ={}) + self.assertTrue(isinstance(request, AuthenticationAPIMixin)) + request.registry = Registry() + request.context = object() + return request + + def _makeFakeOne(self): + class FakeRequest(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' + + def _forget_userid(req): + return [('X-Pyramid-Test', 'forget_userid')] + + def _remember_userid(req, principal, **kw): + return [('X-Pyramid-Test', 'remember_userid')] + + return FakeRequest({}) + +class TestAuthenticatedUserId(AuthenticationAPIMixinTest, unittest.TestCase): + def test_backward_compat_delegates_to_mixin(self): + request = self._makeFakeOne() from pyramid.security import authenticated_userid - return authenticated_userid(request) + self.assertEqual(authenticated_userid(request), 'authenticated_userid') def test_no_authentication_policy(self): - request = _makeRequest() - result = self._callFUT(request) - self.assertEqual(result, None) + request = self._makeOne() + self.assertEqual(request.authenticated_userid, None) def test_with_authentication_policy(self): - request = _makeRequest() + request = self._makeOne() _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 = self._makeOne() + del request.registry _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + self.assertEqual(request.authenticated_userid, 'yo') -class TestUnauthenticatedUserId(unittest.TestCase): - def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() - - def _callFUT(self, request): +class TestUnAuthenticatedUserId(AuthenticationAPIMixinTest, unittest.TestCase): + def test_backward_compat_delegates_to_mixin(self): + request = self._makeFakeOne() from pyramid.security import unauthenticated_userid - return unauthenticated_userid(request) + self.assertEqual(unauthenticated_userid(request), + 'unauthenticated_userid') def test_no_authentication_policy(self): - request = _makeRequest() - result = self._callFUT(request) - self.assertEqual(result, None) + request = self._makeOne() + self.assertEqual(request.unauthenticated_userid, None) def test_with_authentication_policy(self): - request = _makeRequest() + request = self._makeOne() _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 = self._makeOne() + 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() - - def _callFUT(self, request): +class TestEffectivePrincipals(AuthenticationAPIMixinTest, unittest.TestCase): + def test_backward_compat_delegates_to_mixin(self): + request = self._makeFakeOne() from pyramid.security import effective_principals - return effective_principals(request) + self.assertEqual(effective_principals(request), 'effective_principals') def test_no_authentication_policy(self): from pyramid.security import Everyone - request = _makeRequest() - result = self._callFUT(request) - self.assertEqual(result, [Everyone]) + request = self._makeOne() + self.assertEqual(request.effective_principals, [Everyone]) def test_with_authentication_policy(self): - request = _makeRequest() + request = self._makeOne() _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 = self._makeOne() + 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() +class ResponseCallbackTestMixin(AuthenticationAPIMixinTest): - def _callFUT(self, *arg): - from pyramid.security import principals_allowed_by_permission - return principals_allowed_by_permission(*arg) + def assert_response_headers_set(self, request): + request._process_response_callbacks(request.response) + headers = request.response.headerlist + self.assertTrue((_TEST_HEADER, self.principal) in headers, msg=headers) - def test_no_authorization_policy(self): - from pyramid.security import Everyone - context = DummyContext() - result = self._callFUT(context, 'view') - self.assertEqual(result, [Everyone]) +class TestRememberUserId(ResponseCallbackTestMixin, unittest.TestCase): + principal = 'the4th' - def test_with_authorization_policy(self): + def test_backward_compat_delegates_to_mixin(self): + request = self._makeFakeOne() + from pyramid.security import remember + self.assertEqual(remember(request, 'matt'), + [('X-Pyramid-Test', 'remember_userid')]) + + def test_with_no_authentication_policy(self): + request = self._makeOne() + headers_before = request.response.headers + request.remember_userid(self.principal) + self.assertEqual(headers_before, request.response.headers) + + def test_with_authentication_policy(self): + request = self._makeOne() + _registerAuthenticationPolicy(request.registry, self.principal) + request.remember_userid(self.principal) + self.assert_response_headers_set(request) + + def test_with_authentication_policy_no_reg_on_request(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') + request = self._makeOne() + del request.registry + _registerAuthenticationPolicy(registry, self.principal) + request.remember_userid(self.principal) + self.assert_response_headers_set(request) -class TestRemember(unittest.TestCase): - def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() +class TestForgetUserId(ResponseCallbackTestMixin, unittest.TestCase): + principal = 'me-not' - def _callFUT(self, *arg): - from pyramid.security import remember - return remember(*arg) + def _makeOne(self): + request = super(TestForgetUserId, self)._makeOne() + request.response.headers.add(_TEST_HEADER, self.principal) + return request - def test_no_authentication_policy(self): - request = _makeRequest() - result = self._callFUT(request, 'me') - self.assertEqual(result, []) + def test_backward_compat_delegates_to_mixin(self): + request = self._makeFakeOne() + from pyramid.security import forget + self.assertEqual(forget(request), + [('X-Pyramid-Test', 'forget_userid')]) + + def test_with_no_authentication_policy(self): + request = self._makeOne() + headers_before = request.response.headers + request.forget_userid() + self.assertEqual(headers_before, request.response.headers) def test_with_authentication_policy(self): - request = _makeRequest() - registry = request.registry - _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request, 'me') - self.assertEqual(result, 'yo') + request = self._makeOne() + policy = _registerAuthenticationPolicy(request.registry, self.principal) + policy._header_remembered = (_TEST_HEADER, self.principal) + request.forget_userid() + self.assert_response_headers_set(request) 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, 'me') - self.assertEqual(result, 'yo') - -class TestForget(unittest.TestCase): + request = self._makeOne() + del request.registry + policy = _registerAuthenticationPolicy(registry, self.principal) + policy._header_remembered = (_TEST_HEADER, self.principal) + request.forget_userid() + self.assert_response_headers_set(request) + +class TestHasPermission(unittest.TestCase): def setUp(self): cleanUp() def tearDown(self): cleanUp() - def _callFUT(self, *arg): - from pyramid.security import forget - return forget(*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): + 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) def test_no_authentication_policy(self): - request = _makeRequest() - result = self._callFUT(request) - 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() - _registerAuthenticationPolicy(request.registry, 'yo') - result = self._callFUT(request) - 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) - self.assertEqual(result, 'yo') + request = self._makeOne() + del request.registry + _registerAuthenticationPolicy(registry, None) + _registerAuthorizationPolicy(registry, 'yo') + self.assertEqual(request.has_permission('view'), 'yo') + + def test_with_no_context_passed(self): + request = self._makeOne() + self.assertTrue(request.has_permission('view')) + + def test_with_no_context_passed_or_on_request(self): + request = self._makeOne() + del request.context + self.assertRaises(AttributeError, request.has_permission, 'view') 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 +466,12 @@ 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 + return [self._header_remembered] class DummyAuthorizationPolicy: def __init__(self, result): @@ -437,11 +494,3 @@ def _registerAuthorizationPolicy(reg, result): policy = DummyAuthorizationPolicy(result) reg.registerUtility(policy, IAuthorizationPolicy) return policy - -def _makeRequest(): - from pyramid.registry import Registry - request = DummyRequest({}) - request.registry = Registry() - return request - - -- cgit v1.2.3 From 0184b527725cfb634e4d57a1b033450fa8b24502 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 28 Oct 2013 15:26:31 -0400 Subject: Bring change log, API docs, and deprecations in line with normal policies/processes --- CHANGES.txt | 56 +++++++++++-- docs/api/request.rst | 47 ++++++++++- pyramid/config/routes.py | 8 +- pyramid/config/testing.py | 8 +- pyramid/config/views.py | 8 +- pyramid/request.py | 21 +++-- pyramid/security.py | 185 ++++++++++++++++++++++++++++++++--------- pyramid/testing.py | 14 ++-- pyramid/tests/test_security.py | 96 +++++++++++++++------ 9 files changed, 345 insertions(+), 98 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 61f3b63f7..20b7726c4 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,13 +4,34 @@ Unreleased Features -------- -- The :mod:``pyramid.security`` authentication API methods should now be - accessed via the request. The ``pyramid.security`` authoriztion API function - :meth:`has_permission` should now be accessed via the request. - The methods :meth:``pyramid.request.Request.forget_userid``, - meth:``pyramid.request.Request.remember_userid`` now automatically - set the headers on the response, as returned by the corrosponding - method of the current request's :term:``authentication policy``. +- Authentication and authorization APIs have been added as as methods of the + request: ``request.has_permission``, ``request.forget_userid``, and + ``request.remember_userid``. + + ``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. + + ``request.forget_userid`` and ``request.remember_userid`` are method-based + alternatives to ``pyramid.security.forget`` and + ``pyramid.security.remember``. These do not work exacly the same as their + function counterparts, however. These methods automatically set the headers + returned by the authentication policy on the response, whereas the older + function-based APIs returned a sequence of headers and required the caller to + set those headers. The older function-based API still works but 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 @@ -105,6 +126,27 @@ 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.forget`` API is now deprecated. Instead, use + the newly-added ``forget_userid`` method of the request object. + +- The ``pyramid.security.remember`` API is now deprecated. Instead, use + the newly-added ``remember_userid`` 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/docs/api/request.rst b/docs/api/request.rst index 72abddb68..3d1fe020c 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -11,7 +11,10 @@ :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, forget_userid, + remember_userid .. attribute:: context @@ -161,6 +164,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 + :meth:`~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 + :meth:`~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 +254,12 @@ request provided by e.g. the ``pshell`` environment. For more information, see :ref:`subrequest_chapter`. + .. automethod:: remember_userid + + .. automethod:: forget_userid + + .. automethod:: has_permission + .. automethod:: add_response_callback .. automethod:: add_finished_callback diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 5a671c819..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 - :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: + :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 a006c4767..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 - :meth:`pyramid.request.Request.authenticated_userid` or - :meth:`pyramid.request.Request.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 :meth:`pyramid.request.Request.has_permission`, - :meth:`pyramid.request.Request.authenticated_userid`, - :meth:`pyramid.request.Request.effective_principals`, and + :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 b0cd785f5..a3f885504 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1017,10 +1017,10 @@ class ViewsConfiguratorMixin(object): If specified, this value should be a :term:`principal` identifier or a sequence of principal identifiers. If the - :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: + :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/request.py b/pyramid/request.py index da640ea7d..188e968ac 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -21,7 +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.security import ( + AuthenticationAPIMixin, + AuthorizationAPIMixin, + ) from pyramid.url import URLMethodsMixin from pyramid.util import InstancePropertyMixin @@ -137,13 +140,15 @@ class CallbackMethodsMixin(object): callback(self) @implementer(IRequest) -class Request(BaseRequest, - URLMethodsMixin, - CallbackMethodsMixin, - InstancePropertyMixin, - LocalizerRequestMixin, - AuthenticationAPIMixin, - AuthorizationAPIMixin): +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 b5e0a2c78..27612206a 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 ( @@ -37,57 +38,152 @@ def _get_registry(request): reg = get_current_registry() # b/c return reg -# b/c def has_permission(permission, context, request): - """ Backwards compatible wrapper. + """ + 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. - Delegates to the :meth:``pyramid.request.Request.has_permission`` method. + .. 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) -# b/c -def authenticated_userid(request): - """ Backwards compatible wrapper. +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.' + ) + - Delegates to the - :meth:``pyramid.request.Request.authenticated_userid`` method. +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 -# b/c -def unauthenticated_userid(request): - """ Backwards compatible wrapper. +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.' + ) - Delegates to the - :meth:``pyramid.request.Request.unauthenticated_userid`` method. +def unauthenticated_userid(request): + """ + 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 -# b/c -def effective_principals(request): - """ Backwards compatible wrapper. +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.' + ) - Delegates to the - :meth:``pyramid.request.Request.effective_principals`` method. +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 -# b/c -def remember(request, principal, **kw): - """ Backwards compatible wrapper. +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.' + ) - Delegates to the :meth:``pyramid.request.Request.remember_userid`` method. - """ +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):: + + .. code-block:: python + + 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 + do nothing. If used, the composition and + meaning of ``**kw`` must be agreed upon by the calling code and + the effective authentication policy. + + .. deprecated:: 1.5 + Use :meth:`pyramid.request.Request.remember_userid` instead. + but be sure to read its docs first; the remember_userid method is not an + exact analog of the remember function, because it sets headers instead + of returning them. + """ return request._remember_userid(principal, **kw) -# b/c -def forget(request): - """ Backwards compatible wrapper. +deprecated( + 'remember', + 'As of Pyramid 1.5 the "pyramid.security.remember" API is ' + 'now deprecated. It will be removed in Pyramd 1.8. Use the ' + '"remember_userid" method of the Pyramid request instead, but be sure to ' + 'read the docs first; the remember_userid method is not an exact analog of ' + 'the remember function, because it sets headers instead of returning them.' + ) - Delegates to the :meth:``pyramid.request.Request.forget_userid`` method. +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. + + .. deprecated:: 1.5 + Use :meth:`pyramid.request.Request.forget_userid` instead. + but be sure to read its docs first; the forget_userid method is not an + exact analog of the forget function, because it sets headers instead + of returning them. """ return request._forget_userid() +deprecated( + 'forget', + 'As of Pyramid 1.5 the "pyramid.security.forget" API is ' + 'now deprecated. It will be removed in Pyramd 1.8. Use the ' + '"forget_user" method of the Pyramid request instead, but be sure to ' + 'read the docs first; the forget_userid method is not an exact analog of ' + 'the forget function, because it sets headers instead of returning them.' + ) def principals_allowed_by_permission(context, permission): """ Provided a ``context`` (a resource object), and a ``permission`` @@ -234,7 +330,10 @@ class AuthenticationAPIMixin(object): 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.""" + there is no currently authenticated user. + + .. versionadded:: 1.5 + """ policy = self._get_authentication_policy() if policy is None: return None @@ -248,7 +347,10 @@ class AuthenticationAPIMixin(object): 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.""" + userid exists in persistent storage. + + .. versionadded:: 1.5 + """ policy = self._get_authentication_policy() if policy is None: return None @@ -260,7 +362,10 @@ class AuthenticationAPIMixin(object): 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.""" + this will return an empty sequence. + + .. versionadded:: 1.5 + """ policy = self._get_authentication_policy() if policy is None: return [Everyone] @@ -290,7 +395,11 @@ class AuthenticationAPIMixin(object): If no :term:`authentication policy` is in use, this function will do nothing. If used, the composition and meaning of ``**kw`` must be agreed upon by the calling code and - the effective authentication policy.""" + the effective authentication policy. + + .. versionadded:: 1.5 + + """ headers = self._remember_userid(principal, **kw) callback = lambda req, response: response.headerlist.extend(headers) self.add_response_callback(callback) @@ -315,9 +424,12 @@ class AuthenticationAPIMixin(object): request.forget_userid() If no :term:`authentication policy` is in use, this function will - be a noop.""" + be a noop. + + .. versionadded:: 1.5 + """ headers = self._forget_userid() - callback = lambda req, response: response.headerlist.extend(headers) + callback = lambda req, response: response.headerlist.extend(headers) self.add_response_callback(callback) class AuthorizationAPIMixin(object): @@ -333,15 +445,14 @@ class AuthorizationAPIMixin(object): unconditionally if no authentication policy has been registered for this request. - .. versionchanged:: 1.5a3 - If context is None, then attempt to use the context attribute - of self, if not set then the AttributeError is propergated. - :param permission: Does this request have the given permission? :type permission: unicode, str :param context: Typically a resource of a regsitered type. :type context: object :returns: `pyramid.security.PermitsResult` + + .. versionadded:: 1.5 + """ if context is None: context = self.context diff --git a/pyramid/testing.py b/pyramid/testing.py index 2416c7b34..b3460d8aa 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -283,12 +283,14 @@ class DummySession(dict): return token @implementer(IRequest) -class DummyRequest(URLMethodsMixin, - CallbackMethodsMixin, - InstancePropertyMixin, - LocalizerRequestMixin, - AuthenticationAPIMixin, - AuthorizationAPIMixin): +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_security.py b/pyramid/tests/test_security.py index 4b40feaf3..b685ddc97 100644 --- a/pyramid/tests/test_security.py +++ b/pyramid/tests/test_security.py @@ -242,9 +242,17 @@ class AuthenticationAPIMixinTest(object): class TestAuthenticatedUserId(AuthenticationAPIMixinTest, unittest.TestCase): def test_backward_compat_delegates_to_mixin(self): - request = self._makeFakeOne() - from pyramid.security import authenticated_userid - self.assertEqual(authenticated_userid(request), 'authenticated_userid') + from zope.deprecation import __show__ + try: + __show__.off() + request = self._makeFakeOne() + from pyramid.security import authenticated_userid + self.assertEqual( + authenticated_userid(request), + 'authenticated_userid' + ) + finally: + __show__.on() def test_no_authentication_policy(self): request = self._makeOne() @@ -265,10 +273,17 @@ class TestAuthenticatedUserId(AuthenticationAPIMixinTest, unittest.TestCase): class TestUnAuthenticatedUserId(AuthenticationAPIMixinTest, unittest.TestCase): def test_backward_compat_delegates_to_mixin(self): - request = self._makeFakeOne() - from pyramid.security import unauthenticated_userid - self.assertEqual(unauthenticated_userid(request), - 'unauthenticated_userid') + from zope.deprecation import __show__ + try: + __show__.off() + request = self._makeFakeOne() + from pyramid.security import unauthenticated_userid + self.assertEqual( + unauthenticated_userid(request), + 'unauthenticated_userid', + ) + finally: + __show__.on() def test_no_authentication_policy(self): request = self._makeOne() @@ -290,8 +305,16 @@ class TestUnAuthenticatedUserId(AuthenticationAPIMixinTest, unittest.TestCase): class TestEffectivePrincipals(AuthenticationAPIMixinTest, unittest.TestCase): def test_backward_compat_delegates_to_mixin(self): request = self._makeFakeOne() - from pyramid.security import effective_principals - self.assertEqual(effective_principals(request), 'effective_principals') + 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 @@ -322,10 +345,17 @@ class TestRememberUserId(ResponseCallbackTestMixin, unittest.TestCase): principal = 'the4th' def test_backward_compat_delegates_to_mixin(self): - request = self._makeFakeOne() - from pyramid.security import remember - self.assertEqual(remember(request, 'matt'), - [('X-Pyramid-Test', 'remember_userid')]) + from zope.deprecation import __show__ + try: + __show__.off() + request = self._makeFakeOne() + from pyramid.security import remember + self.assertEqual( + remember(request, 'matt'), + [('X-Pyramid-Test', 'remember_userid')] + ) + finally: + __show__.on() def test_with_no_authentication_policy(self): request = self._makeOne() @@ -357,10 +387,17 @@ class TestForgetUserId(ResponseCallbackTestMixin, unittest.TestCase): return request def test_backward_compat_delegates_to_mixin(self): - request = self._makeFakeOne() - from pyramid.security import forget - self.assertEqual(forget(request), - [('X-Pyramid-Test', 'forget_userid')]) + from zope.deprecation import __show__ + try: + __show__.off() + request = self._makeFakeOne() + from pyramid.security import forget + self.assertEqual( + forget(request), + [('X-Pyramid-Test', 'forget_userid')], + ) + finally: + __show__.on() def test_with_no_authentication_policy(self): request = self._makeOne() @@ -401,16 +438,21 @@ class TestHasPermission(unittest.TestCase): return mixin def test_delegates_to_mixin(self): - 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) + 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 = self._makeOne() -- cgit v1.2.3 From 072a2cd56877ce46f9db2fb6f576ef62ecefff15 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 28 Oct 2013 17:06:30 -0400 Subject: add on_exception flag to remember/forget, fix a bug in _remember_userid and _forget_userid (these should always return a sequence even if there is no authentication policy), defactorize tests --- pyramid/security.py | 38 +++++-- pyramid/tests/test_security.py | 249 ++++++++++++++++++++++++++--------------- 2 files changed, 189 insertions(+), 98 deletions(-) diff --git a/pyramid/security.py b/pyramid/security.py index 27612206a..1b52c9cb5 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -130,7 +130,7 @@ def remember(request, principal, **kw): return response If no :term:`authentication policy` is in use, this function will - do nothing. If used, the composition and + 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. @@ -375,10 +375,10 @@ class AuthenticationAPIMixin(object): def _remember_userid(self, principal, **kw): policy = self._get_authentication_policy() if policy is None: - return + return [] return policy.remember(self, principal, **kw) - def remember_userid(self, principal, **kw): + def remember_userid(self, principal, on_exception=False, **kw): """ Sets 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 @@ -397,21 +397,34 @@ class AuthenticationAPIMixin(object): meaning of ``**kw`` must be agreed upon by the calling code and the effective authentication policy. + One special keyword value is understood by this method: + ``on_exception``. Usually if an exception occurs within the same + request after this method is called, the headers provided by the + authentication policy will not be set on the response. If + ``on_exception`` is passed, and as ``True``, then the headers will be + set on the response even if an exception is later raised. By default + this value is ``False``. + .. versionadded:: 1.5 """ headers = self._remember_userid(principal, **kw) - callback = lambda req, response: response.headerlist.extend(headers) + def callback(req, response): + # do not set the headers on an exception unless explicitly + # instructed + exc = getattr(req, 'exception', None) + if exc is None or on_exception: + response.headerlist.extend(headers) self.add_response_callback(callback) # b/c def _forget_userid(self): policy = self._get_authentication_policy() if policy is None: - return + return [] return policy.forget(self) - def forget_userid(self): + def forget_userid(self, on_exception=False): """ Sets a sequence of header tuples (e.g. ``[('Set-Cookie', 'foo=abc')]``) suitable for 'forgetting' the set of credentials possessed by the currently authenticated user on the response. @@ -426,10 +439,21 @@ class AuthenticationAPIMixin(object): If no :term:`authentication policy` is in use, this function will be a noop. + One special keyword value is understood by this method: + ``on_exception``. Usually if an exception occurs within the same + request after this method is called, the headers provided by the + authentication policy will not be set on the response. If + ``on_exception`` is passed, and as ``True``, then the headers will be + set on the response even if an exception is later raised. By default + this value is ``False``. + .. versionadded:: 1.5 """ headers = self._forget_userid() - callback = lambda req, response: response.headerlist.extend(headers) + def callback(req, response): + exc = getattr(req, 'exception', None) + if exc is None or on_exception: + response.headerlist.extend(headers) self.add_response_callback(callback) class AuthorizationAPIMixin(object): diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py index b685ddc97..96f171324 100644 --- a/pyramid/tests/test_security.py +++ b/pyramid/tests/test_security.py @@ -1,15 +1,13 @@ import unittest -from pyramid.testing import cleanUp, DummyRequest - -_TEST_HEADER = 'X-Pyramid-Test' +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 @@ -106,10 +104,10 @@ class TestACLDenied(unittest.TestCase): class TestPrincipalsAllowedByPermission(unittest.TestCase): def setUp(self): - cleanUp() + testing.setUp() def tearDown(self): - cleanUp() + testing.tearDown() def _callFUT(self, *arg): from pyramid.security import principals_allowed_by_permission @@ -131,10 +129,10 @@ class TestPrincipalsAllowedByPermission(unittest.TestCase): class TestViewExecutionPermitted(unittest.TestCase): def setUp(self): - cleanUp() + testing.setUp() def tearDown(self): - cleanUp() + testing.tearDown() def _callFUT(self, *arg, **kw): from pyramid.security import view_execution_permitted @@ -166,7 +164,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() @@ -185,7 +183,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): @@ -197,55 +195,23 @@ 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) -class AuthenticationAPIMixinTest(object): +class TestAuthenticatedUserId(unittest.TestCase): def setUp(self): - cleanUp() + testing.setUp() def tearDown(self): - cleanUp() - - def _makeOne(self): - from pyramid.registry import Registry - from pyramid.security import AuthenticationAPIMixin - request = DummyRequest(environ={}) - self.assertTrue(isinstance(request, AuthenticationAPIMixin)) - request.registry = Registry() - request.context = object() - return request - - def _makeFakeOne(self): - class FakeRequest(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' - - def _forget_userid(req): - return [('X-Pyramid-Test', 'forget_userid')] - - def _remember_userid(req, principal, **kw): - return [('X-Pyramid-Test', 'remember_userid')] - - return FakeRequest({}) - -class TestAuthenticatedUserId(AuthenticationAPIMixinTest, unittest.TestCase): + testing.tearDown() + def test_backward_compat_delegates_to_mixin(self): from zope.deprecation import __show__ try: __show__.off() - request = self._makeFakeOne() + request = _makeFakeRequest() from pyramid.security import authenticated_userid self.assertEqual( authenticated_userid(request), @@ -255,28 +221,34 @@ class TestAuthenticatedUserId(AuthenticationAPIMixinTest, unittest.TestCase): __show__.on() def test_no_authentication_policy(self): - request = self._makeOne() + request = _makeRequest() self.assertEqual(request.authenticated_userid, None) def test_with_authentication_policy(self): - request = self._makeOne() + request = _makeRequest() _registerAuthenticationPolicy(request.registry, 'yo') self.assertEqual(request.authenticated_userid, 'yo') def test_with_authentication_policy_no_reg_on_request(self): from pyramid.threadlocal import get_current_registry registry = get_current_registry() - request = self._makeOne() + request = _makeRequest() del request.registry _registerAuthenticationPolicy(registry, 'yo') self.assertEqual(request.authenticated_userid, 'yo') -class TestUnAuthenticatedUserId(AuthenticationAPIMixinTest, unittest.TestCase): +class TestUnAuthenticatedUserId(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + def test_backward_compat_delegates_to_mixin(self): from zope.deprecation import __show__ try: __show__.off() - request = self._makeFakeOne() + request = _makeFakeRequest() from pyramid.security import unauthenticated_userid self.assertEqual( unauthenticated_userid(request), @@ -286,25 +258,31 @@ class TestUnAuthenticatedUserId(AuthenticationAPIMixinTest, unittest.TestCase): __show__.on() def test_no_authentication_policy(self): - request = self._makeOne() + request = _makeRequest() self.assertEqual(request.unauthenticated_userid, None) def test_with_authentication_policy(self): - request = self._makeOne() + request = _makeRequest() _registerAuthenticationPolicy(request.registry, 'yo') self.assertEqual(request.unauthenticated_userid, 'yo') def test_with_authentication_policy_no_reg_on_request(self): from pyramid.threadlocal import get_current_registry registry = get_current_registry() - request = self._makeOne() + request = _makeRequest() del request.registry _registerAuthenticationPolicy(registry, 'yo') self.assertEqual(request.unauthenticated_userid, 'yo') -class TestEffectivePrincipals(AuthenticationAPIMixinTest, unittest.TestCase): +class TestEffectivePrincipals(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + def test_backward_compat_delegates_to_mixin(self): - request = self._makeFakeOne() + request = _makeFakeRequest() from zope.deprecation import __show__ try: __show__.off() @@ -318,37 +296,41 @@ class TestEffectivePrincipals(AuthenticationAPIMixinTest, unittest.TestCase): def test_no_authentication_policy(self): from pyramid.security import Everyone - request = self._makeOne() + request = _makeRequest() self.assertEqual(request.effective_principals, [Everyone]) def test_with_authentication_policy(self): - request = self._makeOne() + request = _makeRequest() _registerAuthenticationPolicy(request.registry, '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 = self._makeOne() + request = _makeRequest() del request.registry _registerAuthenticationPolicy(registry, 'yo') self.assertEqual(request.effective_principals, 'yo') -class ResponseCallbackTestMixin(AuthenticationAPIMixinTest): +class TestRememberUserId(unittest.TestCase): + principal = 'the4th' + + def setUp(self): + testing.setUp() - def assert_response_headers_set(self, request): + def tearDown(self): + testing.tearDown() + + def assert_response_headers(self, request, expected_headers): request._process_response_callbacks(request.response) headers = request.response.headerlist - self.assertTrue((_TEST_HEADER, self.principal) in headers, msg=headers) - -class TestRememberUserId(ResponseCallbackTestMixin, unittest.TestCase): - principal = 'the4th' + self.assertEqual(list(expected_headers), list(headers)) def test_backward_compat_delegates_to_mixin(self): from zope.deprecation import __show__ try: __show__.off() - request = self._makeFakeOne() + request = _makeFakeRequest() from pyramid.security import remember self.assertEqual( remember(request, 'matt'), @@ -358,31 +340,63 @@ class TestRememberUserId(ResponseCallbackTestMixin, unittest.TestCase): __show__.on() def test_with_no_authentication_policy(self): - request = self._makeOne() - headers_before = request.response.headers + request = _makeRequest() + headers_before = request.response.headerlist request.remember_userid(self.principal) - self.assertEqual(headers_before, request.response.headers) + self.assert_response_headers(request, headers_before) def test_with_authentication_policy(self): - request = self._makeOne() + request = _makeRequest() + headers_before = request.response.headerlist + expected_headers = headers_before[:] + [(_TEST_HEADER, self.principal)] _registerAuthenticationPolicy(request.registry, self.principal) request.remember_userid(self.principal) - self.assert_response_headers_set(request) + self.assert_response_headers(request, expected_headers) def test_with_authentication_policy_no_reg_on_request(self): from pyramid.threadlocal import get_current_registry registry = get_current_registry() - request = self._makeOne() + request = _makeRequest() del request.registry _registerAuthenticationPolicy(registry, self.principal) + headers_before = request.response.headerlist + request.remember_userid(self.principal) + expected_headers = headers_before[:] + [(_TEST_HEADER, self.principal)] + self.assert_response_headers(request, expected_headers) + + def test_request_has_exception_attr_no_on_exception_flag(self): + request = _makeRequest() + headers_before = request.response.headerlist + _registerAuthenticationPolicy(request.registry, self.principal) + request.exception = True request.remember_userid(self.principal) - self.assert_response_headers_set(request) + self.assert_response_headers(request, headers_before) -class TestForgetUserId(ResponseCallbackTestMixin, unittest.TestCase): + def test_request_has_exception_attr_with_on_exception_flag(self): + request = _makeRequest() + headers_before = request.response.headerlist + _registerAuthenticationPolicy(request.registry, self.principal) + request.exception = True + request.remember_userid(self.principal, on_exception=True) + expected_headers = headers_before[:] + [(_TEST_HEADER, self.principal)] + self.assert_response_headers(request, expected_headers) + +class TestForgetUserId(unittest.TestCase): principal = 'me-not' + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def assert_response_headers(self, request, expected_headers): + request._process_response_callbacks(request.response) + headers = request.response.headerlist + self.assertEqual(list(expected_headers), list(headers)) + def _makeOne(self): - request = super(TestForgetUserId, self)._makeOne() + request = _makeRequest() request.response.headers.add(_TEST_HEADER, self.principal) return request @@ -390,7 +404,7 @@ class TestForgetUserId(ResponseCallbackTestMixin, unittest.TestCase): from zope.deprecation import __show__ try: __show__.off() - request = self._makeFakeOne() + request = _makeFakeRequest() from pyramid.security import forget self.assertEqual( forget(request), @@ -401,33 +415,52 @@ class TestForgetUserId(ResponseCallbackTestMixin, unittest.TestCase): def test_with_no_authentication_policy(self): request = self._makeOne() - headers_before = request.response.headers + headers_before = request.response.headerlist request.forget_userid() - self.assertEqual(headers_before, request.response.headers) + self.assert_response_headers(request, headers_before) def test_with_authentication_policy(self): request = self._makeOne() - policy = _registerAuthenticationPolicy(request.registry, self.principal) - policy._header_remembered = (_TEST_HEADER, self.principal) + headers_before = request.response.headerlist + expected_headers = headers_before[:] + [(_TEST_HEADER, 'forget_userid')] + _registerAuthenticationPolicy(request.registry, self.principal) request.forget_userid() - self.assert_response_headers_set(request) - + self.assert_response_headers(request, expected_headers) + def test_with_authentication_policy_no_reg_on_request(self): from pyramid.threadlocal import get_current_registry registry = get_current_registry() request = self._makeOne() del request.registry - policy = _registerAuthenticationPolicy(registry, self.principal) - policy._header_remembered = (_TEST_HEADER, self.principal) + _registerAuthenticationPolicy(registry, self.principal) + headers_before = request.response.headerlist request.forget_userid() - self.assert_response_headers_set(request) + expected_headers = headers_before[:] + [(_TEST_HEADER, 'forget_userid')] + self.assert_response_headers(request, expected_headers) + + def test_request_has_exception_attr_no_on_exception_flag(self): + request = self._makeOne() + headers_before = request.response.headerlist + _registerAuthenticationPolicy(request.registry, self.principal) + request.exception = True + request.forget_userid() + self.assert_response_headers(request, headers_before) + + def test_request_has_exception_attr_with_on_exception_flag(self): + request = self._makeOne() + headers_before = request.response.headerlist + _registerAuthenticationPolicy(request.registry, self.principal) + request.exception = True + request.forget_userid(on_exception=True) + expected_headers = headers_before[:] + [(_TEST_HEADER, 'forget_userid')] + self.assert_response_headers(request, expected_headers) class TestHasPermission(unittest.TestCase): def setUp(self): - cleanUp() + testing.setUp() def tearDown(self): - cleanUp() + testing.tearDown() def _makeOne(self): from pyramid.security import AuthorizationAPIMixin @@ -490,6 +523,8 @@ class TestHasPermission(unittest.TestCase): del request.context self.assertRaises(AttributeError, request.has_permission, 'view') +_TEST_HEADER = 'X-Pyramid-Test' + class DummyContext: def __init__(self, *arg, **kw): self.__dict__.update(kw) @@ -513,7 +548,9 @@ class DummyAuthenticationPolicy: return headers def forget(self, request): - return [self._header_remembered] + headers = [(_TEST_HEADER, 'forget_userid')] + self._header_forgotten = headers[0] + return headers class DummyAuthorizationPolicy: def __init__(self, result): @@ -536,3 +573,33 @@ def _registerAuthorizationPolicy(reg, result): policy = DummyAuthorizationPolicy(result) reg.registerUtility(policy, IAuthorizationPolicy) return policy + +def _makeRequest(): + from pyramid.registry import Registry + 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' + + def _forget_userid(req): + return [('X-Pyramid-Test', 'forget_userid')] + + def _remember_userid(req, principal, **kw): + return [('X-Pyramid-Test', 'remember_userid')] + + return FakeRequest({}) + -- cgit v1.2.3 From 15d4efeb012998e7675657ea3d781562957a4f71 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 28 Oct 2013 17:11:53 -0400 Subject: defer looking up headers until the response callback is called (FBO things like sessionauthenticationpolicy which does its own header-setting when its remember/forget methods are called) --- pyramid/security.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/security.py b/pyramid/security.py index 1b52c9cb5..afca8cd9a 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -408,12 +408,12 @@ class AuthenticationAPIMixin(object): .. versionadded:: 1.5 """ - headers = self._remember_userid(principal, **kw) def callback(req, response): # do not set the headers on an exception unless explicitly # instructed exc = getattr(req, 'exception', None) if exc is None or on_exception: + headers = self._remember_userid(principal, **kw) response.headerlist.extend(headers) self.add_response_callback(callback) @@ -449,10 +449,10 @@ class AuthenticationAPIMixin(object): .. versionadded:: 1.5 """ - headers = self._forget_userid() def callback(req, response): exc = getattr(req, 'exception', None) if exc is None or on_exception: + headers = self._forget_userid() response.headerlist.extend(headers) self.add_response_callback(callback) -- cgit v1.2.3 From 2478de31b2e6d8d7667b9dd0c81f571130f3daf6 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 28 Oct 2013 17:12:54 -0400 Subject: avoid a deprecation warning during test runs --- pyramid/config/predicates.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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): -- cgit v1.2.3 From 5431fdc645019c0b5eb0a60f41cd77aa3457ae07 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 28 Oct 2013 17:19:35 -0400 Subject: add NB notes about recursive add_response_callback policies, use req instead of self for normalization with exception getting --- pyramid/security.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pyramid/security.py b/pyramid/security.py index afca8cd9a..0d10b3998 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -413,7 +413,11 @@ class AuthenticationAPIMixin(object): # instructed exc = getattr(req, 'exception', None) if exc is None or on_exception: - headers = self._remember_userid(principal, **kw) + # NB: this call to _remember_userid should be exactly here + # because some policies actually add another response callback + # when their remember method is called, and we dont want them + # to do that if there's an exception in the default case. + headers = req._remember_userid(principal, **kw) response.headerlist.extend(headers) self.add_response_callback(callback) @@ -452,7 +456,11 @@ class AuthenticationAPIMixin(object): def callback(req, response): exc = getattr(req, 'exception', None) if exc is None or on_exception: - headers = self._forget_userid() + # NB: this call to _forget_userid should be exactly here + # because some policies actually add another response callback + # when their forget method is called, and we dont want them + # to do that if there's an exception in the default case. + headers = req._forget_userid() response.headerlist.extend(headers) self.add_response_callback(callback) -- cgit v1.2.3 From 0921db8b34c6b1967f249d42dee6b652a3b987ef Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 28 Oct 2013 17:24:30 -0400 Subject: wording and specify return value --- pyramid/security.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/pyramid/security.py b/pyramid/security.py index 0d10b3998..b0fd38678 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -380,18 +380,19 @@ class AuthenticationAPIMixin(object): def remember_userid(self, principal, on_exception=False, **kw): """ Sets 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):: + 'foo=abc')]``) on the response eventually returned using a response + callback. These headers are used 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:: .. code-block:: python request.remember_userid('chrism', password='123', max_age='86400') + This method always returns ``None``; it is called only for its side + effects. + If no :term:`authentication policy` is in use, this function will do nothing. If used, the composition and meaning of ``**kw`` must be agreed upon by the calling code and @@ -431,18 +432,15 @@ class AuthenticationAPIMixin(object): def forget_userid(self, on_exception=False): """ Sets a sequence of header tuples (e.g. ``[('Set-Cookie', 'foo=abc')]``) suitable for 'forgetting' the set of credentials - possessed by the currently authenticated user on the response. - 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):: - - .. code-block:: python - - request.forget_userid() + possessed by the currently authenticated user on the response based on + the current :term:`authentication policy`, using a response callback. If no :term:`authentication policy` is in use, this function will be a noop. + This method always returns ``None``; it is called only for its side + effects. + One special keyword value is understood by this method: ``on_exception``. Usually if an exception occurs within the same request after this method is called, the headers provided by the -- cgit v1.2.3 From 696e0e3bd257fdace57adbb4c3d331af377d9e5b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Oct 2013 19:47:52 -0400 Subject: fix zodb tutorial wrt request-based authentication and authorization apis --- docs/tutorials/wiki/authorization.rst | 34 +++++++--------------- .../wiki/src/authorization/tutorial/views.py | 22 +++++--------- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 2bd8c1f1c..bba303d7f 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" @@ -197,15 +197,15 @@ Add the following import statements to the head of ``tutorial/tutorial/views.py``: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 6-13,15-17 + :lines: 6-11 :linenos: - :emphasize-lines: 3,6-9,11 + :emphasize-lines: 3,6 :language: python (Only the highlighted lines, with other necessary modifications, need to be added.) -:meth:`~pyramid.view.forbidden_view_config` will be used +:func:`~pyramid.view.forbidden_view_config` will be used to customize the default 403 Forbidden page. :meth:`~pyramid.request.Request.remember_userid` and :meth:`~pyramid.request.Request.forget_userid` help to create and @@ -214,7 +214,7 @@ expire an auth ticket cookie. Now add the ``login`` and ``logout`` views: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 82-120 + :lines: 76-102 :linenos: :language: python @@ -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,12 +262,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 and a trailing comma on the preceding line need to be added.) -:meth:`~pyramid.security.authenticated_userid()` will return ``None`` +:attr:`~pyramid.request.Request.authenticated_userid` will return ``None`` if the user is not authenticated, or a user id if the user is authenticated. @@ -329,7 +317,7 @@ when we're done: .. literalinclude:: src/authorization/tutorial/views.py :linenos: - :emphasize-lines: 8,11-15,17,24,29,48,52,68,72,80,82-120 + :emphasize-lines: 8,11,18,23,42,46,62,66,74,80,76-107 :language: python (Only the highlighted lines need to be added.) diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index 77956b1e3..57529ac8d 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -8,12 +8,6 @@ from pyramid.view import ( forbidden_view_config, ) -from pyramid.security import ( - remember, - forget, - authenticated_userid, - ) - from .security import USERS from .models import Page @@ -45,7 +39,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 +59,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 +71,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') @@ -95,9 +89,8 @@ def login(request): login = request.params['login'] password = request.params['password'] if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) + request.remember_userid(login) + return HTTPFound(location=came_from) message = 'Failed login' return dict( @@ -110,6 +103,5 @@ def login(request): @view_config(context='.models.Wiki', name='logout') def logout(request): - headers = forget(request) - return HTTPFound(location = request.resource_url(request.context), - headers = headers) + request.forget_userid() + return HTTPFound(location=request.resource_url(request.context)) -- cgit v1.2.3 From f436d7f5cd19e94378737096d9d21635b157fc46 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Oct 2013 20:03:49 -0400 Subject: copy forward views.py changes to tests step --- docs/tutorials/wiki/src/tests/tutorial/views.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py index 77956b1e3..57529ac8d 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki/src/tests/tutorial/views.py @@ -8,12 +8,6 @@ from pyramid.view import ( forbidden_view_config, ) -from pyramid.security import ( - remember, - forget, - authenticated_userid, - ) - from .security import USERS from .models import Page @@ -45,7 +39,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 +59,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 +71,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') @@ -95,9 +89,8 @@ def login(request): login = request.params['login'] password = request.params['password'] if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) + request.remember_userid(login) + return HTTPFound(location=came_from) message = 'Failed login' return dict( @@ -110,6 +103,5 @@ def login(request): @view_config(context='.models.Wiki', name='logout') def logout(request): - headers = forget(request) - return HTTPFound(location = request.resource_url(request.context), - headers = headers) + request.forget_userid() + return HTTPFound(location=request.resource_url(request.context)) -- cgit v1.2.3 From 3657ba974660677050fe4a62441c2073bd71203c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Oct 2013 20:08:58 -0400 Subject: fix wiki2 tutorial wrt request-method security APIs --- docs/tutorials/wiki2/authorization.rst | 27 +++++++--------------- .../wiki2/src/authorization/tutorial/views.py | 22 ++++++------------ docs/tutorials/wiki2/src/tests/tutorial/views.py | 22 ++++++------------ 3 files changed, 22 insertions(+), 49 deletions(-) diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 2b4263610..830cb0277 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -221,14 +221,14 @@ 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-12,19 :linenos: - :emphasize-lines: 3,6-9,11 + :emphasize-lines: 3,5 :language: python (Only the highlighted lines need to be added.) -:meth:`~pyramid.view.forbidden_view_config` will be used +:func:`~pyramid.view.forbidden_view_config` will be used to customize the default 403 Forbidden page. :meth:`~pyramid.request.Request.remember_userid` and :meth:`~pyramid.request.Request.forget_userid` help to create and @@ -237,7 +237,7 @@ expire an auth ticket cookie. Now add the ``login`` and ``logout`` views: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 91-123 + :lines: 85-115 :linenos: :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 :attr:`~pyramid.request.Request.authenticated_userid` property will return +``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,19,25,31,52,55,67,70,82,85-115 :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..110d738c2 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -11,12 +11,6 @@ from pyramid.view import ( forbidden_view_config, ) -from pyramid.security import ( - remember, - forget, - authenticated_userid, - ) - from .models import ( DBSession, Page, @@ -55,7 +49,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 +64,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') @@ -85,7 +79,7 @@ def edit_page(request): return dict( page=page, save_url = request.route_url('edit_page', pagename=pagename), - logged_in=authenticated_userid(request), + logged_in=request.authenticated_userid, ) @view_config(route_name='login', renderer='templates/login.pt') @@ -103,9 +97,8 @@ def login(request): login = request.params['login'] password = request.params['password'] if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) + request.remember_userid(login) + return HTTPFound(location = came_from) message = 'Failed login' return dict( @@ -118,7 +111,6 @@ def login(request): @view_config(route_name='logout') def logout(request): - headers = forget(request) - return HTTPFound(location = request.route_url('view_wiki'), - headers = headers) + request.forget_userid() + return HTTPFound(location = request.route_url('view_wiki')) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py index b6dbbf5f6..110d738c2 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views.py @@ -11,12 +11,6 @@ from pyramid.view import ( forbidden_view_config, ) -from pyramid.security import ( - remember, - forget, - authenticated_userid, - ) - from .models import ( DBSession, Page, @@ -55,7 +49,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 +64,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') @@ -85,7 +79,7 @@ def edit_page(request): return dict( page=page, save_url = request.route_url('edit_page', pagename=pagename), - logged_in=authenticated_userid(request), + logged_in=request.authenticated_userid, ) @view_config(route_name='login', renderer='templates/login.pt') @@ -103,9 +97,8 @@ def login(request): login = request.params['login'] password = request.params['password'] if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) + request.remember_userid(login) + return HTTPFound(location = came_from) message = 'Failed login' return dict( @@ -118,7 +111,6 @@ def login(request): @view_config(route_name='logout') def logout(request): - headers = forget(request) - return HTTPFound(location = request.route_url('view_wiki'), - headers = headers) + request.forget_userid() + return HTTPFound(location = request.route_url('view_wiki')) -- cgit v1.2.3 From 2bdafd4b1abf983d6c0d4c504b58d74d05077523 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Oct 2013 20:10:22 -0400 Subject: rendering --- pyramid/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/security.py b/pyramid/security.py index b0fd38678..f86b7e43a 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -384,7 +384,7 @@ class AuthenticationAPIMixin(object): callback. These headers are used 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:: + look like so within the body of a view function: .. code-block:: python -- cgit v1.2.3 From 63f7ae00096e1121504d43ddcbefbd1e5293a985 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Oct 2013 20:13:15 -0400 Subject: wording --- pyramid/security.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pyramid/security.py b/pyramid/security.py index f86b7e43a..f7750cbef 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -379,12 +379,13 @@ class AuthenticationAPIMixin(object): return policy.remember(self, principal, **kw) def remember_userid(self, principal, on_exception=False, **kw): - """ Sets a sequence of header tuples (e.g. ``[('Set-Cookie', - 'foo=abc')]``) on the response eventually returned using a response - callback. These headers are used 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: + """ Using a response callback, sets authentication headers on the + response eventually returned by the view executed by this request + suitable for loggin a user in. These headers are used 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: .. code-block:: python @@ -430,10 +431,9 @@ class AuthenticationAPIMixin(object): return policy.forget(self) def forget_userid(self, on_exception=False): - """ Sets a sequence of header tuples (e.g. ``[('Set-Cookie', - 'foo=abc')]``) suitable for 'forgetting' the set of credentials - possessed by the currently authenticated user on the response based on - the current :term:`authentication policy`, using a response callback. + """ Using a response callback, sets authentication headers suitable for + logging a user out on the response returned by the view executed during + this request based on the current :term:`authentication policy`. If no :term:`authentication policy` is in use, this function will be a noop. -- cgit v1.2.3 From e1838557e6721b5b42f1267b134b626099703c2c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Oct 2013 20:14:52 -0400 Subject: not methods, attrs --- docs/api/request.rst | 4 ++-- pyramid/security.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/api/request.rst b/docs/api/request.rst index 3d1fe020c..661cdfc91 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -171,7 +171,7 @@ 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 - :meth:`~pyramid.request.Request.unauthenticated_userid`, because the + :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``. @@ -184,7 +184,7 @@ 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 - :meth:`~pyramid.request.Request.authenticated_userid`, because the + :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 diff --git a/pyramid/security.py b/pyramid/security.py index f7750cbef..a5d9c52eb 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -345,9 +345,9 @@ class AuthenticationAPIMixin(object): 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. + :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 """ -- cgit v1.2.3 From a91c19837f5ce579ce2a5bf68ddee30cfaebe034 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Oct 2013 20:19:23 -0400 Subject: note deprecation --- docs/narr/threadlocals.rst | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) 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 -- cgit v1.2.3 From 675e0d4cf01840740490c03a2e3704b0b7d98de3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Oct 2013 20:24:34 -0400 Subject: convert remember/forget to request-method-based --- docs/quick_tutorial/authentication/tutorial/views.py | 17 +++++------------ docs/quick_tutorial/authorization/tutorial/views.py | 17 +++++------------ 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/docs/quick_tutorial/authentication/tutorial/views.py b/docs/quick_tutorial/authentication/tutorial/views.py index 3038b6d9b..240a23d3e 100644 --- a/docs/quick_tutorial/authentication/tutorial/views.py +++ b/docs/quick_tutorial/authentication/tutorial/views.py @@ -1,9 +1,4 @@ from pyramid.httpexceptions import HTTPFound -from pyramid.security import ( - remember, - forget, - authenticated_userid - ) from pyramid.view import ( view_config, view_defaults @@ -16,7 +11,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): @@ -41,9 +36,8 @@ class TutorialViews: login = request.params['login'] password = request.params['password'] if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location=came_from, - headers=headers) + request.remember_userid(login) + return HTTPFound(location=came_from) message = 'Failed login' return dict( @@ -58,7 +52,6 @@ class TutorialViews: @view_config(route_name='logout') def logout(self): request = self.request - headers = forget(request) + request.forget_userid() url = request.route_url('home') - return HTTPFound(location=url, - headers=headers) + return HTTPFound(location=url) diff --git a/docs/quick_tutorial/authorization/tutorial/views.py b/docs/quick_tutorial/authorization/tutorial/views.py index 92c1946ba..2ce2c37b4 100644 --- a/docs/quick_tutorial/authorization/tutorial/views.py +++ b/docs/quick_tutorial/authorization/tutorial/views.py @@ -1,9 +1,4 @@ from pyramid.httpexceptions import HTTPFound -from pyramid.security import ( - remember, - forget, - authenticated_userid - ) from pyramid.view import ( view_config, view_defaults, @@ -17,7 +12,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): @@ -43,9 +38,8 @@ class TutorialViews: login = request.params['login'] password = request.params['password'] if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location=came_from, - headers=headers) + request.remember_userid(login) + return HTTPFound(location=came_from) message = 'Failed login' return dict( @@ -60,7 +54,6 @@ class TutorialViews: @view_config(route_name='logout') def logout(self): request = self.request - headers = forget(request) + request.forget_userid() url = request.route_url('home') - return HTTPFound(location=url, - headers=headers) + return HTTPFound(location=url) -- cgit v1.2.3 From 3bd1fa5dd792d639615e5125b73caef8c65a0a30 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Oct 2013 20:28:53 -0400 Subject: new api --- docs/narr/testing.rst | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index 3f5d5ae6c..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 -: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`? +: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. -- cgit v1.2.3 From 2885a7b96545c037109d7999319f74869a640050 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Oct 2013 20:40:12 -0400 Subject: fix failing test (unrelated to security stuff) --- docs/tutorials/wiki2/src/tests/tutorial/tests.py | 21 --------------------- 1 file changed, 21 deletions(-) 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() -- cgit v1.2.3 From 9a1e1ec235dd187e6aae675873688ab9b086b4ef Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Oct 2013 20:53:37 -0400 Subject: rendering --- pyramid/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/security.py b/pyramid/security.py index a5d9c52eb..7ffd37b8c 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -466,7 +466,7 @@ 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 + 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`. -- cgit v1.2.3 From e0d1af934d85a34a83e2026976ddd05d92d21b5e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Oct 2013 20:55:55 -0400 Subject: indicate default --- pyramid/security.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pyramid/security.py b/pyramid/security.py index 7ffd37b8c..d754aeadf 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -465,15 +465,16 @@ class AuthenticationAPIMixin(object): 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. + """ 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 -- cgit v1.2.3 From c126033112e468cdf858c7c1ad0bb29e7f57f520 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Oct 2013 20:57:24 -0400 Subject: indicate default --- pyramid/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/security.py b/pyramid/security.py index d754aeadf..7a65d22ce 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -478,7 +478,7 @@ class AuthorizationAPIMixin(object): :param permission: Does this request have the given permission? :type permission: unicode, str - :param context: Typically a resource of a regsitered type. + :param context: A resource object or ``None`` :type context: object :returns: `pyramid.security.PermitsResult` -- cgit v1.2.3 From 0dcd56c2c30863c6683c0cf442aa73dfdcd11b13 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 9 Nov 2013 17:11:16 -0500 Subject: undeprecate remember/forget functions and remove remember_userid/forget_userid methods from request --- CHANGES.txt | 14 +- docs/api/request.rst | 7 +- .../authentication/tutorial/views.py | 15 +- .../quick_tutorial/authorization/tutorial/views.py | 15 +- docs/tutorials/wiki/authorization.rst | 19 +- .../wiki/src/authorization/tutorial/views.py | 16 +- docs/tutorials/wiki/src/tests/tutorial/views.py | 16 +- docs/tutorials/wiki2/authorization.rst | 16 +- .../wiki2/src/authorization/tutorial/views.py | 22 ++- docs/tutorials/wiki2/src/tests/tutorial/views.py | 23 ++- pyramid/security.py | 139 ++----------- pyramid/tests/test_config/test_testing.py | 32 +-- pyramid/tests/test_security.py | 214 ++++++--------------- 13 files changed, 183 insertions(+), 365 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 20b7726c4..9f780fe45 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,23 +4,13 @@ Unreleased Features -------- -- Authentication and authorization APIs have been added as as methods of the - request: ``request.has_permission``, ``request.forget_userid``, and - ``request.remember_userid``. +- 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. - ``request.forget_userid`` and ``request.remember_userid`` are method-based - alternatives to ``pyramid.security.forget`` and - ``pyramid.security.remember``. These do not work exacly the same as their - function counterparts, however. These methods automatically set the headers - returned by the authentication policy on the response, whereas the older - function-based APIs returned a sequence of headers and required the caller to - set those headers. The older function-based API still works but 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``. diff --git a/docs/api/request.rst b/docs/api/request.rst index 661cdfc91..b7604020e 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -13,8 +13,7 @@ current_route_path, static_url, static_path, model_url, resource_url, set_property, effective_principals, authenticated_userid, - unauthenticated_userid, has_permission, forget_userid, - remember_userid + unauthenticated_userid, has_permission .. attribute:: context @@ -254,10 +253,6 @@ request provided by e.g. the ``pshell`` environment. For more information, see :ref:`subrequest_chapter`. - .. automethod:: remember_userid - - .. automethod:: forget_userid - .. automethod:: has_permission .. automethod:: add_response_callback diff --git a/docs/quick_tutorial/authentication/tutorial/views.py b/docs/quick_tutorial/authentication/tutorial/views.py index 240a23d3e..ab46eb2dd 100644 --- a/docs/quick_tutorial/authentication/tutorial/views.py +++ b/docs/quick_tutorial/authentication/tutorial/views.py @@ -1,4 +1,9 @@ from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) + from pyramid.view import ( view_config, view_defaults @@ -36,8 +41,9 @@ class TutorialViews: login = request.params['login'] password = request.params['password'] if USERS.get(login) == password: - request.remember_userid(login) - return HTTPFound(location=came_from) + headers = remember(request, login) + return HTTPFound(location=came_from, + headers=headers) message = 'Failed login' return dict( @@ -52,6 +58,7 @@ class TutorialViews: @view_config(route_name='logout') def logout(self): request = self.request - request.forget_userid() + headers = forget(request) url = request.route_url('home') - return HTTPFound(location=url) + return HTTPFound(location=url, + headers=headers) diff --git a/docs/quick_tutorial/authorization/tutorial/views.py b/docs/quick_tutorial/authorization/tutorial/views.py index 2ce2c37b4..43d14455a 100644 --- a/docs/quick_tutorial/authorization/tutorial/views.py +++ b/docs/quick_tutorial/authorization/tutorial/views.py @@ -1,4 +1,9 @@ from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) + from pyramid.view import ( view_config, view_defaults, @@ -38,8 +43,9 @@ class TutorialViews: login = request.params['login'] password = request.params['password'] if USERS.get(login) == password: - request.remember_userid(login) - return HTTPFound(location=came_from) + headers = remember(request, login) + return HTTPFound(location=came_from, + headers=headers) message = 'Failed login' return dict( @@ -54,6 +60,7 @@ class TutorialViews: @view_config(route_name='logout') def logout(self): request = self.request - request.forget_userid() + headers = forget(request) url = request.route_url('home') - return HTTPFound(location=url) + return HTTPFound(location=url, + headers=headers) diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index bba303d7f..62b1164e3 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -197,24 +197,24 @@ Add the following import statements to the head of ``tutorial/tutorial/views.py``: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 6-11 + :lines: 6-13,15-17 :linenos: - :emphasize-lines: 3,6 + :emphasize-lines: 3,6-9,11 :language: python (Only the highlighted lines, with other necessary modifications, need to be added.) -:func:`~pyramid.view.forbidden_view_config` will be used +:meth:`~pyramid.view.forbidden_view_config` will be used to customize the default 403 Forbidden page. -:meth:`~pyramid.request.Request.remember_userid` and -:meth:`~pyramid.request.Request.forget_userid` help to create and +:meth:`~pyramid.security.remember` and +:meth:`~pyramid.security.forget` help to create and expire an auth ticket cookie. Now add the ``login`` and ``logout`` views: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 76-102 + :lines: 82-120 :linenos: :language: python @@ -267,9 +267,8 @@ like this: (Only the highlighted line and a trailing comma on the preceding line need to be added.) -:attr:`~pyramid.request.Request.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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -317,7 +316,7 @@ when we're done: .. literalinclude:: src/authorization/tutorial/views.py :linenos: - :emphasize-lines: 8,11,18,23,42,46,62,66,74,80,76-107 + :emphasize-lines: 8,11-15,17,24,29,48,52,68,72,80,82-120 :language: python (Only the highlighted lines need to be added.) diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index 57529ac8d..62e96e0e7 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -8,6 +8,12 @@ from pyramid.view import ( forbidden_view_config, ) +from pyramid.security import ( + remember, + forget, + ) + + from .security import USERS from .models import Page @@ -89,8 +95,9 @@ def login(request): login = request.params['login'] password = request.params['password'] if USERS.get(login) == password: - request.remember_userid(login) - return HTTPFound(location=came_from) + headers = remember(request, login) + return HTTPFound(location = came_from, + headers = headers) message = 'Failed login' return dict( @@ -103,5 +110,6 @@ def login(request): @view_config(context='.models.Wiki', name='logout') def logout(request): - request.forget_userid() - return HTTPFound(location=request.resource_url(request.context)) + headers = forget(request) + return HTTPFound(location = request.resource_url(request.context), + headers = headers) diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py index 57529ac8d..62e96e0e7 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki/src/tests/tutorial/views.py @@ -8,6 +8,12 @@ from pyramid.view import ( forbidden_view_config, ) +from pyramid.security import ( + remember, + forget, + ) + + from .security import USERS from .models import Page @@ -89,8 +95,9 @@ def login(request): login = request.params['login'] password = request.params['password'] if USERS.get(login) == password: - request.remember_userid(login) - return HTTPFound(location=came_from) + headers = remember(request, login) + return HTTPFound(location = came_from, + headers = headers) message = 'Failed login' return dict( @@ -103,5 +110,6 @@ def login(request): @view_config(context='.models.Wiki', name='logout') def logout(request): - request.forget_userid() - return HTTPFound(location=request.resource_url(request.context)) + headers = forget(request) + return HTTPFound(location = request.resource_url(request.context), + headers = headers) diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 830cb0277..1e5d0dcbf 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -221,23 +221,23 @@ Add the following import statements to the head of ``tutorial/tutorial/views.py``: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 9-12,19 + :lines: 9-19 :linenos: - :emphasize-lines: 3,5 + :emphasize-lines: 3,6-9,11 :language: python (Only the highlighted lines need to be added.) -:func:`~pyramid.view.forbidden_view_config` will be used +:meth:`~pyramid.view.forbidden_view_config` will be used to customize the default 403 Forbidden page. -:meth:`~pyramid.request.Request.remember_userid` and -:meth:`~pyramid.request.Request.forget_userid` help to create and +:meth:`~pyramid.security.remember` and +:meth:`~pyramid.security.forget` help to create and expire an auth ticket cookie. Now add the ``login`` and ``logout`` views: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 85-115 + :lines: 91-123 :linenos: :language: python @@ -289,7 +289,7 @@ like this: (Only the highlighted line needs to be added.) -The :attr:`~pyramid.request.Request.authenticated_userid` property will return +The :meth:`~pyramid.request.Request.authenticated_userid` property will be ``None`` if the user is not authenticated. Add a "Logout" link when logged in @@ -338,7 +338,7 @@ when we're done: .. literalinclude:: src/authorization/tutorial/views.py :linenos: - :emphasize-lines: 11,19,25,31,52,55,67,70,82,85-115 + :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 110d738c2..e954d5a31 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -11,12 +11,18 @@ from pyramid.view import ( forbidden_view_config, ) +from pyramid.security import ( + remember, + forget, + ) + +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+)") @@ -78,8 +84,8 @@ def edit_page(request): pagename=pagename)) return dict( page=page, - save_url = request.route_url('edit_page', pagename=pagename), - logged_in=request.authenticated_userid, + save_url=request.route_url('edit_page', pagename=pagename), + logged_in=request.authenticated_userid ) @view_config(route_name='login', renderer='templates/login.pt') @@ -97,8 +103,9 @@ def login(request): login = request.params['login'] password = request.params['password'] if USERS.get(login) == password: - request.remember_userid(login) - return HTTPFound(location = came_from) + headers = remember(request, login) + return HTTPFound(location = came_from, + headers = headers) message = 'Failed login' return dict( @@ -111,6 +118,7 @@ def login(request): @view_config(route_name='logout') def logout(request): - request.forget_userid() - return HTTPFound(location = request.route_url('view_wiki')) + headers = forget(request) + return HTTPFound(location = request.route_url('view_wiki'), + headers = headers) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py index 110d738c2..41bea4785 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views.py @@ -11,12 +11,18 @@ from pyramid.view import ( forbidden_view_config, ) +from pyramid.security import ( + remember, + forget, + ) + +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+)") @@ -78,8 +84,8 @@ def edit_page(request): pagename=pagename)) return dict( page=page, - save_url = request.route_url('edit_page', pagename=pagename), - logged_in=request.authenticated_userid, + save_url=request.route_url('edit_page', pagename=pagename), + logged_in=request.authenticated_userid ) @view_config(route_name='login', renderer='templates/login.pt') @@ -97,8 +103,9 @@ def login(request): login = request.params['login'] password = request.params['password'] if USERS.get(login) == password: - request.remember_userid(login) - return HTTPFound(location = came_from) + headers = remember(request, login) + return HTTPFound(location = came_from, + headers = headers) message = 'Failed login' return dict( @@ -111,6 +118,6 @@ def login(request): @view_config(route_name='logout') def logout(request): - request.forget_userid() - return HTTPFound(location = request.route_url('view_wiki')) - + headers = forget(request) + return HTTPFound(location = request.route_url('view_wiki'), + headers = headers) diff --git a/pyramid/security.py b/pyramid/security.py index 7a65d22ce..58fa9332a 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -38,6 +38,10 @@ def _get_registry(request): reg = get_current_registry() # b/c return reg +def _get_authentication_policy(request): + registry = _get_registry(request) + return registry.queryUtility(IAuthenticationPolicy) + def has_permission(permission, context, request): """ A function that calls @@ -113,8 +117,8 @@ deprecated( def remember(request, principal, **kw): """ - Returns a sequence of header tuples (e.g. ``[('Set-Cookie', - 'foo=abc')]``) on this request's response. + 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 @@ -126,6 +130,7 @@ def remember(request, principal, **kw): from pyramid.security import remember headers = remember(request, 'chrism', password='123', max_age='86400') + response = request.response response.headerlist.extend(headers) return response @@ -133,23 +138,11 @@ def remember(request, principal, **kw): 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. - - .. deprecated:: 1.5 - Use :meth:`pyramid.request.Request.remember_userid` instead. - but be sure to read its docs first; the remember_userid method is not an - exact analog of the remember function, because it sets headers instead - of returning them. """ - return request._remember_userid(principal, **kw) - -deprecated( - 'remember', - 'As of Pyramid 1.5 the "pyramid.security.remember" API is ' - 'now deprecated. It will be removed in Pyramd 1.8. Use the ' - '"remember_userid" method of the Pyramid request instead, but be sure to ' - 'read the docs first; the remember_userid method is not an exact analog of ' - 'the remember function, because it sets headers instead of returning them.' - ) + policy = _get_authentication_policy(request) + if policy is None: + return [] + return policy.remember(request, principal, **kw) def forget(request): """ @@ -169,21 +162,12 @@ def forget(request): always return an empty sequence. .. deprecated:: 1.5 - Use :meth:`pyramid.request.Request.forget_userid` instead. - but be sure to read its docs first; the forget_userid method is not an - exact analog of the forget function, because it sets headers instead - of returning them. + Use :meth:`pyramid.request.Request.get_logout_headers` instead. """ - return request._forget_userid() - -deprecated( - 'forget', - 'As of Pyramid 1.5 the "pyramid.security.forget" API is ' - 'now deprecated. It will be removed in Pyramd 1.8. Use the ' - '"forget_user" method of the Pyramid request instead, but be sure to ' - 'read the docs first; the forget_userid method is not an exact analog of ' - 'the forget function, because it sets headers instead of returning them.' - ) + policy = _get_authentication_policy(request) + if policy is None: + return [] + return policy.forget(request) def principals_allowed_by_permission(context, permission): """ Provided a ``context`` (a resource object), and a ``permission`` @@ -371,97 +355,6 @@ class AuthenticationAPIMixin(object): return [Everyone] return policy.effective_principals(self) - # b/c - def _remember_userid(self, principal, **kw): - policy = self._get_authentication_policy() - if policy is None: - return [] - return policy.remember(self, principal, **kw) - - def remember_userid(self, principal, on_exception=False, **kw): - """ Using a response callback, sets authentication headers on the - response eventually returned by the view executed by this request - suitable for loggin a user in. These headers are used 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: - - .. code-block:: python - - request.remember_userid('chrism', password='123', max_age='86400') - - This method always returns ``None``; it is called only for its side - effects. - - If no :term:`authentication policy` is in use, this function will - do nothing. If used, the composition and - meaning of ``**kw`` must be agreed upon by the calling code and - the effective authentication policy. - - One special keyword value is understood by this method: - ``on_exception``. Usually if an exception occurs within the same - request after this method is called, the headers provided by the - authentication policy will not be set on the response. If - ``on_exception`` is passed, and as ``True``, then the headers will be - set on the response even if an exception is later raised. By default - this value is ``False``. - - .. versionadded:: 1.5 - - """ - def callback(req, response): - # do not set the headers on an exception unless explicitly - # instructed - exc = getattr(req, 'exception', None) - if exc is None or on_exception: - # NB: this call to _remember_userid should be exactly here - # because some policies actually add another response callback - # when their remember method is called, and we dont want them - # to do that if there's an exception in the default case. - headers = req._remember_userid(principal, **kw) - response.headerlist.extend(headers) - self.add_response_callback(callback) - - # b/c - def _forget_userid(self): - policy = self._get_authentication_policy() - if policy is None: - return [] - return policy.forget(self) - - def forget_userid(self, on_exception=False): - """ Using a response callback, sets authentication headers suitable for - logging a user out on the response returned by the view executed during - this request based on the current :term:`authentication policy`. - - If no :term:`authentication policy` is in use, this function will - be a noop. - - This method always returns ``None``; it is called only for its side - effects. - - One special keyword value is understood by this method: - ``on_exception``. Usually if an exception occurs within the same - request after this method is called, the headers provided by the - authentication policy will not be set on the response. If - ``on_exception`` is passed, and as ``True``, then the headers will be - set on the response even if an exception is later raised. By default - this value is ``False``. - - .. versionadded:: 1.5 - """ - def callback(req, response): - exc = getattr(req, 'exception', None) - if exc is None or on_exception: - # NB: this call to _forget_userid should be exactly here - # because some policies actually add another response callback - # when their forget method is called, and we dont want them - # to do that if there's an exception in the default case. - headers = req._forget_userid() - response.headerlist.extend(headers) - self.add_response_callback(callback) - class AuthorizationAPIMixin(object): def has_permission(self, permission, context=None): diff --git a/pyramid/tests/test_config/test_testing.py b/pyramid/tests/test_config/test_testing.py index d13cb9285..05561bfe9 100644 --- a/pyramid/tests/test_config/test_testing.py +++ b/pyramid/tests/test_config/test_testing.py @@ -25,31 +25,28 @@ class TestingConfiguratorMixinTests(unittest.TestCase): self.assertEqual(ut.permissive, False) def test_testing_securitypolicy_remember_result(self): + from pyramid.security import remember config = self._makeOne(autocommit=True) pol = config.testing_securitypolicy( 'user', ('group1', 'group2'), - permissive=False, - remember_result=[('X-Pyramid-Test', True)]) + permissive=False, remember_result=True) request = DummyRequest() request.registry = config.registry - request.remember_userid('fred') + val = remember(request, 'fred') self.assertEqual(pol.remembered, 'fred') - val = dict(request.response.headerlist).get('X-Pyramid-Test') self.assertEqual(val, True) def test_testing_securitypolicy_forget_result(self): + from pyramid.security import forget config = self._makeOne(autocommit=True) pol = config.testing_securitypolicy( 'user', ('group1', 'group2'), - permissive=False, - forget_result=[('X-Pyramid-Test', True)]) + permissive=False, forget_result=True) request = DummyRequest() request.registry = config.registry - request.response = DummyResponse() - request.forget_userid() + val = forget(request) self.assertEqual(pol.forgotten, True) - val = dict(request.response.headerlist).get('X-Pyramid-Test') - self.assertTrue(val) + self.assertEqual(val, True) def test_testing_resources(self): from pyramid.traversal import find_resource @@ -200,24 +197,9 @@ from zope.interface import implementer class DummyEvent: pass -class DummyResponse(object): - def __init__(self): - self.headers = [] - - @property - def headerlist(self): - return self.headers - class DummyRequest(AuthenticationAPIMixin, AuthorizationAPIMixin): - subpath = () - matchdict = None def __init__(self, environ=None): if environ is None: environ = {} self.environ = environ - self.params = {} - self.cookies = {} - self.response = DummyResponse() - def add_response_callback(self, callback): - callback(self, self.response) diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py index 96f171324..6f08a100c 100644 --- a/pyramid/tests/test_security.py +++ b/pyramid/tests/test_security.py @@ -127,6 +127,69 @@ class TestPrincipalsAllowedByPermission(unittest.TestCase): 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): + 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() @@ -312,149 +375,6 @@ class TestEffectivePrincipals(unittest.TestCase): _registerAuthenticationPolicy(registry, 'yo') self.assertEqual(request.effective_principals, 'yo') -class TestRememberUserId(unittest.TestCase): - principal = 'the4th' - - def setUp(self): - testing.setUp() - - def tearDown(self): - testing.tearDown() - - def assert_response_headers(self, request, expected_headers): - request._process_response_callbacks(request.response) - headers = request.response.headerlist - self.assertEqual(list(expected_headers), list(headers)) - - def test_backward_compat_delegates_to_mixin(self): - from zope.deprecation import __show__ - try: - __show__.off() - request = _makeFakeRequest() - from pyramid.security import remember - self.assertEqual( - remember(request, 'matt'), - [('X-Pyramid-Test', 'remember_userid')] - ) - finally: - __show__.on() - - def test_with_no_authentication_policy(self): - request = _makeRequest() - headers_before = request.response.headerlist - request.remember_userid(self.principal) - self.assert_response_headers(request, headers_before) - - def test_with_authentication_policy(self): - request = _makeRequest() - headers_before = request.response.headerlist - expected_headers = headers_before[:] + [(_TEST_HEADER, self.principal)] - _registerAuthenticationPolicy(request.registry, self.principal) - request.remember_userid(self.principal) - self.assert_response_headers(request, expected_headers) - - 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, self.principal) - headers_before = request.response.headerlist - request.remember_userid(self.principal) - expected_headers = headers_before[:] + [(_TEST_HEADER, self.principal)] - self.assert_response_headers(request, expected_headers) - - def test_request_has_exception_attr_no_on_exception_flag(self): - request = _makeRequest() - headers_before = request.response.headerlist - _registerAuthenticationPolicy(request.registry, self.principal) - request.exception = True - request.remember_userid(self.principal) - self.assert_response_headers(request, headers_before) - - def test_request_has_exception_attr_with_on_exception_flag(self): - request = _makeRequest() - headers_before = request.response.headerlist - _registerAuthenticationPolicy(request.registry, self.principal) - request.exception = True - request.remember_userid(self.principal, on_exception=True) - expected_headers = headers_before[:] + [(_TEST_HEADER, self.principal)] - self.assert_response_headers(request, expected_headers) - -class TestForgetUserId(unittest.TestCase): - principal = 'me-not' - - def setUp(self): - testing.setUp() - - def tearDown(self): - testing.tearDown() - - def assert_response_headers(self, request, expected_headers): - request._process_response_callbacks(request.response) - headers = request.response.headerlist - self.assertEqual(list(expected_headers), list(headers)) - - def _makeOne(self): - request = _makeRequest() - request.response.headers.add(_TEST_HEADER, self.principal) - return request - - def test_backward_compat_delegates_to_mixin(self): - from zope.deprecation import __show__ - try: - __show__.off() - request = _makeFakeRequest() - from pyramid.security import forget - self.assertEqual( - forget(request), - [('X-Pyramid-Test', 'forget_userid')], - ) - finally: - __show__.on() - - def test_with_no_authentication_policy(self): - request = self._makeOne() - headers_before = request.response.headerlist - request.forget_userid() - self.assert_response_headers(request, headers_before) - - def test_with_authentication_policy(self): - request = self._makeOne() - headers_before = request.response.headerlist - expected_headers = headers_before[:] + [(_TEST_HEADER, 'forget_userid')] - _registerAuthenticationPolicy(request.registry, self.principal) - request.forget_userid() - self.assert_response_headers(request, expected_headers) - - def test_with_authentication_policy_no_reg_on_request(self): - from pyramid.threadlocal import get_current_registry - registry = get_current_registry() - request = self._makeOne() - del request.registry - _registerAuthenticationPolicy(registry, self.principal) - headers_before = request.response.headerlist - request.forget_userid() - expected_headers = headers_before[:] + [(_TEST_HEADER, 'forget_userid')] - self.assert_response_headers(request, expected_headers) - - def test_request_has_exception_attr_no_on_exception_flag(self): - request = self._makeOne() - headers_before = request.response.headerlist - _registerAuthenticationPolicy(request.registry, self.principal) - request.exception = True - request.forget_userid() - self.assert_response_headers(request, headers_before) - - def test_request_has_exception_attr_with_on_exception_flag(self): - request = self._makeOne() - headers_before = request.response.headerlist - _registerAuthenticationPolicy(request.registry, self.principal) - request.exception = True - request.forget_userid(on_exception=True) - expected_headers = headers_before[:] + [(_TEST_HEADER, 'forget_userid')] - self.assert_response_headers(request, expected_headers) - class TestHasPermission(unittest.TestCase): def setUp(self): testing.setUp() @@ -548,7 +468,7 @@ class DummyAuthenticationPolicy: return headers def forget(self, request): - headers = [(_TEST_HEADER, 'forget_userid')] + headers = [(_TEST_HEADER, 'logout')] self._header_forgotten = headers[0] return headers @@ -595,11 +515,5 @@ def _makeFakeRequest(): def effective_principals(req): return 'effective_principals' - def _forget_userid(req): - return [('X-Pyramid-Test', 'forget_userid')] - - def _remember_userid(req, principal, **kw): - return [('X-Pyramid-Test', 'remember_userid')] - return FakeRequest({}) -- cgit v1.2.3