Skip to content

Commit 34ef4d0

Browse files
dcramercodex
andcommitted
fix(api): Unwrap org context in alert mutations
Normalize RpcUserOrganizationContext to the underlying organization before resolving target projects for alert-rule and detector mutation permissions. Without that, the new project-scoped alerts:write path breaks during organization object-permission checks because those hooks run before convert_args swaps in the concrete organization. Add team-admin session regressions that force the project-scoped alerts:write authorization path for alert rule and detector updates and deletes, including the workflow-engine fake-detector alert-rule route. Co-Authored-By: Codex <noreply@openai.com>
1 parent 2a711cc commit 34ef4d0

File tree

4 files changed

+119
-7
lines changed

4 files changed

+119
-7
lines changed

src/sentry/incidents/endpoints/bases.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,21 @@
1414
from sentry.incidents.models.alert_rule import AlertRule
1515
from sentry.models.organization import Organization
1616
from sentry.models.project import Project
17+
from sentry.organizations.services.organization import RpcOrganization, RpcUserOrganizationContext
1718
from sentry.workflow_engine.endpoints.utils.ids import to_valid_int_id
1819
from sentry.workflow_engine.models.alertrule_detector import AlertRuleDetector
1920
from sentry.workflow_engine.models.detector import Detector
2021

2122

23+
def _get_organization_id(
24+
organization: Organization | RpcOrganization | RpcUserOrganizationContext,
25+
) -> int:
26+
if isinstance(organization, RpcUserOrganizationContext):
27+
return organization.organization.id
28+
29+
return organization.id
30+
31+
2232
class OrganizationAlertRuleBaseEndpoint(OrganizationEndpoint):
2333
"""
2434
Base endpoint for organization-scoped alert rule creation.
@@ -89,7 +99,9 @@ class OrganizationAlertRuleEndpoint(OrganizationEndpoint):
8999
permission_classes = (OrganizationAlertRulePermission,)
90100

91101
def get_alert_mutation_projects(
92-
self, request: Request, organization: Organization
102+
self,
103+
request: Request,
104+
organization: Organization | RpcOrganization | RpcUserOrganizationContext,
93105
) -> Sequence[Project] | None:
94106
if request.method not in {"PUT", "DELETE"}:
95107
return None
@@ -104,8 +116,9 @@ def get_alert_mutation_projects(
104116
return None
105117

106118
try:
119+
organization_id = _get_organization_id(organization)
107120
alert_rule = AlertRule.objects.prefetch_related("projects").get(
108-
organization=organization, id=validated_alert_rule_id
121+
organization_id=organization_id, id=validated_alert_rule_id
109122
)
110123
return [alert_rule.projects.get()]
111124
except (
@@ -192,7 +205,9 @@ class WorkflowEngineOrganizationAlertRuleEndpoint(OrganizationAlertRuleEndpoint)
192205
workflow_engine_method_flags: dict[str, str] = {}
193206

194207
def get_alert_mutation_projects(
195-
self, request: Request, organization: Organization
208+
self,
209+
request: Request,
210+
organization: Organization | RpcOrganization | RpcUserOrganizationContext,
196211
) -> Sequence[Project] | None:
197212
projects = super().get_alert_mutation_projects(request, organization)
198213
if projects is not None:
@@ -210,11 +225,12 @@ def get_alert_mutation_projects(
210225
except RestFrameworkValidationError:
211226
return None
212227

228+
organization_id = _get_organization_id(organization)
213229
ard = (
214230
AlertRuleDetector.objects.select_related("detector__project")
215231
.filter(
216232
alert_rule_id=validated_alert_rule_id,
217-
detector__project__organization=organization,
233+
detector__project__organization_id=organization_id,
218234
)
219235
.first()
220236
)
@@ -230,7 +246,7 @@ def get_alert_mutation_projects(
230246
Detector.objects.select_related("project")
231247
.filter(
232248
id=detector_id,
233-
project__organization=organization,
249+
project__organization_id=organization_id,
234250
)
235251
.first()
236252
)

src/sentry/workflow_engine/endpoints/organization_detector_details.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from sentry.issues import grouptype
2828
from sentry.models.organization import Organization
2929
from sentry.models.project import Project
30+
from sentry.organizations.services.organization import RpcOrganization, RpcUserOrganizationContext
3031
from sentry.utils.audit import create_audit_entry
3132
from sentry.workflow_engine.endpoints.serializers.detector_serializer import DetectorSerializer
3233
from sentry.workflow_engine.endpoints.utils.ids import to_valid_int_id
@@ -39,6 +40,15 @@
3940
from sentry.workflow_engine.models import Detector
4041

4142

43+
def _get_organization_id(
44+
organization: Organization | RpcOrganization | RpcUserOrganizationContext,
45+
) -> int:
46+
if isinstance(organization, RpcUserOrganizationContext):
47+
return organization.organization.id
48+
49+
return organization.id
50+
51+
4252
def remove_detector(request: Request, organization: Organization, detector: Detector) -> Response:
4353
"""
4454
Delete a given detector. This method is used by the OrganizationAlertRuleDetailsEndpoint DELETE method
@@ -96,7 +106,9 @@ def get_detector_validator(
96106
@extend_schema(tags=["Monitors"])
97107
class OrganizationDetectorDetailsEndpoint(OrganizationEndpoint):
98108
def get_alert_mutation_projects(
99-
self, request: Request, organization: Organization
109+
self,
110+
request: Request,
111+
organization: Organization | RpcOrganization | RpcUserOrganizationContext,
100112
) -> list[Project] | None:
101113
if request.method not in {"PUT", "DELETE"}:
102114
return None
@@ -110,11 +122,12 @@ def get_alert_mutation_projects(
110122
except ValidationError:
111123
return None
112124

125+
organization_id = _get_organization_id(organization)
113126
detector = (
114127
Detector.objects.select_related("project")
115128
.filter(
116129
id=validated_detector_id,
117-
project__organization_id=organization.id,
130+
project__organization_id=organization_id,
118131
)
119132
.first()
120133
)

tests/sentry/incidents/endpoints/test_organization_alert_rule_details.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,28 @@ def test_get_shows_count_when_stored_as_upsampled_count(
10061006
class AlertRuleDetailsPutEndpointTest(AlertRuleDetailsBase):
10071007
method = "put"
10081008

1009+
def test_team_admin_can_update_with_project_scoped_alerts_write(self) -> None:
1010+
team_admin_user = self.create_user()
1011+
self.create_member(
1012+
user=team_admin_user,
1013+
organization=self.organization,
1014+
role="member",
1015+
team_roles=[(self.team, "admin")],
1016+
)
1017+
self.organization.update_option("sentry:alerts_member_write", False)
1018+
self.login_as(team_admin_user)
1019+
1020+
with self.feature("organizations:incidents"):
1021+
response = self.client.put(
1022+
reverse(self.endpoint, args=[self.organization.slug, self.alert_rule.id]),
1023+
data=self.valid_params,
1024+
format="json",
1025+
)
1026+
1027+
assert response.status_code == 200
1028+
self.alert_rule.refresh_from_db()
1029+
assert self.alert_rule.name == self.valid_params["name"]
1030+
10091031
def test_update_requires_alerts_write_scope_for_tokens(self) -> None:
10101032
self.create_member(
10111033
user=self.user, organization=self.organization, role="owner", teams=[self.team]
@@ -2647,6 +2669,27 @@ def test_error_response_from_sentry_app(self) -> None:
26472669
class AlertRuleDetailsDeleteEndpointTest(AlertRuleDetailsBase):
26482670
method = "delete"
26492671

2672+
def test_team_admin_can_delete_workflow_engine_detector_with_project_scoped_alerts_write(
2673+
self,
2674+
) -> None:
2675+
team_admin_user = self.create_user()
2676+
self.create_member(
2677+
user=team_admin_user,
2678+
organization=self.organization,
2679+
role="member",
2680+
team_roles=[(self.team, "admin")],
2681+
)
2682+
self.organization.update_option("sentry:alerts_member_write", False)
2683+
self.login_as(team_admin_user)
2684+
2685+
detector = self.create_detector(project=self.project, type=MetricIssue.slug)
2686+
data_source = self.create_data_source()
2687+
self.create_data_source_detector(data_source, detector)
2688+
fake_detector_id = get_fake_id_from_object_id(detector.id)
2689+
2690+
with self.feature({"organizations:workflow-engine-rule-serializers": True}):
2691+
self.get_success_response(self.organization.slug, fake_detector_id, status_code=204)
2692+
26502693
def test_delete_denies_alerts_write_scope_for_other_team_projects(self) -> None:
26512694
team_admin_user = self.create_user()
26522695
self.create_member(

tests/sentry/workflow_engine/endpoints/test_organization_detector_details.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,28 @@ def test_update_requires_alerts_write_scope_for_tokens(self) -> None:
504504

505505
assert response.status_code == 403
506506

507+
def test_team_admin_can_update_with_project_scoped_alerts_write(self) -> None:
508+
team_admin_user = self.create_user(is_superuser=False)
509+
self.create_member(
510+
user=team_admin_user,
511+
organization=self.organization,
512+
role="member",
513+
team_roles=[(self.team, "admin")],
514+
)
515+
self.organization.update_option("sentry:alerts_member_write", False)
516+
self.login_as(team_admin_user)
517+
518+
with self.tasks():
519+
response = self.client.put(
520+
reverse(self.endpoint, args=[self.organization.slug, self.detector.id]),
521+
data={"enabled": False},
522+
format="json",
523+
)
524+
525+
assert response.status_code == 200
526+
self.detector.refresh_from_db()
527+
assert self.detector.enabled is False
528+
507529
def test_update_allows_alerts_write_scope_for_tokens(self) -> None:
508530
token = self._create_token("alerts:write")
509531

@@ -1029,6 +1051,24 @@ def test_update_data_source_marks_user_updated_when_snapshot_exists(
10291051
class OrganizationDetectorDetailsDeleteTest(OrganizationDetectorDetailsBaseTest):
10301052
method = "DELETE"
10311053

1054+
def test_team_admin_can_delete_with_project_scoped_alerts_write(self) -> None:
1055+
team_admin_user = self.create_user(is_superuser=False)
1056+
self.create_member(
1057+
user=team_admin_user,
1058+
organization=self.organization,
1059+
role="member",
1060+
team_roles=[(self.team, "admin")],
1061+
)
1062+
self.organization.update_option("sentry:alerts_member_write", False)
1063+
self.login_as(team_admin_user)
1064+
1065+
with outbox_runner():
1066+
response = self.client.delete(
1067+
reverse(self.endpoint, args=[self.organization.slug, self.detector.id])
1068+
)
1069+
1070+
assert response.status_code == 204
1071+
10321072
def test_delete_denies_alerts_write_scope_for_other_team_projects(self) -> None:
10331073
team_admin_user = self.create_user(is_superuser=False)
10341074
self.create_member(

0 commit comments

Comments
 (0)