diff --git a/awx/api/generics.py b/awx/api/generics.py index 7d30b6eda5..5b990821b4 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -253,11 +253,7 @@ class ListAPIView(generics.ListAPIView, GenericAPIView): class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView): # Base class for a list view that allows creating new objects. - - def pre_save(self, obj): - super(ListCreateAPIView, self).pre_save(obj) - if isinstance(obj, PrimordialModel): - obj.created_by = self.request.user + pass class SubListAPIView(ListAPIView): # Base class for a read-only sublist view. diff --git a/awx/lib/site-packages/README b/awx/lib/site-packages/README index 817940e954..5c499e9e50 100644 --- a/awx/lib/site-packages/README +++ b/awx/lib/site-packages/README @@ -17,6 +17,7 @@ d2to1==0.2.11 (d2to1/*) distribute==0.7.3 (no files) django-auth-ldap==1.1.8 (django_auth_ldap/*) django-celery==3.1.10 (djcelery/*) +django-crum==0.6.1 (crum/*) django-extensions==1.3.3 (django_extensions/*) django-jsonfield==0.9.12 (jsonfield/*, minor fix in jsonfield/fields.py) django-polymorphic==0.5.3 (polymorphic/*) diff --git a/awx/lib/site-packages/crum/__init__.py b/awx/lib/site-packages/crum/__init__.py new file mode 100644 index 0000000000..fb26142466 --- /dev/null +++ b/awx/lib/site-packages/crum/__init__.py @@ -0,0 +1,87 @@ +# Python +import contextlib +import logging +import threading + +_thread_locals = threading.local() + +_logger = logging.getLogger('crum') + +__version__ = '0.6.1' + +__all__ = ['get_current_request', 'get_current_user', 'impersonate'] + + +@contextlib.contextmanager +def impersonate(user=None): + """Temporarily impersonate the given user for audit trails.""" + try: + current_user = get_current_user(_return_false=True) + set_current_user(user) + yield user + finally: + set_current_user(current_user) + + +def get_current_request(): + """Return the request associated with the current thread.""" + return getattr(_thread_locals, 'request', None) + + +def set_current_request(request=None): + """Update the request associated with the current thread.""" + _thread_locals.request = request + # Clear the current user if also clearing the request. + if not request: + set_current_user(False) + + +def get_current_user(_return_false=False): + """Return the user associated with the current request thread.""" + from crum.signals import current_user_getter + top_priority = -9999 + top_user = False if _return_false else None + results = current_user_getter.send_robust(get_current_user) + for receiver, response in results: + priority = 0 + if isinstance(response, Exception): + _logger.exception('%r raised exception: %s', receiver, response) + continue + elif isinstance(response, (tuple, list)) and response: + user = response[0] + if len(response) > 1: + priority = response[1] + elif response or response in (None, False): + user = response + else: + _logger.error('%r returned invalid response: %r', receiver, + response) + continue + if user is not False: + if priority > top_priority: + top_priority = priority + top_user = user + return top_user + + +def set_current_user(user=None): + """Update the user associated with the current request thread.""" + from crum.signals import current_user_setter + results = current_user_setter.send_robust(set_current_user, user=user) + for receiver, response in results: + if isinstance(response, Exception): + _logger.exception('%r raised exception: %s', receiver, response) + + +class CurrentRequestUserMiddleware(object): + """Middleware to capture the request and user from the current thread.""" + + def process_request(self, request): + set_current_request(request) + + def process_response(self, request, response): + set_current_request(None) + return response + + def process_exception(self, request, exception): + set_current_request(None) diff --git a/awx/lib/site-packages/crum/signals.py b/awx/lib/site-packages/crum/signals.py new file mode 100644 index 0000000000..6ca41a373f --- /dev/null +++ b/awx/lib/site-packages/crum/signals.py @@ -0,0 +1,44 @@ +# Django +from django.dispatch import Signal, receiver + +__all__ = ['current_user_getter'] + + +# Signal used when getting current user. Receivers should return a tuple of +# (user, priority). +current_user_getter = Signal(providing_args=[]) + + +# Signal used when setting current user. Receivers should store the current +# user as needed. Return values are ignored. +current_user_setter = Signal(providing_args=['user']) + + +@receiver(current_user_getter) +def _get_current_user_from_request(sender, **kwargs): + """Signal handler to retrieve current user from request.""" + from crum import get_current_request + return (getattr(get_current_request(), 'user', False), -10) + + +@receiver(current_user_getter) +def _get_current_user_from_thread_locals(sender, **kwargs): + """Signal handler to retrieve current user from thread locals.""" + from crum import _thread_locals + return (getattr(_thread_locals, 'user', False), 10) + + +@receiver(current_user_setter) +def _set_current_user_on_request(sender, **kwargs): + """Signal handler to store current user to request.""" + from crum import get_current_request + request = get_current_request() + if request: + request.user = kwargs['user'] + + +@receiver(current_user_setter) +def _set_current_user_on_thread_locals(sender, **kwargs): + """Signal handler to store current user on thread locals.""" + from crum import _thread_locals + _thread_locals.user = kwargs['user'] diff --git a/awx/main/models/base.py b/awx/main/models/base.py index b0047713dc..3acf82e820 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -27,6 +27,9 @@ from taggit.managers import TaggableManager # Django-Celery from djcelery.models import TaskMeta +# Django-CRUM +from crum import get_current_user + # Ansible Tower from awx.main.utils import encrypt_field @@ -284,6 +287,20 @@ class PrimordialModel(CreatedModifiedModel): self.save(update_fields=update_fields) return update_fields + def save(self, *args, **kwargs): + update_fields = kwargs.get('update_fields', []) + user = get_current_user() + if user and not user.pk: + user = None + if not self.pk: + self.created_by = user + if 'created_by' not in update_fields: + update_fields.append('created_by') + self.modified_by = user + if 'modified_by' not in update_fields: + update_fields.append('modified_by') + super(PrimordialModel, self).save(*args, **kwargs) + def clean_description(self): # Description should always be empty string, never null. return self.description or '' diff --git a/awx/main/signals.py b/awx/main/signals.py index a0fdd61c62..0c85a7bd90 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -12,6 +12,10 @@ from django.conf import settings from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed from django.dispatch import receiver +# Django-CRUM +from crum import get_current_request +from crum.signals import current_user_getter + # AWX from awx.main.models import * from awx.api.serializers import * @@ -361,3 +365,15 @@ def activity_stream_associate(sender, instance, **kwargs): activity_entry.save() getattr(activity_entry, object1).add(obj1) getattr(activity_entry, object2).add(obj2_actual) + + +@receiver(current_user_getter) +def get_current_user_from_drf_request(sender, **kwargs): + ''' + Provider a signal handler to return the current user from the current + request when using Django REST Framework. Requires that the APIView set + drf_request on the underlying Django Request object. + ''' + request = get_current_request() + drf_request = getattr(request, 'drf_request', None) + return (getattr(drf_request, 'user', False), 0) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index cce44e1735..de6b7c562d 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -112,7 +112,8 @@ TEMPLATE_CONTEXT_PROCESSORS += ( MIDDLEWARE_CLASSES += ( 'django.middleware.transaction.TransactionMiddleware', # Middleware loaded after this point will be subject to transactions. - 'awx.main.middleware.ActivityStreamMiddleware' + 'awx.main.middleware.ActivityStreamMiddleware', + 'crum.CurrentRequestUserMiddleware', ) TEMPLATE_DIRS = ( diff --git a/requirements/dev.txt b/requirements/dev.txt index 18c42eb86a..9ee9ccb69f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -12,6 +12,7 @@ Django>=1.4 #boto #django-auth-ldap #django-celery + #django-crum #django-extensions #django-jsonfield #django-split-settings diff --git a/requirements/dev_local.txt b/requirements/dev_local.txt index 7474588025..bdad46971b 100644 --- a/requirements/dev_local.txt +++ b/requirements/dev_local.txt @@ -62,6 +62,7 @@ Django-1.5.5.tar.gz #boto-2.27.0.tar.gz #django-auth-ldap-1.1.8.tar.gz #django-celery-3.1.10.tar.gz + #django-crum-0.6.1.tar.gz #django-extensions-1.3.3.tar.gz #django-jsonfield-0.9.12.tar.gz #django_polymorphic-0.5.3.tar.gz diff --git a/requirements/django-crum-0.6.1.tar.gz b/requirements/django-crum-0.6.1.tar.gz new file mode 100644 index 0000000000..e8191f83d3 Binary files /dev/null and b/requirements/django-crum-0.6.1.tar.gz differ diff --git a/requirements/prod.txt b/requirements/prod.txt index 7ae0f235cf..34cdf96ccd 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -9,6 +9,7 @@ Django>=1.4 #boto #django-auth-ldap #django-celery + #django-crum #django-extensions #django-jsonfield #django-polymorphic diff --git a/requirements/prod_local.txt b/requirements/prod_local.txt index 30ac8e3d07..14ad4be11f 100644 --- a/requirements/prod_local.txt +++ b/requirements/prod_local.txt @@ -60,6 +60,7 @@ Django-1.5.5.tar.gz #boto-2.27.0.tar.gz #django-auth-ldap-1.1.8.tar.gz #django-celery-3.1.10.tar.gz + #django-crum-0.6.1.tar.gz #django-extensions-1.3.3.tar.gz #django-jsonfield-0.9.12.tar.gz #django_polymorphic-0.5.3.tar.gz