-
Notifications
You must be signed in to change notification settings - Fork 289
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
Prepare OnCall for Unified Slack App #4232
Changes from all commits
6ee54a4
3a699c4
9581552
8bd9ef3
2dda654
b187082
c97a994
97d140a
7103c5c
0131c47
4cf952c
5cf0bb7
0215e8c
70f0e53
62204f2
ed0f6de
58b8376
616e6e9
ee58525
f051bad
281b419
a20fabd
c9b9abe
25bf434
6a8c33b
9fba9f6
d126d03
9c440e2
cd9f325
0b9c414
61cbbd2
b1df185
d99a046
ee4439f
5d2f566
0abc787
9235ede
b265b59
779c352
56ce18d
408b751
4df9f69
6d2edc7
8073e3d
252b268
1fa0469
52e8771
ae2eeca
8ddd8a1
b2ddd02
a650357
e717174
9d8b129
a5eb5fd
3afcb4f
705e117
bede2ad
89a3c0c
f3a951d
e3dd665
82f3695
6e6e120
8811757
8d438a3
aae84ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
from django.conf import settings | ||
|
||
SERVICE_TYPE_ONCALL = "oncall" | ||
PROVIDER_TYPE_SLACK = "slack" | ||
|
||
|
||
@dataclass | ||
|
@@ -32,6 +33,15 @@ class Tenant: | |
msteams_links: List[MSTeamsLink] = field(default_factory=list) | ||
|
||
|
||
@dataclass | ||
class OAuthInstallation: | ||
id: str | ||
oauth_response: dict | ||
stack_id: int | ||
provider_type: str | ||
provider_id: str | ||
|
||
|
||
class ChatopsProxyAPIException(Exception): | ||
"""A generic 400 or 500 level exception from the Chatops Proxy API""" | ||
|
||
|
@@ -55,14 +65,15 @@ def __init__(self, url: str, token: str): | |
|
||
# OnCall Tenant | ||
def register_tenant( | ||
self, service_tenant_id: str, cluster_slug: str, service_type: str | ||
self, service_tenant_id: str, cluster_slug: str, service_type: str, stack_id: int | ||
) -> tuple[Tenant, requests.models.Response]: | ||
url = f"{self.api_base_url}/tenants/register" | ||
d = { | ||
"tenant": { | ||
"service_tenant_id": service_tenant_id, | ||
"cluster_slug": cluster_slug, | ||
"service_type": service_type, | ||
"stack_id": stack_id, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this value backfilled/updated somehow for already registered tenants? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will be. Probably I'll add a new endpoint on ChatopsProxy, allowing to PUT tenants and set their stack ids. |
||
} | ||
} | ||
response = requests.post(url=url, json=d, headers=self._headers) | ||
|
@@ -131,6 +142,34 @@ def unlink_slack_team( | |
self._check_response(response) | ||
return response.json()["removed"], response | ||
|
||
def get_slack_oauth_link( | ||
self, stack_id: int, grafana_user_id: int, app_redirect: str, app_type: str | ||
) -> tuple[str, requests.models.Response]: | ||
url = f"{self.api_base_url}/oauth2/start" | ||
d = { | ||
"stack_id": stack_id, | ||
"grafana_user_id": grafana_user_id, | ||
"app_redirect": app_redirect, | ||
"app_type": app_type, | ||
} | ||
response = requests.post(url=url, json=d, headers=self._headers) | ||
self._check_response(response) | ||
return response.json()["install_link"], response | ||
|
||
def get_oauth_installation( | ||
self, | ||
stack_id: int, | ||
provider_type: str, | ||
) -> tuple[OAuthInstallation, requests.models.Response]: | ||
url = f"{self.api_base_url}/oauth_installations/get" | ||
d = { | ||
"stack_id": stack_id, | ||
"provider_type": provider_type, | ||
} | ||
response = requests.post(url=url, json=d, headers=self._headers) | ||
self._check_response(response) | ||
return OAuthInstallation(**response.json()["oauth_installation"]), response | ||
|
||
def _check_response(self, response: requests.models.Response): | ||
""" | ||
Wraps an exceptional response to ChatopsProxyAPIException | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .root_handler import ChatopsEventsHandler # noqa |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import logging | ||
import typing | ||
from abc import ABC, abstractmethod | ||
|
||
from apps.chatops_proxy.client import PROVIDER_TYPE_SLACK | ||
from apps.slack.installation import SlackInstallationExc, install_slack_integration | ||
from apps.user_management.models import Organization | ||
|
||
from .types import INTEGRATION_INSTALLED_EVENT_TYPE, Event, IntegrationInstalledData | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class Handler(ABC): | ||
@classmethod | ||
@abstractmethod | ||
def match(cls, event: Event) -> bool: | ||
pass | ||
|
||
@classmethod | ||
@abstractmethod | ||
def handle(cls, event_data: dict) -> None: | ||
pass | ||
|
||
|
||
class SlackInstallationHandler(Handler): | ||
@classmethod | ||
def match(cls, event: Event) -> bool: | ||
return ( | ||
event.get("event_type") == INTEGRATION_INSTALLED_EVENT_TYPE | ||
and event.get("data", {}).get("provider_type") == PROVIDER_TYPE_SLACK | ||
) | ||
|
||
@classmethod | ||
def handle(cls, data: dict) -> None: | ||
data = typing.cast(IntegrationInstalledData, data) | ||
|
||
stack_id = data.get("stack_id") | ||
user_id = data.get("grafana_user_id") | ||
payload = data.get("payload") | ||
|
||
organization = Organization.objects.get(stack_id=stack_id) | ||
user = organization.users.get(user_id=user_id) | ||
try: | ||
install_slack_integration(organization, user, payload) | ||
except SlackInstallationExc as e: | ||
logger.exception( | ||
f'msg="SlackInstallationHandler: Failed to install Slack integration: %s" org_id={organization.id} stack_id={stack_id}', | ||
e, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import logging | ||
import typing | ||
|
||
from .handlers import Handler, SlackInstallationHandler | ||
from .types import Event | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class ChatopsEventsHandler: | ||
""" | ||
ChatopsEventsHandler is a root handler which receives event from Chatops-Proxy and chooses the handler to process it. | ||
""" | ||
|
||
HANDLERS: typing.List[typing.Type[Handler]] = [SlackInstallationHandler] | ||
|
||
def handle(self, event_data: Event) -> bool: | ||
""" | ||
handle iterates over all handlers and chooses the first one that matches the event. | ||
Returns True if a handler was found and False otherwise. | ||
""" | ||
logger.info(f"msg=\"ChatopsEventsHandler: Handling\" event_type={event_data.get('event_type')}") | ||
for h in self.HANDLERS: | ||
if h.match(event_data): | ||
logger.info( | ||
f"msg=\"ChatopsEventsHandler: Found matching handler {h.__name__}\" event_type={event_data.get('event_type')}" | ||
) | ||
self._exec(h.handle, event_data.get("data", {})) | ||
return True | ||
logger.error(f"msg=\"ChatopsEventsHandler: No handler found\" event_type={event_data.get('event_type')}") | ||
return False | ||
|
||
def _exec(self, handlefunc: typing.Callable[[dict], None], data: dict): | ||
""" | ||
_exec is a helper method to execute a handler's handle method. | ||
""" | ||
return handlefunc(data) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import base64 | ||
import binascii | ||
import hashlib | ||
import hmac | ||
|
||
|
||
def hash(data): | ||
hasher = hashlib.sha256() | ||
hasher.update(data) | ||
return base64.b64encode(hasher.digest()).decode() | ||
|
||
|
||
def generate_signature(data, secret): | ||
h = hmac.new(secret.encode(), data.encode(), hashlib.sha256) | ||
return binascii.hexlify(h.digest()).decode() | ||
|
||
|
||
def verify_signature(request, secret) -> bool: | ||
header = request.META.get("HTTP_X_CHATOPS_SIGNATURE") | ||
if not header: | ||
return False | ||
|
||
signatures = header.split(",") | ||
s = dict(pair.split("=") for pair in signatures) | ||
t = s.get("t") | ||
v1 = s.get("v1") | ||
|
||
payload = request.body | ||
body_hash = hash(payload) | ||
string_to_sign = f"{body_hash}:{t}:v1" | ||
expected = generate_signature(string_to_sign, secret) | ||
|
||
if expected != v1: | ||
return False | ||
|
||
return True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kind was not starting for me on this version