From bc535ac5dff2687bc2daa2dcd29ee3a912c7d670 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Tue, 6 Jun 2023 01:59:12 -0600 Subject: [PATCH 01/17] Webhooks 2 hide secrets (#2104) Replace password and authorization header fields with placeholders when returning data to the UI. Mask the authorization header field when editing and in the status logs. --- engine/apps/api/serializers/webhook.py | 16 ++++++++++++++++ engine/apps/api/tests/test_webhooks.py | 9 +++++---- engine/apps/webhooks/models/webhook.py | 2 ++ engine/apps/webhooks/tasks/trigger_webhook.py | 10 +++++++++- .../OutgoingWebhook2Form.config.tsx | 2 +- 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/engine/apps/api/serializers/webhook.py b/engine/apps/api/serializers/webhook.py index 3d1cd32c..72d6a935 100644 --- a/engine/apps/api/serializers/webhook.py +++ b/engine/apps/api/serializers/webhook.py @@ -4,6 +4,7 @@ from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator from apps.webhooks.models import Webhook, WebhookResponse +from apps.webhooks.models.webhook import WEBHOOK_FIELD_PLACEHOLDER from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField from common.api_helpers.utils import CurrentOrganizationDefault, CurrentTeamDefault, CurrentUserDefault from common.jinja_templater import apply_jinja_template @@ -66,6 +67,21 @@ class WebhookSerializer(serializers.ModelSerializer): validators = [UniqueTogetherValidator(queryset=Webhook.objects.all(), fields=["name", "organization"])] + def to_representation(self, instance): + result = super().to_representation(instance) + if instance.password: + result["password"] = WEBHOOK_FIELD_PLACEHOLDER + if instance.authorization_header: + result["authorization_header"] = WEBHOOK_FIELD_PLACEHOLDER + return result + + def to_internal_value(self, data): + if data.get("password") == WEBHOOK_FIELD_PLACEHOLDER: + data["password"] = self.instance.password + if data.get("authorization_header") == WEBHOOK_FIELD_PLACEHOLDER: + data["authorization_header"] = self.instance.authorization_header + return super().to_internal_value(data) + def _validate_template_field(self, template): try: apply_jinja_template(template, alert_payload=defaultdict(str), alert_group_id="alert_group_1") diff --git a/engine/apps/api/tests/test_webhooks.py b/engine/apps/api/tests/test_webhooks.py index 31015481..f379425b 100644 --- a/engine/apps/api/tests/test_webhooks.py +++ b/engine/apps/api/tests/test_webhooks.py @@ -10,6 +10,7 @@ from rest_framework.test import APIClient from apps.api.permissions import LegacyAccessControlRole from apps.webhooks.models import Webhook +from apps.webhooks.models.webhook import WEBHOOK_FIELD_PLACEHOLDER TEST_URL = "https://some-url" @@ -44,8 +45,8 @@ def test_get_list_webhooks(webhook_internal_api_setup, make_user_auth_headers): "url": "https://github.com/", "data": '{"name": "{{ alert_payload }}"}', "username": "Chris Vanstras", - "password": "qwerty", - "authorization_header": "auth_token", + "password": WEBHOOK_FIELD_PLACEHOLDER, + "authorization_header": WEBHOOK_FIELD_PLACEHOLDER, "forward_all": False, "headers": None, "http_method": "POST", @@ -85,8 +86,8 @@ def test_get_detail_webhook(webhook_internal_api_setup, make_user_auth_headers): "url": "https://github.com/", "data": '{"name": "{{ alert_payload }}"}', "username": "Chris Vanstras", - "password": "qwerty", - "authorization_header": "auth_token", + "password": WEBHOOK_FIELD_PLACEHOLDER, + "authorization_header": WEBHOOK_FIELD_PLACEHOLDER, "forward_all": False, "headers": None, "http_method": "POST", diff --git a/engine/apps/webhooks/models/webhook.py b/engine/apps/webhooks/models/webhook.py index 50233336..97036576 100644 --- a/engine/apps/webhooks/models/webhook.py +++ b/engine/apps/webhooks/models/webhook.py @@ -23,6 +23,8 @@ from common.jinja_templater import apply_jinja_template from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length +WEBHOOK_FIELD_PLACEHOLDER = "****************" + def generate_public_primary_key_for_webhook(): prefix = "WH" diff --git a/engine/apps/webhooks/tasks/trigger_webhook.py b/engine/apps/webhooks/tasks/trigger_webhook.py index fab0df1a..c0e9e566 100644 --- a/engine/apps/webhooks/tasks/trigger_webhook.py +++ b/engine/apps/webhooks/tasks/trigger_webhook.py @@ -11,6 +11,7 @@ from apps.alerts.models import AlertGroup, AlertGroupLogRecord, EscalationPolicy from apps.base.models import UserNotificationPolicyLogRecord from apps.user_management.models import User from apps.webhooks.models import Webhook, WebhookResponse +from apps.webhooks.models.webhook import WEBHOOK_FIELD_PLACEHOLDER from apps.webhooks.utils import ( InvalidWebhookData, InvalidWebhookHeaders, @@ -94,6 +95,12 @@ def _build_payload(webhook, alert_group, user): return data +def mask_authorization_header(headers): + if "Authorization" in headers: + headers["Authorization"] = WEBHOOK_FIELD_PLACEHOLDER + return headers + + def make_request(webhook, alert_group, data): status = { "url": None, @@ -115,7 +122,8 @@ def make_request(webhook, alert_group, data): if triggered: status["url"] = webhook.build_url(data) request_kwargs = webhook.build_request_kwargs(data, raise_data_errors=True) - status["request_headers"] = json.dumps(request_kwargs.get("headers", {})) + headers = mask_authorization_header(request_kwargs.get("headers", {})) + status["request_headers"] = json.dumps(headers) if "json" in request_kwargs: status["request_data"] = json.dumps(request_kwargs["json"]) else: diff --git a/grafana-plugin/src/containers/OutgoingWebhook2Form/OutgoingWebhook2Form.config.tsx b/grafana-plugin/src/containers/OutgoingWebhook2Form/OutgoingWebhook2Form.config.tsx index 52de59f3..06f98e40 100644 --- a/grafana-plugin/src/containers/OutgoingWebhook2Form/OutgoingWebhook2Form.config.tsx +++ b/grafana-plugin/src/containers/OutgoingWebhook2Form/OutgoingWebhook2Form.config.tsx @@ -161,7 +161,7 @@ export const form: { name: string; fields: FormItem[] } = { }, { name: 'authorization_header', - type: FormItemType.Input, + type: FormItemType.Password, }, { name: 'trigger_template', From 0a78b99fd829a87604b39c97c6a256ce0fbde22a Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Tue, 6 Jun 2023 10:46:17 +0200 Subject: [PATCH 02/17] allow mobile app to consume internal schedules api endpoints (#2109) # What this PR does allow mobile app to consume internal schedules api endpoints ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --- CHANGELOG.md | 4 ++++ engine/apps/api/views/schedule.py | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56583ebf..f650206d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Allow mobile app to consume "internal" schedules API endpoints by @joeyorlando ([#2109](https://github.com/grafana/oncall/pull/2109)) + ## v1.2.39 (2023-06-06) ### Changed diff --git a/engine/apps/api/views/schedule.py b/engine/apps/api/views/schedule.py index afdd025b..c87e5b5d 100644 --- a/engine/apps/api/views/schedule.py +++ b/engine/apps/api/views/schedule.py @@ -27,6 +27,7 @@ from apps.api.serializers.user import ScheduleUserSerializer from apps.auth_token.auth import PluginAuthentication from apps.auth_token.constants import SCHEDULE_EXPORT_TOKEN_NAME from apps.auth_token.models import ScheduleExportAuthToken +from apps.mobile_app.auth import MobileAppAuthTokenAuthentication from apps.schedules.models import OnCallSchedule from apps.slack.models import SlackChannel from apps.slack.tasks import update_slack_user_group_for_schedules @@ -72,7 +73,10 @@ class ScheduleView( ModelViewSet, mixins.ListModelMixin, ): - authentication_classes = (PluginAuthentication,) + authentication_classes = ( + MobileAppAuthTokenAuthentication, + PluginAuthentication, + ) permission_classes = (IsAuthenticated, RBACPermission) rbac_permissions = { "metadata": [RBACPermission.Permissions.SCHEDULES_READ], From ed7da1953f577e71f0d3a489ab190030f214e2fe Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Tue, 6 Jun 2023 13:59:21 +0200 Subject: [PATCH 03/17] UID has been added to Integrations table, Integration page and routes (#2112) # What this PR does UID has been added to Integrations table, Integration page and routes --- .../HamburgerMenu/HamburgerMenu.module.css | 19 +++ .../HamburgerMenu/HamburgerMenu.tsx | 39 ++++++ ...xpandedIntegrationRouteDisplay.module.scss | 42 ++++++ .../ExpandedIntegrationRouteDisplay.tsx | 41 +++++- .../integration_2/Integration2.module.scss | 18 +-- .../src/pages/integration_2/Integration2.tsx | 49 +++---- .../integrations_2/Integrations2.module.scss | 26 ++++ .../pages/integrations_2/Integrations2.tsx | 124 ++++++++++++++---- 8 files changed, 286 insertions(+), 72 deletions(-) create mode 100644 grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.module.css create mode 100644 grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.tsx diff --git a/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.module.css b/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.module.css new file mode 100644 index 00000000..0a753b7d --- /dev/null +++ b/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.module.css @@ -0,0 +1,19 @@ +.hamburger-menu { + cursor: pointer; + color: var(--primary-text-color); +} + +.hamburger-menu-withBackground { + display: inline-flex; + flex-direction: column; + align-items: center; + vertical-align: middle; + justify-content: center; + background-color: rgba(204, 204, 220, 0.16); + border: 1px solid transparent; + height: 32px; + width: 30px; + padding: 4px; + cursor: pointer; + color: var(--primary-text-color); +} diff --git a/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.tsx b/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.tsx new file mode 100644 index 00000000..7b7c1b48 --- /dev/null +++ b/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.tsx @@ -0,0 +1,39 @@ +import React, { useRef } from 'react'; + +import { Icon } from '@grafana/ui'; +import cn from 'classnames/bind'; + +import styles from './HamburgerMenu.module.css'; + +interface HamburgerMenuProps { + openMenu: React.MouseEventHandler; + listWidth: number; + listBorder: number; + withBackground?: boolean; + className?: string; +} + +const cx = cn.bind(styles); + +const HamburgerMenu: React.FC = (props) => { + const ref = useRef(); + const { openMenu, listBorder, listWidth, withBackground, className } = props; + return ( +
{ + const boundingRect = ref.current.getBoundingClientRect(); + + openMenu({ + pageX: boundingRect.right - listWidth + listBorder * 2, + pageY: boundingRect.top + boundingRect.height, + } as any); + }} + > + +
+ ); +}; + +export default HamburgerMenu; diff --git a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss index c6ac32bf..72c5715b 100644 --- a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss +++ b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss @@ -6,3 +6,45 @@ width: 700px; } } + +.integrations-actionsList { + display: flex; + flex-direction: column; + width: 200px; + border-radius: 2px; +} + +.integrations-actionItem { + padding: 8px; + display: flex; + align-items: center; + flex-direction: row; + flex-shrink: 0; + white-space: nowrap; + border-left: 2px solid transparent; + cursor: pointer; + min-width: 84px; + display: flex; + gap: 8px; + flex-direction: row; + + &:hover { + background: var(--gray-9); + } +} + +.hamburgerMenu-small { + display: inline-flex; + flex-direction: column; + align-items: center; + vertical-align: middle; + justify-content: center; + background-color: rgba(204, 204, 220, 0.16); + color: var(--secondary-background); + border: 1px solid transparent; + height: 24px; + width: 22px; + padding: 4px; + cursor: pointer; + color: var(--primary-text-color); +} diff --git a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx index cf24abe8..bccbd1c9 100644 --- a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx +++ b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx @@ -14,13 +14,16 @@ import { } from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import HamburgerMenu from 'components/HamburgerMenu/HamburgerMenu'; import IntegrationBlock from 'components/Integrations/IntegrationBlock'; import IntegrationBlockItem from 'components/Integrations/IntegrationBlockItem'; import MonacoEditor from 'components/MonacoEditor/MonacoEditor'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import TooltipBadge from 'components/TooltipBadge/TooltipBadge'; +import { WithContextMenu } from 'components/WithContextMenu/WithContextMenu'; import { ChatOpsConnectors } from 'containers/AlertRules/parts'; import EscalationChainSteps from 'containers/EscalationChainSteps/EscalationChainSteps'; import styles from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss'; @@ -33,6 +36,7 @@ import { EscalationChain } from 'models/escalation_chain/escalation_chain.types' import { MONACO_INPUT_HEIGHT_SMALL, MONACO_OPTIONS } from 'pages/integration_2/Integration2.config'; import IntegrationHelper from 'pages/integration_2/Integration2.helper'; import { useStore } from 'state/useStore'; +import { openNotification } from 'utils'; import { UserActions } from 'utils/authorization'; const cx = cn.bind(styles); @@ -341,11 +345,38 @@ export const RouteButtonsDisplay: React.FC = ({ )} {!channelFilter.is_default && ( - - -