Add support for single-sign on using python-social-auth (with Google/Github OAuth2 and SAML support). Add support for RADIUS as another authentication backend.

This commit is contained in:
Chris Church
2015-10-02 14:57:27 -04:00
parent 2a7f1b7251
commit 2ba5e06e2c
23 changed files with 458 additions and 7 deletions

2
awx/sso/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.

90
awx/sso/middleware.py Normal file
View File

@@ -0,0 +1,90 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
# Python
import urllib
# Six
import six
# Django
from django.contrib.auth import logout
from django.shortcuts import redirect
from django.utils.timezone import now
# Python Social Auth
from social.exceptions import SocialAuthBaseException
from social.utils import social_logger
from social.apps.django_app.middleware import SocialAuthExceptionMiddleware
from awx.main.models import AuthToken
class SocialAuthMiddleware(SocialAuthExceptionMiddleware):
def process_request(self, request):
request.META['SERVER_PORT'] = 80 # FIXME
token_key = request.COOKIES.get('token', '')
token_key = urllib.quote(urllib.unquote(token_key).strip('"'))
if not hasattr(request, 'successful_authenticator'):
request.successful_authenticator = None
if not request.path.startswith('/sso/'):
# If token isn't present but we still have a user logged in via Django
# sessions, log them out.
if not token_key and request.user and request.user.is_authenticated():
logout(request)
# If a token is present, make sure it matches a valid one in the
# database, and log the user via Django session if necessary.
# Otherwise, log the user out via Django sessions.
elif token_key:
try:
auth_token = AuthToken.objects.filter(key=token_key, expires__gt=now())[0]
except IndexError:
auth_token = None
if not auth_token and request.user and request.user.is_authenticated():
logout(request)
elif auth_token and request.user != auth_token.user:
logout(request)
login(request, auth_token.user)
auth_token.refresh()
if auth_token and request.user and request.user.is_authenticated():
request.session.pop('social_auth_error', None)
def process_exception(self, request, exception):
strategy = getattr(request, 'social_strategy', None)
if strategy is None or self.raise_exception(request, exception):
return
if isinstance(exception, SocialAuthBaseException):
backend = getattr(request, 'backend', None)
backend_name = getattr(backend, 'name', 'unknown-backend')
full_backend_name = backend_name
try:
idp_name = strategy.request_data()['RelayState']
full_backend_name = '%s:%s' % (backend_name, idp_name)
except KeyError:
pass
message = self.get_message(request, exception)
social_logger.error(message)
url = self.get_redirect_uri(request, exception)
request.session['social_auth_error'] = (full_backend_name, message)
return redirect(url)
def get_message(self, request, exception):
msg = six.text_type(exception)
if msg and msg[-1] not in '.?!':
msg = msg + '.'
return msg
def get_redirect_uri(self, request, exception):
strategy = getattr(request, 'social_strategy', None)
return strategy.session_get('next', '') or strategy.setting('LOGIN_ERROR_URL')

2
awx/sso/models.py Normal file
View File

@@ -0,0 +1,2 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.

23
awx/sso/pipeline.py Normal file
View File

@@ -0,0 +1,23 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
# Python Social Auth
from social.exceptions import AuthException
class AuthInactive(AuthException):
"""Authentication for this user is forbidden"""
def __str__(self):
return 'Your account is inactive'
def set_is_active_for_new_user(strategy, details, user=None, *args, **kwargs):
if kwargs.get('is_new', False):
details['is_active'] = True
return {'details': details}
def prevent_inactive_login(backend, details, user=None, *args, **kwargs):
if user and not user.is_active:
raise AuthInactive(backend)

12
awx/sso/urls.py Normal file
View File

@@ -0,0 +1,12 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
# Django
from django.conf.urls import include, patterns, url
urlpatterns = patterns('awx.sso.views',
url(r'^complete/$', 'sso_complete', name='sso_complete'),
url(r'^error/$', 'sso_error', name='sso_error'),
url(r'^inactive/$', 'sso_inactive', name='sso_inactive'),
url(r'^metadata/saml/$', 'saml_metadata', name='saml_metadata'),
)

85
awx/sso/views.py Normal file
View File

@@ -0,0 +1,85 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
# Python
import urllib
# Django
from django.contrib.auth import logout as auth_logout
from django.core.urlresolvers import reverse
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
# Django REST Framework
from rest_framework.renderers import JSONRenderer
# Ansible Tower
from awx.main.models import AuthToken
from awx.api.serializers import UserSerializer
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('"'))
url = reverse('ui:index')
if last_path:
return '%s#%s' % (url, last_path)
else:
return url
sso_error = BaseRedirectView.as_view()
sso_inactive = BaseRedirectView.as_view()
class CompleteView(BaseRedirectView):
def dispatch(self, request, *args, **kwargs):
response = super(CompleteView, self).dispatch(request, *args, **kwargs)
if self.request.user and self.request.user.is_authenticated():
request_hash = AuthToken.get_request_hash(self.request)
try:
token = AuthToken.objects.filter(user=request.user,
request_hash=request_hash,
expires__gt=now())[0]
token.refresh()
except IndexError:
token = AuthToken.objects.create(user=request.user,
request_hash=request_hash)
request.session['auth_token_key'] = token.key
token_key = urllib.quote('"%s"' % token.key)
response.set_cookie('token', token_key)
token_expires = token.expires.astimezone(utc).strftime('%Y-%m-%dT%H:%M:%S')
token_expires = '%s.%03dZ' % (token_expires, token.expires.microsecond / 1000)
token_expires = urllib.quote('"%s"' % token_expires)
response.set_cookie('token_expires', token_expires)
response.set_cookie('userLoggedIn', 'true')
current_user = UserSerializer(self.request.user)
current_user = JSONRenderer().render(current_user.data)
current_user = urllib.quote('%s' % current_user, '')
response.set_cookie('current_user', current_user)
return response
sso_complete = CompleteView.as_view()
class MetadataView(View):
def get(self, request, *args, **kwargs):
from social.apps.django_app.utils import load_backend, load_strategy
complete_url = reverse('social:complete', args=('saml', ))
saml_backend = load_backend(
load_strategy(request),
'saml',
redirect_uri=complete_url,
)
metadata, errors = saml_backend.generate_metadata_xml()
if not errors:
return HttpResponse(content=metadata, content_type='text/xml')
else:
return HttpResponse(content=str(errors), content_type='text/plain')
saml_metadata = MetadataView.as_view()