1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 16:51:11 +03:00

Merge pull request #459 from cchurch/slammin-saml

OAuth2/SAML/RADIUS Authentication
This commit is contained in:
Chris Church 2015-10-02 17:29:45 -04:00
commit 8d2f6eaf06
22 changed files with 463 additions and 16 deletions

View File

@ -1,6 +1,9 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
# Python
import urllib
# Django
from django.utils.timezone import now as tz_now
from django.conf import settings
@ -30,6 +33,14 @@ class TokenAuthentication(authentication.TokenAuthentication):
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth
@staticmethod
def _get_auth_token_cookie(request):
token = request.COOKIES.get('token', '')
if token:
token = urllib.unquote(token).strip('"')
return 'token %s' % token
return ''
def authenticate(self, request):
self.request = request
@ -40,7 +51,9 @@ class TokenAuthentication(authentication.TokenAuthentication):
if not auth or auth[0].lower() != 'token':
auth = authentication.get_authorization_header(request).split()
if not auth or auth[0].lower() != 'token':
return None
auth = TokenAuthentication._get_auth_token_cookie(request).split()
if not auth or auth[0].lower() != 'token':
return None
if len(auth) == 1:
msg = 'Invalid token header. No credentials provided.'

View File

@ -142,6 +142,8 @@ class APIView(views.APIView):
'new_in_200': getattr(self, 'new_in_200', False),
'new_in_210': getattr(self, 'new_in_210', False),
'new_in_220': getattr(self, 'new_in_220', False),
'new_in_230': getattr(self, 'new_in_230', False),
'new_in_240': getattr(self, 'new_in_240', False),
}
def get_description(self, html=False):
@ -158,7 +160,7 @@ class APIView(views.APIView):
'''
ret = super(APIView, self).metadata(request)
added_in_version = '1.2'
for version in ('2.2.0', '2.1.0', '2.0.0', '1.4.8', '1.4.5', '1.4', '1.3'):
for version in ('2.4.0', '2.3.0', '2.2.0', '2.1.0', '2.0.0', '1.4.8', '1.4.5', '1.4', '1.3'):
if getattr(self, 'new_in_%s' % version.replace('.', ''), False):
added_in_version = version
break

View File

@ -601,6 +601,8 @@ class UserSerializer(BaseSerializer):
ret = super(UserSerializer, self).to_native(obj)
ret.pop('password', None)
ret.fields.pop('password', None)
if obj:
ret['auth'] = obj.social_auth.values('provider', 'uid')
return ret
def get_validation_exclusions(self):

View File

@ -3,4 +3,6 @@
{% if new_in_145 %}> _Added in Ansible Tower 1.4.5_{% endif %}
{% if new_in_148 %}> _Added in Ansible Tower 1.4.8_{% endif %}
{% if new_in_200 %}> _New in Ansible Tower 2.0.0_{% endif %}
{% if new_in_220 %}> _New in Ansible Tower 2.2.0_{% endif %}
{% if new_in_220 %}> _New in Ansible Tower 2.2.0_{% endif %}
{% if new_in_230 %}> _New in Ansible Tower 2.3.0_{% endif %}
{% if new_in_240 %}> _New in Ansible Tower 2.4.0_{% endif %}

View File

@ -224,6 +224,7 @@ v1_urls = patterns('awx.api.views',
url(r'^$', 'api_v1_root_view'),
url(r'^ping/$', 'api_v1_ping_view'),
url(r'^config/$', 'api_v1_config_view'),
url(r'^auth/$', 'auth_view'),
url(r'^authtoken/$', 'auth_token_view'),
url(r'^me/$', 'user_me_list'),
url(r'^dashboard/$', 'dashboard_view'),

View File

@ -47,6 +47,9 @@ import qsstats
# ANSIConv
import ansiconv
# Python Social Auth
from social.backends.utils import load_backends
# AWX
from awx.main.task_engine import TaskSerializer, TASK_FILE, TEMPORARY_TASK_FILE
from awx.main.tasks import mongodb_control
@ -514,6 +517,37 @@ class ScheduleUnifiedJobsList(SubListAPIView):
view_name = 'Schedule Jobs List'
new_in_148 = True
class AuthView(APIView):
authentication_classes = []
permission_classes = (AllowAny,)
new_in_240 = True
def get(self, request):
data = SortedDict()
err_backend, err_message = request.session.get('social_auth_error', (None, None))
for name, backend in load_backends(settings.AUTHENTICATION_BACKENDS).items():
login_url = reverse('social:begin', args=(name,))
complete_url = request.build_absolute_uri(reverse('social:complete', args=(name,)))
backend_data = {
'login_url': login_url,
'complete_url': complete_url,
}
if name == 'saml':
backend_data['metadata_url'] = reverse('sso:saml_metadata')
for idp in settings.SOCIAL_AUTH_SAML_ENABLED_IDPS.keys():
saml_backend_data = dict(backend_data.items())
saml_backend_data['login_url'] = '%s?idp=%s' % (login_url, idp)
full_backend_name = '%s:%s' % (name, idp)
if err_backend == full_backend_name and err_message:
saml_backend_data['error'] = err_message
data[full_backend_name] = saml_backend_data
else:
if err_backend == name and err_message:
backend_data['error'] = err_message
data[name] = backend_data
return Response(data)
class AuthTokenView(APIView):
authentication_classes = []

View File

@ -118,15 +118,30 @@ REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST']
STDOUT_MAX_BYTES_DISPLAY = 1048576
TEMPLATE_CONTEXT_PROCESSORS += ( # NOQA
TEMPLATE_CONTEXT_PROCESSORS = ( # NOQA
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.i18n',
'django.core.context_processors.media',
'django.core.context_processors.static',
'django.core.context_processors.tz',
'django.contrib.messages.context_processors.messages',
'django.core.context_processors.request',
'awx.ui.context_processors.settings',
'awx.ui.context_processors.version',
'social.apps.django_app.context_processors.backends',
'social.apps.django_app.context_processors.login_redirect',
)
MIDDLEWARE_CLASSES += ( # NOQA
MIDDLEWARE_CLASSES = ( # NOQA
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'awx.main.middleware.HAMiddleware',
'awx.main.middleware.ActivityStreamMiddleware',
'awx.sso.middleware.SocialAuthMiddleware',
'crum.CurrentRequestUserMiddleware',
'awx.main.middleware.AuthTokenTimeoutMiddleware',
)
@ -160,10 +175,12 @@ INSTALLED_APPS = (
'kombu.transport.django',
'polymorphic',
'taggit',
'social.apps.django_app.default',
'awx.main',
'awx.api',
'awx.ui',
'awx.fact',
'awx.sso',
)
INTERNAL_IPS = ('127.0.0.1',)
@ -201,12 +218,23 @@ REST_FRAMEWORK = {
AUTHENTICATION_BACKENDS = (
'awx.main.backend.LDAPBackend',
'radiusauth.backends.RADIUSBackend',
'social.backends.google.GoogleOAuth2',
'social.backends.github.GithubOAuth2',
'social.backends.github.GithubOrganizationOAuth2',
'social.backends.github.GithubTeamOAuth2',
'social.backends.saml.SAMLAuth',
'django.contrib.auth.backends.ModelBackend',
)
# LDAP server (default to None to skip using LDAP authentication).
AUTH_LDAP_SERVER_URI = None
# Radius server settings (default to empty string to skip using Radius auth).
RADIUS_SERVER = ''
RADIUS_PORT = 1812
RADIUS_SECRET = ''
# Seconds before auth tokens expire.
AUTH_TOKEN_EXPIRATION = 1800
@ -312,6 +340,62 @@ CELERYBEAT_SCHEDULE = {
},
}
# Social Auth configuration.
SOCIAL_AUTH_STRATEGY = 'social.strategies.django_strategy.DjangoStrategy'
SOCIAL_AUTH_STORAGE = 'social.apps.django_app.default.models.DjangoStorage'
SOCIAL_AUTH_USER_MODEL = AUTH_USER_MODEL # noqa
SOCIAL_AUTH_PIPELINE = (
'social.pipeline.social_auth.social_details',
'social.pipeline.social_auth.social_uid',
'social.pipeline.social_auth.auth_allowed',
'social.pipeline.social_auth.social_user',
'social.pipeline.user.get_username',
'social.pipeline.social_auth.associate_by_email',
'social.pipeline.mail.mail_validation',
'social.pipeline.user.create_user',
'social.pipeline.social_auth.associate_user',
'social.pipeline.social_auth.load_extra_data',
'awx.sso.pipeline.set_is_active_for_new_user',
'social.pipeline.user.user_details',
'awx.sso.pipeline.prevent_inactive_login',
)
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = ''
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = ''
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['profile']
SOCIAL_AUTH_GITHUB_KEY = ''
SOCIAL_AUTH_GITHUB_SECRET = ''
SOCIAL_AUTH_GITHUB_ORG_KEY = ''
SOCIAL_AUTH_GITHUB_ORG_SECRET = ''
SOCIAL_AUTH_GITHUB_ORG_NAME = ''
SOCIAL_AUTH_GITHUB_TEAM_KEY = ''
SOCIAL_AUTH_GITHUB_TEAM_SECRET = ''
SOCIAL_AUTH_GITHUB_TEAM_ID = ''
SOCIAL_AUTH_SAML_SP_ENTITY_ID = ''
SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = ''
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = ''
SOCIAL_AUTH_SAML_ORG_INFO = {}
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = {}
SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {}
SOCIAL_AUTH_SAML_ENABLED_IDPS = {}
SOCIAL_AUTH_LOGIN_URL = '/'
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/sso/complete/'
SOCIAL_AUTH_LOGIN_ERROR_URL = '/sso/error/'
SOCIAL_AUTH_INACTIVE_USER_URL = '/sso/inactive/'
SOCIAL_AUTH_RAISE_EXCEPTIONS = False
SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL = False
SOCIAL_AUTH_SLUGIFY_USERNAMES = True
SOCIAL_AUTH_CLEAN_USERNAMES = True
SOCIAL_AUTH_SANITIZE_REDIRECTS = True
SOCIAL_AUTH_REDIRECT_IS_HTTPS = False
# Any ANSIBLE_* settings will be passed to the subprocess environment by the
# celery task.

View File

@ -76,10 +76,8 @@ include(optional('/etc/tower/conf.d/*.py'), scope=locals())
# default settings for development. If not present, we can still run using
# only the defaults.
try:
include(
optional('local_*.py'),
scope=locals(),
)
include(optional('local_*.py'), scope=locals())
include('postprocess.py', scope=locals())
except ImportError:
traceback.print_exc()
sys.exit(1)

View File

@ -470,6 +470,61 @@ TEST_AUTH_LDAP_TEAM_MAP_2_RESULT = {
'Everyone Team': {'users': True},
}
###############################################################################
# RADIUS AUTH SETTINGS
###############################################################################
RADIUS_SERVER = ''
RADIUS_PORT = 1812
RADIUS_SECRET = ''
###############################################################################
# SOCIAL AUTH SETTINGS
###############################################################################
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = ''
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = ''
#SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['profile']
#SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS = ['example.com']
#SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS = {'hd': 'example.com'}
SOCIAL_AUTH_GITHUB_KEY = ''
SOCIAL_AUTH_GITHUB_SECRET = ''
SOCIAL_AUTH_GITHUB_ORG_KEY = ''
SOCIAL_AUTH_GITHUB_ORG_SECRET = ''
SOCIAL_AUTH_GITHUB_ORG_NAME = ''
SOCIAL_AUTH_GITHUB_TEAM_KEY = ''
SOCIAL_AUTH_GITHUB_TEAM_SECRET = ''
SOCIAL_AUTH_GITHUB_TEAM_ID = ''
SOCIAL_AUTH_SAML_SP_ENTITY_ID = ''
SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = ''
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = ''
SOCIAL_AUTH_SAML_ORG_INFO = {
'en-US': {
'name': 'example',
'displayname': 'Example',
'url': 'http://www.example.com',
},
}
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = {
'givenName': 'Some User',
'emailAddress': 'suser@example.com',
}
SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {
'givenName': 'Some User',
'emailAddress': 'suser@example.com',
}
SOCIAL_AUTH_SAML_ENABLED_IDPS = {
#'myidp': {
# 'entity_id': 'https://idp.example.com',
# 'url': 'https://myidp.example.com/sso',
# 'x509cert': '',
#},
}
###############################################################################
# INVENTORY IMPORT TEST SETTINGS
###############################################################################

View File

@ -0,0 +1,31 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
# flake8: noqa
# Runs after all configuration files have been loaded to fix/check/update
# settings as needed.
if not AUTH_LDAP_SERVER_URI:
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.main.backend.LDAPBackend']
if not RADIUS_SERVER:
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'radiusauth.backends.RADIUSBackend']
if not all([SOCIAL_AUTH_GOOGLE_OAUTH2_KEY, SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET]):
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'social.backends.google.GoogleOAuth2']
if not all([SOCIAL_AUTH_GITHUB_KEY, SOCIAL_AUTH_GITHUB_SECRET]):
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'social.backends.github.GithubOAuth2']
if not all([SOCIAL_AUTH_GITHUB_ORG_KEY, SOCIAL_AUTH_GITHUB_ORG_SECRET, SOCIAL_AUTH_GITHUB_ORG_NAME]):
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'social.backends.github.GithubOrganizationOAuth2']
if not all([SOCIAL_AUTH_GITHUB_TEAM_KEY, SOCIAL_AUTH_GITHUB_TEAM_SECRET, SOCIAL_AUTH_GITHUB_TEAM_ID]):
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'social.backends.github.GithubTeamOAuth2']
if not all([SOCIAL_AUTH_SAML_SP_ENTITY_ID, SOCIAL_AUTH_SAML_SP_PUBLIC_CERT,
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY, SOCIAL_AUTH_SAML_ORG_INFO,
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
SOCIAL_AUTH_SAML_ENABLED_IDPS]):
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'social.backends.saml.SAMLAuth']

View File

@ -108,11 +108,8 @@ settings_file = os.environ.get('AWX_SETTINGS_FILE',
# Attempt to load settings from /etc/tower/settings.py first, followed by
# /etc/tower/conf.d/*.py.
try:
include(
settings_file,
optional(settings_files),
scope=locals(),
)
include(settings_file, optional(settings_files), scope=locals())
include('postprocess.py', scope=locals())
except ImportError:
traceback.print_exc()
sys.exit(1)

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

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

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

@ -0,0 +1,91 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
# Python
import urllib
# Six
import six
# Django
from django.contrib.auth import login, 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)
auth_token.user.backend = ''
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)

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

@ -0,0 +1,13 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
# Django
from django.conf.urls import 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'),
)

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

@ -0,0 +1,84 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
# Python
import urllib
# Django
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()

BIN
awx/static/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -9,7 +9,9 @@ handler500 = 'awx.main.views.handle_500'
urlpatterns = patterns('',
url(r'', include('awx.ui.urls', namespace='ui', app_name='ui')),
url(r'^api/', include('awx.api.urls', namespace='api', app_name='api')))
url(r'^api/', include('awx.api.urls', namespace='api', app_name='api')),
url(r'^sso/', include('awx.sso.urls', namespace='sso', app_name='sso')),
url(r'^sso/', include('social.apps.django_app.urls', namespace='social')))
urlpatterns += patterns('awx.main.views',
url(r'^403.html$', 'handle_403'),

View File

@ -13,15 +13,18 @@ cliff==1.13.0
cmd2==0.6.8
cryptography==0.9.3
d2to1==0.2.11
defusedxml==0.4.1
Django==1.6.7
django-auth-ldap==1.2.6
django-celery==3.1.10
django-crum==0.6.1
django-extensions==1.3.3
django-polymorphic==0.5.3
django-radius==0.1.1
djangorestframework==2.3.13
django-split-settings==0.1.1
django-taggit==0.11.2
dm.xmlsec.binding==1.3.2
dogpile.cache==0.5.6
dogpile.core==0.4.1
enum34==1.0.4
@ -49,12 +52,14 @@ jsonschema==2.5.1
keyring==4.1
kombu==3.0.21
lxml==3.4.4
M2Crypto==0.22.3
Markdown==2.4.1
mock==1.0.1
mongoengine==0.9.0
msgpack-python==0.4.6
netaddr==0.7.14
netifaces==0.10.4
oauthlib==1.0.3
ordereddict==1.1
os-client-config==1.6.1
os-diskconfig-python-novaclient-ext==0.1.2
@ -74,6 +79,7 @@ psycopg2
pyasn1==0.1.8
pycparser==2.14
pycrypto==2.6.1
PyJWT==1.4.0
pymongo==2.8
pyOpenSSL==0.15.1
pyparsing==2.0.3
@ -85,6 +91,10 @@ python-ironicclient==0.5.0
python-ldap==2.4.20
python-neutronclient==2.3.11
python-novaclient==2.20.0
python-openid==2.2.5
python-radius==1.0
python_social_auth==0.2.13
python-saml==2.1.4
python-swiftclient==2.2.0
python-troveclient==1.0.9
pytz==2014.10
@ -97,9 +107,10 @@ rax-default-network-flags-python-novaclient-ext==0.2.3
rax-scheduled-images-python-novaclient-ext==0.2.1
redis==2.10.3
requests==2.5.1
requests-oauthlib==0.5.0
simplejson==3.6.0
six==1.9.0
South==0.8.4
South==1.0.2
stevedore==1.3.0
suds==0.4
warlock==1.1.0