Skip to content

Commit 4edf43e

Browse files
committed
WIP: Permission framework migration
Convert more code from team services to the experimental permission framework
1 parent 944847e commit 4edf43e

File tree

1 file changed

+139
-53
lines changed
  • django/thunderstore/api/cyberstorm/services

1 file changed

+139
-53
lines changed

django/thunderstore/api/cyberstorm/services/team.py

Lines changed: 139 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import dataclasses
2-
from typing import Any, Generator, List, Optional, Sequence
2+
from typing import List, Optional, Sequence, Union
33

44
from django.core.exceptions import ValidationError
55
from django.db import transaction
@@ -12,109 +12,195 @@
1212
from thunderstore.repository.models.team import TeamMemberRole
1313

1414

15+
class CheckException(ValidationError):
16+
pass
17+
18+
1519
@dataclasses.dataclass
1620
class CheckResult:
17-
check_pass: bool
21+
success: bool
1822
errors: Optional[List[Exception]]
1923

24+
def as_exception(self) -> Union[CheckException, ValidationError, Exception]:
25+
if len(self.errors) == 1:
26+
return self.errors[0]
27+
else:
28+
return CheckException(message=self.errors)
2029

21-
class OpCheck:
22-
"""OperationCheck"""
2330

24-
def run_check(self, _: UserType) -> CheckResult:
31+
class OpCheck:
32+
def run_check(self, agent: UserType) -> CheckResult:
2533
return CheckResult(
26-
check_pass=False, errors=[NotImplementedError("Check not implemented")]
34+
success=False, errors=[ValidationError("Check not implemented")]
2735
)
2836

2937

38+
@dataclasses.dataclass
3039
class TeamOpCheck(OpCheck):
3140
team: Team
3241

33-
def __init__(self, team: Team):
34-
self.team = team
3542

43+
class CheckTeamAccessPermission(TeamOpCheck):
44+
def run_check(self, agent: UserType) -> CheckResult:
45+
try:
46+
self.team.ensure_user_can_access(agent)
47+
return CheckResult(success=True, errors=None)
48+
except Exception as e:
49+
return CheckResult(success=False, errors=[e])
50+
51+
52+
class CheckTeamDisbandPermission(TeamOpCheck):
53+
def run_check(self, agent: UserType) -> CheckResult:
54+
try:
55+
self.team.ensure_user_can_disband(agent)
56+
return CheckResult(success=True, errors=None)
57+
except Exception as e:
58+
return CheckResult(success=False, errors=[e])
3659

37-
class TeamAccessCheck(TeamOpCheck):
38-
def run_check(self, user: UserType) -> CheckResult:
60+
61+
class CheckTeamEditPermission(TeamOpCheck):
62+
def run_check(self, agent: UserType) -> CheckResult:
3963
try:
40-
self.team.ensure_user_can_access(user)
41-
return CheckResult(check_pass=True, errors=None)
64+
self.team.ensure_user_can_edit_info(agent)
65+
return CheckResult(success=True, errors=None)
4266
except Exception as e:
43-
return CheckResult(check_pass=False, errors=[e])
67+
return CheckResult(success=False, errors=[e])
68+
69+
70+
class CheckTeamMemberManagePermission(TeamOpCheck):
71+
def run_check(self, agent: UserType) -> CheckResult:
72+
try:
73+
self.team.ensure_user_can_manage_members(agent)
74+
return CheckResult(success=True, errors=None)
75+
except Exception as e:
76+
return CheckResult(success=False, errors=[e])
77+
4478

79+
@dataclasses.dataclass
80+
class CheckTeamMemberCanBeRemoved(OpCheck):
81+
member: TeamMember
4582

46-
class TeamDisbandCheck(TeamOpCheck):
47-
def run_check(self, user: UserType) -> CheckResult:
83+
def run_check(self, _: UserType) -> CheckResult:
4884
try:
49-
self.team.ensure_user_can_disband(user)
50-
return CheckResult(check_pass=True, errors=None)
85+
self.member.team.ensure_member_can_be_removed(self.member)
86+
return CheckResult(success=True, errors=None)
5187
except Exception as e:
52-
return CheckResult(check_pass=False, errors=[e])
88+
return CheckResult(success=False, errors=[e])
5389

5490

55-
def _run_checks(
56-
agent: UserType, checks: Sequence[OpCheck]
57-
) -> Generator[CheckResult, Any, None]:
58-
return (check.run_check(agent) for check in checks)
91+
@dataclasses.dataclass
92+
class CheckTeamNameFree(OpCheck):
93+
team_name: str
5994

95+
def run_check(self, agent: UserType) -> CheckResult:
96+
if Team.objects.filter(name__iexact=self.team_name).exists():
97+
return CheckResult(
98+
success=False,
99+
errors=[ValidationError("Team with this name already exists")],
100+
)
101+
else:
102+
return CheckResult(success=True, errors=None)
60103

61-
def run_checks(agent: UserType, checks: Sequence[OpCheck]) -> CheckResult:
62-
results = _run_checks(agent, checks)
104+
105+
@dataclasses.dataclass
106+
class CheckNamespaceNameFree(OpCheck):
107+
namespace_name: str
108+
109+
def run_check(self, agent: UserType) -> CheckResult:
110+
if Namespace.objects.filter(name__iexact=self.namespace_name).exists():
111+
return CheckResult(
112+
success=False,
113+
errors=[ValidationError("Namespace with this name already exists")],
114+
)
115+
else:
116+
return CheckResult(success=True, errors=None)
117+
118+
119+
class CheckUserIsAuthenticated(OpCheck):
120+
def run_check(self, agent: UserType) -> CheckResult:
121+
if not agent or not agent.is_authenticated or not agent.is_active:
122+
return CheckResult(
123+
success=False,
124+
errors=[PermissionValidationError("User must be authenticated")],
125+
)
126+
else:
127+
return CheckResult(success=True, errors=None)
128+
129+
130+
class CheckUserIsNotServiceAccount(OpCheck):
131+
def run_check(self, agent: UserType) -> CheckResult:
132+
if getattr(agent, "service_account", None) is not None:
133+
raise PermissionValidationError(
134+
"Service accounts cannot perform this action"
135+
)
136+
137+
138+
def run_checks(agent: UserType, checks: Sequence[Union[OpCheck, None]]) -> CheckResult:
139+
results = (check.run_check(agent) for check in checks if check)
63140
errors = []
64141
check_pass = True
65142
for entry in results:
66143
if entry.errors:
67144
errors += entry.errors
68-
if not entry.check_pass:
145+
if not entry.success:
69146
check_pass = False
70-
return CheckResult(check_pass=check_pass, errors=errors)
147+
return CheckResult(success=check_pass, errors=errors)
71148

72149

73150
@transaction.atomic
74151
def disband_team(agent: UserType, team: Team) -> None:
75152
checks = [
76-
TeamAccessCheck(team),
77-
TeamDisbandCheck(team),
153+
CheckTeamAccessPermission(team=team),
154+
CheckTeamDisbandPermission(team=team),
78155
]
79-
if (check_result := run_checks(agent, checks)).check_pass:
156+
if (check_result := run_checks(agent, checks)).success:
80157
team.delete()
81158
else:
82-
raise check_result.errors
159+
raise check_result.as_exception()
83160

84161

85162
@transaction.atomic
86163
def create_team(agent: UserType, team_name: str) -> Team:
87-
if not agent or not agent.is_authenticated or not agent.is_active:
88-
raise PermissionValidationError("Must be authenticated to create teams")
89-
if getattr(agent, "service_account", None) is not None:
90-
raise PermissionValidationError("Service accounts cannot create teams")
91-
if Team.objects.filter(name__iexact=team_name).exists():
92-
raise ValidationError("Team with this name already exists")
93-
if Namespace.objects.filter(name__iexact=team_name).exists():
94-
raise ValidationError("Namespace with this name already exists")
95-
96-
team = Team.create(name=team_name)
97-
team.add_member(user=agent, role=TeamMemberRole.owner)
98-
return team
164+
checks = [
165+
CheckUserIsAuthenticated(),
166+
CheckUserIsNotServiceAccount(),
167+
CheckTeamNameFree(team_name=team_name),
168+
CheckNamespaceNameFree(namespace_name=team_name),
169+
]
170+
if (result := run_checks(agent, checks)).success:
171+
team = Team.create(name=team_name)
172+
team.add_member(user=agent, role=TeamMemberRole.owner)
173+
return team
174+
else:
175+
raise result.as_exception()
99176

100177

101178
@transaction.atomic
102179
def update_team(agent: UserType, team: Team, donation_link: str) -> Team:
103-
team.ensure_user_can_access(agent)
104-
team.ensure_user_can_edit_info(agent)
105-
106-
team.donation_link = donation_link
107-
team.save()
108-
109-
return team
180+
checks = [
181+
CheckTeamAccessPermission(team=team),
182+
CheckTeamEditPermission(team=team),
183+
]
184+
if (result := run_checks(agent, checks)).success:
185+
team.donation_link = donation_link
186+
team.save()
187+
return team
188+
else:
189+
raise result.as_exception()
110190

111191

112192
@transaction.atomic
113193
def remove_team_member(agent: UserType, member: TeamMember) -> None:
114-
if member.user != agent:
115-
member.team.ensure_user_can_manage_members(agent)
116-
member.team.ensure_member_can_be_removed(member)
117-
member.delete()
194+
checks = [
195+
CheckTeamMemberManagePermission(team=member.team)
196+
if member.user != agent
197+
else None,
198+
CheckTeamMemberCanBeRemoved(member=member),
199+
]
200+
if (result := run_checks(agent, checks)).success:
201+
member.delete()
202+
else:
203+
raise result.as_exception()
118204

119205

120206
@transaction.atomic

0 commit comments

Comments
 (0)