Skip to content

Commit d80087a

Browse files
authored
Allow erasing a foreign Repo (#1112)
1 parent ae83c2f commit d80087a

File tree

13 files changed

+102
-151
lines changed

13 files changed

+102
-151
lines changed

codecov/commands/base.py

+66-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
from django.conf import settings
12
from django.contrib.auth.models import AnonymousUser
23

3-
from codecov.commands.exceptions import MissingService
4+
import services.self_hosted as self_hosted
5+
from codecov.commands.exceptions import (
6+
MissingService,
7+
Unauthenticated,
8+
Unauthorized,
9+
ValidationError,
10+
)
11+
from codecov_auth.helpers import current_user_part_of_org
412
from codecov_auth.models import Owner, User
13+
from core.models import Repository
514

615

716
class BaseCommand:
@@ -44,3 +53,59 @@ def __init__(self, current_owner: Owner, service: str, current_user: User = None
4453

4554
if self.current_owner:
4655
self.current_user = self.current_owner.user
56+
57+
def ensure_is_admin(self, owner: Owner) -> None:
58+
"""
59+
Ensures that the `current_owner` is an admin of `owner`,
60+
or raise `Unauthorized` otherwise.
61+
"""
62+
63+
if not current_user_part_of_org(self.current_owner, owner):
64+
raise Unauthorized()
65+
66+
if settings.IS_ENTERPRISE:
67+
if not self_hosted.is_admin_owner(self.current_owner):
68+
raise Unauthorized()
69+
else:
70+
if not owner.is_admin(self.current_owner):
71+
raise Unauthorized()
72+
73+
def resolve_owner_and_repo(
74+
self,
75+
owner_username: str,
76+
repo_name: str,
77+
ensure_is_admin: bool = False,
78+
only_viewable: bool = False,
79+
only_active: bool = False,
80+
) -> tuple[Owner, Repository]:
81+
"""
82+
Resolves the `Owner` and `Repository` based on the passed `owner_username`
83+
and `repo_name` respectively.
84+
85+
If `ensure_is_admin` is set, this will also ensure that the `current_owner` is an
86+
admin on the resolved `Owner`.
87+
"""
88+
if ensure_is_admin and not self.current_user.is_authenticated:
89+
raise Unauthenticated()
90+
91+
owner = Owner.objects.filter(
92+
service=self.service, username=owner_username
93+
).first()
94+
95+
if not owner:
96+
raise ValidationError("Owner not found")
97+
98+
if ensure_is_admin:
99+
self.ensure_is_admin(owner)
100+
101+
repo_query = Repository.objects
102+
if only_viewable:
103+
repo_query = repo_query.viewable_repos(self.current_owner)
104+
if only_active:
105+
repo_query = repo_query.filter(active=True)
106+
107+
repo = repo_query.filter(author=owner, name=repo_name).first()
108+
if not repo:
109+
raise ValidationError("Repo not found")
110+
111+
return (owner, repo)

core/commands/component/interactors/delete_component_measurements.py

+3-36
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,12 @@
1-
from django.conf import settings
2-
3-
import services.self_hosted as self_hosted
41
from codecov.commands.base import BaseInteractor
5-
from codecov.commands.exceptions import (
6-
Unauthenticated,
7-
Unauthorized,
8-
ValidationError,
9-
)
10-
from codecov_auth.models import Owner
11-
from core.models import Repository
122
from services.task import TaskService
133

144

155
class DeleteComponentMeasurementsInteractor(BaseInteractor):
16-
def validate(self, owner: Owner, repo: Repository):
17-
if not self.current_user.is_authenticated:
18-
raise Unauthenticated()
19-
20-
if not owner:
21-
raise ValidationError("Owner not found")
22-
23-
if not repo:
24-
raise ValidationError("Repo not found")
25-
26-
if settings.IS_ENTERPRISE:
27-
if not self_hosted.is_admin_owner(self.current_owner):
28-
raise Unauthorized()
29-
else:
30-
if not owner.is_admin(self.current_owner):
31-
raise Unauthorized()
32-
336
def execute(self, owner_username: str, repo_name: str, component_id: str):
34-
owner = Owner.objects.filter(
35-
service=self.service, username=owner_username
36-
).first()
37-
38-
repo = None
39-
if owner:
40-
repo = Repository.objects.filter(author_id=owner.pk, name=repo_name).first()
41-
42-
self.validate(owner, repo)
7+
_owner, repo = self.resolve_owner_and_repo(
8+
owner_username, repo_name, ensure_is_admin=True
9+
)
4310

4411
TaskService().delete_component_measurements(
4512
repo.repoid,

core/commands/component/tests/test_component.py

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class ComponentCommandsTest(TransactionTestCase):
1717
def setUp(self):
1818
self.owner = OwnerFactory(username="test-user")
1919
self.org = OwnerFactory(username="test-org", admins=[self.owner.pk])
20+
self.owner.organizations = [self.org.pk]
2021
self.repo = RepositoryFactory(author=self.org)
2122
self.command = ComponentCommands(self.owner, "github")
2223

core/commands/flag/interactors/delete_flag.py

+3-34
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,15 @@
1-
from django.conf import settings
2-
3-
import services.self_hosted as self_hosted
41
from codecov.commands.base import BaseInteractor
52
from codecov.commands.exceptions import (
63
NotFound,
7-
Unauthenticated,
8-
Unauthorized,
9-
ValidationError,
104
)
11-
from codecov_auth.models import Owner
12-
from core.models import Repository
135
from reports.models import RepositoryFlag
146

157

168
class DeleteFlagInteractor(BaseInteractor):
17-
def validate(self, owner: Owner, repo: Repository):
18-
if not self.current_user.is_authenticated:
19-
raise Unauthenticated()
20-
21-
if not owner:
22-
raise ValidationError("Owner not found")
23-
24-
if not repo:
25-
raise ValidationError("Repo not found")
26-
27-
if settings.IS_ENTERPRISE:
28-
if not self_hosted.is_admin_owner(self.current_owner):
29-
raise Unauthorized()
30-
else:
31-
if not owner.is_admin(self.current_owner):
32-
raise Unauthorized()
33-
349
def execute(self, owner_username: str, repo_name: str, flag_name: str):
35-
owner = Owner.objects.filter(
36-
service=self.service, username=owner_username
37-
).first()
38-
39-
repo = None
40-
if owner:
41-
repo = Repository.objects.filter(author_id=owner.pk, name=repo_name).first()
42-
43-
self.validate(owner, repo)
10+
_owner, repo = self.resolve_owner_and_repo(
11+
owner_username, repo_name, ensure_is_admin=True
12+
)
4413

4514
flag = RepositoryFlag.objects.filter(
4615
repository_id=repo.pk, flag_name=flag_name

core/commands/flag/tests/test_flag.py

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class FlagCommandsTest(TransactionTestCase):
1818
def setUp(self):
1919
self.owner = OwnerFactory(username="test-user")
2020
self.org = OwnerFactory(username="test-org", admins=[self.owner.pk])
21+
self.owner.organizations = [self.org.pk]
2122
self.repo = RepositoryFactory(author=self.org)
2223
self.command = FlagCommands(self.owner, "github")
2324
self.flag = RepositoryFlagFactory(repository=self.repo, flag_name="test-flag")

core/commands/repository/interactors/activate_measurements.py

+6-15
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,21 @@
33
from codecov.commands.base import BaseInteractor
44
from codecov.commands.exceptions import ValidationError
55
from codecov.db import sync_to_async
6-
from codecov_auth.models import Owner
7-
from core.models import Repository
86
from timeseries.helpers import trigger_backfill
97
from timeseries.models import Dataset, MeasurementName
108

119

1210
class ActivateMeasurementsInteractor(BaseInteractor):
13-
def validate(self, repo):
14-
if not repo:
15-
raise ValidationError("Repo not found")
16-
if not settings.TIMESERIES_ENABLED:
17-
raise ValidationError("Timeseries storage not enabled")
18-
1911
@sync_to_async
2012
def execute(
2113
self, repo_name: str, owner_name: str, measurement_type: MeasurementName
22-
):
23-
author = Owner.objects.filter(username=owner_name, service=self.service).first()
24-
repo = (
25-
Repository.objects.viewable_repos(self.current_owner)
26-
.filter(author=author, name=repo_name, active=True)
27-
.first()
14+
) -> Dataset:
15+
if not settings.TIMESERIES_ENABLED:
16+
raise ValidationError("Timeseries storage not enabled")
17+
18+
_owner, repo = self.resolve_owner_and_repo(
19+
owner_name, repo_name, only_viewable=True, only_active=True
2820
)
29-
self.validate(repo)
3021

3122
dataset, created = Dataset.objects.get_or_create(
3223
name=measurement_type.value,
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,14 @@
1-
from django.conf import settings
2-
3-
import services.self_hosted as self_hosted
41
from codecov.commands.base import BaseInteractor
5-
from codecov.commands.exceptions import Unauthorized, ValidationError
62
from codecov.db import sync_to_async
7-
from codecov_auth.helpers import current_user_part_of_org
8-
from codecov_auth.models import Owner
9-
from core.models import Repository
103
from services.task.task import TaskService
114

125

136
class EraseRepositoryInteractor(BaseInteractor):
14-
def validate_owner(self, owner: Owner) -> None:
15-
if not current_user_part_of_org(self.current_owner, owner):
16-
raise Unauthorized()
17-
18-
if settings.IS_ENTERPRISE:
19-
if not self_hosted.is_admin_owner(self.current_owner):
20-
raise Unauthorized()
21-
else:
22-
if not owner.is_admin(self.current_owner):
23-
raise Unauthorized()
24-
257
@sync_to_async
26-
def execute(self, repo_name: str, owner: Owner) -> None:
27-
self.validate_owner(owner)
28-
repo = Repository.objects.filter(author_id=owner.pk, name=repo_name).first()
29-
if not repo:
30-
raise ValidationError("Repo not found")
8+
def execute(self, owner_username: str, repo_name: str) -> None:
9+
_owner, repo = self.resolve_owner_and_repo(
10+
owner_username, repo_name, ensure_is_admin=True
11+
)
12+
3113
TaskService().delete_timeseries(repository_id=repo.repoid)
3214
TaskService().flush_repo(repository_id=repo.repoid)

core/commands/repository/interactors/regenerate_repository_token.py

+3-12
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
11
from codecov.commands.base import BaseInteractor
2-
from codecov.commands.exceptions import ValidationError
32
from codecov.db import sync_to_async
4-
from codecov_auth.models import Owner, RepositoryToken
5-
from core.models import Repository
3+
from codecov_auth.models import RepositoryToken
64

75

86
class RegenerateRepositoryTokenInteractor(BaseInteractor):
97
@sync_to_async
108
def execute(self, repo_name: str, owner_username: str, token_type: str):
11-
author = Owner.objects.filter(
12-
username=owner_username, service=self.service
13-
).first()
14-
repo = (
15-
Repository.objects.viewable_repos(self.current_owner)
16-
.filter(author=author, name=repo_name, active=True)
17-
.first()
9+
_owner, repo = self.resolve_owner_and_repo(
10+
owner_username, repo_name, only_viewable=True, only_active=True
1811
)
19-
if not repo:
20-
raise ValidationError("Repo not found")
2112

2213
token, created = RepositoryToken.objects.get_or_create(
2314
repository_id=repo.repoid, token_type=token_type
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,16 @@
11
import uuid
22

33
from codecov.commands.base import BaseInteractor
4-
from codecov.commands.exceptions import ValidationError
54
from codecov.db import sync_to_async
6-
from codecov_auth.models import Owner
7-
from core.models import Repository
85

96

107
class RegenerateRepositoryUploadTokenInteractor(BaseInteractor):
118
@sync_to_async
129
def execute(self, repo_name: str, owner_username: str) -> uuid.UUID:
13-
author = Owner.objects.filter(
14-
username=owner_username, service=self.service
15-
).first()
16-
repo = (
17-
Repository.objects.viewable_repos(self.current_owner)
18-
.filter(author=author, name=repo_name)
19-
.first()
10+
_owner, repo = self.resolve_owner_and_repo(
11+
owner_username, repo_name, only_viewable=True
2012
)
21-
if not repo:
22-
raise ValidationError("Repo not found")
13+
2314
repo.upload_token = uuid.uuid4()
2415
repo.save()
2516
return repo.upload_token

core/commands/repository/interactors/tests/test_erase_repository.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,12 @@ def setUp(self):
1515

1616
def execute_unauthorized_owner(self):
1717
return EraseRepositoryInteractor(self.owner, "github").execute(
18-
repo_name="repo-1",
19-
owner=self.random_user,
18+
self.random_user.username, "repo-1"
2019
)
2120

2221
def execute_user_not_admin(self):
2322
return EraseRepositoryInteractor(self.non_admin_user, "github").execute(
24-
repo_name="repo-1",
25-
owner=self.owner,
23+
self.owner.username, "repo-1"
2624
)
2725

2826
async def test_when_validation_error_unauthorized_owner_not_part_of_org(self):

core/commands/repository/interactors/update_bundle_cache_config.py

+2-11
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,13 @@
88
from codecov.commands.base import BaseInteractor
99
from codecov.commands.exceptions import ValidationError
1010
from codecov.db import sync_to_async
11-
from codecov_auth.models import Owner
1211
from core.models import Repository
1312

1413

1514
class UpdateBundleCacheConfigInteractor(BaseInteractor):
1615
def validate(
1716
self, repo: Repository, cache_config: List[Dict[str, str | bool]]
1817
) -> None:
19-
if not repo:
20-
raise ValidationError("Repo not found")
21-
2218
# Find any missing bundle names
2319
bundle_names = [
2420
bundle["bundle_name"]
@@ -44,13 +40,8 @@ def execute(
4440
repo_name: str,
4541
cache_config: List[Dict[str, str | bool]],
4642
) -> List[Dict[str, str | bool]]:
47-
author = Owner.objects.filter(
48-
username=owner_username, service=self.service
49-
).first()
50-
repo = (
51-
Repository.objects.viewable_repos(self.current_owner)
52-
.filter(author=author, name=repo_name)
53-
.first()
43+
_owner, repo = self.resolve_owner_and_repo(
44+
owner_username, repo_name, only_viewable=True
5445
)
5546

5647
self.validate(repo, cache_config)

core/commands/repository/repository.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,10 @@ def activate_measurements(
7979
repo_name, owner_name, measurement_type
8080
)
8181

82-
def erase_repository(self, repo_name: str, owner: Owner) -> None:
83-
return self.get_interactor(EraseRepositoryInteractor).execute(repo_name, owner)
82+
def erase_repository(self, owner_username: str, repo_name: str) -> None:
83+
return self.get_interactor(EraseRepositoryInteractor).execute(
84+
owner_username, repo_name
85+
)
8486

8587
def encode_secret_string(self, owner: Owner, repo_name: str, value: str) -> str:
8688
return self.get_interactor(EncodeSecretStringInteractor).execute(

graphql_api/types/mutation/erase_repository/erase_repository.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ async def resolve_erase_repository(
1818
command = info.context["executor"].get_command("repository")
1919
current_owner = info.context["request"].current_owner
2020
repo_name = input.get("repo_name")
21-
await command.erase_repository(repo_name=repo_name, owner=current_owner)
21+
# TODO: change the graphql mutation to allow working on other owners
22+
owner_username = current_owner.username
23+
await command.erase_repository(owner_username, repo_name)
2224
return None
2325

2426

0 commit comments

Comments
 (0)