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.change_shrubbery', obj=shrubbery)
# or through Bridgekeeper:
perms['shrubberies.change_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.change_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 |
|
|
User has permission |
|
|
It is possible for the user to
have permission |
n/a |
|
It is possible for the user to
have some permission |
|
n/a |
Filter the queryset |
n/a |
|
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.add_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.