Skip to content
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

Dev to main #3201

Merged
merged 7 commits into from
Oct 25, 2023
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## v1.3.47 (2023-10-25)

### Fixed

- Add filtering term length check for channel filter endpoints @Ferril ([#3192](https://github.com/grafana/oncall/pull/3192))

## v1.3.46 (2023-10-23)

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/make-docs
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ PATHS_mimir='docs/sources/mimir'
PATHS_plugins_grafana_jira_datasource='docs/sources'
PATHS_plugins_grafana_mongodb_datasource='docs/sources'
PATHS_plugins_grafana_splunk_datasource='docs/sources'
Paths_tempo='docs/sources/tempo'
PATHS_tempo='docs/sources/tempo'
PATHS_website='content'

# identifier STR
Expand Down
15 changes: 8 additions & 7 deletions docs/sources/open-source/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,21 +213,22 @@ Grafana OnCall supports Twilio SMS and phone call notifications delivery. If you
notifications using Twilio, complete the following steps:

1. Set `GRAFANA_CLOUD_NOTIFICATIONS_ENABLED` as **False** to ensure the Grafana OSS <-> Cloud connector is disabled.
1. From your **OnCall** environment, select **Env Variables** and configure all variables starting with `TWILIO_`.
2. From your **OnCall** environment, select **Env Variables** and configure all variables starting with `TWILIO_`.

### Zvonok.com

Grafana OnCall supports Zvonok.com phone call notifications delivery. To configure phone call notifications using
Zvonok.com, complete the following steps:

1. Change `PHONE_PROVIDER` value to `zvonok`.
2. Create a public API key on the Profile->Settings page, and assign its value to `ZVONOK_API_KEY`.
3. Create campaign and assign its ID value to `ZVONOK_CAMPAIGN_ID`.
4. If you are planning to use pre-recorded audio instead of a speech synthesizer, you can copy the ID of the audio clip
1. Set `GRAFANA_CLOUD_NOTIFICATIONS_ENABLED` as **False** to ensure the Grafana OSS <-> Cloud connector is disabled.
2. Change `PHONE_PROVIDER` value to `zvonok`.
3. Create a public API key on the Profile->Settings page, and assign its value to `ZVONOK_API_KEY`.
4. Create campaign and assign its ID value to `ZVONOK_CAMPAIGN_ID`.
5. If you are planning to use pre-recorded audio instead of a speech synthesizer, you can copy the ID of the audio clip
to the variable `ZVONOK_AUDIO_ID` (optional step).
5. To make a call with a specific voice, you can set the `ZVONOK_SPEAKER_ID`.
6. To make a call with a specific voice, you can set the `ZVONOK_SPEAKER_ID`.
By default, the ID used is `Salli` (optional step).
6. To process the call status, it is required to add a postback with the GET/POST method on the side of the zvonok.com
7. To process the call status, it is required to add a postback with the GET/POST method on the side of the zvonok.com
service with the following format (optional step):
`${ONCALL_BASE_URL}/zvonok/call_status_events?campaign_id={ct_campaign_id}&call_id={ct_call_id}&status={ct_status}&user_choice={ct_user_choice}`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,8 @@ def _alert_group_action_value(self, **kwargs):
data = {
"organization_id": self.alert_group.channel.organization_id,
"alert_group_pk": self.alert_group.pk,
# eventually replace using alert_group_pk with alert_group_public_pk in slack payload
"alert_group_ppk": self.alert_group.public_primary_key,
**kwargs,
}
return json.dumps(data) # Slack block elements allow to pass value as string only (max 2000 chars)
4 changes: 3 additions & 1 deletion engine/apps/alerts/models/channel_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ class ChannelFilter(OrderedModel):
notification_backends = models.JSONField(null=True, default=None)

created_at = models.DateTimeField(auto_now_add=True)
filtering_term = models.CharField(max_length=1024, null=True, default=None)

FILTERING_TERM_MAX_LENGTH = 1024
filtering_term = models.CharField(max_length=FILTERING_TERM_MAX_LENGTH, null=True, default=None)

FILTERING_TERM_TYPE_REGEX = 0
FILTERING_TERM_TYPE_JINJA2 = 1
Expand Down
7 changes: 6 additions & 1 deletion engine/apps/api/serializers/channel_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ class Meta:
def validate(self, data):
filtering_term = data.get("filtering_term")
filtering_term_type = data.get("filtering_term_type")
if filtering_term is not None:
if len(filtering_term) > ChannelFilter.FILTERING_TERM_MAX_LENGTH:
raise serializers.ValidationError(
f"Expression is too long. Maximum length: {ChannelFilter.FILTERING_TERM_MAX_LENGTH} characters, "
f"current length: {len(filtering_term)}"
)
if filtering_term_type == ChannelFilter.FILTERING_TERM_TYPE_JINJA2:
try:
valid_jinja_template_for_serializer_method_field({"route_template": filtering_term})
Expand Down Expand Up @@ -141,7 +147,6 @@ def get_filtering_term_as_jinja2(self, obj):
class ChannelFilterCreateSerializer(ChannelFilterSerializer):
alert_receive_channel = OrganizationFilteredPrimaryKeyRelatedField(queryset=AlertReceiveChannel.objects)
slack_channel = serializers.CharField(allow_null=True, required=False, source="slack_channel_id")
filtering_term = serializers.CharField(required=False, allow_null=True, allow_blank=True)

class Meta:
model = ChannelFilter
Expand Down
38 changes: 38 additions & 0 deletions engine/apps/api/tests/test_channel_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,3 +552,41 @@ def test_channel_filter_convert_from_regex_to_jinja2(
assert jinja2_channel_filter.filtering_term == final_filtering_term
# Check if the same alert is matched to the channel filter (route) new jinja2
assert bool(jinja2_channel_filter.is_satisfying(payload)) is True


@pytest.mark.django_db
def test_channel_filter_long_filtering_term(
make_organization_and_user_with_plugin_token,
make_alert_receive_channel,
make_escalation_chain,
make_channel_filter,
make_user_auth_headers,
):
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(organization)
make_escalation_chain(organization)
make_channel_filter(alert_receive_channel, is_default=True)
client = APIClient()
long_filtering_term = "a" * (ChannelFilter.FILTERING_TERM_MAX_LENGTH + 1)

url = reverse("api-internal:channel_filter-list")
data_for_creation = {
"alert_receive_channel": alert_receive_channel.public_primary_key,
"filtering_term": long_filtering_term,
}

response = client.post(url, data=data_for_creation, format="json", **make_user_auth_headers(user, token))

assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "Expression is too long" in response.json()["non_field_errors"][0]

channel_filter = make_channel_filter(alert_receive_channel, filtering_term="a", is_default=False)
url = reverse("api-internal:channel_filter-detail", kwargs={"pk": channel_filter.public_primary_key})
data_for_update = {
"filtering_term": long_filtering_term,
}

response = client.put(url, data=data_for_update, format="json", **make_user_auth_headers(user, token))

assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "Expression is too long" in response.json()["non_field_errors"][0]
10 changes: 8 additions & 2 deletions engine/apps/public_api/serializers/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,14 @@ def create(self, validated_data):
return super().create(validated_data)

def validate(self, data):
filtering_term = data.get("routing_regex")
filtering_term_type = data.get("routing_type")
filtering_term = data.get("filtering_term")
filtering_term_type = data.get("filtering_term_type")
if filtering_term is not None:
if len(filtering_term) > ChannelFilter.FILTERING_TERM_MAX_LENGTH:
raise serializers.ValidationError(
f"Expression is too long. Maximum length: {ChannelFilter.FILTERING_TERM_MAX_LENGTH} characters, "
f"current length: {len(filtering_term)}"
)
if filtering_term_type == ChannelFilter.FILTERING_TERM_TYPE_JINJA2:
try:
valid_jinja_template_for_serializer_method_field({"route_template": filtering_term})
Expand Down
33 changes: 33 additions & 0 deletions engine/apps/public_api/tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,36 @@ def test_update_route_with_manual_ordering(

response = client.put(url, format="json", HTTP_AUTHORIZATION=token, data=data_to_update)
assert response.status_code == status.HTTP_400_BAD_REQUEST


@pytest.mark.django_db
def test_routes_long_filtering_term(
route_public_api_setup,
make_channel_filter,
):
organization, _, token, alert_receive_channel, escalation_chain, _ = route_public_api_setup
client = APIClient()
long_filtering_term = "a" * (ChannelFilter.FILTERING_TERM_MAX_LENGTH + 1)

url = reverse("api-public:routes-list")
data_for_creation = {
"integration_id": alert_receive_channel.public_primary_key,
"escalation_chain_id": escalation_chain.public_primary_key,
"routing_regex": long_filtering_term,
}

response = client.post(url, data=data_for_creation, format="json", HTTP_AUTHORIZATION=token)

assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "Expression is too long" in response.json()["non_field_errors"][0]

channel_filter = make_channel_filter(alert_receive_channel, filtering_term="a", is_default=False)
url = reverse("api-public:routes-detail", kwargs={"pk": channel_filter.public_primary_key})
data_for_update = {
"routing_regex": long_filtering_term,
}

response = client.put(url, data=data_for_update, format="json", HTTP_AUTHORIZATION=token)

assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "Expression is too long" in response.json()["non_field_errors"][0]
34 changes: 32 additions & 2 deletions engine/apps/slack/scenarios/step_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,27 @@ def _get_alert_group_from_action(self, payload: EventPayload) -> AlertGroup | No
except (TypeError, json.JSONDecodeError):
return None

# New slack messages from OnCall contain alert group primary key
try:
alert_group_ppk = value["alert_group_ppk"]
return AlertGroup.objects.get(public_primary_key=alert_group_ppk)
except (KeyError, TypeError):
pass

try:
alert_group_pk = value["alert_group_pk"]
organization_pk = value["organization_id"]
except (KeyError, TypeError):
return None

return AlertGroup.objects.get(pk=alert_group_pk)
try:
# check organization as well for cases when the organization was migrated from "us" cluster to "eu" and
# slack message has an outdated payload with incorrect alert group id
alert_group = AlertGroup.objects.get(pk=alert_group_pk, channel__organization_id=organization_pk)
except AlertGroup.DoesNotExist:
return None

return alert_group

def _get_alert_group_from_message(self, payload: EventPayload) -> AlertGroup | None:
"""
Expand All @@ -128,12 +143,27 @@ def _get_alert_group_from_message(self, payload: EventPayload) -> AlertGroup | N
except (TypeError, json.JSONDecodeError):
continue

# New slack messages from OnCall contain alert group primary key
try:
alert_group_ppk = value["alert_group_ppk"]
return AlertGroup.objects.get(public_primary_key=alert_group_ppk)
except (KeyError, TypeError):
pass

try:
alert_group_pk = value["alert_group_pk"]
organization_pk = value["organization_id"]
except (KeyError, TypeError):
continue

return AlertGroup.objects.get(pk=alert_group_pk)
try:
# check the organization as well for cases organization was migrated from "us" cluster to "eu" and
# the slack message has an outdated payload with incorrect alert group id
alert_group = AlertGroup.objects.get(pk=alert_group_pk, channel__organization_id=organization_pk)
except AlertGroup.DoesNotExist:
return None

return alert_group
return None

def _get_alert_group_from_slack_message_in_db(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,21 @@ def test_get_alert_group_from_message(
],
"message": {
"ts": "RANDOM_MESSAGE_TS",
"attachments": [{"blocks": [{"elements": [{"value": json.dumps({"alert_group_pk": alert_group.pk})}]}]}],
"attachments": [
{
"blocks": [
{
"elements": [
{
"value": json.dumps(
{"alert_group_pk": alert_group.pk, "organization_id": organization.pk}
)
}
]
}
]
}
],
},
"channel": {"id": "RANDOM_CHANNEL_ID"},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,16 @@ def manage_responders_setup(

@pytest.mark.django_db
def test_initial_state(manage_responders_setup):
organization, user, slack_team_identity, slack_user_identity = manage_responders_setup
payload = {
"trigger_id": TRIGGER_ID,
"actions": [
{
"type": "button",
"value": json.dumps({"organization_id": ORGANIZATION_ID, "alert_group_pk": ALERT_GROUP_ID}),
"value": json.dumps({"organization_id": organization.pk, "alert_group_pk": ALERT_GROUP_ID}),
}
],
}

organization, user, slack_team_identity, slack_user_identity = manage_responders_setup

step = StartManageResponders(slack_team_identity, organization, user)
with patch.object(step._slack_client, "views_open") as mock_slack_api_call:
step.process_scenario(slack_user_identity, slack_team_identity, payload)
Expand Down
Loading
Loading