diff options
| author | Michael Merickel <michael@merickel.org> | 2019-09-30 22:23:02 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-09-30 22:23:02 -0500 |
| commit | 849463d3c2f5ad2c89b3d10a2abce63e4892082d (patch) | |
| tree | 5bc507d427d8d2000c59ad7837cc03099decf1b5 /src | |
| parent | ada0a977d9190520c21ffaf9500860db2f3a1b3e (diff) | |
| parent | cdb26610782176955cd8cfb0b3c3e242ca819f74 (diff) | |
| download | pyramid-849463d3c2f5ad2c89b3d10a2abce63e4892082d.tar.gz pyramid-849463d3c2f5ad2c89b3d10a2abce63e4892082d.tar.bz2 pyramid-849463d3c2f5ad2c89b3d10a2abce63e4892082d.zip | |
Merge pull request #3465 from luhn/security-policy
Security policy implementation
Diffstat (limited to 'src')
| -rw-r--r-- | src/pyramid/authentication.py | 329 | ||||
| -rw-r--r-- | src/pyramid/authorization.py | 124 | ||||
| -rw-r--r-- | src/pyramid/config/__init__.py | 9 | ||||
| -rw-r--r-- | src/pyramid/config/security.py | 81 | ||||
| -rw-r--r-- | src/pyramid/config/testing.py | 15 | ||||
| -rw-r--r-- | src/pyramid/interfaces.py | 49 | ||||
| -rw-r--r-- | src/pyramid/predicates.py | 19 | ||||
| -rw-r--r-- | src/pyramid/request.py | 4 | ||||
| -rw-r--r-- | src/pyramid/security.py | 214 | ||||
| -rw-r--r-- | src/pyramid/testing.py | 42 | ||||
| -rw-r--r-- | src/pyramid/viewderivers.py | 36 |
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) |
