Skip to content

Commit

Permalink
Merge pull request #3598 from merwok/feature/is_authenticated
Browse files Browse the repository at this point in the history
Add Request.is_authenticated and is_authenticated predicate
  • Loading branch information
digitalresistor authored Jul 6, 2020
2 parents 48a0485 + 5f37acd commit 5269b28
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 7 deletions.
5 changes: 4 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/narr/advanced-features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions docs/narr/viewconfig.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
13 changes: 13 additions & 0 deletions src/pyramid/config/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,17 @@ def add_route(
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
Expand All @@ -282,6 +293,7 @@ def add_route(
.. versionadded:: 1.4a4
.. deprecated:: 2.0
Use ``is_authenticated`` or a custom predicate.
custom_predicates
Expand Down Expand Up @@ -537,6 +549,7 @@ def add_default_route_predicates(self):
('request_param', p.RequestParamPredicate),
('header', p.HeaderPredicate),
('accept', p.AcceptPredicate),
('is_authenticated', p.IsAuthenticatedPredicate),
('effective_principals', p.EffectivePrincipalsPredicate),
('custom', p.CustomPredicate),
('traverse', p.TraversePredicate),
Expand Down
13 changes: 13 additions & 0 deletions src/pyramid/config/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,17 @@ def wrapper(context, request):
.. 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
Expand All @@ -726,6 +737,7 @@ def wrapper(context, request):
.. versionadded:: 1.4a4
.. deprecated:: 2.0
Use ``is_authenticated`` or a custom predicate.
custom_predicates
Expand Down Expand Up @@ -1205,6 +1217,7 @@ def add_default_view_predicates(self):
('request_type', p.RequestTypePredicate),
('match_param', p.MatchParamPredicate),
('physical_path', p.PhysicalPathPredicate),
('is_authenticated', p.IsAuthenticatedPredicate),
('effective_principals', p.EffectivePrincipalsPredicate),
('custom', p.CustomPredicate),
):
Expand Down
15 changes: 15 additions & 0 deletions src/pyramid/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ def app_iter_range(start, stop):
""" 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."""
Expand Down Expand Up @@ -233,6 +243,11 @@ def encode_content(encoding='gzip', lazy=False):

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
Expand Down
13 changes: 13 additions & 0 deletions src/pyramid/predicates.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,19 @@ def __call__(self, context, request):
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):
Expand Down
5 changes: 5 additions & 0 deletions src/pyramid/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,11 @@ def authenticated_userid(self):
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
Expand Down
53 changes: 48 additions & 5 deletions tests/test_config/test_predicates.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def _makeOne(self):
('containment', predicates.ContainmentPredicate),
('request_type', predicates.RequestTypePredicate),
('match_param', predicates.MatchParamPredicate),
('is_authenticated', predicates.IsAuthenticatedPredicate),
('custom', predicates.CustomPredicate),
('traverse', predicates.TraversePredicate),
):
Expand All @@ -38,6 +39,19 @@ def test_ordering_xhr_and_request_method_trump_only_containment(self):
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',
Expand Down Expand Up @@ -121,6 +135,7 @@ def test_ordering_number_of_predicates(self):
)
order11, _, _ = self._callFUT(xhr='xhr')
order12, _, _ = self._callFUT()
self.assertTrue(order1 > order0)
self.assertEqual(order1, order2)
self.assertTrue(order3 > order2)
self.assertTrue(order4 > order3)
Expand All @@ -131,7 +146,7 @@ def test_ordering_number_of_predicates(self):
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
Expand All @@ -145,7 +160,8 @@ def test_ordering_importance_of_predicates(self):
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)
Expand All @@ -157,6 +173,7 @@ def test_ordering_importance_of_predicates(self):
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
Expand Down Expand Up @@ -296,6 +313,7 @@ def test_predicate_text_is_correct(self):
]
),
match_param='foo=bar',
is_authenticated=False,
)
self.assertEqual(predicates[0].text(), 'xhr = True')
self.assertEqual(
Expand All @@ -308,9 +326,10 @@ def test_predicate_text_is_correct(self):
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(
Expand Down Expand Up @@ -434,6 +453,30 @@ def test_header_multiple_mixed_fails(self):
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

Expand Down
12 changes: 12 additions & 0 deletions tests/test_config/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,18 @@ def test_add_route_with_request_param(self):
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

Expand Down
40 changes: 40 additions & 0 deletions tests/test_config/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1742,6 +1742,46 @@ def test_add_view_with_xhr_false(self):
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()
Expand Down
23 changes: 23 additions & 0 deletions tests/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,29 @@ def test_security_policy_trumps_authentication_policy(self):
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()
Expand Down

0 comments on commit 5269b28

Please sign in to comment.