The following example uses a User-Role
relation in order to codify the more complex permission behavior required for Activites. The relation allows both:
- A hierarchical page structure for organizing permissions
- Fine-grained rules around page permissions, no matter their position in the hierarchy.
We begin by defining a Role
type listing all possible roles that a user may have in Activities. Furthermore, we define the Perm
type to enumerate possible permissions
class Role(Enum):
"""Available user roles"""
PO = "Programme Manager"
AM = "Activity Manager"
TO = "Technical Officer"
CO = "Contract Officer"
@classmethod
def all(cls):
return [(k.value, k.name) for k in cls]
class Perm(IntFlag):
"""Permission flags"""
Empty = 0
View = 1
Edit = 2
Publish = 4
Delete = 8
All = 15
There also has to be a way to relate a Role
to a User
. For that, we define snippets. Because different pages (e.g. ProgrammePage
, and ActivityPage
) have different member sets (a ProgammePage
has only PM
roles available, while ActivityPage
has all the others), we define a category for each:
@register_snippet
class ProgrammeMember(Member):
page = models.ForeignKey(ProgrammePage, on_delete=models.CASCADE)
def __str__(self):
return "[%s] %s" % (self.role, self.user.username)
class Meta:
verbose_name_plural="Programme Members"
@register_snippet
class ActivityMember(Member):
page = models.ForeignKey(ActivityPage, on_delete=models.CASCADE)
def __str__(self):
return "[%s] %s" % (self.role, self.user.username)
class Meta:
verbose_name_plural="Activity Members"
To add user's to an Activity or Programme, simply visit the Snippets section and assign them a role and page under the respective page type.
Next, we define a ControlledPage
class as an abstract class which requires that any derived page expose:
- A list of members for that page instance
- A map of permissions that are afforded to the various roles possible for that page type
class ControlledPage(Page):
class Meta:
abstract = True
def members(self, user):
pass
def permissions_for_role(self):
return {}
@staticmethod
def has_permission(page, perm, user):
# ...
This is best demonstrated by example, for say, an ActivityPage
:
class ActivityPage(ControlledPage):
desc = RichTextField(blank=True)
def members(self, user):
return ActivityMember.objects.filter( # Derived class filters own member set
page=self,
user=user
)
def permissions_for_role(self): # Derived class defines own permissions
return {
Role.PO: Perm.All,
Role.AM: Perm.All,
Role.TO: Perm.All ^ Perm.Publish,
Role.CO: Perm.All ^ Perm.Publish
}
content_panels = Page.content_panels + [
FieldPanel('desc'),
]
This allows each page to (1) refer to their own member set (e.g. ActivityMember
) and (2) define their own permissions within the class (should make maintenance easy).
Finally, we tie the permission mechanism into Wagtail. The main permission resolver is in the ControlledPage
class, and is titled: has_permission
:
class ControlledPage(Page):
# ...
@staticmethod
def has_permission(page, perm, user):
if 'admin' == user.username:
return True
if not issubclass(type(page.specific), ControlledPage):
return False
ps = page.specific.permissions_for_role()
ms = page.specific.members(user=user)
if len(ms) > 0 and perm in ps.get(Role(ms[0].role), Perm.Empty):
return True
return ControlledPage.has_permission(page.get_parent(), perm, user)
It first determines if the current user has a role within the page. If the user does not, perhaps they are a member of a parent page. For example, a Programme Manager might be trying to edit an Activity, for which they are not a direct member. In this case, a recursive search is performed on the parent page. If no derived page of ControlledPage
is found, or a suitable permission is located, then the search ceases. We finally link this into the Wagtail admin using hooks:
@hooks.register('before_edit_page')
def before_edit_page(request, page):
if ControlledPage.has_permission(page, Perm.Edit, request.user):
return request
else:
return permission_denied(request, PermissionDenied("Insufficient privileges!"))
# Extend to other hooks here
Benefits of this approach should be:
- You can easily add user roles in the admin
- You get the desired behavior not afforded by the default Wagtail CMS permission structure
Some additional work is needed to make this work seamlessly. For one, each user should be able to access the admin. This means making them an Editor by default should be done. Their ability to edit pages within the admin panel is restricted by the hooks.