|
6 | 6 | from unittest.mock import MagicMock |
7 | 7 |
|
8 | 8 | import ddt |
| 9 | +import pytest |
| 10 | +from django.test import override_settings |
9 | 11 | from lxml import etree |
10 | 12 |
|
11 | 13 | from common.djangoapps.student.tests.factories import UserFactory |
12 | 14 | from common.djangoapps.third_party_auth.models import SAMLProviderData |
13 | 15 | from common.djangoapps.third_party_auth.tests.testutil import TestCase |
14 | 16 | from common.djangoapps.third_party_auth.utils import ( |
| 17 | + SAMLMetadataURLError, |
15 | 18 | create_or_update_bulk_saml_provider_data, |
16 | 19 | get_associated_user_by_email_response, |
17 | 20 | get_user_from_email, |
18 | 21 | is_enterprise_customer_user, |
19 | 22 | is_oauth_provider, |
20 | 23 | parse_metadata_xml, |
21 | 24 | user_exists, |
| 25 | + validate_saml_metadata_url, |
22 | 26 | ) |
23 | 27 | from openedx.core.djangolib.testing.utils import skip_unless_lms |
24 | 28 | from openedx.features.enterprise_support.tests.factories import ( |
@@ -272,3 +276,60 @@ def test_multiple_keys(self): |
272 | 276 | entity_id='http://entity1', |
273 | 277 | ).count() |
274 | 278 | assert count == 2 |
| 279 | + |
| 280 | + |
| 281 | +@ddt.ddt |
| 282 | +@skip_unless_lms |
| 283 | +class TestValidateSAMLMetadataURL(TestCase): |
| 284 | + """Tests for validate_saml_metadata_url.""" |
| 285 | + |
| 286 | + @ddt.data( |
| 287 | + 'https://idp.example.com/metadata', |
| 288 | + 'https://1.1.1.1/metadata', |
| 289 | + ) |
| 290 | + def test_valid_urls_pass(self, url): |
| 291 | + validate_saml_metadata_url(url) # should not raise |
| 292 | + |
| 293 | + @ddt.data( |
| 294 | + ('http://idp.example.com/metadata', 'must use HTTPS'), |
| 295 | + ('ftp://idp.example.com/metadata', 'must use HTTPS'), |
| 296 | + ('https://', 'no hostname'), |
| 297 | + ) |
| 298 | + @ddt.unpack |
| 299 | + def test_invalid_scheme_or_missing_hostname(self, url, expected_fragment): |
| 300 | + with pytest.raises(SAMLMetadataURLError, match=expected_fragment): |
| 301 | + validate_saml_metadata_url(url) |
| 302 | + |
| 303 | + @ddt.data( |
| 304 | + 'https://127.0.0.1/metadata', # IPv4 loopback |
| 305 | + 'https://[::1]/metadata', # IPv6 loopback |
| 306 | + 'https://169.254.169.254/latest', # AWS metadata endpoint |
| 307 | + 'https://169.254.0.1/metadata', # other link-local |
| 308 | + 'https://[fe80::1]/metadata', # IPv6 link-local |
| 309 | + 'https://240.0.0.1/metadata', # reserved (Class E) |
| 310 | + ) |
| 311 | + def test_always_blocked_regardless_of_setting(self, url): |
| 312 | + for allow_private in (False, True): |
| 313 | + with override_settings(SAML_METADATA_URL_ALLOW_PRIVATE_IPS=allow_private): |
| 314 | + with pytest.raises(SAMLMetadataURLError): |
| 315 | + validate_saml_metadata_url(url) |
| 316 | + |
| 317 | + @ddt.data( |
| 318 | + 'https://10.0.0.1/metadata', |
| 319 | + 'https://172.16.0.1/metadata', |
| 320 | + 'https://192.168.1.1/metadata', |
| 321 | + 'https://[fc00::1]/metadata', # IPv6 unique local |
| 322 | + ) |
| 323 | + def test_private_ip_blocked_by_default(self, url): |
| 324 | + with pytest.raises(SAMLMetadataURLError): |
| 325 | + validate_saml_metadata_url(url) |
| 326 | + |
| 327 | + @ddt.data( |
| 328 | + 'https://10.0.0.1/metadata', |
| 329 | + 'https://172.16.0.1/metadata', |
| 330 | + 'https://192.168.1.1/metadata', |
| 331 | + 'https://[fc00::1]/metadata', # IPv6 unique local |
| 332 | + ) |
| 333 | + @override_settings(SAML_METADATA_URL_ALLOW_PRIVATE_IPS=True) |
| 334 | + def test_private_ip_allowed_when_setting_enabled(self, url): |
| 335 | + validate_saml_metadata_url(url) # should not raise |
0 commit comments