Skip to content

Commit

Permalink
v1.3.60
Browse files Browse the repository at this point in the history
  • Loading branch information
joeyorlando authored Nov 20, 2023
2 parents 578ef27 + d2c9e27 commit 8f169eb
Show file tree
Hide file tree
Showing 48 changed files with 681 additions and 608 deletions.
1 change: 1 addition & 0 deletions .github/workflows/daily-e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
with:
grafana-image-tag: ${{ matrix.grafana-image-tag }}
run-expensive-tests: true
browsers: "chromium firefox webkit"
secrets: inherit

post-status-to-slack:
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ name: e2e tests
grafana-image-tag:
required: true
type: string
browsers:
required: true
type: string
run-expensive-tests:
description: >
Whether or not to run Playwright tests that're annotated as "@expensive"
Expand Down Expand Up @@ -126,14 +129,14 @@ jobs:
- name: Install Playwright Browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: grafana-plugin
run: ./node_modules/.bin/playwright install --with-deps chromium firefox webkit
run: ./node_modules/.bin/playwright install --with-deps ${{ inputs.browsers }}

# use the cached browsers, but we still need to install the necessary system dependencies
# (system deps are installed in the cache-miss step above by the --with-deps flag)
- name: Install Playwright System Dependencies
if: steps.playwright-cache.outputs.cache-hit == 'true'
working-directory: grafana-plugin
run: ./node_modules/.bin/playwright install-deps chromium firefox webkit
run: ./node_modules/.bin/playwright install-deps ${{ inputs.browsers }}

# we could instead use the --wait flag for the helm install command above
# but there's no reason to block on that step
Expand Down Expand Up @@ -163,6 +166,7 @@ jobs:
GRAFANA_VIEWER_USERNAME: viewer
GRAFANA_VIEWER_PASSWORD: viewer
MAILSLURP_API_KEY: ${{ secrets.MAILSLURP_API_KEY }}
BROWSERS: ${{ inputs.browsers }}
working-directory: ./grafana-plugin
run: yarn test:e2e

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/linting-and-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -318,4 +318,5 @@ jobs:
# https://raintank-corp.slack.com/archives/C01C4K8DETW/p1692279329797149
grafana-image-tag: 10.0.2
run-expensive-tests: false
browsers: "chromium"
secrets: inherit
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

## v1.3.60 (2023-11-20)

### Fixed

- Fixes forwarding of Amazon SNS headers @mderynck ([#3371](https://github.com/grafana/oncall/pull/3371))
- Fixes issue when using the `/escalate` Slack command and selecting a team by @joeyorlando ([#3381](https://github.com/grafana/oncall/pull/3381))
- Fix issue when RBAC is enabled where Viewers with "Notifications Receiver" role do not properly show up in schedule
rotations by @joeyorlando ([#3378](https://github.com/grafana/oncall/pull/3378))

## v1.3.59 (2023-11-16)

### Added
Expand Down
2 changes: 1 addition & 1 deletion engine/apps/schedules/ical_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def users_in_ical(
organization : apps.user_management.models.organization.Organization
The organization in question
"""
required_permission = RBACPermission.Permissions.SCHEDULES_WRITE
required_permission = RBACPermission.Permissions.NOTIFICATIONS_READ

emails_from_ical = [username.lower() for username in usernames_from_ical]

Expand Down
54 changes: 48 additions & 6 deletions engine/apps/schedules/tests/test_ical_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from django.core.cache import cache
from django.utils import timezone

from apps.api.permissions import LegacyAccessControlRole
from apps.api.permissions import LegacyAccessControlRole, RBACPermission
from apps.schedules.ical_utils import (
get_cached_oncall_users_for_multiple_schedules,
get_icalendar_tz_or_utc,
Expand Down Expand Up @@ -93,17 +93,59 @@ def test_users_in_ical_email_case_insensitive(make_organization_and_user, make_u


@pytest.mark.django_db
def test_users_in_ical_viewers_inclusion(make_organization_and_user, make_user_for_organization):
@pytest.mark.parametrize(
"role,included",
[
(LegacyAccessControlRole.ADMIN, True),
(LegacyAccessControlRole.EDITOR, True),
(LegacyAccessControlRole.VIEWER, False),
(LegacyAccessControlRole.NONE, False),
],
)
def test_users_in_ical_basic_role(make_organization_and_user, make_user_for_organization, role, included):
organization, user = make_organization_and_user()
viewer = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER)
organization.is_rbac_permissions_enabled = False
organization.save()

other_user = make_user_for_organization(organization, role=role)

usernames = [user.username, other_user.username]
expected_result = {user}
if included:
expected_result.add(other_user)

usernames = [user.username, viewer.username]
result = users_in_ical(usernames, organization)
assert set(result) == {user}
assert set(result) == expected_result


@pytest.mark.django_db
@pytest.mark.parametrize(
"permission,included",
[
(RBACPermission.Permissions.NOTIFICATIONS_READ, True),
(RBACPermission.Permissions.SCHEDULES_READ, False),
(None, False),
],
)
def test_users_in_ical_rbac(make_organization_and_user, make_user_for_organization, permission, included):
organization, _ = make_organization_and_user()
organization.is_rbac_permissions_enabled = True
organization.save()

viewer = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER)
usernames = [viewer.username]

# viewer doesn't yet have the required permission, they shouldn't be included
assert len(users_in_ical(usernames, organization)) == 0

viewer.permissions = [{"action": permission.value}] if permission else []
viewer.save()

assert users_in_ical(usernames, organization) == ([viewer] if included else [])


@pytest.mark.django_db
def test_list_users_to_notify_from_ical_viewers_inclusion(
def test_list_users_to_notify_from_ical_viewers_exclusion(
make_organization_and_user, make_user_for_organization, make_schedule, make_on_call_shift
):
organization, user = make_organization_and_user()
Expand Down
5 changes: 2 additions & 3 deletions engine/apps/slack/scenarios/paging.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,9 +570,8 @@ def _get_team_select_blocks(
initial_option_idx = 0
for idx, team in enumerate(teams):
team_pk, team_name = team
team_pk_str = str(team_pk)

if value == team_pk_str:
if value and value.pk == team_pk:
initial_option_idx = idx
team_options.append(
{
Expand All @@ -581,7 +580,7 @@ def _get_team_select_blocks(
"text": team_name,
"emoji": True,
},
"value": team_pk_str,
"value": str(team_pk),
}
)

Expand Down
6 changes: 4 additions & 2 deletions engine/apps/slack/scenarios/resolution_note.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,12 @@ def post_or_update_resolution_note_in_thread(self, resolution_note: "ResolutionN
blocks = self.get_resolution_note_blocks(resolution_note)

if resolution_note_slack_message is None:
resolution_note_text = Truncator(resolution_note.text)
try:
result = self._slack_client.chat_postMessage(
channel=alert_group_slack_message.channel_id,
thread_ts=alert_group_slack_message.slack_id,
text=resolution_note.text,
text=resolution_note_text.chars(BLOCK_SECTION_TEXT_MAX_SIZE),
blocks=blocks,
)
except RESOLUTION_NOTE_EXCEPTIONS:
Expand Down Expand Up @@ -256,11 +257,12 @@ def post_or_update_resolution_note_in_thread(self, resolution_note: "ResolutionN
resolution_note.resolution_note_slack_message = resolution_note_slack_message
resolution_note.save(update_fields=["resolution_note_slack_message"])
elif resolution_note_slack_message.posted_by_bot:
resolution_note_text = Truncator(resolution_note_slack_message.text)
try:
self._slack_client.chat_update(
channel=alert_group_slack_message.channel_id,
ts=resolution_note_slack_message.ts,
text=resolution_note_slack_message.text,
text=resolution_note_text.chars(BLOCK_SECTION_TEXT_MAX_SIZE),
blocks=blocks,
)
except RESOLUTION_NOTE_EXCEPTIONS:
Expand Down
36 changes: 26 additions & 10 deletions engine/apps/slack/tests/test_scenario_steps/test_paging.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,24 +318,40 @@ def test_get_team_select_blocks(

# team selected
organization, _, _, slack_user_identity = make_organization_and_user_with_slack_identities()
team = make_team(organization)
arc = make_alert_receive_channel(organization, team=team, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING)
escalation_chain = make_escalation_chain(organization)
make_channel_filter(arc, is_default=True, escalation_chain=escalation_chain)
team1 = make_team(organization)
team2 = make_team(organization)

def _setup_direct_paging_integration(team):
arc = make_alert_receive_channel(
organization, team=team, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING
)
escalation_chain = make_escalation_chain(organization)
make_channel_filter(arc, is_default=True, escalation_chain=escalation_chain)
return arc

_setup_direct_paging_integration(team1)
team2_direct_paging_arc = _setup_direct_paging_integration(team2)

blocks = _get_team_select_blocks(slack_user_identity, organization, True, team.pk, input_id_prefix)
blocks = _get_team_select_blocks(slack_user_identity, organization, True, team2, input_id_prefix)

assert len(blocks) == 2
input_block, context_block = blocks

team_option = {"text": {"emoji": True, "text": team.name, "type": "plain_text"}, "value": str(team.pk)}
def _contstruct_team_option(team):
return {"text": {"emoji": True, "text": team.name, "type": "plain_text"}, "value": str(team.pk)}

team1_option = _contstruct_team_option(team1)
team2_option = _contstruct_team_option(team2)

def _sort_team_options(options):
return sorted(options, key=lambda o: o["value"])

assert input_block["type"] == "input"
assert len(input_block["element"]["options"]) == 1
assert input_block["element"]["options"] == [team_option]
assert input_block["element"]["initial_option"] == team_option
assert len(input_block["element"]["options"]) == 2
assert _sort_team_options(input_block["element"]["options"]) == _sort_team_options([team1_option, team2_option])
assert input_block["element"]["initial_option"] == team2_option

assert (
context_block["elements"][0]["text"]
== f"Integration <{arc.web_link}|{arc.verbal_name}> will be used for notification."
== f"Integration <{team2_direct_paging_arc.web_link}|{team2_direct_paging_arc.verbal_name}> will be used for notification."
)
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,78 @@ def test_get_resolution_note_blocks_truncate_text(
assert blocks == expected_blocks


@pytest.mark.django_db
def test_post_or_update_resolution_note_in_thread_truncate_message_text(
make_organization_and_user_with_slack_identities,
make_alert_receive_channel,
make_alert_group,
make_slack_message,
make_resolution_note,
):
UpdateResolutionNoteStep = ScenarioStep.get_step("resolution_note", "UpdateResolutionNoteStep")
organization, user, slack_team_identity, _ = make_organization_and_user_with_slack_identities()
step = UpdateResolutionNoteStep(slack_team_identity)

alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
make_slack_message(alert_group=alert_group, channel_id="RANDOM_CHANNEL_ID", slack_id="RANDOM_MESSAGE_ID")
resolution_note = make_resolution_note(alert_group=alert_group, author=user, message_text="a" * 3000)

with patch("apps.slack.client.SlackClient.api_call") as mock_slack_api_call:
mock_slack_api_call.return_value = {
"ts": "timestamp",
"message": {"ts": "timestamp"},
"permalink": "https://link.to.message",
}
step.post_or_update_resolution_note_in_thread(resolution_note)

assert mock_slack_api_call.called
post_message_call = mock_slack_api_call.mock_calls[0]
assert post_message_call.args[0] == "chat.postMessage"
assert post_message_call.kwargs["json"]["text"] == resolution_note.text[: BLOCK_SECTION_TEXT_MAX_SIZE - 1] + "…"


@pytest.mark.django_db
def test_post_or_update_resolution_note_in_thread_update_truncate_message_text(
make_organization_and_user_with_slack_identities,
make_alert_receive_channel,
make_alert_group,
make_slack_message,
make_resolution_note,
make_resolution_note_slack_message,
):
UpdateResolutionNoteStep = ScenarioStep.get_step("resolution_note", "UpdateResolutionNoteStep")
organization, user, slack_team_identity, _ = make_organization_and_user_with_slack_identities()
step = UpdateResolutionNoteStep(slack_team_identity)

alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
make_slack_message(alert_group=alert_group, channel_id="RANDOM_CHANNEL_ID", slack_id="RANDOM_MESSAGE_ID")
resolution_note = make_resolution_note(alert_group=alert_group, author=user, message_text="a" * 3000)
make_resolution_note_slack_message(
alert_group=alert_group,
resolution_note=resolution_note,
user=user,
posted_by_bot=True,
added_by_user=user,
ts=1,
text=resolution_note.text,
)

with patch("apps.slack.client.SlackClient.api_call") as mock_slack_api_call:
mock_slack_api_call.return_value = {
"ts": "timestamp",
"message": {"ts": "timestamp"},
"permalink": "https://link.to.message",
}
step.post_or_update_resolution_note_in_thread(resolution_note)

assert mock_slack_api_call.called
post_message_call = mock_slack_api_call.mock_calls[0]
assert post_message_call.args[0] == "chat.update"
assert post_message_call.kwargs["json"]["text"] == resolution_note.text[: BLOCK_SECTION_TEXT_MAX_SIZE - 1] + "…"


@pytest.mark.django_db
def test_get_resolution_notes_blocks_latest_limit(
make_organization_and_user_with_slack_identities,
Expand Down
12 changes: 9 additions & 3 deletions engine/apps/user_management/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@

logger = logging.getLogger(__name__)

AMAZON_SNS_HEADERS = [
"x-amz-sns-subscription-arn",
"x-amz-sns-topic-arn",
"x-amz-sns-message-id",
"x-amz-sns-message-type",
]


class OrganizationMovedMiddleware(MiddlewareMixin):
def process_exception(self, request, exception):
Expand All @@ -33,9 +40,8 @@ def process_exception(self, request, exception):
headers["Authorization"] = v

if "amazon_sns" in request.path:
for k, v in request.META.items():
if k.startswith("x-amz-sns-"):
headers[k] = v
for k in AMAZON_SNS_HEADERS:
headers[k] = request.headers.get(k)

response = self.make_request(request.method, url, headers, request.body)
return HttpResponse(response.content, status=response.status_code)
Expand Down
14 changes: 9 additions & 5 deletions engine/apps/user_management/tests/test_region.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from apps.integrations.views import AlertManagerAPIView, AmazonSNS
from apps.schedules.models import OnCallScheduleWeb
from apps.user_management.exceptions import OrganizationMovedException
from apps.user_management.middlewares import AMAZON_SNS_HEADERS


@pytest.mark.django_db
Expand Down Expand Up @@ -254,10 +255,10 @@ def test_organization_moved_middleware_amazon_sns_headers(
)

expected_sns_headers = {
"x-amz-sns-subscription-arn": "arn:aws:sns:xxxxxxxxxx:467989492352:oncall-test:3aab6edb-0c5e-4fa9-b876-64409d1f6c63",
"x-amz-sns-topic-arn": "arn:aws:sns:xxxxxxxxxx:467989492352:oncall-test",
"x-amz-sns-message-id": "473efe1d-8ea4-5252-8124-a3d5ff7408c5",
"x-amz-sns-message-type": "Notification",
"HTTP_X_AMZ_SNS_SUBSCRIPTION_ARN": "arn:aws:sns:xxxxxxxxxx:467989492352:oncall-test:3aab6edb-0c5e-4fa9-b876-64409d1f6c63",
"HTTP_X_AMZ_SNS_TOPIC_ARN": "arn:aws:sns:xxxxxxxxxx:467989492352:oncall-test",
"HTTP_X_AMZ_SNS_MESSAGE_ID": "473efe1d-8ea4-5252-8124-a3d5ff7408c5",
"HTTP_X_AMZ_SNS_MESSAGE_TYPE": "Notification",
}
expected_message = bytes(f"Redirected to {region.oncall_backend_url}", "utf-8")
mocked_make_request.return_value = HttpResponse(expected_message, status=status.HTTP_200_OK)
Expand All @@ -268,6 +269,9 @@ def test_organization_moved_middleware_amazon_sns_headers(
data = {"value": "test"}
response = client.post(url, data, format="json", **expected_sns_headers)
assert mocked_make_request.called
assert expected_sns_headers.items() <= mocked_make_request.call_args.args[2].items()
for k in AMAZON_SNS_HEADERS:
assert expected_sns_headers.get(f'HTTP_{k.upper().replace("-","_")}') == mocked_make_request.call_args.args[
2
].get(k)
assert response.content == expected_message
assert response.status_code == status.HTTP_200_OK
Loading

0 comments on commit 8f169eb

Please sign in to comment.