From 17ed18152de314bf0d459d6649fe5774c89b6037 Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Mon, 3 Feb 2025 10:20:56 -0500 Subject: [PATCH 1/5] fix: token retrieval error when config is not setup --- codecov_auth/authentication/repo_auth.py | 6 +++--- upload/helpers.py | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/codecov_auth/authentication/repo_auth.py b/codecov_auth/authentication/repo_auth.py index 21e1c07074..d1580817a4 100644 --- a/codecov_auth/authentication/repo_auth.py +++ b/codecov_auth/authentication/repo_auth.py @@ -9,7 +9,7 @@ from django.utils import timezone from jwt import PyJWTError from rest_framework import authentication, exceptions, serializers -from rest_framework.exceptions import NotAuthenticated +from rest_framework.exceptions import NotAuthenticated, ValidationError from rest_framework.response import Response from rest_framework.views import exception_handler from shared.django_apps.codecov_auth.models import Owner @@ -200,7 +200,7 @@ def authenticate( if not using_global_token: return None # continue to next auth class - service = global_tokens[token] + service = global_tokens.get(token, "") upload_info = get_upload_info_from_request_path(request) if upload_info is None: return None # continue to next auth class @@ -257,7 +257,7 @@ def authenticate_credentials( try: repository = get_repo_with_github_actions_oidc_token(token) - except (ObjectDoesNotExist, PyJWTError): + except (ObjectDoesNotExist, PyJWTError, ValidationError): return None # continue to next auth class log.info( diff --git a/upload/helpers.py b/upload/helpers.py index 9e6cae583a..eec4c4665e 100644 --- a/upload/helpers.py +++ b/upload/helpers.py @@ -243,6 +243,10 @@ def get_repo_with_github_actions_oidc_token(token: str) -> Repository: else: service = "github_enterprise" github_enterprise_url = get_config("github_enterprise", "url") + if not github_enterprise_url: + raise ValidationError( + "GitHub Enterprise URL configuration is not set configuration" + ) # remove trailing slashes if present github_enterprise_url = re.sub(r"/+$", "", github_enterprise_url) jwks_url = f"{github_enterprise_url}/_services/token/.well-known/jwks" @@ -525,7 +529,7 @@ def insert_commit( return commit -def get_global_tokens() -> Dict[str, Any]: +def get_global_tokens() -> Dict[str | None, Any]: """ Enterprise only: check the config to see if global tokens were set for this organization's uploads. From 260978da9cfac66ea341b13bf3d6b1cd72ced125 Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Mon, 3 Feb 2025 11:00:20 -0500 Subject: [PATCH 2/5] add test --- upload/tests/views/test_uploads.py | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/upload/tests/views/test_uploads.py b/upload/tests/views/test_uploads.py index e55ea2576d..64757ec31a 100644 --- a/upload/tests/views/test_uploads.py +++ b/upload/tests/views/test_uploads.py @@ -929,3 +929,49 @@ def test_uploads_post_github_enterprise_oidc_auth_jwks_url( mock_jwks_client.assert_called_with( "https://example.com/_services/token/.well-known/jwks" ) + + def test_uploads_post_github_enterprise_oidc_auth_no_url(self): + # Mock the configuration to set the URL to None + mock_config_helper(self.mocker, configs={"github_enterprise.url": None}) + + # Mock necessary services + self.mocker.patch( + "shared.api_archive.archive.StorageService.create_presigned_put", + return_value="presigned put", + ) + self.mocker.patch("upload.views.uploads.trigger_upload_task", return_value=True) + + repository = RepositoryFactory( + name="the_repo", + author__username="codecov", + author__service="github_enterprise", + author__upload_token_required_for_public_repos=True, + private=False, + ) + token = "ThisValueDoesNotMatterBecauseOf_mock_jwt_decode" + + commit = CommitFactory(repository=repository) + commit_report = CommitReport.objects.create(commit=commit, code="code") + + client = APIClient() + url = reverse( + "new_upload.uploads", + args=[ + "github_enterprise", + "codecov::::the_repo", + commit.commitid, + commit_report.code, + ], + ) + response = client.post( + url, + { + "state": "uploaded", + "flags": ["flag1", "flag2"], + "version": "version", + }, + headers={"Authorization": f"token {token}"}, + ) + + assert response.status_code == 401 + assert response.json().get("detail") == "Not valid tokenless upload" From 9b6abed40dbcae30224f4a29140a874142da0f7a Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Mon, 3 Feb 2025 11:55:48 -0500 Subject: [PATCH 3/5] fix test --- upload/tests/views/test_uploads.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/upload/tests/views/test_uploads.py b/upload/tests/views/test_uploads.py index 64757ec31a..bd9a452b80 100644 --- a/upload/tests/views/test_uploads.py +++ b/upload/tests/views/test_uploads.py @@ -930,11 +930,16 @@ def test_uploads_post_github_enterprise_oidc_auth_jwks_url( "https://example.com/_services/token/.well-known/jwks" ) - def test_uploads_post_github_enterprise_oidc_auth_no_url(self): - # Mock the configuration to set the URL to None + @patch("upload.views.uploads.AnalyticsService") + @patch("upload.helpers.jwt.decode") + @patch("upload.helpers.PyJWKClient") + def test_uploads_post_github_enterprise_oidc_auth_no_url( + self, + mock_jwks_client, + mock_jwt_decode, + analytics_service_mock, + ): mock_config_helper(self.mocker, configs={"github_enterprise.url": None}) - - # Mock necessary services self.mocker.patch( "shared.api_archive.archive.StorageService.create_presigned_put", return_value="presigned put", @@ -948,6 +953,12 @@ def test_uploads_post_github_enterprise_oidc_auth_no_url(self): author__upload_token_required_for_public_repos=True, private=False, ) + mock_jwt_decode.return_value = { + "repository": f"url/{repository.name}", + "repository_owner": repository.author.username, + "iss": "https://enterprise-client.actions.githubusercontent.com", + "audience": [settings.CODECOV_API_URL], + } token = "ThisValueDoesNotMatterBecauseOf_mock_jwt_decode" commit = CommitFactory(repository=repository) @@ -972,6 +983,5 @@ def test_uploads_post_github_enterprise_oidc_auth_no_url(self): }, headers={"Authorization": f"token {token}"}, ) - assert response.status_code == 401 assert response.json().get("detail") == "Not valid tokenless upload" From 1e3ad742ec4e2d9a5698af315e3b6d32fbd63499 Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Mon, 3 Feb 2025 14:39:56 -0500 Subject: [PATCH 4/5] token issuer contains the string --- upload/helpers.py | 2 +- upload/tests/test_helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/upload/helpers.py b/upload/helpers.py index eec4c4665e..056d1c9df2 100644 --- a/upload/helpers.py +++ b/upload/helpers.py @@ -237,7 +237,7 @@ def parse_params(data: Dict[str, Any]) -> Dict[str, Any]: def get_repo_with_github_actions_oidc_token(token: str) -> Repository: unverified_contents = jwt.decode(token, options={"verify_signature": False}) token_issuer = str(unverified_contents.get("iss")) - if token_issuer == "https://token.actions.githubusercontent.com": + if "https://token.actions.githubusercontent.com" in token_issuer: service = "github" jwks_url = "https://token.actions.githubusercontent.com/.well-known/jwks" else: diff --git a/upload/tests/test_helpers.py b/upload/tests/test_helpers.py index 0f2e8c1b75..a0f6f77cb6 100644 --- a/upload/tests/test_helpers.py +++ b/upload/tests/test_helpers.py @@ -337,7 +337,7 @@ def test_determine_repo_for_upload_github_actions(codecov_vcr): repository = RepositoryFactory.create() token = jwt.encode( { - "iss": "https://token.actions.githubusercontent.com", + "iss": "https://token.actions.githubusercontent.com/abcdefg", "aud": [f"{settings.CODECOV_API_URL}"], "repository": f"{repository.author.username}/{repository.name}", "repository_owner": repository.author.username, From 240e98a6d3b23d9dc6924a15d795d7b8c2943704 Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Mon, 3 Feb 2025 14:59:15 -0500 Subject: [PATCH 5/5] use urlparse --- upload/helpers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/upload/helpers.py b/upload/helpers.py index 056d1c9df2..1c463eccab 100644 --- a/upload/helpers.py +++ b/upload/helpers.py @@ -2,6 +2,7 @@ import re from json import dumps from typing import Any, Dict, Optional +from urllib.parse import urlparse import jwt from asgiref.sync import async_to_sync @@ -237,16 +238,15 @@ def parse_params(data: Dict[str, Any]) -> Dict[str, Any]: def get_repo_with_github_actions_oidc_token(token: str) -> Repository: unverified_contents = jwt.decode(token, options={"verify_signature": False}) token_issuer = str(unverified_contents.get("iss")) - if "https://token.actions.githubusercontent.com" in token_issuer: + parsed_url = urlparse(token_issuer) + if parsed_url.hostname == "token.actions.githubusercontent.com": service = "github" jwks_url = "https://token.actions.githubusercontent.com/.well-known/jwks" else: service = "github_enterprise" github_enterprise_url = get_config("github_enterprise", "url") if not github_enterprise_url: - raise ValidationError( - "GitHub Enterprise URL configuration is not set configuration" - ) + raise ValidationError("GitHub Enterprise URL configuration is not set") # remove trailing slashes if present github_enterprise_url = re.sub(r"/+$", "", github_enterprise_url) jwks_url = f"{github_enterprise_url}/_services/token/.well-known/jwks"