From f44a70548c12cf35b4a66036f44670aeaaba40bd Mon Sep 17 00:00:00 2001 From: Paul Marillonnet Date: Wed, 18 Jul 2018 17:51:22 +0200 Subject: [PATCH] support organizational unit automatic roles (#20690) --- src/authentic2/a2_rbac/models.py | 20 +++++++++++ src/authentic2/custom_user/models.py | 4 ++- src/authentic2/manager/user_views.py | 17 +++++++--- src/django_rbac/managers.py | 11 ++++-- tests/conftest.py | 16 +++++++++ tests/test_a2_rbac.py | 51 +++++++++++++++++++++++++++- 6 files changed, 109 insertions(+), 10 deletions(-) diff --git a/src/authentic2/a2_rbac/models.py b/src/authentic2/a2_rbac/models.py index dad83d1f..69e998c2 100644 --- a/src/authentic2/a2_rbac/models.py +++ b/src/authentic2/a2_rbac/models.py @@ -23,6 +23,11 @@ from authentic2.decorators import GlobalCache from . import managers, fields +class OUAutomaticRoles(models.Model): + ou = models.ForeignKey('OrganizationalUnit') + role = models.ForeignKey('Role') + + class OrganizationalUnit(OrganizationalUnitAbstractBase): username_is_unique = models.BooleanField( blank=True, @@ -40,6 +45,8 @@ class OrganizationalUnit(OrganizationalUnitAbstractBase): default=False, verbose_name=_('Validate emails')) + automatic_roles = models.ManyToManyField('Role', through=OUAutomaticRoles) + admin_perms = GenericRelation(rbac_utils.get_permission_model_name(), content_type_field='target_ct', object_id_field='target_id') @@ -55,6 +62,19 @@ class OrganizationalUnit(OrganizationalUnitAbstractBase): ('slug',), ) + def add_automatic_role(self, role): + if not OUAutomaticRoles.objects.filter(ou=self, role=role): + role = OUAutomaticRoles.objects.create(ou=self, role=role) + role.save() + + def delete_automatic_role(self, role): + for role in OUAutomaticRoles.objects.filter(ou=self, role=role): + role.delete() + + def clear_automatic_roles(self): + for role in OUAutomaticRoles.objects.filter(ou=self): + role.delete() + def clean(self): # if we set this ou as the default one, we must unset the other one if # there is diff --git a/src/authentic2/custom_user/models.py b/src/authentic2/custom_user/models.py index 068712a4..69a15a9c 100644 --- a/src/authentic2/custom_user/models.py +++ b/src/authentic2/custom_user/models.py @@ -150,7 +150,9 @@ class User(AbstractBaseUser, PermissionMixin): def roles_and_parents(self): qs1 = self.roles.all() qs2 = qs1.model.objects.filter(child_relation__child=qs1) - qs = (qs1 | qs2).order_by('name').distinct() + qs3 = qs1.model.objects.for_user(user=self) + qs = (qs1 | qs2).distinct() + qs = (qs | qs3).order_by('name').distinct() RoleParenting = get_role_parenting_model() rp_qs = RoleParenting.objects.filter(child=qs1) qs = qs.prefetch_related(models.Prefetch( diff --git a/src/authentic2/manager/user_views.py b/src/authentic2/manager/user_views.py index a1a907ba..22de3c68 100644 --- a/src/authentic2/manager/user_views.py +++ b/src/authentic2/manager/user_views.py @@ -415,9 +415,9 @@ class UserRolesView(HideOUColumnMixin, BaseSubTableView): def get_table_queryset(self): if self.is_ou_specified(): - roles = self.object.roles.all() - User = get_user_model() Role = get_role_model() + roles = Role.objects.for_user(self.object) + User = get_user_model() RoleParenting = get_role_parenting_model() rp_qs = RoleParenting.objects.filter(child=roles) qs = Role.objects.all() @@ -448,6 +448,7 @@ class UserRolesView(HideOUColumnMixin, BaseSubTableView): return redirect(request, 'a2-manager-user-detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): + Role = get_role_model() user = self.object role = form.cleaned_data['role'] action = form.cleaned_data['action'] @@ -463,9 +464,15 @@ class UserRolesView(HideOUColumnMixin, BaseSubTableView): hooks.call_hooks('event', name='manager-add-role-member', user=self.request.user, role=role, member=user) elif action == 'remove': - user.roles.remove(role) - hooks.call_hooks('event', name='manager-remove-role-member', user=self.request.user, - role=role, member=user) + if user.roles.filter(pk=role.pk): + user.roles.remove(role) + hooks.call_hooks('event', name='manager-remove-role-member', user=self.request.user, + role=role, member=user) + elif role in Role.objects.for_user(user): + messages.warning( + self.request, + _('User {user} has role {role} automatically obtained from OU {ou}.') + .format(user=user, role=role, ou=user.ou)) else: messages.warning(self.request, _('You are not authorized')) return super(UserRolesView, self).form_valid(form) diff --git a/src/django_rbac/managers.py b/src/django_rbac/managers.py index 589f13a7..ce1e2886 100644 --- a/src/django_rbac/managers.py +++ b/src/django_rbac/managers.py @@ -78,12 +78,14 @@ class PermissionQueryset(query.QuerySet): return self.by_target_ct(target).filter(target_id=target.pk) def for_user(self, user): - '''Retrieve all permissions hold by an user through its role and + '''Retrieve all permissions held by a user through its role and inherited roles. ''' Role = utils.get_role_model() roles = Role.objects.for_user(user=user) - return self.filter(roles=roles) + if user.ou: + roles = (roles | user.ou.automatic_roles.all().distinct()) + return self.filter(roles=roles.distinct()) def cleanup(self): count = 0 @@ -103,7 +105,10 @@ class IntCast(models.Func): class RoleQuerySet(query.QuerySet): def for_user(self, user): - return self.filter(members=user).parents().distinct() + roles = self.filter(members=user).parents().distinct() + if user.ou: + roles = (roles | user.ou.automatic_roles.all().distinct()) + return roles.distinct() def parents(self, include_self=True, annotate=False): qs = self.model.objects.filter(child_relation__child=self) diff --git a/tests/conftest.py b/tests/conftest.py index 3013379d..9962f326 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -97,6 +97,22 @@ def user_ou2(db, ou2): email='john.doe@example.net', ou=ou2) +@pytest.fixture +def agent_ou1(db, ou1, role_ou1): + user = create_user(username='john.doe', first_name=u'Jôhn', last_name=u'Dôe', + email='john.doe@example.net', ou=ou1) + user.roles.add(role_ou1) + return user + + +@pytest.fixture +def agent_ou2(db, ou2, role_ou2): + user = create_user(username='john.doe', first_name=u'Jôhn', last_name=u'Dôe', + email='john.doe@example.net', ou=ou2) + user.roles.add(role_ou2) + return user + + @pytest.fixture def admin_ou1(db, ou1): user = create_user(username='admin.ou1', first_name=u'Admin', last_name=u'OU1', diff --git a/tests/test_a2_rbac.py b/tests/test_a2_rbac.py index a5069603..5a53c596 100644 --- a/tests/test_a2_rbac.py +++ b/tests/test_a2_rbac.py @@ -1,7 +1,7 @@ import pytest from django.contrib.contenttypes.models import ContentType -from django_rbac.utils import get_permission_model +from django_rbac.utils import get_permission_model, get_role_model from django_rbac.models import Operation from authentic2.a2_rbac.models import Role, OrganizationalUnit as OU, RoleAttribute from authentic2.models import Service @@ -180,3 +180,52 @@ def test_ou_export_json(db): assert ou_dict['email_is_unique'] == ou.email_is_unique assert ou_dict['default'] == ou.default assert ou_dict['validate_emails'] == ou.validate_emails + + +def test_ou_automatic_roles_m2m_changed(db, ou_rando, role_ou1, role_ou2, simple_user): + Role = get_role_model() + + simple_user.ou = ou_rando + simple_user.save() + + assert role_ou1 not in Role.objects.for_user(simple_user) + assert role_ou2 not in Role.objects.for_user(simple_user) + + ou_rando.add_automatic_role(role_ou1) + ou_rando.add_automatic_role(role_ou2) + + assert role_ou1 in Role.objects.for_user(simple_user) + assert role_ou2 in Role.objects.for_user(simple_user) + + ou_rando.delete_automatic_role(role_ou1) + + assert role_ou1 not in Role.objects.for_user(simple_user) + + ou_rando.clear_automatic_roles() + + assert role_ou2 not in Role.objects.for_user(simple_user) + +def test_ou_automatic_roles_user_changed(db, user_ou1, user_ou2, role_ou1, role_ou2, ou1, ou2): + Role = get_role_model() + + for auto_role in ou1.automatic_roles.all(): + assert auto_role in Role.objects.for_user(user_ou1) + + user_ou1.ou = ou2 + user_ou1.save() + + for auto_role in ou2.automatic_roles.all(): + assert auto_role in Role.objects.for_user(user_ou1) + + +def test_ou_automatic_roles_no_sidefx(db, agent_ou1, role_ou1, ou1): + Role = get_role_model() + + assert role_ou1 in agent_ou1.roles.all() + + ou1.add_automatic_role(role_ou1) + ou1.save() + ou1.delete_automatic_role(role_ou1) + ou1.save() + + assert role_ou1 in agent_ou1.roles.all() -- 2.18.0