Skip to content

Commit

Permalink
Mattermost incoming event handler
Browse files Browse the repository at this point in the history
Remove print

Add migrations
  • Loading branch information
ravishankar15 committed Nov 20, 2024
1 parent cc14a36 commit 26520ca
Show file tree
Hide file tree
Showing 18 changed files with 724 additions and 45 deletions.
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.16 on 2024-11-20 16:53

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('alerts', '0064_migrate_resolutionnoteslackmessage_slack_channel_id'),
]

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')]),
),
]
10 changes: 6 additions & 4 deletions engine/apps/mattermost/alert_rendering.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from apps.alerts.incident_appearance.renderers.base_renderer import AlertBaseRenderer, AlertGroupBaseRenderer
from apps.alerts.incident_appearance.templaters.alert_templater import AlertTemplater
from apps.alerts.models import Alert, AlertGroup
from apps.mattermost.events.types import EventAction
from apps.mattermost.utils import MattermostEventAuthenticator
from common.api_helpers.utils import create_engine_url
from common.utils import is_string_with_visible_characters, str_or_backup
Expand Down Expand Up @@ -94,6 +95,7 @@ def _make_actions(id, name, token):
"url": create_engine_url("api/internal/v1/mattermost/event/"),
"context": {
"action": id,
"alert": self.alert_group.pk,
"token": token,
},
},
Expand All @@ -102,14 +104,14 @@ def _make_actions(id, name, token):
token = MattermostEventAuthenticator.create_token(organization=self.alert_group.channel.organization)
if not self.alert_group.resolved:
if self.alert_group.acknowledged:
actions.append(_make_actions("unacknowledge", "Unacknowledge", token))
actions.append(_make_actions(EventAction.UNACKNOWLEDGE.value, "Unacknowledge", token))
else:
actions.append(_make_actions("acknowledge", "Acknowledge", token))
actions.append(_make_actions(EventAction.ACKNOWLEDGE.value, "Acknowledge", token))

if self.alert_group.resolved:
actions.append(_make_actions("unresolve", "Unresolve", token))
actions.append(_make_actions(EventAction.UNRESOLVE.value, "Unresolve", token))
else:
actions.append(_make_actions("resolve", "Resolve", token))
actions.append(_make_actions(EventAction.RESOLVE.value, "Resolve", token))

return actions

Expand Down
29 changes: 29 additions & 0 deletions engine/apps/mattermost/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import logging
import typing

from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication

from apps.mattermost.models import MattermostUser
from apps.mattermost.utils import MattermostEventAuthenticator, MattermostEventTokenInvalid
from apps.user_management.models import User

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)


class MattermostEventAuthentication(BaseAuthentication):
def authenticate(self, request) -> typing.Tuple[User, None]:
if "context" not in request.data or "token" not in request.data["context"]:
raise exceptions.AuthenticationFailed("Auth token is missing")

auth = request.data["context"]["token"]
try:
MattermostEventAuthenticator.verify(auth)
mattermost_user = MattermostUser.objects.get(mattermost_user_id=request.data["user_id"])
except MattermostEventTokenInvalid:
raise exceptions.AuthenticationFailed("Invalid auth token")
except MattermostUser.DoesNotExist:
raise exceptions.AuthenticationFailed("Mattermost user not integrated")

return mattermost_user.user, None
1 change: 1 addition & 0 deletions engine/apps/mattermost/events/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .alert_group_actions_handler import AlertGroupActionHandler # noqa: F401
94 changes: 94 additions & 0 deletions engine/apps/mattermost/events/alert_group_actions_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import logging
import typing

from apps.alerts.constants import ActionSource
from apps.alerts.models import AlertGroup
from apps.mattermost.events.event_handler import MattermostEventHandler
from apps.mattermost.events.types import EventAction
from apps.mattermost.models import MattermostMessage

logger = logging.getLogger(__name__)


class AlertGroupActionHandler(MattermostEventHandler):
"""
Handles the alert group actions from the mattermost message buttons
"""

def is_match(self):
action = self._get_action()
return action and action in [
EventAction.ACKNOWLEDGE,
EventAction.UNACKNOWLEDGE,
EventAction.RESOLVE,
EventAction.UNRESOLVE,
]

def process(self):
alert_group = self._get_alert_group()
action = self._get_action()

if not alert_group or not action:
return

action_fn, fn_kwargs = self._get_action_function(alert_group, action)
action_fn(user=self.user, action_source=ActionSource.MATTERMOST, **fn_kwargs)

def _get_action(self) -> typing.Optional[EventAction]:
if "context" not in self.event or "action" not in self.event["context"]:
return

try:
action = self.event["context"]["action"]
return EventAction(action)
except ValueError:
logger.info(f"Mattermost event action not found {action}")
return

def _get_alert_group(self) -> typing.Optional[AlertGroup]:
return self._get_alert_group_from_event() or self._get_alert_group_from_message()

def _get_alert_group_from_event(self) -> typing.Optional[AlertGroup]:
if "context" not in self.event or "alert" not in self.event["context"]:
return

try:
alert_group = AlertGroup.objects.get(pk=self.event["context"]["alert"])
except AlertGroup.DoesNotExist:
return

return alert_group

def _get_alert_group_from_message(self) -> typing.Optional[AlertGroup]:
try:
mattermost_message = MattermostMessage.objects.get(
channel_id=self.event["channel_id"], post_id=self.event["post_id"]
)
return mattermost_message.alert_group
except MattermostMessage.DoesNotExist:
logger.info(
f"Mattermost message not found for channel_id: {self.event['channel_id']} and post_id {self.event['post_id']}"
)
return

def _get_action_function(self, alert_group: AlertGroup, action: EventAction) -> typing.Tuple[typing.Callable, dict]:
action_to_fn = {
EventAction.ACKNOWLEDGE: {
"fn_name": "acknowledge_by_user_or_backsync",
"kwargs": {},
},
EventAction.UNACKNOWLEDGE: {
"fn_name": "un_acknowledge_by_user_or_backsync",
"kwargs": {},
},
EventAction.RESOLVE: {
"fn_name": "resolve_by_user_or_backsync",
"kwargs": {},
},
EventAction.UNRESOLVE: {"fn_name": "un_resolve_by_user_or_backsync", "kwargs": {}},
}

fn_info = action_to_fn[action]
fn = getattr(alert_group, fn_info["fn_name"])

return fn, fn_info["kwargs"]
18 changes: 18 additions & 0 deletions engine/apps/mattermost/events/event_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from abc import ABC, abstractmethod

from apps.mattermost.events.types import MattermostEvent
from apps.user_management.models import User


class MattermostEventHandler(ABC):
def __init__(self, event: MattermostEvent, user: User):
self.event: MattermostEvent = event
self.user: User = user

@abstractmethod
def is_match(self) -> bool:
pass

@abstractmethod
def process(self) -> None:
pass
37 changes: 37 additions & 0 deletions engine/apps/mattermost/events/event_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import logging
import typing

from rest_framework.request import Request

from apps.mattermost.events.event_handler import MattermostEventHandler
from apps.mattermost.events.types import MattermostEvent
from apps.user_management.models import User

logger = logging.getLogger(__name__)


class EventManager:
"""
Manager for mattermost events
"""

@classmethod
def process_request(cls, request: Request):
user = request.user
event = request.data
handler = cls.select_event_handler(user=user, event=event)
if handler is None:
logger.info("No event handler found")
return

logger.info(f"Processing mattermost event with handler: {handler.__class__.__name__}")
handler.process()

@staticmethod
def select_event_handler(user: User, event: MattermostEvent) -> typing.Optional[MattermostEventHandler]:
handler_classes = MattermostEventHandler.__subclasses__()
for handler_class in handler_classes:
handler = handler_class(user=user, event=event)
if handler.is_match():
return handler
return None
29 changes: 29 additions & 0 deletions engine/apps/mattermost/events/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import enum
import typing


class MattermostAlertGroupContext(typing.TypedDict):
action: str
token: str
alert: int


class MattermostEvent(typing.TypedDict):
user_id: str
user_name: str
channel_id: str
channel_name: str
team_id: str
team_domain: str
post_id: str
trigger_id: str
type: str
data_source: str
context: MattermostAlertGroupContext


class EventAction(enum.StrEnum):
ACKNOWLEDGE = "acknowledge"
UNACKNOWLEDGE = "unacknowledge"
RESOLVE = "resolve"
UNRESOLVE = "unresolve"
40 changes: 23 additions & 17 deletions engine/apps/mattermost/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.2.15 on 2024-11-14 14:35
# Generated by Django 4.2.16 on 2024-11-20 16:53

import apps.mattermost.models.channel
import django.core.validators
Expand All @@ -11,11 +11,25 @@ class Migration(migrations.Migration):
initial = True

dependencies = [
('alerts', '0060_relatedincident'),
('user_management', '0022_alter_team_unique_together'),
('user_management', '0026_auto_20241017_1919'),
('alerts', '0065_alter_alertgrouplogrecord_action_source'),
]

operations = [
migrations.CreateModel(
name='MattermostChannel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('public_primary_key', models.CharField(default=apps.mattermost.models.channel.generate_public_primary_key_for_mattermost_channel, max_length=20, unique=True, validators=[django.core.validators.MinLengthValidator(13)])),
('mattermost_team_id', models.CharField(max_length=100)),
('channel_id', models.CharField(max_length=100)),
('channel_name', models.CharField(default=None, max_length=100)),
('display_name', models.CharField(default=None, max_length=100)),
('is_default_channel', models.BooleanField(default=False, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mattermost_channels', to='user_management.organization')),
],
),
migrations.CreateModel(
name='MattermostUser',
fields=[
Expand All @@ -26,6 +40,9 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='mattermost_user_identity', to='user_management.user')),
],
options={
'indexes': [models.Index(fields=['mattermost_user_id'], name='mattermost__matterm_55d2a0_idx')],
},
),
migrations.CreateModel(
name='MattermostMessage',
Expand All @@ -37,20 +54,9 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True)),
('alert_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mattermost_messages', to='alerts.alertgroup')),
],
),
migrations.CreateModel(
name='MattermostChannel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('public_primary_key', models.CharField(default=apps.mattermost.models.channel.generate_public_primary_key_for_mattermost_channel, max_length=20, unique=True, validators=[django.core.validators.MinLengthValidator(13)])),
('mattermost_team_id', models.CharField(max_length=100)),
('channel_id', models.CharField(max_length=100)),
('channel_name', models.CharField(default=None, max_length=100)),
('display_name', models.CharField(default=None, max_length=100)),
('is_default_channel', models.BooleanField(default=False, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mattermost_channels', to='user_management.organization')),
],
options={
'indexes': [models.Index(fields=['channel_id', 'post_id'], name='mattermost__channel_1fbf8b_idx')],
},
),
migrations.AddConstraint(
model_name='mattermostmessage',
Expand Down
4 changes: 4 additions & 0 deletions engine/apps/mattermost/models/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ class Meta:
)
]

indexes = [
models.Index(fields=["channel_id", "post_id"]),
]

@staticmethod
def create_message(alert_group: AlertGroup, post: MattermostPost, message_type: int):
return MattermostMessage.objects.create(
Expand Down
5 changes: 5 additions & 0 deletions engine/apps/mattermost/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ class MattermostUser(models.Model):
username = models.CharField(max_length=100)
nickname = models.CharField(max_length=100, null=True, blank=True, default=None)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
indexes = [
models.Index(fields=["mattermost_user_id"]),
]
Loading

0 comments on commit 26520ca

Please sign in to comment.