From e746589019273132863fe17cab1548b98b4a0835 Mon Sep 17 00:00:00 2001 From: Peter Braun Date: Thu, 17 Jul 2025 15:02:04 +0200 Subject: [PATCH] Aap 49570 (#7022) * consider global org and team maps for github authenticator * consider global org and team maps for saml authenticator --- .../tests/unit/utils/test_base_migrator.py | 187 +++++++++++++++++- awx/sso/utils/base_migrator.py | 41 ++++ awx/sso/utils/github_migrator.py | 14 +- awx/sso/utils/saml_migrator.py | 5 +- 4 files changed, 239 insertions(+), 8 deletions(-) diff --git a/awx/main/tests/unit/utils/test_base_migrator.py b/awx/main/tests/unit/utils/test_base_migrator.py index 0e2278cee1..d6713cd8b5 100644 --- a/awx/main/tests/unit/utils/test_base_migrator.py +++ b/awx/main/tests/unit/utils/test_base_migrator.py @@ -3,7 +3,7 @@ Unit tests for base authenticator migrator functionality. """ import pytest -from unittest.mock import Mock +from unittest.mock import Mock, patch from awx.sso.utils.base_migrator import BaseAuthenticatorMigrator @@ -684,3 +684,188 @@ def test_mappers_match_structurally_edge_cases(mapper1, mapper2, expected): result = migrator._mappers_match_structurally(mapper1, mapper2) assert result == expected + + +class TestSocialAuthMapFunctions: + """Test cases for social auth map functions.""" + + def setup_method(self): + """Set up test fixtures.""" + self.gateway_client = Mock() + self.command_obj = Mock() + self.migrator = BaseAuthenticatorMigrator(self.gateway_client, self.command_obj) + + @patch('awx.sso.utils.base_migrator.settings') + def test_get_social_org_map_with_authenticator_specific_setting(self, mock_settings): + """Test get_social_org_map returns authenticator-specific setting when available.""" + # Set up mock settings + authenticator_map = {'org1': ['team1', 'team2']} + global_map = {'global_org': ['global_team']} + + mock_settings.SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP = authenticator_map + mock_settings.SOCIAL_AUTH_ORGANIZATION_MAP = global_map + + # Mock getattr to return the specific setting + with patch('awx.sso.utils.base_migrator.getattr') as mock_getattr: + mock_getattr.side_effect = lambda obj, name, default=None: { + 'SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP': authenticator_map, + 'SOCIAL_AUTH_ORGANIZATION_MAP': global_map, + }.get(name, default) + + result = self.migrator.get_social_org_map('SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP') + + assert result == authenticator_map + # Verify it was called with the authenticator-specific setting first + mock_getattr.assert_any_call(mock_settings, 'SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP', None) + + @patch('awx.sso.utils.base_migrator.settings') + def test_get_social_org_map_fallback_to_global(self, mock_settings): + """Test get_social_org_map falls back to global setting when authenticator-specific is empty.""" + # Set up mock settings + global_map = {'global_org': ['global_team']} + + # Mock getattr to return None for authenticator-specific, global for fallback + with patch('awx.sso.utils.base_migrator.getattr') as mock_getattr: + mock_getattr.side_effect = lambda obj, name, default=None: { + 'SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP': None, + 'SOCIAL_AUTH_ORGANIZATION_MAP': global_map, + }.get(name, default) + + result = self.migrator.get_social_org_map('SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP') + + assert result == global_map + # Verify both calls were made + mock_getattr.assert_any_call(mock_settings, 'SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP', None) + mock_getattr.assert_any_call(mock_settings, 'SOCIAL_AUTH_ORGANIZATION_MAP', {}) + + @patch('awx.sso.utils.base_migrator.settings') + def test_get_social_org_map_empty_dict_fallback(self, mock_settings): + """Test get_social_org_map returns empty dict when neither setting exists.""" + # Mock getattr to return None for both settings + with patch('awx.sso.utils.base_migrator.getattr') as mock_getattr: + mock_getattr.side_effect = lambda obj, name, default=None: {'SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP': None, 'SOCIAL_AUTH_ORGANIZATION_MAP': {}}.get( + name, default + ) + + result = self.migrator.get_social_org_map('SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP') + + assert result == {} + + @patch('awx.sso.utils.base_migrator.settings') + def test_get_social_team_map_with_authenticator_specific_setting(self, mock_settings): + """Test get_social_team_map returns authenticator-specific setting when available.""" + # Set up mock settings + authenticator_map = {'team1': {'organization': 'org1'}} + global_map = {'global_team': {'organization': 'global_org'}} + + # Mock getattr to return the specific setting + with patch('awx.sso.utils.base_migrator.getattr') as mock_getattr: + mock_getattr.side_effect = lambda obj, name, default=None: { + 'SOCIAL_AUTH_GITHUB_TEAM_MAP': authenticator_map, + 'SOCIAL_AUTH_TEAM_MAP': global_map, + }.get(name, default) + + result = self.migrator.get_social_team_map('SOCIAL_AUTH_GITHUB_TEAM_MAP') + + assert result == authenticator_map + # Verify it was called with the authenticator-specific setting first + mock_getattr.assert_any_call(mock_settings, 'SOCIAL_AUTH_GITHUB_TEAM_MAP', None) + + @patch('awx.sso.utils.base_migrator.settings') + def test_get_social_team_map_fallback_to_global(self, mock_settings): + """Test get_social_team_map falls back to global setting when authenticator-specific is empty.""" + # Set up mock settings + global_map = {'global_team': {'organization': 'global_org'}} + + # Mock getattr to return None for authenticator-specific, global for fallback + with patch('awx.sso.utils.base_migrator.getattr') as mock_getattr: + mock_getattr.side_effect = lambda obj, name, default=None: {'SOCIAL_AUTH_GITHUB_TEAM_MAP': None, 'SOCIAL_AUTH_TEAM_MAP': global_map}.get( + name, default + ) + + result = self.migrator.get_social_team_map('SOCIAL_AUTH_GITHUB_TEAM_MAP') + + assert result == global_map + # Verify both calls were made + mock_getattr.assert_any_call(mock_settings, 'SOCIAL_AUTH_GITHUB_TEAM_MAP', None) + mock_getattr.assert_any_call(mock_settings, 'SOCIAL_AUTH_TEAM_MAP', {}) + + @patch('awx.sso.utils.base_migrator.settings') + def test_get_social_team_map_empty_dict_fallback(self, mock_settings): + """Test get_social_team_map returns empty dict when neither setting exists.""" + # Mock getattr to return None for both settings + with patch('awx.sso.utils.base_migrator.getattr') as mock_getattr: + mock_getattr.side_effect = lambda obj, name, default=None: {'SOCIAL_AUTH_GITHUB_TEAM_MAP': None, 'SOCIAL_AUTH_TEAM_MAP': {}}.get(name, default) + + result = self.migrator.get_social_team_map('SOCIAL_AUTH_GITHUB_TEAM_MAP') + + assert result == {} + + @patch('awx.sso.utils.base_migrator.settings') + def test_get_social_org_map_with_empty_string_fallback(self, mock_settings): + """Test get_social_org_map falls back to global when authenticator-specific is empty string.""" + # Set up mock settings + global_map = {'global_org': ['global_team']} + + # Mock getattr to return empty string for authenticator-specific + with patch('awx.sso.utils.base_migrator.getattr') as mock_getattr: + mock_getattr.side_effect = lambda obj, name, default=None: { + 'SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP': '', + 'SOCIAL_AUTH_ORGANIZATION_MAP': global_map, + }.get(name, default) + + result = self.migrator.get_social_org_map('SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP') + + assert result == global_map + + @patch('awx.sso.utils.base_migrator.settings') + def test_get_social_team_map_with_empty_dict_fallback(self, mock_settings): + """Test get_social_team_map falls back to global when authenticator-specific is empty dict.""" + # Set up mock settings + global_map = {'global_team': {'organization': 'global_org'}} + + # Mock getattr to return empty dict for authenticator-specific + with patch('awx.sso.utils.base_migrator.getattr') as mock_getattr: + mock_getattr.side_effect = lambda obj, name, default=None: {'SOCIAL_AUTH_GITHUB_TEAM_MAP': {}, 'SOCIAL_AUTH_TEAM_MAP': global_map}.get( + name, default + ) + + result = self.migrator.get_social_team_map('SOCIAL_AUTH_GITHUB_TEAM_MAP') + + # Empty dict is falsy, so it should fall back to global + assert result == global_map + + def test_get_social_org_map_different_authenticators(self): + """Test get_social_org_map works with different authenticator setting names.""" + test_cases = [ + 'SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP', + 'SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP', + 'SOCIAL_AUTH_SAML_ORGANIZATION_MAP', + 'SOCIAL_AUTH_OIDC_ORGANIZATION_MAP', + ] + + for setting_name in test_cases: + with patch('awx.sso.utils.base_migrator.getattr') as mock_getattr: + mock_getattr.side_effect = lambda obj, name, default=None: { + setting_name: {'test_org': ['test_team']}, + 'SOCIAL_AUTH_ORGANIZATION_MAP': {'fallback_org': ['fallback_team']}, + }.get(name, default) + + result = self.migrator.get_social_org_map(setting_name) + + assert result == {'test_org': ['test_team']} + + def test_get_social_team_map_different_authenticators(self): + """Test get_social_team_map works with different authenticator setting names.""" + test_cases = ['SOCIAL_AUTH_GITHUB_TEAM_MAP', 'SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP', 'SOCIAL_AUTH_SAML_TEAM_MAP', 'SOCIAL_AUTH_OIDC_TEAM_MAP'] + + for setting_name in test_cases: + with patch('awx.sso.utils.base_migrator.getattr') as mock_getattr: + mock_getattr.side_effect = lambda obj, name, default=None: { + setting_name: {'test_team': {'organization': 'test_org'}}, + 'SOCIAL_AUTH_TEAM_MAP': {'fallback_team': {'organization': 'fallback_org'}}, + }.get(name, default) + + result = self.migrator.get_social_team_map(setting_name) + + assert result == {'test_team': {'organization': 'test_org'}} diff --git a/awx/sso/utils/base_migrator.py b/awx/sso/utils/base_migrator.py index 9fa2245f34..476e943c66 100644 --- a/awx/sso/utils/base_migrator.py +++ b/awx/sso/utils/base_migrator.py @@ -4,6 +4,7 @@ Base authenticator migrator class. This module defines the contract that all specific authenticator migrators must follow. """ +from django.conf import settings from awx.main.utils.gateway_client import GatewayAPIError @@ -512,6 +513,46 @@ class BaseAuthenticatorMigrator: self._write_output(f' ✗ Unexpected error creating {mapper_type} mapper "{mapper_name}": {str(e)}', 'error') return False + def get_social_org_map(self, authenticator_setting_name): + """ + Get social auth organization map with fallback to global setting. + + Args: + authenticator_setting_name: Name of the authenticator-specific organization map setting + (e.g., 'SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP') + + Returns: + dict: Organization mapping configuration, with fallback to global setting + """ + # Try authenticator-specific setting first + authenticator_map = getattr(settings, authenticator_setting_name, None) + if authenticator_map: + return authenticator_map + + # Fall back to global setting + global_map = getattr(settings, 'SOCIAL_AUTH_ORGANIZATION_MAP', {}) + return global_map + + def get_social_team_map(self, authenticator_setting_name): + """ + Get social auth team map with fallback to global setting. + + Args: + authenticator_setting_name: Name of the authenticator-specific team map setting + (e.g., 'SOCIAL_AUTH_GITHUB_TEAM_MAP') + + Returns: + dict: Team mapping configuration, with fallback to global setting + """ + # Try authenticator-specific setting first + authenticator_map = getattr(settings, authenticator_setting_name, None) + if authenticator_map: + return authenticator_map + + # Fall back to global setting + global_map = getattr(settings, 'SOCIAL_AUTH_TEAM_MAP', {}) + return global_map + def _write_output(self, message, style=None): """Write output message if command is available.""" if self.command: diff --git a/awx/sso/utils/github_migrator.py b/awx/sso/utils/github_migrator.py index b1f4adbccd..273e8bb672 100644 --- a/awx/sso/utils/github_migrator.py +++ b/awx/sso/utils/github_migrator.py @@ -66,8 +66,8 @@ class GitHubMigrator(BaseAuthenticatorMigrator): continue # If we have both key and secret, collect all settings - org_map_value = None - team_map_value = None + org_map_setting_name = None + team_map_setting_name = None for setting_name in category_settings: # Skip if setting_name is not a string (e.g., regex pattern) @@ -76,11 +76,15 @@ class GitHubMigrator(BaseAuthenticatorMigrator): value = getattr(settings, setting_name, None) config_data[setting_name] = value - # Capture org and team map values for special processing + # Capture org and team map setting names for special processing if setting_name.endswith('_ORGANIZATION_MAP'): - org_map_value = value + org_map_setting_name = setting_name elif setting_name.endswith('_TEAM_MAP'): - team_map_value = value + team_map_setting_name = setting_name + + # Get org and team mappings using the new fallback functions + org_map_value = self.get_social_org_map(org_map_setting_name) if org_map_setting_name else {} + team_map_value = self.get_social_team_map(team_map_setting_name) if team_map_setting_name else {} # Convert GitHub org and team mappings from AWX to the Gateway format # Start with order 1 and maintain sequence across both org and team mappers diff --git a/awx/sso/utils/saml_migrator.py b/awx/sso/utils/saml_migrator.py index 4c55d8ac8c..d498029e84 100644 --- a/awx/sso/utils/saml_migrator.py +++ b/awx/sso/utils/saml_migrator.py @@ -49,8 +49,9 @@ class SAMLMigrator(BaseAuthenticatorMigrator): idps = getattr(settings, "SOCIAL_AUTH_SAML_ENABLED_IDPS", {}) security_config = getattr(settings, "SOCIAL_AUTH_SAML_SECURITY_CONFIG", {}) - org_map_value = getattr(settings, "SOCIAL_AUTH_SAML_ORGANIZATION_MAP", None) - team_map_value = getattr(settings, "SOCIAL_AUTH_SAML_TEAM_MAP", None) + # Get org and team mappings using the new fallback functions + org_map_value = self.get_social_org_map("SOCIAL_AUTH_SAML_ORGANIZATION_MAP") + team_map_value = self.get_social_team_map("SOCIAL_AUTH_SAML_TEAM_MAP") extra_data = getattr(settings, "SOCIAL_AUTH_SAML_EXTRA_DATA", None) support_contact = getattr(settings, "SOCIAL_AUTH_SAML_SUPPORT_CONTACT", {}) technical_contact = getattr(settings, "SOCIAL_AUTH_SAML_TECHNICAL_CONTACT", {})