Ver Fonte

Fixes #23584: Enforce SSO allowlist settings in the default pipeline

Corencas há 5 horas atrás
pai
commit
16f6e86a33
2 ficheiros alterados com 78 adições e 0 exclusões
  1. 8 0
      netbox/netbox/settings.py
  2. 70 0
      netbox/netbox/tests/test_authentication.py

+ 8 - 0
netbox/netbox/settings.py

@@ -737,6 +737,7 @@ NETBOX_COPILOT_URL = 'https://static.copilot.netboxlabs.ai/load.js'
 SOCIAL_AUTH_PIPELINE = (
 SOCIAL_AUTH_PIPELINE = (
     'social_core.pipeline.social_auth.social_details',
     'social_core.pipeline.social_auth.social_details',
     'social_core.pipeline.social_auth.social_uid',
     'social_core.pipeline.social_auth.social_uid',
+    'social_core.pipeline.social_auth.auth_allowed',
     'social_core.pipeline.social_auth.social_user',
     'social_core.pipeline.social_auth.social_user',
     'social_core.pipeline.user.get_username',
     'social_core.pipeline.user.get_username',
     'social_core.pipeline.user.create_user',
     'social_core.pipeline.user.create_user',
@@ -758,6 +759,13 @@ for param in dir(configuration):
     if param.startswith('SOCIAL_AUTH_'):
     if param.startswith('SOCIAL_AUTH_'):
         globals()[param] = getattr(configuration, param)
         globals()[param] = getattr(configuration, param)
 
 
+# Warn if a custom pipeline omits the step which enforces allowlists
+if 'social_core.pipeline.social_auth.auth_allowed' not in SOCIAL_AUTH_PIPELINE:
+    warnings.warn(
+        "SOCIAL_AUTH_PIPELINE does not include social_core.pipeline.social_auth.auth_allowed; allowlist settings "
+        "such as WHITELISTED_EMAILS and WHITELISTED_DOMAINS will not be enforced."
+    )
+
 # Force usage of PostgreSQL's JSONB field for extra data
 # Force usage of PostgreSQL's JSONB field for extra data
 SOCIAL_AUTH_JSONFIELD_ENABLED = True
 SOCIAL_AUTH_JSONFIELD_ENABLED = True
 SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'users.utils.clean_username'
 SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'users.utils.clean_username'

+ 70 - 0
netbox/netbox/tests/test_authentication.py

@@ -14,7 +14,9 @@ from django.test.utils import override_settings
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.timezone import now
 from django.utils.timezone import now
 from rest_framework.test import APIClient
 from rest_framework.test import APIClient
+from social_core.backends.base import BaseAuth
 from social_core.exceptions import AuthFailed
 from social_core.exceptions import AuthFailed
+from social_django.models import UserSocialAuth
 
 
 from account.views import LoginView
 from account.views import LoginView
 from core.choices import ManagedFileRootPathChoices
 from core.choices import ManagedFileRootPathChoices
@@ -1275,3 +1277,71 @@ class SocialAuthExceptionMiddlewareTestCase(SimpleTestCase):
         self.assertEqual(response.url, settings.LOGIN_URL)
         self.assertEqual(response.url, settings.LOGIN_URL)
         messages = [str(m) for m in request._messages]
         messages = [str(m) for m in request._messages]
         self.assertEqual(messages, [self.GENERIC_MESSAGE])
         self.assertEqual(messages, [self.GENERIC_MESSAGE])
+
+
+class AllowlistTestBackend(BaseAuth):
+    """
+    Stands in for an identity provider, confirming whichever email address the request names.
+    """
+    name = 'allowlist-test'
+    ID_KEY = 'email'
+
+    def auth_complete(self, *args, **kwargs):
+        kwargs.update(response={'email': self.data['email']}, backend=self)
+        return self.strategy.authenticate(*args, **kwargs)
+
+    def get_user_details(self, response):
+        return {'username': response['email'].split('@')[0], 'email': response['email']}
+
+
+@override_settings(
+    AUTHENTICATION_BACKENDS=[
+        'netbox.tests.test_authentication.AllowlistTestBackend',
+        'netbox.authentication.ObjectPermissionBackend',
+    ],
+    SOCIAL_AUTH_ALLOWLIST_TEST_WHITELISTED_DOMAINS=['example.com'],
+)
+class SocialAuthAllowlistTestCase(DjangoTestCase):
+    """
+    Verify that the SSO login pipeline enforces a backend's allowlist (see #23584).
+    """
+
+    def setUp(self):
+        # load_backends() caches the discovered backends in a module-level dict, so isolate the
+        # backend overridden above from the remainder of the test suite.
+        cache_patcher = patch.dict('social_core.backends.utils.BACKENDSCACHE', {}, clear=True)
+        cache_patcher.start()
+        self.addCleanup(cache_patcher.stop)
+
+        self.url = reverse('social:complete', args=['allowlist-test'])
+
+    def test_email_outside_allowlist_is_refused(self):
+        response = self.client.get(self.url, {'email': 'mallory@evil.test'})
+
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(response.url, settings.LOGIN_URL)
+        self.assertNotIn('_auth_user_id', self.client.session)
+        self.assertFalse(User.objects.filter(username='mallory').exists())
+
+    def test_existing_user_outside_allowlist_is_refused(self):
+        user = User.objects.create_user(username='mallory', email='mallory@evil.test')
+        UserSocialAuth.objects.create(user=user, provider='allowlist-test', uid='mallory@evil.test')
+
+        response = self.client.get(self.url, {'email': 'mallory@evil.test'})
+
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(response.url, settings.LOGIN_URL)
+        self.assertNotIn('_auth_user_id', self.client.session)
+
+    def test_email_inside_allowlist_is_allowed(self):
+        self.client.get(self.url, {'email': 'alice@example.com'})
+
+        self.assertIn('_auth_user_id', self.client.session)
+        self.assertTrue(User.objects.filter(username='alice').exists())
+
+    @override_settings(SOCIAL_AUTH_ALLOWLIST_TEST_WHITELISTED_DOMAINS=[])
+    def test_no_allowlist_allows_any_email(self):
+        self.client.get(self.url, {'email': 'alice@evil.test'})
+
+        self.assertIn('_auth_user_id', self.client.session)
+        self.assertTrue(User.objects.filter(username='alice').exists())