-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
feat(seer-infra-telemetry): PAT modal builder + submission handler for monitoring providers #118607
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from typing import Any | ||
|
|
||
| from rest_framework.request import Request | ||
| from rest_framework.response import Response | ||
|
|
||
|
|
@@ -11,12 +13,17 @@ | |
| ControlSiloOrganizationEndpoint, | ||
| OrganizationPermission, | ||
| ) | ||
| from sentry.identity.datadog.provider import DATADOG_VALID_SITES | ||
| from sentry.organizations.services.organization.model import RpcOrganization | ||
| from sentry.users.models.identity import OrganizationIdentity | ||
|
|
||
| MONITORING_PROVIDERS: dict[str, dict[str, str]] = { | ||
| MONITORING_PROVIDERS: dict[str, dict[str, Any]] = { | ||
| "datadog": {"name": "Datadog"}, | ||
| "datadog_pat": {"name": "Datadog (Personal Access Token)"}, | ||
| "datadog_pat": { | ||
| "name": "Datadog (Personal Access Token)", | ||
| "pat_hint": "Create one at Organization Settings → Access → API Keys in Datadog.", | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll eventually want more instruction/detail than this (and probably displayed in a more readable/understandable way). e.g. we'll want to indicate which scopes/expiration to set for the token (all read scopes, max of 1 year). I'm thinking this is okay for now for testing. |
||
| "sites": DATADOG_VALID_SITES, | ||
| }, | ||
| "gcp": {"name": "Google Cloud Platform"}, | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ | |
| from sentry.api.helpers.group_index import update_groups | ||
| from sentry.auth.access import from_member | ||
| from sentry.exceptions import UnableToAcceptMemberInvitationException | ||
| from sentry.identity.services.identity import identity_service | ||
| from sentry.integrations.messaging.metrics import ( | ||
| MessageInteractionFailureReason, | ||
| MessagingInteractionEvent, | ||
|
|
@@ -46,6 +47,11 @@ | |
| from sentry.integrations.slack.sdk_client import SlackSdkClient | ||
| from sentry.integrations.slack.spec import SlackMessagingSpec | ||
| from sentry.integrations.slack.utils.errors import MODAL_NOT_FOUND, unpack_slack_api_error | ||
| from sentry.integrations.slack.webhooks.monitoring_provider import ( | ||
| MONITORING_PROVIDER_CALLBACK_ID, | ||
| handle_monitoring_provider_submission, | ||
| open_monitoring_provider_modal, | ||
| ) | ||
| from sentry.integrations.types import ExternalProviderEnum, IntegrationProviderSlug | ||
| from sentry.integrations.utils.scope import bind_org_context_from_integration | ||
| from sentry.issues.action_log import ActionSource, GroupActionActor, action_context_scope | ||
|
|
@@ -753,9 +759,27 @@ | |
| if action_option in ["approve_member", "reject_member"]: | ||
| return self.handle_member_approval(slack_request, action_option) | ||
|
|
||
| if action_option in NOTIFICATION_SETTINGS_ACTION_OPTIONS: | ||
|
Check warning on line 762 in src/sentry/integrations/slack/webhooks/action.py
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. User-submitted PAT logged to application logs via slack.action.request The monitoring-provider modal submits a third-party personal access token in Evidence
Identified by Warden security-review · L2P-KS2 |
||
| return self.handle_enable_notifications(slack_request) | ||
|
|
||
| # Monitoring provider modals have no group context, so intercept before _handle_group_actions. | ||
| if slack_request.type == "view_submission": | ||
| view = slack_request.data.get("view", {}) | ||
| if view.get("callback_id") == MONITORING_PROVIDER_CALLBACK_ID: | ||
| result = handle_monitoring_provider_submission(slack_request) | ||
| if result: | ||
| return self.respond(result) | ||
| return self.respond() | ||
|
|
||
| if action_id == SlackAction.CONNECT_MONITORING_PROVIDER.value: | ||
| return self._handle_connect_monitoring_provider(slack_request) | ||
|
|
||
| if action_id in { | ||
| SlackAction.SKIP_MONITORING_PROVIDER.value, | ||
| SlackAction.DECLINE_MONITORING_PROVIDER.value, | ||
| }: | ||
| return self._handle_skip_monitoring_provider(slack_request, action_id) | ||
|
|
||
| action_list = self.get_action_list(slack_request=slack_request) | ||
| return self._handle_group_actions(slack_request, request, action_list) | ||
|
|
||
|
|
@@ -899,6 +923,37 @@ | |
| ) | ||
| return self.respond() | ||
|
|
||
| def _handle_connect_monitoring_provider(self, slack_request: SlackActionRequest) -> Response: | ||
| action_option, _ = self.get_action_option(slack_request=slack_request) | ||
| provider_key = action_option or "" | ||
| error = open_monitoring_provider_modal( | ||
| slack_request, | ||
| provider_key, | ||
| channel_id=slack_request.channel_id, | ||
| ) | ||
| if error: | ||
| return self.respond_ephemeral(error) | ||
| return self.respond() | ||
|
|
||
| def _handle_skip_monitoring_provider( | ||
| self, slack_request: SlackActionRequest, action_id: str | None | ||
| ) -> Response: | ||
| # SKIP_MONITORING_PROVIDER: per-run suppression is handled by the caller that sends | ||
| # the prompt (the Seer operator won't re-prompt for the same run), so an ack is enough. | ||
| # DECLINE_MONITORING_PROVIDER: persists the provider on the Slack Identity so future | ||
| # unsolicited prompts are suppressed across runs. | ||
| if action_id == SlackAction.DECLINE_MONITORING_PROVIDER.value: | ||
| action_option, _ = self.get_action_option(slack_request=slack_request) | ||
| provider_key = action_option or "" | ||
| slack_identity_rpc = slack_request.get_identity() | ||
| if slack_identity_rpc and provider_key: | ||
| data = dict(slack_identity_rpc.data) if slack_identity_rpc.data else {} | ||
| declined = set(data.get("declined_monitoring_providers", [])) | ||
| declined.add(provider_key) | ||
| data["declined_monitoring_providers"] = sorted(declined) | ||
| identity_service.update_data(identity_id=slack_identity_rpc.id, data=data) | ||
| return self.respond() | ||
|
|
||
|
|
||
| class _ModalDialog(ABC): | ||
| @property | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lmk what y'all think about this structure for storing provider-specific configuration for Slack flows. Alternatively we could just add some branching in the Slack message builder to handle providers separately (for the blocks that are different)