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

ENG-1247 Add break glass demo with Jira and Slack #29

Merged
merged 8 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
67 changes: 67 additions & 0 deletions break_glass/approval_message.json.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
[
{
"type": "header",
"text": {
"type": "plain_text",
"emoji": true,
"text": "Title"
pashafateev marked this conversation as resolved.
Show resolved Hide resolved
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "RequestFromMessage"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Ticket"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Reason"
}
},
{
"type": "divider"
},
{
"type": "actions",
"elements": [
{
"type": "button",
"style": "primary",
"text": {
"type": "plain_text",
"emoji": true,
"text": "Approve"
},
"value": "Approve",
"action_id": "Approve RequesterId IssueKey"
},
{
"type": "button",
"style": "danger",
"text": {
"type": "plain_text",
"emoji": true,
"text": "Deny"
},
"value": "Deny",
"action_id": "Deny RequesterId IssueKey"
}
]
}
]
25 changes: 25 additions & 0 deletions break_glass/autokitteh.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
version: v1

project:
name: break_glass
connections:
- name: jira_connection
integration: jira
- name: slack_connection
integration: slack
triggers:
- name: slack_slash_command
connection: slack_connection
event_type: slash_command
call: program.py:on_slack_slash_command
filter: data.text == "break-glass"
- name: form_submission
connection: slack_connection
event_type: interaction
filter: data.type == "view_submission"
call: program.py:on_form_submit
- name: approve_deny
connection: slack_connection
event_type: interaction
filter: data.type == "block_actions"
call: program.py:on_approve_deny
119 changes: 119 additions & 0 deletions break_glass/program.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""This program orchestrates the request and approval process for break glass scenarios.

Break glass scenarios occur when a developer needs to access sensitive data or perform
a critical operation that requires elevated permissions beyond their usual access.

Workflow:
1. A developer initiates the process by using a Slack slash command to request
break glass approval.
2. AutoKitteh sends a form to the developer, requesting details about the reason
for the elevated access.
3. The developer fills out and submits the form, providing the necessary information
and justification for the request.
4. AutoKitteh sends a notification to the SRE (Site Reliability Engineering) team
with an approve/deny message, including the details of the request.
5. The SRE team reviews the request and makes a decision to approve or deny the request.
6. AutoKitteh sends a message to the developer with the decision, notifying them
whether the request was approved or denied.

The program integrates with Jira to verify ticket existence and ensure that the requester
is the assignee of the ticket. It also uses Slack for communication and notifications
throughout the process.
"""

import os
from pathlib import Path

import autokitteh
from autokitteh.atlassian import atlassian_jira_client
from autokitteh.slack import slack_client
from requests.exceptions import HTTPError


APPROVER = "sre-team"
pashafateev marked this conversation as resolved.
Show resolved Hide resolved
jira = atlassian_jira_client("jira_connection")
slack = slack_client("slack_connection")


def on_slack_slash_command(event):
"""Sends a form to request approval for a ticket."""
trigger_id = event.data["trigger_id"]
request_modal = Path("request_modal.json.txt").read_text()
slack.views_open(trigger_id=trigger_id, view=request_modal)


@autokitteh.activity
def on_form_submit(event):
reason, issue_key, base_url, requester_id, requester_name = parse_event_data(event)

if not check_issue_exists(issue_key):
message = f"Ticket {issue_key} does not exist. Please try again."
pashafateev marked this conversation as resolved.
Show resolved Hide resolved
slack.chat_postMessage(channel=requester_id, text=message)
return

email = slack.users_info(user=requester_id)["user"]["profile"]["email"]
if not validate_requester(issue_key, email):
message = f"You are not the assignee for ticket {issue_key}. Please try again."
pashafateev marked this conversation as resolved.
Show resolved Hide resolved
slack.chat_postMessage(channel=requester_id, text=message)
return

send_approval_request(reason, issue_key, base_url, requester_id, requester_name)
slack.chat_postMessage(channel=requester_id, text="Request sent for approval.")


@autokitteh.activity
def on_approve_deny(event):
action_id = event.data["actions"][0]["action_id"]
_, requester, issue_key = action_id.split(" ")
approver_id = event.data["user"]["id"]
approver_info = slack.users_info(user=approver_id)

if event.data["actions"][0]["value"] == "Approve":
approver_email = approver_info["user"]["profile"]["email"]
jira.issue_add_comment(issue_key, f"Request approved by: {approver_email}")
message = f"Request approved by: <@{approver_info['user']['name']}>"
slack.chat_postMessage(channel=requester, text=message)
else:
print(f"Requester: {requester}")
message = f"Request denied by: <@{approver_info["user"]["name"]}>"
slack.chat_postMessage(channel=requester, text=message)


def send_approval_request(reason, issue_key, base_url, requester_id, requester_name):
blocks = Path("approval_message.json.txt").read_text()
changes = [
("Title", "Break Glass Request"),
pashafateev marked this conversation as resolved.
Show resolved Hide resolved
("RequestFromMessage", f"*Request from*: <@{requester_name}>"),
("Ticket", f"*Ticket*: <{base_url}/browse/{issue_key}|{issue_key}>"),
("Reason", "*Reason for request*: " + reason),
("RequesterId", requester_id),
("IssueKey", issue_key),
]
for old, new in changes:
blocks = blocks.replace(old, new)
slack.chat_postMessage(channel=APPROVER, blocks=blocks)


def parse_event_data(event):
form_data = event.data["view"]["state"]["values"]
reason = form_data["block_reason"]["reason"]["value"]
issue_key = form_data["block_issue_key"]["issue_key"]["value"]
base_url = os.getenv("jira_connection__AccessURL")
requester_id = event.data["user"]["id"]
requester_name = slack.users_info(user=requester_id)["user"]["name"]
pashafateev marked this conversation as resolved.
Show resolved Hide resolved
return reason, issue_key, base_url, requester_id, requester_name


def check_issue_exists(issue_key):
try:
jira.issue(issue_key)
return True
except HTTPError as e:
print(f"Error retrieving issue: {e}")
return False


def validate_requester(issue_key, requester):
issue = jira.issue(issue_key)
assignee = issue.get("fields", {}).get("assignee", {}).get("emailAddress", "")
return assignee == requester
47 changes: 47 additions & 0 deletions break_glass/request_modal.json.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"title": {
"type": "plain_text",
"text": "My App",
"emoji": true
},
"submit": {
"type": "plain_text",
"text": "Submit",
"emoji": true
},
"type": "modal",
"close": {
"type": "plain_text",
"text": "Cancel",
"emoji": true
},
"blocks": [
{
"type": "input",
"block_id": "block_issue_key",
"element": {
"type": "plain_text_input",
"action_id": "issue_key"
},
"label": {
"type": "plain_text",
"text": "Ticket Reference ID",
pashafateev marked this conversation as resolved.
Show resolved Hide resolved
"emoji": true
}
},
{
"type": "input",
"block_id": "block_reason",
"element": {
"type": "plain_text_input",
"multiline": true,
"action_id": "reason"
},
"label": {
"type": "plain_text",
"text": "Reason",
"emoji": true
}
}
]
}