Skip to content
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 6 commits into from
Dec 5, 2017
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion rolepermissions/admin.py
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())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool approach, liked it!


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.
43 changes: 43 additions & 0 deletions rolepermissions/management/commands/sync_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from django.conf import settings
from django.core.management.base import BaseCommand
from django.contrib.auth.models import Group
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 !!',
Copy link
Contributor

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!)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool!

for role in roles.RolesManager.get_roles() :
group, created = Group.objects.get_or_create(name=role.get_name())
Copy link
Contributor

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 Groups. We can call it here and here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Will do.

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)
6 changes: 2 additions & 4 deletions rolepermissions/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from rolepermissions.exceptions import (
RolePermissionScopeException, CheckerNotRegistered)
from rolepermissions.roles import get_user_roles
from rolepermissions.roles import get_user_roles, get_or_create_permission


class PermissionsManager(object):
Expand Down Expand Up @@ -37,9 +37,7 @@ def fuction_decorator(func):

def get_permission(permission_name):
"""Get a Permission object from a permission name."""
user_ct = ContentType.objects.get_for_model(get_user_model())
permission, _created = Permission.objects.get_or_create(
content_type=user_ct, codename=permission_name)
permission, created = get_or_create_permission(permission_name)

return permission

Expand Down
31 changes: 25 additions & 6 deletions rolepermissions/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType

from rolepermissions.utils import camelToSnake
from rolepermissions.utils import camelToSnake, camel_or_snake_to_title
from rolepermissions.exceptions import RoleDoesNotExist


Expand All @@ -29,6 +29,10 @@ def retrieve_role(cls, role_name):
def get_roles_names(cls):
return registered_roles.keys()

@classmethod
def get_roles(cls):
return registered_roles.values()


class RolesClassRegister(type):

Expand Down Expand Up @@ -158,11 +162,11 @@ def get_or_create_permissions(cls, permission_names):
permissions = list(Permission.objects.filter(
content_type=user_ct, codename__in=permission_names).all())

if len(permissions) != len(permission_names):
for permission_name in permission_names:
permission, created = Permission.objects.get_or_create(
content_type=user_ct, codename=permission_name)
if created:
missing_permissions = set(permission_names) - set((p.codename for p in permissions))
if len(missing_permissions) > 0:
for permission_name in missing_permissions:
permission, created = get_or_create_permission(permission_name)
if created: # assert created is True
permissions.append(permission)

return permissions
Expand All @@ -172,6 +176,21 @@ def get_default(cls, permission_name):
return cls.available_permissions[permission_name]


def get_or_create_permission(codename, name=camel_or_snake_to_title):
"""
Get a Permission object from a permission name.
@:param codename: permission code name
@:param name: human-readable permissions name (str) or callable that takes codename as argument and returns str
"""
user_ct = ContentType.objects.get_for_model(get_user_model())
# Careful here - don't use name to lookup existing permissions
try:
Copy link
Contributor

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?

Copy link
Contributor Author

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()

?

Copy link
Contributor

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})

Copy link
Contributor Author

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.

return Permission.objects.get(content_type=user_ct, codename=codename), False
except Permission.DoesNotExist:
perm_name = name(codename) if callable(name) else name
return Permission.objects.create(content_type=user_ct, codename=codename, name=perm_name), True


def retrieve_role(role_name):
"""Get a Role object from a role name."""
return RolesManager.retrieve_role(role_name)
Expand Down
91 changes: 91 additions & 0 deletions rolepermissions/tests/test_admin.py
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)


52 changes: 51 additions & 1 deletion rolepermissions/tests/test_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from model_mommy import mommy

from rolepermissions.roles import RolesManager, AbstractUserRole
from rolepermissions.roles import RolesManager, AbstractUserRole, get_or_create_permission


class RolRole1(AbstractUserRole):
Expand All @@ -29,6 +29,11 @@ class RolRole3(AbstractUserRole):
'permission6': False,
}

class RolRole4(AbstractUserRole):
available_permissions = {
'permission_number_7': True,
'PermissionNumber8': True,
}

class AbstractUserRoleTests(TestCase):

Expand Down Expand Up @@ -130,6 +135,18 @@ def test_permission_names_list(self):
self.assertIn('permission3', RolRole2.permission_names_list())
self.assertIn('permission4', RolRole2.permission_names_list())

def test_permission_labels(self):
user = mommy.make(get_user_model())

RolRole4.assign_role_to_user(user)
permissions = user.user_permissions.all()

permission_labels = [perm.name for perm in permissions]

self.assertIn('Permission Number 7', permission_labels)
self.assertIn('Permission Number8', permission_labels)
self.assertEquals(len(permissions), 2)


class RolesManagerTests(TestCase):

Expand All @@ -139,3 +156,36 @@ def setUp(self):
def test_retrieve_role(self):
self.assertEquals(RolesManager.retrieve_role('rol_role1'), RolRole1)
self.assertEquals(RolesManager.retrieve_role('rol_role2'), RolRole2)


class GetOrCreatePermissionsTests(TestCase):

def setUp(self):
pass

def test_create_default_named_permission(self):
perm_snake, _created = get_or_create_permission("my_perm_name1")
self.assertEqual(perm_snake.name, "My Perm Name1")

perm_camel, _created = get_or_create_permission("myPermName2")
self.assertEqual(perm_camel.name, "My Perm Name2")

def test_create_and_get_named_permission(self) :
perm1, _created = get_or_create_permission("my_perm_name", name="My Custom Name")
self.assertEqual(perm1.name, "My Custom Name")

perm2, _created = get_or_create_permission("my_perm_name", name="My Custom Name")
self.assertEqual(perm1, perm2)

def test_create_and_get_specialty_named_permission(self) :
def name_perm(codename):
return "Custom-"+codename
perm, _created = get_or_create_permission("my_perm_name", name_perm)
self.assertEqual(perm.name, "Custom-my_perm_name")

def test_backwards_compat_with_unnamed_permission(self) :
unnamed_perm, _created = get_or_create_permission("my_perm_name", name="")
self.assertEqual(unnamed_perm.name, "")

perm, _created = get_or_create_permission("my_perm_name")
self.assertEqual(unnamed_perm, perm)
22 changes: 22 additions & 0 deletions rolepermissions/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

from django.test import TestCase
from rolepermissions.utils import camelToSnake, snake_to_title, camel_or_snake_to_title


class UtilTests(TestCase):

def setUp(self):
pass

def test_camel_to_snake(self):
self.assertEqual(camelToSnake('camelCaseString'), 'camel_case_string')
self.assertEqual(camelToSnake('Snake_Camel_String'), 'snake__camel__string')

def test_snake_to_title(self):
self.assertEqual(snake_to_title('snake_case_string'), 'Snake Case String')
self.assertEqual(snake_to_title('Even_if__itsFunky'), 'Even If Itsfunky')

def test_camel_or_snake_to_title(self):
self.assertEqual(camel_or_snake_to_title('snake_case_string'), 'Snake Case String')
self.assertEqual(camel_or_snake_to_title('camelCaseString'), 'Camel Case String')
self.assertEqual(camel_or_snake_to_title('mix_itUp_WhyNot'), 'Mix It Up Why Not')
7 changes: 7 additions & 0 deletions rolepermissions/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,10 @@ def camelToSnake(s):

subbed = _underscorer1.sub(r'\1_\2', s)
return _underscorer2.sub(r'\1_\2', subbed).lower()


def snake_to_title(s) :
return ' '.join(x.capitalize() for x in s.split('_'))

def camel_or_snake_to_title(s):
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Good catch.

return snake_to_title(camelToSnake(s))