31.4. Permissions [voting story] – Mastering Plone Development – 31. Create an add-on [The voting story]

Permissions [voting story]

31.4. Permissions [voting story]#

In this part you will:

  • Add custom permissions

  • Protect the voting service using permissions

  • Configure which roles get the permissions

Topics covered:

  • Permissions

  • The rolemap

Check out mastering-plone-votable-add-on at tag actions:

git checkout actions

The code at the end of the chapter:

git checkout permissions

More info in The code for the training

We have a working voting add-on, but it currently allows too many people to vote. Currently, anyone who can view the talks can vote. We should add custom permissions so that we can make sure only the conference program committee can vote.

Add custom permissions#

Plone has lots of built-in permissions like View and Modify portal content. We can also add our own custom permissions in our add-on.

Edit the file backend/src/ploneconf/votable/permissions.zcml:

 1<configure
 2    xmlns="http://namespaces.zope.org/zope"
 3    xmlns:zcml="http://namespaces.zope.org/zcml"
 4    i18n_domain="plone"
 5    >
 6
 7  <permission
 8      id="ploneconf.votable.view_vote"
 9      title="ploneconf.votable: View votes"
10      />
11
12  <permission
13      id="ploneconf.votable.can_vote"
14      title="ploneconf.votable: Can vote"
15      />
16
17  <permission
18      id="ploneconf.votable.clear_votes"
19      title="ploneconf.votable: Clear votes"
20      />
21
22</configure>

We are adding three permissions:

  • View votes will give access to view the voting results

  • Can vote will give access to add a vote

  • Clear votes will give access to reset the votes

We use the add-on name ploneconf.votable as a prefix for the permission names, to make sure they don't conflict with other add-ons.

Each permission has an id and a title. They are used in different places. The id is used when referring to permissions in ZCML, such as in a view declaration. The title is used when checking permissions with plone.api.

Check permissions#

Now we can update the @votes services to check these permissions.

Update {file}`backend/src/ploneconf/votable/services/votes.py:

 1from plone import api
 2from plone.protect.interfaces import IDisableCSRFProtection
 3from plone.restapi.deserializer import json_body
 4from plone.restapi.services import Service
 5from ploneconf.votable.behaviors.votable import IVotable
 6from zExceptions import Unauthorized
 7from zope.interface import alsoProvides
 8
 9
10class VotingGet(Service):
11    """Get voting information about the current object"""
12
13    def reply(self):
14        if not api.user.has_permission(
15            "ploneconf.votable: View votes", obj=self.context
16        ):
17            raise Unauthorized("User not authorized to view votes.")
18        return vote_info(self.context)
19
20
21class VotingPost(Service):
22    """Vote for an object"""
23
24    def reply(self):
25        alsoProvides(self.request, IDisableCSRFProtection)
26        voting = IVotable(self.context)
27        if not api.user.has_permission("ploneconf.votable: Can vote", obj=self.context):
28            raise Unauthorized("User not authorized to vote.")
29        data = json_body(self.request)
30        vote = data["rating"]
31        voting.vote(vote)
32
33        return vote_info(self.context)
34
35
36class VotingDelete(Service):
37    """Clear votes for an object"""
38
39    def reply(self):
40        alsoProvides(self.request, IDisableCSRFProtection)
41        if not api.user.has_permission(
42            "ploneconf.votable: Clear votes", obj=self.context
43        ):
44            raise Unauthorized("User not authorized to clear votes.")
45        voting = IVotable(self.context)
46        voting.clear()
47        return vote_info(self.context)
48
49
50def vote_info(obj):
51    """Returns voting information about the given object."""
52    voting = IVotable(obj)
53    info = {
54        "average_vote": voting.average_vote(),
55        "total_votes": voting.total_votes(),
56        "has_votes": voting.has_votes(),
57        "already_voted": voting.already_voted(),
58        "can_vote": api.user.has_permission("ploneconf.votable: Can vote", obj=obj),
59        "can_clear_votes": api.user.has_permission(
60            "ploneconf.votable: Clear votes", obj=obj
61        ),
62    }
63    return info

Raising the Unauthorized exception returns an HTTP response with status 401.

Tip

We could also update the permission for the services in configure.zcml. But doing the check in Python lets us return a more informative error response.

We're also returning can_vote and can_clear_votes in the vote_info data, so that the frontend component can check what the user is allowed to do.

Configure the rolemap#

Permissions are designed to provide flexibility about who actually gets the permission. We need to configure the rolemap to define which roles get the permissions.

Update the file backend/src/ploneconf/votable/profiles/default/rolemap.xml.

 1<?xml version="1.0" encoding="utf-8"?>
 2<rolemap>
 3  <permissions>
 4
 5    <permission acquire="True"
 6                name="ploneconf.votable: View votes"
 7    >
 8      <role name="Authenticated" />
 9      <role name="Site Administrator" />
10      <role name="Manager" />
11    </permission>
12    <permission acquire="True"
13                name="ploneconf.votable: Can vote"
14    >
15      <role name="Reviewer" />
16    </permission>
17    <permission acquire="True"
18                name="ploneconf.votable: Clear votes"
19    >
20      <role name="Site Administrator" />
21      <role name="Manager" />
22    </permission>
23
24  </permissions>
25</rolemap>

Any authenticated user is allowed to view the voting results.

Only users with the Reviewer role are allowed to add votes. (For the conference site, it would make sense to put the Program Committee members in a group, and assign the Reviewer role to that group.)

Only users with the Site Administrator or Manager roles are allowed to reset the votes.

After reinstalling the add-on, the updated rolemap takes effect.