mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 00:14:02 +00:00
Read private repositories only with the user's own GitHub token
GITHUB_ACCESS_TOKEN belongs to the server, but any user could ingest any repository it can see, private ones included. It is now used only for public repositories; the loader checks the repository's visibility first and asks for a GitHub connection otherwise. The loader also takes a token from the connection a source syncs from. Merging a connection's keys into a plain repository URL no longer fails on json.loads, manual Sync now passes the source's connection, and a token GitHub rejects pauses the connection's sources for reconnect.
This commit is contained in:
1 parent
35d6d80e87
commit
3572d968ec
9 files changed
+357
-22
No files matched your search
@@ -512,3 +512,107 @@ class TestGitHubLoaderStaleTokenFallback:
|
||||
assert mock_get.call_count == 2
|
||||
# Second attempt carried no Authorization header.
|
||||
assert "Authorization" not in mock_get.call_args.kwargs["headers"]
|
||||
|
||||
|
||||
def _stub_repo(loader, monkeypatch):
|
||||
monkeypatch.setattr(loader, "get_default_branch", lambda repo: "main")
|
||||
monkeypatch.setattr(loader, "fetch_repo_tree", lambda repo, branch: ([("README.md", 5)], False))
|
||||
monkeypatch.setattr(loader, "fetch_file_content", lambda repo, path: "readme")
|
||||
|
||||
|
||||
class TestGitHubLoaderConnectionToken:
|
||||
def test_uses_the_connections_token(self, monkeypatch):
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
monkeypatch.setattr(settings, "GITHUB_ACCESS_TOKEN", "instance-token")
|
||||
loader = GitHubLoader()
|
||||
_stub_repo(loader, monkeypatch)
|
||||
with patch("docsgpt.parser.remote.github_loader.requests.get") as get:
|
||||
docs = loader.load_data({"repo_url": "owner/private-repo", "access_token": "user-token"})
|
||||
# A user's own token reads their private repos: no public check.
|
||||
get.assert_not_called()
|
||||
assert loader.headers["Authorization"] == "Bearer user-token"
|
||||
assert [d.doc_id for d in docs] == ["README.md"]
|
||||
|
||||
def test_accepts_a_url_key_and_a_plain_string(self, monkeypatch):
|
||||
loader = GitHubLoader()
|
||||
_stub_repo(loader, monkeypatch)
|
||||
assert loader.load_data({"url": "https://github.com/owner/repo"})[0].extra_info["source"].startswith(
|
||||
"https://github.com/owner/repo/"
|
||||
)
|
||||
assert loader.load_data("owner/repo")
|
||||
|
||||
@patch("docsgpt.parser.remote.github_loader.requests.get")
|
||||
def test_rejected_connection_token_is_not_retried_anonymously(self, mock_get):
|
||||
from docsgpt.parser.remote.github_loader import GitHubTokenRejected
|
||||
|
||||
loader = GitHubLoader(access_token="revoked")
|
||||
mock_get.return_value = MagicMock(status_code=401)
|
||||
with pytest.raises(GitHubTokenRejected):
|
||||
loader._make_request("https://api.github.com/repos/o/r")
|
||||
assert mock_get.call_count == 1
|
||||
|
||||
|
||||
class TestGitHubLoaderInstanceTokenIsPublicOnly:
|
||||
"""GITHUB_ACCESS_TOKEN is shared by every user: it must never read a private repo."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _instance_token(self, monkeypatch):
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
monkeypatch.setattr(settings, "GITHUB_ACCESS_TOKEN", "instance-token")
|
||||
|
||||
@patch("docsgpt.parser.remote.github_loader.requests.get")
|
||||
def test_private_repo_is_refused(self, mock_get, monkeypatch):
|
||||
from docsgpt.parser.remote.github_loader import PrivateRepositoryError
|
||||
|
||||
loader = GitHubLoader()
|
||||
_stub_repo(loader, monkeypatch)
|
||||
fetched = MagicMock()
|
||||
monkeypatch.setattr(loader, "fetch_file_content", fetched)
|
||||
mock_get.return_value = make_response({"private": True, "visibility": "private"})
|
||||
|
||||
with pytest.raises(PrivateRepositoryError, match="Connect"):
|
||||
loader.load_data("https://github.com/acme/secret")
|
||||
fetched.assert_not_called()
|
||||
|
||||
@pytest.mark.parametrize("status", [404, 403])
|
||||
@patch("docsgpt.parser.remote.github_loader.requests.get")
|
||||
def test_unreadable_repo_is_refused(self, mock_get, status, monkeypatch):
|
||||
from docsgpt.parser.remote.github_loader import PrivateRepositoryError
|
||||
|
||||
loader = GitHubLoader()
|
||||
_stub_repo(loader, monkeypatch)
|
||||
mock_get.return_value = make_response({"message": "Not Found"}, status_code=status)
|
||||
with pytest.raises(PrivateRepositoryError):
|
||||
loader.load_data("acme/secret")
|
||||
|
||||
@patch("docsgpt.parser.remote.github_loader.requests.get")
|
||||
def test_internal_repo_is_refused(self, mock_get, monkeypatch):
|
||||
from docsgpt.parser.remote.github_loader import PrivateRepositoryError
|
||||
|
||||
loader = GitHubLoader()
|
||||
_stub_repo(loader, monkeypatch)
|
||||
mock_get.return_value = make_response({"private": False, "visibility": "internal"})
|
||||
with pytest.raises(PrivateRepositoryError):
|
||||
loader.load_data("acme/internal")
|
||||
|
||||
@patch("docsgpt.parser.remote.github_loader.requests.get")
|
||||
def test_public_repo_uses_the_instance_token(self, mock_get, monkeypatch):
|
||||
loader = GitHubLoader()
|
||||
_stub_repo(loader, monkeypatch)
|
||||
mock_get.return_value = make_response({"private": False, "visibility": "public"})
|
||||
|
||||
docs = loader.load_data("owner/repo")
|
||||
|
||||
assert [d.doc_id for d in docs] == ["README.md"]
|
||||
assert mock_get.call_args.kwargs["headers"]["Authorization"] == "Bearer instance-token"
|
||||
|
||||
@patch("docsgpt.parser.remote.github_loader.requests.get")
|
||||
def test_stale_instance_token_reads_public_repos_anonymously(self, mock_get, monkeypatch):
|
||||
loader = GitHubLoader()
|
||||
_stub_repo(loader, monkeypatch)
|
||||
mock_get.side_effect = [MagicMock(status_code=401), make_response({"private": False})]
|
||||
|
||||
assert loader.load_data("owner/repo")
|
||||
assert "Authorization" not in loader.headers
|
||||
Reference in new issue
Block a user