.. _group_security: ==================== Group-Level Security ==================== Groups are a very common way of organizing users and mapping them to a :term:`permission`. It is not very fine-grained, but it is simple, and enough for a lot of applications. It is also a good base to begin other types of authorization. We'll be using Pyramid's default :term:`authorization policy`, the ``ACLAuthorizationPolicy``. The demo application defines 2 groups: "editor" and "admin". It also supports a third principal ``pyramid.security.Authenticated``, which is automatically added by Pyramid's baked-in authentication policies. Goals ===== #. Define 2 groups, "editor" and "admin". #. Restrict creation of pages to `authenticated` users. #. Restrict editing of existing pages to users in the "editor" or "admin" groups. #. Restrict viewing of user-related pages to only "admin" users. Access Control Lists ==================== In Pyramid, groups would be more accurately described as :term:`principal` identifiers, generated by the :term:`authentication policy`. Pyramid's default authentication policies support a `groupfinder` callback which, when passed the detected `userid` should return a list of principals or ``None`` if the `userid` is invalid (e.g. maybe it has been deleted). From the :ref:`base_app`, the ``User`` object already has a list of `groups`. Let's add a `groupfinder` which can map the current user to its groups: .. literalinclude:: ../1.group_security/demo.py :lines: 71-74 The groups are prefixed with the "g:" to help distinguish them as principals related to the user's groups. Pyramid's ``ACLAuthorizationPolicy`` works by mapping the `effective principals` from the `groupfinder` into an access control list (ACL). Each entry in the list is a 3-tuple mapping a :term:`principal` to a :term:`permission`. An entry can either `allow` or `deny` access and the first entry matching one of the principals to the permission wins. Below is an example ACL that would allow any authenticated user the ability to create content, but disallow them from editing content unless they are an "editor" or an "admin". .. code-block:: python [ (Allow, Authenticated, 'create'), (Allow, 'g:editor', 'edit'), (Allow, 'g:admin', ALL_PERMISSIONS), ] Notice how the principals in the ACL match up with the principals returned from the `groupfinder`. Pyramid's default authentication policies will automatically add the ``pyramid.security.Everyone`` principal to the list, and the ``pyramid.security.Authenticated`` principal if the `groupfinder` returned anything other than ``None``. Remember that ``None`` would signify that the user is not authenticated. The ``pyramid.security.ALL_PERMISSIONS`` permission is a "magic" identifier that matches any permission. Root Factory ============ URL Dispatch's use of the :term:`resource tree` is fairly simple. By default the traversal path is effectively ``'/'``, meaning that the `root` of the tree will always be the :term:`context`. As mentioned briefly above, the ``ACLAuthorizationPolicy`` uses the current :term:`context` to compute an ACL which maps principals to permissions. This is done via the ``__acl__`` property of the :term:`context`. .. literalinclude:: ../1.group_security/demo.py :pyobject: Root See how the ``__acl__`` defines the ACL discussed previously. This ``Root`` object is also a :term:`root factory` and the application can be configured to use it as such. Root factories are explained in more detail in :ref:`the_resource_tree`. .. literalinclude:: ../1.group_security/demo.py :lines: 273-284 :emphasize-lines: 6, 12 Securing the Views ================== We've setup a single-node :term:`resource tree`, we've configured our :term:`authentication policy` with a `groupfinder`, and we've set the :term:`authorization policy` to the ``ACLAuthorizationPolicy``. All that's left to do now is to start securing parts of our app. Create Page View ---------------- As mentioned before, only `authenticated` users should be allowed to create new pages in our wiki. Thus, our ACL maps the ``Authenticated`` principal to the "create" permission. Now we can lock down the view for ``'/create_page'`` to require the "create" permission. .. literalinclude:: ../1.group_security/demo.py :lines: 202-208 :emphasize-lines: 3 Edit Page View -------------- Editing of pages in the wiki via ``'/page/{title}/edit'`` should only be allowed by users in the "editor" or "admin" groups. We've already mapped these groups to the "edit" permission, so simply add it to the view. .. literalinclude:: ../1.group_security/demo.py :lines: 234-239 :emphasize-lines: 3 User Views ---------- The user-related pages ``'/users'`` and ``'/user/{login}'`` should only be accessible by "admin" users. There isn't an ACL explicitly mapping the "admin" group to an permission for this, but the ``ALL_PERMISSIONS`` magic permission is a catch-all, so we can make up any permission we want. Here we'll use the "admin" permission. ``'/users'``: .. literalinclude:: ../1.group_security/demo.py :lines: 132-137 :emphasize-lines: 3 ``'/user/{login}'``: .. literalinclude:: ../1.group_security/demo.py :lines: 142-147 :emphasize-lines: 3 Simple Object-Level Authorization ================================= We've added basic group-level access to our views. This scheme can actually do a lot more than simple groups, as hinted by the fact that the `groups` are actually a more general concept of `principals`. Until now, we've treated each :term:`principal` as a `group` and the ``__acl__`` property of the :term:`context` as unchanging. Let's instead think of each principal as representing a resource identifier, and the ACL as being dynamic depending on what resource is being accessed. For example, the :term:`context` knows that the current request is for a ``Page`` object with ``id == 1``. The :term:`context` can make the ``__acl__`` property dynamic and populate it with a special entry giving users with the :term:`principal` ``'page:1'`` access. Now, all the :term:`authentication policy` must do is return a list of all pages the user can access. Below is some pseudocode for playing with this idea: .. code-block:: python class Root(object): @property def __acl__(self): if 'page' in self.request.matchdict: page = self.request.matchdict['page'] return [(Allow, 'page:'+page, 'page')] return [] def __init__(self, request): self.request = request Notice that the ``__acl__`` on ``Root`` is now dynamic, and changes based on the request. The principal ``'page:???'`` must then match one of the principals returned by the groupfinder below. .. code-block:: python def groupfinder(userid, request): user = User.get_by_id(userid) if user is not None: return ['page:'+page for page in user.pages] This method suffers from certain drawbacks, such as needing to query the database for a list of *all* of the objects to which the user has access on every request. If this is a concern, the :ref:`object_security` demo provides a more efficient and flexible way to perform fine-grained security on the resources in the application.