Skip to content
Merged
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
117 changes: 60 additions & 57 deletions src/sentry/integrations/msteams/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
IntegrationProviderSlug,
IntegrationResponse,
)
from sentry.integrations.utils.webhook_viewer_context import webhook_viewer_context
from sentry.models.activity import ActivityIntegration
from sentry.models.apikey import ApiKey
from sentry.models.group import Group
Expand Down Expand Up @@ -434,18 +435,19 @@ def _handle_team_member_removed(self, request: Request) -> Response:
)
if len(org_integrations) > 0:
for org_integration in org_integrations:
create_audit_entry(
request=request,
organization_id=org_integration.organization_id,
target_object=integration.id,
event=audit_log.get_event_id("INTEGRATION_REMOVE"),
actor_label="Teams User",
data={
"provider": integration.provider,
"name": integration.name,
"team_id": team_id,
},
)
with webhook_viewer_context(org_integration.organization_id):
create_audit_entry(
request=request,
organization_id=org_integration.organization_id,
target_object=integration.id,
event=audit_log.get_event_id("INTEGRATION_REMOVE"),
actor_label="Teams User",
data={
"provider": integration.provider,
"name": integration.name,
"team_id": team_id,
},
)

integration_service.delete_integration(integration_id=integration.id)
return self.respond(status=204)
Expand Down Expand Up @@ -584,60 +586,61 @@ def _handle_action_submitted(self, request: Request) -> Response:
)
return self.respond(status=404)

idp = identity_service.get_provider(
provider_type=IntegrationProviderSlug.MSTEAMS.value, provider_ext_id=team_id
)
if idp is None:
logger.info(
"msteams.action.invalid-team-id",
extra={
"team_id": team_id,
"integration_id": integration.id,
"organization_id": group.organization.id,
},
with webhook_viewer_context(group.organization.id):
idp = identity_service.get_provider(
provider_type=IntegrationProviderSlug.MSTEAMS.value, provider_ext_id=team_id
)
return self.respond(status=404)
if idp is None:
logger.info(
"msteams.action.invalid-team-id",
extra={
"team_id": team_id,
"integration_id": integration.id,
"organization_id": group.organization.id,
},
)
return self.respond(status=404)

identity = identity_service.get_identity(
filter={"provider_id": idp.id, "identity_ext_id": user_id}
)
if identity is None:
associate_url = build_linking_url(
integration, group.organization, user_id, team_id, tenant_id
identity = identity_service.get_identity(
filter={"provider_id": idp.id, "identity_ext_id": user_id}
)
if identity is None:
associate_url = build_linking_url(
integration, group.organization, user_id, team_id, tenant_id
)

card = build_linking_card(associate_url)
user_conversation_id = client.get_user_conversation_id(user_id, tenant_id)
client.send_card(user_conversation_id, card)
return self.respond(status=201)
card = build_linking_card(associate_url)
user_conversation_id = client.get_user_conversation_id(user_id, tenant_id)
client.send_card(user_conversation_id, card)
return self.respond(status=201)

# update the state of the issue
issue_change_response = self._issue_state_change(group, identity, data["value"])
# update the state of the issue
issue_change_response = self._issue_state_change(group, identity, data["value"])

# get the rules from the payload
rules = tuple(Rule.objects.filter(id__in=payload["rules"]))
# get the rules from the payload
rules = tuple(Rule.objects.filter(id__in=payload["rules"]))

# pull the event based off our payload
event = eventstore.backend.get_event_by_id(group.project_id, payload["eventId"])
if event is None:
logger.info(
"msteams.action.event-missing",
extra={
"team_id": team_id,
"integration_id": integration.id,
"organization_id": group.organization.id,
"event_id": payload["eventId"],
"project_id": group.project_id,
},
)
return self.respond(status=404)
# pull the event based off our payload
event = eventstore.backend.get_event_by_id(group.project_id, payload["eventId"])
if event is None:
logger.info(
"msteams.action.event-missing",
extra={
"team_id": team_id,
"integration_id": integration.id,
"organization_id": group.organization.id,
"event_id": payload["eventId"],
"project_id": group.project_id,
},
)
return self.respond(status=404)

# refresh issue and update card
group.refresh_from_db()
card = MSTeamsIssueMessageBuilder(group, event, rules, integration).build_group_card()
client.update_card(conversation_id, activity_id, card)
# refresh issue and update card
group.refresh_from_db()
card = MSTeamsIssueMessageBuilder(group, event, rules, integration).build_group_card()
client.update_card(conversation_id, activity_id, card)

return issue_change_response
return issue_change_response

def _handle_channel_message(self, request: Request) -> Response:
data = request.data
Expand Down
27 changes: 27 additions & 0 deletions tests/sentry/integrations/msteams/test_action_state_change.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import time
from typing import Any
from unittest.mock import MagicMock, patch

import orjson
Expand All @@ -22,6 +23,7 @@
from sentry.silo.base import SiloMode
from sentry.testutils.asserts import assert_mock_called_once_with_partial, assert_slo_metric
from sentry.testutils.cases import APITestCase
from sentry.testutils.helpers.options import override_options
from sentry.testutils.silo import assume_test_silo_mode
from sentry.testutils.skips import requires_snuba
from sentry.users.models.identity import Identity, IdentityStatus
Expand Down Expand Up @@ -359,6 +361,31 @@ def test_resolve_with_params(self, verify: MagicMock, client_put: MagicMock) ->

assert_mock_called_once_with_partial(client_put, data=expected_data)

@responses.activate
@override_options({"viewer-context.enabled": True})
@patch("sentry.integrations.msteams.webhook.verify_signature", return_value=True)
def test_action_submitted_sets_viewer_context(self, verify: MagicMock) -> None:
"""ViewerContext is set with org_id and actor_type=INTEGRATION during action handling."""
from sentry.viewer_context import ActorType, ViewerContext, get_viewer_context

captured_contexts: list[ViewerContext | None] = []

original_refresh = Group.refresh_from_db

def capturing_refresh(self_group: Any, *args: Any, **kwargs: Any) -> None:
captured_contexts.append(get_viewer_context())
return original_refresh(self_group, *args, **kwargs)

with patch.object(Group, "refresh_from_db", capturing_refresh):
resp = self.post_webhook(action_type=ACTION_TYPE.RESOLVE, resolve_input="resolved")

assert resp.status_code == 200
assert len(captured_contexts) == 1
ctx = captured_contexts[0]
assert ctx is not None
assert ctx.organization_id == self.org.id
assert ctx.actor_type == ActorType.INTEGRATION

@responses.activate
@patch("sentry.integrations.msteams.webhook.verify_signature", return_value=True)
def test_no_integration(self, verify: MagicMock) -> None:
Expand Down
48 changes: 48 additions & 0 deletions tests/sentry/integrations/msteams/test_webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from sentry.silo.base import SiloMode
from sentry.testutils.asserts import assert_slo_metric
from sentry.testutils.cases import APITestCase
from sentry.testutils.helpers.options import override_options
from sentry.testutils.silo import assume_test_silo_mode
from sentry.users.models.identity import Identity
from sentry.utils import jwt
Expand Down Expand Up @@ -608,3 +609,50 @@ def test_invalid_silo_card_action_payload(
HTTP_AUTHORIZATION=f"Bearer {TOKEN}",
)
assert response.status_code == 400

@responses.activate
@override_options({"viewer-context.enabled": True})
@mock.patch("sentry.utils.jwt.decode")
@mock.patch("time.time")
def test_member_removed_sets_viewer_context(
self, mock_time: MagicMock, mock_decode: MagicMock
) -> None:
"""ViewerContext is set with org_id and actor_type=INTEGRATION during member removal."""
from sentry.viewer_context import ActorType, ViewerContext, get_viewer_context

with assume_test_silo_mode(SiloMode.CONTROL):
integration = self.create_provider_integration(external_id=team_id, provider="msteams")
self.create_organization_integration(
organization_id=self.organization.id, integration=integration
)

captured_contexts: list[ViewerContext | None] = []

original_create_audit_entry = __import__(
"sentry.utils.audit", fromlist=["create_audit_entry"]
).create_audit_entry

def capturing_create_audit_entry(*args: object, **kwargs: object) -> object:
captured_contexts.append(get_viewer_context())
return original_create_audit_entry(*args, **kwargs)

mock_time.return_value = 1594839999 + 60
mock_decode.return_value = DECODED_TOKEN

with mock.patch(
"sentry.integrations.msteams.webhook.create_audit_entry",
side_effect=capturing_create_audit_entry,
):
resp = self.client.post(
path=webhook_url,
data=EXAMPLE_TEAM_MEMBER_REMOVED,
format="json",
HTTP_AUTHORIZATION=f"Bearer {TOKEN}",
)

assert resp.status_code == 204
assert len(captured_contexts) == 1
ctx = captured_contexts[0]
assert ctx is not None
assert ctx.organization_id == self.organization.id
assert ctx.actor_type == ActorType.INTEGRATION
Loading