diff --git a/onelogin/saml2/__init__.py b/onelogin/saml2/__init__.py new file mode 100644 index 0000000..c611f11 --- /dev/null +++ b/onelogin/saml2/__init__.py @@ -0,0 +1,12 @@ +# coding: utf-8 + +""" +SAML2 Authentication Module + +This module provides compatibility for users expecting to import +OneLogin_Saml2_Auth from onelogin.saml2.auth. However, OneLogin_Saml2_Auth is +actually provided by the 'python3-saml' package, not this onelogin package. + +This onelogin package is for OneLogin API management, while python3-saml is +for SAML2 authentication integration. +""" diff --git a/onelogin/saml2/auth.py b/onelogin/saml2/auth.py new file mode 100644 index 0000000..2ce9607 --- /dev/null +++ b/onelogin/saml2/auth.py @@ -0,0 +1,70 @@ +# coding: utf-8 + +""" +SAML2 Authentication Module + +This module provides compatibility for users expecting to import +OneLogin_Saml2_Auth from onelogin.saml2.auth. However, OneLogin_Saml2_Auth is +actually provided by the 'python3-saml' package, not this onelogin package. + +This onelogin package is for OneLogin API management, while python3-saml is +for SAML2 authentication integration. +""" + +import os +import site + + +def _check_python3_saml_installed(): + """Check if python3-saml is installed by looking for its onelogin.saml2.auth module""" + for site_dir in site.getsitepackages() + [site.getusersitepackages()]: + if site_dir and os.path.exists(site_dir): + potential_path = os.path.join(site_dir, 'onelogin', 'saml2', 'auth.py') + if os.path.exists(potential_path): + return True + return False + + +def __getattr__(name): + """ + Intercept attempts to import OneLogin_Saml2_Auth and provide helpful + error message based on whether python3-saml is detected. + """ + if name == "OneLogin_Saml2_Auth": + python3_saml_installed = _check_python3_saml_installed() + + if python3_saml_installed: + # python3-saml is installed but there's a namespace conflict + raise ImportError( + "OneLogin_Saml2_Auth is not available due to a package namespace conflict.\n\n" + "You have both 'onelogin' (API management) and 'python3-saml' (SAML authentication) installed.\n" + "These packages both provide 'onelogin' modules, causing a conflict.\n\n" + "To fix this conflict:\n\n" + "OPTION 1 - If you only need SAML authentication:\n" + " 1. Uninstall this package: pip uninstall onelogin\n" + " 2. Then import will work: from onelogin.saml2.auth import OneLogin_Saml2_Auth\n\n" + "OPTION 2 - If you need both packages:\n" + " 1. Import directly from python3-saml's location in site-packages\n" + " 2. See: https://github.com/onelogin/python3-saml for documentation\n\n" + "OPTION 3 - Use virtual environments to separate the packages\n\n" + "To check your installations: pip list | grep -E '(onelogin|python3-saml)'" + ) + else: + # python3-saml is not installed + raise ImportError( + "OneLogin_Saml2_Auth is not available in this package.\n\n" + "This package (onelogin) is for OneLogin API management.\n" + "For SAML2 authentication, you need the 'python3-saml' package.\n\n" + "To fix this:\n" + "1. Install the correct package: pip install python3-saml\n" + "2. Import from: from onelogin.saml2.auth import OneLogin_Saml2_Auth\n\n" + "For more information: https://github.com/onelogin/python3-saml" + ) + + raise AttributeError( + f"module 'onelogin.saml2.auth' has no attribute '{name}'" + ) + + +# Make module attributes available for inspection +__all__ = [] diff --git a/test/test_saml2_import_compatibility.py b/test/test_saml2_import_compatibility.py new file mode 100644 index 0000000..2b58ef8 --- /dev/null +++ b/test/test_saml2_import_compatibility.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" +Test import behavior for SAML2 compatibility module +""" + +import pytest +import os +import site + + +class TestSaml2ImportCompatibility: + """Test the SAML2 import compatibility module""" + + def test_import_onelogin_saml2_auth_raises_helpful_error(self): + """Test that importing OneLogin_Saml2_Auth raises a helpful error message""" + + # Attempt to import OneLogin_Saml2_Auth + with pytest.raises(ImportError) as exc_info: + from onelogin.saml2.auth import OneLogin_Saml2_Auth + + # Verify the error message contains helpful guidance + error_message = str(exc_info.value) + assert "OneLogin_Saml2_Auth is not available" in error_message + assert "python3-saml" in error_message + + # The specific message depends on whether python3-saml is detected + python3_saml_installed = self._check_python3_saml_installed() + + if python3_saml_installed: + # Should show namespace conflict message + assert "namespace conflict" in error_message + assert "both 'onelogin'" in error_message + assert "pip uninstall onelogin" in error_message + else: + # Should show installation instruction message + assert "pip install python3-saml" in error_message + assert "https://github.com/onelogin/python3-saml" in error_message + + def test_getattr_for_nonexistent_attribute_raises_attribute_error(self): + """Test that accessing non-existent attributes raises AttributeError""" + + import onelogin.saml2.auth + with pytest.raises(AttributeError) as exc_info: + getattr(onelogin.saml2.auth, 'NonExistentClass') + + error_message = str(exc_info.value) + assert "module 'onelogin.saml2.auth' has no attribute 'NonExistentClass'" in error_message + + def test_saml2_module_can_be_imported(self): + """Test that the saml2 module itself can be imported without error""" + + import onelogin.saml2 + # Should not raise any errors + + def test_saml2_auth_module_can_be_imported(self): + """Test that the saml2.auth module itself can be imported without error""" + + import onelogin.saml2.auth + # Should not raise any errors + + def test_saml2_auth_module_has_empty_all_list(self): + """Test that __all__ is defined and empty""" + + import onelogin.saml2.auth + assert hasattr(onelogin.saml2.auth, '__all__') + assert onelogin.saml2.auth.__all__ == [] + + def test_python3_saml_detection_function(self): + """Test that the python3-saml detection function works correctly""" + + import onelogin.saml2.auth + result = onelogin.saml2.auth._check_python3_saml_installed() + # Should return a boolean + assert isinstance(result, bool) + + # Test by checking if the expected path exists manually + expected_exists = False + for site_dir in site.getsitepackages() + [site.getusersitepackages()]: + if site_dir and os.path.exists(site_dir): + potential_path = os.path.join(site_dir, 'onelogin', 'saml2', 'auth.py') + if os.path.exists(potential_path): + expected_exists = True + break + + assert result == expected_exists + + def _check_python3_saml_installed(self): + """Helper method to check if python3-saml is installed""" + for site_dir in site.getsitepackages() + [site.getusersitepackages()]: + if site_dir and os.path.exists(site_dir): + potential_path = os.path.join(site_dir, 'onelogin', 'saml2', 'auth.py') + if os.path.exists(potential_path): + return True + return False \ No newline at end of file