mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 17:37:37 -02:30
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:
@@ -1,6 +1,9 @@
|
|||||||
# Copyright (c) 2015 Ansible, Inc.
|
# Copyright (c) 2015 Ansible, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
# Python
|
||||||
|
import urllib
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.utils.timezone import now as tz_now
|
from django.utils.timezone import now as tz_now
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -30,6 +33,13 @@ class TokenAuthentication(authentication.TokenAuthentication):
|
|||||||
auth = auth.encode(HTTP_HEADER_ENCODING)
|
auth = auth.encode(HTTP_HEADER_ENCODING)
|
||||||
return auth
|
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
|
||||||
|
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
self.request = request
|
self.request = request
|
||||||
|
|
||||||
@@ -40,7 +50,9 @@ class TokenAuthentication(authentication.TokenAuthentication):
|
|||||||
if not auth or auth[0].lower() != 'token':
|
if not auth or auth[0].lower() != 'token':
|
||||||
auth = authentication.get_authorization_header(request).split()
|
auth = authentication.get_authorization_header(request).split()
|
||||||
if not auth or auth[0].lower() != 'token':
|
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:
|
if len(auth) == 1:
|
||||||
msg = 'Invalid token header. No credentials provided.'
|
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_200': getattr(self, 'new_in_200', False),
|
||||||
'new_in_210': getattr(self, 'new_in_210', False),
|
'new_in_210': getattr(self, 'new_in_210', False),
|
||||||
'new_in_220': getattr(self, 'new_in_220', 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):
|
def get_description(self, html=False):
|
||||||
@@ -158,7 +160,7 @@ class APIView(views.APIView):
|
|||||||
'''
|
'''
|
||||||
ret = super(APIView, self).metadata(request)
|
ret = super(APIView, self).metadata(request)
|
||||||
added_in_version = '1.2'
|
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):
|
if getattr(self, 'new_in_%s' % version.replace('.', ''), False):
|
||||||
added_in_version = version
|
added_in_version = version
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -601,6 +601,8 @@ class UserSerializer(BaseSerializer):
|
|||||||
ret = super(UserSerializer, self).to_native(obj)
|
ret = super(UserSerializer, self).to_native(obj)
|
||||||
ret.pop('password', None)
|
ret.pop('password', None)
|
||||||
ret.fields.pop('password', None)
|
ret.fields.pop('password', None)
|
||||||
|
if obj:
|
||||||
|
ret['auth'] = obj.social_auth.values('provider', 'uid')
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def get_validation_exclusions(self):
|
def get_validation_exclusions(self):
|
||||||
|
|||||||
@@ -3,4 +3,6 @@
|
|||||||
{% if new_in_145 %}> _Added in Ansible Tower 1.4.5_{% endif %}
|
{% 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_148 %}> _Added in Ansible Tower 1.4.8_{% endif %}
|
||||||
{% if new_in_200 %}> _New in Ansible Tower 2.0.0_{% 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'^$', 'api_v1_root_view'),
|
||||||
url(r'^ping/$', 'api_v1_ping_view'),
|
url(r'^ping/$', 'api_v1_ping_view'),
|
||||||
url(r'^config/$', 'api_v1_config_view'),
|
url(r'^config/$', 'api_v1_config_view'),
|
||||||
|
url(r'^auth/$', 'auth_view'),
|
||||||
url(r'^authtoken/$', 'auth_token_view'),
|
url(r'^authtoken/$', 'auth_token_view'),
|
||||||
url(r'^me/$', 'user_me_list'),
|
url(r'^me/$', 'user_me_list'),
|
||||||
url(r'^dashboard/$', 'dashboard_view'),
|
url(r'^dashboard/$', 'dashboard_view'),
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ import qsstats
|
|||||||
# ANSIConv
|
# ANSIConv
|
||||||
import ansiconv
|
import ansiconv
|
||||||
|
|
||||||
|
# Python Social Auth
|
||||||
|
from social.backends.utils import load_backends
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.task_engine import TaskSerializer, TASK_FILE, TEMPORARY_TASK_FILE
|
from awx.main.task_engine import TaskSerializer, TASK_FILE, TEMPORARY_TASK_FILE
|
||||||
from awx.main.tasks import mongodb_control
|
from awx.main.tasks import mongodb_control
|
||||||
@@ -514,6 +517,37 @@ class ScheduleUnifiedJobsList(SubListAPIView):
|
|||||||
view_name = 'Schedule Jobs List'
|
view_name = 'Schedule Jobs List'
|
||||||
new_in_148 = True
|
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):
|
class AuthTokenView(APIView):
|
||||||
|
|
||||||
authentication_classes = []
|
authentication_classes = []
|
||||||
|
|||||||
@@ -407,4 +407,5 @@ def get_current_user_from_drf_request(sender, **kwargs):
|
|||||||
'''
|
'''
|
||||||
request = get_current_request()
|
request = get_current_request()
|
||||||
drf_request = getattr(request, 'drf_request', None)
|
drf_request = getattr(request, 'drf_request', None)
|
||||||
|
print drf_request
|
||||||
return (getattr(drf_request, 'user', False), 0)
|
return (getattr(drf_request, 'user', False), 0)
|
||||||
|
|||||||
@@ -118,15 +118,30 @@ REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST']
|
|||||||
|
|
||||||
STDOUT_MAX_BYTES_DISPLAY = 1048576
|
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',
|
'django.core.context_processors.request',
|
||||||
'awx.ui.context_processors.settings',
|
'awx.ui.context_processors.settings',
|
||||||
'awx.ui.context_processors.version',
|
'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.HAMiddleware',
|
||||||
'awx.main.middleware.ActivityStreamMiddleware',
|
'awx.main.middleware.ActivityStreamMiddleware',
|
||||||
|
'awx.sso.middleware.SocialAuthMiddleware',
|
||||||
'crum.CurrentRequestUserMiddleware',
|
'crum.CurrentRequestUserMiddleware',
|
||||||
'awx.main.middleware.AuthTokenTimeoutMiddleware',
|
'awx.main.middleware.AuthTokenTimeoutMiddleware',
|
||||||
)
|
)
|
||||||
@@ -160,10 +175,12 @@ INSTALLED_APPS = (
|
|||||||
'kombu.transport.django',
|
'kombu.transport.django',
|
||||||
'polymorphic',
|
'polymorphic',
|
||||||
'taggit',
|
'taggit',
|
||||||
|
'social.apps.django_app.default',
|
||||||
'awx.main',
|
'awx.main',
|
||||||
'awx.api',
|
'awx.api',
|
||||||
'awx.ui',
|
'awx.ui',
|
||||||
'awx.fact',
|
'awx.fact',
|
||||||
|
'awx.sso',
|
||||||
)
|
)
|
||||||
|
|
||||||
INTERNAL_IPS = ('127.0.0.1',)
|
INTERNAL_IPS = ('127.0.0.1',)
|
||||||
@@ -201,12 +218,23 @@ REST_FRAMEWORK = {
|
|||||||
|
|
||||||
AUTHENTICATION_BACKENDS = (
|
AUTHENTICATION_BACKENDS = (
|
||||||
'awx.main.backend.LDAPBackend',
|
'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',
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
)
|
)
|
||||||
|
|
||||||
# LDAP server (default to None to skip using LDAP authentication).
|
# LDAP server (default to None to skip using LDAP authentication).
|
||||||
AUTH_LDAP_SERVER_URI = None
|
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.
|
# Seconds before auth tokens expire.
|
||||||
AUTH_TOKEN_EXPIRATION = 1800
|
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
|
||||||
|
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
|
# Any ANSIBLE_* settings will be passed to the subprocess environment by the
|
||||||
# celery task.
|
# celery task.
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ include(optional('/etc/tower/conf.d/*.py'), scope=locals())
|
|||||||
try:
|
try:
|
||||||
include(
|
include(
|
||||||
optional('local_*.py'),
|
optional('local_*.py'),
|
||||||
|
'postprocess.py',
|
||||||
scope=locals(),
|
scope=locals(),
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
@@ -470,6 +470,61 @@ TEST_AUTH_LDAP_TEAM_MAP_2_RESULT = {
|
|||||||
'Everyone Team': {'users': True},
|
'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
|
# INVENTORY IMPORT TEST SETTINGS
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|||||||
29
awx/settings/postprocess.py
Normal file
29
awx/settings/postprocess.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Copyright (c) 2015 Ansible, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
# 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']
|
||||||
@@ -111,6 +111,7 @@ try:
|
|||||||
include(
|
include(
|
||||||
settings_file,
|
settings_file,
|
||||||
optional(settings_files),
|
optional(settings_files),
|
||||||
|
'postprocess.py',
|
||||||
scope=locals(),
|
scope=locals(),
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
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.
|
||||||
90
awx/sso/middleware.py
Normal file
90
awx/sso/middleware.py
Normal 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
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)
|
||||||
12
awx/sso/urls.py
Normal file
12
awx/sso/urls.py
Normal 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
85
awx/sso/views.py
Normal 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()
|
||||||
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('',
|
urlpatterns = patterns('',
|
||||||
url(r'', include('awx.ui.urls', namespace='ui', app_name='ui')),
|
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',
|
urlpatterns += patterns('awx.main.views',
|
||||||
url(r'^403.html$', 'handle_403'),
|
url(r'^403.html$', 'handle_403'),
|
||||||
|
|||||||
@@ -13,15 +13,18 @@ cliff==1.13.0
|
|||||||
cmd2==0.6.8
|
cmd2==0.6.8
|
||||||
cryptography==0.9.3
|
cryptography==0.9.3
|
||||||
d2to1==0.2.11
|
d2to1==0.2.11
|
||||||
|
defusedxml==0.4.1
|
||||||
Django==1.6.7
|
Django==1.6.7
|
||||||
django-auth-ldap==1.2.6
|
django-auth-ldap==1.2.6
|
||||||
django-celery==3.1.10
|
django-celery==3.1.10
|
||||||
django-crum==0.6.1
|
django-crum==0.6.1
|
||||||
django-extensions==1.3.3
|
django-extensions==1.3.3
|
||||||
django-polymorphic==0.5.3
|
django-polymorphic==0.5.3
|
||||||
|
django-radius==0.1.1
|
||||||
djangorestframework==2.3.13
|
djangorestframework==2.3.13
|
||||||
django-split-settings==0.1.1
|
django-split-settings==0.1.1
|
||||||
django-taggit==0.11.2
|
django-taggit==0.11.2
|
||||||
|
dm.xmlsec.binding==1.3.2
|
||||||
dogpile.cache==0.5.6
|
dogpile.cache==0.5.6
|
||||||
dogpile.core==0.4.1
|
dogpile.core==0.4.1
|
||||||
enum34==1.0.4
|
enum34==1.0.4
|
||||||
@@ -49,12 +52,14 @@ jsonschema==2.5.1
|
|||||||
keyring==4.1
|
keyring==4.1
|
||||||
kombu==3.0.21
|
kombu==3.0.21
|
||||||
lxml==3.4.4
|
lxml==3.4.4
|
||||||
|
M2Crypto==0.22.3
|
||||||
Markdown==2.4.1
|
Markdown==2.4.1
|
||||||
mock==1.0.1
|
mock==1.0.1
|
||||||
mongoengine==0.9.0
|
mongoengine==0.9.0
|
||||||
msgpack-python==0.4.6
|
msgpack-python==0.4.6
|
||||||
netaddr==0.7.14
|
netaddr==0.7.14
|
||||||
netifaces==0.10.4
|
netifaces==0.10.4
|
||||||
|
oauthlib==1.0.3
|
||||||
ordereddict==1.1
|
ordereddict==1.1
|
||||||
os-client-config==1.6.1
|
os-client-config==1.6.1
|
||||||
os-diskconfig-python-novaclient-ext==0.1.2
|
os-diskconfig-python-novaclient-ext==0.1.2
|
||||||
@@ -74,6 +79,7 @@ psycopg2
|
|||||||
pyasn1==0.1.8
|
pyasn1==0.1.8
|
||||||
pycparser==2.14
|
pycparser==2.14
|
||||||
pycrypto==2.6.1
|
pycrypto==2.6.1
|
||||||
|
PyJWT==1.4.0
|
||||||
pymongo==2.8
|
pymongo==2.8
|
||||||
pyOpenSSL==0.15.1
|
pyOpenSSL==0.15.1
|
||||||
pyparsing==2.0.3
|
pyparsing==2.0.3
|
||||||
@@ -85,6 +91,10 @@ python-ironicclient==0.5.0
|
|||||||
python-ldap==2.4.20
|
python-ldap==2.4.20
|
||||||
python-neutronclient==2.3.11
|
python-neutronclient==2.3.11
|
||||||
python-novaclient==2.20.0
|
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-swiftclient==2.2.0
|
||||||
python-troveclient==1.0.9
|
python-troveclient==1.0.9
|
||||||
pytz==2014.10
|
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
|
rax-scheduled-images-python-novaclient-ext==0.2.1
|
||||||
redis==2.10.3
|
redis==2.10.3
|
||||||
requests==2.5.1
|
requests==2.5.1
|
||||||
|
requests-oauthlib==0.5.0
|
||||||
simplejson==3.6.0
|
simplejson==3.6.0
|
||||||
six==1.9.0
|
six==1.9.0
|
||||||
South==0.8.4
|
South==1.0.2
|
||||||
stevedore==1.3.0
|
stevedore==1.3.0
|
||||||
suds==0.4
|
suds==0.4
|
||||||
warlock==1.1.0
|
warlock==1.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user