Skip to content

Commit 4bf8d00

Browse files
feat(api): Add org membership check to repository token regeneration
The RegenerateRepositoryToken and RegenerateRepositoryUploadToken mutations only checked that a repo was viewable, which includes all public repos. Any authenticated user could rotate tokens for public repos they don't belong to. Add current_user_part_of_org check to both mutations and expose UnauthorizedError in their GraphQL schemas. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ec58642 commit 4bf8d00

File tree

6 files changed

+53
-8
lines changed

6 files changed

+53
-8
lines changed

apps/codecov-api/core/commands/repository/interactors/regenerate_repository_token.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
from asgiref.sync import sync_to_async
22

33
from codecov.commands.base import BaseInteractor
4+
from codecov.commands.exceptions import Unauthorized
5+
from codecov_auth.helpers import current_user_part_of_org
46
from codecov_auth.models import RepositoryToken
57

68

79
class RegenerateRepositoryTokenInteractor(BaseInteractor):
810
@sync_to_async
911
def execute(self, repo_name: str, owner_username: str, token_type: str):
10-
_owner, repo = self.resolve_owner_and_repo(
12+
owner, repo = self.resolve_owner_and_repo(
1113
owner_username, repo_name, only_viewable=True, only_active=True
1214
)
1315

16+
if not current_user_part_of_org(self.current_owner, owner):
17+
raise Unauthorized()
18+
1419
token, created = RepositoryToken.objects.get_or_create(
1520
repository_id=repo.repoid, token_type=token_type
1621
)

apps/codecov-api/core/commands/repository/interactors/regenerate_repository_upload_token.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@
33
from asgiref.sync import sync_to_async
44

55
from codecov.commands.base import BaseInteractor
6+
from codecov.commands.exceptions import Unauthorized
7+
from codecov_auth.helpers import current_user_part_of_org
68

79

810
class RegenerateRepositoryUploadTokenInteractor(BaseInteractor):
911
@sync_to_async
1012
def execute(self, repo_name: str, owner_username: str) -> uuid.UUID:
11-
_owner, repo = self.resolve_owner_and_repo(
13+
owner, repo = self.resolve_owner_and_repo(
1214
owner_username, repo_name, only_viewable=True
1315
)
1416

17+
if not current_user_part_of_org(self.current_owner, owner):
18+
raise Unauthorized()
19+
1520
repo.upload_token = uuid.uuid4()
1621
repo.save()
1722
return repo.upload_token

apps/codecov-api/graphql_api/tests/mutation/test_regenerate_repository_token.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
class RegeneratRepositoryTokenTests(GraphQLTestHelper, TestCase):
2626
def setUp(self):
2727
self.org = OwnerFactory(username="codecov")
28-
self.repo = RepositoryFactory(author=self.org, name="gazebo", active=True)
28+
self.repo = RepositoryFactory(
29+
author=self.org, name="gazebo", active=True, private=False
30+
)
2931

3032
def test_when_unauthenticated(self):
3133
data = self.gql_request(
@@ -43,14 +45,35 @@ def test_when_unauthenticated(self):
4345
== "UnauthenticatedError"
4446
)
4547

48+
def test_when_unauthorized_user_not_part_of_org(self):
49+
random_user = OwnerFactory()
50+
data = self.gql_request(
51+
query,
52+
owner=random_user,
53+
variables={
54+
"input": {
55+
"repoName": "gazebo",
56+
"owner": "codecov",
57+
"tokenType": "PROFILING",
58+
}
59+
},
60+
)
61+
assert (
62+
data["regenerateRepositoryToken"]["error"]["__typename"]
63+
== "UnauthorizedError"
64+
)
65+
4666
def test_when_validation_error_repo_not_viewable(self):
67+
private_repo = RepositoryFactory(
68+
author=self.org, name="private-repo", active=True, private=True
69+
)
4770
random_user = OwnerFactory(organizations=[self.org.ownerid])
4871
data = self.gql_request(
4972
query,
5073
owner=random_user,
5174
variables={
5275
"input": {
53-
"repoName": "gazebo",
76+
"repoName": "private-repo",
5477
"owner": "codecov",
5578
"tokenType": "PROFILING",
5679
}

apps/codecov-api/graphql_api/tests/mutation/test_regenerate_repository_upload_token.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,21 @@
2121
class RegenerateRepositoryUploadTokenTests(GraphQLTestHelper, TestCase):
2222
def setUp(self):
2323
self.org = OwnerFactory(username="codecov")
24-
self.repo = RepositoryFactory(author=self.org, name="gazebo")
24+
self.repo = RepositoryFactory(author=self.org, name="gazebo", private=False)
2525
self.old_repo_token = self.repo.upload_token
2626

27+
def test_when_unauthorized_user_not_part_of_org(self):
28+
random_user = OwnerFactory()
29+
data = self.gql_request(
30+
query,
31+
owner=random_user,
32+
variables={"input": {"repoName": "gazebo", "owner": "codecov"}},
33+
)
34+
assert (
35+
data["regenerateRepositoryUploadToken"]["error"]["__typename"]
36+
== "UnauthorizedError"
37+
)
38+
2739
def test_when_authenticated_updates_token(self):
2840
user = OwnerFactory(
2941
organizations=[self.org.ownerid], permission=[self.repo.repoid]

apps/codecov-api/graphql_api/types/mutation/regenerate_repository_token/regenerate_repository_token.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
union RegenerateRepositoryTokenError = UnauthenticatedError | ValidationError
1+
union RegenerateRepositoryTokenError = UnauthenticatedError | UnauthorizedError | ValidationError
22

33
type RegenerateRepositoryTokenPayload {
44
token: String
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
union RegenerateRepositoryUploadTokenError = ValidationError
1+
union RegenerateRepositoryUploadTokenError = UnauthorizedError | ValidationError
22

33
type RegenerateRepositoryUploadTokenPayload {
44
token: String
5-
error: ValidationError
5+
error: RegenerateRepositoryUploadTokenError
66
}

0 commit comments

Comments
 (0)