Skip to content

Commit

Permalink
Mattermost Installation Flow
Browse files Browse the repository at this point in the history
Mattermost install flow

remove utils

Fix jwt error

Add install flow details

Fix formatting

Fix auth token
  • Loading branch information
ravishankar15 committed Jun 14, 2024
1 parent d8e1a1d commit d58d17d
Show file tree
Hide file tree
Showing 13 changed files with 211 additions and 0 deletions.
4 changes: 4 additions & 0 deletions engine/apps/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .views.organization import (
CurrentOrganizationView,
GetChannelVerificationCode,
GetMattermostSetupDetails,
GetTelegramVerificationCode,
OrganizationConfigChecksView,
SetGeneralChannel,
Expand Down Expand Up @@ -90,6 +91,9 @@
GetChannelVerificationCode.as_view(),
name="api-get-channel-verification-code",
),
optional_slash_path(
"mattermost/setup", GetMattermostSetupDetails.as_view(), name="api-get-mattermost-setup-details"
),
optional_slash_path("slack_settings", SlackTeamSettingsAPIView.as_view(), name="slack-settings"),
optional_slash_path(
"slack_settings/acknowledge_remind_options",
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 @@ -25,6 +25,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 @@ -68,4 +69,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
23 changes: 23 additions & 0 deletions engine/apps/api/views/organization.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from contextlib import suppress

from django.conf import settings
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
Expand Down Expand Up @@ -108,6 +109,28 @@ def get(self, request):
return Response(code)


class GetMattermostSetupDetails(APIView):
authentication_classes = (PluginAuthentication,)
permission_classes = (IsAuthenticated, RBACPermission)

rbac_permissions = {
"get": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
}

def get(self, request):
organization = request.auth.organization
user = request.user
from apps.mattermost.models import MattermostAuthToken

with suppress(MattermostAuthToken.DoesNotExist):
existing_auth_token = organization.mattermost_auth_token
existing_auth_token.delete()
_, auth_token = MattermostAuthToken.create_auth_token(user=user, organization=organization)
manifest_link = f"{settings.BASE_URL}/mattermost/manifest?auth_token={auth_token}"

return Response({"manifest_link": manifest_link})


class SetGeneralChannel(APIView):
authentication_classes = (PluginAuthentication,)
permission_classes = (IsAuthenticated, RBACPermission)
Expand Down
Empty file.
34 changes: 34 additions & 0 deletions engine/apps/mattermost/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Tuple

from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication

from apps.auth_token.exceptions import InvalidToken
from apps.user_management.models import Organization

from .models import MattermostAuthToken


class MattermostAuthTokenAuthentication(BaseAuthentication):
model = MattermostAuthToken

def extract_auth_token(self, request) -> str:
return request.query_params.get("auth_token")

def authenticate(self, request) -> Tuple[Organization, MattermostAuthToken]:
auth = self.extract_auth_token(request=request)
organization, auth_token = self.authenticate_credentials(auth)
return organization, auth_token

def authenticate_credentials(self, token_string: str) -> Tuple[Organization, MattermostAuthToken]:
try:
auth_token = self.model.validate_token_string(token_string)
except InvalidToken:
raise exceptions.AuthenticationFailed("Invalid auth token")

return auth_token.organization, auth_token


class MattermostWebhookAuthTokenAuthentication(MattermostAuthTokenAuthentication):
def extract_auth_token(self, request) -> str:
return request.data.get("state", {}).get("auth_token", "")
31 changes: 31 additions & 0 deletions engine/apps/mattermost/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 4.2.11 on 2024-06-13 04:24

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
('user_management', '0022_alter_team_unique_together'),
]

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)),
('organization', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='mattermost_auth_token', to='user_management.organization')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mattermost_auth_token_set', to='user_management.user')),
],
options={
'abstract': False,
},
),
]
Empty file.
1 change: 1 addition & 0 deletions engine/apps/mattermost/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .mattermost_auth_token import MattermostAuthToken # noqa: F401
34 changes: 34 additions & 0 deletions engine/apps/mattermost/models/mattermost_auth_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Tuple

from django.db import models

from apps.auth_token import constants, crypto
from apps.auth_token.models import BaseAuthToken
from apps.user_management.models import Organization, User


class MattermostAuthToken(BaseAuthToken):
objects: models.Manager["MattermostAuthToken"]

user = models.ForeignKey(
"user_management.User",
related_name="mattermost_auth_token_set",
on_delete=models.CASCADE,
)

organization = models.OneToOneField(
"user_management.Organization", on_delete=models.CASCADE, related_name="mattermost_auth_token"
)

@classmethod
def create_auth_token(cls, user: User, organization: Organization) -> Tuple["MattermostAuthToken", str]:
token_string = crypto.generate_token_string()
digest = crypto.hash_token_string(token_string)

instance = cls.objects.create(
token_key=token_string[: constants.TOKEN_KEY_LENGTH],
digest=digest,
user=user,
organization=organization,
)
return instance, token_string
9 changes: 9 additions & 0 deletions engine/apps/mattermost/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.urls import path

from .views import GetMattermostManifest, MattermostBindings, MattermostInstall

urlpatterns = [
path("manifest", GetMattermostManifest.as_view()),
path("install", MattermostInstall.as_view()),
path("bindings", MattermostBindings.as_view()),
]
64 changes: 64 additions & 0 deletions engine/apps/mattermost/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from apps.mattermost.auth import MattermostAuthTokenAuthentication, MattermostWebhookAuthTokenAuthentication

MATTERMOST_CONNECTED_TEXT = (
"Done! <b>{app_id}</b> Successfully Installed and Linked with organization <b>{organization_title} 🎉</b>"
)


class GetMattermostManifest(APIView):
authentication_classes = (MattermostAuthTokenAuthentication,)

def get(self, request):
auth_token = request.query_params.get("auth_token")
manifest = self._build_manifest(auth_token)
return Response(manifest, status=status.HTTP_200_OK)

def _build_on_install_callback(self, auth_token: str) -> dict:
return {
"path": "/mattermost/install",
"expand": {"app": "summary", "acting_user": "summary"},
"state": {"auth_token": auth_token},
}

def _build_bindings_callback(self, auth_token: str) -> dict:
return {"path": "/mattermost/bindings", "state": {"auth_token": auth_token}}

def _build_manifest(self, auth_token: str) -> dict:
return {
"app_id": "Grafana-Oncall",
"version": "1.0.0",
"display_name": "Grafana Oncall",
"description": "Grafana Oncall app for sending and receiving events from mattermost",
"homepage_url": "https://grafana.com/docs/oncall/latest/",
"requested_permissions": ["act_as_bot"],
"requested_locations": ["/in_post", "/post_menu", "/command"],
"on_install": self._build_on_install_callback(auth_token=auth_token),
"bindings": self._build_bindings_callback(auth_token=auth_token),
"http": {"root_url": "http://host.docker.internal:8080"},
}


class MattermostInstall(APIView):
authentication_classes = (MattermostWebhookAuthTokenAuthentication,)

def post(self, request):
app_id = request.data.get("context").get("app").get("app_id")
view_text = MATTERMOST_CONNECTED_TEXT.format(
app_id=app_id, organization_title=request.auth.organization.org_title
)
# TODO: Create and save bot user and access token and also link org and user with mattermost
response = {"type": "ok", "text": view_text}
return Response(response, status=status.HTTP_200_OK)


class MattermostBindings(APIView):
authentication_classes = (MattermostWebhookAuthTokenAuthentication,)

def post(self, request):
# TODO: Implement bindings or slash commands
response = {"type": "ok"}
return Response(response, status=status.HTTP_200_OK)
5 changes: 5 additions & 0 deletions engine/engine/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
path("telegram/", include("apps.telegram.urls")),
]

if settings.FEATURE_MATTERMOST_INTEGRATION_ENABLED:
urlpatterns += [
path("mattermost/", include("apps.mattermost.urls")),
]

if settings.FEATURE_SLACK_INTEGRATION_ENABLED:
urlpatterns += [
path("api/internal/v1/slack/", include("apps.slack.urls")),
Expand Down
2 changes: 2 additions & 0 deletions engine/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
FEATURE_LIVE_SETTINGS_ENABLED = getenv_boolean("FEATURE_LIVE_SETTINGS_ENABLED", default=True)
FEATURE_TELEGRAM_INTEGRATION_ENABLED = getenv_boolean("FEATURE_TELEGRAM_INTEGRATION_ENABLED", default=True)
FEATURE_TELEGRAM_LONG_POLLING_ENABLED = getenv_boolean("FEATURE_TELEGRAM_LONG_POLLING_ENABLED", default=False)
FEATURE_MATTERMOST_INTEGRATION_ENABLED = getenv_boolean("FEATURE_MATTERMOST_INTEGRATION_ENABLED", default=False)
FEATURE_EMAIL_INTEGRATION_ENABLED = getenv_boolean("FEATURE_EMAIL_INTEGRATION_ENABLED", default=True)
FEATURE_SLACK_INTEGRATION_ENABLED = getenv_boolean("FEATURE_SLACK_INTEGRATION_ENABLED", default=True)
FEATURE_MULTIREGION_ENABLED = getenv_boolean("FEATURE_MULTIREGION_ENABLED", default=False)
Expand Down Expand Up @@ -281,6 +282,7 @@ class DatabaseTypes:
"apps.phone_notifications",
"drf_spectacular",
"apps.google",
"apps.mattermost",
]

REST_FRAMEWORK = {
Expand Down

0 comments on commit d58d17d

Please sign in to comment.