From 6182dad0d41a57a59beb930f93f598ff4cc563ed Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Wed, 6 Apr 2016 16:07:04 -0400 Subject: [PATCH 1/4] Added activity stream events for User Completes development for #1087 --- awx/api/views.py | 11 ++++++++++- awx/main/models/__init__.py | 1 + awx/main/utils.py | 30 ++++++++++++++++++++---------- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 57bc1657fa..6f6004d74e 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -11,6 +11,7 @@ import time import socket import sys import errno +import logging from base64 import b64encode from collections import OrderedDict @@ -22,7 +23,7 @@ from django.core.exceptions import FieldError from django.db.models import Q, Count from django.db import IntegrityError, transaction from django.shortcuts import get_object_or_404 -from django.utils.encoding import force_text +from django.utils.encoding import smart_text, force_text from django.utils.safestring import mark_safe from django.utils.timezone import now from django.views.decorators.csrf import csrf_exempt @@ -71,6 +72,8 @@ from awx.api.metadata import RoleMetadata from awx.main.utils import emit_websocket_notification from awx.main.conf import tower_settings +logger = logging.getLogger('awx.api.generics') + def api_exception_handler(exc, context): ''' Override default API exception handler to catch IntegrityError exceptions. @@ -528,9 +531,13 @@ class AuthTokenView(APIView): expires__gt=now(), reason='')[0] token.refresh() + if 'username' in request.data: + logger.info(smart_text(u"User {} logged in".format(request.data['username']))) except IndexError: token = AuthToken.objects.create(user=serializer.validated_data['user'], request_hash=request_hash) + if 'username' in request.data: + logger.info(smart_text(u"User {} logged in".format(request.data['username']))) # Get user un-expired tokens that are not invalidated that are # over the configured limit. # Mark them as invalid and inform the user @@ -549,6 +556,8 @@ class AuthTokenView(APIView): 'Auth-Token-Timeout': int(tower_settings.AUTH_TOKEN_EXPIRATION) } return Response({'token': token.key, 'expires': token.expires}, headers=headers) + if 'username' in request.data: + logger.warning(smart_text(u"Login failed for user {}".format(request.data['username']))) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class OrganizationList(ListCreateAPIView): diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index d0c62f19b5..847f52bb35 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -85,3 +85,4 @@ activity_stream_registrar.connect(TowerSettings) activity_stream_registrar.connect(Notifier) activity_stream_registrar.connect(Notification) activity_stream_registrar.connect(Label) +activity_stream_registrar.connect(User) diff --git a/awx/main/utils.py b/awx/main/utils.py index e3c0cde887..dea2155597 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -284,6 +284,22 @@ def update_scm_url(scm_type, url, username=True, password=True, return new_url + +def get_allowed_fields(obj, serializer_mapping): + from django.contrib.auth.models import User + + if serializer_mapping is not None and obj.__class__ in serializer_mapping: + serializer_actual = serializer_mapping[obj.__class__]() + allowed_fields = [x for x in serializer_actual.fields if not serializer_actual.fields[x].read_only] + ['id'] + else: + allowed_fields = [x.name for x in obj._meta.fields] + + if isinstance(obj, User): + field_blacklist = ['last_login'] + allowed_fields = [f for f in allowed_fields if f not in field_blacklist] + + return allowed_fields + def model_instance_diff(old, new, serializer_mapping=None): """ Calculate the differences between two model instances. One of the instances may be None (i.e., a newly @@ -301,11 +317,7 @@ def model_instance_diff(old, new, serializer_mapping=None): diff = {} - if serializer_mapping is not None and new.__class__ in serializer_mapping: - serializer_actual = serializer_mapping[new.__class__]() - allowed_fields = [x for x in serializer_actual.fields if not serializer_actual.fields[x].read_only] + ['id'] - else: - allowed_fields = [x.name for x in new._meta.fields] + allowed_fields = get_allowed_fields(new, serializer_mapping) for field in allowed_fields: old_value = getattr(old, field, None) @@ -334,11 +346,9 @@ def model_to_dict(obj, serializer_mapping=None): """ from awx.main.models.credential import Credential attr_d = {} - if serializer_mapping is not None and obj.__class__ in serializer_mapping: - serializer_actual = serializer_mapping[obj.__class__]() - allowed_fields = [x for x in serializer_actual.fields if not serializer_actual.fields[x].read_only] + ['id'] - else: - allowed_fields = [x.name for x in obj._meta.fields] + + allowed_fields = get_allowed_fields(obj, serializer_mapping) + for field in obj._meta.fields: if field.name not in allowed_fields: continue From f3cae7e1f01adfa94a6a124938e50fad346343da Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 11 Apr 2016 15:38:06 -0400 Subject: [PATCH 2/4] Log basic auth requests to the debug log Part of #1087 --- awx/api/authentication.py | 16 +++++++++++++++- awx/api/views.py | 2 +- awx/settings/defaults.py | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/awx/api/authentication.py b/awx/api/authentication.py index 300c5cfc65..c8143facbd 100644 --- a/awx/api/authentication.py +++ b/awx/api/authentication.py @@ -3,9 +3,11 @@ # Python import urllib +import logging # Django from django.utils.timezone import now as tz_now +from django.utils.encoding import smart_text # Django REST Framework from rest_framework import authentication @@ -16,6 +18,8 @@ from rest_framework import HTTP_HEADER_ENCODING from awx.main.models import UnifiedJob, AuthToken from awx.main.conf import tower_settings +logger = logging.getLogger('awx.api.authentication') + class TokenAuthentication(authentication.TokenAuthentication): ''' Custom token authentication using tokens that expire and are associated @@ -93,7 +97,7 @@ class TokenAuthentication(authentication.TokenAuthentication): if not token.in_valid_tokens(now=now): token.invalidate(reason='limit_reached') raise exceptions.AuthenticationFailed(AuthToken.reason_long('limit_reached')) - + # If the user is inactive, then return an error. if not token.user.is_active: raise exceptions.AuthenticationFailed('User inactive or deleted') @@ -116,6 +120,16 @@ class TokenGetAuthentication(TokenAuthentication): return super(TokenGetAuthentication, self).authenticate(request) +class LoggedBasicAuthentication(authentication.BasicAuthentication): + + def authenticate(self, request): + ret = super(LoggedBasicAuthentication, self).authenticate(request) + if ret: + username = ret[0].username if ret[0] else '' + logger.debug(smart_text(u"User {} performed a {} to {} through the API".format(username, request.method, request.path))) + return ret + + class TaskAuthentication(authentication.BaseAuthentication): ''' Custom authentication used for views accessed by the inventory and callback diff --git a/awx/api/views.py b/awx/api/views.py index 6f6004d74e..adb16289c1 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -72,7 +72,7 @@ from awx.api.metadata import RoleMetadata from awx.main.utils import emit_websocket_notification from awx.main.conf import tower_settings -logger = logging.getLogger('awx.api.generics') +logger = logging.getLogger('awx.api.views') def api_exception_handler(exc, context): ''' diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index d9cbbbf2b0..3d189f76ef 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -202,7 +202,7 @@ REST_FRAMEWORK = { 'PAGE_SIZE': 25, 'DEFAULT_AUTHENTICATION_CLASSES': ( 'awx.api.authentication.TokenAuthentication', - 'rest_framework.authentication.BasicAuthentication', + 'awx.api.authentication.LoggedBasicAuthentication', #'rest_framework.authentication.SessionAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( From 24a841a0bf2571eeeed6f00b0c0512e27bbe4543 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 11 Apr 2016 17:02:00 -0400 Subject: [PATCH 3/4] Added sso login logging Part of #1087 This is untested as we need to have a public facing machine to do SSO stuff against. --- awx/sso/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/awx/sso/views.py b/awx/sso/views.py index a75bcd96ae..2923bf5957 100644 --- a/awx/sso/views.py +++ b/awx/sso/views.py @@ -3,6 +3,7 @@ # Python import urllib +import logging # Django from django.core.urlresolvers import reverse @@ -10,6 +11,7 @@ from django.http import HttpResponse from django.utils.timezone import now, utc from django.views.generic import View from django.views.generic.base import RedirectView +from django.utils.encoding import smart_text # Django REST Framework from rest_framework.renderers import JSONRenderer @@ -18,12 +20,14 @@ from rest_framework.renderers import JSONRenderer from awx.main.models import AuthToken from awx.api.serializers import UserSerializer +logger = logging.getLogger('awx.sso.views') class BaseRedirectView(RedirectView): def get_redirect_url(self, *args, **kwargs): last_path = self.request.COOKIES.get('lastPath', '') last_path = urllib.quote(urllib.unquote(last_path).strip('"')) + logger.warning(smart_text(u"Redirecting invalid SSO login attempt".format(last_path))) url = reverse('ui:index') if last_path: return '%s#%s' % (url, last_path) @@ -45,9 +49,11 @@ class CompleteView(BaseRedirectView): request_hash=request_hash, expires__gt=now())[0] token.refresh() + logger.info(smart_text(u"User {} logged in".format(self.request.user.username))) except IndexError: token = AuthToken.objects.create(user=request.user, request_hash=request_hash) + logger.info(smart_text(u"User {} logged in".format(self.request.user.username))) request.session['auth_token_key'] = token.key token_key = urllib.quote('"%s"' % token.key) response.set_cookie('token', token_key) From ecedf491a4a8dd33d648111e0a64328f1f0aadef Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 11 Apr 2016 23:17:10 -0400 Subject: [PATCH 4/4] Removed erroneous sso login error log The log message here does not indicate a login failure at all, in fact it doesn't appear like we get a login failed message, they just don't get authed. --- awx/sso/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/sso/views.py b/awx/sso/views.py index 2923bf5957..962b89943e 100644 --- a/awx/sso/views.py +++ b/awx/sso/views.py @@ -27,7 +27,6 @@ class BaseRedirectView(RedirectView): def get_redirect_url(self, *args, **kwargs): last_path = self.request.COOKIES.get('lastPath', '') last_path = urllib.quote(urllib.unquote(last_path).strip('"')) - logger.warning(smart_text(u"Redirecting invalid SSO login attempt".format(last_path))) url = reverse('ui:index') if last_path: return '%s#%s' % (url, last_path)