From 118ddf97f6736f3ecae5fd10347daa64877487d3 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 1 Apr 2014 21:27:32 -0400 Subject: [PATCH] Get created_by and modified_by updating automatically. --- awx/api/generics.py | 6 +- awx/lib/site-packages/README | 1 + awx/lib/site-packages/crum/__init__.py | 87 +++++++++++++++++++++++++ awx/lib/site-packages/crum/signals.py | 44 +++++++++++++ awx/main/models/base.py | 17 +++++ awx/main/signals.py | 16 +++++ awx/settings/defaults.py | 3 +- requirements/dev.txt | 1 + requirements/dev_local.txt | 1 + requirements/django-crum-0.6.1.tar.gz | Bin 0 -> 4187 bytes requirements/prod.txt | 1 + requirements/prod_local.txt | 1 + 12 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 awx/lib/site-packages/crum/__init__.py create mode 100644 awx/lib/site-packages/crum/signals.py create mode 100644 requirements/django-crum-0.6.1.tar.gz 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 0000000000000000000000000000000000000000..e8191f83d3192a80389b2029fccf9521ea0a0fe7 GIT binary patch literal 4187 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 lRHZ6asY+F!>-m8$%y=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