Skip to content

Commit fd5704f

Browse files
authored
Merge 670304c into 17c7d3e
2 parents 17c7d3e + 670304c commit fd5704f

File tree

5 files changed

+248
-20
lines changed

5 files changed

+248
-20
lines changed

src/sentry/workflow_engine/endpoints/validators/api_docs_help_text.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -825,20 +825,15 @@
825825
"""
826826

827827
OWNER_HELP_TEXT = """
828-
The user or team who owns the monitor.
828+
The ID user or team who owns the monitor or alert prefaced by the string 'user' or 'team'.
829829
830830
**User**
831831
```json
832-
"type": "user",
833-
"id": "12345",
834-
"name": "Jane Doe",
835-
"email": "[email protected]"
832+
"user:123456"
836833
```
837834
838835
**Team**
839836
```json
840-
"type": "team",
841-
"id": "123456789",
842-
"name": "example-team"
837+
"team:456789"
843838
```
844839
"""

src/sentry/workflow_engine/endpoints/validators/base/detector.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
get_unknown_detector_type_error,
2929
log_alerting_quota_hit,
3030
toggle_detector,
31+
update_owner,
3132
)
3233
from sentry.workflow_engine.models import (
3334
DataConditionGroup,
@@ -193,18 +194,9 @@ def update(self, instance: Detector, validated_data: dict[str, Any]) -> Detector
193194

194195
# Handle owner field update
195196
if "owner" in validated_data:
196-
owner = validated_data.get("owner")
197-
if owner:
198-
if owner.is_user:
199-
instance.owner_user_id = owner.id
200-
instance.owner_team_id = None
201-
elif owner.is_team:
202-
instance.owner_user_id = None
203-
instance.owner_team_id = owner.id
204-
else:
205-
# Clear owner if None is passed
206-
instance.owner_user_id = None
207-
instance.owner_team_id = None
197+
instance.owner_user_id, instance.owner_team_id = update_owner(
198+
validated_data.get("owner")
199+
)
208200

209201
if "condition_group" in validated_data:
210202
condition_group = validated_data.pop("condition_group")

src/sentry/workflow_engine/endpoints/validators/base/workflow.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
from rest_framework import serializers
55

66
from sentry import audit_log, features, options
7+
from sentry.api.fields.actor import OwnerActorField
78
from sentry.api.serializers.rest_framework import CamelSnakeSerializer
89
from sentry.api.serializers.rest_framework.environment import EnvironmentField
910
from sentry.db import models
1011
from sentry.models.organization import Organization
1112
from sentry.utils.audit import create_audit_entry
1213
from sentry.workflow_engine.endpoints.validators.api_docs_help_text import (
1314
ACTION_FILTERS_HELP_TEXT,
15+
OWNER_HELP_TEXT,
1416
WORKFLOW_CONFIG_HELP_TEXT,
1517
WORKFLOW_TRIGGERS_HELP_TEXT,
1618
)
@@ -21,6 +23,7 @@
2123
from sentry.workflow_engine.endpoints.validators.utils import (
2224
log_alerting_quota_hit,
2325
remove_items_by_api_input,
26+
update_owner,
2427
validate_json_schema,
2528
)
2629
from sentry.workflow_engine.models import (
@@ -60,6 +63,11 @@ class WorkflowValidator(CamelSnakeSerializer[Any]):
6063
required=False,
6164
help_text=ACTION_FILTERS_HELP_TEXT,
6265
)
66+
owner = OwnerActorField(
67+
required=False,
68+
allow_null=True,
69+
help_text=OWNER_HELP_TEXT,
70+
)
6371

6472
def _split_action_and_condition_group(
6573
self, action_filter: dict[str, Any]
@@ -244,8 +252,15 @@ def update(self, instance: Workflow, validated_data: InputData) -> Workflow:
244252
if action_filters is not None:
245253
self.update_action_filters(action_filters)
246254

255+
# Handle owner field update
256+
if "owner" in validated_data:
257+
instance.owner_user_id, instance.owner_team_id = update_owner(
258+
validated_data.get("owner")
259+
)
260+
247261
# Update the workflow
248262
instance.update(**validated_data)
263+
instance.save()
249264
return instance
250265

251266
def _validate_workflow_limits(self) -> None:
@@ -283,6 +298,15 @@ def create(self, validated_value: InputData) -> Workflow:
283298

284299
environment = validated_value.get("environment")
285300

301+
owner = validated_value.get("owner")
302+
owner_user_id = None
303+
owner_team_id = None
304+
if owner:
305+
if owner.is_user:
306+
owner_user_id = owner.id
307+
elif owner.is_team:
308+
owner_team_id = owner.id
309+
286310
workflow = Workflow.objects.create(
287311
name=validated_value["name"],
288312
enabled=validated_value["enabled"],
@@ -291,6 +315,8 @@ def create(self, validated_value: InputData) -> Workflow:
291315
environment_id=environment.id if environment else None,
292316
when_condition_group=when_condition_group,
293317
created_by_id=self.context["request"].user.id,
318+
owner_user_id=owner_user_id,
319+
owner_team_id=owner_team_id,
294320
)
295321

296322
# TODO -- can we bulk create: actions, dcga's and the workflow dcg?

src/sentry/workflow_engine/endpoints/validators/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from jsonschema import ValidationError as JsonValidationError
66
from jsonschema import validate
77

8+
from sentry.api.fields.actor import OwnerActorField
89
from sentry.issues import grouptype
910
from sentry.models.organization import Organization
1011
from sentry.users.models.user import User
@@ -15,6 +16,22 @@
1516
logger = logging.getLogger(__name__)
1617

1718

19+
def update_owner(owner: OwnerActorField) -> tuple[int | None, int | None]:
20+
if owner:
21+
if owner.is_user:
22+
owner_user_id = owner.id
23+
owner_team_id = None
24+
elif owner.is_team:
25+
owner_user_id = None
26+
owner_team_id = owner.id
27+
else:
28+
# Clear owner if None is passed
29+
owner_user_id = None
30+
owner_team_id = None
31+
32+
return owner_user_id, owner_team_id
33+
34+
1835
def log_alerting_quota_hit(
1936
object_type: str, organization: Organization, actor: User | RpcUser | None
2037
) -> None:

tests/sentry/workflow_engine/endpoints/validators/test_base_workflow.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
from rest_framework.exceptions import ErrorDetail
55
from rest_framework.serializers import ValidationError
66

7+
from sentry.auth.access import SystemAccess
78
from sentry.notifications.models.notificationaction import ActionTarget
89
from sentry.testutils.cases import TestCase
10+
from sentry.types.actor import Actor
911
from sentry.workflow_engine.endpoints.serializers.workflow_serializer import (
1012
TriggerSerializerResponse,
1113
WorkflowSerializer,
@@ -154,6 +156,89 @@ def test_create__simple(self) -> None:
154156
assert workflow.organization_id == self.organization.id
155157
assert workflow.created_by_id == self.user.id
156158

159+
def test_create__owner_user_id(self) -> None:
160+
self.valid_data["owner"] = f"user:{self.user.id}"
161+
validator = WorkflowValidator(data=self.valid_data, context=self.context)
162+
assert validator.is_valid() is True
163+
workflow = validator.create(validator.validated_data)
164+
workflow.refresh_from_db()
165+
assert workflow.owner_user_id == self.user.id
166+
167+
def test_team_owner(self) -> None:
168+
team = self.create_team(organization=self.organization, members=[self.user])
169+
self.valid_data["owner"] = f"team:{team.id}"
170+
validator = WorkflowValidator(data=self.valid_data, context=self.context)
171+
assert validator.is_valid() is True
172+
workflow = validator.create(validator.validated_data)
173+
workflow.refresh_from_db()
174+
assert workflow.owner_team_id == team.id
175+
assert workflow.owner_user_id is None
176+
177+
def test_owner_perms(self) -> None:
178+
other_user = self.create_user()
179+
self.valid_data["owner"] = f"user:{other_user.id}"
180+
validator = WorkflowValidator(data=self.valid_data, context=self.context)
181+
assert validator.is_valid() is False
182+
assert str(validator.errors["owner"][0]) == "User is not a member of this organization"
183+
184+
other_team = self.create_team(self.create_organization())
185+
self.valid_data["owner"] = f"team:{other_team.id}"
186+
validator = WorkflowValidator(data=self.valid_data, context=self.context)
187+
assert validator.is_valid() is False
188+
assert str(validator.errors["owner"][0]) == "Team is not a member of this organization"
189+
190+
def test_team_owner_not_member(self) -> None:
191+
self.organization.flags.allow_joinleave = False
192+
self.organization.save()
193+
194+
team = self.create_team(organization=self.organization)
195+
member_user = self.create_user()
196+
self.create_member(
197+
user=member_user,
198+
organization=self.organization,
199+
role="member",
200+
teams=[self.team],
201+
)
202+
203+
context = {
204+
"organization": self.organization,
205+
"request": self.make_request(user=member_user),
206+
}
207+
self.valid_data["owner"] = f"team:{team.id}"
208+
validator = WorkflowValidator(data=self.valid_data, context=context)
209+
assert validator.is_valid() is False
210+
assert str(validator.errors["owner"][0]) == "You can only assign teams you are a member of"
211+
212+
def test_team_owner_not_member_with_team_admin_scope(self) -> None:
213+
team = self.create_team(organization=self.organization)
214+
215+
context = {
216+
"organization": self.organization,
217+
"request": self.make_request(user=self.user),
218+
"access": SystemAccess(),
219+
}
220+
self.valid_data["owner"] = f"team:{team.id}"
221+
validator = WorkflowValidator(data=self.valid_data, context=context)
222+
assert validator.is_valid() is True
223+
workflow = validator.create(validator.validated_data)
224+
assert workflow.owner_team_id == team.id
225+
assert workflow.owner_user_id is None
226+
227+
def test_user_owner_another_member(self) -> None:
228+
other_user = self.create_user()
229+
self.create_member(
230+
user=other_user,
231+
organization=self.organization,
232+
role="member",
233+
)
234+
235+
self.valid_data["owner"] = f"user:{other_user.id}"
236+
validator = WorkflowValidator(data=self.valid_data, context=self.context)
237+
assert validator.is_valid() is True
238+
workflow = validator.create(validator.validated_data)
239+
assert workflow.owner_user_id == other_user.id
240+
assert workflow.owner_team_id is None
241+
157242
def test_create__validate_triggers_empty(self) -> None:
158243
validator = WorkflowValidator(data=self.valid_data, context=self.context)
159244
assert validator.is_valid() is True
@@ -732,3 +817,116 @@ def test_update__remove_all_actions(self, mock_action_validator: mock.MagicMock)
732817
workflow_condition_group = self.workflow.workflowdataconditiongroup_set.first()
733818
assert workflow_condition_group is not None
734819
assert workflow_condition_group.condition_group.dataconditiongroupaction_set.count() == 0
820+
821+
def test_update_owner_type(self, mock_action_validator: mock.MagicMock) -> None:
822+
team = self.create_team(organization=self.organization, members=[self.user])
823+
context = {**self.context, "request": self.make_request(user=self.user)}
824+
825+
self.valid_data["owner"] = f"team:{team.id}"
826+
validator = WorkflowValidator(data=self.valid_data, context=context)
827+
assert validator.is_valid() is True
828+
workflow = validator.update(self.workflow, validator.validated_data)
829+
workflow.refresh_from_db()
830+
assert workflow.owner_team_id == team.id
831+
assert workflow.owner_user_id is None
832+
833+
self.valid_data["owner"] = f"user:{self.user.id}"
834+
validator = WorkflowValidator(data=self.valid_data, context=context)
835+
assert validator.is_valid() is True
836+
workflow.refresh_from_db()
837+
workflow = validator.update(self.workflow, validator.validated_data)
838+
assert workflow.owner_user_id == self.user.id
839+
assert workflow.owner_team_id is None
840+
841+
def test_team_owner_not_member(self, mock_action_validator: mock.MagicMock) -> None:
842+
self.organization.flags.allow_joinleave = False
843+
self.organization.save()
844+
845+
team = self.create_team(organization=self.organization)
846+
member_user = self.create_user()
847+
self.create_member(
848+
user=member_user,
849+
organization=self.organization,
850+
role="member",
851+
teams=[self.team],
852+
)
853+
854+
context = {**self.context, "request": self.make_request(user=member_user)}
855+
self.valid_data["owner"] = f"team:{team.id}"
856+
validator = WorkflowValidator(data=self.valid_data, context=context)
857+
assert validator.is_valid() is False
858+
assert str(validator.errors["owner"][0]) == "You can only assign teams you are a member of"
859+
860+
def test_team_owner_not_member_with_team_admin_scope(
861+
self, mock_action_validator: mock.MagicMock
862+
) -> None:
863+
team = self.create_team(organization=self.organization)
864+
865+
context = {**self.context, "access": SystemAccess()}
866+
self.valid_data["owner"] = f"team:{team.id}"
867+
validator = WorkflowValidator(data=self.valid_data, context=context)
868+
assert validator.is_valid() is True
869+
workflow = validator.update(self.workflow, validator.validated_data)
870+
assert workflow.owner_team_id == team.id
871+
assert workflow.owner_user_id is None
872+
873+
def test_reassign_owner_from_own_team_to_any_team(
874+
self, mock_action_validator: mock.MagicMock
875+
) -> None:
876+
member_team = self.create_team(organization=self.organization)
877+
member_user = self.create_user()
878+
self.create_member(
879+
user=member_user,
880+
organization=self.organization,
881+
role="member",
882+
teams=[member_team],
883+
)
884+
target_team = self.create_team(organization=self.organization)
885+
886+
self.workflow.owner_team_id = member_team.id
887+
self.workflow.save()
888+
889+
current_owner = Actor.from_id(user_id=None, team_id=member_team.id)
890+
context = {
891+
**self.context,
892+
"request": self.make_request(user=member_user),
893+
"current_owner": current_owner,
894+
}
895+
self.valid_data["owner"] = f"team:{target_team.id}"
896+
validator = WorkflowValidator(data=self.valid_data, context=context)
897+
assert validator.is_valid() is True
898+
workflow = validator.update(self.workflow, validator.validated_data)
899+
assert workflow.owner_team_id == target_team.id
900+
901+
def test_cannot_reassign_owner_from_other_team(
902+
self, mock_action_validator: mock.MagicMock
903+
) -> None:
904+
self.organization.flags.allow_joinleave = False
905+
self.organization.save()
906+
907+
other_team = self.create_team(organization=self.organization)
908+
member_team = self.create_team(organization=self.organization)
909+
member_user = self.create_user()
910+
self.create_member(
911+
user=member_user,
912+
organization=self.organization,
913+
role="member",
914+
teams=[member_team],
915+
)
916+
target_team = self.create_team(organization=self.organization)
917+
918+
self.workflow.owner_team_id = other_team.id
919+
self.workflow.save()
920+
921+
current_owner = Actor.from_id(user_id=None, team_id=other_team.id)
922+
context = {
923+
**self.context,
924+
"request": self.make_request(user=member_user),
925+
"current_owner": current_owner,
926+
}
927+
self.valid_data["owner"] = f"team:{target_team.id}"
928+
validator = WorkflowValidator(data=self.valid_data, context=context)
929+
assert validator.is_valid() is False
930+
assert str(validator.errors["owner"][0]) == "You can only assign teams you are a member of"
931+
self.workflow.refresh_from_db()
932+
assert self.workflow.owner_team_id == other_team.id

0 commit comments

Comments
 (0)