summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/pyramid/authentication.py329
-rw-r--r--src/pyramid/authorization.py124
-rw-r--r--src/pyramid/config/__init__.py9
-rw-r--r--src/pyramid/config/security.py81
-rw-r--r--src/pyramid/config/testing.py15
-rw-r--r--src/pyramid/interfaces.py49
-rw-r--r--src/pyramid/predicates.py19
-rw-r--r--src/pyramid/request.py4
-rw-r--r--src/pyramid/security.py214
-rw-r--r--src/pyramid/testing.py42
-rw-r--r--src/pyramid/viewderivers.py36
11 files changed, 579 insertions, 343 deletions
diff --git a/src/pyramid/authentication.py b/src/pyramid/authentication.py
index 21cfc0c0e..de06fe955 100644
--- a/src/pyramid/authentication.py
+++ b/src/pyramid/authentication.py
@@ -428,150 +428,9 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy):
@implementer(IAuthenticationPolicy)
class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
"""A :app:`Pyramid` :term:`authentication policy` which
- obtains data from a Pyramid "auth ticket" cookie.
-
- Constructor Arguments
-
- ``secret``
-
- The secret (a string) used for auth_tkt cookie signing. This value
- should be unique across all values provided to Pyramid for various
- subsystem secrets (see :ref:`admonishment_against_secret_sharing`).
- Required.
-
- ``callback``
-
- Default: ``None``. A callback passed the userid and the
- request, expected to return ``None`` if the userid doesn't
- exist or a sequence of principal identifiers (possibly empty) if
- the user does exist. If ``callback`` is ``None``, the userid
- will be assumed to exist with no principals. Optional.
-
- ``cookie_name``
-
- Default: ``auth_tkt``. The cookie name used
- (string). Optional.
-
- ``secure``
-
- Default: ``False``. Only send the cookie back over a secure
- conn. Optional.
-
- ``include_ip``
-
- Default: ``False``. Make the requesting IP address part of
- the authentication data in the cookie. Optional.
-
- For IPv6 this option is not recommended. The ``mod_auth_tkt``
- specification does not specify how to handle IPv6 addresses, so using
- this option in combination with IPv6 addresses may cause an
- incompatible cookie. It ties the authentication ticket to that
- individual's IPv6 address.
-
- ``timeout``
-
- Default: ``None``. Maximum number of seconds which a newly
- issued ticket will be considered valid. After this amount of
- time, the ticket will expire (effectively logging the user
- out). If this value is ``None``, the ticket never expires.
- Optional.
-
- ``reissue_time``
-
- Default: ``None``. If this parameter is set, it represents the number
- of seconds that must pass before an authentication token cookie is
- automatically reissued as the result of a request which requires
- authentication. The duration is measured as the number of seconds
- since the last auth_tkt cookie was issued and 'now'. If this value is
- ``0``, a new ticket cookie will be reissued on every request which
- requires authentication.
-
- A good rule of thumb: if you want auto-expired cookies based on
- inactivity: set the ``timeout`` value to 1200 (20 mins) and set the
- ``reissue_time`` value to perhaps a tenth of the ``timeout`` value
- (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower
- than the ``reissue_time`` value, as the ticket will never be reissued
- if so. However, such a configuration is not explicitly prevented.
-
- Optional.
-
- ``max_age``
-
- Default: ``None``. The max age of the auth_tkt cookie, in
- seconds. This differs from ``timeout`` inasmuch as ``timeout``
- represents the lifetime of the ticket contained in the cookie,
- while this value represents the lifetime of the cookie itself.
- When this value is set, the cookie's ``Max-Age`` and
- ``Expires`` settings will be set, allowing the auth_tkt cookie
- to last between browser sessions. It is typically nonsensical
- to set this to a value that is lower than ``timeout`` or
- ``reissue_time``, although it is not explicitly prevented.
- Optional.
-
- ``path``
-
- Default: ``/``. The path for which the auth_tkt cookie is valid.
- May be desirable if the application only serves part of a domain.
- Optional.
-
- ``http_only``
-
- Default: ``False``. Hide cookie from JavaScript by setting the
- HttpOnly flag. Not honored by all browsers.
- Optional.
-
- ``wild_domain``
-
- Default: ``True``. An auth_tkt cookie will be generated for the
- wildcard domain. If your site is hosted as ``example.com`` this
- will make the cookie available for sites underneath ``example.com``
- such as ``www.example.com``.
- Optional.
-
- ``parent_domain``
-
- Default: ``False``. An auth_tkt cookie will be generated for the
- parent domain of the current site. For example if your site is
- hosted under ``www.example.com`` a cookie will be generated for
- ``.example.com``. This can be useful if you have multiple sites
- sharing the same domain. This option supercedes the ``wild_domain``
- option.
- Optional.
-
- ``domain``
-
- Default: ``None``. If provided the auth_tkt cookie will only be
- set for this domain. This option is not compatible with ``wild_domain``
- and ``parent_domain``.
- Optional.
-
- ``hashalg``
-
- Default: ``sha512`` (the literal string).
-
- Any hash algorithm supported by Python's ``hashlib.new()`` function
- can be used as the ``hashalg``.
-
- Cookies generated by different instances of AuthTktAuthenticationPolicy
- using different ``hashalg`` options are not compatible. Switching the
- ``hashalg`` will imply that all existing users with a valid cookie will
- be required to re-login.
-
- Optional.
-
- ``debug``
-
- Default: ``False``. If ``debug`` is ``True``, log messages to the
- Pyramid debug logger about the results of various authentication
- steps. The output from debugging is useful for reporting to maillist
- or IRC channels when asking for support.
-
- ``samesite``
-
- Default: ``'Lax'``. The 'samesite' option of the session cookie. Set
- the value to ``None`` to turn off the samesite option.
-
- This option is available as of :app:`Pyramid` 1.10.
+ obtains data from a Pyramid "auth ticket" cookie. See
+ :class:`.AuthTktCookieHelper` for documentation of the constructor
+ arguments.
.. versionchanged:: 1.4
@@ -823,10 +682,150 @@ def encode_ip_timestamp(ip, timestamp):
class AuthTktCookieHelper(object):
"""
- A helper class for use in third-party authentication policy
- implementations. See
- :class:`pyramid.authentication.AuthTktAuthenticationPolicy` for the
- meanings of the constructor arguments.
+ A helper class for security policies that obtains data from an "auth
+ ticket" cookie.
+
+ Constructor Arguments
+
+ ``secret``
+
+ The secret (a string) used for auth_tkt cookie signing. This value
+ should be unique across all values provided to Pyramid for various
+ subsystem secrets (see :ref:`admonishment_against_secret_sharing`).
+ Required.
+
+ ``callback``
+
+ Default: ``None``. A callback passed the userid and the
+ request, expected to return ``None`` if the userid doesn't
+ exist or a sequence of principal identifiers (possibly empty) if
+ the user does exist. If ``callback`` is ``None``, the userid
+ will be assumed to exist with no principals. Optional.
+
+ ``cookie_name``
+
+ Default: ``auth_tkt``. The cookie name used
+ (string). Optional.
+
+ ``secure``
+
+ Default: ``False``. Only send the cookie back over a secure
+ conn. Optional.
+
+ ``include_ip``
+
+ Default: ``False``. Make the requesting IP address part of
+ the authentication data in the cookie. Optional.
+
+ For IPv6 this option is not recommended. The ``mod_auth_tkt``
+ specification does not specify how to handle IPv6 addresses, so using
+ this option in combination with IPv6 addresses may cause an
+ incompatible cookie. It ties the authentication ticket to that
+ individual's IPv6 address.
+
+ ``timeout``
+
+ Default: ``None``. Maximum number of seconds which a newly
+ issued ticket will be considered valid. After this amount of
+ time, the ticket will expire (effectively logging the user
+ out). If this value is ``None``, the ticket never expires.
+ Optional.
+
+ ``reissue_time``
+
+ Default: ``None``. If this parameter is set, it represents the number
+ of seconds that must pass before an authentication token cookie is
+ automatically reissued as the result of a request which requires
+ authentication. The duration is measured as the number of seconds
+ since the last auth_tkt cookie was issued and 'now'. If this value is
+ ``0``, a new ticket cookie will be reissued on every request which
+ requires authentication.
+
+ A good rule of thumb: if you want auto-expired cookies based on
+ inactivity: set the ``timeout`` value to 1200 (20 mins) and set the
+ ``reissue_time`` value to perhaps a tenth of the ``timeout`` value
+ (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower
+ than the ``reissue_time`` value, as the ticket will never be reissued
+ if so. However, such a configuration is not explicitly prevented.
+
+ Optional.
+
+ ``max_age``
+
+ Default: ``None``. The max age of the auth_tkt cookie, in
+ seconds. This differs from ``timeout`` inasmuch as ``timeout``
+ represents the lifetime of the ticket contained in the cookie,
+ while this value represents the lifetime of the cookie itself.
+ When this value is set, the cookie's ``Max-Age`` and
+ ``Expires`` settings will be set, allowing the auth_tkt cookie
+ to last between browser sessions. It is typically nonsensical
+ to set this to a value that is lower than ``timeout`` or
+ ``reissue_time``, although it is not explicitly prevented.
+ Optional.
+
+ ``path``
+
+ Default: ``/``. The path for which the auth_tkt cookie is valid.
+ May be desirable if the application only serves part of a domain.
+ Optional.
+
+ ``http_only``
+
+ Default: ``False``. Hide cookie from JavaScript by setting the
+ HttpOnly flag. Not honored by all browsers.
+ Optional.
+
+ ``wild_domain``
+
+ Default: ``True``. An auth_tkt cookie will be generated for the
+ wildcard domain. If your site is hosted as ``example.com`` this
+ will make the cookie available for sites underneath ``example.com``
+ such as ``www.example.com``.
+ Optional.
+
+ ``parent_domain``
+
+ Default: ``False``. An auth_tkt cookie will be generated for the
+ parent domain of the current site. For example if your site is
+ hosted under ``www.example.com`` a cookie will be generated for
+ ``.example.com``. This can be useful if you have multiple sites
+ sharing the same domain. This option supercedes the ``wild_domain``
+ option.
+ Optional.
+
+ ``domain``
+
+ Default: ``None``. If provided the auth_tkt cookie will only be
+ set for this domain. This option is not compatible with ``wild_domain``
+ and ``parent_domain``.
+ Optional.
+
+ ``hashalg``
+
+ Default: ``sha512`` (the literal string).
+
+ Any hash algorithm supported by Python's ``hashlib.new()`` function
+ can be used as the ``hashalg``.
+
+ Cookies generated by different instances of AuthTktAuthenticationPolicy
+ using different ``hashalg`` options are not compatible. Switching the
+ ``hashalg`` will imply that all existing users with a valid cookie will
+ be required to re-login.
+
+ Optional.
+
+ ``debug``
+
+ Default: ``False``. If ``debug`` is ``True``, log messages to the
+ Pyramid debug logger about the results of various authentication
+ steps. The output from debugging is useful for reporting to maillist
+ or IRC channels when asking for support.
+
+ ``samesite``
+
+ Default: ``'Lax'``. The 'samesite' option of the session cookie. Set
+ the value to ``None`` to turn off the samesite option.
+
"""
parse_ticket = staticmethod(parse_ticket) # for tests
@@ -1099,9 +1098,36 @@ class SessionAuthenticationPolicy(CallbackAuthenticationPolicy):
def __init__(self, prefix='auth.', callback=None, debug=False):
self.callback = callback
- self.prefix = prefix or ''
- self.userid_key = prefix + 'userid'
self.debug = debug
+ self.helper = SessionAuthenticationHelper(prefix)
+
+ def remember(self, request, userid, **kw):
+ """ Store a userid in the session."""
+ return self.helper.remember(request, userid, **kw)
+
+ def forget(self, request):
+ """ Remove the stored userid from the session."""
+ return self.helper.forget(request)
+
+ def unauthenticated_userid(self, request):
+ return self.helper.identify(request)
+
+
+class SessionAuthenticationHelper:
+ """ A helper for use with a :term:`security policy` which stores user data
+ in the configured :term:`session`.
+
+ Constructor Arguments
+
+ ``prefix``
+
+ A prefix used when storing the authentication parameters in the
+ session. Defaults to 'auth.'. Optional.
+
+ """
+
+ def __init__(self, prefix='auth.'):
+ self.userid_key = prefix + 'userid'
def remember(self, request, userid, **kw):
""" Store a userid in the session."""
@@ -1114,7 +1140,8 @@ class SessionAuthenticationPolicy(CallbackAuthenticationPolicy):
del request.session[self.userid_key]
return []
- def unauthenticated_userid(self, request):
+ def identify(self, request):
+ """ Return the stored userid."""
return request.session.get(self.userid_key)
diff --git a/src/pyramid/authorization.py b/src/pyramid/authorization.py
index 6056a8d25..498938fd5 100644
--- a/src/pyramid/authorization.py
+++ b/src/pyramid/authorization.py
@@ -14,59 +14,74 @@ class ACLAuthorizationPolicy(object):
""" An :term:`authorization policy` which consults an :term:`ACL`
object attached to a :term:`context` to determine authorization
information about a :term:`principal` or multiple principals.
- If the context is part of a :term:`lineage`, the context's parents
- are consulted for ACL information too. The following is true
- about this security policy.
-
- - When checking whether the 'current' user is permitted (via the
- ``permits`` method), the security policy consults the
- ``context`` for an ACL first. If no ACL exists on the context,
- or one does exist but the ACL does not explicitly allow or deny
- access for any of the effective principals, consult the
- context's parent ACL, and so on, until the lineage is exhausted
- or we determine that the policy permits or denies.
-
- During this processing, if any :data:`pyramid.security.Deny`
- ACE is found matching any principal in ``principals``, stop
- processing by returning an
- :class:`pyramid.security.ACLDenied` instance (equals
- ``False``) immediately. If any
- :data:`pyramid.security.Allow` ACE is found matching any
- principal, stop processing by returning an
- :class:`pyramid.security.ACLAllowed` instance (equals
- ``True``) immediately. If we exhaust the context's
- :term:`lineage`, and no ACE has explicitly permitted or denied
- access, return an instance of
- :class:`pyramid.security.ACLDenied` (equals ``False``).
-
- - When computing principals allowed by a permission via the
- :func:`pyramid.security.principals_allowed_by_permission`
- method, we compute the set of principals that are explicitly
- granted the ``permission`` in the provided ``context``. We do
- this by walking 'up' the object graph *from the root* to the
- context. During this walking process, if we find an explicit
- :data:`pyramid.security.Allow` ACE for a principal that
- matches the ``permission``, the principal is included in the
- allow list. However, if later in the walking process that
- principal is mentioned in any :data:`pyramid.security.Deny`
- ACE for the permission, the principal is removed from the allow
- list. If a :data:`pyramid.security.Deny` to the principal
- :data:`pyramid.security.Everyone` is encountered during the
- walking process that matches the ``permission``, the allow list
- is cleared for all principals encountered in previous ACLs. The
- walking process ends after we've processed the any ACL directly
- attached to ``context``; a set of principals is returned.
+ This class is a wrapper around :class:`.ACLHelper`, refer to that class for
+ more detailed documentation.
Objects of this class implement the
:class:`pyramid.interfaces.IAuthorizationPolicy` interface.
+
+ .. deprecated:: 2.0
+
+ Authorization policies have been deprecated by the new security system.
+ See :ref:`upgrading_auth` for more information.
+
"""
+ def __init__(self):
+ self.helper = ACLHelper()
+
def permits(self, context, principals, permission):
""" Return an instance of
:class:`pyramid.security.ACLAllowed` instance if the policy
permits access, return an instance of
:class:`pyramid.security.ACLDenied` if not."""
+ return self.helper.permits(context, principals, permission)
+
+ def principals_allowed_by_permission(self, context, permission):
+ """ Return the set of principals explicitly granted the
+ permission named ``permission`` according to the ACL directly
+ attached to the ``context`` as well as inherited ACLs based on
+ the :term:`lineage`."""
+ return self.helper.principals_allowed_by_permission(
+ context, permission
+ )
+
+
+class ACLHelper:
+ """ A helper for use with constructing a :term:`security policy` which
+ consults an :term:`ACL` object attached to a :term:`context` to determine
+ authorization information about a :term:`principal` or multiple principals.
+ If the context is part of a :term:`lineage`, the context's parents are
+ consulted for ACL information too.
+
+ """
+ def permits(self, context, principals, permission):
+ """ Return an instance of :class:`pyramid.security.ACLAllowed` if the
+ ACL allows access a user with the given principals, return an instance
+ of :class:`pyramid.security.ACLDenied` if not.
+
+ When checking if principals are allowed, the security policy consults
+ the ``context`` for an ACL first. If no ACL exists on the context, or
+ one does exist but the ACL does not explicitly allow or deny access for
+ any of the effective principals, consult the context's parent ACL, and
+ so on, until the lineage is exhausted or we determine that the policy
+ permits or denies.
+
+ During this processing, if any :data:`pyramid.security.Deny`
+ ACE is found matching any principal in ``principals``, stop
+ processing by returning an
+ :class:`pyramid.security.ACLDenied` instance (equals
+ ``False``) immediately. If any
+ :data:`pyramid.security.Allow` ACE is found matching any
+ principal, stop processing by returning an
+ :class:`pyramid.security.ACLAllowed` instance (equals
+ ``True``) immediately. If we exhaust the context's
+ :term:`lineage`, and no ACE has explicitly permitted or denied
+ access, return an instance of
+ :class:`pyramid.security.ACLDenied` (equals ``False``).
+
+ """
acl = '<No ACL found on any object in resource lineage>'
for location in lineage(context):
@@ -100,10 +115,27 @@ class ACLAuthorizationPolicy(object):
)
def principals_allowed_by_permission(self, context, permission):
- """ Return the set of principals explicitly granted the
- permission named ``permission`` according to the ACL directly
- attached to the ``context`` as well as inherited ACLs based on
- the :term:`lineage`."""
+ """ Return the set of principals explicitly granted the permission
+ named ``permission`` according to the ACL directly attached to the
+ ``context`` as well as inherited ACLs based on the :term:`lineage`.
+
+ When computing principals allowed by a permission, we compute the set
+ of principals that are explicitly granted the ``permission`` in the
+ provided ``context``. We do this by walking 'up' the object graph
+ *from the root* to the context. During this walking process, if we
+ find an explicit :data:`pyramid.security.Allow` ACE for a principal
+ that matches the ``permission``, the principal is included in the allow
+ list. However, if later in the walking process that principal is
+ mentioned in any :data:`pyramid.security.Deny` ACE for the permission,
+ the principal is removed from the allow list. If a
+ :data:`pyramid.security.Deny` to the principal
+ :data:`pyramid.security.Everyone` is encountered during the walking
+ process that matches the ``permission``, the allow list is cleared for
+ all principals encountered in previous ACLs. The walking process ends
+ after we've processed the any ACL directly attached to ``context``; a
+ set of principals is returned.
+
+ """
allowed = set()
for location in reversed(list(lineage(context))):
diff --git a/src/pyramid/config/__init__.py b/src/pyramid/config/__init__.py
index 072b654c4..cf1bfad44 100644
--- a/src/pyramid/config/__init__.py
+++ b/src/pyramid/config/__init__.py
@@ -139,6 +139,9 @@ class Configurator(
:term:`dotted Python name` to the same. If it is ``None``, a default
root factory will be used.
+ If ``security_policy`` is passed, it should be an instance of a
+ :term:`security policy` or a :term:`dotted Python name` to the same.
+
If ``authentication_policy`` is passed, it should be an instance
of an :term:`authentication policy` or a :term:`dotted Python
name` to the same.
@@ -278,6 +281,7 @@ class Configurator(
package=None,
settings=None,
root_factory=None,
+ security_policy=None,
authentication_policy=None,
authorization_policy=None,
renderers=None,
@@ -315,6 +319,7 @@ class Configurator(
root_factory=root_factory,
authentication_policy=authentication_policy,
authorization_policy=authorization_policy,
+ security_policy=security_policy,
renderers=renderers,
debug_logger=debug_logger,
locale_negotiator=locale_negotiator,
@@ -330,6 +335,7 @@ class Configurator(
self,
settings=None,
root_factory=None,
+ security_policy=None,
authentication_policy=None,
authorization_policy=None,
renderers=None,
@@ -415,6 +421,9 @@ class Configurator(
if authentication_policy:
self.set_authentication_policy(authentication_policy)
+ if security_policy:
+ self.set_security_policy(security_policy)
+
if default_view_mapper is not None:
self.set_view_mapper(default_view_mapper)
diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py
index 17ac5ded7..32b4db03c 100644
--- a/src/pyramid/config/security.py
+++ b/src/pyramid/config/security.py
@@ -1,4 +1,5 @@
from zope.interface import implementer
+from zope.deprecation import deprecate
from pyramid.interfaces import (
IAuthorizationPolicy,
@@ -6,6 +7,7 @@ from pyramid.interfaces import (
ICSRFStoragePolicy,
IDefaultCSRFOptions,
IDefaultPermission,
+ ISecurityPolicy,
PHASE1_CONFIG,
PHASE2_CONFIG,
)
@@ -13,6 +15,7 @@ from pyramid.interfaces import (
from pyramid.csrf import LegacySessionCSRFStoragePolicy
from pyramid.exceptions import ConfigurationError
from pyramid.util import as_sorted_tuple
+from pyramid.security import LegacySecurityPolicy
from pyramid.config.actions import action_method
@@ -22,8 +25,54 @@ class SecurityConfiguratorMixin(object):
self.set_csrf_storage_policy(LegacySessionCSRFStoragePolicy())
@action_method
+ def set_security_policy(self, policy):
+ """ Override the :app:`Pyramid` :term:`security policy` in the current
+ configuration. The ``policy`` argument must be an instance
+ of a security policy or a :term:`dotted Python name`
+ that points at an instance of a security policy.
+
+ .. note::
+
+ Using the ``security_policy`` argument to the
+ :class:`pyramid.config.Configurator` constructor can be used to
+ achieve the same purpose.
+
+ """
+
+ def register():
+ self.registry.registerUtility(policy, ISecurityPolicy)
+
+ policy = self.maybe_dotted(policy)
+ intr = self.introspectable(
+ 'security policy',
+ None,
+ self.object_description(policy),
+ 'security policy',
+ )
+ intr['policy'] = policy
+ self.action(
+ ISecurityPolicy,
+ register,
+ order=PHASE2_CONFIG,
+ introspectables=(intr,),
+ )
+
+ @deprecate(
+ 'Authentication and authorization policies have been deprecated in '
+ 'favor of security policies. See '
+ 'https://docs.pylonsproject.org/projects/pyramid/en/latest'
+ '/whatsnew-2.0.html#upgrading-authentication-authorization '
+ 'for more information.'
+ )
+ @action_method
def set_authentication_policy(self, policy):
- """ Override the :app:`Pyramid` :term:`authentication policy` in the
+ """
+ .. deprecated:: 2.0
+
+ Authentication policies have been replaced by
+ security policies. See :ref:`upgrading_auth` for more information.
+
+ Override the :app:`Pyramid` :term:`authentication policy` in the
current configuration. The ``policy`` argument must be an instance
of an authentication policy or a :term:`dotted Python name`
that points at an instance of an authentication policy.
@@ -37,14 +86,22 @@ class SecurityConfiguratorMixin(object):
"""
def register():
- self._set_authentication_policy(policy)
+ self.registry.registerUtility(policy, IAuthenticationPolicy)
if self.registry.queryUtility(IAuthorizationPolicy) is None:
raise ConfigurationError(
'Cannot configure an authentication policy without '
'also configuring an authorization policy '
'(use the set_authorization_policy method)'
)
+ if self.registry.queryUtility(ISecurityPolicy) is not None:
+ raise ConfigurationError(
+ 'Cannot configure an authentication and authorization'
+ 'policy with a configured security policy.'
+ )
+ security_policy = LegacySecurityPolicy()
+ self.registry.registerUtility(security_policy, ISecurityPolicy)
+ policy = self.maybe_dotted(policy)
intr = self.introspectable(
'authentication policy',
None,
@@ -60,13 +117,15 @@ class SecurityConfiguratorMixin(object):
introspectables=(intr,),
)
- def _set_authentication_policy(self, policy):
- policy = self.maybe_dotted(policy)
- self.registry.registerUtility(policy, IAuthenticationPolicy)
-
@action_method
def set_authorization_policy(self, policy):
- """ Override the :app:`Pyramid` :term:`authorization policy` in the
+ """
+ .. deprecated:: 2.0
+
+ Authentication policies have been replaced by
+ security policies. See :ref:`upgrading_auth` for more information.
+
+ Override the :app:`Pyramid` :term:`authorization policy` in the
current configuration. The ``policy`` argument must be an instance
of an authorization policy or a :term:`dotted Python name` that points
at an instance of an authorization policy.
@@ -76,10 +135,11 @@ class SecurityConfiguratorMixin(object):
Using the ``authorization_policy`` argument to the
:class:`pyramid.config.Configurator` constructor can be used to
achieve the same purpose.
+
"""
def register():
- self._set_authorization_policy(policy)
+ self.registry.registerUtility(policy, IAuthorizationPolicy)
def ensure():
if self.autocommit:
@@ -91,6 +151,7 @@ class SecurityConfiguratorMixin(object):
'(use the set_authorization_policy method)'
)
+ policy = self.maybe_dotted(policy)
intr = self.introspectable(
'authorization policy',
None,
@@ -108,10 +169,6 @@ class SecurityConfiguratorMixin(object):
)
self.action(None, ensure)
- def _set_authorization_policy(self, policy):
- policy = self.maybe_dotted(policy)
- self.registry.registerUtility(policy, IAuthorizationPolicy)
-
@action_method
def set_default_permission(self, permission):
"""
diff --git a/src/pyramid/config/testing.py b/src/pyramid/config/testing.py
index 9c998840a..21c622656 100644
--- a/src/pyramid/config/testing.py
+++ b/src/pyramid/config/testing.py
@@ -1,11 +1,6 @@
from zope.interface import Interface
-from pyramid.interfaces import (
- ITraverser,
- IAuthorizationPolicy,
- IAuthenticationPolicy,
- IRendererFactory,
-)
+from pyramid.interfaces import ITraverser, ISecurityPolicy, IRendererFactory
from pyramid.renderers import RendererHelper
@@ -18,8 +13,7 @@ class TestingConfiguratorMixin(object):
# testing API
def testing_securitypolicy(
self,
- userid=None,
- groupids=(),
+ identity=None,
permissive=True,
remember_result=None,
forget_result=None,
@@ -69,10 +63,9 @@ class TestingConfiguratorMixin(object):
from pyramid.testing import DummySecurityPolicy
policy = DummySecurityPolicy(
- userid, groupids, permissive, remember_result, forget_result
+ identity, permissive, remember_result, forget_result
)
- self.registry.registerUtility(policy, IAuthorizationPolicy)
- self.registry.registerUtility(policy, IAuthenticationPolicy)
+ self.registry.registerUtility(policy, ISecurityPolicy)
return policy
def testing_resources(self, resources):
diff --git a/src/pyramid/interfaces.py b/src/pyramid/interfaces.py
index 15ae3faaa..2d8b1ac40 100644
--- a/src/pyramid/interfaces.py
+++ b/src/pyramid/interfaces.py
@@ -482,8 +482,46 @@ class IViewMapperFactory(Interface):
"""
+class ISecurityPolicy(Interface):
+ def identify(request):
+ """ Return an object identifying a trusted and verified user. This
+ object may be anything, but should implement a ``__str__`` method that
+ outputs a corresponding :term:`userid`.
+
+ """
+
+ def permits(request, context, identity, permission):
+ """ Return an instance of :class:`pyramid.security.Allowed` if a user
+ of the given identity is allowed the ``permission`` in the current
+ ``context``, else return an instance of
+ :class:`pyramid.security.Denied`.
+
+ """
+
+ def remember(request, userid, **kw):
+ """ Return a set of headers suitable for 'remembering' the
+ :term:`userid` named ``userid`` when set in a response. An
+ individual authentication policy and its consumers can
+ decide on the composition and meaning of ``**kw``.
+
+ """
+
+ def forget(request):
+ """ Return a set of headers suitable for 'forgetting' the
+ current user on subsequent requests.
+
+ """
+
+
class IAuthenticationPolicy(Interface):
- """ An object representing a Pyramid authentication policy. """
+ """ An object representing a Pyramid authentication policy.
+
+ .. deprecated:: 2.0
+
+ Authentication policies have been removed in favor of security
+ policies. See :ref:`upgrading_auth` for more information.
+
+ """
def authenticated_userid(request):
""" Return the authenticated :term:`userid` or ``None`` if
@@ -536,7 +574,14 @@ class IAuthenticationPolicy(Interface):
class IAuthorizationPolicy(Interface):
- """ An object representing a Pyramid authorization policy. """
+ """ An object representing a Pyramid authorization policy.
+
+ .. deprecated:: 2.0
+
+ Authentication policies have been removed in favor of security
+ policies. See :ref:`upgrading_auth` for more information.
+
+ """
def permits(context, principals, permission):
""" Return an instance of :class:`pyramid.security.Allowed` if any
diff --git a/src/pyramid/predicates.py b/src/pyramid/predicates.py
index 5a1127fb3..a267a69a0 100644
--- a/src/pyramid/predicates.py
+++ b/src/pyramid/predicates.py
@@ -1,5 +1,7 @@
import re
+from zope.deprecation import deprecated
+
from pyramid.exceptions import ConfigurationError
from pyramid.csrf import check_csrf_token
@@ -291,6 +293,14 @@ class PhysicalPathPredicate(object):
class EffectivePrincipalsPredicate(object):
+ """
+ .. deprecated:: 2.0
+
+ The new security system has removed the concept of principals. See
+ :ref:`upgrading_auth` for more information.
+
+ """
+
def __init__(self, val, config):
if is_nonstr_iter(val):
self.val = set(val)
@@ -311,6 +321,15 @@ class EffectivePrincipalsPredicate(object):
return False
+deprecated(
+ 'EffectivePrincipalsPredicate',
+ 'The new security policy has removed the concept of principals. See '
+ 'https://docs.pylonsproject.org/projects/pyramid/en/latest'
+ '/whatsnew-2.0.html#upgrading-authentication-authorization '
+ 'for more information.',
+)
+
+
class Notted(object):
def __init__(self, predicate):
self.predicate = predicate
diff --git a/src/pyramid/request.py b/src/pyramid/request.py
index b9bd7451a..5c68abe69 100644
--- a/src/pyramid/request.py
+++ b/src/pyramid/request.py
@@ -15,7 +15,7 @@ from pyramid.interfaces import (
from pyramid.decorator import reify
from pyramid.i18n import LocalizerRequestMixin
from pyramid.response import Response, _get_response_factory
-from pyramid.security import AuthenticationAPIMixin, AuthorizationAPIMixin
+from pyramid.security import SecurityAPIMixin, AuthenticationAPIMixin
from pyramid.url import URLMethodsMixin
from pyramid.util import (
InstancePropertyHelper,
@@ -147,8 +147,8 @@ class Request(
CallbackMethodsMixin,
InstancePropertyMixin,
LocalizerRequestMixin,
+ SecurityAPIMixin,
AuthenticationAPIMixin,
- AuthorizationAPIMixin,
ViewMethodsMixin,
):
"""
diff --git a/src/pyramid/security.py b/src/pyramid/security.py
index 61819588b..08c36b457 100644
--- a/src/pyramid/security.py
+++ b/src/pyramid/security.py
@@ -1,6 +1,8 @@
-from zope.interface import providedBy
+from zope.interface import implementer, providedBy
+from zope.deprecation import deprecated
from pyramid.interfaces import (
+ ISecurityPolicy,
IAuthenticationPolicy,
IAuthorizationPolicy,
ISecuredView,
@@ -35,17 +37,12 @@ DENY_ALL = (Deny, Everyone, ALL_PERMISSIONS)
NO_PERMISSION_REQUIRED = '__no_permission_required__'
-def _get_registry(request):
- try:
- reg = request.registry
- except AttributeError:
- reg = get_current_registry() # b/c
- return reg
+def _get_security_policy(request):
+ return request.registry.queryUtility(ISecurityPolicy)
def _get_authentication_policy(request):
- registry = _get_registry(request)
- return registry.queryUtility(IAuthenticationPolicy)
+ return request.registry.queryUtility(IAuthenticationPolicy)
def remember(request, userid, **kw):
@@ -54,7 +51,7 @@ def remember(request, userid, **kw):
on this request's response.
These headers are suitable for 'remembering' a set of credentials
implied by the data passed as ``userid`` and ``*kw`` using the
- current :term:`authentication policy`. Common usage might look
+ current :term:`security 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):
@@ -67,10 +64,10 @@ def remember(request, userid, **kw):
response.headerlist.extend(headers)
return response
- If no :term:`authentication policy` is in use, this function will
+ If no :term:`security 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.
+ the effective security policy.
.. versionchanged:: 1.6
Deprecated the ``principal`` argument in favor of ``userid`` to clarify
@@ -79,7 +76,7 @@ def remember(request, userid, **kw):
.. versionchanged:: 1.10
Removed the deprecated ``principal`` argument.
"""
- policy = _get_authentication_policy(request)
+ policy = _get_security_policy(request)
if policy is None:
return []
return policy.remember(request, userid, **kw)
@@ -101,17 +98,23 @@ def forget(request):
response.headerlist.extend(headers)
return response
- If no :term:`authentication policy` is in use, this function will
+ If no :term:`security policy` is in use, this function will
always return an empty sequence.
"""
- policy = _get_authentication_policy(request)
+ policy = _get_security_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``
+ """
+ .. deprecated:: 2.0
+
+ The new security policy has removed the concept of principals. See
+ :ref:`upgrading_auth` for more information.
+
+ Provided a ``context`` (a resource object), and a ``permission``
string, if an :term:`authorization policy` is
in effect, return a sequence of :term:`principal` ids that possess
the permission in the ``context``. If no authorization policy is
@@ -126,6 +129,7 @@ def principals_allowed_by_permission(context, permission):
required machinery for this function; those will cause a
:exc:`NotImplementedError` exception to be raised when this
function is invoked.
+
"""
reg = get_current_registry()
policy = reg.queryUtility(IAuthorizationPolicy)
@@ -134,6 +138,15 @@ def principals_allowed_by_permission(context, permission):
return policy.principals_allowed_by_permission(context, permission)
+deprecated(
+ 'principals_allowed_by_permission',
+ 'The new security policy has removed the concept of principals. See '
+ 'https://docs.pylonsproject.org/projects/pyramid/en/latest'
+ '/whatsnew-2.0.html#upgrading-authentication-authorization '
+ 'for more information.',
+)
+
+
def view_execution_permitted(context, request, name=''):
""" If the view specified by ``context`` and ``name`` is protected
by a :term:`permission`, check the permission associated with the
@@ -147,7 +160,7 @@ def view_execution_permitted(context, request, name=''):
An exception is raised if no view is found.
"""
- reg = _get_registry(request)
+ reg = request.registry
provides = [IViewClassifier] + [providedBy(x) for x in (request, context)]
# XXX not sure what to do here about using _find_views or analogue;
# for now let's just keep it as-is
@@ -280,23 +293,82 @@ class ACLAllowed(ACLPermitsResult, Allowed):
"""
-class AuthenticationAPIMixin(object):
+class SecurityAPIMixin(object):
@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.
+ def authenticated_identity(self):
+ """
+ Return an opaque object identifying the current user or ``None`` if no
+ user is authenticated or there is no :term:`security policy` in effect.
- .. versionadded:: 1.5
"""
- policy = _get_authentication_policy(self)
+ policy = _get_security_policy(self)
if policy is None:
return None
- return policy.authenticated_userid(self)
+ return policy.identify(self)
@property
+ def authenticated_userid(self):
+ """
+ Return the :term:`userid` of the currently authenticated user or
+ ``None`` if there is no :term:`security policy` in effect or there is
+ no currently authenticated user.
+
+ .. versionchanged:: 2.0
+
+ When using the new security system, this property outputs the
+ string representation of the :term:`identity`.
+
+ """
+ authn = _get_authentication_policy(self)
+ security = _get_security_policy(self)
+ if authn is not None:
+ return authn.authenticated_userid(self)
+ elif security is not None:
+ return str(security.identify(self))
+ else:
+ return 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
+ 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
+ security policy. Returns
+ :data:`pyramid.security.Allowed` unconditionally if no security
+ 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: str
+ :param context: A resource object or ``None``
+ :type context: object
+ :returns: Either :class:`pyramid.security.Allowed` or
+ :class:`pyramid.security.Denied`.
+
+ """
+ if context is None:
+ context = self.context
+ policy = _get_security_policy(self)
+ if policy is None:
+ return Allowed('No security policy in use.')
+ identity = policy.identify(self)
+ return policy.permits(self, context, identity, permission)
+
+
+class AuthenticationAPIMixin(object):
+ @property
def unauthenticated_userid(self):
- """ Return an object which represents the *claimed* (not verified) user
+ """
+ .. deprecated:: 2.0
+
+ ``unauthenticated_userid`` does not have an equivalent in the new
+ security system. Use :attr:`.authenticated_userid` or
+ :attr:`.identity` instead. See :ref:`upgrading_auth` for more
+ information.
+
+ 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
@@ -304,62 +376,72 @@ class AuthenticationAPIMixin(object):
effective authentication policy will not ensure that a record
associated with the userid exists in persistent storage.
- .. versionadded:: 1.5
"""
- policy = _get_authentication_policy(self)
- if policy is None:
+ authn = _get_authentication_policy(self)
+ security = _get_security_policy(self)
+ if authn is not None:
+ return authn.unauthenticated_userid(self)
+ elif security is not None:
+ return str(security.identify(self))
+ else:
return None
- return policy.unauthenticated_userid(self)
@property
def effective_principals(self):
- """ Return the list of 'effective' :term:`principal` identifiers
+ """
+ .. deprecated:: 2.0
+
+ The new security policy has removed the concept of principals. See
+ :ref:`upgrading_auth` for more information.
+
+ Return the list of 'effective' :term:`principal` identifiers
for the ``request``. If no :term:`authentication policy` is in effect,
this will return a one-element list containing the
:data:`pyramid.security.Everyone` principal.
- .. versionadded:: 1.5
"""
policy = _get_authentication_policy(self)
if policy is None:
return [Everyone]
return policy.effective_principals(self)
+ effective_principals = deprecated(
+ effective_principals,
+ 'The new security policy has removed the concept of principals. See '
+ 'https://docs.pylonsproject.org/projects/pyramid/en/latest'
+ '/whatsnew-2.0.html#upgrading-authentication-authorization '
+ 'for more information.',
+ )
-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. 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: str
- :param context: A resource object or ``None``
- :type context: object
- :returns: Either :class:`pyramid.security.Allowed` or
- :class:`pyramid.security.Denied`.
+@implementer(ISecurityPolicy)
+class LegacySecurityPolicy:
+ """
+ A :term:`security policy` which provides a backwards compatibility shim for
+ the :term:`authentication policy` and the :term:`authorization policy`.
- .. versionadded:: 1.5
+ """
- """
- 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)
+ def _get_authn_policy(self, request):
+ return request.registry.getUtility(IAuthenticationPolicy)
+
+ def _get_authz_policy(self, request):
+ return request.registry.getUtility(IAuthorizationPolicy)
+
+ def identify(self, request):
+ authn = self._get_authn_policy(request)
+ return authn.authenticated_userid(request)
+
+ def remember(self, request, userid, **kw):
+ authn = self._get_authn_policy(request)
+ return authn.remember(request, userid, **kw)
+
+ def forget(self, request):
+ authn = self._get_authn_policy(request)
+ return authn.forget(request)
+
+ def permits(self, request, context, identity, permission):
+ authn = self._get_authn_policy(request)
+ authz = self._get_authz_policy(request)
+ principals = authn.effective_principals(request)
+ return authz.permits(context, principals, permission)
diff --git a/src/pyramid/testing.py b/src/pyramid/testing.py
index 0cfc1a277..3bf3f1898 100644
--- a/src/pyramid/testing.py
+++ b/src/pyramid/testing.py
@@ -14,12 +14,7 @@ from pyramid.path import caller_package
from pyramid.response import _get_response_factory
from pyramid.registry import Registry
-from pyramid.security import (
- Authenticated,
- Everyone,
- AuthenticationAPIMixin,
- AuthorizationAPIMixin,
-)
+from pyramid.security import SecurityAPIMixin, AuthenticationAPIMixin
from pyramid.threadlocal import get_current_registry, manager
@@ -43,18 +38,16 @@ class DummyRootFactory(object):
class DummySecurityPolicy(object):
- """ A standin for both an IAuthentication and IAuthorization policy """
+ """ A standin for a security policy"""
def __init__(
self,
- userid=None,
- groupids=(),
+ identity=None,
permissive=True,
remember_result=None,
forget_result=None,
):
- self.userid = userid
- self.groupids = groupids
+ self.identity = identity
self.permissive = permissive
if remember_result is None:
remember_result = []
@@ -63,19 +56,11 @@ class DummySecurityPolicy(object):
self.remember_result = remember_result
self.forget_result = forget_result
- def authenticated_userid(self, request):
- return self.userid
-
- def unauthenticated_userid(self, request):
- return self.userid
+ def identify(self, request):
+ return self.identity
- def effective_principals(self, request):
- effective_principals = [Everyone]
- if self.userid:
- effective_principals.append(Authenticated)
- effective_principals.append(self.userid)
- effective_principals.extend(self.groupids)
- return effective_principals
+ def permits(self, request, context, identity, permission):
+ return self.permissive
def remember(self, request, userid, **kw):
self.remembered = userid
@@ -85,15 +70,6 @@ class DummySecurityPolicy(object):
self.forgotten = True
return self.forget_result
- def permits(self, context, principals, permission):
- return self.permissive
-
- def principals_allowed_by_permission(self, context, permission):
- if self.permissive:
- return self.effective_principals(None)
- else:
- return []
-
class DummyTemplateRenderer(object):
"""
@@ -303,8 +279,8 @@ class DummyRequest(
CallbackMethodsMixin,
InstancePropertyMixin,
LocalizerRequestMixin,
+ SecurityAPIMixin,
AuthenticationAPIMixin,
- AuthorizationAPIMixin,
ViewMethodsMixin,
):
""" A DummyRequest object (incompletely) imitates a :term:`request` object.
diff --git a/src/pyramid/viewderivers.py b/src/pyramid/viewderivers.py
index c41a57d7e..95c223e61 100644
--- a/src/pyramid/viewderivers.py
+++ b/src/pyramid/viewderivers.py
@@ -7,12 +7,11 @@ from pyramid.csrf import check_csrf_origin, check_csrf_token
from pyramid.response import Response
from pyramid.interfaces import (
- IAuthenticationPolicy,
- IAuthorizationPolicy,
IDefaultCSRFOptions,
IDefaultPermission,
IDebugLogger,
IResponse,
+ ISecurityPolicy,
IViewMapper,
IViewMapperFactory,
)
@@ -308,19 +307,17 @@ def _secured_view(view, info):
# permission, replacing it with no permission at all
permission = None
- wrapped_view = view
- authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
- authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
+ policy = info.registry.queryUtility(ISecurityPolicy)
# no-op on exception-only views without an explicit permission
if explicit_val is None and info.exception_only:
return view
- if authn_policy and authz_policy and (permission is not None):
+ if policy and (permission is not None):
def permitted(context, request):
- principals = authn_policy.effective_principals(request)
- return authz_policy.permits(context, principals, permission)
+ identity = policy.identify(request)
+ return policy.permits(request, context, identity, permission)
def secured_view(context, request):
result = permitted(context, request)
@@ -334,12 +331,12 @@ def _secured_view(view, info):
)
raise HTTPForbidden(msg, result=result)
- wrapped_view = secured_view
- wrapped_view.__call_permissive__ = view
- wrapped_view.__permitted__ = permitted
- wrapped_view.__permission__ = permission
-
- return wrapped_view
+ secured_view.__call_permissive__ = view
+ secured_view.__permitted__ = permitted
+ secured_view.__permission__ = permission
+ return secured_view
+ else:
+ return view
def _authdebug_view(view, info):
@@ -348,8 +345,7 @@ def _authdebug_view(view, info):
permission = explicit_val = info.options.get('permission')
if permission is None:
permission = info.registry.queryUtility(IDefaultPermission)
- authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
- authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
+ policy = info.registry.queryUtility(ISecurityPolicy)
logger = info.registry.queryUtility(IDebugLogger)
# no-op on exception-only views without an explicit permission
@@ -361,18 +357,18 @@ def _authdebug_view(view, info):
def authdebug_view(context, request):
view_name = getattr(request, 'view_name', None)
- if authn_policy and authz_policy:
+ if policy:
if permission is NO_PERMISSION_REQUIRED:
msg = 'Allowed (NO_PERMISSION_REQUIRED)'
elif permission is None:
msg = 'Allowed (no permission registered)'
else:
- principals = authn_policy.effective_principals(request)
+ identity = policy.identify(request)
msg = str(
- authz_policy.permits(context, principals, permission)
+ policy.permits(request, context, identity, permission)
)
else:
- msg = 'Allowed (no authorization policy in use)'
+ msg = 'Allowed (no security policy in use)'
view_name = getattr(request, 'view_name', None)
url = getattr(request, 'url', None)