Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit 038af97

Browse files
authored
True Tokenless for BA and TA (#882)
1 parent 1af0acf commit 038af97

File tree

6 files changed

+366
-9
lines changed

6 files changed

+366
-9
lines changed

codecov_auth/authentication/repo_auth.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from django.http import HttpRequest
99
from django.utils import timezone
1010
from jwt import PyJWTError
11-
from rest_framework import authentication, exceptions
11+
from rest_framework import authentication, exceptions, serializers
1212
from rest_framework.exceptions import NotAuthenticated
1313
from rest_framework.views import exception_handler
1414
from shared.django_apps.codecov_auth.models import Owner
@@ -345,7 +345,7 @@ def get_branch(self, request, repoid=None, commitid=None):
345345
body = json.loads(str(request.body, "utf8"))
346346

347347
# If commit is not created yet (ie first upload for this commit), we just validate branch format.
348-
# However if a commit exists already (ie not the first upload for this commit), we must additionally
348+
# However, if a commit exists already (ie not the first upload for this commit), we must additionally
349349
# validate the saved commit branch matches what is requested in this upload call.
350350
commit = Commit.objects.filter(repository_id=repoid, commitid=commitid).first()
351351
if commit and commit.branch != body.get("branch"):
@@ -381,10 +381,15 @@ def _get_info_from_request_path(
381381

382382
return repository, owner
383383

384+
def get_repository_and_owner(
385+
self, request: HttpRequest
386+
) -> tuple[Repository | None, Owner | None]:
387+
return self._get_info_from_request_path(request)
388+
384389
def authenticate(
385390
self, request: HttpRequest
386391
) -> tuple[RepositoryAsUser, TokenlessAuth] | None:
387-
repository, owner = self._get_info_from_request_path(request)
392+
repository, owner = self.get_repository_and_owner(request)
388393

389394
if (
390395
repository is None
@@ -398,3 +403,54 @@ def authenticate(
398403
RepositoryAsUser(repository),
399404
TokenlessAuth(repository),
400405
)
406+
407+
408+
class UploadTokenRequiredGetFromBodySerializer(serializers.Serializer):
409+
slug = serializers.CharField(required=True)
410+
service = serializers.CharField(required=False) # git_service from TA
411+
git_service = serializers.CharField(required=False) # git_service from BA
412+
413+
414+
class UploadTokenRequiredGetFromBodyAuthenticationCheck(
415+
UploadTokenRequiredAuthenticationCheck
416+
):
417+
"""
418+
Get Repository and Owner from request body instead of path,
419+
then use the same authenticate() as parent class.
420+
"""
421+
422+
def _get_git(self, validated_data):
423+
"""
424+
BA sends this in as git_service, TA sends this in as service.
425+
Use this function so this Check class can be used by both views.
426+
"""
427+
git_service = validated_data.get("git_service") or validated_data.get("service")
428+
return git_service
429+
430+
def _get_info_from_request_body(
431+
self, request: HttpRequest
432+
) -> tuple[Repository | None, Owner | None]:
433+
try:
434+
body = json.loads(str(request.body, "utf8"))
435+
436+
serializer = UploadTokenRequiredGetFromBodySerializer(data=body)
437+
438+
if serializer.is_valid():
439+
git_service = self._get_git(validated_data=serializer.validated_data)
440+
service_enum = Service(git_service)
441+
return get_repository_and_owner_from_string(
442+
service=service_enum,
443+
repo_identifier=serializer.validated_data["slug"],
444+
)
445+
446+
except (json.JSONDecodeError, ValueError):
447+
# exceptions raised by json.loads() and Service()
448+
# catch rather than raise to continue to next auth class
449+
pass
450+
451+
return None, None # continue to next auth class
452+
453+
def get_repository_and_owner(
454+
self, request: HttpRequest
455+
) -> tuple[Repository | None, Owner | None]:
456+
return self._get_info_from_request_body(request)

codecov_auth/tests/unit/test_repo_authentication.py

Lines changed: 229 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from jwt import PyJWTError
1111
from rest_framework import exceptions
1212
from rest_framework.test import APIRequestFactory
13+
from shared.django_apps.codecov_auth.models import Owner, Service
14+
from shared.django_apps.core.models import Repository
1315
from shared.django_apps.core.tests.factories import (
1416
CommitFactory,
1517
OwnerFactory,
@@ -27,6 +29,7 @@
2729
TokenlessAuth,
2830
TokenlessAuthentication,
2931
UploadTokenRequiredAuthenticationCheck,
32+
UploadTokenRequiredGetFromBodyAuthenticationCheck,
3033
)
3134
from codecov_auth.models import SERVICE_GITHUB, OrganizationLevelToken, RepositoryToken
3235

@@ -597,7 +600,7 @@ def test_token_not_required_unknown_owner(self, db):
597600
@pytest.mark.parametrize("request_uri,repo_slug,commitid", valid_params_to_test)
598601
@pytest.mark.parametrize("private", [False, True])
599602
@pytest.mark.parametrize("token_required", [False, True])
600-
def test_token_not_required_matches_paths(
603+
def test_get_repository_and_owner(
601604
self, request_uri, repo_slug, commitid, private, token_required, db
602605
):
603606
author_name, repo_name = repo_slug.split("/")
@@ -612,7 +615,7 @@ def test_token_not_required_matches_paths(
612615
request_uri, {"branch": "fork:branch"}, format="json"
613616
)
614617
authentication = UploadTokenRequiredAuthenticationCheck()
615-
assert authentication._get_info_from_request_path(request) == (
618+
assert authentication.get_repository_and_owner(request) == (
616619
repo,
617620
repo.author,
618621
)
@@ -670,3 +673,227 @@ def test_token_not_required_fork_branch_public_private(
670673
else:
671674
res = authentication.authenticate(request)
672675
assert res is None
676+
677+
678+
class TestUploadTokenRequiredGetFromBodyAuthenticationCheck(object):
679+
def test_token_not_required_invalid_data(self):
680+
request = APIRequestFactory().post(
681+
"/endpoint",
682+
data={"slug": 123, "git_service": "github"},
683+
format="json",
684+
)
685+
authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck()
686+
res = authentication.authenticate(request)
687+
assert res is None
688+
689+
def test_token_not_required_no_data(self):
690+
request = APIRequestFactory().post(
691+
"/endpoint",
692+
format="json",
693+
)
694+
authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck()
695+
res = authentication.authenticate(request)
696+
assert res is None
697+
698+
def test_token_not_required_no_git_service(self, db):
699+
owner = OwnerFactory(upload_token_required_for_public_repos=False)
700+
# their repo
701+
repo = RepositoryFactory(author=owner, private=False)
702+
request_uri = f"/upload/github/{owner.username}::::{repo.name}/commits"
703+
request = APIRequestFactory().post(
704+
request_uri,
705+
data={
706+
"slug": f"{owner.username}::::{repo.name}",
707+
},
708+
format="json",
709+
)
710+
authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck()
711+
assert authentication.get_repository_and_owner(request) == (None, None)
712+
res = authentication.authenticate(request)
713+
assert res is None
714+
715+
def test_token_not_required_unknown_repository(self, db):
716+
an_owner = OwnerFactory(upload_token_required_for_public_repos=False)
717+
# their repo
718+
RepositoryFactory(author=an_owner, private=False)
719+
request_uri = f"/upload/github/{an_owner.username}::::bad/commits"
720+
request = APIRequestFactory().post(
721+
request_uri,
722+
data={
723+
"slug": f"{an_owner.username}::::bad",
724+
"service": an_owner.service,
725+
},
726+
format="json",
727+
)
728+
authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck()
729+
assert authentication.get_repository_and_owner(request) == (None, None)
730+
res = authentication.authenticate(request)
731+
assert res is None
732+
733+
def test_token_not_required_unknown_owner(self, db):
734+
repo = RepositoryFactory(
735+
private=False, author__upload_token_required_for_public_repos=False
736+
)
737+
request_uri = f"/upload/github/bad::::{repo.name}/commits"
738+
request = APIRequestFactory().post(
739+
request_uri,
740+
data={
741+
"slug": f"bad::::{repo.name}",
742+
"service": "github",
743+
},
744+
format="json",
745+
)
746+
authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck()
747+
res = authentication.authenticate(request)
748+
assert res is None
749+
750+
@pytest.mark.parametrize("request_uri,repo_slug,commitid", valid_params_to_test)
751+
@pytest.mark.parametrize("private", [False, True])
752+
@pytest.mark.parametrize("token_required", [False, True])
753+
def test_get_repository_and_owner(
754+
self, request_uri, repo_slug, commitid, private, token_required, db
755+
):
756+
author_name, repo_name = repo_slug.split("/")
757+
repo = RepositoryFactory(
758+
name=repo_name,
759+
author__username=author_name,
760+
private=private,
761+
author__upload_token_required_for_public_repos=token_required,
762+
)
763+
assert repo.service == "github"
764+
request = APIRequestFactory().post(
765+
request_uri,
766+
data={"slug": f"{author_name}::::{repo_name}", "service": "github"},
767+
format="json",
768+
)
769+
authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck()
770+
771+
assert authentication.get_repository_and_owner(request) == (
772+
repo,
773+
repo.author,
774+
)
775+
776+
def test_get_repository_and_owner_with_service(self, db):
777+
authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck()
778+
repo_name = "the-repo"
779+
owner_username = "the-author"
780+
for name, _ in Service.choices:
781+
owner = OwnerFactory(service=name, username=owner_username)
782+
RepositoryFactory(name=repo_name, author=owner)
783+
784+
request = APIRequestFactory().post(
785+
"endpoint/",
786+
data={
787+
"slug": f"{owner_username}::::{repo_name}",
788+
"git_service": Service.BITBUCKET.value,
789+
},
790+
format="json",
791+
)
792+
matching_owner = Owner.objects.get(
793+
service=Service.BITBUCKET.value, username=owner_username
794+
)
795+
matching_repo = Repository.objects.get(
796+
name=repo_name, author__service=Service.BITBUCKET.value
797+
)
798+
assert authentication.get_repository_and_owner(request) == (
799+
matching_repo,
800+
matching_owner,
801+
)
802+
803+
request = APIRequestFactory().post(
804+
"endpoint/",
805+
data={
806+
"slug": f"{owner_username}::::{repo_name}",
807+
"git_service": Service.GITLAB.value,
808+
},
809+
format="json",
810+
)
811+
matching_owner = Owner.objects.get(
812+
service=Service.GITLAB.value, username=owner_username
813+
)
814+
matching_repo = Repository.objects.get(
815+
name=repo_name, author__service=Service.GITLAB.value
816+
)
817+
assert authentication.get_repository_and_owner(request) == (
818+
matching_repo,
819+
matching_owner,
820+
)
821+
822+
request = APIRequestFactory().post(
823+
"endpoint/",
824+
data={
825+
"slug": f"{owner_username}::::{repo_name}",
826+
"git_service": Service.GITHUB.value,
827+
},
828+
format="json",
829+
)
830+
matching_owner = Owner.objects.get(
831+
service=Service.GITHUB.value, username=owner_username
832+
)
833+
matching_repo = Repository.objects.get(
834+
name=repo_name, author__service=Service.GITHUB.value
835+
)
836+
assert authentication.get_repository_and_owner(request) == (
837+
matching_repo,
838+
matching_owner,
839+
)
840+
841+
@pytest.mark.parametrize("private", [False, True])
842+
@pytest.mark.parametrize("branch", ["branch", "fork:branch"])
843+
@pytest.mark.parametrize(
844+
"existing_commit,commit_branch",
845+
[(False, None), (True, "branch"), (True, "fork:branch")],
846+
)
847+
@pytest.mark.parametrize("token_required", [False, True])
848+
def test_token_not_required_fork_branch_public_private(
849+
self,
850+
db,
851+
mocker,
852+
private,
853+
branch,
854+
existing_commit,
855+
commit_branch,
856+
token_required,
857+
):
858+
repo = RepositoryFactory(
859+
private=private,
860+
author__upload_token_required_for_public_repos=token_required,
861+
)
862+
863+
if existing_commit:
864+
commit = CommitFactory()
865+
commit.branch = commit_branch
866+
commit.repository = repo
867+
commit.save()
868+
869+
request = APIRequestFactory().post(
870+
f"/upload/github/{repo.author.username}::::{repo.name}/commits/{commit.commitid}/reports/report_code/uploads",
871+
data={
872+
"slug": f"{repo.author.username}::::{repo.name}",
873+
"git_service": repo.author.service,
874+
},
875+
format="json",
876+
)
877+
878+
else:
879+
request = APIRequestFactory().post(
880+
f"/upload/github/{repo.author.username}::::{repo.name}/commits",
881+
data={
882+
"slug": f"{repo.author.username}::::{repo.name}",
883+
"git_service": repo.author.service,
884+
},
885+
format="json",
886+
)
887+
888+
authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck()
889+
890+
if not private and not token_required:
891+
res = authentication.authenticate(request)
892+
assert res is not None
893+
repo_as_user, auth_class = res
894+
895+
assert repo_as_user.is_authenticated() is True
896+
assert isinstance(auth_class, TokenlessAuth)
897+
else:
898+
res = authentication.authenticate(request)
899+
assert res is None

0 commit comments

Comments
 (0)