I have a Django system that uses Guardian's User Object Permissions for access control. I want to write a function that finds all permissions across all content types associated with an "old" user and assign them to a "new" user. I cannot query UserObjectPermission
directly because our system uses direct foreign key models for performance reasons.
Here is an example of what I want
from django.contrib.auth.models import User
from guardian.shortcuts import get_objects_for_user, assign_perm
def update_permissions_for_user(old_user, new_user):
# Retrieve all USER permission objects for the old user
# this throws exception because it requires a perms param that I do not want to provide. I want all objects across all models
old_user_permissions = get_objects_for_user(old_user, use_groups=False, any_perm=False, with_superuser=False, accept_global_perms=False)
# Iterate through each permission object and update it to point to the new user
for permission in old_user_permissions:
assign_perm(permission.codename, new_user, permission)
remove_perm(permission.codename, old_user, permission)
# Example usage:
old_user = User.objects.get(username='old_username') # Replace 'old_username' with the actual username
new_user = User.objects.get(username='new_username') # Replace 'new_username' with the actual username
update_permissions_for_user(old_user, new_user)
If we were not using Direct Foreign Key models the code would simply be:
user_objects_permission_qs = UserObjectPermission.objects.filter(user=current_user)
affected_rows = user_objects_permission_qs.update(user=new_user)
However, the problem with the example of what I want is that get_objects_for_user
requires a perms
parameter. And, as per the Guardian documentation
If more than one permission is present within sequence, their content type must be the same or MixedContentTypeError exception would be raised.
So this cannot work for objects with different content types. Any suggestions? I also considered using get_user_perms but this also requires an object parameter.
Here is how we create direct foreign key models
class MyModelUserObjectPermission(UserObjectPermissionBase):
content_object = models.ForeignKey(MyModel, on_delete=models.CASCADE)
@willeM_ Van Onsem solution works great in SQLite but raises the following error in MySQL.
Additionally we had to check explicitly for UserObjectPermission class ( because for some models we’re still using this ) and treat this model differently due to it having a different schema
You can't specify target table 'tablename' for update in FROM clause"
We had to change ~Exists
to .exclude()
. In MySQL you cannot join against the table you're updating in the same statement
In this case, we can probably do this in "bulk" as in, one query per subclass of the UserObjectPermissionBase
. There is however a possible caveat: if we want to assign the permission to the new user, and the user already got that permission.
The query for single model would look like:
MyModelUserObjectPermission.objects.filter(user=old_user).update(user=new_user)
but this will fail if the permission already exists for the new user, because then there is a uniqness constraint that will fail.
We can however look before we leap [wiki], with:
from django.db.models import Exists, OuterRef
MyModelUserObjectPermission.objects.filter(
~Exists(
MyModelUserObjectPermission.objects.filter(
user=new_user,
content_object=OuterRef('content_object'),
permission=OuterRef('permission'),
)
),
user=old_user,
).update(user=new_user)
MyModelUserObjectPermission.objects.filter(user=old_user).delete()
This will thus first check if we can change it, so that there is no MyModelUserObjectPermission
for the same object, permission and the new user. If that is the case we update it. In a second query, we then remove the permissions from the old user that still were there.
Now the only thing we still need to do, is do this for all model, we can do this by looking for subclasses of the UserObjectPermissionBase
:
from django.db.models import Exists, OuterRef
from guardian.models.models import UserObjectPermissionBase
def change_perm_for_klass(klass, old_user, new_user):
klass.objects.filter(
~Exists(
klass.objects.filter(
user=new_user,
content_object=OuterRef('content_object'),
permission=OuterRef('permission'),
)
),
user=old_user,
).update(user=new_user)
klass.objects.filter(user=old_user).delete()
def update_permissions_for_user(old_user, new_user):
subclasses = set()
new_gen = {UserObjectPermissionBase}
while new_gen:
subclasses.update(new_gen)
new_gen = {sc for k in new_gen for sc in k.__subclasses__()}
for klass in subclasses:
if not klass._meta.abstract:
change_perm_for_klass(klass, old_user, new_user)
Here we thus first wall down the class hierarchies, to look for all subclasses of UserObjectPermissionBase
, next we will call change_perm_for_klass(…)
for all these models that are not abstract.
The logic of course only works if you implemented django-guardian
by defining subclasses of the UserObjectPermissionBase
.