-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
31456e3
Add permission labels (permission.name) when permissions are created.
powderflask 8913102
Add custom UserAdmin mixin to sync user's roles to user.groups
powderflask 1009050
Merge in tox config that resolved model mommy issues
powderflask e2a783c
PR #72 - improve django-admin integration, suggestion from @filipexim…
powderflask 27ecf53
DRY group and permission get_or_create
powderflask 845819e
Add docs for admin integration
powderflask File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
================= | ||
Admin Integration | ||
================= | ||
|
||
Use Django User Admin Site to manage roles and permissions interactively. | ||
|
||
|
||
Permission Names | ||
================ | ||
|
||
Permissions defined in ``roles.py`` are given 'human-friendly' names. | ||
|
||
All such permissions are assigned to the ``auth | user`` Content Type. | ||
|
||
Permission names are a Title Case version of the snake_case or camelCase permission codename, so... | ||
|
||
* ``create_medical_record`` is named ``auth | user | Create Medical Record`` | ||
* ``enterSurgery`` is named ``auth | user | Enter Surgery`` | ||
|
||
|
||
.. _rolepermissions-useradmin: | ||
|
||
RolePermissions User Admin | ||
========================== | ||
|
||
Assign / remove roles when editing Users in the Django User Admin Site. | ||
|
||
.. function:: RolePermissionsUserAdmin | ||
|
||
Custom ``django.contrib.auth.admin.UserAdmin`` that essentially adds the following logic: | ||
|
||
* ``remove_role(user, group)`` is called for each Group, removed via the Admin, that represents a role. | ||
* ``assign_role(user, group)`` is called for each Group, added via the Admin, that represents a role. | ||
|
||
Opt-in with ``setting``: :ref:`ROLEPERMISSIONS_REGISTER_ADMIN <register-user-admin-setting>` = True | ||
|
||
.. function:: RolePermissionsUserAdminMixin | ||
|
||
Mixin the functionality of ``RolePermissionsUserAdmin`` to your own custom ``UserAdmin`` class | ||
|
||
|
||
.. code-block:: python | ||
class MyCustomUserAdmin(RolePermissionsUserAdminMixin, django.contrib.auth.admin.UserAdmin): | ||
... | ||
.. warning:: ``remove_role`` removes every permission associated with a removed ``Group``, | ||
regardless of how those permissions were originally assigned. | ||
See :ref:`remove_role() <remove-role>` | ||
|
||
|
||
Management Commands | ||
=================== | ||
|
||
.. code-block:: shell | ||
django-admin sync_roles | ||
Ensures that ``django.contrib.auth.models`` ``Group`` and ``Permission`` objects exist | ||
for each role defined in ``roles.py`` | ||
|
||
This makes the roles and permissions defined in code immediately acccessible via the Django User Admin | ||
|
||
.. note:: ``sync_roles`` never deletes a ``Group`` or ``Permission``. | ||
|
||
If you remove a role or permission from ``roles.py``, the corresponding ``Group`` / ``Persission`` | ||
continues to exist until it is manually removed. | ||
|
||
.. code-block:: shell | ||
django-admin sync_roles --reset_user_permissions | ||
Additionally, update every User's permissions to ensure they include all those defined by their current roles. | ||
|
||
.. warning:: ``--reset_user_permissions`` is primarily intended for development, not production! | ||
|
||
Changing which permissions are associated with a role in ``roles.py`` does NOT change any User's actual permissions! | ||
``--reset_user_permissions`` simply clears each User's roles and then re-assign them. | ||
This guarantees that Users will have all permissions defined by their role(s) in ``roles.py``, | ||
but in no way does this imply that any permissions previously granted to the User have been revoked! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ Contents: | |
object_permissions | ||
utils | ||
views_utils | ||
admin | ||
settings | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,40 @@ | ||
from django.conf import settings | ||
from django.contrib import admin, auth | ||
from django.contrib.auth.models import Group | ||
from django.contrib.auth.admin import UserAdmin | ||
from rolepermissions import roles | ||
|
||
from django.contrib import admin | ||
ROLEPERMISSIONS_REGISTER_ADMIN = getattr(settings, 'ROLEPERMISSIONS_REGISTER_ADMIN', False) | ||
UserModel = auth.get_user_model() | ||
|
||
|
||
class RolePermissionsUserAdminMixin(object): | ||
""" Must be mixed in with an UserAdmin class""" | ||
def save_related(self, request, form, formsets, change): | ||
user = UserModel.objects.get(pk=form.instance.pk) | ||
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()) | ||
|
||
for role_name in (old_user_roles - new_user_groups): # roles removed from User's groups | ||
try: # put the recently removed group back, let rolepermissions remove it... | ||
group = Group.objects.get(name=role_name) | ||
user.groups.add(group) | ||
except Group.DoesNotExist: | ||
pass | ||
roles.remove_role(user, role_name) | ||
|
||
for group_name in (new_user_groups - old_user_roles): # groups potentially added to User's roles | ||
try: | ||
roles.assign_role(user, group_name) | ||
except roles.RoleDoesNotExist: | ||
pass | ||
|
||
|
||
class RolePermissionsUserAdmin(RolePermissionsUserAdminMixin, UserAdmin): | ||
pass | ||
|
||
if ROLEPERMISSIONS_REGISTER_ADMIN: | ||
admin.site.unregister(UserModel) | ||
admin.site.register(UserModel, RolePermissionsUserAdmin) |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
from django.conf import settings | ||
from django.core.management.base import BaseCommand | ||
from django.contrib.auth import get_user_model | ||
from rolepermissions import roles | ||
|
||
|
||
class Command(BaseCommand): | ||
ROLEPERMISSIONS_MODULE = getattr(settings, 'ROLEPERMISSIONS_MODULE', 'roles.py') | ||
help = "Synchronize auth Groups and Permissions with UserRoles defined in %s."%ROLEPERMISSIONS_MODULE | ||
version = "1.0.0" | ||
|
||
def get_version(self): | ||
return self.version | ||
|
||
def add_arguments(self, parser): | ||
# Optional argument | ||
parser.add_argument( | ||
'--reset_user_permissions', | ||
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 commentThe 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 = role.get_or_create_group() | ||
if created: | ||
self.stdout.write("Created Group: %s from Role: %s"%(group.name, role.get_name())) | ||
# Sync auth.Permission with permissions for this role | ||
role.get_default_true_permissions() | ||
|
||
if options.get('reset_user_permissions', False): # dj1.7 compat | ||
# Push any permission changes made to roles and remove any unregistered roles from all auth.Users | ||
self.stdout.write("Resetting permissions for ALL Users to defaults defined by roles.") | ||
|
||
for user in get_user_model().objects.all(): | ||
user_roles = roles.get_user_roles(user=user) | ||
roles.clear_roles(user=user) | ||
for role in user_roles: | ||
roles.assign_role(user=user, role=role) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
from collections import namedtuple | ||
from django.core.management import call_command | ||
from django.test import TestCase | ||
from django.utils.six import StringIO | ||
from django.contrib.auth import get_user_model | ||
from django.contrib.auth.models import Group, Permission | ||
|
||
from model_mommy import mommy | ||
|
||
from rolepermissions.roles import AbstractUserRole, get_user_roles | ||
from rolepermissions.admin import RolePermissionsUserAdminMixin | ||
|
||
|
||
class AdminRole1(AbstractUserRole): | ||
available_permissions = { | ||
'admin_perm1': True, | ||
'admin_perm2': False, | ||
} | ||
|
||
|
||
class UserAdminMixinTest(TestCase): | ||
class UserAdminMock: | ||
def save_related(self, request, form, formsets, change) : | ||
pass | ||
class CustomUserAdminMock(RolePermissionsUserAdminMixin, UserAdminMock): | ||
pass | ||
|
||
FormMock = namedtuple('FormMock', ['instance', ]) | ||
|
||
def setup(self): | ||
pass | ||
|
||
def test_admin_save_related_syncs_roles(self): | ||
user = mommy.make(get_user_model()) | ||
grp1 = mommy.make(Group) | ||
grp2 = mommy.make(Group, name=AdminRole1.get_name()) | ||
user.groups.add(grp1) | ||
user.groups.add(grp2) | ||
form = self.FormMock(instance=user) | ||
self.CustomUserAdminMock().save_related(None, form, None, None) | ||
user_roles = get_user_roles(user) | ||
self.assertNotIn(grp1.name, (role.get_name() for role in user_roles)) | ||
self.assertIn(AdminRole1, user_roles) | ||
|
||
|
||
class SyncRolesTest(TestCase): | ||
|
||
def setup(self): | ||
pass | ||
|
||
def test_sync_group(self): | ||
out = StringIO() | ||
call_command('sync_roles', stdout=out) | ||
self.assertIn('Created Group: %s'%AdminRole1.get_name(), out.getvalue()) | ||
group_names = [group['name'] for group in Group.objects.all().values('name')] | ||
self.assertIn(AdminRole1.get_name(), group_names) | ||
|
||
def test_sync_permissions(self): | ||
out = StringIO() | ||
call_command('sync_roles', stdout=out) | ||
permissions = [perm['codename'] for perm in Permission.objects.all().values('codename')] | ||
self.assertIn('admin_perm1', permissions) | ||
self.assertNotIn('admin_perm2', permissions) | ||
|
||
def test_sync_user_role_permissions(self): | ||
user = mommy.make(get_user_model()) | ||
grp1 = mommy.make(Group) | ||
grp2 = mommy.make(Group, name=AdminRole1.get_name()) | ||
user.groups.add(grp1) | ||
user.groups.add(grp2) | ||
out = StringIO() | ||
call_command('sync_roles', reset_user_permissions=True, stdout=out) | ||
|
||
user_group_names = [group['name'] for group in user.groups.all().values('name')] | ||
self.assertIn(grp1.name, user_group_names) | ||
self.assertIn(grp2.name, user_group_names) | ||
|
||
user_permission_names = [perm['codename'] for perm in user.user_permissions.all().values('codename')] | ||
self.assertIn('admin_perm1', user_permission_names) | ||
self.assertNotIn('admin_perm2', user_permission_names) | ||
|
||
def test_sync_preserves_groups(self): | ||
grp1 = mommy.make(Group) | ||
grp2 = mommy.make(Group, name=AdminRole1.get_name()) | ||
out = StringIO() | ||
call_command('sync_roles', stdout=out) | ||
group_names = [group['name'] for group in Group.objects.all().values('name')] | ||
self.assertIn(grp1.name, group_names) | ||
self.assertIn(grp2.name, group_names) | ||
|
||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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!