From 180911dfa8ea26d6495a745339ae296bb12b79b9 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Wed, 24 Feb 2016 14:03:05 -0500 Subject: [PATCH 1/2] Added UserResource --- awx/main/migrations/0003_rbac_changes.py | 28 +++++++++++++++++ awx/main/migrations/_rbac.py | 5 +++ awx/main/models/__init__.py | 1 + awx/main/models/user.py | 30 ++++++++++++++++++ awx/main/signals.py | 31 ++++++++++++++++++- .../functional/test_rbac_userresource.py | 15 +++++++++ 6 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 awx/main/models/user.py create mode 100644 awx/main/tests/functional/test_rbac_userresource.py diff --git a/awx/main/migrations/0003_rbac_changes.py b/awx/main/migrations/0003_rbac_changes.py index 23aee5f92d..59468e2325 100644 --- a/awx/main/migrations/0003_rbac_changes.py +++ b/awx/main/migrations/0003_rbac_changes.py @@ -249,4 +249,32 @@ class Migration(migrations.Migration): name='resource', field=awx.main.fields.ImplicitResourceField(related_name='+', to='main.Resource', null=b'True'), ), + + migrations.CreateModel( + name='UserResource', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', models.DateTimeField(default=None, editable=False)), + ('modified', models.DateTimeField(default=None, editable=False)), + ('description', models.TextField(default=b'', blank=True)), + ('active', models.BooleanField(default=True, editable=False)), + ('name', models.CharField(max_length=512)), + ('admin_role', awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True')), + ('created_by', models.ForeignKey(related_name="{u'class': 'userresource', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)), + ('modified_by', models.ForeignKey(related_name="{u'class': 'userresource', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)), + ('resource', awx.main.fields.ImplicitResourceField(related_name='+', to='main.Resource', null=b'True')), + ('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')), + ('user', awx.main.fields.AutoOneToOneField(related_name='resource', editable=False, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'main_rbac_user_resource', + 'verbose_name': 'user_resource', + 'verbose_name_plural': 'user_resources', + }, + ), + migrations.AlterUniqueTogether( + name='userresource', + unique_together=set([('user', 'admin_role')]), + ), + ] diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 6b96bb943b..414a5009de 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -5,7 +5,12 @@ def migrate_users(apps, schema_editor): migrations = list() User = apps.get_model('auth', "User") Role = apps.get_model('main', "Role") + UserResource = apps.get_model('main', "UserResource") + for user in User.objects.all(): + ur = UserResource.objects.create(user=user) + ur.admin_role.members.add(user) + if user.is_superuser: Role.singleton('System Administrator').members.add(user) migrations.append(user) diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index fe505ff308..85476a19e7 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -18,6 +18,7 @@ from awx.main.models.activity_stream import * # noqa from awx.main.models.ha import * # noqa from awx.main.models.configuration import * # noqa from awx.main.models.rbac import * # noqa +from awx.main.models.user import * # noqa from awx.main.models.mixins import * # noqa # Monkeypatch Django serializer to ignore django-taggit fields (which break diff --git a/awx/main/models/user.py b/awx/main/models/user.py new file mode 100644 index 0000000000..c30696bdb1 --- /dev/null +++ b/awx/main/models/user.py @@ -0,0 +1,30 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved. + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from awx.main.models.base import CommonModelNameNotUnique +from awx.main.models.mixins import ResourceMixin +from awx.main.fields import AutoOneToOneField, ImplicitRoleField + + +class UserResource(CommonModelNameNotUnique, ResourceMixin): + class Meta: + app_label = 'main' + verbose_name = _('user_resource') + verbose_name_plural = _('user_resources') + unique_together = [('user', 'admin_role'),] + db_table = 'main_rbac_user_resource' + + user = AutoOneToOneField( + 'auth.User', + on_delete=models.CASCADE, + related_name='resource', + editable=False, + ) + + admin_role = ImplicitRoleField( + role_name='User Administrator', + permissions = {'all': True}, + ) diff --git a/awx/main/signals.py b/awx/main/signals.py index 15821e3e32..8de53ee4b6 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -147,6 +147,34 @@ def sync_user_to_team_members_role(sender, reverse, model, instance, pk_set, act instance.member_role.members.remove(user) +def sync_user_to_org_members_role(sender, reverse, model, instance, pk_set, action, **kwargs): + 'When a user is added or removed from Organization.users, ensure that is reflected in Organization.member_role' + if action == 'post_add' or action == 'pre_remove': + if reverse: + for org in Organization.objects.filter(id__in=pk_set).all(): + if action == 'post_add': + org.member_role.members.add(instance) + org.admin_role.children.add(instance.resource.admin_role) + if action == 'pre_remove': + org.member_role.members.remove(instance) + org.admin_role.children.remove(instance.resource.admin_role) + else: + for user in User.objects.filter(id__in=pk_set).all(): + if action == 'post_add': + instance.member_role.members.add(user) + instance.admin_role.children.add(user.resource.admin_role) + if action == 'pre_remove': + instance.member_role.members.remove(user) + instance.admin_role.children.remove(user.resource.admin_role) + +def create_user_resource(sender, **kwargs): + instance = kwargs['instance'] + try: + UserResource.objects.get(user=instance) + except UserResource.DoesNotExist: + ur = UserResource.objects.create(user=instance) + ur.admin_role.members.add(instance) + pre_save.connect(store_initial_active_state, sender=Host) post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Host) post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Host) @@ -168,7 +196,8 @@ post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent) m2m_changed.connect(rebuild_role_ancestor_list, Role.parents.through) post_save.connect(sync_superuser_status_to_rbac, sender=User) m2m_changed.connect(sync_user_to_team_members_role, Team.users.through) -#m2m_changed.connect(rebuild_group_parent_roles, Group.parents.through) +post_save.connect(create_user_resource, sender=User) +m2m_changed.connect(sync_user_to_org_members_role, Organization.users.through) # Migrate hosts, groups to parent group(s) whenever a group is deleted or # marked as inactive. diff --git a/awx/main/tests/functional/test_rbac_userresource.py b/awx/main/tests/functional/test_rbac_userresource.py new file mode 100644 index 0000000000..f666fb9eb0 --- /dev/null +++ b/awx/main/tests/functional/test_rbac_userresource.py @@ -0,0 +1,15 @@ +import pytest + +@pytest.mark.django_db +def test_user_resource_org_admin(user, organization): + admin = user('orgadmin') + member = user('orgmember') + + member.organizations.add(organization) + assert not member.resource.accessible_by(admin, {'write':True}) + + organization.admin_role.members.add(admin) + assert member.resource.accessible_by(admin, {'write':True}) + + organization.admin_role.members.remove(admin) + assert not member.resource.accessible_by(admin, {'write':True}) From 5eee8e3a84f2df42dc0864576e9a5823e0aafc1d Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Wed, 24 Feb 2016 14:58:13 -0500 Subject: [PATCH 2/2] Added RBAC sync for Organization.admins --- awx/main/signals.py | 16 ++++++++++ .../functional/test_rbac_userresource.py | 29 ++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/awx/main/signals.py b/awx/main/signals.py index 8de53ee4b6..6451da0fe6 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -146,6 +146,21 @@ def sync_user_to_team_members_role(sender, reverse, model, instance, pk_set, act if action == 'pre_remove': instance.member_role.members.remove(user) +def sync_admin_to_org_admin_role(sender, reverse, model, instance, pk_set, action, **kwargs): + 'When a user is added or removed from Organization.admins, ensure that is reflected in Organization.admin_role' + if action == 'post_add' or action == 'pre_remove': + if reverse: + for org in Organization.objects.filter(id__in=pk_set).all(): + if action == 'post_add': + org.admin_role.members.add(instance) + if action == 'pre_remove': + org.admin_role.members.remove(instance) + else: + for user in User.objects.filter(id__in=pk_set).all(): + if action == 'post_add': + instance.admin_role.members.add(user) + if action == 'pre_remove': + instance.admin_role.members.remove(user) def sync_user_to_org_members_role(sender, reverse, model, instance, pk_set, action, **kwargs): 'When a user is added or removed from Organization.users, ensure that is reflected in Organization.member_role' @@ -198,6 +213,7 @@ post_save.connect(sync_superuser_status_to_rbac, sender=User) m2m_changed.connect(sync_user_to_team_members_role, Team.users.through) post_save.connect(create_user_resource, sender=User) m2m_changed.connect(sync_user_to_org_members_role, Organization.users.through) +m2m_changed.connect(sync_admin_to_org_admin_role, Organization.admins.through) # Migrate hosts, groups to parent group(s) whenever a group is deleted or # marked as inactive. diff --git a/awx/main/tests/functional/test_rbac_userresource.py b/awx/main/tests/functional/test_rbac_userresource.py index f666fb9eb0..f5e83438df 100644 --- a/awx/main/tests/functional/test_rbac_userresource.py +++ b/awx/main/tests/functional/test_rbac_userresource.py @@ -1,7 +1,7 @@ import pytest @pytest.mark.django_db -def test_user_resource_org_admin(user, organization): +def test_user_org_admin(user, organization): admin = user('orgadmin') member = user('orgmember') @@ -13,3 +13,30 @@ def test_user_resource_org_admin(user, organization): organization.admin_role.members.remove(admin) assert not member.resource.accessible_by(admin, {'write':True}) + +@pytest.mark.django_db +def test_org_user_admin(user, organization): + admin = user('orgadmin') + member = user('orgmember') + + organization.users.add(member) + assert not member.resource.accessible_by(admin, {'write':True}) + + organization.admins.add(admin) + assert member.resource.accessible_by(admin, {'write':True}) + + organization.admins.remove(admin) + assert not member.resource.accessible_by(admin, {'write':True}) + +@pytest.mark.django_db +def test_org_user_removed(user, organization): + admin = user('orgadmin') + member = user('orgmember') + + organization.admins.add(admin) + organization.users.add(member) + + assert member.resource.accessible_by(admin, {'write':True}) + + organization.users.remove(member) + assert not member.resource.accessible_by(admin, {'write':True})