Skip to content
Open
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
19 changes: 18 additions & 1 deletion src/sentry/incidents/serializers/alert_rule_trigger_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,16 +192,27 @@ def validate(self, attrs):
should_validate_channel_id = self.context.get("validate_channel_id", True)
# validate_channel_id is assumed to be true unless explicitly passed as false
if attrs["input_channel_id"] and should_validate_channel_id:
validate_slack_entity_id(
# validate_slack_entity_id returns the actual name from Slack API
# This enables auto-correction when users provide channel IDs as names
actual_slack_name = validate_slack_entity_id(
integration_id=attrs["integration_id"],
input_name=identifier,
input_id=attrs["input_channel_id"],
)
# Store the actual name for use in target_display later
attrs["actual_slack_name"] = actual_slack_name
Comment on lines +195 to +203
Copy link

Choose a reason for hiding this comment

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

Bug: The NotificationActionSerializer fails to use the return value from validate_slack_entity_id, preventing auto-correction of Slack channel names from their IDs in the API.
Severity: HIGH | Confidence: High

🔍 Detailed Analysis

The NotificationActionSerializer in notification_action_request.py calls validate_slack_entity_id but does not capture or use its return value. This function was updated to return the actual channel name when a channel ID is provided. While other serializers in the PR correctly use this return value to auto-correct the channel name, this one does not. As a result, if a user provides a Slack channel ID as the target_display when creating or updating a notification action via the API, the ID will be persisted and displayed in the UI instead of the human-readable channel name, defeating the purpose of the auto-correction feature.

💡 Suggested Fix

In notification_action_request.py, capture the return value from the validate_slack_entity_id call. If the returned value is not null, update the target_display in the data dictionary with the corrected channel name before returning it, mirroring the implementation in alert_rule_trigger_action.py.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/sentry/incidents/serializers/alert_rule_trigger_action.py#L195-L203

Potential issue: The `NotificationActionSerializer` in `notification_action_request.py`
calls `validate_slack_entity_id` but does not capture or use its return value. This
function was updated to return the actual channel name when a channel ID is provided.
While other serializers in the PR correctly use this return value to auto-correct the
channel name, this one does not. As a result, if a user provides a Slack channel ID as
the `target_display` when creating or updating a notification action via the API, the ID
will be persisted and displayed in the UI instead of the human-readable channel name,
defeating the purpose of the auto-correction feature.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 8125783

return attrs

def create(self, validated_data):
for key in ("id", "sentry_app_installation_uuid"):
validated_data.pop(key, None)

# If we have the actual Slack name from validation, use it for target_identifier
# This fixes the issue where channel IDs are used as channel names (issue #105478)
actual_slack_name = validated_data.pop("actual_slack_name", None)
if actual_slack_name is not None:
validated_data["target_identifier"] = actual_slack_name

try:
action = create_alert_rule_trigger_action(
trigger=self.context["trigger"], **validated_data
Expand Down Expand Up @@ -229,6 +240,12 @@ def update(self, instance, validated_data):
for key in ("id", "sentry_app_installation_uuid"):
validated_data.pop(key, None)

# If we have the actual Slack name from validation, use it for target_identifier
# This fixes the issue where channel IDs are used as channel names (issue #105478)
actual_slack_name = validated_data.pop("actual_slack_name", None)
if actual_slack_name is not None:
validated_data["target_identifier"] = actual_slack_name

try:
action = update_alert_rule_trigger_action(instance, **validated_data)
except (ApiRateLimitedError, InvalidTriggerActionError) as e:
Expand Down
21 changes: 18 additions & 3 deletions src/sentry/integrations/slack/actions/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from sentry.integrations.services.integration import integration_service
from sentry.integrations.slack.utils.channel import (
get_channel_id,
is_input_a_user_id,
strip_channel_name,
validate_slack_entity_id,
)
Expand Down Expand Up @@ -52,6 +53,10 @@ def clean(self) -> dict[str, Any] | None:
or self.data.get("input_channel_id")
or self.data.get("channel_id")
)
# Track the actual name from Slack API for auto-correction
actual_slack_name: str | None = None
channel_prefix = ""

if channel_id:
logger.info(
"rule.slack.provide_channel_id",
Expand Down Expand Up @@ -80,7 +85,8 @@ def clean(self) -> dict[str, Any] | None:

if channel_id:
try:
validate_slack_entity_id(
# validate_slack_entity_id now returns the actual name from Slack API
actual_slack_name = validate_slack_entity_id(
integration_id=workspace,
input_name=self.data["channel"],
input_id=channel_id,
Expand All @@ -102,7 +108,6 @@ def clean(self) -> dict[str, Any] | None:

channel = cleaned_data.get("channel", "")
timed_out = False
channel_prefix = ""

# XXX(meredith): If the user is creating/updating a rule via the API and provides
# the channel_id in the request, we don't need to call the channel_transformer - we
Expand Down Expand Up @@ -130,7 +135,17 @@ def clean(self) -> dict[str, Any] | None:
code="invalid",
)

channel = strip_channel_name(channel)
# Use the actual name from Slack API if available (auto-correction for channel ID as name)
if actual_slack_name:
channel = actual_slack_name
# If we auto-corrected, we need to ensure the prefix is correct based on the ID type
if is_input_a_user_id(channel_id):
channel_prefix = "@"
else:
channel_prefix = "#"
else:
channel = strip_channel_name(channel)

if channel_id is None and timed_out:
cleaned_data["channel"] = channel_prefix + channel
self._pending_save = True
Expand Down
41 changes: 23 additions & 18 deletions src/sentry/integrations/slack/utils/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,25 @@ def is_input_a_user_id(input_id: str) -> bool:
return input_id.startswith("U") or input_id.startswith("W")


def validate_slack_entity_id(*, integration_id: int, input_name: str, input_id: str) -> None:
def validate_slack_entity_id(*, integration_id: int, input_name: str, input_id: str) -> str:
"""
Accepts a name and input ID that could correspond to a user or channel.
Returns the actual name from Slack API for auto-correction.
"""
if is_input_a_user_id(input_id):
validate_user_id(
return validate_user_id(
input_name=input_name, input_user_id=input_id, integration_id=integration_id
)
else:
validate_channel_id(
return validate_channel_id(
name=input_name, input_channel_id=input_id, integration_id=integration_id
)


def validate_user_id(*, input_name: str, input_user_id: str, integration_id: int) -> None:
def validate_user_id(*, input_name: str, input_user_id: str, integration_id: int) -> str:
"""
Validates that a user-input name and ID correspond to the same valid slack user.
Functionally identical to validate_channel_id, but for users.
Returns the actual username from Slack API for auto-correction.
"""
client = SlackSdkClient(integration_id=integration_id)
try:
Expand Down Expand Up @@ -139,22 +140,25 @@ def validate_user_id(*, input_name: str, input_user_id: str, integration_id: int
if not isinstance(results, dict):
raise IntegrationError("Bad slack user list response.")

stripped_user_name = strip_channel_name(input_name)
possible_name_matches = [
results.get("user", {}).get("name"),
results.get("user", {}).get("profile", {}).get("display_name"),
results.get("user", {}).get("profile", {}).get("display_name_normalized"),
]
if not any(possible_name_matches):
# Get the actual username from Slack API
# Prefer the 'name' field which is the username
actual_username = results.get("user", {}).get("name")
if not actual_username:
# Fallback to display name if username not available
actual_username = results.get("user", {}).get("profile", {}).get("display_name")
if not actual_username:
raise ValidationError("Did not receive user name from API results")
if stripped_user_name not in possible_name_matches:
raise ValidationError("Slack username from ID does not match input username.")

return actual_username

def validate_channel_id(name: str, integration_id: int, input_channel_id: str) -> None:

def validate_channel_id(name: str, integration_id: int, input_channel_id: str) -> str:
"""
In the case that the user is creating an alert via the API and providing the channel ID and name
themselves, we want to make sure both values are correct.

Returns the actual channel name from Slack API, which can be used to auto-correct
user-provided channel names when they mistakenly use channel IDs.
"""

client = SlackSdkClient(integration_id=integration_id)
Expand Down Expand Up @@ -190,12 +194,13 @@ def validate_channel_id(name: str, integration_id: int, input_channel_id: str) -
if not isinstance(results, dict):
raise IntegrationError("Bad slack channel list response.")

stripped_channel_name = strip_channel_name(name)
results_channel_name = results.get("channel", {}).get("name")
if not results_channel_name:
raise ValidationError("Did not receive channel name from API results")
if stripped_channel_name != results_channel_name:
raise ValidationError("Slack channel name from ID does not match input channel name.")

# Return the actual channel name from Slack API
# This allows auto-correction when user provides channel ID as name
return results_channel_name


def get_channel_id_with_timeout(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1606,70 +1606,66 @@ def test_create_slack_alert_with_name_and_channel_id_sdk(self) -> None:
assert stored_action["inputChannelId"] == str(channelID)
assert stored_action["targetIdentifier"] == channelName

def test_create_slack_alert_with_mismatch_name_and_channel_id_sdk(self) -> None:
def test_channel_name_auto_corrected_from_slack_api(self) -> None:
"""
The user specifies the Slack channel and channel ID but they do not match.
When the user provides a channel name that doesn't match the actual channel name from Slack,
the system auto-corrects it to the actual channel name (instead of returning an error).
This is part of the fix for issue #105478.
"""
self.create_member(
user=self.user, organization=self.organization, role="owner", teams=[self.team]
)
self.login_as(self.user)
otherChannel = "some-other-channel"
channelName = "my-channel"
actualChannelName = "some-other-channel"
inputChannelName = "my-channel" # User typed wrong name
# Specifying an inputChannelID will cause the validate_channel_id logic to be triggered
channelID = "C12345678"
channel = {"name": otherChannel}
channel = {"name": actualChannelName}
with self.mock_conversations_info(channel):
with (
assume_test_silo_mode(SiloMode.REGION),
override_settings(SILO_MODE=SiloMode.REGION),
):
resp = self._organization_alert_rule_api_call(
channelName=channelName, channelID=channelID
channelName=inputChannelName, channelID=channelID
)

assert resp.status_code == 400
assert resp.data == {
"nonFieldErrors": [
ErrorDetail(
string="Slack channel name from ID does not match input channel name.",
code="invalid",
)
]
}
# The form should be valid and auto-correct the channel name
assert resp.status_code == 200
stored_action = resp.data["triggers"][0]["actions"][0]
assert stored_action["inputChannelId"] == str(channelID)
# The target identifier should be auto-corrected to the actual channel name
assert stored_action["targetIdentifier"] == actualChannelName

def test_create_slack_alert_with_mismatch_name_and_user_id_sdk(self) -> None:
def test_username_auto_corrected_from_slack_api(self) -> None:
"""
The user specifies the Slack user and user ID but they do not match.
When the user provides a username that doesn't match the actual username from Slack,
the system auto-corrects it to the actual username (instead of returning an error).
This is part of the fix for issue #105478.
"""
self.create_member(
user=self.user, organization=self.organization, role="owner", teams=[self.team]
)
self.login_as(self.user)
otherUserId = "U12345678"
otherUser = {
"id": otherUserId,
"name": "kim.possible",
userId = "U12345678"
actualUser = {
"id": userId,
"name": "kim.possible", # This is the actual username
"profile": {
"display_name": "Kim Possible 🕵️‍♀️",
"display_name_normalized": "Kim Possible",
},
}
inputName = "Ron Stoppable"
inputName = "Ron Stoppable" # User typed wrong name

with self.mock_users_info(user=otherUser):
resp = self._organization_alert_rule_api_call(
channelName=inputName, channelID=otherUserId
)
assert resp.status_code == 400
assert resp.data == {
"nonFieldErrors": [
ErrorDetail(
string="Slack username from ID does not match input username.",
code="invalid",
)
]
}
with self.mock_users_info(user=actualUser):
resp = self._organization_alert_rule_api_call(channelName=inputName, channelID=userId)
# The form should be valid and auto-correct the username
assert resp.status_code == 200
stored_action = resp.data["triggers"][0]["actions"][0]
assert stored_action["inputChannelId"] == str(userId)
# The target identifier should be auto-corrected to the actual username
assert stored_action["targetIdentifier"] == "kim.possible"

def test_create_slack_alert_with_missing_name_from_sdk(self) -> None:
"""
Expand Down
75 changes: 68 additions & 7 deletions tests/sentry/integrations/slack/test_notify_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,64 @@ def test_channel_id_provided_sdk(self) -> None:
form = rule.get_form_instance()
assert form.is_valid()

def test_channel_id_as_channel_name_auto_corrected(self) -> None:
"""
Test that when a user provides a channel ID as the channel name,
the form auto-corrects it to the actual channel name from Slack.
This is for issue #105478.
"""
# User mistakenly puts the channel ID in the channel name field
channel = {"name": "public", "id": "C013TMFDEAV"}
with self.mock_conversations_info(channel):
rule = self.get_rule(
data={
"workspace": self.integration.id,
"channel": "#C013TMFDEAV",
"input_channel_id": "C013TMFDEAV",
"tags": "",
}
)

form = rule.get_form_instance()
# Form should be valid and auto-correct the channel name
self.assert_form_valid(form, "C013TMFDEAV", "#public")

def test_user_id_as_channel_name_auto_corrected(self) -> None:
"""
Test that when a user provides a User ID as the channel name,
the form auto-corrects it to the actual username with @ prefix.
"""
# User mistakenly puts the User ID in the channel name field
user = {"name": "morty", "id": "U1234567"}
with self.mock_conversations_info(user):
rule = self.get_rule(
data={
"workspace": self.integration.id,
"channel": "U1234567",
"input_channel_id": "U1234567",
"tags": "",
}
)

form = rule.get_form_instance()
# Form should be valid and auto-correct to @morty
# Note: The mock helpers in this test file might behave differently for users vs channels
# We need to ensure mock_conversations_info mocks user lookup correctly or use a different mock

# The validate_user_id calls client.users_info.
# mock_conversations_info mocks conversations_info BUT validate_slack_entity_id
# will call validate_user_id if ID starts with U, which calls users_info.
# So we need to mock users_info.

with patch(
"slack_sdk.web.client.WebClient.users_info",
return_value={
"ok": True,
"user": {"name": "morty", "profile": {"display_name": "Morty Smith"}},
},
):
self.assert_form_valid(form, "U1234567", "@morty")

def test_invalid_channel_id_provided_sdk(self) -> None:
with patch(
"slack_sdk.web.client.WebClient.conversations_info",
Expand All @@ -273,23 +331,26 @@ def test_invalid_channel_id_provided_sdk(self) -> None:
assert not form.is_valid()
assert "Channel not found. Invalid ID provided." in str(form.errors.values())

def test_invalid_channel_name_provided_sdk(self) -> None:
def test_mismatched_channel_name_auto_corrected(self) -> None:
"""
Test that when the user-provided channel name doesn't match the actual name from Slack,
the form auto-corrects it to the actual channel name (instead of raising an error).
This is part of the fix for issue #105478.
"""
channel = {"name": "my-channel", "id": "C2349874"}
with self.mock_conversations_info(channel):
rule = self.get_rule(
data={
"workspace": self.integration.id,
"channel": "#my-chanel",
"input_channel_id": "C1234567",
"channel": "#my-chanel", # User typed wrong name
"input_channel_id": "C2349874",
"tags": "",
}
)

form = rule.get_form_instance()
assert not form.is_valid()
assert "Slack: Slack channel name from ID does not match input channel name." in str(
form.errors.values()
)
# Form should be valid and auto-correct the channel name
self.assert_form_valid(form, "C2349874", "#my-channel")

def test_invalid_workspace(self) -> None:
# the workspace _should_ be the integration id
Expand Down
Loading
Loading