Skip to content

Commit 0ddccf5

Browse files
committed
Mark superseded grants in Slack on re-request
When a new access request supersedes an existing scheduled revoke (same user + account + permission set, or same group membership), the old schedule was deleted but the original "GRANTED" Slack message was left untouched. Once the new schedule fired, only the new message was updated to "SESSION COMPLETE"; the older one remained as "GRANTED" indefinitely even though AWS access was revoked. Now the old message header flips to "SUPERSEDED" and its early-revoke button is removed at re-request time. The extend-grant flow reuses the original thread_ts and is explicitly skipped so its message stays "GRANTED".
1 parent 07a339e commit 0ddccf5

5 files changed

Lines changed: 562 additions & 4 deletions

File tree

src/access_control.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ def execute_decision( # noqa: PLR0913
318318
approver=approver,
319319
requester=requester,
320320
user_account_assignment=account_assignment,
321+
slack_client=slack_client,
321322
thread_ts=thread_ts,
322323
permission_set_name=permission_set.name,
323324
account_name=account.name,
@@ -461,6 +462,7 @@ def execute_decision_on_group_request( # noqa: PLR0913
461462
approver=approver,
462463
requester=requester,
463464
group_assignment=group_assignment,
465+
slack_client=slack_client,
464466
thread_ts=thread_ts,
465467
can_extend_expired_grant=can_extend,
466468
extensions_count=0,

src/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ class Config(BaseSettings):
177177
denied_status: str = ":x: *DENIED*"
178178
timed_out_status: str = ":clock1: *TIMED OUT*"
179179
access_ended_status: str = ":checkered_flag: *SESSION COMPLETE*"
180+
superseded_status: str = ":arrows_counterclockwise: *SUPERSEDED*"
180181

181182
@model_validator(mode="before")
182183
@classmethod

src/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,6 +1132,7 @@ def handle_extend_grant_button_click(body: dict, client: WebClient, context: Bol
11321132
approver=approver,
11331133
requester=requester,
11341134
user_account_assignment=account_assignment,
1135+
slack_client=client,
11351136
thread_ts=thread_ts,
11361137
permission_set_name=payload.permission_set_name,
11371138
account_name=payload.account_name,
@@ -1188,6 +1189,7 @@ def handle_extend_grant_button_click(body: dict, client: WebClient, context: Bol
11881189
approver=approver,
11891190
requester=requester,
11901191
group_assignment=group_assignment,
1192+
slack_client=client,
11911193
thread_ts=thread_ts,
11921194
can_extend_expired_grant=True,
11931195
extensions_count=new_extensions_count,

src/schedule.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import botocore.exceptions
77
import jmespath as jp
8+
import slack_sdk
89
from croniter import croniter
910
from mypy_boto3_events import EventBridgeClient
1011
from mypy_boto3_events import type_defs as events_type_defs
@@ -14,6 +15,7 @@
1415

1516
import config
1617
import entities
18+
import slack_helpers
1719
import sso
1820
from events import (
1921
ApproverNotificationEvent,
@@ -159,15 +161,54 @@ def _create_schedule_with_retry(
159161
def 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

173214
def 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

Comments
 (0)