Bridgekeeper¶
Who would cross the Bridge of Deathmust answer me these questions three,ere the other side he see.—The Bridgekeeper, Monty Python and the Holy Grail
Bridgekeeper is a permissions library for Django projects, where permissions are defined in your code, rather than in your database.
It’s heavily inspired by django-rules, but with one important difference: it works on QuerySets as well as individual model instances.
This means that you can efficiently show a ListView
of all of the model instances that your user is allowed to edit, for instance, without having your permission-checking code in two different places.
Bridgekeeper works on Django 2.0+ on Python 3.5+, and is licensed under the MIT License.
Warning
Bridgekeeper (and these docs!) are a work in progress.
Installing Bridgekeeper¶
First, install the bridgekeeper
package from PyPI.
$ pip install bridgekeeper
# or, if you're using pipenv
$ pipenv install bridgekeeper
Then, add Bridgekeeper to your settings.py
:
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
# ...
+ 'bridgekeeper',
)
# ...
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
+ 'bridgekeeper.backends.RulePermissionBackend',
)
Note
Order doesn’t matter for either the INSTALLED_APPS
or AUTHENTICATION_BACKENDS
entry.
You might not already have the AUTHENTICATION_BACKENDS
setting in your settings.py
; if not, you’ll have to add it.
Defining Permissions¶
In this tutorial, we’ll be using a example app, an online stock management portal for shrubberies; we’ll define some permissions for it in this section, then use them in views in the next section. It has a single app called shrubberies
, with a models.py
looks something like this:
from django.contrib.auth.models import User
from django.db import models
class Store(models.Model):
name = models.CharField(max_length=255)
class Branch(models.Model):
store = models.ForeignKey(Store, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
class Shrubbery(models.Model):
branch = models.ForeignKey(Branch, on_delete=models.PROTECT)
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=5, decimal_places=2)
class Profile(models.Model):
"""User profile.
Every user has one Profile object attached to them, which is
automatically created when the user is added, and holds information
about which branch of which store they belong to and what their
role is.
"""
user = models.OneToOneField(User, on_delete=models.CASCADE)
branch = models.ForeignKey(Branch, on_delete=models.PROTECT)
role = models.CharField(max_length=16, choices=(
('apprentice', 'Apprentice Shrubber'),
('shrubber', 'Shrubber'),
))
Defining Our First Permission¶
In Bridgekeeper, permissions are defined by rules. A rule is an object that can be given a user and a model instance, and decides whether or not to allow that user access to that instance.
Note
From that description, you might be thinking that a rule object is just a function with the signature (user, model_instance) -> bool
. While you can certainly think of them that way, internally they’re a little more complex than that, for reasons that will become apparent in the next section.
One of the simplest rules in Bridgekeeper is the built-in is_staff
rule, which answers “yes” if the user trying to log in has is_staff
set, or “no” otherwise.
We turn a rule into a permission by assigning it to a name. We do that by creating a file called permissions.py
inside our app, importing bridgekeeper.perms
(which is a Python dictionary [1] that maps permission names to their corresponding rules) and adding entries to it.
from bridgekeeper import perms
from bridgekeeper.rules import is_staff
perms['shrubbery.create_store'] = is_staff
perms['shrubbery.update_store'] = is_staff
perms['shrubbery.delete_store'] = is_staff
Note
We’ve used permission names that follow the convention set by Django’s built-in permissions mechanism, so that they’re used by other apps that expect that naming convention, such as Django’s built-in admin. You can use whatever permission names you like, although it’s best to namespace them with the name of your app followed by a full stop at the start (e.g. shrubbery.foo
).
These permissions are now fully working; if you wanted, you could skip right through to the next section to see how to use them in your views. Don’t, though, because Bridgekeeper is capable of far more.
Blanket Rules¶
A blanket rule is a rule that decides whether or not to allow access based solely on the user that’s trying to access the resource. They’ll either allow access to everything or nothing at all, hence the name.
We’ve already used one blanket rule—the built-in is_staff
rule—but we can also define our own, by using the blanket_rule
decorator to wrap a function that takes a user and returns a boolean.
In this example, we’re using the role
attribute on each user’s associated Profile
instance to restrict access to users that have been assigned a particular role:
from bridgekeeper.rules import blanket_rule
@blanket_rule
def is_apprentice(user):
return user.profile.role == 'apprentice'
@blanket_rule
def is_shrubber(user):
return user.profile.role == 'shrubber'
If we were given a requirement like this:
Only shrubbers can edit shrubberies.
We could use our new is_shrubber
rule the same way that we used is_staff
before:
from .rules import is_shrubber
perms['shrubbery.update_shrubbery'] = is_shrubber
Matching Against Model Instance Attributes¶
Blanket rules let us allow or deny access to entire model classes based on the user, but we can also allow access to only certain instances. Consider the following requirement:
Users can only edit shrubberies that belong to their branch.
We can model this as a Bridgekeeper rule by creating an instance of the Attribute
class:
from bridgekeeper.rules import Attribute
perms['shrubbery.update_shrubbery'] = Attribute('branch', lambda user: user.profile.branch)
You can think of Attribute
as the Bridgekeeper equivalent to the standard library’s getattr()
function. It will only allow access when the attribute named in the first argument (here, 'branch'
) matches whatever is in the second argument. The second argument can either be a constant, or—as we’ve used here—a function that takes the current user and returns something to match against.
Traversing Relationships¶
What if we change the requirement to something like this?
Users can only edit shrubberies that belong to their store.
Shrubberies don’t have a store
attribute; we have to go through the branch
attribute to figure out which store a shrubbery belongs to, so we can’t use Attribute
.
This is where the Relation
class comes in. Relation
is similar to Attribute
, but instead of taking a constant or function as its last argument, it takes another rule object, which is applied to the other side of the relation.
from bridgekeeper.rules import Relation
from . import models
perms['shrubbery.update_shrubbery'] = Relation(
'branch',
# This rule gets checked against the branch object, not the shrubbery
Attribute('store', lambda user: user.profile.branch.store),
)
Combining Rules Together¶
All of the rules that we’ve seen so far are quite simple, each only checking one thing. Fortunately, Bridgekeeper rules can be combined together, letting us model much more complex requirements.
We do this using the &
, |
and ~
operators. (If you’ve used Q
objects, combining Bridgekeeper rules will feel familiar.)
- Prefixing a rule with
~
inverts it. For example, the expression~is_apprentice
returns a rule that allows access to everyone that is not an apprentice shrubber. - Combining two rules with
|
allows access if either rule matches. For example,is_staff | is_shrubber
allows access to users that are either administrative staff or shrubbers. - Combining two rules with
&
allows access if both rules match. For example,is_staff & is_shrubber
allows access to users that are both administrative staff and shrubbers.
For a more complex example, let’s say that we needed to model the following requirement:
Administrative staff (withis_staff
set) can edit all shrubberies in the system. Shrubbers can edit all shrubberies in the store they belong to. Apprentice shrubbers can edit all shrubberies in their branch.
First, we need to rephrase this requirement so that it’s made up of simpler rules combined with and, or, and not.
Users can edit shrubberies if:
- They are administrative staff (with
is_staff
set), or- They are a shrubber, and the shrubbery belongs to the same store as them, or
- They are an apprentice shrubber, and the shrubbery belongs to the same branch as them
In earlier sections of this chapter, we’ve already talked about rules that allow access to staff users and users with particular roles. We’ve also already discussed rules that allow access only to shrubberies belonging to the same store or branch as the user trying to access them. All we need to do now is combine them together:
from bridgekeeper.rules import is_staff
from .rules import is_shrubber, is_apprentice
from . import models
perms['shrubbery.update_shrubbery'] = is_staff | (
is_shrubber & Relation(
'branch',
Attribute('store', lambda user: user.profile.branch.store),
)
) | (
is_apprentice & Attribute('branch', lambda user: user.profile.branch)
)
[1] | bridgekeeper.perms is actually an instance of PermissionMap , which is a subclass of dict with a few small changes, but you can treat it as a normal dictionary anyway. |
Using Permissions In Views¶
Now that we’ve got our permissions defined, we need to write views that actually use them. If you’ve already used Django’s built-in permission mechanism, Bridgekeeper integrates with that:
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from . import models
def shrubbery_edit(request, shrubbery_id):
shrubbery = get_object_or_404(models.Shrubbery, id=shrubbery_id)
if not request.user.has_perm('shrubberies.update_shrubbery', shrubbery):
raise Http404()
return TemplateResponse(request, 'shrubbery_edit.html', {
'shrubbery': shrubbery,
})
We can also check permissions directly through Bridgekeeper. Remember, bridgekeeper.perms
is more or less just a dict, so we can pull it out of there and call the rule’s check()
method:
from bridgekeeper import perms
def shrubbery_edit(request, shrubbery_id):
# ...
if not perms['shrubberies.update_shrubbery'].check(request.user, shrubbery):
raise Http404()
# ...
Note
If you use Django’s has_perm()
, like in our first example, Django will consult all of your authentication backends to check permissions. For instance, if you’ve assigned permissions to users in your database through Django’s built-in user_permissions
, they’ll be checked as well. Similarly, if you have a third-party authentication backend (e.g. for social media, LDAP or Active Directory integration) that provides some form of permission checking, that will be checked too.
If you use Bridgekeeper directly, like in our second example, only Bridgekeeper permissions will be checked; in most cases this is what you want.
Filtering QuerySets¶
If we’re displaying a list, we can also filter a QuerySet so that it only contains objects that the currently-logged-in user holds a certain permission on.
from bridgekeeper import perms
from django.core.paginator import Paginator
from django.template.response import TemplateResponse
from . import models
def shrubbery_list(request, shrubbery_id):
all_shrubberies = models.Shrubbery.objects.all()
shrubberies = perms['shrubberies.view_shrubbery'].filter(request.user, all_shrubberies)
# 'shrubberies' is just a regular queryset, so we can do anything
# we would do with a normal queryset; in this case, let's paginate it
paginator = Paginator(shrubberies, 10)
page = paginator.page(1)
return TemplateResponse(request, 'shrubbery_list.html', {
'paginator': paginator,
'page': page,
'shrubberies': page.object_list,
})
Class-Based Views¶
All of the examples we’ve used so far have been function-based views. Of course, everything that we’ve covered so far will work inside a class-based view, but Bridgekeeper also comes with a handy shortcut in the form of QuerySetPermissionMixin
.
from bridgekeeper.mixins import QuerySetPermissionMixin
from django.views.generic import ListView, UpdateView
from . import models
class ShrubberyListView(QuerySetPermissionMixin, ListView):
model = models.Shrubbery
permission_name = 'shrubberies.view_shrubbery'
class ShrubberyUpdateView(QuerySetPermissionMixin, UpdateView):
model = models.Shrubbery
permission_name = 'shrubberies.update_shrubbery'
That’s all there is to it; these two views will now only show shrubberies that the currently-logged-in user has permission to view.
What next?¶
That’s the end of the tutorial; you should now be able to get started modelling your permissions with Bridgekeeper now!
You can read about the other ways you can check permissions, including more convenience shortcuts you can enable and ways to check things like whether somebody could, hypothetically, have a permission in the Checking Permissions guide. Or, find out more detail about writing rules and permissions in the Writing Rules and Permissions guide.
If there’s something that you don’t understand after following through this tutorial, or that you think could be explained better, please file a documentation bug so that we can improve the docs for future users.
Writing Rules and Permissions¶
In Bridgekeeper, a rule is something that is given a user and a resource, and either allows or blocks access to the resource. Rules are instances of the Rule
class (or rather, subclasses of that class), and can be combined together into composite rules.
A Bridgekeeper permission consists of a name, usually conforming to Django permission name conventions e.g. shrubberies.update_shrubbery
, and a rule. Permissions are created by assigning a rule instance to a name in bridgekeeper.perms
, which acts like a dictionary:
from bridgekeeper.rules import Attribute, is_staff
from bridgekeeper import perms
perms['foo.update_widget'] = is_staff
The rules
module provides a range of pre-made rule instances as well as rule classes you can instantiate, as shown above. You can also combine rules using the &
(and), |
(or), and ~
(not) operators:
perms['foo.view_widget'] = is_staff | Attribute(
'company', lambda user: user.company)
Finally, if none of the built-in rules do what you want, you can subclass Rule
yourself and write your own.
Blanket Rules¶
We introduced what blanket rules are, as well as how to write a custom one, in the Blanket Rules section of the tutorial. There, we defined one rule for each role, but if we had more than two roles that might get a bit repetitive.
If you need your blanket rules to take arguments, the easiest way is to write a function that returns a rule, like so:
from bridgekeeper.rules import blanket_rule
def has_role(role):
def checker(user):
return user.profile.role == role
return blanket_rule(checker, repr_string=f"has_role({role!r})")
In this case, we’re using the optional repr_string
argument to override how the rule is displayed when debugging, so that we can see what the role
argument is. (We’re using PEP 498 f-strings here, which are supported in Python 3.6+, but you don’t have to.)
Checking Permissions¶
There are two ways to check which permissions a user has using Bridgekeeper.
- Use the methods on the
User
model, which consult Bridgekeeper via its integration into Django’s pluggable authorisation system. You can only make the types of checks Django has built-in support for this way, which means you can’t check against QuerySets. Also, if you have multiple different authorisation backends (including Django’s built inModelBackend
), these methods will consult all of them. - Check against permissions in Bridgekeeper directly. This is the only way to filter QuerySets according to a permission; this method always uses the permissions defined in Bridgekeeper as a single source of truth and does not consult other backends.
Checking Permissions on an Object¶
Given an instance of our Shrubbery
model called shrubbery
, and a User
instance user
, here’s how we’d check to see whether the user has permission to update it:
from bridgekeeper import perms
# through Django:
user.has_perm('shrubberies.update_shrubbery', obj=shrubbery)
# or through Bridgekeeper:
perms['shrubberies.update_shrubbery'].check(user, shrubbery)
Both of these expressions will return either True
or False
. Aside from the caveat described above regarding authorisation backends other than Bridgekeeper, these two calls are equivalent; in fact, when you call has_perm()
, Django will trigger a call to check()
under the hood.
Checking Permissions on a QuerySet¶
Of course, Bridgekeeper’s headline feature is that it works with QuerySets; given a user and a permission, it can filter down a QuerySet to only return instances for which the user holds the permission.
All we need to do is call filter()
instead of check()
, and pass it a QuerySet instead of a single model instance:
qs = models.Shrubbery.objects.all()
filtered_qs = perms['shrubberies.view_shrubbery'].filter(user, qs)
Bridgekeeper’s filter()
takes any QuerySet, and returns another normal QuerySet (it actually just calls the QuerySet’s filter()
method internally). This means you can call filter()
, exclude()
or order_by()
your QuerySet before you pass it in, or you can filter()
, exclude()
, order_by()
, slice or paginate the QuerySet that Bridgekeeper returns to you.
Checking Permissions For All Possible Instances¶
Django’s has_perm()
(and thus also Bridgekeeper’s check()
) allows supplying only a permission name, and not an object instance:
user.has_perm('shrubberies.view_shrubbery')
# or,
perms['shrubberies.view_shrubbery'].check(user)
Once again, these calls are equivalent, aside from the caveat described above regarding authorisation backends other than Bridgekeeper.
When you check permissions like this without supplying an instance, Bridgekeeper will return True
if and only if the user has that permission for every possible instance that could ever exist. (This is not the same thing as checking whether the user has the permission for every instance currently in the database; in fact, this check doesn’t actually hit the database at all.)
As an example of this, let’s say that the shrubberies.view_shrubbery
permission was defined to allow staff users access to all shrubberies, and everyone else access to shrubberies in their own branch:
perms['shrubberies.view_shrubbery'] = is_staff | Attribute(
'branch', lambda user: user.profile.branch,
)
In this case, the check would return True
for a staff user, since they will always have access to every possible shrubbery. It will return False
for a regular user, even if every shrubbery currently in the database belongs to their branch, because it is possible for a shrubbery to be created that belongs to a different branch, which they would then be blocked from editing.
Checking Permissions For Any Possible Instances¶
Bridgekeeper also provides a second method, is_possible_for()
, which is the opposite of the above behaviour, in a way:
perms['shrubberies.update_shrubbery'].is_possible_for(user)
This check will return True
if and only if the user could possibly have that permission for any possible instance that could exist. (Once again, this is not the same as checking whether the user has the permission for at least one instance currently in the database, and once again it doesn’t actually hit the database at all.)
As an example of this, let’s say that the shrubberies.view_shrubbery
permission was defined to allow only shrubbers to edit shrubberies inside their own branch, using the is_shrubber
rule we created in the Blanket Rules section of the tutorial and combining it with an Attribute
check:
perms['shrubberies.view_shrubbery'] = is_shrubber & Attribute(
'branch', lambda user: user.profile.branch,
)
In this case, the check will return False
for a user with the 'apprentice'
role, because only users with the 'shrubber'
role can access anything. It will always return True
for a shrubber, however, even if there are no shrubberies belonging to their branch currently in the database, beacuse it is possible for a shrubbery to exist that belongs to their branch, which they would then be allowed to edit.
Note
The behaviours in this section are effectively implemented by checking whether a permission is always allowed (in the case of check()
) or always denied (in the case of is_possible_for()
) due to the presence of blanket rules.
In normal use, these methods should always behave how you’d expect. However, if you create a combination of rules that just happens to be tautological for a particular user, Bridgekeeper isn’t clever enough to detect that.
This also means that the checks described in this section usually won’t need to hit the database.
has_module_perms()
¶
Bridgekeeper also supports Django’s has_module_perms()
method. The following call:
user.has_module_perms('shrubberies')
is equivalent to calling is_possible_for()
on every permission whose name begins with shrubberies.
, and returning True
if any one of them returns True
.
Permission Check Summary¶
Meaning | Django | Bridgekeeper |
---|---|---|
User has permission foo.bar
for object x |
u.has_perm('foo.bar', x) |
perms['foo.bar'].check(u, x) |
User has permission foo.bar
for all possible objects |
u.has_perm('foo.bar') |
perms['foo.bar'].check(u) |
It is possible for the user to
have permission foo.bar for
some object |
n/a | perms['foo.bar'].is_possible_for(u) |
It is possible for the user to
have some permission foo.*
for some object |
u.has_module_perms('foo') |
n/a |
Filter the queryset qs to
only the objects that the user
has permission foo.bar for |
n/a | perms['foo.bar'].filter(u, qs) |
Using permissions in views¶
Bridgekeeper provides a QuerySetPermissionMixin
, which will filter a view down to only objects that the currently logged-in user has access to. It works on ListView
, DetailView
, and most views that operate on the database except CreateView
, and is used like this:
from bridgekeeper.mixins import QuerySetPermissionMixin
class MyView(QuerySetPermissionMixin, DetailView):
permission_name = 'applicants.view_applicant'
model = Applicant
Caution
QuerySetPermissionMixin
will return 404 both for objects that don’t exist and objects the user can’t access. It might be tempting to try to distinguish between an the two, by returning e.g. 404 for the former and 403 for the latter. Generally, though, it’s desirable from a security perspective to not let the user tell the difference between these two cases unless you really need to.
If you’re concerned about users getting unexpected 404s when they try to access a page without being logged in, one alternative is to reword your 404.html
accordingly, or even embed a login form there if users aren’t logged in.
Bridgekeeper also provides CreatePermissionGuardMixin
, which will validate unsaved model instances in a CreateView
(or any subclass of ModelFormView
) against a given permission, and raise SuspiciousOperation
, thus preventing the call to .save()
, if it does not pass. It’s used like this:
from bridgekeeper.mixins import CreatePermissionGuardMixin
class MyView(CreatePermissionGuardMixin, CreateView):
permission_name = 'applicants.create_applicant'
model = Applicant
Note
Unlike QuerySetPermissionMixin
, CreatePermissionGuardMixin
is only a safety net; you still need to write your forms and views so that a user can’t create instances they shouldn’t be allowed to, but the mixin will protect you against logic errors in your code, possibly combined with malicious users.
Django REST Framework integration¶
Installation¶
If you want to use Django REST Framework and Bridgekeeper together, you’ll need to add the following to your settings.py
:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'bridgekeeper.rest_framework.RulePermissions',
),
'DEFAULT_FILTER_BACKENDS': ('bridgekeeper.rest_framework.RuleFilter',),
}
Warning
These settings only set the default permission classes and filter backends. If you override either permission_classes
or filter_backends
in any APIView
or ViewSet
subclass, you’ll need to make sure Bridgekeeper’s classes are included in those locations too.
Permission Naming¶
Once you’ve changed your settings, all of your API views will automatically apply the appropriate permissions. In order for them to do so, they need to be named according to the conventional Django permission naming scheme. Given a Django app called app_name
and a model called ModelName
, the following permissions will be checked:
app_name.view_modelname
for all requests.app_name.add_modelname
forPOST
requests.app_name.change_modelname
forPUT
andPATCH
requests.app_name.delete_modelname
forDELETE
requests.
One side-effect of this is that your API consumers will not be able to make changes if they have add
, change
or delete
permissions on some object but don’t also have view
permissions for that same object. That being said, it doesn’t make sense for a user to be able to change something they can’t see anyway.
Rules¶
Rule library that forms the core of Bridgekeeper.
This module defines the Rule
base class, as well as a
number of built-in rules.
The Rule
API¶
-
class
bridgekeeper.rules.
Rule
¶ Base class for rules.
All rules are instances of this class, but not directly; use (or write!) a subclass instead, as this class will raise
NotImplementedError
if you try to actually do anything with it.-
check
(user, instance=None)¶ Check if a user satisfies this rule.
Given a user, return a boolean indicating if that user satisfies this rule for a given instance, or if none is provided, every instance.
-
filter
(user, queryset)¶ Filter a queryset to instances that satisfy this rule.
Given a queryset and a user, this method will return a filtered queryset that contains only instances from the original queryset for which the user satisfies this rule.
Parameters: - queryset (django.db.models.QuerySet) – The initial queryset to filter
- user (django.contrib.auth.models.User) – The user to match against.
Returns: A filtered queryset
Return type: django.db.models.QuerySet
Warning
If you are subclassing this class, don’t override this method; override
query()
instead.
-
is_possible_for
(user)¶ Check if it is possible for a user to satisfy this rule.
Returns
True
if it is possible for an instance to exist for which the given user satisfies this rule,False
otherwise.For example, in a multi-tenanted app, you might have a rule that allows access to model instances if a user is a staff user, or if the instance’s tenant matches the user’s tenant.
In that case,
check()
, when called without an instance, would returnTrue
only for staff users (since only they can see every instance). This method would returnTrue
for all users, because every user could possibly see an instance (whether it’s one that exists currently in the database, or a hypothetical one that might in the future).Cases where this method would return
False
include where a user doesn’t have the right role or subscription plan to use a feature at all; this method is the single-permission equivalent of has-module-perms.
-
Built-in Blanket Rules¶
-
bridgekeeper.rules.
always_allow
¶ Rule that always allows access to everything.
-
bridgekeeper.rules.
always_deny
¶ Rule that never allows access to anything.
-
bridgekeeper.rules.
is_authenticated
¶ Rule that allows access to users for whom
is_authenticated
isTrue
.
-
bridgekeeper.rules.
is_superuser
¶ Rule that allows access to users for whom
is_superuser
isTrue
.
Rule Classes¶
-
class
bridgekeeper.rules.
Attribute
(attr, matches)¶ Rule class that checks the value of an instance attribute.
This rule is satisfied by model instances where the attribute given in
attr
matches the value given inmatches
.Parameters: - attr (str) – An attribute name to match against on the model instance.
- value – The value to match against, or a callable that takes a user and returns a value to match against.
For instance, if you had a model class
Widget
with an attributecolour
that was either'red'
,'green'
or'blue'
, you could limit access to blue widgets with the following:blue_widgets_only = Attribute('colour', matches='blue')
Restricting access in a multi-tenanted application by matching a model’s
tenant
to the user’s might look like this:applications_by_tenant = Attribute('tenant', lambda user: user.tenant)
Warning
This rule uses Python equality (
==
) when checking a retrieved Python object, but performs an equality check on the database when filtering a QuerySet. Avoid using it with imprecise types (e.g. floats), and ensure that you are using the correct Python type (e.g.decimal.Decimal
for decimals rather than floats or strings), to prevent inconsistencies.
-
class
bridgekeeper.rules.
Relation
(attr, rule)¶ Check that a rule applies to a ForeignKey.
Parameters: For example, given
Applicant
andApplication
models, to allow access to all applications to anyone who has permission to access the related applicant:perms['foo.view_application'] = Relation( 'applicant', perms['foo.view_applicant'])
-
class
bridgekeeper.rules.
ManyRelation
(query_attr, rule)¶ Check that a rule applies to a many-object relationship.
This can be used in a similar fashion to
Relation
, but across aManyToManyField
, or the remote end of aForeignKey
.Parameters: - query_attr (str) – Name of a many-object relationship to check. This
This is the name that you
use when filtering this relationship using
.filter()
. If you are on the side of the relationship where the field is defined, this is typically the lowercased model name (e.g.mymodel
on its own, notmymodel_set
), unless you’ve setrelated_name
orrelated_query_name
. - rule (Rule) – Rule to check the foreign object against.
For example, given
Agency
andCustomer
models, to allow agency users access only to customers that have a relationship with their agency:perms['foo.view_customer'] = ManyRelation( 'agency', Is(lambda user: user.agency))
- query_attr (str) – Name of a many-object relationship to check. This
This is the name that you
use when filtering this relationship using
-
class
bridgekeeper.rules.
Is
(instance)¶ Rule class that checks the identity of the instance.
This rule is satisfied only by a the provided model instance.
Parameters: instance – The instance to match against, or a callable that takes a user and returns a value to match against. For instance, if you only wanted a user to be able to update their own profile:
own_profile = Is(lambda user: user.profile)
-
class
bridgekeeper.rules.
In
(collection)¶ Rule class that checks the instance is a member of a collection.
This rule is satisfied only by model instances that are members of the provided collection.
Parameters: collection – The collection to match against, or a callable that takes a user and returns a value to match against. For instance, if you only wanted to match groups a user is in:
own_profile = Is(lambda user: user.profile)
Built-in rule instances¶
-
bridgekeeper.rules.
current_user
= Is(<function <lambda>>)¶ Rule class that checks the identity of the instance.
This rule is satisfied only by a the provided model instance.
Parameters: instance – The instance to match against, or a callable that takes a user and returns a value to match against. For instance, if you only wanted a user to be able to update their own profile:
own_profile = Is(lambda user: user.profile)
-
bridgekeeper.rules.
in_current_groups
= In(<function <lambda>>)¶ Rule class that checks the instance is a member of a collection.
This rule is satisfied only by model instances that are members of the provided collection.
Parameters: collection – The collection to match against, or a callable that takes a user and returns a value to match against. For instance, if you only wanted to match groups a user is in:
own_profile = Is(lambda user: user.profile)
Extension Points (For Writing Your Own Rule
Subclasses)¶
-
class
bridgekeeper.rules.
Rule
If you want to create your own rule class, these are the methods you need to override.
-
query
(user)¶ Generate a
Q
object.Note
This method is used internally by
filter()
; subclasses will need to override it but you should never need to call it directly.Given a user, return a
Q
object which will filter a queryset down to only instances for which the given user satisfies this rule.Alternatively, return
UNIVERSAL
if this user satisfies this rule for every possible object, orEMPTY
if this user cannot satisfy this rule for any possible object. (These two values are usually only returned in “blanket rules” which depend only on some property of the user, e.g. the built-inis_staff
, but these are usually best created with theblanket_rule
decorator.)Parameters: user (django.contrib.auth.models.User) – The user to match against. Returns: A query that will filter a queryset to match this rule. Return type: django.db.models.Q
-
check
(user, instance=None) Check if a user satisfies this rule.
Given a user, return a boolean indicating if that user satisfies this rule for a given instance, or if none is provided, every instance.
-
-
bridgekeeper.rules.
UNIVERSAL
= UNIVERSAL¶
-
bridgekeeper.rules.
EMPTY
= EMPTY¶
Convenience Helpers¶
QuerySet and Manager Classes¶
-
class
bridgekeeper.querysets.
PermissionQuerySet
(model=None, query=None, using=None, hints=None)¶ A QuerySet subclass that provides a convenience method.
-
visible_to
(user, permission)¶ Filter the QuerySet to objects a user has a permission for.
Parameters: - user (django.contrib.auth.models.User) – User to check permission against.
- permission (str) – Permission to check.
This method only works with permissions that are defined in
perms
; regular Django row-level permission checkers can’t be invoked on the QuerySet level.It is a convenience wrapper around
filter()
.
-
View Mixins¶
-
class
bridgekeeper.mixins.
CreatePermissionGuardMixin
(permission_map=None, *args, **kwargs)¶ A view that checks permissions before creating model instances.
Use this mixin with
CreateView
, and supply thepermission_name
of a Bridgekeeper permission. Your view will then do two things:- Check that it’s possible for a user to create any new instances
at all (i.e. that
is_possible_for()
returnsTrue
on the supplied permission). If not, the mixin raisesPermissionDenied
. - Just before the form is saved, checks the unsaved model instance
against the supplied permission; if it fails, the mixin raises
SuspiciousOperation
.
Note that unlike
QuerySetPermissionMixin
, this mixin won’t automatically apply permissions for you. Ideally, your view (or the form class your view uses) should make it impossible for users to create instances they’re not allowed to create; fields that must be set to a certain value should be set automatically and not displayed in the form, choice fields should have theirchoices
limited to only values the user is allowed to set, and so on.Bridgekeeper can’t (and arguably shouldn’t) reach into your form and modify it for you. Instead, this mixin provides a last line of defence; if your view has a bug where a user can create something they’re not allowed to, the mixin will prevent the object from actually being created, and crash loudly in a way that your error reporting systems can pick up, allowing you to fix the bug.
-
permission_name
¶ The name of the Bridgekeeper permission to check against, e.g.
'shrubberies.update_shrubbery'
.
- Check that it’s possible for a user to create any new instances
at all (i.e. that
-
class
bridgekeeper.mixins.
QuerySetPermissionMixin
(permission_map=None, *args, **kwargs)¶ View mixin that filters QuerySets according to a permission.
Use this mixin with any class-based view that expects a
get_queryset
method (e.g.ListView
,DetailView
,UpdateView
, or any other views that subclass fromMultipleObjectMixin
orSingleObjectMixin
), and supply apermission_name
attribute with the name of a Bridgekeeper permission.The view’s queryset will then be automatically filtered to objects that the user requesting the page has the supplied permission for. For multiple-object views like
ListView
, objects the user doesn’t have the permission for just won’t be in the list. For single-object views likeUpdateView
, attempts to access objects the user doesn’t have the permission for will just 404.-
permission_name
¶ The name of the Bridgekeeper permission to check against, e.g.
'shrubberies.update_shrubbery'
.
-
Django REST Framework integration¶
-
class
bridgekeeper.rest_framework.
BridgekeeperRESTMixin
¶ Mixin for Django REST Framework integration classes.
-
get_action
(request, view, obj=None)¶ Return the action that a particular request is performing.
Usually, this is one of
'view'
,'add'
,'change'
or'delete'
. This is used byget_permission_name()
to generate the name of the appropriate permission.Returns: Name of an action. Return type: str
-
get_operand_name
(request, view, obj=None)¶ Return the name of the thing that a request is acting on.
The default implementation works if
obj
is a model instance (when it is provided), or ifview
is a view that has either aqueryset
attribute orget_queryset()
method (otherwise).This is used by
get_permission_name()
to generate the name of the appropriate permission.Returns: A tuple in the form (app_label, operand_name). Return type: (str, str)
-
get_permission
(request, view, obj=None)¶ Return a rule object to check against for this request.
The default implementation just looks up the name returned by
get_permission_name()
.Returns: Rule object. Return type: bridgekeeper.rules.Rule
-
get_permission_name
(request, view, obj=None)¶ Return the name of the permission to use for a request.
The default implementation returns a name of the form
'{app_label}.{action}_{operand_name}'
, which will result in something like'shrubberies.view_shrubber'
or'shrubberies.delete_shrubbery'
.app_label
andoperand_name
are provided byget_operand_name()
, andaction
is provided byget_action()
, so if you need to override this behaviour, it may be easier to override those methods instead.Returns: Permission name. Return type: str
-
skip_permission_checks
(request, view, obj=None)¶ Skips all permission checks for certain requests.
The default implementation will skip permission checks for the
APIRootView
view class used by the built-inDefaultRouter
.Returns: Whether to skip permission checks for the given request. Return type: bool
-
-
class
bridgekeeper.rest_framework.
RuleFilter
¶ Django REST Framework filter class for Bridgekeeper.
This filter class doesn’t expect any client interaction or present any UI to the API explorer; it’s simply a mechanism for automatically filtering QuerySets according to Bridgekeeper permissions.
Note that this filter will always check the
view
permission; this means that if a particular user has permissions to edit but not view something, they’ll get 404s on everything. That said, it doesn’t make much sense for users to have edit but not view permissions on something anyway.
-
class
bridgekeeper.rest_framework.
RulePermissions
¶ Django REST Framework permission class for Bridgekeeper.
Note that this class does not, by itself, perform queryset filtering on list views, since Django REST Framework doesn’t provide an API for permission classes to do so.
Changelog¶
dev¶
- Breaking change:
Relation
andManyRelation
no longer require the model class on the other side of the relation to be passed in as an argument. - Breaking change:
ManyRelation
removes theattr
argument, requiring onlyquery_attr
. - Breaking change: Python 3.4 and Django 1.11 are no longer supported.
0.7¶
Add In
permission class, and two predefined rule instances, current_user
and in_current_groups
.
0.5¶
- Minor Django REST Framework-related fixes.
0.4¶
- Added initial support for Django REST Framework.
- Documentation improvements.
0.3¶
- Renamed predicates to rules, because the latter is a more accessible term that describe the concept just as well. Besides, “permissions are made up of rules” sounds a lot better than “permissions are made up of predicates”.
- Renamed ambient predicates to blanket rules, because it’s a more descriptive name. Note that the
@ambient
decorator is now called@blanket_rule
, because having a@blanket
decorator would be weird.
0.2¶
- Renamed
bridgekeeper.registry.registry
tobridgekeeper.perms
. - Renamed
bridgekeeper.predicates.Predicate.apply()
tocheck()
- Changed
bridgekeeper.predicates.Predicate.filter()
so that it takes the user object as the first argument, for consistency with the rest of the library (i.e. it’s singnature went fromfilter(queryset, user)
tofilter(user, queryset)
).