mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 09:27:36 -02:30
Merge pull request #1217 from wwitzel3/rbac
Added user role creation, accessible methods, and fixed up m2m_update
This commit is contained in:
@@ -9,6 +9,8 @@ import logging
|
|||||||
# Django
|
# Django
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models.aggregates import Max
|
||||||
|
|
||||||
# Django REST Framework
|
# Django REST Framework
|
||||||
from rest_framework.exceptions import ParseError, PermissionDenied
|
from rest_framework.exceptions import ParseError, PermissionDenied
|
||||||
@@ -16,11 +18,13 @@ from rest_framework.exceptions import ParseError, PermissionDenied
|
|||||||
# AWX
|
# AWX
|
||||||
from awx.main.utils import * # noqa
|
from awx.main.utils import * # noqa
|
||||||
from awx.main.models import * # noqa
|
from awx.main.models import * # noqa
|
||||||
|
from awx.main.models.mixins import ResourceMixin
|
||||||
from awx.main.models.rbac import ALL_PERMISSIONS
|
from awx.main.models.rbac import ALL_PERMISSIONS
|
||||||
from awx.api.license import LicenseForbids
|
from awx.api.license import LicenseForbids
|
||||||
from awx.main.task_engine import TaskSerializer
|
from awx.main.task_engine import TaskSerializer
|
||||||
|
|
||||||
__all__ = ['get_user_queryset', 'check_user_access']
|
__all__ = ['get_user_queryset', 'check_user_access',
|
||||||
|
'user_accessible_objects', 'user_accessible_by']
|
||||||
|
|
||||||
PERMISSION_TYPES = [
|
PERMISSION_TYPES = [
|
||||||
PERM_INVENTORY_ADMIN,
|
PERM_INVENTORY_ADMIN,
|
||||||
@@ -71,6 +75,26 @@ def register_access(model_class, access_class):
|
|||||||
access_classes = access_registry.setdefault(model_class, [])
|
access_classes = access_registry.setdefault(model_class, [])
|
||||||
access_classes.append(access_class)
|
access_classes.append(access_class)
|
||||||
|
|
||||||
|
def user_accessible_objects(user, permissions):
|
||||||
|
content_type = ContentType.objects.get_for_model(User)
|
||||||
|
qs = RolePermission.objects.filter(
|
||||||
|
content_type=content_type,
|
||||||
|
role__ancestors__members=user
|
||||||
|
)
|
||||||
|
for perm in permissions:
|
||||||
|
qs = qs.annotate(**{'max_' + perm: Max(perm)})
|
||||||
|
qs = qs.filter(**{'max_' + perm: int(permissions[perm])})
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def user_accessible_by(instance, user, permissions):
|
||||||
|
perms = get_user_permissions_on_resource(instance, user)
|
||||||
|
if perms is None:
|
||||||
|
return False
|
||||||
|
for k in permissions:
|
||||||
|
if k not in perms or perms[k] < permissions[k]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def get_user_queryset(user, model_class):
|
def get_user_queryset(user, model_class):
|
||||||
'''
|
'''
|
||||||
Return a queryset for the given model_class containing only the instances
|
Return a queryset for the given model_class containing only the instances
|
||||||
|
|||||||
@@ -181,63 +181,49 @@ class ImplicitRoleField(models.ForeignKey):
|
|||||||
def bind_m2m_changed(self, _self, _role_class, cls):
|
def bind_m2m_changed(self, _self, _role_class, cls):
|
||||||
if not self.parent_role:
|
if not self.parent_role:
|
||||||
return
|
return
|
||||||
|
|
||||||
field_names = self.parent_role
|
field_names = self.parent_role
|
||||||
if type(field_names) is not list:
|
if type(field_names) is not list:
|
||||||
field_names = [field_names]
|
field_names = [field_names]
|
||||||
|
|
||||||
found_m2m_field = False
|
|
||||||
for field_name in field_names:
|
for field_name in field_names:
|
||||||
if field_name.startswith('singleton:'):
|
if field_name.startswith('singleton:'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
first_field_name = field_name.split('.')[0]
|
field_name, sep, field_attr = field_name.partition('.')
|
||||||
field = getattr(cls, first_field_name)
|
field = getattr(cls, field_name)
|
||||||
|
|
||||||
if type(field) is ReverseManyRelatedObjectsDescriptor or \
|
if type(field) is ReverseManyRelatedObjectsDescriptor or \
|
||||||
type(field) is ManyRelatedObjectsDescriptor:
|
type(field) is ManyRelatedObjectsDescriptor:
|
||||||
if found_m2m_field:
|
|
||||||
# This limitation is due to a lack of understanding on my part, the
|
if '.' in field_attr:
|
||||||
# trouble being that I can't seem to get m2m_changed to call anything that
|
|
||||||
# encapsulates what field we're working with. So the workaround that I've
|
|
||||||
# settled on for the time being is to simply iterate over the fields in the
|
|
||||||
# m2m_update and look for the m2m map and update accordingly. This solution
|
|
||||||
# is lame, I'd love for someone to show me the way to get the necessary
|
|
||||||
# state down to the m2m_update callback. - anoek 2016-02-10
|
|
||||||
raise Exception('Multiple ManyToMany fields not allowed in parent_role list')
|
|
||||||
if len(field_name.split('.')) != 2:
|
|
||||||
raise Exception('Referencing deep roles through ManyToMany fields is unsupported.')
|
raise Exception('Referencing deep roles through ManyToMany fields is unsupported.')
|
||||||
|
|
||||||
found_m2m_field = True
|
reverse = type(field) is ReverseManyRelatedObjectsDescriptor
|
||||||
self.m2m_field_name = first_field_name
|
if reverse:
|
||||||
self.m2m_field_attr = field_name.split('.',1)[1]
|
m2m_changed.connect(self.m2m_update(field_attr, reverse), field.through)
|
||||||
|
|
||||||
if type(field) is ReverseManyRelatedObjectsDescriptor:
|
|
||||||
m2m_changed.connect(self.m2m_update, field.through)
|
|
||||||
else:
|
else:
|
||||||
m2m_changed.connect(self.m2m_update_related, field.related.through)
|
m2m_changed.connect(self.m2m_update(field_attr, reverse), field.related.through)
|
||||||
|
|
||||||
|
def m2m_update(self, field_attr, _reverse):
|
||||||
|
def _m2m_update(self, sender, instance, action, reverse, model, pk_set, **kwargs):
|
||||||
|
if action == 'post_add' or action == 'pre_remove':
|
||||||
|
if _reverse:
|
||||||
|
for pk in pk_set:
|
||||||
|
obj = model.objects.get(pk=pk)
|
||||||
|
if action == 'post_add':
|
||||||
|
getattr(instance, self.name).children.add(getattr(obj, field_attr))
|
||||||
|
if action == 'pre_remove':
|
||||||
|
getattr(instance, self.name).children.remove(getattr(obj, field_attr))
|
||||||
|
|
||||||
def m2m_update_related(self, **kwargs):
|
else:
|
||||||
kwargs['reverse'] = not kwargs['reverse']
|
for pk in pk_set:
|
||||||
self.m2m_update(**kwargs)
|
obj = model.objects.get(pk=pk)
|
||||||
|
if action == 'post_add':
|
||||||
def m2m_update(self, sender, instance, action, reverse, model, pk_set, **kwargs):
|
getattr(instance, self.name).parents.add(getattr(obj, field_attr))
|
||||||
if action == 'post_add' or action == 'pre_remove':
|
if action == 'pre_remove':
|
||||||
if reverse:
|
getattr(instance, self.name).parents.remove(getattr(obj, field_attr))
|
||||||
for pk in pk_set:
|
return _m2m_update
|
||||||
obj = model.objects.get(pk=pk)
|
|
||||||
if action == 'post_add':
|
|
||||||
getattr(instance, self.name).children.add(getattr(obj, self.m2m_field_attr))
|
|
||||||
if action == 'pre_remove':
|
|
||||||
getattr(instance, self.name).children.remove(getattr(obj, self.m2m_field_attr))
|
|
||||||
|
|
||||||
else:
|
|
||||||
for pk in pk_set:
|
|
||||||
obj = model.objects.get(pk=pk)
|
|
||||||
if action == 'post_add':
|
|
||||||
getattr(instance, self.name).parents.add(getattr(obj, self.m2m_field_attr))
|
|
||||||
if action == 'pre_remove':
|
|
||||||
getattr(instance, self.name).parents.remove(getattr(obj, self.m2m_field_attr))
|
|
||||||
|
|
||||||
|
|
||||||
def _post_init(self, instance, *args, **kwargs):
|
def _post_init(self, instance, *args, **kwargs):
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ from awx.main.access import * # noqa
|
|||||||
|
|
||||||
User.add_to_class('get_queryset', get_user_queryset)
|
User.add_to_class('get_queryset', get_user_queryset)
|
||||||
User.add_to_class('can_access', check_user_access)
|
User.add_to_class('can_access', check_user_access)
|
||||||
|
User.add_to_class('accessible_by', user_accessible_by)
|
||||||
|
User.add_to_class('accessible_objects', user_accessible_objects)
|
||||||
|
|
||||||
# Import signal handlers only after models have been defined.
|
# Import signal handlers only after models have been defined.
|
||||||
import awx.main.signals # noqa
|
import awx.main.signals # noqa
|
||||||
|
|||||||
@@ -130,13 +130,22 @@ def sync_superuser_status_to_rbac(sender, instance, **kwargs):
|
|||||||
else:
|
else:
|
||||||
Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).members.remove(instance)
|
Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).members.remove(instance)
|
||||||
|
|
||||||
def create_user_resource(sender, **kwargs):
|
def create_user_role(sender, **kwargs):
|
||||||
instance = kwargs['instance']
|
instance = kwargs['instance']
|
||||||
try:
|
try:
|
||||||
UserResource.objects.get(user=instance)
|
Role.objects.get(content_type=ContentType.objects.get_for_model(User), object_id=instance.id)
|
||||||
except UserResource.DoesNotExist:
|
except Role.DoesNotExist:
|
||||||
ur = UserResource.objects.create(user=instance)
|
role = Role.objects.create(
|
||||||
ur.admin_role.members.add(instance)
|
singleton_name = '%s-admin_role' % instance.username,
|
||||||
|
content_object = instance,
|
||||||
|
)
|
||||||
|
role.members.add(instance)
|
||||||
|
RolePermission.objects.create(
|
||||||
|
role = role,
|
||||||
|
resource = instance,
|
||||||
|
create=1, read=1, write=1, delete=1, update=1,
|
||||||
|
execute=1, scm_update=1, use=1,
|
||||||
|
)
|
||||||
|
|
||||||
pre_save.connect(store_initial_active_state, sender=Host)
|
pre_save.connect(store_initial_active_state, sender=Host)
|
||||||
post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Host)
|
post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Host)
|
||||||
@@ -158,7 +167,7 @@ post_save.connect(emit_job_event_detail, sender=JobEvent)
|
|||||||
post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent)
|
post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent)
|
||||||
m2m_changed.connect(rebuild_role_ancestor_list, Role.parents.through)
|
m2m_changed.connect(rebuild_role_ancestor_list, Role.parents.through)
|
||||||
post_save.connect(sync_superuser_status_to_rbac, sender=User)
|
post_save.connect(sync_superuser_status_to_rbac, sender=User)
|
||||||
post_save.connect(create_user_resource, sender=User)
|
post_save.connect(create_user_role, sender=User)
|
||||||
|
|
||||||
# Migrate hosts, groups to parent group(s) whenever a group is deleted or
|
# Migrate hosts, groups to parent group(s) whenever a group is deleted or
|
||||||
# marked as inactive.
|
# marked as inactive.
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from awx.main.migrations import _rbac as rbac
|
|
||||||
from awx.main.models import Role
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from awx.main.migrations import _rbac as rbac
|
||||||
|
from awx.main.access import UserAccess
|
||||||
|
from awx.main.models import Role
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_user_admin(user_project, project, user):
|
def test_user_admin(user_project, project, user):
|
||||||
@@ -23,3 +26,21 @@ def test_user_admin(user_project, project, user):
|
|||||||
assert sa.members.filter(id=joe.id).exists() is False
|
assert sa.members.filter(id=joe.id).exists() is False
|
||||||
assert sa.members.filter(id=admin.id).exists() is True
|
assert sa.members.filter(id=admin.id).exists() is True
|
||||||
assert len(migrations) == 1
|
assert len(migrations) == 1
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_queryset(user):
|
||||||
|
u = user('pete', False)
|
||||||
|
|
||||||
|
access = UserAccess(u)
|
||||||
|
qs = access.get_queryset()
|
||||||
|
assert qs.count() == 1
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_accessible_by(user, organization):
|
||||||
|
admin = user('admin', False)
|
||||||
|
u = user('john', False)
|
||||||
|
|
||||||
|
organization.member_role.members.add(u)
|
||||||
|
organization.admin_role.members.add(admin)
|
||||||
|
|
||||||
|
assert User.accessible_objects(admin, {'read':True}).count() == 2
|
||||||
|
|||||||
Reference in New Issue
Block a user