Comment on page
Permissions framework
Misago brings its own ACL (Access Control Lists) framework for implementing permissions. This document explains to how to use and extend it with your own permissions.
Permissions are stored on special models named "roles" and assigned to users directly and trough ranks. Guest users always have permissions from "Guest" role, and users always have permissions form "Member" role.
During the http request lifecycle Misago uses the
user_acl_middleware
middleware to set user_acl
property on the request
. This user_acl
property is plain dictionary that contains user acl as well as additional data useful for permission checks:user_id
- ID of the user thisuser_acl
belongs to.cache_versions
- copy of cache versions used to create thisuser_acl
.is_authenticated
is_anonymous
is_staff
is_superuser
To obtain
user_acl
outside of http request, you will need to use get_user_acl
function from misago.acl.useracl
module, and get_cache_versions
function from misago.cache.versions
to obtain cache versions:from django.conf.auth import get_user_model
from misago.acl.useracl import get_user_acl
from misago.cache.versions import get_cache_versions
User = get_user_model()
user = User.objects.get(username="MyUser")
cache_versions = get_cache_versions()
user_acl = get_user_acl(user, cache_versions)
Construction of User's ACLs can be costful process, especially once you start installing extensions adding new features to your site. Because of this, Misago is not assigning ACLs to Users, but to combinations of roles. Each user has "ACL key" assigned that allows Misago to associate this user with valid ACL cache, and reuse acls for users with same roles.
ACLs are cached and only rebuild when data affecting ACLs changes. Misago also provides simple utility for clearing all ACL caches:
from misago.acl import cache
cache.clear()
Simplest way to check if user has permission to do something is to look up the appropriate key on their
user_acl
:if not user_acl["can_have_signature"]:
raise PermissionDenied("You don't have permission to set a signature.")
ACL entries may be of different type. For example
categories
entry is dict of dicts:if user_acl['categories'].get(category.pk, {}).get('can_see'):
# We can see category!
If you have object instance, you may annotate it using the
add_acl_to_obj
utility function from misago.acl.objectacl
. Doing so will create an acl
property on the object for you to instrospect its permissions easily:if category.pk in user_acl['visible_categories']:
# Not really shorter, but simpler to remember and works in django templates!
from misago.acl import add_acl
add_acl_to_obj(user_acl, categories)
for category in categories
if category.acl['can_see']:
# Now model instances in categories queryset are aware of their ACLs!
# ACLs are easy to check in templates too now!
In templates user acl is also made available as
user_acl
variable:{% if user_acl.can_search %}
{% include "search_form.html" %}
{% endif %}
Lastly, permission modules may expose permission checking functions:
from misago.category.permissions import allow_browse_category:
# Raises PermissionDenied error with error message that can be shown to user
# if they don't have permission to browse the category
allow_browse_category(user_acl, category)
Because ACL framework is very flexible, different features can have different ways to check their permissions.
Misago adds
Misago User ACL
option to the Django Debug Toolbar menu. This page display user roles permissions as well as final ACL assigned to current request.ACL framework extensions are modules registered in
MISAGO_ACL_EXTENSIONS
setting. By convention, those modules are either named "permissions", or they are located in "permissions" package.Misago checks module for following functions:
Required. This function is called when change permissions form for role is being build for view. It's expected to return Form type or none, if provider is not recognizing role type (eg. there is no sense in adding profiles visibility permissions to forums role form).
Misago provides custom
YesNoSwitch
form field that renders nice "Yes/No" switch as input. This field is simple wrapper around TypedChoiceField
that coerces to int
. If you use use it for your permissions, make sure your ACL implementation handles their values as 1
or 0
, not as True
or False
, or your forms will break!Make sure that all fields in your form have initial value, or your form will make tests suite fail because it will be unable to mock POST requests to admin forms correctly.
Required. Is used in process of building new ACL. Its supplied dict with incomplete ACL, list of user roles and name of key under which its permissions values are stored in roles
permissions
attributes. Its expected to access roles permissions
attributes which are dicts of values coming from permission change forms and return updated acl
dict.Optional. Is called by providers registry after provider module was imported, to allow it to register annotators and serializers for ACL's. Receives only one argument:
registry
- istance of PermissionProviders that imported module.
When module's
register_with
function is called, its passed PermissionProviders
instance that exposes following methods:Registers
func
as ACL annotator for hashable_type
.Registers
func
as user ACL serializer. This function will be called with copy of user_acl
(excluding the cache_versions
key), and is expected to perform any required changes before ACL will be converted to JSON and sent to client. For example, an serializer simplifies the categories
entry to only contain ACL for categories that are browseable by the user.Returns list of annotators registered for type of
obj
or empty list is none exist.Returns list of user acl serializers registered empty list is none exist.
Annotators are functions called when object is being made ACL aware. It always receives two arguments:
user
- user asking to make target aware of its ACL'starget
- target instance, guaranteed to be an single object, not list or other iterable (like queryset)
Consider those three simple permission sets:
roles_permissions = (
{'can_be_knight': False},
{'can_be_knight': True},
{'can_be_knight': False},
)
In order to obtain final ACL, one or more ACLs have to be sum together. Such operation requires loop over ACLs which compares values of dicts keys and picks preffered ones.
This problem can be solved using simple implementation:
final_acl = {'can_be_knight': False}
for acl in roles_permissions:
if acl['can_be_knight']:
final_acl['can_be_knight'] = True
But what if there are 20 permissions in ACL? Or if we are comparing numbers? What if complex rules are involved like popular "greater beats lower, zero beats all"? This brings need for more suffisticated solution and Misago provides one in forum of
misago.acl.algebra
module.This module provides utilities for summing two acls and supports three most common comparisions found in web apps:
- greater: True beats False, 42 beats 13
- lower: False beats True, 13 beats 42
- greater or zero: 42 beats 13, zero beats everything
- lower non zero: 13 beats 42, everything beats zero
This function adds ACLs to result_acl using set or rules provided as additional kwargs. Alternatively, it access iterable of roles and extension key.
Example usage is following:
from misago.acl import algebra
user_acls = [
{
'can_see': 0,
'can_hear': 0,
'max_speed': 10,
'min_age': 16,
'speed_limit': 50,
},
{
'can_see': 1,
'can_hear': 0,
'max_speed': 40,
'min_age': 20,
'speed_limit': 0,
},
{
'can_see': 0,
'can_hear': 1,
'max_speed': 80,
'min_age': 18,
'speed_limit': 40,
},
]
defaults = {
'can_see': 0,
'can_hear': 0,
'max_speed': 30,
'min_age': 18,
'speed_limit': 60,
}
final_acl = algebra.sum_acls(
defaults, acls=user_acls,
can_see=algebra.greater,
can_hear=algebra.greater,
max_speed=algebra.greater,
min_age=algebra.lower,
speed_limit=algebra.greater_or_zero
)
As you can see because tests are callables, its easy to extend
sum_acls
support for new tests specific for your ACLs.Last modified 4yr ago