Skip to content

Commit

Permalink
changing SNS notification to include user object
Browse files Browse the repository at this point in the history
  • Loading branch information
Brad Duncan committed Feb 26, 2024
1 parent c22f04c commit 60b6ba6
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 46 deletions.
4 changes: 2 additions & 2 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
sns_topic_arn = os.getenv('SNS_TOPIC_ARN', '')

# Threshold in minutes to determine the age of users to be considered for deletion
aged_user_threshold_minutes = os.getenv('AGED_USER_THRESHOLD_MINUTES', '1')
aged_user_threshold_minutes = os.getenv('AGED_USER_THRESHOLD_MINUTES', '10080')

# Comma-separated list of user statuses to filter users in Cognito
user_status = os.getenv('USER_STATUS', 'UNCONFIRMED,RESET_REQUIRED,FORCE_CHANGE_PASSWORD')
user_status = os.getenv('USER_STATUS', 'UNCONFIRMED')

# Flag to enable or disable the deletion of users
delete_enabled = os.getenv('DELETE_ENABLED', 'False')
41 changes: 23 additions & 18 deletions lambda_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import config
import pytz
from datetime import datetime
from library.cognito_manager import list_users, delete_users
from library.cognito_manager import process_unconfirmed_users
from library.file_manager import write_deleted_users_to_s3
from library.notification_service import send_email_notification

# Initialize the Boto3 clients and environment variables
# Initialize the Boto3 clients with the region from the config
cognito_client = boto3.client('cognito-idp', region_name=config.aws_region)
s3_client = boto3.client('s3', region_name=config.aws_region)
sns_client = boto3.client('sns', region_name=config.aws_region)
Expand All @@ -15,24 +15,30 @@
last_processed_time_cache = {}
deleted_users_cache = set()

def main_handler(event):
def main_handler(event, context):
current_run_time = datetime.now(pytz.utc)
delete_enabled = event.get('delete_enabled', 'false').lower() == 'true'
delete_enabled = config.delete_enabled.lower() == 'true'

# List all unconfirmed users, considering the last processed time from the cache
unconfirmed_users = list_users(cognito_client, config.cognito_user_pool_id, config.aged_user_threshold_minutes, config.user_status, last_processed_time_cache)
# Process unconfirmed users and get the list of deleted usernames and user objects
deleted_usernames, deleted_user_objects = process_unconfirmed_users(
cognito_client,
config.cognito_user_pool_id,
config.aged_user_threshold_minutes,
config.user_status,
delete_enabled,
last_processed_time_cache,
deleted_users_cache
)

# Print the list of unconfirmed users
print(f"{config.user_status} users older than {config.aged_user_threshold_minutes} minutes: {unconfirmed_users}")
# Print the list of deleted usernames
print(f"Deleted users: {deleted_usernames}")

# Delete the unconfirmed users and collect the ones successfully deleted
deleted_users = [user for user in unconfirmed_users if delete_users(cognito_client, config.cognito_user_pool_id, user, delete_enabled, deleted_users_cache)]

if deleted_users:
# Write the list of deleted users to S3
write_deleted_users_to_s3(s3_client, deleted_users, config.s3_bucket_name, config.s3_key)
# Send an email notification with the list of deleted users
send_email_notification(sns_client, config.sns_topic_arn, deleted_users)
if deleted_user_objects:
# Write the list of deleted user objects to S3 in JSON format
write_deleted_users_to_s3(s3_client, deleted_user_objects, config.s3_bucket_name, config.s3_key)

# Send an email notification with the list of deleted user objects
send_email_notification(sns_client, config.sns_topic_arn, deleted_user_objects)
message = "Deleted users were processed and notifications were sent."

# Update the last_processed_time_cache with the current run time only if users were deleted
Expand All @@ -48,5 +54,4 @@ def main_handler(event):
if __name__ == '__main__':
event = {}
context = {}
main_handler(event)

main_handler(event, context)
39 changes: 27 additions & 12 deletions library/cognito_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def list_users(cognito_client, user_pool_id, age_in_minutes, user_statuses, last
:param age_in_minutes: The age of the users in minutes to filter by
:param user_statuses: Comma separated statuses of the users to filter by
:param last_run_cache: A dictionary serving as cache to store the last run timestamp
:return: A list of usernames of users with specified statuses older than 'age_in_minutes' or since last cache timestamp
:return: A list of user objects with specified statuses older than 'age_in_minutes' or since last cache timestamp
"""
try:
# Ensure age_in_minutes is an integer
Expand All @@ -26,8 +26,8 @@ def list_users(cognito_client, user_pool_id, age_in_minutes, user_statuses, last
# If not available in cache, calculate cutoff time based on age_in_minutes
cutoff_time = datetime.now(pytz.utc) - timedelta(minutes=age_in_minutes)

# Initialise the list to store the filtered usernames
aged_users = []
# Initialise the list to store the filtered user objects
aged_user_objects = []

# Split the user_statuses into a list
statuses = user_statuses.split(',')
Expand All @@ -45,32 +45,34 @@ def list_users(cognito_client, user_pool_id, age_in_minutes, user_statuses, last

# Compare the user creation time with the cutoff time
if user_creation_time < cutoff_time:
aged_users.append(user['Username'])
aged_user_objects.append(user) # Append the entire user object

return aged_users
return aged_user_objects
except Exception as e:
print(f"Error listing users: {e}")
return []



def delete_users(cognito_client, user_pool_id, username, delete_enabled, deleted_users_cache):

def delete_users(cognito_client, user_pool_id, user, delete_enabled, deleted_users_cache):
"""
Deletes a single user from the Cognito user pool if delete_enabled is true.
:param cognito_client: Boto3 Cognito client
:param user_pool_id: String identifier of the Cognito user pool
:param username: String identifier of the user to be deleted
:param user: Dictionary containing user attributes
:param delete_enabled: Boolean value to determine if the user should be deleted
:param deleted_users_cache: Set to store usernames of deleted users
:return: Boolean value indicating the success of the deletion
:return: The user object if deletion was successful, otherwise None
"""
username = user['Username']
if delete_enabled:
try:
# Check if the user has already been deleted
if username in deleted_users_cache:
print(f"User {username} has already been deleted.")
return False
return None

cognito_client.admin_delete_user(
UserPoolId=user_pool_id,
Expand All @@ -79,10 +81,23 @@ def delete_users(cognito_client, user_pool_id, username, delete_enabled, deleted

# Update cache after successful deletion
deleted_users_cache.add(username)
return True
return user # Return the entire user object
except Exception as e:
print(f"Error deleting user {username}: {e}")
return False
return None
else:
print(f"Skipping deletion of user {username} as delete_enabled is not set to 'True'")
return False
return None

def process_unconfirmed_users(cognito_client, user_pool_id, age_in_minutes, user_statuses, delete_enabled, last_run_cache, deleted_users_cache):
unconfirmed_users = list_users(cognito_client, user_pool_id, age_in_minutes, user_statuses, last_run_cache)
deleted_usernames = []
deleted_user_objects = []

for user in unconfirmed_users:
deleted_user = delete_users(cognito_client, user_pool_id, user, delete_enabled, deleted_users_cache)
if deleted_user:
deleted_usernames.append(user['Username'])
deleted_user_objects.append(deleted_user)

return deleted_usernames, deleted_user_objects
18 changes: 10 additions & 8 deletions library/file_manager.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
def write_deleted_users_to_s3(s3_client, deleted_users, bucket_name=None, s3_key=None):
import json

def write_deleted_users_to_s3(s3_client, deleted_users_info, bucket_name=None, s3_key=None):
"""
Writes the list of deleted users to a file in an S3 bucket. Skips writing if
Writes the list of deleted user objects to a file in an S3 bucket in JSON format. Skips writing if
bucket_name or s3_key is not provided.
:param s3_client: Boto3 S3 client
:param deleted_users: List of usernames that were deleted
:param deleted_users_info: List of dictionaries containing user attributes
:param bucket_name: The name of the S3 bucket where the file will be stored, optional
:param s3_key: The S3 key (file path within the bucket) for the file, optional
"""
if not bucket_name or not s3_key:
print("Skipping writing to S3 as either bucket_name or s3_key has not been set...")
return

# Convert the list of deleted users to a string, one username per line
deleted_users_str = "\n".join(deleted_users)
# Convert the list of deleted user objects to a JSON string
deleted_users_json = json.dumps(deleted_users_info, indent=2)

try:
# Proceed to write the string to a file in S3
s3_client.put_object(Bucket=bucket_name, Key=s3_key, Body=deleted_users_str)
# Proceed to write the JSON string to a file in S3
s3_client.put_object(Bucket=bucket_name, Key=s3_key, Body=deleted_users_json)
print(f"Successfully wrote deleted users to {s3_key} in bucket {bucket_name}.")
except Exception as e:
print(f"Failed to write deleted users to S3: {e}")
print(f"Failed to write deleted users to S3: {e}")
16 changes: 10 additions & 6 deletions library/notification_service.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
def send_email_notification(sns_client, topic_arn, deleted_users):
def send_email_notification(sns_client, topic_arn, deleted_users_info):
"""
Sends an email notification with the list of deleted users via AWS SNS.
:param sns_client: Boto3 SNS client
:param topic_arn: The ARN of the SNS topic to publish the message to
:param deleted_users: List of usernames that were deleted
:param deleted_users_info: List of dictionaries containing user attributes
"""
if not topic_arn:
print("Skipping SNS notification as topic has not been configured...")
return
if not sns_client:
raise ValueError("ERROR: sns_client not initialised")
if not deleted_users:
raise ValueError("error: sns_client not initialised")
if not deleted_users_info:
print("No users deleted, skipping SNS notification...")
return

# Creating the message
message = "Deleted Users:\n" + "\n".join(deleted_users)
# Format the message with all user information
message_lines = ["Deleted Users:"]
for user in deleted_users_info:
user_info = "\n".join([f"{attr['Name']}: {attr['Value']}" for attr in user['Attributes']])
message_lines.append(f"Username: {user['Username']}\n{user_info}\n")
message = "\n".join(message_lines)

# Sending the message
try:
Expand Down

0 comments on commit 60b6ba6

Please sign in to comment.