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

feat: add support for mattermost chatops #5321

Open
wants to merge 43 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2c1e7b0
Add mattermost OAuth2 flow
ravishankar15 Aug 6, 2024
b37626c
Correcting the comments
ravishankar15 Aug 13, 2024
85d1b8f
Fix the config checks API
ravishankar15 Aug 13, 2024
3fd7d65
Fixing the lint errors
ravishankar15 Aug 13, 2024
be6612d
Add mattermost OAuth2 flow
ravishankar15 Aug 6, 2024
02b5a4b
Mattermost Connect Channels
ravishankar15 Aug 27, 2024
5398f16
Addressed the review comments
ravishankar15 Aug 30, 2024
b2e7d2b
Adding default action and test cases
ravishankar15 Sep 7, 2024
52da9cf
Review comments
ravishankar15 Sep 10, 2024
0e8d8dc
Fix exception handling of mattermost API
ravishankar15 Sep 11, 2024
a71b235
Make the feature flag true by default
ravishankar15 Sep 12, 2024
7c5e7e0
Review comments and changes for latest update on freature branch
ravishankar15 Sep 14, 2024
2e0e152
Add token to base for testing
ravishankar15 Sep 14, 2024
fe20a28
Remove config checks from OrganizationConfigChecksView
ravishankar15 Sep 17, 2024
c2af483
Remove related config check code
ravishankar15 Sep 17, 2024
e079a9f
Remove missed config check
ravishankar15 Sep 17, 2024
de83c3e
Mattermost Channel Integration UI Changes
ravishankar15 Sep 18, 2024
ec5aee2
Use channel id insted of channel name and team name
ravishankar15 Sep 19, 2024
ca51d5b
Review comments
ravishankar15 Sep 24, 2024
63a575a
Update to emotion styling
ravishankar15 Oct 1, 2024
0068a5a
Mattermost User Integration
ravishankar15 Sep 20, 2024
0d83050
Moving to emotion styling review comments
ravishankar15 Oct 1, 2024
628538e
Remove unnecessary unique index
ravishankar15 Oct 7, 2024
04b2659
Mattermost Alert Flow
ravishankar15 Oct 14, 2024
b4f173f
Fix spelling
ravishankar15 Oct 16, 2024
19e5913
Adding tests and review comments
ravishankar15 Oct 31, 2024
933671d
Fix duplication and lint fixes
ravishankar15 Nov 12, 2024
e4996cc
Add config for ci test
ravishankar15 Nov 12, 2024
ee12818
Address Review comments
ravishankar15 Nov 14, 2024
33d4ca9
Fixing Lint and User auth redirect flow
ravishankar15 Nov 19, 2024
a941c31
Mattermost incoming event handler
ravishankar15 Nov 20, 2024
d442453
Review comments and tests
ravishankar15 Nov 21, 2024
24ccefc
User Notification and Escalation Chain flow
ravishankar15 Nov 26, 2024
2f2ff51
Save notification record and review comments
ravishankar15 Nov 30, 2024
c1e72be
Remove print statement
ravishankar15 Dec 3, 2024
c13b69f
Regenerate migrations
matiasb Dec 3, 2024
9108730
Documentation for mattermost integration
ravishankar15 Dec 4, 2024
9b0d68a
Update docs, fix lint
matiasb Dec 4, 2024
d6624e5
Add mattermost alert group integration flow
ravishankar15 Dec 7, 2024
2fbb421
Add chatops display condition and review comments
ravishankar15 Dec 10, 2024
5f0256a
Minor updates and refactorings
matiasb Dec 27, 2024
8f0b9c3
Updates from review
matiasb Jan 3, 2025
50fe306
Update db migration
matiasb Jan 7, 2025
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
8 changes: 8 additions & 0 deletions dev/.env.dev.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,20 @@ TWILIO_VERIFY_SERVICE_SID=
TWILIO_AUTH_TOKEN=
TWILIO_NUMBER=

MATTERMOST_CLIENT_OAUTH_ID=
MATTERMOST_CLIENT_OAUTH_SECRET=
MATTERMOST_HOST=
MATTERMOST_BOT_TOKEN=
MATTERMOST_LOGIN_RETURN_REDIRECT_HOST=http://localhost:8080
MATTERMOST_SIGNING_SECRET=

DJANGO_SETTINGS_MODULE=settings.dev
SECRET_KEY=jyRnfRIeMjYfKdoFa9dKXcNaEGGc8GH1TChmYoWW
BASE_URL=http://localhost:8080

FEATURE_TELEGRAM_INTEGRATION_ENABLED=True
FEATURE_SLACK_INTEGRATION_ENABLED=True
FEATURE_MATTERMOST_INTEGRATION_ENABLED=True

SLACK_INSTALL_RETURN_REDIRECT_HOST=http://localhost:8080
SOCIAL_AUTH_REDIRECT_IS_HTTPS=False
Expand Down
27 changes: 22 additions & 5 deletions docs/sources/manage/notify/mattermost/index.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Mattermost
menuTitle: Mattermost
description: Explains that a Mattermost integration is not implemented yet.
description: How to connect Mattermost for alert group notifications.
weight: 900
keywords:
- OnCall
Expand All @@ -15,9 +15,26 @@ aliases:
- ../../chat-options/configure-mattermost
---

# Mattermost
# Mattermost integration for Grafana OnCall

Mattermost support is not implemented yet.
The Mattermost integration for Grafana OnCall allows connecting a Mattermost channel directly
into your incident response workflow to help your team focus on alert resolution with less friction.

Please join [GitHub Issue](https://github.com/grafana/oncall/issues/96) or
check [PR](https://github.com/grafana/oncall/pull/606).
At the moment, this integration is only available for OSS installations.

## Before you begin

To install the Mattermost integration, you must have Admin Permissions in your Grafana setup
as well as in the Mattermost instance that you'd like to integrate with.

Follow the steps in our [documentation](https://grafana.com/docs/oncall/latest/open-source/#mattermost-setup).

## Connect to a Mattermost Channel

1. Go to the Mattermost channel you want to connect to, check its information and copy the channel id.
2. In Grafana OnCall, in the Settings section, click on the **ChatOps** tab and select Mattermost in the side menu.
3. Click the **Add Mattermost channel** button, paste the channel id from step (1) and click **Create**.
4. Set a default channel for the alerts.

(Note: Make sure the bot in your setup is member of the team the channel belongs to and
has `read_channel` privileges [Ref](https://api.mattermost.com/#tag/channels/operation/GetChannelByNameForTeamName))
36 changes: 33 additions & 3 deletions docs/sources/set-up/open-source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,40 @@ Refer to the following steps to configure the Telegram integration:
Alternatively, in case you want to connect Telegram channels to your Grafana OnCall environment, navigate
to the **ChatOps** tab.

## Grafana OSS-Cloud Setup
## Mattermost Setup

The benefits of connecting to Grafana Cloud OnCall include:
The Mattermost integration of the Grafana OnCall is designed for collobarative team work and improved incident response.

Refer to the following steps to configure the Mattermost integration:

1. Ensure your Grafana OnCall environment is up and running.
2. Set `FEATURE_MATTERMOST_INTEGRATION_ENABLED` as "True".
3. Create a Mattermost bot account [Ref](https://developers.mattermost.com/integrate/reference/bot-accounts/#bot-account-creation)
and save the token generated (NOTE: you may need to give the bot admin permissions
to make it possible for it to update alert group notifications).
4. Add the bot to the Mattermost team(s) owning the channels you want to connect to
(via Mattermost System console -> User Management -> Teams).
5. Set the token generated in the `MATTERMOST_BOT_TOKEN` variable on the **ENV Variables** page
of your Grafana OnCall instance.
6. [Create OAuth 2.0 Application In Mattermost](https://developers.mattermost.com/integrate/apps/authentication/oauth2/#register-an-oauth-20-application).
The callback url for the OAuth application should be,

```text
https://<ONCALL_ENGINE_PUBLIC_URL>/api/internal/v1/complete/mattermost-login/
```

This will allow users to connect their OnCall accounts with Mattermost. The OAuth credentials will be needed later.
7. Generate a JWT secret for authenticating the incoming event messages from Mattermost
and set it as the `MATTERMOST_SIGNING_SECRET` variable on the **ENV Variables** page of your Grafana OnCall instance.
8. Set the following environment variables too:

```text
MATTERMOST_CLIENT_OAUTH_ID = << Integrations -> OAuth 2.0 Applications -> Client ID >>
MATTERMOST_CLIENT_OAUTH_SECRET = << Integrations -> OAuth 2.0 Applications -> Client Secret >>
MATTERMOST_HOST = << Mattermost server URL >>
MATTERMOST_LOGIN_RETURN_REDIRECT_HOST = << OnCall external URL >>
```

- Grafana Cloud OnCall could monitor OSS OnCall uptime using heartbeat
- SMS for user notifications
- Phone calls for user notifications.

Expand Down Expand Up @@ -354,3 +383,4 @@ To configure this feature as such:

Additionally, if you prefer to disable this feature, you can set the `ESCALATION_AUDITOR_ENABLED` environment variable
to `False`.
e`.
1 change: 1 addition & 0 deletions engine/apps/alerts/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class ActionSource(IntegerChoices):
TELEGRAM = 3, "Telegram"
API = 4, "API"
BACKSYNC = 5, "Backsync"
MATTERMOST = 6, "Mattermost"


TASK_DELAY_SECONDS = 1
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2024-12-03 13:13

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('alerts', '0073_update_direct_paging_integration_non_default_routes'),
]

operations = [
migrations.AlterField(
model_name='alertgrouplogrecord',
name='action_source',
field=models.SmallIntegerField(default=None, null=True, verbose_name=[(0, 'Slack'), (1, 'Web'), (2, 'Phone'), (3, 'Telegram'), (4, 'API'), (5, 'Backsync'), (6, 'Mattermost')]),
),
]
4 changes: 4 additions & 0 deletions engine/apps/api/serializers/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,13 @@ def get_env_status(self, obj):

telegram_configured = not LiveSetting.objects.filter(name__startswith="TELEGRAM", error__isnull=False).exists()
phone_provider_config = get_phone_provider().flags
mattermost_configured = not LiveSetting.objects.filter(
name__startswith="MATTERMOST", error__isnull=False
).exists()
return {
"telegram_configured": telegram_configured,
"phone_provider": asdict(phone_provider_config),
"mattermost_configured": mattermost_configured,
}


Expand Down
83 changes: 81 additions & 2 deletions engine/apps/api/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from rest_framework import status
from rest_framework.test import APIClient

from apps.auth_token.constants import SLACK_AUTH_TOKEN_NAME
from apps.social_auth.backends import SLACK_INSTALLATION_BACKEND
from apps.auth_token.constants import MATTERMOST_AUTH_TOKEN_NAME, SLACK_AUTH_TOKEN_NAME
from apps.social_auth.backends import MATTERMOST_LOGIN_BACKEND, SLACK_INSTALLATION_BACKEND
from common.constants.plugin_ids import PluginID
from common.constants.slack_auth import SLACK_OAUTH_ACCESS_RESPONSE

Expand Down Expand Up @@ -99,6 +99,85 @@ def test_start_slack_ok(
assert response.json() == "https://slack_oauth_redirect.com"


@pytest.mark.django_db
@pytest.mark.parametrize(
"backend_name,expected_url",
((MATTERMOST_LOGIN_BACKEND, "a/grafana-oncall-app/users/me"),),
)
def test_complete_mattermost_auth_redirect_ok(
make_organization,
make_user_for_organization,
make_mattermost_token_for_user,
backend_name,
expected_url,
):
organization = make_organization()
admin = make_user_for_organization(organization)
_, mattermost_token = make_mattermost_token_for_user(admin)

client = APIClient()
url = (
reverse("api-internal:complete-social-auth", kwargs={"backend": backend_name})
+ f"?{MATTERMOST_AUTH_TOKEN_NAME}={mattermost_token}"
)

with patch("apps.api.views.auth.do_complete") as mock_do_complete:
mock_do_complete.return_value = None
response = client.get(url)

assert response.status_code == status.HTTP_302_FOUND
assert response.url == expected_url


@pytest.mark.django_db
def test_complete_mattermost_auth_redirect_error(
make_organization,
make_user_for_organization,
make_mattermost_token_for_user,
):
organization = make_organization()
admin = make_user_for_organization(organization)
_, mattermost_token = make_mattermost_token_for_user(admin)

client = APIClient()
url = (
reverse("api-internal:complete-social-auth", kwargs={"backend": MATTERMOST_LOGIN_BACKEND})
+ f"?{MATTERMOST_AUTH_TOKEN_NAME}={mattermost_token}"
)

def _custom_do_complete(backend, *args, **kwargs):
backend.strategy.session[REDIRECT_FIELD_NAME] = "some-url"
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)

with patch("apps.api.views.auth.do_complete", side_effect=_custom_do_complete):
response = client.get(url)

assert response.status_code == status.HTTP_302_FOUND
assert response.url == "some-url"


@pytest.mark.django_db
def test_start_mattermost_ok(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
):
"""
Covers the case when user starts Mattermost integration installation via Grafana OnCall
"""
_, user, token = make_organization_and_user_with_plugin_token()

client = APIClient()
url = reverse("api-internal:social-auth", kwargs={"backend": MATTERMOST_LOGIN_BACKEND})

mock_do_auth_return = Mock()
mock_do_auth_return.url = "https://mattermost_oauth_redirect.com"
with patch("apps.api.views.auth.do_auth", return_value=mock_do_auth_return) as mock_do_auth:
response = client.get(url, **make_user_auth_headers(user, token))
assert mock_do_auth.called
assert response.status_code == status.HTTP_200_OK
assert response.json() == "https://mattermost_oauth_redirect.com"


@override_settings(UNIFIED_SLACK_APP_ENABLED=True)
@pytest.mark.django_db
def test_start_unified_slack_ok(
Expand Down
1 change: 1 addition & 0 deletions engine/apps/api/tests/test_organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"verification_call": False,
"verification_sms": False,
},
"mattermost_configured": False,
}


Expand Down
13 changes: 9 additions & 4 deletions engine/apps/api/views/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@
from social_django.utils import psa
from social_django.views import _do_login

from apps.auth_token.auth import GoogleTokenAuthentication, PluginAuthentication, SlackTokenAuthentication
from apps.auth_token.auth import (
GoogleTokenAuthentication,
MattermostTokenAuthentication,
PluginAuthentication,
SlackTokenAuthentication,
)
from apps.chatops_proxy.utils import (
get_installation_link_from_chatops_proxy,
get_slack_oauth_response_from_chatops_proxy,
)
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
from apps.slack.installation import install_slack_integration
from apps.social_auth.backends import SLACK_INSTALLATION_BACKEND, LoginSlackOAuth2V2
from apps.social_auth.backends import SLACK_INSTALLATION_BACKEND, LoginMattermostOAuth2, LoginSlackOAuth2V2

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -67,7 +72,7 @@ def overridden_login_social_auth(request: Request, backend: str) -> Response:


@api_view(["GET"])
@authentication_classes([GoogleTokenAuthentication, SlackTokenAuthentication])
@authentication_classes([GoogleTokenAuthentication, SlackTokenAuthentication, MattermostTokenAuthentication])
@never_cache
@csrf_exempt
@psa("social:complete")
Expand Down Expand Up @@ -98,7 +103,7 @@ def overridden_complete_social_auth(request: Request, backend: str, *args, **kwa
# otherwise it pertains to the InstallSlackOAuth2V2 backend, and we should redirect to the chat-ops page
return_to = (
url_builder.user_profile()
if isinstance(request.backend, (LoginSlackOAuth2V2, GoogleOAuth2))
if isinstance(request.backend, (LoginMattermostOAuth2, LoginSlackOAuth2V2, GoogleOAuth2))
else url_builder.chatops()
)

Expand Down
4 changes: 4 additions & 0 deletions engine/apps/api/views/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Feature(enum.StrEnum):
GRAFANA_ALERTING_V2 = "grafana_alerting_v2"
LABELS = "labels"
GOOGLE_OAUTH2 = "google_oauth2"
MATTERMOST = "mattermost"


class FeaturesAPIView(APIView):
Expand Down Expand Up @@ -72,4 +73,7 @@ def _get_enabled_features(self, request):
if settings.GOOGLE_OAUTH2_ENABLED:
enabled_features.append(Feature.GOOGLE_OAUTH2)

if settings.FEATURE_MATTERMOST_INTEGRATION_ENABLED:
enabled_features.append(Feature.MATTERMOST)

return enabled_features
13 changes: 12 additions & 1 deletion engine/apps/auth_token/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@
from common.utils import validate_url
from settings.base import SELF_HOSTED_SETTINGS

from .constants import GOOGLE_OAUTH2_AUTH_TOKEN_NAME, SCHEDULE_EXPORT_TOKEN_NAME, SLACK_AUTH_TOKEN_NAME
from .constants import (
GOOGLE_OAUTH2_AUTH_TOKEN_NAME,
MATTERMOST_AUTH_TOKEN_NAME,
SCHEDULE_EXPORT_TOKEN_NAME,
SLACK_AUTH_TOKEN_NAME,
)
from .exceptions import InvalidToken
from .models import (
ApiAuthToken,
GoogleOAuth2Token,
IntegrationBacksyncAuthToken,
MattermostAuthToken,
PluginAuthToken,
ScheduleExportAuthToken,
ServiceAccountToken,
Expand Down Expand Up @@ -272,6 +278,11 @@ class SlackTokenAuthentication(_SocialAuthTokenAuthentication[SlackAuthToken]):
model = SlackAuthToken


class MattermostTokenAuthentication(_SocialAuthTokenAuthentication[MattermostAuthToken]):
token_query_param_name = MATTERMOST_AUTH_TOKEN_NAME
model = MattermostAuthToken


class GoogleTokenAuthentication(_SocialAuthTokenAuthentication[GoogleOAuth2Token]):
token_query_param_name = GOOGLE_OAUTH2_AUTH_TOKEN_NAME
model = GoogleOAuth2Token
Expand Down
1 change: 1 addition & 0 deletions engine/apps/auth_token/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
MAX_PUBLIC_API_TOKENS_PER_USER = 5

SLACK_AUTH_TOKEN_NAME = "slack_login_token"
MATTERMOST_AUTH_TOKEN_NAME = "state"
GOOGLE_OAUTH2_AUTH_TOKEN_NAME = "state"
"""
We must use the `state` query param, otherwise Google returns a 400 error.
Expand Down
32 changes: 32 additions & 0 deletions engine/apps/auth_token/migrations/0008_mattermostauthtoken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.2.15 on 2024-12-03 13:13

import apps.auth_token.models.mattermost_auth_token
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('user_management', '0029_remove_organization_general_log_channel_id_db'),
('auth_token', '0007_serviceaccounttoken'),
]

operations = [
migrations.CreateModel(
name='MattermostAuthToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token_key', models.CharField(db_index=True, max_length=8)),
('digest', models.CharField(max_length=128)),
('created_at', models.DateTimeField(auto_now_add=True)),
('revoked_at', models.DateTimeField(null=True)),
('expire_date', models.DateTimeField(default=apps.auth_token.models.mattermost_auth_token.get_expire_date)),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mattermost_auth_token_set', to='user_management.organization')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='mattermost_auth_token', to='user_management.user')),
],
options={
'abstract': False,
},
),
]
1 change: 1 addition & 0 deletions engine/apps/auth_token/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .base_auth_token import BaseAuthToken # noqa: F401
from .google_oauth2_token import GoogleOAuth2Token # noqa: F401
from .integration_backsync_auth_token import IntegrationBacksyncAuthToken # noqa: F401
from .mattermost_auth_token import MattermostAuthToken # noqa: F401
from .plugin_auth_token import PluginAuthToken # noqa: F401
from .schedule_export_auth_token import ScheduleExportAuthToken # noqa: F401
from .service_account_token import ServiceAccountToken # noqa: F401
Expand Down
Loading
Loading