Skip to content

Commit 470ba75

Browse files
committed
fix(issue-platform): Check permissions for assignees sent to the issue platform
We should make sure that the assignee for an occurrence actually belongs to the org.
1 parent 2869247 commit 470ba75

File tree

4 files changed

+73
-32
lines changed

4 files changed

+73
-32
lines changed

src/sentry/api/fields/actor.py

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@
44
from drf_spectacular.utils import extend_schema_field
55
from rest_framework import serializers
66

7-
from sentry.models.organizationmember import OrganizationMember
8-
from sentry.models.team import Team
9-
from sentry.models.user import User
10-
from sentry.utils.actor import ActorTuple
7+
from sentry.utils.actor import parse_and_validate_actor
118

129

1310
@extend_schema_field(field=OpenApiTypes.STR)
@@ -20,29 +17,8 @@ def to_representation(self, value):
2017
return value.identifier
2118

2219
def to_internal_value(self, data):
23-
if not data:
24-
return None
20+
actor_tuple = parse_and_validate_actor(data, self.context["organization"].id)
2521

26-
try:
27-
actor = ActorTuple.from_actor_identifier(data)
28-
except Exception:
29-
raise serializers.ValidationError(
30-
"Could not parse actor. Format should be `type:id` where type is `team` or `user`."
31-
)
32-
try:
33-
obj = actor.resolve()
34-
except (Team.DoesNotExist, User.DoesNotExist):
35-
raise serializers.ValidationError(f"{actor.type.__name__} does not exist")
36-
37-
if actor.type == Team:
38-
if obj.organization != self.context["organization"]:
39-
raise serializers.ValidationError("Team is not a member of this organization")
40-
elif actor.type == User:
41-
if not OrganizationMember.objects.filter(
42-
organization=self.context["organization"], user_id=obj.id
43-
).exists():
44-
raise serializers.ValidationError("User is not a member of this organization")
45-
46-
if self.as_actor:
47-
return actor.resolve_to_actor()
48-
return actor
22+
if self.as_actor and actor_tuple:
23+
return actor_tuple.resolve_to_actor()
24+
return actor_tuple

src/sentry/issues/occurrence_consumer.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from sentry.models.organization import Organization
2323
from sentry.models.project import Project
2424
from sentry.utils import metrics
25+
from sentry.utils.actor import parse_and_validate_actor
2526

2627
logger = logging.getLogger(__name__)
2728

@@ -142,6 +143,17 @@ def _get_kwargs(payload: Mapping[str, Any]) -> Mapping[str, Any]:
142143
try:
143144
with metrics.timer("occurrence_ingest.duration", instance="_get_kwargs"):
144145
metrics.distribution("occurrence.ingest.size.data", len(payload), unit="byte")
146+
assignee_identifier = None
147+
project = Project.objects.get_from_cache(id=payload["project_id"])
148+
try:
149+
assignee = parse_and_validate_actor(
150+
payload.get("assignee"), project.organization_id
151+
)
152+
if assignee:
153+
assignee_identifier = assignee.identifier
154+
except Exception:
155+
logger.exception("Failed to validate assignee for occurrence")
156+
145157
occurrence_data = {
146158
"id": UUID(payload["id"]).hex,
147159
"project_id": payload["project_id"],
@@ -154,7 +166,7 @@ def _get_kwargs(payload: Mapping[str, Any]) -> Mapping[str, Any]:
154166
"type": payload["type"],
155167
"detection_time": payload["detection_time"],
156168
"level": payload.get("level", DEFAULT_LEVEL),
157-
"assignee": payload.get("assignee"),
169+
"assignee": assignee_identifier,
158170
}
159171

160172
process_occurrence_data(occurrence_data)

src/sentry/utils/actor.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66

77
from rest_framework import serializers
88

9+
from sentry.services.hybrid_cloud.user import RpcUser
10+
911
if TYPE_CHECKING:
1012
from sentry.models.actor import Actor
1113
from sentry.models.team import Team
1214
from sentry.models.user import User
13-
from sentry.services.hybrid_cloud.user import RpcUser
1415

1516

1617
class ActorTuple(namedtuple("Actor", "id type")):
@@ -164,3 +165,36 @@ def fetch_actor_by_id(cls: type[User] | type[Team], id: int) -> Team | RpcUser:
164165
return user
165166
else:
166167
raise ValueError(f"Cls {cls} is not a valid actor type.")
168+
169+
170+
def parse_and_validate_actor(
171+
actor_identifier: str | None, organization_id: int
172+
) -> ActorTuple | None:
173+
from sentry.models.organizationmember import OrganizationMember
174+
from sentry.models.team import Team
175+
from sentry.models.user import User
176+
177+
if not actor_identifier:
178+
return None
179+
180+
try:
181+
actor = ActorTuple.from_actor_identifier(actor_identifier)
182+
except Exception:
183+
raise serializers.ValidationError(
184+
"Could not parse actor. Format should be `type:id` where type is `team` or `user`."
185+
)
186+
try:
187+
obj = actor.resolve()
188+
except (Team.DoesNotExist, User.DoesNotExist):
189+
raise serializers.ValidationError(f"{actor.type.__name__} does not exist")
190+
191+
if isinstance(obj, Team):
192+
if obj.organization_id != organization_id:
193+
raise serializers.ValidationError("Team is not a member of this organization")
194+
elif isinstance(obj, RpcUser):
195+
if not OrganizationMember.objects.filter(
196+
organization_id=organization_id, user_id=obj.id
197+
).exists():
198+
raise serializers.ValidationError("User is not a member of this organization")
199+
200+
return actor

tests/sentry/issues/test_occurrence_consumer.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,25 @@ def test_assignee(self) -> None:
465465
kwargs = _get_kwargs(message)
466466
assert kwargs["occurrence_data"]["assignee"] == f"user:{self.user.id}"
467467

468+
def test_assignee_perms(self) -> None:
469+
message = deepcopy(get_test_message(self.project.id))
470+
random_user = self.create_user()
471+
message["assignee"] = f"user:{random_user.id}"
472+
kwargs = _get_kwargs(message)
473+
assert kwargs["occurrence_data"]["assignee"] is None
474+
475+
message = deepcopy(get_test_message(self.project.id))
476+
message["assignee"] = "user:99999999999"
477+
kwargs = _get_kwargs(message)
478+
assert kwargs["occurrence_data"]["assignee"] is None
479+
480+
other_org = self.create_organization()
481+
random_team = self.create_team(other_org)
482+
message = deepcopy(get_test_message(self.project.id))
483+
message["assignee"] = f"team:{random_team.id}"
484+
kwargs = _get_kwargs(message)
485+
assert kwargs["occurrence_data"]["assignee"] is None
486+
468487
def test_assignee_none(self) -> None:
469488
kwargs = _get_kwargs(deepcopy(get_test_message(self.project.id)))
470489
assert kwargs["occurrence_data"]["assignee"] is None
@@ -475,4 +494,4 @@ def test_assignee_none(self) -> None:
475494
message = deepcopy(get_test_message(self.project.id))
476495
message["assignee"] = ""
477496
kwargs = _get_kwargs(message)
478-
assert kwargs["occurrence_data"]["assignee"] == ""
497+
assert kwargs["occurrence_data"]["assignee"] is None

0 commit comments

Comments
 (0)