diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 5aa0b834ea..8d42af1ae4 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -477,6 +477,7 @@ SOCIAL_AUTH_SAML_PIPELINE = _SOCIAL_AUTH_PIPELINE_BASE + ( 'awx.sso.pipeline.update_user_orgs', 'awx.sso.pipeline.update_user_teams', ) +SAML_AUTO_CREATE_OBJECTS = True SOCIAL_AUTH_LOGIN_URL = '/' SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/sso/complete/' diff --git a/awx/sso/conf.py b/awx/sso/conf.py index c408d72b40..3406787f6b 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -919,6 +919,17 @@ def get_saml_entity_id(): return settings.TOWER_URL_BASE +register( + 'SAML_AUTO_CREATE_OBJECTS', + field_class=fields.BooleanField, + default=True, + label=_('Automatically Create Organizations and Teams on SAML Login'), + help_text=_('When enabled (the default), mapped Organizations and Teams ' + 'will be created automatically on successful SAML login.'), + category=_('SAML'), + category_slug='saml', +) + register( 'SOCIAL_AUTH_SAML_CALLBACK_URL', field_class=fields.CharField, diff --git a/awx/sso/pipeline.py b/awx/sso/pipeline.py index 6d7e05da90..3e73974474 100644 --- a/awx/sso/pipeline.py +++ b/awx/sso/pipeline.py @@ -10,6 +10,7 @@ import logging from social_core.exceptions import AuthException # Django +from django.core.exceptions import ObjectDoesNotExist from django.utils.translation import ugettext_lazy as _ from django.db.models import Q @@ -80,11 +81,18 @@ def _update_m2m_from_expression(user, related, expr, remove=True): def _update_org_from_attr(user, related, attr, remove, remove_admins, remove_auditors): from awx.main.models import Organization + from django.conf import settings org_ids = [] for org_name in attr: - org = Organization.objects.get_or_create(name=org_name)[0] + try: + if settings.SAML_AUTO_CREATE_OBJECTS: + org = Organization.objects.get_or_create(name=org_name)[0] + else: + org = Organization.objects.get(name=org_name) + except ObjectDoesNotExist: + continue org_ids.append(org.id) getattr(org, related).members.add(user) @@ -199,11 +207,24 @@ def update_user_teams_by_saml_attr(backend, details, user=None, *args, **kwargs) if organization_alias: organization_name = organization_alias - org = Organization.objects.get_or_create(name=organization_name)[0] + + try: + if settings.SAML_AUTO_CREATE_OBJECTS: + org = Organization.objects.get_or_create(name=organization_name)[0] + else: + org = Organization.objects.get(name=organization_name) + except ObjectDoesNotExist: + continue if team_alias: team_name = team_alias - team = Team.objects.get_or_create(name=team_name, organization=org)[0] + try: + if settings.SAML_AUTO_CREATE_OBJECTS: + team = Team.objects.get_or_create(name=team_name, organization=org)[0] + else: + team = Team.objects.get(name=team_name, organization=org) + except ObjectDoesNotExist: + continue team_ids.append(team.id) team.member_role.members.add(user) diff --git a/awx/sso/tests/functional/test_pipeline.py b/awx/sso/tests/functional/test_pipeline.py index 06d5503db8..e691939752 100644 --- a/awx/sso/tests/functional/test_pipeline.py +++ b/awx/sso/tests/functional/test_pipeline.py @@ -174,8 +174,15 @@ class TestSAMLAttr(): return (o1, o2, o3) @pytest.fixture - def mock_settings(self): + def mock_settings(self, request): + fixture_args = request.node.get_closest_marker('fixture_args') + if fixture_args and 'autocreate' in fixture_args.kwargs: + autocreate = fixture_args.kwargs['autocreate'] + else: + autocreate = True + class MockSettings(): + SAML_AUTO_CREATE_OBJECTS = autocreate SOCIAL_AUTH_SAML_ORGANIZATION_ATTR = { 'saml_attr': 'memberOf', 'saml_admin_attr': 'admins', @@ -304,3 +311,41 @@ class TestSAMLAttr(): assert Team.objects.get( name='Yellow_Alias', organization__name='Default4_Alias').member_role.members.count() == 1 + @pytest.mark.fixture_args(autocreate=False) + def test_autocreate_disabled(self, users, kwargs, mock_settings): + kwargs['response']['attributes']['memberOf'] = ['Default1', 'Default2', 'Default3'] + kwargs['response']['attributes']['groups'] = ['Blue', 'Red', 'Green'] + with mock.patch('django.conf.settings', mock_settings): + for u in users: + update_user_orgs_by_saml_attr(None, None, u, **kwargs) + update_user_teams_by_saml_attr(None, None, u, **kwargs) + assert Organization.objects.count() == 0 + assert Team.objects.count() == 0 + + # precreate everything + o1 = Organization.objects.create(name='Default1') + o2 = Organization.objects.create(name='Default2') + o3 = Organization.objects.create(name='Default3') + Team.objects.create(name='Blue', organization_id=o1.id) + Team.objects.create(name='Blue', organization_id=o2.id) + Team.objects.create(name='Blue', organization_id=o3.id) + Team.objects.create(name='Red', organization_id=o1.id) + Team.objects.create(name='Green', organization_id=o1.id) + Team.objects.create(name='Green', organization_id=o3.id) + + for u in users: + update_user_orgs_by_saml_attr(None, None, u, **kwargs) + update_user_teams_by_saml_attr(None, None, u, **kwargs) + + assert o1.member_role.members.count() == 3 + assert o2.member_role.members.count() == 3 + assert o3.member_role.members.count() == 3 + + assert Team.objects.get(name='Blue', organization__name='Default1').member_role.members.count() == 3 + assert Team.objects.get(name='Blue', organization__name='Default2').member_role.members.count() == 3 + assert Team.objects.get(name='Blue', organization__name='Default3').member_role.members.count() == 3 + + assert Team.objects.get(name='Red', organization__name='Default1').member_role.members.count() == 3 + + assert Team.objects.get(name='Green', organization__name='Default1').member_role.members.count() == 3 + assert Team.objects.get(name='Green', organization__name='Default3').member_role.members.count() == 3