summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst5
-rw-r--r--docs/narr/advanced-features.rst2
-rw-r--r--docs/narr/viewconfig.rst12
-rw-r--r--src/pyramid/config/routes.py13
-rw-r--r--src/pyramid/config/views.py13
-rw-r--r--src/pyramid/interfaces.py15
-rw-r--r--src/pyramid/predicates.py13
-rw-r--r--src/pyramid/security.py5
-rw-r--r--tests/test_config/test_predicates.py53
-rw-r--r--tests/test_config/test_routes.py12
-rw-r--r--tests/test_config/test_views.py40
-rw-r--r--tests/test_security.py23
12 files changed, 199 insertions, 7 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 6b6e1ebbb..753997bf4 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -22,10 +22,13 @@ Features
- ``pyramid.config.Configurator.set_security_policy``.
- ``pyramid.interfaces.ISecurityPolicy``
- ``pyramid.request.Request.authenticated_identity``.
+ - ``pyramid.request.Request.is_authenticated``
- ``pyramid.authentication.SessionAuthenticationHelper``
- ``pyramid.authorization.ACLHelper``
+ - ``is_authenticated=True/False`` predicate for route and view configs
- See https://github.com/Pylons/pyramid/pull/3465
+ See https://github.com/Pylons/pyramid/pull/3465 and
+ https://github.com/Pylons/pyramid/pull/3598
- Changed the default ``serializer`` on
``pyramid.session.SignedCookieSessionFactory`` to use
diff --git a/docs/narr/advanced-features.rst b/docs/narr/advanced-features.rst
index 6e819ff5b..6f3b4e11b 100644
--- a/docs/narr/advanced-features.rst
+++ b/docs/narr/advanced-features.rst
@@ -34,7 +34,7 @@ For our example above, you can do this instead:
.. code-block:: python
:linenos:
- @view_config(route_name="items", effective_principals=pyramid.authorization.Authenticated)
+ @view_config(route_name="items", is_authenticated=True)
def auth_view(request):
# do one thing
diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst
index b43ebb93e..de82fc075 100644
--- a/docs/narr/viewconfig.rst
+++ b/docs/narr/viewconfig.rst
@@ -494,6 +494,15 @@ configured view.
.. versionadded:: 1.4a3
+``is_authenticated``
+ This value, if specified, must be either ``True`` or ``False``.
+ If it is specified and ``True``, only a request from an authenticated user, as
+ determined by the :term:`security policy` in use, will satisfy the predicate.
+ If it is specified and ``False``, only a request from a user who is not
+ authenticated will satisfy the predicate.
+
+ .. versionadded:: 2.0
+
``effective_principals``
If specified, this value should be a :term:`principal` identifier or a
sequence of principal identifiers. If the
@@ -505,6 +514,9 @@ configured view.
.. versionadded:: 1.4a4
+ .. deprecated:: 2.0
+ Use ``is_authenticated`` or a custom predicate.
+
``custom_predicates``
If ``custom_predicates`` is specified, it must be a sequence of references to
custom predicate callables. Custom predicates can be combined with
diff --git a/src/pyramid/config/routes.py b/src/pyramid/config/routes.py
index a12e18fa8..4f3440c40 100644
--- a/src/pyramid/config/routes.py
+++ b/src/pyramid/config/routes.py
@@ -268,6 +268,17 @@ class RoutesConfiguratorMixin:
Removed support for media ranges.
+ is_authenticated
+
+ This value, if specified, must be either ``True`` or ``False``.
+ If it is specified and ``True``, only a request from an authenticated
+ user, as determined by the :term:`security policy` in use, will
+ satisfy the predicate.
+ If it is specified and ``False``, only a request from a user who is
+ not authenticated will satisfy the predicate.
+
+ .. versionadded:: 2.0
+
effective_principals
If specified, this value should be a :term:`principal` identifier or
@@ -282,6 +293,7 @@ class RoutesConfiguratorMixin:
.. versionadded:: 1.4a4
.. deprecated:: 2.0
+ Use ``is_authenticated`` or a custom predicate.
custom_predicates
@@ -537,6 +549,7 @@ class RoutesConfiguratorMixin:
('request_param', p.RequestParamPredicate),
('header', p.HeaderPredicate),
('accept', p.AcceptPredicate),
+ ('is_authenticated', p.IsAuthenticatedPredicate),
('effective_principals', p.EffectivePrincipalsPredicate),
('custom', p.CustomPredicate),
('traverse', p.TraversePredicate),
diff --git a/src/pyramid/config/views.py b/src/pyramid/config/views.py
index a064ebd05..26b69beb9 100644
--- a/src/pyramid/config/views.py
+++ b/src/pyramid/config/views.py
@@ -712,6 +712,17 @@ class ViewsConfiguratorMixin:
.. versionadded:: 1.4a3
+ is_authenticated
+
+ This value, if specified, must be either ``True`` or ``False``.
+ If it is specified and ``True``, only a request from an authenticated
+ user, as determined by the :term:`security policy` in use, will
+ satisfy the predicate.
+ If it is specified and ``False``, only a request from a user who is
+ not authenticated will satisfy the predicate.
+
+ .. versionadded:: 2.0
+
effective_principals
If specified, this value should be a :term:`principal` identifier or
@@ -726,6 +737,7 @@ class ViewsConfiguratorMixin:
.. versionadded:: 1.4a4
.. deprecated:: 2.0
+ Use ``is_authenticated`` or a custom predicate.
custom_predicates
@@ -1205,6 +1217,7 @@ class ViewsConfiguratorMixin:
('request_type', p.RequestTypePredicate),
('match_param', p.MatchParamPredicate),
('physical_path', p.PhysicalPathPredicate),
+ ('is_authenticated', p.IsAuthenticatedPredicate),
('effective_principals', p.EffectivePrincipalsPredicate),
('custom', p.CustomPredicate),
):
diff --git a/src/pyramid/interfaces.py b/src/pyramid/interfaces.py
index e92662f11..b8c8d06a9 100644
--- a/src/pyramid/interfaces.py
+++ b/src/pyramid/interfaces.py
@@ -113,6 +113,16 @@ class IResponse(Interface):
""" Return a new app_iter built from the response app_iter that
serves up only the given start:stop range. """
+ authenticated_identity = Attribute(
+ """An object representing the authenticated user, as determined by
+ the security policy in use, or ``None`` for unauthenticated requests.
+ The object's class and meaning is defined by the security policy."""
+ )
+
+ authenticated_userid = Attribute(
+ """A string to identify the authenticated user or ``None``."""
+ )
+
body = Attribute(
"""The body of the response, as a str. This will read in the entire
app_iter if necessary."""
@@ -233,6 +243,11 @@ class IResponse(Interface):
headers = Attribute(""" The headers in a dictionary-like object """)
+ is_authenticated = Attribute(
+ """A boolean indicating whether the request has an authenticated
+ user, as determined by the security policy in use."""
+ )
+
last_modified = Attribute(
""" Gets and sets and deletes the Last-Modified header. For more
information on Last-Modified see RFC 2616 section 14.29. Converts
diff --git a/src/pyramid/predicates.py b/src/pyramid/predicates.py
index 576bbbce6..fe8bc228c 100644
--- a/src/pyramid/predicates.py
+++ b/src/pyramid/predicates.py
@@ -276,6 +276,19 @@ class PhysicalPathPredicate:
return False
+class IsAuthenticatedPredicate:
+ def __init__(self, val, config):
+ self.val = val
+
+ def text(self):
+ return "is_authenticated = %r" % (self.val,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ return request.is_authenticated == self.val
+
+
class EffectivePrincipalsPredicate:
def __init__(self, val, config):
if is_nonstr_iter(val):
diff --git a/src/pyramid/security.py b/src/pyramid/security.py
index 58bc72116..2a1ef24bd 100644
--- a/src/pyramid/security.py
+++ b/src/pyramid/security.py
@@ -244,6 +244,11 @@ class SecurityAPIMixin:
return None
return policy.authenticated_userid(self)
+ @property
+ def is_authenticated(self):
+ """Return ``True`` if a user is authenticated for this request."""
+ return self.authenticated_identity is not None
+
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
diff --git a/tests/test_config/test_predicates.py b/tests/test_config/test_predicates.py
index 8017fc898..7e2f32786 100644
--- a/tests/test_config/test_predicates.py
+++ b/tests/test_config/test_predicates.py
@@ -19,6 +19,7 @@ class TestPredicateList(unittest.TestCase):
('containment', predicates.ContainmentPredicate),
('request_type', predicates.RequestTypePredicate),
('match_param', predicates.MatchParamPredicate),
+ ('is_authenticated', predicates.IsAuthenticatedPredicate),
('custom', predicates.CustomPredicate),
('traverse', predicates.TraversePredicate),
):
@@ -38,6 +39,19 @@ class TestPredicateList(unittest.TestCase):
def test_ordering_number_of_predicates(self):
from pyramid.config.predicates import predvalseq
+ order0, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ path_info='path_info',
+ request_param='param',
+ match_param='foo=bar',
+ header='header',
+ accept='accept',
+ is_authenticated=True,
+ containment='containment',
+ request_type='request_type',
+ custom=predvalseq([DummyCustomPredicate()]),
+ )
order1, _, _ = self._callFUT(
xhr='xhr',
request_method='request_method',
@@ -121,6 +135,7 @@ class TestPredicateList(unittest.TestCase):
)
order11, _, _ = self._callFUT(xhr='xhr')
order12, _, _ = self._callFUT()
+ self.assertTrue(order1 > order0)
self.assertEqual(order1, order2)
self.assertTrue(order3 > order2)
self.assertTrue(order4 > order3)
@@ -131,7 +146,7 @@ class TestPredicateList(unittest.TestCase):
self.assertTrue(order9 > order8)
self.assertTrue(order10 > order9)
self.assertTrue(order11 > order10)
- self.assertTrue(order12 > order10)
+ self.assertTrue(order12 > order11)
def test_ordering_importance_of_predicates(self):
from pyramid.config.predicates import predvalseq
@@ -145,7 +160,8 @@ class TestPredicateList(unittest.TestCase):
order7, _, _ = self._callFUT(containment='containment')
order8, _, _ = self._callFUT(request_type='request_type')
order9, _, _ = self._callFUT(match_param='foo=bar')
- order10, _, _ = self._callFUT(
+ order10, _, _ = self._callFUT(is_authenticated=True)
+ order11, _, _ = self._callFUT(
custom=predvalseq([DummyCustomPredicate()])
)
self.assertTrue(order1 > order2)
@@ -157,6 +173,7 @@ class TestPredicateList(unittest.TestCase):
self.assertTrue(order7 > order8)
self.assertTrue(order8 > order9)
self.assertTrue(order9 > order10)
+ self.assertTrue(order10 > order11)
def test_ordering_importance_and_number(self):
from pyramid.config.predicates import predvalseq
@@ -296,6 +313,7 @@ class TestPredicateList(unittest.TestCase):
]
),
match_param='foo=bar',
+ is_authenticated=False,
)
self.assertEqual(predicates[0].text(), 'xhr = True')
self.assertEqual(
@@ -308,9 +326,10 @@ class TestPredicateList(unittest.TestCase):
self.assertEqual(predicates[6].text(), 'containment = containment')
self.assertEqual(predicates[7].text(), 'request_type = request_type')
self.assertEqual(predicates[8].text(), "match_param foo=bar")
- self.assertEqual(predicates[9].text(), 'custom predicate')
- self.assertEqual(predicates[10].text(), 'classmethod predicate')
- self.assertTrue(predicates[11].text().startswith('custom predicate'))
+ self.assertEqual(predicates[9].text(), "is_authenticated = False")
+ self.assertEqual(predicates[10].text(), 'custom predicate')
+ self.assertEqual(predicates[11].text(), 'classmethod predicate')
+ self.assertTrue(predicates[12].text().startswith('custom predicate'))
def test_predicate_text_is_correct_when_multiple(self):
_, predicates, _ = self._callFUT(
@@ -434,6 +453,30 @@ class TestPredicateList(unittest.TestCase):
request.headers = {'foo': 'nobar', 'spamme': 'ham'}
self.assertFalse(predicates[0](Dummy(), request))
+ def test_is_authenticated_true_matches(self):
+ _, predicates, _ = self._callFUT(is_authenticated=True)
+ request = DummyRequest()
+ request.is_authenticated = True
+ self.assertTrue(predicates[0](Dummy(), request))
+
+ def test_is_authenticated_true_fails(self):
+ _, predicates, _ = self._callFUT(is_authenticated=True)
+ request = DummyRequest()
+ request.is_authenticated = False
+ self.assertFalse(predicates[0](Dummy(), request))
+
+ def test_is_authenticated_false_matches(self):
+ _, predicates, _ = self._callFUT(is_authenticated=False)
+ request = DummyRequest()
+ request.is_authenticated = False
+ self.assertTrue(predicates[0](Dummy(), request))
+
+ def test_is_authenticated_false_fails(self):
+ _, predicates, _ = self._callFUT(is_authenticated=False)
+ request = DummyRequest()
+ request.is_authenticated = True
+ self.assertFalse(predicates[0](Dummy(), request))
+
def test_unknown_predicate(self):
from pyramid.exceptions import ConfigurationError
diff --git a/tests/test_config/test_routes.py b/tests/test_config/test_routes.py
index 8227784ec..605f61857 100644
--- a/tests/test_config/test_routes.py
+++ b/tests/test_config/test_routes.py
@@ -184,6 +184,18 @@ class RoutesConfiguratorMixinTests(unittest.TestCase):
request.params = {}
self.assertEqual(predicate(None, request), False)
+ def test_add_route_with_is_authenticated(self):
+ config = self._makeOne(autocommit=True)
+ config.add_route('name', 'path', is_authenticated=True)
+ route = self._assertRoute(config, 'name', 'path', 1)
+ predicate = route.predicates[0]
+ request = self._makeRequest(config)
+ request.is_authenticated = True
+ self.assertEqual(predicate(None, request), True)
+ request = self._makeRequest(config)
+ request.is_authenticated = False
+ self.assertEqual(predicate(None, request), False)
+
def test_add_route_with_custom_predicates(self):
import warnings
diff --git a/tests/test_config/test_views.py b/tests/test_config/test_views.py
index 2a55ad45d..09714d82e 100644
--- a/tests/test_config/test_views.py
+++ b/tests/test_config/test_views.py
@@ -1742,6 +1742,46 @@ class TestViewsConfigurationMixin(unittest.TestCase):
request.is_xhr = False
self._assertNotFound(wrapper, None, request)
+ def test_add_view_with_is_authenticated_true_matches(self):
+ from pyramid.renderers import null_renderer as nr
+
+ view = lambda *arg: 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_view(view=view, is_authenticated=True, renderer=nr)
+ wrapper = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ request.is_authenticated = True
+ self.assertEqual(wrapper(None, request), 'OK')
+
+ def test_add_view_with_is_authenticated_true_no_match(self):
+ view = lambda *arg: 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_view(view=view, is_authenticated=True)
+ wrapper = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ request.is_authenticated = False
+ self._assertNotFound(wrapper, None, request)
+
+ def test_add_view_with_is_authenticated_false_matches(self):
+ from pyramid.renderers import null_renderer as nr
+
+ view = lambda *arg: 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_view(view=view, is_authenticated=False, renderer=nr)
+ wrapper = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ request.is_authenticated = False
+ self.assertEqual(wrapper(None, request), 'OK')
+
+ def test_add_view_with_is_authenticated_false_no_match(self):
+ view = lambda *arg: 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_view(view=view, is_authenticated=False)
+ wrapper = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ request.is_authenticated = True
+ self._assertNotFound(wrapper, None, request)
+
def test_add_view_with_header_badregex(self):
view = lambda *arg: 'OK'
config = self._makeOne()
diff --git a/tests/test_security.py b/tests/test_security.py
index bf2908100..72598f570 100644
--- a/tests/test_security.py
+++ b/tests/test_security.py
@@ -393,6 +393,29 @@ class TestUnAuthenticatedUserId(unittest.TestCase):
self.assertEqual(request.unauthenticated_userid, 'wat')
+class TestIsAuthenticated(unittest.TestCase):
+ def setUp(self):
+ testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def test_no_security_policy(self):
+ request = _makeRequest()
+ self.assertIs(request.is_authenticated, False)
+
+ def test_with_security_policy(self):
+ request = _makeRequest()
+ _registerSecurityPolicy(request.registry, '123')
+ self.assertIs(request.is_authenticated, True)
+
+ def test_with_legacy_security_policy(self):
+ request = _makeRequest()
+ _registerAuthenticationPolicy(request.registry, 'yo')
+ _registerLegacySecurityPolicy(request.registry)
+ self.assertEqual(request.authenticated_userid, 'yo')
+
+
class TestEffectivePrincipals(unittest.TestCase):
def setUp(self):
testing.setUp()