-
Notifications
You must be signed in to change notification settings - Fork 114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Manage roles and permissions via the django admin UserAdmin Groups and Permissions. #72
Conversation
I think this patch also resolves issue #46, although in a simplified form from what was suggested by @filipeximenes |
Add sync_roles management command to sync auth.Group to UserRoles
This PR now introduces 3 related features that allow role-permissions to play nice with the django UserAdmin:
(1) does not change API - it simply uses the permission codename to formulate a human-friendly label. Tests included for all 3 features. |
help = "Synchronize auth Groups and Permissions with UserRoles defined in %s."%ROLEPERMISSIONS_MODULE | ||
|
||
def handle(self, *args, **options): | ||
# Sync auth.Group with current registered roles (leaving existing groups intact!) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool!
role.get_default_true_permissions() | ||
|
||
# Push any permission changes made to roles and remove any unregistered roles from all auth.Users | ||
for user in UserModel.objects.all(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is dangerous. Id rather have this as an example script in the documentation. This is something you only want to run if you are absolutely sure about what the script is doing. It could go right after the docs for the command. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. I think you are just talking about the second part that syncs User permissions, right? Creating a Group for each defined Role so they are immediately available in the Admin seems harmless?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the first part is cool. Only worried about the second (from line 22 on).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pulled out the second part, made it an optional parameter:
sync_roles --reset_user_permissions
Willing to document this clearly.
rolepermissions/roles.py
Outdated
"""Get a Permission object from a permission name.""" | ||
user_ct = ContentType.objects.get_for_model(get_user_model()) | ||
return Permission.objects.get_or_create( | ||
content_type=user_ct, codename=permission_name, name=camel_or_snake_to_title(permission_name)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think name
would be better as a defaults
parameter. This way it would be possible to change it manually after creation without the risk of breaking the functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also write a test for this behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh! That's such a good idea! thanks.
Yes - sorry, didn't see that was missing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see -- this code would cause problems when looking up existing permissions whose name were set by some other method (or not at all, e.g.) Will fix and add test cases for backwards compat.
def snake_to_title(s) : | ||
return ' '.join(x.capitalize() for x in s.split('_')) | ||
|
||
def camel_or_snake_to_title(s): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you provide some tests for those functions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Good catch.
rolepermissions/admin.py
Outdated
# re-load and take a copy of user's newly saved roles | ||
user = UserModel.objects.get(pk=form.instance.pk) | ||
groups = list(user.groups.all()) | ||
# reset user's roles to match the list of groups just saved |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is dangerous because people might be managing permissions regardless of user roles. So this will make those individual changes to be lost. This is rather a "role reset" rather than a simple "sync". Would it be possible to overwrite the adding and removing of groups
and replace them with assign
and remove
role functions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, this will need a very descriptive documentation so people understand what is going on under the hood :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, some good docs are required for all 3 features in this PR :-)
The problem this feature is trying to address is:
- Edit / Create User in Admin; Assign the User to a Group
- Group Permissions (defined by Role in code) are not assigned to the User.
I have very limited experience with the use-cases for rolepermissions, but for my use-case, this is important. I'm not clear what you mean by
"Would it be possible to overwrite the adding and removing of groups and replace them with assign and remove role functions?"
Doesn't the code below use the roles functions? I'm happy to re-write this in any way that makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. Ok, so despite the ability to manage user permissions by assigning a role
to a user, it's also possible to manage permissions individually for a user regardless of its role
. Example:
class Nurse(AbstractUserRole):
available_permissions = {
'enter_surgery_room`: True,
}
In the common case Nurses
are permitted to enter the surgery room. But let's say we are instantiating a novice Nurse
we could do this:
new_nurse = User.objects.create_user(...)
assign_role(new_nurse, Nurse)
revoke_permission(new_nurse, 'enter_surgery_room')
This way the user still has a Nurse
role, but is not [yet] allowed in the surgery room.
Now, if you run this script in that situation, the novice nurse will end up with access to the surgery room.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clear -- thanks!
Yes - I can see my code is a like a sledge hammer where something more delicate is required. I'll give it another shot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to overwrite the adding and removing of groups and replace them with assign and remove role functions?
This was such a good suggestion @filipeximenes
Code does exactly this now - basically overrides the adding and removing of groups with logic that assigns and removes roles. Works very much as expected, even in tricky cases where roles with overlapping permissions are added/removed in the same operation.
Will push changes to the PR shortly. Thanks for the excellent code review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! There a are few minor comments. Let's add some docs and ship it!
old_user_roles = set(r.get_name() for r in roles.get_user_roles(user)) | ||
super(RolePermissionsUserAdminMixin, self).save_related(request, form, formsets, change) | ||
|
||
new_user_groups = set(g.name for g in user.groups.all()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool approach, liked it!
action='store_true', | ||
dest='reset_user_permissions', | ||
default=False, | ||
help='Re-assign all User roles -- resets user Permissions to defaults defined by role(s) !! CAUTION !!', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool!
def handle(self, *args, **options): | ||
# Sync auth.Group with current registered roles (leaving existing groups intact!) | ||
for role in roles.RolesManager.get_roles() : | ||
group, created = Group.objects.get_or_create(name=role.get_name()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's create a method in AbstractUserRole
to deal with the creation of Group
s. We can call it here and here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. Will do.
rolepermissions/roles.py
Outdated
""" | ||
user_ct = ContentType.objects.get_for_model(get_user_model()) | ||
# Careful here - don't use name to lookup existing permissions | ||
try: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use a get_or_create
with codename
in the defaults
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like...
perm, created = Permission.objects.get_or_create(content_type=user_ct, codename=codename)
if created:
perm.name=name(codename) if callable(name) else name
perm.save()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@powderflask get_or_create
allows a defaults
value that will to the trick. See here https://docs.djangoproject.com/en/1.11/ref/models/querysets/#get-or-create
name = name(codename) if callable(name) else name
perm, created = Permission.objects.get_or_create(
content_type=user_ct, codename=codename
defaults={'name': name})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha ha - new to me, go figure !! :-P so simple, duh...
return Permission.objects.get_or_create(content_type=user_ct, codename=codename, defaults={'name':name(codename) if callable(name) else name})
Thanks.
I think all* docs could fit under heading: Admin Integration
|
@powderflask 'Admin Integration' seems like a good place to have it! |
New Admin Integration section added to docs for these 3 new features. |
Back from vacations, looking good! Thanks very much! |
Thank you for all the attentive and helpful code review and suggestions - been fun to work on this. |
Happy to help! 😄 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
solid work
I can't seem to get anything change on Admin site, using a custom model so I sued RolesPermissionsUserAdminMixin to register the role, but can't see any difference. Do i still need to use 'ROLEPERMISSIONS_REGISTER_ADMIN = True' ? If i do I get 'django.contrib.admin.sites.NotRegistered: The model User is not registered' |
@breneser - the admin integration has no visible UI elements - it simply hooks role-permissions logic into existing admin actions for managing groups and perms. |
An alternate patch for #71
This patch abstracts the logic for creating permissions so it can be shared in both places that is done.
It also tries to create a human-friendly name from the permission codename.
The tests pass in all tox environments from py27-django17 on. Test for earlier tox envs seem to fail for unrelated reasons.