Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.

Commit 12b55e7

Browse files
authored
Merge branch 'main' into update-slack-emojis
2 parents 28401aa + 2b76d9d commit 12b55e7

File tree

6 files changed

+168
-41
lines changed

6 files changed

+168
-41
lines changed

src/dispatch/auth/permissions.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from abc import ABC, abstractmethod
3+
import json
34

45
from fastapi import HTTPException
56
from starlette.requests import Request
@@ -16,6 +17,7 @@
1617
from dispatch.organization import service as organization_service
1718
from dispatch.organization.models import OrganizationRead
1819
from dispatch.participant_role.enums import ParticipantRoleType
20+
from dispatch.task import service as task_service
1921

2022
log = logging.getLogger(__name__)
2123

@@ -335,6 +337,51 @@ def has_required_permissions(
335337
)
336338

337339

340+
class IncidentTaskCreateEditPermission(BasePermission):
341+
"""
342+
Permissions dependency to apply incident edit permissions to task-based requests.
343+
"""
344+
345+
def has_required_permissions(self, request: Request) -> bool:
346+
incident_id = None
347+
# for task creation, retrieve the incident id from the payload
348+
if request.method == "POST" and hasattr(request, "_body"):
349+
try:
350+
body = json.loads(request._body.decode())
351+
incident_id = body["incident"]["id"]
352+
except (json.JSONDecodeError, KeyError, AttributeError):
353+
log.error(
354+
"Encountered create_task request without expected incident ID. Cannot properly ascertain incident permissions."
355+
)
356+
return False
357+
else: # otherwise, retrieve via the task id
358+
pk = PrimaryKeyModel(id=request.path_params["task_id"])
359+
current_task = task_service.get(db_session=request.state.db, task_id=pk.id)
360+
if not current_task or not current_task.incident:
361+
return False
362+
incident_id = current_task.incident.id
363+
364+
# minimal object with the attributes required for IncidentViewPermission
365+
incident_request = type(
366+
"IncidentRequest",
367+
(),
368+
{
369+
"path_params": {**request.path_params, "incident_id": incident_id},
370+
"state": request.state,
371+
},
372+
)()
373+
374+
# copy necessary request attributes
375+
for attr in ["headers", "method", "url", "query_params"]:
376+
if hasattr(request, attr):
377+
setattr(incident_request, attr, getattr(request, attr))
378+
379+
return any_permission(
380+
permissions=[IncidentEditPermission],
381+
request=incident_request,
382+
)
383+
384+
338385
class IncidentReporterPermission(BasePermission):
339386
def has_required_permissions(
340387
self,

src/dispatch/case/flows.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -184,19 +184,19 @@ def case_remove_participant_flow(
184184
)
185185

186186
# we also try to remove the user from the Slack conversation
187-
try:
188-
slack_conversation_plugin = plugin_service.get_active_instance(
189-
db_session=db_session, project_id=case.project.id, plugin_type="conversation"
190-
)
187+
slack_conversation_plugin = plugin_service.get_active_instance(
188+
db_session=db_session, project_id=case.project.id, plugin_type="conversation"
189+
)
191190

192-
if not slack_conversation_plugin:
193-
log.warning(f"{user_email} not updated. No conversation plugin enabled.")
194-
return
191+
if not slack_conversation_plugin:
192+
log.warning(f"{user_email} not updated. No conversation plugin enabled.")
193+
return
195194

196-
if not case.conversation:
197-
log.warning("No conversation enabled for this case.")
198-
return
195+
if not case.conversation:
196+
log.warning("No conversation enabled for this case.")
197+
return
199198

199+
try:
200200
slack_conversation_plugin.instance.remove_user(
201201
conversation_id=case.conversation.channel_id,
202202
user_email=user_email

src/dispatch/incident/flows.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,19 +1178,19 @@ def incident_remove_participant_flow(
11781178
)
11791179

11801180
# we also try to remove the user from the Slack conversation
1181-
try:
1182-
slack_conversation_plugin = plugin_service.get_active_instance(
1183-
db_session=db_session, project_id=incident.project.id, plugin_type="conversation"
1184-
)
1181+
slack_conversation_plugin = plugin_service.get_active_instance(
1182+
db_session=db_session, project_id=incident.project.id, plugin_type="conversation"
1183+
)
11851184

1186-
if not slack_conversation_plugin:
1187-
log.warning(f"{user_email} not updated. No conversation plugin enabled.")
1188-
return
1185+
if not slack_conversation_plugin:
1186+
log.warning(f"{user_email} not updated. No conversation plugin enabled.")
1187+
return
11891188

1190-
if not incident.conversation:
1191-
log.warning("No conversation enabled for this incident.")
1192-
return
1189+
if not incident.conversation:
1190+
log.warning("No conversation enabled for this incident.")
1191+
return
11931192

1193+
try:
11941194
slack_conversation_plugin.instance.remove_user(
11951195
conversation_id=incident.conversation.channel_id,
11961196
user_email=user_email

src/dispatch/plugins/dispatch_slack/case/interactive.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,23 @@ def engage(
469469
return
470470

471471
engagement = form_data[DefaultBlockIds.description_input]
472-
user = client.users_lookupByEmail(email=user_email)
472+
473+
try:
474+
user = client.users_lookupByEmail(email=user_email)
475+
except SlackApiError as e:
476+
if e.response.get("error") == SlackAPIErrorCode.USERS_NOT_FOUND:
477+
log.warning(
478+
f"Failed to find Slack user for email {user_email}. "
479+
"User may have been deactivated or never had Slack access."
480+
)
481+
client.chat_postMessage(
482+
text=f"Unable to engage user {user_email} - user not found in Slack workspace.",
483+
channel=case.conversation.channel_id,
484+
thread_ts=case.conversation.thread_id if case.has_thread else None,
485+
)
486+
return
487+
else:
488+
raise
473489

474490
result = client.chat_postMessage(
475491
text="Engaging user...",
@@ -1983,9 +1999,19 @@ def edit_button_click(
19831999
ack()
19842000
case = case_service.get(db_session=db_session, case_id=int(context["subject"].id))
19852001

1986-
assignee_initial_user = client.users_lookupByEmail(email=case.assignee.individual.email)[
1987-
"user"
1988-
]["id"]
2002+
try:
2003+
assignee_initial_user = client.users_lookupByEmail(email=case.assignee.individual.email)[
2004+
"user"
2005+
]["id"]
2006+
except SlackApiError as e:
2007+
if e.response.get("error") == SlackAPIErrorCode.USERS_NOT_FOUND:
2008+
log.warning(
2009+
f"Assignee {case.assignee.individual.email} not found in Slack workspace. "
2010+
"Using None for initial assignee selection."
2011+
)
2012+
assignee_initial_user = None
2013+
else:
2014+
raise
19892015

19902016
blocks = [
19912017
title_input(initial_value=case.title),

src/dispatch/plugins/dispatch_slack/plugin.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -390,13 +390,48 @@ def set_description(self, conversation_id: str, description: str):
390390
return set_conversation_description(client, conversation_id, description)
391391

392392
def remove_user(self, conversation_id: str, user_email: str):
393-
"""Removes a user from a conversation."""
393+
"""Removes a user from a conversation.
394+
395+
Args:
396+
conversation_id: The Slack conversation/channel ID
397+
user_email: The email address of the user to remove
398+
399+
Returns:
400+
The API response if successful, None if user not found
401+
402+
Raises:
403+
SlackApiError: For non-recoverable Slack API errors
404+
"""
394405
client = create_slack_client(self.configuration)
395-
user_id = resolve_user(client, user_email).get("id")
396-
if user_id:
397-
return remove_member_from_channel(
398-
client=client, conversation_id=conversation_id, user_id=user_id
399-
)
406+
407+
try:
408+
user_info = resolve_user(client, user_email)
409+
user_id = user_info.get("id")
410+
411+
if user_id:
412+
return remove_member_from_channel(
413+
client=client, conversation_id=conversation_id, user_id=user_id
414+
)
415+
else:
416+
logger.warning(
417+
"Cannot remove user %s from conversation %s: "
418+
"User ID not found in resolve_user response",
419+
user_email, conversation_id
420+
)
421+
return None
422+
423+
except SlackApiError as e:
424+
if e.response.get("error") == SlackAPIErrorCode.USERS_NOT_FOUND:
425+
logger.warning(
426+
"User %s not found in Slack workspace. "
427+
"Cannot remove from conversation %s. "
428+
"User may have been deactivated or never had Slack access.",
429+
user_email, conversation_id
430+
)
431+
return None
432+
else:
433+
# Re-raise for other Slack API errors
434+
raise
400435

401436
def add_bookmark(self, conversation_id: str, weblink: str, title: str):
402437
"""Adds a bookmark to the conversation."""

src/dispatch/task/views.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import json
2-
from fastapi import APIRouter, HTTPException, Query, status
2+
from fastapi import APIRouter, HTTPException, Query, status, Depends
33

44

55
from dispatch.auth.service import CurrentUser
6+
from dispatch.auth.permissions import PermissionsDependency, IncidentTaskCreateEditPermission
67
from dispatch.common.utils.views import create_pydantic_include
78
from dispatch.database.core import DbSession
89
from dispatch.database.service import CommonParameters, search_filter_sort_paginate
@@ -43,7 +44,12 @@ def get_tasks(common: CommonParameters, include: list[str] = Query([], alias="in
4344
return json.loads(TaskPagination(**pagination).json())
4445

4546

46-
@router.post("", response_model=TaskRead, tags=["tasks"])
47+
@router.post(
48+
"",
49+
response_model=TaskRead,
50+
tags=["tasks"],
51+
dependencies=[Depends(PermissionsDependency([IncidentTaskCreateEditPermission]))],
52+
)
4753
def create_task(
4854
db_session: DbSession,
4955
task_in: TaskCreate,
@@ -64,11 +70,12 @@ def create_task(
6470
return task
6571

6672

67-
@router.post("/ticket/{task_id}", tags=["tasks"])
68-
def create_ticket(
69-
db_session: DbSession,
70-
task_id: PrimaryKey,
71-
):
73+
@router.post(
74+
"/ticket/{task_id}",
75+
tags=["tasks"],
76+
dependencies=[Depends(PermissionsDependency([IncidentTaskCreateEditPermission]))],
77+
)
78+
def create_ticket(db_session: DbSession, task_id: PrimaryKey, current_user: CurrentUser):
7279
"""Creates a ticket for an existing task."""
7380
task = get(db_session=db_session, task_id=task_id)
7481
if not task:
@@ -79,8 +86,15 @@ def create_ticket(
7986
return create_task_ticket(task=task, db_session=db_session)
8087

8188

82-
@router.put("/{task_id}", response_model=TaskRead, tags=["tasks"])
83-
def update_task(db_session: DbSession, task_id: PrimaryKey, task_in: TaskUpdate):
89+
@router.put(
90+
"/{task_id}",
91+
response_model=TaskRead,
92+
tags=["tasks"],
93+
dependencies=[Depends(PermissionsDependency([IncidentTaskCreateEditPermission]))],
94+
)
95+
def update_task(
96+
db_session: DbSession, task_id: PrimaryKey, task_in: TaskUpdate, current_user: CurrentUser
97+
):
8498
"""Updates an existing task."""
8599
task = get(db_session=db_session, task_id=task_id)
86100
if not task:
@@ -104,8 +118,13 @@ def update_task(db_session: DbSession, task_id: PrimaryKey, task_in: TaskUpdate)
104118
return task
105119

106120

107-
@router.delete("/{task_id}", response_model=None, tags=["tasks"])
108-
def delete_task(db_session: DbSession, task_id: PrimaryKey):
121+
@router.delete(
122+
"/{task_id}",
123+
response_model=None,
124+
tags=["tasks"],
125+
dependencies=[Depends(PermissionsDependency([IncidentTaskCreateEditPermission]))],
126+
)
127+
def delete_task(db_session: DbSession, task_id: PrimaryKey, current_user: CurrentUser):
109128
"""Deletes an existing task."""
110129
task = get(db_session=db_session, task_id=task_id)
111130
if not task:

0 commit comments

Comments
 (0)