Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Request.is_authenticated and is_authenticated predicate #3598

Merged
merged 3 commits into from
Jul 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

merwok marked this conversation as resolved.
Show resolved Hide resolved
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

merwok marked this conversation as resolved.
Show resolved Hide resolved
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(
mmerickel marked this conversation as resolved.
Show resolved Hide resolved
"""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
mmerickel marked this conversation as resolved.
Show resolved Hide resolved

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(
mmerickel marked this conversation as resolved.
Show resolved Hide resolved
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