* consider global org and team maps for github authenticator

* consider global org and team maps for saml authenticator
This commit is contained in:
Peter Braun
2025-07-17 15:02:04 +02:00
committed by thedoubl3j
parent c4a6b28b87
commit e746589019
4 changed files with 239 additions and 8 deletions

View File

@@ -3,7 +3,7 @@ Unit tests for base authenticator migrator functionality.
""" """
import pytest import pytest
from unittest.mock import Mock from unittest.mock import Mock, patch
from awx.sso.utils.base_migrator import BaseAuthenticatorMigrator 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) result = migrator._mappers_match_structurally(mapper1, mapper2)
assert result == expected 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'}}

View File

@@ -4,6 +4,7 @@ Base authenticator migrator class.
This module defines the contract that all specific authenticator migrators must follow. 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 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') self._write_output(f' ✗ Unexpected error creating {mapper_type} mapper "{mapper_name}": {str(e)}', 'error')
return False 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): def _write_output(self, message, style=None):
"""Write output message if command is available.""" """Write output message if command is available."""
if self.command: if self.command:

View File

@@ -66,8 +66,8 @@ class GitHubMigrator(BaseAuthenticatorMigrator):
continue continue
# If we have both key and secret, collect all settings # If we have both key and secret, collect all settings
org_map_value = None org_map_setting_name = None
team_map_value = None team_map_setting_name = None
for setting_name in category_settings: for setting_name in category_settings:
# Skip if setting_name is not a string (e.g., regex pattern) # 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) value = getattr(settings, setting_name, None)
config_data[setting_name] = value 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'): if setting_name.endswith('_ORGANIZATION_MAP'):
org_map_value = value org_map_setting_name = setting_name
elif setting_name.endswith('_TEAM_MAP'): 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 # 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 # Start with order 1 and maintain sequence across both org and team mappers

View File

@@ -49,8 +49,9 @@ class SAMLMigrator(BaseAuthenticatorMigrator):
idps = getattr(settings, "SOCIAL_AUTH_SAML_ENABLED_IDPS", {}) idps = getattr(settings, "SOCIAL_AUTH_SAML_ENABLED_IDPS", {})
security_config = getattr(settings, "SOCIAL_AUTH_SAML_SECURITY_CONFIG", {}) security_config = getattr(settings, "SOCIAL_AUTH_SAML_SECURITY_CONFIG", {})
org_map_value = getattr(settings, "SOCIAL_AUTH_SAML_ORGANIZATION_MAP", None) # Get org and team mappings using the new fallback functions
team_map_value = getattr(settings, "SOCIAL_AUTH_SAML_TEAM_MAP", None) 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) extra_data = getattr(settings, "SOCIAL_AUTH_SAML_EXTRA_DATA", None)
support_contact = getattr(settings, "SOCIAL_AUTH_SAML_SUPPORT_CONTACT", {}) support_contact = getattr(settings, "SOCIAL_AUTH_SAML_SUPPORT_CONTACT", {})
technical_contact = getattr(settings, "SOCIAL_AUTH_SAML_TECHNICAL_CONTACT", {}) technical_contact = getattr(settings, "SOCIAL_AUTH_SAML_TECHNICAL_CONTACT", {})