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