55
66import botocore .exceptions
77import jmespath as jp
8+ import slack_sdk
89from croniter import croniter
910from mypy_boto3_events import EventBridgeClient
1011from mypy_boto3_events import type_defs as events_type_defs
1415
1516import config
1617import entities
18+ import slack_helpers
1719import sso
1820from events import (
1921 ApproverNotificationEvent ,
@@ -159,15 +161,54 @@ def _create_schedule_with_retry(
159161def get_and_delete_scheduled_revoke_event_if_already_exist (
160162 client : EventBridgeSchedulerClient ,
161163 event : sso .UserAccountAssignment | sso .GroupAssignment ,
162- ) -> None :
164+ ) -> list [str ]:
165+ """Delete any existing scheduled revoke for this assignment.
166+
167+ Returns the thread_ts of each orphaned grant message so the caller can flip its
168+ header to SUPERSEDED. The extend-grant flow reuses the original thread_ts, so the
169+ caller is expected to skip any orphan whose ts matches the new request's thread_ts.
170+ """
171+ orphaned_thread_ts : list [str ] = []
163172 for scheduled_event in get_scheduled_events (client ):
164173 logger .debug ("Checking if schedule already exist" , extra = {"scheduled_event" : scheduled_event })
165174 if isinstance (scheduled_event , ScheduledRevokeEvent ) and scheduled_event .revoke_event .user_account_assignment == event :
166175 logger .info ("Schedule already exist, deleting it" , extra = {"schedule_name" : scheduled_event .revoke_event .schedule_name })
167176 delete_schedule (client , scheduled_event .revoke_event .schedule_name )
177+ if scheduled_event .revoke_event .thread_ts :
178+ orphaned_thread_ts .append (scheduled_event .revoke_event .thread_ts )
168179 if isinstance (scheduled_event , ScheduledGroupRevokeEvent ) and scheduled_event .revoke_event .group_assignment == event :
169180 logger .info ("Schedule already exist, deleting it" , extra = {"schedule_name" : scheduled_event .revoke_event .schedule_name })
170181 delete_schedule (client , scheduled_event .revoke_event .schedule_name )
182+ if scheduled_event .revoke_event .thread_ts :
183+ orphaned_thread_ts .append (scheduled_event .revoke_event .thread_ts )
184+ return orphaned_thread_ts
185+
186+
187+ def mark_superseded (slack_client : slack_sdk .WebClient , thread_ts : str ) -> None :
188+ """Flip an orphaned grant message's header to SUPERSEDED and remove its early-revoke button.
189+
190+ Called when a newer request replaces the schedule for the same assignment, leaving the
191+ old grant message with no revoker to update it on expiry.
192+ """
193+ message = slack_helpers .get_message_from_timestamp (
194+ channel_id = cfg .slack_channel_id ,
195+ message_ts = thread_ts ,
196+ slack_client = slack_client ,
197+ )
198+ if message is None :
199+ logger .warning ("Could not find orphaned grant message to mark superseded" , extra = {"thread_ts" : thread_ts })
200+ return
201+ blocks = slack_helpers .HeaderSectionBlock .set_status (
202+ blocks = message ["blocks" ],
203+ status_text = cfg .superseded_status ,
204+ )
205+ slack_client .chat_update (
206+ channel = cfg .slack_channel_id ,
207+ ts = thread_ts ,
208+ blocks = blocks ,
209+ text = "Superseded by newer request" ,
210+ )
211+ slack_helpers .delete_early_revoke_button (slack_client , cfg .slack_channel_id , thread_ts )
171212
172213
173214def event_bridge_schedule_after (td : timedelta ) -> str :
@@ -181,6 +222,7 @@ def schedule_revoke_event( # noqa: PLR0913
181222 approver : entities .slack .User ,
182223 requester : entities .slack .User ,
183224 user_account_assignment : sso .UserAccountAssignment ,
225+ slack_client : slack_sdk .WebClient ,
184226 thread_ts : str | None = None ,
185227 permission_set_name : str | None = None ,
186228 account_name : str | None = None ,
@@ -193,7 +235,12 @@ def schedule_revoke_event( # noqa: PLR0913
193235 Tuple of (CreateScheduleOutput, schedule_name)
194236 """
195237 logger .info ("Scheduling revoke event" )
196- get_and_delete_scheduled_revoke_event_if_already_exist (schedule_client , user_account_assignment )
238+ orphaned_thread_ts = get_and_delete_scheduled_revoke_event_if_already_exist (schedule_client , user_account_assignment )
239+ for orphan_ts in orphaned_thread_ts :
240+ # Extend-grant flow reuses thread_ts — don't flip the message we're about to keep using.
241+ if orphan_ts == thread_ts :
242+ continue
243+ mark_superseded (slack_client , orphan_ts )
197244
198245 def build_input (schedule_name : str ) -> str :
199246 revoke_event = RevokeEvent (
@@ -236,6 +283,7 @@ def schedule_group_revoke_event( # noqa: PLR0913
236283 approver : entities .slack .User ,
237284 requester : entities .slack .User ,
238285 group_assignment : sso .GroupAssignment ,
286+ slack_client : slack_sdk .WebClient ,
239287 thread_ts : str | None = None ,
240288 can_extend_expired_grant : bool = False ,
241289 extensions_count : int = 0 ,
@@ -246,7 +294,11 @@ def schedule_group_revoke_event( # noqa: PLR0913
246294 Tuple of (CreateScheduleOutput, schedule_name)
247295 """
248296 logger .info ("Scheduling revoke event" )
249- get_and_delete_scheduled_revoke_event_if_already_exist (schedule_client , group_assignment )
297+ orphaned_thread_ts = get_and_delete_scheduled_revoke_event_if_already_exist (schedule_client , group_assignment )
298+ for orphan_ts in orphaned_thread_ts :
299+ if orphan_ts == thread_ts :
300+ continue
301+ mark_superseded (slack_client , orphan_ts )
250302
251303 def build_input (schedule_name : str ) -> str :
252304 revoke_event = GroupRevokeEvent (
0 commit comments