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:
commit
8d2f6eaf06
@ -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.'
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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 %}
|
@ -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'),
|
||||
|
@ -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 = []
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
###############################################################################
|
||||
|
31
awx/settings/postprocess.py
Normal file
31
awx/settings/postprocess.py
Normal 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']
|
@ -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
2
awx/sso/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
91
awx/sso/middleware.py
Normal file
91
awx/sso/middleware.py
Normal 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
2
awx/sso/models.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
23
awx/sso/pipeline.py
Normal file
23
awx/sso/pipeline.py
Normal 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
13
awx/sso/urls.py
Normal 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
84
awx/sso/views.py
Normal 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
BIN
awx/static/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
BIN
awx/static/img/tower_console_bug.png
Normal file
BIN
awx/static/img/tower_console_bug.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
BIN
awx/static/img/tower_console_logo.png
Normal file
BIN
awx/static/img/tower_console_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
@ -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'),
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user