djangodjango-guardian

Reassign all Guardian User Object Permissions from one Django User to another Django User


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)

Post Script

@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


Solution

  • 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.