Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@
from sentry.rules.actions.integrations.create_ticket.form import IntegrationNotifyServiceForm
from sentry.rules.actions.notify_event_service import NotifyEventServiceForm
from sentry.rules.actions.sentry_apps.utils import validate_sentry_app_action
from sentry.sentry_apps.services.app import RpcSentryAppInstallation, app_service
from sentry.sentry_apps.services.app import app_service
from sentry.sentry_apps.utils.errors import SentryAppBaseError
from sentry.utils import json
from sentry.workflow_engine.models.action import Action
from sentry.workflow_engine.processors.action import get_notification_plugins_for_org
from sentry.workflow_engine.typings.notification_action import SentryAppIdentifier

from .types import BaseActionValidatorHandler

Expand Down Expand Up @@ -206,45 +205,15 @@ def __init__(self, validated_data: dict[str, Any], organization: Organization) -
self.validated_data = validated_data
self.organization = organization

def _get_sentry_app_installation(
self, sentry_app_identifier: SentryAppIdentifier, target_identifier: str
) -> RpcSentryAppInstallation | None:
"""
Get the sentry app installation based on whether the target identifier is an installation id or sentry app id
We do not want to accept SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID long term, this is temporary until we migrate the data over
"""
installations = None
installation = None

if sentry_app_identifier == SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID:
installations = app_service.get_many(
filter=dict(uuids=[target_identifier], organization_id=self.organization.id)
)
else:
installations = app_service.get_many(
filter=dict(app_ids=[int(target_identifier)], organization_id=self.organization.id)
)
if installations:
installation = installations[0]

return installation

def clean_data(self) -> dict[str, Any]:
sentry_app_identifier = SentryAppIdentifier(
self.validated_data["config"]["sentry_app_identifier"]
)
target_identifier = self.validated_data["config"]["target_identifier"]
installation = self._get_sentry_app_installation(sentry_app_identifier, target_identifier)
installations = app_service.get_many(
filter=dict(app_ids=[int(target_identifier)], organization_id=self.organization.id)
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unhandled ValueError when legacy identifier format used

Medium Severity

The clean_data method calls int(target_identifier) without error handling. The config schema in sentry_app_handler.py still includes SENTRY_APP_INSTALLATION_UUID in the sentry_app_identifier enum, allowing requests with UUID-formatted target_identifier values to pass schema validation. When such a request reaches this code, int() raises an unhandled ValueError, resulting in a 500 error instead of a proper validation error response.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is why we don't merge this until the migration has run

installation = installations[0] if len(installations) else None
if not installation:
raise ValidationError("Sentry app installation not found.")

if sentry_app_identifier == SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID:
# convert to use sentry_app_id until we can migrate all the data
self.validated_data["config"][
"sentry_app_identifier"
] = SentryAppIdentifier.SENTRY_APP_ID
self.validated_data["config"]["target_identifier"] = str(installation.sentry_app.id)

settings = self.validated_data["data"].get("settings", [])
action = {
"settings": settings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
ActionFieldMappingKeys,
SentryAppDataBlob,
SentryAppFormConfigDataBlob,
SentryAppIdentifier,
)


Expand All @@ -33,23 +32,9 @@ def get_target_identifier(
if target_identifier is None:
raise ValueError(f"No target identifier found for action type: {action.type}")

# Figure out what type of key we are dealing with
sentry_app_installations = None
if (
action.config.get("sentry_app_identifier")
== SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID
):
# It is a sentry app installation uuid
sentry_app_installations = app_service.get_many(
filter=dict(
uuids=[target_identifier],
)
)
else:
# It is a sentry app id
sentry_app_installations = app_service.get_many(
filter=dict(app_ids=[target_identifier], organization_id=organization_id)
)
sentry_app_installations = app_service.get_many(
filter=dict(app_ids=[target_identifier], organization_id=organization_id)
)

if sentry_app_installations is None or len(sentry_app_installations) != 1:
raise ValueError(
Expand Down
1 change: 0 additions & 1 deletion src/sentry/receivers/outbox/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ def process_sentry_app_installation_deletes(
action_service.update_action_status_for_sentry_app_via_uuid__region(
region_name=region_name,
status=ObjectStatus.DISABLED,
sentry_app_install_uuid=payload["uuid"],
sentry_app_id=payload.get("sentry_app_id"),
organization_id=payload.get("organization_id"),
)
Expand Down
1 change: 0 additions & 1 deletion src/sentry/sentry_apps/models/sentry_app_installation.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ def outboxes_for_delete(self) -> list[ControlOutbox]:
category=OutboxCategory.SENTRY_APP_INSTALLATION_DELETE,
region_name=region_name,
payload={
"uuid": self.uuid,
"sentry_app_id": self.sentry_app_id,
"organization_id": self.organization_id,
},
Expand Down
36 changes: 6 additions & 30 deletions src/sentry/workflow_engine/service/action/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,9 @@ def update_action_status_for_sentry_app_via_uuid(
*,
organization_id: int,
status: int,
sentry_app_install_uuid: str,
sentry_app_id: int | None = None,
) -> None:
sentry_app_id_actions = Action.objects.none()
sentry_app_id_actions = None

if sentry_app_id:
sentry_app_id_actions = Action.objects.filter(
Expand All @@ -45,27 +44,18 @@ def update_action_status_for_sentry_app_via_uuid(
type=Action.Type.SENTRY_APP,
dataconditiongroupaction__condition_group__organization_id=organization_id,
)
installation_uuid_actions = Action.objects.filter(
config__sentry_app_identifier=SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID,
config__target_identifier=sentry_app_install_uuid,
type=Action.Type.SENTRY_APP,
dataconditiongroupaction__condition_group__organization_id=organization_id,
)

actions = sentry_app_id_actions | installation_uuid_actions
if actions:
actions.update(status=status)
if sentry_app_id_actions:
sentry_app_id_actions.update(status=status)

def update_action_status_for_sentry_app_via_uuid__region(
self,
*,
region_name: str,
status: int,
sentry_app_install_uuid: str,
organization_id: int | None = None,
sentry_app_id: int | None = None,
) -> None:
sentry_app_id_actions = Action.objects.none()
sentry_app_id_actions = None

if sentry_app_id and organization_id:
sentry_app_id_actions = Action.objects.filter(
Expand All @@ -74,23 +64,9 @@ def update_action_status_for_sentry_app_via_uuid__region(
type=Action.Type.SENTRY_APP,
dataconditiongroupaction__condition_group__organization_id=organization_id,
)
if organization_id:
installation_uuid_actions = Action.objects.filter(
config__target_identifier=sentry_app_install_uuid,
type=Action.Type.SENTRY_APP,
config__sentry_app_identifier=SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID,
dataconditiongroupaction__condition_group__organization_id=organization_id,
)
else:
installation_uuid_actions = Action.objects.filter(
config__target_identifier=sentry_app_install_uuid,
type=Action.Type.SENTRY_APP,
config__sentry_app_identifier=SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID,
)

actions = sentry_app_id_actions | installation_uuid_actions
if actions:
actions.update(status=status)
if sentry_app_id_actions:
sentry_app_id_actions.update(status=status)

def update_action_status_for_sentry_app_via_sentry_app_id(
self,
Expand Down
2 changes: 0 additions & 2 deletions src/sentry/workflow_engine/service/action/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ def update_action_status_for_sentry_app_via_uuid(
*,
organization_id: int,
status: int,
sentry_app_install_uuid: str,
sentry_app_id: int | None = None,
) -> None:
pass
Expand All @@ -53,7 +52,6 @@ def update_action_status_for_sentry_app_via_uuid__region(
*,
region_name: str,
status: int,
sentry_app_install_uuid: str,
organization_id: int | None = None,
sentry_app_id: int | None = None,
) -> None:
Expand Down
6 changes: 3 additions & 3 deletions tests/sentry/deletions/test_sentry_app_installations.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ def test_disables_actions(self) -> None:
action = self.create_action(
type=Action.Type.SENTRY_APP,
config={
"target_identifier": self.install.uuid,
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID,
"target_identifier": str(self.install.sentry_app_id),
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_ID,
"target_type": ActionTarget.SENTRY_APP,
},
)
Expand All @@ -134,7 +134,7 @@ def test_disables_actions(self) -> None:
type=Action.Type.SENTRY_APP,
config={
"target_identifier": "1234567890",
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID,
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_ID,
"target_type": ActionTarget.SENTRY_APP,
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -712,8 +712,8 @@ def test_build_rule_action_blob_sentry_app_sentry_app_installation_uuid(self) ->
type=Action.Type.SENTRY_APP,
data={"settings": data_blob},
config={
"target_identifier": self.sentry_app_installation.uuid,
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID,
"target_identifier": str(self.sentry_app_installation.sentry_app_id),
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_ID,
"target_type": ActionTarget.SENTRY_APP.value,
},
)
Expand All @@ -733,8 +733,8 @@ def test_build_rule_action_blob_sentry_app_sentry_app_installation_uuid(self) ->
"settings": data_blob,
},
config={
"target_identifier": self.sentry_app_installation2.uuid,
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID,
"target_identifier": str(self.sentry_app_installation2.sentry_app_id),
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_ID,
"target_type": ActionTarget.SENTRY_APP.value,
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ def test_delete_install(self, record: MagicMock) -> None:
action = self.create_action(
type=Action.Type.SENTRY_APP,
config={
"target_identifier": self.installation2.uuid,
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID,
"target_identifier": str(self.installation2.sentry_app_id),
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_ID,
"target_type": ActionTarget.SENTRY_APP,
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,66 +176,6 @@ def test_update_add_sentry_app_action(self) -> None:
}
assert action.data["settings"] == self.sentry_app_settings

@responses.activate
def test_update_add_sentry_app_installation_uuid_action(self) -> None:
"""
Test that adding a sentry app action to a workflow works as expected when we pass the installation UUID instead of the sentry app id.
We do not create new actions like this today, but we have data like this in the DB and need to make sure it still works until we can migrate away from it
to use the sentry app id
"""
responses.add(
method=responses.POST,
url="https://example.com/sentry/alert-rule",
status=200,
)
# update the settings
updated_sentry_app_settings = [
{"name": "alert_prefix", "value": "[Very Good]"},
{"name": "channel", "value": "#pay-attention-to-errors"},
{"name": "best_emoji", "value": ":ice:"},
{"name": "teamId", "value": "1"},
{"name": "assigneeId", "value": "3"},
]
self.valid_workflow["actionFilters"] = [
{
"logicType": "any",
"conditions": [
{
"type": Condition.EQUAL.value,
"comparison": 1,
"conditionResult": True,
}
],
"actions": [
{
"config": {
"sentryAppIdentifier": SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID,
"targetIdentifier": self.sentry_app_installation.uuid,
"targetType": ActionType.SENTRY_APP,
},
"data": {"settings": updated_sentry_app_settings},
"type": Action.Type.SENTRY_APP,
},
],
}
]
response = self.get_success_response(
self.organization.slug, self.workflow.id, raw_data=self.valid_workflow
)
updated_workflow = Workflow.objects.get(id=response.data.get("id"))
action_filter = WorkflowDataConditionGroup.objects.get(workflow=updated_workflow)
dcga = DataConditionGroupAction.objects.get(condition_group=action_filter.condition_group)
action = dcga.action

assert response.status_code == 200
assert action.type == Action.Type.SENTRY_APP
assert action.config == {
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_ID,
"target_identifier": str(self.sentry_app.id),
"target_type": ActionTarget.SENTRY_APP.value,
}
assert action.data["settings"] == updated_sentry_app_settings

def test_update_triggers_with_empty_conditions(self) -> None:
"""Test that passing an empty list to triggers.conditions clears all conditions"""
# Create a workflow with a trigger condition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ def setUp(self) -> None:
self.valid_data = {
"type": Action.Type.SENTRY_APP,
"config": {
"sentryAppIdentifier": SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID,
"sentryAppIdentifier": SentryAppIdentifier.SENTRY_APP_ID,
"targetType": ActionType.SENTRY_APP,
"targetIdentifier": self.sentry_app_installation.uuid,
"targetIdentifier": str(self.sentry_app.id),
},
"data": {"settings": self.sentry_app_settings},
}
Expand Down Expand Up @@ -62,34 +62,6 @@ def test_validate(self, mock_trigger_sentry_app_action_creators: mock.MagicMock)
assert result is True
validator.save()

@mock.patch(
"sentry.rules.actions.sentry_apps.utils.app_service.trigger_sentry_app_action_creators"
)
def test_validate_sentry_app_id(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The basic test_validate covers this case now that the setup data is using sentry app id

self, mock_trigger_sentry_app_action_creators: mock.MagicMock
) -> None:
mock_trigger_sentry_app_action_creators.return_value = RpcAlertRuleActionResult(
success=True, message="success"
)
valid_data = {
"type": Action.Type.SENTRY_APP,
"config": {
"sentryAppIdentifier": SentryAppIdentifier.SENTRY_APP_ID,
"targetType": ActionType.SENTRY_APP,
"targetIdentifier": str(self.sentry_app.id),
},
"data": {"settings": self.sentry_app_settings},
}

validator = BaseActionValidator(
data=valid_data,
context={"organization": self.organization},
)

result = validator.is_valid()
assert result is True
validator.save()

@mock.patch(
"sentry.rules.actions.sentry_apps.utils.app_service.trigger_sentry_app_action_creators"
)
Expand Down Expand Up @@ -124,20 +96,18 @@ def test_validate__rpc_failure(
def test_validate_settings_action_trigger(
self, mock_trigger_sentry_app_action_creators: mock.MagicMock
) -> None:
self.create_sentry_app(
sentry_app = self.create_sentry_app(
organization=self.organization,
name="Test Application",
is_alertable=True,
)
install = self.create_sentry_app_installation(
slug="test-application", organization=self.organization
)
self.create_sentry_app_installation(slug=sentry_app.slug, organization=self.organization)
self.valid_data = {
"type": Action.Type.SENTRY_APP,
"config": {
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_INSTALLATION_UUID,
"sentry_app_identifier": SentryAppIdentifier.SENTRY_APP_ID,
"targetType": ActionType.SENTRY_APP,
"target_identifier": install.uuid,
"target_identifier": str(sentry_app.id),
},
"data": {
"settings": [
Expand Down
Loading
Loading