From 753d596aa2c93bed1fc95fb71bbeef383646c2cb Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 17 Feb 2019 12:15:48 -0800 Subject: Add ISecurityPolicy interface. Deprecate IAuthenticationPolicy and IAuthorizationPolicy. --- src/pyramid/interfaces.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/pyramid/interfaces.py b/src/pyramid/interfaces.py index f1e238c6b..9dabb9cfc 100644 --- a/src/pyramid/interfaces.py +++ b/src/pyramid/interfaces.py @@ -482,8 +482,40 @@ class IViewMapperFactory(Interface): """ +class ISecurityPolicy(Interface): + def identify(request): + """ Return an object identifying a trusted and verified user. """ + + 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 + + Use :class:`ISecurityPolicy`. + + """ def authenticated_userid(request): """ Return the authenticated :term:`userid` or ``None`` if @@ -536,7 +568,13 @@ class IAuthenticationPolicy(Interface): class IAuthorizationPolicy(Interface): - """ An object representing a Pyramid authorization policy. """ + """ An object representing a Pyramid authorization policy. + + .. deprecated:: 2.0 + + Use :class:`ISecurityPolicy`. + + """ def permits(context, principals, permission): """ Return an instance of :class:`pyramid.security.Allowed` if any -- cgit v1.2.3 From a6234e4e19efab838b202d0935de0de92c2ee00f Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 17 Feb 2019 12:44:02 -0800 Subject: Implement setting ISecurityPolicy in the configurator. --- src/pyramid/config/__init__.py | 14 ++++++++++++-- src/pyramid/config/security.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/pyramid/config/__init__.py b/src/pyramid/config/__init__.py index 072b654c4..d8961268a 100644 --- a/src/pyramid/config/__init__.py +++ b/src/pyramid/config/__init__.py @@ -139,13 +139,17 @@ 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. + name` to the same. (Deprecated as of Pyramid 2.0 in favor of + ``security_policy``.) If ``authorization_policy`` is passed, it should be an instance of an :term:`authorization policy` or a :term:`dotted Python name` to - the same. + the same. (Deprecated as of Pyramid 2.0 in favor of ``security_policy``.) .. note:: A ``ConfigurationError`` will be raised when an authorization policy is supplied without also supplying an @@ -278,6 +282,7 @@ class Configurator( package=None, settings=None, root_factory=None, + security_policy=None, authentication_policy=None, authorization_policy=None, renderers=None, @@ -315,6 +320,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 +336,7 @@ class Configurator( self, settings=None, root_factory=None, + security_policy=None, authentication_policy=None, authorization_policy=None, renderers=None, @@ -415,6 +422,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 08e7cb81a..b023917aa 100644 --- a/src/pyramid/config/security.py +++ b/src/pyramid/config/security.py @@ -6,6 +6,7 @@ from pyramid.interfaces import ( ICSRFStoragePolicy, IDefaultCSRFOptions, IDefaultPermission, + ISecurityPolicy, PHASE1_CONFIG, PHASE2_CONFIG, ) @@ -21,6 +22,38 @@ class SecurityConfiguratorMixin(object): def add_default_security(self): 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._set_security_policy(policy) + + intr = self.introspectable( + 'security policy', + None, + self.object_description(policy), + 'security policy', + ) + intr['policy'] = policy + # authentication policy used by view config (phase 3) + self.action(IAuthenticationPolicy, register, introspectables=(intr,)) + + def _set_security_policy(self, policy): + policy = self.maybe_dotted(policy) + self.registry.registerUtility(policy, ISecurityPolicy) + @action_method def set_authentication_policy(self, policy): """ Override the :app:`Pyramid` :term:`authentication policy` in the -- cgit v1.2.3 From 4c3c826ca9a6069f47fee439576966cf625df528 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 17 Feb 2019 16:38:53 -0800 Subject: Implement legacy security policy. --- src/pyramid/security.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/pyramid/security.py b/src/pyramid/security.py index 61819588b..abb9c7442 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -1,6 +1,7 @@ -from zope.interface import providedBy +from zope.interface import implementer, providedBy from pyramid.interfaces import ( + ISecurityPolicy, IAuthenticationPolicy, IAuthorizationPolicy, ISecuredView, @@ -363,3 +364,36 @@ class AuthorizationAPIMixin(object): ) # should never happen principals = authn_policy.effective_principals(self) return authz_policy.permits(context, principals, permission) + + +@implementer(ISecurityPolicy) +class LegacySecurityPolicy: + """ + A :term:`security policy` which provides a backwards compatibility shim for + the :term:`authentication policy` and the :term:`authorization policy`. + + """ + + 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) -- cgit v1.2.3 From 839bfb427093b94a5e75b22a11a5df20ad94cb1e Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 17 Feb 2019 19:02:33 -0800 Subject: Set legacy policy when using authn/authz policies. --- src/pyramid/config/security.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'src') diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py index b023917aa..42554db97 100644 --- a/src/pyramid/config/security.py +++ b/src/pyramid/config/security.py @@ -14,6 +14,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 @@ -77,6 +78,7 @@ class SecurityConfiguratorMixin(object): 'also configuring an authorization policy ' '(use the set_authorization_policy method)' ) + self._set_legacy_policy() intr = self.introspectable( 'authentication policy', @@ -97,6 +99,15 @@ class SecurityConfiguratorMixin(object): policy = self.maybe_dotted(policy) self.registry.registerUtility(policy, IAuthenticationPolicy) + def _set_legacy_policy(self): + if self.registry.queryUtility(ISecurityPolicy) is not None: + raise ConfigurationError( + 'Cannot configure an authentication and authorization policy ' + 'with a configured security policy.' + ) + policy = LegacySecurityPolicy() + self.registry.registerUtility(policy, ISecurityPolicy) + @action_method def set_authorization_policy(self, policy): """ Override the :app:`Pyramid` :term:`authorization policy` in the -- cgit v1.2.3 From aae1c513c0a940c1c31c97b4d79fec3d09cbd01e Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Tue, 26 Feb 2019 17:49:36 -0800 Subject: Add `request.identity`. --- src/pyramid/request.py | 5 ++++- src/pyramid/security.py | 32 +++++++++++++++++++++++++++++--- src/pyramid/testing.py | 2 ++ 3 files changed, 35 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/pyramid/request.py b/src/pyramid/request.py index b9bd7451a..726f485e7 100644 --- a/src/pyramid/request.py +++ b/src/pyramid/request.py @@ -15,7 +15,9 @@ 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, AuthorizationAPIMixin, +) from pyramid.url import URLMethodsMixin from pyramid.util import ( InstancePropertyHelper, @@ -147,6 +149,7 @@ class Request( CallbackMethodsMixin, InstancePropertyMixin, LocalizerRequestMixin, + SecurityAPIMixin, AuthenticationAPIMixin, AuthorizationAPIMixin, ViewMethodsMixin, diff --git a/src/pyramid/security.py b/src/pyramid/security.py index abb9c7442..efc0c193c 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -44,6 +44,11 @@ def _get_registry(request): return reg +def _get_security_policy(request): + registry = _get_registry(request) + return registry.queryUtility(ISecurityPolicy) + + def _get_authentication_policy(request): registry = _get_registry(request) return registry.queryUtility(IAuthenticationPolicy) @@ -281,6 +286,20 @@ class ACLAllowed(ACLPermitsResult, Allowed): """ +class SecurityAPIMixin(object): + @property + def 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. + + """ + policy = _get_security_policy(self) + if policy is None: + return None + return policy.identify(self) + + class AuthenticationAPIMixin(object): @property def authenticated_userid(self): @@ -288,7 +307,10 @@ class AuthenticationAPIMixin(object): ``None`` if there is no :term:`authentication policy` in effect or there is no currently authenticated user. - .. versionadded:: 1.5 + .. deprecated:: 2.0 + + Use ``request.identity`` instead. + """ policy = _get_authentication_policy(self) if policy is None: @@ -305,7 +327,10 @@ class AuthenticationAPIMixin(object): effective authentication policy will not ensure that a record associated with the userid exists in persistent storage. - .. versionadded:: 1.5 + .. deprecated:: 2.0 + + Use ``request.identity`` instead. + """ policy = _get_authentication_policy(self) if policy is None: @@ -319,7 +344,8 @@ class AuthenticationAPIMixin(object): this will return a one-element list containing the :data:`pyramid.security.Everyone` principal. - .. versionadded:: 1.5 + .. deprecated:: 2.0 + """ policy = _get_authentication_policy(self) if policy is None: diff --git a/src/pyramid/testing.py b/src/pyramid/testing.py index ffddd233f..7a85aff85 100644 --- a/src/pyramid/testing.py +++ b/src/pyramid/testing.py @@ -17,6 +17,7 @@ from pyramid.registry import Registry from pyramid.security import ( Authenticated, Everyone, + SecurityAPIMixin, AuthenticationAPIMixin, AuthorizationAPIMixin, ) @@ -303,6 +304,7 @@ class DummyRequest( CallbackMethodsMixin, InstancePropertyMixin, LocalizerRequestMixin, + SecurityAPIMixin, AuthenticationAPIMixin, AuthorizationAPIMixin, ViewMethodsMixin, -- cgit v1.2.3 From 140fdbb54c467159313ede564dd3ad4077e30f20 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sat, 2 Mar 2019 11:26:37 -0800 Subject: Implement bw-compat authenticated_userid and unauthenticated_userid --- src/pyramid/request.py | 4 +++- src/pyramid/security.py | 20 ++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/pyramid/request.py b/src/pyramid/request.py index 726f485e7..bb0dcaa2b 100644 --- a/src/pyramid/request.py +++ b/src/pyramid/request.py @@ -16,7 +16,9 @@ from pyramid.decorator import reify from pyramid.i18n import LocalizerRequestMixin from pyramid.response import Response, _get_response_factory from pyramid.security import ( - SecurityAPIMixin, AuthenticationAPIMixin, AuthorizationAPIMixin, + SecurityAPIMixin, + AuthenticationAPIMixin, + AuthorizationAPIMixin, ) from pyramid.url import URLMethodsMixin from pyramid.util import ( diff --git a/src/pyramid/security.py b/src/pyramid/security.py index efc0c193c..66e314f79 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -312,10 +312,14 @@ class AuthenticationAPIMixin(object): Use ``request.identity`` instead. """ - 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.authenticated_userid(self) + elif security is not None: + return security.identify(self) + else: return None - return policy.authenticated_userid(self) @property def unauthenticated_userid(self): @@ -332,10 +336,14 @@ class AuthenticationAPIMixin(object): Use ``request.identity`` instead. """ - 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 security.identify(self) + else: return None - return policy.unauthenticated_userid(self) @property def effective_principals(self): -- cgit v1.2.3 From 5abdd1d7636a8f7c5cda4c8fcf2669c3937c1186 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 3 Mar 2019 08:49:58 -0800 Subject: Implement new request.has_permission. Deleted AuthorizationAPIMixin --- src/pyramid/request.py | 7 +----- src/pyramid/security.py | 67 +++++++++++++++++++++---------------------------- src/pyramid/testing.py | 2 -- 3 files changed, 29 insertions(+), 47 deletions(-) (limited to 'src') diff --git a/src/pyramid/request.py b/src/pyramid/request.py index bb0dcaa2b..5c68abe69 100644 --- a/src/pyramid/request.py +++ b/src/pyramid/request.py @@ -15,11 +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 ( - SecurityAPIMixin, - AuthenticationAPIMixin, - AuthorizationAPIMixin, -) +from pyramid.security import SecurityAPIMixin, AuthenticationAPIMixin from pyramid.url import URLMethodsMixin from pyramid.util import ( InstancePropertyHelper, @@ -153,7 +149,6 @@ class Request( LocalizerRequestMixin, SecurityAPIMixin, AuthenticationAPIMixin, - AuthorizationAPIMixin, ViewMethodsMixin, ): """ diff --git a/src/pyramid/security.py b/src/pyramid/security.py index 66e314f79..4881d94a6 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -299,6 +299,34 @@ class SecurityAPIMixin(object): return None return policy.identify(self) + 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 @@ -361,45 +389,6 @@ class AuthenticationAPIMixin(object): return policy.effective_principals(self) -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`. - - .. 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) - - @implementer(ISecurityPolicy) class LegacySecurityPolicy: """ diff --git a/src/pyramid/testing.py b/src/pyramid/testing.py index 7a85aff85..90a49c04a 100644 --- a/src/pyramid/testing.py +++ b/src/pyramid/testing.py @@ -19,7 +19,6 @@ from pyramid.security import ( Everyone, SecurityAPIMixin, AuthenticationAPIMixin, - AuthorizationAPIMixin, ) from pyramid.threadlocal import get_current_registry, manager @@ -306,7 +305,6 @@ class DummyRequest( LocalizerRequestMixin, SecurityAPIMixin, AuthenticationAPIMixin, - AuthorizationAPIMixin, ViewMethodsMixin, ): """ A DummyRequest object (incompletely) imitates a :term:`request` object. -- cgit v1.2.3 From edf7ef0c379361f3a056014b068a01657decfb76 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sat, 9 Mar 2019 12:02:24 -0800 Subject: Implement secured view deriver. Some tests still need fixing. --- src/pyramid/viewderivers.py | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/pyramid/viewderivers.py b/src/pyramid/viewderivers.py index 181cc9e5c..22659d2a3 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) -- cgit v1.2.3 From 027d3cbd461be0555e7f4e44b508428228a4b56f Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sat, 9 Mar 2019 12:39:41 -0800 Subject: Revamp tests for EffectivePrincipalsPredicate. --- src/pyramid/predicates.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src') diff --git a/src/pyramid/predicates.py b/src/pyramid/predicates.py index 5a1127fb3..974f41cc5 100644 --- a/src/pyramid/predicates.py +++ b/src/pyramid/predicates.py @@ -291,6 +291,13 @@ class PhysicalPathPredicate(object): class EffectivePrincipalsPredicate(object): + """ + .. deprecated:: 2.0 + + No longer applicable with the new :term:`security policy`. + + """ + def __init__(self, val, config): if is_nonstr_iter(val): self.val = set(val) -- cgit v1.2.3 From 7cdaabd655b3ba1deba6c22ef7c956529e51cb79 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 3 Mar 2019 09:01:12 -0800 Subject: Reimplement remember and forget. This breaks some more tests. --- src/pyramid/security.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/pyramid/security.py b/src/pyramid/security.py index 4881d94a6..9088a9746 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -60,7 +60,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): @@ -73,10 +73,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 @@ -85,7 +85,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) @@ -107,10 +107,10 @@ 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) @@ -132,6 +132,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) -- cgit v1.2.3 From 94a16a7ddd7e9d313e1b447bf478bc51c053e58a Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sat, 9 Mar 2019 12:29:23 -0800 Subject: Implement new dummy security policy. --- src/pyramid/config/testing.py | 15 ++++----------- src/pyramid/testing.py | 40 ++++++++-------------------------------- 2 files changed, 12 insertions(+), 43 deletions(-) (limited to 'src') 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/testing.py b/src/pyramid/testing.py index 90a49c04a..4bf6d281f 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, - SecurityAPIMixin, - AuthenticationAPIMixin, -) +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 identify(self, request): + return self.identity - def unauthenticated_userid(self, request): - return self.userid - - 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): """ -- cgit v1.2.3 From 8a1f8d4acc77cd9fe57405384b5c20e9fa3fb078 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sat, 9 Mar 2019 13:27:13 -0800 Subject: Get integration tests working again. --- src/pyramid/config/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py index 42554db97..37dfeac3e 100644 --- a/src/pyramid/config/security.py +++ b/src/pyramid/config/security.py @@ -49,7 +49,7 @@ class SecurityConfiguratorMixin(object): ) intr['policy'] = policy # authentication policy used by view config (phase 3) - self.action(IAuthenticationPolicy, register, introspectables=(intr,)) + self.action(ISecurityPolicy, register, introspectables=(intr,)) def _set_security_policy(self, policy): policy = self.maybe_dotted(policy) -- cgit v1.2.3 From 282a59b05704c10307a5def54fe3531cfd49dcb7 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 17 Mar 2019 10:52:44 -0700 Subject: Clean up configurator methods. --- src/pyramid/config/security.py | 38 +++++++++++++------------------------- 1 file changed, 13 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py index 37dfeac3e..ac7dcef43 100644 --- a/src/pyramid/config/security.py +++ b/src/pyramid/config/security.py @@ -39,8 +39,9 @@ class SecurityConfiguratorMixin(object): """ def register(): - self._set_security_policy(policy) + self.registry.registerUtility(policy, ISecurityPolicy) + policy = self.maybe_dotted(policy) intr = self.introspectable( 'security policy', None, @@ -51,10 +52,6 @@ class SecurityConfiguratorMixin(object): # authentication policy used by view config (phase 3) self.action(ISecurityPolicy, register, introspectables=(intr,)) - def _set_security_policy(self, policy): - policy = self.maybe_dotted(policy) - self.registry.registerUtility(policy, ISecurityPolicy) - @action_method def set_authentication_policy(self, policy): """ Override the :app:`Pyramid` :term:`authentication policy` in the @@ -71,15 +68,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)' ) - self._set_legacy_policy() + 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, @@ -95,19 +99,6 @@ class SecurityConfiguratorMixin(object): introspectables=(intr,), ) - def _set_authentication_policy(self, policy): - policy = self.maybe_dotted(policy) - self.registry.registerUtility(policy, IAuthenticationPolicy) - - def _set_legacy_policy(self): - if self.registry.queryUtility(ISecurityPolicy) is not None: - raise ConfigurationError( - 'Cannot configure an authentication and authorization policy ' - 'with a configured security policy.' - ) - policy = LegacySecurityPolicy() - self.registry.registerUtility(policy, ISecurityPolicy) - @action_method def set_authorization_policy(self, policy): """ Override the :app:`Pyramid` :term:`authorization policy` in the @@ -123,7 +114,7 @@ class SecurityConfiguratorMixin(object): """ def register(): - self._set_authorization_policy(policy) + self.registry.registerUtility(policy, IAuthorizationPolicy) def ensure(): if self.autocommit: @@ -135,6 +126,7 @@ class SecurityConfiguratorMixin(object): '(use the set_authorization_policy method)' ) + policy = self.maybe_dotted(policy) intr = self.introspectable( 'authorization policy', None, @@ -152,10 +144,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): """ -- cgit v1.2.3 From 31998bcdd0396316c1a0fdeb50bee59e4b9e14ed Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sat, 30 Mar 2019 10:08:31 -0700 Subject: Implement pyramid.security.ACLHelper Mostly a lift-and-shift of the code in ACLAuthorizationPolicy. --- src/pyramid/authorization.py | 79 +++-------------------- src/pyramid/security.py | 145 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 140 insertions(+), 84 deletions(-) (limited to 'src') diff --git a/src/pyramid/authorization.py b/src/pyramid/authorization.py index 6056a8d25..19b96e3d1 100644 --- a/src/pyramid/authorization.py +++ b/src/pyramid/authorization.py @@ -2,11 +2,7 @@ from zope.interface import implementer from pyramid.interfaces import IAuthorizationPolicy -from pyramid.location import lineage - -from pyramid.security import ACLAllowed, ACLDenied, Allow, Deny, Everyone - -from pyramid.util import is_nonstr_iter +from pyramid.security import ACLHelper @implementer(IAuthorizationPolicy) @@ -61,80 +57,21 @@ class ACLAuthorizationPolicy(object): :class:`pyramid.interfaces.IAuthorizationPolicy` interface. """ + 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.""" - - acl = '' - - for location in lineage(context): - try: - acl = location.__acl__ - except AttributeError: - continue - - if acl and callable(acl): - acl = acl() - - for ace in acl: - ace_action, ace_principal, ace_permissions = ace - if ace_principal in principals: - if not is_nonstr_iter(ace_permissions): - ace_permissions = [ace_permissions] - if permission in ace_permissions: - if ace_action == Allow: - return ACLAllowed( - ace, acl, permission, principals, location - ) - else: - return ACLDenied( - ace, acl, permission, principals, location - ) - - # default deny (if no ACL in lineage at all, or if none of the - # principals were mentioned in any ACE we found) - return ACLDenied( - '', acl, permission, principals, context - ) + 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`.""" - allowed = set() - - for location in reversed(list(lineage(context))): - # NB: we're walking *up* the object graph from the root - try: - acl = location.__acl__ - except AttributeError: - continue - - allowed_here = set() - denied_here = set() - - if acl and callable(acl): - acl = acl() - - for ace_action, ace_principal, ace_permissions in acl: - if not is_nonstr_iter(ace_permissions): - ace_permissions = [ace_permissions] - if (ace_action == Allow) and (permission in ace_permissions): - if ace_principal not in denied_here: - allowed_here.add(ace_principal) - if (ace_action == Deny) and (permission in ace_permissions): - denied_here.add(ace_principal) - if ace_principal == Everyone: - # clear the entire allowed set, as we've hit a - # deny of Everyone ala (Deny, Everyone, ALL) - allowed = set() - break - elif ace_principal in allowed: - allowed.remove(ace_principal) - - allowed.update(allowed_here) - - return allowed + return self.helper.principals_allowed_by_permission( + context, permission + ) diff --git a/src/pyramid/security.py b/src/pyramid/security.py index 9088a9746..bfd505a98 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -9,6 +9,10 @@ from pyramid.interfaces import ( IViewClassifier, ) +from pyramid.location import lineage + +from pyramid.util import is_nonstr_iter + from pyramid.threadlocal import get_current_registry Everyone = 'system.Everyone' @@ -36,22 +40,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): - registry = _get_registry(request) - return registry.queryUtility(ISecurityPolicy) + 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): @@ -154,7 +148,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 @@ -421,3 +415,128 @@ class LegacySecurityPolicy: authz = self._get_authz_policy(request) principals = authn.effective_principals(request) return authz.permits(context, principals, 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 = '' + + for location in lineage(context): + try: + acl = location.__acl__ + except AttributeError: + continue + + if acl and callable(acl): + acl = acl() + + for ace in acl: + ace_action, ace_principal, ace_permissions = ace + if ace_principal in principals: + if not is_nonstr_iter(ace_permissions): + ace_permissions = [ace_permissions] + if permission in ace_permissions: + if ace_action == Allow: + return ACLAllowed( + ace, acl, permission, principals, location + ) + else: + return ACLDenied( + ace, acl, permission, principals, location + ) + + # default deny (if no ACL in lineage at all, or if none of the + # principals were mentioned in any ACE we found) + return ACLDenied( + '', acl, permission, principals, context + ) + + 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`. + + 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))): + # NB: we're walking *up* the object graph from the root + try: + acl = location.__acl__ + except AttributeError: + continue + + allowed_here = set() + denied_here = set() + + if acl and callable(acl): + acl = acl() + + for ace_action, ace_principal, ace_permissions in acl: + if not is_nonstr_iter(ace_permissions): + ace_permissions = [ace_permissions] + if (ace_action == Allow) and (permission in ace_permissions): + if ace_principal not in denied_here: + allowed_here.add(ace_principal) + if (ace_action == Deny) and (permission in ace_permissions): + denied_here.add(ace_principal) + if ace_principal == Everyone: + # clear the entire allowed set, as we've hit a + # deny of Everyone ala (Deny, Everyone, ALL) + allowed = set() + break + elif ace_principal in allowed: + allowed.remove(ace_principal) + + allowed.update(allowed_here) + + return allowed -- cgit v1.2.3 From 6aba89d19cc384021864d3b83d53082f56c3f419 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sat, 30 Mar 2019 10:31:17 -0700 Subject: Add SessionAuthenticationHelper. --- src/pyramid/security.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'src') diff --git a/src/pyramid/security.py b/src/pyramid/security.py index bfd505a98..671cd3569 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -540,3 +540,34 @@ class ACLHelper: allowed.update(allowed_here) return allowed + + +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.""" + request.session[self.userid_key] = userid + return [] + + def forget(self, request): + """ Remove the stored userid from the session.""" + if self.userid_key in request.session: + del request.session[self.userid_key] + return [] + + def identify(self, request): + return request.session.get(self.userid_key) -- cgit v1.2.3 From 9f267dd842c5e93336f0392f2809da75a716039a Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sat, 30 Mar 2019 11:09:44 -0700 Subject: Migrate AuthTktCookieHelper to pyramid.security. --- src/pyramid/authentication.py | 444 +-------------------------------- src/pyramid/security.py | 558 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 570 insertions(+), 432 deletions(-) (limited to 'src') diff --git a/src/pyramid/authentication.py b/src/pyramid/authentication.py index 21cfc0c0e..12f31e5dd 100644 --- a/src/pyramid/authentication.py +++ b/src/pyramid/authentication.py @@ -1,26 +1,23 @@ import binascii -from codecs import utf_8_decode -from codecs import utf_8_encode from collections import namedtuple -import hashlib -import base64 -import re -import time as time_mod -from urllib.parse import quote, unquote -import warnings from zope.interface import implementer -from webob.cookies import CookieProfile - from pyramid.interfaces import IAuthenticationPolicy, IDebugLogger -from pyramid.security import Authenticated, Everyone - -from pyramid.util import strings_differ, bytes_, ascii_, text_ -from pyramid.util import SimpleSerializer - -VALID_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*$") +from pyramid.security import Authenticated, Everyone, AuthTktCookieHelper + +# bw compat after moving AuthTktHelper and friends to pyramid.security +from pyramid.security import ( # noqa + VALID_TOKEN, + b64encode, + b64decode, + AuthTicket, + BadTicket, + parse_ticket, + calculate_digest, + encode_ip_timestamp, +) class CallbackAuthenticationPolicy(object): @@ -651,421 +648,6 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): return self.cookie.forget(request) -def b64encode(v): - return base64.b64encode(bytes_(v)).strip().replace(b'\n', b'') - - -def b64decode(v): - return base64.b64decode(bytes_(v)) - - -# this class licensed under the MIT license (stolen from Paste) -class AuthTicket(object): - """ - This class represents an authentication token. You must pass in - the shared secret, the userid, and the IP address. Optionally you - can include tokens (a list of strings, representing role names), - 'user_data', which is arbitrary data available for your own use in - later scripts. Lastly, you can override the cookie name and - timestamp. - - Once you provide all the arguments, use .cookie_value() to - generate the appropriate authentication ticket. - - Usage:: - - token = AuthTicket('sharedsecret', 'username', - os.environ['REMOTE_ADDR'], tokens=['admin']) - val = token.cookie_value() - - """ - - def __init__( - self, - secret, - userid, - ip, - tokens=(), - user_data='', - time=None, - cookie_name='auth_tkt', - secure=False, - hashalg='md5', - ): - self.secret = secret - self.userid = userid - self.ip = ip - self.tokens = ','.join(tokens) - self.user_data = user_data - if time is None: - self.time = time_mod.time() - else: - self.time = time - self.cookie_name = cookie_name - self.secure = secure - self.hashalg = hashalg - - def digest(self): - return calculate_digest( - self.ip, - self.time, - self.secret, - self.userid, - self.tokens, - self.user_data, - self.hashalg, - ) - - def cookie_value(self): - v = '%s%08x%s!' % (self.digest(), int(self.time), quote(self.userid)) - if self.tokens: - v += self.tokens + '!' - v += self.user_data - return v - - -# this class licensed under the MIT license (stolen from Paste) -class BadTicket(Exception): - """ - Exception raised when a ticket can't be parsed. If we get far enough to - determine what the expected digest should have been, expected is set. - This should not be shown by default, but can be useful for debugging. - """ - - def __init__(self, msg, expected=None): - self.expected = expected - Exception.__init__(self, msg) - - -# this function licensed under the MIT license (stolen from Paste) -def parse_ticket(secret, ticket, ip, hashalg='md5'): - """ - Parse the ticket, returning (timestamp, userid, tokens, user_data). - - If the ticket cannot be parsed, a ``BadTicket`` exception will be raised - with an explanation. - """ - ticket = text_(ticket).strip('"') - digest_size = hashlib.new(hashalg).digest_size * 2 - digest = ticket[:digest_size] - try: - timestamp = int(ticket[digest_size : digest_size + 8], 16) - except ValueError as e: - raise BadTicket('Timestamp is not a hex integer: %s' % e) - try: - userid, data = ticket[digest_size + 8 :].split('!', 1) - except ValueError: - raise BadTicket('userid is not followed by !') - userid = unquote(userid) - if '!' in data: - tokens, user_data = data.split('!', 1) - else: # pragma: no cover (never generated) - # @@: Is this the right order? - tokens = '' - user_data = data - - expected = calculate_digest( - ip, timestamp, secret, userid, tokens, user_data, hashalg - ) - - # Avoid timing attacks (see - # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) - if strings_differ(expected, digest): - raise BadTicket( - 'Digest signature is not correct', expected=(expected, digest) - ) - - tokens = tokens.split(',') - - return (timestamp, userid, tokens, user_data) - - -# this function licensed under the MIT license (stolen from Paste) -def calculate_digest( - ip, timestamp, secret, userid, tokens, user_data, hashalg='md5' -): - secret = bytes_(secret, 'utf-8') - userid = bytes_(userid, 'utf-8') - tokens = bytes_(tokens, 'utf-8') - user_data = bytes_(user_data, 'utf-8') - hash_obj = hashlib.new(hashalg) - - # Check to see if this is an IPv6 address - if ':' in ip: - ip_timestamp = ip + str(int(timestamp)) - ip_timestamp = bytes_(ip_timestamp) - else: - # encode_ip_timestamp not required, left in for backwards compatibility - ip_timestamp = encode_ip_timestamp(ip, timestamp) - - hash_obj.update( - ip_timestamp + secret + userid + b'\0' + tokens + b'\0' + user_data - ) - digest = hash_obj.hexdigest() - hash_obj2 = hashlib.new(hashalg) - hash_obj2.update(bytes_(digest) + secret) - return hash_obj2.hexdigest() - - -# this function licensed under the MIT license (stolen from Paste) -def encode_ip_timestamp(ip, timestamp): - ip_chars = ''.join(map(chr, map(int, ip.split('.')))) - t = int(timestamp) - ts = ( - (t & 0xFF000000) >> 24, - (t & 0xFF0000) >> 16, - (t & 0xFF00) >> 8, - t & 0xFF, - ) - ts_chars = ''.join(map(chr, ts)) - return bytes_(ip_chars + ts_chars) - - -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. - """ - - parse_ticket = staticmethod(parse_ticket) # for tests - AuthTicket = AuthTicket # for tests - BadTicket = BadTicket # for tests - now = None # for tests - - userid_type_decoders = { - 'int': int, - 'unicode': lambda x: utf_8_decode(x)[0], # bw compat for old cookies - 'b64unicode': lambda x: utf_8_decode(b64decode(x))[0], - 'b64str': lambda x: b64decode(x), - } - - userid_type_encoders = { - int: ('int', str), - str: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])), - bytes: ('b64str', lambda x: b64encode(x)), - } - - def __init__( - self, - secret, - cookie_name='auth_tkt', - secure=False, - include_ip=False, - timeout=None, - reissue_time=None, - max_age=None, - http_only=False, - path="/", - wild_domain=True, - hashalg='md5', - parent_domain=False, - domain=None, - samesite='Lax', - ): - self.cookie_profile = CookieProfile( - cookie_name=cookie_name, - secure=secure, - max_age=max_age, - httponly=http_only, - path=path, - serializer=SimpleSerializer(), - samesite=samesite, - ) - - self.secret = secret - self.cookie_name = cookie_name - self.secure = secure - self.include_ip = include_ip - self.timeout = timeout if timeout is None else int(timeout) - self.reissue_time = ( - reissue_time if reissue_time is None else int(reissue_time) - ) - self.max_age = max_age if max_age is None else int(max_age) - self.wild_domain = wild_domain - self.parent_domain = parent_domain - self.domain = domain - self.hashalg = hashalg - - def _get_cookies(self, request, value, max_age=None): - cur_domain = request.domain - - domains = [] - if self.domain: - domains.append(self.domain) - else: - if self.parent_domain and cur_domain.count('.') > 1: - domains.append('.' + cur_domain.split('.', 1)[1]) - else: - domains.append(None) - domains.append(cur_domain) - if self.wild_domain: - domains.append('.' + cur_domain) - - profile = self.cookie_profile(request) - - kw = {} - kw['domains'] = domains - if max_age is not None: - kw['max_age'] = max_age - - headers = profile.get_headers(value, **kw) - return headers - - def identify(self, request): - """ Return a dictionary with authentication information, or ``None`` - if no valid auth_tkt is attached to ``request``""" - environ = request.environ - cookie = request.cookies.get(self.cookie_name) - - if cookie is None: - return None - - if self.include_ip: - remote_addr = environ['REMOTE_ADDR'] - else: - remote_addr = '0.0.0.0' - - try: - timestamp, userid, tokens, user_data = self.parse_ticket( - self.secret, cookie, remote_addr, self.hashalg - ) - except self.BadTicket: - return None - - now = self.now # service tests - - if now is None: - now = time_mod.time() - - if self.timeout and ((timestamp + self.timeout) < now): - # the auth_tkt data has expired - return None - - userid_typename = 'userid_type:' - user_data_info = user_data.split('|') - for datum in filter(None, user_data_info): - if datum.startswith(userid_typename): - userid_type = datum[len(userid_typename) :] - decoder = self.userid_type_decoders.get(userid_type) - if decoder: - userid = decoder(userid) - - reissue = self.reissue_time is not None - - if reissue and not hasattr(request, '_authtkt_reissued'): - if (now - timestamp) > self.reissue_time: - # See https://github.com/Pylons/pyramid/issues#issue/108 - tokens = list(filter(None, tokens)) - headers = self.remember( - request, userid, max_age=self.max_age, tokens=tokens - ) - - def reissue_authtkt(request, response): - if not hasattr(request, '_authtkt_reissue_revoked'): - for k, v in headers: - response.headerlist.append((k, v)) - - request.add_response_callback(reissue_authtkt) - request._authtkt_reissued = True - - environ['REMOTE_USER_TOKENS'] = tokens - environ['REMOTE_USER_DATA'] = user_data - environ['AUTH_TYPE'] = 'cookie' - - identity = {} - identity['timestamp'] = timestamp - identity['userid'] = userid - identity['tokens'] = tokens - identity['userdata'] = user_data - return identity - - def forget(self, request): - """ Return a set of expires Set-Cookie headers, which will destroy - any existing auth_tkt cookie when attached to a response""" - request._authtkt_reissue_revoked = True - return self._get_cookies(request, None) - - def remember(self, request, userid, max_age=None, tokens=()): - """ Return a set of Set-Cookie headers; when set into a response, - these headers will represent a valid authentication ticket. - - ``max_age`` - The max age of the auth_tkt cookie, in seconds. 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. If - this value is ``None``, the ``max_age`` value provided to the - helper itself will be used as the ``max_age`` value. Default: - ``None``. - - ``tokens`` - A sequence of strings that will be placed into the auth_tkt tokens - field. Each string in the sequence must be of the Python ``str`` - type and must match the regex ``^[A-Za-z][A-Za-z0-9+_-]*$``. - Tokens are available in the returned identity when an auth_tkt is - found in the request and unpacked. Default: ``()``. - """ - max_age = self.max_age if max_age is None else int(max_age) - - environ = request.environ - - if self.include_ip: - remote_addr = environ['REMOTE_ADDR'] - else: - remote_addr = '0.0.0.0' - - user_data = '' - - encoding_data = self.userid_type_encoders.get(type(userid)) - - if encoding_data: - encoding, encoder = encoding_data - else: - warnings.warn( - "userid is of type {}, and is not supported by the " - "AuthTktAuthenticationPolicy. Explicitly converting to string " - "and storing as base64. Subsequent requests will receive a " - "string as the userid, it will not be decoded back to the " - "type provided.".format(type(userid)), - RuntimeWarning, - ) - encoding, encoder = self.userid_type_encoders.get(str) - userid = str(userid) - - userid = encoder(userid) - user_data = 'userid_type:%s' % encoding - - new_tokens = [] - for token in tokens: - if isinstance(token, str): - try: - token = ascii_(token) - except UnicodeEncodeError: - raise ValueError("Invalid token %r" % (token,)) - if not (isinstance(token, str) and VALID_TOKEN.match(token)): - raise ValueError("Invalid token %r" % (token,)) - new_tokens.append(token) - tokens = tuple(new_tokens) - - if hasattr(request, '_authtkt_reissued'): - request._authtkt_reissue_revoked = True - - ticket = self.AuthTicket( - self.secret, - userid, - remote_addr, - tokens=tokens, - user_data=user_data, - cookie_name=self.cookie_name, - secure=self.secure, - hashalg=self.hashalg, - ) - - cookie_value = ticket.cookie_value() - return self._get_cookies(request, cookie_value, max_age) - - @implementer(IAuthenticationPolicy) class SessionAuthenticationPolicy(CallbackAuthenticationPolicy): """ A :app:`Pyramid` authentication policy which gets its data from the diff --git a/src/pyramid/security.py b/src/pyramid/security.py index 671cd3569..e28ce0f1c 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -1,3 +1,14 @@ +from codecs import utf_8_decode +from codecs import utf_8_encode +import hashlib +import base64 +import time as time_mod +from urllib.parse import quote, unquote +import warnings +import re + +from webob.cookies import CookieProfile + from zope.interface import implementer, providedBy from pyramid.interfaces import ( @@ -11,10 +22,14 @@ from pyramid.interfaces import ( from pyramid.location import lineage -from pyramid.util import is_nonstr_iter +from pyramid.util import is_nonstr_iter, strings_differ, bytes_, ascii_, text_ + +from pyramid.util import SimpleSerializer from pyramid.threadlocal import get_current_registry +VALID_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*$") + Everyone = 'system.Everyone' Authenticated = 'system.Authenticated' Allow = 'Allow' @@ -571,3 +586,544 @@ class SessionAuthenticationHelper: def identify(self, request): return request.session.get(self.userid_key) + + +def b64encode(v): + return base64.b64encode(bytes_(v)).strip().replace(b'\n', b'') + + +def b64decode(v): + return base64.b64decode(bytes_(v)) + + +# this class licensed under the MIT license (stolen from Paste) +class AuthTicket(object): + """ + This class represents an authentication token. You must pass in + the shared secret, the userid, and the IP address. Optionally you + can include tokens (a list of strings, representing role names), + 'user_data', which is arbitrary data available for your own use in + later scripts. Lastly, you can override the cookie name and + timestamp. + + Once you provide all the arguments, use .cookie_value() to + generate the appropriate authentication ticket. + + Usage:: + + token = AuthTicket('sharedsecret', 'username', + os.environ['REMOTE_ADDR'], tokens=['admin']) + val = token.cookie_value() + + """ + + def __init__( + self, + secret, + userid, + ip, + tokens=(), + user_data='', + time=None, + cookie_name='auth_tkt', + secure=False, + hashalg='md5', + ): + self.secret = secret + self.userid = userid + self.ip = ip + self.tokens = ','.join(tokens) + self.user_data = user_data + if time is None: + self.time = time_mod.time() + else: + self.time = time + self.cookie_name = cookie_name + self.secure = secure + self.hashalg = hashalg + + def digest(self): + return calculate_digest( + self.ip, + self.time, + self.secret, + self.userid, + self.tokens, + self.user_data, + self.hashalg, + ) + + def cookie_value(self): + v = '%s%08x%s!' % (self.digest(), int(self.time), quote(self.userid)) + if self.tokens: + v += self.tokens + '!' + v += self.user_data + return v + + +# this class licensed under the MIT license (stolen from Paste) +class BadTicket(Exception): + """ + Exception raised when a ticket can't be parsed. If we get far enough to + determine what the expected digest should have been, expected is set. + This should not be shown by default, but can be useful for debugging. + """ + + def __init__(self, msg, expected=None): + self.expected = expected + Exception.__init__(self, msg) + + +# this function licensed under the MIT license (stolen from Paste) +def parse_ticket(secret, ticket, ip, hashalg='md5'): + """ + Parse the ticket, returning (timestamp, userid, tokens, user_data). + + If the ticket cannot be parsed, a ``BadTicket`` exception will be raised + with an explanation. + """ + ticket = text_(ticket).strip('"') + digest_size = hashlib.new(hashalg).digest_size * 2 + digest = ticket[:digest_size] + try: + timestamp = int(ticket[digest_size : digest_size + 8], 16) + except ValueError as e: + raise BadTicket('Timestamp is not a hex integer: %s' % e) + try: + userid, data = ticket[digest_size + 8 :].split('!', 1) + except ValueError: + raise BadTicket('userid is not followed by !') + userid = unquote(userid) + if '!' in data: + tokens, user_data = data.split('!', 1) + else: # pragma: no cover (never generated) + # @@: Is this the right order? + tokens = '' + user_data = data + + expected = calculate_digest( + ip, timestamp, secret, userid, tokens, user_data, hashalg + ) + + # Avoid timing attacks (see + # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) + if strings_differ(expected, digest): + raise BadTicket( + 'Digest signature is not correct', expected=(expected, digest) + ) + + tokens = tokens.split(',') + + return (timestamp, userid, tokens, user_data) + + +# this function licensed under the MIT license (stolen from Paste) +def calculate_digest( + ip, timestamp, secret, userid, tokens, user_data, hashalg='md5' +): + secret = bytes_(secret, 'utf-8') + userid = bytes_(userid, 'utf-8') + tokens = bytes_(tokens, 'utf-8') + user_data = bytes_(user_data, 'utf-8') + hash_obj = hashlib.new(hashalg) + + # Check to see if this is an IPv6 address + if ':' in ip: + ip_timestamp = ip + str(int(timestamp)) + ip_timestamp = bytes_(ip_timestamp) + else: + # encode_ip_timestamp not required, left in for backwards compatibility + ip_timestamp = encode_ip_timestamp(ip, timestamp) + + hash_obj.update( + ip_timestamp + secret + userid + b'\0' + tokens + b'\0' + user_data + ) + digest = hash_obj.hexdigest() + hash_obj2 = hashlib.new(hashalg) + hash_obj2.update(bytes_(digest) + secret) + return hash_obj2.hexdigest() + + +# this function licensed under the MIT license (stolen from Paste) +def encode_ip_timestamp(ip, timestamp): + ip_chars = ''.join(map(chr, map(int, ip.split('.')))) + t = int(timestamp) + ts = ( + (t & 0xFF000000) >> 24, + (t & 0xFF0000) >> 16, + (t & 0xFF00) >> 8, + t & 0xFF, + ) + ts_chars = ''.join(map(chr, ts)) + return bytes_(ip_chars + ts_chars) + + +class AuthTktCookieHelper: + """ + A helper class used for constructing a :term:`security policy` with stores + the user identity in a signed 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. + + ``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. + + ``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. + """ + + parse_ticket = staticmethod(parse_ticket) # for tests + AuthTicket = AuthTicket # for tests + BadTicket = BadTicket # for tests + now = None # for tests + + userid_type_decoders = { + 'int': int, + 'unicode': lambda x: utf_8_decode(x)[0], # bw compat for old cookies + 'b64unicode': lambda x: utf_8_decode(b64decode(x))[0], + 'b64str': lambda x: b64decode(x), + } + + userid_type_encoders = { + int: ('int', str), + str: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])), + bytes: ('b64str', lambda x: b64encode(x)), + } + + def __init__( + self, + secret, + cookie_name='auth_tkt', + secure=False, + include_ip=False, + timeout=None, + reissue_time=None, + max_age=None, + http_only=False, + path="/", + wild_domain=True, + hashalg='md5', + parent_domain=False, + domain=None, + samesite='Lax', + ): + self.cookie_profile = CookieProfile( + cookie_name=cookie_name, + secure=secure, + max_age=max_age, + httponly=http_only, + path=path, + serializer=SimpleSerializer(), + samesite=samesite, + ) + + self.secret = secret + self.cookie_name = cookie_name + self.secure = secure + self.include_ip = include_ip + self.timeout = timeout if timeout is None else int(timeout) + self.reissue_time = ( + reissue_time if reissue_time is None else int(reissue_time) + ) + self.max_age = max_age if max_age is None else int(max_age) + self.wild_domain = wild_domain + self.parent_domain = parent_domain + self.domain = domain + self.hashalg = hashalg + + def _get_cookies(self, request, value, max_age=None): + cur_domain = request.domain + + domains = [] + if self.domain: + domains.append(self.domain) + else: + if self.parent_domain and cur_domain.count('.') > 1: + domains.append('.' + cur_domain.split('.', 1)[1]) + else: + domains.append(None) + domains.append(cur_domain) + if self.wild_domain: + domains.append('.' + cur_domain) + + profile = self.cookie_profile(request) + + kw = {} + kw['domains'] = domains + if max_age is not None: + kw['max_age'] = max_age + + headers = profile.get_headers(value, **kw) + return headers + + def identify(self, request): + """ Return a dictionary with authentication information, or ``None`` + if no valid auth_tkt is attached to ``request``""" + environ = request.environ + cookie = request.cookies.get(self.cookie_name) + + if cookie is None: + return None + + if self.include_ip: + remote_addr = environ['REMOTE_ADDR'] + else: + remote_addr = '0.0.0.0' + + try: + timestamp, userid, tokens, user_data = self.parse_ticket( + self.secret, cookie, remote_addr, self.hashalg + ) + except self.BadTicket: + return None + + now = self.now # service tests + + if now is None: + now = time_mod.time() + + if self.timeout and ((timestamp + self.timeout) < now): + # the auth_tkt data has expired + return None + + userid_typename = 'userid_type:' + user_data_info = user_data.split('|') + for datum in filter(None, user_data_info): + if datum.startswith(userid_typename): + userid_type = datum[len(userid_typename) :] + decoder = self.userid_type_decoders.get(userid_type) + if decoder: + userid = decoder(userid) + + reissue = self.reissue_time is not None + + if reissue and not hasattr(request, '_authtkt_reissued'): + if (now - timestamp) > self.reissue_time: + # See https://github.com/Pylons/pyramid/issues#issue/108 + tokens = list(filter(None, tokens)) + headers = self.remember( + request, userid, max_age=self.max_age, tokens=tokens + ) + + def reissue_authtkt(request, response): + if not hasattr(request, '_authtkt_reissue_revoked'): + for k, v in headers: + response.headerlist.append((k, v)) + + request.add_response_callback(reissue_authtkt) + request._authtkt_reissued = True + + environ['REMOTE_USER_TOKENS'] = tokens + environ['REMOTE_USER_DATA'] = user_data + environ['AUTH_TYPE'] = 'cookie' + + identity = {} + identity['timestamp'] = timestamp + identity['userid'] = userid + identity['tokens'] = tokens + identity['userdata'] = user_data + return identity + + def forget(self, request): + """ Return a set of expires Set-Cookie headers, which will destroy + any existing auth_tkt cookie when attached to a response""" + request._authtkt_reissue_revoked = True + return self._get_cookies(request, None) + + def remember(self, request, userid, max_age=None, tokens=()): + """ Return a set of Set-Cookie headers; when set into a response, + these headers will represent a valid authentication ticket. + + ``max_age`` + The max age of the auth_tkt cookie, in seconds. 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. If + this value is ``None``, the ``max_age`` value provided to the + helper itself will be used as the ``max_age`` value. Default: + ``None``. + + ``tokens`` + A sequence of strings that will be placed into the auth_tkt tokens + field. Each string in the sequence must be of the Python ``str`` + type and must match the regex ``^[A-Za-z][A-Za-z0-9+_-]*$``. + Tokens are available in the returned identity when an auth_tkt is + found in the request and unpacked. Default: ``()``. + """ + max_age = self.max_age if max_age is None else int(max_age) + + environ = request.environ + + if self.include_ip: + remote_addr = environ['REMOTE_ADDR'] + else: + remote_addr = '0.0.0.0' + + user_data = '' + + encoding_data = self.userid_type_encoders.get(type(userid)) + + if encoding_data: + encoding, encoder = encoding_data + else: + warnings.warn( + "userid is of type {}, and is not supported by the " + "AuthTktAuthenticationPolicy. Explicitly converting to string " + "and storing as base64. Subsequent requests will receive a " + "string as the userid, it will not be decoded back to the " + "type provided.".format(type(userid)), + RuntimeWarning, + ) + encoding, encoder = self.userid_type_encoders.get(str) + userid = str(userid) + + userid = encoder(userid) + user_data = 'userid_type:%s' % encoding + + new_tokens = [] + for token in tokens: + if isinstance(token, str): + try: + token = ascii_(token) + except UnicodeEncodeError: + raise ValueError("Invalid token %r" % (token,)) + if not (isinstance(token, str) and VALID_TOKEN.match(token)): + raise ValueError("Invalid token %r" % (token,)) + new_tokens.append(token) + tokens = tuple(new_tokens) + + if hasattr(request, '_authtkt_reissued'): + request._authtkt_reissue_revoked = True + + ticket = self.AuthTicket( + self.secret, + userid, + remote_addr, + tokens=tokens, + user_data=user_data, + cookie_name=self.cookie_name, + secure=self.secure, + hashalg=self.hashalg, + ) + + cookie_value = ticket.cookie_value() + return self._get_cookies(request, cookie_value, max_age) -- cgit v1.2.3 From 8536f3ab834b4da7cc24a88855a3f883929b5b1e Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Mon, 15 Apr 2019 19:01:37 -0700 Subject: Register security policy in phase 2. --- src/pyramid/config/security.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py index ac7dcef43..e8ea0878d 100644 --- a/src/pyramid/config/security.py +++ b/src/pyramid/config/security.py @@ -49,8 +49,12 @@ class SecurityConfiguratorMixin(object): 'security policy', ) intr['policy'] = policy - # authentication policy used by view config (phase 3) - self.action(ISecurityPolicy, register, introspectables=(intr,)) + self.action( + ISecurityPolicy, + register, + order=PHASE2_CONFIG, + introspectables=(intr,), + ) @action_method def set_authentication_policy(self, policy): -- cgit v1.2.3 From 47f8935bb5423718ec293bba4709307be7d2f51c Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Mon, 15 Apr 2019 19:04:35 -0700 Subject: Stringify identity in legacy authenticated_userid. --- src/pyramid/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/pyramid/security.py b/src/pyramid/security.py index e28ce0f1c..a55320ce6 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -355,7 +355,7 @@ class AuthenticationAPIMixin(object): if authn is not None: return authn.authenticated_userid(self) elif security is not None: - return security.identify(self) + return str(security.identify(self)) else: return None -- cgit v1.2.3 From f4c6c993ded900b4e32d1dd49207ca1e18b11336 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Mon, 15 Apr 2019 19:05:47 -0700 Subject: Revert "Migrate AuthTktCookieHelper to pyramid.security." This reverts commit 9f267dd842c5e93336f0392f2809da75a716039a. --- src/pyramid/authentication.py | 444 ++++++++++++++++++++++++++++++++- src/pyramid/security.py | 558 +----------------------------------------- 2 files changed, 432 insertions(+), 570 deletions(-) (limited to 'src') diff --git a/src/pyramid/authentication.py b/src/pyramid/authentication.py index 12f31e5dd..21cfc0c0e 100644 --- a/src/pyramid/authentication.py +++ b/src/pyramid/authentication.py @@ -1,23 +1,26 @@ import binascii +from codecs import utf_8_decode +from codecs import utf_8_encode from collections import namedtuple +import hashlib +import base64 +import re +import time as time_mod +from urllib.parse import quote, unquote +import warnings from zope.interface import implementer +from webob.cookies import CookieProfile + from pyramid.interfaces import IAuthenticationPolicy, IDebugLogger -from pyramid.security import Authenticated, Everyone, AuthTktCookieHelper - -# bw compat after moving AuthTktHelper and friends to pyramid.security -from pyramid.security import ( # noqa - VALID_TOKEN, - b64encode, - b64decode, - AuthTicket, - BadTicket, - parse_ticket, - calculate_digest, - encode_ip_timestamp, -) +from pyramid.security import Authenticated, Everyone + +from pyramid.util import strings_differ, bytes_, ascii_, text_ +from pyramid.util import SimpleSerializer + +VALID_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*$") class CallbackAuthenticationPolicy(object): @@ -648,6 +651,421 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): return self.cookie.forget(request) +def b64encode(v): + return base64.b64encode(bytes_(v)).strip().replace(b'\n', b'') + + +def b64decode(v): + return base64.b64decode(bytes_(v)) + + +# this class licensed under the MIT license (stolen from Paste) +class AuthTicket(object): + """ + This class represents an authentication token. You must pass in + the shared secret, the userid, and the IP address. Optionally you + can include tokens (a list of strings, representing role names), + 'user_data', which is arbitrary data available for your own use in + later scripts. Lastly, you can override the cookie name and + timestamp. + + Once you provide all the arguments, use .cookie_value() to + generate the appropriate authentication ticket. + + Usage:: + + token = AuthTicket('sharedsecret', 'username', + os.environ['REMOTE_ADDR'], tokens=['admin']) + val = token.cookie_value() + + """ + + def __init__( + self, + secret, + userid, + ip, + tokens=(), + user_data='', + time=None, + cookie_name='auth_tkt', + secure=False, + hashalg='md5', + ): + self.secret = secret + self.userid = userid + self.ip = ip + self.tokens = ','.join(tokens) + self.user_data = user_data + if time is None: + self.time = time_mod.time() + else: + self.time = time + self.cookie_name = cookie_name + self.secure = secure + self.hashalg = hashalg + + def digest(self): + return calculate_digest( + self.ip, + self.time, + self.secret, + self.userid, + self.tokens, + self.user_data, + self.hashalg, + ) + + def cookie_value(self): + v = '%s%08x%s!' % (self.digest(), int(self.time), quote(self.userid)) + if self.tokens: + v += self.tokens + '!' + v += self.user_data + return v + + +# this class licensed under the MIT license (stolen from Paste) +class BadTicket(Exception): + """ + Exception raised when a ticket can't be parsed. If we get far enough to + determine what the expected digest should have been, expected is set. + This should not be shown by default, but can be useful for debugging. + """ + + def __init__(self, msg, expected=None): + self.expected = expected + Exception.__init__(self, msg) + + +# this function licensed under the MIT license (stolen from Paste) +def parse_ticket(secret, ticket, ip, hashalg='md5'): + """ + Parse the ticket, returning (timestamp, userid, tokens, user_data). + + If the ticket cannot be parsed, a ``BadTicket`` exception will be raised + with an explanation. + """ + ticket = text_(ticket).strip('"') + digest_size = hashlib.new(hashalg).digest_size * 2 + digest = ticket[:digest_size] + try: + timestamp = int(ticket[digest_size : digest_size + 8], 16) + except ValueError as e: + raise BadTicket('Timestamp is not a hex integer: %s' % e) + try: + userid, data = ticket[digest_size + 8 :].split('!', 1) + except ValueError: + raise BadTicket('userid is not followed by !') + userid = unquote(userid) + if '!' in data: + tokens, user_data = data.split('!', 1) + else: # pragma: no cover (never generated) + # @@: Is this the right order? + tokens = '' + user_data = data + + expected = calculate_digest( + ip, timestamp, secret, userid, tokens, user_data, hashalg + ) + + # Avoid timing attacks (see + # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) + if strings_differ(expected, digest): + raise BadTicket( + 'Digest signature is not correct', expected=(expected, digest) + ) + + tokens = tokens.split(',') + + return (timestamp, userid, tokens, user_data) + + +# this function licensed under the MIT license (stolen from Paste) +def calculate_digest( + ip, timestamp, secret, userid, tokens, user_data, hashalg='md5' +): + secret = bytes_(secret, 'utf-8') + userid = bytes_(userid, 'utf-8') + tokens = bytes_(tokens, 'utf-8') + user_data = bytes_(user_data, 'utf-8') + hash_obj = hashlib.new(hashalg) + + # Check to see if this is an IPv6 address + if ':' in ip: + ip_timestamp = ip + str(int(timestamp)) + ip_timestamp = bytes_(ip_timestamp) + else: + # encode_ip_timestamp not required, left in for backwards compatibility + ip_timestamp = encode_ip_timestamp(ip, timestamp) + + hash_obj.update( + ip_timestamp + secret + userid + b'\0' + tokens + b'\0' + user_data + ) + digest = hash_obj.hexdigest() + hash_obj2 = hashlib.new(hashalg) + hash_obj2.update(bytes_(digest) + secret) + return hash_obj2.hexdigest() + + +# this function licensed under the MIT license (stolen from Paste) +def encode_ip_timestamp(ip, timestamp): + ip_chars = ''.join(map(chr, map(int, ip.split('.')))) + t = int(timestamp) + ts = ( + (t & 0xFF000000) >> 24, + (t & 0xFF0000) >> 16, + (t & 0xFF00) >> 8, + t & 0xFF, + ) + ts_chars = ''.join(map(chr, ts)) + return bytes_(ip_chars + ts_chars) + + +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. + """ + + parse_ticket = staticmethod(parse_ticket) # for tests + AuthTicket = AuthTicket # for tests + BadTicket = BadTicket # for tests + now = None # for tests + + userid_type_decoders = { + 'int': int, + 'unicode': lambda x: utf_8_decode(x)[0], # bw compat for old cookies + 'b64unicode': lambda x: utf_8_decode(b64decode(x))[0], + 'b64str': lambda x: b64decode(x), + } + + userid_type_encoders = { + int: ('int', str), + str: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])), + bytes: ('b64str', lambda x: b64encode(x)), + } + + def __init__( + self, + secret, + cookie_name='auth_tkt', + secure=False, + include_ip=False, + timeout=None, + reissue_time=None, + max_age=None, + http_only=False, + path="/", + wild_domain=True, + hashalg='md5', + parent_domain=False, + domain=None, + samesite='Lax', + ): + self.cookie_profile = CookieProfile( + cookie_name=cookie_name, + secure=secure, + max_age=max_age, + httponly=http_only, + path=path, + serializer=SimpleSerializer(), + samesite=samesite, + ) + + self.secret = secret + self.cookie_name = cookie_name + self.secure = secure + self.include_ip = include_ip + self.timeout = timeout if timeout is None else int(timeout) + self.reissue_time = ( + reissue_time if reissue_time is None else int(reissue_time) + ) + self.max_age = max_age if max_age is None else int(max_age) + self.wild_domain = wild_domain + self.parent_domain = parent_domain + self.domain = domain + self.hashalg = hashalg + + def _get_cookies(self, request, value, max_age=None): + cur_domain = request.domain + + domains = [] + if self.domain: + domains.append(self.domain) + else: + if self.parent_domain and cur_domain.count('.') > 1: + domains.append('.' + cur_domain.split('.', 1)[1]) + else: + domains.append(None) + domains.append(cur_domain) + if self.wild_domain: + domains.append('.' + cur_domain) + + profile = self.cookie_profile(request) + + kw = {} + kw['domains'] = domains + if max_age is not None: + kw['max_age'] = max_age + + headers = profile.get_headers(value, **kw) + return headers + + def identify(self, request): + """ Return a dictionary with authentication information, or ``None`` + if no valid auth_tkt is attached to ``request``""" + environ = request.environ + cookie = request.cookies.get(self.cookie_name) + + if cookie is None: + return None + + if self.include_ip: + remote_addr = environ['REMOTE_ADDR'] + else: + remote_addr = '0.0.0.0' + + try: + timestamp, userid, tokens, user_data = self.parse_ticket( + self.secret, cookie, remote_addr, self.hashalg + ) + except self.BadTicket: + return None + + now = self.now # service tests + + if now is None: + now = time_mod.time() + + if self.timeout and ((timestamp + self.timeout) < now): + # the auth_tkt data has expired + return None + + userid_typename = 'userid_type:' + user_data_info = user_data.split('|') + for datum in filter(None, user_data_info): + if datum.startswith(userid_typename): + userid_type = datum[len(userid_typename) :] + decoder = self.userid_type_decoders.get(userid_type) + if decoder: + userid = decoder(userid) + + reissue = self.reissue_time is not None + + if reissue and not hasattr(request, '_authtkt_reissued'): + if (now - timestamp) > self.reissue_time: + # See https://github.com/Pylons/pyramid/issues#issue/108 + tokens = list(filter(None, tokens)) + headers = self.remember( + request, userid, max_age=self.max_age, tokens=tokens + ) + + def reissue_authtkt(request, response): + if not hasattr(request, '_authtkt_reissue_revoked'): + for k, v in headers: + response.headerlist.append((k, v)) + + request.add_response_callback(reissue_authtkt) + request._authtkt_reissued = True + + environ['REMOTE_USER_TOKENS'] = tokens + environ['REMOTE_USER_DATA'] = user_data + environ['AUTH_TYPE'] = 'cookie' + + identity = {} + identity['timestamp'] = timestamp + identity['userid'] = userid + identity['tokens'] = tokens + identity['userdata'] = user_data + return identity + + def forget(self, request): + """ Return a set of expires Set-Cookie headers, which will destroy + any existing auth_tkt cookie when attached to a response""" + request._authtkt_reissue_revoked = True + return self._get_cookies(request, None) + + def remember(self, request, userid, max_age=None, tokens=()): + """ Return a set of Set-Cookie headers; when set into a response, + these headers will represent a valid authentication ticket. + + ``max_age`` + The max age of the auth_tkt cookie, in seconds. 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. If + this value is ``None``, the ``max_age`` value provided to the + helper itself will be used as the ``max_age`` value. Default: + ``None``. + + ``tokens`` + A sequence of strings that will be placed into the auth_tkt tokens + field. Each string in the sequence must be of the Python ``str`` + type and must match the regex ``^[A-Za-z][A-Za-z0-9+_-]*$``. + Tokens are available in the returned identity when an auth_tkt is + found in the request and unpacked. Default: ``()``. + """ + max_age = self.max_age if max_age is None else int(max_age) + + environ = request.environ + + if self.include_ip: + remote_addr = environ['REMOTE_ADDR'] + else: + remote_addr = '0.0.0.0' + + user_data = '' + + encoding_data = self.userid_type_encoders.get(type(userid)) + + if encoding_data: + encoding, encoder = encoding_data + else: + warnings.warn( + "userid is of type {}, and is not supported by the " + "AuthTktAuthenticationPolicy. Explicitly converting to string " + "and storing as base64. Subsequent requests will receive a " + "string as the userid, it will not be decoded back to the " + "type provided.".format(type(userid)), + RuntimeWarning, + ) + encoding, encoder = self.userid_type_encoders.get(str) + userid = str(userid) + + userid = encoder(userid) + user_data = 'userid_type:%s' % encoding + + new_tokens = [] + for token in tokens: + if isinstance(token, str): + try: + token = ascii_(token) + except UnicodeEncodeError: + raise ValueError("Invalid token %r" % (token,)) + if not (isinstance(token, str) and VALID_TOKEN.match(token)): + raise ValueError("Invalid token %r" % (token,)) + new_tokens.append(token) + tokens = tuple(new_tokens) + + if hasattr(request, '_authtkt_reissued'): + request._authtkt_reissue_revoked = True + + ticket = self.AuthTicket( + self.secret, + userid, + remote_addr, + tokens=tokens, + user_data=user_data, + cookie_name=self.cookie_name, + secure=self.secure, + hashalg=self.hashalg, + ) + + cookie_value = ticket.cookie_value() + return self._get_cookies(request, cookie_value, max_age) + + @implementer(IAuthenticationPolicy) class SessionAuthenticationPolicy(CallbackAuthenticationPolicy): """ A :app:`Pyramid` authentication policy which gets its data from the diff --git a/src/pyramid/security.py b/src/pyramid/security.py index a55320ce6..dda61ef27 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -1,14 +1,3 @@ -from codecs import utf_8_decode -from codecs import utf_8_encode -import hashlib -import base64 -import time as time_mod -from urllib.parse import quote, unquote -import warnings -import re - -from webob.cookies import CookieProfile - from zope.interface import implementer, providedBy from pyramid.interfaces import ( @@ -22,14 +11,10 @@ from pyramid.interfaces import ( from pyramid.location import lineage -from pyramid.util import is_nonstr_iter, strings_differ, bytes_, ascii_, text_ - -from pyramid.util import SimpleSerializer +from pyramid.util import is_nonstr_iter from pyramid.threadlocal import get_current_registry -VALID_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*$") - Everyone = 'system.Everyone' Authenticated = 'system.Authenticated' Allow = 'Allow' @@ -586,544 +571,3 @@ class SessionAuthenticationHelper: def identify(self, request): return request.session.get(self.userid_key) - - -def b64encode(v): - return base64.b64encode(bytes_(v)).strip().replace(b'\n', b'') - - -def b64decode(v): - return base64.b64decode(bytes_(v)) - - -# this class licensed under the MIT license (stolen from Paste) -class AuthTicket(object): - """ - This class represents an authentication token. You must pass in - the shared secret, the userid, and the IP address. Optionally you - can include tokens (a list of strings, representing role names), - 'user_data', which is arbitrary data available for your own use in - later scripts. Lastly, you can override the cookie name and - timestamp. - - Once you provide all the arguments, use .cookie_value() to - generate the appropriate authentication ticket. - - Usage:: - - token = AuthTicket('sharedsecret', 'username', - os.environ['REMOTE_ADDR'], tokens=['admin']) - val = token.cookie_value() - - """ - - def __init__( - self, - secret, - userid, - ip, - tokens=(), - user_data='', - time=None, - cookie_name='auth_tkt', - secure=False, - hashalg='md5', - ): - self.secret = secret - self.userid = userid - self.ip = ip - self.tokens = ','.join(tokens) - self.user_data = user_data - if time is None: - self.time = time_mod.time() - else: - self.time = time - self.cookie_name = cookie_name - self.secure = secure - self.hashalg = hashalg - - def digest(self): - return calculate_digest( - self.ip, - self.time, - self.secret, - self.userid, - self.tokens, - self.user_data, - self.hashalg, - ) - - def cookie_value(self): - v = '%s%08x%s!' % (self.digest(), int(self.time), quote(self.userid)) - if self.tokens: - v += self.tokens + '!' - v += self.user_data - return v - - -# this class licensed under the MIT license (stolen from Paste) -class BadTicket(Exception): - """ - Exception raised when a ticket can't be parsed. If we get far enough to - determine what the expected digest should have been, expected is set. - This should not be shown by default, but can be useful for debugging. - """ - - def __init__(self, msg, expected=None): - self.expected = expected - Exception.__init__(self, msg) - - -# this function licensed under the MIT license (stolen from Paste) -def parse_ticket(secret, ticket, ip, hashalg='md5'): - """ - Parse the ticket, returning (timestamp, userid, tokens, user_data). - - If the ticket cannot be parsed, a ``BadTicket`` exception will be raised - with an explanation. - """ - ticket = text_(ticket).strip('"') - digest_size = hashlib.new(hashalg).digest_size * 2 - digest = ticket[:digest_size] - try: - timestamp = int(ticket[digest_size : digest_size + 8], 16) - except ValueError as e: - raise BadTicket('Timestamp is not a hex integer: %s' % e) - try: - userid, data = ticket[digest_size + 8 :].split('!', 1) - except ValueError: - raise BadTicket('userid is not followed by !') - userid = unquote(userid) - if '!' in data: - tokens, user_data = data.split('!', 1) - else: # pragma: no cover (never generated) - # @@: Is this the right order? - tokens = '' - user_data = data - - expected = calculate_digest( - ip, timestamp, secret, userid, tokens, user_data, hashalg - ) - - # Avoid timing attacks (see - # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) - if strings_differ(expected, digest): - raise BadTicket( - 'Digest signature is not correct', expected=(expected, digest) - ) - - tokens = tokens.split(',') - - return (timestamp, userid, tokens, user_data) - - -# this function licensed under the MIT license (stolen from Paste) -def calculate_digest( - ip, timestamp, secret, userid, tokens, user_data, hashalg='md5' -): - secret = bytes_(secret, 'utf-8') - userid = bytes_(userid, 'utf-8') - tokens = bytes_(tokens, 'utf-8') - user_data = bytes_(user_data, 'utf-8') - hash_obj = hashlib.new(hashalg) - - # Check to see if this is an IPv6 address - if ':' in ip: - ip_timestamp = ip + str(int(timestamp)) - ip_timestamp = bytes_(ip_timestamp) - else: - # encode_ip_timestamp not required, left in for backwards compatibility - ip_timestamp = encode_ip_timestamp(ip, timestamp) - - hash_obj.update( - ip_timestamp + secret + userid + b'\0' + tokens + b'\0' + user_data - ) - digest = hash_obj.hexdigest() - hash_obj2 = hashlib.new(hashalg) - hash_obj2.update(bytes_(digest) + secret) - return hash_obj2.hexdigest() - - -# this function licensed under the MIT license (stolen from Paste) -def encode_ip_timestamp(ip, timestamp): - ip_chars = ''.join(map(chr, map(int, ip.split('.')))) - t = int(timestamp) - ts = ( - (t & 0xFF000000) >> 24, - (t & 0xFF0000) >> 16, - (t & 0xFF00) >> 8, - t & 0xFF, - ) - ts_chars = ''.join(map(chr, ts)) - return bytes_(ip_chars + ts_chars) - - -class AuthTktCookieHelper: - """ - A helper class used for constructing a :term:`security policy` with stores - the user identity in a signed 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. - - ``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. - - ``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. - """ - - parse_ticket = staticmethod(parse_ticket) # for tests - AuthTicket = AuthTicket # for tests - BadTicket = BadTicket # for tests - now = None # for tests - - userid_type_decoders = { - 'int': int, - 'unicode': lambda x: utf_8_decode(x)[0], # bw compat for old cookies - 'b64unicode': lambda x: utf_8_decode(b64decode(x))[0], - 'b64str': lambda x: b64decode(x), - } - - userid_type_encoders = { - int: ('int', str), - str: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])), - bytes: ('b64str', lambda x: b64encode(x)), - } - - def __init__( - self, - secret, - cookie_name='auth_tkt', - secure=False, - include_ip=False, - timeout=None, - reissue_time=None, - max_age=None, - http_only=False, - path="/", - wild_domain=True, - hashalg='md5', - parent_domain=False, - domain=None, - samesite='Lax', - ): - self.cookie_profile = CookieProfile( - cookie_name=cookie_name, - secure=secure, - max_age=max_age, - httponly=http_only, - path=path, - serializer=SimpleSerializer(), - samesite=samesite, - ) - - self.secret = secret - self.cookie_name = cookie_name - self.secure = secure - self.include_ip = include_ip - self.timeout = timeout if timeout is None else int(timeout) - self.reissue_time = ( - reissue_time if reissue_time is None else int(reissue_time) - ) - self.max_age = max_age if max_age is None else int(max_age) - self.wild_domain = wild_domain - self.parent_domain = parent_domain - self.domain = domain - self.hashalg = hashalg - - def _get_cookies(self, request, value, max_age=None): - cur_domain = request.domain - - domains = [] - if self.domain: - domains.append(self.domain) - else: - if self.parent_domain and cur_domain.count('.') > 1: - domains.append('.' + cur_domain.split('.', 1)[1]) - else: - domains.append(None) - domains.append(cur_domain) - if self.wild_domain: - domains.append('.' + cur_domain) - - profile = self.cookie_profile(request) - - kw = {} - kw['domains'] = domains - if max_age is not None: - kw['max_age'] = max_age - - headers = profile.get_headers(value, **kw) - return headers - - def identify(self, request): - """ Return a dictionary with authentication information, or ``None`` - if no valid auth_tkt is attached to ``request``""" - environ = request.environ - cookie = request.cookies.get(self.cookie_name) - - if cookie is None: - return None - - if self.include_ip: - remote_addr = environ['REMOTE_ADDR'] - else: - remote_addr = '0.0.0.0' - - try: - timestamp, userid, tokens, user_data = self.parse_ticket( - self.secret, cookie, remote_addr, self.hashalg - ) - except self.BadTicket: - return None - - now = self.now # service tests - - if now is None: - now = time_mod.time() - - if self.timeout and ((timestamp + self.timeout) < now): - # the auth_tkt data has expired - return None - - userid_typename = 'userid_type:' - user_data_info = user_data.split('|') - for datum in filter(None, user_data_info): - if datum.startswith(userid_typename): - userid_type = datum[len(userid_typename) :] - decoder = self.userid_type_decoders.get(userid_type) - if decoder: - userid = decoder(userid) - - reissue = self.reissue_time is not None - - if reissue and not hasattr(request, '_authtkt_reissued'): - if (now - timestamp) > self.reissue_time: - # See https://github.com/Pylons/pyramid/issues#issue/108 - tokens = list(filter(None, tokens)) - headers = self.remember( - request, userid, max_age=self.max_age, tokens=tokens - ) - - def reissue_authtkt(request, response): - if not hasattr(request, '_authtkt_reissue_revoked'): - for k, v in headers: - response.headerlist.append((k, v)) - - request.add_response_callback(reissue_authtkt) - request._authtkt_reissued = True - - environ['REMOTE_USER_TOKENS'] = tokens - environ['REMOTE_USER_DATA'] = user_data - environ['AUTH_TYPE'] = 'cookie' - - identity = {} - identity['timestamp'] = timestamp - identity['userid'] = userid - identity['tokens'] = tokens - identity['userdata'] = user_data - return identity - - def forget(self, request): - """ Return a set of expires Set-Cookie headers, which will destroy - any existing auth_tkt cookie when attached to a response""" - request._authtkt_reissue_revoked = True - return self._get_cookies(request, None) - - def remember(self, request, userid, max_age=None, tokens=()): - """ Return a set of Set-Cookie headers; when set into a response, - these headers will represent a valid authentication ticket. - - ``max_age`` - The max age of the auth_tkt cookie, in seconds. 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. If - this value is ``None``, the ``max_age`` value provided to the - helper itself will be used as the ``max_age`` value. Default: - ``None``. - - ``tokens`` - A sequence of strings that will be placed into the auth_tkt tokens - field. Each string in the sequence must be of the Python ``str`` - type and must match the regex ``^[A-Za-z][A-Za-z0-9+_-]*$``. - Tokens are available in the returned identity when an auth_tkt is - found in the request and unpacked. Default: ``()``. - """ - max_age = self.max_age if max_age is None else int(max_age) - - environ = request.environ - - if self.include_ip: - remote_addr = environ['REMOTE_ADDR'] - else: - remote_addr = '0.0.0.0' - - user_data = '' - - encoding_data = self.userid_type_encoders.get(type(userid)) - - if encoding_data: - encoding, encoder = encoding_data - else: - warnings.warn( - "userid is of type {}, and is not supported by the " - "AuthTktAuthenticationPolicy. Explicitly converting to string " - "and storing as base64. Subsequent requests will receive a " - "string as the userid, it will not be decoded back to the " - "type provided.".format(type(userid)), - RuntimeWarning, - ) - encoding, encoder = self.userid_type_encoders.get(str) - userid = str(userid) - - userid = encoder(userid) - user_data = 'userid_type:%s' % encoding - - new_tokens = [] - for token in tokens: - if isinstance(token, str): - try: - token = ascii_(token) - except UnicodeEncodeError: - raise ValueError("Invalid token %r" % (token,)) - if not (isinstance(token, str) and VALID_TOKEN.match(token)): - raise ValueError("Invalid token %r" % (token,)) - new_tokens.append(token) - tokens = tuple(new_tokens) - - if hasattr(request, '_authtkt_reissued'): - request._authtkt_reissue_revoked = True - - ticket = self.AuthTicket( - self.secret, - userid, - remote_addr, - tokens=tokens, - user_data=user_data, - cookie_name=self.cookie_name, - secure=self.secure, - hashalg=self.hashalg, - ) - - cookie_value = ticket.cookie_value() - return self._get_cookies(request, cookie_value, max_age) -- cgit v1.2.3 From d6e543bc01d2f1aa3bb29f005171911f6f09da02 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Mon, 15 Apr 2019 19:14:42 -0700 Subject: Move SessionAuthenticationHelper to pyramid.authentication. --- src/pyramid/authentication.py | 31 +++++++++++++++++++++++++++++++ src/pyramid/security.py | 31 ------------------------------- 2 files changed, 31 insertions(+), 31 deletions(-) (limited to 'src') diff --git a/src/pyramid/authentication.py b/src/pyramid/authentication.py index 21cfc0c0e..4f8077309 100644 --- a/src/pyramid/authentication.py +++ b/src/pyramid/authentication.py @@ -1118,6 +1118,37 @@ class SessionAuthenticationPolicy(CallbackAuthenticationPolicy): return request.session.get(self.userid_key) +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.""" + request.session[self.userid_key] = userid + return [] + + def forget(self, request): + """ Remove the stored userid from the session.""" + if self.userid_key in request.session: + del request.session[self.userid_key] + return [] + + def identify(self, request): + return request.session.get(self.userid_key) + + @implementer(IAuthenticationPolicy) class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy): """ A :app:`Pyramid` authentication policy which uses HTTP standard basic diff --git a/src/pyramid/security.py b/src/pyramid/security.py index dda61ef27..5d157d219 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -540,34 +540,3 @@ class ACLHelper: allowed.update(allowed_here) return allowed - - -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.""" - request.session[self.userid_key] = userid - return [] - - def forget(self, request): - """ Remove the stored userid from the session.""" - if self.userid_key in request.session: - del request.session[self.userid_key] - return [] - - def identify(self, request): - return request.session.get(self.userid_key) -- cgit v1.2.3 From 600ffe25e1d332852f31756a38f6052d876b0c90 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Mon, 15 Apr 2019 19:15:36 -0700 Subject: Use SessionAuthenticationHelper in SessionAuthenticationPolicy. --- src/pyramid/authentication.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/pyramid/authentication.py b/src/pyramid/authentication.py index 4f8077309..c1e010516 100644 --- a/src/pyramid/authentication.py +++ b/src/pyramid/authentication.py @@ -1099,23 +1099,19 @@ 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.""" - request.session[self.userid_key] = userid - return [] + return self.helper.remember(request, userid, **kw) def forget(self, request): """ Remove the stored userid from the session.""" - if self.userid_key in request.session: - del request.session[self.userid_key] - return [] + return self.helper.forget(request) def unauthenticated_userid(self, request): - return request.session.get(self.userid_key) + return self.helper.identify(request) class SessionAuthenticationHelper: -- cgit v1.2.3 From 5497c0f7166308031b3cc3ce2510d22eb214b2ef Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Mon, 15 Apr 2019 19:32:11 -0700 Subject: Move ACLHelper to pyramid.authorizations. --- src/pyramid/authorization.py | 131 ++++++++++++++++++++++++++++++++++++++++++- src/pyramid/security.py | 129 ------------------------------------------ 2 files changed, 130 insertions(+), 130 deletions(-) (limited to 'src') diff --git a/src/pyramid/authorization.py b/src/pyramid/authorization.py index 19b96e3d1..cb629e257 100644 --- a/src/pyramid/authorization.py +++ b/src/pyramid/authorization.py @@ -2,7 +2,11 @@ from zope.interface import implementer from pyramid.interfaces import IAuthorizationPolicy -from pyramid.security import ACLHelper +from pyramid.location import lineage + +from pyramid.security import ACLAllowed, ACLDenied, Allow, Deny, Everyone + +from pyramid.util import is_nonstr_iter @implementer(IAuthorizationPolicy) @@ -75,3 +79,128 @@ class ACLAuthorizationPolicy(object): 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 = '' + + for location in lineage(context): + try: + acl = location.__acl__ + except AttributeError: + continue + + if acl and callable(acl): + acl = acl() + + for ace in acl: + ace_action, ace_principal, ace_permissions = ace + if ace_principal in principals: + if not is_nonstr_iter(ace_permissions): + ace_permissions = [ace_permissions] + if permission in ace_permissions: + if ace_action == Allow: + return ACLAllowed( + ace, acl, permission, principals, location + ) + else: + return ACLDenied( + ace, acl, permission, principals, location + ) + + # default deny (if no ACL in lineage at all, or if none of the + # principals were mentioned in any ACE we found) + return ACLDenied( + '', acl, permission, principals, context + ) + + 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`. + + 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))): + # NB: we're walking *up* the object graph from the root + try: + acl = location.__acl__ + except AttributeError: + continue + + allowed_here = set() + denied_here = set() + + if acl and callable(acl): + acl = acl() + + for ace_action, ace_principal, ace_permissions in acl: + if not is_nonstr_iter(ace_permissions): + ace_permissions = [ace_permissions] + if (ace_action == Allow) and (permission in ace_permissions): + if ace_principal not in denied_here: + allowed_here.add(ace_principal) + if (ace_action == Deny) and (permission in ace_permissions): + denied_here.add(ace_principal) + if ace_principal == Everyone: + # clear the entire allowed set, as we've hit a + # deny of Everyone ala (Deny, Everyone, ALL) + allowed = set() + break + elif ace_principal in allowed: + allowed.remove(ace_principal) + + allowed.update(allowed_here) + + return allowed diff --git a/src/pyramid/security.py b/src/pyramid/security.py index 5d157d219..5d8e916d7 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -9,10 +9,6 @@ from pyramid.interfaces import ( IViewClassifier, ) -from pyramid.location import lineage - -from pyramid.util import is_nonstr_iter - from pyramid.threadlocal import get_current_registry Everyone = 'system.Everyone' @@ -415,128 +411,3 @@ class LegacySecurityPolicy: authz = self._get_authz_policy(request) principals = authn.effective_principals(request) return authz.permits(context, principals, 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 = '' - - for location in lineage(context): - try: - acl = location.__acl__ - except AttributeError: - continue - - if acl and callable(acl): - acl = acl() - - for ace in acl: - ace_action, ace_principal, ace_permissions = ace - if ace_principal in principals: - if not is_nonstr_iter(ace_permissions): - ace_permissions = [ace_permissions] - if permission in ace_permissions: - if ace_action == Allow: - return ACLAllowed( - ace, acl, permission, principals, location - ) - else: - return ACLDenied( - ace, acl, permission, principals, location - ) - - # default deny (if no ACL in lineage at all, or if none of the - # principals were mentioned in any ACE we found) - return ACLDenied( - '', acl, permission, principals, context - ) - - 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`. - - 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))): - # NB: we're walking *up* the object graph from the root - try: - acl = location.__acl__ - except AttributeError: - continue - - allowed_here = set() - denied_here = set() - - if acl and callable(acl): - acl = acl() - - for ace_action, ace_principal, ace_permissions in acl: - if not is_nonstr_iter(ace_permissions): - ace_permissions = [ace_permissions] - if (ace_action == Allow) and (permission in ace_permissions): - if ace_principal not in denied_here: - allowed_here.add(ace_principal) - if (ace_action == Deny) and (permission in ace_permissions): - denied_here.add(ace_principal) - if ace_principal == Everyone: - # clear the entire allowed set, as we've hit a - # deny of Everyone ala (Deny, Everyone, ALL) - allowed = set() - break - elif ace_principal in allowed: - allowed.remove(ace_principal) - - allowed.update(allowed_here) - - return allowed -- cgit v1.2.3 From 08d5eddb2932a894fac03917508da95f480bfe7d Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 12 May 2019 10:55:01 -0700 Subject: Rename request.identity to request.authenticated_identity. --- src/pyramid/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/pyramid/security.py b/src/pyramid/security.py index 5d8e916d7..fd05746bd 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -279,7 +279,7 @@ class ACLAllowed(ACLPermitsResult, Allowed): class SecurityAPIMixin(object): @property - def identity(self): + 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. -- cgit v1.2.3 From 4c95ccd5e9b9657165f6ba061ee795fc4a5fcd30 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 12 May 2019 12:13:26 -0700 Subject: Narrative docs WIP --- src/pyramid/interfaces.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/pyramid/interfaces.py b/src/pyramid/interfaces.py index 9dabb9cfc..bc52e324e 100644 --- a/src/pyramid/interfaces.py +++ b/src/pyramid/interfaces.py @@ -484,13 +484,18 @@ class IViewMapperFactory(Interface): class ISecurityPolicy(Interface): def identify(request): - """ Return an object identifying a trusted and verified user. """ + """ Return an object identifying a trusted and verified user. This + object may be anything, but should implement a ``__str__`` method for + logging and debugging purposes. + + """ 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): -- cgit v1.2.3 From f9fba4fb3e6f6906821ed0f39e6d11cb0f26ce9d Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 26 May 2019 10:13:49 -0700 Subject: API docs. --- src/pyramid/authentication.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/pyramid/authentication.py b/src/pyramid/authentication.py index c1e010516..aaae36c5c 100644 --- a/src/pyramid/authentication.py +++ b/src/pyramid/authentication.py @@ -1142,6 +1142,7 @@ class SessionAuthenticationHelper: return [] def identify(self, request): + """ Return the stored userid.""" return request.session.get(self.userid_key) -- cgit v1.2.3 From 3d34d658e6f88d56f8ccf6c2ad4bceaa7b23e18b Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sat, 8 Jun 2019 11:41:02 -0700 Subject: Fix bw-compat for `unauthenticated_userid`. --- src/pyramid/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/pyramid/security.py b/src/pyramid/security.py index fd05746bd..eb5ba3e19 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -360,7 +360,7 @@ class AuthenticationAPIMixin(object): if authn is not None: return authn.unauthenticated_userid(self) elif security is not None: - return security.identify(self) + return str(security.identify(self)) else: return None -- cgit v1.2.3 From 14d569ea34409ed796296f860f03b8403859412f Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 23 Jun 2019 12:19:06 -0700 Subject: Deprecation notices. --- src/pyramid/config/__init__.py | 5 ++--- src/pyramid/config/security.py | 17 ++++++++++++++-- src/pyramid/interfaces.py | 6 ++++-- src/pyramid/predicates.py | 3 ++- src/pyramid/security.py | 44 ++++++++++++++++++++++++++++-------------- 5 files changed, 53 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/pyramid/config/__init__.py b/src/pyramid/config/__init__.py index d8961268a..cf1bfad44 100644 --- a/src/pyramid/config/__init__.py +++ b/src/pyramid/config/__init__.py @@ -144,12 +144,11 @@ class Configurator( If ``authentication_policy`` is passed, it should be an instance of an :term:`authentication policy` or a :term:`dotted Python - name` to the same. (Deprecated as of Pyramid 2.0 in favor of - ``security_policy``.) + name` to the same. If ``authorization_policy`` is passed, it should be an instance of an :term:`authorization policy` or a :term:`dotted Python name` to - the same. (Deprecated as of Pyramid 2.0 in favor of ``security_policy``.) + the same. .. note:: A ``ConfigurationError`` will be raised when an authorization policy is supplied without also supplying an diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py index e8ea0878d..a68581ff1 100644 --- a/src/pyramid/config/security.py +++ b/src/pyramid/config/security.py @@ -58,7 +58,13 @@ class SecurityConfiguratorMixin(object): @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. @@ -105,7 +111,13 @@ class SecurityConfiguratorMixin(object): @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. @@ -115,6 +127,7 @@ 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(): diff --git a/src/pyramid/interfaces.py b/src/pyramid/interfaces.py index bc52e324e..06ab1c32e 100644 --- a/src/pyramid/interfaces.py +++ b/src/pyramid/interfaces.py @@ -518,7 +518,8 @@ class IAuthenticationPolicy(Interface): .. deprecated:: 2.0 - Use :class:`ISecurityPolicy`. + Authentication policies have been removed in favor of security + policies. See :ref:`upgrading_auth` for more information. """ @@ -577,7 +578,8 @@ class IAuthorizationPolicy(Interface): .. deprecated:: 2.0 - Use :class:`ISecurityPolicy`. + Authentication policies have been removed in favor of security + policies. See :ref:`upgrading_auth` for more information. """ diff --git a/src/pyramid/predicates.py b/src/pyramid/predicates.py index 974f41cc5..eafd4b890 100644 --- a/src/pyramid/predicates.py +++ b/src/pyramid/predicates.py @@ -294,7 +294,8 @@ class EffectivePrincipalsPredicate(object): """ .. deprecated:: 2.0 - No longer applicable with the new :term:`security policy`. + The new security system has removed the concept of principals. See + :ref:`upgrading_auth` for more information. """ diff --git a/src/pyramid/security.py b/src/pyramid/security.py index eb5ba3e19..44ae75196 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -107,7 +107,13 @@ def 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 @@ -322,13 +328,16 @@ class SecurityAPIMixin(object): class AuthenticationAPIMixin(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. - + """ .. deprecated:: 2.0 - Use ``request.identity`` instead. + ``authenticated_userid`` has been replaced by + :attr:`authenticated_identity` in the new security system. See + :ref:`upgrading_auth` for more information. + + 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. """ authn = _get_authentication_policy(self) @@ -342,7 +351,14 @@ class AuthenticationAPIMixin(object): @property def unauthenticated_userid(self): - """ Return an object which represents the *claimed* (not verified) user + """ + .. deprecated:: 2.0 + + ``unauthenticated_userid`` has been replaced by + :attr:`authenticated_identity` in the new security system. 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 @@ -350,10 +366,6 @@ class AuthenticationAPIMixin(object): effective authentication policy will not ensure that a record associated with the userid exists in persistent storage. - .. deprecated:: 2.0 - - Use ``request.identity`` instead. - """ authn = _get_authentication_policy(self) security = _get_security_policy(self) @@ -366,13 +378,17 @@ class AuthenticationAPIMixin(object): @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. - .. deprecated:: 2.0 - """ policy = _get_authentication_policy(self) if policy is None: -- cgit v1.2.3 From 514f75fc6dfa6b7f5e58e48da229c7a23059ba31 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 23 Jun 2019 12:45:47 -0700 Subject: Add deprecation warnings. --- src/pyramid/config/security.py | 8 ++++++++ src/pyramid/predicates.py | 11 +++++++++++ src/pyramid/security.py | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+) (limited to 'src') diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py index a68581ff1..8f0a108c5 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, @@ -56,6 +57,13 @@ class SecurityConfiguratorMixin(object): 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): """ diff --git a/src/pyramid/predicates.py b/src/pyramid/predicates.py index eafd4b890..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 @@ -319,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/security.py b/src/pyramid/security.py index 44ae75196..64e840801 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -1,4 +1,5 @@ from zope.interface import implementer, providedBy +from zope.deprecation import deprecated from pyramid.interfaces import ( ISecurityPolicy, @@ -137,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 @@ -395,6 +405,14 @@ class AuthenticationAPIMixin(object): 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.', + ) + @implementer(ISecurityPolicy) class LegacySecurityPolicy: -- cgit v1.2.3 From c69994778de79041d703fd121ec9d1aaf8a6b7c8 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sat, 13 Jul 2019 10:10:46 -0700 Subject: Improve authn/authz API docs. --- src/pyramid/authentication.py | 295 +++++++++++++++++++++--------------------- src/pyramid/authorization.py | 50 ++----- 2 files changed, 155 insertions(+), 190 deletions(-) (limited to 'src') diff --git a/src/pyramid/authentication.py b/src/pyramid/authentication.py index aaae36c5c..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 diff --git a/src/pyramid/authorization.py b/src/pyramid/authorization.py index cb629e257..498938fd5 100644 --- a/src/pyramid/authorization.py +++ b/src/pyramid/authorization.py @@ -14,51 +14,17 @@ 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): -- cgit v1.2.3 From d2d20b92158088e7d646393733092e67120058f0 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 21 Jul 2019 09:20:44 -0700 Subject: Un-deprecate authenticated_userid. --- src/pyramid/interfaces.py | 4 ++-- src/pyramid/security.py | 52 +++++++++++++++++++++++------------------------ 2 files changed, 28 insertions(+), 28 deletions(-) (limited to 'src') diff --git a/src/pyramid/interfaces.py b/src/pyramid/interfaces.py index 06ab1c32e..d97c3811b 100644 --- a/src/pyramid/interfaces.py +++ b/src/pyramid/interfaces.py @@ -485,8 +485,8 @@ 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 for - logging and debugging purposes. + object may be anything, but should implement a ``__str__`` method that + outputs a corresponding :term:`userid`. """ diff --git a/src/pyramid/security.py b/src/pyramid/security.py index 64e840801..08c36b457 100644 --- a/src/pyramid/security.py +++ b/src/pyramid/security.py @@ -306,6 +306,28 @@ class SecurityAPIMixin(object): return None 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 @@ -336,37 +358,15 @@ class SecurityAPIMixin(object): class AuthenticationAPIMixin(object): - @property - def authenticated_userid(self): - """ - .. deprecated:: 2.0 - - ``authenticated_userid`` has been replaced by - :attr:`authenticated_identity` in the new security system. See - :ref:`upgrading_auth` for more information. - - 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. - - """ - 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 - @property def unauthenticated_userid(self): """ .. deprecated:: 2.0 - ``unauthenticated_userid`` has been replaced by - :attr:`authenticated_identity` in the new security system. See - :ref:`upgrading_auth` for more information. + ``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 -- cgit v1.2.3