diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/pyramid/authorization.py | 79 | ||||
| -rw-r--r-- | src/pyramid/config/__init__.py | 14 | ||||
| -rw-r--r-- | src/pyramid/config/security.py | 52 | ||||
| -rw-r--r-- | src/pyramid/config/testing.py | 15 | ||||
| -rw-r--r-- | src/pyramid/interfaces.py | 42 | ||||
| -rw-r--r-- | src/pyramid/predicates.py | 7 | ||||
| -rw-r--r-- | src/pyramid/request.py | 4 | ||||
| -rw-r--r-- | src/pyramid/security.py | 324 | ||||
| -rw-r--r-- | src/pyramid/testing.py | 42 | ||||
| -rw-r--r-- | src/pyramid/viewderivers.py | 36 |
10 files changed, 406 insertions, 209 deletions
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 = '<No ACL found on any object in resource lineage>' - - 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( - '<default deny>', 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/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..ac7dcef43 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, ) @@ -13,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 @@ -22,6 +24,35 @@ class SecurityConfiguratorMixin(object): self.set_csrf_storage_policy(LegacySessionCSRFStoragePolicy()) @action_method + def set_security_policy(self, policy): + """ Override the :app:`Pyramid` :term:`security policy` in the current + configuration. The ``policy`` argument must be an instance + of a security policy or a :term:`dotted Python name` + that points at an instance of a security policy. + + .. note:: + + Using the ``security_policy`` argument to the + :class:`pyramid.config.Configurator` constructor can be used to + achieve the same purpose. + + """ + + def register(): + self.registry.registerUtility(policy, ISecurityPolicy) + + policy = self.maybe_dotted(policy) + intr = self.introspectable( + 'security policy', + None, + self.object_description(policy), + 'security policy', + ) + intr['policy'] = policy + # authentication policy used by view config (phase 3) + self.action(ISecurityPolicy, register, introspectables=(intr,)) + + @action_method def set_authentication_policy(self, policy): """ Override the :app:`Pyramid` :term:`authentication policy` in the current configuration. The ``policy`` argument must be an instance @@ -37,14 +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)' ) + if self.registry.queryUtility(ISecurityPolicy) is not None: + raise ConfigurationError( + 'Cannot configure an authentication and authorization' + 'policy with a configured security policy.' + ) + security_policy = LegacySecurityPolicy() + self.registry.registerUtility(security_policy, ISecurityPolicy) + policy = self.maybe_dotted(policy) intr = self.introspectable( 'authentication policy', None, @@ -60,10 +99,6 @@ class SecurityConfiguratorMixin(object): introspectables=(intr,), ) - def _set_authentication_policy(self, policy): - policy = self.maybe_dotted(policy) - self.registry.registerUtility(policy, IAuthenticationPolicy) - @action_method def set_authorization_policy(self, policy): """ Override the :app:`Pyramid` :term:`authorization policy` in the @@ -79,7 +114,7 @@ class SecurityConfiguratorMixin(object): """ def register(): - self._set_authorization_policy(policy) + self.registry.registerUtility(policy, IAuthorizationPolicy) def ensure(): if self.autocommit: @@ -91,6 +126,7 @@ class SecurityConfiguratorMixin(object): '(use the set_authorization_policy method)' ) + policy = self.maybe_dotted(policy) intr = self.introspectable( 'authorization policy', None, @@ -108,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): """ diff --git a/src/pyramid/config/testing.py b/src/pyramid/config/testing.py index 9c998840a..21c622656 100644 --- a/src/pyramid/config/testing.py +++ b/src/pyramid/config/testing.py @@ -1,11 +1,6 @@ from zope.interface import Interface -from pyramid.interfaces import ( - ITraverser, - IAuthorizationPolicy, - IAuthenticationPolicy, - IRendererFactory, -) +from pyramid.interfaces import ITraverser, ISecurityPolicy, IRendererFactory from pyramid.renderers import RendererHelper @@ -18,8 +13,7 @@ class TestingConfiguratorMixin(object): # testing API def testing_securitypolicy( self, - userid=None, - groupids=(), + identity=None, permissive=True, remember_result=None, forget_result=None, @@ -69,10 +63,9 @@ class TestingConfiguratorMixin(object): from pyramid.testing import DummySecurityPolicy policy = DummySecurityPolicy( - userid, groupids, permissive, remember_result, forget_result + identity, permissive, remember_result, forget_result ) - self.registry.registerUtility(policy, IAuthorizationPolicy) - self.registry.registerUtility(policy, IAuthenticationPolicy) + self.registry.registerUtility(policy, ISecurityPolicy) return policy def testing_resources(self, resources): diff --git a/src/pyramid/interfaces.py b/src/pyramid/interfaces.py index 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 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) diff --git a/src/pyramid/request.py b/src/pyramid/request.py index b9bd7451a..5c68abe69 100644 --- a/src/pyramid/request.py +++ b/src/pyramid/request.py @@ -15,7 +15,7 @@ from pyramid.interfaces import ( from pyramid.decorator import reify from pyramid.i18n import LocalizerRequestMixin from pyramid.response import Response, _get_response_factory -from pyramid.security import AuthenticationAPIMixin, AuthorizationAPIMixin +from pyramid.security import SecurityAPIMixin, AuthenticationAPIMixin from pyramid.url import URLMethodsMixin from pyramid.util import ( InstancePropertyHelper, @@ -147,8 +147,8 @@ class Request( CallbackMethodsMixin, InstancePropertyMixin, LocalizerRequestMixin, + SecurityAPIMixin, AuthenticationAPIMixin, - AuthorizationAPIMixin, ViewMethodsMixin, ): """ diff --git a/src/pyramid/security.py b/src/pyramid/security.py index 61819588b..671cd3569 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, @@ -8,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' @@ -35,17 +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): + return request.registry.queryUtility(ISecurityPolicy) def _get_authentication_policy(request): - registry = _get_registry(request) - return registry.queryUtility(IAuthenticationPolicy) + return request.registry.queryUtility(IAuthenticationPolicy) def remember(request, userid, **kw): @@ -54,7 +54,7 @@ def remember(request, userid, **kw): on this request's response. These headers are suitable for 'remembering' a set of credentials implied by the data passed as ``userid`` and ``*kw`` using the - current :term:`authentication policy`. Common usage might look + current :term:`security policy`. Common usage might look like so within the body of a view function (``response`` is assumed to be a :term:`WebOb` -style :term:`response` object computed previously by the view code): @@ -67,10 +67,10 @@ def remember(request, userid, **kw): response.headerlist.extend(headers) return response - If no :term:`authentication policy` is in use, this function will + If no :term:`security policy` is in use, this function will always return an empty sequence. If used, the composition and meaning of ``**kw`` must be agreed upon by the calling code and - the effective authentication policy. + the effective security policy. .. versionchanged:: 1.6 Deprecated the ``principal`` argument in favor of ``userid`` to clarify @@ -79,7 +79,7 @@ def remember(request, userid, **kw): .. versionchanged:: 1.10 Removed the deprecated ``principal`` argument. """ - policy = _get_authentication_policy(request) + policy = _get_security_policy(request) if policy is None: return [] return policy.remember(request, userid, **kw) @@ -101,10 +101,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) @@ -126,6 +126,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) @@ -147,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 @@ -280,6 +281,48 @@ 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) + + def has_permission(self, permission, context=None): + """ Given a permission and an optional context, returns an instance of + :data:`pyramid.security.Allowed` if the permission is granted to this + request with the provided context, or the context already associated + with the request. Otherwise, returns an instance of + :data:`pyramid.security.Denied`. This method delegates to the current + security policy. Returns + :data:`pyramid.security.Allowed` unconditionally if no security + policy has been registered for this request. If ``context`` is not + supplied or is supplied as ``None``, the context used is the + ``request.context`` attribute. + + :param permission: Does this request have the given permission? + :type permission: str + :param context: A resource object or ``None`` + :type context: object + :returns: Either :class:`pyramid.security.Allowed` or + :class:`pyramid.security.Denied`. + + """ + if context is None: + context = self.context + policy = _get_security_policy(self) + if policy is None: + return Allowed('No security policy in use.') + identity = policy.identify(self) + return policy.permits(self, context, identity, permission) + + class AuthenticationAPIMixin(object): @property def authenticated_userid(self): @@ -287,12 +330,19 @@ 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: + 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): @@ -304,12 +354,19 @@ 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: + 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): @@ -318,7 +375,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: @@ -326,40 +384,190 @@ 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. +@implementer(ISecurityPolicy) +class LegacySecurityPolicy: + """ + A :term:`security policy` which provides a backwards compatibility shim for + the :term:`authentication policy` and the :term:`authorization policy`. - :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 + 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) + + +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``). """ - 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) + acl = '<No ACL found on any object in resource lineage>' + + 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( + '<default deny>', 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 + + +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) diff --git a/src/pyramid/testing.py b/src/pyramid/testing.py index ffddd233f..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, - AuthenticationAPIMixin, - AuthorizationAPIMixin, -) +from pyramid.security import SecurityAPIMixin, AuthenticationAPIMixin from pyramid.threadlocal import get_current_registry, manager @@ -43,18 +38,16 @@ class DummyRootFactory(object): class DummySecurityPolicy(object): - """ A standin for both an IAuthentication and IAuthorization policy """ + """ A standin for a security policy""" def __init__( self, - userid=None, - groupids=(), + identity=None, permissive=True, remember_result=None, forget_result=None, ): - self.userid = userid - self.groupids = groupids + self.identity = identity self.permissive = permissive if remember_result is None: remember_result = [] @@ -63,19 +56,11 @@ class DummySecurityPolicy(object): self.remember_result = remember_result self.forget_result = forget_result - def authenticated_userid(self, request): - return self.userid - - def unauthenticated_userid(self, request): - return self.userid + def identify(self, request): + return self.identity - def effective_principals(self, request): - effective_principals = [Everyone] - if self.userid: - effective_principals.append(Authenticated) - effective_principals.append(self.userid) - effective_principals.extend(self.groupids) - return effective_principals + def permits(self, request, context, identity, permission): + return self.permissive def remember(self, request, userid, **kw): self.remembered = userid @@ -85,15 +70,6 @@ class DummySecurityPolicy(object): self.forgotten = True return self.forget_result - def permits(self, context, principals, permission): - return self.permissive - - def principals_allowed_by_permission(self, context, permission): - if self.permissive: - return self.effective_principals(None) - else: - return [] - class DummyTemplateRenderer(object): """ @@ -303,8 +279,8 @@ class DummyRequest( CallbackMethodsMixin, InstancePropertyMixin, LocalizerRequestMixin, + SecurityAPIMixin, AuthenticationAPIMixin, - AuthorizationAPIMixin, ViewMethodsMixin, ): """ A DummyRequest object (incompletely) imitates a :term:`request` object. diff --git a/src/pyramid/viewderivers.py b/src/pyramid/viewderivers.py index 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) |
