diff --git a/break_glass/approval_message.json.txt b/break_glass/approval_message.json.txt new file mode 100644 index 00000000..343d9a60 --- /dev/null +++ b/break_glass/approval_message.json.txt @@ -0,0 +1,67 @@ +[ + { + "type": "header", + "text": { + "type": "plain_text", + "emoji": true, + "text": "Break-Glass Request" + } + }, + { + "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" + } + ] + } +] diff --git a/break_glass/autokitteh.yaml b/break_glass/autokitteh.yaml new file mode 100644 index 00000000..2994cea1 --- /dev/null +++ b/break_glass/autokitteh.yaml @@ -0,0 +1,28 @@ +version: v1 + +project: + name: break_glass + vars: + - name: APPROVAL_CHANNEL + value: "sre-team" + 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 diff --git a/break_glass/program.py b/break_glass/program.py new file mode 100644 index 00000000..ea5cc941 --- /dev/null +++ b/break_glass/program.py @@ -0,0 +1,118 @@ +"""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 + + +APPROVAL_CHANNEL = os.getenv("APPROVAL_CHANNEL") +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 = parse_event_data(event) + + if not check_issue_exists(issue_key): + message = f"Ticket `{issue_key}` does not exist. Please try again." + 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): + issue_link = f"<{base_url}/browse/{issue_key}|{issue_key}>" + message = f"You are not the assignee in ticket {issue_link}. Please try again." + slack.chat_postMessage(channel=requester_id, text=message) + return + + send_approval_request(reason, issue_key, base_url, requester_id) + 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): + blocks = Path("approval_message.json.txt").read_text() + changes = [ + ("RequestFromMessage", f"*Request from*: <@{requester_id}>"), + ("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=APPROVAL_CHANNEL, 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"] + return reason, issue_key, base_url, requester_id + + +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 diff --git a/break_glass/request_modal.json.txt b/break_glass/request_modal.json.txt new file mode 100644 index 00000000..ee96cbee --- /dev/null +++ b/break_glass/request_modal.json.txt @@ -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": "Reference Ticket ID", + "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 + } + } + ] +}