summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2019-09-30 22:23:02 -0500
committerGitHub <noreply@github.com>2019-09-30 22:23:02 -0500
commit849463d3c2f5ad2c89b3d10a2abce63e4892082d (patch)
tree5bc507d427d8d2000c59ad7837cc03099decf1b5
parentada0a977d9190520c21ffaf9500860db2f3a1b3e (diff)
parentcdb26610782176955cd8cfb0b3c3e242ca819f74 (diff)
downloadpyramid-849463d3c2f5ad2c89b3d10a2abce63e4892082d.tar.gz
pyramid-849463d3c2f5ad2c89b3d10a2abce63e4892082d.tar.bz2
pyramid-849463d3c2f5ad2c89b3d10a2abce63e4892082d.zip
Merge pull request #3465 from luhn/security-policy
Security policy implementation
-rw-r--r--docs/api/authentication.rst36
-rw-r--r--docs/api/authorization.rst3
-rw-r--r--docs/api/config.rst1
-rw-r--r--docs/api/request.rst17
-rw-r--r--docs/glossary.rst29
-rw-r--r--docs/index.rst1
-rw-r--r--docs/narr/advanced-features.rst9
-rw-r--r--docs/narr/security.rst491
-rw-r--r--docs/whatsnew-2.0.rst103
-rw-r--r--src/pyramid/authentication.py329
-rw-r--r--src/pyramid/authorization.py124
-rw-r--r--src/pyramid/config/__init__.py9
-rw-r--r--src/pyramid/config/security.py81
-rw-r--r--src/pyramid/config/testing.py15
-rw-r--r--src/pyramid/interfaces.py49
-rw-r--r--src/pyramid/predicates.py19
-rw-r--r--src/pyramid/request.py4
-rw-r--r--src/pyramid/security.py214
-rw-r--r--src/pyramid/testing.py42
-rw-r--r--src/pyramid/viewderivers.py36
-rw-r--r--tests/pkgs/defpermbugapp/__init__.py4
-rw-r--r--tests/pkgs/forbiddenapp/__init__.py4
-rw-r--r--tests/pkgs/legacysecurityapp/__init__.py37
-rw-r--r--tests/pkgs/securityapp/__init__.py41
-rw-r--r--tests/pkgs/staticpermapp/__init__.py4
-rw-r--r--tests/test_authentication.py50
-rw-r--r--tests/test_authorization.py261
-rw-r--r--tests/test_config/test_init.py9
-rw-r--r--tests/test_config/test_security.py32
-rw-r--r--tests/test_config/test_testing.py28
-rw-r--r--tests/test_config/test_views.py29
-rw-r--r--tests/test_integration.py50
-rw-r--r--tests/test_predicates.py22
-rw-r--r--tests/test_request.py4
-rw-r--r--tests/test_security.py175
-rw-r--r--tests/test_testing.py45
-rw-r--r--tests/test_viewderivers.py134
37 files changed, 1643 insertions, 898 deletions
diff --git a/docs/api/authentication.rst b/docs/api/authentication.rst
index 57f32327a..f3a25ee64 100644
--- a/docs/api/authentication.rst
+++ b/docs/api/authentication.rst
@@ -3,10 +3,30 @@
:mod:`pyramid.authentication`
--------------------------------
+Helper Classes
+~~~~~~~~~~~~~~
+
+.. automodule:: pyramid.authentication
+
+ .. autoclass:: SessionAuthenticationHelper
+ :members:
+
+ .. autoclass:: AuthTktCookieHelper
+ :members:
+
+Helper Functions
+~~~~~~~~~~~~~~~~
+
+ .. autofunction:: extract_http_basic_credentials
+
+ .. autoclass:: HTTPBasicCredentials
+ :members:
+
Authentication Policies
~~~~~~~~~~~~~~~~~~~~~~~
-.. automodule:: pyramid.authentication
+Authentication policies have been deprecated by the new security system. See
+:ref:`upgrading_auth` for more information.
.. autoclass:: AuthTktAuthenticationPolicy
:members:
@@ -27,17 +47,3 @@ Authentication Policies
.. autoclass:: RepozeWho1AuthenticationPolicy
:members:
:inherited-members:
-
-Helper Classes
-~~~~~~~~~~~~~~
-
- .. autoclass:: AuthTktCookieHelper
- :members:
-
- .. autoclass:: HTTPBasicCredentials
- :members:
-
-Helper Functions
-~~~~~~~~~~~~~~~~
-
- .. autofunction:: extract_http_basic_credentials
diff --git a/docs/api/authorization.rst b/docs/api/authorization.rst
index 5f5435b94..c6b3d090e 100644
--- a/docs/api/authorization.rst
+++ b/docs/api/authorization.rst
@@ -5,5 +5,8 @@
.. automodule:: pyramid.authorization
+ .. autoclass:: ACLHelper
+ :members:
+
.. autoclass:: ACLAuthorizationPolicy
diff --git a/docs/api/config.rst b/docs/api/config.rst
index 4fe0e855d..a925f42d9 100644
--- a/docs/api/config.rst
+++ b/docs/api/config.rst
@@ -35,6 +35,7 @@
:methodcategory:`Using Security`
+ .. automethod:: set_security_policy
.. automethod:: set_authentication_policy
.. automethod:: set_authorization_policy
.. automethod:: set_default_csrf_options
diff --git a/docs/api/request.rst b/docs/api/request.rst
index e7b2edc9a..8e0f77b87 100644
--- a/docs/api/request.rst
+++ b/docs/api/request.rst
@@ -166,7 +166,11 @@
.. attribute:: authenticated_userid
- .. versionadded:: 1.5
+ .. deprecated:: 2.0
+
+ ``authenticated_userid`` has been replaced by
+ :attr:`authenticated_identity` in the new security system. See
+ :ref:`upgrading_auth` for more information.
A property which returns the :term:`userid` of the currently
authenticated user or ``None`` if there is no :term:`authentication
@@ -178,7 +182,11 @@
.. attribute:: unauthenticated_userid
- .. versionadded:: 1.5
+ .. deprecated:: 2.0
+
+ ``unauthenticated_userid`` has been replaced by
+ :attr:`authenticated_identity` in the new security system. See
+ :ref:`upgrading_auth` for more information.
A property which returns a value which represents the *claimed* (not
verified) :term:`userid` of the credentials present in the
@@ -193,7 +201,10 @@
.. attribute:: effective_principals
- .. versionadded:: 1.5
+ .. deprecated:: 2.0
+
+ The new security policy has removed the concept of principals. See
+ :ref:`upgrading_auth` for more information.
A property which returns the list of 'effective' :term:`principal`
identifiers for this request. This list typically includes the
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 8df70f475..2d2595592 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -298,13 +298,20 @@ Glossary
foo` and `group bar`.
userid
- A *userid* is a string used to identify and authenticate
- a real-world user or client. A userid is supplied to an
- :term:`authentication policy` in order to discover the user's
- :term:`principals <principal>`. In the authentication policies which
- :app:`Pyramid` provides, the default behavior returns the user's userid as
- a principal, but this is not strictly necessary in custom policies that
- define their principals differently.
+ A *userid* is the string representation of an :term:`identity`. Just like
+ the identity, it should identify the user associated with the current
+ request. Oftentimes this is the ID of the user object in a database.
+
+ identity
+ An identity is an object identifying the user associated with the
+ current request. The identity can be any object, but should implement a
+ ``__str__`` method that outputs a corresponding :term:`userid`.
+
+ security policy
+ A security policy in :app:`Pyramid` terms is a bit of code which has an
+ API which identifies the user associated with the current request (perhaps
+ via a cookie or ``Authorization`` header) and determines whether or not
+ that user is permitted to access the requested resource.
authorization policy
An authorization policy in :app:`Pyramid` terms is a bit of
@@ -313,11 +320,19 @@ Glossary
associated with a permission, based on the information found on the
:term:`context` resource.
+ .. deprecated:: 2.0
+ Authorization policies have been deprecated in favor of a
+ :term:`security policy`.
+
authentication policy
An authentication policy in :app:`Pyramid` terms is a bit of
code which has an API which determines the current
:term:`principal` (or principals) associated with a request.
+ .. deprecated:: 2.0
+ Authentication policies have been deprecated in favor of a
+ :term:`security policy`.
+
WSGI
`Web Server Gateway Interface <https://wsgi.readthedocs.io/en/latest/>`_.
This is a Python standard for connecting web applications to web servers,
diff --git a/docs/index.rst b/docs/index.rst
index 09a3b56b0..c1f6db81a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -174,6 +174,7 @@ Change History
.. toctree::
:maxdepth: 1
+ whatsnew-2.0
whatsnew-1.10
whatsnew-1.9
whatsnew-1.8
diff --git a/docs/narr/advanced-features.rst b/docs/narr/advanced-features.rst
index b24208bc4..8d99f7291 100644
--- a/docs/narr/advanced-features.rst
+++ b/docs/narr/advanced-features.rst
@@ -104,13 +104,14 @@ For example, if you want to reuse an existing application that already has a bun
Authenticate Users Your Way
---------------------------
-:app:`Pyramid` ships with prebuilt, well-tested authentication and authorization schemes out of the box. Using a scheme is a matter of configuration. So if you need to change approaches later, you need only update your configuration.
-
-In addition, the system that handles authentication and authorization is flexible and pluggable. If you want to use another security add-on, or define your own, you can. And again, you need only update your application configuration to make the change.
+:app:`Pyramid` has a powerful security system that can be tailored to your
+needs. Build your own security policy tailored to your needs, or use one of
+the many helpers provided to easily implement common authentication and
+authorization patterns.
.. seealso::
- See also :ref:`enabling_authorization_policy`.
+ See also :ref:`writing_security_policy`.
Build Trees of Resources
------------------------
diff --git a/docs/narr/security.rst b/docs/narr/security.rst
index 2b0a2f032..2a7034a19 100644
--- a/docs/narr/security.rst
+++ b/docs/narr/security.rst
@@ -6,17 +6,12 @@
Security
========
-:app:`Pyramid` provides an optional, declarative, security system. Security in
-:app:`Pyramid` is separated into authentication and authorization. The two
-systems communicate via :term:`principal` identifiers. Authentication is merely
-the mechanism by which credentials provided in the :term:`request` are resolved
-to one or more :term:`principal` identifiers. These identifiers represent the
-users and groups that are in effect during the request. Authorization then
-determines access based on the :term:`principal` identifiers, the requested
-:term:`permission`, and a :term:`context`.
-
-The :app:`Pyramid` authorization system can prevent a :term:`view` from being
-invoked based on an :term:`authorization policy`. Before a view is invoked, the
+:app:`Pyramid` provides an optional, declarative security system. The system
+determines the identity of the current user (authentication) and whether or not
+the user has access to certain resources (authorization).
+
+The :app:`Pyramid` security system can prevent a :term:`view` from being
+invoked based on the :term:`security policy`. Before a view is invoked, the
authorization system can use the credentials in the :term:`request` along with
the :term:`context` resource to determine if access will be allowed. Here's
how it works at a high level:
@@ -37,89 +32,144 @@ how it works at a high level:
- A :term:`view callable` is located by :term:`view lookup` using the context
as well as other attributes of the request.
-- If an :term:`authentication policy` is in effect, it is passed the request.
- It will return some number of :term:`principal` identifiers. To do this, the
- policy would need to determine the authenticated :term:`userid` present in
- the request.
+- If a :term:`security policy` is in effect, it is passed the request and
+ returns the :term:`identity` of the current user.
-- If an :term:`authorization policy` is in effect and the :term:`view
+- If a :term:`security policy` is in effect and the :term:`view
configuration` associated with the view callable that was found has a
- :term:`permission` associated with it, the authorization policy is passed the
- :term:`context`, some number of :term:`principal` identifiers returned by the
- authentication policy, and the :term:`permission` associated with the view;
- it will allow or deny access.
+ :term:`permission` associated with it, the policy is passed the
+ :term:`context`, the current :term:`identity`, and the :term:`permission`
+ associated with the view; it will allow or deny access.
-- If the authorization policy allows access, the view callable is invoked.
+- If the security policy allows access, the view callable is invoked.
-- If the authorization policy denies access, the view callable is not invoked.
+- If the security policy denies access, the view callable is not invoked.
Instead the :term:`forbidden view` is invoked.
-Authorization is enabled by modifying your application to include an
-:term:`authentication policy` and :term:`authorization policy`. :app:`Pyramid`
-comes with a variety of implementations of these policies. To provide maximal
-flexibility, :app:`Pyramid` also allows you to create custom authentication
-policies and authorization policies.
+The security system is enabled by modifying your application to include a
+:term:`security policy`. :app:`Pyramid` comes with a variety of helpers to
+assist in the creation of this policy.
.. index::
- single: authorization policy
-
-.. _enabling_authorization_policy:
+ single: security policy
-Enabling an Authorization Policy
---------------------------------
+.. _writing_security_policy:
-:app:`Pyramid` does not enable any authorization policy by default. All views
-are accessible by completely anonymous users. In order to begin protecting
-views from execution based on security settings, you need to enable an
-authorization policy.
+Writing a Security Policy
+-------------------------
-Enabling an Authorization Policy Imperatively
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+:app:`Pyramid` does not enable any security policy by default. All views are
+accessible by completely anonymous users. In order to begin protecting views
+from execution based on security settings, you need to write a security policy.
-Use the :meth:`~pyramid.config.Configurator.set_authorization_policy` method of
-the :class:`~pyramid.config.Configurator` to enable an authorization policy.
+Security policies are simple classes implementing a
+:class:`pyramid.interfaces.ISecurityPolicy`, defined as follows:
-You must also enable an :term:`authentication policy` in order to enable the
-authorization policy. This is because authorization, in general, depends upon
-authentication. Use the
-:meth:`~pyramid.config.Configurator.set_authentication_policy` method during
-application setup to specify the authentication policy.
+.. autointerface:: pyramid.interfaces.ISecurityPolicy
+ :members:
-For example:
+A simple security policy might look like the following:
.. code-block:: python
:linenos:
- from pyramid.config import Configurator
- from pyramid.authentication import AuthTktAuthenticationPolicy
- from pyramid.authorization import ACLAuthorizationPolicy
- authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512')
- authz_policy = ACLAuthorizationPolicy()
- config = Configurator()
- config.set_authentication_policy(authn_policy)
- config.set_authorization_policy(authz_policy)
+ from pyramid.security import Allowed, Denied
-.. note:: The ``authentication_policy`` and ``authorization_policy`` arguments
- may also be passed to their respective methods mentioned above as
- :term:`dotted Python name` values, each representing the dotted name path to
- a suitable implementation global defined at Python module scope.
+ class SessionSecurityPolicy:
+ def identify(self, request):
+ """ Return the user ID stored in the session. """
+ return request.session.get('userid')
-The above configuration enables a policy which compares the value of an "auth
-ticket" cookie passed in the request's environment which contains a reference
-to a single :term:`userid`, and matches that userid's :term:`principals
-<principal>` against the principals present in any :term:`ACL` found in the
-resource tree when attempting to call some :term:`view`.
+ def permits(self, request, context, identity, permission):
+ """ Allow access to everything if signed in. """
+ if identity is not None:
+ return Allowed('User is signed in.')
+ else:
+ return Denied('User is not signed in.')
-While it is possible to mix and match different authentication and
-authorization policies, it is an error to configure a Pyramid application with
-an authentication policy but without the authorization policy or vice versa. If
-you do this, you'll receive an error at application startup time.
+ def remember(request, userid, **kw):
+ request.session.get('userid')
+ return []
+
+ def forget(request):
+ del request.session['userid']
+ return []
+
+Use the :meth:`~pyramid.config.Configurator.set_security_policy` method of
+the :class:`~pyramid.config.Configurator` to enforce the security policy on
+your application.
.. seealso::
- See also the :mod:`pyramid.authorization` and :mod:`pyramid.authentication`
- modules for alternative implementations of authorization and authentication
- policies.
+ For more information on implementing the ``permits`` method, see
+ :ref:`security_policy_permits`.
+
+Writing a Security Policy Using Helpers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To assist in writing common security policies, Pyramid provides several
+helpers. The following authentication helpers assist with implementing
+``identity``, ``remember``, and ``forget``.
+
++-------------------------------+-------------------------------------------------------------------+
+| Use Case | Helper |
++===============================+===================================================================+
+| Store the :term:`userid` | :class:`pyramid.authentication.SessionAuthenticationHelper` |
+| in the :term:`session`. | |
++-------------------------------+-------------------------------------------------------------------+
+| Store the :term:`userid` | :class:`pyramid.authentication.AuthTktCookieHelper` |
+| with an "auth ticket" cookie. | |
++-------------------------------+-------------------------------------------------------------------+
+| Retrieve user credentials | Use :func:`pyramid.authentication.extract_http_basic_credentials` |
+| using HTTP Basic Auth. | to retrieve credentials. |
++-------------------------------+-------------------------------------------------------------------+
+| Retrieve the :term:`userid` | ``REMOTE_USER`` can be accessed with |
+| from ``REMOTE_USER`` in the | ``request.environ.get('REMOTE_USER')``. |
+| WSGI environment. | |
++-------------------------------+-------------------------------------------------------------------+
+
+For example, our above security policy can leverage these helpers like so:
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.security import Allowed, Denied
+ from pyramid.authentication import SessionAuthenticationHelper
+
+ class SessionSecurityPolicy:
+ def __init__(self):
+ self.helper = SessionAuthenticationHelper()
+
+ def identify(self, request):
+ """ Return the user ID stored in the session. """
+ return self.helper.identify(request)
+
+ def permits(self, request, context, identity, permission):
+ """ Allow access to everything if signed in. """
+ if identity is not None:
+ return Allowed('User is signed in.')
+ else:
+ return Denied('User is not signed in.')
+
+ def remember(request, userid, **kw):
+ return self.helper.remember(request, userid, **kw)
+
+ def forget(request):
+ return self.helper.forget(request)
+
+Helpers are intended to be used with application-specific code, so perhaps your
+authentication also queries the database to ensure the identity is valid.
+
+.. code-block:: python
+ :linenos:
+
+ def identify(self, request):
+ """ Return the user ID stored in the session. """
+ user_id = self.helper.identify(request)
+ if validate_user_id(user_id):
+ return user_id
+ else:
+ return None
.. index::
single: permissions
@@ -165,11 +215,53 @@ performed via the ``@view_config`` decorator:
pass
As a result of any of these various view configuration statements, if an
-authorization policy is in place when the view callable is found during normal
-application operations, the requesting user will need to possess the ``add``
-permission against the :term:`context` resource in order to be able to invoke
-the ``blog_entry_add_view`` view. If they do not, the :term:`Forbidden view`
-will be invoked.
+security policy is in place when the view callable is found during normal
+application operations, the security policy will be queried to see if the
+requesting user is allowed the ``add`` permission within the current
+:term:`context`. If the policy allows access, ``blog_entry_add_view`` will be
+invoked. If not, the :term:`Forbidden view` will be invoked.
+
+.. _security_policy_permits:
+
+Allowing and Denying Access With a Security Policy
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To determine whether access is allowed to a view with an attached permission,
+Pyramid calls the ``permits`` method of the security policy. ``permits``
+should return an instance of :class:`pyramid.security.Allowed` or
+:class:`pyramid.security.Denied`. Both classes accept a string as an argument,
+which should detail why access was allowed or denied.
+
+A simple ``permits`` implementation that grants access based on a user role
+might look like so:
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.security import Allowed, Denied
+
+ class SecurityPolicy:
+ def permits(self, request, context, identity, permission):
+ if identity is None:
+ return Denied('User is not signed in.')
+ if identity.role == 'admin':
+ allowed = ['read', 'write', 'delete']
+ elif identity.role == 'editor':
+ allowed = ['read', 'write']
+ else:
+ allowed = ['read']
+ if permission in allowed:
+ return Allowed(
+ 'Access granted for user %s with role %s.',
+ identity,
+ identity.role,
+ )
+ else:
+ return Denied(
+ 'Access denied for user %s with role %s.',
+ identity,
+ identity.role,
+ )
.. index::
pair: permission; default
@@ -180,7 +272,7 @@ Setting a Default Permission
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If a permission is not supplied to a view configuration, the registered view
-will always be executable by entirely anonymous users: any authorization policy
+will always be executable by entirely anonymous users: any security policy
in effect is ignored.
In support of making it easier to configure applications which are "secure by
@@ -217,16 +309,39 @@ When a default permission is registered:
.. _assigning_acls:
-Assigning ACLs to Your Resource Objects
----------------------------------------
+Implementing ACL Authorization
+------------------------------
+
+A common way to implement authorization is using an :term:`ACL`. An ACL is a
+:term:`context`-specific list of access control entries, which allow or deny
+access to permissions based on a user's principals.
+
+Pyramid provides :class:`pyramid.authorization.ACLHelper` to assist with an
+ACL-based implementation of ``permits``. Application-specific code should
+construct a list of principals for the user and call
+:meth:`pyramid.authorization.ACLHelper.permits`, which will return an
+:class:`pyramid.security.ACLAllowed` or :class:`pyramid.security.ACLDenied`
+object. An implementation might look like this:
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.security import Everyone, Authenticated
+ from pyramid.authorization import ACLHelper
+
+ class SecurityPolicy:
+ def permits(self, request, context, identity, permission):
+ principals = [Everyone]
+ if identity is not None:
+ principals.append(Authenticated)
+ principals.append('user:' + identity.id)
+ principals.append('group:' + identity.group)
+ return ACLHelper().permits(context, principals, permission)
-When the default :app:`Pyramid` :term:`authorization policy` determines whether
-a user possesses a particular permission with respect to a resource, it
-examines the :term:`ACL` associated with the resource. An ACL is associated
-with a resource by adding an ``__acl__`` attribute to the resource object.
-This attribute can be defined on the resource *instance* if you need
-instance-level security, or it can be defined on the resource *class* if you
-just need type-level security.
+To associate an ACL with a resource, add an ``__acl__`` attribute to the
+resource object. This attribute can be defined on the resource *instance* if
+you need instance-level security, or it can be defined on the resource *class*
+if you just need type-level security.
For example, an ACL might be attached to the resource for a blog via its class:
@@ -334,11 +449,9 @@ matches. The second element is a :term:`principal`. The third argument is a
permission or sequence of permission names.
A principal is usually a user id, however it also may be a group id if your
-authentication system provides group information and the effective
-:term:`authentication policy` policy is written to respect group information.
-See :ref:`extending_default_authentication_policies`.
+authentication system provides group information.
-Each ACE in an ACL is processed by an authorization policy *in the order
+Each ACE in an ACL is processed by the ACL helper *in the order
dictated by the ACL*. So if you have an ACL like this:
.. code-block:: python
@@ -353,9 +466,9 @@ dictated by the ACL*. So if you have an ACL like this:
(Deny, Everyone, 'view'),
]
-The default authorization policy will *allow* everyone the view permission,
-even though later in the ACL you have an ACE that denies everyone the view
-permission. On the other hand, if you have an ACL like this:
+The ACL helper will *allow* everyone the view permission, even though later in
+the ACL you have an ACE that denies everyone the view permission. On the other
+hand, if you have an ACL like this:
.. code-block:: python
:linenos:
@@ -369,7 +482,7 @@ permission. On the other hand, if you have an ACL like this:
(Allow, Everyone, 'view'),
]
-The authorization policy will deny everyone the view permission, even though
+The ACL helper will deny everyone the view permission, even though
later in the ACL, there is an ACE that allows everyone.
The third argument in an ACE can also be a sequence of permission names instead
@@ -388,6 +501,7 @@ can collapse this into a single ACE, as below.
(Allow, 'group:editors', ('add', 'edit')),
]
+.. _special_principals:
.. index::
single: principal
@@ -445,8 +559,7 @@ permissions in :data:`pyramid.security.DENY_ALL`. This ACE is often used as
the *last* ACE of an ACL to explicitly cause inheriting authorization policies
to "stop looking up the traversal tree" (effectively breaking any inheritance).
For example, an ACL which allows *only* ``fred`` the view permission for a
-particular resource, despite what inherited ACLs may say when the default
-authorization policy is in effect, might look like so:
+particular resource, despite what inherited ACLs may say, might look like so:
.. code-block:: python
:linenos:
@@ -472,11 +585,10 @@ following:
ACL Inheritance and Location-Awareness
--------------------------------------
-While the default :term:`authorization policy` is in place, if a resource
-object does not have an ACL when it is the context, its *parent* is consulted
-for an ACL. If that object does not have an ACL, *its* parent is consulted for
-an ACL, ad infinitum, until we've reached the root and there are no more
-parents left.
+While the ACL helper is in place, if a resource object does not have an ACL
+when it is the context, its *parent* is consulted for an ACL. If that object
+does not have an ACL, *its* parent is consulted for an ACL, ad infinitum, until
+we've reached the root and there are no more parents left.
In order to allow the security machinery to perform ACL inheritance, resource
objects must provide *location-awareness*. Providing *location-awareness*
@@ -567,184 +679,16 @@ denied or allowed. Introspecting this information in the debugger or via print
statements when a call to :meth:`~pyramid.request.Request.has_permission` fails
is often useful.
-.. index::
- single: authentication policy (extending)
-
-.. _extending_default_authentication_policies:
-
-Extending Default Authentication Policies
------------------------------------------
-
-Pyramid ships with some built in authentication policies for use in your
-applications. See :mod:`pyramid.authentication` for the available policies.
-They differ on their mechanisms for tracking authentication credentials between
-requests, however they all interface with your application in mostly the same
-way.
-
-Above you learned about :ref:`assigning_acls`. Each :term:`principal` used in
-the :term:`ACL` is matched against the list returned from
-:meth:`pyramid.interfaces.IAuthenticationPolicy.effective_principals`.
-Similarly, :meth:`pyramid.request.Request.authenticated_userid` maps to
-:meth:`pyramid.interfaces.IAuthenticationPolicy.authenticated_userid`.
-
-You may control these values by subclassing the default authentication
-policies. For example, below we subclass the
-:class:`pyramid.authentication.AuthTktAuthenticationPolicy` and define extra
-functionality to query our database before confirming that the :term:`userid`
-is valid in order to avoid blindly trusting the value in the cookie (what if
-the cookie is still valid, but the user has deleted their account?). We then
-use that :term:`userid` to augment the ``effective_principals`` with
-information about groups and other state for that user.
-
-.. code-block:: python
- :linenos:
-
- from pyramid.authentication import AuthTktAuthenticationPolicy
-
- class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
- def authenticated_userid(self, request):
- userid = self.unauthenticated_userid(request)
- if userid:
- if request.verify_userid_is_still_valid(userid):
- return userid
-
- def effective_principals(self, request):
- principals = [Everyone]
- userid = self.authenticated_userid(request)
- if userid:
- principals += [Authenticated, str(userid)]
- return principals
-
-In most instances ``authenticated_userid`` and ``effective_principals`` are
-application-specific, whereas ``unauthenticated_userid``, ``remember``, and
-``forget`` are generic and focused on transport and serialization of data
-between consecutive requests.
-
-.. index::
- single: authentication policy (creating)
-
-.. _creating_an_authentication_policy:
-
-Creating Your Own Authentication Policy
----------------------------------------
-
-:app:`Pyramid` ships with a number of useful out-of-the-box security policies
-(see :mod:`pyramid.authentication`). However, creating your own authentication
-policy is often necessary when you want to control the "horizontal and
-vertical" of how your users authenticate. Doing so is a matter of creating an
-instance of something that implements the following interface:
-
-.. code-block:: python
- :linenos:
-
- class IAuthenticationPolicy(object):
- """ An object representing a Pyramid authentication policy. """
-
- def authenticated_userid(self, request):
- """ Return the authenticated :term:`userid` or ``None`` if
- no authenticated userid can be found. This method of the
- policy should ensure that a record exists in whatever
- persistent store is used related to the user (the user
- should not have been deleted); if a record associated with
- the current id does not exist in a persistent store, it
- should return ``None``.
- """
-
- def unauthenticated_userid(self, request):
- """ Return the *unauthenticated* userid. This method
- performs the same duty as ``authenticated_userid`` but is
- permitted to return the userid based only on data present
- in the request; it needn't (and shouldn't) check any
- persistent store to ensure that the user record related to
- the request userid exists.
-
- This method is intended primarily a helper to assist the
- ``authenticated_userid`` method in pulling credentials out
- of the request data, abstracting away the specific headers,
- query strings, etc that are used to authenticate the request.
- """
-
- def effective_principals(self, request):
- """ Return a sequence representing the effective principals
- typically including the :term:`userid` and any groups belonged
- to by the current user, always including 'system' groups such
- as ``pyramid.security.Everyone`` and
- ``pyramid.security.Authenticated``.
- """
-
- def remember(self, 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(self, request):
- """ Return a set of headers suitable for 'forgetting' the
- current user on subsequent requests.
- """
-
-After you do so, you can pass an instance of such a class into the
-:class:`~pyramid.config.Configurator.set_authentication_policy` method at
-configuration time to use it.
-
-.. index::
- single: authorization policy (creating)
-
-.. _creating_an_authorization_policy:
-
-Creating Your Own Authorization Policy
---------------------------------------
-
-An authorization policy is a policy that allows or denies access after a user
-has been authenticated. Most :app:`Pyramid` applications will use the default
-:class:`pyramid.authorization.ACLAuthorizationPolicy`.
-
-However, in some cases, it's useful to be able to use a different authorization
-policy than the default :class:`~pyramid.authorization.ACLAuthorizationPolicy`.
-For example, it might be desirable to construct an alternate authorization
-policy which allows the application to use an authorization mechanism that does
-not involve :term:`ACL` objects.
-
-:app:`Pyramid` ships with only a single default authorization policy, so you'll
-need to create your own if you'd like to use a different one. Creating and
-using your own authorization policy is a matter of creating an instance of an
-object that implements the following interface:
-
-.. code-block:: python
- :linenos:
-
- class IAuthorizationPolicy(Interface):
- """ An object representing a Pyramid authorization policy. """
- def permits(context, principals, permission):
- """ Return an instance of :class:`pyramid.security.Allowed` if any
- of the ``principals`` is allowed the ``permission`` in the current
- ``context``, else return an instance of
- :class:`pyramid.security.Denied`.
- """
-
- def principals_allowed_by_permission(context, permission):
- """ Return a set of principal identifiers allowed by the
- ``permission`` in ``context``. This behavior is optional; if you
- choose to not implement it you should define this method as
- something which raises a ``NotImplementedError``. This method
- will only be called when the
- ``pyramid.security.principals_allowed_by_permission`` API is
- used."""
-
-After you do so, you can pass an instance of such a class into the
-:class:`~pyramid.config.Configurator.set_authorization_policy` method at
-configuration time to use it.
-
.. _admonishment_against_secret_sharing:
Admonishment Against Secret-Sharing
-----------------------------------
A "secret" is required by various components of Pyramid. For example, the
-:term:`authentication policy` below uses a secret value ``seekrit``::
+helper below might be used for a security policy and uses a secret value
+``seekrit``::
- authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512')
+ helper = AuthTktCookieHelper('seekrit', hashalg='sha512')
A :term:`session factory` also requires a secret::
@@ -752,9 +696,8 @@ A :term:`session factory` also requires a secret::
It is tempting to use the same secret for multiple Pyramid subsystems. For
example, you might be tempted to use the value ``seekrit`` as the secret for
-both the authentication policy and the session factory defined above. This is
-a bad idea, because in both cases, these secrets are used to sign the payload
-of the data.
+both the helper and the session factory defined above. This is a bad idea,
+because in both cases, these secrets are used to sign the payload of the data.
If you use the same secret for two different parts of your application for
signing purposes, it may allow an attacker to get his chosen plaintext signed,
diff --git a/docs/whatsnew-2.0.rst b/docs/whatsnew-2.0.rst
new file mode 100644
index 000000000..446fcda21
--- /dev/null
+++ b/docs/whatsnew-2.0.rst
@@ -0,0 +1,103 @@
+What's New in Pyramid 2.0
+=========================
+
+This article explains the new features in :app:`Pyramid` version 2.0 as
+compared to its predecessor, :app:`Pyramid` 1.10. It also documents backwards
+incompatibilities between the two versions and deprecations added to
+:app:`Pyramid` 2.0, as well as software dependency changes and notable
+documentation additions.
+
+Feature Additions
+-----------------
+
+The feature additions in Pyramid 2.0 are as follows:
+
+- The authentication and authorization policies of Pyramid 1.x have been merged
+ into a single :term:`security policy` in Pyramid 2.0. For details on how to
+ migrate to the new security policy, see :ref:`upgrading_auth`.
+ Authentication and authorization policies can still be used and will continue
+ to function normally for the time being.
+
+Deprecations
+------------
+
+- Authentication and authorization policies have been deprecated in favor of
+ the new :term:`security policy`.
+
+.. _upgrading_auth:
+
+Upgrading Authentication/Authorization
+--------------------------------------
+
+The authentication and authorization policies of Pyramid 1.x have been merged
+into a single :term:`security policy` in Pyramid 2.0. Authentication and
+authorization policies can still be used and will continue to function
+normally, however they have been deprecated and support may be removed in
+upcoming versions.
+
+The new security policy should implement
+:class:`pyramid.interfaces.ISecurityPolicy` and can be set via the
+``security_policy`` argument of :class:`pyramid.config.Configurator` or
+:meth:`pyramid.config.Configurator.set_security_policy`.
+
+The new security policy adds the concept of an :term:`identity`, which is an
+object representing the user associated with the current request. The identity
+can be accessed via :attr:`pyramid.request.Request.authenticated_identity`.
+The object can be of any shape, such as a simple ID string or an ORM object,
+but should implement a ``__str__`` method that outputs a string identifying the
+current user, e.g. the ID of the user object in a database. The string
+representation is return as
+:attr:`pyramid.request.Request.authenticated_userid`.
+(:attr:`pyramid.request.Request.unauthenticated_userid` has been deprecated.)
+
+The concept of :term:`principals <principal>` has been removed; the
+``permits`` method is passed an identity object. This change gives much more
+flexibility in authorization implementations, especially those that do not
+match the ACL pattern. If you were previously using
+:class:`pyramid.authorization.ACLAuthorizationPolicy`, you can achieve the same
+results by writing your own ``permits`` method using
+:class:`pyraid.authorization.ACLHelper`. For more details on implementing an
+ACL, see :ref:`assigning_acls`.
+
+Pyramid does not provide any built-in security policies. Similiar
+functionality of the authentication and authorization policies is now provided
+by helpers, which can be utilized to implement your own security policy. The
+functionality of the legacy authentication policies roughly correspond to the
+following helpers:
+
++----------------------------------------------------------------+-------------------------------------------------------------------+
+| Authentication Policy | Security Policy Helper |
++================================================================+===================================================================+
+| :class:`pyramid.authentication.SessionAuthenticationPolicy` | :class:`pyramid.authentication.SessionAuthenticationHelper` |
++----------------------------------------------------------------+-------------------------------------------------------------------+
+| :class:`pyramid.authentication.AuthTktAuthenticationPolicy` | :class:`pyramid.authentication.AuthTktCookieHelper` |
++----------------------------------------------------------------+-------------------------------------------------------------------+
+| :class:`pyramid.authentication.BasicAuthAuthenticationPolicy` | Use :func:`pyramid.authentication.extract_http_basic_credentials` |
+| | to retrieve credentials. |
++----------------------------------------------------------------+-------------------------------------------------------------------+
+| :class:`pyramid.authentication.RemoteUserAuthenticationPolicy` | ``REMOTE_USER`` can be accessed with |
+| | ``request.environ.get('REMOTE_USER')``. |
++----------------------------------------------------------------+-------------------------------------------------------------------+
+| :class:`pyramid.authentication.RepozeWho1AuthenticationPolicy` | No equivalent. |
++----------------------------------------------------------------+-------------------------------------------------------------------+
+
+For further documentation on implementing security policies, see
+:ref:`writing_security_policy`.
+
+.. _behavior_of_legacy_auth:
+
+Behavior of the Legacy System
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Legacy authentication and authorization policies will continue to function as
+normal, as well as all related :class:`pyramid.request.Request` properties.
+The new :attr:`pyramid.request.Request.authenticated_identity` property will
+output the same result as :attr:`pyramid.request.Request.authenticated_userid`.
+
+If using a security policy,
+:attr:`pyramid.request.Request.unauthenticated_userid` and
+:attr:`pyramid.request.Request.authenticated_userid` will both return the
+string representation of the :term:`identity`.
+:attr:`pyramid.request.Request.effective_principals` will always return a
+one-element list containing the :data:`pyramid.security.Everyone` principal, as
+there is no equivalent in the new security policy.
diff --git a/src/pyramid/authentication.py b/src/pyramid/authentication.py
index 21cfc0c0e..de06fe955 100644
--- a/src/pyramid/authentication.py
+++ b/src/pyramid/authentication.py
@@ -428,150 +428,9 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy):
@implementer(IAuthenticationPolicy)
class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
"""A :app:`Pyramid` :term:`authentication policy` which
- obtains data from a Pyramid "auth ticket" cookie.
-
- Constructor Arguments
-
- ``secret``
-
- The secret (a string) used for auth_tkt cookie signing. This value
- should be unique across all values provided to Pyramid for various
- subsystem secrets (see :ref:`admonishment_against_secret_sharing`).
- Required.
-
- ``callback``
-
- Default: ``None``. A callback passed the userid and the
- request, expected to return ``None`` if the userid doesn't
- exist or a sequence of principal identifiers (possibly empty) if
- the user does exist. If ``callback`` is ``None``, the userid
- will be assumed to exist with no principals. Optional.
-
- ``cookie_name``
-
- Default: ``auth_tkt``. The cookie name used
- (string). Optional.
-
- ``secure``
-
- Default: ``False``. Only send the cookie back over a secure
- conn. Optional.
-
- ``include_ip``
-
- Default: ``False``. Make the requesting IP address part of
- the authentication data in the cookie. Optional.
-
- For IPv6 this option is not recommended. The ``mod_auth_tkt``
- specification does not specify how to handle IPv6 addresses, so using
- this option in combination with IPv6 addresses may cause an
- incompatible cookie. It ties the authentication ticket to that
- individual's IPv6 address.
-
- ``timeout``
-
- Default: ``None``. Maximum number of seconds which a newly
- issued ticket will be considered valid. After this amount of
- time, the ticket will expire (effectively logging the user
- out). If this value is ``None``, the ticket never expires.
- Optional.
-
- ``reissue_time``
-
- Default: ``None``. If this parameter is set, it represents the number
- of seconds that must pass before an authentication token cookie is
- automatically reissued as the result of a request which requires
- authentication. The duration is measured as the number of seconds
- since the last auth_tkt cookie was issued and 'now'. If this value is
- ``0``, a new ticket cookie will be reissued on every request which
- requires authentication.
-
- A good rule of thumb: if you want auto-expired cookies based on
- inactivity: set the ``timeout`` value to 1200 (20 mins) and set the
- ``reissue_time`` value to perhaps a tenth of the ``timeout`` value
- (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower
- than the ``reissue_time`` value, as the ticket will never be reissued
- if so. However, such a configuration is not explicitly prevented.
-
- Optional.
-
- ``max_age``
-
- Default: ``None``. The max age of the auth_tkt cookie, in
- seconds. This differs from ``timeout`` inasmuch as ``timeout``
- represents the lifetime of the ticket contained in the cookie,
- while this value represents the lifetime of the cookie itself.
- When this value is set, the cookie's ``Max-Age`` and
- ``Expires`` settings will be set, allowing the auth_tkt cookie
- to last between browser sessions. It is typically nonsensical
- to set this to a value that is lower than ``timeout`` or
- ``reissue_time``, although it is not explicitly prevented.
- Optional.
-
- ``path``
-
- Default: ``/``. The path for which the auth_tkt cookie is valid.
- May be desirable if the application only serves part of a domain.
- Optional.
-
- ``http_only``
-
- Default: ``False``. Hide cookie from JavaScript by setting the
- HttpOnly flag. Not honored by all browsers.
- Optional.
-
- ``wild_domain``
-
- Default: ``True``. An auth_tkt cookie will be generated for the
- wildcard domain. If your site is hosted as ``example.com`` this
- will make the cookie available for sites underneath ``example.com``
- such as ``www.example.com``.
- Optional.
-
- ``parent_domain``
-
- Default: ``False``. An auth_tkt cookie will be generated for the
- parent domain of the current site. For example if your site is
- hosted under ``www.example.com`` a cookie will be generated for
- ``.example.com``. This can be useful if you have multiple sites
- sharing the same domain. This option supercedes the ``wild_domain``
- option.
- Optional.
-
- ``domain``
-
- Default: ``None``. If provided the auth_tkt cookie will only be
- set for this domain. This option is not compatible with ``wild_domain``
- and ``parent_domain``.
- Optional.
-
- ``hashalg``
-
- Default: ``sha512`` (the literal string).
-
- Any hash algorithm supported by Python's ``hashlib.new()`` function
- can be used as the ``hashalg``.
-
- Cookies generated by different instances of AuthTktAuthenticationPolicy
- using different ``hashalg`` options are not compatible. Switching the
- ``hashalg`` will imply that all existing users with a valid cookie will
- be required to re-login.
-
- Optional.
-
- ``debug``
-
- Default: ``False``. If ``debug`` is ``True``, log messages to the
- Pyramid debug logger about the results of various authentication
- steps. The output from debugging is useful for reporting to maillist
- or IRC channels when asking for support.
-
- ``samesite``
-
- Default: ``'Lax'``. The 'samesite' option of the session cookie. Set
- the value to ``None`` to turn off the samesite option.
-
- This option is available as of :app:`Pyramid` 1.10.
+ obtains data from a Pyramid "auth ticket" cookie. See
+ :class:`.AuthTktCookieHelper` for documentation of the constructor
+ arguments.
.. versionchanged:: 1.4
@@ -823,10 +682,150 @@ def encode_ip_timestamp(ip, timestamp):
class AuthTktCookieHelper(object):
"""
- A helper class for use in third-party authentication policy
- implementations. See
- :class:`pyramid.authentication.AuthTktAuthenticationPolicy` for the
- meanings of the constructor arguments.
+ A helper class for security policies that obtains data from an "auth
+ ticket" cookie.
+
+ Constructor Arguments
+
+ ``secret``
+
+ The secret (a string) used for auth_tkt cookie signing. This value
+ should be unique across all values provided to Pyramid for various
+ subsystem secrets (see :ref:`admonishment_against_secret_sharing`).
+ Required.
+
+ ``callback``
+
+ Default: ``None``. A callback passed the userid and the
+ request, expected to return ``None`` if the userid doesn't
+ exist or a sequence of principal identifiers (possibly empty) if
+ the user does exist. If ``callback`` is ``None``, the userid
+ will be assumed to exist with no principals. Optional.
+
+ ``cookie_name``
+
+ Default: ``auth_tkt``. The cookie name used
+ (string). Optional.
+
+ ``secure``
+
+ Default: ``False``. Only send the cookie back over a secure
+ conn. Optional.
+
+ ``include_ip``
+
+ Default: ``False``. Make the requesting IP address part of
+ the authentication data in the cookie. Optional.
+
+ For IPv6 this option is not recommended. The ``mod_auth_tkt``
+ specification does not specify how to handle IPv6 addresses, so using
+ this option in combination with IPv6 addresses may cause an
+ incompatible cookie. It ties the authentication ticket to that
+ individual's IPv6 address.
+
+ ``timeout``
+
+ Default: ``None``. Maximum number of seconds which a newly
+ issued ticket will be considered valid. After this amount of
+ time, the ticket will expire (effectively logging the user
+ out). If this value is ``None``, the ticket never expires.
+ Optional.
+
+ ``reissue_time``
+
+ Default: ``None``. If this parameter is set, it represents the number
+ of seconds that must pass before an authentication token cookie is
+ automatically reissued as the result of a request which requires
+ authentication. The duration is measured as the number of seconds
+ since the last auth_tkt cookie was issued and 'now'. If this value is
+ ``0``, a new ticket cookie will be reissued on every request which
+ requires authentication.
+
+ A good rule of thumb: if you want auto-expired cookies based on
+ inactivity: set the ``timeout`` value to 1200 (20 mins) and set the
+ ``reissue_time`` value to perhaps a tenth of the ``timeout`` value
+ (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower
+ than the ``reissue_time`` value, as the ticket will never be reissued
+ if so. However, such a configuration is not explicitly prevented.
+
+ Optional.
+
+ ``max_age``
+
+ Default: ``None``. The max age of the auth_tkt cookie, in
+ seconds. This differs from ``timeout`` inasmuch as ``timeout``
+ represents the lifetime of the ticket contained in the cookie,
+ while this value represents the lifetime of the cookie itself.
+ When this value is set, the cookie's ``Max-Age`` and
+ ``Expires`` settings will be set, allowing the auth_tkt cookie
+ to last between browser sessions. It is typically nonsensical
+ to set this to a value that is lower than ``timeout`` or
+ ``reissue_time``, although it is not explicitly prevented.
+ Optional.
+
+ ``path``
+
+ Default: ``/``. The path for which the auth_tkt cookie is valid.
+ May be desirable if the application only serves part of a domain.
+ Optional.
+
+ ``http_only``
+
+ Default: ``False``. Hide cookie from JavaScript by setting the
+ HttpOnly flag. Not honored by all browsers.
+ Optional.
+
+ ``wild_domain``
+
+ Default: ``True``. An auth_tkt cookie will be generated for the
+ wildcard domain. If your site is hosted as ``example.com`` this
+ will make the cookie available for sites underneath ``example.com``
+ such as ``www.example.com``.
+ Optional.
+
+ ``parent_domain``
+
+ Default: ``False``. An auth_tkt cookie will be generated for the
+ parent domain of the current site. For example if your site is
+ hosted under ``www.example.com`` a cookie will be generated for
+ ``.example.com``. This can be useful if you have multiple sites
+ sharing the same domain. This option supercedes the ``wild_domain``
+ option.
+ Optional.
+
+ ``domain``
+
+ Default: ``None``. If provided the auth_tkt cookie will only be
+ set for this domain. This option is not compatible with ``wild_domain``
+ and ``parent_domain``.
+ Optional.
+
+ ``hashalg``
+
+ Default: ``sha512`` (the literal string).
+
+ Any hash algorithm supported by Python's ``hashlib.new()`` function
+ can be used as the ``hashalg``.
+
+ Cookies generated by different instances of AuthTktAuthenticationPolicy
+ using different ``hashalg`` options are not compatible. Switching the
+ ``hashalg`` will imply that all existing users with a valid cookie will
+ be required to re-login.
+
+ Optional.
+
+ ``debug``
+
+ Default: ``False``. If ``debug`` is ``True``, log messages to the
+ Pyramid debug logger about the results of various authentication
+ steps. The output from debugging is useful for reporting to maillist
+ or IRC channels when asking for support.
+
+ ``samesite``
+
+ Default: ``'Lax'``. The 'samesite' option of the session cookie. Set
+ the value to ``None`` to turn off the samesite option.
+
"""
parse_ticket = staticmethod(parse_ticket) # for tests
@@ -1099,9 +1098,36 @@ class SessionAuthenticationPolicy(CallbackAuthenticationPolicy):
def __init__(self, prefix='auth.', callback=None, debug=False):
self.callback = callback
- self.prefix = prefix or ''
- self.userid_key = prefix + 'userid'
self.debug = debug
+ self.helper = SessionAuthenticationHelper(prefix)
+
+ def remember(self, request, userid, **kw):
+ """ Store a userid in the session."""
+ return self.helper.remember(request, userid, **kw)
+
+ def forget(self, request):
+ """ Remove the stored userid from the session."""
+ return self.helper.forget(request)
+
+ def unauthenticated_userid(self, request):
+ return self.helper.identify(request)
+
+
+class SessionAuthenticationHelper:
+ """ A helper for use with a :term:`security policy` which stores user data
+ in the configured :term:`session`.
+
+ Constructor Arguments
+
+ ``prefix``
+
+ A prefix used when storing the authentication parameters in the
+ session. Defaults to 'auth.'. Optional.
+
+ """
+
+ def __init__(self, prefix='auth.'):
+ self.userid_key = prefix + 'userid'
def remember(self, request, userid, **kw):
""" Store a userid in the session."""
@@ -1114,7 +1140,8 @@ class SessionAuthenticationPolicy(CallbackAuthenticationPolicy):
del request.session[self.userid_key]
return []
- def unauthenticated_userid(self, request):
+ def identify(self, request):
+ """ Return the stored userid."""
return request.session.get(self.userid_key)
diff --git a/src/pyramid/authorization.py b/src/pyramid/authorization.py
index 6056a8d25..498938fd5 100644
--- a/src/pyramid/authorization.py
+++ b/src/pyramid/authorization.py
@@ -14,59 +14,74 @@ class ACLAuthorizationPolicy(object):
""" An :term:`authorization policy` which consults an :term:`ACL`
object attached to a :term:`context` to determine authorization
information about a :term:`principal` or multiple principals.
- If the context is part of a :term:`lineage`, the context's parents
- are consulted for ACL information too. The following is true
- about this security policy.
-
- - When checking whether the 'current' user is permitted (via the
- ``permits`` method), the security policy consults the
- ``context`` for an ACL first. If no ACL exists on the context,
- or one does exist but the ACL does not explicitly allow or deny
- access for any of the effective principals, consult the
- context's parent ACL, and so on, until the lineage is exhausted
- or we determine that the policy permits or denies.
-
- During this processing, if any :data:`pyramid.security.Deny`
- ACE is found matching any principal in ``principals``, stop
- processing by returning an
- :class:`pyramid.security.ACLDenied` instance (equals
- ``False``) immediately. If any
- :data:`pyramid.security.Allow` ACE is found matching any
- principal, stop processing by returning an
- :class:`pyramid.security.ACLAllowed` instance (equals
- ``True``) immediately. If we exhaust the context's
- :term:`lineage`, and no ACE has explicitly permitted or denied
- access, return an instance of
- :class:`pyramid.security.ACLDenied` (equals ``False``).
-
- - When computing principals allowed by a permission via the
- :func:`pyramid.security.principals_allowed_by_permission`
- method, we compute the set of principals that are explicitly
- granted the ``permission`` in the provided ``context``. We do
- this by walking 'up' the object graph *from the root* to the
- context. During this walking process, if we find an explicit
- :data:`pyramid.security.Allow` ACE for a principal that
- matches the ``permission``, the principal is included in the
- allow list. However, if later in the walking process that
- principal is mentioned in any :data:`pyramid.security.Deny`
- ACE for the permission, the principal is removed from the allow
- list. If a :data:`pyramid.security.Deny` to the principal
- :data:`pyramid.security.Everyone` is encountered during the
- walking process that matches the ``permission``, the allow list
- is cleared for all principals encountered in previous ACLs. The
- walking process ends after we've processed the any ACL directly
- attached to ``context``; a set of principals is returned.
+ This class is a wrapper around :class:`.ACLHelper`, refer to that class for
+ more detailed documentation.
Objects of this class implement the
:class:`pyramid.interfaces.IAuthorizationPolicy` interface.
+
+ .. deprecated:: 2.0
+
+ Authorization policies have been deprecated by the new security system.
+ See :ref:`upgrading_auth` for more information.
+
"""
+ def __init__(self):
+ self.helper = ACLHelper()
+
def permits(self, context, principals, permission):
""" Return an instance of
:class:`pyramid.security.ACLAllowed` instance if the policy
permits access, return an instance of
:class:`pyramid.security.ACLDenied` if not."""
+ return self.helper.permits(context, principals, permission)
+
+ def principals_allowed_by_permission(self, context, permission):
+ """ Return the set of principals explicitly granted the
+ permission named ``permission`` according to the ACL directly
+ attached to the ``context`` as well as inherited ACLs based on
+ the :term:`lineage`."""
+ return self.helper.principals_allowed_by_permission(
+ context, permission
+ )
+
+
+class ACLHelper:
+ """ A helper for use with constructing a :term:`security policy` which
+ consults an :term:`ACL` object attached to a :term:`context` to determine
+ authorization information about a :term:`principal` or multiple principals.
+ If the context is part of a :term:`lineage`, the context's parents are
+ consulted for ACL information too.
+
+ """
+ def permits(self, context, principals, permission):
+ """ Return an instance of :class:`pyramid.security.ACLAllowed` if the
+ ACL allows access a user with the given principals, return an instance
+ of :class:`pyramid.security.ACLDenied` if not.
+
+ When checking if principals are allowed, the security policy consults
+ the ``context`` for an ACL first. If no ACL exists on the context, or
+ one does exist but the ACL does not explicitly allow or deny access for
+ any of the effective principals, consult the context's parent ACL, and
+ so on, until the lineage is exhausted or we determine that the policy
+ permits or denies.
+
+ During this processing, if any :data:`pyramid.security.Deny`
+ ACE is found matching any principal in ``principals``, stop
+ processing by returning an
+ :class:`pyramid.security.ACLDenied` instance (equals
+ ``False``) immediately. If any
+ :data:`pyramid.security.Allow` ACE is found matching any
+ principal, stop processing by returning an
+ :class:`pyramid.security.ACLAllowed` instance (equals
+ ``True``) immediately. If we exhaust the context's
+ :term:`lineage`, and no ACE has explicitly permitted or denied
+ access, return an instance of
+ :class:`pyramid.security.ACLDenied` (equals ``False``).
+
+ """
acl = '<No ACL found on any object in resource lineage>'
for location in lineage(context):
@@ -100,10 +115,27 @@ class ACLAuthorizationPolicy(object):
)
def principals_allowed_by_permission(self, context, permission):
- """ Return the set of principals explicitly granted the
- permission named ``permission`` according to the ACL directly
- attached to the ``context`` as well as inherited ACLs based on
- the :term:`lineage`."""
+ """ Return the set of principals explicitly granted the permission
+ named ``permission`` according to the ACL directly attached to the
+ ``context`` as well as inherited ACLs based on the :term:`lineage`.
+
+ When computing principals allowed by a permission, we compute the set
+ of principals that are explicitly granted the ``permission`` in the
+ provided ``context``. We do this by walking 'up' the object graph
+ *from the root* to the context. During this walking process, if we
+ find an explicit :data:`pyramid.security.Allow` ACE for a principal
+ that matches the ``permission``, the principal is included in the allow
+ list. However, if later in the walking process that principal is
+ mentioned in any :data:`pyramid.security.Deny` ACE for the permission,
+ the principal is removed from the allow list. If a
+ :data:`pyramid.security.Deny` to the principal
+ :data:`pyramid.security.Everyone` is encountered during the walking
+ process that matches the ``permission``, the allow list is cleared for
+ all principals encountered in previous ACLs. The walking process ends
+ after we've processed the any ACL directly attached to ``context``; a
+ set of principals is returned.
+
+ """
allowed = set()
for location in reversed(list(lineage(context))):
diff --git a/src/pyramid/config/__init__.py b/src/pyramid/config/__init__.py
index 072b654c4..cf1bfad44 100644
--- a/src/pyramid/config/__init__.py
+++ b/src/pyramid/config/__init__.py
@@ -139,6 +139,9 @@ class Configurator(
:term:`dotted Python name` to the same. If it is ``None``, a default
root factory will be used.
+ If ``security_policy`` is passed, it should be an instance of a
+ :term:`security policy` or a :term:`dotted Python name` to the same.
+
If ``authentication_policy`` is passed, it should be an instance
of an :term:`authentication policy` or a :term:`dotted Python
name` to the same.
@@ -278,6 +281,7 @@ class Configurator(
package=None,
settings=None,
root_factory=None,
+ security_policy=None,
authentication_policy=None,
authorization_policy=None,
renderers=None,
@@ -315,6 +319,7 @@ class Configurator(
root_factory=root_factory,
authentication_policy=authentication_policy,
authorization_policy=authorization_policy,
+ security_policy=security_policy,
renderers=renderers,
debug_logger=debug_logger,
locale_negotiator=locale_negotiator,
@@ -330,6 +335,7 @@ class Configurator(
self,
settings=None,
root_factory=None,
+ security_policy=None,
authentication_policy=None,
authorization_policy=None,
renderers=None,
@@ -415,6 +421,9 @@ class Configurator(
if authentication_policy:
self.set_authentication_policy(authentication_policy)
+ if security_policy:
+ self.set_security_policy(security_policy)
+
if default_view_mapper is not None:
self.set_view_mapper(default_view_mapper)
diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py
index 17ac5ded7..32b4db03c 100644
--- a/src/pyramid/config/security.py
+++ b/src/pyramid/config/security.py
@@ -1,4 +1,5 @@
from zope.interface import implementer
+from zope.deprecation import deprecate
from pyramid.interfaces import (
IAuthorizationPolicy,
@@ -6,6 +7,7 @@ from pyramid.interfaces import (
ICSRFStoragePolicy,
IDefaultCSRFOptions,
IDefaultPermission,
+ ISecurityPolicy,
PHASE1_CONFIG,
PHASE2_CONFIG,
)
@@ -13,6 +15,7 @@ from pyramid.interfaces import (
from pyramid.csrf import LegacySessionCSRFStoragePolicy
from pyramid.exceptions import ConfigurationError
from pyramid.util import as_sorted_tuple
+from pyramid.security import LegacySecurityPolicy
from pyramid.config.actions import action_method
@@ -22,8 +25,54 @@ class SecurityConfiguratorMixin(object):
self.set_csrf_storage_policy(LegacySessionCSRFStoragePolicy())
@action_method
+ def set_security_policy(self, policy):
+ """ Override the :app:`Pyramid` :term:`security policy` in the current
+ configuration. The ``policy`` argument must be an instance
+ of a security policy or a :term:`dotted Python name`
+ that points at an instance of a security policy.
+
+ .. note::
+
+ Using the ``security_policy`` argument to the
+ :class:`pyramid.config.Configurator` constructor can be used to
+ achieve the same purpose.
+
+ """
+
+ def register():
+ self.registry.registerUtility(policy, ISecurityPolicy)
+
+ policy = self.maybe_dotted(policy)
+ intr = self.introspectable(
+ 'security policy',
+ None,
+ self.object_description(policy),
+ 'security policy',
+ )
+ intr['policy'] = policy
+ self.action(
+ ISecurityPolicy,
+ register,
+ order=PHASE2_CONFIG,
+ introspectables=(intr,),
+ )
+
+ @deprecate(
+ 'Authentication and authorization policies have been deprecated in '
+ 'favor of security policies. See '
+ 'https://docs.pylonsproject.org/projects/pyramid/en/latest'
+ '/whatsnew-2.0.html#upgrading-authentication-authorization '
+ 'for more information.'
+ )
+ @action_method
def set_authentication_policy(self, policy):
- """ Override the :app:`Pyramid` :term:`authentication policy` in the
+ """
+ .. deprecated:: 2.0
+
+ Authentication policies have been replaced by
+ security policies. See :ref:`upgrading_auth` for more information.
+
+ Override the :app:`Pyramid` :term:`authentication policy` in the
current configuration. The ``policy`` argument must be an instance
of an authentication policy or a :term:`dotted Python name`
that points at an instance of an authentication policy.
@@ -37,14 +86,22 @@ class SecurityConfiguratorMixin(object):
"""
def register():
- self._set_authentication_policy(policy)
+ self.registry.registerUtility(policy, IAuthenticationPolicy)
if self.registry.queryUtility(IAuthorizationPolicy) is None:
raise ConfigurationError(
'Cannot configure an authentication policy without '
'also configuring an authorization policy '
'(use the set_authorization_policy method)'
)
+ if self.registry.queryUtility(ISecurityPolicy) is not None:
+ raise ConfigurationError(
+ 'Cannot configure an authentication and authorization'
+ 'policy with a configured security policy.'
+ )
+ security_policy = LegacySecurityPolicy()
+ self.registry.registerUtility(security_policy, ISecurityPolicy)
+ policy = self.maybe_dotted(policy)
intr = self.introspectable(
'authentication policy',
None,
@@ -60,13 +117,15 @@ class SecurityConfiguratorMixin(object):
introspectables=(intr,),
)
- def _set_authentication_policy(self, policy):
- policy = self.maybe_dotted(policy)
- self.registry.registerUtility(policy, IAuthenticationPolicy)
-
@action_method
def set_authorization_policy(self, policy):
- """ Override the :app:`Pyramid` :term:`authorization policy` in the
+ """
+ .. deprecated:: 2.0
+
+ Authentication policies have been replaced by
+ security policies. See :ref:`upgrading_auth` for more information.
+
+ Override the :app:`Pyramid` :term:`authorization policy` in the
current configuration. The ``policy`` argument must be an instance
of an authorization policy or a :term:`dotted Python name` that points
at an instance of an authorization policy.
@@ -76,10 +135,11 @@ class SecurityConfiguratorMixin(object):
Using the ``authorization_policy`` argument to the
:class:`pyramid.config.Configurator` constructor can be used to
achieve the same purpose.
+
"""
def register():
- self._set_authorization_policy(policy)
+ self.registry.registerUtility(policy, IAuthorizationPolicy)
def ensure():
if self.autocommit:
@@ -91,6 +151,7 @@ class SecurityConfiguratorMixin(object):
'(use the set_authorization_policy method)'
)
+ policy = self.maybe_dotted(policy)
intr = self.introspectable(
'authorization policy',
None,
@@ -108,10 +169,6 @@ class SecurityConfiguratorMixin(object):
)
self.action(None, ensure)
- def _set_authorization_policy(self, policy):
- policy = self.maybe_dotted(policy)
- self.registry.registerUtility(policy, IAuthorizationPolicy)
-
@action_method
def set_default_permission(self, permission):
"""
diff --git a/src/pyramid/config/testing.py b/src/pyramid/config/testing.py
index 9c998840a..21c622656 100644
--- a/src/pyramid/config/testing.py
+++ b/src/pyramid/config/testing.py
@@ -1,11 +1,6 @@
from zope.interface import Interface
-from pyramid.interfaces import (
- ITraverser,
- IAuthorizationPolicy,
- IAuthenticationPolicy,
- IRendererFactory,
-)
+from pyramid.interfaces import ITraverser, ISecurityPolicy, IRendererFactory
from pyramid.renderers import RendererHelper
@@ -18,8 +13,7 @@ class TestingConfiguratorMixin(object):
# testing API
def testing_securitypolicy(
self,
- userid=None,
- groupids=(),
+ identity=None,
permissive=True,
remember_result=None,
forget_result=None,
@@ -69,10 +63,9 @@ class TestingConfiguratorMixin(object):
from pyramid.testing import DummySecurityPolicy
policy = DummySecurityPolicy(
- userid, groupids, permissive, remember_result, forget_result
+ identity, permissive, remember_result, forget_result
)
- self.registry.registerUtility(policy, IAuthorizationPolicy)
- self.registry.registerUtility(policy, IAuthenticationPolicy)
+ self.registry.registerUtility(policy, ISecurityPolicy)
return policy
def testing_resources(self, resources):
diff --git a/src/pyramid/interfaces.py b/src/pyramid/interfaces.py
index 15ae3faaa..2d8b1ac40 100644
--- a/src/pyramid/interfaces.py
+++ b/src/pyramid/interfaces.py
@@ -482,8 +482,46 @@ class IViewMapperFactory(Interface):
"""
+class ISecurityPolicy(Interface):
+ def identify(request):
+ """ Return an object identifying a trusted and verified user. This
+ object may be anything, but should implement a ``__str__`` method that
+ outputs a corresponding :term:`userid`.
+
+ """
+
+ def permits(request, context, identity, permission):
+ """ Return an instance of :class:`pyramid.security.Allowed` if a user
+ of the given identity is allowed the ``permission`` in the current
+ ``context``, else return an instance of
+ :class:`pyramid.security.Denied`.
+
+ """
+
+ def remember(request, userid, **kw):
+ """ Return a set of headers suitable for 'remembering' the
+ :term:`userid` named ``userid`` when set in a response. An
+ individual authentication policy and its consumers can
+ decide on the composition and meaning of ``**kw``.
+
+ """
+
+ def forget(request):
+ """ Return a set of headers suitable for 'forgetting' the
+ current user on subsequent requests.
+
+ """
+
+
class IAuthenticationPolicy(Interface):
- """ An object representing a Pyramid authentication policy. """
+ """ An object representing a Pyramid authentication policy.
+
+ .. deprecated:: 2.0
+
+ Authentication policies have been removed in favor of security
+ policies. See :ref:`upgrading_auth` for more information.
+
+ """
def authenticated_userid(request):
""" Return the authenticated :term:`userid` or ``None`` if
@@ -536,7 +574,14 @@ class IAuthenticationPolicy(Interface):
class IAuthorizationPolicy(Interface):
- """ An object representing a Pyramid authorization policy. """
+ """ An object representing a Pyramid authorization policy.
+
+ .. deprecated:: 2.0
+
+ Authentication policies have been removed in favor of security
+ policies. See :ref:`upgrading_auth` for more information.
+
+ """
def permits(context, principals, permission):
""" Return an instance of :class:`pyramid.security.Allowed` if any
diff --git a/src/pyramid/predicates.py b/src/pyramid/predicates.py
index 5a1127fb3..a267a69a0 100644
--- a/src/pyramid/predicates.py
+++ b/src/pyramid/predicates.py
@@ -1,5 +1,7 @@
import re
+from zope.deprecation import deprecated
+
from pyramid.exceptions import ConfigurationError
from pyramid.csrf import check_csrf_token
@@ -291,6 +293,14 @@ class PhysicalPathPredicate(object):
class EffectivePrincipalsPredicate(object):
+ """
+ .. deprecated:: 2.0
+
+ The new security system has removed the concept of principals. See
+ :ref:`upgrading_auth` for more information.
+
+ """
+
def __init__(self, val, config):
if is_nonstr_iter(val):
self.val = set(val)
@@ -311,6 +321,15 @@ class EffectivePrincipalsPredicate(object):
return False
+deprecated(
+ 'EffectivePrincipalsPredicate',
+ 'The new security policy has removed the concept of principals. See '
+ 'https://docs.pylonsproject.org/projects/pyramid/en/latest'
+ '/whatsnew-2.0.html#upgrading-authentication-authorization '
+ 'for more information.',
+)
+
+
class Notted(object):
def __init__(self, predicate):
self.predicate = predicate
diff --git a/src/pyramid/request.py b/src/pyramid/request.py
index b9bd7451a..5c68abe69 100644
--- a/src/pyramid/request.py
+++ b/src/pyramid/request.py
@@ -15,7 +15,7 @@ from pyramid.interfaces import (
from pyramid.decorator import reify
from pyramid.i18n import LocalizerRequestMixin
from pyramid.response import Response, _get_response_factory
-from pyramid.security import AuthenticationAPIMixin, AuthorizationAPIMixin
+from pyramid.security import SecurityAPIMixin, AuthenticationAPIMixin
from pyramid.url import URLMethodsMixin
from pyramid.util import (
InstancePropertyHelper,
@@ -147,8 +147,8 @@ class Request(
CallbackMethodsMixin,
InstancePropertyMixin,
LocalizerRequestMixin,
+ SecurityAPIMixin,
AuthenticationAPIMixin,
- AuthorizationAPIMixin,
ViewMethodsMixin,
):
"""
diff --git a/src/pyramid/security.py b/src/pyramid/security.py
index 61819588b..08c36b457 100644
--- a/src/pyramid/security.py
+++ b/src/pyramid/security.py
@@ -1,6 +1,8 @@
-from zope.interface import providedBy
+from zope.interface import implementer, providedBy
+from zope.deprecation import deprecated
from pyramid.interfaces import (
+ ISecurityPolicy,
IAuthenticationPolicy,
IAuthorizationPolicy,
ISecuredView,
@@ -35,17 +37,12 @@ DENY_ALL = (Deny, Everyone, ALL_PERMISSIONS)
NO_PERMISSION_REQUIRED = '__no_permission_required__'
-def _get_registry(request):
- try:
- reg = request.registry
- except AttributeError:
- reg = get_current_registry() # b/c
- return reg
+def _get_security_policy(request):
+ return request.registry.queryUtility(ISecurityPolicy)
def _get_authentication_policy(request):
- registry = _get_registry(request)
- return registry.queryUtility(IAuthenticationPolicy)
+ return request.registry.queryUtility(IAuthenticationPolicy)
def remember(request, userid, **kw):
@@ -54,7 +51,7 @@ def remember(request, userid, **kw):
on this request's response.
These headers are suitable for 'remembering' a set of credentials
implied by the data passed as ``userid`` and ``*kw`` using the
- current :term:`authentication policy`. Common usage might look
+ current :term:`security policy`. Common usage might look
like so within the body of a view function (``response`` is
assumed to be a :term:`WebOb` -style :term:`response` object
computed previously by the view code):
@@ -67,10 +64,10 @@ def remember(request, userid, **kw):
response.headerlist.extend(headers)
return response
- If no :term:`authentication policy` is in use, this function will
+ If no :term:`security policy` is in use, this function will
always return an empty sequence. If used, the composition and
meaning of ``**kw`` must be agreed upon by the calling code and
- the effective authentication policy.
+ the effective security policy.
.. versionchanged:: 1.6
Deprecated the ``principal`` argument in favor of ``userid`` to clarify
@@ -79,7 +76,7 @@ def remember(request, userid, **kw):
.. versionchanged:: 1.10
Removed the deprecated ``principal`` argument.
"""
- policy = _get_authentication_policy(request)
+ policy = _get_security_policy(request)
if policy is None:
return []
return policy.remember(request, userid, **kw)
@@ -101,17 +98,23 @@ def forget(request):
response.headerlist.extend(headers)
return response
- If no :term:`authentication policy` is in use, this function will
+ If no :term:`security policy` is in use, this function will
always return an empty sequence.
"""
- policy = _get_authentication_policy(request)
+ policy = _get_security_policy(request)
if policy is None:
return []
return policy.forget(request)
def principals_allowed_by_permission(context, permission):
- """ Provided a ``context`` (a resource object), and a ``permission``
+ """
+ .. deprecated:: 2.0
+
+ The new security policy has removed the concept of principals. See
+ :ref:`upgrading_auth` for more information.
+
+ Provided a ``context`` (a resource object), and a ``permission``
string, if an :term:`authorization policy` is
in effect, return a sequence of :term:`principal` ids that possess
the permission in the ``context``. If no authorization policy is
@@ -126,6 +129,7 @@ def principals_allowed_by_permission(context, permission):
required machinery for this function; those will cause a
:exc:`NotImplementedError` exception to be raised when this
function is invoked.
+
"""
reg = get_current_registry()
policy = reg.queryUtility(IAuthorizationPolicy)
@@ -134,6 +138,15 @@ def principals_allowed_by_permission(context, permission):
return policy.principals_allowed_by_permission(context, permission)
+deprecated(
+ 'principals_allowed_by_permission',
+ 'The new security policy has removed the concept of principals. See '
+ 'https://docs.pylonsproject.org/projects/pyramid/en/latest'
+ '/whatsnew-2.0.html#upgrading-authentication-authorization '
+ 'for more information.',
+)
+
+
def view_execution_permitted(context, request, name=''):
""" If the view specified by ``context`` and ``name`` is protected
by a :term:`permission`, check the permission associated with the
@@ -147,7 +160,7 @@ def view_execution_permitted(context, request, name=''):
An exception is raised if no view is found.
"""
- reg = _get_registry(request)
+ reg = request.registry
provides = [IViewClassifier] + [providedBy(x) for x in (request, context)]
# XXX not sure what to do here about using _find_views or analogue;
# for now let's just keep it as-is
@@ -280,23 +293,82 @@ class ACLAllowed(ACLPermitsResult, Allowed):
"""
-class AuthenticationAPIMixin(object):
+class SecurityAPIMixin(object):
@property
- def authenticated_userid(self):
- """ Return the userid of the currently authenticated user or
- ``None`` if there is no :term:`authentication policy` in effect or
- there is no currently authenticated user.
+ def authenticated_identity(self):
+ """
+ Return an opaque object identifying the current user or ``None`` if no
+ user is authenticated or there is no :term:`security policy` in effect.
- .. versionadded:: 1.5
"""
- policy = _get_authentication_policy(self)
+ policy = _get_security_policy(self)
if policy is None:
return None
- return policy.authenticated_userid(self)
+ return policy.identify(self)
@property
+ def authenticated_userid(self):
+ """
+ Return the :term:`userid` of the currently authenticated user or
+ ``None`` if there is no :term:`security policy` in effect or there is
+ no currently authenticated user.
+
+ .. versionchanged:: 2.0
+
+ When using the new security system, this property outputs the
+ string representation of the :term:`identity`.
+
+ """
+ authn = _get_authentication_policy(self)
+ security = _get_security_policy(self)
+ if authn is not None:
+ return authn.authenticated_userid(self)
+ elif security is not None:
+ return str(security.identify(self))
+ else:
+ return None
+
+ def has_permission(self, permission, context=None):
+ """ Given a permission and an optional context, returns an instance of
+ :data:`pyramid.security.Allowed` if the permission is granted to this
+ request with the provided context, or the context already associated
+ with the request. Otherwise, returns an instance of
+ :data:`pyramid.security.Denied`. This method delegates to the current
+ security policy. Returns
+ :data:`pyramid.security.Allowed` unconditionally if no security
+ policy has been registered for this request. If ``context`` is not
+ supplied or is supplied as ``None``, the context used is the
+ ``request.context`` attribute.
+
+ :param permission: Does this request have the given permission?
+ :type permission: str
+ :param context: A resource object or ``None``
+ :type context: object
+ :returns: Either :class:`pyramid.security.Allowed` or
+ :class:`pyramid.security.Denied`.
+
+ """
+ if context is None:
+ context = self.context
+ policy = _get_security_policy(self)
+ if policy is None:
+ return Allowed('No security policy in use.')
+ identity = policy.identify(self)
+ return policy.permits(self, context, identity, permission)
+
+
+class AuthenticationAPIMixin(object):
+ @property
def unauthenticated_userid(self):
- """ Return an object which represents the *claimed* (not verified) user
+ """
+ .. deprecated:: 2.0
+
+ ``unauthenticated_userid`` does not have an equivalent in the new
+ security system. Use :attr:`.authenticated_userid` or
+ :attr:`.identity` instead. See :ref:`upgrading_auth` for more
+ information.
+
+ Return an object which represents the *claimed* (not verified) user
id of the credentials present in the request. ``None`` if there is no
:term:`authentication policy` in effect or there is no user data
associated with the current request. This differs from
@@ -304,62 +376,72 @@ class AuthenticationAPIMixin(object):
effective authentication policy will not ensure that a record
associated with the userid exists in persistent storage.
- .. versionadded:: 1.5
"""
- policy = _get_authentication_policy(self)
- if policy is None:
+ authn = _get_authentication_policy(self)
+ security = _get_security_policy(self)
+ if authn is not None:
+ return authn.unauthenticated_userid(self)
+ elif security is not None:
+ return str(security.identify(self))
+ else:
return None
- return policy.unauthenticated_userid(self)
@property
def effective_principals(self):
- """ Return the list of 'effective' :term:`principal` identifiers
+ """
+ .. deprecated:: 2.0
+
+ The new security policy has removed the concept of principals. See
+ :ref:`upgrading_auth` for more information.
+
+ Return the list of 'effective' :term:`principal` identifiers
for the ``request``. If no :term:`authentication policy` is in effect,
this will return a one-element list containing the
:data:`pyramid.security.Everyone` principal.
- .. versionadded:: 1.5
"""
policy = _get_authentication_policy(self)
if policy is None:
return [Everyone]
return policy.effective_principals(self)
+ effective_principals = deprecated(
+ effective_principals,
+ 'The new security policy has removed the concept of principals. See '
+ 'https://docs.pylonsproject.org/projects/pyramid/en/latest'
+ '/whatsnew-2.0.html#upgrading-authentication-authorization '
+ 'for more information.',
+ )
-class AuthorizationAPIMixin(object):
- def has_permission(self, permission, context=None):
- """ Given a permission and an optional context, returns an instance of
- :data:`pyramid.security.Allowed` if the permission is granted to this
- request with the provided context, or the context already associated
- with the request. Otherwise, returns an instance of
- :data:`pyramid.security.Denied`. This method delegates to the current
- authentication and authorization policies. Returns
- :data:`pyramid.security.Allowed` unconditionally if no authentication
- policy has been registered for this request. If ``context`` is not
- supplied or is supplied as ``None``, the context used is the
- ``request.context`` attribute.
- :param permission: Does this request have the given permission?
- :type permission: str
- :param context: A resource object or ``None``
- :type context: object
- :returns: Either :class:`pyramid.security.Allowed` or
- :class:`pyramid.security.Denied`.
+@implementer(ISecurityPolicy)
+class LegacySecurityPolicy:
+ """
+ A :term:`security policy` which provides a backwards compatibility shim for
+ the :term:`authentication policy` and the :term:`authorization policy`.
- .. versionadded:: 1.5
+ """
- """
- if context is None:
- context = self.context
- reg = _get_registry(self)
- authn_policy = reg.queryUtility(IAuthenticationPolicy)
- if authn_policy is None:
- return Allowed('No authentication policy in use.')
- authz_policy = reg.queryUtility(IAuthorizationPolicy)
- if authz_policy is None:
- raise ValueError(
- 'Authentication policy registered without '
- 'authorization policy'
- ) # should never happen
- principals = authn_policy.effective_principals(self)
- return authz_policy.permits(context, principals, permission)
+ def _get_authn_policy(self, request):
+ return request.registry.getUtility(IAuthenticationPolicy)
+
+ def _get_authz_policy(self, request):
+ return request.registry.getUtility(IAuthorizationPolicy)
+
+ def identify(self, request):
+ authn = self._get_authn_policy(request)
+ return authn.authenticated_userid(request)
+
+ def remember(self, request, userid, **kw):
+ authn = self._get_authn_policy(request)
+ return authn.remember(request, userid, **kw)
+
+ def forget(self, request):
+ authn = self._get_authn_policy(request)
+ return authn.forget(request)
+
+ def permits(self, request, context, identity, permission):
+ authn = self._get_authn_policy(request)
+ authz = self._get_authz_policy(request)
+ principals = authn.effective_principals(request)
+ return authz.permits(context, principals, permission)
diff --git a/src/pyramid/testing.py b/src/pyramid/testing.py
index 0cfc1a277..3bf3f1898 100644
--- a/src/pyramid/testing.py
+++ b/src/pyramid/testing.py
@@ -14,12 +14,7 @@ from pyramid.path import caller_package
from pyramid.response import _get_response_factory
from pyramid.registry import Registry
-from pyramid.security import (
- Authenticated,
- Everyone,
- AuthenticationAPIMixin,
- AuthorizationAPIMixin,
-)
+from pyramid.security import SecurityAPIMixin, AuthenticationAPIMixin
from pyramid.threadlocal import get_current_registry, manager
@@ -43,18 +38,16 @@ class DummyRootFactory(object):
class DummySecurityPolicy(object):
- """ A standin for both an IAuthentication and IAuthorization policy """
+ """ A standin for a security policy"""
def __init__(
self,
- userid=None,
- groupids=(),
+ identity=None,
permissive=True,
remember_result=None,
forget_result=None,
):
- self.userid = userid
- self.groupids = groupids
+ self.identity = identity
self.permissive = permissive
if remember_result is None:
remember_result = []
@@ -63,19 +56,11 @@ class DummySecurityPolicy(object):
self.remember_result = remember_result
self.forget_result = forget_result
- def authenticated_userid(self, request):
- return self.userid
-
- def unauthenticated_userid(self, request):
- return self.userid
+ def identify(self, request):
+ return self.identity
- def effective_principals(self, request):
- effective_principals = [Everyone]
- if self.userid:
- effective_principals.append(Authenticated)
- effective_principals.append(self.userid)
- effective_principals.extend(self.groupids)
- return effective_principals
+ def permits(self, request, context, identity, permission):
+ return self.permissive
def remember(self, request, userid, **kw):
self.remembered = userid
@@ -85,15 +70,6 @@ class DummySecurityPolicy(object):
self.forgotten = True
return self.forget_result
- def permits(self, context, principals, permission):
- return self.permissive
-
- def principals_allowed_by_permission(self, context, permission):
- if self.permissive:
- return self.effective_principals(None)
- else:
- return []
-
class DummyTemplateRenderer(object):
"""
@@ -303,8 +279,8 @@ class DummyRequest(
CallbackMethodsMixin,
InstancePropertyMixin,
LocalizerRequestMixin,
+ SecurityAPIMixin,
AuthenticationAPIMixin,
- AuthorizationAPIMixin,
ViewMethodsMixin,
):
""" A DummyRequest object (incompletely) imitates a :term:`request` object.
diff --git a/src/pyramid/viewderivers.py b/src/pyramid/viewderivers.py
index c41a57d7e..95c223e61 100644
--- a/src/pyramid/viewderivers.py
+++ b/src/pyramid/viewderivers.py
@@ -7,12 +7,11 @@ from pyramid.csrf import check_csrf_origin, check_csrf_token
from pyramid.response import Response
from pyramid.interfaces import (
- IAuthenticationPolicy,
- IAuthorizationPolicy,
IDefaultCSRFOptions,
IDefaultPermission,
IDebugLogger,
IResponse,
+ ISecurityPolicy,
IViewMapper,
IViewMapperFactory,
)
@@ -308,19 +307,17 @@ def _secured_view(view, info):
# permission, replacing it with no permission at all
permission = None
- wrapped_view = view
- authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
- authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
+ policy = info.registry.queryUtility(ISecurityPolicy)
# no-op on exception-only views without an explicit permission
if explicit_val is None and info.exception_only:
return view
- if authn_policy and authz_policy and (permission is not None):
+ if policy and (permission is not None):
def permitted(context, request):
- principals = authn_policy.effective_principals(request)
- return authz_policy.permits(context, principals, permission)
+ identity = policy.identify(request)
+ return policy.permits(request, context, identity, permission)
def secured_view(context, request):
result = permitted(context, request)
@@ -334,12 +331,12 @@ def _secured_view(view, info):
)
raise HTTPForbidden(msg, result=result)
- wrapped_view = secured_view
- wrapped_view.__call_permissive__ = view
- wrapped_view.__permitted__ = permitted
- wrapped_view.__permission__ = permission
-
- return wrapped_view
+ secured_view.__call_permissive__ = view
+ secured_view.__permitted__ = permitted
+ secured_view.__permission__ = permission
+ return secured_view
+ else:
+ return view
def _authdebug_view(view, info):
@@ -348,8 +345,7 @@ def _authdebug_view(view, info):
permission = explicit_val = info.options.get('permission')
if permission is None:
permission = info.registry.queryUtility(IDefaultPermission)
- authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
- authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
+ policy = info.registry.queryUtility(ISecurityPolicy)
logger = info.registry.queryUtility(IDebugLogger)
# no-op on exception-only views without an explicit permission
@@ -361,18 +357,18 @@ def _authdebug_view(view, info):
def authdebug_view(context, request):
view_name = getattr(request, 'view_name', None)
- if authn_policy and authz_policy:
+ if policy:
if permission is NO_PERMISSION_REQUIRED:
msg = 'Allowed (NO_PERMISSION_REQUIRED)'
elif permission is None:
msg = 'Allowed (no permission registered)'
else:
- principals = authn_policy.effective_principals(request)
+ identity = policy.identify(request)
msg = str(
- authz_policy.permits(context, principals, permission)
+ policy.permits(request, context, identity, permission)
)
else:
- msg = 'Allowed (no authorization policy in use)'
+ msg = 'Allowed (no security policy in use)'
view_name = getattr(request, 'view_name', None)
url = getattr(request, 'url', None)
diff --git a/tests/pkgs/defpermbugapp/__init__.py b/tests/pkgs/defpermbugapp/__init__.py
index 81897e86a..af78404ae 100644
--- a/tests/pkgs/defpermbugapp/__init__.py
+++ b/tests/pkgs/defpermbugapp/__init__.py
@@ -25,6 +25,6 @@ def includeme(config):
authn_policy = AuthTktAuthenticationPolicy('seekt1t', hashalg='sha512')
authz_policy = ACLAuthorizationPolicy()
config.scan('tests.pkgs.defpermbugapp')
- config._set_authentication_policy(authn_policy)
- config._set_authorization_policy(authz_policy)
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(authz_policy)
config.set_default_permission('private')
diff --git a/tests/pkgs/forbiddenapp/__init__.py b/tests/pkgs/forbiddenapp/__init__.py
index 31ea4dd52..79670dd32 100644
--- a/tests/pkgs/forbiddenapp/__init__.py
+++ b/tests/pkgs/forbiddenapp/__init__.py
@@ -22,7 +22,7 @@ def includeme(config):
authn_policy = AuthTktAuthenticationPolicy('seekr1t', hashalg='sha512')
authz_policy = ACLAuthorizationPolicy()
- config._set_authentication_policy(authn_policy)
- config._set_authorization_policy(authz_policy)
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(authz_policy)
config.add_view(x_view, name='x', permission='private')
config.add_view(forbidden_view, context=HTTPForbidden)
diff --git a/tests/pkgs/legacysecurityapp/__init__.py b/tests/pkgs/legacysecurityapp/__init__.py
new file mode 100644
index 000000000..12fb6104e
--- /dev/null
+++ b/tests/pkgs/legacysecurityapp/__init__.py
@@ -0,0 +1,37 @@
+from pyramid.response import Response
+from pyramid.authentication import RemoteUserAuthenticationPolicy
+from pyramid.security import Allowed, Denied
+
+
+class AuthorizationPolicy:
+ def permits(self, context, principals, permission):
+ if 'bob' in principals and permission == 'foo':
+ return Allowed('')
+ else:
+ return Denied('')
+
+ def principals_allowed_by_permission(self, context, permission):
+ raise NotImplementedError() # pragma: no cover
+
+
+def public(context, request):
+ return Response('Hello')
+
+
+def private(context, request):
+ return Response('Secret')
+
+
+def inaccessible(context, request):
+ raise AssertionError() # pragma: no cover
+
+
+def includeme(config):
+ config.set_authentication_policy(RemoteUserAuthenticationPolicy())
+ config.set_authorization_policy(AuthorizationPolicy())
+ config.add_route('public', '/public')
+ config.add_view(public, route_name='public')
+ config.add_route('private', '/private')
+ config.add_view(private, route_name='private', permission='foo')
+ config.add_route('inaccessible', '/inaccessible')
+ config.add_view(inaccessible, route_name='inaccessible', permission='bar')
diff --git a/tests/pkgs/securityapp/__init__.py b/tests/pkgs/securityapp/__init__.py
new file mode 100644
index 000000000..6ddba585b
--- /dev/null
+++ b/tests/pkgs/securityapp/__init__.py
@@ -0,0 +1,41 @@
+from pyramid.response import Response
+from pyramid.security import Allowed, Denied
+
+
+class SecurityPolicy:
+ def identify(self, request):
+ return request.environ.get('REMOTE_USER')
+
+ def permits(self, request, context, identity, permission):
+ if identity and permission == 'foo':
+ return Allowed('')
+ else:
+ return Denied('')
+
+ def remember(self, request, userid, **kw):
+ raise NotImplementedError() # pragma: no cover
+
+ def forget(self, request):
+ raise NotImplementedError() # pragma: no cover
+
+
+def public(context, request):
+ return Response('Hello')
+
+
+def private(context, request):
+ return Response('Secret')
+
+
+def inaccessible(context, request):
+ raise AssertionError() # pragma: no cover
+
+
+def includeme(config):
+ config.set_security_policy(SecurityPolicy())
+ config.add_route('public', '/public')
+ config.add_view(public, route_name='public')
+ config.add_route('private', '/private')
+ config.add_view(private, route_name='private', permission='foo')
+ config.add_route('inaccessible', '/inaccessible')
+ config.add_view(inaccessible, route_name='inaccessible', permission='bar')
diff --git a/tests/pkgs/staticpermapp/__init__.py b/tests/pkgs/staticpermapp/__init__.py
index ffc87d39a..a12eac2d3 100644
--- a/tests/pkgs/staticpermapp/__init__.py
+++ b/tests/pkgs/staticpermapp/__init__.py
@@ -18,8 +18,8 @@ def includeme(config):
authn_policy = RemoteUserAuthenticationPolicy()
authz_policy = ACLAuthorizationPolicy()
- config._set_authentication_policy(authn_policy)
- config._set_authorization_policy(authz_policy)
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(authz_policy)
config.add_static_view('allowed', 'tests:fixtures/static/')
config.add_static_view(
'protected', 'tests:fixtures/static/', permission='view'
diff --git a/tests/test_authentication.py b/tests/test_authentication.py
index 8671eba05..710e87423 100644
--- a/tests/test_authentication.py
+++ b/tests/test_authentication.py
@@ -1693,6 +1693,56 @@ class TestSessionAuthenticationPolicy(unittest.TestCase):
self.assertEqual(result, [])
+class TestSessionAuthenticationHelper(unittest.TestCase):
+ def _makeRequest(self, session=None):
+ from types import SimpleNamespace
+
+ if session is None:
+ session = dict()
+ return SimpleNamespace(session=session)
+
+ def _makeOne(self, prefix=''):
+ from pyramid.authentication import SessionAuthenticationHelper
+
+ return SessionAuthenticationHelper(prefix=prefix)
+
+ def test_identify(self):
+ request = self._makeRequest({'userid': 'fred'})
+ helper = self._makeOne()
+ self.assertEqual(helper.identify(request), 'fred')
+
+ def test_identify_with_prefix(self):
+ request = self._makeRequest({'foo.userid': 'fred'})
+ helper = self._makeOne(prefix='foo.')
+ self.assertEqual(helper.identify(request), 'fred')
+
+ def test_identify_none(self):
+ request = self._makeRequest()
+ helper = self._makeOne()
+ self.assertEqual(helper.identify(request), None)
+
+ def test_remember(self):
+ request = self._makeRequest()
+ helper = self._makeOne()
+ result = helper.remember(request, 'fred')
+ self.assertEqual(request.session.get('userid'), 'fred')
+ self.assertEqual(result, [])
+
+ def test_forget(self):
+ request = self._makeRequest({'userid': 'fred'})
+ helper = self._makeOne()
+ result = helper.forget(request)
+ self.assertEqual(request.session.get('userid'), None)
+ self.assertEqual(result, [])
+
+ def test_forget_no_identity(self):
+ request = self._makeRequest()
+ helper = self._makeOne()
+ result = helper.forget(request)
+ self.assertEqual(request.session.get('userid'), None)
+ self.assertEqual(result, [])
+
+
class TestBasicAuthAuthenticationPolicy(unittest.TestCase):
def _getTargetClass(self):
from pyramid.authentication import BasicAuthAuthenticationPolicy as cls
diff --git a/tests/test_authorization.py b/tests/test_authorization.py
index efb84b203..399b3da60 100644
--- a/tests/test_authorization.py
+++ b/tests/test_authorization.py
@@ -272,6 +272,267 @@ class TestACLAuthorizationPolicy(unittest.TestCase):
self.assertTrue(result)
+class TestACLHelper(unittest.TestCase):
+ def test_no_acl(self):
+ from pyramid.authorization import ACLHelper
+
+ context = DummyContext()
+ helper = ACLHelper()
+ result = helper.permits(context, ['foo'], 'permission')
+ self.assertEqual(result, False)
+ self.assertEqual(result.ace, '<default deny>')
+ self.assertEqual(
+ result.acl, '<No ACL found on any object in resource lineage>'
+ )
+ self.assertEqual(result.permission, 'permission')
+ self.assertEqual(result.principals, ['foo'])
+ self.assertEqual(result.context, context)
+
+ def test_acl(self):
+ from pyramid.authorization import ACLHelper
+ from pyramid.security import Deny
+ from pyramid.security import Allow
+ from pyramid.security import Everyone
+ from pyramid.security import Authenticated
+ from pyramid.security import ALL_PERMISSIONS
+ from pyramid.security import DENY_ALL
+
+ helper = ACLHelper()
+ root = DummyContext()
+ community = DummyContext(__name__='community', __parent__=root)
+ blog = DummyContext(__name__='blog', __parent__=community)
+ root.__acl__ = [(Allow, Authenticated, VIEW)]
+ community.__acl__ = [
+ (Allow, 'fred', ALL_PERMISSIONS),
+ (Allow, 'wilma', VIEW),
+ DENY_ALL,
+ ]
+ blog.__acl__ = [
+ (Allow, 'barney', MEMBER_PERMS),
+ (Allow, 'wilma', VIEW),
+ ]
+
+ result = helper.permits(
+ blog, [Everyone, Authenticated, 'wilma'], 'view'
+ )
+ self.assertEqual(result, True)
+ self.assertEqual(result.context, blog)
+ self.assertEqual(result.ace, (Allow, 'wilma', VIEW))
+ self.assertEqual(result.acl, blog.__acl__)
+
+ result = helper.permits(
+ blog, [Everyone, Authenticated, 'wilma'], 'delete'
+ )
+ self.assertEqual(result, False)
+ self.assertEqual(result.context, community)
+ self.assertEqual(result.ace, (Deny, Everyone, ALL_PERMISSIONS))
+ self.assertEqual(result.acl, community.__acl__)
+
+ result = helper.permits(
+ blog, [Everyone, Authenticated, 'fred'], 'view'
+ )
+ self.assertEqual(result, True)
+ self.assertEqual(result.context, community)
+ self.assertEqual(result.ace, (Allow, 'fred', ALL_PERMISSIONS))
+ result = helper.permits(
+ blog, [Everyone, Authenticated, 'fred'], 'doesntevenexistyet'
+ )
+ self.assertEqual(result, True)
+ self.assertEqual(result.context, community)
+ self.assertEqual(result.ace, (Allow, 'fred', ALL_PERMISSIONS))
+ self.assertEqual(result.acl, community.__acl__)
+
+ result = helper.permits(
+ blog, [Everyone, Authenticated, 'barney'], 'view'
+ )
+ self.assertEqual(result, True)
+ self.assertEqual(result.context, blog)
+ self.assertEqual(result.ace, (Allow, 'barney', MEMBER_PERMS))
+ result = helper.permits(
+ blog, [Everyone, Authenticated, 'barney'], 'administer'
+ )
+ self.assertEqual(result, False)
+ self.assertEqual(result.context, community)
+ self.assertEqual(result.ace, (Deny, Everyone, ALL_PERMISSIONS))
+ self.assertEqual(result.acl, community.__acl__)
+
+ result = helper.permits(
+ root, [Everyone, Authenticated, 'someguy'], 'view'
+ )
+ self.assertEqual(result, True)
+ self.assertEqual(result.context, root)
+ self.assertEqual(result.ace, (Allow, Authenticated, VIEW))
+ result = helper.permits(
+ blog, [Everyone, Authenticated, 'someguy'], 'view'
+ )
+ self.assertEqual(result, False)
+ self.assertEqual(result.context, community)
+ self.assertEqual(result.ace, (Deny, Everyone, ALL_PERMISSIONS))
+ self.assertEqual(result.acl, community.__acl__)
+
+ result = helper.permits(root, [Everyone], 'view')
+ self.assertEqual(result, False)
+ self.assertEqual(result.context, root)
+ self.assertEqual(result.ace, '<default deny>')
+ self.assertEqual(result.acl, root.__acl__)
+
+ context = DummyContext()
+ result = helper.permits(context, [Everyone], 'view')
+ self.assertEqual(result, False)
+ self.assertEqual(result.ace, '<default deny>')
+ self.assertEqual(
+ result.acl, '<No ACL found on any object in resource lineage>'
+ )
+
+ def test_string_permissions_in_acl(self):
+ from pyramid.authorization import ACLHelper
+ from pyramid.security import Allow
+
+ helper = ACLHelper()
+ root = DummyContext()
+ root.__acl__ = [(Allow, 'wilma', 'view_stuff')]
+
+ result = helper.permits(root, ['wilma'], 'view')
+ # would be True if matching against 'view_stuff' instead of against
+ # ['view_stuff']
+ self.assertEqual(result, False)
+
+ def test_callable_acl(self):
+ from pyramid.authorization import ACLHelper
+ from pyramid.security import Allow
+
+ helper = ACLHelper()
+ context = DummyContext()
+ fn = lambda self: [(Allow, 'bob', 'read')]
+ context.__acl__ = fn.__get__(context, context.__class__)
+ result = helper.permits(context, ['bob'], 'read')
+ self.assertTrue(result)
+
+ def test_principals_allowed_by_permission_direct(self):
+ from pyramid.authorization import ACLHelper
+ from pyramid.security import Allow
+ from pyramid.security import DENY_ALL
+
+ helper = ACLHelper()
+ context = DummyContext()
+ acl = [
+ (Allow, 'chrism', ('read', 'write')),
+ DENY_ALL,
+ (Allow, 'other', 'read'),
+ ]
+ context.__acl__ = acl
+ result = sorted(
+ helper.principals_allowed_by_permission(context, 'read')
+ )
+ self.assertEqual(result, ['chrism'])
+
+ def test_principals_allowed_by_permission_callable_acl(self):
+ from pyramid.authorization import ACLHelper
+ from pyramid.security import Allow
+ from pyramid.security import DENY_ALL
+
+ helper = ACLHelper()
+ context = DummyContext()
+ acl = lambda: [
+ (Allow, 'chrism', ('read', 'write')),
+ DENY_ALL,
+ (Allow, 'other', 'read'),
+ ]
+ context.__acl__ = acl
+ result = sorted(
+ helper.principals_allowed_by_permission(context, 'read')
+ )
+ self.assertEqual(result, ['chrism'])
+
+ def test_principals_allowed_by_permission_string_permission(self):
+ from pyramid.authorization import ACLHelper
+ from pyramid.security import Allow
+
+ helper = ACLHelper()
+ context = DummyContext()
+ acl = [(Allow, 'chrism', 'read_it')]
+ context.__acl__ = acl
+ result = helper.principals_allowed_by_permission(context, 'read')
+ # would be ['chrism'] if 'read' were compared against 'read_it' instead
+ # of against ['read_it']
+ self.assertEqual(list(result), [])
+
+ def test_principals_allowed_by_permission(self):
+ from pyramid.authorization import ACLHelper
+ from pyramid.security import Allow
+ from pyramid.security import Deny
+ from pyramid.security import DENY_ALL
+ from pyramid.security import ALL_PERMISSIONS
+
+ helper = ACLHelper()
+ root = DummyContext(__name__='', __parent__=None)
+ community = DummyContext(__name__='community', __parent__=root)
+ blog = DummyContext(__name__='blog', __parent__=community)
+ root.__acl__ = [
+ (Allow, 'chrism', ('read', 'write')),
+ (Allow, 'other', ('read',)),
+ (Allow, 'jim', ALL_PERMISSIONS),
+ ]
+ community.__acl__ = [
+ (Deny, 'flooz', 'read'),
+ (Allow, 'flooz', 'read'),
+ (Allow, 'mork', 'read'),
+ (Deny, 'jim', 'read'),
+ (Allow, 'someguy', 'manage'),
+ ]
+ blog.__acl__ = [(Allow, 'fred', 'read'), DENY_ALL]
+
+ result = sorted(helper.principals_allowed_by_permission(blog, 'read'))
+ self.assertEqual(result, ['fred'])
+ result = sorted(
+ helper.principals_allowed_by_permission(community, 'read')
+ )
+ self.assertEqual(result, ['chrism', 'mork', 'other'])
+ result = sorted(
+ helper.principals_allowed_by_permission(community, 'read')
+ )
+ result = sorted(helper.principals_allowed_by_permission(root, 'read'))
+ self.assertEqual(result, ['chrism', 'jim', 'other'])
+
+ def test_principals_allowed_by_permission_no_acls(self):
+ from pyramid.authorization import ACLHelper
+
+ helper = ACLHelper()
+ context = DummyContext()
+ result = sorted(
+ helper.principals_allowed_by_permission(context, 'read')
+ )
+ self.assertEqual(result, [])
+
+ def test_principals_allowed_by_permission_deny_not_permission_in_acl(self):
+ from pyramid.authorization import ACLHelper
+ from pyramid.security import Deny
+ from pyramid.security import Everyone
+
+ helper = ACLHelper()
+ context = DummyContext()
+ acl = [(Deny, Everyone, 'write')]
+ context.__acl__ = acl
+ result = sorted(
+ helper.principals_allowed_by_permission(context, 'read')
+ )
+ self.assertEqual(result, [])
+
+ def test_principals_allowed_by_permission_deny_permission_in_acl(self):
+ from pyramid.authorization import ACLHelper
+ from pyramid.security import Deny
+ from pyramid.security import Everyone
+
+ helper = ACLHelper()
+ context = DummyContext()
+ acl = [(Deny, Everyone, 'read')]
+ context.__acl__ = acl
+ result = sorted(
+ helper.principals_allowed_by_permission(context, 'read')
+ )
+ self.assertEqual(result, [])
+
+
class DummyContext:
def __init__(self, *arg, **kw):
self.__dict__.update(kw)
diff --git a/tests/test_config/test_init.py b/tests/test_config/test_init.py
index ce2b042ec..661654ef0 100644
--- a/tests/test_config/test_init.py
+++ b/tests/test_config/test_init.py
@@ -205,6 +205,15 @@ class ConfiguratorTests(unittest.TestCase):
result = config.registry.getUtility(IDebugLogger)
self.assertEqual(logger, result)
+ def test_ctor_security_policy(self):
+ from pyramid.interfaces import ISecurityPolicy
+
+ policy = object()
+ config = self._makeOne(security_policy=policy)
+ config.commit()
+ result = config.registry.getUtility(ISecurityPolicy)
+ self.assertEqual(policy, result)
+
def test_ctor_authentication_policy(self):
from pyramid.interfaces import IAuthenticationPolicy
diff --git a/tests/test_config/test_security.py b/tests/test_config/test_security.py
index 6257960b8..0ae199239 100644
--- a/tests/test_config/test_security.py
+++ b/tests/test_config/test_security.py
@@ -11,6 +11,28 @@ class ConfiguratorSecurityMethodsTests(unittest.TestCase):
config = Configurator(*arg, **kw)
return config
+ def test_set_security_policy(self):
+ from pyramid.interfaces import ISecurityPolicy
+
+ config = self._makeOne()
+ policy = object()
+ config.set_security_policy(policy)
+ config.commit()
+ self.assertEqual(config.registry.getUtility(ISecurityPolicy), policy)
+
+ def test_set_authentication_policy_with_security_policy(self):
+ from pyramid.interfaces import IAuthorizationPolicy
+ from pyramid.interfaces import ISecurityPolicy
+
+ config = self._makeOne()
+ security_policy = object()
+ authn_policy = object()
+ authz_policy = object()
+ config.registry.registerUtility(security_policy, ISecurityPolicy)
+ config.registry.registerUtility(authz_policy, IAuthorizationPolicy)
+ config.set_authentication_policy(authn_policy)
+ self.assertRaises(ConfigurationError, config.commit)
+
def test_set_authentication_policy_no_authz_policy(self):
config = self._makeOne()
policy = object()
@@ -27,6 +49,8 @@ class ConfiguratorSecurityMethodsTests(unittest.TestCase):
def test_set_authentication_policy_with_authz_policy(self):
from pyramid.interfaces import IAuthenticationPolicy
from pyramid.interfaces import IAuthorizationPolicy
+ from pyramid.interfaces import ISecurityPolicy
+ from pyramid.security import LegacySecurityPolicy
config = self._makeOne()
authn_policy = object()
@@ -37,10 +61,15 @@ class ConfiguratorSecurityMethodsTests(unittest.TestCase):
self.assertEqual(
config.registry.getUtility(IAuthenticationPolicy), authn_policy
)
+ self.assertIsInstance(
+ config.registry.getUtility(ISecurityPolicy), LegacySecurityPolicy
+ )
def test_set_authentication_policy_with_authz_policy_autocommit(self):
from pyramid.interfaces import IAuthenticationPolicy
from pyramid.interfaces import IAuthorizationPolicy
+ from pyramid.interfaces import ISecurityPolicy
+ from pyramid.security import LegacySecurityPolicy
config = self._makeOne(autocommit=True)
authn_policy = object()
@@ -51,6 +80,9 @@ class ConfiguratorSecurityMethodsTests(unittest.TestCase):
self.assertEqual(
config.registry.getUtility(IAuthenticationPolicy), authn_policy
)
+ self.assertIsInstance(
+ config.registry.getUtility(ISecurityPolicy), LegacySecurityPolicy
+ )
def test_set_authorization_policy_no_authn_policy(self):
config = self._makeOne()
diff --git a/tests/test_config/test_testing.py b/tests/test_config/test_testing.py
index 0fb73d268..500aedeae 100644
--- a/tests/test_config/test_testing.py
+++ b/tests/test_config/test_testing.py
@@ -1,7 +1,7 @@
import unittest
from zope.interface import implementer
-from pyramid.security import AuthenticationAPIMixin, AuthorizationAPIMixin
+from pyramid.security import SecurityAPIMixin, AuthenticationAPIMixin
from pyramid.util import text_
from . import IDummy
@@ -17,28 +17,20 @@ class TestingConfiguratorMixinTests(unittest.TestCase):
from pyramid.testing import DummySecurityPolicy
config = self._makeOne(autocommit=True)
- config.testing_securitypolicy(
- 'user', ('group1', 'group2'), permissive=False
- )
- from pyramid.interfaces import IAuthenticationPolicy
- from pyramid.interfaces import IAuthorizationPolicy
+ config.testing_securitypolicy('user', permissive=False)
+ from pyramid.interfaces import ISecurityPolicy
- ut = config.registry.getUtility(IAuthenticationPolicy)
- self.assertTrue(isinstance(ut, DummySecurityPolicy))
- ut = config.registry.getUtility(IAuthorizationPolicy)
- self.assertEqual(ut.userid, 'user')
- self.assertEqual(ut.groupids, ('group1', 'group2'))
- self.assertEqual(ut.permissive, False)
+ policy = config.registry.getUtility(ISecurityPolicy)
+ self.assertTrue(isinstance(policy, DummySecurityPolicy))
+ self.assertEqual(policy.identity, 'user')
+ self.assertEqual(policy.permissive, False)
def test_testing_securitypolicy_remember_result(self):
from pyramid.security import remember
config = self._makeOne(autocommit=True)
pol = config.testing_securitypolicy(
- 'user',
- ('group1', 'group2'),
- permissive=False,
- remember_result=True,
+ 'user', permissive=False, remember_result=True
)
request = DummyRequest()
request.registry = config.registry
@@ -51,7 +43,7 @@ class TestingConfiguratorMixinTests(unittest.TestCase):
config = self._makeOne(autocommit=True)
pol = config.testing_securitypolicy(
- 'user', ('group1', 'group2'), permissive=False, forget_result=True
+ 'user', permissive=False, forget_result=True
)
request = DummyRequest()
request.registry = config.registry
@@ -232,7 +224,7 @@ class DummyEvent:
pass
-class DummyRequest(AuthenticationAPIMixin, AuthorizationAPIMixin):
+class DummyRequest(SecurityAPIMixin, AuthenticationAPIMixin):
def __init__(self, environ=None):
if environ is None:
environ = {}
diff --git a/tests/test_config/test_views.py b/tests/test_config/test_views.py
index 685b81a0f..28b7a9fb1 100644
--- a/tests/test_config/test_views.py
+++ b/tests/test_config/test_views.py
@@ -2059,22 +2059,19 @@ class TestViewsConfigurationMixin(unittest.TestCase):
outerself = self
class DummyPolicy(object):
- def effective_principals(self, r):
+ def identify(self, r):
outerself.assertEqual(r, request)
- return ['abc']
+ return 123
- def permits(self, context, principals, permission):
+ def permits(self, r, context, identity, permission):
+ outerself.assertEqual(r, request)
outerself.assertEqual(context, None)
- outerself.assertEqual(principals, ['abc'])
+ outerself.assertEqual(identity, 123)
outerself.assertEqual(permission, 'view')
return True
policy = DummyPolicy()
- config = self._makeOne(
- authorization_policy=policy,
- authentication_policy=policy,
- autocommit=True,
- )
+ config = self._makeOne(security_policy=policy, autocommit=True)
config.add_view(view=view1, permission='view', renderer=null_renderer)
view = self._getViewCallable(config)
request = self._makeRequest(config)
@@ -2087,22 +2084,20 @@ class TestViewsConfigurationMixin(unittest.TestCase):
outerself = self
class DummyPolicy(object):
- def effective_principals(self, r):
+ def identify(self, r):
outerself.assertEqual(r, request)
- return ['abc']
+ return 123
- def permits(self, context, principals, permission):
+ def permits(self, r, context, identity, permission):
+ outerself.assertEqual(r, request)
outerself.assertEqual(context, None)
- outerself.assertEqual(principals, ['abc'])
+ outerself.assertEqual(identity, 123)
outerself.assertEqual(permission, 'view')
return True
policy = DummyPolicy()
config = self._makeOne(
- authorization_policy=policy,
- authentication_policy=policy,
- default_permission='view',
- autocommit=True,
+ security_policy=policy, default_permission='view', autocommit=True
)
config.add_view(view=view1, renderer=null_renderer)
view = self._getViewCallable(config)
diff --git a/tests/test_integration.py b/tests/test_integration.py
index e6dccbb5b..331542d7d 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -521,6 +521,48 @@ class TestExceptionViewsApp(IntegrationBase, unittest.TestCase):
self.assertTrue(b'caught' in res.body)
+class TestSecurityApp(IntegrationBase, unittest.TestCase):
+ package = 'tests.pkgs.securityapp'
+
+ def test_public(self):
+ res = self.testapp.get('/public', status=200)
+ self.assertEqual(res.body, b'Hello')
+
+ def test_private_denied(self):
+ self.testapp.get('/private', status=403)
+
+ def test_private_allowed(self):
+ self.testapp.extra_environ = {'REMOTE_USER': 'bob'}
+ res = self.testapp.get('/private', status=200)
+ self.assertEqual(res.body, b'Secret')
+
+ def test_inaccessible(self):
+ self.testapp.get('/inaccessible', status=403)
+ self.testapp.extra_environ = {'REMOTE_USER': 'bob'}
+ self.testapp.get('/inaccessible', status=403)
+
+
+class TestLegacySecurityApp(IntegrationBase, unittest.TestCase):
+ package = 'tests.pkgs.legacysecurityapp'
+
+ def test_public(self):
+ res = self.testapp.get('/public', status=200)
+ self.assertEqual(res.body, b'Hello')
+
+ def test_private_denied(self):
+ self.testapp.get('/private', status=403)
+
+ def test_private_allowed(self):
+ self.testapp.extra_environ = {'REMOTE_USER': 'bob'}
+ res = self.testapp.get('/private', status=200)
+ self.assertEqual(res.body, b'Secret')
+
+ def test_inaccessible(self):
+ self.testapp.get('/inaccessible', status=403)
+ self.testapp.extra_environ = {'REMOTE_USER': 'bob'}
+ self.testapp.get('/inaccessible', status=403)
+
+
class TestConflictApp(unittest.TestCase):
package = 'tests.pkgs.conflictapp'
@@ -581,10 +623,12 @@ class TestConflictApp(unittest.TestCase):
def test_overridden_authorization_policy(self):
config = self._makeConfig()
config.include(self.package)
- from pyramid.testing import DummySecurityPolicy
- config.set_authorization_policy(DummySecurityPolicy('fred'))
- config.set_authentication_policy(DummySecurityPolicy(permissive=True))
+ class DummySecurityPolicy:
+ def permits(self, context, principals, permission):
+ return True
+
+ config.set_authorization_policy(DummySecurityPolicy())
app = config.make_wsgi_app()
self.testapp = TestApp(app)
res = self.testapp.get('/protected', status=200)
diff --git a/tests/test_predicates.py b/tests/test_predicates.py
index a99651a8f..60e36047e 100644
--- a/tests/test_predicates.py
+++ b/tests/test_predicates.py
@@ -502,6 +502,22 @@ class Test_EffectivePrincipalsPredicate(unittest.TestCase):
return EffectivePrincipalsPredicate(val, config)
+ def _testing_authn_policy(self, userid, groupids=tuple()):
+ from pyramid.interfaces import IAuthenticationPolicy
+ from pyramid.security import Everyone, Authenticated
+
+ class DummyPolicy:
+ def effective_principals(self, request):
+ p = [Everyone]
+ if userid:
+ p.append(Authenticated)
+ p.append(userid)
+ p.extend(groupids)
+ return p
+
+ registry = self.config.registry
+ registry.registerUtility(DummyPolicy(), IAuthenticationPolicy)
+
def test_text(self):
inst = self._makeOne(('verna', 'fred'), None)
self.assertEqual(
@@ -526,7 +542,7 @@ class Test_EffectivePrincipalsPredicate(unittest.TestCase):
def test_it_call_authentication_policy_provides_superset(self):
request = testing.DummyRequest()
- self.config.testing_securitypolicy('fred', groupids=('verna', 'bambi'))
+ self._testing_authn_policy('fred', groupids=('verna', 'bambi'))
inst = self._makeOne(('verna', 'fred'), None)
context = Dummy()
self.assertTrue(inst(context, request))
@@ -535,14 +551,14 @@ class Test_EffectivePrincipalsPredicate(unittest.TestCase):
from pyramid.security import Authenticated
request = testing.DummyRequest()
- self.config.testing_securitypolicy('fred', groupids=('verna', 'bambi'))
+ self._testing_authn_policy('fred', groupids=('verna', 'bambi'))
inst = self._makeOne(Authenticated, None)
context = Dummy()
self.assertTrue(inst(context, request))
def test_it_call_authentication_policy_doesnt_provide_superset(self):
request = testing.DummyRequest()
- self.config.testing_securitypolicy('fred')
+ self._testing_authn_policy('fred')
inst = self._makeOne(('verna', 'fred'), None)
context = Dummy()
self.assertFalse(inst(context, request))
diff --git a/tests/test_request.py b/tests/test_request.py
index 484d86e01..1a10a8509 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -1,7 +1,7 @@
import unittest
from pyramid import testing
-from pyramid.security import AuthenticationAPIMixin, AuthorizationAPIMixin
+from pyramid.security import SecurityAPIMixin, AuthenticationAPIMixin
from pyramid.util import text_, bytes_
@@ -54,7 +54,7 @@ class TestRequest(unittest.TestCase):
self.assertEqual(cls.ResponseClass, Response)
def test_implements_security_apis(self):
- apis = (AuthenticationAPIMixin, AuthorizationAPIMixin)
+ apis = (SecurityAPIMixin, AuthenticationAPIMixin)
r = self._makeOne()
self.assertTrue(isinstance(r, apis))
diff --git a/tests/test_security.py b/tests/test_security.py
index 8b8028f61..2a8847f3b 100644
--- a/tests/test_security.py
+++ b/tests/test_security.py
@@ -187,32 +187,22 @@ class TestRemember(unittest.TestCase):
return remember(*arg, **kwarg)
- def test_no_authentication_policy(self):
+ def test_no_security_policy(self):
request = _makeRequest()
result = self._callFUT(request, 'me')
self.assertEqual(result, [])
- def test_with_authentication_policy(self):
+ def test_with_security_policy(self):
request = _makeRequest()
registry = request.registry
- _registerAuthenticationPolicy(registry, 'yo')
- result = self._callFUT(request, 'me')
- self.assertEqual(result, [('X-Pyramid-Test', 'me')])
-
- def test_with_authentication_policy_no_reg_on_request(self):
- from pyramid.threadlocal import get_current_registry
-
- registry = get_current_registry()
- request = _makeRequest()
- del request.registry
- _registerAuthenticationPolicy(registry, 'yo')
+ _registerSecurityPolicy(registry, 'yo')
result = self._callFUT(request, 'me')
self.assertEqual(result, [('X-Pyramid-Test', 'me')])
def test_with_missing_arg(self):
request = _makeRequest()
registry = request.registry
- _registerAuthenticationPolicy(registry, 'yo')
+ _registerSecurityPolicy(registry, 'yo')
self.assertRaises(TypeError, lambda: self._callFUT(request))
@@ -228,24 +218,14 @@ class TestForget(unittest.TestCase):
return forget(*arg)
- def test_no_authentication_policy(self):
+ def test_no_security_policy(self):
request = _makeRequest()
result = self._callFUT(request)
self.assertEqual(result, [])
- def test_with_authentication_policy(self):
+ def test_with_security_policy(self):
request = _makeRequest()
- _registerAuthenticationPolicy(request.registry, 'yo')
- result = self._callFUT(request)
- self.assertEqual(result, [('X-Pyramid-Test', 'logout')])
-
- def test_with_authentication_policy_no_reg_on_request(self):
- from pyramid.threadlocal import get_current_registry
-
- registry = get_current_registry()
- request = _makeRequest()
- del request.registry
- _registerAuthenticationPolicy(registry, 'yo')
+ _registerSecurityPolicy(request.registry, 'yo')
result = self._callFUT(request)
self.assertEqual(result, [('X-Pyramid-Test', 'logout')])
@@ -338,6 +318,23 @@ class TestViewExecutionPermitted(unittest.TestCase):
self.assertTrue(result)
+class TestAuthenticatedIdentity(unittest.TestCase):
+ def setUp(self):
+ testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def test_identity_no_security_policy(self):
+ request = _makeRequest()
+ self.assertEquals(request.authenticated_identity, None)
+
+ def test_identity(self):
+ request = _makeRequest()
+ _registerSecurityPolicy(request.registry, 'yo')
+ self.assertEqual(request.authenticated_identity, 'yo')
+
+
class TestAuthenticatedUserId(unittest.TestCase):
def setUp(self):
testing.setUp()
@@ -352,8 +349,15 @@ class TestAuthenticatedUserId(unittest.TestCase):
def test_with_authentication_policy(self):
request = _makeRequest()
_registerAuthenticationPolicy(request.registry, 'yo')
+ _registerSecurityPolicy(request.registry, 'wat')
self.assertEqual(request.authenticated_userid, 'yo')
+ def test_with_security_policy(self):
+ request = _makeRequest()
+ # Ensure the identity is stringified.
+ _registerSecurityPolicy(request.registry, 123)
+ self.assertEqual(request.authenticated_userid, '123')
+
def test_with_authentication_policy_no_reg_on_request(self):
from pyramid.threadlocal import get_current_registry
@@ -378,6 +382,12 @@ class TestUnAuthenticatedUserId(unittest.TestCase):
def test_with_authentication_policy(self):
request = _makeRequest()
_registerAuthenticationPolicy(request.registry, 'yo')
+ _registerSecurityPolicy(request.registry, 'wat')
+ self.assertEqual(request.unauthenticated_userid, 'yo')
+
+ def test_with_security_policy(self):
+ request = _makeRequest()
+ _registerSecurityPolicy(request.registry, 'yo')
self.assertEqual(request.unauthenticated_userid, 'yo')
def test_with_authentication_policy_no_reg_on_request(self):
@@ -426,43 +436,25 @@ class TestHasPermission(unittest.TestCase):
testing.tearDown()
def _makeOne(self):
- from pyramid.security import AuthorizationAPIMixin
+ from pyramid.security import SecurityAPIMixin
from pyramid.registry import Registry
- mixin = AuthorizationAPIMixin()
+ mixin = SecurityAPIMixin()
mixin.registry = Registry()
mixin.context = object()
return mixin
- def test_no_authentication_policy(self):
+ def test_no_security_policy(self):
request = self._makeOne()
result = request.has_permission('view')
self.assertTrue(result)
- self.assertEqual(result.msg, 'No authentication policy in use.')
+ self.assertEqual(result.msg, 'No security policy in use.')
- def test_with_no_authorization_policy(self):
+ def test_with_security_registered(self):
request = self._makeOne()
- _registerAuthenticationPolicy(request.registry, None)
- self.assertRaises(
- ValueError, request.has_permission, 'view', context=None
- )
-
- def test_with_authn_and_authz_policies_registered(self):
- request = self._makeOne()
- _registerAuthenticationPolicy(request.registry, None)
- _registerAuthorizationPolicy(request.registry, 'yo')
+ _registerSecurityPolicy(request.registry, 'yo')
self.assertEqual(request.has_permission('view', context=None), 'yo')
- def test_with_no_reg_on_request(self):
- from pyramid.threadlocal import get_current_registry
-
- registry = get_current_registry()
- request = self._makeOne()
- del request.registry
- _registerAuthenticationPolicy(registry, None)
- _registerAuthorizationPolicy(registry, 'yo')
- self.assertEqual(request.has_permission('view'), 'yo')
-
def test_with_no_context_passed(self):
request = self._makeOne()
self.assertTrue(request.has_permission('view'))
@@ -473,6 +465,58 @@ class TestHasPermission(unittest.TestCase):
self.assertRaises(AttributeError, request.has_permission, 'view')
+class TestLegacySecurityPolicy(unittest.TestCase):
+ def setUp(self):
+ testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def test_identity(self):
+ from pyramid.security import LegacySecurityPolicy
+
+ request = _makeRequest()
+ policy = LegacySecurityPolicy()
+ _registerAuthenticationPolicy(request.registry, 'userid')
+
+ self.assertEqual(policy.identify(request), 'userid')
+
+ def test_remember(self):
+ from pyramid.security import LegacySecurityPolicy
+
+ request = _makeRequest()
+ policy = LegacySecurityPolicy()
+ _registerAuthenticationPolicy(request.registry, None)
+
+ self.assertEqual(
+ policy.remember(request, 'userid'), [('X-Pyramid-Test', 'userid')]
+ )
+
+ def test_forget(self):
+ from pyramid.security import LegacySecurityPolicy
+
+ request = _makeRequest()
+ policy = LegacySecurityPolicy()
+ _registerAuthenticationPolicy(request.registry, None)
+
+ self.assertEqual(
+ policy.forget(request), [('X-Pyramid-Test', 'logout')]
+ )
+
+ def test_permits(self):
+ from pyramid.security import LegacySecurityPolicy
+
+ request = _makeRequest()
+ policy = LegacySecurityPolicy()
+ _registerAuthenticationPolicy(request.registry, ['p1', 'p2'])
+ _registerAuthorizationPolicy(request.registry, True)
+
+ self.assertIs(
+ policy.permits(request, request.context, 'userid', 'permission'),
+ True,
+ )
+
+
_TEST_HEADER = 'X-Pyramid-Test'
@@ -481,6 +525,27 @@ class DummyContext:
self.__dict__.update(kw)
+class DummySecurityPolicy:
+ def __init__(self, result):
+ self.result = result
+
+ def identify(self, request):
+ return self.result
+
+ def permits(self, request, context, identity, permission):
+ return self.result
+
+ def remember(self, request, userid, **kw):
+ headers = [(_TEST_HEADER, userid)]
+ self._header_remembered = headers[0]
+ return headers
+
+ def forget(self, request):
+ headers = [(_TEST_HEADER, 'logout')]
+ self._header_forgotten = headers[0]
+ return headers
+
+
class DummyAuthenticationPolicy:
def __init__(self, result):
self.result = result
@@ -516,6 +581,14 @@ class DummyAuthorizationPolicy:
return self.result
+def _registerSecurityPolicy(reg, result):
+ from pyramid.interfaces import ISecurityPolicy
+
+ policy = DummySecurityPolicy(result)
+ reg.registerUtility(policy, ISecurityPolicy)
+ return policy
+
+
def _registerAuthenticationPolicy(reg, result):
from pyramid.interfaces import IAuthenticationPolicy
diff --git a/tests/test_testing.py b/tests/test_testing.py
index ddcb35612..ebeafe21d 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -23,52 +23,17 @@ class TestDummySecurityPolicy(unittest.TestCase):
return DummySecurityPolicy
- def _makeOne(self, userid=None, groupids=(), permissive=True):
+ def _makeOne(self, identity=None, permissive=True):
klass = self._getTargetClass()
- return klass(userid, groupids, permissive)
+ return klass(identity, permissive)
- def test_authenticated_userid(self):
+ def test_identify(self):
policy = self._makeOne('user')
- self.assertEqual(policy.authenticated_userid(None), 'user')
-
- def test_unauthenticated_userid(self):
- policy = self._makeOne('user')
- self.assertEqual(policy.unauthenticated_userid(None), 'user')
-
- def test_effective_principals_userid(self):
- policy = self._makeOne('user', ('group1',))
- from pyramid.security import Everyone
- from pyramid.security import Authenticated
-
- self.assertEqual(
- policy.effective_principals(None),
- [Everyone, Authenticated, 'user', 'group1'],
- )
-
- def test_effective_principals_nouserid(self):
- policy = self._makeOne()
- from pyramid.security import Everyone
-
- self.assertEqual(policy.effective_principals(None), [Everyone])
+ self.assertEqual(policy.identify(None), 'user')
def test_permits(self):
policy = self._makeOne()
- self.assertEqual(policy.permits(None, None, None), True)
-
- def test_principals_allowed_by_permission(self):
- policy = self._makeOne('user', ('group1',))
- from pyramid.security import Everyone
- from pyramid.security import Authenticated
-
- result = policy.principals_allowed_by_permission(None, None)
- self.assertEqual(result, [Everyone, Authenticated, 'user', 'group1'])
-
- def test_principals_allowed_by_permission_not_permissive(self):
- policy = self._makeOne('user', ('group1',))
- policy.permissive = False
-
- result = policy.principals_allowed_by_permission(None, None)
- self.assertEqual(result, [])
+ self.assertEqual(policy.permits(None, None, None, None), True)
def test_forget(self):
policy = self._makeOne()
diff --git a/tests/test_viewderivers.py b/tests/test_viewderivers.py
index 3ca5f8534..12a903eaa 100644
--- a/tests/test_viewderivers.py
+++ b/tests/test_viewderivers.py
@@ -28,12 +28,11 @@ class TestDeriveView(unittest.TestCase):
return logger
def _registerSecurityPolicy(self, permissive):
- from pyramid.interfaces import IAuthenticationPolicy
- from pyramid.interfaces import IAuthorizationPolicy
+ from pyramid.interfaces import ISecurityPolicy
policy = DummySecurityPolicy(permissive)
- self.config.registry.registerUtility(policy, IAuthenticationPolicy)
- self.config.registry.registerUtility(policy, IAuthorizationPolicy)
+ self.config.registry.registerUtility(policy, ISecurityPolicy)
+ return policy
def test_function_returns_non_adaptable(self):
def view(request):
@@ -421,7 +420,7 @@ class TestDeriveView(unittest.TestCase):
self.assertFalse(hasattr(result, '__call_permissive__'))
self.assertEqual(result(None, None), response)
- def test_with_debug_authorization_no_authpol(self):
+ def test_with_debug_authorization_no_security_policy(self):
response = DummyResponse()
view = lambda *arg: response
self.config.registry.settings = dict(
@@ -442,59 +441,7 @@ class TestDeriveView(unittest.TestCase):
logger.messages[0],
"debug_authorization of url url (view name "
"'view_name' against context None): Allowed "
- "(no authorization policy in use)",
- )
-
- def test_with_debug_authorization_authn_policy_no_authz_policy(self):
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = dict(debug_authorization=True)
- from pyramid.interfaces import IAuthenticationPolicy
-
- policy = DummySecurityPolicy(False)
- self.config.registry.registerUtility(policy, IAuthenticationPolicy)
- logger = self._registerLogger()
- result = self.config._derive_view(view, permission='view')
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- self.assertEqual(result(None, request), response)
- self.assertEqual(len(logger.messages), 1)
- self.assertEqual(
- logger.messages[0],
- "debug_authorization of url url (view name "
- "'view_name' against context None): Allowed "
- "(no authorization policy in use)",
- )
-
- def test_with_debug_authorization_authz_policy_no_authn_policy(self):
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = dict(debug_authorization=True)
- from pyramid.interfaces import IAuthorizationPolicy
-
- policy = DummySecurityPolicy(False)
- self.config.registry.registerUtility(policy, IAuthorizationPolicy)
- logger = self._registerLogger()
- result = self.config._derive_view(view, permission='view')
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- self.assertEqual(result(None, request), response)
- self.assertEqual(len(logger.messages), 1)
- self.assertEqual(
- logger.messages[0],
- "debug_authorization of url url (view name "
- "'view_name' against context None): Allowed "
- "(no authorization policy in use)",
+ "(no security policy in use)",
)
def test_with_debug_authorization_no_permission(self):
@@ -665,32 +612,11 @@ class TestDeriveView(unittest.TestCase):
"'view_name' against context Exception()): True",
)
- def test_secured_view_authn_policy_no_authz_policy(self):
+ def test_secured_view_authn_policy_no_security_policy(self):
response = DummyResponse()
view = lambda *arg: response
self.config.registry.settings = {}
- from pyramid.interfaces import IAuthenticationPolicy
- policy = DummySecurityPolicy(False)
- self.config.registry.registerUtility(policy, IAuthenticationPolicy)
- result = self.config._derive_view(view, permission='view')
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- self.assertEqual(result(None, request), response)
-
- def test_secured_view_authz_policy_no_authn_policy(self):
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = {}
- from pyramid.interfaces import IAuthorizationPolicy
-
- policy = DummySecurityPolicy(False)
- self.config.registry.registerUtility(policy, IAuthorizationPolicy)
result = self.config._derive_view(view, permission='view')
self.assertEqual(view.__module__, result.__module__)
self.assertEqual(view.__doc__, result.__doc__)
@@ -702,53 +628,41 @@ class TestDeriveView(unittest.TestCase):
self.assertEqual(result(None, request), response)
def test_secured_view_raises_forbidden_no_name(self):
- from pyramid.interfaces import IAuthenticationPolicy
- from pyramid.interfaces import IAuthorizationPolicy
from pyramid.httpexceptions import HTTPForbidden
response = DummyResponse()
view = lambda *arg: response
self.config.registry.settings = {}
- policy = DummySecurityPolicy(False)
- self.config.registry.registerUtility(policy, IAuthenticationPolicy)
- self.config.registry.registerUtility(policy, IAuthorizationPolicy)
+ self._registerSecurityPolicy(False)
result = self.config._derive_view(view, permission='view')
request = self._makeRequest()
request.view_name = 'view_name'
request.url = 'url'
- try:
+ with self.assertRaises(HTTPForbidden) as cm:
result(None, request)
- except HTTPForbidden as e:
- self.assertEqual(
- e.message, 'Unauthorized: <lambda> failed permission check'
- )
- else: # pragma: no cover
- raise AssertionError
+ self.assertEqual(
+ cm.exception.message,
+ 'Unauthorized: <lambda> failed permission check',
+ )
def test_secured_view_raises_forbidden_with_name(self):
- from pyramid.interfaces import IAuthenticationPolicy
- from pyramid.interfaces import IAuthorizationPolicy
from pyramid.httpexceptions import HTTPForbidden
def myview(request): # pragma: no cover
pass
self.config.registry.settings = {}
- policy = DummySecurityPolicy(False)
- self.config.registry.registerUtility(policy, IAuthenticationPolicy)
- self.config.registry.registerUtility(policy, IAuthorizationPolicy)
+ self._registerSecurityPolicy(False)
result = self.config._derive_view(myview, permission='view')
request = self._makeRequest()
request.view_name = 'view_name'
request.url = 'url'
- try:
+ with self.assertRaises(HTTPForbidden) as cm:
result(None, request)
- except HTTPForbidden as e:
- self.assertEqual(
- e.message, 'Unauthorized: myview failed permission check'
- )
- else: # pragma: no cover
- raise AssertionError
+ self.assertEqual(
+ cm.exception.message,
+ 'Unauthorized: myview failed permission check',
+ )
def test_secured_view_skipped_by_default_on_exception_view(self):
from pyramid.request import Request
@@ -794,12 +708,8 @@ class TestDeriveView(unittest.TestCase):
app = self.config.make_wsgi_app()
request = Request.blank('/foo', base_url='http://example.com')
request.method = 'POST'
- try:
+ with self.assertRaises(HTTPForbidden):
request.get_response(app)
- except HTTPForbidden:
- pass
- else: # pragma: no cover
- raise AssertionError
def test_secured_view_passed_on_explicit_exception_view(self):
from pyramid.request import Request
@@ -2151,10 +2061,10 @@ class DummySecurityPolicy:
def __init__(self, permitted=True):
self.permitted = permitted
- def effective_principals(self, request):
- return []
+ def identify(self, request):
+ return 123
- def permits(self, context, principals, permission):
+ def permits(self, request, context, identity, permission):
return self.permitted