From 7eedefb2dfad7f186ec1e1b8e9728ae65787e44b Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Wed, 21 Jan 2026 15:24:44 -0600 Subject: [PATCH] Fixes #20902: Avoid conflict when Git URL contains embedded username When cloning Git data sources with URLs containing embedded usernames (e.g., https://user@bitbucket.org/...), NetBox was also passing explicit username/password kwargs to dulwich, causing authentication failures. Now checks for URL-embedded credentials before passing explicit kwargs. --- netbox/core/data_backends.py | 5 ++- netbox/core/tests/test_data_backends.py | 59 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 netbox/core/tests/test_data_backends.py diff --git a/netbox/core/data_backends.py b/netbox/core/data_backends.py index 9ba1d5dfd..bc8cdb869 100644 --- a/netbox/core/data_backends.py +++ b/netbox/core/data_backends.py @@ -102,7 +102,10 @@ class GitBackend(DataBackend): clone_args['pool_manager'] = ProxyPoolManager(self.socks_proxy) if self.url_scheme in ('http', 'https'): - if self.params.get('username'): + # Only pass explicit credentials if URL doesn't already contain embedded username + # to avoid credential conflicts + parsed_url = urlparse(self.url) + if not parsed_url.username and self.params.get('username'): clone_args.update( { "username": self.params.get('username'), diff --git a/netbox/core/tests/test_data_backends.py b/netbox/core/tests/test_data_backends.py new file mode 100644 index 000000000..4c2bb27a4 --- /dev/null +++ b/netbox/core/tests/test_data_backends.py @@ -0,0 +1,59 @@ +from unittest.mock import patch + +from django.test import TestCase + +from core.data_backends import GitBackend + + +class GitBackendCredentialTests(TestCase): + + def _get_clone_kwargs(self, url, **params): + backend = GitBackend(url=url, **params) + + with patch('dulwich.porcelain.clone') as mock_clone, \ + patch('dulwich.porcelain.NoneStream'): + try: + with backend.fetch(): + pass + except Exception: + pass + + if mock_clone.called: + return mock_clone.call_args.kwargs + return {} + + def test_url_with_embedded_username_skips_explicit_credentials(self): + kwargs = self._get_clone_kwargs( + url='https://myuser@bitbucket.org/workspace/repo.git', + username='myuser', + password='my-api-key' + ) + + self.assertEqual(kwargs.get('username'), None) + self.assertEqual(kwargs.get('password'), None) + + def test_url_without_embedded_username_passes_explicit_credentials(self): + kwargs = self._get_clone_kwargs( + url='https://bitbucket.org/workspace/repo.git', + username='myuser', + password='my-api-key' + ) + + self.assertEqual(kwargs.get('username'), 'myuser') + self.assertEqual(kwargs.get('password'), 'my-api-key') + + def test_url_with_embedded_username_no_explicit_credentials(self): + kwargs = self._get_clone_kwargs( + url='https://myuser@bitbucket.org/workspace/repo.git' + ) + + self.assertEqual(kwargs.get('username'), None) + self.assertEqual(kwargs.get('password'), None) + + def test_public_repo_no_credentials(self): + kwargs = self._get_clone_kwargs( + url='https://github.com/public/repo.git' + ) + + self.assertEqual(kwargs.get('username'), None) + self.assertEqual(kwargs.get('password'), None)