mirror of
https://github.com/ansible/awx.git
synced 2026-02-23 05:55:59 -03:30
Social auth and SSO updates:
* Move auth backends into sso app. * Add support for mapping social auth users into organizations and teams. * Return social auth backends in a consistent order in the API. * Remove custom SAML attribute mapping and use options provided by PSA. * Add pipeline function to raise an exception if no user has been found or created; added comments on how to disable new user creation. * Add comments for defining a custom social auth pipeline function.
This commit is contained in:
@@ -527,7 +527,10 @@ class AuthView(APIView):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
data = SortedDict()
|
data = SortedDict()
|
||||||
err_backend, err_message = request.session.get('social_auth_error', (None, None))
|
err_backend, err_message = request.session.get('social_auth_error', (None, None))
|
||||||
for name, backend in load_backends(settings.AUTHENTICATION_BACKENDS).items():
|
auth_backends = load_backends(settings.AUTHENTICATION_BACKENDS).items()
|
||||||
|
# Return auth backends in consistent order: Google, GitHub, SAML.
|
||||||
|
auth_backends.sort(key=lambda x: 'g' if x[0] == 'google-oauth2' else x[0])
|
||||||
|
for name, backend in auth_backends:
|
||||||
if (not feature_exists('enterprise_auth') and
|
if (not feature_exists('enterprise_auth') and
|
||||||
not feature_enabled('ldap')) or \
|
not feature_enabled('ldap')) or \
|
||||||
(not feature_enabled('enterprise_auth') and
|
(not feature_enabled('enterprise_auth') and
|
||||||
@@ -541,7 +544,7 @@ class AuthView(APIView):
|
|||||||
}
|
}
|
||||||
if name == 'saml':
|
if name == 'saml':
|
||||||
backend_data['metadata_url'] = reverse('sso:saml_metadata')
|
backend_data['metadata_url'] = reverse('sso:saml_metadata')
|
||||||
for idp in settings.SOCIAL_AUTH_SAML_ENABLED_IDPS.keys():
|
for idp in sorted(settings.SOCIAL_AUTH_SAML_ENABLED_IDPS.keys()):
|
||||||
saml_backend_data = dict(backend_data.items())
|
saml_backend_data = dict(backend_data.items())
|
||||||
saml_backend_data['login_url'] = '%s?idp=%s' % (login_url, idp)
|
saml_backend_data['login_url'] = '%s?idp=%s' % (login_url, idp)
|
||||||
full_backend_name = '%s:%s' % (name, idp)
|
full_backend_name = '%s:%s' % (name, idp)
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ from django.test.utils import override_settings
|
|||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.models import * # noqa
|
from awx.main.models import * # noqa
|
||||||
from awx.main.backend import LDAPSettings
|
|
||||||
from awx.main.management.commands.run_callback_receiver import CallbackReceiver
|
from awx.main.management.commands.run_callback_receiver import CallbackReceiver
|
||||||
from awx.main.management.commands.run_task_system import run_taskmanager
|
from awx.main.management.commands.run_task_system import run_taskmanager
|
||||||
from awx.main.utils import get_ansible_version
|
from awx.main.utils import get_ansible_version
|
||||||
from awx.main.task_engine import TaskEngager as LicenseWriter
|
from awx.main.task_engine import TaskEngager as LicenseWriter
|
||||||
|
from awx.sso.backends import LDAPSettings
|
||||||
|
|
||||||
TEST_PLAYBOOK = '''- hosts: mygroup
|
TEST_PLAYBOOK = '''- hosts: mygroup
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re # noqa
|
||||||
import sys
|
import sys
|
||||||
import djcelery
|
import djcelery
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -217,13 +218,13 @@ REST_FRAMEWORK = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = (
|
AUTHENTICATION_BACKENDS = (
|
||||||
'awx.main.backend.LDAPBackend',
|
'awx.sso.backends.LDAPBackend',
|
||||||
'awx.main.backend.RADIUSBackend',
|
'awx.sso.backends.RADIUSBackend',
|
||||||
'social.backends.google.GoogleOAuth2',
|
'social.backends.google.GoogleOAuth2',
|
||||||
'social.backends.github.GithubOAuth2',
|
'social.backends.github.GithubOAuth2',
|
||||||
'social.backends.github.GithubOrganizationOAuth2',
|
'social.backends.github.GithubOrganizationOAuth2',
|
||||||
'social.backends.github.GithubTeamOAuth2',
|
'social.backends.github.GithubTeamOAuth2',
|
||||||
'awx.main.backend.SAMLAuth',
|
'awx.sso.backends.SAMLAuth',
|
||||||
'django.contrib.auth.backends.ModelBackend',
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -355,13 +356,15 @@ SOCIAL_AUTH_PIPELINE = (
|
|||||||
'social.pipeline.social_auth.social_user',
|
'social.pipeline.social_auth.social_user',
|
||||||
'social.pipeline.user.get_username',
|
'social.pipeline.user.get_username',
|
||||||
'social.pipeline.social_auth.associate_by_email',
|
'social.pipeline.social_auth.associate_by_email',
|
||||||
'social.pipeline.mail.mail_validation',
|
|
||||||
'social.pipeline.user.create_user',
|
'social.pipeline.user.create_user',
|
||||||
|
'awx.sso.pipeline.check_user_found_or_created',
|
||||||
'social.pipeline.social_auth.associate_user',
|
'social.pipeline.social_auth.associate_user',
|
||||||
'social.pipeline.social_auth.load_extra_data',
|
'social.pipeline.social_auth.load_extra_data',
|
||||||
'awx.sso.pipeline.set_is_active_for_new_user',
|
'awx.sso.pipeline.set_is_active_for_new_user',
|
||||||
'social.pipeline.user.user_details',
|
'social.pipeline.user.user_details',
|
||||||
'awx.sso.pipeline.prevent_inactive_login',
|
'awx.sso.pipeline.prevent_inactive_login',
|
||||||
|
'awx.sso.pipeline.update_user_orgs',
|
||||||
|
'awx.sso.pipeline.update_user_teams',
|
||||||
)
|
)
|
||||||
|
|
||||||
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = ''
|
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = ''
|
||||||
@@ -390,14 +393,6 @@ SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = {}
|
|||||||
SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {}
|
SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {}
|
||||||
SOCIAL_AUTH_SAML_ENABLED_IDPS = {}
|
SOCIAL_AUTH_SAML_ENABLED_IDPS = {}
|
||||||
|
|
||||||
SOCIAL_AUTH_SAML_ATTRS_PERMANENT_ID = "name_id"
|
|
||||||
SOCIAL_AUTH_SAML_ATTRS_MAP = {
|
|
||||||
"first_name": "User.FirstName",
|
|
||||||
"last_name": "User.LastName",
|
|
||||||
"username": "User.email",
|
|
||||||
"email": "User.email",
|
|
||||||
}
|
|
||||||
|
|
||||||
SOCIAL_AUTH_LOGIN_URL = '/'
|
SOCIAL_AUTH_LOGIN_URL = '/'
|
||||||
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/sso/complete/'
|
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/sso/complete/'
|
||||||
SOCIAL_AUTH_LOGIN_ERROR_URL = '/sso/error/'
|
SOCIAL_AUTH_LOGIN_ERROR_URL = '/sso/error/'
|
||||||
@@ -411,6 +406,9 @@ SOCIAL_AUTH_CLEAN_USERNAMES = True
|
|||||||
SOCIAL_AUTH_SANITIZE_REDIRECTS = True
|
SOCIAL_AUTH_SANITIZE_REDIRECTS = True
|
||||||
SOCIAL_AUTH_REDIRECT_IS_HTTPS = False
|
SOCIAL_AUTH_REDIRECT_IS_HTTPS = False
|
||||||
|
|
||||||
|
SOCIAL_AUTH_ORGANIZATION_MAP = {}
|
||||||
|
SOCIAL_AUTH_TEAM_MAP = {}
|
||||||
|
|
||||||
# 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.
|
||||||
|
|
||||||
|
|||||||
@@ -525,8 +525,80 @@ SOCIAL_AUTH_SAML_ENABLED_IDPS = {
|
|||||||
# 'url': 'https://myidp.example.com/sso',
|
# 'url': 'https://myidp.example.com/sso',
|
||||||
# 'x509cert': '',
|
# 'x509cert': '',
|
||||||
#},
|
#},
|
||||||
|
#'onelogin': {
|
||||||
|
# 'entity_id': 'https://app.onelogin.com/saml/metadata/123456',
|
||||||
|
# 'url': 'https://example.onelogin.com/trust/saml2/http-post/sso/123456',
|
||||||
|
# 'x509cert': '',
|
||||||
|
# 'attr_user_permanent_id': 'name_id',
|
||||||
|
# 'attr_first_name': 'User.FirstName',
|
||||||
|
# 'attr_last_name': 'User.LastName',
|
||||||
|
# 'attr_username': 'User.email',
|
||||||
|
# 'attr_email': 'User.email',
|
||||||
|
#},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SOCIAL_AUTH_ORGANIZATION_MAP = {
|
||||||
|
# Add all users to the default organization.
|
||||||
|
'Default': {
|
||||||
|
'users': True,
|
||||||
|
},
|
||||||
|
#'Test Org': {
|
||||||
|
# 'admins': ['admin@example.com'],
|
||||||
|
# 'users': True,
|
||||||
|
#},
|
||||||
|
#'Test Org 2': {
|
||||||
|
# 'admins': ['admin@example.com', re.compile(r'^tower-[^@]+*?@.*$],
|
||||||
|
# 'users': re.compile(r'^[^@].*?@example\.com$'),
|
||||||
|
#},
|
||||||
|
}
|
||||||
|
|
||||||
|
#SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP = {}
|
||||||
|
#SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP = {}
|
||||||
|
#SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP = {}
|
||||||
|
#SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP = {}
|
||||||
|
#SOCIAL_AUTH_SAML_ORGANIZATION_MAP = {}
|
||||||
|
|
||||||
|
SOCIAL_AUTH_TEAM_MAP = {
|
||||||
|
#'My Team': {
|
||||||
|
# 'organization': 'Test Org',
|
||||||
|
# 'users': ['re.compile(r'^[^@]+?@test\.example\.com$')'],
|
||||||
|
# 'remove': True,
|
||||||
|
#},
|
||||||
|
#'Other Team': {
|
||||||
|
# 'organization': 'Test Org 2',
|
||||||
|
# 'users': re.compile(r'^[^@]+?@test2\.example\.com$'),
|
||||||
|
# 'remove': False,
|
||||||
|
#},
|
||||||
|
}
|
||||||
|
|
||||||
|
#SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP = {}
|
||||||
|
#SOCIAL_AUTH_GITHUB_TEAM_MAP = {}
|
||||||
|
#SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP = {}
|
||||||
|
#SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP = {}
|
||||||
|
#SOCIAL_AUTH_SAML_TEAM_MAP = {}
|
||||||
|
|
||||||
|
# Uncomment one or more of the lines below to prevent new user accounts from
|
||||||
|
# being created for the selected social auth providers. Only users who have
|
||||||
|
# previously logged in using social auth or have a user account with a matching
|
||||||
|
# email address will be able to login.
|
||||||
|
|
||||||
|
#SOCIAL_AUTH_USER_FIELDS = []
|
||||||
|
#SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = []
|
||||||
|
#SOCIAL_AUTH_GITHUB_USER_FIELDS = []
|
||||||
|
#SOCIAL_AUTH_GITHUB_ORG_USER_FIELDS = []
|
||||||
|
#SOCIAL_AUTH_GITHUB_TEAM_USER_FIELDS = []
|
||||||
|
#SOCIAL_AUTH_SAML_USER_FIELDS = []
|
||||||
|
|
||||||
|
# It is also possible to add custom functions to the social auth pipeline for
|
||||||
|
# more advanced organization and team mapping. Use at your own risk.
|
||||||
|
|
||||||
|
#def custom_social_auth_pipeline_function(backend, details, user=None, *args, **kwargs):
|
||||||
|
# print 'custom:', backend, details, user, args, kwargs
|
||||||
|
|
||||||
|
#SOCIAL_AUTH_PIPELINE += (
|
||||||
|
# 'awx.settings.development.custom_social_auth_pipeline_function',
|
||||||
|
#)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# INVENTORY IMPORT TEST SETTINGS
|
# INVENTORY IMPORT TEST SETTINGS
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|||||||
@@ -523,8 +523,80 @@ SOCIAL_AUTH_SAML_ENABLED_IDPS = {
|
|||||||
# 'url': 'https://myidp.example.com/sso',
|
# 'url': 'https://myidp.example.com/sso',
|
||||||
# 'x509cert': '',
|
# 'x509cert': '',
|
||||||
#},
|
#},
|
||||||
|
#'onelogin': {
|
||||||
|
# 'entity_id': 'https://app.onelogin.com/saml/metadata/123456',
|
||||||
|
# 'url': 'https://example.onelogin.com/trust/saml2/http-post/sso/123456',
|
||||||
|
# 'x509cert': '',
|
||||||
|
# 'attr_user_permanent_id': 'name_id',
|
||||||
|
# 'attr_first_name': 'User.FirstName',
|
||||||
|
# 'attr_last_name': 'User.LastName',
|
||||||
|
# 'attr_username': 'User.email',
|
||||||
|
# 'attr_email': 'User.email',
|
||||||
|
#},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SOCIAL_AUTH_ORGANIZATION_MAP = {
|
||||||
|
# Add all users to the default organization.
|
||||||
|
'Default': {
|
||||||
|
'users': True,
|
||||||
|
},
|
||||||
|
#'Test Org': {
|
||||||
|
# 'admins': ['admin@example.com'],
|
||||||
|
# 'users': True,
|
||||||
|
#},
|
||||||
|
#'Test Org 2': {
|
||||||
|
# 'admins': ['admin@example.com', re.compile(r'^tower-[^@]+*?@.*$],
|
||||||
|
# 'users': re.compile(r'^[^@].*?@example\.com$'),
|
||||||
|
#},
|
||||||
|
}
|
||||||
|
|
||||||
|
#SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP = {}
|
||||||
|
#SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP = {}
|
||||||
|
#SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP = {}
|
||||||
|
#SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP = {}
|
||||||
|
#SOCIAL_AUTH_SAML_ORGANIZATION_MAP = {}
|
||||||
|
|
||||||
|
SOCIAL_AUTH_TEAM_MAP = {
|
||||||
|
#'My Team': {
|
||||||
|
# 'organization': 'Test Org',
|
||||||
|
# 'users': ['re.compile(r'^[^@]+?@test\.example\.com$')'],
|
||||||
|
# 'remove': True,
|
||||||
|
#},
|
||||||
|
#'Other Team': {
|
||||||
|
# 'organization': 'Test Org 2',
|
||||||
|
# 'users': re.compile(r'^[^@]+?@test2\.example\.com$'),
|
||||||
|
# 'remove': False,
|
||||||
|
#},
|
||||||
|
}
|
||||||
|
|
||||||
|
#SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP = {}
|
||||||
|
#SOCIAL_AUTH_GITHUB_TEAM_MAP = {}
|
||||||
|
#SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP = {}
|
||||||
|
#SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP = {}
|
||||||
|
#SOCIAL_AUTH_SAML_TEAM_MAP = {}
|
||||||
|
|
||||||
|
# Uncomment one or more of the lines below to prevent new user accounts from
|
||||||
|
# being created for the selected social auth providers. Only users who have
|
||||||
|
# previously logged in using social auth or have a user account with a matching
|
||||||
|
# email address will be able to login.
|
||||||
|
|
||||||
|
#SOCIAL_AUTH_USER_FIELDS = []
|
||||||
|
#SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = []
|
||||||
|
#SOCIAL_AUTH_GITHUB_USER_FIELDS = []
|
||||||
|
#SOCIAL_AUTH_GITHUB_ORG_USER_FIELDS = []
|
||||||
|
#SOCIAL_AUTH_GITHUB_TEAM_USER_FIELDS = []
|
||||||
|
#SOCIAL_AUTH_SAML_USER_FIELDS = []
|
||||||
|
|
||||||
|
# It is also possible to add custom functions to the social auth pipeline for
|
||||||
|
# more advanced organization and team mapping. Use at your own risk.
|
||||||
|
|
||||||
|
#def custom_social_auth_pipeline_function(backend, details, user=None, *args, **kwargs):
|
||||||
|
# print 'custom:', backend, details, user, args, kwargs
|
||||||
|
|
||||||
|
#SOCIAL_AUTH_PIPELINE += (
|
||||||
|
# 'awx.settings.development.custom_social_auth_pipeline_function',
|
||||||
|
#)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# INVENTORY IMPORT TEST SETTINGS
|
# INVENTORY IMPORT TEST SETTINGS
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
# settings as needed.
|
# settings as needed.
|
||||||
|
|
||||||
if not AUTH_LDAP_SERVER_URI:
|
if not AUTH_LDAP_SERVER_URI:
|
||||||
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.main.backend.LDAPBackend']
|
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.sso.backends.LDAPBackend']
|
||||||
|
|
||||||
if not RADIUS_SERVER:
|
if not RADIUS_SERVER:
|
||||||
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.main.backend.RADIUSBackend']
|
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.sso.backends.RADIUSBackend']
|
||||||
|
|
||||||
if not all([SOCIAL_AUTH_GOOGLE_OAUTH2_KEY, SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET]):
|
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']
|
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'social.backends.google.GoogleOAuth2']
|
||||||
@@ -28,8 +28,7 @@ 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_SP_PRIVATE_KEY, SOCIAL_AUTH_SAML_ORG_INFO,
|
||||||
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
|
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
|
||||||
SOCIAL_AUTH_SAML_ENABLED_IDPS]):
|
SOCIAL_AUTH_SAML_ENABLED_IDPS]):
|
||||||
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.main.backend.SAMLAuth']
|
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.sso.backends.SAMLAuth']
|
||||||
|
|
||||||
if not AUTH_BASIC_ENABLED:
|
if not AUTH_BASIC_ENABLED:
|
||||||
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = [x for x in REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] if x != 'rest_framework.authentication.BasicAuthentication']
|
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = [x for x in REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] if x != 'rest_framework.authentication.BasicAuthentication']
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,15 @@ from django_auth_ldap.backend import populate_user
|
|||||||
from radiusauth.backends import RADIUSBackend as BaseRADIUSBackend
|
from radiusauth.backends import RADIUSBackend as BaseRADIUSBackend
|
||||||
|
|
||||||
# social
|
# social
|
||||||
|
from social.backends.saml import OID_USERID
|
||||||
from social.backends.saml import SAMLAuth as BaseSAMLAuth
|
from social.backends.saml import SAMLAuth as BaseSAMLAuth
|
||||||
from social.backends.saml import SAMLIdentityProvider as BaseSAMLIdentityProvider
|
from social.backends.saml import SAMLIdentityProvider as BaseSAMLIdentityProvider
|
||||||
|
|
||||||
# Ansible Tower
|
# Ansible Tower
|
||||||
from awx.api.license import feature_enabled
|
from awx.api.license import feature_enabled
|
||||||
|
|
||||||
logger = logging.getLogger('awx.main.backend')
|
logger = logging.getLogger('awx.sso.backends')
|
||||||
|
|
||||||
|
|
||||||
class LDAPSettings(BaseLDAPSettings):
|
class LDAPSettings(BaseLDAPSettings):
|
||||||
|
|
||||||
@@ -80,6 +82,7 @@ class LDAPBackend(BaseLDAPBackend):
|
|||||||
def get_group_permissions(self, user, obj=None):
|
def get_group_permissions(self, user, obj=None):
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
|
||||||
class RADIUSBackend(BaseRADIUSBackend):
|
class RADIUSBackend(BaseRADIUSBackend):
|
||||||
'''
|
'''
|
||||||
Custom Radius backend to verify license status
|
Custom Radius backend to verify license status
|
||||||
@@ -101,26 +104,31 @@ class RADIUSBackend(BaseRADIUSBackend):
|
|||||||
return None
|
return None
|
||||||
return super(RADIUSBackend, self).get_user(user_id)
|
return super(RADIUSBackend, self).get_user(user_id)
|
||||||
|
|
||||||
|
|
||||||
class TowerSAMLIdentityProvider(BaseSAMLIdentityProvider):
|
class TowerSAMLIdentityProvider(BaseSAMLIdentityProvider):
|
||||||
'''
|
'''
|
||||||
Custom Identity Provider to make attributes to what we expect
|
Custom Identity Provider to make attributes to what we expect.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def get_user_permanent_id(self, attributes):
|
def get_user_permanent_id(self, attributes):
|
||||||
return attributes[django_settings.SOCIAL_AUTH_SAML_ATTRS_PERMANENT_ID]
|
uid = attributes[self.conf.get('attr_user_permanent_id', OID_USERID)]
|
||||||
|
if isinstance(uid, basestring):
|
||||||
|
return uid
|
||||||
|
return uid[0]
|
||||||
|
|
||||||
def get_user_details(self, attributes):
|
def get_attr(self, attributes, conf_key, default_attribute):
|
||||||
"""
|
"""
|
||||||
Given the SAML attributes extracted from the SSO response, get
|
Get the attribute 'default_attribute' out of the attributes,
|
||||||
the user data like name.
|
unless self.conf[conf_key] overrides the default by specifying
|
||||||
|
another attribute to use.
|
||||||
"""
|
"""
|
||||||
attrs = dict()
|
key = self.conf.get(conf_key, default_attribute)
|
||||||
for social_attr in django_settings.SOCIAL_AUTH_SAML_ATTRS_MAP:
|
value = attributes[key][0] if key in attributes else None
|
||||||
map_attr = django_settings.SOCIAL_AUTH_SAML_ATTRS_MAP[social_attr]
|
if conf_key in ('attr_first_name', 'attr_last_name', 'attr_username', 'attr_email') and value is None:
|
||||||
attrs[social_attr] = unicode(attributes[map_attr][0]) if map_attr in attributes else None
|
logger.warn("Could not map user detail '%s' from SAML attribute '%s'; "
|
||||||
if attrs[social_attr] is None:
|
"update SOCIAL_AUTH_SAML_ENABLED_IDPS['%s']['%s'] with the correct SAML attribute.",
|
||||||
logger.warn("Could not map SAML attribute '%s', update SOCIAL_AUTH_SAML_ATTRS_MAP with the correct value" % social_attr)
|
conf_key[5:], key, self.name, conf_key)
|
||||||
return attrs
|
return unicode(value) if value is not None else value
|
||||||
|
|
||||||
|
|
||||||
class SAMLAuth(BaseSAMLAuth):
|
class SAMLAuth(BaseSAMLAuth):
|
||||||
@@ -129,7 +137,6 @@ class SAMLAuth(BaseSAMLAuth):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
def get_idp(self, idp_name):
|
def get_idp(self, idp_name):
|
||||||
"""Given the name of an IdP, get a SAMLIdentityProvider instance"""
|
|
||||||
idp_config = self.setting('ENABLED_IDPS')[idp_name]
|
idp_config = self.setting('ENABLED_IDPS')[idp_name]
|
||||||
return TowerSAMLIdentityProvider(idp_name, **idp_config)
|
return TowerSAMLIdentityProvider(idp_name, **idp_config)
|
||||||
|
|
||||||
@@ -155,6 +162,7 @@ class SAMLAuth(BaseSAMLAuth):
|
|||||||
return None
|
return None
|
||||||
return super(SAMLAuth, self).get_user(user_id)
|
return super(SAMLAuth, self).get_user(user_id)
|
||||||
|
|
||||||
|
|
||||||
def _update_m2m_from_groups(user, ldap_user, rel, opts, remove=False):
|
def _update_m2m_from_groups(user, ldap_user, rel, opts, remove=False):
|
||||||
'''
|
'''
|
||||||
Hepler function to update m2m relationship based on LDAP group membership.
|
Hepler function to update m2m relationship based on LDAP group membership.
|
||||||
@@ -179,6 +187,7 @@ def _update_m2m_from_groups(user, ldap_user, rel, opts, remove=False):
|
|||||||
elif remove:
|
elif remove:
|
||||||
rel.remove(user)
|
rel.remove(user)
|
||||||
|
|
||||||
|
|
||||||
@receiver(populate_user)
|
@receiver(populate_user)
|
||||||
def on_populate_user(sender, **kwargs):
|
def on_populate_user(sender, **kwargs):
|
||||||
'''
|
'''
|
||||||
@@ -17,8 +17,10 @@ from social.exceptions import SocialAuthBaseException
|
|||||||
from social.utils import social_logger
|
from social.utils import social_logger
|
||||||
from social.apps.django_app.middleware import SocialAuthExceptionMiddleware
|
from social.apps.django_app.middleware import SocialAuthExceptionMiddleware
|
||||||
|
|
||||||
|
# Ansible Tower
|
||||||
from awx.main.models import AuthToken
|
from awx.main.models import AuthToken
|
||||||
|
|
||||||
|
|
||||||
class SocialAuthMiddleware(SocialAuthExceptionMiddleware):
|
class SocialAuthMiddleware(SocialAuthExceptionMiddleware):
|
||||||
|
|
||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
@@ -61,7 +63,7 @@ class SocialAuthMiddleware(SocialAuthExceptionMiddleware):
|
|||||||
if strategy is None or self.raise_exception(request, exception):
|
if strategy is None or self.raise_exception(request, exception):
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(exception, SocialAuthBaseException):
|
if isinstance(exception, SocialAuthBaseException) or request.path.startswith('/sso/'):
|
||||||
backend = getattr(request, 'backend', None)
|
backend = getattr(request, 'backend', None)
|
||||||
backend_name = getattr(backend, 'name', 'unknown-backend')
|
backend_name = getattr(backend, 'name', 'unknown-backend')
|
||||||
full_backend_name = backend_name
|
full_backend_name = backend_name
|
||||||
|
|||||||
@@ -1,17 +1,38 @@
|
|||||||
# Copyright (c) 2015 Ansible, Inc.
|
# Copyright (c) 2015 Ansible, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
# Python
|
||||||
|
import re
|
||||||
|
|
||||||
# Python Social Auth
|
# Python Social Auth
|
||||||
from social.exceptions import AuthException
|
from social.exceptions import AuthException
|
||||||
|
|
||||||
|
# Tower
|
||||||
|
from awx.api.license import feature_enabled
|
||||||
|
|
||||||
|
|
||||||
|
class AuthNotFound(AuthException):
|
||||||
|
|
||||||
|
def __init__(self, backend, email_or_uid, *args, **kwargs):
|
||||||
|
self.email_or_uid = email_or_uid
|
||||||
|
super(AuthNotFound, self).__init__(backend, *args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'An account cannot be found for {0}'.format(self.email_or_uid)
|
||||||
|
|
||||||
|
|
||||||
class AuthInactive(AuthException):
|
class AuthInactive(AuthException):
|
||||||
"""Authentication for this user is forbidden"""
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'Your account is inactive'
|
return 'Your account is inactive'
|
||||||
|
|
||||||
|
|
||||||
|
def check_user_found_or_created(backend, details, user=None, *args, **kwargs):
|
||||||
|
if not user:
|
||||||
|
email_or_uid = details.get('email') or kwargs.get('email') or kwargs.get('uid') or '???'
|
||||||
|
raise AuthNotFound(backend, email_or_uid)
|
||||||
|
|
||||||
|
|
||||||
def set_is_active_for_new_user(strategy, details, user=None, *args, **kwargs):
|
def set_is_active_for_new_user(strategy, details, user=None, *args, **kwargs):
|
||||||
if kwargs.get('is_new', False):
|
if kwargs.get('is_new', False):
|
||||||
details['is_active'] = True
|
details['is_active'] = True
|
||||||
@@ -21,3 +42,96 @@ def set_is_active_for_new_user(strategy, details, user=None, *args, **kwargs):
|
|||||||
def prevent_inactive_login(backend, details, user=None, *args, **kwargs):
|
def prevent_inactive_login(backend, details, user=None, *args, **kwargs):
|
||||||
if user and not user.is_active:
|
if user and not user.is_active:
|
||||||
raise AuthInactive(backend)
|
raise AuthInactive(backend)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_m2m_from_expression(user, rel, expr, remove=False):
|
||||||
|
'''
|
||||||
|
Helper function to update m2m relationship based on user matching one or
|
||||||
|
more expressions.
|
||||||
|
'''
|
||||||
|
should_add = False
|
||||||
|
if expr is None:
|
||||||
|
return
|
||||||
|
elif not expr:
|
||||||
|
pass
|
||||||
|
elif expr is True:
|
||||||
|
should_add = True
|
||||||
|
else:
|
||||||
|
if isinstance(expr, (basestring, type(re.compile('')))):
|
||||||
|
expr = [expr]
|
||||||
|
for ex in expr:
|
||||||
|
if isinstance(ex, basestring):
|
||||||
|
if user.username == ex or user.email == ex:
|
||||||
|
should_add = True
|
||||||
|
elif isinstance(ex, type(re.compile(''))):
|
||||||
|
if ex.match(user.username) or ex.match(user.email):
|
||||||
|
should_add = True
|
||||||
|
if should_add:
|
||||||
|
rel.add(user)
|
||||||
|
elif remove:
|
||||||
|
rel.remove(user)
|
||||||
|
|
||||||
|
|
||||||
|
def update_user_orgs(backend, details, user=None, *args, **kwargs):
|
||||||
|
'''
|
||||||
|
Update organization memberships for the given user based on mapping rules
|
||||||
|
defined in settings.
|
||||||
|
'''
|
||||||
|
if not user:
|
||||||
|
return
|
||||||
|
from awx.main.models import Organization
|
||||||
|
multiple_orgs = feature_enabled('multiple_organizations')
|
||||||
|
org_map = backend.setting('ORGANIZATION_MAP') or {}
|
||||||
|
for org_name, org_opts in org_map.items():
|
||||||
|
|
||||||
|
# Get or create the org to update. If the license only allows for one
|
||||||
|
# org, always use the first active org, unless no org exists.
|
||||||
|
if multiple_orgs:
|
||||||
|
org = Organization.objects.get_or_create(name=org_name)[0]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
org = Organization.objects.filter(active=True).order_by('pk')[0]
|
||||||
|
except IndexError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update org admins from expression(s).
|
||||||
|
remove = bool(org_opts.get('remove', False))
|
||||||
|
admins_expr = org_opts.get('admins', None)
|
||||||
|
remove_admins = bool(org_opts.get('remove_admins', remove))
|
||||||
|
_update_m2m_from_expression(user, org.admins, admins_expr, remove_admins)
|
||||||
|
|
||||||
|
# Update org users from expression(s).
|
||||||
|
users_expr = org_opts.get('users', None)
|
||||||
|
remove_users = bool(org_opts.get('remove_users', remove))
|
||||||
|
_update_m2m_from_expression(user, org.users, users_expr, remove_users)
|
||||||
|
|
||||||
|
|
||||||
|
def update_user_teams(backend, details, user=None, *args, **kwargs):
|
||||||
|
'''
|
||||||
|
Update team memberships for the given user based on mapping rules defined
|
||||||
|
in settings.
|
||||||
|
'''
|
||||||
|
if not user:
|
||||||
|
return
|
||||||
|
from awx.main.models import Organization, Team
|
||||||
|
multiple_orgs = feature_enabled('multiple_organizations')
|
||||||
|
team_map = backend.setting('TEAM_MAP') or {}
|
||||||
|
for team_name, team_opts in team_map.items():
|
||||||
|
|
||||||
|
# Get or create the org to update. If the license only allows for one
|
||||||
|
# org, always use the first active org, unless no org exists.
|
||||||
|
if multiple_orgs:
|
||||||
|
if 'organization' not in team_opts:
|
||||||
|
continue
|
||||||
|
org = Organization.objects.get_or_create(name=team_opts['organization'])[0]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
org = Organization.objects.filter(active=True).order_by('pk')[0]
|
||||||
|
except IndexError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update team members from expression(s).
|
||||||
|
team = Team.objects.get_or_create(name=team_name, organization=org)[0]
|
||||||
|
users_expr = team_opts.get('users', None)
|
||||||
|
remove = bool(team_opts.get('remove', False))
|
||||||
|
_update_m2m_from_expression(user, team.users, users_expr, remove)
|
||||||
|
|||||||
Reference in New Issue
Block a user